Merge remote-tracking branch 'upstream/develop' into rust-verification
This commit is contained in:
		
						commit
						85e4b5eb49
					
				@ -63,7 +63,7 @@ Supported filename extensions are:
 | 
			
		||||
- ``.bugfix``: Signifying a bug fix.
 | 
			
		||||
- ``.doc``: Signifying a documentation improvement.
 | 
			
		||||
- ``.removal``: Signifying a deprecation or removal of public API. Can be used to notifying about API change in the Matrix SDK
 | 
			
		||||
- ``.misc``: A ticket has been closed, but it is not of interest to users. Note that in this case, the content of the file will not be output, but just the issue/PR number.
 | 
			
		||||
- ``.misc``: Any other changes.
 | 
			
		||||
 | 
			
		||||
See https://github.com/twisted/towncrier#news-fragments if you need more details.
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -2,7 +2,7 @@
 | 
			
		||||
 | 
			
		||||
buildscript {
 | 
			
		||||
    // Ref: https://kotlinlang.org/releases.html
 | 
			
		||||
    ext.kotlin_version = '1.5.10'
 | 
			
		||||
    ext.kotlin_version = '1.5.20'
 | 
			
		||||
    ext.kotlin_coroutines_version = "1.5.0"
 | 
			
		||||
    repositories {
 | 
			
		||||
        google()
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										1
									
								
								changelog.d/3545.feature
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								changelog.d/3545.feature
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1 @@
 | 
			
		||||
Reveal password: use facility from com.google.android.material.textfield.TextInputLayout instead of manual handling.
 | 
			
		||||
							
								
								
									
										1
									
								
								changelog.d/3547.feature
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								changelog.d/3547.feature
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1 @@
 | 
			
		||||
Implements new design for Jump to unread and quick fix visibility issues.
 | 
			
		||||
							
								
								
									
										1
									
								
								changelog.d/3564.bugfix
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								changelog.d/3564.bugfix
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1 @@
 | 
			
		||||
Fix call invite processed after call is ended because of fastlane mode.
 | 
			
		||||
							
								
								
									
										1
									
								
								changelog.d/3577.bugfix
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								changelog.d/3577.bugfix
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1 @@
 | 
			
		||||
Fix crash after video call.
 | 
			
		||||
							
								
								
									
										
											BIN
										
									
								
								gradle/wrapper/gradle-wrapper.jar
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										
											BIN
										
									
								
								gradle/wrapper/gradle-wrapper.jar
									
									
									
									
										vendored
									
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										4
									
								
								gradle/wrapper/gradle-wrapper.properties
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										4
									
								
								gradle/wrapper/gradle-wrapper.properties
									
									
									
									
										vendored
									
									
								
							@ -1,6 +1,6 @@
 | 
			
		||||
distributionBase=GRADLE_USER_HOME
 | 
			
		||||
distributionPath=wrapper/dists
 | 
			
		||||
distributionSha256Sum=13bf8d3cf8eeeb5770d19741a59bde9bd966dd78d17f1bbad787a05ef19d1c2d
 | 
			
		||||
distributionUrl=https\://services.gradle.org/distributions/gradle-7.0.2-all.zip
 | 
			
		||||
distributionSha256Sum=a9e356a21595348b6f04b024ed0b08ac8aea6b2ac37e6c0ef58e51549cd7b9cb
 | 
			
		||||
distributionUrl=https\://services.gradle.org/distributions/gradle-7.1-all.zip
 | 
			
		||||
zipStoreBase=GRADLE_USER_HOME
 | 
			
		||||
zipStorePath=wrapper/dists
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										2
									
								
								gradlew
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								gradlew
									
									
									
									
										vendored
									
									
								
							@ -72,7 +72,7 @@ case "`uname`" in
 | 
			
		||||
  Darwin* )
 | 
			
		||||
    darwin=true
 | 
			
		||||
    ;;
 | 
			
		||||
  MINGW* )
 | 
			
		||||
  MSYS* | MINGW* )
 | 
			
		||||
    msys=true
 | 
			
		||||
    ;;
 | 
			
		||||
  NONSTOP* )
 | 
			
		||||
 | 
			
		||||
@ -24,6 +24,7 @@ import com.google.android.material.dialog.MaterialAlertDialogBuilder
 | 
			
		||||
import com.google.android.material.snackbar.Snackbar
 | 
			
		||||
import im.vector.lib.ui.styles.R
 | 
			
		||||
import im.vector.lib.ui.styles.databinding.ActivityDebugMaterialThemeBinding
 | 
			
		||||
import im.vector.lib.ui.styles.dialogs.MaterialProgressDialog
 | 
			
		||||
 | 
			
		||||
// Rendering is not the same with VectorBaseActivity
 | 
			
		||||
abstract class DebugMaterialThemeActivity : AppCompatActivity() {
 | 
			
		||||
@ -50,14 +51,20 @@ abstract class DebugMaterialThemeActivity : AppCompatActivity() {
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        views.debugShowDialog.setOnClickListener {
 | 
			
		||||
            MaterialAlertDialogBuilder(this)
 | 
			
		||||
                    .setTitle("Dialog title")
 | 
			
		||||
                    .setMessage("Dialog content")
 | 
			
		||||
                    .setIcon(R.drawable.ic_debug_icon)
 | 
			
		||||
                    .setPositiveButton("Positive", null)
 | 
			
		||||
                    .setNegativeButton("Negative", null)
 | 
			
		||||
                    .setNeutralButton("Neutral", null)
 | 
			
		||||
                    .show()
 | 
			
		||||
            showTestDialog(0)
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        views.debugShowDialogDestructive.setOnClickListener {
 | 
			
		||||
            showTestDialog(R.style.ThemeOverlay_Vector_MaterialAlertDialog_Destructive)
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        views.debugShowDialogNegativeDestructive.setOnClickListener {
 | 
			
		||||
            showTestDialog(R.style.ThemeOverlay_Vector_MaterialAlertDialog_NegativeDestructive)
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        views.debugShowProgressDialog.setOnClickListener {
 | 
			
		||||
            MaterialProgressDialog(this)
 | 
			
		||||
                    .show(message = "Progress Dialog\nLine 2", cancellable = true)
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        views.debugShowBottomSheet.setOnClickListener {
 | 
			
		||||
@ -65,6 +72,17 @@ abstract class DebugMaterialThemeActivity : AppCompatActivity() {
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private fun showTestDialog(theme: Int) {
 | 
			
		||||
        MaterialAlertDialogBuilder(this, theme)
 | 
			
		||||
                .setTitle("Dialog title")
 | 
			
		||||
                .setMessage("Dialog content\nLine 2")
 | 
			
		||||
                .setIcon(R.drawable.ic_debug_icon)
 | 
			
		||||
                .setPositiveButton("Positive", null)
 | 
			
		||||
                .setNegativeButton("Negative", null)
 | 
			
		||||
                .setNeutralButton("Neutral", null)
 | 
			
		||||
                .show()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun onCreateOptionsMenu(menu: Menu): Boolean {
 | 
			
		||||
        menuInflater.inflate(R.menu.menu_debug, menu)
 | 
			
		||||
        return true
 | 
			
		||||
 | 
			
		||||
@ -17,6 +17,8 @@
 | 
			
		||||
package im.vector.lib.ui.styles.debug
 | 
			
		||||
 | 
			
		||||
import android.os.Bundle
 | 
			
		||||
import android.text.InputType
 | 
			
		||||
import android.widget.EditText
 | 
			
		||||
import androidx.appcompat.app.AppCompatActivity
 | 
			
		||||
import im.vector.lib.ui.styles.databinding.ActivityDebugTextViewBinding
 | 
			
		||||
 | 
			
		||||
@ -27,5 +29,20 @@ abstract class DebugVectorTextViewActivity : AppCompatActivity() {
 | 
			
		||||
        super.onCreate(savedInstanceState)
 | 
			
		||||
        val views = ActivityDebugTextViewBinding.inflate(layoutInflater)
 | 
			
		||||
        setContentView(views.root)
 | 
			
		||||
 | 
			
		||||
        views.debugShowPassword.setOnClickListener {
 | 
			
		||||
            views.debugTextInputEditText.showPassword(true)
 | 
			
		||||
        }
 | 
			
		||||
        views.debugHidePassword.setOnClickListener {
 | 
			
		||||
            views.debugTextInputEditText.showPassword(false)
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private fun EditText.showPassword(visible: Boolean) {
 | 
			
		||||
        if (visible) {
 | 
			
		||||
            inputType = InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD
 | 
			
		||||
        } else {
 | 
			
		||||
            inputType = InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_VARIATION_PASSWORD
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -452,6 +452,27 @@
 | 
			
		||||
                    android:layout_gravity="center_horizontal"
 | 
			
		||||
                    android:text="Show Dialog" />
 | 
			
		||||
 | 
			
		||||
                <Button
 | 
			
		||||
                    android:id="@+id/debugShowDialogDestructive"
 | 
			
		||||
                    android:layout_width="wrap_content"
 | 
			
		||||
                    android:layout_height="wrap_content"
 | 
			
		||||
                    android:layout_gravity="center_horizontal"
 | 
			
		||||
                    android:text="Show Dialog Destructive" />
 | 
			
		||||
 | 
			
		||||
                <Button
 | 
			
		||||
                    android:id="@+id/debugShowDialogNegativeDestructive"
 | 
			
		||||
                    android:layout_width="wrap_content"
 | 
			
		||||
                    android:layout_height="wrap_content"
 | 
			
		||||
                    android:layout_gravity="center_horizontal"
 | 
			
		||||
                    android:text="Show Dialog Neg Destructive" />
 | 
			
		||||
 | 
			
		||||
                <Button
 | 
			
		||||
                    android:id="@+id/debugShowProgressDialog"
 | 
			
		||||
                    android:layout_width="wrap_content"
 | 
			
		||||
                    android:layout_height="wrap_content"
 | 
			
		||||
                    android:layout_gravity="center_horizontal"
 | 
			
		||||
                    android:text="Show Progress Dialog" />
 | 
			
		||||
 | 
			
		||||
                <Button
 | 
			
		||||
                    android:id="@+id/debugShowBottomSheet"
 | 
			
		||||
                    android:layout_width="wrap_content"
 | 
			
		||||
 | 
			
		||||
@ -1,9 +1,11 @@
 | 
			
		||||
<?xml version="1.0" encoding="utf-8"?>
 | 
			
		||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
 | 
			
		||||
    xmlns:app="http://schemas.android.com/apk/res-auto"
 | 
			
		||||
    xmlns:tools="http://schemas.android.com/tools"
 | 
			
		||||
    android:layout_width="match_parent"
 | 
			
		||||
    android:layout_height="match_parent"
 | 
			
		||||
    android:orientation="vertical"
 | 
			
		||||
    android:padding="16dp"
 | 
			
		||||
    tools:ignore="HardcodedText">
 | 
			
		||||
 | 
			
		||||
    <TextView
 | 
			
		||||
@ -65,4 +67,34 @@
 | 
			
		||||
        android:layout_height="wrap_content"
 | 
			
		||||
        android:text="Default (TextAppearance.Vector.Body)\nline 2" />
 | 
			
		||||
 | 
			
		||||
    <com.google.android.material.textfield.TextInputLayout
 | 
			
		||||
        android:id="@+id/debugTextInputLayout"
 | 
			
		||||
        style="@style/Widget.Vector.TextInputLayout.Password"
 | 
			
		||||
        android:layout_width="match_parent"
 | 
			
		||||
        android:layout_height="wrap_content"
 | 
			
		||||
        android:layout_marginTop="16dp"
 | 
			
		||||
        android:hint="Password"
 | 
			
		||||
        app:errorEnabled="true">
 | 
			
		||||
 | 
			
		||||
        <com.google.android.material.textfield.TextInputEditText
 | 
			
		||||
            android:id="@+id/debugTextInputEditText"
 | 
			
		||||
            android:layout_width="match_parent"
 | 
			
		||||
            android:layout_height="wrap_content"
 | 
			
		||||
            android:inputType="textPassword"
 | 
			
		||||
            android:maxLines="1" />
 | 
			
		||||
 | 
			
		||||
    </com.google.android.material.textfield.TextInputLayout>
 | 
			
		||||
 | 
			
		||||
    <Button
 | 
			
		||||
        android:id="@+id/debugShowPassword"
 | 
			
		||||
        android:layout_width="wrap_content"
 | 
			
		||||
        android:layout_height="wrap_content"
 | 
			
		||||
        android:text="Show password" />
 | 
			
		||||
 | 
			
		||||
    <Button
 | 
			
		||||
        android:id="@+id/debugHidePassword"
 | 
			
		||||
        android:layout_width="wrap_content"
 | 
			
		||||
        android:layout_height="wrap_content"
 | 
			
		||||
        android:text="Hide password" />
 | 
			
		||||
 | 
			
		||||
</LinearLayout>
 | 
			
		||||
							
								
								
									
										37
									
								
								library/ui-styles/src/main/java/MaterialProgressDialog.kt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										37
									
								
								library/ui-styles/src/main/java/MaterialProgressDialog.kt
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,37 @@
 | 
			
		||||
/*
 | 
			
		||||
 * Copyright (c) 2021 New Vector Ltd
 | 
			
		||||
 *
 | 
			
		||||
 * Licensed under the Apache License, Version 2.0 (the "License");
 | 
			
		||||
 * you may not use this file except in compliance with the License.
 | 
			
		||||
 * You may obtain a copy of the License at
 | 
			
		||||
 *
 | 
			
		||||
 *     http://www.apache.org/licenses/LICENSE-2.0
 | 
			
		||||
 *
 | 
			
		||||
 * Unless required by applicable law or agreed to in writing, software
 | 
			
		||||
 * distributed under the License is distributed on an "AS IS" BASIS,
 | 
			
		||||
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | 
			
		||||
 * See the License for the specific language governing permissions and
 | 
			
		||||
 * limitations under the License.
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
package im.vector.lib.ui.styles.dialogs
 | 
			
		||||
 | 
			
		||||
import android.content.Context
 | 
			
		||||
import android.view.LayoutInflater
 | 
			
		||||
import androidx.appcompat.app.AlertDialog
 | 
			
		||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
 | 
			
		||||
import im.vector.lib.ui.styles.R
 | 
			
		||||
import im.vector.lib.ui.styles.databinding.DialogProgressMaterialBinding
 | 
			
		||||
 | 
			
		||||
class MaterialProgressDialog(val context: Context) {
 | 
			
		||||
    fun show(message: CharSequence, cancellable: Boolean = false): AlertDialog {
 | 
			
		||||
        val view = LayoutInflater.from(context).inflate(R.layout.dialog_progress_material, null)
 | 
			
		||||
        val views = DialogProgressMaterialBinding.bind(view)
 | 
			
		||||
        views.message.text = message
 | 
			
		||||
 | 
			
		||||
        return MaterialAlertDialogBuilder(context)
 | 
			
		||||
                .setCancelable(cancellable)
 | 
			
		||||
                .setView(view)
 | 
			
		||||
                .show()
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,37 @@
 | 
			
		||||
<?xml version="1.0" encoding="utf-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">
 | 
			
		||||
 | 
			
		||||
    <!-- Inspired from https://android.googlesource.com/platform/frameworks/base/+/master/core/res/res/layout/progress_dialog.xml -->
 | 
			
		||||
    <LinearLayout
 | 
			
		||||
        android:id="@+id/body"
 | 
			
		||||
        android:layout_width="match_parent"
 | 
			
		||||
        android:layout_height="match_parent"
 | 
			
		||||
        android:baselineAligned="false"
 | 
			
		||||
        android:orientation="horizontal"
 | 
			
		||||
        android:paddingStart="8dp"
 | 
			
		||||
        android:paddingTop="10dp"
 | 
			
		||||
        android:paddingEnd="8dp"
 | 
			
		||||
        android:paddingBottom="10dp">
 | 
			
		||||
 | 
			
		||||
        <ProgressBar
 | 
			
		||||
            android:id="@android:id/progress"
 | 
			
		||||
            style="?android:attr/progressBarStyle"
 | 
			
		||||
            android:layout_width="wrap_content"
 | 
			
		||||
            android:layout_height="wrap_content"
 | 
			
		||||
            android:layout_marginEnd="12dp"
 | 
			
		||||
            android:max="10000" />
 | 
			
		||||
 | 
			
		||||
        <TextView
 | 
			
		||||
            android:id="@+id/message"
 | 
			
		||||
            style="@style/Widget.Vector.TextView.Body"
 | 
			
		||||
            android:layout_width="match_parent"
 | 
			
		||||
            android:layout_height="wrap_content"
 | 
			
		||||
            android:layout_gravity="center_vertical"
 | 
			
		||||
            tools:text="Content\nLine 2" />
 | 
			
		||||
 | 
			
		||||
    </LinearLayout>
 | 
			
		||||
 | 
			
		||||
</FrameLayout>
 | 
			
		||||
							
								
								
									
										13
									
								
								library/ui-styles/src/main/res/values/dimens_font.xml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								library/ui-styles/src/main/res/values/dimens_font.xml
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,13 @@
 | 
			
		||||
<?xml version="1.0" encoding="utf-8"?>
 | 
			
		||||
<resources>
 | 
			
		||||
 | 
			
		||||
    <dimen name="text_size_title">24sp</dimen>
 | 
			
		||||
    <dimen name="text_size_headline">18sp</dimen>
 | 
			
		||||
    <dimen name="text_size_subtitle">16sp</dimen>
 | 
			
		||||
    <dimen name="text_size_body">14sp</dimen>
 | 
			
		||||
    <dimen name="text_size_caption">12sp</dimen>
 | 
			
		||||
    <dimen name="text_size_micro">10sp</dimen>
 | 
			
		||||
 | 
			
		||||
    <dimen name="text_size_button">14sp</dimen>
 | 
			
		||||
 | 
			
		||||
</resources>
 | 
			
		||||
@ -1,28 +1,40 @@
 | 
			
		||||
<?xml version="1.0" encoding="utf-8"?>
 | 
			
		||||
<resources>
 | 
			
		||||
 | 
			
		||||
    <style name="AlertDialog.Vector.Light" parent="ThemeOverlay.MaterialComponents.MaterialAlertDialog">
 | 
			
		||||
        <item name="colorPrimary">@color/palette_element_green</item>
 | 
			
		||||
        <item name="colorSecondary">@color/palette_element_green</item>
 | 
			
		||||
        <item name="colorSurface">@color/element_system_light</item>
 | 
			
		||||
        <item name="colorOnSurface">@color/element_content_primary_light</item>
 | 
			
		||||
        <item name="colorError">@color/element_alert_light</item>
 | 
			
		||||
        <!--item name="alertDialogStyle">@style/MaterialAlertDialog.App</item>
 | 
			
		||||
        <item name="materialAlertDialogTitleTextStyle">@style/MaterialAlertDialog.App.Title.Text</item>
 | 
			
		||||
        <item name="buttonBarPositiveButtonStyle">@style/Widget.App.Button</item>
 | 
			
		||||
        <item name="buttonBarNeutralButtonStyle">@style/Widget.App.Button</item-->
 | 
			
		||||
    <style name="ThemeOverlay.Vector.MaterialAlertDialog" parent="ThemeOverlay.MaterialComponents.MaterialAlertDialog">
 | 
			
		||||
        <item name="materialAlertDialogTitleTextStyle">@style/MaterialAlertDialog.Vector.Title.Text</item>
 | 
			
		||||
        <item name="materialAlertDialogBodyTextStyle">@style/MaterialAlertDialog.Vector.Body.Text</item>
 | 
			
		||||
        <item name="buttonBarPositiveButtonStyle">@style/Widget.Vector.Button.TextButton.Dialog</item>
 | 
			
		||||
        <item name="buttonBarNegativeButtonStyle">@style/Widget.Vector.Button.TextButton.Dialog</item>
 | 
			
		||||
        <item name="buttonBarNeutralButtonStyle">@style/Widget.Vector.Button.TextButton.Dialog</item>
 | 
			
		||||
    </style>
 | 
			
		||||
 | 
			
		||||
    <style name="AlertDialog.Vector.Dark" parent="ThemeOverlay.MaterialComponents.MaterialAlertDialog">
 | 
			
		||||
        <item name="colorPrimary">@color/palette_element_green</item>
 | 
			
		||||
        <item name="colorSecondary">@color/palette_element_green</item>
 | 
			
		||||
        <item name="colorSurface">@color/element_system_dark</item>
 | 
			
		||||
        <item name="colorOnSurface">@color/element_content_primary_dark</item>
 | 
			
		||||
        <item name="colorError">@color/element_alert_dark</item>
 | 
			
		||||
        <!--item name="alertDialogStyle">@style/MaterialAlertDialog.App</item>
 | 
			
		||||
        <item name="materialAlertDialogTitleTextStyle">@style/MaterialAlertDialog.App.Title.Text</item>
 | 
			
		||||
        <item name="buttonBarPositiveButtonStyle">@style/Widget.App.Button</item>
 | 
			
		||||
        <item name="buttonBarNeutralButtonStyle">@style/Widget.App.Button</item-->
 | 
			
		||||
    <style name="ThemeOverlay.Vector.MaterialAlertDialog.Destructive">
 | 
			
		||||
        <item name="buttonBarPositiveButtonStyle">@style/Widget.Vector.Button.TextButton.Dialog.Destructive</item>
 | 
			
		||||
    </style>
 | 
			
		||||
 | 
			
		||||
    <style name="ThemeOverlay.Vector.MaterialAlertDialog.NegativeDestructive">
 | 
			
		||||
        <item name="buttonBarNegativeButtonStyle">@style/Widget.Vector.Button.TextButton.Dialog.Destructive</item>
 | 
			
		||||
    </style>
 | 
			
		||||
 | 
			
		||||
    <!-- Title -->
 | 
			
		||||
    <style name="MaterialAlertDialog.Vector.Title.Text" parent="MaterialAlertDialog.MaterialComponents.Title.Text">
 | 
			
		||||
        <item name="android:textAppearance">@style/TextAppearance.Vector.Subtitle</item>
 | 
			
		||||
    </style>
 | 
			
		||||
 | 
			
		||||
    <!-- Body -->
 | 
			
		||||
    <style name="MaterialAlertDialog.Vector.Body.Text" parent="MaterialAlertDialog.MaterialComponents.Body.Text">
 | 
			
		||||
        <item name="android:textAppearance">@style/TextAppearance.Vector.Body</item>
 | 
			
		||||
        <item name="lineHeight">20sp</item>
 | 
			
		||||
    </style>
 | 
			
		||||
 | 
			
		||||
    <!-- Buttons -->
 | 
			
		||||
    <style name="Widget.Vector.Button.TextButton.Dialog" parent="Widget.MaterialComponents.Button.TextButton.Dialog">
 | 
			
		||||
        <item name="android:textAppearance">@style/TextAppearance.Vector.Button</item>
 | 
			
		||||
    </style>
 | 
			
		||||
 | 
			
		||||
    <style name="Widget.Vector.Button.TextButton.Dialog.Destructive">
 | 
			
		||||
        <item name="materialThemeOverlay">@style/VectorMaterialThemeOverlayDestructive</item>
 | 
			
		||||
    </style>
 | 
			
		||||
 | 
			
		||||
</resources>
 | 
			
		||||
@ -6,6 +6,7 @@
 | 
			
		||||
        <item name="android:paddingRight">16dp</item>
 | 
			
		||||
        <item name="android:minWidth">94dp</item>
 | 
			
		||||
        <item name="android:textAppearance">@style/TextAppearance.Vector.Button</item>
 | 
			
		||||
        <item name="cornerRadius">8dp</item>
 | 
			
		||||
        <item name="lineHeight">24sp</item>
 | 
			
		||||
    </style>
 | 
			
		||||
 | 
			
		||||
@ -32,6 +33,7 @@
 | 
			
		||||
        <item name="android:paddingRight">16dp</item>
 | 
			
		||||
        <item name="android:minWidth">94dp</item>
 | 
			
		||||
        <item name="android:textAppearance">@style/TextAppearance.Vector.Button</item>
 | 
			
		||||
        <item name="cornerRadius">8dp</item>
 | 
			
		||||
        <item name="lineHeight">24sp</item>
 | 
			
		||||
    </style>
 | 
			
		||||
 | 
			
		||||
@ -48,6 +50,7 @@
 | 
			
		||||
        <item name="colorControlHighlight">?colorSecondary</item>
 | 
			
		||||
        <item name="materialThemeOverlay">@style/VectorMaterialThemeOverlayPositive</item>
 | 
			
		||||
        <item name="android:textAppearance">@style/TextAppearance.Vector.Button</item>
 | 
			
		||||
        <item name="cornerRadius">8dp</item>
 | 
			
		||||
        <item name="lineHeight">24sp</item>
 | 
			
		||||
    </style>
 | 
			
		||||
 | 
			
		||||
@ -61,6 +64,7 @@
 | 
			
		||||
        <item name="strokeColor">@color/button_background_tint_selector</item>
 | 
			
		||||
        <item name="strokeWidth">1dp</item>
 | 
			
		||||
        <item name="android:textAppearance">@style/TextAppearance.Vector.Button</item>
 | 
			
		||||
        <item name="cornerRadius">8dp</item>
 | 
			
		||||
        <item name="lineHeight">24sp</item>
 | 
			
		||||
    </style>
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -4,6 +4,11 @@
 | 
			
		||||
    <!-- Default style for TextInputLayout -->
 | 
			
		||||
    <style name="Widget.Vector.TextInputLayout" parent="Widget.MaterialComponents.TextInputLayout.OutlinedBox" />
 | 
			
		||||
 | 
			
		||||
    <style name="Widget.Vector.TextInputLayout.Password">
 | 
			
		||||
        <item name="endIconMode">password_toggle</item>
 | 
			
		||||
        <item name="endIconTint">?vctr_content_secondary</item>
 | 
			
		||||
    </style>
 | 
			
		||||
 | 
			
		||||
    <style name="Widget.Vector.EditText.Composer" parent="Widget.AppCompat.EditText">
 | 
			
		||||
        <item name="android:background">@android:color/transparent</item>
 | 
			
		||||
        <item name="android:inputType">textCapSentences|textMultiLine</item>
 | 
			
		||||
 | 
			
		||||
@ -0,0 +1,27 @@
 | 
			
		||||
<?xml version="1.0" encoding="utf-8"?>
 | 
			
		||||
<resources>
 | 
			
		||||
 | 
			
		||||
    <attr name="vctr_jump_to_unread_style" format="reference" />
 | 
			
		||||
 | 
			
		||||
    <style name="Widget.Vector.JumpToUnread.Base" parent="Widget.MaterialComponents.Chip.Action">
 | 
			
		||||
        <item name="android:textAppearance">@style/TextAppearance.Vector.Body.Medium</item>
 | 
			
		||||
        <item name="chipEndPadding">12dp</item>
 | 
			
		||||
        <item name="chipIconSize">24dp</item>
 | 
			
		||||
        <item name="chipMinHeight">44dp</item>
 | 
			
		||||
        <item name="chipStartPadding">12dp</item>
 | 
			
		||||
        <item name="closeIconVisible">true</item>
 | 
			
		||||
        <item name="android:elevation">6dp</item>
 | 
			
		||||
        <item name="closeIconSize">24dp</item>
 | 
			
		||||
    </style>
 | 
			
		||||
 | 
			
		||||
    <style name="Widget.Vector.JumpToUnread.Light" parent="Widget.Vector.JumpToUnread.Base">
 | 
			
		||||
        <item name="chipBackgroundColor">@color/element_background_light</item>
 | 
			
		||||
        <item name="closeIconTint">?vctr_content_secondary</item>
 | 
			
		||||
    </style>
 | 
			
		||||
 | 
			
		||||
    <style name="Widget.Vector.JumpToUnread.Dark" parent="Widget.Vector.JumpToUnread.Base">
 | 
			
		||||
        <item name="chipBackgroundColor">@color/element_system_dark</item>
 | 
			
		||||
        <item name="closeIconTint">?vctr_content_quaternary</item>
 | 
			
		||||
    </style>
 | 
			
		||||
 | 
			
		||||
</resources>
 | 
			
		||||
@ -14,7 +14,7 @@
 | 
			
		||||
    <style name="TextAppearance.Vector.Title" parent="TextAppearance.MaterialComponents.Headline3">
 | 
			
		||||
        <item name="fontFamily">sans-serif</item>
 | 
			
		||||
        <item name="android:fontFamily">sans-serif</item>
 | 
			
		||||
        <item name="android:textSize">24sp</item>
 | 
			
		||||
        <item name="android:textSize">@dimen/text_size_title</item>
 | 
			
		||||
        <item name="android:letterSpacing">0</item>
 | 
			
		||||
        <item name="android:textColor">?vctr_content_primary</item>
 | 
			
		||||
    </style>
 | 
			
		||||
@ -27,7 +27,7 @@
 | 
			
		||||
    <style name="TextAppearance.Vector.Headline.Medium" parent="TextAppearance.MaterialComponents.Headline1">
 | 
			
		||||
        <item name="fontFamily">sans-serif-medium</item>
 | 
			
		||||
        <item name="android:fontFamily">sans-serif-medium</item>
 | 
			
		||||
        <item name="android:textSize">18sp</item>
 | 
			
		||||
        <item name="android:textSize">@dimen/text_size_headline</item>
 | 
			
		||||
        <item name="android:letterSpacing">0</item>
 | 
			
		||||
        <item name="android:textColor">?vctr_content_primary</item>
 | 
			
		||||
    </style>
 | 
			
		||||
@ -35,7 +35,7 @@
 | 
			
		||||
    <style name="TextAppearance.Vector.Subtitle" parent="TextAppearance.MaterialComponents.Subtitle1">
 | 
			
		||||
        <item name="fontFamily">sans-serif</item>
 | 
			
		||||
        <item name="android:fontFamily">sans-serif</item>
 | 
			
		||||
        <item name="android:textSize">16sp</item>
 | 
			
		||||
        <item name="android:textSize">@dimen/text_size_subtitle</item>
 | 
			
		||||
        <item name="android:letterSpacing">0</item>
 | 
			
		||||
        <item name="android:textColor">?vctr_content_secondary</item>
 | 
			
		||||
    </style>
 | 
			
		||||
@ -49,7 +49,7 @@
 | 
			
		||||
    <style name="TextAppearance.Vector.Body" parent="TextAppearance.MaterialComponents.Body1">
 | 
			
		||||
        <item name="fontFamily">sans-serif</item>
 | 
			
		||||
        <item name="android:fontFamily">sans-serif</item>
 | 
			
		||||
        <item name="android:textSize">14sp</item>
 | 
			
		||||
        <item name="android:textSize">@dimen/text_size_body</item>
 | 
			
		||||
        <item name="android:letterSpacing">0</item>
 | 
			
		||||
        <item name="android:textColor">?vctr_content_primary</item>
 | 
			
		||||
    </style>
 | 
			
		||||
@ -62,7 +62,7 @@
 | 
			
		||||
    <style name="TextAppearance.Vector.Caption" parent="TextAppearance.MaterialComponents.Caption">
 | 
			
		||||
        <item name="fontFamily">sans-serif</item>
 | 
			
		||||
        <item name="android:fontFamily">sans-serif</item>
 | 
			
		||||
        <item name="android:textSize">12sp</item>
 | 
			
		||||
        <item name="android:textSize">@dimen/text_size_caption</item>
 | 
			
		||||
        <item name="android:letterSpacing">0</item>
 | 
			
		||||
        <item name="android:textColor">?vctr_content_secondary</item>
 | 
			
		||||
    </style>
 | 
			
		||||
@ -70,14 +70,14 @@
 | 
			
		||||
    <style name="TextAppearance.Vector.Micro" parent="TextAppearance.MaterialComponents.Caption">
 | 
			
		||||
        <item name="fontFamily">sans-serif</item>
 | 
			
		||||
        <item name="android:fontFamily">sans-serif</item>
 | 
			
		||||
        <item name="android:textSize">10sp</item>
 | 
			
		||||
        <item name="android:textSize">@dimen/text_size_micro</item>
 | 
			
		||||
        <item name="android:letterSpacing">0</item>
 | 
			
		||||
    </style>
 | 
			
		||||
 | 
			
		||||
    <style name="TextAppearance.Vector.Button" parent="TextAppearance.MaterialComponents.Button">
 | 
			
		||||
        <item name="fontFamily">sans-serif-medium</item>
 | 
			
		||||
        <item name="android:fontFamily">sans-serif-medium</item>
 | 
			
		||||
        <item name="android:textSize">16sp</item>
 | 
			
		||||
        <item name="android:textSize">@dimen/text_size_button</item>
 | 
			
		||||
        <item name="android:letterSpacing">0.02</item>
 | 
			
		||||
    </style>
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -76,7 +76,6 @@
 | 
			
		||||
        <item name="android:textViewStyle">@style/Widget.Vector.TextView.Body</item>
 | 
			
		||||
        <item name="materialButtonStyle">@style/Widget.Vector.Button</item>
 | 
			
		||||
        <item name="toolbarStyle">@style/Widget.Vector.Toolbar</item>
 | 
			
		||||
        <item name="materialAlertDialogTheme">@style/AlertDialog.Vector.Dark</item>
 | 
			
		||||
        <item name="bottomNavigationStyle">@style/BottomNavigation.Vector</item>
 | 
			
		||||
        <item name="searchViewStyle">@style/Widget.Vector.SearchView</item>
 | 
			
		||||
        <item name="textInputStyle">@style/Widget.Vector.TextInputLayout</item>
 | 
			
		||||
@ -89,6 +88,7 @@
 | 
			
		||||
 | 
			
		||||
        <!-- Default theme -->
 | 
			
		||||
        <item name="bottomSheetDialogTheme">@style/Theme.Vector.BottomSheetDialog.Dark</item>
 | 
			
		||||
        <item name="materialAlertDialogTheme">@style/ThemeOverlay.Vector.MaterialAlertDialog</item>
 | 
			
		||||
 | 
			
		||||
        <item name="android:textColorLink">@color/element_link_dark</item>
 | 
			
		||||
 | 
			
		||||
@ -132,6 +132,9 @@
 | 
			
		||||
        <item name="vctr_social_login_button_twitter_style">@style/Widget.Vector.Button.Outlined.SocialLogin.Twitter.Dark</item>
 | 
			
		||||
        <item name="vctr_social_login_button_apple_style">@style/Widget.Vector.Button.Outlined.SocialLogin.Apple.Dark</item>
 | 
			
		||||
        <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>
 | 
			
		||||
 | 
			
		||||
    </style>
 | 
			
		||||
 | 
			
		||||
    <style name="Theme.Vector.Dark" parent="Base.Theme.Vector.Dark" />
 | 
			
		||||
 | 
			
		||||
@ -76,7 +76,6 @@
 | 
			
		||||
        <item name="android:textViewStyle">@style/Widget.Vector.TextView.Body</item>
 | 
			
		||||
        <item name="materialButtonStyle">@style/Widget.Vector.Button</item>
 | 
			
		||||
        <item name="toolbarStyle">@style/Widget.Vector.Toolbar</item>
 | 
			
		||||
        <item name="materialAlertDialogTheme">@style/AlertDialog.Vector.Light</item>
 | 
			
		||||
        <item name="bottomNavigationStyle">@style/BottomNavigation.Vector</item>
 | 
			
		||||
        <item name="searchViewStyle">@style/Widget.Vector.SearchView</item>
 | 
			
		||||
        <item name="textInputStyle">@style/Widget.Vector.TextInputLayout</item>
 | 
			
		||||
@ -89,6 +88,7 @@
 | 
			
		||||
 | 
			
		||||
        <!-- Default theme -->
 | 
			
		||||
        <item name="bottomSheetDialogTheme">@style/Theme.Vector.BottomSheetDialog.Light</item>
 | 
			
		||||
        <item name="materialAlertDialogTheme">@style/ThemeOverlay.Vector.MaterialAlertDialog</item>
 | 
			
		||||
 | 
			
		||||
        <item name="android:textColorLink">@color/element_link_light</item>
 | 
			
		||||
 | 
			
		||||
@ -134,6 +134,9 @@
 | 
			
		||||
        <item name="vctr_social_login_button_twitter_style">@style/Widget.Vector.Button.Outlined.SocialLogin.Twitter.Light</item>
 | 
			
		||||
        <item name="vctr_social_login_button_apple_style">@style/Widget.Vector.Button.Outlined.SocialLogin.Apple.Light</item>
 | 
			
		||||
        <item name="vctr_social_login_button_gitlab_style">@style/Widget.Vector.Button.Outlined.SocialLogin.Gitlab.Light</item>
 | 
			
		||||
 | 
			
		||||
        <item name="vctr_jump_to_unread_style">@style/Widget.Vector.JumpToUnread.Light</item>
 | 
			
		||||
 | 
			
		||||
    </style>
 | 
			
		||||
 | 
			
		||||
    <style name="Theme.Vector.Light" parent="Base.Theme.Vector.Light" />
 | 
			
		||||
 | 
			
		||||
@ -9,7 +9,7 @@ buildscript {
 | 
			
		||||
        mavenCentral()
 | 
			
		||||
    }
 | 
			
		||||
    dependencies {
 | 
			
		||||
        classpath "io.realm:realm-gradle-plugin:10.5.0"
 | 
			
		||||
        classpath "io.realm:realm-gradle-plugin:10.6.0"
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -185,14 +185,14 @@ dependencies {
 | 
			
		||||
    implementation 'com.otaliastudios:transcoder:0.10.3'
 | 
			
		||||
 | 
			
		||||
    // Phone number https://github.com/google/libphonenumber
 | 
			
		||||
    implementation 'com.googlecode.libphonenumber:libphonenumber:8.12.24'
 | 
			
		||||
    implementation 'com.googlecode.libphonenumber:libphonenumber:8.12.26'
 | 
			
		||||
 | 
			
		||||
    testImplementation 'junit:junit:4.13.2'
 | 
			
		||||
    testImplementation 'org.robolectric:robolectric:4.5.1'
 | 
			
		||||
    //testImplementation 'org.robolectric:shadows-support-v4:3.0'
 | 
			
		||||
    // Note: version sticks to 1.9.2 due to https://github.com/mockk/mockk/issues/281
 | 
			
		||||
    testImplementation 'io.mockk:mockk:1.11.0'
 | 
			
		||||
    testImplementation 'org.amshove.kluent:kluent-android:1.65'
 | 
			
		||||
    testImplementation 'org.amshove.kluent:kluent-android:1.67'
 | 
			
		||||
    testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$kotlin_coroutines_version"
 | 
			
		||||
    // Plant Timber tree for test
 | 
			
		||||
    testImplementation 'net.lachlanmckee:timber-junit-rule:1.0.1'
 | 
			
		||||
 | 
			
		||||
@ -125,6 +125,12 @@ interface RoomService {
 | 
			
		||||
     */
 | 
			
		||||
    suspend fun deleteRoomAlias(roomAlias: String)
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Return the current local changes membership for the given room.
 | 
			
		||||
     * see [getChangeMembershipsLive] for more details.
 | 
			
		||||
     */
 | 
			
		||||
    fun getChangeMemberships(roomIdOrAlias: String): ChangeMembershipState
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Return a live data of all local changes membership that happened since the session has been opened.
 | 
			
		||||
     * It allows you to track this in your client to known what is currently being processed by the SDK.
 | 
			
		||||
 | 
			
		||||
@ -59,9 +59,8 @@ internal class CallEventProcessor @Inject constructor(private val callSignalingH
 | 
			
		||||
        return eventType == EventType.CALL_INVITE
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    suspend fun processFastLane(event: Event) {
 | 
			
		||||
        eventsToPostProcess.add(event)
 | 
			
		||||
        onPostProcess()
 | 
			
		||||
    fun processFastLane(event: Event) {
 | 
			
		||||
        dispatchToCallSignalingHandlerIfNeeded(event)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override suspend fun onPostProcess() {
 | 
			
		||||
@ -73,13 +72,12 @@ internal class CallEventProcessor @Inject constructor(private val callSignalingH
 | 
			
		||||
 | 
			
		||||
    private fun dispatchToCallSignalingHandlerIfNeeded(event: Event) {
 | 
			
		||||
        val now = System.currentTimeMillis()
 | 
			
		||||
        // TODO might check if an invite is not closed (hangup/answered) in the same event batch?
 | 
			
		||||
        event.roomId ?: return Unit.also {
 | 
			
		||||
            Timber.w("Event with no room id ${event.eventId}")
 | 
			
		||||
        }
 | 
			
		||||
        val age = now - (event.ageLocalTs ?: now)
 | 
			
		||||
        if (age > 40_000) {
 | 
			
		||||
            // To old to ring?
 | 
			
		||||
            // Too old to ring?
 | 
			
		||||
            return
 | 
			
		||||
        }
 | 
			
		||||
        callSignalingHandler.onCallEvent(event)
 | 
			
		||||
 | 
			
		||||
@ -41,6 +41,7 @@ internal class CallSignalingHandler @Inject constructor(private val activeCallHa
 | 
			
		||||
                                                        private val mxCallFactory: MxCallFactory,
 | 
			
		||||
                                                        @UserId private val userId: String) {
 | 
			
		||||
 | 
			
		||||
    private val invitedCallIds = mutableSetOf<String>()
 | 
			
		||||
    private val callListeners = mutableSetOf<CallListener>()
 | 
			
		||||
    private val callListenersDispatcher = CallListenersDispatcher(callListeners)
 | 
			
		||||
 | 
			
		||||
@ -182,17 +183,17 @@ internal class CallSignalingHandler @Inject constructor(private val activeCallHa
 | 
			
		||||
        val content = event.getClearContent().toModel<CallInviteContent>() ?: return
 | 
			
		||||
 | 
			
		||||
        content.callId ?: return
 | 
			
		||||
        if (activeCallHandler.getCallWithId(content.callId) != null) {
 | 
			
		||||
        if (invitedCallIds.contains(content.callId)) {
 | 
			
		||||
            // Call is already known, maybe due to fast lane. Ignore
 | 
			
		||||
            Timber.d("Ignoring already known call invite")
 | 
			
		||||
            return
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        val incomingCall = mxCallFactory.createIncomingCall(
 | 
			
		||||
                roomId = event.roomId,
 | 
			
		||||
                opponentUserId = event.senderId,
 | 
			
		||||
                content = content
 | 
			
		||||
        ) ?: return
 | 
			
		||||
        invitedCallIds.add(content.callId)
 | 
			
		||||
        activeCallHandler.addCall(incomingCall)
 | 
			
		||||
        callListenersDispatcher.onCallInviteReceived(incomingCall, content)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@ -29,7 +29,7 @@ internal class DefaultEventService @Inject constructor(
 | 
			
		||||
 | 
			
		||||
    override suspend fun getEvent(roomId: String, eventId: String): Event {
 | 
			
		||||
        val event = getEventTask.execute(GetEventTask.Params(roomId, eventId))
 | 
			
		||||
 | 
			
		||||
        event.ageLocalTs = event.unsignedData?.age?.let { System.currentTimeMillis() - it }
 | 
			
		||||
        // Fast lane to the call event processors: try to make the incoming call ring faster
 | 
			
		||||
        if (callEventProcessor.shouldProcessFastLane(event.getClearType())) {
 | 
			
		||||
            callEventProcessor.processFastLane(event)
 | 
			
		||||
 | 
			
		||||
@ -134,6 +134,10 @@ internal class DefaultRoomService @Inject constructor(
 | 
			
		||||
        deleteRoomAliasTask.execute(DeleteRoomAliasTask.Params(roomAlias))
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun getChangeMemberships(roomIdOrAlias: String): ChangeMembershipState {
 | 
			
		||||
        return roomChangeMembershipStateDataSource.getState(roomIdOrAlias)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun getChangeMembershipsLive(): LiveData<Map<String, ChangeMembershipState>> {
 | 
			
		||||
        return roomChangeMembershipStateDataSource.getLiveStates()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@ -21,6 +21,7 @@ import androidx.lifecycle.MutableLiveData
 | 
			
		||||
import org.matrix.android.sdk.api.session.room.members.ChangeMembershipState
 | 
			
		||||
import org.matrix.android.sdk.api.session.room.model.Membership
 | 
			
		||||
import org.matrix.android.sdk.internal.session.SessionScope
 | 
			
		||||
import java.util.concurrent.ConcurrentHashMap
 | 
			
		||||
import javax.inject.Inject
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
@ -30,7 +31,7 @@ import javax.inject.Inject
 | 
			
		||||
internal class RoomChangeMembershipStateDataSource @Inject constructor() {
 | 
			
		||||
 | 
			
		||||
    private val mutableLiveStates = MutableLiveData<Map<String, ChangeMembershipState>>(emptyMap())
 | 
			
		||||
    private val states = HashMap<String, ChangeMembershipState>()
 | 
			
		||||
    private val states = ConcurrentHashMap<String, ChangeMembershipState>()
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * This will update local states to be synced with the server.
 | 
			
		||||
 | 
			
		||||
@ -54,6 +54,10 @@ internal class DefaultJoinRoomTask @Inject constructor(
 | 
			
		||||
) : JoinRoomTask {
 | 
			
		||||
 | 
			
		||||
    override suspend fun execute(params: JoinRoomTask.Params) {
 | 
			
		||||
        val currentState = roomChangeMembershipStateDataSource.getState(params.roomIdOrAlias)
 | 
			
		||||
        if (currentState.isInProgress() || currentState == ChangeMembershipState.Joined) {
 | 
			
		||||
            return
 | 
			
		||||
        }
 | 
			
		||||
        roomChangeMembershipStateDataSource.updateState(params.roomIdOrAlias, ChangeMembershipState.Joining)
 | 
			
		||||
        val joinRoomResponse = try {
 | 
			
		||||
            executeRequest(globalErrorReceiver) {
 | 
			
		||||
 | 
			
		||||
@ -46,7 +46,7 @@ internal object WorkerParamsFactory {
 | 
			
		||||
 | 
			
		||||
    inline fun <reified T> fromData(data: Data) = fromData(T::class.java, data)
 | 
			
		||||
 | 
			
		||||
    fun <T> fromData(clazz: Class<T>, data: Data): T? = tryOrNull("Unable to parse work parameters") {
 | 
			
		||||
    fun <T> fromData(clazz: Class<T>, data: Data): T? = tryOrNull<T?>("Unable to parse work parameters") {
 | 
			
		||||
        val json = data.getString(KEY)
 | 
			
		||||
        return if (json == null) {
 | 
			
		||||
            null
 | 
			
		||||
 | 
			
		||||
@ -43,7 +43,7 @@ android {
 | 
			
		||||
dependencies {
 | 
			
		||||
    implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
 | 
			
		||||
    implementation 'androidx.appcompat:appcompat:1.3.0'
 | 
			
		||||
    implementation "androidx.fragment:fragment-ktx:1.3.4"
 | 
			
		||||
    implementation "androidx.fragment:fragment-ktx:1.3.5"
 | 
			
		||||
    implementation 'androidx.exifinterface:exifinterface:1.3.2'
 | 
			
		||||
 | 
			
		||||
    // Log
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										1
									
								
								newsfragment/3207.bugfix
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								newsfragment/3207.bugfix
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1 @@
 | 
			
		||||
Space Explore Rooms no feedback on failed to join
 | 
			
		||||
							
								
								
									
										1
									
								
								newsfragment/3520.misc
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								newsfragment/3520.misc
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1 @@
 | 
			
		||||
VoIP: Merge virtual room timeline in corresponding native room (call events only).
 | 
			
		||||
							
								
								
									
										1
									
								
								newsfragment/3531.feature
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								newsfragment/3531.feature
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1 @@
 | 
			
		||||
Introduces AutoAcceptInvites which can be enabled at compile time.
 | 
			
		||||
@ -300,7 +300,7 @@ android {
 | 
			
		||||
dependencies {
 | 
			
		||||
 | 
			
		||||
    def epoxy_version = '4.6.2'
 | 
			
		||||
    def fragment_version = '1.3.4'
 | 
			
		||||
    def fragment_version = '1.3.5'
 | 
			
		||||
    def arrow_version = "0.8.2"
 | 
			
		||||
    def markwon_version = '4.1.2'
 | 
			
		||||
    def big_image_viewer_version = '1.8.0'
 | 
			
		||||
@ -315,7 +315,7 @@ dependencies {
 | 
			
		||||
    def jjwt_version = '0.11.2'
 | 
			
		||||
 | 
			
		||||
    // Tests
 | 
			
		||||
    def kluent_version = '1.65'
 | 
			
		||||
    def kluent_version = '1.67'
 | 
			
		||||
    def androidxTest_version = '1.3.0'
 | 
			
		||||
    def espresso_version = '3.3.0'
 | 
			
		||||
 | 
			
		||||
@ -355,7 +355,7 @@ dependencies {
 | 
			
		||||
    implementation 'com.facebook.stetho:stetho:1.6.0'
 | 
			
		||||
 | 
			
		||||
    // Phone number https://github.com/google/libphonenumber
 | 
			
		||||
    implementation 'com.googlecode.libphonenumber:libphonenumber:8.12.24'
 | 
			
		||||
    implementation 'com.googlecode.libphonenumber:libphonenumber:8.12.26'
 | 
			
		||||
 | 
			
		||||
    // rx
 | 
			
		||||
    implementation 'io.reactivex.rxjava2:rxkotlin:2.4.0'
 | 
			
		||||
 | 
			
		||||
@ -114,6 +114,12 @@
 | 
			
		||||
                    android:text="Vector" />
 | 
			
		||||
            </LinearLayout>
 | 
			
		||||
 | 
			
		||||
            <Button
 | 
			
		||||
                android:id="@+id/debug_open_button_styles_dark"
 | 
			
		||||
                android:layout_width="wrap_content"
 | 
			
		||||
                android:layout_height="wrap_content"
 | 
			
		||||
                android:text="See button dark" />
 | 
			
		||||
 | 
			
		||||
            <Button
 | 
			
		||||
                android:id="@+id/debug_test_text_view_dark"
 | 
			
		||||
                android:layout_width="wrap_content"
 | 
			
		||||
@ -122,12 +128,6 @@
 | 
			
		||||
                android:layout_weight="1"
 | 
			
		||||
                android:text="Text Views Dark" />
 | 
			
		||||
 | 
			
		||||
            <Button
 | 
			
		||||
                android:id="@+id/debug_open_button_styles_dark"
 | 
			
		||||
                android:layout_width="wrap_content"
 | 
			
		||||
                android:layout_height="wrap_content"
 | 
			
		||||
                android:text="See button dark" />
 | 
			
		||||
 | 
			
		||||
            <Button
 | 
			
		||||
                android:id="@+id/debug_show_sas_emoji"
 | 
			
		||||
                android:layout_width="wrap_content"
 | 
			
		||||
 | 
			
		||||
@ -49,13 +49,12 @@ fun RoomGroupingMethod.group() = (this as? RoomGroupingMethod.ByLegacyGroup)?.gr
 | 
			
		||||
// TODO Keep this class for now, will maybe be used fro Space
 | 
			
		||||
@Singleton
 | 
			
		||||
class AppStateHandler @Inject constructor(
 | 
			
		||||
        sessionDataSource: ActiveSessionDataSource,
 | 
			
		||||
        private val sessionDataSource: ActiveSessionDataSource,
 | 
			
		||||
        private val uiStateRepository: UiStateRepository,
 | 
			
		||||
        private val activeSessionHolder: ActiveSessionHolder
 | 
			
		||||
) : LifecycleObserver {
 | 
			
		||||
 | 
			
		||||
    private val compositeDisposable = CompositeDisposable()
 | 
			
		||||
 | 
			
		||||
    private val selectedSpaceDataSource = BehaviorDataSource<Option<RoomGroupingMethod>>(Option.empty())
 | 
			
		||||
 | 
			
		||||
    val selectedRoomGroupingObservable = selectedSpaceDataSource.observe()
 | 
			
		||||
@ -92,11 +91,11 @@ class AppStateHandler @Inject constructor(
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    init {
 | 
			
		||||
    private fun observeActiveSession() {
 | 
			
		||||
        sessionDataSource.observe()
 | 
			
		||||
                .distinctUntilChanged()
 | 
			
		||||
                .subscribe {
 | 
			
		||||
                    // sessionDataSource could already return a session while acitveSession holder still returns null
 | 
			
		||||
                    // sessionDataSource could already return a session while activeSession holder still returns null
 | 
			
		||||
                    it.orNull()?.let { session ->
 | 
			
		||||
                        if (uiStateRepository.isGroupingMethodSpace(session.sessionId)) {
 | 
			
		||||
                            setCurrentSpace(uiStateRepository.getSelectedSpace(session.sessionId), session)
 | 
			
		||||
@ -119,6 +118,7 @@ class AppStateHandler @Inject constructor(
 | 
			
		||||
 | 
			
		||||
    @OnLifecycleEvent(Lifecycle.Event.ON_RESUME)
 | 
			
		||||
    fun entersForeground() {
 | 
			
		||||
        observeActiveSession()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @OnLifecycleEvent(Lifecycle.Event.ON_PAUSE)
 | 
			
		||||
@ -126,7 +126,7 @@ class AppStateHandler @Inject constructor(
 | 
			
		||||
        compositeDisposable.clear()
 | 
			
		||||
        val session = activeSessionHolder.getSafeActiveSession() ?: return
 | 
			
		||||
        when (val currentMethod = selectedSpaceDataSource.currentValue?.orNull() ?: RoomGroupingMethod.BySpace(null)) {
 | 
			
		||||
            is RoomGroupingMethod.BySpace -> {
 | 
			
		||||
            is RoomGroupingMethod.BySpace       -> {
 | 
			
		||||
                uiStateRepository.storeGroupingMethod(true, session.sessionId)
 | 
			
		||||
                uiStateRepository.storeSelectedSpace(currentMethod.spaceSummary?.roomId, session.sessionId)
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
@ -47,6 +47,7 @@ import im.vector.app.core.rx.RxConfig
 | 
			
		||||
import im.vector.app.features.call.webrtc.WebRtcCallManager
 | 
			
		||||
import im.vector.app.features.configuration.VectorConfiguration
 | 
			
		||||
import im.vector.app.features.disclaimer.doNotShowDisclaimerDialog
 | 
			
		||||
import im.vector.app.features.invite.InvitesAcceptor
 | 
			
		||||
import im.vector.app.features.lifecycle.VectorActivityLifecycleCallbacks
 | 
			
		||||
import im.vector.app.features.notifications.NotificationDrawerManager
 | 
			
		||||
import im.vector.app.features.notifications.NotificationUtils
 | 
			
		||||
@ -95,6 +96,7 @@ class VectorApplication :
 | 
			
		||||
    @Inject lateinit var popupAlertManager: PopupAlertManager
 | 
			
		||||
    @Inject lateinit var pinLocker: PinLocker
 | 
			
		||||
    @Inject lateinit var callManager: WebRtcCallManager
 | 
			
		||||
    @Inject lateinit var invitesAcceptor: InvitesAcceptor
 | 
			
		||||
 | 
			
		||||
    lateinit var vectorComponent: VectorComponent
 | 
			
		||||
 | 
			
		||||
@ -116,6 +118,7 @@ class VectorApplication :
 | 
			
		||||
        appContext = this
 | 
			
		||||
        vectorComponent = DaggerVectorComponent.factory().create(this)
 | 
			
		||||
        vectorComponent.inject(this)
 | 
			
		||||
        invitesAcceptor.initialize()
 | 
			
		||||
        vectorUncaughtExceptionHandler.activate(this)
 | 
			
		||||
        rxConfig.setupRxPlugin()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -50,6 +50,7 @@ import im.vector.app.features.home.room.detail.widget.RoomWidgetsBottomSheet
 | 
			
		||||
import im.vector.app.features.home.room.filtered.FilteredRoomsActivity
 | 
			
		||||
import im.vector.app.features.home.room.list.RoomListModule
 | 
			
		||||
import im.vector.app.features.home.room.list.actions.RoomListQuickActionsBottomSheet
 | 
			
		||||
import im.vector.app.features.invite.AutoAcceptInvites
 | 
			
		||||
import im.vector.app.features.invite.InviteUsersToRoomActivity
 | 
			
		||||
import im.vector.app.features.invite.VectorInviteView
 | 
			
		||||
import im.vector.app.features.link.LinkHandlerActivity
 | 
			
		||||
@ -122,6 +123,7 @@ interface ScreenComponent {
 | 
			
		||||
    fun errorFormatter(): ErrorFormatter
 | 
			
		||||
    fun uiStateRepository(): UiStateRepository
 | 
			
		||||
    fun unrecognizedCertificateDialog(): UnrecognizedCertificateDialog
 | 
			
		||||
    fun autoAcceptInvites(): AutoAcceptInvites
 | 
			
		||||
 | 
			
		||||
    /* ==========================================================================================
 | 
			
		||||
     * Activities
 | 
			
		||||
 | 
			
		||||
@ -42,6 +42,7 @@ import im.vector.app.features.home.room.detail.timeline.helper.MatrixItemColorPr
 | 
			
		||||
import im.vector.app.features.home.room.detail.timeline.helper.RoomSummariesHolder
 | 
			
		||||
import im.vector.app.features.html.EventHtmlRenderer
 | 
			
		||||
import im.vector.app.features.html.VectorHtmlCompressor
 | 
			
		||||
import im.vector.app.features.invite.AutoAcceptInvites
 | 
			
		||||
import im.vector.app.features.login.ReAuthHelper
 | 
			
		||||
import im.vector.app.features.navigation.Navigator
 | 
			
		||||
import im.vector.app.features.notifications.NotifiableEventResolver
 | 
			
		||||
@ -160,6 +161,8 @@ interface VectorComponent {
 | 
			
		||||
 | 
			
		||||
    fun pinLocker(): PinLocker
 | 
			
		||||
 | 
			
		||||
    fun autoAcceptInvites(): AutoAcceptInvites
 | 
			
		||||
 | 
			
		||||
    fun webRtcCallManager(): WebRtcCallManager
 | 
			
		||||
 | 
			
		||||
    fun roomSummaryHolder(): RoomSummariesHolder
 | 
			
		||||
 | 
			
		||||
@ -25,6 +25,8 @@ import dagger.Module
 | 
			
		||||
import dagger.Provides
 | 
			
		||||
import im.vector.app.core.error.DefaultErrorFormatter
 | 
			
		||||
import im.vector.app.core.error.ErrorFormatter
 | 
			
		||||
import im.vector.app.features.invite.AutoAcceptInvites
 | 
			
		||||
import im.vector.app.features.invite.CompileTimeAutoAcceptInvites
 | 
			
		||||
import im.vector.app.features.navigation.DefaultNavigator
 | 
			
		||||
import im.vector.app.features.navigation.Navigator
 | 
			
		||||
import im.vector.app.features.pin.PinCodeStore
 | 
			
		||||
@ -105,4 +107,7 @@ abstract class VectorModule {
 | 
			
		||||
 | 
			
		||||
    @Binds
 | 
			
		||||
    abstract fun bindPinCodeStore(store: SharedPrefPinCodeStore): PinCodeStore
 | 
			
		||||
 | 
			
		||||
    @Binds
 | 
			
		||||
    abstract fun bindAutoAcceptInvites(autoAcceptInvites: CompileTimeAutoAcceptInvites): AutoAcceptInvites
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -20,14 +20,11 @@ import android.app.Activity
 | 
			
		||||
import android.text.Editable
 | 
			
		||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
 | 
			
		||||
import im.vector.app.R
 | 
			
		||||
import im.vector.app.core.extensions.showPassword
 | 
			
		||||
import im.vector.app.core.platform.SimpleTextWatcher
 | 
			
		||||
import im.vector.app.databinding.DialogExportE2eKeysBinding
 | 
			
		||||
 | 
			
		||||
class ExportKeysDialog {
 | 
			
		||||
 | 
			
		||||
    private var passwordVisible = false
 | 
			
		||||
 | 
			
		||||
    fun show(activity: Activity, exportKeyDialogListener: ExportKeyDialogListener) {
 | 
			
		||||
        val dialogLayout = activity.layoutInflater.inflate(R.layout.dialog_export_e2e_keys, null)
 | 
			
		||||
        val views = DialogExportE2eKeysBinding.bind(dialogLayout)
 | 
			
		||||
@ -57,13 +54,6 @@ class ExportKeysDialog {
 | 
			
		||||
        views.exportDialogEt.addTextChangedListener(textWatcher)
 | 
			
		||||
        views.exportDialogEtConfirm.addTextChangedListener(textWatcher)
 | 
			
		||||
 | 
			
		||||
        views.exportDialogShowPassword.setOnClickListener {
 | 
			
		||||
            passwordVisible = !passwordVisible
 | 
			
		||||
            views.exportDialogEt.showPassword(passwordVisible)
 | 
			
		||||
            views.exportDialogEtConfirm.showPassword(passwordVisible)
 | 
			
		||||
            views.exportDialogShowPassword.render(passwordVisible)
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        val exportDialog = builder.show()
 | 
			
		||||
 | 
			
		||||
        views.exportDialogSubmit.setOnClickListener {
 | 
			
		||||
 | 
			
		||||
@ -1,27 +0,0 @@
 | 
			
		||||
/*
 | 
			
		||||
 * Copyright 2019 New Vector Ltd
 | 
			
		||||
 *
 | 
			
		||||
 * Licensed under the Apache License, Version 2.0 (the "License");
 | 
			
		||||
 * you may not use this file except in compliance with the License.
 | 
			
		||||
 * You may obtain a copy of the License at
 | 
			
		||||
 *
 | 
			
		||||
 *     http://www.apache.org/licenses/LICENSE-2.0
 | 
			
		||||
 *
 | 
			
		||||
 * Unless required by applicable law or agreed to in writing, software
 | 
			
		||||
 * distributed under the License is distributed on an "AS IS" BASIS,
 | 
			
		||||
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | 
			
		||||
 * See the License for the specific language governing permissions and
 | 
			
		||||
 * limitations under the License.
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
package im.vector.app.core.dialogs
 | 
			
		||||
 | 
			
		||||
import androidx.annotation.AttrRes
 | 
			
		||||
import androidx.appcompat.app.AlertDialog
 | 
			
		||||
import im.vector.app.R
 | 
			
		||||
import im.vector.app.features.themes.ThemeUtils
 | 
			
		||||
 | 
			
		||||
fun AlertDialog.withColoredButton(whichButton: Int, @AttrRes color: Int = R.attr.colorError): AlertDialog {
 | 
			
		||||
    getButton(whichButton)?.setTextColor(ThemeUtils.getColor(context, color))
 | 
			
		||||
    return this
 | 
			
		||||
}
 | 
			
		||||
@ -1,83 +0,0 @@
 | 
			
		||||
/*
 | 
			
		||||
 * Copyright 2020 New Vector Ltd
 | 
			
		||||
 *
 | 
			
		||||
 * Licensed under the Apache License, Version 2.0 (the "License");
 | 
			
		||||
 * you may not use this file except in compliance with the License.
 | 
			
		||||
 * You may obtain a copy of the License at
 | 
			
		||||
 *
 | 
			
		||||
 * http://www.apache.org/licenses/LICENSE-2.0
 | 
			
		||||
 *
 | 
			
		||||
 * Unless required by applicable law or agreed to in writing, software
 | 
			
		||||
 * distributed under the License is distributed on an "AS IS" BASIS,
 | 
			
		||||
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | 
			
		||||
 * See the License for the specific language governing permissions and
 | 
			
		||||
 * limitations under the License.
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
package im.vector.app.core.dialogs
 | 
			
		||||
 | 
			
		||||
import android.app.Activity
 | 
			
		||||
import android.content.DialogInterface
 | 
			
		||||
import android.text.Editable
 | 
			
		||||
import android.view.KeyEvent
 | 
			
		||||
import androidx.appcompat.app.AlertDialog
 | 
			
		||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
 | 
			
		||||
import im.vector.app.R
 | 
			
		||||
import im.vector.app.core.extensions.hideKeyboard
 | 
			
		||||
import im.vector.app.core.extensions.showPassword
 | 
			
		||||
import im.vector.app.core.platform.SimpleTextWatcher
 | 
			
		||||
import im.vector.app.databinding.DialogPromptPasswordBinding
 | 
			
		||||
 | 
			
		||||
class PromptPasswordDialog {
 | 
			
		||||
 | 
			
		||||
    private var passwordVisible = false
 | 
			
		||||
 | 
			
		||||
    fun show(activity: Activity, listener: (String) -> Unit) {
 | 
			
		||||
        val dialogLayout = activity.layoutInflater.inflate(R.layout.dialog_prompt_password, null)
 | 
			
		||||
        val views = DialogPromptPasswordBinding.bind(dialogLayout)
 | 
			
		||||
        val textWatcher = object : SimpleTextWatcher() {
 | 
			
		||||
            override fun afterTextChanged(s: Editable) {
 | 
			
		||||
                views.promptPasswordTil.error = null
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        views.promptPassword.addTextChangedListener(textWatcher)
 | 
			
		||||
 | 
			
		||||
        views.promptPasswordPasswordReveal.setOnClickListener {
 | 
			
		||||
            passwordVisible = !passwordVisible
 | 
			
		||||
            views.promptPassword.showPassword(passwordVisible)
 | 
			
		||||
            views.promptPasswordPasswordReveal.render(passwordVisible)
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        MaterialAlertDialogBuilder(activity)
 | 
			
		||||
                .setIcon(android.R.drawable.ic_dialog_alert)
 | 
			
		||||
                .setTitle(R.string.devices_delete_dialog_title)
 | 
			
		||||
                .setView(dialogLayout)
 | 
			
		||||
                .setPositiveButton(R.string.auth_submit, null)
 | 
			
		||||
                .setNegativeButton(R.string.cancel, null)
 | 
			
		||||
                .setOnKeyListener(DialogInterface.OnKeyListener { dialog, keyCode, event ->
 | 
			
		||||
                    if (event.action == KeyEvent.ACTION_UP && keyCode == KeyEvent.KEYCODE_BACK) {
 | 
			
		||||
                        dialog.cancel()
 | 
			
		||||
                        return@OnKeyListener true
 | 
			
		||||
                    }
 | 
			
		||||
                    false
 | 
			
		||||
                })
 | 
			
		||||
                .setOnDismissListener {
 | 
			
		||||
                    dialogLayout.hideKeyboard()
 | 
			
		||||
                }
 | 
			
		||||
                .create()
 | 
			
		||||
                .apply {
 | 
			
		||||
                    setOnShowListener {
 | 
			
		||||
                        getButton(AlertDialog.BUTTON_POSITIVE)
 | 
			
		||||
                                .setOnClickListener {
 | 
			
		||||
                                    if (views.promptPassword.text.toString().isEmpty()) {
 | 
			
		||||
                                        views.promptPasswordTil.error = activity.getString(R.string.error_empty_field_your_password)
 | 
			
		||||
                                    } else {
 | 
			
		||||
                                        listener.invoke(views.promptPassword.text.toString())
 | 
			
		||||
                                        dismiss()
 | 
			
		||||
                                    }
 | 
			
		||||
                                }
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
                .show()
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@ -40,13 +40,8 @@ fun SearchView.withoutLeftMargin() {
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
fun EditText.showPassword(visible: Boolean, updateCursor: Boolean = true) {
 | 
			
		||||
    if (visible) {
 | 
			
		||||
        inputType = InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD
 | 
			
		||||
    } else {
 | 
			
		||||
        inputType = InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_VARIATION_PASSWORD
 | 
			
		||||
    }
 | 
			
		||||
    if (updateCursor) setSelection(text?.length ?: 0)
 | 
			
		||||
fun EditText.hidePassword() {
 | 
			
		||||
    inputType = InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
fun View.getMeasurements(): Pair<Int, Int> {
 | 
			
		||||
 | 
			
		||||
@ -14,11 +14,8 @@
 | 
			
		||||
 * limitations under the License.
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
@file:Suppress("DEPRECATION")
 | 
			
		||||
 | 
			
		||||
package im.vector.app.core.platform
 | 
			
		||||
 | 
			
		||||
import android.app.ProgressDialog
 | 
			
		||||
import android.content.Context
 | 
			
		||||
import android.os.Bundle
 | 
			
		||||
import android.os.Parcelable
 | 
			
		||||
@ -29,11 +26,12 @@ import android.view.View
 | 
			
		||||
import android.view.ViewGroup
 | 
			
		||||
import androidx.annotation.CallSuper
 | 
			
		||||
import androidx.annotation.MainThread
 | 
			
		||||
import com.google.android.material.appbar.MaterialToolbar
 | 
			
		||||
import androidx.appcompat.app.AlertDialog
 | 
			
		||||
import androidx.lifecycle.ViewModelProvider
 | 
			
		||||
import androidx.viewbinding.ViewBinding
 | 
			
		||||
import com.airbnb.mvrx.BaseMvRxFragment
 | 
			
		||||
import com.bumptech.glide.util.Util.assertMainThread
 | 
			
		||||
import com.google.android.material.appbar.MaterialToolbar
 | 
			
		||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
 | 
			
		||||
import com.jakewharton.rxbinding3.view.clicks
 | 
			
		||||
import im.vector.app.R
 | 
			
		||||
@ -44,14 +42,14 @@ import im.vector.app.core.dialogs.UnrecognizedCertificateDialog
 | 
			
		||||
import im.vector.app.core.error.ErrorFormatter
 | 
			
		||||
import im.vector.app.core.extensions.toMvRxBundle
 | 
			
		||||
import im.vector.app.features.navigation.Navigator
 | 
			
		||||
import im.vector.lib.ui.styles.dialogs.MaterialProgressDialog
 | 
			
		||||
import io.reactivex.android.schedulers.AndroidSchedulers
 | 
			
		||||
import io.reactivex.disposables.CompositeDisposable
 | 
			
		||||
import io.reactivex.disposables.Disposable
 | 
			
		||||
 | 
			
		||||
import timber.log.Timber
 | 
			
		||||
import java.util.concurrent.TimeUnit
 | 
			
		||||
 | 
			
		||||
abstract class VectorBaseFragment<VB: ViewBinding> : BaseMvRxFragment(), HasScreenInjector {
 | 
			
		||||
abstract class VectorBaseFragment<VB : ViewBinding> : BaseMvRxFragment(), HasScreenInjector {
 | 
			
		||||
 | 
			
		||||
    protected val vectorBaseActivity: VectorBaseActivity<*> by lazy {
 | 
			
		||||
        activity as VectorBaseActivity<*>
 | 
			
		||||
@ -67,7 +65,7 @@ abstract class VectorBaseFragment<VB: ViewBinding> : BaseMvRxFragment(), HasScre
 | 
			
		||||
    protected lateinit var errorFormatter: ErrorFormatter
 | 
			
		||||
    protected lateinit var unrecognizedCertificateDialog: UnrecognizedCertificateDialog
 | 
			
		||||
 | 
			
		||||
    private var progress: ProgressDialog? = null
 | 
			
		||||
    private var progress: AlertDialog? = null
 | 
			
		||||
 | 
			
		||||
    /* ==========================================================================================
 | 
			
		||||
     * View model
 | 
			
		||||
@ -203,14 +201,10 @@ abstract class VectorBaseFragment<VB: ViewBinding> : BaseMvRxFragment(), HasScre
 | 
			
		||||
        vectorBaseActivity.getCoordinatorLayout()?.showOptimizedSnackbar(errorFormatter.toHumanReadable(throwable))
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    protected fun showLoadingDialog(message: CharSequence? = null, cancelable: Boolean = false) {
 | 
			
		||||
    protected fun showLoadingDialog(message: CharSequence? = null) {
 | 
			
		||||
        progress?.dismiss()
 | 
			
		||||
        progress = ProgressDialog(requireContext()).apply {
 | 
			
		||||
            setCancelable(cancelable)
 | 
			
		||||
            setMessage(message ?: getString(R.string.please_wait))
 | 
			
		||||
            setProgressStyle(ProgressDialog.STYLE_SPINNER)
 | 
			
		||||
            show()
 | 
			
		||||
        }
 | 
			
		||||
        progress = MaterialProgressDialog(requireContext())
 | 
			
		||||
                .show(message ?: getString(R.string.please_wait))
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    protected fun dismissLoadingDialog() {
 | 
			
		||||
 | 
			
		||||
@ -1,56 +0,0 @@
 | 
			
		||||
/*
 | 
			
		||||
 * Copyright 2019 New Vector Ltd
 | 
			
		||||
 *
 | 
			
		||||
 * Licensed under the Apache License, Version 2.0 (the "License");
 | 
			
		||||
 * you may not use this file except in compliance with the License.
 | 
			
		||||
 * You may obtain a copy of the License at
 | 
			
		||||
 *
 | 
			
		||||
 *     http://www.apache.org/licenses/LICENSE-2.0
 | 
			
		||||
 *
 | 
			
		||||
 * Unless required by applicable law or agreed to in writing, software
 | 
			
		||||
 * distributed under the License is distributed on an "AS IS" BASIS,
 | 
			
		||||
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | 
			
		||||
 * See the License for the specific language governing permissions and
 | 
			
		||||
 * limitations under the License.
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
package im.vector.app.core.ui.views
 | 
			
		||||
 | 
			
		||||
import android.content.Context
 | 
			
		||||
import android.util.AttributeSet
 | 
			
		||||
import android.view.View
 | 
			
		||||
import android.widget.RelativeLayout
 | 
			
		||||
import androidx.core.content.ContextCompat
 | 
			
		||||
import im.vector.app.R
 | 
			
		||||
import im.vector.app.databinding.ViewJumpToReadMarkerBinding
 | 
			
		||||
 | 
			
		||||
class JumpToReadMarkerView @JvmOverloads constructor(
 | 
			
		||||
        context: Context,
 | 
			
		||||
        attrs: AttributeSet? = null,
 | 
			
		||||
        defStyleAttr: Int = 0
 | 
			
		||||
) : RelativeLayout(context, attrs, defStyleAttr) {
 | 
			
		||||
 | 
			
		||||
    interface Callback {
 | 
			
		||||
        fun onJumpToReadMarkerClicked()
 | 
			
		||||
        fun onClearReadMarkerClicked()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    var callback: Callback? = null
 | 
			
		||||
 | 
			
		||||
    init {
 | 
			
		||||
        setupView()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private fun setupView() {
 | 
			
		||||
        inflate(context, R.layout.view_jump_to_read_marker, this)
 | 
			
		||||
        val views = ViewJumpToReadMarkerBinding.bind(this)
 | 
			
		||||
        setBackgroundColor(ContextCompat.getColor(context, R.color.notification_accent_color))
 | 
			
		||||
        views.jumpToReadMarkerLabelView.setOnClickListener {
 | 
			
		||||
            callback?.onJumpToReadMarkerClicked()
 | 
			
		||||
        }
 | 
			
		||||
        views.closeJumpToReadMarkerView.setOnClickListener {
 | 
			
		||||
            visibility = View.INVISIBLE
 | 
			
		||||
            callback?.onClearReadMarkerClicked()
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@ -1,43 +0,0 @@
 | 
			
		||||
/*
 | 
			
		||||
 * Copyright (c) 2021 New Vector Ltd
 | 
			
		||||
 *
 | 
			
		||||
 * Licensed under the Apache License, Version 2.0 (the "License");
 | 
			
		||||
 * you may not use this file except in compliance with the License.
 | 
			
		||||
 * You may obtain a copy of the License at
 | 
			
		||||
 *
 | 
			
		||||
 *     http://www.apache.org/licenses/LICENSE-2.0
 | 
			
		||||
 *
 | 
			
		||||
 * Unless required by applicable law or agreed to in writing, software
 | 
			
		||||
 * distributed under the License is distributed on an "AS IS" BASIS,
 | 
			
		||||
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | 
			
		||||
 * See the License for the specific language governing permissions and
 | 
			
		||||
 * limitations under the License.
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
package im.vector.app.core.ui.views
 | 
			
		||||
 | 
			
		||||
import android.content.Context
 | 
			
		||||
import android.util.AttributeSet
 | 
			
		||||
import androidx.appcompat.widget.AppCompatImageView
 | 
			
		||||
import im.vector.app.R
 | 
			
		||||
 | 
			
		||||
class RevealPasswordImageView @JvmOverloads constructor(
 | 
			
		||||
        context: Context,
 | 
			
		||||
        attrs: AttributeSet? = null,
 | 
			
		||||
        defStyleAttr: Int = 0
 | 
			
		||||
) : AppCompatImageView(context, attrs, defStyleAttr) {
 | 
			
		||||
 | 
			
		||||
    init {
 | 
			
		||||
        render(false)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun render(isPasswordShown: Boolean) {
 | 
			
		||||
        if (isPasswordShown) {
 | 
			
		||||
            contentDescription = context.getString(R.string.a11y_hide_password)
 | 
			
		||||
            setImageResource(R.drawable.ic_eye_closed)
 | 
			
		||||
        } else {
 | 
			
		||||
            contentDescription = context.getString(R.string.a11y_show_password)
 | 
			
		||||
            setImageResource(R.drawable.ic_eye)
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@ -24,7 +24,6 @@ import androidx.core.view.isVisible
 | 
			
		||||
import com.airbnb.mvrx.activityViewModel
 | 
			
		||||
import com.airbnb.mvrx.withState
 | 
			
		||||
import im.vector.app.R
 | 
			
		||||
import im.vector.app.core.extensions.showPassword
 | 
			
		||||
import im.vector.app.core.platform.VectorBaseFragment
 | 
			
		||||
import im.vector.app.databinding.FragmentReauthConfirmBinding
 | 
			
		||||
import org.matrix.android.sdk.api.auth.data.LoginFlowTypes
 | 
			
		||||
@ -41,13 +40,6 @@ class PromptFragment : VectorBaseFragment<FragmentReauthConfirmBinding>() {
 | 
			
		||||
        views.reAuthConfirmButton.debouncedClicks {
 | 
			
		||||
            onButtonClicked()
 | 
			
		||||
        }
 | 
			
		||||
        views.passwordReveal.debouncedClicks {
 | 
			
		||||
            viewModel.handle(ReAuthActions.StartSSOFallback)
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        views.passwordReveal.debouncedClicks {
 | 
			
		||||
            viewModel.handle(ReAuthActions.TogglePassVisibility)
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private fun onButtonClicked() = withState(viewModel) { state ->
 | 
			
		||||
@ -74,11 +66,11 @@ class PromptFragment : VectorBaseFragment<FragmentReauthConfirmBinding>() {
 | 
			
		||||
    override fun invalidate() = withState(viewModel) {
 | 
			
		||||
        when (it.flowType) {
 | 
			
		||||
            LoginFlowTypes.SSO -> {
 | 
			
		||||
                views.passwordContainer.isVisible = false
 | 
			
		||||
                views.passwordFieldTil.isVisible = false
 | 
			
		||||
                views.reAuthConfirmButton.text = getString(R.string.auth_login_sso)
 | 
			
		||||
            }
 | 
			
		||||
            LoginFlowTypes.PASSWORD -> {
 | 
			
		||||
                views.passwordContainer.isVisible = true
 | 
			
		||||
                views.passwordFieldTil.isVisible = true
 | 
			
		||||
                views.reAuthConfirmButton.text = getString(R.string._continue)
 | 
			
		||||
            }
 | 
			
		||||
            else                    -> {
 | 
			
		||||
@ -86,9 +78,6 @@ class PromptFragment : VectorBaseFragment<FragmentReauthConfirmBinding>() {
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        views.passwordField.showPassword(it.passwordVisible)
 | 
			
		||||
        views.passwordReveal.render(it.passwordVisible)
 | 
			
		||||
 | 
			
		||||
        if (it.lastErrorCode != null) {
 | 
			
		||||
            when (it.flowType) {
 | 
			
		||||
                LoginFlowTypes.SSO      -> {
 | 
			
		||||
 | 
			
		||||
@ -22,6 +22,5 @@ sealed class ReAuthActions : VectorViewModelAction {
 | 
			
		||||
    object StartSSOFallback : ReAuthActions()
 | 
			
		||||
    object FallBackPageLoaded : ReAuthActions()
 | 
			
		||||
    object FallBackPageClosed : ReAuthActions()
 | 
			
		||||
    object TogglePassVisibility : ReAuthActions()
 | 
			
		||||
    data class ReAuthWithPass(val password: String) : ReAuthActions()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -23,7 +23,6 @@ data class ReAuthState(
 | 
			
		||||
        val session: String? = null,
 | 
			
		||||
        val flowType: String? = null,
 | 
			
		||||
        val ssoFallbackPageWasShown: Boolean = false,
 | 
			
		||||
        val passwordVisible: Boolean = false,
 | 
			
		||||
        val lastErrorCode: String? = null,
 | 
			
		||||
        val resultKeyStoreAlias: String = ""
 | 
			
		||||
) : MvRxState {
 | 
			
		||||
 | 
			
		||||
@ -65,13 +65,6 @@ class ReAuthViewModel @AssistedInject constructor(
 | 
			
		||||
            ReAuthActions.FallBackPageClosed -> {
 | 
			
		||||
                // Should we do something here?
 | 
			
		||||
            }
 | 
			
		||||
            ReAuthActions.TogglePassVisibility -> {
 | 
			
		||||
                setState {
 | 
			
		||||
                    copy(
 | 
			
		||||
                            passwordVisible = !state.passwordVisible
 | 
			
		||||
                    )
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            is ReAuthActions.ReAuthWithPass -> {
 | 
			
		||||
                val safeForIntentCypher = ByteArrayOutputStream().also {
 | 
			
		||||
                    it.use {
 | 
			
		||||
 | 
			
		||||
@ -27,11 +27,21 @@ import org.matrix.android.sdk.api.session.room.model.create.CreateRoomParams
 | 
			
		||||
class CallUserMapper(private val session: Session, private val protocolsChecker: CallProtocolsChecker) {
 | 
			
		||||
 | 
			
		||||
    fun nativeRoomForVirtualRoom(roomId: String): String? {
 | 
			
		||||
        if (!protocolsChecker.supportVirtualRooms) return null
 | 
			
		||||
        val virtualRoom = session.getRoom(roomId) ?: return null
 | 
			
		||||
        val virtualRoomEvent = virtualRoom.getAccountDataEvent(RoomAccountDataTypes.EVENT_TYPE_VIRTUAL_ROOM)
 | 
			
		||||
        return virtualRoomEvent?.content?.toModel<RoomVirtualContent>()?.nativeRoomId
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun virtualRoomForNativeRoom(roomId: String): String? {
 | 
			
		||||
        if (!protocolsChecker.supportVirtualRooms) return null
 | 
			
		||||
        val virtualRoomEvents = session.accountDataService().getRoomAccountDataEvents(setOf(RoomAccountDataTypes.EVENT_TYPE_VIRTUAL_ROOM))
 | 
			
		||||
        return virtualRoomEvents.firstOrNull {
 | 
			
		||||
            val virtualRoomContent = it.content.toModel<RoomVirtualContent>()
 | 
			
		||||
            virtualRoomContent?.nativeRoomId == roomId
 | 
			
		||||
        }?.roomId
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    suspend fun getOrCreateVirtualRoomForRoom(roomId: String, opponentUserId: String): String? {
 | 
			
		||||
        protocolsChecker.awaitCheckProtocols()
 | 
			
		||||
        if (!protocolsChecker.supportVirtualRooms) return null
 | 
			
		||||
@ -57,10 +67,6 @@ class CallUserMapper(private val session: Session, private val protocolsChecker:
 | 
			
		||||
                // will make sure we know where how to map calls and also allow us know not to display
 | 
			
		||||
                // it in the future.
 | 
			
		||||
                invitedRoom.markVirtual(nativeRoomId)
 | 
			
		||||
                // also auto-join the virtual room if we have a matching native room
 | 
			
		||||
                // (possibly we should only join if we've also joined the native room, then we'd also have
 | 
			
		||||
                // to make sure we joined virtual rooms on joining a native one)
 | 
			
		||||
                session.joinRoom(invitedRoomId)
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@ -83,9 +83,9 @@ import java.util.concurrent.TimeUnit
 | 
			
		||||
import javax.inject.Provider
 | 
			
		||||
import kotlin.coroutines.CoroutineContext
 | 
			
		||||
 | 
			
		||||
private const val STREAM_ID = "ARDAMS"
 | 
			
		||||
private const val AUDIO_TRACK_ID = "ARDAMSa0"
 | 
			
		||||
private const val VIDEO_TRACK_ID = "ARDAMSv0"
 | 
			
		||||
private const val STREAM_ID = "userMedia"
 | 
			
		||||
private const val AUDIO_TRACK_ID = "${STREAM_ID}a0"
 | 
			
		||||
private const val VIDEO_TRACK_ID = "${STREAM_ID}v0"
 | 
			
		||||
private val DEFAULT_AUDIO_CONSTRAINTS = MediaConstraints()
 | 
			
		||||
 | 
			
		||||
class WebRtcCall(
 | 
			
		||||
@ -274,12 +274,77 @@ class WebRtcCall(
 | 
			
		||||
        peerConnection = peerConnectionFactory.createPeerConnection(rtcConfig, PeerConnectionObserver(this))
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun attachViewRenderers(localViewRenderer: SurfaceViewRenderer?, remoteViewRenderer: SurfaceViewRenderer, mode: String?) {
 | 
			
		||||
        Timber.v("## VOIP attachViewRenderers localRendeder $localViewRenderer / $remoteViewRenderer")
 | 
			
		||||
        localSurfaceRenderers.addIfNeeded(localViewRenderer)
 | 
			
		||||
        remoteSurfaceRenderers.addIfNeeded(remoteViewRenderer)
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Without consultation
 | 
			
		||||
     */
 | 
			
		||||
    fun transferToUser(targetUserId: String, targetRoomId: String?) {
 | 
			
		||||
        sessionScope?.launch(dispatcher) {
 | 
			
		||||
            mxCall.transfer(
 | 
			
		||||
                    targetUserId = targetUserId,
 | 
			
		||||
                    targetRoomId = targetRoomId,
 | 
			
		||||
                    createCallId = CallIdGenerator.generate(),
 | 
			
		||||
                    awaitCallId = null
 | 
			
		||||
            )
 | 
			
		||||
            endCall(sendEndSignaling = false)
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * With consultation
 | 
			
		||||
     */
 | 
			
		||||
    fun transferToCall(transferTargetCall: WebRtcCall) {
 | 
			
		||||
        sessionScope?.launch(dispatcher) {
 | 
			
		||||
            val newCallId = CallIdGenerator.generate()
 | 
			
		||||
            transferTargetCall.mxCall.transfer(
 | 
			
		||||
                    targetUserId = mxCall.opponentUserId,
 | 
			
		||||
                    targetRoomId = null,
 | 
			
		||||
                    createCallId = null,
 | 
			
		||||
                    awaitCallId = newCallId
 | 
			
		||||
            )
 | 
			
		||||
            mxCall.transfer(
 | 
			
		||||
                    targetUserId = transferTargetCall.mxCall.opponentUserId,
 | 
			
		||||
                    targetRoomId = null,
 | 
			
		||||
                    createCallId = newCallId,
 | 
			
		||||
                    awaitCallId = null
 | 
			
		||||
            )
 | 
			
		||||
            endCall(sendEndSignaling = false)
 | 
			
		||||
            transferTargetCall.endCall(sendEndSignaling = false)
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun acceptIncomingCall() {
 | 
			
		||||
        sessionScope?.launch {
 | 
			
		||||
            Timber.v("## VOIP acceptIncomingCall from state ${mxCall.state}")
 | 
			
		||||
            if (mxCall.state == CallState.LocalRinging) {
 | 
			
		||||
                internalAcceptIncomingCall()
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Sends a DTMF digit to the other party
 | 
			
		||||
     * @param digit The digit (nb. string - '#' and '*' are dtmf too)
 | 
			
		||||
     */
 | 
			
		||||
    fun sendDtmfDigit(digit: String) {
 | 
			
		||||
        sessionScope?.launch {
 | 
			
		||||
            for (sender in peerConnection?.senders.orEmpty()) {
 | 
			
		||||
                if (sender.track()?.kind() == "audio" && sender.dtmf()?.canInsertDtmf() == true) {
 | 
			
		||||
                    try {
 | 
			
		||||
                        sender.dtmf()?.insertDtmf(digit, 100, 70)
 | 
			
		||||
                        return@launch
 | 
			
		||||
                    } catch (failure: Throwable) {
 | 
			
		||||
                        Timber.v("Fail to send Dtmf digit")
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun attachViewRenderers(localViewRenderer: SurfaceViewRenderer?, remoteViewRenderer: SurfaceViewRenderer, mode: String?) {
 | 
			
		||||
        sessionScope?.launch(dispatcher) {
 | 
			
		||||
            Timber.v("## VOIP attachViewRenderers localRendeder $localViewRenderer / $remoteViewRenderer")
 | 
			
		||||
            localSurfaceRenderers.addIfNeeded(localViewRenderer)
 | 
			
		||||
            remoteSurfaceRenderers.addIfNeeded(remoteViewRenderer)
 | 
			
		||||
            when (mode) {
 | 
			
		||||
                VectorCallActivity.INCOMING_ACCEPT  -> {
 | 
			
		||||
                    internalAcceptIncomingCall()
 | 
			
		||||
@ -299,67 +364,31 @@ class WebRtcCall(
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Without consultation
 | 
			
		||||
     */
 | 
			
		||||
    suspend fun transferToUser(targetUserId: String, targetRoomId: String?) {
 | 
			
		||||
        mxCall.transfer(
 | 
			
		||||
                targetUserId = targetUserId,
 | 
			
		||||
                targetRoomId = targetRoomId,
 | 
			
		||||
                createCallId = CallIdGenerator.generate(),
 | 
			
		||||
                awaitCallId = null
 | 
			
		||||
        )
 | 
			
		||||
        endCall(sendEndSignaling = false)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * With consultation
 | 
			
		||||
     */
 | 
			
		||||
    suspend fun transferToCall(transferTargetCall: WebRtcCall) {
 | 
			
		||||
        val newCallId = CallIdGenerator.generate()
 | 
			
		||||
        transferTargetCall.mxCall.transfer(
 | 
			
		||||
                targetUserId = mxCall.opponentUserId,
 | 
			
		||||
                targetRoomId = null,
 | 
			
		||||
                createCallId = null,
 | 
			
		||||
                awaitCallId = newCallId
 | 
			
		||||
        )
 | 
			
		||||
        mxCall.transfer(
 | 
			
		||||
                targetUserId = transferTargetCall.mxCall.opponentUserId,
 | 
			
		||||
                targetRoomId = null,
 | 
			
		||||
                createCallId = newCallId,
 | 
			
		||||
                awaitCallId = null
 | 
			
		||||
        )
 | 
			
		||||
        endCall(sendEndSignaling = false)
 | 
			
		||||
        transferTargetCall.endCall(sendEndSignaling = false)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun acceptIncomingCall() {
 | 
			
		||||
        sessionScope?.launch {
 | 
			
		||||
            Timber.v("## VOIP acceptIncomingCall from state ${mxCall.state}")
 | 
			
		||||
            if (mxCall.state == CallState.LocalRinging) {
 | 
			
		||||
                internalAcceptIncomingCall()
 | 
			
		||||
    private suspend fun attachViewRenderersInternal() = withContext(dispatcher) {
 | 
			
		||||
        // render local video in pip view
 | 
			
		||||
        localSurfaceRenderers.forEach { renderer ->
 | 
			
		||||
            renderer.get()?.let { pipSurface ->
 | 
			
		||||
                pipSurface.setMirror(cameraInUse?.type == CameraType.FRONT)
 | 
			
		||||
                // no need to check if already added, addSink is checking that
 | 
			
		||||
                localVideoTrack?.addSink(pipSurface)
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Sends a DTMF digit to the other party
 | 
			
		||||
     * @param digit The digit (nb. string - '#' and '*' are dtmf too)
 | 
			
		||||
     */
 | 
			
		||||
    fun sendDtmfDigit(digit: String) {
 | 
			
		||||
        for (sender in peerConnection?.senders.orEmpty()) {
 | 
			
		||||
            if (sender.track()?.kind() == "audio" && sender.dtmf()?.canInsertDtmf() == true) {
 | 
			
		||||
                try {
 | 
			
		||||
                    sender.dtmf()?.insertDtmf(digit, 100, 70)
 | 
			
		||||
                    return
 | 
			
		||||
                } catch (failure: Throwable) {
 | 
			
		||||
                    Timber.v("Fail to send Dtmf digit")
 | 
			
		||||
                }
 | 
			
		||||
        // If remote track exists, then sink it to surface
 | 
			
		||||
        remoteSurfaceRenderers.forEach { renderer ->
 | 
			
		||||
            renderer.get()?.let { participantSurface ->
 | 
			
		||||
                remoteVideoTrack?.addSink(participantSurface)
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun detachRenderers(renderers: List<SurfaceViewRenderer>?) {
 | 
			
		||||
        sessionScope?.launch(dispatcher) {
 | 
			
		||||
            detachRenderersInternal(renderers)
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private suspend fun detachRenderersInternal(renderers: List<SurfaceViewRenderer>?) = withContext(dispatcher) {
 | 
			
		||||
        Timber.v("## VOIP detachRenderers")
 | 
			
		||||
        if (renderers.isNullOrEmpty()) {
 | 
			
		||||
            // remove all sinks
 | 
			
		||||
@ -452,24 +481,6 @@ class WebRtcCall(
 | 
			
		||||
        })
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private fun attachViewRenderersInternal() {
 | 
			
		||||
        // render local video in pip view
 | 
			
		||||
        localSurfaceRenderers.forEach { renderer ->
 | 
			
		||||
            renderer.get()?.let { pipSurface ->
 | 
			
		||||
                pipSurface.setMirror(this.cameraInUse?.type == CameraType.FRONT)
 | 
			
		||||
                // no need to check if already added, addSink is checking that
 | 
			
		||||
                localVideoTrack?.addSink(pipSurface)
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // If remote track exists, then sink it to surface
 | 
			
		||||
        remoteSurfaceRenderers.forEach { renderer ->
 | 
			
		||||
            renderer.get()?.let { participantSurface ->
 | 
			
		||||
                remoteVideoTrack?.addSink(participantSurface)
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private suspend fun getTurnServer(): TurnServerResponse? {
 | 
			
		||||
        return tryOrNull {
 | 
			
		||||
            sessionProvider.get()?.callSignalingService()?.getTurnServer()
 | 
			
		||||
@ -580,9 +591,11 @@ class WebRtcCall(
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun setCaptureFormat(format: CaptureFormat) {
 | 
			
		||||
        Timber.v("## VOIP setCaptureFormat $format")
 | 
			
		||||
        videoCapturer?.changeCaptureFormat(format.width, format.height, format.fps)
 | 
			
		||||
        currentCaptureFormat = format
 | 
			
		||||
        sessionScope?.launch(dispatcher) {
 | 
			
		||||
            Timber.v("## VOIP setCaptureFormat $format")
 | 
			
		||||
            videoCapturer?.changeCaptureFormat(format.width, format.height, format.fps)
 | 
			
		||||
            currentCaptureFormat = format
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private fun updateMuteStatus() {
 | 
			
		||||
@ -645,13 +658,17 @@ class WebRtcCall(
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun muteCall(muted: Boolean) {
 | 
			
		||||
        micMuted = muted
 | 
			
		||||
        updateMuteStatus()
 | 
			
		||||
        sessionScope?.launch(dispatcher) {
 | 
			
		||||
            micMuted = muted
 | 
			
		||||
            updateMuteStatus()
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun enableVideo(enabled: Boolean) {
 | 
			
		||||
        videoMuted = !enabled
 | 
			
		||||
        updateMuteStatus()
 | 
			
		||||
        sessionScope?.launch(dispatcher) {
 | 
			
		||||
            videoMuted = !enabled
 | 
			
		||||
            updateMuteStatus()
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun canSwitchCamera(): Boolean {
 | 
			
		||||
@ -668,28 +685,30 @@ class WebRtcCall(
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun switchCamera() {
 | 
			
		||||
        Timber.v("## VOIP switchCamera")
 | 
			
		||||
        if (mxCall.state is CallState.Connected && mxCall.isVideoCall) {
 | 
			
		||||
            val oppositeCamera = getOppositeCameraIfAny() ?: return
 | 
			
		||||
            videoCapturer?.switchCamera(
 | 
			
		||||
                    object : CameraVideoCapturer.CameraSwitchHandler {
 | 
			
		||||
                        // Invoked on success. |isFrontCamera| is true if the new camera is front facing.
 | 
			
		||||
                        override fun onCameraSwitchDone(isFrontCamera: Boolean) {
 | 
			
		||||
                            Timber.v("## VOIP onCameraSwitchDone isFront $isFrontCamera")
 | 
			
		||||
                            cameraInUse = oppositeCamera
 | 
			
		||||
                            localSurfaceRenderers.forEach {
 | 
			
		||||
                                it.get()?.setMirror(isFrontCamera)
 | 
			
		||||
        sessionScope?.launch(dispatcher) {
 | 
			
		||||
            Timber.v("## VOIP switchCamera")
 | 
			
		||||
            if (mxCall.state is CallState.Connected && mxCall.isVideoCall) {
 | 
			
		||||
                val oppositeCamera = getOppositeCameraIfAny() ?: return@launch
 | 
			
		||||
                videoCapturer?.switchCamera(
 | 
			
		||||
                        object : CameraVideoCapturer.CameraSwitchHandler {
 | 
			
		||||
                            // Invoked on success. |isFrontCamera| is true if the new camera is front facing.
 | 
			
		||||
                            override fun onCameraSwitchDone(isFrontCamera: Boolean) {
 | 
			
		||||
                                Timber.v("## VOIP onCameraSwitchDone isFront $isFrontCamera")
 | 
			
		||||
                                cameraInUse = oppositeCamera
 | 
			
		||||
                                localSurfaceRenderers.forEach {
 | 
			
		||||
                                    it.get()?.setMirror(isFrontCamera)
 | 
			
		||||
                                }
 | 
			
		||||
                                listeners.forEach {
 | 
			
		||||
                                    tryOrNull { it.onCameraChanged() }
 | 
			
		||||
                                }
 | 
			
		||||
                            }
 | 
			
		||||
                            listeners.forEach {
 | 
			
		||||
                                tryOrNull { it.onCameraChanged() }
 | 
			
		||||
                            }
 | 
			
		||||
                        }
 | 
			
		||||
 | 
			
		||||
                        override fun onCameraSwitchError(errorDescription: String?) {
 | 
			
		||||
                            Timber.v("## VOIP onCameraSwitchError isFront $errorDescription")
 | 
			
		||||
                        }
 | 
			
		||||
                    }, oppositeCamera.name
 | 
			
		||||
            )
 | 
			
		||||
                            override fun onCameraSwitchError(errorDescription: String?) {
 | 
			
		||||
                                Timber.v("## VOIP onCameraSwitchError isFront $errorDescription")
 | 
			
		||||
                            }
 | 
			
		||||
                        }, oppositeCamera.name
 | 
			
		||||
                )
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@ -718,11 +737,12 @@ class WebRtcCall(
 | 
			
		||||
        return currentCaptureFormat
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private fun release() {
 | 
			
		||||
    private suspend fun release() {
 | 
			
		||||
        listeners.clear()
 | 
			
		||||
        mxCall.removeListener(this)
 | 
			
		||||
        timer.stop()
 | 
			
		||||
        timer.tickListener = null
 | 
			
		||||
        detachRenderersInternal(null)
 | 
			
		||||
        videoCapturer?.stopCapture()
 | 
			
		||||
        videoCapturer?.dispose()
 | 
			
		||||
        videoCapturer = null
 | 
			
		||||
@ -736,6 +756,8 @@ class WebRtcCall(
 | 
			
		||||
        localAudioTrack = null
 | 
			
		||||
        localVideoSource = null
 | 
			
		||||
        localVideoTrack = null
 | 
			
		||||
        remoteAudioTrack = null
 | 
			
		||||
        remoteVideoTrack = null
 | 
			
		||||
        cameraAvailabilityCallback = null
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@ -745,7 +767,7 @@ class WebRtcCall(
 | 
			
		||||
            if (stream.audioTracks.size > 1 || stream.videoTracks.size > 1) {
 | 
			
		||||
                Timber.e("## VOIP StreamObserver weird looking stream: $stream")
 | 
			
		||||
                // TODO maybe do something more??
 | 
			
		||||
                mxCall.hangUp()
 | 
			
		||||
                endCall(true)
 | 
			
		||||
                return@launch
 | 
			
		||||
            }
 | 
			
		||||
            if (stream.audioTracks.size == 1) {
 | 
			
		||||
@ -774,27 +796,27 @@ class WebRtcCall(
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun endCall(sendEndSignaling: Boolean = true, reason: CallHangupContent.Reason? = null) {
 | 
			
		||||
        if (mxCall.state == CallState.Terminated) {
 | 
			
		||||
            return
 | 
			
		||||
        }
 | 
			
		||||
        // Close tracks ASAP
 | 
			
		||||
        localVideoTrack?.setEnabled(false)
 | 
			
		||||
        localVideoTrack?.setEnabled(false)
 | 
			
		||||
        cameraAvailabilityCallback?.let { cameraAvailabilityCallback ->
 | 
			
		||||
            val cameraManager = context.getSystemService<CameraManager>()!!
 | 
			
		||||
            cameraManager.unregisterAvailabilityCallback(cameraAvailabilityCallback)
 | 
			
		||||
        }
 | 
			
		||||
        val wasRinging = mxCall.state is CallState.LocalRinging
 | 
			
		||||
        mxCall.state = CallState.Terminated
 | 
			
		||||
        sessionScope?.launch(dispatcher) {
 | 
			
		||||
            if (mxCall.state == CallState.Terminated) {
 | 
			
		||||
                return@launch
 | 
			
		||||
            }
 | 
			
		||||
            // Close tracks ASAP
 | 
			
		||||
            localVideoTrack?.setEnabled(false)
 | 
			
		||||
            localVideoTrack?.setEnabled(false)
 | 
			
		||||
            cameraAvailabilityCallback?.let { cameraAvailabilityCallback ->
 | 
			
		||||
                val cameraManager = context.getSystemService<CameraManager>()!!
 | 
			
		||||
                cameraManager.unregisterAvailabilityCallback(cameraAvailabilityCallback)
 | 
			
		||||
            }
 | 
			
		||||
            val wasRinging = mxCall.state is CallState.LocalRinging
 | 
			
		||||
            mxCall.state = CallState.Terminated
 | 
			
		||||
            release()
 | 
			
		||||
            onCallEnded(callId)
 | 
			
		||||
        }
 | 
			
		||||
        if (sendEndSignaling) {
 | 
			
		||||
            if (wasRinging) {
 | 
			
		||||
                mxCall.reject()
 | 
			
		||||
            } else {
 | 
			
		||||
                mxCall.hangUp(reason)
 | 
			
		||||
            if (sendEndSignaling) {
 | 
			
		||||
                if (wasRinging) {
 | 
			
		||||
                    mxCall.reject()
 | 
			
		||||
                } else {
 | 
			
		||||
                    mxCall.hangUp(reason)
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@ -25,7 +25,6 @@ import android.view.inputmethod.EditorInfo
 | 
			
		||||
import androidx.core.text.set
 | 
			
		||||
import androidx.core.widget.doOnTextChanged
 | 
			
		||||
import im.vector.app.R
 | 
			
		||||
import im.vector.app.core.extensions.showPassword
 | 
			
		||||
import im.vector.app.core.platform.VectorBaseFragment
 | 
			
		||||
import im.vector.app.databinding.FragmentKeysBackupRestoreFromPassphraseBinding
 | 
			
		||||
 | 
			
		||||
@ -40,10 +39,6 @@ class KeysBackupRestoreFromPassphraseFragment @Inject constructor() : VectorBase
 | 
			
		||||
    private lateinit var viewModel: KeysBackupRestoreFromPassphraseViewModel
 | 
			
		||||
    private lateinit var sharedViewModel: KeysBackupRestoreSharedViewModel
 | 
			
		||||
 | 
			
		||||
    private fun toggleVisibilityMode() {
 | 
			
		||||
        viewModel.showPasswordMode.value = !(viewModel.showPasswordMode.value ?: false)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
 | 
			
		||||
        super.onViewCreated(view, savedInstanceState)
 | 
			
		||||
 | 
			
		||||
@ -56,12 +51,6 @@ class KeysBackupRestoreFromPassphraseFragment @Inject constructor() : VectorBase
 | 
			
		||||
 | 
			
		||||
        views.helperTextWithLink.text = spannableStringForHelperText()
 | 
			
		||||
 | 
			
		||||
        viewModel.showPasswordMode.observe(viewLifecycleOwner) {
 | 
			
		||||
            val shouldBeVisible = it ?: false
 | 
			
		||||
            views.keysBackupPassphraseEnterEdittext.showPassword(shouldBeVisible)
 | 
			
		||||
            views.keysBackupViewShowPassword.render(shouldBeVisible)
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        views.keysBackupPassphraseEnterEdittext.setOnEditorActionListener { _, actionId, _ ->
 | 
			
		||||
            if (actionId == EditorInfo.IME_ACTION_DONE) {
 | 
			
		||||
                onRestoreBackup()
 | 
			
		||||
@ -70,7 +59,6 @@ class KeysBackupRestoreFromPassphraseFragment @Inject constructor() : VectorBase
 | 
			
		||||
            return@setOnEditorActionListener false
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        views.keysBackupViewShowPassword.setOnClickListener { toggleVisibilityMode() }
 | 
			
		||||
        views.helperTextWithLink.setOnClickListener { onUseRecoveryKey() }
 | 
			
		||||
        views.keysBackupRestoreWithPassphraseSubmit.setOnClickListener { onRestoreBackup() }
 | 
			
		||||
        views.keysBackupPassphraseEnterEdittext.doOnTextChanged { text, _, _, _ -> onPassphraseTextEditChange(text) }
 | 
			
		||||
 | 
			
		||||
@ -30,12 +30,10 @@ class KeysBackupRestoreFromPassphraseViewModel @Inject constructor(
 | 
			
		||||
 | 
			
		||||
    var passphrase: MutableLiveData<String> = MutableLiveData()
 | 
			
		||||
    var passphraseErrorText: MutableLiveData<String> = MutableLiveData()
 | 
			
		||||
    var showPasswordMode: MutableLiveData<Boolean> = MutableLiveData()
 | 
			
		||||
 | 
			
		||||
    init {
 | 
			
		||||
        passphrase.value = null
 | 
			
		||||
        passphraseErrorText.value = null
 | 
			
		||||
        showPasswordMode.value = false
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // ========= Actions =========
 | 
			
		||||
 | 
			
		||||
@ -64,7 +64,6 @@ class KeysBackupSetupSharedViewModel @Inject constructor() : ViewModel() {
 | 
			
		||||
    var confirmPassphraseError: MutableLiveData<String> = MutableLiveData()
 | 
			
		||||
 | 
			
		||||
    var passwordStrength: MutableLiveData<Strength> = MutableLiveData()
 | 
			
		||||
    var showPasswordMode: MutableLiveData<Boolean> = MutableLiveData()
 | 
			
		||||
 | 
			
		||||
    // Step 3
 | 
			
		||||
    // Var to ignore events from previous request(s) to generate a recovery key
 | 
			
		||||
@ -80,7 +79,6 @@ class KeysBackupSetupSharedViewModel @Inject constructor() : ViewModel() {
 | 
			
		||||
    var loadingStatus: MutableLiveData<WaitingViewData> = MutableLiveData()
 | 
			
		||||
 | 
			
		||||
    init {
 | 
			
		||||
        showPasswordMode.value = false
 | 
			
		||||
        recoveryKey.value = null
 | 
			
		||||
        isCreatingBackupVersion.value = false
 | 
			
		||||
        prepareRecoverFailError.value = null
 | 
			
		||||
@ -97,9 +95,6 @@ class KeysBackupSetupSharedViewModel @Inject constructor() : ViewModel() {
 | 
			
		||||
        currentRequestId.value = System.currentTimeMillis()
 | 
			
		||||
        isCreatingBackupVersion.value = true
 | 
			
		||||
 | 
			
		||||
        // Ensure passphrase is hidden during the process
 | 
			
		||||
        showPasswordMode.value = false
 | 
			
		||||
 | 
			
		||||
        recoveryKey.value = null
 | 
			
		||||
        prepareRecoverFailError.value = null
 | 
			
		||||
        session.let { mxSession ->
 | 
			
		||||
 | 
			
		||||
@ -25,7 +25,7 @@ import androidx.lifecycle.viewModelScope
 | 
			
		||||
import androidx.transition.TransitionManager
 | 
			
		||||
import com.nulabinc.zxcvbn.Zxcvbn
 | 
			
		||||
import im.vector.app.R
 | 
			
		||||
import im.vector.app.core.extensions.showPassword
 | 
			
		||||
import im.vector.app.core.extensions.hidePassword
 | 
			
		||||
import im.vector.app.core.platform.VectorBaseFragment
 | 
			
		||||
import im.vector.app.databinding.FragmentKeysBackupSetupStep2Binding
 | 
			
		||||
import im.vector.app.features.settings.VectorLocale
 | 
			
		||||
@ -113,13 +113,6 @@ class KeysBackupSetupStep2Fragment @Inject constructor() : VectorBaseFragment<Fr
 | 
			
		||||
 | 
			
		||||
        views.keysBackupSetupStep2PassphraseConfirmEditText.setText(viewModel.confirmPassphrase.value)
 | 
			
		||||
 | 
			
		||||
        viewModel.showPasswordMode.observe(viewLifecycleOwner) {
 | 
			
		||||
            val shouldBeVisible = it ?: false
 | 
			
		||||
            views.keysBackupSetupStep2PassphraseEnterEdittext.showPassword(shouldBeVisible)
 | 
			
		||||
            views.keysBackupSetupStep2PassphraseConfirmEditText.showPassword(shouldBeVisible)
 | 
			
		||||
            views.keysBackupSetupStep2ShowPassword.render(shouldBeVisible)
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        viewModel.confirmPassphraseError.observe(viewLifecycleOwner) {
 | 
			
		||||
            TransitionManager.beginDelayedTransition(views.keysBackupRoot)
 | 
			
		||||
            views.keysBackupSetupStep2PassphraseConfirmTil.error = it
 | 
			
		||||
@ -135,7 +128,6 @@ class KeysBackupSetupStep2Fragment @Inject constructor() : VectorBaseFragment<Fr
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private fun setupViews() {
 | 
			
		||||
        views.keysBackupSetupStep2ShowPassword.setOnClickListener { toggleVisibilityMode() }
 | 
			
		||||
        views.keysBackupSetupStep2Button.setOnClickListener { doNext() }
 | 
			
		||||
        views.keysBackupSetupStep2SkipButton.setOnClickListener { skipPassphrase() }
 | 
			
		||||
 | 
			
		||||
@ -143,10 +135,6 @@ class KeysBackupSetupStep2Fragment @Inject constructor() : VectorBaseFragment<Fr
 | 
			
		||||
        views.keysBackupSetupStep2PassphraseConfirmEditText.doOnTextChanged { _, _, _, _ -> onConfirmPassphraseChanged() }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private fun toggleVisibilityMode() {
 | 
			
		||||
        viewModel.showPasswordMode.value = !(viewModel.showPasswordMode.value ?: false)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private fun doNext() {
 | 
			
		||||
        when {
 | 
			
		||||
            viewModel.passphrase.value.isNullOrEmpty()                      -> {
 | 
			
		||||
@ -161,6 +149,9 @@ class KeysBackupSetupStep2Fragment @Inject constructor() : VectorBaseFragment<Fr
 | 
			
		||||
            else                                                            -> {
 | 
			
		||||
                viewModel.megolmBackupCreationInfo = null
 | 
			
		||||
 | 
			
		||||
                // Ensure passphrase is hidden during the process
 | 
			
		||||
                views.keysBackupSetupStep2PassphraseEnterEdittext.hidePassword()
 | 
			
		||||
                views.keysBackupSetupStep2PassphraseConfirmEditText.hidePassword()
 | 
			
		||||
                viewModel.prepareRecoveryKey(requireActivity(), viewModel.passphrase.value)
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
@ -172,6 +163,9 @@ class KeysBackupSetupStep2Fragment @Inject constructor() : VectorBaseFragment<Fr
 | 
			
		||||
                // Generate a recovery key for the user
 | 
			
		||||
                viewModel.megolmBackupCreationInfo = null
 | 
			
		||||
 | 
			
		||||
                // Ensure passphrase is hidden during the process
 | 
			
		||||
                views.keysBackupSetupStep2PassphraseEnterEdittext.hidePassword()
 | 
			
		||||
                views.keysBackupSetupStep2PassphraseConfirmEditText.hidePassword()
 | 
			
		||||
                viewModel.prepareRecoveryKey(requireActivity(), null)
 | 
			
		||||
            }
 | 
			
		||||
            else                                       -> {
 | 
			
		||||
 | 
			
		||||
@ -21,8 +21,6 @@ import im.vector.app.core.platform.VectorViewModelAction
 | 
			
		||||
import im.vector.app.core.platform.WaitingViewData
 | 
			
		||||
 | 
			
		||||
sealed class SharedSecureStorageAction : VectorViewModelAction {
 | 
			
		||||
 | 
			
		||||
    object TogglePasswordVisibility : SharedSecureStorageAction()
 | 
			
		||||
    object UseKey : SharedSecureStorageAction()
 | 
			
		||||
    object Back : SharedSecureStorageAction()
 | 
			
		||||
    object Cancel : SharedSecureStorageAction()
 | 
			
		||||
 | 
			
		||||
@ -50,7 +50,6 @@ import java.io.ByteArrayOutputStream
 | 
			
		||||
data class SharedSecureStorageViewState(
 | 
			
		||||
        val ready: Boolean = false,
 | 
			
		||||
        val hasPassphrase: Boolean = true,
 | 
			
		||||
        val passphraseVisible: Boolean = false,
 | 
			
		||||
        val checkingSSSSAction: Async<Unit> = Uninitialized,
 | 
			
		||||
        val step: Step = Step.EnterPassphrase,
 | 
			
		||||
        val activeDeviceCount: Int = 0,
 | 
			
		||||
@ -128,7 +127,6 @@ class SharedSecureStorageViewModel @AssistedInject constructor(
 | 
			
		||||
 | 
			
		||||
    override fun handle(action: SharedSecureStorageAction) = withState {
 | 
			
		||||
        when (action) {
 | 
			
		||||
            is SharedSecureStorageAction.TogglePasswordVisibility -> handleTogglePasswordVisibility()
 | 
			
		||||
            is SharedSecureStorageAction.Cancel                   -> handleCancel()
 | 
			
		||||
            is SharedSecureStorageAction.SubmitPassphrase         -> handleSubmitPassphrase(action)
 | 
			
		||||
            SharedSecureStorageAction.UseKey                      -> handleUseKey()
 | 
			
		||||
@ -319,14 +317,6 @@ class SharedSecureStorageViewModel @AssistedInject constructor(
 | 
			
		||||
        _viewEvents.post(SharedSecureStorageViewEvent.Dismiss)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private fun handleTogglePasswordVisibility() {
 | 
			
		||||
        setState {
 | 
			
		||||
            copy(
 | 
			
		||||
                    passphraseVisible = !passphraseVisible
 | 
			
		||||
            )
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    companion object : MvRxViewModelFactory<SharedSecureStorageViewModel, SharedSecureStorageViewState> {
 | 
			
		||||
 | 
			
		||||
        @JvmStatic
 | 
			
		||||
 | 
			
		||||
@ -23,11 +23,9 @@ import android.view.ViewGroup
 | 
			
		||||
import android.view.inputmethod.EditorInfo
 | 
			
		||||
import androidx.core.text.toSpannable
 | 
			
		||||
import com.airbnb.mvrx.activityViewModel
 | 
			
		||||
import com.airbnb.mvrx.withState
 | 
			
		||||
import com.jakewharton.rxbinding3.widget.editorActionEvents
 | 
			
		||||
import com.jakewharton.rxbinding3.widget.textChanges
 | 
			
		||||
import im.vector.app.R
 | 
			
		||||
import im.vector.app.core.extensions.showPassword
 | 
			
		||||
import im.vector.app.core.platform.VectorBaseFragment
 | 
			
		||||
import im.vector.app.core.resources.ColorProvider
 | 
			
		||||
import im.vector.app.databinding.FragmentSsssAccessFromPassphraseBinding
 | 
			
		||||
@ -92,7 +90,6 @@ class SharedSecuredStoragePassphraseFragment @Inject constructor(
 | 
			
		||||
 | 
			
		||||
        views.ssssPassphraseSubmit.debouncedClicks { submit() }
 | 
			
		||||
        views.ssssPassphraseUseKey.debouncedClicks { sharedViewModel.handle(SharedSecureStorageAction.UseKey) }
 | 
			
		||||
        views.ssssViewShowPassword.debouncedClicks { sharedViewModel.handle(SharedSecureStorageAction.TogglePasswordVisibility) }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun submit() {
 | 
			
		||||
@ -101,10 +98,4 @@ class SharedSecuredStoragePassphraseFragment @Inject constructor(
 | 
			
		||||
        views.ssssPassphraseSubmit.isEnabled = false
 | 
			
		||||
        sharedViewModel.handle(SharedSecureStorageAction.SubmitPassphrase(text))
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun invalidate() = withState(sharedViewModel) { state ->
 | 
			
		||||
        val shouldBeVisible = state.passphraseVisible
 | 
			
		||||
        views.ssssPassphraseEnterEdittext.showPassword(shouldBeVisible)
 | 
			
		||||
        views.ssssViewShowPassword.render(shouldBeVisible)
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -34,7 +34,6 @@ sealed class BootstrapActions : VectorViewModelAction {
 | 
			
		||||
 | 
			
		||||
    data class DoInitialize(val passphrase: String) : BootstrapActions()
 | 
			
		||||
    object DoInitializeGeneratedKey : BootstrapActions()
 | 
			
		||||
    object TogglePasswordVisibility : BootstrapActions()
 | 
			
		||||
    data class UpdateCandidatePassphrase(val pass: String) : BootstrapActions()
 | 
			
		||||
    data class UpdateConfirmCandidatePassphrase(val pass: String) : BootstrapActions()
 | 
			
		||||
//    data class ReAuth(val pass: String) : BootstrapActions()
 | 
			
		||||
 | 
			
		||||
@ -28,7 +28,6 @@ import com.jakewharton.rxbinding3.widget.editorActionEvents
 | 
			
		||||
import com.jakewharton.rxbinding3.widget.textChanges
 | 
			
		||||
import im.vector.app.R
 | 
			
		||||
import im.vector.app.core.extensions.hideKeyboard
 | 
			
		||||
import im.vector.app.core.extensions.showPassword
 | 
			
		||||
import im.vector.app.core.platform.VectorBaseFragment
 | 
			
		||||
import im.vector.app.databinding.FragmentBootstrapEnterPassphraseBinding
 | 
			
		||||
import io.reactivex.android.schedulers.AndroidSchedulers
 | 
			
		||||
@ -84,7 +83,6 @@ class BootstrapConfirmPassphraseFragment @Inject constructor()
 | 
			
		||||
//            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        views.ssssViewShowPassword.debouncedClicks { sharedViewModel.handle(BootstrapActions.TogglePasswordVisibility) }
 | 
			
		||||
        views.bootstrapSubmit.debouncedClicks { submit() }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@ -104,12 +102,4 @@ class BootstrapConfirmPassphraseFragment @Inject constructor()
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun invalidate() = withState(sharedViewModel) { state ->
 | 
			
		||||
        if (state.step is BootstrapStep.ConfirmPassphrase) {
 | 
			
		||||
            val isPasswordVisible = state.step.isPasswordVisible
 | 
			
		||||
            views.ssssPassphraseEnterEdittext.showPassword(isPasswordVisible, updateCursor = false)
 | 
			
		||||
            views.ssssViewShowPassword.render(isPasswordVisible)
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -26,7 +26,6 @@ import com.airbnb.mvrx.withState
 | 
			
		||||
import com.jakewharton.rxbinding3.widget.editorActionEvents
 | 
			
		||||
import com.jakewharton.rxbinding3.widget.textChanges
 | 
			
		||||
import im.vector.app.R
 | 
			
		||||
import im.vector.app.core.extensions.showPassword
 | 
			
		||||
import im.vector.app.core.platform.VectorBaseFragment
 | 
			
		||||
import im.vector.app.databinding.FragmentBootstrapEnterPassphraseBinding
 | 
			
		||||
import im.vector.app.features.settings.VectorLocale
 | 
			
		||||
@ -80,7 +79,6 @@ class BootstrapEnterPassphraseFragment @Inject constructor()
 | 
			
		||||
//            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        views.ssssViewShowPassword.debouncedClicks { sharedViewModel.handle(BootstrapActions.TogglePasswordVisibility) }
 | 
			
		||||
        views.bootstrapSubmit.debouncedClicks { submit() }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@ -101,10 +99,6 @@ class BootstrapEnterPassphraseFragment @Inject constructor()
 | 
			
		||||
 | 
			
		||||
    override fun invalidate() = withState(sharedViewModel) { state ->
 | 
			
		||||
        if (state.step is BootstrapStep.SetupPassphrase) {
 | 
			
		||||
            val isPasswordVisible = state.step.isPasswordVisible
 | 
			
		||||
            views.ssssPassphraseEnterEdittext.showPassword(isPasswordVisible, updateCursor = false)
 | 
			
		||||
            views.ssssViewShowPassword.render(isPasswordVisible)
 | 
			
		||||
 | 
			
		||||
            state.passphraseStrength.invoke()?.let { strength ->
 | 
			
		||||
                val score = strength.score
 | 
			
		||||
                views.ssssPassphraseSecurityProgress.strength = score
 | 
			
		||||
 | 
			
		||||
@ -34,7 +34,6 @@ import com.jakewharton.rxbinding3.widget.textChanges
 | 
			
		||||
import im.vector.app.R
 | 
			
		||||
import im.vector.app.core.extensions.hideKeyboard
 | 
			
		||||
import im.vector.app.core.extensions.registerStartForActivityResult
 | 
			
		||||
import im.vector.app.core.extensions.showPassword
 | 
			
		||||
import im.vector.app.core.platform.VectorBaseFragment
 | 
			
		||||
import im.vector.app.core.resources.ColorProvider
 | 
			
		||||
import im.vector.app.core.utils.colorizeMatchingText
 | 
			
		||||
@ -84,7 +83,6 @@ class BootstrapMigrateBackupFragment @Inject constructor(
 | 
			
		||||
 | 
			
		||||
        // sharedViewModel.observeViewEvents {}
 | 
			
		||||
        views.bootstrapMigrateContinueButton.debouncedClicks { submit() }
 | 
			
		||||
        views.bootstrapMigrateShowPassword.debouncedClicks { sharedViewModel.handle(BootstrapActions.TogglePasswordVisibility) }
 | 
			
		||||
        views.bootstrapMigrateForgotPassphrase.debouncedClicks { sharedViewModel.handle(BootstrapActions.HandleForgotBackupPassphrase) }
 | 
			
		||||
        views.bootstrapMigrateUseFile.debouncedClicks { startImportTextFromFileIntent(requireContext(), importFileStartForActivityResult) }
 | 
			
		||||
    }
 | 
			
		||||
@ -116,7 +114,6 @@ class BootstrapMigrateBackupFragment @Inject constructor(
 | 
			
		||||
        val isEnteringKey = getBackupSecretForMigration.useKey()
 | 
			
		||||
 | 
			
		||||
        if (isEnteringKey) {
 | 
			
		||||
            views.bootstrapMigrateShowPassword.isVisible = false
 | 
			
		||||
            views.bootstrapMigrateEditText.inputType = TYPE_CLASS_TEXT or TYPE_TEXT_VARIATION_VISIBLE_PASSWORD or TYPE_TEXT_FLAG_MULTI_LINE
 | 
			
		||||
 | 
			
		||||
            val recKey = getString(R.string.bootstrap_migration_backup_recovery_key)
 | 
			
		||||
@ -128,14 +125,6 @@ class BootstrapMigrateBackupFragment @Inject constructor(
 | 
			
		||||
            views.bootstrapMigrateForgotPassphrase.isVisible = false
 | 
			
		||||
            views.bootstrapMigrateUseFile.isVisible = true
 | 
			
		||||
        } else {
 | 
			
		||||
            views.bootstrapMigrateShowPassword.isVisible = true
 | 
			
		||||
 | 
			
		||||
            if (state.step is BootstrapStep.GetBackupSecretPassForMigration) {
 | 
			
		||||
                val isPasswordVisible = state.step.isPasswordVisible
 | 
			
		||||
                views.bootstrapMigrateEditText.showPassword(isPasswordVisible, updateCursor = false)
 | 
			
		||||
                views.bootstrapMigrateShowPassword.render(isPasswordVisible)
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            views.bootstrapDescriptionText.text = getString(R.string.bootstrap_migration_enter_backup_password)
 | 
			
		||||
 | 
			
		||||
            views.bootstrapMigrateEditText.hint = getString(R.string.passphrase_enter_passphrase)
 | 
			
		||||
 | 
			
		||||
@ -139,7 +139,7 @@ class BootstrapSharedViewModel @AssistedInject constructor(
 | 
			
		||||
    private fun handleStartMigratingKeyBackup() {
 | 
			
		||||
        if (isBackupCreatedFromPassphrase) {
 | 
			
		||||
            setState {
 | 
			
		||||
                copy(step = BootstrapStep.GetBackupSecretPassForMigration(isPasswordVisible = false, useKey = false))
 | 
			
		||||
                copy(step = BootstrapStep.GetBackupSecretPassForMigration(useKey = false))
 | 
			
		||||
            }
 | 
			
		||||
        } else {
 | 
			
		||||
            setState {
 | 
			
		||||
@ -151,29 +151,6 @@ class BootstrapSharedViewModel @AssistedInject constructor(
 | 
			
		||||
    override fun handle(action: BootstrapActions) = withState { state ->
 | 
			
		||||
        when (action) {
 | 
			
		||||
            is BootstrapActions.GoBack                           -> queryBack()
 | 
			
		||||
            BootstrapActions.TogglePasswordVisibility            -> {
 | 
			
		||||
                when (state.step) {
 | 
			
		||||
                    is BootstrapStep.SetupPassphrase                 -> {
 | 
			
		||||
                        setState {
 | 
			
		||||
                            copy(step = state.step.copy(isPasswordVisible = !state.step.isPasswordVisible))
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
                    is BootstrapStep.ConfirmPassphrase               -> {
 | 
			
		||||
                        setState {
 | 
			
		||||
                            copy(step = state.step.copy(isPasswordVisible = !state.step.isPasswordVisible))
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
                    is BootstrapStep.AccountReAuth                   -> {
 | 
			
		||||
                        // nop
 | 
			
		||||
                    }
 | 
			
		||||
                    is BootstrapStep.GetBackupSecretPassForMigration -> {
 | 
			
		||||
                        setState {
 | 
			
		||||
                            copy(step = state.step.copy(isPasswordVisible = !state.step.isPasswordVisible))
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
                    else                                             -> Unit
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            BootstrapActions.StartKeyBackupMigration             -> {
 | 
			
		||||
                handleStartMigratingKeyBackup()
 | 
			
		||||
            }
 | 
			
		||||
@ -193,9 +170,7 @@ class BootstrapSharedViewModel @AssistedInject constructor(
 | 
			
		||||
                setState {
 | 
			
		||||
                    copy(
 | 
			
		||||
                            passphrase = action.passphrase,
 | 
			
		||||
                            step = BootstrapStep.ConfirmPassphrase(
 | 
			
		||||
                                    isPasswordVisible = (state.step as? BootstrapStep.SetupPassphrase)?.isPasswordVisible ?: false
 | 
			
		||||
                            )
 | 
			
		||||
                            step = BootstrapStep.ConfirmPassphrase
 | 
			
		||||
                    )
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
@ -255,7 +230,7 @@ class BootstrapSharedViewModel @AssistedInject constructor(
 | 
			
		||||
            BootstrapActions.HandleForgotBackupPassphrase        -> {
 | 
			
		||||
                if (state.step is BootstrapStep.GetBackupSecretPassForMigration) {
 | 
			
		||||
                    setState {
 | 
			
		||||
                        copy(step = BootstrapStep.GetBackupSecretPassForMigration(state.step.isPasswordVisible, true))
 | 
			
		||||
                        copy(step = BootstrapStep.GetBackupSecretPassForMigration(true))
 | 
			
		||||
                    }
 | 
			
		||||
                } else return@withState
 | 
			
		||||
            }
 | 
			
		||||
@ -293,7 +268,7 @@ class BootstrapSharedViewModel @AssistedInject constructor(
 | 
			
		||||
        if (action.userWantsToEnterPassphrase) {
 | 
			
		||||
            setState {
 | 
			
		||||
                copy(
 | 
			
		||||
                        step = BootstrapStep.SetupPassphrase(isPasswordVisible = false)
 | 
			
		||||
                        step = BootstrapStep.SetupPassphrase
 | 
			
		||||
                )
 | 
			
		||||
            }
 | 
			
		||||
        } else {
 | 
			
		||||
@ -493,7 +468,6 @@ class BootstrapSharedViewModel @AssistedInject constructor(
 | 
			
		||||
                    setState {
 | 
			
		||||
                        copy(
 | 
			
		||||
                                step = BootstrapStep.GetBackupSecretPassForMigration(
 | 
			
		||||
                                        isPasswordVisible = state.step.isPasswordVisible,
 | 
			
		||||
                                        useKey = false
 | 
			
		||||
                                )
 | 
			
		||||
                        )
 | 
			
		||||
@ -524,9 +498,7 @@ class BootstrapSharedViewModel @AssistedInject constructor(
 | 
			
		||||
            is BootstrapStep.ConfirmPassphrase               -> {
 | 
			
		||||
                setState {
 | 
			
		||||
                    copy(
 | 
			
		||||
                            step = BootstrapStep.SetupPassphrase(
 | 
			
		||||
                                    isPasswordVisible = state.step.isPasswordVisible
 | 
			
		||||
                            )
 | 
			
		||||
                            step = BootstrapStep.SetupPassphrase
 | 
			
		||||
                    )
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
@ -91,13 +91,13 @@ sealed class BootstrapStep {
 | 
			
		||||
    // Use will be asked to choose between passphrase or recovery key, or to start process if a key backup exists
 | 
			
		||||
    data class FirstForm(val keyBackUpExist: Boolean, val reset: Boolean = false) : BootstrapStep()
 | 
			
		||||
 | 
			
		||||
    data class SetupPassphrase(val isPasswordVisible: Boolean) : BootstrapStep()
 | 
			
		||||
    data class ConfirmPassphrase(val isPasswordVisible: Boolean) : BootstrapStep()
 | 
			
		||||
    object SetupPassphrase : BootstrapStep()
 | 
			
		||||
    object ConfirmPassphrase : BootstrapStep()
 | 
			
		||||
 | 
			
		||||
    data class AccountReAuth(val failure: String? = null) : BootstrapStep()
 | 
			
		||||
 | 
			
		||||
    abstract class GetBackupSecretForMigration : BootstrapStep()
 | 
			
		||||
    data class GetBackupSecretPassForMigration(val isPasswordVisible: Boolean, val useKey: Boolean) : GetBackupSecretForMigration()
 | 
			
		||||
    data class GetBackupSecretPassForMigration(val useKey: Boolean) : GetBackupSecretForMigration()
 | 
			
		||||
    object GetBackupSecretKeyForMigration : GetBackupSecretForMigration()
 | 
			
		||||
 | 
			
		||||
    object Initializing : BootstrapStep()
 | 
			
		||||
 | 
			
		||||
@ -31,6 +31,8 @@ import im.vector.app.features.call.dialpad.DialPadLookup
 | 
			
		||||
import im.vector.app.features.call.lookup.CallProtocolsChecker
 | 
			
		||||
import im.vector.app.features.call.webrtc.WebRtcCallManager
 | 
			
		||||
import im.vector.app.features.createdirect.DirectRoomHelper
 | 
			
		||||
import im.vector.app.features.invite.AutoAcceptInvites
 | 
			
		||||
import im.vector.app.features.invite.showInvites
 | 
			
		||||
import im.vector.app.features.ui.UiStateRepository
 | 
			
		||||
import io.reactivex.schedulers.Schedulers
 | 
			
		||||
import kotlinx.coroutines.Dispatchers
 | 
			
		||||
@ -56,7 +58,8 @@ class HomeDetailViewModel @AssistedInject constructor(@Assisted initialState: Ho
 | 
			
		||||
                                                      private val uiStateRepository: UiStateRepository,
 | 
			
		||||
                                                      private val callManager: WebRtcCallManager,
 | 
			
		||||
                                                      private val directRoomHelper: DirectRoomHelper,
 | 
			
		||||
                                                      private val appStateHandler: AppStateHandler)
 | 
			
		||||
                                                      private val appStateHandler: AppStateHandler,
 | 
			
		||||
private val autoAcceptInvites: AutoAcceptInvites)
 | 
			
		||||
    : VectorViewModel<HomeDetailViewState, HomeDetailAction, HomeDetailViewEvents>(initialState),
 | 
			
		||||
        CallProtocolsChecker.Listener {
 | 
			
		||||
 | 
			
		||||
@ -204,21 +207,25 @@ class HomeDetailViewModel @AssistedInject constructor(@Assisted initialState: Ho
 | 
			
		||||
                        }
 | 
			
		||||
                        is RoomGroupingMethod.BySpace       -> {
 | 
			
		||||
                            val activeSpaceRoomId = groupingMethod.spaceSummary?.roomId
 | 
			
		||||
                            val dmInvites = session.getRoomSummaries(
 | 
			
		||||
                                    roomSummaryQueryParams {
 | 
			
		||||
                                        memberships = listOf(Membership.INVITE)
 | 
			
		||||
                                        roomCategoryFilter = RoomCategoryFilter.ONLY_DM
 | 
			
		||||
                                        activeSpaceFilter = activeSpaceRoomId?.let { ActiveSpaceFilter.ActiveSpace(it) } ?: ActiveSpaceFilter.None
 | 
			
		||||
                                    }
 | 
			
		||||
                            ).size
 | 
			
		||||
                            var dmInvites = 0
 | 
			
		||||
                            var roomsInvite = 0
 | 
			
		||||
                            if (autoAcceptInvites.showInvites()) {
 | 
			
		||||
                                dmInvites = session.getRoomSummaries(
 | 
			
		||||
                                        roomSummaryQueryParams {
 | 
			
		||||
                                            memberships = listOf(Membership.INVITE)
 | 
			
		||||
                                            roomCategoryFilter = RoomCategoryFilter.ONLY_DM
 | 
			
		||||
                                            activeSpaceFilter = activeSpaceRoomId?.let { ActiveSpaceFilter.ActiveSpace(it) } ?: ActiveSpaceFilter.None
 | 
			
		||||
                                        }
 | 
			
		||||
                                ).size
 | 
			
		||||
 | 
			
		||||
                            val roomsInvite = session.getRoomSummaries(
 | 
			
		||||
                                    roomSummaryQueryParams {
 | 
			
		||||
                                        memberships = listOf(Membership.INVITE)
 | 
			
		||||
                                        roomCategoryFilter = RoomCategoryFilter.ONLY_ROOMS
 | 
			
		||||
                                        activeSpaceFilter = ActiveSpaceFilter.ActiveSpace(groupingMethod.spaceSummary?.roomId)
 | 
			
		||||
                                    }
 | 
			
		||||
                            ).size
 | 
			
		||||
                                roomsInvite = session.getRoomSummaries(
 | 
			
		||||
                                        roomSummaryQueryParams {
 | 
			
		||||
                                            memberships = listOf(Membership.INVITE)
 | 
			
		||||
                                            roomCategoryFilter = RoomCategoryFilter.ONLY_ROOMS
 | 
			
		||||
                                            activeSpaceFilter = ActiveSpaceFilter.ActiveSpace(groupingMethod.spaceSummary?.roomId)
 | 
			
		||||
                                        }
 | 
			
		||||
                                ).size
 | 
			
		||||
                            }
 | 
			
		||||
 | 
			
		||||
                            val dmRooms = session.getNotificationCountForRooms(
 | 
			
		||||
                                    roomSummaryQueryParams {
 | 
			
		||||
 | 
			
		||||
@ -29,6 +29,7 @@ import im.vector.app.RoomGroupingMethod
 | 
			
		||||
import im.vector.app.core.platform.EmptyAction
 | 
			
		||||
import im.vector.app.core.platform.EmptyViewEvents
 | 
			
		||||
import im.vector.app.core.platform.VectorViewModel
 | 
			
		||||
import im.vector.app.features.invite.AutoAcceptInvites
 | 
			
		||||
import im.vector.app.features.settings.VectorPreferences
 | 
			
		||||
import io.reactivex.Observable
 | 
			
		||||
import io.reactivex.schedulers.Schedulers
 | 
			
		||||
@ -54,7 +55,8 @@ data class CountInfo(
 | 
			
		||||
class UnreadMessagesSharedViewModel @AssistedInject constructor(@Assisted initialState: UnreadMessagesState,
 | 
			
		||||
                                                                session: Session,
 | 
			
		||||
                                                                private val vectorPreferences: VectorPreferences,
 | 
			
		||||
                                                                appStateHandler: AppStateHandler)
 | 
			
		||||
                                                                appStateHandler: AppStateHandler,
 | 
			
		||||
                                                                private val autoAcceptInvites: AutoAcceptInvites)
 | 
			
		||||
    : VectorViewModel<UnreadMessagesState, EmptyAction, EmptyViewEvents>(initialState) {
 | 
			
		||||
 | 
			
		||||
    @AssistedFactory
 | 
			
		||||
@ -92,12 +94,17 @@ class UnreadMessagesSharedViewModel @AssistedInject constructor(@Assisted initia
 | 
			
		||||
                                this.activeSpaceFilter = ActiveSpaceFilter.ActiveSpace(null)
 | 
			
		||||
                            }
 | 
			
		||||
                    )
 | 
			
		||||
                    val invites = session.getRoomSummaries(
 | 
			
		||||
                            roomSummaryQueryParams {
 | 
			
		||||
                                this.memberships = listOf(Membership.INVITE)
 | 
			
		||||
                                this.activeSpaceFilter = ActiveSpaceFilter.ActiveSpace(null)
 | 
			
		||||
                            }
 | 
			
		||||
                    ).size
 | 
			
		||||
                    val invites = if (autoAcceptInvites.hideInvites) {
 | 
			
		||||
                        0
 | 
			
		||||
                    } else {
 | 
			
		||||
                        session.getRoomSummaries(
 | 
			
		||||
                                roomSummaryQueryParams {
 | 
			
		||||
                                    this.memberships = listOf(Membership.INVITE)
 | 
			
		||||
                                    this.activeSpaceFilter = ActiveSpaceFilter.ActiveSpace(null)
 | 
			
		||||
                                }
 | 
			
		||||
                        ).size
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                    copy(
 | 
			
		||||
                            homeSpaceUnread = RoomAggregateNotificationCount(
 | 
			
		||||
                                    counts.notificationCount + invites,
 | 
			
		||||
@ -129,10 +136,13 @@ class UnreadMessagesSharedViewModel @AssistedInject constructor(@Assisted initia
 | 
			
		||||
                        is RoomGroupingMethod.BySpace       -> {
 | 
			
		||||
                            val selectedSpace = appStateHandler.safeActiveSpaceId()
 | 
			
		||||
 | 
			
		||||
                            val inviteCount = session.getRoomSummaries(
 | 
			
		||||
                                    roomSummaryQueryParams { this.memberships = listOf(Membership.INVITE) }
 | 
			
		||||
                            ).size
 | 
			
		||||
 | 
			
		||||
                            val inviteCount = if (autoAcceptInvites.hideInvites) {
 | 
			
		||||
                                0
 | 
			
		||||
                            } else {
 | 
			
		||||
                                session.getRoomSummaries(
 | 
			
		||||
                                        roomSummaryQueryParams { this.memberships = listOf(Membership.INVITE) }
 | 
			
		||||
                                ).size
 | 
			
		||||
                            }
 | 
			
		||||
                            val totalCount = session.getNotificationCountForRooms(
 | 
			
		||||
                                    roomSummaryQueryParams {
 | 
			
		||||
                                        this.memberships = listOf(Membership.JOIN)
 | 
			
		||||
 | 
			
		||||
@ -18,7 +18,6 @@ package im.vector.app.features.home.room.detail
 | 
			
		||||
 | 
			
		||||
import android.annotation.SuppressLint
 | 
			
		||||
import android.app.Activity
 | 
			
		||||
import android.content.DialogInterface
 | 
			
		||||
import android.content.Intent
 | 
			
		||||
import android.content.res.Configuration
 | 
			
		||||
import android.graphics.Color
 | 
			
		||||
@ -75,7 +74,6 @@ import com.vanniktech.emoji.EmojiPopup
 | 
			
		||||
import im.vector.app.R
 | 
			
		||||
import im.vector.app.core.dialogs.ConfirmationDialogBuilder
 | 
			
		||||
import im.vector.app.core.dialogs.GalleryOrCameraDialogHelper
 | 
			
		||||
import im.vector.app.core.dialogs.withColoredButton
 | 
			
		||||
import im.vector.app.core.epoxy.LayoutManagerStateRestorer
 | 
			
		||||
import im.vector.app.core.extensions.cleanup
 | 
			
		||||
import im.vector.app.core.extensions.exhaustive
 | 
			
		||||
@ -94,7 +92,6 @@ import im.vector.app.core.resources.ColorProvider
 | 
			
		||||
import im.vector.app.core.ui.views.ActiveConferenceView
 | 
			
		||||
import im.vector.app.core.ui.views.CurrentCallsView
 | 
			
		||||
import im.vector.app.core.ui.views.FailedMessagesWarningView
 | 
			
		||||
import im.vector.app.core.ui.views.JumpToReadMarkerView
 | 
			
		||||
import im.vector.app.core.ui.views.KnownCallsViewHolder
 | 
			
		||||
import im.vector.app.core.ui.views.NotificationAreaView
 | 
			
		||||
import im.vector.app.core.utils.Debouncer
 | 
			
		||||
@ -239,7 +236,6 @@ class RoomDetailFragment @Inject constructor(
 | 
			
		||||
        VectorBaseFragment<FragmentRoomDetailBinding>(),
 | 
			
		||||
        TimelineEventController.Callback,
 | 
			
		||||
        VectorInviteView.Callback,
 | 
			
		||||
        JumpToReadMarkerView.Callback,
 | 
			
		||||
        AttachmentTypeSelectorView.Callback,
 | 
			
		||||
        AttachmentsHelper.Callback,
 | 
			
		||||
        GalleryOrCameraDialogHelper.Listener,
 | 
			
		||||
@ -356,6 +352,10 @@ class RoomDetailFragment @Inject constructor(
 | 
			
		||||
            renderTombstoneEventHandling(it)
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        roomDetailViewModel.selectSubscribe(RoomDetailViewState::canShowJumpToReadMarker, RoomDetailViewState::unreadState) { _, _ ->
 | 
			
		||||
            updateJumpToReadMarkerViewVisibility()
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        roomDetailViewModel.selectSubscribe(RoomDetailViewState::sendMode, RoomDetailViewState::canSendMessage) { mode, canSend ->
 | 
			
		||||
            if (!canSend) {
 | 
			
		||||
                return@selectSubscribe
 | 
			
		||||
@ -725,7 +725,12 @@ class RoomDetailFragment @Inject constructor(
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private fun setupJumpToReadMarkerView() {
 | 
			
		||||
        views.jumpToReadMarkerView.callback = this
 | 
			
		||||
        views.jumpToReadMarkerView.setOnClickListener {
 | 
			
		||||
            onJumpToReadMarkerClicked()
 | 
			
		||||
        }
 | 
			
		||||
        views.jumpToReadMarkerView.setOnCloseIconClickListener {
 | 
			
		||||
            roomDetailViewModel.handle(RoomDetailAction.MarkAllAsRead)
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private fun setupActiveCallView() {
 | 
			
		||||
@ -1054,7 +1059,13 @@ class RoomDetailFragment @Inject constructor(
 | 
			
		||||
        timelineEventController.timeline = roomDetailViewModel.timeline
 | 
			
		||||
 | 
			
		||||
        views.timelineRecyclerView.trackItemsVisibilityChange()
 | 
			
		||||
        layoutManager = LinearLayoutManager(context, RecyclerView.VERTICAL, true)
 | 
			
		||||
        layoutManager = object : LinearLayoutManager(context, RecyclerView.VERTICAL, true) {
 | 
			
		||||
            override fun onLayoutCompleted(state: RecyclerView.State?) {
 | 
			
		||||
                super.onLayoutCompleted(state)
 | 
			
		||||
                updateJumpToReadMarkerViewVisibility()
 | 
			
		||||
                jumpToBottomViewVisibilityManager.maybeShowJumpToBottomViewVisibilityWithDelay()
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        val stateRestorer = LayoutManagerStateRestorer(layoutManager).register()
 | 
			
		||||
        scrollOnNewMessageCallback = ScrollOnNewMessageCallback(layoutManager, timelineEventController)
 | 
			
		||||
        scrollOnHighlightedEventCallback = ScrollOnHighlightedEventCallback(views.timelineRecyclerView, layoutManager, timelineEventController)
 | 
			
		||||
@ -1065,8 +1076,6 @@ class RoomDetailFragment @Inject constructor(
 | 
			
		||||
            it.dispatchTo(stateRestorer)
 | 
			
		||||
            it.dispatchTo(scrollOnNewMessageCallback)
 | 
			
		||||
            it.dispatchTo(scrollOnHighlightedEventCallback)
 | 
			
		||||
            updateJumpToReadMarkerViewVisibility()
 | 
			
		||||
            jumpToBottomViewVisibilityManager.maybeShowJumpToBottomViewVisibilityWithDelay()
 | 
			
		||||
        }
 | 
			
		||||
        timelineEventController.addModelBuildListener(modelBuildListener)
 | 
			
		||||
        views.timelineRecyclerView.adapter = timelineEventController.adapter
 | 
			
		||||
@ -1122,7 +1131,7 @@ class RoomDetailFragment @Inject constructor(
 | 
			
		||||
                    is UnreadState.ReadMarkerNotLoaded -> true
 | 
			
		||||
                    is UnreadState.HasUnread           -> {
 | 
			
		||||
                        if (it.canShowJumpToReadMarker) {
 | 
			
		||||
                            val lastVisibleItem = layoutManager.findLastVisibleItemPosition()
 | 
			
		||||
                            val lastVisibleItem = layoutManager.findLastCompletelyVisibleItemPosition()
 | 
			
		||||
                            val positionOfReadMarker = timelineEventController.getPositionOfReadMarker()
 | 
			
		||||
                            if (positionOfReadMarker == null) {
 | 
			
		||||
                                false
 | 
			
		||||
@ -1410,7 +1419,7 @@ class RoomDetailFragment @Inject constructor(
 | 
			
		||||
            is RoomDetailAction.ReportContent             -> {
 | 
			
		||||
                when {
 | 
			
		||||
                    data.spam          -> {
 | 
			
		||||
                        MaterialAlertDialogBuilder(requireActivity())
 | 
			
		||||
                        MaterialAlertDialogBuilder(requireActivity(), R.style.ThemeOverlay_Vector_MaterialAlertDialog_NegativeDestructive)
 | 
			
		||||
                                .setTitle(R.string.content_reported_as_spam_title)
 | 
			
		||||
                                .setMessage(R.string.content_reported_as_spam_content)
 | 
			
		||||
                                .setPositiveButton(R.string.ok, null)
 | 
			
		||||
@ -1418,10 +1427,9 @@ class RoomDetailFragment @Inject constructor(
 | 
			
		||||
                                    roomDetailViewModel.handle(RoomDetailAction.IgnoreUser(data.senderId))
 | 
			
		||||
                                }
 | 
			
		||||
                                .show()
 | 
			
		||||
                                .withColoredButton(DialogInterface.BUTTON_NEGATIVE)
 | 
			
		||||
                    }
 | 
			
		||||
                    data.inappropriate -> {
 | 
			
		||||
                        MaterialAlertDialogBuilder(requireActivity())
 | 
			
		||||
                        MaterialAlertDialogBuilder(requireActivity(), R.style.ThemeOverlay_Vector_MaterialAlertDialog_NegativeDestructive)
 | 
			
		||||
                                .setTitle(R.string.content_reported_as_inappropriate_title)
 | 
			
		||||
                                .setMessage(R.string.content_reported_as_inappropriate_content)
 | 
			
		||||
                                .setPositiveButton(R.string.ok, null)
 | 
			
		||||
@ -1429,10 +1437,9 @@ class RoomDetailFragment @Inject constructor(
 | 
			
		||||
                                    roomDetailViewModel.handle(RoomDetailAction.IgnoreUser(data.senderId))
 | 
			
		||||
                                }
 | 
			
		||||
                                .show()
 | 
			
		||||
                                .withColoredButton(DialogInterface.BUTTON_NEGATIVE)
 | 
			
		||||
                    }
 | 
			
		||||
                    else               -> {
 | 
			
		||||
                        MaterialAlertDialogBuilder(requireActivity())
 | 
			
		||||
                        MaterialAlertDialogBuilder(requireActivity(), R.style.ThemeOverlay_Vector_MaterialAlertDialog_NegativeDestructive)
 | 
			
		||||
                                .setTitle(R.string.content_reported_title)
 | 
			
		||||
                                .setMessage(R.string.content_reported_content)
 | 
			
		||||
                                .setPositiveButton(R.string.ok, null)
 | 
			
		||||
@ -1440,7 +1447,6 @@ class RoomDetailFragment @Inject constructor(
 | 
			
		||||
                                    roomDetailViewModel.handle(RoomDetailAction.IgnoreUser(data.senderId))
 | 
			
		||||
                                }
 | 
			
		||||
                                .show()
 | 
			
		||||
                                .withColoredButton(DialogInterface.BUTTON_NEGATIVE)
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
@ -1502,7 +1508,7 @@ class RoomDetailFragment @Inject constructor(
 | 
			
		||||
                .subscribe { managed ->
 | 
			
		||||
                    if (!managed) {
 | 
			
		||||
                        if (title.isValidUrl() && url.isValidUrl() && URL(title).host != URL(url).host) {
 | 
			
		||||
                            MaterialAlertDialogBuilder(requireActivity())
 | 
			
		||||
                            MaterialAlertDialogBuilder(requireActivity(), R.style.ThemeOverlay_Vector_MaterialAlertDialog_NegativeDestructive)
 | 
			
		||||
                                    .setTitle(R.string.external_link_confirmation_title)
 | 
			
		||||
                                    .setMessage(
 | 
			
		||||
                                            getString(R.string.external_link_confirmation_message, title, url)
 | 
			
		||||
@ -1515,7 +1521,6 @@ class RoomDetailFragment @Inject constructor(
 | 
			
		||||
                                    }
 | 
			
		||||
                                    .setNegativeButton(R.string.cancel, null)
 | 
			
		||||
                                    .show()
 | 
			
		||||
                                    .withColoredButton(DialogInterface.BUTTON_NEGATIVE)
 | 
			
		||||
                        } else {
 | 
			
		||||
                            // Open in external browser, in a new Tab
 | 
			
		||||
                            openUrlInExternalBrowser(requireContext(), url)
 | 
			
		||||
@ -1614,8 +1619,7 @@ class RoomDetailFragment @Inject constructor(
 | 
			
		||||
 | 
			
		||||
    override fun onEventLongClicked(informationData: MessageInformationData, messageContent: Any?, view: View): Boolean {
 | 
			
		||||
        view.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS)
 | 
			
		||||
        val roomId = roomDetailArgs.roomId
 | 
			
		||||
 | 
			
		||||
        val roomId = roomDetailViewModel.timeline.getTimelineEventWithId(informationData.eventId)?.roomId ?: return false
 | 
			
		||||
        this.view?.hideKeyboard()
 | 
			
		||||
 | 
			
		||||
        MessageActionsBottomSheet
 | 
			
		||||
@ -1699,7 +1703,6 @@ class RoomDetailFragment @Inject constructor(
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun onReadMarkerVisible() {
 | 
			
		||||
        updateJumpToReadMarkerViewVisibility()
 | 
			
		||||
        roomDetailViewModel.handle(RoomDetailAction.EnterTrackingUnreadMessagesState)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@ -1868,7 +1871,7 @@ class RoomDetailFragment @Inject constructor(
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private fun askConfirmationToIgnoreUser(senderId: String) {
 | 
			
		||||
        MaterialAlertDialogBuilder(requireContext())
 | 
			
		||||
        MaterialAlertDialogBuilder(requireContext(), R.style.ThemeOverlay_Vector_MaterialAlertDialog_Destructive)
 | 
			
		||||
                .setTitle(R.string.room_participants_action_ignore_title)
 | 
			
		||||
                .setMessage(R.string.room_participants_action_ignore_prompt_msg)
 | 
			
		||||
                .setNegativeButton(R.string.cancel, null)
 | 
			
		||||
@ -1876,7 +1879,6 @@ class RoomDetailFragment @Inject constructor(
 | 
			
		||||
                    roomDetailViewModel.handle(RoomDetailAction.IgnoreUser(senderId))
 | 
			
		||||
                }
 | 
			
		||||
                .show()
 | 
			
		||||
                .withColoredButton(DialogInterface.BUTTON_POSITIVE)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
@ -1959,10 +1961,7 @@ class RoomDetailFragment @Inject constructor(
 | 
			
		||||
        roomDetailViewModel.handle(RoomDetailAction.RejectInvite)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
// JumpToReadMarkerView.Callback
 | 
			
		||||
 | 
			
		||||
    override fun onJumpToReadMarkerClicked() = withState(roomDetailViewModel) {
 | 
			
		||||
        views.jumpToReadMarkerView.isVisible = false
 | 
			
		||||
    private fun onJumpToReadMarkerClicked() = withState(roomDetailViewModel) {
 | 
			
		||||
        if (it.unreadState is UnreadState.HasUnread) {
 | 
			
		||||
            roomDetailViewModel.handle(RoomDetailAction.NavigateToEvent(it.unreadState.firstUnreadEventId, false))
 | 
			
		||||
        }
 | 
			
		||||
@ -1971,10 +1970,6 @@ class RoomDetailFragment @Inject constructor(
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun onClearReadMarkerClicked() {
 | 
			
		||||
        roomDetailViewModel.handle(RoomDetailAction.MarkAllAsRead)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
// AttachmentTypeSelectorView.Callback
 | 
			
		||||
 | 
			
		||||
    private val typeSelectedActivityResultLauncher = registerForPermissionsResult { allGranted ->
 | 
			
		||||
 | 
			
		||||
@ -48,8 +48,8 @@ import im.vector.app.features.crypto.keysrequest.OutboundSessionKeySharingStrate
 | 
			
		||||
import im.vector.app.features.crypto.verification.SupportedVerificationMethodsProvider
 | 
			
		||||
import im.vector.app.features.home.room.detail.composer.rainbow.RainbowGenerator
 | 
			
		||||
import im.vector.app.features.home.room.detail.sticker.StickerPickerActionHandler
 | 
			
		||||
import im.vector.app.features.home.room.detail.timeline.factory.TimelineFactory
 | 
			
		||||
import im.vector.app.features.home.room.detail.timeline.helper.RoomSummariesHolder
 | 
			
		||||
import im.vector.app.features.home.room.detail.timeline.helper.TimelineSettingsFactory
 | 
			
		||||
import im.vector.app.features.home.room.detail.timeline.url.PreviewUrlRetriever
 | 
			
		||||
import im.vector.app.features.home.room.typing.TypingHelper
 | 
			
		||||
import im.vector.app.features.powerlevel.PowerLevelsObservableFactory
 | 
			
		||||
@ -118,7 +118,7 @@ class RoomDetailViewModel @AssistedInject constructor(
 | 
			
		||||
        private val chatEffectManager: ChatEffectManager,
 | 
			
		||||
        private val directRoomHelper: DirectRoomHelper,
 | 
			
		||||
        private val jitsiService: JitsiService,
 | 
			
		||||
        timelineSettingsFactory: TimelineSettingsFactory
 | 
			
		||||
        timelineFactory: TimelineFactory
 | 
			
		||||
) : VectorViewModel<RoomDetailViewState, RoomDetailAction, RoomDetailViewEvents>(initialState),
 | 
			
		||||
        Timeline.Listener, ChatEffectManager.Delegate, CallProtocolsChecker.Listener {
 | 
			
		||||
 | 
			
		||||
@ -126,9 +126,8 @@ class RoomDetailViewModel @AssistedInject constructor(
 | 
			
		||||
    private val eventId = initialState.eventId
 | 
			
		||||
    private val invisibleEventsObservable = BehaviorRelay.create<RoomDetailAction.TimelineEventTurnsInvisible>()
 | 
			
		||||
    private val visibleEventsObservable = BehaviorRelay.create<RoomDetailAction.TimelineEventTurnsVisible>()
 | 
			
		||||
    private val timelineSettings = timelineSettingsFactory.create()
 | 
			
		||||
    private var timelineEvents = PublishRelay.create<List<TimelineEvent>>()
 | 
			
		||||
    val timeline = room.createTimeline(eventId, timelineSettings)
 | 
			
		||||
    val timeline = timelineFactory.createTimeline(viewModelScope, room, eventId)
 | 
			
		||||
 | 
			
		||||
    // Same lifecycle than the ViewModel (survive to screen rotation)
 | 
			
		||||
    val previewUrlRetriever = PreviewUrlRetriever(session, viewModelScope)
 | 
			
		||||
@ -1244,6 +1243,7 @@ class RoomDetailViewModel @AssistedInject constructor(
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private fun handleMarkAllAsRead() {
 | 
			
		||||
        setState { copy(unreadState = UnreadState.HasNoUnread) }
 | 
			
		||||
        viewModelScope.launch {
 | 
			
		||||
            tryOrNull { room.markAsRead(ReadService.MarkAsReadParams.BOTH) }
 | 
			
		||||
        }
 | 
			
		||||
@ -1380,7 +1380,6 @@ class RoomDetailViewModel @AssistedInject constructor(
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
                .subscribe {
 | 
			
		||||
                    Timber.v("Unread state: $it")
 | 
			
		||||
                    setState { copy(unreadState = it) }
 | 
			
		||||
                }
 | 
			
		||||
                .disposeOnClear()
 | 
			
		||||
 | 
			
		||||
@ -20,7 +20,6 @@ import androidx.recyclerview.widget.LinearLayoutManager
 | 
			
		||||
import androidx.recyclerview.widget.RecyclerView
 | 
			
		||||
import im.vector.app.core.platform.DefaultListUpdateCallback
 | 
			
		||||
import im.vector.app.features.home.room.detail.timeline.TimelineEventController
 | 
			
		||||
import timber.log.Timber
 | 
			
		||||
import java.util.concurrent.atomic.AtomicReference
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
@ -42,19 +41,10 @@ class ScrollOnHighlightedEventCallback(private val recyclerView: RecyclerView,
 | 
			
		||||
 | 
			
		||||
    private fun scrollIfNeeded() {
 | 
			
		||||
        val eventId = scheduledEventId.get() ?: return
 | 
			
		||||
        val positionToScroll = timelineEventController.searchPositionOfEvent(eventId)
 | 
			
		||||
        if (positionToScroll != null) {
 | 
			
		||||
            val firstVisibleItem = layoutManager.findFirstCompletelyVisibleItemPosition()
 | 
			
		||||
            val lastVisibleItem = layoutManager.findLastCompletelyVisibleItemPosition()
 | 
			
		||||
 | 
			
		||||
            // Do not scroll it item is already visible
 | 
			
		||||
            if (positionToScroll !in firstVisibleItem..lastVisibleItem) {
 | 
			
		||||
                Timber.v("Scroll to $positionToScroll")
 | 
			
		||||
                recyclerView.stopScroll()
 | 
			
		||||
                layoutManager.scrollToPosition(positionToScroll)
 | 
			
		||||
            }
 | 
			
		||||
            scheduledEventId.set(null)
 | 
			
		||||
        }
 | 
			
		||||
        val positionToScroll = timelineEventController.searchPositionOfEvent(eventId) ?: return
 | 
			
		||||
        recyclerView.stopScroll()
 | 
			
		||||
        layoutManager.scrollToPosition(positionToScroll)
 | 
			
		||||
        scheduledEventId.set(null)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun scheduleScrollTo(eventId: String?) {
 | 
			
		||||
 | 
			
		||||
@ -16,6 +16,7 @@
 | 
			
		||||
package im.vector.app.features.home.room.detail.timeline.factory
 | 
			
		||||
 | 
			
		||||
import im.vector.app.core.epoxy.VectorEpoxyModel
 | 
			
		||||
import im.vector.app.features.call.vectorCallService
 | 
			
		||||
import im.vector.app.features.call.webrtc.WebRtcCallManager
 | 
			
		||||
import im.vector.app.features.home.room.detail.timeline.MessageColorProvider
 | 
			
		||||
import im.vector.app.features.home.room.detail.timeline.TimelineEventController
 | 
			
		||||
@ -26,6 +27,7 @@ import im.vector.app.features.home.room.detail.timeline.helper.RoomSummariesHold
 | 
			
		||||
import im.vector.app.features.home.room.detail.timeline.item.CallTileTimelineItem
 | 
			
		||||
import im.vector.app.features.home.room.detail.timeline.item.CallTileTimelineItem_
 | 
			
		||||
import im.vector.app.features.home.room.detail.timeline.item.MessageInformationData
 | 
			
		||||
import org.matrix.android.sdk.api.session.Session
 | 
			
		||||
import org.matrix.android.sdk.api.session.events.model.EventType
 | 
			
		||||
import org.matrix.android.sdk.api.session.events.model.toModel
 | 
			
		||||
import org.matrix.android.sdk.api.session.room.model.call.CallAnswerContent
 | 
			
		||||
@ -38,6 +40,7 @@ import org.matrix.android.sdk.api.util.toMatrixItem
 | 
			
		||||
import javax.inject.Inject
 | 
			
		||||
 | 
			
		||||
class CallItemFactory @Inject constructor(
 | 
			
		||||
        private val session: Session,
 | 
			
		||||
        private val messageColorProvider: MessageColorProvider,
 | 
			
		||||
        private val messageInformationDataFactory: MessageInformationDataFactory,
 | 
			
		||||
        private val messageItemAttributesFactory: MessageItemAttributesFactory,
 | 
			
		||||
@ -132,7 +135,8 @@ class CallItemFactory @Inject constructor(
 | 
			
		||||
            isStillActive: Boolean,
 | 
			
		||||
            callback: TimelineEventController.Callback?
 | 
			
		||||
    ): CallTileTimelineItem? {
 | 
			
		||||
        val userOfInterest = roomSummariesHolder.get(roomId)?.toMatrixItem() ?: return null
 | 
			
		||||
        val correctedRoomId = session.vectorCallService.userMapper.nativeRoomForVirtualRoom(roomId) ?: roomId
 | 
			
		||||
        val userOfInterest = roomSummariesHolder.get(correctedRoomId)?.toMatrixItem() ?: return null
 | 
			
		||||
        val attributes = messageItemAttributesFactory.create(null, informationData, callback).let {
 | 
			
		||||
            CallTileTimelineItem.Attributes(
 | 
			
		||||
                    callId = callId,
 | 
			
		||||
 | 
			
		||||
@ -0,0 +1,59 @@
 | 
			
		||||
/*
 | 
			
		||||
 * Copyright (c) 2021 New Vector Ltd
 | 
			
		||||
 *
 | 
			
		||||
 * Licensed under the Apache License, Version 2.0 (the "License");
 | 
			
		||||
 * you may not use this file except in compliance with the License.
 | 
			
		||||
 * You may obtain a copy of the License at
 | 
			
		||||
 *
 | 
			
		||||
 *     http://www.apache.org/licenses/LICENSE-2.0
 | 
			
		||||
 *
 | 
			
		||||
 * Unless required by applicable law or agreed to in writing, software
 | 
			
		||||
 * distributed under the License is distributed on an "AS IS" BASIS,
 | 
			
		||||
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | 
			
		||||
 * See the License for the specific language governing permissions and
 | 
			
		||||
 * limitations under the License.
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
package im.vector.app.features.home.room.detail.timeline.factory
 | 
			
		||||
 | 
			
		||||
import im.vector.app.features.call.vectorCallService
 | 
			
		||||
import im.vector.app.features.home.room.detail.timeline.helper.TimelineSettingsFactory
 | 
			
		||||
import im.vector.app.features.home.room.detail.timeline.merged.MergedTimelines
 | 
			
		||||
import kotlinx.coroutines.CoroutineScope
 | 
			
		||||
import org.matrix.android.sdk.api.session.Session
 | 
			
		||||
import org.matrix.android.sdk.api.session.events.model.EventType
 | 
			
		||||
import org.matrix.android.sdk.api.session.room.Room
 | 
			
		||||
import org.matrix.android.sdk.api.session.room.timeline.Timeline
 | 
			
		||||
import javax.inject.Inject
 | 
			
		||||
 | 
			
		||||
private val secondaryTimelineAllowedTypes = listOf(
 | 
			
		||||
        EventType.CALL_HANGUP,
 | 
			
		||||
        EventType.CALL_INVITE,
 | 
			
		||||
        EventType.CALL_REJECT,
 | 
			
		||||
        EventType.CALL_ANSWER
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
class TimelineFactory @Inject constructor(private val session: Session, private val timelineSettingsFactory: TimelineSettingsFactory) {
 | 
			
		||||
 | 
			
		||||
    fun createTimeline(coroutineScope: CoroutineScope, mainRoom: Room, eventId: String?): Timeline {
 | 
			
		||||
        val settings = timelineSettingsFactory.create()
 | 
			
		||||
        if (!session.vectorCallService.protocolChecker.supportVirtualRooms) {
 | 
			
		||||
            return mainRoom.createTimeline(eventId, settings)
 | 
			
		||||
        }
 | 
			
		||||
        val virtualRoomId = session.vectorCallService.userMapper.virtualRoomForNativeRoom(mainRoom.roomId)
 | 
			
		||||
        return if (virtualRoomId == null) {
 | 
			
		||||
            mainRoom.createTimeline(eventId, settings)
 | 
			
		||||
        } else {
 | 
			
		||||
            val virtualRoom = session.getRoom(virtualRoomId)!!
 | 
			
		||||
            MergedTimelines(
 | 
			
		||||
                    coroutineScope = coroutineScope,
 | 
			
		||||
                    mainTimeline = mainRoom.createTimeline(eventId, settings),
 | 
			
		||||
                    secondaryTimelineParams = MergedTimelines.SecondaryTimelineParams(
 | 
			
		||||
                            timeline = virtualRoom.createTimeline(null, settings),
 | 
			
		||||
                            shouldFilterTypes = true,
 | 
			
		||||
                            allowedTypes = secondaryTimelineAllowedTypes
 | 
			
		||||
                    )
 | 
			
		||||
            )
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,223 @@
 | 
			
		||||
/*
 | 
			
		||||
 * Copyright (c) 2021 New Vector Ltd
 | 
			
		||||
 *
 | 
			
		||||
 * Licensed under the Apache License, Version 2.0 (the "License");
 | 
			
		||||
 * you may not use this file except in compliance with the License.
 | 
			
		||||
 * You may obtain a copy of the License at
 | 
			
		||||
 *
 | 
			
		||||
 *     http://www.apache.org/licenses/LICENSE-2.0
 | 
			
		||||
 *
 | 
			
		||||
 * Unless required by applicable law or agreed to in writing, software
 | 
			
		||||
 * distributed under the License is distributed on an "AS IS" BASIS,
 | 
			
		||||
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | 
			
		||||
 * See the License for the specific language governing permissions and
 | 
			
		||||
 * limitations under the License.
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
package im.vector.app.features.home.room.detail.timeline.merged
 | 
			
		||||
 | 
			
		||||
import kotlinx.coroutines.CoroutineScope
 | 
			
		||||
import kotlinx.coroutines.Dispatchers
 | 
			
		||||
import kotlinx.coroutines.launch
 | 
			
		||||
import kotlinx.coroutines.sync.Semaphore
 | 
			
		||||
import kotlinx.coroutines.sync.withPermit
 | 
			
		||||
import kotlinx.coroutines.withContext
 | 
			
		||||
import org.matrix.android.sdk.api.extensions.tryOrNull
 | 
			
		||||
import org.matrix.android.sdk.api.session.room.sender.SenderInfo
 | 
			
		||||
import org.matrix.android.sdk.api.session.room.timeline.Timeline
 | 
			
		||||
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
 | 
			
		||||
import kotlin.reflect.KMutableProperty0
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * This can be use to merge timeline tiles from 2 different rooms.
 | 
			
		||||
 * Be aware it wont work properly with permalink.
 | 
			
		||||
 */
 | 
			
		||||
class MergedTimelines(
 | 
			
		||||
        private val coroutineScope: CoroutineScope,
 | 
			
		||||
        private val mainTimeline: Timeline,
 | 
			
		||||
        private val secondaryTimelineParams: SecondaryTimelineParams) : Timeline by mainTimeline {
 | 
			
		||||
 | 
			
		||||
    data class SecondaryTimelineParams(
 | 
			
		||||
            val timeline: Timeline,
 | 
			
		||||
            val disableReadReceipts: Boolean = true,
 | 
			
		||||
            val shouldFilterTypes: Boolean = false,
 | 
			
		||||
            val allowedTypes: List<String> = emptyList()
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    private var mainIsInit = false
 | 
			
		||||
    private var secondaryIsInit = false
 | 
			
		||||
    private val secondaryTimeline = secondaryTimelineParams.timeline
 | 
			
		||||
 | 
			
		||||
    private val listenersMapping = HashMap<Timeline.Listener, List<ListenerInterceptor>>()
 | 
			
		||||
    private val mainTimelineEvents = ArrayList<TimelineEvent>()
 | 
			
		||||
    private val secondaryTimelineEvents = ArrayList<TimelineEvent>()
 | 
			
		||||
    private val positionsMapping = HashMap<String, Int>()
 | 
			
		||||
    private val mergedEvents = ArrayList<TimelineEvent>()
 | 
			
		||||
 | 
			
		||||
    private val processingSemaphore = Semaphore(1)
 | 
			
		||||
 | 
			
		||||
    private class ListenerInterceptor(
 | 
			
		||||
            var timeline: Timeline?,
 | 
			
		||||
            private val wrappedListener: Timeline.Listener,
 | 
			
		||||
            private val shouldFilterTypes: Boolean,
 | 
			
		||||
            private val allowedTypes: List<String>,
 | 
			
		||||
            private val onTimelineUpdate: (List<TimelineEvent>) -> Unit
 | 
			
		||||
    ) : Timeline.Listener by wrappedListener {
 | 
			
		||||
 | 
			
		||||
        override fun onTimelineUpdated(snapshot: List<TimelineEvent>) {
 | 
			
		||||
            val filteredEvents = if (shouldFilterTypes) {
 | 
			
		||||
                snapshot.filter {
 | 
			
		||||
                    allowedTypes.contains(it.root.getClearType())
 | 
			
		||||
                }
 | 
			
		||||
            } else {
 | 
			
		||||
                snapshot
 | 
			
		||||
            }
 | 
			
		||||
            onTimelineUpdate(filteredEvents)
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun addListener(listener: Timeline.Listener): Boolean {
 | 
			
		||||
        val mainTimelineListener = ListenerInterceptor(
 | 
			
		||||
                timeline = mainTimeline,
 | 
			
		||||
                wrappedListener = listener,
 | 
			
		||||
                shouldFilterTypes = false,
 | 
			
		||||
                allowedTypes = emptyList()) {
 | 
			
		||||
            processTimelineUpdates(::mainIsInit, mainTimelineEvents, it)
 | 
			
		||||
        }
 | 
			
		||||
        val secondaryTimelineListener = ListenerInterceptor(
 | 
			
		||||
                timeline = secondaryTimeline,
 | 
			
		||||
                wrappedListener = listener,
 | 
			
		||||
                shouldFilterTypes = secondaryTimelineParams.shouldFilterTypes,
 | 
			
		||||
                allowedTypes = secondaryTimelineParams.allowedTypes) {
 | 
			
		||||
            processTimelineUpdates(::secondaryIsInit, secondaryTimelineEvents, it)
 | 
			
		||||
        }
 | 
			
		||||
        listenersMapping[listener] = listOf(mainTimelineListener, secondaryTimelineListener)
 | 
			
		||||
        return mainTimeline.addListener(mainTimelineListener) && secondaryTimeline.addListener(secondaryTimelineListener)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun removeListener(listener: Timeline.Listener): Boolean {
 | 
			
		||||
        return listenersMapping.remove(listener)?.let {
 | 
			
		||||
            it.forEach { listener ->
 | 
			
		||||
                listener.timeline?.removeListener(listener)
 | 
			
		||||
                listener.timeline = null
 | 
			
		||||
            }
 | 
			
		||||
            true
 | 
			
		||||
        } ?: false
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun removeAllListeners() {
 | 
			
		||||
        mainTimeline.removeAllListeners()
 | 
			
		||||
        secondaryTimeline.removeAllListeners()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun start() {
 | 
			
		||||
        mainTimeline.start()
 | 
			
		||||
        secondaryTimeline.start()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun dispose() {
 | 
			
		||||
        mainTimeline.dispose()
 | 
			
		||||
        secondaryTimeline.dispose()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun restartWithEventId(eventId: String?) {
 | 
			
		||||
        mainTimeline.restartWithEventId(eventId)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun hasMoreToLoad(direction: Timeline.Direction): Boolean {
 | 
			
		||||
        return mainTimeline.hasMoreToLoad(direction) || secondaryTimeline.hasMoreToLoad(direction)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun paginate(direction: Timeline.Direction, count: Int) {
 | 
			
		||||
        mainTimeline.paginate(direction, count)
 | 
			
		||||
        secondaryTimeline.paginate(direction, count)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun pendingEventCount(): Int {
 | 
			
		||||
        return mainTimeline.pendingEventCount() + secondaryTimeline.pendingEventCount()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun failedToDeliverEventCount(): Int {
 | 
			
		||||
        return mainTimeline.pendingEventCount() + secondaryTimeline.pendingEventCount()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun getTimelineEventAtIndex(index: Int): TimelineEvent? {
 | 
			
		||||
        return mergedEvents.getOrNull(index)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun getIndexOfEvent(eventId: String?): Int? {
 | 
			
		||||
        return positionsMapping[eventId]
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun getTimelineEventWithId(eventId: String?): TimelineEvent? {
 | 
			
		||||
        return positionsMapping[eventId]?.let {
 | 
			
		||||
            getTimelineEventAtIndex(it)
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private fun processTimelineUpdates(isInit: KMutableProperty0<Boolean>, eventsRef: MutableList<TimelineEvent>, newData: List<TimelineEvent>) {
 | 
			
		||||
        coroutineScope.launch(Dispatchers.Default) {
 | 
			
		||||
            processingSemaphore.withPermit {
 | 
			
		||||
                isInit.set(true)
 | 
			
		||||
                eventsRef.apply {
 | 
			
		||||
                    clear()
 | 
			
		||||
                    addAll(newData)
 | 
			
		||||
                }
 | 
			
		||||
                mergeTimeline()
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private suspend fun mergeTimeline() {
 | 
			
		||||
        val merged = mutableListOf<TimelineEvent>()
 | 
			
		||||
        val mainItr = mainTimelineEvents.toList().listIterator()
 | 
			
		||||
        val secondaryItr = secondaryTimelineEvents.toList().listIterator()
 | 
			
		||||
        var index = 0
 | 
			
		||||
        var correctedSenderInfo: SenderInfo? = mainTimelineEvents.firstOrNull()?.senderInfo
 | 
			
		||||
        if (!mainIsInit || !secondaryIsInit) {
 | 
			
		||||
            return
 | 
			
		||||
        }
 | 
			
		||||
        while (merged.size < mainTimelineEvents.size + secondaryTimelineEvents.size) {
 | 
			
		||||
            if (mainItr.hasNext()) {
 | 
			
		||||
                val nextMain = mainItr.next()
 | 
			
		||||
                correctedSenderInfo = nextMain.senderInfo
 | 
			
		||||
                if (secondaryItr.hasNext()) {
 | 
			
		||||
                    val nextSecondary = secondaryItr.next()
 | 
			
		||||
                    if (nextSecondary.root.originServerTs ?: 0 > nextMain.root.originServerTs ?: 0) {
 | 
			
		||||
                        positionsMapping[nextSecondary.eventId] = index
 | 
			
		||||
                        merged.add(nextSecondary.correctBeforeMerging(correctedSenderInfo))
 | 
			
		||||
                        mainItr.previous()
 | 
			
		||||
                    } else {
 | 
			
		||||
                        positionsMapping[nextMain.eventId] = index
 | 
			
		||||
                        merged.add(nextMain)
 | 
			
		||||
                        secondaryItr.previous()
 | 
			
		||||
                    }
 | 
			
		||||
                } else {
 | 
			
		||||
                    positionsMapping[nextMain.eventId] = index
 | 
			
		||||
                    merged.add(nextMain)
 | 
			
		||||
                }
 | 
			
		||||
            } else if (secondaryItr.hasNext()) {
 | 
			
		||||
                val nextSecondary = secondaryItr.next()
 | 
			
		||||
                positionsMapping[nextSecondary.eventId] = index
 | 
			
		||||
                merged.add(nextSecondary.correctBeforeMerging(correctedSenderInfo))
 | 
			
		||||
            }
 | 
			
		||||
            index++
 | 
			
		||||
        }
 | 
			
		||||
        mergedEvents.apply {
 | 
			
		||||
            clear()
 | 
			
		||||
            addAll(merged)
 | 
			
		||||
        }
 | 
			
		||||
        withContext(Dispatchers.Main) {
 | 
			
		||||
            listenersMapping.keys.forEach { listener ->
 | 
			
		||||
                tryOrNull { listener.onTimelineUpdated(merged) }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private fun TimelineEvent.correctBeforeMerging(correctedSenderInfo: SenderInfo?): TimelineEvent {
 | 
			
		||||
        return copy(
 | 
			
		||||
                senderInfo = correctedSenderInfo ?: senderInfo,
 | 
			
		||||
                readReceipts = if (secondaryTimelineParams.disableReadReceipts) emptyList() else readReceipts
 | 
			
		||||
        )
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@ -22,6 +22,8 @@ import im.vector.app.R
 | 
			
		||||
import im.vector.app.RoomGroupingMethod
 | 
			
		||||
import im.vector.app.core.resources.StringProvider
 | 
			
		||||
import im.vector.app.features.home.RoomListDisplayMode
 | 
			
		||||
import im.vector.app.features.invite.AutoAcceptInvites
 | 
			
		||||
import im.vector.app.features.invite.showInvites
 | 
			
		||||
import io.reactivex.disposables.Disposable
 | 
			
		||||
import io.reactivex.schedulers.Schedulers
 | 
			
		||||
import kotlinx.coroutines.CoroutineScope
 | 
			
		||||
@ -38,6 +40,7 @@ class GroupRoomListSectionBuilder(
 | 
			
		||||
        val stringProvider: StringProvider,
 | 
			
		||||
        val viewModelScope: CoroutineScope,
 | 
			
		||||
        val appStateHandler: AppStateHandler,
 | 
			
		||||
        private val autoAcceptInvites: AutoAcceptInvites,
 | 
			
		||||
        val onDisposable: (Disposable) -> Unit,
 | 
			
		||||
        val onUdpatable: (UpdatableLivePageResult) -> Unit
 | 
			
		||||
) : RoomListSectionBuilder {
 | 
			
		||||
@ -48,15 +51,15 @@ class GroupRoomListSectionBuilder(
 | 
			
		||||
        val actualGroupId = appStateHandler.safeActiveGroupId()
 | 
			
		||||
 | 
			
		||||
        when (mode) {
 | 
			
		||||
            RoomListDisplayMode.PEOPLE -> {
 | 
			
		||||
            RoomListDisplayMode.PEOPLE        -> {
 | 
			
		||||
                // 3 sections Invites / Fav / Dms
 | 
			
		||||
                buildPeopleSections(sections, activeGroupAwareQueries, actualGroupId)
 | 
			
		||||
            }
 | 
			
		||||
            RoomListDisplayMode.ROOMS -> {
 | 
			
		||||
            RoomListDisplayMode.ROOMS         -> {
 | 
			
		||||
                // 5 sections invites / Fav / Rooms / Low Priority / Server notice
 | 
			
		||||
                buildRoomsSections(sections, activeGroupAwareQueries, actualGroupId)
 | 
			
		||||
            }
 | 
			
		||||
            RoomListDisplayMode.FILTERED -> {
 | 
			
		||||
            RoomListDisplayMode.FILTERED      -> {
 | 
			
		||||
                // Used when searching for rooms
 | 
			
		||||
                withQueryParams(
 | 
			
		||||
                        {
 | 
			
		||||
@ -73,17 +76,18 @@ class GroupRoomListSectionBuilder(
 | 
			
		||||
                )
 | 
			
		||||
            }
 | 
			
		||||
            RoomListDisplayMode.NOTIFICATIONS -> {
 | 
			
		||||
                addSection(
 | 
			
		||||
                        sections,
 | 
			
		||||
                        activeGroupAwareQueries,
 | 
			
		||||
                        R.string.invitations_header,
 | 
			
		||||
                        true
 | 
			
		||||
                ) {
 | 
			
		||||
                    it.memberships = listOf(Membership.INVITE)
 | 
			
		||||
                    it.roomCategoryFilter = RoomCategoryFilter.ALL
 | 
			
		||||
                    it.activeGroupId = actualGroupId
 | 
			
		||||
                if (autoAcceptInvites.showInvites()) {
 | 
			
		||||
                    addSection(
 | 
			
		||||
                            sections,
 | 
			
		||||
                            activeGroupAwareQueries,
 | 
			
		||||
                            R.string.invitations_header,
 | 
			
		||||
                            true
 | 
			
		||||
                    ) {
 | 
			
		||||
                        it.memberships = listOf(Membership.INVITE)
 | 
			
		||||
                        it.roomCategoryFilter = RoomCategoryFilter.ALL
 | 
			
		||||
                        it.activeGroupId = actualGroupId
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                addSection(
 | 
			
		||||
                        sections,
 | 
			
		||||
                        activeGroupAwareQueries,
 | 
			
		||||
@ -115,15 +119,17 @@ class GroupRoomListSectionBuilder(
 | 
			
		||||
    private fun buildRoomsSections(sections: MutableList<RoomsSection>,
 | 
			
		||||
                                   activeSpaceAwareQueries: MutableList<UpdatableLivePageResult>,
 | 
			
		||||
                                   actualGroupId: String?) {
 | 
			
		||||
        addSection(
 | 
			
		||||
                sections,
 | 
			
		||||
                activeSpaceAwareQueries,
 | 
			
		||||
                R.string.invitations_header,
 | 
			
		||||
                true
 | 
			
		||||
        ) {
 | 
			
		||||
            it.memberships = listOf(Membership.INVITE)
 | 
			
		||||
            it.roomCategoryFilter = RoomCategoryFilter.ONLY_ROOMS
 | 
			
		||||
            it.activeGroupId = actualGroupId
 | 
			
		||||
        if (autoAcceptInvites.showInvites()) {
 | 
			
		||||
            addSection(
 | 
			
		||||
                    sections,
 | 
			
		||||
                    activeSpaceAwareQueries,
 | 
			
		||||
                    R.string.invitations_header,
 | 
			
		||||
                    true
 | 
			
		||||
            ) {
 | 
			
		||||
                it.memberships = listOf(Membership.INVITE)
 | 
			
		||||
                it.roomCategoryFilter = RoomCategoryFilter.ONLY_ROOMS
 | 
			
		||||
                it.activeGroupId = actualGroupId
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        addSection(
 | 
			
		||||
@ -180,14 +186,16 @@ class GroupRoomListSectionBuilder(
 | 
			
		||||
            activeSpaceAwareQueries: MutableList<UpdatableLivePageResult>,
 | 
			
		||||
            actualGroupId: String?
 | 
			
		||||
    ) {
 | 
			
		||||
        addSection(sections,
 | 
			
		||||
                activeSpaceAwareQueries,
 | 
			
		||||
                R.string.invitations_header,
 | 
			
		||||
                true
 | 
			
		||||
        ) {
 | 
			
		||||
            it.memberships = listOf(Membership.INVITE)
 | 
			
		||||
            it.roomCategoryFilter = RoomCategoryFilter.ONLY_DM
 | 
			
		||||
            it.activeGroupId = actualGroupId
 | 
			
		||||
        if (autoAcceptInvites.showInvites()) {
 | 
			
		||||
            addSection(sections,
 | 
			
		||||
                    activeSpaceAwareQueries,
 | 
			
		||||
                    R.string.invitations_header,
 | 
			
		||||
                    true
 | 
			
		||||
            ) {
 | 
			
		||||
                it.memberships = listOf(Membership.INVITE)
 | 
			
		||||
                it.roomCategoryFilter = RoomCategoryFilter.ONLY_DM
 | 
			
		||||
                it.activeGroupId = actualGroupId
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        addSection(
 | 
			
		||||
 | 
			
		||||
@ -16,7 +16,6 @@
 | 
			
		||||
 | 
			
		||||
package im.vector.app.features.home.room.list
 | 
			
		||||
 | 
			
		||||
import android.content.DialogInterface
 | 
			
		||||
import android.os.Bundle
 | 
			
		||||
import android.os.Parcelable
 | 
			
		||||
import android.view.LayoutInflater
 | 
			
		||||
@ -34,7 +33,6 @@ import com.airbnb.mvrx.fragmentViewModel
 | 
			
		||||
import com.airbnb.mvrx.withState
 | 
			
		||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
 | 
			
		||||
import im.vector.app.R
 | 
			
		||||
import im.vector.app.core.dialogs.withColoredButton
 | 
			
		||||
import im.vector.app.core.epoxy.LayoutManagerStateRestorer
 | 
			
		||||
import im.vector.app.core.extensions.cleanup
 | 
			
		||||
import im.vector.app.core.extensions.exhaustive
 | 
			
		||||
@ -386,7 +384,7 @@ class RoomListFragment @Inject constructor(
 | 
			
		||||
                append(getString(R.string.room_participants_leave_private_warning))
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        MaterialAlertDialogBuilder(requireContext())
 | 
			
		||||
        MaterialAlertDialogBuilder(requireContext(), if (isPublicRoom) 0 else R.style.ThemeOverlay_Vector_MaterialAlertDialog_Destructive)
 | 
			
		||||
                .setTitle(R.string.room_participants_leave_prompt_title)
 | 
			
		||||
                .setMessage(message)
 | 
			
		||||
                .setPositiveButton(R.string.leave) { _, _ ->
 | 
			
		||||
@ -394,11 +392,6 @@ class RoomListFragment @Inject constructor(
 | 
			
		||||
                }
 | 
			
		||||
                .setNegativeButton(R.string.cancel, null)
 | 
			
		||||
                .show()
 | 
			
		||||
                .apply {
 | 
			
		||||
                    if (!isPublicRoom) {
 | 
			
		||||
                        withColoredButton(DialogInterface.BUTTON_POSITIVE)
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun invalidate() = withState(roomListViewModel) { state ->
 | 
			
		||||
 | 
			
		||||
@ -30,6 +30,7 @@ import im.vector.app.RoomGroupingMethod
 | 
			
		||||
import im.vector.app.core.extensions.exhaustive
 | 
			
		||||
import im.vector.app.core.platform.VectorViewModel
 | 
			
		||||
import im.vector.app.core.resources.StringProvider
 | 
			
		||||
import im.vector.app.features.invite.AutoAcceptInvites
 | 
			
		||||
import im.vector.app.features.settings.VectorPreferences
 | 
			
		||||
import kotlinx.coroutines.Dispatchers
 | 
			
		||||
import kotlinx.coroutines.launch
 | 
			
		||||
@ -49,7 +50,8 @@ class RoomListViewModel @Inject constructor(
 | 
			
		||||
        private val session: Session,
 | 
			
		||||
        private val stringProvider: StringProvider,
 | 
			
		||||
        private val appStateHandler: AppStateHandler,
 | 
			
		||||
        private val vectorPreferences: VectorPreferences
 | 
			
		||||
        private val vectorPreferences: VectorPreferences,
 | 
			
		||||
        private val autoAcceptInvites: AutoAcceptInvites
 | 
			
		||||
) : VectorViewModel<RoomListViewState, RoomListAction, RoomListViewEvents>(initialState) {
 | 
			
		||||
 | 
			
		||||
    interface Factory {
 | 
			
		||||
@ -126,6 +128,7 @@ class RoomListViewModel @Inject constructor(
 | 
			
		||||
                    appStateHandler,
 | 
			
		||||
                    viewModelScope,
 | 
			
		||||
                    suggestedRoomJoiningState,
 | 
			
		||||
                    autoAcceptInvites,
 | 
			
		||||
                    {
 | 
			
		||||
                        it.disposeOnClear()
 | 
			
		||||
                    },
 | 
			
		||||
@ -140,6 +143,7 @@ class RoomListViewModel @Inject constructor(
 | 
			
		||||
                    stringProvider,
 | 
			
		||||
                    viewModelScope,
 | 
			
		||||
                    appStateHandler,
 | 
			
		||||
                    autoAcceptInvites,
 | 
			
		||||
                    {
 | 
			
		||||
                        it.disposeOnClear()
 | 
			
		||||
                    },
 | 
			
		||||
 | 
			
		||||
@ -18,6 +18,7 @@ package im.vector.app.features.home.room.list
 | 
			
		||||
 | 
			
		||||
import im.vector.app.AppStateHandler
 | 
			
		||||
import im.vector.app.core.resources.StringProvider
 | 
			
		||||
import im.vector.app.features.invite.AutoAcceptInvites
 | 
			
		||||
import im.vector.app.features.settings.VectorPreferences
 | 
			
		||||
import org.matrix.android.sdk.api.session.Session
 | 
			
		||||
import javax.inject.Inject
 | 
			
		||||
@ -26,16 +27,18 @@ import javax.inject.Provider
 | 
			
		||||
class RoomListViewModelFactory @Inject constructor(private val session: Provider<Session>,
 | 
			
		||||
                                                   private val appStateHandler: AppStateHandler,
 | 
			
		||||
                                                   private val stringProvider: StringProvider,
 | 
			
		||||
                                                   private val vectorPreferences: VectorPreferences)
 | 
			
		||||
                                                   private val vectorPreferences: VectorPreferences,
 | 
			
		||||
                                                   private val autoAcceptInvites: AutoAcceptInvites)
 | 
			
		||||
    : RoomListViewModel.Factory {
 | 
			
		||||
 | 
			
		||||
    override fun create(initialState: RoomListViewState): RoomListViewModel {
 | 
			
		||||
        return RoomListViewModel(
 | 
			
		||||
                initialState,
 | 
			
		||||
                session.get(),
 | 
			
		||||
                stringProvider,
 | 
			
		||||
                appStateHandler,
 | 
			
		||||
                vectorPreferences
 | 
			
		||||
                initialState = initialState,
 | 
			
		||||
                session = session.get(),
 | 
			
		||||
                stringProvider = stringProvider,
 | 
			
		||||
                appStateHandler = appStateHandler,
 | 
			
		||||
                vectorPreferences = vectorPreferences,
 | 
			
		||||
                autoAcceptInvites = autoAcceptInvites
 | 
			
		||||
        )
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -17,11 +17,13 @@
 | 
			
		||||
package im.vector.app.features.home.room.list
 | 
			
		||||
 | 
			
		||||
import com.airbnb.mvrx.Async
 | 
			
		||||
import com.airbnb.mvrx.Fail
 | 
			
		||||
import com.airbnb.mvrx.Loading
 | 
			
		||||
import im.vector.app.R
 | 
			
		||||
import im.vector.app.core.date.DateFormatKind
 | 
			
		||||
import im.vector.app.core.date.VectorDateFormatter
 | 
			
		||||
import im.vector.app.core.epoxy.VectorEpoxyModel
 | 
			
		||||
import im.vector.app.core.error.ErrorFormatter
 | 
			
		||||
import im.vector.app.core.resources.StringProvider
 | 
			
		||||
import im.vector.app.features.home.AvatarRenderer
 | 
			
		||||
import im.vector.app.features.home.room.detail.timeline.format.DisplayableEventFormatter
 | 
			
		||||
@ -37,7 +39,8 @@ class RoomSummaryItemFactory @Inject constructor(private val displayableEventFor
 | 
			
		||||
                                                 private val dateFormatter: VectorDateFormatter,
 | 
			
		||||
                                                 private val stringProvider: StringProvider,
 | 
			
		||||
                                                 private val typingHelper: TypingHelper,
 | 
			
		||||
                                                 private val avatarRenderer: AvatarRenderer) {
 | 
			
		||||
                                                 private val avatarRenderer: AvatarRenderer,
 | 
			
		||||
                                                 private val errorFormatter: ErrorFormatter) {
 | 
			
		||||
 | 
			
		||||
    fun create(roomSummary: RoomSummary,
 | 
			
		||||
               roomChangeMembershipStates: Map<String, ChangeMembershipState>,
 | 
			
		||||
@ -55,12 +58,21 @@ class RoomSummaryItemFactory @Inject constructor(private val displayableEventFor
 | 
			
		||||
    fun createSuggestion(spaceChildInfo: SpaceChildInfo,
 | 
			
		||||
                         suggestedRoomJoiningStates: Map<String, Async<Unit>>,
 | 
			
		||||
                         listener: RoomListListener?): VectorEpoxyModel<*> {
 | 
			
		||||
        val error = (suggestedRoomJoiningStates[spaceChildInfo.childRoomId] as? Fail)?.error
 | 
			
		||||
        return SpaceChildInfoItem_()
 | 
			
		||||
                .id("sug_${spaceChildInfo.childRoomId}")
 | 
			
		||||
                .matrixItem(spaceChildInfo.toMatrixItem())
 | 
			
		||||
                .avatarRenderer(avatarRenderer)
 | 
			
		||||
                .topic(spaceChildInfo.topic)
 | 
			
		||||
                .buttonLabel(stringProvider.getString(R.string.join))
 | 
			
		||||
                .errorLabel(
 | 
			
		||||
                        error?.let {
 | 
			
		||||
                            stringProvider.getString(R.string.error_failed_to_join_room, errorFormatter.toHumanReadable(it))
 | 
			
		||||
                        }
 | 
			
		||||
                )
 | 
			
		||||
                .buttonLabel(
 | 
			
		||||
                        if (error != null) stringProvider.getString(R.string.global_retry)
 | 
			
		||||
                        else stringProvider.getString(R.string.join)
 | 
			
		||||
                )
 | 
			
		||||
                .loading(suggestedRoomJoiningStates[spaceChildInfo.childRoomId] is Loading)
 | 
			
		||||
                .memberCount(spaceChildInfo.activeMemberCount ?: 0)
 | 
			
		||||
                .buttonClickListener { listener?.onJoinSuggestedRoom(spaceChildInfo) }
 | 
			
		||||
 | 
			
		||||
@ -33,6 +33,7 @@ import im.vector.app.core.epoxy.ClickListener
 | 
			
		||||
import im.vector.app.core.epoxy.VectorEpoxyHolder
 | 
			
		||||
import im.vector.app.core.epoxy.VectorEpoxyModel
 | 
			
		||||
import im.vector.app.core.epoxy.onClick
 | 
			
		||||
import im.vector.app.core.extensions.setTextOrHide
 | 
			
		||||
import im.vector.app.features.home.AvatarRenderer
 | 
			
		||||
import im.vector.app.features.themes.ThemeUtils
 | 
			
		||||
import me.gujun.android.span.image
 | 
			
		||||
@ -52,6 +53,7 @@ abstract class SpaceChildInfoItem : VectorEpoxyModel<SpaceChildInfoItem.Holder>(
 | 
			
		||||
    @EpoxyAttribute var loading: Boolean = false
 | 
			
		||||
 | 
			
		||||
    @EpoxyAttribute var buttonLabel: String? = null
 | 
			
		||||
    @EpoxyAttribute var errorLabel: String? = null
 | 
			
		||||
 | 
			
		||||
    @EpoxyAttribute(EpoxyAttribute.Option.DoNotHash) var itemLongClickListener: View.OnLongClickListener? = null
 | 
			
		||||
    @EpoxyAttribute(EpoxyAttribute.Option.DoNotHash) var itemClickListener: ClickListener? = null
 | 
			
		||||
@ -97,6 +99,8 @@ abstract class SpaceChildInfoItem : VectorEpoxyModel<SpaceChildInfoItem.Holder>(
 | 
			
		||||
            holder.joinButton.isVisible = true
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        holder.errorTextView.setTextOrHide(errorLabel)
 | 
			
		||||
 | 
			
		||||
        holder.joinButton.onClick {
 | 
			
		||||
            // local echo
 | 
			
		||||
            holder.joinButton.isEnabled = false
 | 
			
		||||
@ -120,5 +124,6 @@ abstract class SpaceChildInfoItem : VectorEpoxyModel<SpaceChildInfoItem.Holder>(
 | 
			
		||||
        val descriptionText by bind<TextView>(R.id.suggestedRoomDescription)
 | 
			
		||||
        val avatarImageView by bind<ImageView>(R.id.roomAvatarImageView)
 | 
			
		||||
        val rootView by bind<ViewGroup>(R.id.itemRoomLayout)
 | 
			
		||||
        val errorTextView by bind<TextView>(R.id.inlineErrorText)
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -26,6 +26,8 @@ import im.vector.app.AppStateHandler
 | 
			
		||||
import im.vector.app.R
 | 
			
		||||
import im.vector.app.core.resources.StringProvider
 | 
			
		||||
import im.vector.app.features.home.RoomListDisplayMode
 | 
			
		||||
import im.vector.app.features.invite.AutoAcceptInvites
 | 
			
		||||
import im.vector.app.features.invite.showInvites
 | 
			
		||||
import im.vector.app.space
 | 
			
		||||
import io.reactivex.Observable
 | 
			
		||||
import io.reactivex.disposables.Disposable
 | 
			
		||||
@ -50,6 +52,7 @@ class SpaceRoomListSectionBuilder(
 | 
			
		||||
        val appStateHandler: AppStateHandler,
 | 
			
		||||
        val viewModelScope: CoroutineScope,
 | 
			
		||||
        private val suggestedRoomJoiningState: LiveData<Map<String, Async<Unit>>>,
 | 
			
		||||
        private val autoAcceptInvites: AutoAcceptInvites,
 | 
			
		||||
        val onDisposable: (Disposable) -> Unit,
 | 
			
		||||
        val onUdpatable: (UpdatableLivePageResult) -> Unit,
 | 
			
		||||
        val onlyOrphansInHome: Boolean = false
 | 
			
		||||
@ -66,13 +69,13 @@ class SpaceRoomListSectionBuilder(
 | 
			
		||||
        val sections = mutableListOf<RoomsSection>()
 | 
			
		||||
        val activeSpaceAwareQueries = mutableListOf<RoomListViewModel.ActiveSpaceQueryUpdater>()
 | 
			
		||||
        when (mode) {
 | 
			
		||||
            RoomListDisplayMode.PEOPLE -> {
 | 
			
		||||
            RoomListDisplayMode.PEOPLE        -> {
 | 
			
		||||
                buildDmSections(sections, activeSpaceAwareQueries)
 | 
			
		||||
            }
 | 
			
		||||
            RoomListDisplayMode.ROOMS -> {
 | 
			
		||||
            RoomListDisplayMode.ROOMS         -> {
 | 
			
		||||
                buildRoomsSections(sections, activeSpaceAwareQueries)
 | 
			
		||||
            }
 | 
			
		||||
            RoomListDisplayMode.FILTERED -> {
 | 
			
		||||
            RoomListDisplayMode.FILTERED      -> {
 | 
			
		||||
                withQueryParams(
 | 
			
		||||
                        {
 | 
			
		||||
                            it.memberships = Membership.activeMemberships()
 | 
			
		||||
@ -88,20 +91,22 @@ class SpaceRoomListSectionBuilder(
 | 
			
		||||
                )
 | 
			
		||||
            }
 | 
			
		||||
            RoomListDisplayMode.NOTIFICATIONS -> {
 | 
			
		||||
                addSection(
 | 
			
		||||
                        sections = sections,
 | 
			
		||||
                        activeSpaceUpdaters = activeSpaceAwareQueries,
 | 
			
		||||
                        nameRes = R.string.invitations_header,
 | 
			
		||||
                        notifyOfLocalEcho = true,
 | 
			
		||||
                        spaceFilterStrategy = if (onlyOrphansInHome) {
 | 
			
		||||
                            RoomListViewModel.SpaceFilterStrategy.ORPHANS_IF_SPACE_NULL
 | 
			
		||||
                        } else {
 | 
			
		||||
                            RoomListViewModel.SpaceFilterStrategy.ALL_IF_SPACE_NULL
 | 
			
		||||
                        },
 | 
			
		||||
                        countRoomAsNotif = true
 | 
			
		||||
                ) {
 | 
			
		||||
                    it.memberships = listOf(Membership.INVITE)
 | 
			
		||||
                    it.roomCategoryFilter = RoomCategoryFilter.ALL
 | 
			
		||||
                if (autoAcceptInvites.showInvites()) {
 | 
			
		||||
                    addSection(
 | 
			
		||||
                            sections = sections,
 | 
			
		||||
                            activeSpaceUpdaters = activeSpaceAwareQueries,
 | 
			
		||||
                            nameRes = R.string.invitations_header,
 | 
			
		||||
                            notifyOfLocalEcho = true,
 | 
			
		||||
                            spaceFilterStrategy = if (onlyOrphansInHome) {
 | 
			
		||||
                                RoomListViewModel.SpaceFilterStrategy.ORPHANS_IF_SPACE_NULL
 | 
			
		||||
                            } else {
 | 
			
		||||
                                RoomListViewModel.SpaceFilterStrategy.ALL_IF_SPACE_NULL
 | 
			
		||||
                            },
 | 
			
		||||
                            countRoomAsNotif = true
 | 
			
		||||
                    ) {
 | 
			
		||||
                        it.memberships = listOf(Membership.INVITE)
 | 
			
		||||
                        it.roomCategoryFilter = RoomCategoryFilter.ALL
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                addSection(
 | 
			
		||||
@ -136,16 +141,18 @@ class SpaceRoomListSectionBuilder(
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private fun buildRoomsSections(sections: MutableList<RoomsSection>, activeSpaceAwareQueries: MutableList<RoomListViewModel.ActiveSpaceQueryUpdater>) {
 | 
			
		||||
        addSection(
 | 
			
		||||
                sections = sections,
 | 
			
		||||
                activeSpaceUpdaters = activeSpaceAwareQueries,
 | 
			
		||||
                nameRes = R.string.invitations_header,
 | 
			
		||||
                notifyOfLocalEcho = true,
 | 
			
		||||
                spaceFilterStrategy = RoomListViewModel.SpaceFilterStrategy.ALL_IF_SPACE_NULL,
 | 
			
		||||
                countRoomAsNotif = true
 | 
			
		||||
        ) {
 | 
			
		||||
            it.memberships = listOf(Membership.INVITE)
 | 
			
		||||
            it.roomCategoryFilter = RoomCategoryFilter.ONLY_ROOMS
 | 
			
		||||
        if (autoAcceptInvites.showInvites()) {
 | 
			
		||||
            addSection(
 | 
			
		||||
                    sections = sections,
 | 
			
		||||
                    activeSpaceUpdaters = activeSpaceAwareQueries,
 | 
			
		||||
                    nameRes = R.string.invitations_header,
 | 
			
		||||
                    notifyOfLocalEcho = true,
 | 
			
		||||
                    spaceFilterStrategy = RoomListViewModel.SpaceFilterStrategy.ALL_IF_SPACE_NULL,
 | 
			
		||||
                    countRoomAsNotif = true
 | 
			
		||||
            ) {
 | 
			
		||||
                it.memberships = listOf(Membership.INVITE)
 | 
			
		||||
                it.roomCategoryFilter = RoomCategoryFilter.ONLY_ROOMS
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        addSection(
 | 
			
		||||
@ -253,15 +260,17 @@ class SpaceRoomListSectionBuilder(
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private fun buildDmSections(sections: MutableList<RoomsSection>, activeSpaceAwareQueries: MutableList<RoomListViewModel.ActiveSpaceQueryUpdater>) {
 | 
			
		||||
        addSection(sections = sections,
 | 
			
		||||
                activeSpaceUpdaters = activeSpaceAwareQueries,
 | 
			
		||||
                nameRes = R.string.invitations_header,
 | 
			
		||||
                notifyOfLocalEcho = true,
 | 
			
		||||
                spaceFilterStrategy = RoomListViewModel.SpaceFilterStrategy.ALL_IF_SPACE_NULL,
 | 
			
		||||
                countRoomAsNotif = true
 | 
			
		||||
        ) {
 | 
			
		||||
            it.memberships = listOf(Membership.INVITE)
 | 
			
		||||
            it.roomCategoryFilter = RoomCategoryFilter.ONLY_DM
 | 
			
		||||
        if (autoAcceptInvites.showInvites()) {
 | 
			
		||||
            addSection(sections = sections,
 | 
			
		||||
                    activeSpaceUpdaters = activeSpaceAwareQueries,
 | 
			
		||||
                    nameRes = R.string.invitations_header,
 | 
			
		||||
                    notifyOfLocalEcho = true,
 | 
			
		||||
                    spaceFilterStrategy = RoomListViewModel.SpaceFilterStrategy.ALL_IF_SPACE_NULL,
 | 
			
		||||
                    countRoomAsNotif = true
 | 
			
		||||
            ) {
 | 
			
		||||
                it.memberships = listOf(Membership.INVITE)
 | 
			
		||||
                it.roomCategoryFilter = RoomCategoryFilter.ONLY_DM
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        addSection(sections,
 | 
			
		||||
@ -387,7 +396,7 @@ class SpaceRoomListSectionBuilder(
 | 
			
		||||
                        activeSpaceFilter = ActiveSpaceFilter.ActiveSpace(currentSpace)
 | 
			
		||||
                )
 | 
			
		||||
            }
 | 
			
		||||
            RoomListViewModel.SpaceFilterStrategy.ALL_IF_SPACE_NULL -> {
 | 
			
		||||
            RoomListViewModel.SpaceFilterStrategy.ALL_IF_SPACE_NULL     -> {
 | 
			
		||||
                if (currentSpace == null) {
 | 
			
		||||
                    copy(
 | 
			
		||||
                            activeSpaceFilter = ActiveSpaceFilter.None
 | 
			
		||||
@ -398,7 +407,7 @@ class SpaceRoomListSectionBuilder(
 | 
			
		||||
                    )
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            RoomListViewModel.SpaceFilterStrategy.NONE -> this
 | 
			
		||||
            RoomListViewModel.SpaceFilterStrategy.NONE                  -> this
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -0,0 +1,45 @@
 | 
			
		||||
/*
 | 
			
		||||
 * Copyright (c) 2021 New Vector Ltd
 | 
			
		||||
 *
 | 
			
		||||
 * Licensed under the Apache License, Version 2.0 (the "License");
 | 
			
		||||
 * you may not use this file except in compliance with the License.
 | 
			
		||||
 * You may obtain a copy of the License at
 | 
			
		||||
 *
 | 
			
		||||
 *     http://www.apache.org/licenses/LICENSE-2.0
 | 
			
		||||
 *
 | 
			
		||||
 * Unless required by applicable law or agreed to in writing, software
 | 
			
		||||
 * distributed under the License is distributed on an "AS IS" BASIS,
 | 
			
		||||
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | 
			
		||||
 * See the License for the specific language governing permissions and
 | 
			
		||||
 * limitations under the License.
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
package im.vector.app.features.invite
 | 
			
		||||
 | 
			
		||||
import javax.inject.Inject
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * This interface defines 2 flags so you can handle auto accept invites.
 | 
			
		||||
 * At the moment we only have [CompileTimeAutoAcceptInvites] implementation.
 | 
			
		||||
 */
 | 
			
		||||
interface AutoAcceptInvites {
 | 
			
		||||
    /**
 | 
			
		||||
     * Enable auto-accept invites. It means, as soon as you got an invite from the sync, it will try to join it.
 | 
			
		||||
     */
 | 
			
		||||
    val isEnabled: Boolean
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Hide invites from the UI (from notifications, notification count and room list). By default invites are hidden when [isEnabled] is true
 | 
			
		||||
     */
 | 
			
		||||
    val hideInvites: Boolean
 | 
			
		||||
        get() = isEnabled
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
fun AutoAcceptInvites.showInvites() = !hideInvites
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Simple compile time implementation of AutoAcceptInvites flags.
 | 
			
		||||
 */
 | 
			
		||||
class CompileTimeAutoAcceptInvites @Inject constructor() : AutoAcceptInvites {
 | 
			
		||||
    override val isEnabled = false
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,143 @@
 | 
			
		||||
/*
 | 
			
		||||
 * Copyright (c) 2021 New Vector Ltd
 | 
			
		||||
 *
 | 
			
		||||
 * Licensed under the Apache License, Version 2.0 (the "License");
 | 
			
		||||
 * you may not use this file except in compliance with the License.
 | 
			
		||||
 * You may obtain a copy of the License at
 | 
			
		||||
 *
 | 
			
		||||
 *     http://www.apache.org/licenses/LICENSE-2.0
 | 
			
		||||
 *
 | 
			
		||||
 * Unless required by applicable law or agreed to in writing, software
 | 
			
		||||
 * distributed under the License is distributed on an "AS IS" BASIS,
 | 
			
		||||
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | 
			
		||||
 * See the License for the specific language governing permissions and
 | 
			
		||||
 * limitations under the License.
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
package im.vector.app.features.invite
 | 
			
		||||
 | 
			
		||||
import im.vector.app.ActiveSessionDataSource
 | 
			
		||||
import im.vector.app.features.session.coroutineScope
 | 
			
		||||
import io.reactivex.Observable
 | 
			
		||||
import io.reactivex.disposables.Disposable
 | 
			
		||||
import kotlinx.coroutines.async
 | 
			
		||||
import kotlinx.coroutines.launch
 | 
			
		||||
import kotlinx.coroutines.sync.Semaphore
 | 
			
		||||
import kotlinx.coroutines.sync.withPermit
 | 
			
		||||
import org.matrix.android.sdk.api.extensions.orFalse
 | 
			
		||||
import org.matrix.android.sdk.api.failure.Failure
 | 
			
		||||
import org.matrix.android.sdk.api.session.Session
 | 
			
		||||
import org.matrix.android.sdk.api.session.room.Room
 | 
			
		||||
import org.matrix.android.sdk.api.session.room.members.ChangeMembershipState
 | 
			
		||||
import org.matrix.android.sdk.api.session.room.model.Membership
 | 
			
		||||
import org.matrix.android.sdk.api.session.room.roomSummaryQueryParams
 | 
			
		||||
import org.matrix.android.sdk.rx.rx
 | 
			
		||||
import timber.log.Timber
 | 
			
		||||
import java.util.concurrent.TimeUnit
 | 
			
		||||
import javax.inject.Inject
 | 
			
		||||
import javax.inject.Singleton
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * This class is responsible for auto accepting invites.
 | 
			
		||||
 * It's listening to invites and membershipChanges so it can retry automatically if needed.
 | 
			
		||||
 * This mechanism will be on only if AutoAcceptInvites.isEnabled is true.
 | 
			
		||||
 */
 | 
			
		||||
@Singleton
 | 
			
		||||
class InvitesAcceptor @Inject constructor(
 | 
			
		||||
        private val sessionDataSource: ActiveSessionDataSource,
 | 
			
		||||
        private val autoAcceptInvites: AutoAcceptInvites
 | 
			
		||||
) : Session.Listener {
 | 
			
		||||
 | 
			
		||||
    private lateinit var activeSessionDisposable: Disposable
 | 
			
		||||
    private val shouldRejectRoomIds = mutableSetOf<String>()
 | 
			
		||||
    private val invitedRoomDisposables = HashMap<String, Disposable>()
 | 
			
		||||
    private val semaphore = Semaphore(1)
 | 
			
		||||
 | 
			
		||||
    fun initialize() {
 | 
			
		||||
        observeActiveSession()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private fun observeActiveSession() {
 | 
			
		||||
        activeSessionDisposable = sessionDataSource.observe()
 | 
			
		||||
                .distinctUntilChanged()
 | 
			
		||||
                .subscribe {
 | 
			
		||||
                    it.orNull()?.let { session ->
 | 
			
		||||
                        onSessionActive(session)
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private fun onSessionActive(session: Session) {
 | 
			
		||||
        if (!autoAcceptInvites.isEnabled) {
 | 
			
		||||
            return
 | 
			
		||||
        }
 | 
			
		||||
        if (invitedRoomDisposables.containsKey(session.sessionId)) {
 | 
			
		||||
            return
 | 
			
		||||
        }
 | 
			
		||||
        session.addListener(this)
 | 
			
		||||
        val roomQueryParams = roomSummaryQueryParams {
 | 
			
		||||
            this.memberships = listOf(Membership.INVITE)
 | 
			
		||||
        }
 | 
			
		||||
        val rxSession = session.rx()
 | 
			
		||||
        Observable
 | 
			
		||||
                .combineLatest(
 | 
			
		||||
                        rxSession.liveRoomSummaries(roomQueryParams),
 | 
			
		||||
                        rxSession.liveRoomChangeMembershipState().debounce(1, TimeUnit.SECONDS),
 | 
			
		||||
                        { invitedRooms, _ -> invitedRooms.map { it.roomId } }
 | 
			
		||||
                )
 | 
			
		||||
                .filter { it.isNotEmpty() }
 | 
			
		||||
                .subscribe { invitedRoomIds ->
 | 
			
		||||
                    session.coroutineScope.launch {
 | 
			
		||||
                        semaphore.withPermit {
 | 
			
		||||
                            Timber.v("Invited roomIds: $invitedRoomIds")
 | 
			
		||||
                            for (roomId in invitedRoomIds) {
 | 
			
		||||
                                async { session.joinRoomSafely(roomId) }.start()
 | 
			
		||||
                            }
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
                .also {
 | 
			
		||||
                    invitedRoomDisposables[session.sessionId] = it
 | 
			
		||||
                }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private suspend fun Session.joinRoomSafely(roomId: String) {
 | 
			
		||||
        if (shouldRejectRoomIds.contains(roomId)) {
 | 
			
		||||
            getRoom(roomId)?.rejectInviteSafely()
 | 
			
		||||
            return
 | 
			
		||||
        }
 | 
			
		||||
        val roomMembershipChanged = getChangeMemberships(roomId)
 | 
			
		||||
        if (roomMembershipChanged != ChangeMembershipState.Joined && !roomMembershipChanged.isInProgress()) {
 | 
			
		||||
            try {
 | 
			
		||||
                Timber.v("Try auto join room: $roomId")
 | 
			
		||||
                joinRoom(roomId)
 | 
			
		||||
            } catch (failure: Throwable) {
 | 
			
		||||
                Timber.v("Failed auto join room: $roomId")
 | 
			
		||||
                // if we got 404 on invites, the inviting user have left or the hs is off.
 | 
			
		||||
                if (failure is Failure.ServerError && failure.httpCode == 404) {
 | 
			
		||||
                    val room = getRoom(roomId) ?: return
 | 
			
		||||
                    val inviterId = room.roomSummary()?.inviterId
 | 
			
		||||
                    // if the inviting user is on the same HS, there can only be one cause: they left, so we try to reject the invite.
 | 
			
		||||
                    if (inviterId?.endsWith(sessionParams.credentials.homeServer.orEmpty()).orFalse()) {
 | 
			
		||||
                        shouldRejectRoomIds.add(roomId)
 | 
			
		||||
                        room.rejectInviteSafely()
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private suspend fun Room.rejectInviteSafely() {
 | 
			
		||||
        try {
 | 
			
		||||
            leave(null)
 | 
			
		||||
            shouldRejectRoomIds.remove(roomId)
 | 
			
		||||
        } catch (failure: Throwable) {
 | 
			
		||||
            Timber.v("Fail rejecting invite for room: $roomId")
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun onSessionStopped(session: Session) {
 | 
			
		||||
        session.removeListener(this)
 | 
			
		||||
        invitedRoomDisposables.remove(session.sessionId)?.dispose()
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@ -32,7 +32,7 @@ import com.jakewharton.rxbinding3.widget.textChanges
 | 
			
		||||
import im.vector.app.R
 | 
			
		||||
import im.vector.app.core.extensions.exhaustive
 | 
			
		||||
import im.vector.app.core.extensions.hideKeyboard
 | 
			
		||||
import im.vector.app.core.extensions.showPassword
 | 
			
		||||
import im.vector.app.core.extensions.hidePassword
 | 
			
		||||
import im.vector.app.core.extensions.toReducedUrl
 | 
			
		||||
import im.vector.app.databinding.FragmentLoginBinding
 | 
			
		||||
import io.reactivex.Observable
 | 
			
		||||
@ -53,7 +53,6 @@ import javax.inject.Inject
 | 
			
		||||
 */
 | 
			
		||||
class LoginFragment @Inject constructor() : AbstractSSOLoginFragment<FragmentLoginBinding>() {
 | 
			
		||||
 | 
			
		||||
    private var passwordShown = false
 | 
			
		||||
    private var isSignupMode = false
 | 
			
		||||
 | 
			
		||||
    // Temporary patch for https://github.com/vector-im/riotX-android/issues/1410,
 | 
			
		||||
@ -69,7 +68,6 @@ class LoginFragment @Inject constructor() : AbstractSSOLoginFragment<FragmentLog
 | 
			
		||||
 | 
			
		||||
        setupSubmitButton()
 | 
			
		||||
        setupForgottenPasswordButton()
 | 
			
		||||
        setupPasswordReveal()
 | 
			
		||||
 | 
			
		||||
        views.passwordField.setOnEditorActionListener { _, actionId, _ ->
 | 
			
		||||
            if (actionId == EditorInfo.IME_ACTION_DONE) {
 | 
			
		||||
@ -247,23 +245,6 @@ class LoginFragment @Inject constructor() : AbstractSSOLoginFragment<FragmentLog
 | 
			
		||||
        loginViewModel.handle(LoginAction.PostViewEvent(LoginViewEvents.OnForgetPasswordClicked))
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private fun setupPasswordReveal() {
 | 
			
		||||
        passwordShown = false
 | 
			
		||||
 | 
			
		||||
        views.passwordReveal.setOnClickListener {
 | 
			
		||||
            passwordShown = !passwordShown
 | 
			
		||||
 | 
			
		||||
            renderPasswordField()
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        renderPasswordField()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private fun renderPasswordField() {
 | 
			
		||||
        views.passwordField.showPassword(passwordShown)
 | 
			
		||||
        views.passwordReveal.render(passwordShown)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun resetViewModel() {
 | 
			
		||||
        loginViewModel.handle(LoginAction.ResetLogin)
 | 
			
		||||
    }
 | 
			
		||||
@ -290,8 +271,7 @@ class LoginFragment @Inject constructor() : AbstractSSOLoginFragment<FragmentLog
 | 
			
		||||
        when (state.asyncLoginAction) {
 | 
			
		||||
            is Loading -> {
 | 
			
		||||
                // Ensure password is hidden
 | 
			
		||||
                passwordShown = false
 | 
			
		||||
                renderPasswordField()
 | 
			
		||||
                views.passwordField.hidePassword()
 | 
			
		||||
            }
 | 
			
		||||
            is Fail    -> {
 | 
			
		||||
                val error = state.asyncLoginAction.error
 | 
			
		||||
@ -317,8 +297,7 @@ class LoginFragment @Inject constructor() : AbstractSSOLoginFragment<FragmentLog
 | 
			
		||||
        when (state.asyncRegistration) {
 | 
			
		||||
            is Loading -> {
 | 
			
		||||
                // Ensure password is hidden
 | 
			
		||||
                passwordShown = false
 | 
			
		||||
                renderPasswordField()
 | 
			
		||||
                views.passwordField.hidePassword()
 | 
			
		||||
            }
 | 
			
		||||
            // Success is handled by the LoginActivity
 | 
			
		||||
            is Success -> Unit
 | 
			
		||||
 | 
			
		||||
@ -28,7 +28,7 @@ import com.jakewharton.rxbinding3.widget.textChanges
 | 
			
		||||
import im.vector.app.R
 | 
			
		||||
import im.vector.app.core.extensions.hideKeyboard
 | 
			
		||||
import im.vector.app.core.extensions.isEmail
 | 
			
		||||
import im.vector.app.core.extensions.showPassword
 | 
			
		||||
import im.vector.app.core.extensions.hidePassword
 | 
			
		||||
import im.vector.app.core.extensions.toReducedUrl
 | 
			
		||||
import im.vector.app.databinding.FragmentLoginResetPasswordBinding
 | 
			
		||||
import io.reactivex.Observable
 | 
			
		||||
@ -41,8 +41,6 @@ import javax.inject.Inject
 | 
			
		||||
 */
 | 
			
		||||
class LoginResetPasswordFragment @Inject constructor() : AbstractLoginFragment<FragmentLoginResetPasswordBinding>() {
 | 
			
		||||
 | 
			
		||||
    private var passwordShown = false
 | 
			
		||||
 | 
			
		||||
    // Show warning only once
 | 
			
		||||
    private var showWarning = true
 | 
			
		||||
 | 
			
		||||
@ -54,7 +52,6 @@ class LoginResetPasswordFragment @Inject constructor() : AbstractLoginFragment<F
 | 
			
		||||
        super.onViewCreated(view, savedInstanceState)
 | 
			
		||||
 | 
			
		||||
        setupSubmitButton()
 | 
			
		||||
        setupPasswordReveal()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private fun setupUi(state: LoginViewState) {
 | 
			
		||||
@ -112,23 +109,6 @@ class LoginResetPasswordFragment @Inject constructor() : AbstractLoginFragment<F
 | 
			
		||||
        views.passwordFieldTil.error = null
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private fun setupPasswordReveal() {
 | 
			
		||||
        passwordShown = false
 | 
			
		||||
 | 
			
		||||
        views.passwordReveal.setOnClickListener {
 | 
			
		||||
            passwordShown = !passwordShown
 | 
			
		||||
 | 
			
		||||
            renderPasswordField()
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        renderPasswordField()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private fun renderPasswordField() {
 | 
			
		||||
        views.passwordField.showPassword(passwordShown)
 | 
			
		||||
        views.passwordReveal.render(passwordShown)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun resetViewModel() {
 | 
			
		||||
        loginViewModel.handle(LoginAction.ResetResetPassword)
 | 
			
		||||
    }
 | 
			
		||||
@ -139,8 +119,7 @@ class LoginResetPasswordFragment @Inject constructor() : AbstractLoginFragment<F
 | 
			
		||||
        when (state.asyncResetPassword) {
 | 
			
		||||
            is Loading -> {
 | 
			
		||||
                // Ensure new password is hidden
 | 
			
		||||
                passwordShown = false
 | 
			
		||||
                renderPasswordField()
 | 
			
		||||
                views.passwordField.hidePassword()
 | 
			
		||||
            }
 | 
			
		||||
            is Fail    -> {
 | 
			
		||||
                views.resetPasswordEmailTil.error = errorFormatter.toHumanReadable(state.asyncResetPassword.error)
 | 
			
		||||
 | 
			
		||||
@ -28,7 +28,7 @@ import com.airbnb.mvrx.Fail
 | 
			
		||||
import com.jakewharton.rxbinding3.widget.textChanges
 | 
			
		||||
import im.vector.app.R
 | 
			
		||||
import im.vector.app.core.extensions.hideKeyboard
 | 
			
		||||
import im.vector.app.core.extensions.showPassword
 | 
			
		||||
import im.vector.app.core.extensions.hidePassword
 | 
			
		||||
import im.vector.app.databinding.FragmentLoginSigninPassword2Binding
 | 
			
		||||
import im.vector.app.features.home.AvatarRenderer
 | 
			
		||||
import io.reactivex.rxkotlin.subscribeBy
 | 
			
		||||
@ -47,8 +47,6 @@ class LoginFragmentSigninPassword2 @Inject constructor(
 | 
			
		||||
        private val avatarRenderer: AvatarRenderer
 | 
			
		||||
) : AbstractSSOLoginFragment2<FragmentLoginSigninPassword2Binding>() {
 | 
			
		||||
 | 
			
		||||
    private var passwordShown = false
 | 
			
		||||
 | 
			
		||||
    override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentLoginSigninPassword2Binding {
 | 
			
		||||
        return FragmentLoginSigninPassword2Binding.inflate(inflater, container, false)
 | 
			
		||||
    }
 | 
			
		||||
@ -58,7 +56,6 @@ class LoginFragmentSigninPassword2 @Inject constructor(
 | 
			
		||||
 | 
			
		||||
        setupSubmitButton()
 | 
			
		||||
        setupForgottenPasswordButton()
 | 
			
		||||
        setupPasswordReveal()
 | 
			
		||||
        setupAutoFill()
 | 
			
		||||
 | 
			
		||||
        views.passwordField.setOnEditorActionListener { _, actionId, _ ->
 | 
			
		||||
@ -135,23 +132,6 @@ class LoginFragmentSigninPassword2 @Inject constructor(
 | 
			
		||||
        loginViewModel.handle(LoginAction2.PostViewEvent(LoginViewEvents2.OpenResetPasswordScreen))
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private fun setupPasswordReveal() {
 | 
			
		||||
        passwordShown = false
 | 
			
		||||
 | 
			
		||||
        views.passwordReveal.setOnClickListener {
 | 
			
		||||
            passwordShown = !passwordShown
 | 
			
		||||
 | 
			
		||||
            renderPasswordField()
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        renderPasswordField()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private fun renderPasswordField() {
 | 
			
		||||
        views.passwordField.showPassword(passwordShown)
 | 
			
		||||
        views.passwordReveal.render(passwordShown)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun resetViewModel() {
 | 
			
		||||
        // loginViewModel.handle(LoginAction2.ResetSignin)
 | 
			
		||||
    }
 | 
			
		||||
@ -169,8 +149,7 @@ class LoginFragmentSigninPassword2 @Inject constructor(
 | 
			
		||||
 | 
			
		||||
        if (state.isLoading) {
 | 
			
		||||
            // Ensure password is hidden
 | 
			
		||||
            passwordShown = false
 | 
			
		||||
            renderPasswordField()
 | 
			
		||||
            views.passwordField.hidePassword()
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -26,7 +26,7 @@ import androidx.autofill.HintConstants
 | 
			
		||||
import com.jakewharton.rxbinding3.widget.textChanges
 | 
			
		||||
import im.vector.app.R
 | 
			
		||||
import im.vector.app.core.extensions.hideKeyboard
 | 
			
		||||
import im.vector.app.core.extensions.showPassword
 | 
			
		||||
import im.vector.app.core.extensions.hidePassword
 | 
			
		||||
import im.vector.app.databinding.FragmentLoginSignupPassword2Binding
 | 
			
		||||
import io.reactivex.rxkotlin.subscribeBy
 | 
			
		||||
import javax.inject.Inject
 | 
			
		||||
@ -37,8 +37,6 @@ import javax.inject.Inject
 | 
			
		||||
 */
 | 
			
		||||
class LoginFragmentSignupPassword2 @Inject constructor() : AbstractLoginFragment2<FragmentLoginSignupPassword2Binding>() {
 | 
			
		||||
 | 
			
		||||
    private var passwordShown = false
 | 
			
		||||
 | 
			
		||||
    override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentLoginSignupPassword2Binding {
 | 
			
		||||
        return FragmentLoginSignupPassword2Binding.inflate(inflater, container, false)
 | 
			
		||||
    }
 | 
			
		||||
@ -48,7 +46,6 @@ class LoginFragmentSignupPassword2 @Inject constructor() : AbstractLoginFragment
 | 
			
		||||
 | 
			
		||||
        setupSubmitButton()
 | 
			
		||||
        setupAutoFill()
 | 
			
		||||
        setupPasswordReveal()
 | 
			
		||||
 | 
			
		||||
        views.passwordField.setOnEditorActionListener { _, actionId, _ ->
 | 
			
		||||
            if (actionId == EditorInfo.IME_ACTION_DONE) {
 | 
			
		||||
@ -97,23 +94,6 @@ class LoginFragmentSignupPassword2 @Inject constructor() : AbstractLoginFragment
 | 
			
		||||
                .disposeOnDestroyView()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private fun setupPasswordReveal() {
 | 
			
		||||
        passwordShown = false
 | 
			
		||||
 | 
			
		||||
        views.passwordReveal.setOnClickListener {
 | 
			
		||||
            passwordShown = !passwordShown
 | 
			
		||||
 | 
			
		||||
            renderPasswordField()
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        renderPasswordField()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private fun renderPasswordField() {
 | 
			
		||||
        views.passwordReveal.render(passwordShown)
 | 
			
		||||
        views.passwordField.showPassword(passwordShown)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun resetViewModel() {
 | 
			
		||||
        // loginViewModel.handle(LoginAction2.ResetSignup)
 | 
			
		||||
    }
 | 
			
		||||
@ -127,8 +107,7 @@ class LoginFragmentSignupPassword2 @Inject constructor() : AbstractLoginFragment
 | 
			
		||||
 | 
			
		||||
        if (state.isLoading) {
 | 
			
		||||
            // Ensure password is hidden
 | 
			
		||||
            passwordShown = false
 | 
			
		||||
            renderPasswordField()
 | 
			
		||||
            views.passwordField.hidePassword()
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -27,7 +27,7 @@ import androidx.core.view.isVisible
 | 
			
		||||
import com.jakewharton.rxbinding3.widget.textChanges
 | 
			
		||||
import im.vector.app.R
 | 
			
		||||
import im.vector.app.core.extensions.hideKeyboard
 | 
			
		||||
import im.vector.app.core.extensions.showPassword
 | 
			
		||||
import im.vector.app.core.extensions.hidePassword
 | 
			
		||||
import im.vector.app.core.extensions.toReducedUrl
 | 
			
		||||
import im.vector.app.databinding.FragmentLoginSigninToAny2Binding
 | 
			
		||||
import im.vector.app.features.login.LoginMode
 | 
			
		||||
@ -48,8 +48,6 @@ import javax.inject.Inject
 | 
			
		||||
 */
 | 
			
		||||
class LoginFragmentToAny2 @Inject constructor() : AbstractSSOLoginFragment2<FragmentLoginSigninToAny2Binding>() {
 | 
			
		||||
 | 
			
		||||
    private var passwordShown = false
 | 
			
		||||
 | 
			
		||||
    override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentLoginSigninToAny2Binding {
 | 
			
		||||
        return FragmentLoginSigninToAny2Binding.inflate(inflater, container, false)
 | 
			
		||||
    }
 | 
			
		||||
@ -59,7 +57,6 @@ class LoginFragmentToAny2 @Inject constructor() : AbstractSSOLoginFragment2<Frag
 | 
			
		||||
 | 
			
		||||
        setupSubmitButton()
 | 
			
		||||
        setupForgottenPasswordButton()
 | 
			
		||||
        setupPasswordReveal()
 | 
			
		||||
        setupAutoFill()
 | 
			
		||||
        setupSocialLoginButtons()
 | 
			
		||||
 | 
			
		||||
@ -159,23 +156,6 @@ class LoginFragmentToAny2 @Inject constructor() : AbstractSSOLoginFragment2<Frag
 | 
			
		||||
        loginViewModel.handle(LoginAction2.PostViewEvent(LoginViewEvents2.OpenResetPasswordScreen))
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private fun setupPasswordReveal() {
 | 
			
		||||
        passwordShown = false
 | 
			
		||||
 | 
			
		||||
        views.passwordReveal.setOnClickListener {
 | 
			
		||||
            passwordShown = !passwordShown
 | 
			
		||||
 | 
			
		||||
            renderPasswordField()
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        renderPasswordField()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private fun renderPasswordField() {
 | 
			
		||||
        views.passwordField.showPassword(passwordShown)
 | 
			
		||||
        views.passwordReveal.render(passwordShown)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun resetViewModel() {
 | 
			
		||||
        // loginViewModel.handle(LoginAction2.ResetSignin)
 | 
			
		||||
    }
 | 
			
		||||
@ -208,8 +188,7 @@ class LoginFragmentToAny2 @Inject constructor() : AbstractSSOLoginFragment2<Frag
 | 
			
		||||
 | 
			
		||||
        if (state.isLoading) {
 | 
			
		||||
            // Ensure password is hidden
 | 
			
		||||
            passwordShown = false
 | 
			
		||||
            renderPasswordField()
 | 
			
		||||
            views.passwordField.hidePassword()
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -28,13 +28,12 @@ import com.jakewharton.rxbinding3.widget.textChanges
 | 
			
		||||
import im.vector.app.R
 | 
			
		||||
import im.vector.app.core.extensions.hideKeyboard
 | 
			
		||||
import im.vector.app.core.extensions.isEmail
 | 
			
		||||
import im.vector.app.core.extensions.showPassword
 | 
			
		||||
import im.vector.app.core.extensions.hidePassword
 | 
			
		||||
import im.vector.app.core.extensions.toReducedUrl
 | 
			
		||||
import im.vector.app.core.utils.autoResetTextInputLayoutErrors
 | 
			
		||||
import im.vector.app.databinding.FragmentLoginResetPassword2Binding
 | 
			
		||||
import io.reactivex.Observable
 | 
			
		||||
import io.reactivex.rxkotlin.subscribeBy
 | 
			
		||||
 | 
			
		||||
import javax.inject.Inject
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
@ -42,8 +41,6 @@ import javax.inject.Inject
 | 
			
		||||
 */
 | 
			
		||||
class LoginResetPasswordFragment2 @Inject constructor() : AbstractLoginFragment2<FragmentLoginResetPassword2Binding>() {
 | 
			
		||||
 | 
			
		||||
    private var passwordShown = false
 | 
			
		||||
 | 
			
		||||
    // Show warning only once
 | 
			
		||||
    private var showWarning = true
 | 
			
		||||
 | 
			
		||||
@ -55,7 +52,6 @@ class LoginResetPasswordFragment2 @Inject constructor() : AbstractLoginFragment2
 | 
			
		||||
        super.onViewCreated(view, savedInstanceState)
 | 
			
		||||
 | 
			
		||||
        setupSubmitButton()
 | 
			
		||||
        setupPasswordReveal()
 | 
			
		||||
        setupAutoFill()
 | 
			
		||||
 | 
			
		||||
        autoResetTextInputLayoutErrors(listOf(views.resetPasswordEmailTil, views.passwordFieldTil))
 | 
			
		||||
@ -148,23 +144,6 @@ class LoginResetPasswordFragment2 @Inject constructor() : AbstractLoginFragment2
 | 
			
		||||
        views.passwordFieldTil.error = null
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private fun setupPasswordReveal() {
 | 
			
		||||
        passwordShown = false
 | 
			
		||||
 | 
			
		||||
        views.passwordReveal.setOnClickListener {
 | 
			
		||||
            passwordShown = !passwordShown
 | 
			
		||||
 | 
			
		||||
            renderPasswordField()
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        renderPasswordField()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private fun renderPasswordField() {
 | 
			
		||||
        views.passwordField.showPassword(passwordShown)
 | 
			
		||||
        views.passwordReveal.render(passwordShown)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun resetViewModel() {
 | 
			
		||||
        loginViewModel.handle(LoginAction2.ResetResetPassword)
 | 
			
		||||
    }
 | 
			
		||||
@ -178,8 +157,7 @@ class LoginResetPasswordFragment2 @Inject constructor() : AbstractLoginFragment2
 | 
			
		||||
 | 
			
		||||
        if (state.isLoading) {
 | 
			
		||||
            // Ensure new password is hidden
 | 
			
		||||
            passwordShown = false
 | 
			
		||||
            renderPasswordField()
 | 
			
		||||
            views.passwordField.hidePassword()
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -27,6 +27,7 @@ import im.vector.app.BuildConfig
 | 
			
		||||
import im.vector.app.R
 | 
			
		||||
import im.vector.app.core.resources.StringProvider
 | 
			
		||||
import im.vector.app.core.utils.FirstThrottler
 | 
			
		||||
import im.vector.app.features.invite.AutoAcceptInvites
 | 
			
		||||
import im.vector.app.features.settings.VectorPreferences
 | 
			
		||||
import me.gujun.android.span.span
 | 
			
		||||
import org.matrix.android.sdk.api.session.Session
 | 
			
		||||
@ -50,7 +51,8 @@ class NotificationDrawerManager @Inject constructor(private val context: Context
 | 
			
		||||
                                                    private val activeSessionDataSource: ActiveSessionDataSource,
 | 
			
		||||
                                                    private val iconLoader: IconLoader,
 | 
			
		||||
                                                    private val bitmapLoader: BitmapLoader,
 | 
			
		||||
                                                    private val outdatedDetector: OutdatedEventDetector?) {
 | 
			
		||||
                                                    private val outdatedDetector: OutdatedEventDetector?,
 | 
			
		||||
                                                    private val autoAcceptInvites: AutoAcceptInvites) {
 | 
			
		||||
 | 
			
		||||
    private val handlerThread: HandlerThread = HandlerThread("NotificationDrawerManager", Thread.MIN_PRIORITY)
 | 
			
		||||
    private var backgroundHandler: Handler
 | 
			
		||||
@ -253,7 +255,14 @@ class NotificationDrawerManager @Inject constructor(private val context: Context
 | 
			
		||||
                            roomEvents.add(event)
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
                    is InviteNotifiableEvent  -> invitationEvents.add(event)
 | 
			
		||||
                    is InviteNotifiableEvent  -> {
 | 
			
		||||
                        if (autoAcceptInvites.hideInvites) {
 | 
			
		||||
                            // Forget this event
 | 
			
		||||
                           eventIterator.remove()
 | 
			
		||||
                        } else {
 | 
			
		||||
                            invitationEvents.add(event)
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
                    is SimpleNotifiableEvent  -> simpleEvents.add(event)
 | 
			
		||||
                    else                      -> Timber.w("Type not handled")
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
@ -17,7 +17,6 @@
 | 
			
		||||
 | 
			
		||||
package im.vector.app.features.roomprofile
 | 
			
		||||
 | 
			
		||||
import android.content.DialogInterface
 | 
			
		||||
import android.os.Bundle
 | 
			
		||||
import android.os.Parcelable
 | 
			
		||||
import android.view.LayoutInflater
 | 
			
		||||
@ -33,7 +32,6 @@ import com.google.android.material.dialog.MaterialAlertDialogBuilder
 | 
			
		||||
import im.vector.app.R
 | 
			
		||||
import im.vector.app.core.animations.AppBarStateChangeListener
 | 
			
		||||
import im.vector.app.core.animations.MatrixItemAppBarStateChangeListener
 | 
			
		||||
import im.vector.app.core.dialogs.withColoredButton
 | 
			
		||||
import im.vector.app.core.extensions.cleanup
 | 
			
		||||
import im.vector.app.core.extensions.configureWith
 | 
			
		||||
import im.vector.app.core.extensions.copyOnLongClick
 | 
			
		||||
@ -268,7 +266,7 @@ class RoomProfileFragment @Inject constructor(
 | 
			
		||||
                append(getString(R.string.room_participants_leave_private_warning))
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        MaterialAlertDialogBuilder(requireContext())
 | 
			
		||||
        MaterialAlertDialogBuilder(requireContext(), if (isPublicRoom) 0 else R.style.ThemeOverlay_Vector_MaterialAlertDialog_Destructive)
 | 
			
		||||
                .setTitle(R.string.room_participants_leave_prompt_title)
 | 
			
		||||
                .setMessage(message)
 | 
			
		||||
                .setPositiveButton(R.string.leave) { _, _ ->
 | 
			
		||||
@ -276,11 +274,6 @@ class RoomProfileFragment @Inject constructor(
 | 
			
		||||
                }
 | 
			
		||||
                .setNegativeButton(R.string.cancel, null)
 | 
			
		||||
                .show()
 | 
			
		||||
                .apply {
 | 
			
		||||
                    if (!isPublicRoom) {
 | 
			
		||||
                        withColoredButton(DialogInterface.BUTTON_POSITIVE)
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun onRoomAliasesClicked() {
 | 
			
		||||
 | 
			
		||||
@ -16,7 +16,6 @@
 | 
			
		||||
 | 
			
		||||
package im.vector.app.features.roomprofile.alias
 | 
			
		||||
 | 
			
		||||
import android.content.DialogInterface
 | 
			
		||||
import android.os.Bundle
 | 
			
		||||
import android.view.LayoutInflater
 | 
			
		||||
import android.view.View
 | 
			
		||||
@ -27,7 +26,6 @@ import com.airbnb.mvrx.fragmentViewModel
 | 
			
		||||
import com.airbnb.mvrx.withState
 | 
			
		||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
 | 
			
		||||
import im.vector.app.R
 | 
			
		||||
import im.vector.app.core.dialogs.withColoredButton
 | 
			
		||||
import im.vector.app.core.extensions.cleanup
 | 
			
		||||
import im.vector.app.core.extensions.configureWith
 | 
			
		||||
import im.vector.app.core.extensions.exhaustive
 | 
			
		||||
@ -133,7 +131,7 @@ class RoomAliasFragment @Inject constructor(
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private fun unpublishAlias(alias: String) {
 | 
			
		||||
        MaterialAlertDialogBuilder(requireContext())
 | 
			
		||||
        MaterialAlertDialogBuilder(requireContext(), R.style.ThemeOverlay_Vector_MaterialAlertDialog_Destructive)
 | 
			
		||||
                .setTitle(R.string.dialog_title_confirmation)
 | 
			
		||||
                .setMessage(getString(R.string.room_alias_unpublish_confirmation, alias))
 | 
			
		||||
                .setNegativeButton(R.string.cancel, null)
 | 
			
		||||
@ -141,7 +139,6 @@ class RoomAliasFragment @Inject constructor(
 | 
			
		||||
                    viewModel.handle(RoomAliasAction.UnpublishAlias(alias))
 | 
			
		||||
                }
 | 
			
		||||
                .show()
 | 
			
		||||
                .withColoredButton(DialogInterface.BUTTON_POSITIVE)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun toggleManualPublishForm() {
 | 
			
		||||
@ -185,7 +182,7 @@ class RoomAliasFragment @Inject constructor(
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private fun removeLocalAlias(alias: String) {
 | 
			
		||||
        MaterialAlertDialogBuilder(requireContext())
 | 
			
		||||
        MaterialAlertDialogBuilder(requireContext(), R.style.ThemeOverlay_Vector_MaterialAlertDialog_Destructive)
 | 
			
		||||
                .setTitle(R.string.dialog_title_confirmation)
 | 
			
		||||
                .setMessage(getString(R.string.room_alias_delete_confirmation, alias))
 | 
			
		||||
                .setNegativeButton(R.string.cancel, null)
 | 
			
		||||
@ -193,6 +190,5 @@ class RoomAliasFragment @Inject constructor(
 | 
			
		||||
                    viewModel.handle(RoomAliasAction.RemoveLocalAlias(alias))
 | 
			
		||||
                }
 | 
			
		||||
                .show()
 | 
			
		||||
                .withColoredButton(DialogInterface.BUTTON_POSITIVE)
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -37,7 +37,7 @@ import com.google.android.material.dialog.MaterialAlertDialogBuilder
 | 
			
		||||
import im.vector.app.R
 | 
			
		||||
import im.vector.app.core.dialogs.GalleryOrCameraDialogHelper
 | 
			
		||||
import im.vector.app.core.extensions.hideKeyboard
 | 
			
		||||
import im.vector.app.core.extensions.showPassword
 | 
			
		||||
import im.vector.app.core.extensions.hidePassword
 | 
			
		||||
import im.vector.app.core.intent.getFilenameFromUri
 | 
			
		||||
import im.vector.app.core.platform.SimpleTextWatcher
 | 
			
		||||
import im.vector.app.core.preference.UserAvatarPreference
 | 
			
		||||
@ -66,7 +66,7 @@ import javax.inject.Inject
 | 
			
		||||
 | 
			
		||||
class VectorSettingsGeneralFragment @Inject constructor(
 | 
			
		||||
        colorProvider: ColorProvider
 | 
			
		||||
):
 | 
			
		||||
) :
 | 
			
		||||
        VectorSettingsBaseFragment(),
 | 
			
		||||
        GalleryOrCameraDialogHelper.Listener {
 | 
			
		||||
 | 
			
		||||
@ -347,18 +347,6 @@ class VectorSettingsGeneralFragment @Inject constructor(
 | 
			
		||||
            val view: ViewGroup = activity.layoutInflater.inflate(R.layout.dialog_change_password, null) as ViewGroup
 | 
			
		||||
            val views = DialogChangePasswordBinding.bind(view)
 | 
			
		||||
 | 
			
		||||
            var passwordShown = false
 | 
			
		||||
 | 
			
		||||
            views.changePasswordShowPasswords.setOnClickListener {
 | 
			
		||||
                passwordShown = !passwordShown
 | 
			
		||||
 | 
			
		||||
                views.changePasswordOldPwdText.showPassword(passwordShown)
 | 
			
		||||
                views.changePasswordNewPwdText.showPassword(passwordShown)
 | 
			
		||||
                views.changePasswordConfirmNewPwdText.showPassword(passwordShown)
 | 
			
		||||
 | 
			
		||||
                views.changePasswordShowPasswords.render(passwordShown)
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            val dialog = MaterialAlertDialogBuilder(activity)
 | 
			
		||||
                    .setView(view)
 | 
			
		||||
                    .setCancelable(false)
 | 
			
		||||
@ -377,13 +365,8 @@ class VectorSettingsGeneralFragment @Inject constructor(
 | 
			
		||||
                fun updateUi() {
 | 
			
		||||
                    val oldPwd = views.changePasswordOldPwdText.text.toString()
 | 
			
		||||
                    val newPwd = views.changePasswordNewPwdText.text.toString()
 | 
			
		||||
                    val newConfirmPwd = views.changePasswordConfirmNewPwdText.text.toString()
 | 
			
		||||
 | 
			
		||||
                    updateButton.isEnabled = oldPwd.isNotEmpty() && newPwd.isNotEmpty() && newPwd == newConfirmPwd
 | 
			
		||||
 | 
			
		||||
                    if (newPwd.isNotEmpty() && newConfirmPwd.isNotEmpty() && newPwd != newConfirmPwd) {
 | 
			
		||||
                        views.changePasswordConfirmNewPwdTil.error = getString(R.string.passwords_do_not_match)
 | 
			
		||||
                    }
 | 
			
		||||
                    updateButton.isEnabled = oldPwd.isNotEmpty() && newPwd.isNotEmpty()
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                views.changePasswordOldPwdText.addTextChangedListener(object : SimpleTextWatcher() {
 | 
			
		||||
@ -395,32 +378,20 @@ class VectorSettingsGeneralFragment @Inject constructor(
 | 
			
		||||
 | 
			
		||||
                views.changePasswordNewPwdText.addTextChangedListener(object : SimpleTextWatcher() {
 | 
			
		||||
                    override fun afterTextChanged(s: Editable) {
 | 
			
		||||
                        views.changePasswordConfirmNewPwdTil.error = null
 | 
			
		||||
                        updateUi()
 | 
			
		||||
                    }
 | 
			
		||||
                })
 | 
			
		||||
 | 
			
		||||
                views.changePasswordConfirmNewPwdText.addTextChangedListener(object : SimpleTextWatcher() {
 | 
			
		||||
                    override fun afterTextChanged(s: Editable) {
 | 
			
		||||
                        views.changePasswordConfirmNewPwdTil.error = null
 | 
			
		||||
                        updateUi()
 | 
			
		||||
                    }
 | 
			
		||||
                })
 | 
			
		||||
 | 
			
		||||
                fun showPasswordLoadingView(toShow: Boolean) {
 | 
			
		||||
                    if (toShow) {
 | 
			
		||||
                        views.changePasswordShowPasswords.isEnabled = false
 | 
			
		||||
                        views.changePasswordOldPwdText.isEnabled = false
 | 
			
		||||
                        views.changePasswordNewPwdText.isEnabled = false
 | 
			
		||||
                        views.changePasswordConfirmNewPwdText.isEnabled = false
 | 
			
		||||
                        views.changePasswordLoader.isVisible = true
 | 
			
		||||
                        updateButton.isEnabled = false
 | 
			
		||||
                        cancelButton.isEnabled = false
 | 
			
		||||
                    } else {
 | 
			
		||||
                        views.changePasswordShowPasswords.isEnabled = true
 | 
			
		||||
                        views.changePasswordOldPwdText.isEnabled = true
 | 
			
		||||
                        views.changePasswordNewPwdText.isEnabled = true
 | 
			
		||||
                        views.changePasswordConfirmNewPwdText.isEnabled = true
 | 
			
		||||
                        views.changePasswordLoader.isVisible = false
 | 
			
		||||
                        updateButton.isEnabled = true
 | 
			
		||||
                        cancelButton.isEnabled = true
 | 
			
		||||
@ -428,10 +399,9 @@ class VectorSettingsGeneralFragment @Inject constructor(
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                updateButton.setOnClickListener {
 | 
			
		||||
                    if (passwordShown) {
 | 
			
		||||
                        // Hide passwords during processing
 | 
			
		||||
                        views.changePasswordShowPasswords.performClick()
 | 
			
		||||
                    }
 | 
			
		||||
                    // Hide passwords during processing
 | 
			
		||||
                    views.changePasswordOldPwdText.hidePassword()
 | 
			
		||||
                    views.changePasswordNewPwdText.hidePassword()
 | 
			
		||||
 | 
			
		||||
                    view.hideKeyboard()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -37,7 +37,6 @@ import im.vector.app.core.di.ActiveSessionHolder
 | 
			
		||||
import im.vector.app.core.dialogs.ExportKeysDialog
 | 
			
		||||
import im.vector.app.core.extensions.queryExportKeys
 | 
			
		||||
import im.vector.app.core.extensions.registerStartForActivityResult
 | 
			
		||||
import im.vector.app.core.extensions.showPassword
 | 
			
		||||
import im.vector.app.core.intent.ExternalIntentData
 | 
			
		||||
import im.vector.app.core.intent.analyseIntent
 | 
			
		||||
import im.vector.app.core.intent.getFilenameFromUri
 | 
			
		||||
@ -451,14 +450,6 @@ class VectorSettingsSecurityPrivacyFragment @Inject constructor(
 | 
			
		||||
                    .setTitle(R.string.encryption_import_room_keys)
 | 
			
		||||
                    .setView(dialogLayout)
 | 
			
		||||
 | 
			
		||||
            var passwordVisible = false
 | 
			
		||||
 | 
			
		||||
            views.importDialogShowPassword.setOnClickListener {
 | 
			
		||||
                passwordVisible = !passwordVisible
 | 
			
		||||
                views.dialogE2eKeysPassphraseEditText.showPassword(passwordVisible)
 | 
			
		||||
                views.importDialogShowPassword.render(passwordVisible)
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            views.dialogE2eKeysPassphraseEditText.addTextChangedListener(object : SimpleTextWatcher() {
 | 
			
		||||
                override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) {
 | 
			
		||||
                    views.dialogE2eKeysImportButton.isEnabled = !views.dialogE2eKeysPassphraseEditText.text.isNullOrEmpty()
 | 
			
		||||
 | 
			
		||||
@ -19,7 +19,6 @@ package im.vector.app.features.settings.account.deactivation
 | 
			
		||||
import im.vector.app.core.platform.VectorViewModelAction
 | 
			
		||||
 | 
			
		||||
sealed class DeactivateAccountAction : VectorViewModelAction {
 | 
			
		||||
    object TogglePassword : DeactivateAccountAction()
 | 
			
		||||
    data class DeactivateAccount(val eraseAllData: Boolean) : DeactivateAccountAction()
 | 
			
		||||
 | 
			
		||||
    object SsoAuthDone: DeactivateAccountAction()
 | 
			
		||||
 | 
			
		||||
@ -41,7 +41,7 @@ import kotlin.coroutines.resume
 | 
			
		||||
import kotlin.coroutines.resumeWithException
 | 
			
		||||
 | 
			
		||||
data class DeactivateAccountViewState(
 | 
			
		||||
        val passwordShown: Boolean = false
 | 
			
		||||
        val dummy: Boolean = false
 | 
			
		||||
) : MvRxState
 | 
			
		||||
 | 
			
		||||
class DeactivateAccountViewModel @AssistedInject constructor(@Assisted private val initialState: DeactivateAccountViewState,
 | 
			
		||||
@ -58,7 +58,6 @@ class DeactivateAccountViewModel @AssistedInject constructor(@Assisted private v
 | 
			
		||||
 | 
			
		||||
    override fun handle(action: DeactivateAccountAction) {
 | 
			
		||||
        when (action) {
 | 
			
		||||
            DeactivateAccountAction.TogglePassword -> handleTogglePassword()
 | 
			
		||||
            is DeactivateAccountAction.DeactivateAccount -> handleDeactivateAccount(action)
 | 
			
		||||
            DeactivateAccountAction.SsoAuthDone -> {
 | 
			
		||||
                Timber.d("## UIA - FallBack success")
 | 
			
		||||
@ -87,12 +86,6 @@ class DeactivateAccountViewModel @AssistedInject constructor(@Assisted private v
 | 
			
		||||
        }.exhaustive
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private fun handleTogglePassword() = withState {
 | 
			
		||||
        setState {
 | 
			
		||||
            copy(passwordShown = !passwordShown)
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private fun handleDeactivateAccount(action: DeactivateAccountAction.DeactivateAccount) {
 | 
			
		||||
        _viewEvents.post(DeactivateAccountViewEvents.Loading())
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -16,7 +16,6 @@
 | 
			
		||||
 | 
			
		||||
package im.vector.app.features.settings.devtools
 | 
			
		||||
 | 
			
		||||
import android.content.DialogInterface
 | 
			
		||||
import android.os.Bundle
 | 
			
		||||
import android.view.LayoutInflater
 | 
			
		||||
import android.view.View
 | 
			
		||||
@ -26,7 +25,6 @@ import com.airbnb.mvrx.fragmentViewModel
 | 
			
		||||
import com.airbnb.mvrx.withState
 | 
			
		||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
 | 
			
		||||
import im.vector.app.R
 | 
			
		||||
import im.vector.app.core.dialogs.withColoredButton
 | 
			
		||||
import im.vector.app.core.extensions.cleanup
 | 
			
		||||
import im.vector.app.core.extensions.configureWith
 | 
			
		||||
import im.vector.app.core.platform.VectorBaseFragment
 | 
			
		||||
@ -85,7 +83,7 @@ class AccountDataFragment @Inject constructor(
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun didLongTap(data: UserAccountDataEvent) {
 | 
			
		||||
        MaterialAlertDialogBuilder(requireActivity())
 | 
			
		||||
        MaterialAlertDialogBuilder(requireActivity(), R.style.ThemeOverlay_Vector_MaterialAlertDialog_Destructive)
 | 
			
		||||
                .setTitle(R.string.delete)
 | 
			
		||||
                .setMessage(getString(R.string.delete_account_data_warning, data.type))
 | 
			
		||||
                .setNegativeButton(R.string.cancel, null)
 | 
			
		||||
@ -93,6 +91,5 @@ class AccountDataFragment @Inject constructor(
 | 
			
		||||
                    viewModel.handle(AccountDataAction.DeleteAccountData(data.type))
 | 
			
		||||
                }
 | 
			
		||||
                .show()
 | 
			
		||||
                .withColoredButton(DialogInterface.BUTTON_POSITIVE)
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
Some files were not shown because too many files have changed in this diff Show More
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user