Merge branch 'develop' into feature/aris/threads

# Conflicts:
#	matrix-sdk-android-rx/src/main/java/org/matrix/android/sdk/rx/RxRoom.kt
#	matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/room/RoomSyncHandler.kt
#	vector/src/main/java/im/vector/app/features/command/Command.kt
#	vector/src/main/java/im/vector/app/features/command/CommandParser.kt
#	vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerViewModel.kt
This commit is contained in:
ariskotsomitopoulos 2022-01-18 12:41:40 +02:00
commit 636474b748
169 changed files with 1327 additions and 1014 deletions

View file

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

View file

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

View file

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

View file

@ -158,13 +158,3 @@ ext {
// }
// }
//}
//
//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
View file

@ -0,0 +1 @@
Explore Rooms overflow menu - content update include "Create room"

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

@ -0,0 +1 @@
Fix sync timeout after returning from background

1
changelog.d/4811.feature Normal file
View file

@ -0,0 +1 @@
Enabling native support for window resizing

1
changelog.d/4865.misc Normal file
View file

@ -0,0 +1 @@
"/kick" command is replaced with "/remove". Also replaced all occurrences in string resources

1
changelog.d/4880.wip Normal file
View file

@ -0,0 +1 @@
Updates the onboarding carousel images, copy and improves the handling of different device sizes

1
changelog.d/4895.removal Normal file
View file

@ -0,0 +1 @@
`StateService.sendStateEvent()` now takes a non-nullable String for the parameter `stateKey`. If null was used, just now use an empty string.

1
changelog.d/4914.wip Normal file
View file

@ -0,0 +1 @@
Disabling onboarding automatic carousel transitions on user interaction

1
changelog.d/4918.wip Normal file
View file

@ -0,0 +1 @@
Locking phones to portrait during the FTUE onboarding

1
changelog.d/4927.wip Normal file
View file

@ -0,0 +1 @@
Adds a messaging use case screen to the FTUE onboarding

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

@ -0,0 +1 @@
Fix a wrong network error issue in the Legals screen

1
changelog.d/4942.misc Normal file
View file

@ -0,0 +1 @@
Remove unused module matrix-sdk-android-rx and do some cleanup

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

@ -0,0 +1 @@
Prevent Alerts to be displayed in the automatically displayed analytics opt-in screen

1
changelog.d/4960.misc Normal file
View file

@ -0,0 +1 @@
Improves local echo blinking when non room events received

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

@ -0,0 +1 @@
Fix for stuck local event messages at the bottom of the screen

View file

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

@ -0,0 +1 @@
/build

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

View file

@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest package="im.vector.lib.core.utils" />

View file

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

View file

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

View file

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

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

View file

@ -2,5 +2,6 @@
<resources>
<dimen name="width_percent">0.6</dimen>
<bool name="is_tablet">true</bool>
</resources>

View file

@ -55,4 +55,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>

View file

@ -2,5 +2,6 @@
<resources>
<dimen name="width_percent">1</dimen>
<bool name="is_tablet">false</bool>
</resources>

View file

@ -1 +0,0 @@
/build

View file

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

View file

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

View file

@ -1 +0,0 @@
<manifest package="org.matrix.android.sdk.rx" />

View file

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

View file

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

View file

@ -1,161 +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 = attachment,
compressBeforeSending = compressBeforeSending,
roomIds = roomIds)
}
}
fun Room.rx(): RxRoom {
return RxRoom(this)
}

View file

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

View file

@ -118,6 +118,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'

View file

@ -62,7 +62,7 @@ class EncryptionTest : InstrumentedTest {
// Send an encryption Event as a State Event
room.sendStateEvent(
eventType = EventType.STATE_ROOM_ENCRYPTION,
stateKey = null,
stateKey = "",
body = EncryptionEventContent(algorithm = MXCRYPTO_ALGORITHM_MEGOLM).toContent()
)
}

View file

@ -550,7 +550,7 @@ class SpaceHierarchyTest : InstrumentedTest {
?.setUserPowerLevel(aliceSession.myUserId, Role.Admin.value)
?.toContent()
room.sendStateEvent(EventType.STATE_ROOM_POWER_LEVELS, null, newPowerLevelsContent!!)
room.sendStateEvent(EventType.STATE_ROOM_POWER_LEVELS, stateKey = "", newPowerLevelsContent!!)
it.countDown()
}

View file

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

View file

@ -68,8 +68,11 @@ interface StateService {
/**
* Send a state event to the room
* @param eventType The type of event to send.
* @param stateKey The state_key for the state to send. Can be an empty string.
* @param body The content object of the event; the fields in this object will vary depending on the type of event
*/
suspend fun sendStateEvent(eventType: String, stateKey: String?, body: JsonDict)
suspend fun sendStateEvent(eventType: String, stateKey: String, body: JsonDict)
/**
* Get a state event of the room

View file

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

View file

@ -130,7 +130,7 @@ internal class DefaultRoom(override val roomId: String,
else -> {
val params = SendStateTask.Params(
roomId = roomId,
stateKey = null,
stateKey = "",
eventType = EventType.STATE_ROOM_ENCRYPTION,
body = mapOf(
"algorithm" to algorithm

View file

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

View file

@ -68,7 +68,7 @@ internal class DefaultStateService @AssistedInject constructor(@Assisted private
override suspend fun sendStateEvent(
eventType: String,
stateKey: String?,
stateKey: String,
body: JsonDict
) {
val params = SendStateTask.Params(
@ -92,7 +92,7 @@ internal class DefaultStateService @AssistedInject constructor(@Assisted private
sendStateEvent(
eventType = EventType.STATE_ROOM_TOPIC,
body = mapOf("topic" to topic),
stateKey = null
stateKey = ""
)
}
@ -100,7 +100,7 @@ internal class DefaultStateService @AssistedInject constructor(@Assisted private
sendStateEvent(
eventType = EventType.STATE_ROOM_NAME,
body = mapOf("name" to name),
stateKey = null
stateKey = ""
)
}
@ -117,7 +117,7 @@ internal class DefaultStateService @AssistedInject constructor(@Assisted private
// Sort for the cleanup
.sorted()
).toContent(),
stateKey = null
stateKey = ""
)
}
@ -125,7 +125,7 @@ internal class DefaultStateService @AssistedInject constructor(@Assisted private
sendStateEvent(
eventType = EventType.STATE_ROOM_HISTORY_VISIBILITY,
body = mapOf("history_visibility" to readability),
stateKey = null
stateKey = ""
)
}
@ -142,14 +142,14 @@ internal class DefaultStateService @AssistedInject constructor(@Assisted private
sendStateEvent(
eventType = EventType.STATE_ROOM_JOIN_RULES,
body = body,
stateKey = null
stateKey = ""
)
}
if (guestAccess != null) {
sendStateEvent(
eventType = EventType.STATE_ROOM_GUEST_ACCESS,
body = mapOf("guest_access" to guestAccess),
stateKey = null
stateKey = ""
)
}
}
@ -159,7 +159,7 @@ internal class DefaultStateService @AssistedInject constructor(@Assisted private
sendStateEvent(
eventType = EventType.STATE_ROOM_AVATAR,
body = mapOf("url" to response.contentUri),
stateKey = null
stateKey = ""
)
}
@ -167,7 +167,7 @@ internal class DefaultStateService @AssistedInject constructor(@Assisted private
sendStateEvent(
eventType = EventType.STATE_ROOM_AVATAR,
body = emptyMap(),
stateKey = null
stateKey = ""
)
}

View file

@ -26,7 +26,7 @@ import javax.inject.Inject
internal interface SendStateTask : Task<SendStateTask.Params, Unit> {
data class Params(
val roomId: String,
val stateKey: String?,
val stateKey: String,
val eventType: String,
val body: JsonDict
)
@ -39,7 +39,7 @@ internal class DefaultSendStateTask @Inject constructor(
override suspend fun execute(params: SendStateTask.Params) {
return executeRequest(globalErrorReceiver) {
if (params.stateKey == null) {
if (params.stateKey.isEmpty()) {
roomAPI.sendStateEvent(
roomId = params.roomId,
stateEventType = params.eventType,

View file

@ -440,7 +440,8 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle
}
}
}
// Handle deletion of [stuck] local echos if needed
deleteLocalEchosIfNeeded(insertType, roomEntity, eventList)
optimizedThreadSummaryMap.updateThreadSummaryIfNeeded(
roomId = roomId,
realm = realm,
@ -448,7 +449,6 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle
// posting new events to timeline if any is registered
timelineInput.onNewTimelineEvents(roomId = roomId, eventIds = eventIds)
return chunkEntity
}
@ -499,4 +499,49 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle
return result
}
/**
* There are multiple issues like #516 that report stuck local echo events
* at the bottom of each room timeline.
*
* That can happen when a message is SENT but not received back from the /sync.
* Until now we use unsignedData.transactionId to determine whether or not the local
* event should be deleted on every /sync. However, this is partially correct, lets have a look
* at the following scenario:
*
* [There is no Internet connection] --> [10 Messages are sent] --> [The 10 messages are in the queue] -->
* [Internet comes back for 1 second] --> [3 messages are sent] --> [Internet drops again] -->
* [No /sync response is triggered | home server can even replied with /sync but never arrived while we are offline]
*
* So the state until now is that we have 7 pending events to send and 3 sent but not received them back from /sync
* Subsequently, those 3 local messages will not be deleted while there is no transactionId from the /sync
*
* lets continue:
* [Now lets assume that in the same room another user sent 15 events] -->
* [We are finally back online!] -->
* [We will receive the 10 latest events for the room and of course sent the pending 7 messages] -->
* Now /sync response will NOT contain the 3 local messages so our events will stuck in the device.
*
* Someone can say, yes but it will come with the rooms/{roomId}/messages while paginating,
* so the problem will be solved. No that is not the case for two reasons:
* 1. rooms/{roomId}/messages response do not contain the unsignedData.transactionId so we cannot know which event
* to delete
* 2. even if transactionId was there, currently we are not deleting it from the pagination
*
* ---------------------------------------------------------------------------------------------
* 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 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)
}
}
}

View file

@ -64,7 +64,7 @@ internal class DefaultTermsService @Inject constructor(
*/
override suspend fun getHomeserverTerms(baseUrl: String): TermsResponse {
return try {
val request = baseUrl + NetworkConstants.URI_API_PREFIX_PATH_R0 + "register"
val request = baseUrl.ensureTrailingSlash() + NetworkConstants.URI_API_PREFIX_PATH_R0 + "register"
executeRequest(null) {
termsAPI.register(request)
}

View file

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

View file

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

View file

@ -336,6 +336,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

View file

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

View file

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

View file

@ -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"
@ -402,7 +403,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>

View file

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

View file

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

View file

@ -105,6 +105,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
@ -450,6 +451,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)

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -67,7 +67,7 @@ class AutocompleteCommandPresenter @AssistedInject constructor(
if (query.isNullOrEmpty()) {
true
} else {
it.command.startsWith(query, 1, true)
it.startsWith(query)
}
}
controller.setData(data)

View file

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

View file

@ -24,42 +24,51 @@ 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, val isThreadCommand: Boolean) {
EMOTE("/me", "<message>", R.string.command_description_emote, false, true),
BAN_USER("/ban", "<user-id> [reason]", R.string.command_description_ban_user, false, false),
UNBAN_USER("/unban", "<user-id> [reason]", R.string.command_description_unban_user, false, false),
IGNORE_USER("/ignore", "<user-id> [reason]", R.string.command_description_ignore_user, false, true),
UNIGNORE_USER("/unignore", "<user-id>", R.string.command_description_unignore_user, false, true),
SET_USER_POWER_LEVEL("/op", "<user-id> [<power-level>]", R.string.command_description_op_user, false, false),
RESET_USER_POWER_LEVEL("/deop", "<user-id>", R.string.command_description_deop_user, false, false),
ROOM_NAME("/roomname", "<name>", R.string.command_description_room_name, false, false),
INVITE("/invite", "<user-id> [reason]", R.string.command_description_invite_user, false, false),
JOIN_ROOM("/join", "<room-address> [reason]", R.string.command_description_join_room, false, false),
PART("/part", "[<room-address>]", R.string.command_description_part_room, false, false),
TOPIC("/topic", "<topic>", R.string.command_description_topic, false, false),
KICK_USER("/kick", "<user-id> [reason]", R.string.command_description_kick_user, false, false),
CHANGE_DISPLAY_NAME("/nick", "<display-name>", R.string.command_description_nick, false, false),
CHANGE_DISPLAY_NAME_FOR_ROOM("/myroomnick", "<display-name>", R.string.command_description_nick_for_room, false, false),
ROOM_AVATAR("/roomavatar", "<mxc_url>", R.string.command_description_room_avatar, true /* Since user has to know the mxc url */, false),
CHANGE_AVATAR_FOR_ROOM("/myroomavatar", "<mxc_url>", R.string.command_description_avatar_for_room, true /* Since user has to know the mxc url */, false),
MARKDOWN("/markdown", "<on|off>", R.string.command_description_markdown, false, false),
RAINBOW("/rainbow", "<message>", R.string.command_description_rainbow, false, true),
RAINBOW_EMOTE("/rainbowme", "<message>", R.string.command_description_rainbow_emote, false, true),
CLEAR_SCALAR_TOKEN("/clear_scalar_token", "", R.string.command_description_clear_scalar_token, false, false),
SPOILER("/spoiler", "<message>", R.string.command_description_spoiler, false, true),
SHRUG("/shrug", "<message>", R.string.command_description_shrug, false, true),
LENNY("/lenny", "<message>", R.string.command_description_lenny, false, true),
PLAIN("/plain", "<message>", R.string.command_description_plain, false, true),
WHOIS("/whois", "<user-id>", R.string.command_description_whois, false, true),
DISCARD_SESSION("/discardsession", "", R.string.command_description_discard_session, false, false),
CONFETTI("/confetti", "<message>", R.string.command_confetti, false, false),
SNOWFALL("/snowfall", "<message>", R.string.command_snow, false, false),
CREATE_SPACE("/createspace", "<name> <invitee>*", R.string.command_description_create_space, true, false),
ADD_TO_SPACE("/addToSpace", "spaceId", R.string.command_description_add_to_space, true, false),
JOIN_SPACE("/joinSpace", "spaceId", R.string.command_description_join_space, true, false),
LEAVE_ROOM("/leave", "<roomId?>", R.string.command_description_leave_room, true, false),
UPGRADE_ROOM("/upgraderoom", "newVersion", R.string.command_description_upgrade_room, true, false);
enum class Command(val command: String,
val aliases: Array<CharSequence>?,
val parameters: String,
@StringRes val description: Int,
val isDevCommand: Boolean,
val isThreadCommand: Boolean) {
EMOTE("/me", null, "<message>", R.string.command_description_emote, false, true),
BAN_USER("/ban", null, "<user-id> [reason]", R.string.command_description_ban_user, false, false),
UNBAN_USER("/unban", null, "<user-id> [reason]", R.string.command_description_unban_user, false, false),
IGNORE_USER("/ignore", null, "<user-id> [reason]", R.string.command_description_ignore_user, false, true),
UNIGNORE_USER("/unignore", null, "<user-id>", R.string.command_description_unignore_user, false, true),
SET_USER_POWER_LEVEL("/op", null, "<user-id> [<power-level>]", R.string.command_description_op_user, false, false),
RESET_USER_POWER_LEVEL("/deop", null, "<user-id>", R.string.command_description_deop_user, false, false),
ROOM_NAME("/roomname", null, "<name>", R.string.command_description_room_name, false, false),
INVITE("/invite", null, "<user-id> [reason]", R.string.command_description_invite_user, false, false),
JOIN_ROOM("/join", arrayOf("/j", "/goto"), "<room-address> [reason]", R.string.command_description_join_room, false, false),
PART("/part", null, "[<room-address>]", R.string.command_description_part_room, false, false),
TOPIC("/topic", null, "<topic>", R.string.command_description_topic, false, false),
REMOVE_USER("/remove", arrayOf("/kick"), "<user-id> [reason]", R.string.command_description_kick_user, false, false),
CHANGE_DISPLAY_NAME("/nick", null, "<display-name>", R.string.command_description_nick, false, false),
CHANGE_DISPLAY_NAME_FOR_ROOM("/myroomnick", arrayOf("/roomnick"), "<display-name>", R.string.command_description_nick_for_room, false, false),
ROOM_AVATAR("/roomavatar", null, "<mxc_url>", R.string.command_description_room_avatar, true /* Since user has to know the mxc url */, false),
CHANGE_AVATAR_FOR_ROOM("/myroomavatar", null, "<mxc_url>", R.string.command_description_avatar_for_room, true /* Since user has to know the mxc url */, false),
MARKDOWN("/markdown", null, "<on|off>", R.string.command_description_markdown, false, false),
RAINBOW("/rainbow", null, "<message>", R.string.command_description_rainbow, false, true),
RAINBOW_EMOTE("/rainbowme", null, "<message>", R.string.command_description_rainbow_emote, false, true),
CLEAR_SCALAR_TOKEN("/clear_scalar_token", null, "", R.string.command_description_clear_scalar_token, false, false),
SPOILER("/spoiler", null, "<message>", R.string.command_description_spoiler, false, true),
SHRUG("/shrug", null, "<message>", R.string.command_description_shrug, false, true),
LENNY("/lenny", null, "<message>", R.string.command_description_lenny, false, true),
PLAIN("/plain", null, "<message>", R.string.command_description_plain, false, true),
WHOIS("/whois", null, "<user-id>", R.string.command_description_whois, false, true),
DISCARD_SESSION("/discardsession", null, "", R.string.command_description_discard_session, false, false),
CONFETTI("/confetti", null, "<message>", R.string.command_confetti, false, false),
SNOWFALL("/snowfall", null, "<message>", R.string.command_snow, false, false),
CREATE_SPACE("/createspace", null, "<name> <invitee>*", R.string.command_description_create_space, true, false),
ADD_TO_SPACE("/addToSpace", null, "spaceId", R.string.command_description_add_to_space, true, false),
JOIN_SPACE("/joinSpace", null, "spaceId", R.string.command_description_join_space, true, false),
LEAVE_ROOM("/leave", null, "<roomId?>", R.string.command_description_leave_room, true, false),
UPGRADE_ROOM("/upgraderoom", null, "newVersion", R.string.command_description_upgrade_room, true, false);
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) }
}

View file

@ -33,12 +33,12 @@ object CommandParser {
* @param textMessage the text message
* @return a parsed slash command (ok or error)
*/
fun parseSplashCommand(textMessage: CharSequence, isInThreadTimeline: Boolean): ParsedCommand {
fun parseSlashCommand(textMessage: CharSequence, isInThreadTimeline: Boolean): 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) {
@ -53,7 +53,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
}
@ -62,10 +62,10 @@ object CommandParser {
return ParsedCommand.ErrorEmptySlashCommand
}
// If the command is not supported by threads return error
val slashCommand = messageParts.first()
val message = textMessage.substring(slashCommand.length).trim()
if (BuildConfig.THREADING_ENABLED && isInThreadTimeline) {
val slashCommand = messageParts.first()
val notSupportedCommandsInThreads = Command.values().filter {
!it.isThreadCommand
}.map {
@ -76,35 +76,29 @@ object CommandParser {
}
}
return when (val slashCommand = messageParts.first()) {
Command.PLAIN.command -> {
val text = textMessage.substring(Command.PLAIN.command.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]
@ -117,7 +111,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]
@ -130,40 +124,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)
@ -172,23 +168,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]
@ -196,9 +190,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() -> {
@ -215,34 +207,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)
@ -251,16 +239,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)
@ -269,7 +255,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]
@ -282,7 +268,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]
@ -295,7 +281,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)) {
@ -315,7 +301,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]
@ -328,7 +314,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)
@ -339,31 +325,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]
@ -376,57 +365,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() }
}
}

View file

@ -53,7 +53,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()

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -174,8 +174,8 @@ class RoomDevToolViewModel @AssistedInject constructor(
?: throw IllegalArgumentException(stringProvider.getString(R.string.dev_tools_error_no_content))
room.sendStateEvent(
state.selectedEvent?.type ?: "",
state.selectedEvent?.stateKey,
state.selectedEvent?.type.orEmpty(),
state.selectedEvent?.stateKey.orEmpty(),
json
)
@ -213,7 +213,7 @@ class RoomDevToolViewModel @AssistedInject constructor(
if (isState) {
room.sendStateEvent(
eventType,
state.sendEventDraft.stateKey,
state.sendEventDraft.stateKey.orEmpty(),
json
)
} else {

View file

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

View file

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

View file

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

View file

@ -2256,7 +2256,7 @@ class TimelineFragment @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 = timelineViewModel.getMember(userId)
// TODO move logic outside of fragment

View file

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

View file

@ -183,7 +183,9 @@ class MessageComposerViewModel @AssistedInject constructor(
withState { state ->
when (state.sendMode) {
is SendMode.Regular -> {
when (val slashCommandResult = CommandParser.parseSplashCommand(action.text, state.isInThreadTimeline())) {
when (val slashCommandResult = CommandParser.parseSplashCommand(
textMessage = action.text,
isInThreadTimeline = state.isInThreadTimeline())) {
is ParsedCommand.ErrorNotACommand -> {
// Send the text message to the room
if (state.rootThreadEventId != null) {
@ -257,8 +259,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)
@ -625,7 +627,7 @@ class MessageComposerViewModel @AssistedInject constructor(
?: return
launchSlashCommandFlowSuspendable {
room.sendStateEvent(EventType.STATE_ROOM_POWER_LEVELS, null, newPowerLevelsContent)
room.sendStateEvent(EventType.STATE_ROOM_POWER_LEVELS, stateKey = "", newPowerLevelsContent)
}
}
@ -649,9 +651,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)
}
}
@ -692,7 +694,7 @@ class MessageComposerViewModel @AssistedInject constructor(
private fun handleChangeRoomAvatarSlashCommand(changeAvatar: ParsedCommand.ChangeRoomAvatar) {
launchSlashCommandFlowSuspendable {
room.sendStateEvent(EventType.STATE_ROOM_AVATAR, null, RoomAvatarContent(changeAvatar.url).toContent())
room.sendStateEvent(EventType.STATE_ROOM_AVATAR, stateKey = "", RoomAvatarContent(changeAvatar.url).toContent())
}
}

View file

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

View file

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

View file

@ -104,7 +104,7 @@ class RoomListSectionBuilderGroup(
}
}
appStateHandler.selectedRoomGroupingObservable
appStateHandler.selectedRoomGroupingFlow
.distinctUntilChanged()
.onEach { groupingMethod ->
val selectedGroupId = (groupingMethod.orNull() as? RoomGroupingMethod.ByLegacyGroup)?.groupSummary?.groupId

View file

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

View file

@ -92,7 +92,7 @@ class RoomListViewModel @AssistedInject constructor(
init {
observeMembershipChanges()
appStateHandler.selectedRoomGroupingObservable
appStateHandler.selectedRoomGroupingFlow
.distinctUntilChanged()
.execute {
copy(

View file

@ -317,8 +317,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)
}

View file

@ -77,7 +77,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)

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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() }
}
@ -80,17 +81,26 @@ class FtueAuthSplashCarouselFragment @Inject constructor(
"Branch: ${BuildConfig.GIT_BRANCH_NAME}"
views.loginSplashVersion.debouncedClicks { navigator.openDebug(requireContext()) }
}
views.splashCarousel.registerAutomaticUntilInteractionTransitions()
}
views.splashCarousel.apply {
var scheduledTransition: Job? = null
registerOnPageChangeCallback(object : ViewPager2.OnPageChangeCallback() {
override fun onPageSelected(position: Int) {
scheduledTransition?.cancel()
private fun ViewPager2.registerAutomaticUntilInteractionTransitions() {
var scheduledTransition: Job? = null
registerOnPageChangeCallback(object : ViewPager2.OnPageChangeCallback() {
private var hasUserManuallyInteractedWithCarousel: Boolean = false
override fun onPageScrolled(position: Int, positionOffset: Float, positionOffsetPixels: Int) {
hasUserManuallyInteractedWithCarousel = !isFakeDragging
}
override fun onPageSelected(position: Int) {
scheduledTransition?.cancel()
// only schedule automatic transitions whilst the user has not interacted with the carousel
if (!hasUserManuallyInteractedWithCarousel) {
scheduledTransition = scheduleCarouselTransition()
}
})
scheduledTransition = scheduleCarouselTransition()
}
}
})
}
private fun ViewPager2.scheduleCarouselTransition(): Job {
@ -102,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))
}

View file

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

View file

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

View file

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

View file

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

View file

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

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