mirror of
https://github.com/SchildiChat/SchildiChat-android.git
synced 2024-11-22 09:25:49 +03:00
Merge branch 'develop' into feature/fga/message_bubbles
This commit is contained in:
commit
7ff8483592
156 changed files with 1244 additions and 984 deletions
|
@ -61,8 +61,9 @@ Supported filename extensions are:
|
|||
|
||||
- ``.feature``: Signifying a new feature in Element Android or in the Matrix SDK.
|
||||
- ``.bugfix``: Signifying a bug fix.
|
||||
- ``.wip``: Signifying a work in progress change, typically a component of a larger feature which will be enabled once all tasks are complete.
|
||||
- ``.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
|
||||
- ``.sdk``: Signifying a change to the Matrix SDK, this could be an addition, deprecation or removal of a public API.
|
||||
- ``.misc``: Any other changes.
|
||||
|
||||
See https://github.com/twisted/towncrier#news-fragments if you need more details.
|
||||
|
|
|
@ -47,12 +47,10 @@ android {
|
|||
|
||||
dependencies {
|
||||
implementation project(":library:ui-styles")
|
||||
implementation project(":library:core-utils")
|
||||
|
||||
implementation 'com.github.chrisbanes:PhotoView:2.3.0'
|
||||
|
||||
implementation libs.rx.rxKotlin
|
||||
implementation libs.rx.rxAndroid
|
||||
|
||||
implementation libs.androidx.core
|
||||
implementation libs.androidx.appCompat
|
||||
implementation libs.androidx.recyclerview
|
||||
|
|
|
@ -20,12 +20,9 @@ import android.util.Log
|
|||
import android.view.View
|
||||
import androidx.core.view.isVisible
|
||||
import im.vector.lib.attachmentviewer.databinding.ItemVideoAttachmentBinding
|
||||
import io.reactivex.Observable
|
||||
import io.reactivex.android.schedulers.AndroidSchedulers
|
||||
import io.reactivex.disposables.Disposable
|
||||
import im.vector.lib.core.utils.timer.CountUpTimer
|
||||
import java.io.File
|
||||
import java.lang.ref.WeakReference
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
// TODO, it would be probably better to use a unique media player
|
||||
// for better customization and control
|
||||
|
@ -35,7 +32,7 @@ class VideoViewHolder constructor(itemView: View) :
|
|||
|
||||
private var isSelected = false
|
||||
private var mVideoPath: String? = null
|
||||
private var progressDisposable: Disposable? = null
|
||||
private var countUpTimer: CountUpTimer? = null
|
||||
private var progress: Int = 0
|
||||
private var wasPaused = false
|
||||
|
||||
|
@ -47,8 +44,7 @@ class VideoViewHolder constructor(itemView: View) :
|
|||
|
||||
override fun onRecycled() {
|
||||
super.onRecycled()
|
||||
progressDisposable?.dispose()
|
||||
progressDisposable = null
|
||||
stopTimer()
|
||||
mVideoPath = null
|
||||
}
|
||||
|
||||
|
@ -72,8 +68,7 @@ class VideoViewHolder constructor(itemView: View) :
|
|||
override fun entersBackground() {
|
||||
if (views.videoView.isPlaying) {
|
||||
progress = views.videoView.currentPosition
|
||||
progressDisposable?.dispose()
|
||||
progressDisposable = null
|
||||
stopTimer()
|
||||
views.videoView.stopPlayback()
|
||||
views.videoView.pause()
|
||||
}
|
||||
|
@ -91,8 +86,7 @@ class VideoViewHolder constructor(itemView: View) :
|
|||
} else {
|
||||
progress = 0
|
||||
}
|
||||
progressDisposable?.dispose()
|
||||
progressDisposable = null
|
||||
stopTimer()
|
||||
} else {
|
||||
if (mVideoPath != null) {
|
||||
startPlaying()
|
||||
|
@ -107,17 +101,19 @@ class VideoViewHolder constructor(itemView: View) :
|
|||
views.videoView.isVisible = true
|
||||
|
||||
views.videoView.setOnPreparedListener {
|
||||
progressDisposable?.dispose()
|
||||
progressDisposable = Observable.interval(100, TimeUnit.MILLISECONDS)
|
||||
.timeInterval()
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe {
|
||||
stopTimer()
|
||||
countUpTimer = CountUpTimer(100).also {
|
||||
it.tickListener = object : CountUpTimer.TickListener {
|
||||
override fun onTick(milliseconds: Long) {
|
||||
val duration = views.videoView.duration
|
||||
val progress = views.videoView.currentPosition
|
||||
val isPlaying = views.videoView.isPlaying
|
||||
// Log.v("FOO", "isPlaying $isPlaying $progress/$duration")
|
||||
eventListener?.get()?.onEvent(AttachmentEvents.VideoEvent(isPlaying, progress, duration))
|
||||
}
|
||||
}
|
||||
it.resume()
|
||||
}
|
||||
}
|
||||
try {
|
||||
views.videoView.setVideoPath(mVideoPath)
|
||||
|
@ -134,6 +130,11 @@ class VideoViewHolder constructor(itemView: View) :
|
|||
}
|
||||
}
|
||||
|
||||
private fun stopTimer() {
|
||||
countUpTimer?.stop()
|
||||
countUpTimer = null
|
||||
}
|
||||
|
||||
override fun handleCommand(commands: AttachmentCommands) {
|
||||
if (!isSelected) return
|
||||
when (commands) {
|
||||
|
|
10
build.gradle
10
build.gradle
|
@ -153,13 +153,3 @@ project(":diff-match-patch") {
|
|||
// }
|
||||
// }
|
||||
//}
|
||||
//
|
||||
//project(":matrix-sdk-android-rx") {
|
||||
// sonarqube {
|
||||
// properties {
|
||||
// property "sonar.sources", project(":matrix-sdk-android-rx").android.sourceSets.main.java.srcDirs
|
||||
// // exclude source code from analyses separated by a colon (:)
|
||||
// // property "sonar.exclusions", "**/*.*"
|
||||
// }
|
||||
// }
|
||||
//}
|
||||
|
|
1
changelog.d/3932.bugfix
Normal file
1
changelog.d/3932.bugfix
Normal file
|
@ -0,0 +1 @@
|
|||
Explore Rooms overflow menu - content update include "Create room"
|
1
changelog.d/4669.bugfix
Normal file
1
changelog.d/4669.bugfix
Normal file
|
@ -0,0 +1 @@
|
|||
Fix sync timeout after returning from background
|
1
changelog.d/4811.feature
Normal file
1
changelog.d/4811.feature
Normal file
|
@ -0,0 +1 @@
|
|||
Enabling native support for window resizing
|
1
changelog.d/4865.misc
Normal file
1
changelog.d/4865.misc
Normal file
|
@ -0,0 +1 @@
|
|||
"/kick" command is replaced with "/remove". Also replaced all occurrences in string resources
|
1
changelog.d/4880.wip
Normal file
1
changelog.d/4880.wip
Normal file
|
@ -0,0 +1 @@
|
|||
Updates the onboarding carousel images, copy and improves the handling of different device sizes
|
1
changelog.d/4914.wip
Normal file
1
changelog.d/4914.wip
Normal file
|
@ -0,0 +1 @@
|
|||
Disabling onboarding automatic carousel transitions on user interaction
|
1
changelog.d/4918.wip
Normal file
1
changelog.d/4918.wip
Normal file
|
@ -0,0 +1 @@
|
|||
Locking phones to portrait during the FTUE onboarding
|
1
changelog.d/4927.wip
Normal file
1
changelog.d/4927.wip
Normal file
|
@ -0,0 +1 @@
|
|||
Adds a messaging use case screen to the FTUE onboarding
|
1
changelog.d/4942.misc
Normal file
1
changelog.d/4942.misc
Normal file
|
@ -0,0 +1 @@
|
|||
Remove unused module matrix-sdk-android-rx and do some cleanup
|
1
changelog.d/4948.bugfix
Normal file
1
changelog.d/4948.bugfix
Normal file
|
@ -0,0 +1 @@
|
|||
Prevent Alerts to be displayed in the automatically displayed analytics opt-in screen
|
1
changelog.d/4960.misc
Normal file
1
changelog.d/4960.misc
Normal file
|
@ -0,0 +1 @@
|
|||
Improves local echo blinking when non room events received
|
|
@ -42,7 +42,6 @@ ext.libs = [
|
|||
jetbrains : [
|
||||
'coroutinesCore' : "org.jetbrains.kotlinx:kotlinx-coroutines-core:$kotlinCoroutines",
|
||||
'coroutinesAndroid' : "org.jetbrains.kotlinx:kotlinx-coroutines-android:$kotlinCoroutines",
|
||||
'coroutinesRx2' : "org.jetbrains.kotlinx:kotlinx-coroutines-rx2:$kotlinCoroutines",
|
||||
'coroutinesTest' : "org.jetbrains.kotlinx:kotlinx-coroutines-test:$kotlinCoroutines"
|
||||
],
|
||||
androidx : [
|
||||
|
@ -87,8 +86,7 @@ ext.libs = [
|
|||
'retrofitMoshi' : "com.squareup.retrofit2:converter-moshi:$retrofit"
|
||||
],
|
||||
rx : [
|
||||
'rxKotlin' : "io.reactivex.rxjava2:rxkotlin:2.4.0",
|
||||
'rxAndroid' : "io.reactivex.rxjava2:rxandroid:2.1.1"
|
||||
'rxKotlin' : "io.reactivex.rxjava2:rxkotlin:2.4.0"
|
||||
],
|
||||
arrow : [
|
||||
'core' : "io.arrow-kt:arrow-core:$arrow",
|
||||
|
|
1
library/core-utils/.gitignore
vendored
Normal file
1
library/core-utils/.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
/build
|
55
library/core-utils/build.gradle
Normal file
55
library/core-utils/build.gradle
Normal file
|
@ -0,0 +1,55 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
plugins {
|
||||
id 'com.android.library'
|
||||
id 'kotlin-android'
|
||||
}
|
||||
|
||||
android {
|
||||
compileSdk versions.compileSdk
|
||||
defaultConfig {
|
||||
minSdk versions.minSdk
|
||||
targetSdk versions.targetSdk
|
||||
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
consumerProguardFiles "consumer-rules.pro"
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
minifyEnabled false
|
||||
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
|
||||
}
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility versions.sourceCompat
|
||||
targetCompatibility versions.targetCompat
|
||||
}
|
||||
|
||||
kotlinOptions {
|
||||
jvmTarget = "11"
|
||||
freeCompilerArgs += [
|
||||
"-Xopt-in=kotlin.RequiresOptIn"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation libs.androidx.appCompat
|
||||
implementation libs.jetbrains.coroutinesAndroid
|
||||
}
|
2
library/core-utils/src/main/AndroidManifest.xml
Normal file
2
library/core-utils/src/main/AndroidManifest.xml
Normal file
|
@ -0,0 +1,2 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest package="im.vector.lib.core.utils" />
|
|
@ -14,7 +14,7 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.app.core.flow
|
||||
package im.vector.lib.core.utils.flow
|
||||
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
|
@ -85,10 +85,12 @@ fun <T> Flow<T>.throttleFirst(windowDuration: Long): Flow<T> = flow {
|
|||
}
|
||||
}
|
||||
|
||||
@ExperimentalCoroutinesApi
|
||||
fun tickerFlow(scope: CoroutineScope, delayMillis: Long, initialDelayMillis: Long = delayMillis): Flow<Unit> {
|
||||
return scope.fixedPeriodTicker(delayMillis, initialDelayMillis).consumeAsFlow()
|
||||
}
|
||||
|
||||
@ExperimentalCoroutinesApi
|
||||
private fun CoroutineScope.fixedPeriodTicker(delayMillis: Long, initialDelayMillis: Long = delayMillis): ReceiveChannel<Unit> {
|
||||
require(delayMillis >= 0) { "Expected non-negative delay, but has $delayMillis ms" }
|
||||
require(initialDelayMillis >= 0) { "Expected non-negative initial delay, but has $initialDelayMillis ms" }
|
|
@ -14,9 +14,9 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.app.core.utils
|
||||
package im.vector.lib.core.utils.timer
|
||||
|
||||
import im.vector.app.core.flow.tickerFlow
|
||||
import im.vector.lib.core.utils.flow.tickerFlow
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.cancel
|
||||
|
@ -27,6 +27,7 @@ import kotlinx.coroutines.flow.onEach
|
|||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
import java.util.concurrent.atomic.AtomicLong
|
||||
|
||||
@OptIn(kotlinx.coroutines.ExperimentalCoroutinesApi::class)
|
||||
class CountUpTimer(private val intervalInMs: Long = 1_000) {
|
||||
|
||||
private val coroutineScope = CoroutineScope(Dispatchers.Main)
|
|
@ -0,0 +1,7 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<gradient
|
||||
android:angle="270"
|
||||
android:endColor="?vctr_system"
|
||||
android:startColor="#000000" />
|
||||
</shape>
|
5
library/ui-styles/src/main/res/values-h720dp/dimens.xml
Normal file
5
library/ui-styles/src/main/res/values-h720dp/dimens.xml
Normal file
|
@ -0,0 +1,5 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<item name="ftue_auth_carousel_item_spacing" format="float" type="dimen">0.05</item>
|
||||
<item name="ftue_auth_carousel_item_image_height" format="float" type="dimen">0.40</item>
|
||||
</resources>
|
|
@ -2,5 +2,6 @@
|
|||
<resources>
|
||||
|
||||
<dimen name="width_percent">0.6</dimen>
|
||||
<bool name="is_tablet">true</bool>
|
||||
|
||||
</resources>
|
|
@ -58,4 +58,7 @@
|
|||
<!-- Onboarding -->
|
||||
<item name="ftue_auth_gutter_start_percent" format="float" type="dimen">0.05</item>
|
||||
<item name="ftue_auth_gutter_end_percent" format="float" type="dimen">0.95</item>
|
||||
|
||||
<item name="ftue_auth_carousel_item_spacing" format="float" type="dimen">0.01</item>
|
||||
<item name="ftue_auth_carousel_item_image_height" format="float" type="dimen">0.35</item>
|
||||
</resources>
|
|
@ -2,5 +2,6 @@
|
|||
<resources>
|
||||
|
||||
<dimen name="width_percent">1</dimen>
|
||||
<bool name="is_tablet">false</bool>
|
||||
|
||||
</resources>
|
1
matrix-sdk-android-rx/.gitignore
vendored
1
matrix-sdk-android-rx/.gitignore
vendored
|
@ -1 +0,0 @@
|
|||
/build
|
|
@ -1,47 +0,0 @@
|
|||
apply plugin: 'com.android.library'
|
||||
apply plugin: 'kotlin-android'
|
||||
apply plugin: 'kotlin-kapt'
|
||||
|
||||
android {
|
||||
compileSdk versions.compileSdk
|
||||
|
||||
defaultConfig {
|
||||
minSdk versions.minSdk
|
||||
targetSdk versions.targetSdk
|
||||
|
||||
// Multidex is useful for tests
|
||||
multiDexEnabled true
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
minifyEnabled false
|
||||
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
|
||||
}
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility versions.sourceCompat
|
||||
targetCompatibility versions.targetCompat
|
||||
}
|
||||
|
||||
kotlinOptions {
|
||||
jvmTarget = "11"
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
|
||||
implementation project(":matrix-sdk-android")
|
||||
implementation libs.androidx.appCompat
|
||||
implementation libs.rx.rxKotlin
|
||||
implementation libs.rx.rxAndroid
|
||||
implementation libs.jetbrains.coroutinesRx2
|
||||
|
||||
// Paging
|
||||
implementation libs.androidx.pagingRuntimeKtx
|
||||
|
||||
// Logging
|
||||
implementation libs.jakewharton.timber
|
||||
}
|
21
matrix-sdk-android-rx/proguard-rules.pro
vendored
21
matrix-sdk-android-rx/proguard-rules.pro
vendored
|
@ -1,21 +0,0 @@
|
|||
# Add project specific ProGuard rules here.
|
||||
# You can control the set of applied configuration files using the
|
||||
# proguardFiles setting in build.gradle.
|
||||
#
|
||||
# For more details, see
|
||||
# http://developer.android.com/guide/developing/tools/proguard.html
|
||||
|
||||
# If your project uses WebView with JS, uncomment the following
|
||||
# and specify the fully qualified class name to the JavaScript interface
|
||||
# class:
|
||||
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
|
||||
# public *;
|
||||
#}
|
||||
|
||||
# Uncomment this to preserve the line number information for
|
||||
# debugging stack traces.
|
||||
#-keepattributes SourceFile,LineNumberTable
|
||||
|
||||
# If you keep the line number information, uncomment this to
|
||||
# hide the original source file name.
|
||||
#-renamesourcefileattribute SourceFile
|
|
@ -1 +0,0 @@
|
|||
<manifest package="org.matrix.android.sdk.rx" />
|
|
@ -1,71 +0,0 @@
|
|||
/*
|
||||
* Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.matrix.android.sdk.rx
|
||||
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.Observer
|
||||
import io.reactivex.Observable
|
||||
import io.reactivex.android.MainThreadDisposable
|
||||
import io.reactivex.android.schedulers.AndroidSchedulers
|
||||
import io.reactivex.schedulers.Schedulers
|
||||
|
||||
private class LiveDataObservable<T>(
|
||||
private val liveData: LiveData<T>,
|
||||
private val valueIfNull: T? = null
|
||||
) : Observable<T>() {
|
||||
|
||||
override fun subscribeActual(observer: io.reactivex.Observer<in T>) {
|
||||
val relay = RemoveObserverInMainThread(observer)
|
||||
observer.onSubscribe(relay)
|
||||
liveData.observeForever(relay)
|
||||
}
|
||||
|
||||
private inner class RemoveObserverInMainThread(private val observer: io.reactivex.Observer<in T>) :
|
||||
MainThreadDisposable(), Observer<T> {
|
||||
|
||||
override fun onChanged(t: T?) {
|
||||
if (!isDisposed) {
|
||||
if (t == null) {
|
||||
if (valueIfNull != null) {
|
||||
observer.onNext(valueIfNull)
|
||||
} else {
|
||||
observer.onError(NullPointerException(
|
||||
"convert liveData value t to RxJava onNext(t), t cannot be null"))
|
||||
}
|
||||
} else {
|
||||
observer.onNext(t)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDispose() {
|
||||
liveData.removeObserver(this)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun <T> LiveData<T>.asObservable(): Observable<T> {
|
||||
return LiveDataObservable(this).observeOn(Schedulers.computation())
|
||||
}
|
||||
|
||||
internal fun <T> Observable<T>.startWithCallable(supplier: () -> T): Observable<T> {
|
||||
val startObservable = Observable
|
||||
.fromCallable(supplier)
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
return startWith(startObservable)
|
||||
}
|
|
@ -1,30 +0,0 @@
|
|||
/*
|
||||
* Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.matrix.android.sdk.rx
|
||||
|
||||
import io.reactivex.Observable
|
||||
import org.matrix.android.sdk.api.util.Optional
|
||||
|
||||
fun <T : Any> Observable<Optional<T>>.unwrap(): Observable<T> {
|
||||
return filter { it.hasValue() }.map { it.get() }
|
||||
}
|
||||
|
||||
fun <T : Any, U : Any> Observable<Optional<T>>.mapOptional(fn: (T) -> U?): Observable<Optional<U>> {
|
||||
return map {
|
||||
it.map(fn)
|
||||
}
|
||||
}
|
|
@ -1,158 +0,0 @@
|
|||
/*
|
||||
* Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.matrix.android.sdk.rx
|
||||
|
||||
import android.net.Uri
|
||||
import io.reactivex.Completable
|
||||
import io.reactivex.Observable
|
||||
import io.reactivex.Single
|
||||
import kotlinx.coroutines.rx2.rxCompletable
|
||||
import kotlinx.coroutines.rx2.rxSingle
|
||||
import org.matrix.android.sdk.api.query.QueryStringValue
|
||||
import org.matrix.android.sdk.api.session.content.ContentAttachmentData
|
||||
import org.matrix.android.sdk.api.session.events.model.Event
|
||||
import org.matrix.android.sdk.api.session.identity.ThreePid
|
||||
import org.matrix.android.sdk.api.session.room.Room
|
||||
import org.matrix.android.sdk.api.session.room.members.RoomMemberQueryParams
|
||||
import org.matrix.android.sdk.api.session.room.model.EventAnnotationsSummary
|
||||
import org.matrix.android.sdk.api.session.room.model.GuestAccess
|
||||
import org.matrix.android.sdk.api.session.room.model.ReadReceipt
|
||||
import org.matrix.android.sdk.api.session.room.model.RoomHistoryVisibility
|
||||
import org.matrix.android.sdk.api.session.room.model.RoomJoinRules
|
||||
import org.matrix.android.sdk.api.session.room.model.RoomMemberSummary
|
||||
import org.matrix.android.sdk.api.session.room.model.RoomSummary
|
||||
import org.matrix.android.sdk.api.session.room.notification.RoomNotificationState
|
||||
import org.matrix.android.sdk.api.session.room.send.UserDraft
|
||||
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
|
||||
import org.matrix.android.sdk.api.util.Optional
|
||||
import org.matrix.android.sdk.api.util.toOptional
|
||||
|
||||
class RxRoom(private val room: Room) {
|
||||
|
||||
fun liveRoomSummary(): Observable<Optional<RoomSummary>> {
|
||||
return room.getRoomSummaryLive()
|
||||
.asObservable()
|
||||
.startWithCallable { room.roomSummary().toOptional() }
|
||||
}
|
||||
|
||||
fun liveRoomMembers(queryParams: RoomMemberQueryParams): Observable<List<RoomMemberSummary>> {
|
||||
return room.getRoomMembersLive(queryParams).asObservable()
|
||||
.startWithCallable {
|
||||
room.getRoomMembers(queryParams)
|
||||
}
|
||||
}
|
||||
|
||||
fun liveAnnotationSummary(eventId: String): Observable<Optional<EventAnnotationsSummary>> {
|
||||
return room.getEventAnnotationsSummaryLive(eventId).asObservable()
|
||||
.startWithCallable {
|
||||
room.getEventAnnotationsSummary(eventId).toOptional()
|
||||
}
|
||||
}
|
||||
|
||||
fun liveTimelineEvent(eventId: String): Observable<Optional<TimelineEvent>> {
|
||||
return room.getTimeLineEventLive(eventId).asObservable()
|
||||
.startWithCallable {
|
||||
room.getTimeLineEvent(eventId).toOptional()
|
||||
}
|
||||
}
|
||||
|
||||
fun liveStateEvent(eventType: String, stateKey: QueryStringValue): Observable<Optional<Event>> {
|
||||
return room.getStateEventLive(eventType, stateKey).asObservable()
|
||||
.startWithCallable {
|
||||
room.getStateEvent(eventType, stateKey).toOptional()
|
||||
}
|
||||
}
|
||||
|
||||
fun liveStateEvents(eventTypes: Set<String>): Observable<List<Event>> {
|
||||
return room.getStateEventsLive(eventTypes).asObservable()
|
||||
.startWithCallable {
|
||||
room.getStateEvents(eventTypes)
|
||||
}
|
||||
}
|
||||
|
||||
fun liveReadMarker(): Observable<Optional<String>> {
|
||||
return room.getReadMarkerLive().asObservable()
|
||||
}
|
||||
|
||||
fun liveReadReceipt(): Observable<Optional<String>> {
|
||||
return room.getMyReadReceiptLive().asObservable()
|
||||
}
|
||||
|
||||
fun loadRoomMembersIfNeeded(): Single<Unit> = rxSingle {
|
||||
room.loadRoomMembersIfNeeded()
|
||||
}
|
||||
|
||||
fun joinRoom(reason: String? = null,
|
||||
viaServers: List<String> = emptyList()): Single<Unit> = rxSingle {
|
||||
room.join(reason, viaServers)
|
||||
}
|
||||
|
||||
fun liveEventReadReceipts(eventId: String): Observable<List<ReadReceipt>> {
|
||||
return room.getEventReadReceiptsLive(eventId).asObservable()
|
||||
}
|
||||
|
||||
fun liveDraft(): Observable<Optional<UserDraft>> {
|
||||
return room.getDraftLive().asObservable()
|
||||
.startWithCallable {
|
||||
room.getDraft().toOptional()
|
||||
}
|
||||
}
|
||||
|
||||
fun liveNotificationState(): Observable<RoomNotificationState> {
|
||||
return room.getLiveRoomNotificationState().asObservable()
|
||||
}
|
||||
|
||||
fun invite(userId: String, reason: String? = null): Completable = rxCompletable {
|
||||
room.invite(userId, reason)
|
||||
}
|
||||
|
||||
fun invite3pid(threePid: ThreePid): Completable = rxCompletable {
|
||||
room.invite3pid(threePid)
|
||||
}
|
||||
|
||||
fun updateTopic(topic: String): Completable = rxCompletable {
|
||||
room.updateTopic(topic)
|
||||
}
|
||||
|
||||
fun updateName(name: String): Completable = rxCompletable {
|
||||
room.updateName(name)
|
||||
}
|
||||
|
||||
fun updateHistoryReadability(readability: RoomHistoryVisibility): Completable = rxCompletable {
|
||||
room.updateHistoryReadability(readability)
|
||||
}
|
||||
|
||||
fun updateJoinRule(joinRules: RoomJoinRules?, guestAccess: GuestAccess?): Completable = rxCompletable {
|
||||
room.updateJoinRule(joinRules, guestAccess)
|
||||
}
|
||||
|
||||
fun updateAvatar(avatarUri: Uri, fileName: String): Completable = rxCompletable {
|
||||
room.updateAvatar(avatarUri, fileName)
|
||||
}
|
||||
|
||||
fun deleteAvatar(): Completable = rxCompletable {
|
||||
room.deleteAvatar()
|
||||
}
|
||||
|
||||
fun sendMedia(attachment: ContentAttachmentData, compressBeforeSending: Boolean, roomIds: Set<String>): Completable = rxCompletable {
|
||||
room.sendMedia(attachment, compressBeforeSending, roomIds)
|
||||
}
|
||||
}
|
||||
|
||||
fun Room.rx(): RxRoom {
|
||||
return RxRoom(this)
|
||||
}
|
|
@ -1,251 +0,0 @@
|
|||
/*
|
||||
* Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.matrix.android.sdk.rx
|
||||
|
||||
import androidx.paging.PagedList
|
||||
import io.reactivex.Observable
|
||||
import io.reactivex.Single
|
||||
import io.reactivex.functions.Function3
|
||||
import kotlinx.coroutines.rx2.rxSingle
|
||||
import org.matrix.android.sdk.api.extensions.orFalse
|
||||
import org.matrix.android.sdk.api.query.QueryStringValue
|
||||
import org.matrix.android.sdk.api.session.Session
|
||||
import org.matrix.android.sdk.api.session.accountdata.UserAccountDataEvent
|
||||
import org.matrix.android.sdk.api.session.crypto.crosssigning.KEYBACKUP_SECRET_SSSS_NAME
|
||||
import org.matrix.android.sdk.api.session.crypto.crosssigning.MASTER_KEY_SSSS_NAME
|
||||
import org.matrix.android.sdk.api.session.crypto.crosssigning.MXCrossSigningInfo
|
||||
import org.matrix.android.sdk.api.session.crypto.crosssigning.SELF_SIGNING_KEY_SSSS_NAME
|
||||
import org.matrix.android.sdk.api.session.crypto.crosssigning.USER_SIGNING_KEY_SSSS_NAME
|
||||
import org.matrix.android.sdk.api.session.group.GroupSummaryQueryParams
|
||||
import org.matrix.android.sdk.api.session.group.model.GroupSummary
|
||||
import org.matrix.android.sdk.api.session.identity.FoundThreePid
|
||||
import org.matrix.android.sdk.api.session.identity.ThreePid
|
||||
import org.matrix.android.sdk.api.session.pushers.Pusher
|
||||
import org.matrix.android.sdk.api.session.room.RoomSummaryQueryParams
|
||||
import org.matrix.android.sdk.api.session.room.accountdata.RoomAccountDataEvent
|
||||
import org.matrix.android.sdk.api.session.room.members.ChangeMembershipState
|
||||
import org.matrix.android.sdk.api.session.room.model.RoomMemberSummary
|
||||
import org.matrix.android.sdk.api.session.room.model.RoomSummary
|
||||
import org.matrix.android.sdk.api.session.room.model.create.CreateRoomParams
|
||||
import org.matrix.android.sdk.api.session.space.SpaceSummaryQueryParams
|
||||
import org.matrix.android.sdk.api.session.sync.SyncState
|
||||
import org.matrix.android.sdk.api.session.user.model.User
|
||||
import org.matrix.android.sdk.api.session.widgets.model.Widget
|
||||
import org.matrix.android.sdk.api.util.JsonDict
|
||||
import org.matrix.android.sdk.api.util.Optional
|
||||
import org.matrix.android.sdk.api.util.toOptional
|
||||
import org.matrix.android.sdk.internal.crypto.model.CryptoDeviceInfo
|
||||
import org.matrix.android.sdk.internal.crypto.model.rest.DeviceInfo
|
||||
import org.matrix.android.sdk.internal.crypto.store.PrivateKeysInfo
|
||||
import org.matrix.android.sdk.internal.session.room.alias.RoomAliasDescription
|
||||
|
||||
class RxSession(private val session: Session) {
|
||||
|
||||
fun liveRoomSummaries(queryParams: RoomSummaryQueryParams): Observable<List<RoomSummary>> {
|
||||
return session.getRoomSummariesLive(queryParams).asObservable()
|
||||
.startWithCallable {
|
||||
session.getRoomSummaries(queryParams)
|
||||
}
|
||||
}
|
||||
|
||||
fun liveGroupSummaries(queryParams: GroupSummaryQueryParams): Observable<List<GroupSummary>> {
|
||||
return session.getGroupSummariesLive(queryParams).asObservable()
|
||||
.startWithCallable {
|
||||
session.getGroupSummaries(queryParams)
|
||||
}
|
||||
}
|
||||
|
||||
fun liveSpaceSummaries(queryParams: SpaceSummaryQueryParams): Observable<List<RoomSummary>> {
|
||||
return session.spaceService().getSpaceSummariesLive(queryParams).asObservable()
|
||||
.startWithCallable {
|
||||
session.spaceService().getSpaceSummaries(queryParams)
|
||||
}
|
||||
}
|
||||
|
||||
fun liveBreadcrumbs(queryParams: RoomSummaryQueryParams): Observable<List<RoomSummary>> {
|
||||
return session.getBreadcrumbsLive(queryParams).asObservable()
|
||||
.startWithCallable {
|
||||
session.getBreadcrumbs(queryParams)
|
||||
}
|
||||
}
|
||||
|
||||
fun liveMyDevicesInfo(): Observable<List<DeviceInfo>> {
|
||||
return session.cryptoService().getLiveMyDevicesInfo().asObservable()
|
||||
.startWithCallable {
|
||||
session.cryptoService().getMyDevicesInfo()
|
||||
}
|
||||
}
|
||||
|
||||
fun liveSyncState(): Observable<SyncState> {
|
||||
return session.getSyncStateLive().asObservable()
|
||||
}
|
||||
|
||||
fun livePushers(): Observable<List<Pusher>> {
|
||||
return session.getPushersLive().asObservable()
|
||||
}
|
||||
|
||||
fun liveUser(userId: String): Observable<Optional<User>> {
|
||||
return session.getUserLive(userId).asObservable()
|
||||
.startWithCallable {
|
||||
session.getUser(userId).toOptional()
|
||||
}
|
||||
}
|
||||
|
||||
fun liveRoomMember(userId: String, roomId: String): Observable<Optional<RoomMemberSummary>> {
|
||||
return session.getRoomMemberLive(userId, roomId).asObservable()
|
||||
.startWithCallable {
|
||||
session.getRoomMember(userId, roomId).toOptional()
|
||||
}
|
||||
}
|
||||
|
||||
fun liveUsers(): Observable<List<User>> {
|
||||
return session.getUsersLive().asObservable()
|
||||
}
|
||||
|
||||
fun liveIgnoredUsers(): Observable<List<User>> {
|
||||
return session.getIgnoredUsersLive().asObservable()
|
||||
}
|
||||
|
||||
fun livePagedUsers(filter: String? = null, excludedUserIds: Set<String>? = null): Observable<PagedList<User>> {
|
||||
return session.getPagedUsersLive(filter, excludedUserIds).asObservable()
|
||||
}
|
||||
|
||||
fun liveThreePIds(refreshData: Boolean): Observable<List<ThreePid>> {
|
||||
return session.getThreePidsLive(refreshData).asObservable()
|
||||
.startWithCallable { session.getThreePids() }
|
||||
}
|
||||
|
||||
fun livePendingThreePIds(): Observable<List<ThreePid>> {
|
||||
return session.getPendingThreePidsLive().asObservable()
|
||||
.startWithCallable { session.getPendingThreePids() }
|
||||
}
|
||||
|
||||
fun createRoom(roomParams: CreateRoomParams): Single<String> = rxSingle {
|
||||
session.createRoom(roomParams)
|
||||
}
|
||||
|
||||
fun searchUsersDirectory(search: String,
|
||||
limit: Int,
|
||||
excludedUserIds: Set<String>): Single<List<User>> = rxSingle {
|
||||
session.searchUsersDirectory(search, limit, excludedUserIds)
|
||||
}
|
||||
|
||||
fun joinRoom(roomIdOrAlias: String,
|
||||
reason: String? = null,
|
||||
viaServers: List<String> = emptyList()): Single<Unit> = rxSingle {
|
||||
session.joinRoom(roomIdOrAlias, reason, viaServers)
|
||||
}
|
||||
|
||||
fun getRoomIdByAlias(roomAlias: String,
|
||||
searchOnServer: Boolean): Single<Optional<RoomAliasDescription>> = rxSingle {
|
||||
session.getRoomIdByAlias(roomAlias, searchOnServer)
|
||||
}
|
||||
|
||||
fun getProfileInfo(userId: String): Single<JsonDict> = rxSingle {
|
||||
session.getProfile(userId)
|
||||
}
|
||||
|
||||
fun liveUserCryptoDevices(userId: String): Observable<List<CryptoDeviceInfo>> {
|
||||
return session.cryptoService().getLiveCryptoDeviceInfo(userId).asObservable().startWithCallable {
|
||||
session.cryptoService().getCryptoDeviceInfo(userId)
|
||||
}
|
||||
}
|
||||
|
||||
fun liveCrossSigningInfo(userId: String): Observable<Optional<MXCrossSigningInfo>> {
|
||||
return session.cryptoService().crossSigningService().getLiveCrossSigningKeys(userId).asObservable()
|
||||
.startWithCallable {
|
||||
session.cryptoService().crossSigningService().getUserCrossSigningKeys(userId).toOptional()
|
||||
}
|
||||
}
|
||||
|
||||
fun liveCrossSigningPrivateKeys(): Observable<Optional<PrivateKeysInfo>> {
|
||||
return session.cryptoService().crossSigningService().getLiveCrossSigningPrivateKeys().asObservable()
|
||||
.startWithCallable {
|
||||
session.cryptoService().crossSigningService().getCrossSigningPrivateKeys().toOptional()
|
||||
}
|
||||
}
|
||||
|
||||
fun liveUserAccountData(types: Set<String>): Observable<List<UserAccountDataEvent>> {
|
||||
return session.accountDataService().getLiveUserAccountDataEvents(types).asObservable()
|
||||
.startWithCallable {
|
||||
session.accountDataService().getUserAccountDataEvents(types)
|
||||
}
|
||||
}
|
||||
|
||||
fun liveRoomAccountData(types: Set<String>): Observable<List<RoomAccountDataEvent>> {
|
||||
return session.accountDataService().getLiveRoomAccountDataEvents(types).asObservable()
|
||||
.startWithCallable {
|
||||
session.accountDataService().getRoomAccountDataEvents(types)
|
||||
}
|
||||
}
|
||||
|
||||
fun liveRoomWidgets(
|
||||
roomId: String,
|
||||
widgetId: QueryStringValue,
|
||||
widgetTypes: Set<String>? = null,
|
||||
excludedTypes: Set<String>? = null
|
||||
): Observable<List<Widget>> {
|
||||
return session.widgetService().getRoomWidgetsLive(roomId, widgetId, widgetTypes, excludedTypes).asObservable()
|
||||
.startWithCallable {
|
||||
session.widgetService().getRoomWidgets(roomId, widgetId, widgetTypes, excludedTypes)
|
||||
}
|
||||
}
|
||||
|
||||
fun liveRoomChangeMembershipState(): Observable<Map<String, ChangeMembershipState>> {
|
||||
return session.getChangeMembershipsLive().asObservable()
|
||||
}
|
||||
|
||||
fun liveSecretSynchronisationInfo(): Observable<SecretsSynchronisationInfo> {
|
||||
return Observable.combineLatest<List<UserAccountDataEvent>, Optional<MXCrossSigningInfo>, Optional<PrivateKeysInfo>, SecretsSynchronisationInfo>(
|
||||
liveUserAccountData(setOf(MASTER_KEY_SSSS_NAME, USER_SIGNING_KEY_SSSS_NAME, SELF_SIGNING_KEY_SSSS_NAME, KEYBACKUP_SECRET_SSSS_NAME)),
|
||||
liveCrossSigningInfo(session.myUserId),
|
||||
liveCrossSigningPrivateKeys(),
|
||||
Function3 { _, crossSigningInfo, pInfo ->
|
||||
// first check if 4S is already setup
|
||||
val is4SSetup = session.sharedSecretStorageService.isRecoverySetup()
|
||||
val isCrossSigningEnabled = crossSigningInfo.getOrNull() != null
|
||||
val isCrossSigningTrusted = crossSigningInfo.getOrNull()?.isTrusted() == true
|
||||
val allPrivateKeysKnown = pInfo.getOrNull()?.allKnown().orFalse()
|
||||
|
||||
val keysBackupService = session.cryptoService().keysBackupService()
|
||||
val currentBackupVersion = keysBackupService.currentBackupVersion
|
||||
val megolmBackupAvailable = currentBackupVersion != null
|
||||
val savedBackupKey = keysBackupService.getKeyBackupRecoveryKeyInfo()
|
||||
|
||||
val megolmKeyKnown = savedBackupKey?.version == currentBackupVersion
|
||||
SecretsSynchronisationInfo(
|
||||
isBackupSetup = is4SSetup,
|
||||
isCrossSigningEnabled = isCrossSigningEnabled,
|
||||
isCrossSigningTrusted = isCrossSigningTrusted,
|
||||
allPrivateKeysKnown = allPrivateKeysKnown,
|
||||
megolmBackupAvailable = megolmBackupAvailable,
|
||||
megolmSecretKnown = megolmKeyKnown,
|
||||
isMegolmKeyIn4S = session.sharedSecretStorageService.isMegolmKeyInBackup()
|
||||
)
|
||||
}
|
||||
)
|
||||
.distinctUntilChanged()
|
||||
}
|
||||
|
||||
fun lookupThreePid(threePid: ThreePid): Single<Optional<FoundThreePid>> = rxSingle {
|
||||
session.identityService().lookUp(listOf(threePid)).firstOrNull().toOptional()
|
||||
}
|
||||
}
|
||||
|
||||
fun Session.rx(): RxSession {
|
||||
return RxSession(this)
|
||||
}
|
|
@ -116,6 +116,11 @@ dependencies {
|
|||
implementation libs.squareup.retrofit
|
||||
implementation libs.squareup.retrofitMoshi
|
||||
|
||||
// When version of okhttp is updated (current is 4.9.3), consider removing the workaround
|
||||
// to force usage of Protocol.HTTP_1_1. Check the status of:
|
||||
// - https://github.com/square/okhttp/issues/3278
|
||||
// - https://github.com/square/okhttp/issues/4455
|
||||
// - https://github.com/square/okhttp/issues/3146
|
||||
implementation(platform("com.squareup.okhttp3:okhttp-bom:4.9.3"))
|
||||
implementation 'com.squareup.okhttp3:okhttp'
|
||||
implementation 'com.squareup.okhttp3:logging-interceptor'
|
||||
|
|
|
@ -75,9 +75,12 @@ interface MembershipService {
|
|||
suspend fun unban(userId: String, reason: String? = null)
|
||||
|
||||
/**
|
||||
* Kick a user from the room
|
||||
* Remove a user from the room
|
||||
*/
|
||||
suspend fun kick(userId: String, reason: String? = null)
|
||||
suspend fun remove(userId: String, reason: String? = null)
|
||||
|
||||
@Deprecated("Use remove instead", ReplaceWith("remove(userId, reason)"))
|
||||
suspend fun kick(userId: String, reason: String? = null) = remove(userId, reason)
|
||||
|
||||
/**
|
||||
* Join the room, or accept an invitation.
|
||||
|
|
|
@ -22,6 +22,7 @@ import dagger.Module
|
|||
import dagger.Provides
|
||||
import okhttp3.ConnectionSpec
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Protocol
|
||||
import okhttp3.logging.HttpLoggingInterceptor
|
||||
import org.matrix.android.sdk.BuildConfig
|
||||
import org.matrix.android.sdk.api.MatrixConfiguration
|
||||
|
@ -71,6 +72,8 @@ internal object NetworkModule {
|
|||
val spec = ConnectionSpec.Builder(matrixConfiguration.connectionSpec).build()
|
||||
|
||||
return OkHttpClient.Builder()
|
||||
// workaround for #4669
|
||||
.protocols(listOf(Protocol.HTTP_1_1))
|
||||
.connectTimeout(30, TimeUnit.SECONDS)
|
||||
.readTimeout(60, TimeUnit.SECONDS)
|
||||
.writeTimeout(60, TimeUnit.SECONDS)
|
||||
|
|
|
@ -125,7 +125,7 @@ internal class DefaultMembershipService @AssistedInject constructor(
|
|||
membershipAdminTask.execute(params)
|
||||
}
|
||||
|
||||
override suspend fun kick(userId: String, reason: String?) {
|
||||
override suspend fun remove(userId: String, reason: String?) {
|
||||
val params = MembershipAdminTask.Params(MembershipAdminTask.Type.KICK, roomId, userId, reason)
|
||||
membershipAdminTask.execute(params)
|
||||
}
|
||||
|
|
|
@ -129,8 +129,6 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle
|
|||
} else {
|
||||
handlingStrategy.data.mapWithProgress(reporter, InitSyncStep.ImportingAccountJoinedRooms, 0.6f) {
|
||||
handleJoinedRoom(realm, it.key, it.value, insertType, syncLocalTimeStampMillis, aggregator)
|
||||
}.also {
|
||||
fixStuckLocalEcho(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -428,6 +426,10 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Handle deletion of [stuck] local echos if needed
|
||||
deleteLocalEchosIfNeeded(insertType, roomEntity, eventList)
|
||||
|
||||
// posting new events to timeline if any is registered
|
||||
timelineInput.onNewTimelineEvents(roomId = roomId, eventIds = eventIds)
|
||||
return chunkEntity
|
||||
|
@ -513,14 +515,16 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle
|
|||
* While we cannot know when a specific event arrived from the pagination (no transactionId included), after each room /sync
|
||||
* we clear all SENT events, and we are sure that we will receive it from /sync or pagination
|
||||
*/
|
||||
private fun fixStuckLocalEcho(rooms: List<RoomEntity>) {
|
||||
rooms.forEach { roomEntity ->
|
||||
roomEntity.sendingTimelineEvents.filter { timelineEvent ->
|
||||
timelineEvent.root?.sendState == SendState.SENT
|
||||
}.forEach {
|
||||
roomEntity.sendingTimelineEvents.remove(it)
|
||||
it.deleteOnCascade(true)
|
||||
}
|
||||
private fun deleteLocalEchosIfNeeded(insertType: EventInsertType, roomEntity: RoomEntity, eventList: List<Event>) {
|
||||
// Skip deletion if we are on initial sync
|
||||
if (insertType == EventInsertType.INITIAL_SYNC) return
|
||||
// Skip deletion if there are no timeline events or there is no event received from the current user
|
||||
if (eventList.firstOrNull { it.senderId == userId } == null) return
|
||||
roomEntity.sendingTimelineEvents.filter { timelineEvent ->
|
||||
timelineEvent.root?.sendState == SendState.SENT
|
||||
}.forEach {
|
||||
roomEntity.sendingTimelineEvents.remove(it)
|
||||
it.deleteOnCascade(true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
include ':vector'
|
||||
include ':matrix-sdk-android'
|
||||
include ':matrix-sdk-android-rx'
|
||||
include ':diff-match-patch'
|
||||
include ':attachment-viewer'
|
||||
include ':multipicker'
|
||||
include ':library:core-utils'
|
||||
include ':library:ui-styles'
|
||||
include ':matrix-sdk-android-flow'
|
||||
|
|
|
@ -15,13 +15,18 @@
|
|||
name = "Bugfixes 🐛"
|
||||
showcontent = true
|
||||
|
||||
[[tool.towncrier.type]]
|
||||
directory = "wip"
|
||||
name = "In development 🚧"
|
||||
showcontent = true
|
||||
|
||||
[[tool.towncrier.type]]
|
||||
directory = "doc"
|
||||
name = "Improved Documentation 📚"
|
||||
showcontent = true
|
||||
|
||||
[[tool.towncrier.type]]
|
||||
directory = "removal"
|
||||
directory = "sdk"
|
||||
name = "SDK API changes ⚠️"
|
||||
showcontent = true
|
||||
|
||||
|
|
|
@ -333,6 +333,7 @@ dependencies {
|
|||
implementation project(":multipicker")
|
||||
implementation project(":attachment-viewer")
|
||||
implementation project(":library:ui-styles")
|
||||
implementation project(":library:core-utils")
|
||||
implementation 'androidx.multidex:multidex:2.0.1'
|
||||
|
||||
implementation libs.jetbrains.coroutinesCore
|
||||
|
|
|
@ -36,13 +36,18 @@ class DebugFeaturesStateFactory @Inject constructor(
|
|||
),
|
||||
createBooleanFeature(
|
||||
label = "FTUE Splash - I already have an account",
|
||||
factory = VectorFeatures::isAlreadyHaveAccountSplashEnabled,
|
||||
key = DebugFeatureKeys.alreadyHaveAnAccount
|
||||
key = DebugFeatureKeys.onboardingAlreadyHaveAnAccount,
|
||||
factory = VectorFeatures::isOnboardingAlreadyHaveAccountSplashEnabled
|
||||
),
|
||||
createBooleanFeature(
|
||||
label = "FTUE Splash - Carousel",
|
||||
factory = VectorFeatures::isSplashCarouselEnabled,
|
||||
key = DebugFeatureKeys.splashCarousel
|
||||
label = "FTUE Splash - carousel",
|
||||
key = DebugFeatureKeys.onboardingSplashCarousel,
|
||||
factory = VectorFeatures::isOnboardingSplashCarouselEnabled
|
||||
),
|
||||
createBooleanFeature(
|
||||
label = "FTUE Use Case",
|
||||
key = DebugFeatureKeys.onboardingUseCase,
|
||||
factory = VectorFeatures::isOnboardingUseCaseEnabled
|
||||
)
|
||||
))
|
||||
}
|
||||
|
|
|
@ -43,10 +43,13 @@ class DebugVectorFeatures(
|
|||
return readPreferences().getEnum<VectorFeatures.OnboardingVariant>() ?: vectorFeatures.onboardingVariant()
|
||||
}
|
||||
|
||||
override fun isAlreadyHaveAccountSplashEnabled(): Boolean = read(DebugFeatureKeys.alreadyHaveAnAccount)
|
||||
?: vectorFeatures.isAlreadyHaveAccountSplashEnabled()
|
||||
override fun isOnboardingAlreadyHaveAccountSplashEnabled(): Boolean = read(DebugFeatureKeys.onboardingAlreadyHaveAnAccount)
|
||||
?: vectorFeatures.isOnboardingAlreadyHaveAccountSplashEnabled()
|
||||
|
||||
override fun isSplashCarouselEnabled(): Boolean = read(DebugFeatureKeys.splashCarousel) ?: vectorFeatures.isSplashCarouselEnabled()
|
||||
override fun isOnboardingSplashCarouselEnabled(): Boolean = read(DebugFeatureKeys.onboardingSplashCarousel)
|
||||
?: vectorFeatures.isOnboardingSplashCarouselEnabled()
|
||||
|
||||
override fun isOnboardingUseCaseEnabled(): Boolean = read(DebugFeatureKeys.onboardingUseCase) ?: vectorFeatures.isOnboardingUseCaseEnabled()
|
||||
|
||||
fun <T> override(value: T?, key: Preferences.Key<T>) = updatePreferences {
|
||||
if (value == null) {
|
||||
|
@ -96,6 +99,7 @@ private inline fun <reified T : Enum<T>> enumPreferencesKey() = enumPreferencesK
|
|||
private fun <T : Enum<T>> enumPreferencesKey(type: KClass<T>) = stringPreferencesKey("enum-${type.simpleName}")
|
||||
|
||||
object DebugFeatureKeys {
|
||||
val alreadyHaveAnAccount = booleanPreferencesKey("already-have-an-account")
|
||||
val splashCarousel = booleanPreferencesKey("splash-carousel")
|
||||
val onboardingAlreadyHaveAnAccount = booleanPreferencesKey("onboarding-already-have-an-account")
|
||||
val onboardingSplashCarousel = booleanPreferencesKey("onboarding-splash-carousel")
|
||||
val onboardingUseCase = booleanPreferencesKey("onbboarding-splash-carousel")
|
||||
}
|
||||
|
|
|
@ -76,6 +76,7 @@
|
|||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="@string/app_name"
|
||||
android:networkSecurityConfig="@xml/network_security_config"
|
||||
android:resizeableActivity="true"
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/Theme.Vector.Light"
|
||||
|
@ -400,7 +401,8 @@
|
|||
android:value="androidx.startup"
|
||||
tools:node="remove" />
|
||||
<!-- We init the lib ourself in EmojiCompatWrapper -->
|
||||
<meta-data android:name="androidx.emoji2.text.EmojiCompatInitializer"
|
||||
<meta-data
|
||||
android:name="androidx.emoji2.text.EmojiCompatInitializer"
|
||||
tools:node="remove" />
|
||||
</provider>
|
||||
|
||||
|
|
|
@ -61,7 +61,7 @@ class AppStateHandler @Inject constructor(
|
|||
private val coroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.Main)
|
||||
private val selectedSpaceDataSource = BehaviorDataSource<Option<RoomGroupingMethod>>(Option.empty())
|
||||
|
||||
val selectedRoomGroupingObservable = selectedSpaceDataSource.stream()
|
||||
val selectedRoomGroupingFlow = selectedSpaceDataSource.stream()
|
||||
|
||||
fun getCurrentRoomGroupingMethod(): RoomGroupingMethod? {
|
||||
// XXX we should somehow make it live :/ just a work around
|
||||
|
|
|
@ -31,7 +31,7 @@ import javax.inject.Inject
|
|||
import javax.inject.Singleton
|
||||
|
||||
@Singleton
|
||||
class ActiveSessionHolder @Inject constructor(private val sessionObservableStore: ActiveSessionDataSource,
|
||||
class ActiveSessionHolder @Inject constructor(private val activeSessionDataSource: ActiveSessionDataSource,
|
||||
private val keyRequestHandler: KeyRequestHandler,
|
||||
private val incomingVerificationRequestHandler: IncomingVerificationRequestHandler,
|
||||
private val callManager: WebRtcCallManager,
|
||||
|
@ -46,7 +46,7 @@ class ActiveSessionHolder @Inject constructor(private val sessionObservableStore
|
|||
fun setActiveSession(session: Session) {
|
||||
Timber.w("setActiveSession of ${session.myUserId}")
|
||||
activeSession.set(session)
|
||||
sessionObservableStore.post(Option.just(session))
|
||||
activeSessionDataSource.post(Option.just(session))
|
||||
|
||||
keyRequestHandler.start(session)
|
||||
incomingVerificationRequestHandler.start(session)
|
||||
|
@ -66,7 +66,7 @@ class ActiveSessionHolder @Inject constructor(private val sessionObservableStore
|
|||
}
|
||||
|
||||
activeSession.set(null)
|
||||
sessionObservableStore.post(Option.empty())
|
||||
activeSessionDataSource.post(Option.empty())
|
||||
|
||||
keyRequestHandler.stop()
|
||||
incomingVerificationRequestHandler.stop()
|
||||
|
|
|
@ -104,6 +104,7 @@ import im.vector.app.features.onboarding.ftueauth.FtueAuthServerSelectionFragmen
|
|||
import im.vector.app.features.onboarding.ftueauth.FtueAuthSignUpSignInSelectionFragment
|
||||
import im.vector.app.features.onboarding.ftueauth.FtueAuthSplashCarouselFragment
|
||||
import im.vector.app.features.onboarding.ftueauth.FtueAuthSplashFragment
|
||||
import im.vector.app.features.onboarding.ftueauth.FtueAuthUseCaseFragment
|
||||
import im.vector.app.features.onboarding.ftueauth.FtueAuthWaitForEmailFragment
|
||||
import im.vector.app.features.onboarding.ftueauth.FtueAuthWebFragment
|
||||
import im.vector.app.features.onboarding.ftueauth.terms.FtueAuthTermsFragment
|
||||
|
@ -449,6 +450,11 @@ interface FragmentModule {
|
|||
@FragmentKey(FtueAuthSplashCarouselFragment::class)
|
||||
fun bindFtueAuthSplashCarouselFragment(fragment: FtueAuthSplashCarouselFragment): Fragment
|
||||
|
||||
@Binds
|
||||
@IntoMap
|
||||
@FragmentKey(FtueAuthUseCaseFragment::class)
|
||||
fun bindFtueAuthUseCaseFragment(fragment: FtueAuthUseCaseFragment): Fragment
|
||||
|
||||
@Binds
|
||||
@IntoMap
|
||||
@FragmentKey(FtueAuthWaitForEmailFragment::class)
|
||||
|
|
|
@ -82,7 +82,7 @@ fun TextView.setTextWithColoredPart(@StringRes fullTextRes: Int,
|
|||
fun TextView.setTextWithColoredPart(fullText: String,
|
||||
coloredPart: String,
|
||||
@AttrRes colorAttribute: Int = R.attr.colorPrimary,
|
||||
underline: Boolean = false,
|
||||
underline: Boolean = true,
|
||||
onClick: (() -> Unit)? = null) {
|
||||
val color = ThemeUtils.getColor(context, colorAttribute)
|
||||
|
||||
|
@ -101,7 +101,6 @@ fun TextView.setTextWithColoredPart(fullText: String,
|
|||
|
||||
override fun updateDrawState(ds: TextPaint) {
|
||||
ds.color = color
|
||||
ds.isUnderlineText = !underline
|
||||
}
|
||||
}
|
||||
setSpan(clickableSpan, index, index + coloredPart.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
|
||||
|
|
|
@ -0,0 +1,42 @@
|
|||
/*
|
||||
* Copyright (c) 2022 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.platform
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.pm.ActivityInfo
|
||||
import android.content.res.Resources
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import im.vector.app.R
|
||||
import javax.inject.Inject
|
||||
|
||||
class ScreenOrientationLocker @Inject constructor(
|
||||
private val resources: Resources
|
||||
) {
|
||||
|
||||
// Some screens do not provide enough value for us to provide phone landscape experiences
|
||||
@SuppressLint("SourceLockedOrientationActivity")
|
||||
fun lockPhonesToPortrait(activity: AppCompatActivity) {
|
||||
when (resources.getBoolean(R.bool.is_tablet)) {
|
||||
true -> {
|
||||
// do nothing
|
||||
}
|
||||
false -> {
|
||||
activity.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -27,3 +27,5 @@ class LocaleProvider @Inject constructor(private val resources: Resources) {
|
|||
return ConfigurationCompat.getLocales(resources.configuration)[0]
|
||||
}
|
||||
}
|
||||
|
||||
fun LocaleProvider.isEnglishSpeaking() = current().language.startsWith("en")
|
||||
|
|
|
@ -21,10 +21,9 @@ import im.vector.app.BuildConfig
|
|||
interface VectorFeatures {
|
||||
|
||||
fun onboardingVariant(): OnboardingVariant
|
||||
|
||||
fun isAlreadyHaveAccountSplashEnabled(): Boolean
|
||||
|
||||
fun isSplashCarouselEnabled(): Boolean
|
||||
fun isOnboardingAlreadyHaveAccountSplashEnabled(): Boolean
|
||||
fun isOnboardingSplashCarouselEnabled(): Boolean
|
||||
fun isOnboardingUseCaseEnabled(): Boolean
|
||||
|
||||
enum class OnboardingVariant {
|
||||
LEGACY,
|
||||
|
@ -35,6 +34,7 @@ interface VectorFeatures {
|
|||
|
||||
class DefaultVectorFeatures : VectorFeatures {
|
||||
override fun onboardingVariant(): VectorFeatures.OnboardingVariant = BuildConfig.ONBOARDING_VARIANT
|
||||
override fun isAlreadyHaveAccountSplashEnabled() = true
|
||||
override fun isSplashCarouselEnabled() = false
|
||||
override fun isOnboardingAlreadyHaveAccountSplashEnabled() = true
|
||||
override fun isOnboardingSplashCarouselEnabled() = false
|
||||
override fun isOnboardingUseCaseEnabled() = false
|
||||
}
|
||||
|
|
|
@ -16,9 +16,9 @@
|
|||
|
||||
package im.vector.app.features.analytics
|
||||
|
||||
import im.vector.app.core.flow.tickerFlow
|
||||
import im.vector.app.core.time.Clock
|
||||
import im.vector.app.features.analytics.plan.Error
|
||||
import im.vector.lib.core.utils.flow.tickerFlow
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
|
|
|
@ -20,8 +20,10 @@ import com.airbnb.mvrx.viewModel
|
|||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import im.vector.app.core.extensions.addFragment
|
||||
import im.vector.app.core.extensions.exhaustive
|
||||
import im.vector.app.core.platform.ScreenOrientationLocker
|
||||
import im.vector.app.core.platform.VectorBaseActivity
|
||||
import im.vector.app.databinding.ActivitySimpleBinding
|
||||
import javax.inject.Inject
|
||||
|
||||
/**
|
||||
* Simple container for AnalyticsOptInFragment
|
||||
|
@ -29,6 +31,8 @@ import im.vector.app.databinding.ActivitySimpleBinding
|
|||
@AndroidEntryPoint
|
||||
class AnalyticsOptInActivity : VectorBaseActivity<ActivitySimpleBinding>() {
|
||||
|
||||
@Inject lateinit var orientationLocker: ScreenOrientationLocker
|
||||
|
||||
private val viewModel: AnalyticsConsentViewModel by viewModel()
|
||||
|
||||
override fun getBinding() = ActivitySimpleBinding.inflate(layoutInflater)
|
||||
|
@ -36,6 +40,7 @@ class AnalyticsOptInActivity : VectorBaseActivity<ActivitySimpleBinding>() {
|
|||
override fun getCoordinatorLayout() = views.coordinatorLayout
|
||||
|
||||
override fun initUiAndData() {
|
||||
orientationLocker.lockPhonesToPortrait(this)
|
||||
if (isFirstCreation()) {
|
||||
addFragment(views.simpleFragmentContainer, AnalyticsOptInFragment::class.java)
|
||||
}
|
||||
|
|
|
@ -50,7 +50,7 @@ class AutocompleteCommandPresenter @Inject constructor(context: Context,
|
|||
if (query.isNullOrEmpty()) {
|
||||
true
|
||||
} else {
|
||||
it.command.startsWith(query, 1, true)
|
||||
it.startsWith(query)
|
||||
}
|
||||
}
|
||||
controller.setData(data)
|
||||
|
|
|
@ -19,9 +19,7 @@ package im.vector.app.features.call.webrtc
|
|||
import android.content.Context
|
||||
import android.hardware.camera2.CameraManager
|
||||
import androidx.core.content.getSystemService
|
||||
import im.vector.app.core.flow.chunk
|
||||
import im.vector.app.core.services.CallService
|
||||
import im.vector.app.core.utils.CountUpTimer
|
||||
import im.vector.app.core.utils.PublishDataSource
|
||||
import im.vector.app.core.utils.TextUtils.formatDuration
|
||||
import im.vector.app.features.call.CameraEventsHandlerAdapter
|
||||
|
@ -37,6 +35,8 @@ import im.vector.app.features.call.utils.awaitSetLocalDescription
|
|||
import im.vector.app.features.call.utils.awaitSetRemoteDescription
|
||||
import im.vector.app.features.call.utils.mapToCallCandidate
|
||||
import im.vector.app.features.session.coroutineScope
|
||||
import im.vector.lib.core.utils.flow.chunk
|
||||
import im.vector.lib.core.utils.timer.CountUpTimer
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Deferred
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
|
|
|
@ -24,42 +24,50 @@ import im.vector.app.R
|
|||
* the user can write theses messages to perform some actions
|
||||
* the list will be displayed in this order
|
||||
*/
|
||||
enum class Command(val command: String, val parameters: String, @StringRes val description: Int, val isDevCommand: Boolean) {
|
||||
EMOTE("/me", "<message>", R.string.command_description_emote, false),
|
||||
BAN_USER("/ban", "<user-id> [reason]", R.string.command_description_ban_user, false),
|
||||
UNBAN_USER("/unban", "<user-id> [reason]", R.string.command_description_unban_user, false),
|
||||
IGNORE_USER("/ignore", "<user-id> [reason]", R.string.command_description_ignore_user, false),
|
||||
UNIGNORE_USER("/unignore", "<user-id>", R.string.command_description_unignore_user, false),
|
||||
SET_USER_POWER_LEVEL("/op", "<user-id> [<power-level>]", R.string.command_description_op_user, false),
|
||||
RESET_USER_POWER_LEVEL("/deop", "<user-id>", R.string.command_description_deop_user, false),
|
||||
ROOM_NAME("/roomname", "<name>", R.string.command_description_room_name, false),
|
||||
INVITE("/invite", "<user-id> [reason]", R.string.command_description_invite_user, false),
|
||||
JOIN_ROOM("/join", "<room-address> [reason]", R.string.command_description_join_room, false),
|
||||
PART("/part", "[<room-address>]", R.string.command_description_part_room, false),
|
||||
TOPIC("/topic", "<topic>", R.string.command_description_topic, false),
|
||||
KICK_USER("/kick", "<user-id> [reason]", R.string.command_description_kick_user, false),
|
||||
CHANGE_DISPLAY_NAME("/nick", "<display-name>", R.string.command_description_nick, false),
|
||||
CHANGE_DISPLAY_NAME_FOR_ROOM("/myroomnick", "<display-name>", R.string.command_description_nick_for_room, false),
|
||||
ROOM_AVATAR("/roomavatar", "<mxc_url>", R.string.command_description_room_avatar, true /* Since user has to know the mxc url */),
|
||||
CHANGE_AVATAR_FOR_ROOM("/myroomavatar", "<mxc_url>", R.string.command_description_avatar_for_room, true /* Since user has to know the mxc url */),
|
||||
MARKDOWN("/markdown", "<on|off>", R.string.command_description_markdown, false),
|
||||
RAINBOW("/rainbow", "<message>", R.string.command_description_rainbow, false),
|
||||
RAINBOW_EMOTE("/rainbowme", "<message>", R.string.command_description_rainbow_emote, false),
|
||||
CLEAR_SCALAR_TOKEN("/clear_scalar_token", "", R.string.command_description_clear_scalar_token, false),
|
||||
SPOILER("/spoiler", "<message>", R.string.command_description_spoiler, false),
|
||||
SHRUG("/shrug", "<message>", R.string.command_description_shrug, false),
|
||||
LENNY("/lenny", "<message>", R.string.command_description_lenny, false),
|
||||
PLAIN("/plain", "<message>", R.string.command_description_plain, false),
|
||||
WHOIS("/whois", "<user-id>", R.string.command_description_whois, false),
|
||||
DISCARD_SESSION("/discardsession", "", R.string.command_description_discard_session, false),
|
||||
CONFETTI("/confetti", "<message>", R.string.command_confetti, false),
|
||||
SNOWFALL("/snowfall", "<message>", R.string.command_snow, false),
|
||||
CREATE_SPACE("/createspace", "<name> <invitee>*", R.string.command_description_create_space, true),
|
||||
ADD_TO_SPACE("/addToSpace", "spaceId", R.string.command_description_add_to_space, true),
|
||||
JOIN_SPACE("/joinSpace", "spaceId", R.string.command_description_join_space, true),
|
||||
LEAVE_ROOM("/leave", "<roomId?>", R.string.command_description_leave_room, true),
|
||||
UPGRADE_ROOM("/upgraderoom", "newVersion", R.string.command_description_upgrade_room, true);
|
||||
enum class Command(val command: String,
|
||||
val aliases: Array<CharSequence>?,
|
||||
val parameters: String,
|
||||
@StringRes val description: Int,
|
||||
val isDevCommand: Boolean) {
|
||||
EMOTE("/me", null, "<message>", R.string.command_description_emote, false),
|
||||
BAN_USER("/ban", null, "<user-id> [reason]", R.string.command_description_ban_user, false),
|
||||
UNBAN_USER("/unban", null, "<user-id> [reason]", R.string.command_description_unban_user, false),
|
||||
IGNORE_USER("/ignore", null, "<user-id> [reason]", R.string.command_description_ignore_user, false),
|
||||
UNIGNORE_USER("/unignore", null, "<user-id>", R.string.command_description_unignore_user, false),
|
||||
SET_USER_POWER_LEVEL("/op", null, "<user-id> [<power-level>]", R.string.command_description_op_user, false),
|
||||
RESET_USER_POWER_LEVEL("/deop", null, "<user-id>", R.string.command_description_deop_user, false),
|
||||
ROOM_NAME("/roomname", null, "<name>", R.string.command_description_room_name, false),
|
||||
INVITE("/invite", null, "<user-id> [reason]", R.string.command_description_invite_user, false),
|
||||
JOIN_ROOM("/join", arrayOf("/j", "/goto"), "<room-address> [reason]", R.string.command_description_join_room, false),
|
||||
PART("/part", null, "[<room-address>]", R.string.command_description_part_room, false),
|
||||
TOPIC("/topic", null, "<topic>", R.string.command_description_topic, false),
|
||||
REMOVE_USER("/remove", arrayOf("/kick"), "<user-id> [reason]", R.string.command_description_kick_user, false),
|
||||
CHANGE_DISPLAY_NAME("/nick", null, "<display-name>", R.string.command_description_nick, false),
|
||||
CHANGE_DISPLAY_NAME_FOR_ROOM("/myroomnick", arrayOf("/roomnick"), "<display-name>", R.string.command_description_nick_for_room, false),
|
||||
ROOM_AVATAR("/roomavatar", null, "<mxc_url>", R.string.command_description_room_avatar, true /* Since user has to know the mxc url */),
|
||||
CHANGE_AVATAR_FOR_ROOM("/myroomavatar", null, "<mxc_url>", R.string.command_description_avatar_for_room, true /* Since user has to know the mxc url */),
|
||||
MARKDOWN("/markdown", null, "<on|off>", R.string.command_description_markdown, false),
|
||||
RAINBOW("/rainbow", null, "<message>", R.string.command_description_rainbow, false),
|
||||
RAINBOW_EMOTE("/rainbowme", null, "<message>", R.string.command_description_rainbow_emote, false),
|
||||
CLEAR_SCALAR_TOKEN("/clear_scalar_token", null, "", R.string.command_description_clear_scalar_token, false),
|
||||
SPOILER("/spoiler", null, "<message>", R.string.command_description_spoiler, false),
|
||||
SHRUG("/shrug", null, "<message>", R.string.command_description_shrug, false),
|
||||
LENNY("/lenny", null, "<message>", R.string.command_description_lenny, false),
|
||||
PLAIN("/plain", null, "<message>", R.string.command_description_plain, false),
|
||||
WHOIS("/whois", null, "<user-id>", R.string.command_description_whois, false),
|
||||
DISCARD_SESSION("/discardsession", null, "", R.string.command_description_discard_session, false),
|
||||
CONFETTI("/confetti", null, "<message>", R.string.command_confetti, false),
|
||||
SNOWFALL("/snowfall", null, "<message>", R.string.command_snow, false),
|
||||
CREATE_SPACE("/createspace", null, "<name> <invitee>*", R.string.command_description_create_space, true),
|
||||
ADD_TO_SPACE("/addToSpace", null, "spaceId", R.string.command_description_add_to_space, true),
|
||||
JOIN_SPACE("/joinSpace", null, "spaceId", R.string.command_description_join_space, true),
|
||||
LEAVE_ROOM("/leave", null, "<roomId?>", R.string.command_description_leave_room, true),
|
||||
UPGRADE_ROOM("/upgraderoom", null, "newVersion", R.string.command_description_upgrade_room, true);
|
||||
|
||||
val length
|
||||
get() = command.length + 1
|
||||
val allAliases = arrayOf(command, *aliases.orEmpty())
|
||||
|
||||
fun matches(inputCommand: CharSequence) = allAliases.any { it.contentEquals(inputCommand, true) }
|
||||
|
||||
fun startsWith(input: CharSequence) =
|
||||
allAliases.any { it.startsWith(input, 1, true) }
|
||||
}
|
||||
|
|
|
@ -32,12 +32,12 @@ object CommandParser {
|
|||
* @param textMessage the text message
|
||||
* @return a parsed slash command (ok or error)
|
||||
*/
|
||||
fun parseSplashCommand(textMessage: CharSequence): ParsedCommand {
|
||||
fun parseSlashCommand(textMessage: CharSequence): ParsedCommand {
|
||||
// check if it has the Slash marker
|
||||
if (!textMessage.startsWith("/")) {
|
||||
return ParsedCommand.ErrorNotACommand
|
||||
} else {
|
||||
Timber.v("parseSplashCommand")
|
||||
Timber.v("parseSlashCommand")
|
||||
|
||||
// "/" only
|
||||
if (textMessage.length == 1) {
|
||||
|
@ -52,7 +52,7 @@ object CommandParser {
|
|||
val messageParts = try {
|
||||
textMessage.split("\\s+".toRegex()).dropLastWhile { it.isEmpty() }
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e, "## manageSplashCommand() : split failed")
|
||||
Timber.e(e, "## manageSlashCommand() : split failed")
|
||||
null
|
||||
}
|
||||
|
||||
|
@ -61,35 +61,32 @@ object CommandParser {
|
|||
return ParsedCommand.ErrorEmptySlashCommand
|
||||
}
|
||||
|
||||
return when (val slashCommand = messageParts.first()) {
|
||||
Command.PLAIN.command -> {
|
||||
val text = textMessage.substring(Command.PLAIN.command.length).trim()
|
||||
val slashCommand = messageParts.first()
|
||||
val message = textMessage.substring(slashCommand.length).trim()
|
||||
|
||||
if (text.isNotEmpty()) {
|
||||
ParsedCommand.SendPlainText(text)
|
||||
return when {
|
||||
Command.PLAIN.matches(slashCommand) -> {
|
||||
if (message.isNotEmpty()) {
|
||||
ParsedCommand.SendPlainText(message = message)
|
||||
} else {
|
||||
ParsedCommand.ErrorSyntax(Command.PLAIN)
|
||||
}
|
||||
}
|
||||
Command.CHANGE_DISPLAY_NAME.command -> {
|
||||
val newDisplayName = textMessage.substring(Command.CHANGE_DISPLAY_NAME.command.length).trim()
|
||||
|
||||
if (newDisplayName.isNotEmpty()) {
|
||||
ParsedCommand.ChangeDisplayName(newDisplayName)
|
||||
Command.CHANGE_DISPLAY_NAME.matches(slashCommand) -> {
|
||||
if (message.isNotEmpty()) {
|
||||
ParsedCommand.ChangeDisplayName(displayName = message)
|
||||
} else {
|
||||
ParsedCommand.ErrorSyntax(Command.CHANGE_DISPLAY_NAME)
|
||||
}
|
||||
}
|
||||
Command.CHANGE_DISPLAY_NAME_FOR_ROOM.command -> {
|
||||
val newDisplayName = textMessage.substring(Command.CHANGE_DISPLAY_NAME_FOR_ROOM.command.length).trim()
|
||||
|
||||
if (newDisplayName.isNotEmpty()) {
|
||||
ParsedCommand.ChangeDisplayNameForRoom(newDisplayName)
|
||||
Command.CHANGE_DISPLAY_NAME_FOR_ROOM.matches(slashCommand) -> {
|
||||
if (message.isNotEmpty()) {
|
||||
ParsedCommand.ChangeDisplayNameForRoom(displayName = message)
|
||||
} else {
|
||||
ParsedCommand.ErrorSyntax(Command.CHANGE_DISPLAY_NAME_FOR_ROOM)
|
||||
}
|
||||
}
|
||||
Command.ROOM_AVATAR.command -> {
|
||||
Command.ROOM_AVATAR.matches(slashCommand) -> {
|
||||
if (messageParts.size == 2) {
|
||||
val url = messageParts[1]
|
||||
|
||||
|
@ -102,7 +99,7 @@ object CommandParser {
|
|||
ParsedCommand.ErrorSyntax(Command.ROOM_AVATAR)
|
||||
}
|
||||
}
|
||||
Command.CHANGE_AVATAR_FOR_ROOM.command -> {
|
||||
Command.CHANGE_AVATAR_FOR_ROOM.matches(slashCommand) -> {
|
||||
if (messageParts.size == 2) {
|
||||
val url = messageParts[1]
|
||||
|
||||
|
@ -115,40 +112,42 @@ object CommandParser {
|
|||
ParsedCommand.ErrorSyntax(Command.CHANGE_AVATAR_FOR_ROOM)
|
||||
}
|
||||
}
|
||||
Command.TOPIC.command -> {
|
||||
val newTopic = textMessage.substring(Command.TOPIC.command.length).trim()
|
||||
|
||||
if (newTopic.isNotEmpty()) {
|
||||
ParsedCommand.ChangeTopic(newTopic)
|
||||
Command.TOPIC.matches(slashCommand) -> {
|
||||
if (message.isNotEmpty()) {
|
||||
ParsedCommand.ChangeTopic(topic = message)
|
||||
} else {
|
||||
ParsedCommand.ErrorSyntax(Command.TOPIC)
|
||||
}
|
||||
}
|
||||
Command.EMOTE.command -> {
|
||||
val message = textMessage.subSequence(Command.EMOTE.command.length, textMessage.length).trim()
|
||||
|
||||
ParsedCommand.SendEmote(message)
|
||||
Command.EMOTE.matches(slashCommand) -> {
|
||||
if (message.isNotEmpty()) {
|
||||
ParsedCommand.SendEmote(message)
|
||||
} else {
|
||||
ParsedCommand.ErrorSyntax(Command.EMOTE)
|
||||
}
|
||||
}
|
||||
Command.RAINBOW.command -> {
|
||||
val message = textMessage.subSequence(Command.RAINBOW.command.length, textMessage.length).trim()
|
||||
|
||||
ParsedCommand.SendRainbow(message)
|
||||
Command.RAINBOW.matches(slashCommand) -> {
|
||||
if (message.isNotEmpty()) {
|
||||
ParsedCommand.SendRainbow(message)
|
||||
} else {
|
||||
ParsedCommand.ErrorSyntax(Command.RAINBOW)
|
||||
}
|
||||
}
|
||||
Command.RAINBOW_EMOTE.command -> {
|
||||
val message = textMessage.subSequence(Command.RAINBOW_EMOTE.command.length, textMessage.length).trim()
|
||||
|
||||
ParsedCommand.SendRainbowEmote(message)
|
||||
Command.RAINBOW_EMOTE.matches(slashCommand) -> {
|
||||
if (message.isNotEmpty()) {
|
||||
ParsedCommand.SendRainbowEmote(message)
|
||||
} else {
|
||||
ParsedCommand.ErrorSyntax(Command.RAINBOW_EMOTE)
|
||||
}
|
||||
}
|
||||
Command.JOIN_ROOM.command -> {
|
||||
Command.JOIN_ROOM.matches(slashCommand) -> {
|
||||
if (messageParts.size >= 2) {
|
||||
val roomAlias = messageParts[1]
|
||||
|
||||
if (roomAlias.isNotEmpty()) {
|
||||
ParsedCommand.JoinRoom(
|
||||
roomAlias,
|
||||
textMessage.substring(Command.JOIN_ROOM.length + roomAlias.length)
|
||||
.trim()
|
||||
.takeIf { it.isNotBlank() }
|
||||
trimParts(textMessage, messageParts.take(2))
|
||||
)
|
||||
} else {
|
||||
ParsedCommand.ErrorSyntax(Command.JOIN_ROOM)
|
||||
|
@ -157,23 +156,21 @@ object CommandParser {
|
|||
ParsedCommand.ErrorSyntax(Command.JOIN_ROOM)
|
||||
}
|
||||
}
|
||||
Command.PART.command -> {
|
||||
Command.PART.matches(slashCommand) -> {
|
||||
when (messageParts.size) {
|
||||
1 -> ParsedCommand.PartRoom(null)
|
||||
2 -> ParsedCommand.PartRoom(messageParts[1])
|
||||
else -> ParsedCommand.ErrorSyntax(Command.PART)
|
||||
}
|
||||
}
|
||||
Command.ROOM_NAME.command -> {
|
||||
val newRoomName = textMessage.substring(Command.ROOM_NAME.command.length).trim()
|
||||
|
||||
if (newRoomName.isNotEmpty()) {
|
||||
ParsedCommand.ChangeRoomName(newRoomName)
|
||||
Command.ROOM_NAME.matches(slashCommand) -> {
|
||||
if (message.isNotEmpty()) {
|
||||
ParsedCommand.ChangeRoomName(name = message)
|
||||
} else {
|
||||
ParsedCommand.ErrorSyntax(Command.ROOM_NAME)
|
||||
}
|
||||
}
|
||||
Command.INVITE.command -> {
|
||||
Command.INVITE.matches(slashCommand) -> {
|
||||
if (messageParts.size >= 2) {
|
||||
val userId = messageParts[1]
|
||||
|
||||
|
@ -181,9 +178,7 @@ object CommandParser {
|
|||
MatrixPatterns.isUserId(userId) -> {
|
||||
ParsedCommand.Invite(
|
||||
userId,
|
||||
textMessage.substring(Command.INVITE.length + userId.length)
|
||||
.trim()
|
||||
.takeIf { it.isNotBlank() }
|
||||
trimParts(textMessage, messageParts.take(2))
|
||||
)
|
||||
}
|
||||
userId.isEmail() -> {
|
||||
|
@ -200,34 +195,30 @@ object CommandParser {
|
|||
ParsedCommand.ErrorSyntax(Command.INVITE)
|
||||
}
|
||||
}
|
||||
Command.KICK_USER.command -> {
|
||||
Command.REMOVE_USER.matches(slashCommand) -> {
|
||||
if (messageParts.size >= 2) {
|
||||
val userId = messageParts[1]
|
||||
|
||||
if (MatrixPatterns.isUserId(userId)) {
|
||||
ParsedCommand.KickUser(
|
||||
ParsedCommand.RemoveUser(
|
||||
userId,
|
||||
textMessage.substring(Command.KICK_USER.length + userId.length)
|
||||
.trim()
|
||||
.takeIf { it.isNotBlank() }
|
||||
trimParts(textMessage, messageParts.take(2))
|
||||
)
|
||||
} else {
|
||||
ParsedCommand.ErrorSyntax(Command.KICK_USER)
|
||||
ParsedCommand.ErrorSyntax(Command.REMOVE_USER)
|
||||
}
|
||||
} else {
|
||||
ParsedCommand.ErrorSyntax(Command.KICK_USER)
|
||||
ParsedCommand.ErrorSyntax(Command.REMOVE_USER)
|
||||
}
|
||||
}
|
||||
Command.BAN_USER.command -> {
|
||||
Command.BAN_USER.matches(slashCommand) -> {
|
||||
if (messageParts.size >= 2) {
|
||||
val userId = messageParts[1]
|
||||
|
||||
if (MatrixPatterns.isUserId(userId)) {
|
||||
ParsedCommand.BanUser(
|
||||
userId,
|
||||
textMessage.substring(Command.BAN_USER.length + userId.length)
|
||||
.trim()
|
||||
.takeIf { it.isNotBlank() }
|
||||
trimParts(textMessage, messageParts.take(2))
|
||||
)
|
||||
} else {
|
||||
ParsedCommand.ErrorSyntax(Command.BAN_USER)
|
||||
|
@ -236,16 +227,14 @@ object CommandParser {
|
|||
ParsedCommand.ErrorSyntax(Command.BAN_USER)
|
||||
}
|
||||
}
|
||||
Command.UNBAN_USER.command -> {
|
||||
Command.UNBAN_USER.matches(slashCommand) -> {
|
||||
if (messageParts.size >= 2) {
|
||||
val userId = messageParts[1]
|
||||
|
||||
if (MatrixPatterns.isUserId(userId)) {
|
||||
ParsedCommand.UnbanUser(
|
||||
userId,
|
||||
textMessage.substring(Command.UNBAN_USER.length + userId.length)
|
||||
.trim()
|
||||
.takeIf { it.isNotBlank() }
|
||||
trimParts(textMessage, messageParts.take(2))
|
||||
)
|
||||
} else {
|
||||
ParsedCommand.ErrorSyntax(Command.UNBAN_USER)
|
||||
|
@ -254,7 +243,7 @@ object CommandParser {
|
|||
ParsedCommand.ErrorSyntax(Command.UNBAN_USER)
|
||||
}
|
||||
}
|
||||
Command.IGNORE_USER.command -> {
|
||||
Command.IGNORE_USER.matches(slashCommand) -> {
|
||||
if (messageParts.size == 2) {
|
||||
val userId = messageParts[1]
|
||||
|
||||
|
@ -267,7 +256,7 @@ object CommandParser {
|
|||
ParsedCommand.ErrorSyntax(Command.IGNORE_USER)
|
||||
}
|
||||
}
|
||||
Command.UNIGNORE_USER.command -> {
|
||||
Command.UNIGNORE_USER.matches(slashCommand) -> {
|
||||
if (messageParts.size == 2) {
|
||||
val userId = messageParts[1]
|
||||
|
||||
|
@ -280,7 +269,7 @@ object CommandParser {
|
|||
ParsedCommand.ErrorSyntax(Command.UNIGNORE_USER)
|
||||
}
|
||||
}
|
||||
Command.SET_USER_POWER_LEVEL.command -> {
|
||||
Command.SET_USER_POWER_LEVEL.matches(slashCommand) -> {
|
||||
if (messageParts.size == 3) {
|
||||
val userId = messageParts[1]
|
||||
if (MatrixPatterns.isUserId(userId)) {
|
||||
|
@ -300,7 +289,7 @@ object CommandParser {
|
|||
ParsedCommand.ErrorSyntax(Command.SET_USER_POWER_LEVEL)
|
||||
}
|
||||
}
|
||||
Command.RESET_USER_POWER_LEVEL.command -> {
|
||||
Command.RESET_USER_POWER_LEVEL.matches(slashCommand) -> {
|
||||
if (messageParts.size == 2) {
|
||||
val userId = messageParts[1]
|
||||
|
||||
|
@ -313,7 +302,7 @@ object CommandParser {
|
|||
ParsedCommand.ErrorSyntax(Command.SET_USER_POWER_LEVEL)
|
||||
}
|
||||
}
|
||||
Command.MARKDOWN.command -> {
|
||||
Command.MARKDOWN.matches(slashCommand) -> {
|
||||
if (messageParts.size == 2) {
|
||||
when {
|
||||
"on".equals(messageParts[1], true) -> ParsedCommand.SetMarkdown(true)
|
||||
|
@ -324,31 +313,34 @@ object CommandParser {
|
|||
ParsedCommand.ErrorSyntax(Command.MARKDOWN)
|
||||
}
|
||||
}
|
||||
Command.CLEAR_SCALAR_TOKEN.command -> {
|
||||
Command.CLEAR_SCALAR_TOKEN.matches(slashCommand) -> {
|
||||
if (messageParts.size == 1) {
|
||||
ParsedCommand.ClearScalarToken
|
||||
} else {
|
||||
ParsedCommand.ErrorSyntax(Command.CLEAR_SCALAR_TOKEN)
|
||||
}
|
||||
}
|
||||
Command.SPOILER.command -> {
|
||||
val message = textMessage.substring(Command.SPOILER.command.length).trim()
|
||||
ParsedCommand.SendSpoiler(message)
|
||||
Command.SPOILER.matches(slashCommand) -> {
|
||||
if (message.isNotEmpty()) {
|
||||
ParsedCommand.SendSpoiler(message)
|
||||
} else {
|
||||
ParsedCommand.ErrorSyntax(Command.SPOILER)
|
||||
}
|
||||
}
|
||||
Command.SHRUG.command -> {
|
||||
val message = textMessage.substring(Command.SHRUG.command.length).trim()
|
||||
|
||||
Command.SHRUG.matches(slashCommand) -> {
|
||||
ParsedCommand.SendShrug(message)
|
||||
}
|
||||
Command.LENNY.command -> {
|
||||
val message = textMessage.substring(Command.LENNY.command.length).trim()
|
||||
|
||||
Command.LENNY.matches(slashCommand) -> {
|
||||
ParsedCommand.SendLenny(message)
|
||||
}
|
||||
Command.DISCARD_SESSION.command -> {
|
||||
ParsedCommand.DiscardSession
|
||||
Command.DISCARD_SESSION.matches(slashCommand) -> {
|
||||
if (messageParts.size == 1) {
|
||||
ParsedCommand.DiscardSession
|
||||
} else {
|
||||
ParsedCommand.ErrorSyntax(Command.DISCARD_SESSION)
|
||||
}
|
||||
}
|
||||
Command.WHOIS.command -> {
|
||||
Command.WHOIS.matches(slashCommand) -> {
|
||||
if (messageParts.size == 2) {
|
||||
val userId = messageParts[1]
|
||||
|
||||
|
@ -361,57 +353,57 @@ object CommandParser {
|
|||
ParsedCommand.ErrorSyntax(Command.WHOIS)
|
||||
}
|
||||
}
|
||||
Command.CONFETTI.command -> {
|
||||
val message = textMessage.substring(Command.CONFETTI.command.length).trim()
|
||||
Command.CONFETTI.matches(slashCommand) -> {
|
||||
ParsedCommand.SendChatEffect(ChatEffect.CONFETTI, message)
|
||||
}
|
||||
Command.SNOWFALL.command -> {
|
||||
val message = textMessage.substring(Command.SNOWFALL.command.length).trim()
|
||||
Command.SNOWFALL.matches(slashCommand) -> {
|
||||
ParsedCommand.SendChatEffect(ChatEffect.SNOWFALL, message)
|
||||
}
|
||||
Command.CREATE_SPACE.command -> {
|
||||
val rawCommand = textMessage.substring(Command.CREATE_SPACE.command.length).trim()
|
||||
val split = rawCommand.split(" ").map { it.trim() }
|
||||
if (split.isEmpty()) {
|
||||
ParsedCommand.ErrorSyntax(Command.CREATE_SPACE)
|
||||
} else {
|
||||
Command.CREATE_SPACE.matches(slashCommand) -> {
|
||||
if (messageParts.size >= 2) {
|
||||
ParsedCommand.CreateSpace(
|
||||
split[0],
|
||||
split.subList(1, split.size)
|
||||
messageParts[1],
|
||||
messageParts.drop(2)
|
||||
)
|
||||
}
|
||||
}
|
||||
Command.ADD_TO_SPACE.command -> {
|
||||
val rawCommand = textMessage.substring(Command.ADD_TO_SPACE.command.length).trim()
|
||||
ParsedCommand.AddToSpace(
|
||||
rawCommand
|
||||
)
|
||||
}
|
||||
Command.JOIN_SPACE.command -> {
|
||||
val spaceIdOrAlias = textMessage.substring(Command.JOIN_SPACE.command.length).trim()
|
||||
ParsedCommand.JoinSpace(
|
||||
spaceIdOrAlias
|
||||
)
|
||||
}
|
||||
Command.LEAVE_ROOM.command -> {
|
||||
val spaceIdOrAlias = textMessage.substring(Command.LEAVE_ROOM.command.length).trim()
|
||||
ParsedCommand.LeaveRoom(
|
||||
spaceIdOrAlias
|
||||
)
|
||||
}
|
||||
Command.UPGRADE_ROOM.command -> {
|
||||
val newVersion = textMessage.substring(Command.UPGRADE_ROOM.command.length).trim()
|
||||
if (newVersion.isEmpty()) {
|
||||
ParsedCommand.ErrorSyntax(Command.UPGRADE_ROOM)
|
||||
} else {
|
||||
ParsedCommand.UpgradeRoom(newVersion)
|
||||
ParsedCommand.ErrorSyntax(Command.CREATE_SPACE)
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
Command.ADD_TO_SPACE.matches(slashCommand) -> {
|
||||
if (messageParts.size == 1) {
|
||||
ParsedCommand.AddToSpace(spaceId = message)
|
||||
} else {
|
||||
ParsedCommand.ErrorSyntax(Command.ADD_TO_SPACE)
|
||||
}
|
||||
}
|
||||
Command.JOIN_SPACE.matches(slashCommand) -> {
|
||||
if (messageParts.size == 1) {
|
||||
ParsedCommand.JoinSpace(spaceIdOrAlias = message)
|
||||
} else {
|
||||
ParsedCommand.ErrorSyntax(Command.JOIN_SPACE)
|
||||
}
|
||||
}
|
||||
Command.LEAVE_ROOM.matches(slashCommand) -> {
|
||||
ParsedCommand.LeaveRoom(roomId = message)
|
||||
}
|
||||
Command.UPGRADE_ROOM.matches(slashCommand) -> {
|
||||
if (message.isNotEmpty()) {
|
||||
ParsedCommand.UpgradeRoom(newVersion = message)
|
||||
} else {
|
||||
ParsedCommand.ErrorSyntax(Command.UPGRADE_ROOM)
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
// Unknown command
|
||||
ParsedCommand.ErrorUnknownSlashCommand(slashCommand)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun trimParts(message: CharSequence, messageParts: List<String>): String? {
|
||||
val partsSize = messageParts.sumOf { it.length }
|
||||
val gapsNumber = messageParts.size - 1
|
||||
return message.substring(partsSize + gapsNumber).trim().takeIf { it.isNotEmpty() }
|
||||
}
|
||||
}
|
||||
|
|
|
@ -51,7 +51,7 @@ sealed class ParsedCommand {
|
|||
class JoinRoom(val roomAlias: String, val reason: String?) : ParsedCommand()
|
||||
class PartRoom(val roomAlias: String?) : ParsedCommand()
|
||||
class ChangeTopic(val topic: String) : ParsedCommand()
|
||||
class KickUser(val userId: String, val reason: String?) : ParsedCommand()
|
||||
class RemoveUser(val userId: String, val reason: String?) : ParsedCommand()
|
||||
class ChangeDisplayName(val displayName: String) : ParsedCommand()
|
||||
class ChangeDisplayNameForRoom(val displayName: String) : ParsedCommand()
|
||||
class ChangeRoomAvatar(val url: String) : ParsedCommand()
|
||||
|
|
|
@ -26,10 +26,10 @@ import androidx.lifecycle.lifecycleScope
|
|||
import com.airbnb.mvrx.activityViewModel
|
||||
import im.vector.app.R
|
||||
import im.vector.app.core.extensions.registerStartForActivityResult
|
||||
import im.vector.app.core.flow.throttleFirst
|
||||
import im.vector.app.core.platform.VectorBaseFragment
|
||||
import im.vector.app.core.utils.startImportTextFromFileIntent
|
||||
import im.vector.app.databinding.FragmentSsssAccessFromKeyBinding
|
||||
import im.vector.lib.core.utils.flow.throttleFirst
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import org.matrix.android.sdk.api.extensions.tryOrNull
|
||||
|
|
|
@ -25,10 +25,10 @@ import androidx.core.text.toSpannable
|
|||
import androidx.lifecycle.lifecycleScope
|
||||
import com.airbnb.mvrx.activityViewModel
|
||||
import im.vector.app.R
|
||||
import im.vector.app.core.flow.throttleFirst
|
||||
import im.vector.app.core.platform.VectorBaseFragment
|
||||
import im.vector.app.core.resources.ColorProvider
|
||||
import im.vector.app.databinding.FragmentSsssAccessFromPassphraseBinding
|
||||
import im.vector.lib.core.utils.flow.throttleFirst
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import reactivecircus.flowbinding.android.widget.editorActionEvents
|
||||
|
|
|
@ -27,9 +27,9 @@ import com.airbnb.mvrx.parentFragmentViewModel
|
|||
import com.airbnb.mvrx.withState
|
||||
import im.vector.app.R
|
||||
import im.vector.app.core.extensions.hideKeyboard
|
||||
import im.vector.app.core.flow.throttleFirst
|
||||
import im.vector.app.core.platform.VectorBaseFragment
|
||||
import im.vector.app.databinding.FragmentBootstrapEnterPassphraseBinding
|
||||
import im.vector.lib.core.utils.flow.throttleFirst
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import reactivecircus.flowbinding.android.widget.editorActionEvents
|
||||
|
|
|
@ -25,10 +25,10 @@ import androidx.lifecycle.lifecycleScope
|
|||
import com.airbnb.mvrx.parentFragmentViewModel
|
||||
import com.airbnb.mvrx.withState
|
||||
import im.vector.app.R
|
||||
import im.vector.app.core.flow.throttleFirst
|
||||
import im.vector.app.core.platform.VectorBaseFragment
|
||||
import im.vector.app.databinding.FragmentBootstrapEnterPassphraseBinding
|
||||
import im.vector.app.features.settings.VectorLocale
|
||||
import im.vector.lib.core.utils.flow.throttleFirst
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import reactivecircus.flowbinding.android.widget.editorActionEvents
|
||||
|
|
|
@ -33,12 +33,12 @@ import com.airbnb.mvrx.withState
|
|||
import im.vector.app.R
|
||||
import im.vector.app.core.extensions.hideKeyboard
|
||||
import im.vector.app.core.extensions.registerStartForActivityResult
|
||||
import im.vector.app.core.flow.throttleFirst
|
||||
import im.vector.app.core.platform.VectorBaseFragment
|
||||
import im.vector.app.core.resources.ColorProvider
|
||||
import im.vector.app.core.utils.colorizeMatchingText
|
||||
import im.vector.app.core.utils.startImportTextFromFileIntent
|
||||
import im.vector.app.databinding.FragmentBootstrapMigrateBackupBinding
|
||||
import im.vector.lib.core.utils.flow.throttleFirst
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import org.matrix.android.sdk.api.extensions.tryOrNull
|
||||
|
|
|
@ -27,7 +27,6 @@ import im.vector.app.RoomGroupingMethod
|
|||
import im.vector.app.core.di.MavericksAssistedViewModelFactory
|
||||
import im.vector.app.core.di.hiltMavericksViewModelFactory
|
||||
import im.vector.app.core.extensions.singletonEntryPoint
|
||||
import im.vector.app.core.flow.throttleFirst
|
||||
import im.vector.app.core.platform.VectorViewModel
|
||||
import im.vector.app.features.call.dialpad.DialPadLookup
|
||||
import im.vector.app.features.call.lookup.CallProtocolsChecker
|
||||
|
@ -37,6 +36,7 @@ import im.vector.app.features.invite.AutoAcceptInvites
|
|||
import im.vector.app.features.invite.showInvites
|
||||
import im.vector.app.features.settings.VectorDataStore
|
||||
import im.vector.app.features.ui.UiStateRepository
|
||||
import im.vector.lib.core.utils.flow.throttleFirst
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.filterIsInstance
|
||||
|
@ -197,7 +197,7 @@ class HomeDetailViewModel @AssistedInject constructor(
|
|||
}
|
||||
|
||||
private fun observeRoomGroupingMethod() {
|
||||
appStateHandler.selectedRoomGroupingObservable
|
||||
appStateHandler.selectedRoomGroupingFlow
|
||||
.setOnEach {
|
||||
copy(
|
||||
roomGroupingMethod = it.orNull() ?: RoomGroupingMethod.BySpace(null)
|
||||
|
@ -206,7 +206,7 @@ class HomeDetailViewModel @AssistedInject constructor(
|
|||
}
|
||||
|
||||
private fun observeRoomSummaries() {
|
||||
appStateHandler.selectedRoomGroupingObservable.distinctUntilChanged().flatMapLatest {
|
||||
appStateHandler.selectedRoomGroupingFlow.distinctUntilChanged().flatMapLatest {
|
||||
// we use it as a trigger to all changes in room, but do not really load
|
||||
// the actual models
|
||||
session.getPagedRoomSummariesLive(
|
||||
|
|
|
@ -50,7 +50,7 @@ class PromoteRestrictedViewModel @AssistedInject constructor(
|
|||
) : VectorViewModel<ActiveSpaceViewState, EmptyAction, EmptyViewEvents>(initialState) {
|
||||
|
||||
init {
|
||||
appStateHandler.selectedRoomGroupingObservable.distinctUntilChanged().execute { state ->
|
||||
appStateHandler.selectedRoomGroupingFlow.distinctUntilChanged().execute { state ->
|
||||
val groupingMethod = state.invoke()?.orNull()
|
||||
val isSpaceMode = groupingMethod is RoomGroupingMethod.BySpace
|
||||
val currentSpace = (groupingMethod as? RoomGroupingMethod.BySpace)?.spaceSummary
|
||||
|
|
|
@ -26,12 +26,12 @@ import im.vector.app.AppStateHandler
|
|||
import im.vector.app.RoomGroupingMethod
|
||||
import im.vector.app.core.di.MavericksAssistedViewModelFactory
|
||||
import im.vector.app.core.di.hiltMavericksViewModelFactory
|
||||
import im.vector.app.core.flow.throttleFirst
|
||||
import im.vector.app.core.platform.EmptyAction
|
||||
import im.vector.app.core.platform.EmptyViewEvents
|
||||
import im.vector.app.core.platform.VectorViewModel
|
||||
import im.vector.app.features.invite.AutoAcceptInvites
|
||||
import im.vector.app.features.settings.VectorPreferences
|
||||
import im.vector.lib.core.utils.flow.throttleFirst
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
|
@ -107,8 +107,8 @@ class UnreadMessagesSharedViewModel @AssistedInject constructor(@Assisted initia
|
|||
}
|
||||
|
||||
combine(
|
||||
appStateHandler.selectedRoomGroupingObservable.distinctUntilChanged(),
|
||||
appStateHandler.selectedRoomGroupingObservable.flatMapLatest {
|
||||
appStateHandler.selectedRoomGroupingFlow.distinctUntilChanged(),
|
||||
appStateHandler.selectedRoomGroupingFlow.flatMapLatest {
|
||||
session.getPagedRoomSummariesLive(
|
||||
roomSummaryQueryParams {
|
||||
this.memberships = Membership.activeMemberships()
|
||||
|
|
|
@ -2113,7 +2113,7 @@ class RoomDetailFragment @Inject constructor(
|
|||
userId == session.myUserId) {
|
||||
// Empty composer, current user: start an emote
|
||||
views.composerLayout.views.composerEditText.setText(Command.EMOTE.command + " ")
|
||||
views.composerLayout.views.composerEditText.setSelection(Command.EMOTE.length)
|
||||
views.composerLayout.views.composerEditText.setSelection(Command.EMOTE.command.length + 1)
|
||||
} else {
|
||||
val roomMember = roomDetailViewModel.getMember(userId)
|
||||
// TODO move logic outside of fragment
|
||||
|
|
|
@ -33,7 +33,6 @@ import im.vector.app.R
|
|||
import im.vector.app.core.di.MavericksAssistedViewModelFactory
|
||||
import im.vector.app.core.di.hiltMavericksViewModelFactory
|
||||
import im.vector.app.core.extensions.exhaustive
|
||||
import im.vector.app.core.flow.chunk
|
||||
import im.vector.app.core.mvrx.runCatchingToAsync
|
||||
import im.vector.app.core.platform.VectorViewModel
|
||||
import im.vector.app.core.resources.StringProvider
|
||||
|
@ -55,6 +54,7 @@ import im.vector.app.features.powerlevel.PowerLevelsFlowFactory
|
|||
import im.vector.app.features.session.coroutineScope
|
||||
import im.vector.app.features.settings.VectorDataStore
|
||||
import im.vector.app.features.settings.VectorPreferences
|
||||
import im.vector.lib.core.utils.flow.chunk
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
|
|
|
@ -183,7 +183,7 @@ class MessageComposerViewModel @AssistedInject constructor(
|
|||
withState { state ->
|
||||
when (state.sendMode) {
|
||||
is SendMode.Regular -> {
|
||||
when (val slashCommandResult = CommandParser.parseSplashCommand(action.text)) {
|
||||
when (val slashCommandResult = CommandParser.parseSlashCommand(action.text)) {
|
||||
is ParsedCommand.ErrorNotACommand -> {
|
||||
// Send the text message to the room
|
||||
room.sendTextMessage(action.text, autoMarkdown = action.autoMarkdown)
|
||||
|
@ -239,8 +239,8 @@ class MessageComposerViewModel @AssistedInject constructor(
|
|||
is ParsedCommand.UnignoreUser -> {
|
||||
handleUnignoreSlashCommand(slashCommandResult)
|
||||
}
|
||||
is ParsedCommand.KickUser -> {
|
||||
handleKickSlashCommand(slashCommandResult)
|
||||
is ParsedCommand.RemoveUser -> {
|
||||
handleRemoveSlashCommand(slashCommandResult)
|
||||
}
|
||||
is ParsedCommand.JoinRoom -> {
|
||||
handleJoinToAnotherRoomSlashCommand(slashCommandResult)
|
||||
|
@ -598,9 +598,9 @@ class MessageComposerViewModel @AssistedInject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
private fun handleKickSlashCommand(kick: ParsedCommand.KickUser) {
|
||||
private fun handleRemoveSlashCommand(removeUser: ParsedCommand.RemoveUser) {
|
||||
launchSlashCommandFlowSuspendable {
|
||||
room.kick(kick.userId, kick.reason)
|
||||
room.remove(removeUser.userId, removeUser.reason)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -21,11 +21,11 @@ import android.media.AudioAttributes
|
|||
import android.media.MediaPlayer
|
||||
import androidx.core.content.FileProvider
|
||||
import im.vector.app.BuildConfig
|
||||
import im.vector.app.core.utils.CountUpTimer
|
||||
import im.vector.app.features.home.room.detail.timeline.helper.VoiceMessagePlaybackTracker
|
||||
import im.vector.app.features.voice.VoiceFailure
|
||||
import im.vector.app.features.voice.VoiceRecorder
|
||||
import im.vector.app.features.voice.VoiceRecorderProvider
|
||||
import im.vector.lib.core.utils.timer.CountUpTimer
|
||||
import im.vector.lib.multipicker.entity.MultiPickerAudioType
|
||||
import im.vector.lib.multipicker.utils.toMultiPickerAudioType
|
||||
import org.matrix.android.sdk.api.extensions.orFalse
|
||||
|
|
|
@ -26,10 +26,10 @@ import im.vector.app.R
|
|||
import im.vector.app.core.extensions.exhaustive
|
||||
import im.vector.app.core.hardware.vibrate
|
||||
import im.vector.app.core.time.Clock
|
||||
import im.vector.app.core.utils.CountUpTimer
|
||||
import im.vector.app.core.utils.DimensionConverter
|
||||
import im.vector.app.databinding.ViewVoiceMessageRecorderBinding
|
||||
import im.vector.app.features.home.room.detail.timeline.helper.VoiceMessagePlaybackTracker
|
||||
import im.vector.lib.core.utils.timer.CountUpTimer
|
||||
import javax.inject.Inject
|
||||
import kotlin.math.floor
|
||||
|
||||
|
|
|
@ -104,7 +104,7 @@ class RoomListSectionBuilderGroup(
|
|||
}
|
||||
}
|
||||
|
||||
appStateHandler.selectedRoomGroupingObservable
|
||||
appStateHandler.selectedRoomGroupingFlow
|
||||
.distinctUntilChanged()
|
||||
.onEach { groupingMethod ->
|
||||
val selectedGroupId = (groupingMethod.orNull() as? RoomGroupingMethod.ByLegacyGroup)?.groupSummary?.groupId
|
||||
|
|
|
@ -132,7 +132,7 @@ class RoomListSectionBuilderSpace(
|
|||
}
|
||||
}
|
||||
|
||||
appStateHandler.selectedRoomGroupingObservable
|
||||
appStateHandler.selectedRoomGroupingFlow
|
||||
.distinctUntilChanged()
|
||||
.onEach { groupingMethod ->
|
||||
val selectedSpace = groupingMethod.orNull()?.space()
|
||||
|
@ -222,7 +222,7 @@ class RoomListSectionBuilderSpace(
|
|||
|
||||
// add suggested rooms
|
||||
val suggestedRoomsFlow = // MutableLiveData<List<SpaceChildInfo>>()
|
||||
appStateHandler.selectedRoomGroupingObservable
|
||||
appStateHandler.selectedRoomGroupingFlow
|
||||
.distinctUntilChanged()
|
||||
.flatMapLatest { groupingMethod ->
|
||||
val selectedSpace = groupingMethod.orNull()?.space()
|
||||
|
|
|
@ -92,7 +92,7 @@ class RoomListViewModel @AssistedInject constructor(
|
|||
init {
|
||||
observeMembershipChanges()
|
||||
|
||||
appStateHandler.selectedRoomGroupingObservable
|
||||
appStateHandler.selectedRoomGroupingFlow
|
||||
.distinctUntilChanged()
|
||||
.execute {
|
||||
copy(
|
||||
|
|
|
@ -309,8 +309,8 @@ class DefaultNavigator @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
override fun openCreateRoom(context: Context, initialName: String) {
|
||||
val intent = CreateRoomActivity.getIntent(context, initialName)
|
||||
override fun openCreateRoom(context: Context, initialName: String, openAfterCreate: Boolean) {
|
||||
val intent = CreateRoomActivity.getIntent(context = context, initialName = initialName, openAfterCreate = openAfterCreate)
|
||||
context.startActivity(intent)
|
||||
}
|
||||
|
||||
|
|
|
@ -76,7 +76,7 @@ interface Navigator {
|
|||
|
||||
fun openMatrixToBottomSheet(context: Context, link: String)
|
||||
|
||||
fun openCreateRoom(context: Context, initialName: String = "")
|
||||
fun openCreateRoom(context: Context, initialName: String = "", openAfterCreate: Boolean = true)
|
||||
|
||||
fun openCreateDirectRoom(context: Context)
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||
* Copyright (c) 2022 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.
|
||||
|
@ -14,14 +14,11 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.matrix.android.sdk.rx
|
||||
package im.vector.app.features.onboarding
|
||||
|
||||
data class SecretsSynchronisationInfo(
|
||||
val isBackupSetup: Boolean,
|
||||
val isCrossSigningEnabled: Boolean,
|
||||
val isCrossSigningTrusted: Boolean,
|
||||
val allPrivateKeysKnown: Boolean,
|
||||
val megolmBackupAvailable: Boolean,
|
||||
val megolmSecretKnown: Boolean,
|
||||
val isMegolmKeyIn4S: Boolean
|
||||
)
|
||||
enum class FtueUseCase {
|
||||
FRIENDS_FAMILY,
|
||||
TEAMS,
|
||||
COMMUNITIES,
|
||||
SKIP
|
||||
}
|
|
@ -31,6 +31,8 @@ sealed class OnboardingAction : VectorViewModelAction {
|
|||
|
||||
data class UpdateServerType(val serverType: ServerType) : OnboardingAction()
|
||||
data class UpdateHomeServer(val homeServerUrl: String) : OnboardingAction()
|
||||
data class UpdateUseCase(val useCase: FtueUseCase) : OnboardingAction()
|
||||
object ResetUseCase : OnboardingAction()
|
||||
data class UpdateSignMode(val signMode: SignMode) : OnboardingAction()
|
||||
data class LoginWithToken(val loginToken: String) : OnboardingAction()
|
||||
data class WebLoginSuccess(val credentials: Credentials) : OnboardingAction()
|
||||
|
|
|
@ -16,6 +16,7 @@
|
|||
|
||||
package im.vector.app.features.onboarding
|
||||
|
||||
import im.vector.app.core.platform.ScreenOrientationLocker
|
||||
import im.vector.app.databinding.ActivityLoginBinding
|
||||
import im.vector.app.features.VectorFeatures
|
||||
import im.vector.app.features.login2.LoginViewModel2
|
||||
|
@ -24,6 +25,7 @@ import javax.inject.Inject
|
|||
|
||||
class OnboardingVariantFactory @Inject constructor(
|
||||
private val vectorFeatures: VectorFeatures,
|
||||
private val orientationLocker: ScreenOrientationLocker,
|
||||
) {
|
||||
|
||||
fun create(activity: OnboardingActivity,
|
||||
|
@ -37,7 +39,8 @@ class OnboardingVariantFactory @Inject constructor(
|
|||
onboardingViewModel = onboardingViewModel.value,
|
||||
activity = activity,
|
||||
supportFragmentManager = activity.supportFragmentManager,
|
||||
vectorFeatures = vectorFeatures
|
||||
vectorFeatures = vectorFeatures,
|
||||
orientationLocker = orientationLocker
|
||||
)
|
||||
VectorFeatures.OnboardingVariant.LOGIN_2 -> Login2Variant(
|
||||
views = views,
|
||||
|
|
|
@ -34,6 +34,7 @@ sealed class OnboardingViewEvents : VectorViewEvents {
|
|||
|
||||
// Navigation event
|
||||
|
||||
object OpenUseCaseSelection : OnboardingViewEvents()
|
||||
object OpenServerSelection : OnboardingViewEvents()
|
||||
data class OnServerSelectionDone(val serverType: ServerType) : OnboardingViewEvents()
|
||||
object OnLoginFlowRetrieved : OnboardingViewEvents()
|
||||
|
|
|
@ -35,6 +35,7 @@ import im.vector.app.core.extensions.exhaustive
|
|||
import im.vector.app.core.platform.VectorViewModel
|
||||
import im.vector.app.core.resources.StringProvider
|
||||
import im.vector.app.core.utils.ensureTrailingSlash
|
||||
import im.vector.app.features.VectorFeatures
|
||||
import im.vector.app.features.login.HomeServerConnectionConfigFactory
|
||||
import im.vector.app.features.login.LoginConfig
|
||||
import im.vector.app.features.login.LoginMode
|
||||
|
@ -71,7 +72,8 @@ class OnboardingViewModel @AssistedInject constructor(
|
|||
private val homeServerConnectionConfigFactory: HomeServerConnectionConfigFactory,
|
||||
private val reAuthHelper: ReAuthHelper,
|
||||
private val stringProvider: StringProvider,
|
||||
private val homeServerHistoryService: HomeServerHistoryService
|
||||
private val homeServerHistoryService: HomeServerHistoryService,
|
||||
private val vectorFeatures: VectorFeatures
|
||||
) : VectorViewModel<OnboardingViewState, OnboardingAction, OnboardingViewEvents>(initialState) {
|
||||
|
||||
@AssistedFactory
|
||||
|
@ -123,6 +125,8 @@ class OnboardingViewModel @AssistedInject constructor(
|
|||
when (action) {
|
||||
is OnboardingAction.OnGetStarted -> handleSplashAction(action.resetLoginConfig, action.onboardingFlow)
|
||||
is OnboardingAction.OnIAlreadyHaveAnAccount -> handleSplashAction(action.resetLoginConfig, action.onboardingFlow)
|
||||
is OnboardingAction.UpdateUseCase -> handleUpdateUseCase()
|
||||
OnboardingAction.ResetUseCase -> resetUseCase()
|
||||
is OnboardingAction.UpdateServerType -> handleUpdateServerType(action)
|
||||
is OnboardingAction.UpdateSignMode -> handleUpdateSignMode(action)
|
||||
is OnboardingAction.InitWith -> handleInitWith(action)
|
||||
|
@ -154,15 +158,28 @@ class OnboardingViewModel @AssistedInject constructor(
|
|||
if (homeServerConnectionConfig == null) {
|
||||
// Url is invalid, in this case, just use the regular flow
|
||||
Timber.w("Url from config url was invalid: $configUrl")
|
||||
_viewEvents.post(OnboardingViewEvents.OpenServerSelection)
|
||||
continueToPageAfterSplash(onboardingFlow)
|
||||
} else {
|
||||
getLoginFlow(homeServerConnectionConfig, ServerType.Other)
|
||||
}
|
||||
} else {
|
||||
_viewEvents.post(OnboardingViewEvents.OpenServerSelection)
|
||||
continueToPageAfterSplash(onboardingFlow)
|
||||
}
|
||||
}
|
||||
|
||||
private fun continueToPageAfterSplash(onboardingFlow: OnboardingFlow) {
|
||||
val nextOnboardingStep = when (onboardingFlow) {
|
||||
OnboardingFlow.SignUp -> if (vectorFeatures.isOnboardingUseCaseEnabled()) {
|
||||
OnboardingViewEvents.OpenUseCaseSelection
|
||||
} else {
|
||||
OnboardingViewEvents.OpenServerSelection
|
||||
}
|
||||
OnboardingFlow.SignIn,
|
||||
OnboardingFlow.SignInSignUp -> OnboardingViewEvents.OpenServerSelection
|
||||
}
|
||||
_viewEvents.post(nextOnboardingStep)
|
||||
}
|
||||
|
||||
private fun handleUserAcceptCertificate(action: OnboardingAction.UserAcceptCertificate) {
|
||||
// It happens when we get the login flow, or during direct authentication.
|
||||
// So alter the homeserver config and retrieve again the login flow
|
||||
|
@ -441,6 +458,15 @@ class OnboardingViewModel @AssistedInject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
private fun handleUpdateUseCase() {
|
||||
// TODO act on the use case selection
|
||||
_viewEvents.post(OnboardingViewEvents.OpenServerSelection)
|
||||
}
|
||||
|
||||
private fun resetUseCase() {
|
||||
// TODO remove stored use case
|
||||
}
|
||||
|
||||
private fun handleUpdateServerType(action: OnboardingAction.UpdateServerType) {
|
||||
setState {
|
||||
copy(
|
||||
|
|
|
@ -49,7 +49,8 @@ private const val CAROUSEL_TRANSITION_TIME_MS = 500L
|
|||
class FtueAuthSplashCarouselFragment @Inject constructor(
|
||||
private val vectorPreferences: VectorPreferences,
|
||||
private val vectorFeatures: VectorFeatures,
|
||||
private val carouselController: SplashCarouselController
|
||||
private val carouselController: SplashCarouselController,
|
||||
private val carouselStateFactory: SplashCarouselStateFactory
|
||||
) : AbstractFtueAuthFragment<FragmentFtueSplashCarouselBinding>() {
|
||||
|
||||
override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentFtueSplashCarouselBinding {
|
||||
|
@ -65,11 +66,11 @@ class FtueAuthSplashCarouselFragment @Inject constructor(
|
|||
val carouselAdapter = carouselController.adapter
|
||||
views.splashCarousel.adapter = carouselAdapter
|
||||
TabLayoutMediator(views.carouselIndicator, views.splashCarousel) { _, _ -> }.attach()
|
||||
carouselController.setData(SplashCarouselState())
|
||||
carouselController.setData(carouselStateFactory.create())
|
||||
|
||||
views.loginSplashSubmit.debouncedClicks { getStarted() }
|
||||
views.loginSplashAlreadyHaveAccount.apply {
|
||||
isVisible = vectorFeatures.isAlreadyHaveAccountSplashEnabled()
|
||||
isVisible = vectorFeatures.isOnboardingAlreadyHaveAccountSplashEnabled()
|
||||
debouncedClicks { alreadyHaveAnAccount() }
|
||||
}
|
||||
|
||||
|
@ -111,7 +112,7 @@ class FtueAuthSplashCarouselFragment @Inject constructor(
|
|||
}
|
||||
|
||||
private fun getStarted() {
|
||||
val getStartedFlow = if (vectorFeatures.isAlreadyHaveAccountSplashEnabled()) OnboardingFlow.SignUp else OnboardingFlow.SignInSignUp
|
||||
val getStartedFlow = if (vectorFeatures.isOnboardingAlreadyHaveAccountSplashEnabled()) OnboardingFlow.SignUp else OnboardingFlow.SignInSignUp
|
||||
viewModel.handle(OnboardingAction.OnGetStarted(resetLoginConfig = false, onboardingFlow = getStartedFlow))
|
||||
}
|
||||
|
||||
|
|
|
@ -55,7 +55,7 @@ class FtueAuthSplashFragment @Inject constructor(
|
|||
private fun setupViews() {
|
||||
views.loginSplashSubmit.debouncedClicks { getStarted() }
|
||||
views.loginSplashAlreadyHaveAccount.apply {
|
||||
isVisible = vectorFeatures.isAlreadyHaveAccountSplashEnabled()
|
||||
isVisible = vectorFeatures.isOnboardingAlreadyHaveAccountSplashEnabled()
|
||||
debouncedClicks { alreadyHaveAnAccount() }
|
||||
}
|
||||
|
||||
|
@ -70,7 +70,7 @@ class FtueAuthSplashFragment @Inject constructor(
|
|||
}
|
||||
|
||||
private fun getStarted() {
|
||||
val getStartedFlow = if (vectorFeatures.isAlreadyHaveAccountSplashEnabled()) OnboardingFlow.SignUp else OnboardingFlow.SignInSignUp
|
||||
val getStartedFlow = if (vectorFeatures.isOnboardingAlreadyHaveAccountSplashEnabled()) OnboardingFlow.SignUp else OnboardingFlow.SignInSignUp
|
||||
viewModel.handle(OnboardingAction.OnGetStarted(resetLoginConfig = false, onboardingFlow = getStartedFlow))
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,72 @@
|
|||
/*
|
||||
* 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.onboarding.ftueauth
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.TextView
|
||||
import androidx.annotation.StringRes
|
||||
import im.vector.app.R
|
||||
import im.vector.app.core.extensions.setTextWithColoredPart
|
||||
import im.vector.app.databinding.FragmentFtueAuthUseCaseBinding
|
||||
import im.vector.app.features.login.ServerType
|
||||
import im.vector.app.features.onboarding.FtueUseCase
|
||||
import im.vector.app.features.onboarding.OnboardingAction
|
||||
import javax.inject.Inject
|
||||
|
||||
class FtueAuthUseCaseFragment @Inject constructor() : AbstractFtueAuthFragment<FragmentFtueAuthUseCaseBinding>() {
|
||||
|
||||
override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentFtueAuthUseCaseBinding {
|
||||
return FragmentFtueAuthUseCaseBinding.inflate(inflater, container, false)
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
setupViews()
|
||||
}
|
||||
|
||||
private fun setupViews() {
|
||||
views.useCaseOptionOne.setUseCase(R.string.ftue_auth_use_case_option_one, FtueUseCase.FRIENDS_FAMILY)
|
||||
views.useCaseOptionTwo.setUseCase(R.string.ftue_auth_use_case_option_two, FtueUseCase.TEAMS)
|
||||
views.useCaseOptionThree.setUseCase(R.string.ftue_auth_use_case_option_three, FtueUseCase.COMMUNITIES)
|
||||
|
||||
views.useCaseSkip.setTextWithColoredPart(
|
||||
fullTextRes = R.string.ftue_auth_use_case_skip,
|
||||
coloredTextRes = R.string.ftue_auth_use_case_skip_partial,
|
||||
underline = false,
|
||||
colorAttribute = R.attr.colorAccent,
|
||||
onClick = { viewModel.handle(OnboardingAction.UpdateUseCase(FtueUseCase.SKIP)) }
|
||||
)
|
||||
|
||||
views.useCaseConnectToServer.setOnClickListener {
|
||||
viewModel.handle(OnboardingAction.UpdateServerType(ServerType.Other))
|
||||
}
|
||||
}
|
||||
|
||||
override fun resetViewModel() {
|
||||
viewModel.handle(OnboardingAction.ResetUseCase)
|
||||
}
|
||||
|
||||
private fun TextView.setUseCase(@StringRes label: Int, useCase: FtueUseCase) {
|
||||
setText(label)
|
||||
debouncedClicks {
|
||||
viewModel.handle(OnboardingAction.UpdateUseCase(useCase))
|
||||
}
|
||||
}
|
||||
}
|
|
@ -15,6 +15,7 @@
|
|||
*/
|
||||
|
||||
package im.vector.app.features.onboarding.ftueauth
|
||||
|
||||
import android.content.Intent
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
|
@ -31,6 +32,7 @@ import im.vector.app.core.extensions.POP_BACK_STACK_EXCLUSIVE
|
|||
import im.vector.app.core.extensions.addFragment
|
||||
import im.vector.app.core.extensions.addFragmentToBackstack
|
||||
import im.vector.app.core.extensions.exhaustive
|
||||
import im.vector.app.core.platform.ScreenOrientationLocker
|
||||
import im.vector.app.core.platform.VectorBaseActivity
|
||||
import im.vector.app.databinding.ActivityLoginBinding
|
||||
import im.vector.app.features.VectorFeatures
|
||||
|
@ -62,7 +64,8 @@ class FtueAuthVariant(
|
|||
private val onboardingViewModel: OnboardingViewModel,
|
||||
private val activity: VectorBaseActivity<ActivityLoginBinding>,
|
||||
private val supportFragmentManager: FragmentManager,
|
||||
private val vectorFeatures: VectorFeatures
|
||||
private val vectorFeatures: VectorFeatures,
|
||||
private val orientationLocker: ScreenOrientationLocker,
|
||||
) : OnboardingVariant {
|
||||
|
||||
private val enterAnim = R.anim.enter_fade_in
|
||||
|
@ -91,6 +94,7 @@ class FtueAuthVariant(
|
|||
}
|
||||
|
||||
with(activity) {
|
||||
orientationLocker.lockPhonesToPortrait(this)
|
||||
onboardingViewModel.onEach {
|
||||
updateWithState(it)
|
||||
}
|
||||
|
@ -109,7 +113,7 @@ class FtueAuthVariant(
|
|||
}
|
||||
|
||||
private fun addFirstFragment() {
|
||||
val splashFragment = when (vectorFeatures.isSplashCarouselEnabled()) {
|
||||
val splashFragment = when (vectorFeatures.isOnboardingSplashCarouselEnabled()) {
|
||||
true -> FtueAuthSplashCarouselFragment::class.java
|
||||
else -> FtueAuthSplashFragment::class.java
|
||||
}
|
||||
|
@ -151,13 +155,16 @@ class FtueAuthVariant(
|
|||
activity.addFragmentToBackstack(views.loginFragmentContainer,
|
||||
FtueAuthServerSelectionFragment::class.java,
|
||||
option = { ft ->
|
||||
activity.findViewById<View?>(R.id.loginSplashLogo)?.let { ft.addSharedElement(it, ViewCompat.getTransitionName(it) ?: "") }
|
||||
// Disable transition of text
|
||||
// findViewById<View?>(R.id.loginSplashTitle)?.let { ft.addSharedElement(it, ViewCompat.getTransitionName(it) ?: "") }
|
||||
// No transition here now actually
|
||||
// findViewById<View?>(R.id.loginSplashSubmit)?.let { ft.addSharedElement(it, ViewCompat.getTransitionName(it) ?: "") }
|
||||
// TODO Disabled because it provokes a flickering
|
||||
// ft.setCustomAnimations(enterAnim, exitAnim, popEnterAnim, popExitAnim)
|
||||
if (vectorFeatures.isOnboardingUseCaseEnabled()) {
|
||||
ft.setCustomAnimations(enterAnim, exitAnim, popEnterAnim, popExitAnim)
|
||||
} else {
|
||||
activity.findViewById<View?>(R.id.loginSplashLogo)?.let { ft.addSharedElement(it, ViewCompat.getTransitionName(it) ?: "") }
|
||||
// TODO Disabled because it provokes a flickering
|
||||
// Disable transition of text
|
||||
// findViewById<View?>(R.id.loginSplashTitle)?.let { ft.addSharedElement(it, ViewCompat.getTransitionName(it) ?: "") }
|
||||
// No transition here now actually
|
||||
// findViewById<View?>(R.id.loginSplashSubmit)?.let { ft.addSharedElement(it, ViewCompat.getTransitionName(it) ?: "") }
|
||||
}
|
||||
})
|
||||
is OnboardingViewEvents.OnServerSelectionDone -> onServerSelectionDone(viewEvents)
|
||||
is OnboardingViewEvents.OnSignModeSelected -> onSignModeSelected(viewEvents)
|
||||
|
@ -208,6 +215,11 @@ class FtueAuthVariant(
|
|||
is OnboardingViewEvents.Loading ->
|
||||
// This is handled by the Fragments
|
||||
Unit
|
||||
OnboardingViewEvents.OpenUseCaseSelection -> {
|
||||
activity.addFragmentToBackstack(views.loginFragmentContainer,
|
||||
FtueAuthUseCaseFragment::class.java,
|
||||
option = commonOption)
|
||||
}
|
||||
}.exhaustive
|
||||
}
|
||||
|
||||
|
|
|
@ -35,7 +35,7 @@ abstract class SplashCarouselItem : VectorEpoxyModel<SplashCarouselItem.Holder>(
|
|||
|
||||
holder.view.setBackgroundResource(item.pageBackground)
|
||||
holder.image.setImageResource(item.image)
|
||||
holder.title.setText(item.title)
|
||||
holder.title.text = item.title.charSequence
|
||||
holder.body.setText(item.body)
|
||||
}
|
||||
|
||||
|
|
|
@ -18,38 +18,13 @@ package im.vector.app.features.onboarding.ftueauth
|
|||
|
||||
import androidx.annotation.DrawableRes
|
||||
import androidx.annotation.StringRes
|
||||
import im.vector.app.R
|
||||
import im.vector.app.core.epoxy.charsequence.EpoxyCharSequence
|
||||
|
||||
data class SplashCarouselState(
|
||||
val items: List<Item> = listOf(
|
||||
Item(
|
||||
R.string.ftue_auth_carousel_1_title,
|
||||
R.string.ftue_auth_carousel_1_body,
|
||||
R.drawable.onboarding_carousel_conversations,
|
||||
R.drawable.bg_carousel_page_1
|
||||
),
|
||||
Item(
|
||||
R.string.ftue_auth_carousel_2_title,
|
||||
R.string.ftue_auth_carousel_2_body,
|
||||
R.drawable.onboarding_carousel_ems,
|
||||
R.drawable.bg_carousel_page_2
|
||||
),
|
||||
Item(
|
||||
R.string.ftue_auth_carousel_3_title,
|
||||
R.string.ftue_auth_carousel_3_body,
|
||||
R.drawable.onboarding_carousel_connect,
|
||||
R.drawable.bg_carousel_page_3
|
||||
),
|
||||
Item(
|
||||
R.string.ftue_auth_carousel_4_title,
|
||||
R.string.ftue_auth_carousel_4_body,
|
||||
R.drawable.onboarding_carousel_universal,
|
||||
R.drawable.bg_carousel_page_4
|
||||
)
|
||||
)
|
||||
val items: List<Item>
|
||||
) {
|
||||
data class Item(
|
||||
@StringRes val title: Int,
|
||||
val title: EpoxyCharSequence,
|
||||
@StringRes val body: Int,
|
||||
@DrawableRes val image: Int,
|
||||
@DrawableRes val pageBackground: Int
|
||||
|
|
|
@ -0,0 +1,94 @@
|
|||
/*
|
||||
* Copyright (c) 2022 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.onboarding.ftueauth
|
||||
|
||||
import android.content.Context
|
||||
import androidx.annotation.AttrRes
|
||||
import androidx.annotation.DrawableRes
|
||||
import im.vector.app.R
|
||||
import im.vector.app.core.epoxy.charsequence.EpoxyCharSequence
|
||||
import im.vector.app.core.epoxy.charsequence.toEpoxyCharSequence
|
||||
import im.vector.app.core.resources.LocaleProvider
|
||||
import im.vector.app.core.resources.StringProvider
|
||||
import im.vector.app.core.resources.isEnglishSpeaking
|
||||
import im.vector.app.features.themes.ThemeProvider
|
||||
import im.vector.app.features.themes.ThemeUtils
|
||||
import me.gujun.android.span.span
|
||||
import javax.inject.Inject
|
||||
|
||||
class SplashCarouselStateFactory @Inject constructor(
|
||||
private val context: Context,
|
||||
private val stringProvider: StringProvider,
|
||||
private val localeProvider: LocaleProvider,
|
||||
private val themeProvider: ThemeProvider,
|
||||
) {
|
||||
|
||||
fun create(): SplashCarouselState {
|
||||
val lightTheme = themeProvider.isLightTheme()
|
||||
fun background(@DrawableRes lightDrawable: Int) = if (lightTheme) lightDrawable else R.drawable.bg_carousel_page_dark
|
||||
fun hero(@DrawableRes lightDrawable: Int, @DrawableRes darkDrawable: Int) = if (lightTheme) lightDrawable else darkDrawable
|
||||
return SplashCarouselState(listOf(
|
||||
SplashCarouselState.Item(
|
||||
R.string.ftue_auth_carousel_1_title.colorTerminatingFullStop(R.attr.colorAccent),
|
||||
R.string.ftue_auth_carousel_body_secure,
|
||||
hero(R.drawable.ic_splash_conversations, R.drawable.ic_splash_conversations_dark),
|
||||
background(R.drawable.bg_carousel_page_1)
|
||||
),
|
||||
SplashCarouselState.Item(
|
||||
R.string.ftue_auth_carousel_2_title.colorTerminatingFullStop(R.attr.colorAccent),
|
||||
R.string.ftue_auth_carousel_body_control,
|
||||
hero(R.drawable.ic_splash_control, R.drawable.ic_splash_control_dark),
|
||||
background(R.drawable.bg_carousel_page_2)
|
||||
),
|
||||
SplashCarouselState.Item(
|
||||
R.string.ftue_auth_carousel_3_title.colorTerminatingFullStop(R.attr.colorAccent),
|
||||
R.string.ftue_auth_carousel_body_encrypted,
|
||||
hero(R.drawable.ic_splash_secure, R.drawable.ic_splash_secure_dark),
|
||||
background(R.drawable.bg_carousel_page_3)
|
||||
),
|
||||
SplashCarouselState.Item(
|
||||
collaborationTitle().colorTerminatingFullStop(R.attr.colorAccent),
|
||||
R.string.ftue_auth_carousel_body_workplace,
|
||||
hero(R.drawable.ic_splash_collaboration, R.drawable.ic_splash_collaboration_dark),
|
||||
background(R.drawable.bg_carousel_page_4)
|
||||
)
|
||||
))
|
||||
}
|
||||
|
||||
private fun collaborationTitle(): Int {
|
||||
return when {
|
||||
localeProvider.isEnglishSpeaking() -> R.string.cut_the_slack_from_teams
|
||||
else -> R.string.ftue_auth_carousel_title_messaging
|
||||
}
|
||||
}
|
||||
|
||||
private fun Int.colorTerminatingFullStop(@AttrRes color: Int): EpoxyCharSequence {
|
||||
val string = stringProvider.getString(this)
|
||||
val fullStop = "."
|
||||
val charSequence = if (string.endsWith(fullStop)) {
|
||||
span {
|
||||
+string.removeSuffix(fullStop)
|
||||
span(fullStop) {
|
||||
textColor = ThemeUtils.getColor(context, color)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
string
|
||||
}
|
||||
return charSequence.toEpoxyCharSequence()
|
||||
}
|
||||
}
|
|
@ -25,6 +25,7 @@ import com.tapadoo.alerter.Alerter
|
|||
import im.vector.app.R
|
||||
import im.vector.app.core.platform.VectorBaseActivity
|
||||
import im.vector.app.core.utils.isAnimationDisabled
|
||||
import im.vector.app.features.analytics.ui.consent.AnalyticsOptInActivity
|
||||
import im.vector.app.features.pin.PinActivity
|
||||
import im.vector.app.features.signout.hard.SignedOutActivity
|
||||
import im.vector.app.features.themes.ThemeUtils
|
||||
|
@ -300,6 +301,7 @@ class PopupAlertManager @Inject constructor() {
|
|||
return alert != null &&
|
||||
activity !is PinActivity &&
|
||||
activity !is SignedOutActivity &&
|
||||
activity !is AnalyticsOptInActivity &&
|
||||
activity is VectorBaseActivity<*> &&
|
||||
alert.shouldBeDisplayedIn.invoke(activity)
|
||||
}
|
||||
|
|
|
@ -32,10 +32,10 @@ import dagger.hilt.android.AndroidEntryPoint
|
|||
import im.vector.app.EmojiCompatFontProvider
|
||||
import im.vector.app.R
|
||||
import im.vector.app.core.extensions.observeEvent
|
||||
import im.vector.app.core.flow.throttleFirst
|
||||
import im.vector.app.core.platform.VectorBaseActivity
|
||||
import im.vector.app.databinding.ActivityEmojiReactionPickerBinding
|
||||
import im.vector.app.features.reactions.data.EmojiDataSource
|
||||
import im.vector.lib.core.utils.flow.throttleFirst
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.launch
|
||||
|
|
|
@ -25,5 +25,6 @@ sealed class RoomDirectorySharedAction : VectorSharedAction {
|
|||
object Back : RoomDirectorySharedAction()
|
||||
object CreateRoom : RoomDirectorySharedAction()
|
||||
object Close : RoomDirectorySharedAction()
|
||||
data class CreateRoomSuccess(val createdRoomId: String) : RoomDirectorySharedAction()
|
||||
object ChangeProtocol : RoomDirectorySharedAction()
|
||||
}
|
||||
|
|
|
@ -16,10 +16,12 @@
|
|||
|
||||
package im.vector.app.features.roomdirectory.createroom
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.airbnb.mvrx.Mavericks
|
||||
import com.google.android.material.appbar.MaterialToolbar
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import im.vector.app.core.extensions.addFragment
|
||||
|
@ -49,13 +51,11 @@ class CreateRoomActivity : VectorBaseActivity<ActivitySimpleBinding>(), ToolbarC
|
|||
|
||||
override fun initUiAndData() {
|
||||
if (isFirstCreation()) {
|
||||
val fragmentArgs: CreateRoomArgs = intent?.extras?.getParcelable(Mavericks.KEY_ARG) ?: return
|
||||
addFragment(
|
||||
views.simpleFragmentContainer,
|
||||
CreateRoomFragment::class.java,
|
||||
CreateRoomArgs(
|
||||
intent?.getStringExtra(INITIAL_NAME) ?: "",
|
||||
isSpace = intent?.getBooleanExtra(IS_SPACE, false) ?: false
|
||||
)
|
||||
fragmentArgs
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -68,21 +68,40 @@ class CreateRoomActivity : VectorBaseActivity<ActivitySimpleBinding>(), ToolbarC
|
|||
.onEach { sharedAction ->
|
||||
when (sharedAction) {
|
||||
is RoomDirectorySharedAction.Back,
|
||||
is RoomDirectorySharedAction.Close -> finish()
|
||||
is RoomDirectorySharedAction.Close -> finish()
|
||||
is RoomDirectorySharedAction.CreateRoomSuccess -> {
|
||||
setResult(Activity.RESULT_OK, Intent().apply { putExtra(RESULT_CREATED_ROOM_ID, sharedAction.createdRoomId) })
|
||||
finish()
|
||||
}
|
||||
else -> {
|
||||
// nop
|
||||
}
|
||||
}
|
||||
}
|
||||
.launchIn(lifecycleScope)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val INITIAL_NAME = "INITIAL_NAME"
|
||||
private const val IS_SPACE = "IS_SPACE"
|
||||
|
||||
fun getIntent(context: Context, initialName: String = "", isSpace: Boolean = false): Intent {
|
||||
private const val RESULT_CREATED_ROOM_ID = "RESULT_CREATED_ROOM_ID"
|
||||
|
||||
fun getIntent(context: Context,
|
||||
initialName: String = "",
|
||||
isSpace: Boolean = false,
|
||||
openAfterCreate: Boolean = true,
|
||||
currentSpaceId: String? = null): Intent {
|
||||
return Intent(context, CreateRoomActivity::class.java).apply {
|
||||
putExtra(INITIAL_NAME, initialName)
|
||||
putExtra(IS_SPACE, isSpace)
|
||||
putExtra(Mavericks.KEY_ARG, CreateRoomArgs(
|
||||
initialName = initialName,
|
||||
isSpace = isSpace,
|
||||
openAfterCreate = openAfterCreate,
|
||||
parentSpaceId = currentSpaceId
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
fun getCreatedRoomId(data: Intent?): String? {
|
||||
return data?.extras?.getString(RESULT_CREATED_ROOM_ID)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -56,7 +56,8 @@ import javax.inject.Inject
|
|||
data class CreateRoomArgs(
|
||||
val initialName: String,
|
||||
val parentSpaceId: String? = null,
|
||||
val isSpace: Boolean = false
|
||||
val isSpace: Boolean = false,
|
||||
val openAfterCreate: Boolean = true
|
||||
) : Parcelable
|
||||
|
||||
class CreateRoomFragment @Inject constructor(
|
||||
|
@ -226,16 +227,19 @@ class CreateRoomFragment @Inject constructor(
|
|||
views.waitingView.root.isVisible = async is Loading
|
||||
if (async is Success) {
|
||||
// Navigate to freshly created room
|
||||
if (state.isSubSpace) {
|
||||
navigator.switchToSpace(
|
||||
requireContext(),
|
||||
async(),
|
||||
Navigator.PostSwitchSpaceAction.None
|
||||
)
|
||||
} else {
|
||||
navigator.openRoom(requireActivity(), async())
|
||||
if (state.openAfterCreate) {
|
||||
if (state.isSubSpace) {
|
||||
navigator.switchToSpace(
|
||||
requireContext(),
|
||||
async(),
|
||||
Navigator.PostSwitchSpaceAction.None
|
||||
)
|
||||
} else {
|
||||
navigator.openRoom(requireActivity(), async())
|
||||
}
|
||||
}
|
||||
|
||||
sharedActionViewModel.post(RoomDirectorySharedAction.CreateRoomSuccess(async()))
|
||||
sharedActionViewModel.post(RoomDirectorySharedAction.Close)
|
||||
} else {
|
||||
// Populate list with Epoxy
|
||||
|
|
|
@ -25,6 +25,7 @@ import com.airbnb.mvrx.Uninitialized
|
|||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedFactory
|
||||
import dagger.assisted.AssistedInject
|
||||
import im.vector.app.AppStateHandler
|
||||
import im.vector.app.core.di.MavericksAssistedViewModelFactory
|
||||
import im.vector.app.core.di.hiltMavericksViewModelFactory
|
||||
import im.vector.app.core.extensions.exhaustive
|
||||
|
@ -53,7 +54,8 @@ import timber.log.Timber
|
|||
|
||||
class CreateRoomViewModel @AssistedInject constructor(@Assisted private val initialState: CreateRoomViewState,
|
||||
private val session: Session,
|
||||
private val rawService: RawService
|
||||
private val rawService: RawService,
|
||||
appStateHandler: AppStateHandler
|
||||
) : VectorViewModel<CreateRoomViewState, CreateRoomAction, CreateRoomViewEvents>(initialState) {
|
||||
|
||||
@AssistedFactory
|
||||
|
@ -67,10 +69,12 @@ class CreateRoomViewModel @AssistedInject constructor(@Assisted private val init
|
|||
initHomeServerName()
|
||||
initAdminE2eByDefault()
|
||||
|
||||
val parentSpaceId = initialState.parentSpaceId ?: appStateHandler.safeActiveSpaceId()
|
||||
|
||||
val restrictedSupport = session.getHomeServerCapabilities().isFeatureSupported(HomeServerCapabilities.ROOM_CAP_RESTRICTED)
|
||||
val createRestricted = restrictedSupport == HomeServerCapabilities.RoomCapabilitySupport.SUPPORTED
|
||||
|
||||
val defaultJoinRules = if (initialState.parentSpaceId != null && createRestricted) {
|
||||
val defaultJoinRules = if (parentSpaceId != null && createRestricted) {
|
||||
RoomJoinRules.RESTRICTED
|
||||
} else {
|
||||
RoomJoinRules.INVITE
|
||||
|
@ -78,9 +82,10 @@ class CreateRoomViewModel @AssistedInject constructor(@Assisted private val init
|
|||
|
||||
setState {
|
||||
copy(
|
||||
parentSpaceId = parentSpaceId,
|
||||
supportsRestricted = createRestricted,
|
||||
roomJoinRules = defaultJoinRules,
|
||||
parentSpaceSummary = initialState.parentSpaceId?.let { session.getRoomSummary(it) }
|
||||
parentSpaceSummary = parentSpaceId?.let { session.getRoomSummary(it) }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -156,7 +161,7 @@ class CreateRoomViewModel @AssistedInject constructor(@Assisted private val init
|
|||
CreateRoomViewState(
|
||||
isEncrypted = adminE2EByDefault,
|
||||
hsAdminHasDisabledE2E = !adminE2EByDefault,
|
||||
parentSpaceId = initialState.parentSpaceId
|
||||
parentSpaceId = this.parentSpaceId
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -292,11 +297,11 @@ class CreateRoomViewModel @AssistedInject constructor(@Assisted private val init
|
|||
runCatching { session.createRoom(createRoomParams) }.fold(
|
||||
{ roomId ->
|
||||
|
||||
if (initialState.parentSpaceId != null) {
|
||||
if (state.parentSpaceId != null) {
|
||||
// add it as a child
|
||||
try {
|
||||
session.spaceService()
|
||||
.getSpace(initialState.parentSpaceId)
|
||||
.getSpace(state.parentSpaceId)
|
||||
?.addChildren(roomId, viaServers = null, order = null)
|
||||
} catch (failure: Throwable) {
|
||||
Timber.w(failure, "Failed to add as a child")
|
||||
|
|
|
@ -39,13 +39,15 @@ data class CreateRoomViewState(
|
|||
val parentSpaceSummary: RoomSummary? = null,
|
||||
val supportsRestricted: Boolean = false,
|
||||
val aliasLocalPart: String? = null,
|
||||
val isSubSpace: Boolean = false
|
||||
val isSubSpace: Boolean = false,
|
||||
val openAfterCreate: Boolean = true
|
||||
) : MavericksState {
|
||||
|
||||
constructor(args: CreateRoomArgs) : this(
|
||||
roomName = args.initialName,
|
||||
parentSpaceId = args.parentSpaceId,
|
||||
isSubSpace = args.isSpace
|
||||
isSubSpace = args.isSpace,
|
||||
openAfterCreate = args.openAfterCreate
|
||||
)
|
||||
|
||||
/**
|
||||
|
|
|
@ -255,7 +255,7 @@ class RoomMemberProfileViewModel @AssistedInject constructor(
|
|||
viewModelScope.launch {
|
||||
try {
|
||||
_viewEvents.post(RoomMemberProfileViewEvents.Loading())
|
||||
room.kick(initialState.userId, action.reason)
|
||||
room.remove(initialState.userId, action.reason)
|
||||
_viewEvents.post(RoomMemberProfileViewEvents.OnKickActionSuccess)
|
||||
} catch (failure: Throwable) {
|
||||
_viewEvents.post(RoomMemberProfileViewEvents.Failure(failure))
|
||||
|
|
|
@ -29,12 +29,12 @@ import dagger.assisted.AssistedInject
|
|||
import im.vector.app.R
|
||||
import im.vector.app.core.di.MavericksAssistedViewModelFactory
|
||||
import im.vector.app.core.di.hiltMavericksViewModelFactory
|
||||
import im.vector.app.core.flow.throttleFirst
|
||||
import im.vector.app.core.platform.VectorViewModel
|
||||
import im.vector.app.core.resources.StringProvider
|
||||
import im.vector.app.core.utils.PublishDataSource
|
||||
import im.vector.app.features.auth.ReAuthActivity
|
||||
import im.vector.app.features.login.ReAuthHelper
|
||||
import im.vector.lib.core.utils.flow.throttleFirst
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
|
|
|
@ -16,6 +16,7 @@
|
|||
|
||||
package im.vector.app.features.spaces
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
|
@ -25,13 +26,16 @@ import com.airbnb.mvrx.Mavericks
|
|||
import com.airbnb.mvrx.viewModel
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import im.vector.app.R
|
||||
import im.vector.app.core.extensions.registerStartForActivityResult
|
||||
import im.vector.app.core.extensions.replaceFragment
|
||||
import im.vector.app.core.platform.VectorBaseActivity
|
||||
import im.vector.app.databinding.ActivitySimpleBinding
|
||||
import im.vector.app.features.matrixto.MatrixToBottomSheet
|
||||
import im.vector.app.features.navigation.Navigator
|
||||
import im.vector.app.features.roomdirectory.createroom.CreateRoomActivity
|
||||
import im.vector.app.features.spaces.explore.SpaceDirectoryArgs
|
||||
import im.vector.app.features.spaces.explore.SpaceDirectoryFragment
|
||||
import im.vector.app.features.spaces.explore.SpaceDirectoryViewAction
|
||||
import im.vector.app.features.spaces.explore.SpaceDirectoryViewEvents
|
||||
import im.vector.app.features.spaces.explore.SpaceDirectoryViewModel
|
||||
|
||||
|
@ -44,6 +48,15 @@ class SpaceExploreActivity : VectorBaseActivity<ActivitySimpleBinding>(), Matrix
|
|||
|
||||
val sharedViewModel: SpaceDirectoryViewModel by viewModel()
|
||||
|
||||
private val createRoomResultLauncher = registerStartForActivityResult { activityResult ->
|
||||
if (activityResult.resultCode == Activity.RESULT_OK) {
|
||||
CreateRoomActivity.getCreatedRoomId(activityResult.data)?.let {
|
||||
// we want to refresh from API
|
||||
sharedViewModel.handle(SpaceDirectoryViewAction.RefreshUntilFound(it))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val fragmentLifecycleCallbacks = object : FragmentManager.FragmentLifecycleCallbacks() {
|
||||
override fun onFragmentResumed(fm: FragmentManager, f: Fragment) {
|
||||
if (f is MatrixToBottomSheet) {
|
||||
|
@ -84,6 +97,13 @@ class SpaceExploreActivity : VectorBaseActivity<ActivitySimpleBinding>(), Matrix
|
|||
is SpaceDirectoryViewEvents.NavigateToMxToBottomSheet -> {
|
||||
MatrixToBottomSheet.withLink(it.link).show(supportFragmentManager, "ShowChild")
|
||||
}
|
||||
is SpaceDirectoryViewEvents.NavigateToCreateNewRoom -> {
|
||||
createRoomResultLauncher.launch(CreateRoomActivity.getIntent(
|
||||
this,
|
||||
openAfterCreate = false,
|
||||
currentSpaceId = it.currentSpaceId
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue