Merge branch 'develop' into feature/ons/edit_polls

* develop: (66 commits)
  toolbar management  (#4887)
  adding changelog entry
  adding back periodic flag when scheduling automatic background workers
  Fix enum class warning
  Split long lines Done by https://github.com/matrix-org/matrix-analytics-events/pull/16
  Add new class in analytics plan
  Fix conditional for Delight issue automation
  Add missing import in kdoc
  Update kdoc
  Enable Delight issue automation
  Fix an error in string resource (#4997)
  Changelog
  Add some unit test for the command parser. Not all commands are covered, could add more tests later.
  data class.
  use sealed interface
  Small cleanup
  Command parser is not a static object anymore
  Add changelog
  Use Throwable.isLimitExceededError extension
  Do not automatically retry 429 with a too long delay
  ...
This commit is contained in:
Onuray Sahin 2022-01-23 22:15:36 +03:00
commit 7f97e78ba3
237 changed files with 1634 additions and 1138 deletions

View file

@ -10,6 +10,8 @@ updates:
directory: "/" directory: "/"
schedule: schedule:
interval: "weekly" interval: "weekly"
ignore:
- dependency-name: "*github-script*"
# Updates for Gradle dependencies used in the app # Updates for Gradle dependencies used in the app
- package-ecosystem: gradle - package-ecosystem: gradle
directory: "/" directory: "/"

View file

@ -100,32 +100,33 @@ jobs:
PROJECT_ID: "PN_kwDOAM0swc4AAg6N" PROJECT_ID: "PN_kwDOAM0swc4AAg6N"
GITHUB_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN }} GITHUB_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN }}
# delight_issues_to_board: delight_issues_to_board:
# name: Spaces issues to new Delight project board name: Spaces issues to Delight project board
# runs-on: ubuntu-latest runs-on: ubuntu-latest
# # Skip in forks # Skip in forks
# if: > if: >
# github.repository == 'vector-im/element-android' && github.repository == 'vector-im/element-android' &&
# contains(github.event.issue.labels.*.name, 'A-Spaces') || (contains(github.event.issue.labels.*.name, 'A-Spaces') ||
# contains(github.event.issue.labels.*.name, 'A-Space-Settings') || contains(github.event.issue.labels.*.name, 'A-Space-Settings') ||
# contains(github.event.issue.labels.*.name, 'A-Subspaces') contains(github.event.issue.labels.*.name, 'A-Subspaces') ||
# steps: contains(github.event.issue.labels.*.name, 'Z-IA'))
# - uses: octokit/graphql-action@v2.x steps:
# with: - uses: octokit/graphql-action@v2.x
# headers: '{"GraphQL-Features": "projects_next_graphql"}' with:
# query: | headers: '{"GraphQL-Features": "projects_next_graphql"}'
# mutation add_to_project($projectid:ID!,$contentid:ID!) { query: |
# addProjectNextItem(input:{projectId:$projectid contentId:$contentid}) { mutation add_to_project($projectid:ID!,$contentid:ID!) {
# projectNextItem { addProjectNextItem(input:{projectId:$projectid contentId:$contentid}) {
# id projectNextItem {
# } id
# } }
# } }
# projectid: ${{ env.PROJECT_ID }} }
# contentid: ${{ github.event.issue.node_id }} projectid: ${{ env.PROJECT_ID }}
# env: contentid: ${{ github.event.issue.node_id }}
# PROJECT_ID: "PN_kwDOAM0swc1HvQ" env:
# GITHUB_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN }} PROJECT_ID: "PN_kwDOAM0swc1HvQ"
GITHUB_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN }}
move_voice-message_issues: move_voice-message_issues:
name: A-Voice Messages to voice message board name: A-Voice Messages to voice message board

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

@ -0,0 +1 @@
Notification does not take me to the room when another space was last viewed

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

@ -0,0 +1 @@
Enables the FTUE splash carousel

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

@ -0,0 +1 @@
Analytics: send more Events

3
changelog.d/4884.misc Normal file
View file

@ -0,0 +1,3 @@
Toolbar management rework. Toolbar title's and subtitle's text appearance now controlled by theme without local overrides. Helper class introduced to
help with toolbar configuration. Toolbar title, subtitle and navigation button widgets are removed where it is possible and replaced with built-in
toolbar widgets.

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

@ -0,0 +1 @@
Including onboarding server options in the all screen sanity test suite

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

@ -0,0 +1 @@
Exclude dependabot upgrade for @github-script@v3

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

@ -0,0 +1 @@
EmojiPopupDismissListener not being triggered after dismissing the EmojiPopup

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

@ -0,0 +1 @@
429 are not automatically retried anymore in case of too long retry delay

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

@ -0,0 +1 @@
Fix an error in string resource

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

@ -0,0 +1 @@
Small iteration on command parser and unit test it.

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

@ -0,0 +1 @@
Fixing missing notifications in FDroid variants using `optimised for battery` background sync mode

View file

@ -14,7 +14,7 @@
* limitations under the License. * limitations under the License.
*/ */
package im.vector.app.core.utils.compat package im.vector.lib.core.utils.compat
import android.os.Build import android.os.Build

View file

@ -14,7 +14,7 @@
* limitations under the License. * limitations under the License.
*/ */
package im.vector.app.core.epoxy.charsequence package im.vector.lib.core.utils.epoxy.charsequence
/** /**
* Wrapper for a CharSequence, which support mutation of the CharSequence, which can happen during rendering * Wrapper for a CharSequence, which support mutation of the CharSequence, which can happen during rendering

View file

@ -14,7 +14,7 @@
* limitations under the License. * limitations under the License.
*/ */
package im.vector.app.core.epoxy.charsequence package im.vector.lib.core.utils.epoxy.charsequence
/** /**
* Extensions to wrap CharSequence to EpoxyCharSequence * Extensions to wrap CharSequence to EpoxyCharSequence

View file

@ -42,6 +42,8 @@ android {
} }
dependencies { dependencies {
implementation project(":library:core-utils")
implementation libs.androidx.appCompat implementation libs.androidx.appCompat
implementation libs.androidx.core implementation libs.androidx.core
@ -57,7 +59,7 @@ dependencies {
implementation libs.jetbrains.coroutinesCore implementation libs.jetbrains.coroutinesCore
implementation libs.jetbrains.coroutinesAndroid implementation libs.jetbrains.coroutinesAndroid
testImplementation 'org.json:json:20190722' testImplementation 'org.json:json:20211205'
testImplementation libs.tests.junit testImplementation libs.tests.junit
androidTestImplementation libs.androidx.junit androidTestImplementation libs.androidx.junit
androidTestImplementation libs.androidx.espressoCore androidTestImplementation libs.androidx.espressoCore

View file

@ -21,6 +21,7 @@ import android.view.View
import com.airbnb.epoxy.TypedEpoxyController import com.airbnb.epoxy.TypedEpoxyController
import com.airbnb.mvrx.Fail import com.airbnb.mvrx.Fail
import com.airbnb.mvrx.Success import com.airbnb.mvrx.Success
import im.vector.lib.core.utils.epoxy.charsequence.toEpoxyCharSequence
import me.gujun.android.span.Span import me.gujun.android.span.Span
import me.gujun.android.span.span import me.gujun.android.span.span
@ -40,7 +41,7 @@ internal class JSonViewerEpoxyController(private val context: Context) :
is Fail -> { is Fail -> {
valueItem { valueItem {
id("fail") id("fail")
text(async.error.localizedMessage?.toSafeCharSequence()) text(async.error.localizedMessage?.toEpoxyCharSequence())
} }
} }
is Success -> { is Success -> {
@ -94,7 +95,7 @@ internal class JSonViewerEpoxyController(private val context: Context) :
+"{+${model.keys.size}}" +"{+${model.keys.size}}"
textColor = host.styleProvider.baseColor textColor = host.styleProvider.baseColor
} }
}.toSafeCharSequence() }.toEpoxyCharSequence()
) )
itemClickListener(View.OnClickListener { host.itemClicked(model) }) itemClickListener(View.OnClickListener { host.itemClicked(model) })
} }
@ -133,7 +134,7 @@ internal class JSonViewerEpoxyController(private val context: Context) :
+"[+${model.items.size}]" +"[+${model.items.size}]"
textColor = host.styleProvider.baseColor textColor = host.styleProvider.baseColor
} }
}.toSafeCharSequence() }.toEpoxyCharSequence()
) )
itemClickListener(View.OnClickListener { host.itemClicked(model) }) itemClickListener(View.OnClickListener { host.itemClicked(model) })
} }
@ -163,7 +164,7 @@ internal class JSonViewerEpoxyController(private val context: Context) :
} }
} }
append(host.valueToSpan(model)) append(host.valueToSpan(model))
}.toSafeCharSequence() }.toEpoxyCharSequence()
) )
copyValue(model.stringRes) copyValue(model.stringRes)
} }
@ -233,7 +234,7 @@ internal class JSonViewerEpoxyController(private val context: Context) :
span("{".takeIf { isObject } ?: "[") { span("{".takeIf { isObject } ?: "[") {
textColor = host.styleProvider.baseColor textColor = host.styleProvider.baseColor
} }
}.toSafeCharSequence() }.toEpoxyCharSequence()
) )
itemClickListener(View.OnClickListener { host.itemClicked(composed) }) itemClickListener(View.OnClickListener { host.itemClicked(composed) })
} }
@ -253,7 +254,7 @@ internal class JSonViewerEpoxyController(private val context: Context) :
span { span {
text = "}".takeIf { isObject } ?: "]" text = "}".takeIf { isObject } ?: "]"
textColor = host.styleProvider.baseColor textColor = host.styleProvider.baseColor
}.toSafeCharSequence() }.toEpoxyCharSequence()
) )
} }
} }

View file

@ -1,30 +0,0 @@
/*
* 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 org.billcarsonfr.jsonviewer
/**
* Wrapper for a CharSequence, which support mutation of the CharSequence, which can happen during rendering
* TODO Mutualize
*/
internal class SafeCharSequence(val charSequence: CharSequence) {
private val hash = charSequence.toString().hashCode()
override fun hashCode() = hash
override fun equals(other: Any?) = other is SafeCharSequence && other.hash == hash
}
internal fun CharSequence.toSafeCharSequence() = SafeCharSequence(this)

View file

@ -19,9 +19,6 @@ package org.billcarsonfr.jsonviewer
import android.content.Context import android.content.Context
import android.util.TypedValue import android.util.TypedValue
/**
* TODO Mutualize
*/
internal object Utils { internal object Utils {
fun dpToPx(dp: Int, context: Context): Int { fun dpToPx(dp: Int, context: Context): Int {
return TypedValue.applyDimension( return TypedValue.applyDimension(

View file

@ -28,12 +28,13 @@ import com.airbnb.epoxy.EpoxyAttribute
import com.airbnb.epoxy.EpoxyHolder import com.airbnb.epoxy.EpoxyHolder
import com.airbnb.epoxy.EpoxyModelClass import com.airbnb.epoxy.EpoxyModelClass
import com.airbnb.epoxy.EpoxyModelWithHolder import com.airbnb.epoxy.EpoxyModelWithHolder
import im.vector.lib.core.utils.epoxy.charsequence.EpoxyCharSequence
@EpoxyModelClass(layout = R2.layout.item_jv_base_value) @EpoxyModelClass(layout = R2.layout.item_jv_base_value)
internal abstract class ValueItem : EpoxyModelWithHolder<ValueItem.Holder>() { internal abstract class ValueItem : EpoxyModelWithHolder<ValueItem.Holder>() {
@EpoxyAttribute @EpoxyAttribute
var text: SafeCharSequence? = null var text: EpoxyCharSequence? = null
@EpoxyAttribute @EpoxyAttribute
var depth: Int = 0 var depth: Int = 0

View file

@ -1,7 +1,4 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"> <shape xmlns:android="http://schemas.android.com/apk/res/android">
<gradient <solid android:color="?android:colorBackground" />
android:angle="270"
android:endColor="?vctr_system"
android:startColor="#000000" />
</shape> </shape>

View file

@ -6,10 +6,12 @@
<item name="elevation">0dp</item> <item name="elevation">0dp</item>
<!-- main text --> <!-- main text -->
<item name="titleTextStyle">@style/Widget.Vector.TextView.ActionBarTitle</item> <item name="titleTextAppearance">@style/TextAppearance.Vector.Widget.ActionBarTitle</item>
<!-- sub text --> <!-- sub text -->
<item name="subtitleTextStyle">@style/Widget.Vector.TextView.ActionBarSubTitle</item> <item name="subtitleTextAppearance">@style/TextAppearance.Vector.Widget.ActionBarSubTitle</item>
<item name="navigationIconTint">?vctr_content_secondary</item>
</style> </style>
<!-- Default toolbar style --> <!-- Default toolbar style -->
@ -22,16 +24,18 @@
<!-- Toolbar text style --> <!-- Toolbar text style -->
<!-- main text --> <!-- main text -->
<style name="Widget.Vector.TextView.ActionBarTitle" parent="TextAppearance.AppCompat.Widget.ActionBar.Title"> <style name="TextAppearance.Vector.Widget.ActionBarTitle" parent="TextAppearance.AppCompat.Widget.ActionBar.Title">
<item name="android:textColor">?vctr_content_primary</item> <item name="android:textColor">?vctr_content_primary</item>
<item name="android:fontFamily">"sans-serif-medium"</item> <item name="android:fontFamily">sans-serif-medium</item>
<item name="android:textSize">20sp</item> <item name="fontFamily">sans-serif-medium</item>
<item name="android:textSize">18sp</item>
</style> </style>
<!-- sub text --> <!-- sub text -->
<style name="Widget.Vector.TextView.ActionBarSubTitle" parent="TextAppearance.AppCompat.Widget.ActionBar.Subtitle"> <style name="TextAppearance.Vector.Widget.ActionBarSubTitle" parent="TextAppearance.AppCompat.Widget.ActionBar.Subtitle">
<item name="android:textColor">?vctr_content_primary</item> <item name="android:textColor">?vctr_content_secondary</item>
<item name="android:fontFamily">"sans-serif-medium"</item> <item name="android:fontFamily">sans-serif</item>
<item name="fontFamily">sans-serif</item>
<item name="android:textSize">12sp</item> <item name="android:textSize">12sp</item>
</style> </style>

View file

@ -32,13 +32,18 @@ fun Throwable.is401() =
fun Throwable.isTokenError() = fun Throwable.isTokenError() =
this is Failure.ServerError && this is Failure.ServerError &&
(error.code == MatrixError.M_UNKNOWN_TOKEN || (error.code == MatrixError.M_UNKNOWN_TOKEN ||
error.code == MatrixError.M_MISSING_TOKEN || error.code == MatrixError.M_MISSING_TOKEN ||
error.code == MatrixError.ORG_MATRIX_EXPIRED_ACCOUNT) error.code == MatrixError.ORG_MATRIX_EXPIRED_ACCOUNT)
fun Throwable.isLimitExceededError() =
this is Failure.ServerError &&
httpCode == 429 &&
error.code == MatrixError.M_LIMIT_EXCEEDED
fun Throwable.shouldBeRetried(): Boolean { fun Throwable.shouldBeRetried(): Boolean {
return this is Failure.NetworkConnection || return this is Failure.NetworkConnection ||
this is IOException || this is IOException ||
(this is Failure.ServerError && error.code == MatrixError.M_LIMIT_EXCEEDED) this.isLimitExceededError()
} }
/** /**

View file

@ -19,8 +19,9 @@ package org.matrix.android.sdk.internal.network
import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import org.matrix.android.sdk.api.failure.Failure import org.matrix.android.sdk.api.failure.Failure
import org.matrix.android.sdk.api.failure.MatrixError import org.matrix.android.sdk.api.failure.GlobalError
import org.matrix.android.sdk.api.failure.getRetryDelay import org.matrix.android.sdk.api.failure.getRetryDelay
import org.matrix.android.sdk.api.failure.isLimitExceededError
import org.matrix.android.sdk.api.failure.shouldBeRetried import org.matrix.android.sdk.api.failure.shouldBeRetried
import org.matrix.android.sdk.internal.network.ssl.CertUtil import org.matrix.android.sdk.internal.network.ssl.CertUtil
import retrofit2.HttpException import retrofit2.HttpException
@ -33,7 +34,8 @@ import java.io.IOException
* *
* @param globalErrorReceiver will be use to notify error such as invalid token error. See [GlobalError] * @param globalErrorReceiver will be use to notify error such as invalid token error. See [GlobalError]
* @param canRetry if set to true, the request will be executed again in case of error, after a delay * @param canRetry if set to true, the request will be executed again in case of error, after a delay
* @param maxDelayBeforeRetry the max delay to wait before a retry * @param maxDelayBeforeRetry the max delay to wait before a retry. Note that in the case of a 429, if the provided delay exceeds this value, the error will
* be propagated as it does not make sense to retry it with a shorter delay.
* @param maxRetriesCount the max number of retries * @param maxRetriesCount the max number of retries
* @param requestBlock a suspend lambda to perform the network request * @param requestBlock a suspend lambda to perform the network request
*/ */
@ -74,23 +76,26 @@ internal suspend inline fun <DATA> executeRequest(globalErrorReceiver: GlobalErr
currentRetryCount++ currentRetryCount++
if (exception is Failure.ServerError && if (exception.isLimitExceededError() && currentRetryCount < maxRetriesCount) {
exception.httpCode == 429 &&
exception.error.code == MatrixError.M_LIMIT_EXCEEDED &&
currentRetryCount < maxRetriesCount) {
// 429, we can retry // 429, we can retry
delay(exception.getRetryDelay(1_000)) val retryDelay = exception.getRetryDelay(1_000)
if (retryDelay <= maxDelayBeforeRetry) {
delay(retryDelay)
} else {
// delay is too high to be retried, propagate the exception
throw exception
}
} else if (canRetry && currentRetryCount < maxRetriesCount && exception.shouldBeRetried()) { } else if (canRetry && currentRetryCount < maxRetriesCount && exception.shouldBeRetried()) {
delay(currentDelay) delay(currentDelay)
currentDelay = currentDelay.times(2L).coerceAtMost(maxDelayBeforeRetry) currentDelay = currentDelay.times(2L).coerceAtMost(maxDelayBeforeRetry)
// Try again (loop) // Try again (loop)
} else { } else {
throw when (exception) { throw when (exception) {
is IOException -> Failure.NetworkConnection(exception) is IOException -> Failure.NetworkConnection(exception)
is Failure.ServerError, is Failure.ServerError,
is Failure.OtherServerError, is Failure.OtherServerError,
is CancellationException -> exception is CancellationException -> exception
else -> Failure.Unknown(exception) else -> Failure.Unknown(exception)
} }
} }
} }

View file

@ -23,8 +23,8 @@ import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import org.matrix.android.sdk.api.auth.data.SessionParams import org.matrix.android.sdk.api.auth.data.SessionParams
import org.matrix.android.sdk.api.failure.Failure import org.matrix.android.sdk.api.failure.Failure
import org.matrix.android.sdk.api.failure.MatrixError
import org.matrix.android.sdk.api.failure.getRetryDelay import org.matrix.android.sdk.api.failure.getRetryDelay
import org.matrix.android.sdk.api.failure.isLimitExceededError
import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.Session
import org.matrix.android.sdk.api.session.crypto.CryptoService import org.matrix.android.sdk.api.session.crypto.CryptoService
import org.matrix.android.sdk.api.session.events.model.Event import org.matrix.android.sdk.api.session.events.model.Event
@ -145,17 +145,17 @@ internal class EventSenderProcessorCoroutine @Inject constructor(
task.execute() task.execute()
} catch (exception: Throwable) { } catch (exception: Throwable) {
when { when {
exception is IOException || exception is Failure.NetworkConnection -> { exception is IOException || exception is Failure.NetworkConnection -> {
canReachServer.set(false) canReachServer.set(false)
task.markAsFailedOrRetry(exception, 0) task.markAsFailedOrRetry(exception, 0)
} }
(exception is Failure.ServerError && exception.error.code == MatrixError.M_LIMIT_EXCEEDED) -> { (exception.isLimitExceededError()) -> {
task.markAsFailedOrRetry(exception, exception.getRetryDelay(3_000)) task.markAsFailedOrRetry(exception, exception.getRetryDelay(3_000))
} }
exception is CancellationException -> { exception is CancellationException -> {
Timber.v("## $task has been cancelled, try next task") Timber.v("## $task has been cancelled, try next task")
} }
else -> { else -> {
Timber.v("## un-retryable error for $task, try next task") Timber.v("## un-retryable error for $task, try next task")
// this task is in error, check next one? // this task is in error, check next one?
task.onTaskFailed() task.onTaskFailed()

View file

@ -23,7 +23,7 @@ import org.matrix.android.sdk.api.auth.data.SessionParams
import org.matrix.android.sdk.api.auth.data.sessionId import org.matrix.android.sdk.api.auth.data.sessionId
import org.matrix.android.sdk.api.extensions.tryOrNull import org.matrix.android.sdk.api.extensions.tryOrNull
import org.matrix.android.sdk.api.failure.Failure import org.matrix.android.sdk.api.failure.Failure
import org.matrix.android.sdk.api.failure.MatrixError import org.matrix.android.sdk.api.failure.isLimitExceededError
import org.matrix.android.sdk.api.failure.isTokenError import org.matrix.android.sdk.api.failure.isTokenError
import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.Session
import org.matrix.android.sdk.api.session.crypto.CryptoService import org.matrix.android.sdk.api.session.crypto.CryptoService
@ -171,7 +171,7 @@ internal class EventSenderProcessorThread @Inject constructor(
break@retryLoop break@retryLoop
} catch (exception: Throwable) { } catch (exception: Throwable) {
when { when {
exception is IOException || exception is Failure.NetworkConnection -> { exception is IOException || exception is Failure.NetworkConnection -> {
canReachServer = false canReachServer = false
if (task.retryCount.getAndIncrement() >= 3) task.onTaskFailed() if (task.retryCount.getAndIncrement() >= 3) task.onTaskFailed()
while (!canReachServer) { while (!canReachServer) {
@ -180,7 +180,7 @@ internal class EventSenderProcessorThread @Inject constructor(
waitForNetwork() waitForNetwork()
} }
} }
(exception is Failure.ServerError && exception.error.code == MatrixError.M_LIMIT_EXCEEDED) -> { (exception.isLimitExceededError()) -> {
if (task.retryCount.getAndIncrement() >= 3) task.onTaskFailed() if (task.retryCount.getAndIncrement() >= 3) task.onTaskFailed()
Timber.v("## SendThread retryLoop retryable error for $task reason: ${exception.localizedMessage}") Timber.v("## SendThread retryLoop retryable error for $task reason: ${exception.localizedMessage}")
// wait a bit // wait a bit
@ -188,17 +188,17 @@ internal class EventSenderProcessorThread @Inject constructor(
sleep(3_000) sleep(3_000)
continue@retryLoop continue@retryLoop
} }
exception.isTokenError() -> { exception.isTokenError() -> {
Timber.v("## SendThread retryLoop retryable TOKEN error, interrupt") Timber.v("## SendThread retryLoop retryable TOKEN error, interrupt")
// we can exit the loop // we can exit the loop
task.onTaskFailed() task.onTaskFailed()
throw InterruptedException() throw InterruptedException()
} }
exception is CancellationException -> { exception is CancellationException -> {
Timber.v("## SendThread task has been cancelled") Timber.v("## SendThread task has been cancelled")
break@retryLoop break@retryLoop
} }
else -> { else -> {
Timber.v("## SendThread retryLoop Un-Retryable error, try next task") Timber.v("## SendThread retryLoop Un-Retryable error, try next task")
// this task is in error, check next one? // this task is in error, check next one?
task.onTaskFailed() task.onTaskFailed()

View file

@ -151,6 +151,7 @@ internal class SyncWorker(context: Context, workerParameters: WorkerParameters,
sessionId = sessionId, sessionId = sessionId,
timeout = serverTimeoutInSeconds, timeout = serverTimeoutInSeconds,
delay = delayInSeconds, delay = delayInSeconds,
periodic = true,
forceImmediate = forceImmediate forceImmediate = forceImmediate
) )
) )

View file

@ -160,7 +160,7 @@ Formatter\.formatShortFileSize===1
# android\.text\.TextUtils # android\.text\.TextUtils
### This is not a rule, but a warning: the number of "enum class" has changed. For Json classes, it is mandatory that they have `@JsonClass(generateAdapter = false)`. If the enum is not used as a Json class, change the value in file forbidden_strings_in_code.txt ### This is not a rule, but a warning: the number of "enum class" has changed. For Json classes, it is mandatory that they have `@JsonClass(generateAdapter = false)`. If the enum is not used as a Json class, change the value in file forbidden_strings_in_code.txt
enum class===119 enum class===121
### Do not import temporary legacy classes ### Do not import temporary legacy classes
import org.matrix.android.sdk.internal.legacy.riot===3 import org.matrix.android.sdk.internal.legacy.riot===3

View file

@ -55,6 +55,10 @@ class UiAllScreensSanityTest {
fun allScreensTest() { fun allScreensTest() {
IdlingPolicies.setMasterPolicyTimeout(120, TimeUnit.SECONDS) IdlingPolicies.setMasterPolicyTimeout(120, TimeUnit.SECONDS)
elementRobot.onboarding {
crawl()
}
// Create an account // Create an account
val userId = "UiTest_" + UUID.randomUUID().toString() val userId = "UiTest_" + UUID.randomUUID().toString()
elementRobot.signUp(userId) elementRobot.signUp(userId)

View file

@ -40,6 +40,10 @@ import timber.log.Timber
class ElementRobot { class ElementRobot {
fun onboarding(block: OnboardingRobot.() -> Unit) {
block(OnboardingRobot())
}
fun signUp(userId: String) { fun signUp(userId: String) {
val onboardingRobot = OnboardingRobot() val onboardingRobot = OnboardingRobot()
onboardingRobot.createAccount(userId = userId) onboardingRobot.createAccount(userId = userId)

View file

@ -18,6 +18,7 @@ package im.vector.app.ui.robot
import androidx.test.espresso.Espresso.closeSoftKeyboard import androidx.test.espresso.Espresso.closeSoftKeyboard
import androidx.test.espresso.Espresso.onView import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.Espresso.pressBack
import androidx.test.espresso.matcher.ViewMatchers.isRoot import androidx.test.espresso.matcher.ViewMatchers.isRoot
import androidx.test.espresso.matcher.ViewMatchers.withId import androidx.test.espresso.matcher.ViewMatchers.withId
import com.adevinta.android.barista.assertion.BaristaEnabledAssertions.assertDisabled import com.adevinta.android.barista.assertion.BaristaEnabledAssertions.assertDisabled
@ -31,6 +32,24 @@ import im.vector.app.waitForView
class OnboardingRobot { class OnboardingRobot {
fun crawl() {
waitUntilViewVisible(withId(R.id.loginSplashSubmit))
crawlGetStarted()
crawlAlreadyHaveAccount()
}
private fun crawlGetStarted() {
clickOn(R.id.loginSplashSubmit)
OnboardingServersRobot().crawlSignUp()
pressBack()
}
private fun crawlAlreadyHaveAccount() {
clickOn(R.id.loginSplashAlreadyHaveAccount)
OnboardingServersRobot().crawlSignIn()
pressBack()
}
fun createAccount(userId: String, password: String = "password", homeServerUrl: String = "http://10.0.2.2:8080") { fun createAccount(userId: String, password: String = "password", homeServerUrl: String = "http://10.0.2.2:8080") {
initSession(true, userId, password, homeServerUrl) initSession(true, userId, password, homeServerUrl)
} }
@ -44,7 +63,7 @@ class OnboardingRobot {
password: String, password: String,
homeServerUrl: String) { homeServerUrl: String) {
waitUntilViewVisible(withId(R.id.loginSplashSubmit)) waitUntilViewVisible(withId(R.id.loginSplashSubmit))
assertDisplayed(R.id.loginSplashSubmit, R.string.login_splash_submit) assertDisplayed(R.id.loginSplashSubmit, R.string.login_splash_create_account)
if (createAccount) { if (createAccount) {
clickOn(R.id.loginSplashSubmit) clickOn(R.id.loginSplashSubmit)
} else { } else {

View file

@ -0,0 +1,97 @@
/*
* 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.ui.robot
import androidx.test.espresso.Espresso
import androidx.test.espresso.matcher.ViewMatchers
import com.adevinta.android.barista.assertion.BaristaVisibilityAssertions
import com.adevinta.android.barista.interaction.BaristaClickInteractions
import com.adevinta.android.barista.interaction.BaristaEditTextInteractions
import im.vector.app.R
import im.vector.app.espresso.tools.waitUntilViewVisible
class OnboardingServersRobot {
fun crawlSignUp() {
BaristaVisibilityAssertions.assertDisplayed(R.id.loginServerTitle, R.string.login_server_title)
crawlMatrixServer(isSignUp = true)
crawlEmsServer()
crawlOtherServer(isSignUp = true)
crawlSignInWithMatrixId()
}
fun crawlSignIn() {
BaristaVisibilityAssertions.assertDisplayed(R.id.loginServerTitle, R.string.login_server_title)
crawlMatrixServer(isSignUp = false)
crawlEmsServer()
crawlOtherServer(isSignUp = false)
crawlSignInWithMatrixId()
}
private fun crawlOtherServer(isSignUp: Boolean) {
BaristaClickInteractions.clickOn(R.id.loginServerChoiceOther)
waitUntilViewVisible(ViewMatchers.withId(R.id.loginServerUrlFormTitle))
BaristaEditTextInteractions.writeTo(R.id.loginServerUrlFormHomeServerUrl, "https://chat.mozilla.org")
BaristaClickInteractions.clickOn(R.id.loginServerUrlFormSubmit)
waitUntilViewVisible(ViewMatchers.withId(R.id.loginSignupSigninTitle))
BaristaVisibilityAssertions.assertDisplayed(R.id.loginSignupSigninText, "Connect to chat.mozilla.org")
BaristaVisibilityAssertions.assertDisplayed(R.id.loginSignupSigninSubmit, R.string.login_signin_sso)
Espresso.pressBack()
BaristaEditTextInteractions.writeTo(R.id.loginServerUrlFormHomeServerUrl, "https://matrix.org")
BaristaClickInteractions.clickOn(R.id.loginServerUrlFormSubmit)
assetMatrixSignInOptions(isSignUp)
Espresso.pressBack()
Espresso.pressBack()
}
private fun crawlEmsServer() {
BaristaClickInteractions.clickOn(R.id.loginServerChoiceEms)
waitUntilViewVisible(ViewMatchers.withId(R.id.loginServerUrlFormTitle))
BaristaVisibilityAssertions.assertDisplayed(R.id.loginServerUrlFormTitle, R.string.login_connect_to_modular)
BaristaEditTextInteractions.writeTo(R.id.loginServerUrlFormHomeServerUrl, "https://one.ems.host")
BaristaClickInteractions.clickOn(R.id.loginServerUrlFormSubmit)
waitUntilViewVisible(ViewMatchers.withId(R.id.loginSignupSigninTitle))
BaristaVisibilityAssertions.assertDisplayed(R.id.loginSignupSigninText, "one.ems.host")
BaristaVisibilityAssertions.assertDisplayed(R.id.loginSignupSigninSubmit, R.string.login_signin_sso)
Espresso.pressBack()
Espresso.pressBack()
}
private fun crawlMatrixServer(isSignUp: Boolean) {
BaristaClickInteractions.clickOn(R.id.loginServerChoiceMatrixOrg)
assetMatrixSignInOptions(isSignUp)
Espresso.pressBack()
}
private fun assetMatrixSignInOptions(isSignUp: Boolean) {
waitUntilViewVisible(ViewMatchers.withId(R.id.loginTitle))
when (isSignUp) {
true -> BaristaVisibilityAssertions.assertDisplayed(R.id.loginTitle, "Sign up to matrix.org")
false -> BaristaVisibilityAssertions.assertDisplayed(R.id.loginTitle, "Connect to matrix.org")
}
}
private fun crawlSignInWithMatrixId() {
BaristaClickInteractions.clickOn(R.id.loginServerIKnowMyIdSubmit)
waitUntilViewVisible(ViewMatchers.withId(R.id.loginTitle))
BaristaVisibilityAssertions.assertDisplayed(R.id.loginTitle, R.string.login_signin_matrix_id_title)
Espresso.pressBack()
}
}

View file

@ -37,7 +37,6 @@ import im.vector.app.features.home.room.detail.timeline.action.MessageActionsBot
import im.vector.app.features.home.room.detail.timeline.reactions.ViewReactionsBottomSheet import im.vector.app.features.home.room.detail.timeline.reactions.ViewReactionsBottomSheet
import im.vector.app.features.reactions.data.EmojiDataSource import im.vector.app.features.reactions.data.EmojiDataSource
import im.vector.app.interactWithSheet import im.vector.app.interactWithSheet
import im.vector.app.waitForView
import im.vector.app.withRetry import im.vector.app.withRetry
import java.lang.Thread.sleep import java.lang.Thread.sleep
@ -127,7 +126,7 @@ class RoomDetailRobot {
fun openSettings(block: RoomSettingsRobot.() -> Unit) { fun openSettings(block: RoomSettingsRobot.() -> Unit) {
clickMenu(R.id.timeline_setting) clickMenu(R.id.timeline_setting)
waitForView(withId(R.id.roomProfileAvatarView)) waitUntilViewVisible(withId(R.id.roomProfileAvatarView))
sleep(1000) sleep(1000)
block(RoomSettingsRobot()) block(RoomSettingsRobot())
pressBack() pressBack()

View file

@ -78,11 +78,17 @@ class AppStateHandler @Inject constructor(
} }
} }
fun setCurrentSpace(spaceId: String?, session: Session? = null) { fun setCurrentSpace(spaceId: String?, session: Session? = null, persistNow: Boolean = false) {
val uSession = session ?: activeSessionHolder.getSafeActiveSession() ?: return val uSession = session ?: activeSessionHolder.getSafeActiveSession() ?: return
if (selectedSpaceDataSource.currentValue?.orNull() is RoomGroupingMethod.BySpace && if (selectedSpaceDataSource.currentValue?.orNull() is RoomGroupingMethod.BySpace &&
spaceId == selectedSpaceDataSource.currentValue?.orNull()?.space()?.roomId) return spaceId == selectedSpaceDataSource.currentValue?.orNull()?.space()?.roomId) return
val spaceSum = spaceId?.let { uSession.getRoomSummary(spaceId) } val spaceSum = spaceId?.let { uSession.getRoomSummary(spaceId) }
if (persistNow) {
uiStateRepository.storeGroupingMethod(true, uSession.sessionId)
uiStateRepository.storeSelectedSpace(spaceSum?.roomId, uSession.sessionId)
}
selectedSpaceDataSource.post(Option.just(RoomGroupingMethod.BySpace(spaceSum))) selectedSpaceDataSource.post(Option.just(RoomGroupingMethod.BySpace(spaceSum)))
if (spaceId != null) { if (spaceId != null) {
uSession.coroutineScope.launch(Dispatchers.IO) { uSession.coroutineScope.launch(Dispatchers.IO) {

View file

@ -21,7 +21,7 @@ import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent import dagger.hilt.components.SingletonComponent
import im.vector.app.core.dialogs.UnrecognizedCertificateDialog import im.vector.app.core.dialogs.UnrecognizedCertificateDialog
import im.vector.app.core.error.ErrorFormatter import im.vector.app.core.error.ErrorFormatter
import im.vector.app.features.analytics.VectorAnalytics import im.vector.app.features.analytics.AnalyticsTracker
import im.vector.app.features.call.webrtc.WebRtcCallManager import im.vector.app.features.call.webrtc.WebRtcCallManager
import im.vector.app.features.home.AvatarRenderer import im.vector.app.features.home.AvatarRenderer
import im.vector.app.features.navigation.Navigator import im.vector.app.features.navigation.Navigator
@ -56,7 +56,7 @@ interface SingletonEntryPoint {
fun pinLocker(): PinLocker fun pinLocker(): PinLocker
fun analytics(): VectorAnalytics fun analyticsTracker(): AnalyticsTracker
fun webRtcCallManager(): WebRtcCallManager fun webRtcCallManager(): WebRtcCallManager

View file

@ -33,6 +33,7 @@ import im.vector.app.core.error.DefaultErrorFormatter
import im.vector.app.core.error.ErrorFormatter import im.vector.app.core.error.ErrorFormatter
import im.vector.app.core.time.Clock import im.vector.app.core.time.Clock
import im.vector.app.core.time.DefaultClock import im.vector.app.core.time.DefaultClock
import im.vector.app.features.analytics.AnalyticsTracker
import im.vector.app.features.analytics.VectorAnalytics import im.vector.app.features.analytics.VectorAnalytics
import im.vector.app.features.analytics.impl.DefaultVectorAnalytics import im.vector.app.features.analytics.impl.DefaultVectorAnalytics
import im.vector.app.features.invite.AutoAcceptInvites import im.vector.app.features.invite.AutoAcceptInvites
@ -64,6 +65,9 @@ abstract class VectorBindModule {
@Binds @Binds
abstract fun bindVectorAnalytics(analytics: DefaultVectorAnalytics): VectorAnalytics abstract fun bindVectorAnalytics(analytics: DefaultVectorAnalytics): VectorAnalytics
@Binds
abstract fun bindAnalyticsTracker(analytics: DefaultVectorAnalytics): AnalyticsTracker
@Binds @Binds
abstract fun bindErrorFormatter(formatter: DefaultErrorFormatter): ErrorFormatter abstract fun bindErrorFormatter(formatter: DefaultErrorFormatter): ErrorFormatter

View file

@ -26,7 +26,6 @@ import im.vector.app.R
import im.vector.app.core.epoxy.ClickListener import im.vector.app.core.epoxy.ClickListener
import im.vector.app.core.epoxy.VectorEpoxyHolder import im.vector.app.core.epoxy.VectorEpoxyHolder
import im.vector.app.core.epoxy.VectorEpoxyModel import im.vector.app.core.epoxy.VectorEpoxyModel
import im.vector.app.core.epoxy.charsequence.EpoxyCharSequence
import im.vector.app.core.epoxy.onClick import im.vector.app.core.epoxy.onClick
import im.vector.app.core.extensions.setTextOrHide import im.vector.app.core.extensions.setTextOrHide
import im.vector.app.features.displayname.getBestName import im.vector.app.features.displayname.getBestName
@ -34,6 +33,7 @@ import im.vector.app.features.home.AvatarRenderer
import im.vector.app.features.home.room.detail.timeline.item.BindingOptions import im.vector.app.features.home.room.detail.timeline.item.BindingOptions
import im.vector.app.features.home.room.detail.timeline.tools.findPillsAndProcess import im.vector.app.features.home.room.detail.timeline.tools.findPillsAndProcess
import im.vector.app.features.media.ImageContentRenderer import im.vector.app.features.media.ImageContentRenderer
import im.vector.lib.core.utils.epoxy.charsequence.EpoxyCharSequence
import org.matrix.android.sdk.api.util.MatrixItem import org.matrix.android.sdk.api.util.MatrixItem
/** /**

View file

@ -24,6 +24,7 @@ import org.matrix.android.sdk.api.failure.Failure
import org.matrix.android.sdk.api.failure.MatrixError import org.matrix.android.sdk.api.failure.MatrixError
import org.matrix.android.sdk.api.failure.MatrixIdFailure import org.matrix.android.sdk.api.failure.MatrixIdFailure
import org.matrix.android.sdk.api.failure.isInvalidPassword import org.matrix.android.sdk.api.failure.isInvalidPassword
import org.matrix.android.sdk.api.failure.isLimitExceededError
import org.matrix.android.sdk.api.session.identity.IdentityServiceError import org.matrix.android.sdk.api.session.identity.IdentityServiceError
import java.net.HttpURLConnection import java.net.HttpURLConnection
import java.net.SocketTimeoutException import java.net.SocketTimeoutException
@ -58,53 +59,53 @@ class DefaultErrorFormatter @Inject constructor(
} }
is Failure.ServerError -> { is Failure.ServerError -> {
when { when {
throwable.error.code == MatrixError.M_CONSENT_NOT_GIVEN -> { throwable.error.code == MatrixError.M_CONSENT_NOT_GIVEN -> {
// Special case for terms and conditions // Special case for terms and conditions
stringProvider.getString(R.string.error_terms_not_accepted) stringProvider.getString(R.string.error_terms_not_accepted)
} }
throwable.isInvalidPassword() -> { throwable.isInvalidPassword() -> {
stringProvider.getString(R.string.auth_invalid_login_param) stringProvider.getString(R.string.auth_invalid_login_param)
} }
throwable.error.code == MatrixError.M_USER_IN_USE -> { throwable.error.code == MatrixError.M_USER_IN_USE -> {
stringProvider.getString(R.string.login_signup_error_user_in_use) stringProvider.getString(R.string.login_signup_error_user_in_use)
} }
throwable.error.code == MatrixError.M_BAD_JSON -> { throwable.error.code == MatrixError.M_BAD_JSON -> {
stringProvider.getString(R.string.login_error_bad_json) stringProvider.getString(R.string.login_error_bad_json)
} }
throwable.error.code == MatrixError.M_NOT_JSON -> { throwable.error.code == MatrixError.M_NOT_JSON -> {
stringProvider.getString(R.string.login_error_not_json) stringProvider.getString(R.string.login_error_not_json)
} }
throwable.error.code == MatrixError.M_THREEPID_DENIED -> { throwable.error.code == MatrixError.M_THREEPID_DENIED -> {
stringProvider.getString(R.string.login_error_threepid_denied) stringProvider.getString(R.string.login_error_threepid_denied)
} }
throwable.error.code == MatrixError.M_LIMIT_EXCEEDED -> { throwable.isLimitExceededError() -> {
limitExceededError(throwable.error) limitExceededError(throwable.error)
} }
throwable.error.code == MatrixError.M_TOO_LARGE -> { throwable.error.code == MatrixError.M_TOO_LARGE -> {
stringProvider.getString(R.string.error_file_too_big_simple) stringProvider.getString(R.string.error_file_too_big_simple)
} }
throwable.error.code == MatrixError.M_THREEPID_NOT_FOUND -> { throwable.error.code == MatrixError.M_THREEPID_NOT_FOUND -> {
stringProvider.getString(R.string.login_reset_password_error_not_found) stringProvider.getString(R.string.login_reset_password_error_not_found)
} }
throwable.error.code == MatrixError.M_USER_DEACTIVATED -> { throwable.error.code == MatrixError.M_USER_DEACTIVATED -> {
stringProvider.getString(R.string.auth_invalid_login_deactivated_account) stringProvider.getString(R.string.auth_invalid_login_deactivated_account)
} }
throwable.error.code == MatrixError.M_THREEPID_IN_USE && throwable.error.code == MatrixError.M_THREEPID_IN_USE &&
throwable.error.message == "Email is already in use" -> { throwable.error.message == "Email is already in use" -> {
stringProvider.getString(R.string.account_email_already_used_error) stringProvider.getString(R.string.account_email_already_used_error)
} }
throwable.error.code == MatrixError.M_THREEPID_IN_USE && throwable.error.code == MatrixError.M_THREEPID_IN_USE &&
throwable.error.message == "MSISDN is already in use" -> { throwable.error.message == "MSISDN is already in use" -> {
stringProvider.getString(R.string.account_phone_number_already_used_error) stringProvider.getString(R.string.account_phone_number_already_used_error)
} }
throwable.error.code == MatrixError.M_THREEPID_AUTH_FAILED -> { throwable.error.code == MatrixError.M_THREEPID_AUTH_FAILED -> {
stringProvider.getString(R.string.error_threepid_auth_failed) stringProvider.getString(R.string.error_threepid_auth_failed)
} }
throwable.error.code == MatrixError.M_UNKNOWN && throwable.error.code == MatrixError.M_UNKNOWN &&
throwable.error.message == "Not allowed to join this room" -> { throwable.error.message == "Not allowed to join this room" -> {
stringProvider.getString(R.string.room_error_access_unauthorized) stringProvider.getString(R.string.room_error_access_unauthorized)
} }
else -> { else -> {
throwable.error.message.takeIf { it.isNotEmpty() } throwable.error.message.takeIf { it.isNotEmpty() }
?: throwable.error.code.takeIf { it.isNotEmpty() } ?: throwable.error.code.takeIf { it.isNotEmpty() }
} }

View file

@ -30,7 +30,8 @@ abstract class SimpleFragmentActivity : VectorBaseActivity<ActivityBinding>() {
final override fun getCoordinatorLayout() = views.coordinatorLayout final override fun getCoordinatorLayout() = views.coordinatorLayout
override fun initUiAndData() { override fun initUiAndData() {
configureToolbar(views.toolbar) setupToolbar(views.toolbar)
.allowBack(true)
waitingView = views.waitingView.waitingView waitingView = views.waitingView.waitingView
} }

View file

@ -62,10 +62,13 @@ import im.vector.app.core.extensions.restart
import im.vector.app.core.extensions.setTextOrHide import im.vector.app.core.extensions.setTextOrHide
import im.vector.app.core.extensions.singletonEntryPoint import im.vector.app.core.extensions.singletonEntryPoint
import im.vector.app.core.extensions.toMvRxBundle import im.vector.app.core.extensions.toMvRxBundle
import im.vector.app.core.utils.ToolbarConfig
import im.vector.app.core.utils.toast import im.vector.app.core.utils.toast
import im.vector.app.features.MainActivity import im.vector.app.features.MainActivity
import im.vector.app.features.MainActivityArgs import im.vector.app.features.MainActivityArgs
import im.vector.app.features.analytics.VectorAnalytics import im.vector.app.features.analytics.AnalyticsTracker
import im.vector.app.features.analytics.plan.Screen
import im.vector.app.features.analytics.screen.ScreenEvent
import im.vector.app.features.configuration.VectorConfiguration import im.vector.app.features.configuration.VectorConfiguration
import im.vector.app.features.consent.ConsentNotGivenHelper import im.vector.app.features.consent.ConsentNotGivenHelper
import im.vector.app.features.navigation.Navigator import im.vector.app.features.navigation.Navigator
@ -90,6 +93,15 @@ import timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
abstract class VectorBaseActivity<VB : ViewBinding> : AppCompatActivity(), MavericksView { abstract class VectorBaseActivity<VB : ViewBinding> : AppCompatActivity(), MavericksView {
/* ==========================================================================================
* Analytics
* ========================================================================================== */
protected var analyticsScreenName: Screen.ScreenName? = null
private var screenEvent: ScreenEvent? = null
protected lateinit var analyticsTracker: AnalyticsTracker
/* ========================================================================================== /* ==========================================================================================
* View * View
* ========================================================================================== */ * ========================================================================================== */
@ -115,6 +127,8 @@ abstract class VectorBaseActivity<VB : ViewBinding> : AppCompatActivity(), Maver
.launchIn(lifecycleScope) .launchIn(lifecycleScope)
} }
var toolbar: ToolbarConfig? = null
/* ========================================================================================== /* ==========================================================================================
* Views * Views
* ========================================================================================== */ * ========================================================================================== */
@ -133,7 +147,6 @@ abstract class VectorBaseActivity<VB : ViewBinding> : AppCompatActivity(), Maver
private lateinit var sessionListener: SessionListener private lateinit var sessionListener: SessionListener
protected lateinit var bugReporter: BugReporter protected lateinit var bugReporter: BugReporter
private lateinit var pinLocker: PinLocker private lateinit var pinLocker: PinLocker
protected lateinit var analytics: VectorAnalytics
@Inject @Inject
lateinit var rageShake: RageShake lateinit var rageShake: RageShake
@ -189,7 +202,7 @@ abstract class VectorBaseActivity<VB : ViewBinding> : AppCompatActivity(), Maver
configurationViewModel = viewModelProvider.get(ConfigurationViewModel::class.java) configurationViewModel = viewModelProvider.get(ConfigurationViewModel::class.java)
bugReporter = singletonEntryPoint.bugReporter() bugReporter = singletonEntryPoint.bugReporter()
pinLocker = singletonEntryPoint.pinLocker() pinLocker = singletonEntryPoint.pinLocker()
analytics = singletonEntryPoint.analytics() analyticsTracker = singletonEntryPoint.analyticsTracker()
navigator = singletonEntryPoint.navigator() navigator = singletonEntryPoint.navigator()
activeSessionHolder = singletonEntryPoint.activeSessionHolder() activeSessionHolder = singletonEntryPoint.activeSessionHolder()
vectorPreferences = singletonEntryPoint.vectorPreferences() vectorPreferences = singletonEntryPoint.vectorPreferences()
@ -324,7 +337,7 @@ abstract class VectorBaseActivity<VB : ViewBinding> : AppCompatActivity(), Maver
override fun onResume() { override fun onResume() {
super.onResume() super.onResume()
Timber.i("onResume Activity ${javaClass.simpleName}") Timber.i("onResume Activity ${javaClass.simpleName}")
screenEvent = analyticsScreenName?.let { ScreenEvent(it) }
configurationViewModel.onActivityResumed() configurationViewModel.onActivityResumed()
if (this !is BugReportActivity && vectorPreferences.useRageshake()) { if (this !is BugReportActivity && vectorPreferences.useRageshake()) {
@ -363,6 +376,7 @@ abstract class VectorBaseActivity<VB : ViewBinding> : AppCompatActivity(), Maver
override fun onPause() { override fun onPause() {
super.onPause() super.onPause()
screenEvent?.send(analyticsTracker, analyticsScreenName)
Timber.i("onPause Activity ${javaClass.simpleName}") Timber.i("onPause Activity ${javaClass.simpleName}")
rageShake.stop() rageShake.stop()
@ -497,18 +511,6 @@ abstract class VectorBaseActivity<VB : ViewBinding> : AppCompatActivity(), Maver
*/ */
protected fun isFirstCreation() = savedInstanceState == null protected fun isFirstCreation() = savedInstanceState == null
/**
* Configure the Toolbar, with default back button.
*/
protected fun configureToolbar(toolbar: MaterialToolbar, displayBack: Boolean = true) {
setSupportActionBar(toolbar)
supportActionBar?.let {
it.setDisplayShowHomeEnabled(displayBack)
it.setDisplayHomeAsUpEnabled(displayBack)
it.title = null
}
}
// ============================================================================================== // ==============================================================================================
// Handle loading view (also called waiting view or spinner view) // Handle loading view (also called waiting view or spinner view)
// ============================================================================================== // ==============================================================================================
@ -618,4 +620,13 @@ abstract class VectorBaseActivity<VB : ViewBinding> : AppCompatActivity(), Maver
toast(getString(R.string.not_implemented)) toast(getString(R.string.not_implemented))
} }
} }
/**
* Sets toolbar as actionBar
*
* @return Instance of [ToolbarConfig] with set of helper methods to configure toolbar
* */
fun setupToolbar(toolbar: MaterialToolbar) = ToolbarConfig(this, toolbar).also {
this.toolbar = it.setup()
}
} }

View file

@ -37,7 +37,9 @@ import im.vector.app.core.di.ActivityEntryPoint
import im.vector.app.core.extensions.singletonEntryPoint import im.vector.app.core.extensions.singletonEntryPoint
import im.vector.app.core.extensions.toMvRxBundle import im.vector.app.core.extensions.toMvRxBundle
import im.vector.app.core.utils.DimensionConverter import im.vector.app.core.utils.DimensionConverter
import im.vector.app.features.analytics.VectorAnalytics import im.vector.app.features.analytics.AnalyticsTracker
import im.vector.app.features.analytics.plan.Screen
import im.vector.app.features.analytics.screen.ScreenEvent
import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
import reactivecircus.flowbinding.android.view.clicks import reactivecircus.flowbinding.android.view.clicks
@ -47,6 +49,14 @@ import timber.log.Timber
* Add Mavericks capabilities, handle DI and bindings. * Add Mavericks capabilities, handle DI and bindings.
*/ */
abstract class VectorBaseBottomSheetDialogFragment<VB : ViewBinding> : BottomSheetDialogFragment(), MavericksView { abstract class VectorBaseBottomSheetDialogFragment<VB : ViewBinding> : BottomSheetDialogFragment(), MavericksView {
/* ==========================================================================================
* Analytics
* ========================================================================================== */
protected var analyticsScreenName: Screen.ScreenName? = null
private var screenEvent: ScreenEvent? = null
protected lateinit var analyticsTracker: AnalyticsTracker
/* ========================================================================================== /* ==========================================================================================
* View * View
@ -84,8 +94,6 @@ abstract class VectorBaseBottomSheetDialogFragment<VB : ViewBinding> : BottomShe
open val showExpanded = false open val showExpanded = false
protected lateinit var analytics: VectorAnalytics
interface ResultListener { interface ResultListener {
fun onBottomSheetResult(resultCode: Int, data: Any?) fun onBottomSheetResult(resultCode: Int, data: Any?)
@ -124,13 +132,19 @@ abstract class VectorBaseBottomSheetDialogFragment<VB : ViewBinding> : BottomShe
val activityEntryPoint = EntryPointAccessors.fromActivity(vectorBaseActivity, ActivityEntryPoint::class.java) val activityEntryPoint = EntryPointAccessors.fromActivity(vectorBaseActivity, ActivityEntryPoint::class.java)
viewModelFactory = activityEntryPoint.viewModelFactory() viewModelFactory = activityEntryPoint.viewModelFactory()
val singletonEntryPoint = context.singletonEntryPoint() val singletonEntryPoint = context.singletonEntryPoint()
analytics = singletonEntryPoint.analytics() analyticsTracker = singletonEntryPoint.analyticsTracker()
super.onAttach(context) super.onAttach(context)
} }
override fun onResume() { override fun onResume() {
super.onResume() super.onResume()
Timber.i("onResume BottomSheet ${javaClass.simpleName}") Timber.i("onResume BottomSheet ${javaClass.simpleName}")
screenEvent = analyticsScreenName?.let { ScreenEvent(it) }
}
override fun onPause() {
super.onPause()
screenEvent?.send(analyticsTracker)
} }
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {

View file

@ -42,7 +42,10 @@ import im.vector.app.core.dialogs.UnrecognizedCertificateDialog
import im.vector.app.core.error.ErrorFormatter import im.vector.app.core.error.ErrorFormatter
import im.vector.app.core.extensions.singletonEntryPoint import im.vector.app.core.extensions.singletonEntryPoint
import im.vector.app.core.extensions.toMvRxBundle import im.vector.app.core.extensions.toMvRxBundle
import im.vector.app.features.analytics.VectorAnalytics import im.vector.app.core.utils.ToolbarConfig
import im.vector.app.features.analytics.AnalyticsTracker
import im.vector.app.features.analytics.plan.Screen
import im.vector.app.features.analytics.screen.ScreenEvent
import im.vector.app.features.navigation.Navigator import im.vector.app.features.navigation.Navigator
import im.vector.lib.ui.styles.dialogs.MaterialProgressDialog import im.vector.lib.ui.styles.dialogs.MaterialProgressDialog
import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.launchIn
@ -51,6 +54,18 @@ import reactivecircus.flowbinding.android.view.clicks
import timber.log.Timber import timber.log.Timber
abstract class VectorBaseFragment<VB : ViewBinding> : Fragment(), MavericksView { abstract class VectorBaseFragment<VB : ViewBinding> : Fragment(), MavericksView {
/* ==========================================================================================
* Analytics
* ========================================================================================== */
protected var analyticsScreenName: Screen.ScreenName? = null
private var screenEvent: ScreenEvent? = null
protected lateinit var analyticsTracker: AnalyticsTracker
/* ==========================================================================================
* Activity
* ========================================================================================== */
protected val vectorBaseActivity: VectorBaseActivity<*> by lazy { protected val vectorBaseActivity: VectorBaseActivity<*> by lazy {
activity as VectorBaseActivity<*> activity as VectorBaseActivity<*>
@ -61,12 +76,17 @@ abstract class VectorBaseFragment<VB : ViewBinding> : Fragment(), MavericksView
* ========================================================================================== */ * ========================================================================================== */
protected lateinit var navigator: Navigator protected lateinit var navigator: Navigator
protected lateinit var analytics: VectorAnalytics
protected lateinit var errorFormatter: ErrorFormatter protected lateinit var errorFormatter: ErrorFormatter
protected lateinit var unrecognizedCertificateDialog: UnrecognizedCertificateDialog protected lateinit var unrecognizedCertificateDialog: UnrecognizedCertificateDialog
private var progress: AlertDialog? = null private var progress: AlertDialog? = null
/**
* [ToolbarConfig] instance from host activity
* */
protected var toolbar: ToolbarConfig? = null
get() = (activity as? VectorBaseActivity<*>)?.toolbar
private set
/* ========================================================================================== /* ==========================================================================================
* View model * View model
* ========================================================================================== */ * ========================================================================================== */
@ -98,7 +118,7 @@ abstract class VectorBaseFragment<VB : ViewBinding> : Fragment(), MavericksView
val activityEntryPoint = EntryPointAccessors.fromActivity(vectorBaseActivity, ActivityEntryPoint::class.java) val activityEntryPoint = EntryPointAccessors.fromActivity(vectorBaseActivity, ActivityEntryPoint::class.java)
navigator = singletonEntryPoint.navigator() navigator = singletonEntryPoint.navigator()
errorFormatter = singletonEntryPoint.errorFormatter() errorFormatter = singletonEntryPoint.errorFormatter()
analytics = singletonEntryPoint.analytics() analyticsTracker = singletonEntryPoint.analyticsTracker()
unrecognizedCertificateDialog = singletonEntryPoint.unrecognizedCertificateDialog() unrecognizedCertificateDialog = singletonEntryPoint.unrecognizedCertificateDialog()
viewModelFactory = activityEntryPoint.viewModelFactory() viewModelFactory = activityEntryPoint.viewModelFactory()
childFragmentManager.fragmentFactory = activityEntryPoint.fragmentFactory() childFragmentManager.fragmentFactory = activityEntryPoint.fragmentFactory()
@ -125,12 +145,14 @@ abstract class VectorBaseFragment<VB : ViewBinding> : Fragment(), MavericksView
override fun onResume() { override fun onResume() {
super.onResume() super.onResume()
Timber.i("onResume Fragment ${javaClass.simpleName}") Timber.i("onResume Fragment ${javaClass.simpleName}")
screenEvent = analyticsScreenName?.let { ScreenEvent(it) }
} }
@CallSuper @CallSuper
override fun onPause() { override fun onPause() {
super.onPause() super.onPause()
Timber.i("onPause Fragment ${javaClass.simpleName}") Timber.i("onPause Fragment ${javaClass.simpleName}")
screenEvent?.send(analyticsTracker)
} }
@CallSuper @CallSuper
@ -213,13 +235,12 @@ abstract class VectorBaseFragment<VB : ViewBinding> : Fragment(), MavericksView
* ========================================================================================== */ * ========================================================================================== */
/** /**
* Configure the Toolbar. * Sets toolbar as actionBar for current activity
*/ *
protected fun setupToolbar(toolbar: MaterialToolbar) { * @return Instance of [ToolbarConfig] with set of helper methods to configure toolbar
val parentActivity = vectorBaseActivity * */
if (parentActivity is ToolbarConfigurable) { protected fun setupToolbar(toolbar: MaterialToolbar): ToolbarConfig {
parentActivity.configure(toolbar) return vectorBaseActivity.setupToolbar(toolbar)
}
} }
/* ========================================================================================== /* ==========================================================================================

View file

@ -24,10 +24,10 @@ import im.vector.app.R
import im.vector.app.core.epoxy.ClickListener import im.vector.app.core.epoxy.ClickListener
import im.vector.app.core.epoxy.VectorEpoxyHolder import im.vector.app.core.epoxy.VectorEpoxyHolder
import im.vector.app.core.epoxy.VectorEpoxyModel import im.vector.app.core.epoxy.VectorEpoxyModel
import im.vector.app.core.epoxy.charsequence.EpoxyCharSequence
import im.vector.app.core.epoxy.onClick import im.vector.app.core.epoxy.onClick
import im.vector.app.core.extensions.setTextOrHide import im.vector.app.core.extensions.setTextOrHide
import im.vector.app.features.themes.ThemeUtils import im.vector.app.features.themes.ThemeUtils
import im.vector.lib.core.utils.epoxy.charsequence.EpoxyCharSequence
/** /**
* A generic list item. * A generic list item.

View file

@ -28,9 +28,9 @@ import im.vector.app.R
import im.vector.app.core.epoxy.ClickListener import im.vector.app.core.epoxy.ClickListener
import im.vector.app.core.epoxy.VectorEpoxyHolder import im.vector.app.core.epoxy.VectorEpoxyHolder
import im.vector.app.core.epoxy.VectorEpoxyModel import im.vector.app.core.epoxy.VectorEpoxyModel
import im.vector.app.core.epoxy.charsequence.EpoxyCharSequence
import im.vector.app.core.epoxy.onClick import im.vector.app.core.epoxy.onClick
import im.vector.app.core.extensions.setTextOrHide import im.vector.app.core.extensions.setTextOrHide
import im.vector.lib.core.utils.epoxy.charsequence.EpoxyCharSequence
/** /**
* A generic list item. * A generic list item.

View file

@ -28,10 +28,10 @@ import im.vector.app.R
import im.vector.app.core.epoxy.ClickListener import im.vector.app.core.epoxy.ClickListener
import im.vector.app.core.epoxy.VectorEpoxyHolder import im.vector.app.core.epoxy.VectorEpoxyHolder
import im.vector.app.core.epoxy.VectorEpoxyModel import im.vector.app.core.epoxy.VectorEpoxyModel
import im.vector.app.core.epoxy.charsequence.EpoxyCharSequence
import im.vector.app.core.epoxy.onClick import im.vector.app.core.epoxy.onClick
import im.vector.app.core.extensions.setTextOrHide import im.vector.app.core.extensions.setTextOrHide
import im.vector.app.features.themes.ThemeUtils import im.vector.app.features.themes.ThemeUtils
import im.vector.lib.core.utils.epoxy.charsequence.EpoxyCharSequence
/** /**
* A generic list item with a rounded corner background and an optional icon * A generic list item with a rounded corner background and an optional icon

View file

@ -27,10 +27,10 @@ import im.vector.app.R
import im.vector.app.core.epoxy.ClickListener import im.vector.app.core.epoxy.ClickListener
import im.vector.app.core.epoxy.VectorEpoxyHolder import im.vector.app.core.epoxy.VectorEpoxyHolder
import im.vector.app.core.epoxy.VectorEpoxyModel import im.vector.app.core.epoxy.VectorEpoxyModel
import im.vector.app.core.epoxy.charsequence.EpoxyCharSequence
import im.vector.app.core.epoxy.onClick import im.vector.app.core.epoxy.onClick
import im.vector.app.core.extensions.setTextOrHide import im.vector.app.core.extensions.setTextOrHide
import im.vector.app.features.themes.ThemeUtils import im.vector.app.features.themes.ThemeUtils
import im.vector.lib.core.utils.epoxy.charsequence.EpoxyCharSequence
/** /**
* A generic list item. * A generic list item.

View file

@ -0,0 +1,82 @@
/*
* 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.utils
import androidx.annotation.StringRes
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.content.res.AppCompatResources
import com.google.android.material.appbar.MaterialToolbar
import im.vector.app.R
/**
* Helper class to configure toolbar.
* Wraps [MaterialToolbar] providing set of methods to configure it
*/
class ToolbarConfig(val activity: AppCompatActivity, val toolbar: MaterialToolbar) {
private var customBackResId: Int? = null
fun setup() = apply {
activity.setSupportActionBar(toolbar)
}
/**
* Delegating property for [toolbar.title]
* */
var title: CharSequence? by toolbar::title
/**
* Delegating property for [toolbar.subtitle]
* */
var subtitle: CharSequence? by toolbar::subtitle
/**
* Sets toolbar's title text
* */
fun setTitle(title: CharSequence?) = apply { toolbar.title = title }
/**
* Sets toolbar's title text using provided string resource
* */
fun setTitle(@StringRes titleRes: Int) = apply { toolbar.setTitle(titleRes) }
/**
* Sets toolbar's subtitle text
* */
fun setSubtitle(subtitle: String?) = apply { toolbar.subtitle = subtitle }
/**
* Sets toolbar's title text using provided string resource
* */
fun setSubtitle(@StringRes subtitleRes: Int) = apply { toolbar.subtitle = activity.getString(subtitleRes) }
/**
* Enables/disables navigate back button
*
* @param isAllowed defines if back button is enabled. Default [true]
* @param useCross defines if cross icon should be used instead of arrow. Default [false]
* */
fun allowBack(isAllowed: Boolean = true, useCross: Boolean = false) = apply {
activity.supportActionBar?.let {
it.setDisplayShowHomeEnabled(isAllowed)
it.setDisplayHomeAsUpEnabled(isAllowed)
if (isAllowed && useCross) {
val navResId = customBackResId ?: R.drawable.ic_x_18dp
toolbar.navigationIcon = AppCompatResources.getDrawable(activity, navResId)
}
}
}
}

View file

@ -35,6 +35,6 @@ interface VectorFeatures {
class DefaultVectorFeatures : VectorFeatures { class DefaultVectorFeatures : VectorFeatures {
override fun onboardingVariant(): VectorFeatures.OnboardingVariant = BuildConfig.ONBOARDING_VARIANT override fun onboardingVariant(): VectorFeatures.OnboardingVariant = BuildConfig.ONBOARDING_VARIANT
override fun isOnboardingAlreadyHaveAccountSplashEnabled() = true override fun isOnboardingAlreadyHaveAccountSplashEnabled() = true
override fun isOnboardingSplashCarouselEnabled() = false override fun isOnboardingSplashCarouselEnabled() = true
override fun isOnboardingUseCaseEnabled() = false override fun isOnboardingUseCaseEnabled() = false
} }

View file

@ -1,11 +1,11 @@
/* /*
* Copyright 2019 New Vector Ltd * Copyright (c) 2021 New Vector Ltd
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
* You may obtain a copy of the License at * You may obtain a copy of the License at
* *
* http://www.apache.org/licenses/LICENSE-2.0 * http://www.apache.org/licenses/LICENSE-2.0
* *
* Unless required by applicable law or agreed to in writing, software * Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, * distributed under the License is distributed on an "AS IS" BASIS,
@ -14,11 +14,19 @@
* limitations under the License. * limitations under the License.
*/ */
package im.vector.app.core.platform package im.vector.app.features.analytics
import com.google.android.material.appbar.MaterialToolbar import im.vector.app.features.analytics.itf.VectorAnalyticsEvent
import im.vector.app.features.analytics.itf.VectorAnalyticsScreen
interface ToolbarConfigurable { interface AnalyticsTracker {
/**
* Capture an Event
*/
fun capture(event: VectorAnalyticsEvent)
fun configure(toolbar: MaterialToolbar) /**
* Track a displayed screen
*/
fun screen(screen: VectorAnalyticsScreen)
} }

View file

@ -17,8 +17,8 @@
package im.vector.app.features.analytics package im.vector.app.features.analytics
import im.vector.app.core.time.Clock import im.vector.app.core.time.Clock
import im.vector.app.core.utils.compat.removeIfCompat
import im.vector.app.features.analytics.plan.Error import im.vector.app.features.analytics.plan.Error
import im.vector.lib.core.utils.compat.removeIfCompat
import im.vector.lib.core.utils.flow.tickerFlow import im.vector.lib.core.utils.flow.tickerFlow
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
@ -49,7 +49,7 @@ private const val CHECK_INTERVAL = 2_000L
*/ */
@Singleton @Singleton
class DecryptionFailureTracker @Inject constructor( class DecryptionFailureTracker @Inject constructor(
private val vectorAnalytics: VectorAnalytics, private val analyticsTracker: AnalyticsTracker,
private val clock: Clock private val clock: Clock
) { ) {
@ -136,7 +136,7 @@ class DecryptionFailureTracker @Inject constructor(
// for now we ignore events already reported even if displayed again? // for now we ignore events already reported even if displayed again?
.filter { alreadyReported.contains(it).not() } .filter { alreadyReported.contains(it).not() }
.forEach { failedEventId -> .forEach { failedEventId ->
vectorAnalytics.capture(Error(failedEventId, Error.Domain.E2EE, aggregation.key)) analyticsTracker.capture(Error(failedEventId, Error.Domain.E2EE, aggregation.key))
alreadyReported.add(failedEventId) alreadyReported.add(failedEventId)
} }
} }

View file

@ -16,11 +16,9 @@
package im.vector.app.features.analytics package im.vector.app.features.analytics
import im.vector.app.features.analytics.itf.VectorAnalyticsEvent
import im.vector.app.features.analytics.itf.VectorAnalyticsScreen
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
interface VectorAnalytics { interface VectorAnalytics : AnalyticsTracker {
/** /**
* Return a Flow of Boolean, true if the user has given their consent * Return a Flow of Boolean, true if the user has given their consent
*/ */
@ -60,14 +58,4 @@ interface VectorAnalytics {
* To be called when application is started * To be called when application is started
*/ */
fun init() fun init()
/**
* Capture an Event
*/
fun capture(event: VectorAnalyticsEvent)
/**
* Track a displayed screen
*/
fun screen(screen: VectorAnalyticsScreen)
} }

View file

@ -0,0 +1,47 @@
/*
* 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.analytics.extensions
import im.vector.app.features.analytics.plan.JoinedRoom
import org.matrix.android.sdk.api.extensions.orFalse
import org.matrix.android.sdk.api.session.room.model.RoomSummary
import org.matrix.android.sdk.api.session.room.model.roomdirectory.PublicRoom
fun Int?.toAnalyticsRoomSize(): JoinedRoom.RoomSize {
return when (this) {
null,
2 -> JoinedRoom.RoomSize.Two
in 3..10 -> JoinedRoom.RoomSize.ThreeToTen
in 11..100 -> JoinedRoom.RoomSize.ElevenToOneHundred
in 101..1000 -> JoinedRoom.RoomSize.OneHundredAndOneToAThousand
else -> JoinedRoom.RoomSize.MoreThanAThousand
}
}
fun RoomSummary?.toAnalyticsJoinedRoom(): JoinedRoom {
return JoinedRoom(
isDM = this?.isDirect.orFalse(),
roomSize = this?.joinedMembersCount?.toAnalyticsRoomSize() ?: JoinedRoom.RoomSize.Two
)
}
fun PublicRoom.toAnalyticsJoinedRoom(): JoinedRoom {
return JoinedRoom(
isDM = false,
roomSize = numJoinedMembers.toAnalyticsRoomSize()
)
}

View file

@ -25,22 +25,22 @@ import im.vector.app.features.analytics.itf.VectorAnalyticsEvent
* Triggered when a call has ended. * Triggered when a call has ended.
*/ */
data class CallEnded( data class CallEnded(
/** /**
* The duration of the call in milliseconds. * The duration of the call in milliseconds.
*/ */
val durationMs: Int, val durationMs: Int,
/** /**
* Whether its a video call or not. * Whether its a video call or not.
*/ */
val isVideo: Boolean, val isVideo: Boolean,
/** /**
* Number of participants in the call. * Number of participants in the call.
*/ */
val numParticipants: Int, val numParticipants: Int,
/** /**
* Whether this user placed it. * Whether this user placed it.
*/ */
val placed: Boolean, val placed: Boolean,
) : VectorAnalyticsEvent { ) : VectorAnalyticsEvent {
override fun getName() = "CallEnded" override fun getName() = "CallEnded"

View file

@ -25,18 +25,18 @@ import im.vector.app.features.analytics.itf.VectorAnalyticsEvent
* Triggered when an error occurred in a call. * Triggered when an error occurred in a call.
*/ */
data class CallError( data class CallError(
/** /**
* Whether its a video call or not. * Whether its a video call or not.
*/ */
val isVideo: Boolean, val isVideo: Boolean,
/** /**
* Number of participants in the call. * Number of participants in the call.
*/ */
val numParticipants: Int, val numParticipants: Int,
/** /**
* Whether this user placed it. * Whether this user placed it.
*/ */
val placed: Boolean, val placed: Boolean,
) : VectorAnalyticsEvent { ) : VectorAnalyticsEvent {
override fun getName() = "CallError" override fun getName() = "CallError"

View file

@ -25,18 +25,18 @@ import im.vector.app.features.analytics.itf.VectorAnalyticsEvent
* Triggered when a call is started. * Triggered when a call is started.
*/ */
data class CallStarted( data class CallStarted(
/** /**
* Whether its a video call or not. * Whether its a video call or not.
*/ */
val isVideo: Boolean, val isVideo: Boolean,
/** /**
* Number of participants in the call. * Number of participants in the call.
*/ */
val numParticipants: Int, val numParticipants: Int,
/** /**
* Whether this user placed it. * Whether this user placed it.
*/ */
val placed: Boolean, val placed: Boolean,
) : VectorAnalyticsEvent { ) : VectorAnalyticsEvent {
override fun getName() = "CallStarted" override fun getName() = "CallStarted"

View file

@ -25,14 +25,14 @@ import im.vector.app.features.analytics.itf.VectorAnalyticsEvent
* Triggered when the user clicks/taps on a UI element. * Triggered when the user clicks/taps on a UI element.
*/ */
data class Click( data class Click(
/** /**
* The index of the element, if its in a list of elements. * The index of the element, if its in a list of elements.
*/ */
val index: Int? = null, val index: Int? = null,
/** /**
* The unique name of this element. * The unique name of this element.
*/ */
val name: Name, val name: Name,
) : VectorAnalyticsEvent { ) : VectorAnalyticsEvent {
enum class Name { enum class Name {

View file

@ -25,10 +25,10 @@ import im.vector.app.features.analytics.itf.VectorAnalyticsEvent
* Triggered when the user creates a room. * Triggered when the user creates a room.
*/ */
data class CreatedRoom( data class CreatedRoom(
/** /**
* Whether the room is a DM. * Whether the room is a DM.
*/ */
val isDM: Boolean, val isDM: Boolean,
) : VectorAnalyticsEvent { ) : VectorAnalyticsEvent {
override fun getName() = "CreatedRoom" override fun getName() = "CreatedRoom"

View file

@ -25,12 +25,12 @@ import im.vector.app.features.analytics.itf.VectorAnalyticsEvent
* Triggered when an error occurred * Triggered when an error occurred
*/ */
data class Error( data class Error(
/** /**
* Context - client defined, can be used for debugging * Context - client defined, can be used for debugging
*/ */
val context: String? = null, val context: String? = null,
val domain: Domain, val domain: Domain,
val name: Name, val name: Name,
) : VectorAnalyticsEvent { ) : VectorAnalyticsEvent {
enum class Domain { enum class Domain {

View file

@ -0,0 +1,63 @@
/*
* 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.analytics.plan
import im.vector.app.features.analytics.itf.VectorAnalyticsEvent
// GENERATED FILE, DO NOT EDIT. FOR MORE INFORMATION VISIT
// https://github.com/matrix-org/matrix-analytics-events/
/**
* The user properties to apply when identifying
*/
data class Identity(
/**
* The selected messaging use case during the onboarding flow.
*/
val ftueUseCaseSelection: FtueUseCaseSelection? = null,
) : VectorAnalyticsEvent {
enum class FtueUseCaseSelection {
/**
* The third option, Communities.
*/
CommunityMessaging,
/**
* The first option, Friends and family.
*/
PersonalMessaging,
/**
* The footer option to skip the question.
*/
Skip,
/**
* The second option, Teams.
*/
WorkMessaging,
}
override fun getName() = "Identity"
override fun getProperties(): Map<String, Any>? {
return mutableMapOf<String, Any>().apply {
ftueUseCaseSelection?.let { put("ftueUseCaseSelection", it.name) }
}.takeIf { it.isNotEmpty() }
}
}

View file

@ -25,14 +25,14 @@ import im.vector.app.features.analytics.itf.VectorAnalyticsEvent
* Triggered when the user joins a room. * Triggered when the user joins a room.
*/ */
data class JoinedRoom( data class JoinedRoom(
/** /**
* Whether the room is a DM. * Whether the room is a DM.
*/ */
val isDM: Boolean, val isDM: Boolean,
/** /**
* The size of the room. * The size of the room.
*/ */
val roomSize: RoomSize, val roomSize: RoomSize,
) : VectorAnalyticsEvent { ) : VectorAnalyticsEvent {
enum class RoomSize { enum class RoomSize {

View file

@ -25,22 +25,23 @@ import im.vector.app.features.analytics.itf.VectorAnalyticsEvent
* Triggered after timing an operation in the app. * Triggered after timing an operation in the app.
*/ */
data class PerformanceTimer( data class PerformanceTimer(
/** /**
* Client defined, can be used for debugging. * Client defined, can be used for debugging.
*/ */
val context: String? = null, val context: String? = null,
/** /**
* Client defined, an optional value to indicate how many items were handled during the operation. * Client defined, an optional value to indicate how many items were
*/ * handled during the operation.
val itemCount: Int? = null, */
/** val itemCount: Int? = null,
* The timer that is being reported. /**
*/ * The timer that is being reported.
val name: Name, */
/** val name: Name,
* The time reported by the timer in milliseconds. /**
*/ * The time reported by the timer in milliseconds.
val timeMs: Int, */
val timeMs: Int,
) : VectorAnalyticsEvent { ) : VectorAnalyticsEvent {
enum class Name { enum class Name {
@ -55,7 +56,8 @@ data class PerformanceTimer(
InitialSyncRequest, InitialSyncRequest,
/** /**
* The time taken to display an event in the timeline that was opened from a notification. * The time taken to display an event in the timeline that was opened
* from a notification.
*/ */
NotificationsOpenEvent, NotificationsOpenEvent,
@ -65,7 +67,8 @@ data class PerformanceTimer(
StartupIncrementalSync, StartupIncrementalSync,
/** /**
* The duration of an initial /sync request during startup (if the store has been wiped). * The duration of an initial /sync request during startup (if the store
* has been wiped).
*/ */
StartupInitialSync, StartupInitialSync,
@ -80,7 +83,8 @@ data class PerformanceTimer(
StartupStorePreload, StartupStorePreload,
/** /**
* The time to load all data from the store (including StartupStorePreload time). * The time to load all data from the store (including
* StartupStorePreload time).
*/ */
StartupStoreReady, StartupStoreReady,
} }

View file

@ -25,28 +25,221 @@ import im.vector.app.features.analytics.itf.VectorAnalyticsScreen
* Triggered when the user changed screen * Triggered when the user changed screen
*/ */
data class Screen( data class Screen(
/** /**
* How long the screen was displayed for in milliseconds. * How long the screen was displayed for in milliseconds.
*/ */
val durationMs: Int? = null, val durationMs: Int? = null,
val screenName: ScreenName, val screenName: ScreenName,
) : VectorAnalyticsScreen { ) : VectorAnalyticsScreen {
enum class ScreenName { enum class ScreenName {
/**
* The screen shown to create a new (non-direct) room.
*/
CreateRoom,
/**
* The confirmation screen shown before deactivating an account.
*/
DeactivateAccount,
/**
* The form for the forgot password use case
*/
ForgotPassword,
/**
* Legacy: The screen that shows information about a specific group.
*/
Group, Group,
/**
* The Home tab on iOS | possibly the same on Android? | The Home space
* on Web?
*/
Home, Home,
/**
* The screen that displays the login flow (when the user already has an
* account).
*/
Login,
/**
* The screen that displays the user's breadcrumbs.
*/
MobileBreadcrumbs,
/**
* The tab on mobile that displays the dialpad.
*/
MobileDialpad,
/**
* The Favourites tab on mobile that lists your favourite people/rooms.
*/
MobileFavourites,
/**
* The screen shown to share a link to download the app.
*/
MobileInviteFriends,
/**
* The People tab on mobile that lists all the DM rooms you have joined.
*/
MobilePeople,
/**
* The Rooms tab on mobile that lists all the (non-direct) rooms you've
* joined.
*/
MobileRooms,
/**
* The Files tab shown in the global search screen on Mobile.
*/
MobileSearchFiles,
/**
* The Messages tab shown in the global search screen on Mobile.
*/
MobileSearchMessages,
/**
* The People tab shown in the global search screen on Mobile.
*/
MobileSearchPeople,
/**
* The Rooms tab shown in the global search screen on Mobile.
*/
MobileSearchRooms,
/**
* The sidebar shown on mobile with spaces, settings etc.
*/
MobileSidebar,
/**
* The screen shown to select which room directory you'd like to use.
*/
MobileSwitchDirectory,
/**
* Legacy: The screen that shows all groups/communities you have joined.
*/
MyGroups, MyGroups,
/**
* The screen that displays the registration flow (when the user wants
* to create an account)
*/
Register,
/**
* The screen that displays the messages and events received in a room.
*/
Room, Room,
/**
* The screen shown when tapping the name of a room from the Room
* screen.
*/
RoomDetails,
/**
* The screen that lists public rooms for you to discover.
*/
RoomDirectory, RoomDirectory,
/**
* The screen that lists all the user's rooms and let them filter the
* rooms.
*/
RoomFilter,
/**
* The screen that displays the list of members that are part of a room.
*/
RoomMembers,
/**
* The notifications settings screen shown from the Room Details screen.
*/
RoomNotifications,
/**
* The screen that allows you to search for messages/files in a specific
* room.
*/
RoomSearch,
/**
* The settings screen shown from the Room Details screen.
*/
RoomSettings,
/**
* The screen that allows you to see all of the files sent in a specific
* room.
*/
RoomUploads,
/**
* The global settings screen shown in the app.
*/
Settings,
/**
* The settings screen to change the default notification options.
*/
SettingsDefaultNotifications,
/**
* The settings screen to manage notification mentions and keywords.
*/
SettingsMentionsAndKeywords,
/**
* The global security settings screen.
*/
SettingsSecurity,
/**
* The screen shown to create a new direct room.
*/
StartChat,
/**
* A screen that shows information about a room member.
*/
User, User,
/**
* ?
*/
WebCompleteSecurity, WebCompleteSecurity,
/**
* ?
*/
WebE2ESetup, WebE2ESetup,
WebForgotPassword,
/**
* ?
*/
WebLoading, WebLoading,
WebLogin,
WebRegister, /**
* ?
*/
WebSoftLogout, WebSoftLogout,
WebWelcome,
/**
* The splash screen.
*/
Welcome,
} }
override fun getName() = screenName.name override fun getName() = screenName.name

View file

@ -0,0 +1,66 @@
/*
* 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.analytics.plan
import im.vector.app.features.analytics.itf.VectorAnalyticsEvent
// GENERATED FILE, DO NOT EDIT. FOR MORE INFORMATION VISIT
// https://github.com/matrix-org/matrix-analytics-events/
/**
* Triggered when the user becomes unauthenticated without actually clicking
* sign out(E.g. Due to expiry of an access token without a way to refresh).
*/
data class UnauthenticatedError(
/**
* The error code as defined in matrix spec. The source of this error is
* from the homeserver.
*/
val errorCode: ErrorCode,
/**
* The reason for the error. The source of this error is from the
* homeserver, the reason can vary and is subject to change so there is
* no enum of possible values.
*/
val errorReason: String,
/**
* Whether the auth mechanism is refresh-token-based.
*/
val refreshTokenAuth: Boolean,
/**
* Whether a soft logout or hard logout was triggered.
*/
val softLogout: Boolean,
) : VectorAnalyticsEvent {
enum class ErrorCode {
M_FORBIDDEN,
M_UNKNOWN,
M_UNKNOWN_TOKEN,
}
override fun getName() = "UnauthenticatedError"
override fun getProperties(): Map<String, Any>? {
return mutableMapOf<String, Any>().apply {
put("errorCode", errorCode.name)
put("errorReason", errorReason)
put("refreshTokenAuth", refreshTokenAuth)
put("softLogout", softLogout)
}.takeIf { it.isNotEmpty() }
}
}

View file

@ -0,0 +1,50 @@
/*
* 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.analytics.screen
import android.os.SystemClock
import im.vector.app.features.analytics.AnalyticsTracker
import im.vector.app.features.analytics.plan.Screen
import timber.log.Timber
/**
* Track a screen display. Unique usage.
*/
class ScreenEvent(val screenName: Screen.ScreenName) {
private val startTime = SystemClock.elapsedRealtime()
// Protection to avoid multiple sending
private var isSent = false
/**
* @param screenNameOverride can be used to override the screen name passed in constructor parameter
*/
fun send(analyticsTracker: AnalyticsTracker,
screenNameOverride: Screen.ScreenName? = null) {
if (isSent) {
Timber.w("Event $screenName Already sent!")
return
}
isSent = true
analyticsTracker.screen(
Screen(
screenName = screenNameOverride ?: screenName,
durationMs = (SystemClock.elapsedRealtime() - startTime).toInt()
)
)
}
}

View file

@ -19,17 +19,15 @@ package im.vector.app.features.attachments.preview
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import com.google.android.material.appbar.MaterialToolbar
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import im.vector.app.core.extensions.addFragment import im.vector.app.core.extensions.addFragment
import im.vector.app.core.platform.ToolbarConfigurable
import im.vector.app.core.platform.VectorBaseActivity import im.vector.app.core.platform.VectorBaseActivity
import im.vector.app.databinding.ActivitySimpleBinding import im.vector.app.databinding.ActivitySimpleBinding
import im.vector.app.features.themes.ActivityOtherThemes import im.vector.app.features.themes.ActivityOtherThemes
import org.matrix.android.sdk.api.session.content.ContentAttachmentData import org.matrix.android.sdk.api.session.content.ContentAttachmentData
@AndroidEntryPoint @AndroidEntryPoint
class AttachmentsPreviewActivity : VectorBaseActivity<ActivitySimpleBinding>(), ToolbarConfigurable { class AttachmentsPreviewActivity : VectorBaseActivity<ActivitySimpleBinding>() {
companion object { companion object {
private const val EXTRA_FRAGMENT_ARGS = "EXTRA_FRAGMENT_ARGS" private const val EXTRA_FRAGMENT_ARGS = "EXTRA_FRAGMENT_ARGS"
@ -72,8 +70,4 @@ class AttachmentsPreviewActivity : VectorBaseActivity<ActivitySimpleBinding>(),
setResult(RESULT_OK, resultIntent) setResult(RESULT_OK, resultIntent)
finish() finish()
} }
override fun configure(toolbar: MaterialToolbar) {
configureToolbar(toolbar)
}
} }

View file

@ -126,7 +126,7 @@ class VectorCallActivity : VectorBaseActivity<ActivityCallBinding>(), CallContro
if (savedInstanceState != null) { if (savedInstanceState != null) {
(supportFragmentManager.findFragmentByTag(FRAGMENT_DIAL_PAD_TAG) as? CallDialPadBottomSheet)?.callback = dialPadCallback (supportFragmentManager.findFragmentByTag(FRAGMENT_DIAL_PAD_TAG) as? CallDialPadBottomSheet)?.callback = dialPadCallback
} }
setSupportActionBar(views.callToolbar) setupToolbar(views.callToolbar)
configureCallViews() configureCallViews()
callViewModel.onEach { callViewModel.onEach {
@ -257,18 +257,18 @@ class VectorCallActivity : VectorBaseActivity<ActivityCallBinding>(), CallContro
views.fullscreenRenderer.isVisible = false views.fullscreenRenderer.isVisible = false
views.pipRendererWrapper.isVisible = false views.pipRendererWrapper.isVisible = false
views.callInfoGroup.isVisible = true views.callInfoGroup.isVisible = true
views.callToolbar.setSubtitle(R.string.call_ringing) toolbar?.setSubtitle(R.string.call_ringing)
configureCallInfo(state) configureCallInfo(state)
} }
is CallState.Answering -> { is CallState.Answering -> {
views.fullscreenRenderer.isVisible = false views.fullscreenRenderer.isVisible = false
views.pipRendererWrapper.isVisible = false views.pipRendererWrapper.isVisible = false
views.callInfoGroup.isVisible = true views.callInfoGroup.isVisible = true
views.callToolbar.setSubtitle(R.string.call_connecting) toolbar?.setSubtitle(R.string.call_connecting)
configureCallInfo(state) configureCallInfo(state)
} }
is CallState.Connected -> { is CallState.Connected -> {
views.callToolbar.subtitle = state.formattedDuration toolbar?.subtitle = state.formattedDuration
if (callState.iceConnectionState == MxPeerConnectionState.CONNECTED) { if (callState.iceConnectionState == MxPeerConnectionState.CONNECTED) {
if (state.isLocalOnHold || state.isRemoteOnHold) { if (state.isLocalOnHold || state.isRemoteOnHold) {
views.smallIsHeldIcon.isVisible = true views.smallIsHeldIcon.isVisible = true
@ -280,11 +280,11 @@ class VectorCallActivity : VectorBaseActivity<ActivityCallBinding>(), CallContro
views.callActionText.setText(R.string.call_resume_action) views.callActionText.setText(R.string.call_resume_action)
views.callActionText.isVisible = true views.callActionText.isVisible = true
views.callActionText.setOnClickListener { callViewModel.handle(VectorCallViewActions.ToggleHoldResume) } views.callActionText.setOnClickListener { callViewModel.handle(VectorCallViewActions.ToggleHoldResume) }
views.callToolbar.setSubtitle(R.string.call_held_by_you) toolbar?.setSubtitle(R.string.call_held_by_you)
} else { } else {
views.callActionText.isInvisible = true views.callActionText.isInvisible = true
state.callInfo?.opponentUserItem?.let { state.callInfo?.opponentUserItem?.let {
views.callToolbar.subtitle = getString(R.string.call_held_by_user, it.getBestName()) toolbar?.subtitle = getString(R.string.call_held_by_user, it.getBestName())
} }
} }
} else if (state.transferee !is VectorCallViewState.TransfereeState.NoTransferee) { } else if (state.transferee !is VectorCallViewState.TransfereeState.NoTransferee) {
@ -316,14 +316,14 @@ class VectorCallActivity : VectorBaseActivity<ActivityCallBinding>(), CallContro
views.pipRendererWrapper.isVisible = false views.pipRendererWrapper.isVisible = false
views.callInfoGroup.isVisible = true views.callInfoGroup.isVisible = true
configureCallInfo(state) configureCallInfo(state)
views.callToolbar.setSubtitle(R.string.call_connecting) toolbar?.setSubtitle(R.string.call_connecting)
} }
} }
is CallState.Ended -> { is CallState.Ended -> {
views.fullscreenRenderer.isVisible = false views.fullscreenRenderer.isVisible = false
views.pipRendererWrapper.isVisible = false views.pipRendererWrapper.isVisible = false
views.callInfoGroup.isVisible = true views.callInfoGroup.isVisible = true
views.callToolbar.setSubtitle(R.string.call_ended) toolbar?.setSubtitle(R.string.call_ended)
configureCallInfo(state) configureCallInfo(state)
} }
else -> { else -> {
@ -410,7 +410,7 @@ class VectorCallActivity : VectorBaseActivity<ActivityCallBinding>(), CallContro
avatarRenderer.renderBlur(it, views.bgCallView, sampling = 20, rounded = false, colorFilter = colorFilter, addPlaceholder = false) avatarRenderer.renderBlur(it, views.bgCallView, sampling = 20, rounded = false, colorFilter = colorFilter, addPlaceholder = false)
if (state.transferee is VectorCallViewState.TransfereeState.NoTransferee) { if (state.transferee is VectorCallViewState.TransfereeState.NoTransferee) {
views.participantNameText.setTextOrHide(null) views.participantNameText.setTextOrHide(null)
views.callToolbar.title = if (state.isVideoCall) { toolbar?.title = if (state.isVideoCall) {
getString(R.string.video_call_with_participant, it.getBestName()) getString(R.string.video_call_with_participant, it.getBestName())
} else { } else {
getString(R.string.audio_call_with_participant, it.getBestName()) getString(R.string.audio_call_with_participant, it.getBestName())

View file

@ -17,6 +17,7 @@
package im.vector.app.features.call.dialpad package im.vector.app.features.call.dialpad
import android.content.ClipboardManager import android.content.ClipboardManager
import android.content.Context
import android.content.res.ColorStateList import android.content.res.ColorStateList
import android.os.Bundle import android.os.Bundle
import android.telephony.PhoneNumberFormattingTextWatcher import android.telephony.PhoneNumberFormattingTextWatcher
@ -37,6 +38,10 @@ import androidx.fragment.app.Fragment
import com.android.dialer.dialpadview.DialpadView import com.android.dialer.dialpadview.DialpadView
import com.android.dialer.dialpadview.DigitsEditText import com.android.dialer.dialpadview.DigitsEditText
import im.vector.app.R import im.vector.app.R
import im.vector.app.core.extensions.singletonEntryPoint
import im.vector.app.features.analytics.AnalyticsTracker
import im.vector.app.features.analytics.plan.Screen
import im.vector.app.features.analytics.screen.ScreenEvent
import im.vector.app.features.themes.ThemeUtils import im.vector.app.features.themes.ThemeUtils
class DialPadFragment : Fragment(), TextWatcher { class DialPadFragment : Fragment(), TextWatcher {
@ -53,6 +58,25 @@ class DialPadFragment : Fragment(), TextWatcher {
private var enableDelete = true private var enableDelete = true
private var enableFabOk = true private var enableFabOk = true
private lateinit var analyticsTracker: AnalyticsTracker
override fun onAttach(context: Context) {
super.onAttach(context)
val singletonEntryPoint = context.singletonEntryPoint()
analyticsTracker = singletonEntryPoint.analyticsTracker()
}
private var screenEvent: ScreenEvent? = null
override fun onResume() {
super.onResume()
screenEvent = ScreenEvent(Screen.ScreenName.MobileDialpad)
}
override fun onPause() {
super.onPause()
screenEvent?.send(analyticsTracker)
}
override fun onCreateView( override fun onCreateView(
inflater: LayoutInflater, inflater: LayoutInflater,
container: ViewGroup?, container: ViewGroup?,

View file

@ -70,7 +70,8 @@ class CallTransferActivity : VectorBaseActivity<ActivityCallTransferBinding>() {
CallTransferPagerAdapter.DIAL_PAD_INDEX -> tab.text = getString(R.string.call_dial_pad_title) CallTransferPagerAdapter.DIAL_PAD_INDEX -> tab.text = getString(R.string.call_dial_pad_title)
} }
}.attach() }.attach()
configureToolbar(views.callTransferToolbar) setupToolbar(views.callTransferToolbar)
.allowBack()
views.callTransferToolbar.title = getString(R.string.call_transfer_title) views.callTransferToolbar.title = getString(R.string.call_transfer_title)
setupConnectAction() setupConnectAction()
} }

View file

@ -270,6 +270,10 @@ class WebRtcCall(
} }
} }
fun durationMillis(): Int {
return timer.elapsedTime().toInt()
}
fun formattedDuration(): String { fun formattedDuration(): String {
return formatDuration( return formatDuration(
Duration.ofMillis(timer.elapsedTime()) Duration.ofMillis(timer.elapsedTime())

View file

@ -22,6 +22,9 @@ import androidx.lifecycle.LifecycleOwner
import im.vector.app.ActiveSessionDataSource import im.vector.app.ActiveSessionDataSource
import im.vector.app.BuildConfig import im.vector.app.BuildConfig
import im.vector.app.core.services.CallService import im.vector.app.core.services.CallService
import im.vector.app.features.analytics.AnalyticsTracker
import im.vector.app.features.analytics.plan.CallEnded
import im.vector.app.features.analytics.plan.CallStarted
import im.vector.app.features.call.VectorCallActivity import im.vector.app.features.call.VectorCallActivity
import im.vector.app.features.call.audio.CallAudioManager import im.vector.app.features.call.audio.CallAudioManager
import im.vector.app.features.call.lookup.CallProtocolsChecker import im.vector.app.features.call.lookup.CallProtocolsChecker
@ -68,7 +71,8 @@ private val loggerTag = LoggerTag("WebRtcCallManager", LoggerTag.VOIP)
@Singleton @Singleton
class WebRtcCallManager @Inject constructor( class WebRtcCallManager @Inject constructor(
private val context: Context, private val context: Context,
private val activeSessionDataSource: ActiveSessionDataSource private val activeSessionDataSource: ActiveSessionDataSource,
private val analyticsTracker: AnalyticsTracker
) : CallListener, ) : CallListener,
DefaultLifecycleObserver { DefaultLifecycleObserver {
@ -237,6 +241,7 @@ class WebRtcCallManager @Inject constructor(
val currentCall = getCurrentCall().takeIf { it != call } val currentCall = getCurrentCall().takeIf { it != call }
currentCall?.updateRemoteOnHold(onHold = true) currentCall?.updateRemoteOnHold(onHold = true)
audioManager.setMode(if (call.mxCall.isVideoCall) CallAudioManager.Mode.VIDEO_CALL else CallAudioManager.Mode.AUDIO_CALL) audioManager.setMode(if (call.mxCall.isVideoCall) CallAudioManager.Mode.VIDEO_CALL else CallAudioManager.Mode.AUDIO_CALL)
call.trackCallStarted()
this.currentCall.setAndNotify(call) this.currentCall.setAndNotify(call)
} }
@ -245,6 +250,7 @@ class WebRtcCallManager @Inject constructor(
val webRtcCall = callsByCallId.remove(callId) ?: return Unit.also { val webRtcCall = callsByCallId.remove(callId) ?: return Unit.also {
Timber.tag(loggerTag.value).v("On call ended for unknown call $callId") Timber.tag(loggerTag.value).v("On call ended for unknown call $callId")
} }
webRtcCall.trackCallEnded()
CallService.onCallTerminated(context, callId, endCallReason, rejected) CallService.onCallTerminated(context, callId, endCallReason, rejected)
callsByRoomId[webRtcCall.signalingRoomId]?.remove(webRtcCall) callsByRoomId[webRtcCall.signalingRoomId]?.remove(webRtcCall)
callsByRoomId[webRtcCall.nativeRoomId]?.remove(webRtcCall) callsByRoomId[webRtcCall.nativeRoomId]?.remove(webRtcCall)
@ -443,4 +449,28 @@ class WebRtcCallManager @Inject constructor(
} }
call.onCallAssertedIdentityReceived(callAssertedIdentityContent) call.onCallAssertedIdentityReceived(callAssertedIdentityContent)
} }
/**
* Analytics
*/
private fun WebRtcCall.trackCallStarted() {
analyticsTracker.capture(
CallStarted(
isVideo = mxCall.isVideoCall,
numParticipants = 2,
placed = mxCall.isOutgoing
)
)
}
private fun WebRtcCall.trackCallEnded() {
analyticsTracker.capture(
CallEnded(
durationMs = durationMillis(),
isVideo = mxCall.isVideoCall,
numParticipants = 2,
placed = mxCall.isOutgoing
)
)
}
} }

View file

@ -23,8 +23,9 @@ import org.matrix.android.sdk.api.MatrixPatterns
import org.matrix.android.sdk.api.MatrixUrls.isMxcUrl import org.matrix.android.sdk.api.MatrixUrls.isMxcUrl
import org.matrix.android.sdk.api.session.identity.ThreePid import org.matrix.android.sdk.api.session.identity.ThreePid
import timber.log.Timber import timber.log.Timber
import javax.inject.Inject
object CommandParser { class CommandParser @Inject constructor() {
/** /**
* Convert the text message into a Slash command. * Convert the text message into a Slash command.
@ -34,11 +35,9 @@ object CommandParser {
*/ */
fun parseSlashCommand(textMessage: CharSequence): ParsedCommand { fun parseSlashCommand(textMessage: CharSequence): ParsedCommand {
// check if it has the Slash marker // check if it has the Slash marker
if (!textMessage.startsWith("/")) { return if (!textMessage.startsWith("/")) {
return ParsedCommand.ErrorNotACommand ParsedCommand.ErrorNotACommand
} else { } else {
Timber.v("parseSlashCommand")
// "/" only // "/" only
if (textMessage.length == 1) { if (textMessage.length == 1) {
return ParsedCommand.ErrorEmptySlashCommand return ParsedCommand.ErrorEmptySlashCommand
@ -52,7 +51,7 @@ object CommandParser {
val messageParts = try { val messageParts = try {
textMessage.split("\\s+".toRegex()).dropLastWhile { it.isEmpty() } textMessage.split("\\s+".toRegex()).dropLastWhile { it.isEmpty() }
} catch (e: Exception) { } catch (e: Exception) {
Timber.e(e, "## manageSlashCommand() : split failed") Timber.e(e, "## parseSlashCommand() : split failed")
null null
} }
@ -64,7 +63,7 @@ object CommandParser {
val slashCommand = messageParts.first() val slashCommand = messageParts.first()
val message = textMessage.substring(slashCommand.length).trim() val message = textMessage.substring(slashCommand.length).trim()
return when { when {
Command.PLAIN.matches(slashCommand) -> { Command.PLAIN.matches(slashCommand) -> {
if (message.isNotEmpty()) { if (message.isNotEmpty()) {
ParsedCommand.SendPlainText(message = message) ParsedCommand.SendPlainText(message = message)

View file

@ -22,51 +22,51 @@ import org.matrix.android.sdk.api.session.identity.ThreePid
/** /**
* Represent a parsed command * Represent a parsed command
*/ */
sealed class ParsedCommand { sealed interface ParsedCommand {
// This is not a Slash command // This is not a Slash command
object ErrorNotACommand : ParsedCommand() object ErrorNotACommand : ParsedCommand
object ErrorEmptySlashCommand : ParsedCommand() object ErrorEmptySlashCommand : ParsedCommand
// Unknown/Unsupported slash command // Unknown/Unsupported slash command
class ErrorUnknownSlashCommand(val slashCommand: String) : ParsedCommand() data class ErrorUnknownSlashCommand(val slashCommand: String) : ParsedCommand
// A slash command is detected, but there is an error // A slash command is detected, but there is an error
class ErrorSyntax(val command: Command) : ParsedCommand() data class ErrorSyntax(val command: Command) : ParsedCommand
// Valid commands: // Valid commands:
class SendPlainText(val message: CharSequence) : ParsedCommand() data class SendPlainText(val message: CharSequence) : ParsedCommand
class SendEmote(val message: CharSequence) : ParsedCommand() data class SendEmote(val message: CharSequence) : ParsedCommand
class SendRainbow(val message: CharSequence) : ParsedCommand() data class SendRainbow(val message: CharSequence) : ParsedCommand
class SendRainbowEmote(val message: CharSequence) : ParsedCommand() data class SendRainbowEmote(val message: CharSequence) : ParsedCommand
class BanUser(val userId: String, val reason: String?) : ParsedCommand() data class BanUser(val userId: String, val reason: String?) : ParsedCommand
class UnbanUser(val userId: String, val reason: String?) : ParsedCommand() data class UnbanUser(val userId: String, val reason: String?) : ParsedCommand
class IgnoreUser(val userId: String) : ParsedCommand() data class IgnoreUser(val userId: String) : ParsedCommand
class UnignoreUser(val userId: String) : ParsedCommand() data class UnignoreUser(val userId: String) : ParsedCommand
class SetUserPowerLevel(val userId: String, val powerLevel: Int?) : ParsedCommand() data class SetUserPowerLevel(val userId: String, val powerLevel: Int?) : ParsedCommand
class ChangeRoomName(val name: String) : ParsedCommand() data class ChangeRoomName(val name: String) : ParsedCommand
class Invite(val userId: String, val reason: String?) : ParsedCommand() data class Invite(val userId: String, val reason: String?) : ParsedCommand
class Invite3Pid(val threePid: ThreePid) : ParsedCommand() data class Invite3Pid(val threePid: ThreePid) : ParsedCommand
class JoinRoom(val roomAlias: String, val reason: String?) : ParsedCommand() data class JoinRoom(val roomAlias: String, val reason: String?) : ParsedCommand
class PartRoom(val roomAlias: String?) : ParsedCommand() data class PartRoom(val roomAlias: String?) : ParsedCommand
class ChangeTopic(val topic: String) : ParsedCommand() data class ChangeTopic(val topic: String) : ParsedCommand
class RemoveUser(val userId: String, val reason: String?) : ParsedCommand() data class RemoveUser(val userId: String, val reason: String?) : ParsedCommand
class ChangeDisplayName(val displayName: String) : ParsedCommand() data class ChangeDisplayName(val displayName: String) : ParsedCommand
class ChangeDisplayNameForRoom(val displayName: String) : ParsedCommand() data class ChangeDisplayNameForRoom(val displayName: String) : ParsedCommand
class ChangeRoomAvatar(val url: String) : ParsedCommand() data class ChangeRoomAvatar(val url: String) : ParsedCommand
class ChangeAvatarForRoom(val url: String) : ParsedCommand() data class ChangeAvatarForRoom(val url: String) : ParsedCommand
class SetMarkdown(val enable: Boolean) : ParsedCommand() data class SetMarkdown(val enable: Boolean) : ParsedCommand
object ClearScalarToken : ParsedCommand() object ClearScalarToken : ParsedCommand
class SendSpoiler(val message: String) : ParsedCommand() data class SendSpoiler(val message: String) : ParsedCommand
class SendShrug(val message: CharSequence) : ParsedCommand() data class SendShrug(val message: CharSequence) : ParsedCommand
class SendLenny(val message: CharSequence) : ParsedCommand() data class SendLenny(val message: CharSequence) : ParsedCommand
object DiscardSession : ParsedCommand() object DiscardSession : ParsedCommand
class ShowUser(val userId: String) : ParsedCommand() data class ShowUser(val userId: String) : ParsedCommand
class SendChatEffect(val chatEffect: ChatEffect, val message: String) : ParsedCommand() data class SendChatEffect(val chatEffect: ChatEffect, val message: String) : ParsedCommand
class CreateSpace(val name: String, val invitees: List<String>) : ParsedCommand() data class CreateSpace(val name: String, val invitees: List<String>) : ParsedCommand
class AddToSpace(val spaceId: String) : ParsedCommand() data class AddToSpace(val spaceId: String) : ParsedCommand
class JoinSpace(val spaceIdOrAlias: String) : ParsedCommand() data class JoinSpace(val spaceIdOrAlias: String) : ParsedCommand
class LeaveRoom(val roomId: String) : ParsedCommand() data class LeaveRoom(val roomId: String) : ParsedCommand
class UpgradeRoom(val newVersion: String) : ParsedCommand() data class UpgradeRoom(val newVersion: String) : ParsedCommand
} }

View file

@ -67,7 +67,8 @@ class ContactsBookFragment @Inject constructor(
setupFilterView() setupFilterView()
setupConsentView() setupConsentView()
setupOnlyBoundContactsView() setupOnlyBoundContactsView()
setupCloseView() setupToolbar(views.phoneBookToolbar)
.allowBack(useCross = true)
contactsBookViewModel.observeViewEvents { contactsBookViewModel.observeViewEvents {
when (it) { when (it) {
is ContactsBookViewEvents.Failure -> showFailure(it.throwable) is ContactsBookViewEvents.Failure -> showFailure(it.throwable)
@ -119,12 +120,6 @@ class ContactsBookFragment @Inject constructor(
views.phoneBookRecyclerView.configureWith(contactsBookController) views.phoneBookRecyclerView.configureWith(contactsBookController)
} }
private fun setupCloseView() {
views.phoneBookClose.debouncedClicks {
sharedActionViewModel.post(UserListSharedAction.GoBack)
}
}
override fun invalidate() = withState(contactsBookViewModel) { state -> override fun invalidate() = withState(contactsBookViewModel) { state ->
views.phoneBookSearchForMatrixContacts.isVisible = state.filteredMappedContacts.isNotEmpty() && state.identityServerUrl != null && !state.userConsent views.phoneBookSearchForMatrixContacts.isVisible = state.filteredMappedContacts.isNotEmpty() && state.identityServerUrl != null && !state.userConsent
views.phoneBookOnlyBoundContacts.isVisible = state.isBoundRetrieved views.phoneBookOnlyBoundContacts.isVisible = state.isBoundRetrieved

View file

@ -42,6 +42,7 @@ import im.vector.app.core.utils.PERMISSIONS_FOR_TAKING_PHOTO
import im.vector.app.core.utils.checkPermissions import im.vector.app.core.utils.checkPermissions
import im.vector.app.core.utils.onPermissionDeniedSnackbar import im.vector.app.core.utils.onPermissionDeniedSnackbar
import im.vector.app.core.utils.registerForPermissionsResult import im.vector.app.core.utils.registerForPermissionsResult
import im.vector.app.features.analytics.plan.Screen
import im.vector.app.features.contactsbook.ContactsBookFragment import im.vector.app.features.contactsbook.ContactsBookFragment
import im.vector.app.features.userdirectory.UserListFragment import im.vector.app.features.userdirectory.UserListFragment
import im.vector.app.features.userdirectory.UserListFragmentArgs import im.vector.app.features.userdirectory.UserListFragmentArgs
@ -63,6 +64,7 @@ class CreateDirectRoomActivity : SimpleFragmentActivity() {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
analyticsScreenName = Screen.ScreenName.StartChat
views.toolbar.visibility = View.GONE views.toolbar.visibility = View.GONE
sharedActionViewModel = viewModelProvider.get(UserListSharedActionViewModel::class.java) sharedActionViewModel = viewModelProvider.get(UserListSharedActionViewModel::class.java)

View file

@ -64,11 +64,8 @@ class CreateDirectRoomByQrCodeFragment @Inject constructor() : VectorBaseFragmen
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
setupToolbar(views.qrScannerToolbar) setupToolbar(views.qrScannerToolbar)
.setTitle(R.string.add_by_qr_code)
views.qrScannerClose.debouncedClicks { .allowBack(useCross = true)
requireActivity().onBackPressed()
}
views.qrScannerTitle.text = getString(R.string.add_by_qr_code)
} }
override fun onResume() { override fun onResume() {

View file

@ -22,13 +22,13 @@ import com.airbnb.mvrx.Loading
import com.airbnb.mvrx.Success import com.airbnb.mvrx.Success
import com.airbnb.mvrx.Uninitialized import com.airbnb.mvrx.Uninitialized
import im.vector.app.R import im.vector.app.R
import im.vector.app.core.epoxy.charsequence.toEpoxyCharSequence
import im.vector.app.core.epoxy.errorWithRetryItem import im.vector.app.core.epoxy.errorWithRetryItem
import im.vector.app.core.epoxy.loadingItem import im.vector.app.core.epoxy.loadingItem
import im.vector.app.core.resources.StringProvider import im.vector.app.core.resources.StringProvider
import im.vector.app.core.ui.list.ItemStyle import im.vector.app.core.ui.list.ItemStyle
import im.vector.app.core.ui.list.genericItem import im.vector.app.core.ui.list.genericItem
import im.vector.app.features.settings.VectorPreferences import im.vector.app.features.settings.VectorPreferences
import im.vector.lib.core.utils.epoxy.charsequence.toEpoxyCharSequence
import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.Session
import org.matrix.android.sdk.api.session.crypto.keysbackup.KeysBackupState import org.matrix.android.sdk.api.session.crypto.keysbackup.KeysBackupState
import org.matrix.android.sdk.internal.crypto.keysbackup.model.KeysBackupVersionTrust import org.matrix.android.sdk.internal.crypto.keysbackup.model.KeysBackupVersionTrust

View file

@ -20,14 +20,14 @@ import androidx.core.text.toSpannable
import com.airbnb.epoxy.EpoxyController import com.airbnb.epoxy.EpoxyController
import im.vector.app.R import im.vector.app.R
import im.vector.app.core.epoxy.bottomSheetDividerItem import im.vector.app.core.epoxy.bottomSheetDividerItem
import im.vector.app.core.epoxy.charsequence.EpoxyCharSequence
import im.vector.app.core.epoxy.charsequence.toEpoxyCharSequence
import im.vector.app.core.resources.ColorProvider import im.vector.app.core.resources.ColorProvider
import im.vector.app.core.resources.StringProvider import im.vector.app.core.resources.StringProvider
import im.vector.app.core.utils.colorizeMatchingText import im.vector.app.core.utils.colorizeMatchingText
import im.vector.app.features.crypto.verification.VerificationBottomSheetViewState import im.vector.app.features.crypto.verification.VerificationBottomSheetViewState
import im.vector.app.features.crypto.verification.epoxy.bottomSheetVerificationActionItem import im.vector.app.features.crypto.verification.epoxy.bottomSheetVerificationActionItem
import im.vector.app.features.crypto.verification.epoxy.bottomSheetVerificationNoticeItem import im.vector.app.features.crypto.verification.epoxy.bottomSheetVerificationNoticeItem
import im.vector.lib.core.utils.epoxy.charsequence.EpoxyCharSequence
import im.vector.lib.core.utils.epoxy.charsequence.toEpoxyCharSequence
import javax.inject.Inject import javax.inject.Inject
class VerificationCancelController @Inject constructor( class VerificationCancelController @Inject constructor(

View file

@ -19,13 +19,13 @@ package im.vector.app.features.crypto.verification.cancel
import com.airbnb.epoxy.EpoxyController import com.airbnb.epoxy.EpoxyController
import im.vector.app.R import im.vector.app.R
import im.vector.app.core.epoxy.bottomSheetDividerItem import im.vector.app.core.epoxy.bottomSheetDividerItem
import im.vector.app.core.epoxy.charsequence.toEpoxyCharSequence
import im.vector.app.core.resources.ColorProvider import im.vector.app.core.resources.ColorProvider
import im.vector.app.core.resources.StringProvider import im.vector.app.core.resources.StringProvider
import im.vector.app.features.crypto.verification.VerificationBottomSheetViewState import im.vector.app.features.crypto.verification.VerificationBottomSheetViewState
import im.vector.app.features.crypto.verification.epoxy.bottomSheetVerificationActionItem import im.vector.app.features.crypto.verification.epoxy.bottomSheetVerificationActionItem
import im.vector.app.features.crypto.verification.epoxy.bottomSheetVerificationNoticeItem import im.vector.app.features.crypto.verification.epoxy.bottomSheetVerificationNoticeItem
import im.vector.app.features.html.EventHtmlRenderer import im.vector.app.features.html.EventHtmlRenderer
import im.vector.lib.core.utils.epoxy.charsequence.toEpoxyCharSequence
import javax.inject.Inject import javax.inject.Inject
class VerificationNotMeController @Inject constructor( class VerificationNotMeController @Inject constructor(

View file

@ -19,12 +19,12 @@ package im.vector.app.features.crypto.verification.choose
import com.airbnb.epoxy.EpoxyController import com.airbnb.epoxy.EpoxyController
import im.vector.app.R import im.vector.app.R
import im.vector.app.core.epoxy.bottomSheetDividerItem import im.vector.app.core.epoxy.bottomSheetDividerItem
import im.vector.app.core.epoxy.charsequence.toEpoxyCharSequence
import im.vector.app.core.resources.ColorProvider import im.vector.app.core.resources.ColorProvider
import im.vector.app.core.resources.StringProvider import im.vector.app.core.resources.StringProvider
import im.vector.app.features.crypto.verification.epoxy.bottomSheetVerificationActionItem import im.vector.app.features.crypto.verification.epoxy.bottomSheetVerificationActionItem
import im.vector.app.features.crypto.verification.epoxy.bottomSheetVerificationNoticeItem import im.vector.app.features.crypto.verification.epoxy.bottomSheetVerificationNoticeItem
import im.vector.app.features.crypto.verification.epoxy.bottomSheetVerificationQrCodeItem import im.vector.app.features.crypto.verification.epoxy.bottomSheetVerificationQrCodeItem
import im.vector.lib.core.utils.epoxy.charsequence.toEpoxyCharSequence
import javax.inject.Inject import javax.inject.Inject
class VerificationChooseMethodController @Inject constructor( class VerificationChooseMethodController @Inject constructor(

View file

@ -19,13 +19,13 @@ package im.vector.app.features.crypto.verification.conclusion
import com.airbnb.epoxy.EpoxyController import com.airbnb.epoxy.EpoxyController
import im.vector.app.R import im.vector.app.R
import im.vector.app.core.epoxy.bottomSheetDividerItem import im.vector.app.core.epoxy.bottomSheetDividerItem
import im.vector.app.core.epoxy.charsequence.toEpoxyCharSequence
import im.vector.app.core.resources.ColorProvider import im.vector.app.core.resources.ColorProvider
import im.vector.app.core.resources.StringProvider import im.vector.app.core.resources.StringProvider
import im.vector.app.features.crypto.verification.epoxy.bottomSheetVerificationActionItem import im.vector.app.features.crypto.verification.epoxy.bottomSheetVerificationActionItem
import im.vector.app.features.crypto.verification.epoxy.bottomSheetVerificationBigImageItem import im.vector.app.features.crypto.verification.epoxy.bottomSheetVerificationBigImageItem
import im.vector.app.features.crypto.verification.epoxy.bottomSheetVerificationNoticeItem import im.vector.app.features.crypto.verification.epoxy.bottomSheetVerificationNoticeItem
import im.vector.app.features.html.EventHtmlRenderer import im.vector.app.features.html.EventHtmlRenderer
import im.vector.lib.core.utils.epoxy.charsequence.toEpoxyCharSequence
import org.matrix.android.sdk.api.crypto.RoomEncryptionTrustLevel import org.matrix.android.sdk.api.crypto.RoomEncryptionTrustLevel
import javax.inject.Inject import javax.inject.Inject

View file

@ -21,7 +21,6 @@ import com.airbnb.mvrx.Fail
import com.airbnb.mvrx.Success import com.airbnb.mvrx.Success
import im.vector.app.R import im.vector.app.R
import im.vector.app.core.epoxy.bottomSheetDividerItem import im.vector.app.core.epoxy.bottomSheetDividerItem
import im.vector.app.core.epoxy.charsequence.toEpoxyCharSequence
import im.vector.app.core.epoxy.errorWithRetryItem import im.vector.app.core.epoxy.errorWithRetryItem
import im.vector.app.core.error.ErrorFormatter import im.vector.app.core.error.ErrorFormatter
import im.vector.app.core.resources.ColorProvider import im.vector.app.core.resources.ColorProvider
@ -32,6 +31,7 @@ import im.vector.app.features.crypto.verification.epoxy.bottomSheetVerificationE
import im.vector.app.features.crypto.verification.epoxy.bottomSheetVerificationNoticeItem import im.vector.app.features.crypto.verification.epoxy.bottomSheetVerificationNoticeItem
import im.vector.app.features.crypto.verification.epoxy.bottomSheetVerificationWaitingItem import im.vector.app.features.crypto.verification.epoxy.bottomSheetVerificationWaitingItem
import im.vector.app.features.displayname.getBestName import im.vector.app.features.displayname.getBestName
import im.vector.lib.core.utils.epoxy.charsequence.toEpoxyCharSequence
import javax.inject.Inject import javax.inject.Inject
class VerificationEmojiCodeController @Inject constructor( class VerificationEmojiCodeController @Inject constructor(

View file

@ -22,7 +22,7 @@ import com.airbnb.epoxy.EpoxyModelClass
import im.vector.app.R import im.vector.app.R
import im.vector.app.core.epoxy.VectorEpoxyHolder import im.vector.app.core.epoxy.VectorEpoxyHolder
import im.vector.app.core.epoxy.VectorEpoxyModel import im.vector.app.core.epoxy.VectorEpoxyModel
import im.vector.app.core.epoxy.charsequence.EpoxyCharSequence import im.vector.lib.core.utils.epoxy.charsequence.EpoxyCharSequence
/** /**
* A action for bottom sheet. * A action for bottom sheet.

View file

@ -18,12 +18,12 @@ package im.vector.app.features.crypto.verification.qrconfirmation
import com.airbnb.epoxy.EpoxyController import com.airbnb.epoxy.EpoxyController
import im.vector.app.R import im.vector.app.R
import im.vector.app.core.epoxy.charsequence.toEpoxyCharSequence
import im.vector.app.core.resources.ColorProvider import im.vector.app.core.resources.ColorProvider
import im.vector.app.core.resources.StringProvider import im.vector.app.core.resources.StringProvider
import im.vector.app.features.crypto.verification.epoxy.bottomSheetVerificationBigImageItem import im.vector.app.features.crypto.verification.epoxy.bottomSheetVerificationBigImageItem
import im.vector.app.features.crypto.verification.epoxy.bottomSheetVerificationNoticeItem import im.vector.app.features.crypto.verification.epoxy.bottomSheetVerificationNoticeItem
import im.vector.app.features.crypto.verification.epoxy.bottomSheetVerificationWaitingItem import im.vector.app.features.crypto.verification.epoxy.bottomSheetVerificationWaitingItem
import im.vector.lib.core.utils.epoxy.charsequence.toEpoxyCharSequence
import org.matrix.android.sdk.api.crypto.RoomEncryptionTrustLevel import org.matrix.android.sdk.api.crypto.RoomEncryptionTrustLevel
import javax.inject.Inject import javax.inject.Inject

View file

@ -19,7 +19,6 @@ package im.vector.app.features.crypto.verification.qrconfirmation
import com.airbnb.epoxy.EpoxyController import com.airbnb.epoxy.EpoxyController
import im.vector.app.R import im.vector.app.R
import im.vector.app.core.epoxy.bottomSheetDividerItem import im.vector.app.core.epoxy.bottomSheetDividerItem
import im.vector.app.core.epoxy.charsequence.toEpoxyCharSequence
import im.vector.app.core.resources.ColorProvider import im.vector.app.core.resources.ColorProvider
import im.vector.app.core.resources.StringProvider import im.vector.app.core.resources.StringProvider
import im.vector.app.features.crypto.verification.VerificationBottomSheetViewState import im.vector.app.features.crypto.verification.VerificationBottomSheetViewState
@ -27,6 +26,7 @@ import im.vector.app.features.crypto.verification.epoxy.bottomSheetVerificationA
import im.vector.app.features.crypto.verification.epoxy.bottomSheetVerificationBigImageItem import im.vector.app.features.crypto.verification.epoxy.bottomSheetVerificationBigImageItem
import im.vector.app.features.crypto.verification.epoxy.bottomSheetVerificationNoticeItem import im.vector.app.features.crypto.verification.epoxy.bottomSheetVerificationNoticeItem
import im.vector.app.features.displayname.getBestName import im.vector.app.features.displayname.getBestName
import im.vector.lib.core.utils.epoxy.charsequence.toEpoxyCharSequence
import org.matrix.android.sdk.api.crypto.RoomEncryptionTrustLevel import org.matrix.android.sdk.api.crypto.RoomEncryptionTrustLevel
import javax.inject.Inject import javax.inject.Inject

View file

@ -23,7 +23,6 @@ import com.airbnb.mvrx.Success
import com.airbnb.mvrx.Uninitialized import com.airbnb.mvrx.Uninitialized
import im.vector.app.R import im.vector.app.R
import im.vector.app.core.epoxy.bottomSheetDividerItem import im.vector.app.core.epoxy.bottomSheetDividerItem
import im.vector.app.core.epoxy.charsequence.toEpoxyCharSequence
import im.vector.app.core.resources.ColorProvider import im.vector.app.core.resources.ColorProvider
import im.vector.app.core.resources.StringProvider import im.vector.app.core.resources.StringProvider
import im.vector.app.core.utils.colorizeMatchingText import im.vector.app.core.utils.colorizeMatchingText
@ -33,6 +32,7 @@ import im.vector.app.features.crypto.verification.epoxy.bottomSheetVerificationA
import im.vector.app.features.crypto.verification.epoxy.bottomSheetVerificationNoticeItem import im.vector.app.features.crypto.verification.epoxy.bottomSheetVerificationNoticeItem
import im.vector.app.features.crypto.verification.epoxy.bottomSheetVerificationWaitingItem import im.vector.app.features.crypto.verification.epoxy.bottomSheetVerificationWaitingItem
import im.vector.app.features.displayname.getBestName import im.vector.app.features.displayname.getBestName
import im.vector.lib.core.utils.epoxy.charsequence.toEpoxyCharSequence
import javax.inject.Inject import javax.inject.Inject
class VerificationRequestController @Inject constructor( class VerificationRequestController @Inject constructor(

View file

@ -18,11 +18,11 @@ package im.vector.app.features.devtools
import com.airbnb.epoxy.TypedEpoxyController import com.airbnb.epoxy.TypedEpoxyController
import im.vector.app.R import im.vector.app.R
import im.vector.app.core.epoxy.charsequence.toEpoxyCharSequence
import im.vector.app.core.resources.StringProvider import im.vector.app.core.resources.StringProvider
import im.vector.app.core.ui.list.genericFooterItem import im.vector.app.core.ui.list.genericFooterItem
import im.vector.app.features.form.formEditTextItem import im.vector.app.features.form.formEditTextItem
import im.vector.app.features.form.formMultiLineEditTextItem import im.vector.app.features.form.formMultiLineEditTextItem
import im.vector.lib.core.utils.epoxy.charsequence.toEpoxyCharSequence
import javax.inject.Inject import javax.inject.Inject
class RoomDevToolSendFormController @Inject constructor( class RoomDevToolSendFormController @Inject constructor(

View file

@ -18,11 +18,11 @@ package im.vector.app.features.devtools
import com.airbnb.epoxy.TypedEpoxyController import com.airbnb.epoxy.TypedEpoxyController
import im.vector.app.R import im.vector.app.R
import im.vector.app.core.epoxy.charsequence.toEpoxyCharSequence
import im.vector.app.core.epoxy.noResultItem import im.vector.app.core.epoxy.noResultItem
import im.vector.app.core.resources.ColorProvider import im.vector.app.core.resources.ColorProvider
import im.vector.app.core.resources.StringProvider import im.vector.app.core.resources.StringProvider
import im.vector.app.core.ui.list.genericItem import im.vector.app.core.ui.list.genericItem
import im.vector.lib.core.utils.epoxy.charsequence.toEpoxyCharSequence
import me.gujun.android.span.span import me.gujun.android.span.span
import org.json.JSONObject import org.json.JSONObject
import javax.inject.Inject import javax.inject.Inject
@ -37,7 +37,7 @@ class RoomStateListController @Inject constructor(
override fun buildModels(data: RoomDevToolViewState?) { override fun buildModels(data: RoomDevToolViewState?) {
val host = this val host = this
when (data?.displayMode) { when (data?.displayMode) {
RoomDevToolViewState.Mode.StateEventList -> { RoomDevToolViewState.Mode.StateEventList -> {
val stateEventsGroups = data.stateEvents.invoke().orEmpty().groupBy { it.getClearType() } val stateEventsGroups = data.stateEvents.invoke().orEmpty().groupBy { it.getClearType() }
if (stateEventsGroups.isEmpty()) { if (stateEventsGroups.isEmpty()) {

View file

@ -24,6 +24,7 @@ import android.os.Bundle
import android.os.Parcelable import android.os.Parcelable
import android.view.Menu import android.view.Menu
import android.view.MenuItem import android.view.MenuItem
import android.view.View
import androidx.core.view.GravityCompat import androidx.core.view.GravityCompat
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.drawerlayout.widget.DrawerLayout import androidx.drawerlayout.widget.DrawerLayout
@ -32,7 +33,6 @@ import androidx.fragment.app.FragmentManager
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import com.airbnb.mvrx.Mavericks import com.airbnb.mvrx.Mavericks
import com.airbnb.mvrx.viewModel import com.airbnb.mvrx.viewModel
import com.google.android.material.appbar.MaterialToolbar
import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.dialog.MaterialAlertDialogBuilder
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import im.vector.app.AppStateHandler import im.vector.app.AppStateHandler
@ -42,13 +42,14 @@ import im.vector.app.core.extensions.exhaustive
import im.vector.app.core.extensions.hideKeyboard import im.vector.app.core.extensions.hideKeyboard
import im.vector.app.core.extensions.registerStartForActivityResult import im.vector.app.core.extensions.registerStartForActivityResult
import im.vector.app.core.extensions.replaceFragment import im.vector.app.core.extensions.replaceFragment
import im.vector.app.core.platform.ToolbarConfigurable
import im.vector.app.core.platform.VectorBaseActivity import im.vector.app.core.platform.VectorBaseActivity
import im.vector.app.core.pushers.PushersManager import im.vector.app.core.pushers.PushersManager
import im.vector.app.databinding.ActivityHomeBinding import im.vector.app.databinding.ActivityHomeBinding
import im.vector.app.features.MainActivity import im.vector.app.features.MainActivity
import im.vector.app.features.MainActivityArgs import im.vector.app.features.MainActivityArgs
import im.vector.app.features.analytics.accountdata.AnalyticsAccountDataViewModel import im.vector.app.features.analytics.accountdata.AnalyticsAccountDataViewModel
import im.vector.app.features.analytics.plan.Screen
import im.vector.app.features.analytics.screen.ScreenEvent
import im.vector.app.features.disclaimer.showDisclaimerDialog import im.vector.app.features.disclaimer.showDisclaimerDialog
import im.vector.app.features.matrixto.MatrixToBottomSheet import im.vector.app.features.matrixto.MatrixToBottomSheet
import im.vector.app.features.navigation.Navigator import im.vector.app.features.navigation.Navigator
@ -96,7 +97,6 @@ data class HomeActivityArgs(
@AndroidEntryPoint @AndroidEntryPoint
class HomeActivity : class HomeActivity :
VectorBaseActivity<ActivityHomeBinding>(), VectorBaseActivity<ActivityHomeBinding>(),
ToolbarConfigurable,
NavigationInterceptor, NavigationInterceptor,
SpaceInviteBottomSheet.InteractionListener, SpaceInviteBottomSheet.InteractionListener,
MatrixToBottomSheet.InteractionListener { MatrixToBottomSheet.InteractionListener {
@ -104,6 +104,7 @@ class HomeActivity :
private lateinit var sharedActionViewModel: HomeSharedActionViewModel private lateinit var sharedActionViewModel: HomeSharedActionViewModel
private val homeActivityViewModel: HomeActivityViewModel by viewModel() private val homeActivityViewModel: HomeActivityViewModel by viewModel()
@Suppress("UNUSED") @Suppress("UNUSED")
private val analyticsAccountDataViewModel: AnalyticsAccountDataViewModel by viewModel() private val analyticsAccountDataViewModel: AnalyticsAccountDataViewModel by viewModel()
@Suppress("UNUSED") @Suppress("UNUSED")
@ -164,6 +165,16 @@ class HomeActivity :
} }
private val drawerListener = object : DrawerLayout.SimpleDrawerListener() { private val drawerListener = object : DrawerLayout.SimpleDrawerListener() {
private var drawerScreenEvent: ScreenEvent? = null
override fun onDrawerOpened(drawerView: View) {
drawerScreenEvent = ScreenEvent(Screen.ScreenName.MobileSidebar)
}
override fun onDrawerClosed(drawerView: View) {
drawerScreenEvent?.send(analyticsTracker)
drawerScreenEvent = null
}
override fun onDrawerStateChanged(newState: Int) { override fun onDrawerStateChanged(newState: Int) {
hideKeyboard() hideKeyboard()
} }
@ -175,6 +186,7 @@ class HomeActivity :
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
analyticsScreenName = Screen.ScreenName.Home
supportFragmentManager.registerFragmentLifecycleCallbacks(fragmentLifecycleCallbacks, false) supportFragmentManager.registerFragmentLifecycleCallbacks(fragmentLifecycleCallbacks, false)
FcmHelper.ensureFcmTokenIsRetrieved(this, pushManager, vectorPreferences.areNotificationEnabledForDevice()) FcmHelper.ensureFcmTokenIsRetrieved(this, pushManager, vectorPreferences.areNotificationEnabledForDevice())
sharedActionViewModel = viewModelProvider.get(HomeSharedActionViewModel::class.java) sharedActionViewModel = viewModelProvider.get(HomeSharedActionViewModel::class.java)
@ -478,10 +490,6 @@ class HomeActivity :
serverBackupStatusViewModel.refreshRemoteStateIfNeeded() serverBackupStatusViewModel.refreshRemoteStateIfNeeded()
} }
override fun configure(toolbar: MaterialToolbar) {
configureToolbar(toolbar, false)
}
override fun getMenuRes() = R.menu.home override fun getMenuRes() = R.menu.home
override fun onPrepareOptionsMenu(menu: Menu): Boolean { override fun onPrepareOptionsMenu(menu: Menu): Boolean {

View file

@ -33,7 +33,6 @@ import im.vector.app.R
import im.vector.app.RoomGroupingMethod import im.vector.app.RoomGroupingMethod
import im.vector.app.core.extensions.commitTransaction import im.vector.app.core.extensions.commitTransaction
import im.vector.app.core.extensions.toMvRxBundle import im.vector.app.core.extensions.toMvRxBundle
import im.vector.app.core.platform.ToolbarConfigurable
import im.vector.app.core.platform.VectorBaseActivity import im.vector.app.core.platform.VectorBaseActivity
import im.vector.app.core.platform.VectorBaseFragment import im.vector.app.core.platform.VectorBaseFragment
import im.vector.app.core.resources.ColorProvider import im.vector.app.core.resources.ColorProvider
@ -314,11 +313,9 @@ class HomeDetailFragment @Inject constructor(
} }
private fun setupToolbar() { private fun setupToolbar() {
val parentActivity = vectorBaseActivity setupToolbar(views.groupToolbar)
if (parentActivity is ToolbarConfigurable) { .setTitle(null)
parentActivity.configure(views.groupToolbar)
}
views.groupToolbar.title = ""
views.groupToolbarAvatarImageView.debouncedClicks { views.groupToolbarAvatarImageView.debouncedClicks {
sharedActionViewModel.post(HomeActivitySharedAction.OpenDrawer) sharedActionViewModel.post(HomeActivitySharedAction.OpenDrawer)
} }

View file

@ -30,6 +30,7 @@ import im.vector.app.core.extensions.replaceChildFragment
import im.vector.app.core.platform.VectorBaseFragment import im.vector.app.core.platform.VectorBaseFragment
import im.vector.app.core.utils.startSharePlainTextIntent import im.vector.app.core.utils.startSharePlainTextIntent
import im.vector.app.databinding.FragmentHomeDrawerBinding import im.vector.app.databinding.FragmentHomeDrawerBinding
import im.vector.app.features.analytics.plan.Screen
import im.vector.app.features.settings.VectorPreferences import im.vector.app.features.settings.VectorPreferences
import im.vector.app.features.settings.VectorSettingsActivity import im.vector.app.features.settings.VectorSettingsActivity
import im.vector.app.features.spaces.SpaceListFragment import im.vector.app.features.spaces.SpaceListFragment
@ -97,6 +98,7 @@ class HomeDrawerFragment @Inject constructor(
views.homeDrawerInviteFriendButton.debouncedClicks { views.homeDrawerInviteFriendButton.debouncedClicks {
session.permalinkService().createPermalink(sharedActionViewModel.session.myUserId)?.let { permalink -> session.permalinkService().createPermalink(sharedActionViewModel.session.myUserId)?.let { permalink ->
analyticsTracker.screen(Screen(screenName = Screen.ScreenName.MobileInviteFriends))
val text = getString(R.string.invite_friends_text, permalink) val text = getString(R.string.invite_friends_text, permalink)
startSharePlainTextIntent( startSharePlainTextIntent(

View file

@ -19,6 +19,7 @@ package im.vector.app.features.home.room.detail
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.os.Bundle import android.os.Bundle
import android.view.View
import android.widget.Toast import android.widget.Toast
import androidx.core.view.GravityCompat import androidx.core.view.GravityCompat
import androidx.drawerlayout.widget.DrawerLayout import androidx.drawerlayout.widget.DrawerLayout
@ -27,15 +28,15 @@ import androidx.fragment.app.FragmentManager
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import com.airbnb.mvrx.Mavericks import com.airbnb.mvrx.Mavericks
import com.airbnb.mvrx.viewModel import com.airbnb.mvrx.viewModel
import com.google.android.material.appbar.MaterialToolbar
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import im.vector.app.core.extensions.endKeepScreenOn import im.vector.app.core.extensions.endKeepScreenOn
import im.vector.app.core.extensions.hideKeyboard import im.vector.app.core.extensions.hideKeyboard
import im.vector.app.core.extensions.keepScreenOn import im.vector.app.core.extensions.keepScreenOn
import im.vector.app.core.extensions.replaceFragment import im.vector.app.core.extensions.replaceFragment
import im.vector.app.core.platform.ToolbarConfigurable
import im.vector.app.core.platform.VectorBaseActivity import im.vector.app.core.platform.VectorBaseActivity
import im.vector.app.databinding.ActivityRoomDetailBinding import im.vector.app.databinding.ActivityRoomDetailBinding
import im.vector.app.features.analytics.plan.Screen
import im.vector.app.features.analytics.screen.ScreenEvent
import im.vector.app.features.home.room.breadcrumbs.BreadcrumbsFragment import im.vector.app.features.home.room.breadcrumbs.BreadcrumbsFragment
import im.vector.app.features.home.room.detail.timeline.helper.VoiceMessagePlaybackTracker import im.vector.app.features.home.room.detail.timeline.helper.VoiceMessagePlaybackTracker
import im.vector.app.features.matrixto.MatrixToBottomSheet import im.vector.app.features.matrixto.MatrixToBottomSheet
@ -50,7 +51,6 @@ import javax.inject.Inject
@AndroidEntryPoint @AndroidEntryPoint
class RoomDetailActivity : class RoomDetailActivity :
VectorBaseActivity<ActivityRoomDetailBinding>(), VectorBaseActivity<ActivityRoomDetailBinding>(),
ToolbarConfigurable,
MatrixToBottomSheet.InteractionListener { MatrixToBottomSheet.InteractionListener {
override fun getBinding(): ActivityRoomDetailBinding { override fun getBinding(): ActivityRoomDetailBinding {
@ -156,11 +156,17 @@ class RoomDetailActivity :
super.onDestroy() super.onDestroy()
} }
override fun configure(toolbar: MaterialToolbar) {
configureToolbar(toolbar)
}
private val drawerListener = object : DrawerLayout.SimpleDrawerListener() { private val drawerListener = object : DrawerLayout.SimpleDrawerListener() {
private var drawerScreenEvent: ScreenEvent? = null
override fun onDrawerOpened(drawerView: View) {
drawerScreenEvent = ScreenEvent(Screen.ScreenName.MobileBreadcrumbs)
}
override fun onDrawerClosed(drawerView: View) {
drawerScreenEvent?.send(analyticsTracker)
drawerScreenEvent = null
}
override fun onDrawerStateChanged(newState: Int) { override fun onDrawerStateChanged(newState: Int) {
hideKeyboard() hideKeyboard()

View file

@ -116,6 +116,8 @@ import im.vector.app.core.utils.startInstallFromSourceIntent
import im.vector.app.core.utils.toast import im.vector.app.core.utils.toast
import im.vector.app.databinding.DialogReportContentBinding import im.vector.app.databinding.DialogReportContentBinding
import im.vector.app.databinding.FragmentRoomDetailBinding import im.vector.app.databinding.FragmentRoomDetailBinding
import im.vector.app.features.analytics.plan.Click
import im.vector.app.features.analytics.plan.Screen
import im.vector.app.features.attachments.AttachmentTypeSelectorView import im.vector.app.features.attachments.AttachmentTypeSelectorView
import im.vector.app.features.attachments.AttachmentsHelper import im.vector.app.features.attachments.AttachmentsHelper
import im.vector.app.features.attachments.ContactAttachment import im.vector.app.features.attachments.ContactAttachment
@ -239,7 +241,8 @@ data class RoomDetailArgs(
val roomId: String, val roomId: String,
val eventId: String? = null, val eventId: String? = null,
val sharedData: SharedData? = null, val sharedData: SharedData? = null,
val openShareSpaceForId: String? = null val openShareSpaceForId: String? = null,
val switchToParentSpace: Boolean = false
) : Parcelable ) : Parcelable
class RoomDetailFragment @Inject constructor( class RoomDetailFragment @Inject constructor(
@ -337,6 +340,7 @@ class RoomDetailFragment @Inject constructor(
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
analyticsScreenName = Screen.ScreenName.Room
setFragmentResultListener(MigrateRoomBottomSheet.REQUEST_KEY) { _, bundle -> setFragmentResultListener(MigrateRoomBottomSheet.REQUEST_KEY) { _, bundle ->
bundle.getString(MigrateRoomBottomSheet.BUNDLE_KEY_REPLACEMENT_ROOM)?.let { replacementRoomId -> bundle.getString(MigrateRoomBottomSheet.BUNDLE_KEY_REPLACEMENT_ROOM)?.let { replacementRoomId ->
roomDetailViewModel.handle(RoomDetailAction.RoomUpgradeSuccess(replacementRoomId)) roomDetailViewModel.handle(RoomDetailAction.RoomUpgradeSuccess(replacementRoomId))
@ -363,6 +367,7 @@ class RoomDetailFragment @Inject constructor(
keyboardStateUtils = KeyboardStateUtils(requireActivity()) keyboardStateUtils = KeyboardStateUtils(requireActivity())
lazyLoadedViews.bind(views) lazyLoadedViews.bind(views)
setupToolbar(views.roomToolbar) setupToolbar(views.roomToolbar)
.allowBack()
setupRecyclerView() setupRecyclerView()
setupComposer() setupComposer()
setupNotificationView() setupNotificationView()
@ -677,7 +682,7 @@ class RoomDetailFragment @Inject constructor(
*/ */
private fun EmojiPopup.Builder.setOnEmojiPopupDismissListenerLifecycleAware(action: () -> Unit): EmojiPopup.Builder { private fun EmojiPopup.Builder.setOnEmojiPopupDismissListenerLifecycleAware(action: () -> Unit): EmojiPopup.Builder {
return setOnEmojiPopupDismissListener { return setOnEmojiPopupDismissListener {
if (lifecycle.currentState == Lifecycle.State.STARTED) { if (lifecycle.currentState.isAtLeast(Lifecycle.State.STARTED)) {
action() action()
} }
} }
@ -1397,6 +1402,7 @@ class RoomDetailFragment @Inject constructor(
return return
} }
if (text.isNotBlank()) { if (text.isNotBlank()) {
analyticsTracker.capture(Click(name = Click.Name.SendMessageButton))
// We collapse ASAP, if not there will be a slight annoying delay // We collapse ASAP, if not there will be a slight annoying delay
views.composerLayout.collapse(true) views.composerLayout.collapse(true)
lockSendButton = true lockSendButton = true
@ -1511,7 +1517,7 @@ class RoomDetailFragment @Inject constructor(
views.roomToolbarSubtitleView.apply { views.roomToolbarSubtitleView.apply {
setTextOrHide(subtitle) setTextOrHide(subtitle)
if (typingMessage.isNullOrBlank()) { if (typingMessage.isNullOrBlank()) {
setTextColor(colorProvider.getColorFromAttribute(R.attr.vctr_content_primary)) setTextColor(colorProvider.getColorFromAttribute(R.attr.vctr_content_secondary))
setTypeface(null, Typeface.NORMAL) setTypeface(null, Typeface.NORMAL)
} else { } else {
setTextColor(colorProvider.getColorFromAttribute(R.attr.colorPrimary)) setTextColor(colorProvider.getColorFromAttribute(R.attr.colorPrimary))

View file

@ -28,6 +28,7 @@ import com.airbnb.mvrx.Uninitialized
import dagger.assisted.Assisted import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject import dagger.assisted.AssistedInject
import im.vector.app.AppStateHandler
import im.vector.app.BuildConfig import im.vector.app.BuildConfig
import im.vector.app.R import im.vector.app.R
import im.vector.app.core.di.MavericksAssistedViewModelFactory import im.vector.app.core.di.MavericksAssistedViewModelFactory
@ -37,7 +38,9 @@ import im.vector.app.core.mvrx.runCatchingToAsync
import im.vector.app.core.platform.VectorViewModel import im.vector.app.core.platform.VectorViewModel
import im.vector.app.core.resources.StringProvider import im.vector.app.core.resources.StringProvider
import im.vector.app.core.utils.BehaviorDataSource import im.vector.app.core.utils.BehaviorDataSource
import im.vector.app.features.analytics.AnalyticsTracker
import im.vector.app.features.analytics.DecryptionFailureTracker import im.vector.app.features.analytics.DecryptionFailureTracker
import im.vector.app.features.analytics.extensions.toAnalyticsJoinedRoom
import im.vector.app.features.call.conference.ConferenceEvent import im.vector.app.features.call.conference.ConferenceEvent
import im.vector.app.features.call.conference.JitsiActiveConferenceHolder import im.vector.app.features.call.conference.JitsiActiveConferenceHolder
import im.vector.app.features.call.conference.JitsiService import im.vector.app.features.call.conference.JitsiService
@ -54,6 +57,7 @@ import im.vector.app.features.powerlevel.PowerLevelsFlowFactory
import im.vector.app.features.session.coroutineScope import im.vector.app.features.session.coroutineScope
import im.vector.app.features.settings.VectorDataStore import im.vector.app.features.settings.VectorDataStore
import im.vector.app.features.settings.VectorPreferences import im.vector.app.features.settings.VectorPreferences
import im.vector.app.space
import im.vector.lib.core.utils.flow.chunk import im.vector.lib.core.utils.flow.chunk
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableSharedFlow
@ -112,9 +116,11 @@ class RoomDetailViewModel @AssistedInject constructor(
private val chatEffectManager: ChatEffectManager, private val chatEffectManager: ChatEffectManager,
private val directRoomHelper: DirectRoomHelper, private val directRoomHelper: DirectRoomHelper,
private val jitsiService: JitsiService, private val jitsiService: JitsiService,
private val analyticsTracker: AnalyticsTracker,
private val activeConferenceHolder: JitsiActiveConferenceHolder, private val activeConferenceHolder: JitsiActiveConferenceHolder,
private val decryptionFailureTracker: DecryptionFailureTracker, private val decryptionFailureTracker: DecryptionFailureTracker,
timelineFactory: TimelineFactory timelineFactory: TimelineFactory,
appStateHandler: AppStateHandler
) : VectorViewModel<RoomDetailViewState, RoomDetailAction, RoomDetailViewEvents>(initialState), ) : VectorViewModel<RoomDetailViewState, RoomDetailAction, RoomDetailViewEvents>(initialState),
Timeline.Listener, ChatEffectManager.Delegate, CallProtocolsChecker.Listener { Timeline.Listener, ChatEffectManager.Delegate, CallProtocolsChecker.Listener {
@ -179,6 +185,24 @@ class RoomDetailViewModel @AssistedInject constructor(
if (OutboundSessionKeySharingStrategy.WhenEnteringRoom == BuildConfig.outboundSessionKeySharingStrategy && room.isEncrypted()) { if (OutboundSessionKeySharingStrategy.WhenEnteringRoom == BuildConfig.outboundSessionKeySharingStrategy && room.isEncrypted()) {
prepareForEncryption() prepareForEncryption()
} }
if (initialState.switchToParentSpace) {
// We are coming from a notification, try to switch to the most relevant space
// so that when hitting back the room will appear in the list
appStateHandler.getCurrentRoomGroupingMethod()?.space().let { currentSpace ->
val currentRoomSummary = room.roomSummary() ?: return@let
// nothing we are good
if (currentSpace == null || !currentRoomSummary.flattenParentIds.contains(currentSpace.roomId)) {
// take first one or switch to home
appStateHandler.setCurrentSpace(
currentRoomSummary
.flattenParentIds.firstOrNull { it.isNotBlank() },
// force persist, because if not on resume the AppStateHandler will resume
// the current space from what was persisted on enter background
persistNow = true)
}
}
}
} }
private fun observeDataStore() { private fun observeDataStore() {
@ -709,7 +733,10 @@ class RoomDetailViewModel @AssistedInject constructor(
private fun handleAcceptInvite() { private fun handleAcceptInvite() {
viewModelScope.launch { viewModelScope.launch {
tryOrNull { room.join() } tryOrNull {
room.join()
analyticsTracker.capture(room.roomSummary().toAnalyticsJoinedRoom())
}
} }
} }

View file

@ -66,14 +66,16 @@ data class RoomDetailViewState(
val isAllowedToStartWebRTCCall: Boolean = true, val isAllowedToStartWebRTCCall: Boolean = true,
val isAllowedToSetupEncryption: Boolean = true, val isAllowedToSetupEncryption: Boolean = true,
val hasFailedSending: Boolean = false, val hasFailedSending: Boolean = false,
val jitsiState: JitsiState = JitsiState() val jitsiState: JitsiState = JitsiState(),
val switchToParentSpace: Boolean = false
) : MavericksState { ) : MavericksState {
constructor(args: RoomDetailArgs) : this( constructor(args: RoomDetailArgs) : this(
roomId = args.roomId, roomId = args.roomId,
eventId = args.eventId, eventId = args.eventId,
// Also highlight the target event, if any // Also highlight the target event, if any
highlightedEventId = args.eventId highlightedEventId = args.eventId,
switchToParentSpace = args.switchToParentSpace
) )
fun isWebRTCCallOptionAvailable() = (asyncRoomSummary.invoke()?.joinedMembersCount ?: 0) <= 2 fun isWebRTCCallOptionAvailable() = (asyncRoomSummary.invoke()?.joinedMembersCount ?: 0) <= 2

View file

@ -26,6 +26,8 @@ import im.vector.app.core.di.hiltMavericksViewModelFactory
import im.vector.app.core.extensions.exhaustive import im.vector.app.core.extensions.exhaustive
import im.vector.app.core.platform.VectorViewModel import im.vector.app.core.platform.VectorViewModel
import im.vector.app.core.resources.StringProvider import im.vector.app.core.resources.StringProvider
import im.vector.app.features.analytics.AnalyticsTracker
import im.vector.app.features.analytics.extensions.toAnalyticsJoinedRoom
import im.vector.app.features.attachments.toContentAttachmentData import im.vector.app.features.attachments.toContentAttachmentData
import im.vector.app.features.command.CommandParser import im.vector.app.features.command.CommandParser
import im.vector.app.features.command.ParsedCommand import im.vector.app.features.command.ParsedCommand
@ -66,8 +68,10 @@ class MessageComposerViewModel @AssistedInject constructor(
private val session: Session, private val session: Session,
private val stringProvider: StringProvider, private val stringProvider: StringProvider,
private val vectorPreferences: VectorPreferences, private val vectorPreferences: VectorPreferences,
private val commandParser: CommandParser,
private val rainbowGenerator: RainbowGenerator, private val rainbowGenerator: RainbowGenerator,
private val voiceMessageHelper: VoiceMessageHelper, private val voiceMessageHelper: VoiceMessageHelper,
private val analyticsTracker: AnalyticsTracker,
private val voicePlayerHelper: VoicePlayerHelper private val voicePlayerHelper: VoicePlayerHelper
) : VectorViewModel<MessageComposerViewState, MessageComposerAction, MessageComposerViewEvents>(initialState) { ) : VectorViewModel<MessageComposerViewState, MessageComposerAction, MessageComposerViewEvents>(initialState) {
@ -183,7 +187,7 @@ class MessageComposerViewModel @AssistedInject constructor(
withState { state -> withState { state ->
when (state.sendMode) { when (state.sendMode) {
is SendMode.Regular -> { is SendMode.Regular -> {
when (val slashCommandResult = CommandParser.parseSlashCommand(action.text)) { when (val slashCommandResult = commandParser.parseSlashCommand(action.text)) {
is ParsedCommand.ErrorNotACommand -> { is ParsedCommand.ErrorNotACommand -> {
// Send the text message to the room // Send the text message to the room
room.sendTextMessage(action.text, autoMarkdown = action.autoMarkdown) room.sendTextMessage(action.text, autoMarkdown = action.autoMarkdown)
@ -520,6 +524,7 @@ class MessageComposerViewModel @AssistedInject constructor(
return@launch return@launch
} }
session.getRoomSummary(command.roomAlias) session.getRoomSummary(command.roomAlias)
?.also { analyticsTracker.capture(it.toAnalyticsJoinedRoom()) }
?.roomId ?.roomId
?.let { ?.let {
_viewEvents.post(MessageComposerViewEvents.JoinRoomCommandSuccess(it)) _viewEvents.post(MessageComposerViewEvents.JoinRoomCommandSuccess(it))

View file

@ -40,7 +40,8 @@ class SearchActivity : VectorBaseActivity<ActivitySearchBinding>() {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
configureToolbar(views.searchToolbar) setupToolbar(views.searchToolbar)
.allowBack()
} }
override fun initUiAndData() { override fun initUiAndData() {

View file

@ -26,12 +26,12 @@ import com.airbnb.epoxy.VisibilityState
import im.vector.app.R import im.vector.app.R
import im.vector.app.core.date.DateFormatKind import im.vector.app.core.date.DateFormatKind
import im.vector.app.core.date.VectorDateFormatter import im.vector.app.core.date.VectorDateFormatter
import im.vector.app.core.epoxy.charsequence.toEpoxyCharSequence
import im.vector.app.core.epoxy.loadingItem import im.vector.app.core.epoxy.loadingItem
import im.vector.app.core.epoxy.noResultItem import im.vector.app.core.epoxy.noResultItem
import im.vector.app.core.resources.StringProvider import im.vector.app.core.resources.StringProvider
import im.vector.app.core.ui.list.GenericHeaderItem_ import im.vector.app.core.ui.list.GenericHeaderItem_
import im.vector.app.features.home.AvatarRenderer import im.vector.app.features.home.AvatarRenderer
import im.vector.lib.core.utils.epoxy.charsequence.toEpoxyCharSequence
import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.Session
import org.matrix.android.sdk.api.session.events.model.Content import org.matrix.android.sdk.api.session.events.model.Content
import org.matrix.android.sdk.api.session.events.model.Event import org.matrix.android.sdk.api.session.events.model.Event

View file

@ -24,11 +24,11 @@ import im.vector.app.R
import im.vector.app.core.epoxy.ClickListener import im.vector.app.core.epoxy.ClickListener
import im.vector.app.core.epoxy.VectorEpoxyHolder import im.vector.app.core.epoxy.VectorEpoxyHolder
import im.vector.app.core.epoxy.VectorEpoxyModel import im.vector.app.core.epoxy.VectorEpoxyModel
import im.vector.app.core.epoxy.charsequence.EpoxyCharSequence
import im.vector.app.core.epoxy.onClick import im.vector.app.core.epoxy.onClick
import im.vector.app.core.extensions.setTextOrHide import im.vector.app.core.extensions.setTextOrHide
import im.vector.app.features.displayname.getBestName import im.vector.app.features.displayname.getBestName
import im.vector.app.features.home.AvatarRenderer import im.vector.app.features.home.AvatarRenderer
import im.vector.lib.core.utils.epoxy.charsequence.EpoxyCharSequence
import org.matrix.android.sdk.api.util.MatrixItem import org.matrix.android.sdk.api.util.MatrixItem
@EpoxyModelClass(layout = R.layout.item_search_result) @EpoxyModelClass(layout = R.layout.item_search_result)

View file

@ -27,7 +27,6 @@ import im.vector.app.core.epoxy.bottomsheet.bottomSheetActionItem
import im.vector.app.core.epoxy.bottomsheet.bottomSheetMessagePreviewItem import im.vector.app.core.epoxy.bottomsheet.bottomSheetMessagePreviewItem
import im.vector.app.core.epoxy.bottomsheet.bottomSheetQuickReactionsItem import im.vector.app.core.epoxy.bottomsheet.bottomSheetQuickReactionsItem
import im.vector.app.core.epoxy.bottomsheet.bottomSheetSendStateItem import im.vector.app.core.epoxy.bottomsheet.bottomSheetSendStateItem
import im.vector.app.core.epoxy.charsequence.toEpoxyCharSequence
import im.vector.app.core.error.ErrorFormatter import im.vector.app.core.error.ErrorFormatter
import im.vector.app.core.resources.StringProvider import im.vector.app.core.resources.StringProvider
import im.vector.app.core.utils.DimensionConverter import im.vector.app.core.utils.DimensionConverter
@ -40,6 +39,7 @@ import im.vector.app.features.home.room.detail.timeline.tools.createLinkMovement
import im.vector.app.features.home.room.detail.timeline.tools.linkify import im.vector.app.features.home.room.detail.timeline.tools.linkify
import im.vector.app.features.html.SpanUtils import im.vector.app.features.html.SpanUtils
import im.vector.app.features.media.ImageContentRenderer import im.vector.app.features.media.ImageContentRenderer
import im.vector.lib.core.utils.epoxy.charsequence.toEpoxyCharSequence
import org.matrix.android.sdk.api.extensions.orFalse import org.matrix.android.sdk.api.extensions.orFalse
import org.matrix.android.sdk.api.failure.Failure import org.matrix.android.sdk.api.failure.Failure
import org.matrix.android.sdk.api.session.room.send.SendState import org.matrix.android.sdk.api.session.room.send.SendState

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