Merge branch 'develop' into feature/bca/room_upgrade

This commit is contained in:
Benoit Marty 2021-06-30 17:56:32 +02:00 committed by GitHub
commit 3e53fa710a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
143 changed files with 1478 additions and 1622 deletions

View file

@ -63,7 +63,7 @@ Supported filename extensions are:
- ``.bugfix``: Signifying a bug fix. - ``.bugfix``: Signifying a bug fix.
- ``.doc``: Signifying a documentation improvement. - ``.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 - ``.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. See https://github.com/twisted/towncrier#news-fragments if you need more details.

View file

@ -2,7 +2,7 @@
buildscript { buildscript {
// Ref: https://kotlinlang.org/releases.html // 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" ext.kotlin_coroutines_version = "1.5.0"
repositories { repositories {
google() google()

1
changelog.d/3545.feature Normal file
View 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
View file

@ -0,0 +1 @@
Implements new design for Jump to unread and quick fix visibility issues.

1
changelog.d/3564.bugfix Normal file
View file

@ -0,0 +1 @@
Fix call invite processed after call is ended because of fastlane mode.

1
changelog.d/3577.bugfix Normal file
View file

@ -0,0 +1 @@
Fix crash after video call.

Binary file not shown.

View file

@ -1,6 +1,6 @@
distributionBase=GRADLE_USER_HOME distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists distributionPath=wrapper/dists
distributionSha256Sum=13bf8d3cf8eeeb5770d19741a59bde9bd966dd78d17f1bbad787a05ef19d1c2d distributionSha256Sum=a9e356a21595348b6f04b024ed0b08ac8aea6b2ac37e6c0ef58e51549cd7b9cb
distributionUrl=https\://services.gradle.org/distributions/gradle-7.0.2-all.zip distributionUrl=https\://services.gradle.org/distributions/gradle-7.1-all.zip
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists zipStorePath=wrapper/dists

2
gradlew vendored
View file

@ -72,7 +72,7 @@ case "`uname`" in
Darwin* ) Darwin* )
darwin=true darwin=true
;; ;;
MINGW* ) MSYS* | MINGW* )
msys=true msys=true
;; ;;
NONSTOP* ) NONSTOP* )

View file

@ -24,6 +24,7 @@ import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
import im.vector.lib.ui.styles.R import im.vector.lib.ui.styles.R
import im.vector.lib.ui.styles.databinding.ActivityDebugMaterialThemeBinding import im.vector.lib.ui.styles.databinding.ActivityDebugMaterialThemeBinding
import im.vector.lib.ui.styles.dialogs.MaterialProgressDialog
// Rendering is not the same with VectorBaseActivity // Rendering is not the same with VectorBaseActivity
abstract class DebugMaterialThemeActivity : AppCompatActivity() { abstract class DebugMaterialThemeActivity : AppCompatActivity() {
@ -50,14 +51,20 @@ abstract class DebugMaterialThemeActivity : AppCompatActivity() {
} }
views.debugShowDialog.setOnClickListener { views.debugShowDialog.setOnClickListener {
MaterialAlertDialogBuilder(this) showTestDialog(0)
.setTitle("Dialog title") }
.setMessage("Dialog content")
.setIcon(R.drawable.ic_debug_icon) views.debugShowDialogDestructive.setOnClickListener {
.setPositiveButton("Positive", null) showTestDialog(R.style.ThemeOverlay_Vector_MaterialAlertDialog_Destructive)
.setNegativeButton("Negative", null) }
.setNeutralButton("Neutral", null)
.show() 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 { 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 { override fun onCreateOptionsMenu(menu: Menu): Boolean {
menuInflater.inflate(R.menu.menu_debug, menu) menuInflater.inflate(R.menu.menu_debug, menu)
return true return true

View file

@ -17,6 +17,8 @@
package im.vector.lib.ui.styles.debug package im.vector.lib.ui.styles.debug
import android.os.Bundle import android.os.Bundle
import android.text.InputType
import android.widget.EditText
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import im.vector.lib.ui.styles.databinding.ActivityDebugTextViewBinding import im.vector.lib.ui.styles.databinding.ActivityDebugTextViewBinding
@ -27,5 +29,20 @@ abstract class DebugVectorTextViewActivity : AppCompatActivity() {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
val views = ActivityDebugTextViewBinding.inflate(layoutInflater) val views = ActivityDebugTextViewBinding.inflate(layoutInflater)
setContentView(views.root) 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
}
} }
} }

View file

@ -452,6 +452,27 @@
android:layout_gravity="center_horizontal" android:layout_gravity="center_horizontal"
android:text="Show Dialog" /> 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 <Button
android:id="@+id/debugShowBottomSheet" android:id="@+id/debugShowBottomSheet"
android:layout_width="wrap_content" android:layout_width="wrap_content"

View file

@ -1,9 +1,11 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" <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" xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:orientation="vertical" android:orientation="vertical"
android:padding="16dp"
tools:ignore="HardcodedText"> tools:ignore="HardcodedText">
<TextView <TextView
@ -65,4 +67,34 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:text="Default (TextAppearance.Vector.Body)\nline 2" /> 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> </LinearLayout>

View 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()
}
}

View file

@ -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>

View 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>

View file

@ -1,28 +1,40 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources> <resources>
<style name="AlertDialog.Vector.Light" parent="ThemeOverlay.MaterialComponents.MaterialAlertDialog"> <style name="ThemeOverlay.Vector.MaterialAlertDialog" parent="ThemeOverlay.MaterialComponents.MaterialAlertDialog">
<item name="colorPrimary">@color/palette_element_green</item> <item name="materialAlertDialogTitleTextStyle">@style/MaterialAlertDialog.Vector.Title.Text</item>
<item name="colorSecondary">@color/palette_element_green</item> <item name="materialAlertDialogBodyTextStyle">@style/MaterialAlertDialog.Vector.Body.Text</item>
<item name="colorSurface">@color/element_system_light</item> <item name="buttonBarPositiveButtonStyle">@style/Widget.Vector.Button.TextButton.Dialog</item>
<item name="colorOnSurface">@color/element_content_primary_light</item> <item name="buttonBarNegativeButtonStyle">@style/Widget.Vector.Button.TextButton.Dialog</item>
<item name="colorError">@color/element_alert_light</item> <item name="buttonBarNeutralButtonStyle">@style/Widget.Vector.Button.TextButton.Dialog</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> </style>
<style name="AlertDialog.Vector.Dark" parent="ThemeOverlay.MaterialComponents.MaterialAlertDialog"> <style name="ThemeOverlay.Vector.MaterialAlertDialog.Destructive">
<item name="colorPrimary">@color/palette_element_green</item> <item name="buttonBarPositiveButtonStyle">@style/Widget.Vector.Button.TextButton.Dialog.Destructive</item>
<item name="colorSecondary">@color/palette_element_green</item> </style>
<item name="colorSurface">@color/element_system_dark</item>
<item name="colorOnSurface">@color/element_content_primary_dark</item> <style name="ThemeOverlay.Vector.MaterialAlertDialog.NegativeDestructive">
<item name="colorError">@color/element_alert_dark</item> <item name="buttonBarNegativeButtonStyle">@style/Widget.Vector.Button.TextButton.Dialog.Destructive</item>
<!--item name="alertDialogStyle">@style/MaterialAlertDialog.App</item> </style>
<item name="materialAlertDialogTitleTextStyle">@style/MaterialAlertDialog.App.Title.Text</item>
<item name="buttonBarPositiveButtonStyle">@style/Widget.App.Button</item> <!-- Title -->
<item name="buttonBarNeutralButtonStyle">@style/Widget.App.Button</item--> <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> </style>
</resources> </resources>

View file

@ -6,6 +6,7 @@
<item name="android:paddingRight">16dp</item> <item name="android:paddingRight">16dp</item>
<item name="android:minWidth">94dp</item> <item name="android:minWidth">94dp</item>
<item name="android:textAppearance">@style/TextAppearance.Vector.Button</item> <item name="android:textAppearance">@style/TextAppearance.Vector.Button</item>
<item name="cornerRadius">8dp</item>
<item name="lineHeight">24sp</item> <item name="lineHeight">24sp</item>
</style> </style>
@ -32,6 +33,7 @@
<item name="android:paddingRight">16dp</item> <item name="android:paddingRight">16dp</item>
<item name="android:minWidth">94dp</item> <item name="android:minWidth">94dp</item>
<item name="android:textAppearance">@style/TextAppearance.Vector.Button</item> <item name="android:textAppearance">@style/TextAppearance.Vector.Button</item>
<item name="cornerRadius">8dp</item>
<item name="lineHeight">24sp</item> <item name="lineHeight">24sp</item>
</style> </style>
@ -48,6 +50,7 @@
<item name="colorControlHighlight">?colorSecondary</item> <item name="colorControlHighlight">?colorSecondary</item>
<item name="materialThemeOverlay">@style/VectorMaterialThemeOverlayPositive</item> <item name="materialThemeOverlay">@style/VectorMaterialThemeOverlayPositive</item>
<item name="android:textAppearance">@style/TextAppearance.Vector.Button</item> <item name="android:textAppearance">@style/TextAppearance.Vector.Button</item>
<item name="cornerRadius">8dp</item>
<item name="lineHeight">24sp</item> <item name="lineHeight">24sp</item>
</style> </style>
@ -61,6 +64,7 @@
<item name="strokeColor">@color/button_background_tint_selector</item> <item name="strokeColor">@color/button_background_tint_selector</item>
<item name="strokeWidth">1dp</item> <item name="strokeWidth">1dp</item>
<item name="android:textAppearance">@style/TextAppearance.Vector.Button</item> <item name="android:textAppearance">@style/TextAppearance.Vector.Button</item>
<item name="cornerRadius">8dp</item>
<item name="lineHeight">24sp</item> <item name="lineHeight">24sp</item>
</style> </style>

View file

@ -4,6 +4,11 @@
<!-- Default style for TextInputLayout --> <!-- Default style for TextInputLayout -->
<style name="Widget.Vector.TextInputLayout" parent="Widget.MaterialComponents.TextInputLayout.OutlinedBox" /> <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"> <style name="Widget.Vector.EditText.Composer" parent="Widget.AppCompat.EditText">
<item name="android:background">@android:color/transparent</item> <item name="android:background">@android:color/transparent</item>
<item name="android:inputType">textCapSentences|textMultiLine</item> <item name="android:inputType">textCapSentences|textMultiLine</item>

View file

@ -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>

View file

@ -14,7 +14,7 @@
<style name="TextAppearance.Vector.Title" parent="TextAppearance.MaterialComponents.Headline3"> <style name="TextAppearance.Vector.Title" parent="TextAppearance.MaterialComponents.Headline3">
<item name="fontFamily">sans-serif</item> <item name="fontFamily">sans-serif</item>
<item name="android: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:letterSpacing">0</item>
<item name="android:textColor">?vctr_content_primary</item> <item name="android:textColor">?vctr_content_primary</item>
</style> </style>
@ -27,7 +27,7 @@
<style name="TextAppearance.Vector.Headline.Medium" parent="TextAppearance.MaterialComponents.Headline1"> <style name="TextAppearance.Vector.Headline.Medium" parent="TextAppearance.MaterialComponents.Headline1">
<item name="fontFamily">sans-serif-medium</item> <item name="fontFamily">sans-serif-medium</item>
<item name="android: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:letterSpacing">0</item>
<item name="android:textColor">?vctr_content_primary</item> <item name="android:textColor">?vctr_content_primary</item>
</style> </style>
@ -35,7 +35,7 @@
<style name="TextAppearance.Vector.Subtitle" parent="TextAppearance.MaterialComponents.Subtitle1"> <style name="TextAppearance.Vector.Subtitle" parent="TextAppearance.MaterialComponents.Subtitle1">
<item name="fontFamily">sans-serif</item> <item name="fontFamily">sans-serif</item>
<item name="android: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:letterSpacing">0</item>
<item name="android:textColor">?vctr_content_secondary</item> <item name="android:textColor">?vctr_content_secondary</item>
</style> </style>
@ -49,7 +49,7 @@
<style name="TextAppearance.Vector.Body" parent="TextAppearance.MaterialComponents.Body1"> <style name="TextAppearance.Vector.Body" parent="TextAppearance.MaterialComponents.Body1">
<item name="fontFamily">sans-serif</item> <item name="fontFamily">sans-serif</item>
<item name="android: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:letterSpacing">0</item>
<item name="android:textColor">?vctr_content_primary</item> <item name="android:textColor">?vctr_content_primary</item>
</style> </style>
@ -62,7 +62,7 @@
<style name="TextAppearance.Vector.Caption" parent="TextAppearance.MaterialComponents.Caption"> <style name="TextAppearance.Vector.Caption" parent="TextAppearance.MaterialComponents.Caption">
<item name="fontFamily">sans-serif</item> <item name="fontFamily">sans-serif</item>
<item name="android: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:letterSpacing">0</item>
<item name="android:textColor">?vctr_content_secondary</item> <item name="android:textColor">?vctr_content_secondary</item>
</style> </style>
@ -70,14 +70,14 @@
<style name="TextAppearance.Vector.Micro" parent="TextAppearance.MaterialComponents.Caption"> <style name="TextAppearance.Vector.Micro" parent="TextAppearance.MaterialComponents.Caption">
<item name="fontFamily">sans-serif</item> <item name="fontFamily">sans-serif</item>
<item name="android: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> <item name="android:letterSpacing">0</item>
</style> </style>
<style name="TextAppearance.Vector.Button" parent="TextAppearance.MaterialComponents.Button"> <style name="TextAppearance.Vector.Button" parent="TextAppearance.MaterialComponents.Button">
<item name="fontFamily">sans-serif-medium</item> <item name="fontFamily">sans-serif-medium</item>
<item name="android: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> <item name="android:letterSpacing">0.02</item>
</style> </style>

View file

@ -76,7 +76,6 @@
<item name="android:textViewStyle">@style/Widget.Vector.TextView.Body</item> <item name="android:textViewStyle">@style/Widget.Vector.TextView.Body</item>
<item name="materialButtonStyle">@style/Widget.Vector.Button</item> <item name="materialButtonStyle">@style/Widget.Vector.Button</item>
<item name="toolbarStyle">@style/Widget.Vector.Toolbar</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="bottomNavigationStyle">@style/BottomNavigation.Vector</item>
<item name="searchViewStyle">@style/Widget.Vector.SearchView</item> <item name="searchViewStyle">@style/Widget.Vector.SearchView</item>
<item name="textInputStyle">@style/Widget.Vector.TextInputLayout</item> <item name="textInputStyle">@style/Widget.Vector.TextInputLayout</item>
@ -89,6 +88,7 @@
<!-- Default theme --> <!-- Default theme -->
<item name="bottomSheetDialogTheme">@style/Theme.Vector.BottomSheetDialog.Dark</item> <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> <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_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_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_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>
<style name="Theme.Vector.Dark" parent="Base.Theme.Vector.Dark" /> <style name="Theme.Vector.Dark" parent="Base.Theme.Vector.Dark" />

View file

@ -76,7 +76,6 @@
<item name="android:textViewStyle">@style/Widget.Vector.TextView.Body</item> <item name="android:textViewStyle">@style/Widget.Vector.TextView.Body</item>
<item name="materialButtonStyle">@style/Widget.Vector.Button</item> <item name="materialButtonStyle">@style/Widget.Vector.Button</item>
<item name="toolbarStyle">@style/Widget.Vector.Toolbar</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="bottomNavigationStyle">@style/BottomNavigation.Vector</item>
<item name="searchViewStyle">@style/Widget.Vector.SearchView</item> <item name="searchViewStyle">@style/Widget.Vector.SearchView</item>
<item name="textInputStyle">@style/Widget.Vector.TextInputLayout</item> <item name="textInputStyle">@style/Widget.Vector.TextInputLayout</item>
@ -89,6 +88,7 @@
<!-- Default theme --> <!-- Default theme -->
<item name="bottomSheetDialogTheme">@style/Theme.Vector.BottomSheetDialog.Light</item> <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> <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_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_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_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>
<style name="Theme.Vector.Light" parent="Base.Theme.Vector.Light" /> <style name="Theme.Vector.Light" parent="Base.Theme.Vector.Light" />

View file

@ -9,7 +9,7 @@ buildscript {
mavenCentral() mavenCentral()
} }
dependencies { dependencies {
classpath "io.realm:realm-gradle-plugin:10.5.0" classpath "io.realm:realm-gradle-plugin:10.6.0"
} }
} }
@ -169,14 +169,14 @@ dependencies {
implementation 'com.otaliastudios:transcoder:0.10.3' implementation 'com.otaliastudios:transcoder:0.10.3'
// Phone number https://github.com/google/libphonenumber // 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 'junit:junit:4.13.2'
testImplementation 'org.robolectric:robolectric:4.5.1' testImplementation 'org.robolectric:robolectric:4.5.1'
//testImplementation 'org.robolectric:shadows-support-v4:3.0' //testImplementation 'org.robolectric:shadows-support-v4:3.0'
// Note: version sticks to 1.9.2 due to https://github.com/mockk/mockk/issues/281 // Note: version sticks to 1.9.2 due to https://github.com/mockk/mockk/issues/281
testImplementation 'io.mockk:mockk:1.11.0' 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" testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$kotlin_coroutines_version"
// Plant Timber tree for test // Plant Timber tree for test
testImplementation 'net.lachlanmckee:timber-junit-rule:1.0.1' testImplementation 'net.lachlanmckee:timber-junit-rule:1.0.1'

View file

@ -125,6 +125,12 @@ interface RoomService {
*/ */
suspend fun deleteRoomAlias(roomAlias: String) 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. * 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. * It allows you to track this in your client to known what is currently being processed by the SDK.

View file

@ -59,9 +59,8 @@ internal class CallEventProcessor @Inject constructor(private val callSignalingH
return eventType == EventType.CALL_INVITE return eventType == EventType.CALL_INVITE
} }
suspend fun processFastLane(event: Event) { fun processFastLane(event: Event) {
eventsToPostProcess.add(event) dispatchToCallSignalingHandlerIfNeeded(event)
onPostProcess()
} }
override suspend fun onPostProcess() { override suspend fun onPostProcess() {
@ -73,13 +72,12 @@ internal class CallEventProcessor @Inject constructor(private val callSignalingH
private fun dispatchToCallSignalingHandlerIfNeeded(event: Event) { private fun dispatchToCallSignalingHandlerIfNeeded(event: Event) {
val now = System.currentTimeMillis() 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 { event.roomId ?: return Unit.also {
Timber.w("Event with no room id ${event.eventId}") Timber.w("Event with no room id ${event.eventId}")
} }
val age = now - (event.ageLocalTs ?: now) val age = now - (event.ageLocalTs ?: now)
if (age > 40_000) { if (age > 40_000) {
// To old to ring? // Too old to ring?
return return
} }
callSignalingHandler.onCallEvent(event) callSignalingHandler.onCallEvent(event)

View file

@ -41,6 +41,7 @@ internal class CallSignalingHandler @Inject constructor(private val activeCallHa
private val mxCallFactory: MxCallFactory, private val mxCallFactory: MxCallFactory,
@UserId private val userId: String) { @UserId private val userId: String) {
private val invitedCallIds = mutableSetOf<String>()
private val callListeners = mutableSetOf<CallListener>() private val callListeners = mutableSetOf<CallListener>()
private val callListenersDispatcher = CallListenersDispatcher(callListeners) private val callListenersDispatcher = CallListenersDispatcher(callListeners)
@ -182,17 +183,17 @@ internal class CallSignalingHandler @Inject constructor(private val activeCallHa
val content = event.getClearContent().toModel<CallInviteContent>() ?: return val content = event.getClearContent().toModel<CallInviteContent>() ?: return
content.callId ?: 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 // Call is already known, maybe due to fast lane. Ignore
Timber.d("Ignoring already known call invite") Timber.d("Ignoring already known call invite")
return return
} }
val incomingCall = mxCallFactory.createIncomingCall( val incomingCall = mxCallFactory.createIncomingCall(
roomId = event.roomId, roomId = event.roomId,
opponentUserId = event.senderId, opponentUserId = event.senderId,
content = content content = content
) ?: return ) ?: return
invitedCallIds.add(content.callId)
activeCallHandler.addCall(incomingCall) activeCallHandler.addCall(incomingCall)
callListenersDispatcher.onCallInviteReceived(incomingCall, content) callListenersDispatcher.onCallInviteReceived(incomingCall, content)
} }

View file

@ -29,7 +29,7 @@ internal class DefaultEventService @Inject constructor(
override suspend fun getEvent(roomId: String, eventId: String): Event { override suspend fun getEvent(roomId: String, eventId: String): Event {
val event = getEventTask.execute(GetEventTask.Params(roomId, eventId)) 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 // Fast lane to the call event processors: try to make the incoming call ring faster
if (callEventProcessor.shouldProcessFastLane(event.getClearType())) { if (callEventProcessor.shouldProcessFastLane(event.getClearType())) {
callEventProcessor.processFastLane(event) callEventProcessor.processFastLane(event)

View file

@ -134,6 +134,10 @@ internal class DefaultRoomService @Inject constructor(
deleteRoomAliasTask.execute(DeleteRoomAliasTask.Params(roomAlias)) deleteRoomAliasTask.execute(DeleteRoomAliasTask.Params(roomAlias))
} }
override fun getChangeMemberships(roomIdOrAlias: String): ChangeMembershipState {
return roomChangeMembershipStateDataSource.getState(roomIdOrAlias)
}
override fun getChangeMembershipsLive(): LiveData<Map<String, ChangeMembershipState>> { override fun getChangeMembershipsLive(): LiveData<Map<String, ChangeMembershipState>> {
return roomChangeMembershipStateDataSource.getLiveStates() return roomChangeMembershipStateDataSource.getLiveStates()
} }

View file

@ -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.members.ChangeMembershipState
import org.matrix.android.sdk.api.session.room.model.Membership import org.matrix.android.sdk.api.session.room.model.Membership
import org.matrix.android.sdk.internal.session.SessionScope import org.matrix.android.sdk.internal.session.SessionScope
import java.util.concurrent.ConcurrentHashMap
import javax.inject.Inject import javax.inject.Inject
/** /**
@ -30,7 +31,7 @@ import javax.inject.Inject
internal class RoomChangeMembershipStateDataSource @Inject constructor() { internal class RoomChangeMembershipStateDataSource @Inject constructor() {
private val mutableLiveStates = MutableLiveData<Map<String, ChangeMembershipState>>(emptyMap()) 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. * This will update local states to be synced with the server.

View file

@ -54,6 +54,10 @@ internal class DefaultJoinRoomTask @Inject constructor(
) : JoinRoomTask { ) : JoinRoomTask {
override suspend fun execute(params: JoinRoomTask.Params) { 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) roomChangeMembershipStateDataSource.updateState(params.roomIdOrAlias, ChangeMembershipState.Joining)
val joinRoomResponse = try { val joinRoomResponse = try {
executeRequest(globalErrorReceiver) { executeRequest(globalErrorReceiver) {

View file

@ -46,7 +46,7 @@ internal object WorkerParamsFactory {
inline fun <reified T> fromData(data: Data) = fromData(T::class.java, data) 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) val json = data.getString(KEY)
return if (json == null) { return if (json == null) {
null null

View file

@ -43,7 +43,7 @@ android {
dependencies { dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
implementation 'androidx.appcompat:appcompat:1.3.0' 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' implementation 'androidx.exifinterface:exifinterface:1.3.2'
// Log // Log

1
newsfragment/3207.bugfix Normal file
View file

@ -0,0 +1 @@
Space Explore Rooms no feedback on failed to join

1
newsfragment/3520.misc Normal file
View file

@ -0,0 +1 @@
VoIP: Merge virtual room timeline in corresponding native room (call events only).

View file

@ -0,0 +1 @@
Introduces AutoAcceptInvites which can be enabled at compile time.

View file

@ -300,7 +300,7 @@ android {
dependencies { dependencies {
def epoxy_version = '4.6.2' 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 arrow_version = "0.8.2"
def markwon_version = '4.1.2' def markwon_version = '4.1.2'
def big_image_viewer_version = '1.8.0' def big_image_viewer_version = '1.8.0'
@ -315,7 +315,7 @@ dependencies {
def jjwt_version = '0.11.2' def jjwt_version = '0.11.2'
// Tests // Tests
def kluent_version = '1.65' def kluent_version = '1.67'
def androidxTest_version = '1.3.0' def androidxTest_version = '1.3.0'
def espresso_version = '3.3.0' def espresso_version = '3.3.0'
@ -355,7 +355,7 @@ dependencies {
implementation 'com.facebook.stetho:stetho:1.6.0' implementation 'com.facebook.stetho:stetho:1.6.0'
// Phone number https://github.com/google/libphonenumber // Phone number https://github.com/google/libphonenumber
implementation 'com.googlecode.libphonenumber:libphonenumber:8.12.24' implementation 'com.googlecode.libphonenumber:libphonenumber:8.12.26'
// rx // rx
implementation 'io.reactivex.rxjava2:rxkotlin:2.4.0' implementation 'io.reactivex.rxjava2:rxkotlin:2.4.0'

View file

@ -114,6 +114,12 @@
android:text="Vector" /> android:text="Vector" />
</LinearLayout> </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 <Button
android:id="@+id/debug_test_text_view_dark" android:id="@+id/debug_test_text_view_dark"
android:layout_width="wrap_content" android:layout_width="wrap_content"
@ -122,12 +128,6 @@
android:layout_weight="1" android:layout_weight="1"
android:text="Text Views Dark" /> 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 <Button
android:id="@+id/debug_show_sas_emoji" android:id="@+id/debug_show_sas_emoji"
android:layout_width="wrap_content" android:layout_width="wrap_content"

View file

@ -49,13 +49,12 @@ fun RoomGroupingMethod.group() = (this as? RoomGroupingMethod.ByLegacyGroup)?.gr
// TODO Keep this class for now, will maybe be used fro Space // TODO Keep this class for now, will maybe be used fro Space
@Singleton @Singleton
class AppStateHandler @Inject constructor( class AppStateHandler @Inject constructor(
sessionDataSource: ActiveSessionDataSource, private val sessionDataSource: ActiveSessionDataSource,
private val uiStateRepository: UiStateRepository, private val uiStateRepository: UiStateRepository,
private val activeSessionHolder: ActiveSessionHolder private val activeSessionHolder: ActiveSessionHolder
) : LifecycleObserver { ) : LifecycleObserver {
private val compositeDisposable = CompositeDisposable() private val compositeDisposable = CompositeDisposable()
private val selectedSpaceDataSource = BehaviorDataSource<Option<RoomGroupingMethod>>(Option.empty()) private val selectedSpaceDataSource = BehaviorDataSource<Option<RoomGroupingMethod>>(Option.empty())
val selectedRoomGroupingObservable = selectedSpaceDataSource.observe() val selectedRoomGroupingObservable = selectedSpaceDataSource.observe()
@ -92,11 +91,11 @@ class AppStateHandler @Inject constructor(
} }
} }
init { private fun observeActiveSession() {
sessionDataSource.observe() sessionDataSource.observe()
.distinctUntilChanged() .distinctUntilChanged()
.subscribe { .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 -> it.orNull()?.let { session ->
if (uiStateRepository.isGroupingMethodSpace(session.sessionId)) { if (uiStateRepository.isGroupingMethodSpace(session.sessionId)) {
setCurrentSpace(uiStateRepository.getSelectedSpace(session.sessionId), session) setCurrentSpace(uiStateRepository.getSelectedSpace(session.sessionId), session)
@ -119,6 +118,7 @@ class AppStateHandler @Inject constructor(
@OnLifecycleEvent(Lifecycle.Event.ON_RESUME) @OnLifecycleEvent(Lifecycle.Event.ON_RESUME)
fun entersForeground() { fun entersForeground() {
observeActiveSession()
} }
@OnLifecycleEvent(Lifecycle.Event.ON_PAUSE) @OnLifecycleEvent(Lifecycle.Event.ON_PAUSE)

View file

@ -47,6 +47,7 @@ import im.vector.app.core.rx.RxConfig
import im.vector.app.features.call.webrtc.WebRtcCallManager import im.vector.app.features.call.webrtc.WebRtcCallManager
import im.vector.app.features.configuration.VectorConfiguration import im.vector.app.features.configuration.VectorConfiguration
import im.vector.app.features.disclaimer.doNotShowDisclaimerDialog 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.lifecycle.VectorActivityLifecycleCallbacks
import im.vector.app.features.notifications.NotificationDrawerManager import im.vector.app.features.notifications.NotificationDrawerManager
import im.vector.app.features.notifications.NotificationUtils import im.vector.app.features.notifications.NotificationUtils
@ -95,6 +96,7 @@ class VectorApplication :
@Inject lateinit var popupAlertManager: PopupAlertManager @Inject lateinit var popupAlertManager: PopupAlertManager
@Inject lateinit var pinLocker: PinLocker @Inject lateinit var pinLocker: PinLocker
@Inject lateinit var callManager: WebRtcCallManager @Inject lateinit var callManager: WebRtcCallManager
@Inject lateinit var invitesAcceptor: InvitesAcceptor
lateinit var vectorComponent: VectorComponent lateinit var vectorComponent: VectorComponent
@ -116,6 +118,7 @@ class VectorApplication :
appContext = this appContext = this
vectorComponent = DaggerVectorComponent.factory().create(this) vectorComponent = DaggerVectorComponent.factory().create(this)
vectorComponent.inject(this) vectorComponent.inject(this)
invitesAcceptor.initialize()
vectorUncaughtExceptionHandler.activate(this) vectorUncaughtExceptionHandler.activate(this)
rxConfig.setupRxPlugin() rxConfig.setupRxPlugin()

View file

@ -52,6 +52,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.filtered.FilteredRoomsActivity
import im.vector.app.features.home.room.list.RoomListModule import im.vector.app.features.home.room.list.RoomListModule
import im.vector.app.features.home.room.list.actions.RoomListQuickActionsBottomSheet 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.InviteUsersToRoomActivity
import im.vector.app.features.invite.VectorInviteView import im.vector.app.features.invite.VectorInviteView
import im.vector.app.features.link.LinkHandlerActivity import im.vector.app.features.link.LinkHandlerActivity
@ -124,6 +125,7 @@ interface ScreenComponent {
fun errorFormatter(): ErrorFormatter fun errorFormatter(): ErrorFormatter
fun uiStateRepository(): UiStateRepository fun uiStateRepository(): UiStateRepository
fun unrecognizedCertificateDialog(): UnrecognizedCertificateDialog fun unrecognizedCertificateDialog(): UnrecognizedCertificateDialog
fun autoAcceptInvites(): AutoAcceptInvites
/* ========================================================================================== /* ==========================================================================================
* Activities * Activities

View file

@ -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.home.room.detail.timeline.helper.RoomSummariesHolder
import im.vector.app.features.html.EventHtmlRenderer import im.vector.app.features.html.EventHtmlRenderer
import im.vector.app.features.html.VectorHtmlCompressor 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.login.ReAuthHelper
import im.vector.app.features.navigation.Navigator import im.vector.app.features.navigation.Navigator
import im.vector.app.features.notifications.NotifiableEventResolver import im.vector.app.features.notifications.NotifiableEventResolver
@ -160,6 +161,8 @@ interface VectorComponent {
fun pinLocker(): PinLocker fun pinLocker(): PinLocker
fun autoAcceptInvites(): AutoAcceptInvites
fun webRtcCallManager(): WebRtcCallManager fun webRtcCallManager(): WebRtcCallManager
fun roomSummaryHolder(): RoomSummariesHolder fun roomSummaryHolder(): RoomSummariesHolder

View file

@ -25,6 +25,8 @@ import dagger.Module
import dagger.Provides import dagger.Provides
import im.vector.app.core.error.DefaultErrorFormatter import im.vector.app.core.error.DefaultErrorFormatter
import im.vector.app.core.error.ErrorFormatter 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.DefaultNavigator
import im.vector.app.features.navigation.Navigator import im.vector.app.features.navigation.Navigator
import im.vector.app.features.pin.PinCodeStore import im.vector.app.features.pin.PinCodeStore
@ -105,4 +107,7 @@ abstract class VectorModule {
@Binds @Binds
abstract fun bindPinCodeStore(store: SharedPrefPinCodeStore): PinCodeStore abstract fun bindPinCodeStore(store: SharedPrefPinCodeStore): PinCodeStore
@Binds
abstract fun bindAutoAcceptInvites(autoAcceptInvites: CompileTimeAutoAcceptInvites): AutoAcceptInvites
} }

View file

@ -20,14 +20,11 @@ import android.app.Activity
import android.text.Editable import android.text.Editable
import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.dialog.MaterialAlertDialogBuilder
import im.vector.app.R import im.vector.app.R
import im.vector.app.core.extensions.showPassword
import im.vector.app.core.platform.SimpleTextWatcher import im.vector.app.core.platform.SimpleTextWatcher
import im.vector.app.databinding.DialogExportE2eKeysBinding import im.vector.app.databinding.DialogExportE2eKeysBinding
class ExportKeysDialog { class ExportKeysDialog {
private var passwordVisible = false
fun show(activity: Activity, exportKeyDialogListener: ExportKeyDialogListener) { fun show(activity: Activity, exportKeyDialogListener: ExportKeyDialogListener) {
val dialogLayout = activity.layoutInflater.inflate(R.layout.dialog_export_e2e_keys, null) val dialogLayout = activity.layoutInflater.inflate(R.layout.dialog_export_e2e_keys, null)
val views = DialogExportE2eKeysBinding.bind(dialogLayout) val views = DialogExportE2eKeysBinding.bind(dialogLayout)
@ -57,13 +54,6 @@ class ExportKeysDialog {
views.exportDialogEt.addTextChangedListener(textWatcher) views.exportDialogEt.addTextChangedListener(textWatcher)
views.exportDialogEtConfirm.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() val exportDialog = builder.show()
views.exportDialogSubmit.setOnClickListener { views.exportDialogSubmit.setOnClickListener {

View file

@ -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
}

View file

@ -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()
}
}

View file

@ -40,13 +40,8 @@ fun SearchView.withoutLeftMargin() {
} }
} }
fun EditText.showPassword(visible: Boolean, updateCursor: Boolean = true) { fun EditText.hidePassword() {
if (visible) {
inputType = InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD 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 View.getMeasurements(): Pair<Int, Int> { fun View.getMeasurements(): Pair<Int, Int> {

View file

@ -14,11 +14,8 @@
* limitations under the License. * limitations under the License.
*/ */
@file:Suppress("DEPRECATION")
package im.vector.app.core.platform package im.vector.app.core.platform
import android.app.ProgressDialog
import android.content.Context import android.content.Context
import android.os.Bundle import android.os.Bundle
import android.os.Parcelable import android.os.Parcelable
@ -29,11 +26,12 @@ import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.annotation.CallSuper import androidx.annotation.CallSuper
import androidx.annotation.MainThread import androidx.annotation.MainThread
import com.google.android.material.appbar.MaterialToolbar import androidx.appcompat.app.AlertDialog
import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.ViewModelProvider
import androidx.viewbinding.ViewBinding import androidx.viewbinding.ViewBinding
import com.airbnb.mvrx.BaseMvRxFragment import com.airbnb.mvrx.BaseMvRxFragment
import com.bumptech.glide.util.Util.assertMainThread import com.bumptech.glide.util.Util.assertMainThread
import com.google.android.material.appbar.MaterialToolbar
import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.jakewharton.rxbinding3.view.clicks import com.jakewharton.rxbinding3.view.clicks
import im.vector.app.R import im.vector.app.R
@ -44,10 +42,10 @@ import im.vector.app.core.dialogs.UnrecognizedCertificateDialog
import im.vector.app.core.error.ErrorFormatter import im.vector.app.core.error.ErrorFormatter
import im.vector.app.core.extensions.toMvRxBundle import im.vector.app.core.extensions.toMvRxBundle
import im.vector.app.features.navigation.Navigator import im.vector.app.features.navigation.Navigator
import im.vector.lib.ui.styles.dialogs.MaterialProgressDialog
import io.reactivex.android.schedulers.AndroidSchedulers import io.reactivex.android.schedulers.AndroidSchedulers
import io.reactivex.disposables.CompositeDisposable import io.reactivex.disposables.CompositeDisposable
import io.reactivex.disposables.Disposable import io.reactivex.disposables.Disposable
import timber.log.Timber import timber.log.Timber
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
@ -67,7 +65,7 @@ abstract class VectorBaseFragment<VB: ViewBinding> : BaseMvRxFragment(), HasScre
protected lateinit var errorFormatter: ErrorFormatter protected lateinit var errorFormatter: ErrorFormatter
protected lateinit var unrecognizedCertificateDialog: UnrecognizedCertificateDialog protected lateinit var unrecognizedCertificateDialog: UnrecognizedCertificateDialog
private var progress: ProgressDialog? = null private var progress: AlertDialog? = null
/* ========================================================================================== /* ==========================================================================================
* View model * View model
@ -203,14 +201,10 @@ abstract class VectorBaseFragment<VB: ViewBinding> : BaseMvRxFragment(), HasScre
vectorBaseActivity.getCoordinatorLayout()?.showOptimizedSnackbar(errorFormatter.toHumanReadable(throwable)) vectorBaseActivity.getCoordinatorLayout()?.showOptimizedSnackbar(errorFormatter.toHumanReadable(throwable))
} }
protected fun showLoadingDialog(message: CharSequence? = null, cancelable: Boolean = false) { protected fun showLoadingDialog(message: CharSequence? = null) {
progress?.dismiss() progress?.dismiss()
progress = ProgressDialog(requireContext()).apply { progress = MaterialProgressDialog(requireContext())
setCancelable(cancelable) .show(message ?: getString(R.string.please_wait))
setMessage(message ?: getString(R.string.please_wait))
setProgressStyle(ProgressDialog.STYLE_SPINNER)
show()
}
} }
protected fun dismissLoadingDialog() { protected fun dismissLoadingDialog() {

View file

@ -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()
}
}
}

View file

@ -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)
}
}
}

View file

@ -24,7 +24,6 @@ import androidx.core.view.isVisible
import com.airbnb.mvrx.activityViewModel import com.airbnb.mvrx.activityViewModel
import com.airbnb.mvrx.withState import com.airbnb.mvrx.withState
import im.vector.app.R import im.vector.app.R
import im.vector.app.core.extensions.showPassword
import im.vector.app.core.platform.VectorBaseFragment import im.vector.app.core.platform.VectorBaseFragment
import im.vector.app.databinding.FragmentReauthConfirmBinding import im.vector.app.databinding.FragmentReauthConfirmBinding
import org.matrix.android.sdk.api.auth.data.LoginFlowTypes import org.matrix.android.sdk.api.auth.data.LoginFlowTypes
@ -41,13 +40,6 @@ class PromptFragment : VectorBaseFragment<FragmentReauthConfirmBinding>() {
views.reAuthConfirmButton.debouncedClicks { views.reAuthConfirmButton.debouncedClicks {
onButtonClicked() onButtonClicked()
} }
views.passwordReveal.debouncedClicks {
viewModel.handle(ReAuthActions.StartSSOFallback)
}
views.passwordReveal.debouncedClicks {
viewModel.handle(ReAuthActions.TogglePassVisibility)
}
} }
private fun onButtonClicked() = withState(viewModel) { state -> private fun onButtonClicked() = withState(viewModel) { state ->
@ -74,11 +66,11 @@ class PromptFragment : VectorBaseFragment<FragmentReauthConfirmBinding>() {
override fun invalidate() = withState(viewModel) { override fun invalidate() = withState(viewModel) {
when (it.flowType) { when (it.flowType) {
LoginFlowTypes.SSO -> { LoginFlowTypes.SSO -> {
views.passwordContainer.isVisible = false views.passwordFieldTil.isVisible = false
views.reAuthConfirmButton.text = getString(R.string.auth_login_sso) views.reAuthConfirmButton.text = getString(R.string.auth_login_sso)
} }
LoginFlowTypes.PASSWORD -> { LoginFlowTypes.PASSWORD -> {
views.passwordContainer.isVisible = true views.passwordFieldTil.isVisible = true
views.reAuthConfirmButton.text = getString(R.string._continue) views.reAuthConfirmButton.text = getString(R.string._continue)
} }
else -> { else -> {
@ -86,9 +78,6 @@ class PromptFragment : VectorBaseFragment<FragmentReauthConfirmBinding>() {
} }
} }
views.passwordField.showPassword(it.passwordVisible)
views.passwordReveal.render(it.passwordVisible)
if (it.lastErrorCode != null) { if (it.lastErrorCode != null) {
when (it.flowType) { when (it.flowType) {
LoginFlowTypes.SSO -> { LoginFlowTypes.SSO -> {

View file

@ -22,6 +22,5 @@ sealed class ReAuthActions : VectorViewModelAction {
object StartSSOFallback : ReAuthActions() object StartSSOFallback : ReAuthActions()
object FallBackPageLoaded : ReAuthActions() object FallBackPageLoaded : ReAuthActions()
object FallBackPageClosed : ReAuthActions() object FallBackPageClosed : ReAuthActions()
object TogglePassVisibility : ReAuthActions()
data class ReAuthWithPass(val password: String) : ReAuthActions() data class ReAuthWithPass(val password: String) : ReAuthActions()
} }

View file

@ -23,7 +23,6 @@ data class ReAuthState(
val session: String? = null, val session: String? = null,
val flowType: String? = null, val flowType: String? = null,
val ssoFallbackPageWasShown: Boolean = false, val ssoFallbackPageWasShown: Boolean = false,
val passwordVisible: Boolean = false,
val lastErrorCode: String? = null, val lastErrorCode: String? = null,
val resultKeyStoreAlias: String = "" val resultKeyStoreAlias: String = ""
) : MvRxState { ) : MvRxState {

View file

@ -65,13 +65,6 @@ class ReAuthViewModel @AssistedInject constructor(
ReAuthActions.FallBackPageClosed -> { ReAuthActions.FallBackPageClosed -> {
// Should we do something here? // Should we do something here?
} }
ReAuthActions.TogglePassVisibility -> {
setState {
copy(
passwordVisible = !state.passwordVisible
)
}
}
is ReAuthActions.ReAuthWithPass -> { is ReAuthActions.ReAuthWithPass -> {
val safeForIntentCypher = ByteArrayOutputStream().also { val safeForIntentCypher = ByteArrayOutputStream().also {
it.use { it.use {

View file

@ -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) { class CallUserMapper(private val session: Session, private val protocolsChecker: CallProtocolsChecker) {
fun nativeRoomForVirtualRoom(roomId: String): String? { fun nativeRoomForVirtualRoom(roomId: String): String? {
if (!protocolsChecker.supportVirtualRooms) return null
val virtualRoom = session.getRoom(roomId) ?: return null val virtualRoom = session.getRoom(roomId) ?: return null
val virtualRoomEvent = virtualRoom.getAccountDataEvent(RoomAccountDataTypes.EVENT_TYPE_VIRTUAL_ROOM) val virtualRoomEvent = virtualRoom.getAccountDataEvent(RoomAccountDataTypes.EVENT_TYPE_VIRTUAL_ROOM)
return virtualRoomEvent?.content?.toModel<RoomVirtualContent>()?.nativeRoomId 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? { suspend fun getOrCreateVirtualRoomForRoom(roomId: String, opponentUserId: String): String? {
protocolsChecker.awaitCheckProtocols() protocolsChecker.awaitCheckProtocols()
if (!protocolsChecker.supportVirtualRooms) return null 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 // will make sure we know where how to map calls and also allow us know not to display
// it in the future. // it in the future.
invitedRoom.markVirtual(nativeRoomId) 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)
} }
} }
} }

View file

@ -83,9 +83,9 @@ import java.util.concurrent.TimeUnit
import javax.inject.Provider import javax.inject.Provider
import kotlin.coroutines.CoroutineContext import kotlin.coroutines.CoroutineContext
private const val STREAM_ID = "ARDAMS" private const val STREAM_ID = "userMedia"
private const val AUDIO_TRACK_ID = "ARDAMSa0" private const val AUDIO_TRACK_ID = "${STREAM_ID}a0"
private const val VIDEO_TRACK_ID = "ARDAMSv0" private const val VIDEO_TRACK_ID = "${STREAM_ID}v0"
private val DEFAULT_AUDIO_CONSTRAINTS = MediaConstraints() private val DEFAULT_AUDIO_CONSTRAINTS = MediaConstraints()
class WebRtcCall( class WebRtcCall(
@ -274,12 +274,77 @@ class WebRtcCall(
peerConnection = peerConnectionFactory.createPeerConnection(rtcConfig, PeerConnectionObserver(this)) peerConnection = peerConnectionFactory.createPeerConnection(rtcConfig, PeerConnectionObserver(this))
} }
/**
* 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?) { fun attachViewRenderers(localViewRenderer: SurfaceViewRenderer?, remoteViewRenderer: SurfaceViewRenderer, mode: String?) {
sessionScope?.launch(dispatcher) {
Timber.v("## VOIP attachViewRenderers localRendeder $localViewRenderer / $remoteViewRenderer") Timber.v("## VOIP attachViewRenderers localRendeder $localViewRenderer / $remoteViewRenderer")
localSurfaceRenderers.addIfNeeded(localViewRenderer) localSurfaceRenderers.addIfNeeded(localViewRenderer)
remoteSurfaceRenderers.addIfNeeded(remoteViewRenderer) remoteSurfaceRenderers.addIfNeeded(remoteViewRenderer)
sessionScope?.launch(dispatcher) {
when (mode) { when (mode) {
VectorCallActivity.INCOMING_ACCEPT -> { VectorCallActivity.INCOMING_ACCEPT -> {
internalAcceptIncomingCall() internalAcceptIncomingCall()
@ -299,67 +364,31 @@ class WebRtcCall(
} }
} }
/** private suspend fun attachViewRenderersInternal() = withContext(dispatcher) {
* Without consultation // render local video in pip view
*/ localSurfaceRenderers.forEach { renderer ->
suspend fun transferToUser(targetUserId: String, targetRoomId: String?) { renderer.get()?.let { pipSurface ->
mxCall.transfer( pipSurface.setMirror(cameraInUse?.type == CameraType.FRONT)
targetUserId = targetUserId, // no need to check if already added, addSink is checking that
targetRoomId = targetRoomId, localVideoTrack?.addSink(pipSurface)
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()
}
} }
} }
/** // If remote track exists, then sink it to surface
* Sends a DTMF digit to the other party remoteSurfaceRenderers.forEach { renderer ->
* @param digit The digit (nb. string - '#' and '*' are dtmf too) renderer.get()?.let { participantSurface ->
*/ remoteVideoTrack?.addSink(participantSurface)
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")
}
} }
} }
} }
fun detachRenderers(renderers: List<SurfaceViewRenderer>?) { fun detachRenderers(renderers: List<SurfaceViewRenderer>?) {
sessionScope?.launch(dispatcher) {
detachRenderersInternal(renderers)
}
}
private suspend fun detachRenderersInternal(renderers: List<SurfaceViewRenderer>?) = withContext(dispatcher) {
Timber.v("## VOIP detachRenderers") Timber.v("## VOIP detachRenderers")
if (renderers.isNullOrEmpty()) { if (renderers.isNullOrEmpty()) {
// remove all sinks // 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? { private suspend fun getTurnServer(): TurnServerResponse? {
return tryOrNull { return tryOrNull {
sessionProvider.get()?.callSignalingService()?.getTurnServer() sessionProvider.get()?.callSignalingService()?.getTurnServer()
@ -580,10 +591,12 @@ class WebRtcCall(
} }
fun setCaptureFormat(format: CaptureFormat) { fun setCaptureFormat(format: CaptureFormat) {
sessionScope?.launch(dispatcher) {
Timber.v("## VOIP setCaptureFormat $format") Timber.v("## VOIP setCaptureFormat $format")
videoCapturer?.changeCaptureFormat(format.width, format.height, format.fps) videoCapturer?.changeCaptureFormat(format.width, format.height, format.fps)
currentCaptureFormat = format currentCaptureFormat = format
} }
}
private fun updateMuteStatus() { private fun updateMuteStatus() {
val micShouldBeMuted = micMuted || remoteOnHold val micShouldBeMuted = micMuted || remoteOnHold
@ -645,14 +658,18 @@ class WebRtcCall(
} }
fun muteCall(muted: Boolean) { fun muteCall(muted: Boolean) {
sessionScope?.launch(dispatcher) {
micMuted = muted micMuted = muted
updateMuteStatus() updateMuteStatus()
} }
}
fun enableVideo(enabled: Boolean) { fun enableVideo(enabled: Boolean) {
sessionScope?.launch(dispatcher) {
videoMuted = !enabled videoMuted = !enabled
updateMuteStatus() updateMuteStatus()
} }
}
fun canSwitchCamera(): Boolean { fun canSwitchCamera(): Boolean {
return availableCamera.size > 1 return availableCamera.size > 1
@ -668,9 +685,10 @@ class WebRtcCall(
} }
fun switchCamera() { fun switchCamera() {
sessionScope?.launch(dispatcher) {
Timber.v("## VOIP switchCamera") Timber.v("## VOIP switchCamera")
if (mxCall.state is CallState.Connected && mxCall.isVideoCall) { if (mxCall.state is CallState.Connected && mxCall.isVideoCall) {
val oppositeCamera = getOppositeCameraIfAny() ?: return val oppositeCamera = getOppositeCameraIfAny() ?: return@launch
videoCapturer?.switchCamera( videoCapturer?.switchCamera(
object : CameraVideoCapturer.CameraSwitchHandler { object : CameraVideoCapturer.CameraSwitchHandler {
// Invoked on success. |isFrontCamera| is true if the new camera is front facing. // Invoked on success. |isFrontCamera| is true if the new camera is front facing.
@ -692,6 +710,7 @@ class WebRtcCall(
) )
} }
} }
}
private suspend fun createAnswer(): SessionDescription? { private suspend fun createAnswer(): SessionDescription? {
Timber.w("## VOIP createAnswer") Timber.w("## VOIP createAnswer")
@ -718,11 +737,12 @@ class WebRtcCall(
return currentCaptureFormat return currentCaptureFormat
} }
private fun release() { private suspend fun release() {
listeners.clear() listeners.clear()
mxCall.removeListener(this) mxCall.removeListener(this)
timer.stop() timer.stop()
timer.tickListener = null timer.tickListener = null
detachRenderersInternal(null)
videoCapturer?.stopCapture() videoCapturer?.stopCapture()
videoCapturer?.dispose() videoCapturer?.dispose()
videoCapturer = null videoCapturer = null
@ -736,6 +756,8 @@ class WebRtcCall(
localAudioTrack = null localAudioTrack = null
localVideoSource = null localVideoSource = null
localVideoTrack = null localVideoTrack = null
remoteAudioTrack = null
remoteVideoTrack = null
cameraAvailabilityCallback = null cameraAvailabilityCallback = null
} }
@ -745,7 +767,7 @@ class WebRtcCall(
if (stream.audioTracks.size > 1 || stream.videoTracks.size > 1) { if (stream.audioTracks.size > 1 || stream.videoTracks.size > 1) {
Timber.e("## VOIP StreamObserver weird looking stream: $stream") Timber.e("## VOIP StreamObserver weird looking stream: $stream")
// TODO maybe do something more?? // TODO maybe do something more??
mxCall.hangUp() endCall(true)
return@launch return@launch
} }
if (stream.audioTracks.size == 1) { if (stream.audioTracks.size == 1) {
@ -774,8 +796,9 @@ class WebRtcCall(
} }
fun endCall(sendEndSignaling: Boolean = true, reason: CallHangupContent.Reason? = null) { fun endCall(sendEndSignaling: Boolean = true, reason: CallHangupContent.Reason? = null) {
sessionScope?.launch(dispatcher) {
if (mxCall.state == CallState.Terminated) { if (mxCall.state == CallState.Terminated) {
return return@launch
} }
// Close tracks ASAP // Close tracks ASAP
localVideoTrack?.setEnabled(false) localVideoTrack?.setEnabled(false)
@ -786,10 +809,8 @@ class WebRtcCall(
} }
val wasRinging = mxCall.state is CallState.LocalRinging val wasRinging = mxCall.state is CallState.LocalRinging
mxCall.state = CallState.Terminated mxCall.state = CallState.Terminated
sessionScope?.launch(dispatcher) {
release() release()
onCallEnded(callId) onCallEnded(callId)
}
if (sendEndSignaling) { if (sendEndSignaling) {
if (wasRinging) { if (wasRinging) {
mxCall.reject() mxCall.reject()
@ -798,6 +819,7 @@ class WebRtcCall(
} }
} }
} }
}
// Call listener // Call listener

View file

@ -25,7 +25,6 @@ import android.view.inputmethod.EditorInfo
import androidx.core.text.set import androidx.core.text.set
import androidx.core.widget.doOnTextChanged import androidx.core.widget.doOnTextChanged
import im.vector.app.R import im.vector.app.R
import im.vector.app.core.extensions.showPassword
import im.vector.app.core.platform.VectorBaseFragment import im.vector.app.core.platform.VectorBaseFragment
import im.vector.app.databinding.FragmentKeysBackupRestoreFromPassphraseBinding import im.vector.app.databinding.FragmentKeysBackupRestoreFromPassphraseBinding
@ -40,10 +39,6 @@ class KeysBackupRestoreFromPassphraseFragment @Inject constructor() : VectorBase
private lateinit var viewModel: KeysBackupRestoreFromPassphraseViewModel private lateinit var viewModel: KeysBackupRestoreFromPassphraseViewModel
private lateinit var sharedViewModel: KeysBackupRestoreSharedViewModel private lateinit var sharedViewModel: KeysBackupRestoreSharedViewModel
private fun toggleVisibilityMode() {
viewModel.showPasswordMode.value = !(viewModel.showPasswordMode.value ?: false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
@ -56,12 +51,6 @@ class KeysBackupRestoreFromPassphraseFragment @Inject constructor() : VectorBase
views.helperTextWithLink.text = spannableStringForHelperText() views.helperTextWithLink.text = spannableStringForHelperText()
viewModel.showPasswordMode.observe(viewLifecycleOwner) {
val shouldBeVisible = it ?: false
views.keysBackupPassphraseEnterEdittext.showPassword(shouldBeVisible)
views.keysBackupViewShowPassword.render(shouldBeVisible)
}
views.keysBackupPassphraseEnterEdittext.setOnEditorActionListener { _, actionId, _ -> views.keysBackupPassphraseEnterEdittext.setOnEditorActionListener { _, actionId, _ ->
if (actionId == EditorInfo.IME_ACTION_DONE) { if (actionId == EditorInfo.IME_ACTION_DONE) {
onRestoreBackup() onRestoreBackup()
@ -70,7 +59,6 @@ class KeysBackupRestoreFromPassphraseFragment @Inject constructor() : VectorBase
return@setOnEditorActionListener false return@setOnEditorActionListener false
} }
views.keysBackupViewShowPassword.setOnClickListener { toggleVisibilityMode() }
views.helperTextWithLink.setOnClickListener { onUseRecoveryKey() } views.helperTextWithLink.setOnClickListener { onUseRecoveryKey() }
views.keysBackupRestoreWithPassphraseSubmit.setOnClickListener { onRestoreBackup() } views.keysBackupRestoreWithPassphraseSubmit.setOnClickListener { onRestoreBackup() }
views.keysBackupPassphraseEnterEdittext.doOnTextChanged { text, _, _, _ -> onPassphraseTextEditChange(text) } views.keysBackupPassphraseEnterEdittext.doOnTextChanged { text, _, _, _ -> onPassphraseTextEditChange(text) }

View file

@ -30,12 +30,10 @@ class KeysBackupRestoreFromPassphraseViewModel @Inject constructor(
var passphrase: MutableLiveData<String> = MutableLiveData() var passphrase: MutableLiveData<String> = MutableLiveData()
var passphraseErrorText: MutableLiveData<String> = MutableLiveData() var passphraseErrorText: MutableLiveData<String> = MutableLiveData()
var showPasswordMode: MutableLiveData<Boolean> = MutableLiveData()
init { init {
passphrase.value = null passphrase.value = null
passphraseErrorText.value = null passphraseErrorText.value = null
showPasswordMode.value = false
} }
// ========= Actions ========= // ========= Actions =========

View file

@ -64,7 +64,6 @@ class KeysBackupSetupSharedViewModel @Inject constructor() : ViewModel() {
var confirmPassphraseError: MutableLiveData<String> = MutableLiveData() var confirmPassphraseError: MutableLiveData<String> = MutableLiveData()
var passwordStrength: MutableLiveData<Strength> = MutableLiveData() var passwordStrength: MutableLiveData<Strength> = MutableLiveData()
var showPasswordMode: MutableLiveData<Boolean> = MutableLiveData()
// Step 3 // Step 3
// Var to ignore events from previous request(s) to generate a recovery key // 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() var loadingStatus: MutableLiveData<WaitingViewData> = MutableLiveData()
init { init {
showPasswordMode.value = false
recoveryKey.value = null recoveryKey.value = null
isCreatingBackupVersion.value = false isCreatingBackupVersion.value = false
prepareRecoverFailError.value = null prepareRecoverFailError.value = null
@ -97,9 +95,6 @@ class KeysBackupSetupSharedViewModel @Inject constructor() : ViewModel() {
currentRequestId.value = System.currentTimeMillis() currentRequestId.value = System.currentTimeMillis()
isCreatingBackupVersion.value = true isCreatingBackupVersion.value = true
// Ensure passphrase is hidden during the process
showPasswordMode.value = false
recoveryKey.value = null recoveryKey.value = null
prepareRecoverFailError.value = null prepareRecoverFailError.value = null
session.let { mxSession -> session.let { mxSession ->

View file

@ -25,7 +25,7 @@ import androidx.lifecycle.viewModelScope
import androidx.transition.TransitionManager import androidx.transition.TransitionManager
import com.nulabinc.zxcvbn.Zxcvbn import com.nulabinc.zxcvbn.Zxcvbn
import im.vector.app.R 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.core.platform.VectorBaseFragment
import im.vector.app.databinding.FragmentKeysBackupSetupStep2Binding import im.vector.app.databinding.FragmentKeysBackupSetupStep2Binding
import im.vector.app.features.settings.VectorLocale import im.vector.app.features.settings.VectorLocale
@ -113,13 +113,6 @@ class KeysBackupSetupStep2Fragment @Inject constructor() : VectorBaseFragment<Fr
views.keysBackupSetupStep2PassphraseConfirmEditText.setText(viewModel.confirmPassphrase.value) 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) { viewModel.confirmPassphraseError.observe(viewLifecycleOwner) {
TransitionManager.beginDelayedTransition(views.keysBackupRoot) TransitionManager.beginDelayedTransition(views.keysBackupRoot)
views.keysBackupSetupStep2PassphraseConfirmTil.error = it views.keysBackupSetupStep2PassphraseConfirmTil.error = it
@ -135,7 +128,6 @@ class KeysBackupSetupStep2Fragment @Inject constructor() : VectorBaseFragment<Fr
} }
private fun setupViews() { private fun setupViews() {
views.keysBackupSetupStep2ShowPassword.setOnClickListener { toggleVisibilityMode() }
views.keysBackupSetupStep2Button.setOnClickListener { doNext() } views.keysBackupSetupStep2Button.setOnClickListener { doNext() }
views.keysBackupSetupStep2SkipButton.setOnClickListener { skipPassphrase() } views.keysBackupSetupStep2SkipButton.setOnClickListener { skipPassphrase() }
@ -143,10 +135,6 @@ class KeysBackupSetupStep2Fragment @Inject constructor() : VectorBaseFragment<Fr
views.keysBackupSetupStep2PassphraseConfirmEditText.doOnTextChanged { _, _, _, _ -> onConfirmPassphraseChanged() } views.keysBackupSetupStep2PassphraseConfirmEditText.doOnTextChanged { _, _, _, _ -> onConfirmPassphraseChanged() }
} }
private fun toggleVisibilityMode() {
viewModel.showPasswordMode.value = !(viewModel.showPasswordMode.value ?: false)
}
private fun doNext() { private fun doNext() {
when { when {
viewModel.passphrase.value.isNullOrEmpty() -> { viewModel.passphrase.value.isNullOrEmpty() -> {
@ -161,6 +149,9 @@ class KeysBackupSetupStep2Fragment @Inject constructor() : VectorBaseFragment<Fr
else -> { else -> {
viewModel.megolmBackupCreationInfo = null viewModel.megolmBackupCreationInfo = null
// Ensure passphrase is hidden during the process
views.keysBackupSetupStep2PassphraseEnterEdittext.hidePassword()
views.keysBackupSetupStep2PassphraseConfirmEditText.hidePassword()
viewModel.prepareRecoveryKey(requireActivity(), viewModel.passphrase.value) viewModel.prepareRecoveryKey(requireActivity(), viewModel.passphrase.value)
} }
} }
@ -172,6 +163,9 @@ class KeysBackupSetupStep2Fragment @Inject constructor() : VectorBaseFragment<Fr
// Generate a recovery key for the user // Generate a recovery key for the user
viewModel.megolmBackupCreationInfo = null viewModel.megolmBackupCreationInfo = null
// Ensure passphrase is hidden during the process
views.keysBackupSetupStep2PassphraseEnterEdittext.hidePassword()
views.keysBackupSetupStep2PassphraseConfirmEditText.hidePassword()
viewModel.prepareRecoveryKey(requireActivity(), null) viewModel.prepareRecoveryKey(requireActivity(), null)
} }
else -> { else -> {

View file

@ -21,8 +21,6 @@ import im.vector.app.core.platform.VectorViewModelAction
import im.vector.app.core.platform.WaitingViewData import im.vector.app.core.platform.WaitingViewData
sealed class SharedSecureStorageAction : VectorViewModelAction { sealed class SharedSecureStorageAction : VectorViewModelAction {
object TogglePasswordVisibility : SharedSecureStorageAction()
object UseKey : SharedSecureStorageAction() object UseKey : SharedSecureStorageAction()
object Back : SharedSecureStorageAction() object Back : SharedSecureStorageAction()
object Cancel : SharedSecureStorageAction() object Cancel : SharedSecureStorageAction()

View file

@ -50,7 +50,6 @@ import java.io.ByteArrayOutputStream
data class SharedSecureStorageViewState( data class SharedSecureStorageViewState(
val ready: Boolean = false, val ready: Boolean = false,
val hasPassphrase: Boolean = true, val hasPassphrase: Boolean = true,
val passphraseVisible: Boolean = false,
val checkingSSSSAction: Async<Unit> = Uninitialized, val checkingSSSSAction: Async<Unit> = Uninitialized,
val step: Step = Step.EnterPassphrase, val step: Step = Step.EnterPassphrase,
val activeDeviceCount: Int = 0, val activeDeviceCount: Int = 0,
@ -128,7 +127,6 @@ class SharedSecureStorageViewModel @AssistedInject constructor(
override fun handle(action: SharedSecureStorageAction) = withState { override fun handle(action: SharedSecureStorageAction) = withState {
when (action) { when (action) {
is SharedSecureStorageAction.TogglePasswordVisibility -> handleTogglePasswordVisibility()
is SharedSecureStorageAction.Cancel -> handleCancel() is SharedSecureStorageAction.Cancel -> handleCancel()
is SharedSecureStorageAction.SubmitPassphrase -> handleSubmitPassphrase(action) is SharedSecureStorageAction.SubmitPassphrase -> handleSubmitPassphrase(action)
SharedSecureStorageAction.UseKey -> handleUseKey() SharedSecureStorageAction.UseKey -> handleUseKey()
@ -319,14 +317,6 @@ class SharedSecureStorageViewModel @AssistedInject constructor(
_viewEvents.post(SharedSecureStorageViewEvent.Dismiss) _viewEvents.post(SharedSecureStorageViewEvent.Dismiss)
} }
private fun handleTogglePasswordVisibility() {
setState {
copy(
passphraseVisible = !passphraseVisible
)
}
}
companion object : MvRxViewModelFactory<SharedSecureStorageViewModel, SharedSecureStorageViewState> { companion object : MvRxViewModelFactory<SharedSecureStorageViewModel, SharedSecureStorageViewState> {
@JvmStatic @JvmStatic

View file

@ -23,11 +23,9 @@ import android.view.ViewGroup
import android.view.inputmethod.EditorInfo import android.view.inputmethod.EditorInfo
import androidx.core.text.toSpannable import androidx.core.text.toSpannable
import com.airbnb.mvrx.activityViewModel import com.airbnb.mvrx.activityViewModel
import com.airbnb.mvrx.withState
import com.jakewharton.rxbinding3.widget.editorActionEvents import com.jakewharton.rxbinding3.widget.editorActionEvents
import com.jakewharton.rxbinding3.widget.textChanges import com.jakewharton.rxbinding3.widget.textChanges
import im.vector.app.R import im.vector.app.R
import im.vector.app.core.extensions.showPassword
import im.vector.app.core.platform.VectorBaseFragment import im.vector.app.core.platform.VectorBaseFragment
import im.vector.app.core.resources.ColorProvider import im.vector.app.core.resources.ColorProvider
import im.vector.app.databinding.FragmentSsssAccessFromPassphraseBinding import im.vector.app.databinding.FragmentSsssAccessFromPassphraseBinding
@ -92,7 +90,6 @@ class SharedSecuredStoragePassphraseFragment @Inject constructor(
views.ssssPassphraseSubmit.debouncedClicks { submit() } views.ssssPassphraseSubmit.debouncedClicks { submit() }
views.ssssPassphraseUseKey.debouncedClicks { sharedViewModel.handle(SharedSecureStorageAction.UseKey) } views.ssssPassphraseUseKey.debouncedClicks { sharedViewModel.handle(SharedSecureStorageAction.UseKey) }
views.ssssViewShowPassword.debouncedClicks { sharedViewModel.handle(SharedSecureStorageAction.TogglePasswordVisibility) }
} }
fun submit() { fun submit() {
@ -101,10 +98,4 @@ class SharedSecuredStoragePassphraseFragment @Inject constructor(
views.ssssPassphraseSubmit.isEnabled = false views.ssssPassphraseSubmit.isEnabled = false
sharedViewModel.handle(SharedSecureStorageAction.SubmitPassphrase(text)) sharedViewModel.handle(SharedSecureStorageAction.SubmitPassphrase(text))
} }
override fun invalidate() = withState(sharedViewModel) { state ->
val shouldBeVisible = state.passphraseVisible
views.ssssPassphraseEnterEdittext.showPassword(shouldBeVisible)
views.ssssViewShowPassword.render(shouldBeVisible)
}
} }

View file

@ -34,7 +34,6 @@ sealed class BootstrapActions : VectorViewModelAction {
data class DoInitialize(val passphrase: String) : BootstrapActions() data class DoInitialize(val passphrase: String) : BootstrapActions()
object DoInitializeGeneratedKey : BootstrapActions() object DoInitializeGeneratedKey : BootstrapActions()
object TogglePasswordVisibility : BootstrapActions()
data class UpdateCandidatePassphrase(val pass: String) : BootstrapActions() data class UpdateCandidatePassphrase(val pass: String) : BootstrapActions()
data class UpdateConfirmCandidatePassphrase(val pass: String) : BootstrapActions() data class UpdateConfirmCandidatePassphrase(val pass: String) : BootstrapActions()
// data class ReAuth(val pass: String) : BootstrapActions() // data class ReAuth(val pass: String) : BootstrapActions()

View file

@ -28,7 +28,6 @@ import com.jakewharton.rxbinding3.widget.editorActionEvents
import com.jakewharton.rxbinding3.widget.textChanges import com.jakewharton.rxbinding3.widget.textChanges
import im.vector.app.R import im.vector.app.R
import im.vector.app.core.extensions.hideKeyboard 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.core.platform.VectorBaseFragment
import im.vector.app.databinding.FragmentBootstrapEnterPassphraseBinding import im.vector.app.databinding.FragmentBootstrapEnterPassphraseBinding
import io.reactivex.android.schedulers.AndroidSchedulers 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() } 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)
}
}
} }

View file

@ -26,7 +26,6 @@ import com.airbnb.mvrx.withState
import com.jakewharton.rxbinding3.widget.editorActionEvents import com.jakewharton.rxbinding3.widget.editorActionEvents
import com.jakewharton.rxbinding3.widget.textChanges import com.jakewharton.rxbinding3.widget.textChanges
import im.vector.app.R import im.vector.app.R
import im.vector.app.core.extensions.showPassword
import im.vector.app.core.platform.VectorBaseFragment import im.vector.app.core.platform.VectorBaseFragment
import im.vector.app.databinding.FragmentBootstrapEnterPassphraseBinding import im.vector.app.databinding.FragmentBootstrapEnterPassphraseBinding
import im.vector.app.features.settings.VectorLocale 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() } views.bootstrapSubmit.debouncedClicks { submit() }
} }
@ -101,10 +99,6 @@ class BootstrapEnterPassphraseFragment @Inject constructor()
override fun invalidate() = withState(sharedViewModel) { state -> override fun invalidate() = withState(sharedViewModel) { state ->
if (state.step is BootstrapStep.SetupPassphrase) { 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 -> state.passphraseStrength.invoke()?.let { strength ->
val score = strength.score val score = strength.score
views.ssssPassphraseSecurityProgress.strength = score views.ssssPassphraseSecurityProgress.strength = score

View file

@ -34,7 +34,6 @@ import com.jakewharton.rxbinding3.widget.textChanges
import im.vector.app.R import im.vector.app.R
import im.vector.app.core.extensions.hideKeyboard import im.vector.app.core.extensions.hideKeyboard
import im.vector.app.core.extensions.registerStartForActivityResult 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.platform.VectorBaseFragment
import im.vector.app.core.resources.ColorProvider import im.vector.app.core.resources.ColorProvider
import im.vector.app.core.utils.colorizeMatchingText import im.vector.app.core.utils.colorizeMatchingText
@ -84,7 +83,6 @@ class BootstrapMigrateBackupFragment @Inject constructor(
// sharedViewModel.observeViewEvents {} // sharedViewModel.observeViewEvents {}
views.bootstrapMigrateContinueButton.debouncedClicks { submit() } views.bootstrapMigrateContinueButton.debouncedClicks { submit() }
views.bootstrapMigrateShowPassword.debouncedClicks { sharedViewModel.handle(BootstrapActions.TogglePasswordVisibility) }
views.bootstrapMigrateForgotPassphrase.debouncedClicks { sharedViewModel.handle(BootstrapActions.HandleForgotBackupPassphrase) } views.bootstrapMigrateForgotPassphrase.debouncedClicks { sharedViewModel.handle(BootstrapActions.HandleForgotBackupPassphrase) }
views.bootstrapMigrateUseFile.debouncedClicks { startImportTextFromFileIntent(requireContext(), importFileStartForActivityResult) } views.bootstrapMigrateUseFile.debouncedClicks { startImportTextFromFileIntent(requireContext(), importFileStartForActivityResult) }
} }
@ -116,7 +114,6 @@ class BootstrapMigrateBackupFragment @Inject constructor(
val isEnteringKey = getBackupSecretForMigration.useKey() val isEnteringKey = getBackupSecretForMigration.useKey()
if (isEnteringKey) { if (isEnteringKey) {
views.bootstrapMigrateShowPassword.isVisible = false
views.bootstrapMigrateEditText.inputType = TYPE_CLASS_TEXT or TYPE_TEXT_VARIATION_VISIBLE_PASSWORD or TYPE_TEXT_FLAG_MULTI_LINE 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) val recKey = getString(R.string.bootstrap_migration_backup_recovery_key)
@ -128,14 +125,6 @@ class BootstrapMigrateBackupFragment @Inject constructor(
views.bootstrapMigrateForgotPassphrase.isVisible = false views.bootstrapMigrateForgotPassphrase.isVisible = false
views.bootstrapMigrateUseFile.isVisible = true views.bootstrapMigrateUseFile.isVisible = true
} else { } 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.bootstrapDescriptionText.text = getString(R.string.bootstrap_migration_enter_backup_password)
views.bootstrapMigrateEditText.hint = getString(R.string.passphrase_enter_passphrase) views.bootstrapMigrateEditText.hint = getString(R.string.passphrase_enter_passphrase)

View file

@ -139,7 +139,7 @@ class BootstrapSharedViewModel @AssistedInject constructor(
private fun handleStartMigratingKeyBackup() { private fun handleStartMigratingKeyBackup() {
if (isBackupCreatedFromPassphrase) { if (isBackupCreatedFromPassphrase) {
setState { setState {
copy(step = BootstrapStep.GetBackupSecretPassForMigration(isPasswordVisible = false, useKey = false)) copy(step = BootstrapStep.GetBackupSecretPassForMigration(useKey = false))
} }
} else { } else {
setState { setState {
@ -151,29 +151,6 @@ class BootstrapSharedViewModel @AssistedInject constructor(
override fun handle(action: BootstrapActions) = withState { state -> override fun handle(action: BootstrapActions) = withState { state ->
when (action) { when (action) {
is BootstrapActions.GoBack -> queryBack() 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 -> { BootstrapActions.StartKeyBackupMigration -> {
handleStartMigratingKeyBackup() handleStartMigratingKeyBackup()
} }
@ -193,9 +170,7 @@ class BootstrapSharedViewModel @AssistedInject constructor(
setState { setState {
copy( copy(
passphrase = action.passphrase, passphrase = action.passphrase,
step = BootstrapStep.ConfirmPassphrase( step = BootstrapStep.ConfirmPassphrase
isPasswordVisible = (state.step as? BootstrapStep.SetupPassphrase)?.isPasswordVisible ?: false
)
) )
} }
} }
@ -255,7 +230,7 @@ class BootstrapSharedViewModel @AssistedInject constructor(
BootstrapActions.HandleForgotBackupPassphrase -> { BootstrapActions.HandleForgotBackupPassphrase -> {
if (state.step is BootstrapStep.GetBackupSecretPassForMigration) { if (state.step is BootstrapStep.GetBackupSecretPassForMigration) {
setState { setState {
copy(step = BootstrapStep.GetBackupSecretPassForMigration(state.step.isPasswordVisible, true)) copy(step = BootstrapStep.GetBackupSecretPassForMigration(true))
} }
} else return@withState } else return@withState
} }
@ -293,7 +268,7 @@ class BootstrapSharedViewModel @AssistedInject constructor(
if (action.userWantsToEnterPassphrase) { if (action.userWantsToEnterPassphrase) {
setState { setState {
copy( copy(
step = BootstrapStep.SetupPassphrase(isPasswordVisible = false) step = BootstrapStep.SetupPassphrase
) )
} }
} else { } else {
@ -493,7 +468,6 @@ class BootstrapSharedViewModel @AssistedInject constructor(
setState { setState {
copy( copy(
step = BootstrapStep.GetBackupSecretPassForMigration( step = BootstrapStep.GetBackupSecretPassForMigration(
isPasswordVisible = state.step.isPasswordVisible,
useKey = false useKey = false
) )
) )
@ -524,9 +498,7 @@ class BootstrapSharedViewModel @AssistedInject constructor(
is BootstrapStep.ConfirmPassphrase -> { is BootstrapStep.ConfirmPassphrase -> {
setState { setState {
copy( copy(
step = BootstrapStep.SetupPassphrase( step = BootstrapStep.SetupPassphrase
isPasswordVisible = state.step.isPasswordVisible
)
) )
} }
} }

View file

@ -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 // 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 FirstForm(val keyBackUpExist: Boolean, val reset: Boolean = false) : BootstrapStep()
data class SetupPassphrase(val isPasswordVisible: Boolean) : BootstrapStep() object SetupPassphrase : BootstrapStep()
data class ConfirmPassphrase(val isPasswordVisible: Boolean) : BootstrapStep() object ConfirmPassphrase : BootstrapStep()
data class AccountReAuth(val failure: String? = null) : BootstrapStep() data class AccountReAuth(val failure: String? = null) : BootstrapStep()
abstract class GetBackupSecretForMigration : 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 GetBackupSecretKeyForMigration : GetBackupSecretForMigration()
object Initializing : BootstrapStep() object Initializing : BootstrapStep()

View file

@ -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.lookup.CallProtocolsChecker
import im.vector.app.features.call.webrtc.WebRtcCallManager import im.vector.app.features.call.webrtc.WebRtcCallManager
import im.vector.app.features.createdirect.DirectRoomHelper 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 im.vector.app.features.ui.UiStateRepository
import io.reactivex.schedulers.Schedulers import io.reactivex.schedulers.Schedulers
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
@ -56,7 +58,8 @@ class HomeDetailViewModel @AssistedInject constructor(@Assisted initialState: Ho
private val uiStateRepository: UiStateRepository, private val uiStateRepository: UiStateRepository,
private val callManager: WebRtcCallManager, private val callManager: WebRtcCallManager,
private val directRoomHelper: DirectRoomHelper, private val directRoomHelper: DirectRoomHelper,
private val appStateHandler: AppStateHandler) private val appStateHandler: AppStateHandler,
private val autoAcceptInvites: AutoAcceptInvites)
: VectorViewModel<HomeDetailViewState, HomeDetailAction, HomeDetailViewEvents>(initialState), : VectorViewModel<HomeDetailViewState, HomeDetailAction, HomeDetailViewEvents>(initialState),
CallProtocolsChecker.Listener { CallProtocolsChecker.Listener {
@ -204,7 +207,10 @@ class HomeDetailViewModel @AssistedInject constructor(@Assisted initialState: Ho
} }
is RoomGroupingMethod.BySpace -> { is RoomGroupingMethod.BySpace -> {
val activeSpaceRoomId = groupingMethod.spaceSummary?.roomId val activeSpaceRoomId = groupingMethod.spaceSummary?.roomId
val dmInvites = session.getRoomSummaries( var dmInvites = 0
var roomsInvite = 0
if (autoAcceptInvites.showInvites()) {
dmInvites = session.getRoomSummaries(
roomSummaryQueryParams { roomSummaryQueryParams {
memberships = listOf(Membership.INVITE) memberships = listOf(Membership.INVITE)
roomCategoryFilter = RoomCategoryFilter.ONLY_DM roomCategoryFilter = RoomCategoryFilter.ONLY_DM
@ -212,13 +218,14 @@ class HomeDetailViewModel @AssistedInject constructor(@Assisted initialState: Ho
} }
).size ).size
val roomsInvite = session.getRoomSummaries( roomsInvite = session.getRoomSummaries(
roomSummaryQueryParams { roomSummaryQueryParams {
memberships = listOf(Membership.INVITE) memberships = listOf(Membership.INVITE)
roomCategoryFilter = RoomCategoryFilter.ONLY_ROOMS roomCategoryFilter = RoomCategoryFilter.ONLY_ROOMS
activeSpaceFilter = ActiveSpaceFilter.ActiveSpace(groupingMethod.spaceSummary?.roomId) activeSpaceFilter = ActiveSpaceFilter.ActiveSpace(groupingMethod.spaceSummary?.roomId)
} }
).size ).size
}
val dmRooms = session.getNotificationCountForRooms( val dmRooms = session.getNotificationCountForRooms(
roomSummaryQueryParams { roomSummaryQueryParams {

View file

@ -29,6 +29,7 @@ import im.vector.app.RoomGroupingMethod
import im.vector.app.core.platform.EmptyAction import im.vector.app.core.platform.EmptyAction
import im.vector.app.core.platform.EmptyViewEvents import im.vector.app.core.platform.EmptyViewEvents
import im.vector.app.core.platform.VectorViewModel import im.vector.app.core.platform.VectorViewModel
import im.vector.app.features.invite.AutoAcceptInvites
import im.vector.app.features.settings.VectorPreferences import im.vector.app.features.settings.VectorPreferences
import io.reactivex.Observable import io.reactivex.Observable
import io.reactivex.schedulers.Schedulers import io.reactivex.schedulers.Schedulers
@ -54,7 +55,8 @@ data class CountInfo(
class UnreadMessagesSharedViewModel @AssistedInject constructor(@Assisted initialState: UnreadMessagesState, class UnreadMessagesSharedViewModel @AssistedInject constructor(@Assisted initialState: UnreadMessagesState,
session: Session, session: Session,
private val vectorPreferences: VectorPreferences, private val vectorPreferences: VectorPreferences,
appStateHandler: AppStateHandler) appStateHandler: AppStateHandler,
private val autoAcceptInvites: AutoAcceptInvites)
: VectorViewModel<UnreadMessagesState, EmptyAction, EmptyViewEvents>(initialState) { : VectorViewModel<UnreadMessagesState, EmptyAction, EmptyViewEvents>(initialState) {
@AssistedFactory @AssistedFactory
@ -92,12 +94,17 @@ class UnreadMessagesSharedViewModel @AssistedInject constructor(@Assisted initia
this.activeSpaceFilter = ActiveSpaceFilter.ActiveSpace(null) this.activeSpaceFilter = ActiveSpaceFilter.ActiveSpace(null)
} }
) )
val invites = session.getRoomSummaries( val invites = if (autoAcceptInvites.hideInvites) {
0
} else {
session.getRoomSummaries(
roomSummaryQueryParams { roomSummaryQueryParams {
this.memberships = listOf(Membership.INVITE) this.memberships = listOf(Membership.INVITE)
this.activeSpaceFilter = ActiveSpaceFilter.ActiveSpace(null) this.activeSpaceFilter = ActiveSpaceFilter.ActiveSpace(null)
} }
).size ).size
}
copy( copy(
homeSpaceUnread = RoomAggregateNotificationCount( homeSpaceUnread = RoomAggregateNotificationCount(
counts.notificationCount + invites, counts.notificationCount + invites,
@ -129,10 +136,13 @@ class UnreadMessagesSharedViewModel @AssistedInject constructor(@Assisted initia
is RoomGroupingMethod.BySpace -> { is RoomGroupingMethod.BySpace -> {
val selectedSpace = appStateHandler.safeActiveSpaceId() val selectedSpace = appStateHandler.safeActiveSpaceId()
val inviteCount = session.getRoomSummaries( val inviteCount = if (autoAcceptInvites.hideInvites) {
0
} else {
session.getRoomSummaries(
roomSummaryQueryParams { this.memberships = listOf(Membership.INVITE) } roomSummaryQueryParams { this.memberships = listOf(Membership.INVITE) }
).size ).size
}
val totalCount = session.getNotificationCountForRooms( val totalCount = session.getNotificationCountForRooms(
roomSummaryQueryParams { roomSummaryQueryParams {
this.memberships = listOf(Membership.JOIN) this.memberships = listOf(Membership.JOIN)

View file

@ -18,7 +18,6 @@ package im.vector.app.features.home.room.detail
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.app.Activity import android.app.Activity
import android.content.DialogInterface
import android.content.Intent import android.content.Intent
import android.content.res.Configuration import android.content.res.Configuration
import android.graphics.Color import android.graphics.Color
@ -76,7 +75,6 @@ import com.vanniktech.emoji.EmojiPopup
import im.vector.app.R import im.vector.app.R
import im.vector.app.core.dialogs.ConfirmationDialogBuilder import im.vector.app.core.dialogs.ConfirmationDialogBuilder
import im.vector.app.core.dialogs.GalleryOrCameraDialogHelper 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.epoxy.LayoutManagerStateRestorer
import im.vector.app.core.extensions.cleanup import im.vector.app.core.extensions.cleanup
import im.vector.app.core.extensions.exhaustive import im.vector.app.core.extensions.exhaustive
@ -95,7 +93,6 @@ import im.vector.app.core.resources.ColorProvider
import im.vector.app.core.ui.views.ActiveConferenceView import im.vector.app.core.ui.views.ActiveConferenceView
import im.vector.app.core.ui.views.CurrentCallsView import im.vector.app.core.ui.views.CurrentCallsView
import im.vector.app.core.ui.views.FailedMessagesWarningView 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.KnownCallsViewHolder
import im.vector.app.core.ui.views.NotificationAreaView import im.vector.app.core.ui.views.NotificationAreaView
import im.vector.app.core.utils.Debouncer import im.vector.app.core.utils.Debouncer
@ -241,7 +238,6 @@ class RoomDetailFragment @Inject constructor(
VectorBaseFragment<FragmentRoomDetailBinding>(), VectorBaseFragment<FragmentRoomDetailBinding>(),
TimelineEventController.Callback, TimelineEventController.Callback,
VectorInviteView.Callback, VectorInviteView.Callback,
JumpToReadMarkerView.Callback,
AttachmentTypeSelectorView.Callback, AttachmentTypeSelectorView.Callback,
AttachmentsHelper.Callback, AttachmentsHelper.Callback,
GalleryOrCameraDialogHelper.Listener, GalleryOrCameraDialogHelper.Listener,
@ -367,6 +363,10 @@ class RoomDetailFragment @Inject constructor(
renderTombstoneEventHandling(it) renderTombstoneEventHandling(it)
} }
roomDetailViewModel.selectSubscribe(RoomDetailViewState::canShowJumpToReadMarker, RoomDetailViewState::unreadState) { _, _ ->
updateJumpToReadMarkerViewVisibility()
}
roomDetailViewModel.selectSubscribe(RoomDetailViewState::sendMode, RoomDetailViewState::canSendMessage) { mode, canSend -> roomDetailViewModel.selectSubscribe(RoomDetailViewState::sendMode, RoomDetailViewState::canSendMessage) { mode, canSend ->
if (!canSend) { if (!canSend) {
return@selectSubscribe return@selectSubscribe
@ -751,7 +751,12 @@ class RoomDetailFragment @Inject constructor(
} }
private fun setupJumpToReadMarkerView() { private fun setupJumpToReadMarkerView() {
views.jumpToReadMarkerView.callback = this views.jumpToReadMarkerView.setOnClickListener {
onJumpToReadMarkerClicked()
}
views.jumpToReadMarkerView.setOnCloseIconClickListener {
roomDetailViewModel.handle(RoomDetailAction.MarkAllAsRead)
}
} }
private fun setupActiveCallView() { private fun setupActiveCallView() {
@ -1080,7 +1085,13 @@ class RoomDetailFragment @Inject constructor(
timelineEventController.timeline = roomDetailViewModel.timeline timelineEventController.timeline = roomDetailViewModel.timeline
views.timelineRecyclerView.trackItemsVisibilityChange() 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() val stateRestorer = LayoutManagerStateRestorer(layoutManager).register()
scrollOnNewMessageCallback = ScrollOnNewMessageCallback(layoutManager, timelineEventController) scrollOnNewMessageCallback = ScrollOnNewMessageCallback(layoutManager, timelineEventController)
scrollOnHighlightedEventCallback = ScrollOnHighlightedEventCallback(views.timelineRecyclerView, layoutManager, timelineEventController) scrollOnHighlightedEventCallback = ScrollOnHighlightedEventCallback(views.timelineRecyclerView, layoutManager, timelineEventController)
@ -1091,8 +1102,6 @@ class RoomDetailFragment @Inject constructor(
it.dispatchTo(stateRestorer) it.dispatchTo(stateRestorer)
it.dispatchTo(scrollOnNewMessageCallback) it.dispatchTo(scrollOnNewMessageCallback)
it.dispatchTo(scrollOnHighlightedEventCallback) it.dispatchTo(scrollOnHighlightedEventCallback)
updateJumpToReadMarkerViewVisibility()
jumpToBottomViewVisibilityManager.maybeShowJumpToBottomViewVisibilityWithDelay()
} }
timelineEventController.addModelBuildListener(modelBuildListener) timelineEventController.addModelBuildListener(modelBuildListener)
views.timelineRecyclerView.adapter = timelineEventController.adapter views.timelineRecyclerView.adapter = timelineEventController.adapter
@ -1148,7 +1157,7 @@ class RoomDetailFragment @Inject constructor(
is UnreadState.ReadMarkerNotLoaded -> true is UnreadState.ReadMarkerNotLoaded -> true
is UnreadState.HasUnread -> { is UnreadState.HasUnread -> {
if (it.canShowJumpToReadMarker) { if (it.canShowJumpToReadMarker) {
val lastVisibleItem = layoutManager.findLastVisibleItemPosition() val lastVisibleItem = layoutManager.findLastCompletelyVisibleItemPosition()
val positionOfReadMarker = timelineEventController.getPositionOfReadMarker() val positionOfReadMarker = timelineEventController.getPositionOfReadMarker()
if (positionOfReadMarker == null) { if (positionOfReadMarker == null) {
false false
@ -1434,7 +1443,7 @@ class RoomDetailFragment @Inject constructor(
is RoomDetailAction.ReportContent -> { is RoomDetailAction.ReportContent -> {
when { when {
data.spam -> { data.spam -> {
MaterialAlertDialogBuilder(requireActivity()) MaterialAlertDialogBuilder(requireActivity(), R.style.ThemeOverlay_Vector_MaterialAlertDialog_NegativeDestructive)
.setTitle(R.string.content_reported_as_spam_title) .setTitle(R.string.content_reported_as_spam_title)
.setMessage(R.string.content_reported_as_spam_content) .setMessage(R.string.content_reported_as_spam_content)
.setPositiveButton(R.string.ok, null) .setPositiveButton(R.string.ok, null)
@ -1442,10 +1451,9 @@ class RoomDetailFragment @Inject constructor(
roomDetailViewModel.handle(RoomDetailAction.IgnoreUser(data.senderId)) roomDetailViewModel.handle(RoomDetailAction.IgnoreUser(data.senderId))
} }
.show() .show()
.withColoredButton(DialogInterface.BUTTON_NEGATIVE)
} }
data.inappropriate -> { data.inappropriate -> {
MaterialAlertDialogBuilder(requireActivity()) MaterialAlertDialogBuilder(requireActivity(), R.style.ThemeOverlay_Vector_MaterialAlertDialog_NegativeDestructive)
.setTitle(R.string.content_reported_as_inappropriate_title) .setTitle(R.string.content_reported_as_inappropriate_title)
.setMessage(R.string.content_reported_as_inappropriate_content) .setMessage(R.string.content_reported_as_inappropriate_content)
.setPositiveButton(R.string.ok, null) .setPositiveButton(R.string.ok, null)
@ -1453,10 +1461,9 @@ class RoomDetailFragment @Inject constructor(
roomDetailViewModel.handle(RoomDetailAction.IgnoreUser(data.senderId)) roomDetailViewModel.handle(RoomDetailAction.IgnoreUser(data.senderId))
} }
.show() .show()
.withColoredButton(DialogInterface.BUTTON_NEGATIVE)
} }
else -> { else -> {
MaterialAlertDialogBuilder(requireActivity()) MaterialAlertDialogBuilder(requireActivity(), R.style.ThemeOverlay_Vector_MaterialAlertDialog_NegativeDestructive)
.setTitle(R.string.content_reported_title) .setTitle(R.string.content_reported_title)
.setMessage(R.string.content_reported_content) .setMessage(R.string.content_reported_content)
.setPositiveButton(R.string.ok, null) .setPositiveButton(R.string.ok, null)
@ -1464,7 +1471,6 @@ class RoomDetailFragment @Inject constructor(
roomDetailViewModel.handle(RoomDetailAction.IgnoreUser(data.senderId)) roomDetailViewModel.handle(RoomDetailAction.IgnoreUser(data.senderId))
} }
.show() .show()
.withColoredButton(DialogInterface.BUTTON_NEGATIVE)
} }
} }
} }
@ -1526,7 +1532,7 @@ class RoomDetailFragment @Inject constructor(
.subscribe { managed -> .subscribe { managed ->
if (!managed) { if (!managed) {
if (title.isValidUrl() && url.isValidUrl() && URL(title).host != URL(url).host) { 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) .setTitle(R.string.external_link_confirmation_title)
.setMessage( .setMessage(
getString(R.string.external_link_confirmation_message, title, url) getString(R.string.external_link_confirmation_message, title, url)
@ -1539,7 +1545,6 @@ class RoomDetailFragment @Inject constructor(
} }
.setNegativeButton(R.string.cancel, null) .setNegativeButton(R.string.cancel, null)
.show() .show()
.withColoredButton(DialogInterface.BUTTON_NEGATIVE)
} else { } else {
// Open in external browser, in a new Tab // Open in external browser, in a new Tab
openUrlInExternalBrowser(requireContext(), url) openUrlInExternalBrowser(requireContext(), url)
@ -1638,8 +1643,7 @@ class RoomDetailFragment @Inject constructor(
override fun onEventLongClicked(informationData: MessageInformationData, messageContent: Any?, view: View): Boolean { override fun onEventLongClicked(informationData: MessageInformationData, messageContent: Any?, view: View): Boolean {
view.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS) view.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS)
val roomId = roomDetailArgs.roomId val roomId = roomDetailViewModel.timeline.getTimelineEventWithId(informationData.eventId)?.roomId ?: return false
this.view?.hideKeyboard() this.view?.hideKeyboard()
MessageActionsBottomSheet MessageActionsBottomSheet
@ -1723,7 +1727,6 @@ class RoomDetailFragment @Inject constructor(
} }
override fun onReadMarkerVisible() { override fun onReadMarkerVisible() {
updateJumpToReadMarkerViewVisibility()
roomDetailViewModel.handle(RoomDetailAction.EnterTrackingUnreadMessagesState) roomDetailViewModel.handle(RoomDetailAction.EnterTrackingUnreadMessagesState)
} }
@ -1892,7 +1895,7 @@ class RoomDetailFragment @Inject constructor(
} }
private fun askConfirmationToIgnoreUser(senderId: String) { private fun askConfirmationToIgnoreUser(senderId: String) {
MaterialAlertDialogBuilder(requireContext()) MaterialAlertDialogBuilder(requireContext(), R.style.ThemeOverlay_Vector_MaterialAlertDialog_Destructive)
.setTitle(R.string.room_participants_action_ignore_title) .setTitle(R.string.room_participants_action_ignore_title)
.setMessage(R.string.room_participants_action_ignore_prompt_msg) .setMessage(R.string.room_participants_action_ignore_prompt_msg)
.setNegativeButton(R.string.cancel, null) .setNegativeButton(R.string.cancel, null)
@ -1900,7 +1903,6 @@ class RoomDetailFragment @Inject constructor(
roomDetailViewModel.handle(RoomDetailAction.IgnoreUser(senderId)) roomDetailViewModel.handle(RoomDetailAction.IgnoreUser(senderId))
} }
.show() .show()
.withColoredButton(DialogInterface.BUTTON_POSITIVE)
} }
/** /**
@ -1983,10 +1985,7 @@ class RoomDetailFragment @Inject constructor(
roomDetailViewModel.handle(RoomDetailAction.RejectInvite) roomDetailViewModel.handle(RoomDetailAction.RejectInvite)
} }
// JumpToReadMarkerView.Callback private fun onJumpToReadMarkerClicked() = withState(roomDetailViewModel) {
override fun onJumpToReadMarkerClicked() = withState(roomDetailViewModel) {
views.jumpToReadMarkerView.isVisible = false
if (it.unreadState is UnreadState.HasUnread) { if (it.unreadState is UnreadState.HasUnread) {
roomDetailViewModel.handle(RoomDetailAction.NavigateToEvent(it.unreadState.firstUnreadEventId, false)) roomDetailViewModel.handle(RoomDetailAction.NavigateToEvent(it.unreadState.firstUnreadEventId, false))
} }
@ -1995,10 +1994,6 @@ class RoomDetailFragment @Inject constructor(
} }
} }
override fun onClearReadMarkerClicked() {
roomDetailViewModel.handle(RoomDetailAction.MarkAllAsRead)
}
// AttachmentTypeSelectorView.Callback // AttachmentTypeSelectorView.Callback
private val typeSelectedActivityResultLauncher = registerForPermissionsResult { allGranted -> private val typeSelectedActivityResultLauncher = registerForPermissionsResult { allGranted ->

View file

@ -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.crypto.verification.SupportedVerificationMethodsProvider
import im.vector.app.features.home.room.detail.composer.rainbow.RainbowGenerator 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.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.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.detail.timeline.url.PreviewUrlRetriever
import im.vector.app.features.home.room.typing.TypingHelper import im.vector.app.features.home.room.typing.TypingHelper
import im.vector.app.features.powerlevel.PowerLevelsObservableFactory import im.vector.app.features.powerlevel.PowerLevelsObservableFactory
@ -118,7 +118,7 @@ class RoomDetailViewModel @AssistedInject constructor(
private val chatEffectManager: ChatEffectManager, private val chatEffectManager: ChatEffectManager,
private val directRoomHelper: DirectRoomHelper, private val directRoomHelper: DirectRoomHelper,
private val jitsiService: JitsiService, private val jitsiService: JitsiService,
timelineSettingsFactory: TimelineSettingsFactory timelineFactory: TimelineFactory
) : VectorViewModel<RoomDetailViewState, RoomDetailAction, RoomDetailViewEvents>(initialState), ) : VectorViewModel<RoomDetailViewState, RoomDetailAction, RoomDetailViewEvents>(initialState),
Timeline.Listener, ChatEffectManager.Delegate, CallProtocolsChecker.Listener { Timeline.Listener, ChatEffectManager.Delegate, CallProtocolsChecker.Listener {
@ -126,9 +126,8 @@ class RoomDetailViewModel @AssistedInject constructor(
private val eventId = initialState.eventId private val eventId = initialState.eventId
private val invisibleEventsObservable = BehaviorRelay.create<RoomDetailAction.TimelineEventTurnsInvisible>() private val invisibleEventsObservable = BehaviorRelay.create<RoomDetailAction.TimelineEventTurnsInvisible>()
private val visibleEventsObservable = BehaviorRelay.create<RoomDetailAction.TimelineEventTurnsVisible>() private val visibleEventsObservable = BehaviorRelay.create<RoomDetailAction.TimelineEventTurnsVisible>()
private val timelineSettings = timelineSettingsFactory.create()
private var timelineEvents = PublishRelay.create<List<TimelineEvent>>() 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) // Same lifecycle than the ViewModel (survive to screen rotation)
val previewUrlRetriever = PreviewUrlRetriever(session, viewModelScope) val previewUrlRetriever = PreviewUrlRetriever(session, viewModelScope)
@ -1264,6 +1263,7 @@ class RoomDetailViewModel @AssistedInject constructor(
} }
private fun handleMarkAllAsRead() { private fun handleMarkAllAsRead() {
setState { copy(unreadState = UnreadState.HasNoUnread) }
viewModelScope.launch { viewModelScope.launch {
tryOrNull { room.markAsRead(ReadService.MarkAsReadParams.BOTH) } tryOrNull { room.markAsRead(ReadService.MarkAsReadParams.BOTH) }
} }
@ -1400,7 +1400,6 @@ class RoomDetailViewModel @AssistedInject constructor(
} }
} }
.subscribe { .subscribe {
Timber.v("Unread state: $it")
setState { copy(unreadState = it) } setState { copy(unreadState = it) }
} }
.disposeOnClear() .disposeOnClear()

View file

@ -20,7 +20,6 @@ import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import im.vector.app.core.platform.DefaultListUpdateCallback import im.vector.app.core.platform.DefaultListUpdateCallback
import im.vector.app.features.home.room.detail.timeline.TimelineEventController import im.vector.app.features.home.room.detail.timeline.TimelineEventController
import timber.log.Timber
import java.util.concurrent.atomic.AtomicReference import java.util.concurrent.atomic.AtomicReference
/** /**
@ -42,20 +41,11 @@ class ScrollOnHighlightedEventCallback(private val recyclerView: RecyclerView,
private fun scrollIfNeeded() { private fun scrollIfNeeded() {
val eventId = scheduledEventId.get() ?: return val eventId = scheduledEventId.get() ?: return
val positionToScroll = timelineEventController.searchPositionOfEvent(eventId) val positionToScroll = timelineEventController.searchPositionOfEvent(eventId) ?: return
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() recyclerView.stopScroll()
layoutManager.scrollToPosition(positionToScroll) layoutManager.scrollToPosition(positionToScroll)
}
scheduledEventId.set(null) scheduledEventId.set(null)
} }
}
fun scheduleScrollTo(eventId: String?) { fun scheduleScrollTo(eventId: String?) {
scheduledEventId.set(eventId) scheduledEventId.set(eventId)

View file

@ -16,6 +16,7 @@
package im.vector.app.features.home.room.detail.timeline.factory package im.vector.app.features.home.room.detail.timeline.factory
import im.vector.app.core.epoxy.VectorEpoxyModel 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.call.webrtc.WebRtcCallManager
import im.vector.app.features.home.room.detail.timeline.MessageColorProvider import im.vector.app.features.home.room.detail.timeline.MessageColorProvider
import im.vector.app.features.home.room.detail.timeline.TimelineEventController 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.CallTileTimelineItem_ import im.vector.app.features.home.room.detail.timeline.item.CallTileTimelineItem_
import im.vector.app.features.home.room.detail.timeline.item.MessageInformationData 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.EventType
import org.matrix.android.sdk.api.session.events.model.toModel import org.matrix.android.sdk.api.session.events.model.toModel
import org.matrix.android.sdk.api.session.room.model.call.CallAnswerContent 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 import javax.inject.Inject
class CallItemFactory @Inject constructor( class CallItemFactory @Inject constructor(
private val session: Session,
private val messageColorProvider: MessageColorProvider, private val messageColorProvider: MessageColorProvider,
private val messageInformationDataFactory: MessageInformationDataFactory, private val messageInformationDataFactory: MessageInformationDataFactory,
private val messageItemAttributesFactory: MessageItemAttributesFactory, private val messageItemAttributesFactory: MessageItemAttributesFactory,
@ -132,7 +135,8 @@ class CallItemFactory @Inject constructor(
isStillActive: Boolean, isStillActive: Boolean,
callback: TimelineEventController.Callback? callback: TimelineEventController.Callback?
): CallTileTimelineItem? { ): 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 { val attributes = messageItemAttributesFactory.create(null, informationData, callback).let {
CallTileTimelineItem.Attributes( CallTileTimelineItem.Attributes(
callId = callId, callId = callId,

View file

@ -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
)
)
}
}
}

View file

@ -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
)
}
}

View file

@ -22,6 +22,8 @@ import im.vector.app.R
import im.vector.app.RoomGroupingMethod import im.vector.app.RoomGroupingMethod
import im.vector.app.core.resources.StringProvider import im.vector.app.core.resources.StringProvider
import im.vector.app.features.home.RoomListDisplayMode 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.disposables.Disposable
import io.reactivex.schedulers.Schedulers import io.reactivex.schedulers.Schedulers
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
@ -38,6 +40,7 @@ class GroupRoomListSectionBuilder(
val stringProvider: StringProvider, val stringProvider: StringProvider,
val viewModelScope: CoroutineScope, val viewModelScope: CoroutineScope,
val appStateHandler: AppStateHandler, val appStateHandler: AppStateHandler,
private val autoAcceptInvites: AutoAcceptInvites,
val onDisposable: (Disposable) -> Unit, val onDisposable: (Disposable) -> Unit,
val onUdpatable: (UpdatableLivePageResult) -> Unit val onUdpatable: (UpdatableLivePageResult) -> Unit
) : RoomListSectionBuilder { ) : RoomListSectionBuilder {
@ -73,6 +76,7 @@ class GroupRoomListSectionBuilder(
) )
} }
RoomListDisplayMode.NOTIFICATIONS -> { RoomListDisplayMode.NOTIFICATIONS -> {
if (autoAcceptInvites.showInvites()) {
addSection( addSection(
sections, sections,
activeGroupAwareQueries, activeGroupAwareQueries,
@ -83,7 +87,7 @@ class GroupRoomListSectionBuilder(
it.roomCategoryFilter = RoomCategoryFilter.ALL it.roomCategoryFilter = RoomCategoryFilter.ALL
it.activeGroupId = actualGroupId it.activeGroupId = actualGroupId
} }
}
addSection( addSection(
sections, sections,
activeGroupAwareQueries, activeGroupAwareQueries,
@ -115,6 +119,7 @@ class GroupRoomListSectionBuilder(
private fun buildRoomsSections(sections: MutableList<RoomsSection>, private fun buildRoomsSections(sections: MutableList<RoomsSection>,
activeSpaceAwareQueries: MutableList<UpdatableLivePageResult>, activeSpaceAwareQueries: MutableList<UpdatableLivePageResult>,
actualGroupId: String?) { actualGroupId: String?) {
if (autoAcceptInvites.showInvites()) {
addSection( addSection(
sections, sections,
activeSpaceAwareQueries, activeSpaceAwareQueries,
@ -125,6 +130,7 @@ class GroupRoomListSectionBuilder(
it.roomCategoryFilter = RoomCategoryFilter.ONLY_ROOMS it.roomCategoryFilter = RoomCategoryFilter.ONLY_ROOMS
it.activeGroupId = actualGroupId it.activeGroupId = actualGroupId
} }
}
addSection( addSection(
sections, sections,
@ -180,6 +186,7 @@ class GroupRoomListSectionBuilder(
activeSpaceAwareQueries: MutableList<UpdatableLivePageResult>, activeSpaceAwareQueries: MutableList<UpdatableLivePageResult>,
actualGroupId: String? actualGroupId: String?
) { ) {
if (autoAcceptInvites.showInvites()) {
addSection(sections, addSection(sections,
activeSpaceAwareQueries, activeSpaceAwareQueries,
R.string.invitations_header, R.string.invitations_header,
@ -189,6 +196,7 @@ class GroupRoomListSectionBuilder(
it.roomCategoryFilter = RoomCategoryFilter.ONLY_DM it.roomCategoryFilter = RoomCategoryFilter.ONLY_DM
it.activeGroupId = actualGroupId it.activeGroupId = actualGroupId
} }
}
addSection( addSection(
sections, sections,

View file

@ -16,7 +16,6 @@
package im.vector.app.features.home.room.list package im.vector.app.features.home.room.list
import android.content.DialogInterface
import android.os.Bundle import android.os.Bundle
import android.os.Parcelable import android.os.Parcelable
import android.view.LayoutInflater import android.view.LayoutInflater
@ -34,7 +33,6 @@ import com.airbnb.mvrx.fragmentViewModel
import com.airbnb.mvrx.withState import com.airbnb.mvrx.withState
import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.dialog.MaterialAlertDialogBuilder
import im.vector.app.R import im.vector.app.R
import im.vector.app.core.dialogs.withColoredButton
import im.vector.app.core.epoxy.LayoutManagerStateRestorer import im.vector.app.core.epoxy.LayoutManagerStateRestorer
import im.vector.app.core.extensions.cleanup import im.vector.app.core.extensions.cleanup
import im.vector.app.core.extensions.exhaustive import im.vector.app.core.extensions.exhaustive
@ -386,7 +384,7 @@ class RoomListFragment @Inject constructor(
append(getString(R.string.room_participants_leave_private_warning)) 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) .setTitle(R.string.room_participants_leave_prompt_title)
.setMessage(message) .setMessage(message)
.setPositiveButton(R.string.leave) { _, _ -> .setPositiveButton(R.string.leave) { _, _ ->
@ -394,11 +392,6 @@ class RoomListFragment @Inject constructor(
} }
.setNegativeButton(R.string.cancel, null) .setNegativeButton(R.string.cancel, null)
.show() .show()
.apply {
if (!isPublicRoom) {
withColoredButton(DialogInterface.BUTTON_POSITIVE)
}
}
} }
override fun invalidate() = withState(roomListViewModel) { state -> override fun invalidate() = withState(roomListViewModel) { state ->

View file

@ -30,6 +30,7 @@ import im.vector.app.RoomGroupingMethod
import im.vector.app.core.extensions.exhaustive import im.vector.app.core.extensions.exhaustive
import im.vector.app.core.platform.VectorViewModel import im.vector.app.core.platform.VectorViewModel
import im.vector.app.core.resources.StringProvider import im.vector.app.core.resources.StringProvider
import im.vector.app.features.invite.AutoAcceptInvites
import im.vector.app.features.settings.VectorPreferences import im.vector.app.features.settings.VectorPreferences
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@ -49,7 +50,8 @@ class RoomListViewModel @Inject constructor(
private val session: Session, private val session: Session,
private val stringProvider: StringProvider, private val stringProvider: StringProvider,
private val appStateHandler: AppStateHandler, private val appStateHandler: AppStateHandler,
private val vectorPreferences: VectorPreferences private val vectorPreferences: VectorPreferences,
private val autoAcceptInvites: AutoAcceptInvites
) : VectorViewModel<RoomListViewState, RoomListAction, RoomListViewEvents>(initialState) { ) : VectorViewModel<RoomListViewState, RoomListAction, RoomListViewEvents>(initialState) {
interface Factory { interface Factory {
@ -126,6 +128,7 @@ class RoomListViewModel @Inject constructor(
appStateHandler, appStateHandler,
viewModelScope, viewModelScope,
suggestedRoomJoiningState, suggestedRoomJoiningState,
autoAcceptInvites,
{ {
it.disposeOnClear() it.disposeOnClear()
}, },
@ -140,6 +143,7 @@ class RoomListViewModel @Inject constructor(
stringProvider, stringProvider,
viewModelScope, viewModelScope,
appStateHandler, appStateHandler,
autoAcceptInvites,
{ {
it.disposeOnClear() it.disposeOnClear()
}, },

View file

@ -18,6 +18,7 @@ package im.vector.app.features.home.room.list
import im.vector.app.AppStateHandler import im.vector.app.AppStateHandler
import im.vector.app.core.resources.StringProvider import im.vector.app.core.resources.StringProvider
import im.vector.app.features.invite.AutoAcceptInvites
import im.vector.app.features.settings.VectorPreferences import im.vector.app.features.settings.VectorPreferences
import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.Session
import javax.inject.Inject import javax.inject.Inject
@ -26,16 +27,18 @@ import javax.inject.Provider
class RoomListViewModelFactory @Inject constructor(private val session: Provider<Session>, class RoomListViewModelFactory @Inject constructor(private val session: Provider<Session>,
private val appStateHandler: AppStateHandler, private val appStateHandler: AppStateHandler,
private val stringProvider: StringProvider, private val stringProvider: StringProvider,
private val vectorPreferences: VectorPreferences) private val vectorPreferences: VectorPreferences,
private val autoAcceptInvites: AutoAcceptInvites)
: RoomListViewModel.Factory { : RoomListViewModel.Factory {
override fun create(initialState: RoomListViewState): RoomListViewModel { override fun create(initialState: RoomListViewState): RoomListViewModel {
return RoomListViewModel( return RoomListViewModel(
initialState, initialState = initialState,
session.get(), session = session.get(),
stringProvider, stringProvider = stringProvider,
appStateHandler, appStateHandler = appStateHandler,
vectorPreferences vectorPreferences = vectorPreferences,
autoAcceptInvites = autoAcceptInvites
) )
} }
} }

View file

@ -17,11 +17,13 @@
package im.vector.app.features.home.room.list package im.vector.app.features.home.room.list
import com.airbnb.mvrx.Async import com.airbnb.mvrx.Async
import com.airbnb.mvrx.Fail
import com.airbnb.mvrx.Loading import com.airbnb.mvrx.Loading
import im.vector.app.R import im.vector.app.R
import im.vector.app.core.date.DateFormatKind import im.vector.app.core.date.DateFormatKind
import im.vector.app.core.date.VectorDateFormatter import im.vector.app.core.date.VectorDateFormatter
import im.vector.app.core.epoxy.VectorEpoxyModel 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.core.resources.StringProvider
import im.vector.app.features.home.AvatarRenderer import im.vector.app.features.home.AvatarRenderer
import im.vector.app.features.home.room.detail.timeline.format.DisplayableEventFormatter 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 dateFormatter: VectorDateFormatter,
private val stringProvider: StringProvider, private val stringProvider: StringProvider,
private val typingHelper: TypingHelper, private val typingHelper: TypingHelper,
private val avatarRenderer: AvatarRenderer) { private val avatarRenderer: AvatarRenderer,
private val errorFormatter: ErrorFormatter) {
fun create(roomSummary: RoomSummary, fun create(roomSummary: RoomSummary,
roomChangeMembershipStates: Map<String, ChangeMembershipState>, roomChangeMembershipStates: Map<String, ChangeMembershipState>,
@ -55,12 +58,21 @@ class RoomSummaryItemFactory @Inject constructor(private val displayableEventFor
fun createSuggestion(spaceChildInfo: SpaceChildInfo, fun createSuggestion(spaceChildInfo: SpaceChildInfo,
suggestedRoomJoiningStates: Map<String, Async<Unit>>, suggestedRoomJoiningStates: Map<String, Async<Unit>>,
listener: RoomListListener?): VectorEpoxyModel<*> { listener: RoomListListener?): VectorEpoxyModel<*> {
val error = (suggestedRoomJoiningStates[spaceChildInfo.childRoomId] as? Fail)?.error
return SpaceChildInfoItem_() return SpaceChildInfoItem_()
.id("sug_${spaceChildInfo.childRoomId}") .id("sug_${spaceChildInfo.childRoomId}")
.matrixItem(spaceChildInfo.toMatrixItem()) .matrixItem(spaceChildInfo.toMatrixItem())
.avatarRenderer(avatarRenderer) .avatarRenderer(avatarRenderer)
.topic(spaceChildInfo.topic) .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) .loading(suggestedRoomJoiningStates[spaceChildInfo.childRoomId] is Loading)
.memberCount(spaceChildInfo.activeMemberCount ?: 0) .memberCount(spaceChildInfo.activeMemberCount ?: 0)
.buttonClickListener { listener?.onJoinSuggestedRoom(spaceChildInfo) } .buttonClickListener { listener?.onJoinSuggestedRoom(spaceChildInfo) }

View file

@ -33,6 +33,7 @@ import im.vector.app.core.epoxy.ClickListener
import im.vector.app.core.epoxy.VectorEpoxyHolder import im.vector.app.core.epoxy.VectorEpoxyHolder
import im.vector.app.core.epoxy.VectorEpoxyModel import im.vector.app.core.epoxy.VectorEpoxyModel
import im.vector.app.core.epoxy.onClick 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.home.AvatarRenderer
import im.vector.app.features.themes.ThemeUtils import im.vector.app.features.themes.ThemeUtils
import me.gujun.android.span.image import me.gujun.android.span.image
@ -52,6 +53,7 @@ abstract class SpaceChildInfoItem : VectorEpoxyModel<SpaceChildInfoItem.Holder>(
@EpoxyAttribute var loading: Boolean = false @EpoxyAttribute var loading: Boolean = false
@EpoxyAttribute var buttonLabel: String? = null @EpoxyAttribute var buttonLabel: String? = null
@EpoxyAttribute var errorLabel: String? = null
@EpoxyAttribute(EpoxyAttribute.Option.DoNotHash) var itemLongClickListener: View.OnLongClickListener? = null @EpoxyAttribute(EpoxyAttribute.Option.DoNotHash) var itemLongClickListener: View.OnLongClickListener? = null
@EpoxyAttribute(EpoxyAttribute.Option.DoNotHash) var itemClickListener: ClickListener? = null @EpoxyAttribute(EpoxyAttribute.Option.DoNotHash) var itemClickListener: ClickListener? = null
@ -97,6 +99,8 @@ abstract class SpaceChildInfoItem : VectorEpoxyModel<SpaceChildInfoItem.Holder>(
holder.joinButton.isVisible = true holder.joinButton.isVisible = true
} }
holder.errorTextView.setTextOrHide(errorLabel)
holder.joinButton.onClick { holder.joinButton.onClick {
// local echo // local echo
holder.joinButton.isEnabled = false holder.joinButton.isEnabled = false
@ -120,5 +124,6 @@ abstract class SpaceChildInfoItem : VectorEpoxyModel<SpaceChildInfoItem.Holder>(
val descriptionText by bind<TextView>(R.id.suggestedRoomDescription) val descriptionText by bind<TextView>(R.id.suggestedRoomDescription)
val avatarImageView by bind<ImageView>(R.id.roomAvatarImageView) val avatarImageView by bind<ImageView>(R.id.roomAvatarImageView)
val rootView by bind<ViewGroup>(R.id.itemRoomLayout) val rootView by bind<ViewGroup>(R.id.itemRoomLayout)
val errorTextView by bind<TextView>(R.id.inlineErrorText)
} }
} }

View file

@ -26,6 +26,8 @@ import im.vector.app.AppStateHandler
import im.vector.app.R import im.vector.app.R
import im.vector.app.core.resources.StringProvider import im.vector.app.core.resources.StringProvider
import im.vector.app.features.home.RoomListDisplayMode 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 im.vector.app.space
import io.reactivex.Observable import io.reactivex.Observable
import io.reactivex.disposables.Disposable import io.reactivex.disposables.Disposable
@ -50,6 +52,7 @@ class SpaceRoomListSectionBuilder(
val appStateHandler: AppStateHandler, val appStateHandler: AppStateHandler,
val viewModelScope: CoroutineScope, val viewModelScope: CoroutineScope,
private val suggestedRoomJoiningState: LiveData<Map<String, Async<Unit>>>, private val suggestedRoomJoiningState: LiveData<Map<String, Async<Unit>>>,
private val autoAcceptInvites: AutoAcceptInvites,
val onDisposable: (Disposable) -> Unit, val onDisposable: (Disposable) -> Unit,
val onUdpatable: (UpdatableLivePageResult) -> Unit, val onUdpatable: (UpdatableLivePageResult) -> Unit,
val onlyOrphansInHome: Boolean = false val onlyOrphansInHome: Boolean = false
@ -88,6 +91,7 @@ class SpaceRoomListSectionBuilder(
) )
} }
RoomListDisplayMode.NOTIFICATIONS -> { RoomListDisplayMode.NOTIFICATIONS -> {
if (autoAcceptInvites.showInvites()) {
addSection( addSection(
sections = sections, sections = sections,
activeSpaceUpdaters = activeSpaceAwareQueries, activeSpaceUpdaters = activeSpaceAwareQueries,
@ -103,6 +107,7 @@ class SpaceRoomListSectionBuilder(
it.memberships = listOf(Membership.INVITE) it.memberships = listOf(Membership.INVITE)
it.roomCategoryFilter = RoomCategoryFilter.ALL it.roomCategoryFilter = RoomCategoryFilter.ALL
} }
}
addSection( addSection(
sections = sections, sections = sections,
@ -136,6 +141,7 @@ class SpaceRoomListSectionBuilder(
} }
private fun buildRoomsSections(sections: MutableList<RoomsSection>, activeSpaceAwareQueries: MutableList<RoomListViewModel.ActiveSpaceQueryUpdater>) { private fun buildRoomsSections(sections: MutableList<RoomsSection>, activeSpaceAwareQueries: MutableList<RoomListViewModel.ActiveSpaceQueryUpdater>) {
if (autoAcceptInvites.showInvites()) {
addSection( addSection(
sections = sections, sections = sections,
activeSpaceUpdaters = activeSpaceAwareQueries, activeSpaceUpdaters = activeSpaceAwareQueries,
@ -147,6 +153,7 @@ class SpaceRoomListSectionBuilder(
it.memberships = listOf(Membership.INVITE) it.memberships = listOf(Membership.INVITE)
it.roomCategoryFilter = RoomCategoryFilter.ONLY_ROOMS it.roomCategoryFilter = RoomCategoryFilter.ONLY_ROOMS
} }
}
addSection( addSection(
sections, sections,
@ -253,6 +260,7 @@ class SpaceRoomListSectionBuilder(
} }
private fun buildDmSections(sections: MutableList<RoomsSection>, activeSpaceAwareQueries: MutableList<RoomListViewModel.ActiveSpaceQueryUpdater>) { private fun buildDmSections(sections: MutableList<RoomsSection>, activeSpaceAwareQueries: MutableList<RoomListViewModel.ActiveSpaceQueryUpdater>) {
if (autoAcceptInvites.showInvites()) {
addSection(sections = sections, addSection(sections = sections,
activeSpaceUpdaters = activeSpaceAwareQueries, activeSpaceUpdaters = activeSpaceAwareQueries,
nameRes = R.string.invitations_header, nameRes = R.string.invitations_header,
@ -263,6 +271,7 @@ class SpaceRoomListSectionBuilder(
it.memberships = listOf(Membership.INVITE) it.memberships = listOf(Membership.INVITE)
it.roomCategoryFilter = RoomCategoryFilter.ONLY_DM it.roomCategoryFilter = RoomCategoryFilter.ONLY_DM
} }
}
addSection(sections, addSection(sections,
activeSpaceAwareQueries, activeSpaceAwareQueries,

View file

@ -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
}

View file

@ -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()
}
}

View file

@ -32,7 +32,7 @@ import com.jakewharton.rxbinding3.widget.textChanges
import im.vector.app.R import im.vector.app.R
import im.vector.app.core.extensions.exhaustive import im.vector.app.core.extensions.exhaustive
import im.vector.app.core.extensions.hideKeyboard 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.core.extensions.toReducedUrl
import im.vector.app.databinding.FragmentLoginBinding import im.vector.app.databinding.FragmentLoginBinding
import io.reactivex.Observable import io.reactivex.Observable
@ -53,7 +53,6 @@ import javax.inject.Inject
*/ */
class LoginFragment @Inject constructor() : AbstractSSOLoginFragment<FragmentLoginBinding>() { class LoginFragment @Inject constructor() : AbstractSSOLoginFragment<FragmentLoginBinding>() {
private var passwordShown = false
private var isSignupMode = false private var isSignupMode = false
// Temporary patch for https://github.com/vector-im/riotX-android/issues/1410, // Temporary patch for https://github.com/vector-im/riotX-android/issues/1410,
@ -69,7 +68,6 @@ class LoginFragment @Inject constructor() : AbstractSSOLoginFragment<FragmentLog
setupSubmitButton() setupSubmitButton()
setupForgottenPasswordButton() setupForgottenPasswordButton()
setupPasswordReveal()
views.passwordField.setOnEditorActionListener { _, actionId, _ -> views.passwordField.setOnEditorActionListener { _, actionId, _ ->
if (actionId == EditorInfo.IME_ACTION_DONE) { if (actionId == EditorInfo.IME_ACTION_DONE) {
@ -247,23 +245,6 @@ class LoginFragment @Inject constructor() : AbstractSSOLoginFragment<FragmentLog
loginViewModel.handle(LoginAction.PostViewEvent(LoginViewEvents.OnForgetPasswordClicked)) 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() { override fun resetViewModel() {
loginViewModel.handle(LoginAction.ResetLogin) loginViewModel.handle(LoginAction.ResetLogin)
} }
@ -290,8 +271,7 @@ class LoginFragment @Inject constructor() : AbstractSSOLoginFragment<FragmentLog
when (state.asyncLoginAction) { when (state.asyncLoginAction) {
is Loading -> { is Loading -> {
// Ensure password is hidden // Ensure password is hidden
passwordShown = false views.passwordField.hidePassword()
renderPasswordField()
} }
is Fail -> { is Fail -> {
val error = state.asyncLoginAction.error val error = state.asyncLoginAction.error
@ -317,8 +297,7 @@ class LoginFragment @Inject constructor() : AbstractSSOLoginFragment<FragmentLog
when (state.asyncRegistration) { when (state.asyncRegistration) {
is Loading -> { is Loading -> {
// Ensure password is hidden // Ensure password is hidden
passwordShown = false views.passwordField.hidePassword()
renderPasswordField()
} }
// Success is handled by the LoginActivity // Success is handled by the LoginActivity
is Success -> Unit is Success -> Unit

View file

@ -28,7 +28,7 @@ import com.jakewharton.rxbinding3.widget.textChanges
import im.vector.app.R import im.vector.app.R
import im.vector.app.core.extensions.hideKeyboard import im.vector.app.core.extensions.hideKeyboard
import im.vector.app.core.extensions.isEmail 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.extensions.toReducedUrl
import im.vector.app.databinding.FragmentLoginResetPasswordBinding import im.vector.app.databinding.FragmentLoginResetPasswordBinding
import io.reactivex.Observable import io.reactivex.Observable
@ -41,8 +41,6 @@ import javax.inject.Inject
*/ */
class LoginResetPasswordFragment @Inject constructor() : AbstractLoginFragment<FragmentLoginResetPasswordBinding>() { class LoginResetPasswordFragment @Inject constructor() : AbstractLoginFragment<FragmentLoginResetPasswordBinding>() {
private var passwordShown = false
// Show warning only once // Show warning only once
private var showWarning = true private var showWarning = true
@ -54,7 +52,6 @@ class LoginResetPasswordFragment @Inject constructor() : AbstractLoginFragment<F
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
setupSubmitButton() setupSubmitButton()
setupPasswordReveal()
} }
private fun setupUi(state: LoginViewState) { private fun setupUi(state: LoginViewState) {
@ -112,23 +109,6 @@ class LoginResetPasswordFragment @Inject constructor() : AbstractLoginFragment<F
views.passwordFieldTil.error = null 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() { override fun resetViewModel() {
loginViewModel.handle(LoginAction.ResetResetPassword) loginViewModel.handle(LoginAction.ResetResetPassword)
} }
@ -139,8 +119,7 @@ class LoginResetPasswordFragment @Inject constructor() : AbstractLoginFragment<F
when (state.asyncResetPassword) { when (state.asyncResetPassword) {
is Loading -> { is Loading -> {
// Ensure new password is hidden // Ensure new password is hidden
passwordShown = false views.passwordField.hidePassword()
renderPasswordField()
} }
is Fail -> { is Fail -> {
views.resetPasswordEmailTil.error = errorFormatter.toHumanReadable(state.asyncResetPassword.error) views.resetPasswordEmailTil.error = errorFormatter.toHumanReadable(state.asyncResetPassword.error)

View file

@ -28,7 +28,7 @@ import com.airbnb.mvrx.Fail
import com.jakewharton.rxbinding3.widget.textChanges import com.jakewharton.rxbinding3.widget.textChanges
import im.vector.app.R import im.vector.app.R
import im.vector.app.core.extensions.hideKeyboard 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.databinding.FragmentLoginSigninPassword2Binding
import im.vector.app.features.home.AvatarRenderer import im.vector.app.features.home.AvatarRenderer
import io.reactivex.rxkotlin.subscribeBy import io.reactivex.rxkotlin.subscribeBy
@ -47,8 +47,6 @@ class LoginFragmentSigninPassword2 @Inject constructor(
private val avatarRenderer: AvatarRenderer private val avatarRenderer: AvatarRenderer
) : AbstractSSOLoginFragment2<FragmentLoginSigninPassword2Binding>() { ) : AbstractSSOLoginFragment2<FragmentLoginSigninPassword2Binding>() {
private var passwordShown = false
override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentLoginSigninPassword2Binding { override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentLoginSigninPassword2Binding {
return FragmentLoginSigninPassword2Binding.inflate(inflater, container, false) return FragmentLoginSigninPassword2Binding.inflate(inflater, container, false)
} }
@ -58,7 +56,6 @@ class LoginFragmentSigninPassword2 @Inject constructor(
setupSubmitButton() setupSubmitButton()
setupForgottenPasswordButton() setupForgottenPasswordButton()
setupPasswordReveal()
setupAutoFill() setupAutoFill()
views.passwordField.setOnEditorActionListener { _, actionId, _ -> views.passwordField.setOnEditorActionListener { _, actionId, _ ->
@ -135,23 +132,6 @@ class LoginFragmentSigninPassword2 @Inject constructor(
loginViewModel.handle(LoginAction2.PostViewEvent(LoginViewEvents2.OpenResetPasswordScreen)) 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() { override fun resetViewModel() {
// loginViewModel.handle(LoginAction2.ResetSignin) // loginViewModel.handle(LoginAction2.ResetSignin)
} }
@ -169,8 +149,7 @@ class LoginFragmentSigninPassword2 @Inject constructor(
if (state.isLoading) { if (state.isLoading) {
// Ensure password is hidden // Ensure password is hidden
passwordShown = false views.passwordField.hidePassword()
renderPasswordField()
} }
} }

View file

@ -26,7 +26,7 @@ import androidx.autofill.HintConstants
import com.jakewharton.rxbinding3.widget.textChanges import com.jakewharton.rxbinding3.widget.textChanges
import im.vector.app.R import im.vector.app.R
import im.vector.app.core.extensions.hideKeyboard 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 im.vector.app.databinding.FragmentLoginSignupPassword2Binding
import io.reactivex.rxkotlin.subscribeBy import io.reactivex.rxkotlin.subscribeBy
import javax.inject.Inject import javax.inject.Inject
@ -37,8 +37,6 @@ import javax.inject.Inject
*/ */
class LoginFragmentSignupPassword2 @Inject constructor() : AbstractLoginFragment2<FragmentLoginSignupPassword2Binding>() { class LoginFragmentSignupPassword2 @Inject constructor() : AbstractLoginFragment2<FragmentLoginSignupPassword2Binding>() {
private var passwordShown = false
override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentLoginSignupPassword2Binding { override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentLoginSignupPassword2Binding {
return FragmentLoginSignupPassword2Binding.inflate(inflater, container, false) return FragmentLoginSignupPassword2Binding.inflate(inflater, container, false)
} }
@ -48,7 +46,6 @@ class LoginFragmentSignupPassword2 @Inject constructor() : AbstractLoginFragment
setupSubmitButton() setupSubmitButton()
setupAutoFill() setupAutoFill()
setupPasswordReveal()
views.passwordField.setOnEditorActionListener { _, actionId, _ -> views.passwordField.setOnEditorActionListener { _, actionId, _ ->
if (actionId == EditorInfo.IME_ACTION_DONE) { if (actionId == EditorInfo.IME_ACTION_DONE) {
@ -97,23 +94,6 @@ class LoginFragmentSignupPassword2 @Inject constructor() : AbstractLoginFragment
.disposeOnDestroyView() .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() { override fun resetViewModel() {
// loginViewModel.handle(LoginAction2.ResetSignup) // loginViewModel.handle(LoginAction2.ResetSignup)
} }
@ -127,8 +107,7 @@ class LoginFragmentSignupPassword2 @Inject constructor() : AbstractLoginFragment
if (state.isLoading) { if (state.isLoading) {
// Ensure password is hidden // Ensure password is hidden
passwordShown = false views.passwordField.hidePassword()
renderPasswordField()
} }
} }
} }

View file

@ -27,7 +27,7 @@ import androidx.core.view.isVisible
import com.jakewharton.rxbinding3.widget.textChanges import com.jakewharton.rxbinding3.widget.textChanges
import im.vector.app.R import im.vector.app.R
import im.vector.app.core.extensions.hideKeyboard 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.core.extensions.toReducedUrl
import im.vector.app.databinding.FragmentLoginSigninToAny2Binding import im.vector.app.databinding.FragmentLoginSigninToAny2Binding
import im.vector.app.features.login.LoginMode import im.vector.app.features.login.LoginMode
@ -48,8 +48,6 @@ import javax.inject.Inject
*/ */
class LoginFragmentToAny2 @Inject constructor() : AbstractSSOLoginFragment2<FragmentLoginSigninToAny2Binding>() { class LoginFragmentToAny2 @Inject constructor() : AbstractSSOLoginFragment2<FragmentLoginSigninToAny2Binding>() {
private var passwordShown = false
override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentLoginSigninToAny2Binding { override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentLoginSigninToAny2Binding {
return FragmentLoginSigninToAny2Binding.inflate(inflater, container, false) return FragmentLoginSigninToAny2Binding.inflate(inflater, container, false)
} }
@ -59,7 +57,6 @@ class LoginFragmentToAny2 @Inject constructor() : AbstractSSOLoginFragment2<Frag
setupSubmitButton() setupSubmitButton()
setupForgottenPasswordButton() setupForgottenPasswordButton()
setupPasswordReveal()
setupAutoFill() setupAutoFill()
setupSocialLoginButtons() setupSocialLoginButtons()
@ -159,23 +156,6 @@ class LoginFragmentToAny2 @Inject constructor() : AbstractSSOLoginFragment2<Frag
loginViewModel.handle(LoginAction2.PostViewEvent(LoginViewEvents2.OpenResetPasswordScreen)) 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() { override fun resetViewModel() {
// loginViewModel.handle(LoginAction2.ResetSignin) // loginViewModel.handle(LoginAction2.ResetSignin)
} }
@ -208,8 +188,7 @@ class LoginFragmentToAny2 @Inject constructor() : AbstractSSOLoginFragment2<Frag
if (state.isLoading) { if (state.isLoading) {
// Ensure password is hidden // Ensure password is hidden
passwordShown = false views.passwordField.hidePassword()
renderPasswordField()
} }
} }

View file

@ -28,13 +28,12 @@ import com.jakewharton.rxbinding3.widget.textChanges
import im.vector.app.R import im.vector.app.R
import im.vector.app.core.extensions.hideKeyboard import im.vector.app.core.extensions.hideKeyboard
import im.vector.app.core.extensions.isEmail 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.extensions.toReducedUrl
import im.vector.app.core.utils.autoResetTextInputLayoutErrors import im.vector.app.core.utils.autoResetTextInputLayoutErrors
import im.vector.app.databinding.FragmentLoginResetPassword2Binding import im.vector.app.databinding.FragmentLoginResetPassword2Binding
import io.reactivex.Observable import io.reactivex.Observable
import io.reactivex.rxkotlin.subscribeBy import io.reactivex.rxkotlin.subscribeBy
import javax.inject.Inject import javax.inject.Inject
/** /**
@ -42,8 +41,6 @@ import javax.inject.Inject
*/ */
class LoginResetPasswordFragment2 @Inject constructor() : AbstractLoginFragment2<FragmentLoginResetPassword2Binding>() { class LoginResetPasswordFragment2 @Inject constructor() : AbstractLoginFragment2<FragmentLoginResetPassword2Binding>() {
private var passwordShown = false
// Show warning only once // Show warning only once
private var showWarning = true private var showWarning = true
@ -55,7 +52,6 @@ class LoginResetPasswordFragment2 @Inject constructor() : AbstractLoginFragment2
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
setupSubmitButton() setupSubmitButton()
setupPasswordReveal()
setupAutoFill() setupAutoFill()
autoResetTextInputLayoutErrors(listOf(views.resetPasswordEmailTil, views.passwordFieldTil)) autoResetTextInputLayoutErrors(listOf(views.resetPasswordEmailTil, views.passwordFieldTil))
@ -148,23 +144,6 @@ class LoginResetPasswordFragment2 @Inject constructor() : AbstractLoginFragment2
views.passwordFieldTil.error = null 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() { override fun resetViewModel() {
loginViewModel.handle(LoginAction2.ResetResetPassword) loginViewModel.handle(LoginAction2.ResetResetPassword)
} }
@ -178,8 +157,7 @@ class LoginResetPasswordFragment2 @Inject constructor() : AbstractLoginFragment2
if (state.isLoading) { if (state.isLoading) {
// Ensure new password is hidden // Ensure new password is hidden
passwordShown = false views.passwordField.hidePassword()
renderPasswordField()
} }
} }
} }

View file

@ -27,6 +27,7 @@ import im.vector.app.BuildConfig
import im.vector.app.R import im.vector.app.R
import im.vector.app.core.resources.StringProvider import im.vector.app.core.resources.StringProvider
import im.vector.app.core.utils.FirstThrottler import im.vector.app.core.utils.FirstThrottler
import im.vector.app.features.invite.AutoAcceptInvites
import im.vector.app.features.settings.VectorPreferences import im.vector.app.features.settings.VectorPreferences
import me.gujun.android.span.span import me.gujun.android.span.span
import org.matrix.android.sdk.api.session.Session 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 activeSessionDataSource: ActiveSessionDataSource,
private val iconLoader: IconLoader, private val iconLoader: IconLoader,
private val bitmapLoader: BitmapLoader, 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 val handlerThread: HandlerThread = HandlerThread("NotificationDrawerManager", Thread.MIN_PRIORITY)
private var backgroundHandler: Handler private var backgroundHandler: Handler
@ -253,7 +255,14 @@ class NotificationDrawerManager @Inject constructor(private val context: Context
roomEvents.add(event) 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) is SimpleNotifiableEvent -> simpleEvents.add(event)
else -> Timber.w("Type not handled") else -> Timber.w("Type not handled")
} }

View file

@ -17,7 +17,6 @@
package im.vector.app.features.roomprofile package im.vector.app.features.roomprofile
import android.content.DialogInterface
import android.os.Bundle import android.os.Bundle
import android.os.Parcelable import android.os.Parcelable
import android.view.LayoutInflater import android.view.LayoutInflater
@ -33,7 +32,6 @@ import com.google.android.material.dialog.MaterialAlertDialogBuilder
import im.vector.app.R import im.vector.app.R
import im.vector.app.core.animations.AppBarStateChangeListener import im.vector.app.core.animations.AppBarStateChangeListener
import im.vector.app.core.animations.MatrixItemAppBarStateChangeListener 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.cleanup
import im.vector.app.core.extensions.configureWith import im.vector.app.core.extensions.configureWith
import im.vector.app.core.extensions.copyOnLongClick import im.vector.app.core.extensions.copyOnLongClick
@ -269,7 +267,7 @@ class RoomProfileFragment @Inject constructor(
append(getString(R.string.room_participants_leave_private_warning)) 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) .setTitle(R.string.room_participants_leave_prompt_title)
.setMessage(message) .setMessage(message)
.setPositiveButton(R.string.leave) { _, _ -> .setPositiveButton(R.string.leave) { _, _ ->
@ -277,11 +275,6 @@ class RoomProfileFragment @Inject constructor(
} }
.setNegativeButton(R.string.cancel, null) .setNegativeButton(R.string.cancel, null)
.show() .show()
.apply {
if (!isPublicRoom) {
withColoredButton(DialogInterface.BUTTON_POSITIVE)
}
}
} }
override fun onRoomAliasesClicked() { override fun onRoomAliasesClicked() {

View file

@ -16,7 +16,6 @@
package im.vector.app.features.roomprofile.alias package im.vector.app.features.roomprofile.alias
import android.content.DialogInterface
import android.os.Bundle import android.os.Bundle
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
@ -27,7 +26,6 @@ import com.airbnb.mvrx.fragmentViewModel
import com.airbnb.mvrx.withState import com.airbnb.mvrx.withState
import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.dialog.MaterialAlertDialogBuilder
import im.vector.app.R 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.cleanup
import im.vector.app.core.extensions.configureWith import im.vector.app.core.extensions.configureWith
import im.vector.app.core.extensions.exhaustive import im.vector.app.core.extensions.exhaustive
@ -133,7 +131,7 @@ class RoomAliasFragment @Inject constructor(
} }
private fun unpublishAlias(alias: String) { private fun unpublishAlias(alias: String) {
MaterialAlertDialogBuilder(requireContext()) MaterialAlertDialogBuilder(requireContext(), R.style.ThemeOverlay_Vector_MaterialAlertDialog_Destructive)
.setTitle(R.string.dialog_title_confirmation) .setTitle(R.string.dialog_title_confirmation)
.setMessage(getString(R.string.room_alias_unpublish_confirmation, alias)) .setMessage(getString(R.string.room_alias_unpublish_confirmation, alias))
.setNegativeButton(R.string.cancel, null) .setNegativeButton(R.string.cancel, null)
@ -141,7 +139,6 @@ class RoomAliasFragment @Inject constructor(
viewModel.handle(RoomAliasAction.UnpublishAlias(alias)) viewModel.handle(RoomAliasAction.UnpublishAlias(alias))
} }
.show() .show()
.withColoredButton(DialogInterface.BUTTON_POSITIVE)
} }
override fun toggleManualPublishForm() { override fun toggleManualPublishForm() {
@ -185,7 +182,7 @@ class RoomAliasFragment @Inject constructor(
} }
private fun removeLocalAlias(alias: String) { private fun removeLocalAlias(alias: String) {
MaterialAlertDialogBuilder(requireContext()) MaterialAlertDialogBuilder(requireContext(), R.style.ThemeOverlay_Vector_MaterialAlertDialog_Destructive)
.setTitle(R.string.dialog_title_confirmation) .setTitle(R.string.dialog_title_confirmation)
.setMessage(getString(R.string.room_alias_delete_confirmation, alias)) .setMessage(getString(R.string.room_alias_delete_confirmation, alias))
.setNegativeButton(R.string.cancel, null) .setNegativeButton(R.string.cancel, null)
@ -193,6 +190,5 @@ class RoomAliasFragment @Inject constructor(
viewModel.handle(RoomAliasAction.RemoveLocalAlias(alias)) viewModel.handle(RoomAliasAction.RemoveLocalAlias(alias))
} }
.show() .show()
.withColoredButton(DialogInterface.BUTTON_POSITIVE)
} }
} }

View file

@ -37,7 +37,7 @@ import com.google.android.material.dialog.MaterialAlertDialogBuilder
import im.vector.app.R import im.vector.app.R
import im.vector.app.core.dialogs.GalleryOrCameraDialogHelper import im.vector.app.core.dialogs.GalleryOrCameraDialogHelper
import im.vector.app.core.extensions.hideKeyboard 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.intent.getFilenameFromUri
import im.vector.app.core.platform.SimpleTextWatcher import im.vector.app.core.platform.SimpleTextWatcher
import im.vector.app.core.preference.UserAvatarPreference import im.vector.app.core.preference.UserAvatarPreference
@ -347,18 +347,6 @@ class VectorSettingsGeneralFragment @Inject constructor(
val view: ViewGroup = activity.layoutInflater.inflate(R.layout.dialog_change_password, null) as ViewGroup val view: ViewGroup = activity.layoutInflater.inflate(R.layout.dialog_change_password, null) as ViewGroup
val views = DialogChangePasswordBinding.bind(view) 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) val dialog = MaterialAlertDialogBuilder(activity)
.setView(view) .setView(view)
.setCancelable(false) .setCancelable(false)
@ -377,13 +365,8 @@ class VectorSettingsGeneralFragment @Inject constructor(
fun updateUi() { fun updateUi() {
val oldPwd = views.changePasswordOldPwdText.text.toString() val oldPwd = views.changePasswordOldPwdText.text.toString()
val newPwd = views.changePasswordNewPwdText.text.toString() val newPwd = views.changePasswordNewPwdText.text.toString()
val newConfirmPwd = views.changePasswordConfirmNewPwdText.text.toString()
updateButton.isEnabled = oldPwd.isNotEmpty() && newPwd.isNotEmpty() && newPwd == newConfirmPwd updateButton.isEnabled = oldPwd.isNotEmpty() && newPwd.isNotEmpty()
if (newPwd.isNotEmpty() && newConfirmPwd.isNotEmpty() && newPwd != newConfirmPwd) {
views.changePasswordConfirmNewPwdTil.error = getString(R.string.passwords_do_not_match)
}
} }
views.changePasswordOldPwdText.addTextChangedListener(object : SimpleTextWatcher() { views.changePasswordOldPwdText.addTextChangedListener(object : SimpleTextWatcher() {
@ -395,32 +378,20 @@ class VectorSettingsGeneralFragment @Inject constructor(
views.changePasswordNewPwdText.addTextChangedListener(object : SimpleTextWatcher() { views.changePasswordNewPwdText.addTextChangedListener(object : SimpleTextWatcher() {
override fun afterTextChanged(s: Editable) { 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() updateUi()
} }
}) })
fun showPasswordLoadingView(toShow: Boolean) { fun showPasswordLoadingView(toShow: Boolean) {
if (toShow) { if (toShow) {
views.changePasswordShowPasswords.isEnabled = false
views.changePasswordOldPwdText.isEnabled = false views.changePasswordOldPwdText.isEnabled = false
views.changePasswordNewPwdText.isEnabled = false views.changePasswordNewPwdText.isEnabled = false
views.changePasswordConfirmNewPwdText.isEnabled = false
views.changePasswordLoader.isVisible = true views.changePasswordLoader.isVisible = true
updateButton.isEnabled = false updateButton.isEnabled = false
cancelButton.isEnabled = false cancelButton.isEnabled = false
} else { } else {
views.changePasswordShowPasswords.isEnabled = true
views.changePasswordOldPwdText.isEnabled = true views.changePasswordOldPwdText.isEnabled = true
views.changePasswordNewPwdText.isEnabled = true views.changePasswordNewPwdText.isEnabled = true
views.changePasswordConfirmNewPwdText.isEnabled = true
views.changePasswordLoader.isVisible = false views.changePasswordLoader.isVisible = false
updateButton.isEnabled = true updateButton.isEnabled = true
cancelButton.isEnabled = true cancelButton.isEnabled = true
@ -428,10 +399,9 @@ class VectorSettingsGeneralFragment @Inject constructor(
} }
updateButton.setOnClickListener { updateButton.setOnClickListener {
if (passwordShown) {
// Hide passwords during processing // Hide passwords during processing
views.changePasswordShowPasswords.performClick() views.changePasswordOldPwdText.hidePassword()
} views.changePasswordNewPwdText.hidePassword()
view.hideKeyboard() view.hideKeyboard()

View file

@ -37,7 +37,6 @@ import im.vector.app.core.di.ActiveSessionHolder
import im.vector.app.core.dialogs.ExportKeysDialog import im.vector.app.core.dialogs.ExportKeysDialog
import im.vector.app.core.extensions.queryExportKeys import im.vector.app.core.extensions.queryExportKeys
import im.vector.app.core.extensions.registerStartForActivityResult 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.ExternalIntentData
import im.vector.app.core.intent.analyseIntent import im.vector.app.core.intent.analyseIntent
import im.vector.app.core.intent.getFilenameFromUri import im.vector.app.core.intent.getFilenameFromUri
@ -451,14 +450,6 @@ class VectorSettingsSecurityPrivacyFragment @Inject constructor(
.setTitle(R.string.encryption_import_room_keys) .setTitle(R.string.encryption_import_room_keys)
.setView(dialogLayout) .setView(dialogLayout)
var passwordVisible = false
views.importDialogShowPassword.setOnClickListener {
passwordVisible = !passwordVisible
views.dialogE2eKeysPassphraseEditText.showPassword(passwordVisible)
views.importDialogShowPassword.render(passwordVisible)
}
views.dialogE2eKeysPassphraseEditText.addTextChangedListener(object : SimpleTextWatcher() { views.dialogE2eKeysPassphraseEditText.addTextChangedListener(object : SimpleTextWatcher() {
override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) { override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) {
views.dialogE2eKeysImportButton.isEnabled = !views.dialogE2eKeysPassphraseEditText.text.isNullOrEmpty() views.dialogE2eKeysImportButton.isEnabled = !views.dialogE2eKeysPassphraseEditText.text.isNullOrEmpty()

View file

@ -19,7 +19,6 @@ package im.vector.app.features.settings.account.deactivation
import im.vector.app.core.platform.VectorViewModelAction import im.vector.app.core.platform.VectorViewModelAction
sealed class DeactivateAccountAction : VectorViewModelAction { sealed class DeactivateAccountAction : VectorViewModelAction {
object TogglePassword : DeactivateAccountAction()
data class DeactivateAccount(val eraseAllData: Boolean) : DeactivateAccountAction() data class DeactivateAccount(val eraseAllData: Boolean) : DeactivateAccountAction()
object SsoAuthDone: DeactivateAccountAction() object SsoAuthDone: DeactivateAccountAction()

View file

@ -41,7 +41,7 @@ import kotlin.coroutines.resume
import kotlin.coroutines.resumeWithException import kotlin.coroutines.resumeWithException
data class DeactivateAccountViewState( data class DeactivateAccountViewState(
val passwordShown: Boolean = false val dummy: Boolean = false
) : MvRxState ) : MvRxState
class DeactivateAccountViewModel @AssistedInject constructor(@Assisted private val initialState: DeactivateAccountViewState, 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) { override fun handle(action: DeactivateAccountAction) {
when (action) { when (action) {
DeactivateAccountAction.TogglePassword -> handleTogglePassword()
is DeactivateAccountAction.DeactivateAccount -> handleDeactivateAccount(action) is DeactivateAccountAction.DeactivateAccount -> handleDeactivateAccount(action)
DeactivateAccountAction.SsoAuthDone -> { DeactivateAccountAction.SsoAuthDone -> {
Timber.d("## UIA - FallBack success") Timber.d("## UIA - FallBack success")
@ -87,12 +86,6 @@ class DeactivateAccountViewModel @AssistedInject constructor(@Assisted private v
}.exhaustive }.exhaustive
} }
private fun handleTogglePassword() = withState {
setState {
copy(passwordShown = !passwordShown)
}
}
private fun handleDeactivateAccount(action: DeactivateAccountAction.DeactivateAccount) { private fun handleDeactivateAccount(action: DeactivateAccountAction.DeactivateAccount) {
_viewEvents.post(DeactivateAccountViewEvents.Loading()) _viewEvents.post(DeactivateAccountViewEvents.Loading())

View file

@ -16,7 +16,6 @@
package im.vector.app.features.settings.devtools package im.vector.app.features.settings.devtools
import android.content.DialogInterface
import android.os.Bundle import android.os.Bundle
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
@ -26,7 +25,6 @@ import com.airbnb.mvrx.fragmentViewModel
import com.airbnb.mvrx.withState import com.airbnb.mvrx.withState
import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.dialog.MaterialAlertDialogBuilder
import im.vector.app.R 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.cleanup
import im.vector.app.core.extensions.configureWith import im.vector.app.core.extensions.configureWith
import im.vector.app.core.platform.VectorBaseFragment import im.vector.app.core.platform.VectorBaseFragment
@ -85,7 +83,7 @@ class AccountDataFragment @Inject constructor(
} }
override fun didLongTap(data: UserAccountDataEvent) { override fun didLongTap(data: UserAccountDataEvent) {
MaterialAlertDialogBuilder(requireActivity()) MaterialAlertDialogBuilder(requireActivity(), R.style.ThemeOverlay_Vector_MaterialAlertDialog_Destructive)
.setTitle(R.string.delete) .setTitle(R.string.delete)
.setMessage(getString(R.string.delete_account_data_warning, data.type)) .setMessage(getString(R.string.delete_account_data_warning, data.type))
.setNegativeButton(R.string.cancel, null) .setNegativeButton(R.string.cancel, null)
@ -93,6 +91,5 @@ class AccountDataFragment @Inject constructor(
viewModel.handle(AccountDataAction.DeleteAccountData(data.type)) viewModel.handle(AccountDataAction.DeleteAccountData(data.type))
} }
.show() .show()
.withColoredButton(DialogInterface.BUTTON_POSITIVE)
} }
} }

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