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: "/"
schedule:
interval: "weekly"
ignore:
- dependency-name: "*github-script*"
# Updates for Gradle dependencies used in the app
- package-ecosystem: gradle
directory: "/"

View file

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

View file

@ -14,7 +14,7 @@
* 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

View file

@ -14,7 +14,7 @@
* 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

View file

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

View file

@ -21,6 +21,7 @@ import android.view.View
import com.airbnb.epoxy.TypedEpoxyController
import com.airbnb.mvrx.Fail
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
@ -40,7 +41,7 @@ internal class JSonViewerEpoxyController(private val context: Context) :
is Fail -> {
valueItem {
id("fail")
text(async.error.localizedMessage?.toSafeCharSequence())
text(async.error.localizedMessage?.toEpoxyCharSequence())
}
}
is Success -> {
@ -94,7 +95,7 @@ internal class JSonViewerEpoxyController(private val context: Context) :
+"{+${model.keys.size}}"
textColor = host.styleProvider.baseColor
}
}.toSafeCharSequence()
}.toEpoxyCharSequence()
)
itemClickListener(View.OnClickListener { host.itemClicked(model) })
}
@ -133,7 +134,7 @@ internal class JSonViewerEpoxyController(private val context: Context) :
+"[+${model.items.size}]"
textColor = host.styleProvider.baseColor
}
}.toSafeCharSequence()
}.toEpoxyCharSequence()
)
itemClickListener(View.OnClickListener { host.itemClicked(model) })
}
@ -163,7 +164,7 @@ internal class JSonViewerEpoxyController(private val context: Context) :
}
}
append(host.valueToSpan(model))
}.toSafeCharSequence()
}.toEpoxyCharSequence()
)
copyValue(model.stringRes)
}
@ -233,7 +234,7 @@ internal class JSonViewerEpoxyController(private val context: Context) :
span("{".takeIf { isObject } ?: "[") {
textColor = host.styleProvider.baseColor
}
}.toSafeCharSequence()
}.toEpoxyCharSequence()
)
itemClickListener(View.OnClickListener { host.itemClicked(composed) })
}
@ -253,7 +254,7 @@ internal class JSonViewerEpoxyController(private val context: Context) :
span {
text = "}".takeIf { isObject } ?: "]"
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.util.TypedValue
/**
* TODO Mutualize
*/
internal object Utils {
fun dpToPx(dp: Int, context: Context): Int {
return TypedValue.applyDimension(

View file

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

View file

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

View file

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

View file

@ -32,13 +32,18 @@ fun Throwable.is401() =
fun Throwable.isTokenError() =
this is Failure.ServerError &&
(error.code == MatrixError.M_UNKNOWN_TOKEN ||
error.code == MatrixError.M_MISSING_TOKEN ||
error.code == MatrixError.ORG_MATRIX_EXPIRED_ACCOUNT)
error.code == MatrixError.M_MISSING_TOKEN ||
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 {
return this is Failure.NetworkConnection ||
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.delay
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.isLimitExceededError
import org.matrix.android.sdk.api.failure.shouldBeRetried
import org.matrix.android.sdk.internal.network.ssl.CertUtil
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 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 requestBlock a suspend lambda to perform the network request
*/
@ -74,23 +76,26 @@ internal suspend inline fun <DATA> executeRequest(globalErrorReceiver: GlobalErr
currentRetryCount++
if (exception is Failure.ServerError &&
exception.httpCode == 429 &&
exception.error.code == MatrixError.M_LIMIT_EXCEEDED &&
currentRetryCount < maxRetriesCount) {
if (exception.isLimitExceededError() && currentRetryCount < maxRetriesCount) {
// 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()) {
delay(currentDelay)
currentDelay = currentDelay.times(2L).coerceAtMost(maxDelayBeforeRetry)
// Try again (loop)
} else {
throw when (exception) {
is IOException -> Failure.NetworkConnection(exception)
is IOException -> Failure.NetworkConnection(exception)
is Failure.ServerError,
is Failure.OtherServerError,
is CancellationException -> exception
else -> Failure.Unknown(exception)
is CancellationException -> exception
else -> Failure.Unknown(exception)
}
}
}

View file

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

View file

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

View file

@ -160,7 +160,7 @@ Formatter\.formatShortFileSize===1
# 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
enum class===119
enum class===121
### Do not import temporary legacy classes
import org.matrix.android.sdk.internal.legacy.riot===3

View file

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

View file

@ -40,6 +40,10 @@ import timber.log.Timber
class ElementRobot {
fun onboarding(block: OnboardingRobot.() -> Unit) {
block(OnboardingRobot())
}
fun signUp(userId: String) {
val onboardingRobot = OnboardingRobot()
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.onView
import androidx.test.espresso.Espresso.pressBack
import androidx.test.espresso.matcher.ViewMatchers.isRoot
import androidx.test.espresso.matcher.ViewMatchers.withId
import com.adevinta.android.barista.assertion.BaristaEnabledAssertions.assertDisabled
@ -31,6 +32,24 @@ import im.vector.app.waitForView
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") {
initSession(true, userId, password, homeServerUrl)
}
@ -44,7 +63,7 @@ class OnboardingRobot {
password: String,
homeServerUrl: String) {
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) {
clickOn(R.id.loginSplashSubmit)
} 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.reactions.data.EmojiDataSource
import im.vector.app.interactWithSheet
import im.vector.app.waitForView
import im.vector.app.withRetry
import java.lang.Thread.sleep
@ -127,7 +126,7 @@ class RoomDetailRobot {
fun openSettings(block: RoomSettingsRobot.() -> Unit) {
clickMenu(R.id.timeline_setting)
waitForView(withId(R.id.roomProfileAvatarView))
waitUntilViewVisible(withId(R.id.roomProfileAvatarView))
sleep(1000)
block(RoomSettingsRobot())
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
if (selectedSpaceDataSource.currentValue?.orNull() is RoomGroupingMethod.BySpace &&
spaceId == selectedSpaceDataSource.currentValue?.orNull()?.space()?.roomId) return
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)))
if (spaceId != null) {
uSession.coroutineScope.launch(Dispatchers.IO) {

View file

@ -21,7 +21,7 @@ import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import im.vector.app.core.dialogs.UnrecognizedCertificateDialog
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.home.AvatarRenderer
import im.vector.app.features.navigation.Navigator
@ -56,7 +56,7 @@ interface SingletonEntryPoint {
fun pinLocker(): PinLocker
fun analytics(): VectorAnalytics
fun analyticsTracker(): AnalyticsTracker
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.time.Clock
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.impl.DefaultVectorAnalytics
import im.vector.app.features.invite.AutoAcceptInvites
@ -64,6 +65,9 @@ abstract class VectorBindModule {
@Binds
abstract fun bindVectorAnalytics(analytics: DefaultVectorAnalytics): VectorAnalytics
@Binds
abstract fun bindAnalyticsTracker(analytics: DefaultVectorAnalytics): AnalyticsTracker
@Binds
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.VectorEpoxyHolder
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.extensions.setTextOrHide
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.tools.findPillsAndProcess
import im.vector.app.features.media.ImageContentRenderer
import im.vector.lib.core.utils.epoxy.charsequence.EpoxyCharSequence
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.MatrixIdFailure
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 java.net.HttpURLConnection
import java.net.SocketTimeoutException
@ -58,53 +59,53 @@ class DefaultErrorFormatter @Inject constructor(
}
is Failure.ServerError -> {
when {
throwable.error.code == MatrixError.M_CONSENT_NOT_GIVEN -> {
throwable.error.code == MatrixError.M_CONSENT_NOT_GIVEN -> {
// Special case for terms and conditions
stringProvider.getString(R.string.error_terms_not_accepted)
}
throwable.isInvalidPassword() -> {
throwable.isInvalidPassword() -> {
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)
}
throwable.error.code == MatrixError.M_BAD_JSON -> {
throwable.error.code == MatrixError.M_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)
}
throwable.error.code == MatrixError.M_THREEPID_DENIED -> {
throwable.error.code == MatrixError.M_THREEPID_DENIED -> {
stringProvider.getString(R.string.login_error_threepid_denied)
}
throwable.error.code == MatrixError.M_LIMIT_EXCEEDED -> {
throwable.isLimitExceededError() -> {
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)
}
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)
}
throwable.error.code == MatrixError.M_USER_DEACTIVATED -> {
throwable.error.code == MatrixError.M_USER_DEACTIVATED -> {
stringProvider.getString(R.string.auth_invalid_login_deactivated_account)
}
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)
}
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)
}
throwable.error.code == MatrixError.M_THREEPID_AUTH_FAILED -> {
throwable.error.code == MatrixError.M_THREEPID_AUTH_FAILED -> {
stringProvider.getString(R.string.error_threepid_auth_failed)
}
throwable.error.code == MatrixError.M_UNKNOWN &&
throwable.error.message == "Not allowed to join this room" -> {
stringProvider.getString(R.string.room_error_access_unauthorized)
}
else -> {
else -> {
throwable.error.message.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
override fun initUiAndData() {
configureToolbar(views.toolbar)
setupToolbar(views.toolbar)
.allowBack(true)
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.singletonEntryPoint
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.features.MainActivity
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.consent.ConsentNotGivenHelper
import im.vector.app.features.navigation.Navigator
@ -90,6 +93,15 @@ import timber.log.Timber
import javax.inject.Inject
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
* ========================================================================================== */
@ -115,6 +127,8 @@ abstract class VectorBaseActivity<VB : ViewBinding> : AppCompatActivity(), Maver
.launchIn(lifecycleScope)
}
var toolbar: ToolbarConfig? = null
/* ==========================================================================================
* Views
* ========================================================================================== */
@ -133,7 +147,6 @@ abstract class VectorBaseActivity<VB : ViewBinding> : AppCompatActivity(), Maver
private lateinit var sessionListener: SessionListener
protected lateinit var bugReporter: BugReporter
private lateinit var pinLocker: PinLocker
protected lateinit var analytics: VectorAnalytics
@Inject
lateinit var rageShake: RageShake
@ -189,7 +202,7 @@ abstract class VectorBaseActivity<VB : ViewBinding> : AppCompatActivity(), Maver
configurationViewModel = viewModelProvider.get(ConfigurationViewModel::class.java)
bugReporter = singletonEntryPoint.bugReporter()
pinLocker = singletonEntryPoint.pinLocker()
analytics = singletonEntryPoint.analytics()
analyticsTracker = singletonEntryPoint.analyticsTracker()
navigator = singletonEntryPoint.navigator()
activeSessionHolder = singletonEntryPoint.activeSessionHolder()
vectorPreferences = singletonEntryPoint.vectorPreferences()
@ -324,7 +337,7 @@ abstract class VectorBaseActivity<VB : ViewBinding> : AppCompatActivity(), Maver
override fun onResume() {
super.onResume()
Timber.i("onResume Activity ${javaClass.simpleName}")
screenEvent = analyticsScreenName?.let { ScreenEvent(it) }
configurationViewModel.onActivityResumed()
if (this !is BugReportActivity && vectorPreferences.useRageshake()) {
@ -363,6 +376,7 @@ abstract class VectorBaseActivity<VB : ViewBinding> : AppCompatActivity(), Maver
override fun onPause() {
super.onPause()
screenEvent?.send(analyticsTracker, analyticsScreenName)
Timber.i("onPause Activity ${javaClass.simpleName}")
rageShake.stop()
@ -497,18 +511,6 @@ abstract class VectorBaseActivity<VB : ViewBinding> : AppCompatActivity(), Maver
*/
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)
// ==============================================================================================
@ -618,4 +620,13 @@ abstract class VectorBaseActivity<VB : ViewBinding> : AppCompatActivity(), Maver
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.toMvRxBundle
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.onEach
import reactivecircus.flowbinding.android.view.clicks
@ -47,6 +49,14 @@ import timber.log.Timber
* Add Mavericks capabilities, handle DI and bindings.
*/
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
@ -84,8 +94,6 @@ abstract class VectorBaseBottomSheetDialogFragment<VB : ViewBinding> : BottomShe
open val showExpanded = false
protected lateinit var analytics: VectorAnalytics
interface ResultListener {
fun onBottomSheetResult(resultCode: Int, data: Any?)
@ -124,13 +132,19 @@ abstract class VectorBaseBottomSheetDialogFragment<VB : ViewBinding> : BottomShe
val activityEntryPoint = EntryPointAccessors.fromActivity(vectorBaseActivity, ActivityEntryPoint::class.java)
viewModelFactory = activityEntryPoint.viewModelFactory()
val singletonEntryPoint = context.singletonEntryPoint()
analytics = singletonEntryPoint.analytics()
analyticsTracker = singletonEntryPoint.analyticsTracker()
super.onAttach(context)
}
override fun onResume() {
super.onResume()
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 {

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

View file

@ -27,10 +27,10 @@ import im.vector.app.R
import im.vector.app.core.epoxy.ClickListener
import im.vector.app.core.epoxy.VectorEpoxyHolder
import im.vector.app.core.epoxy.VectorEpoxyModel
import im.vector.app.core.epoxy.charsequence.EpoxyCharSequence
import im.vector.app.core.epoxy.onClick
import im.vector.app.core.extensions.setTextOrHide
import im.vector.app.features.themes.ThemeUtils
import im.vector.lib.core.utils.epoxy.charsequence.EpoxyCharSequence
/**
* 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 {
override fun onboardingVariant(): VectorFeatures.OnboardingVariant = BuildConfig.ONBOARDING_VARIANT
override fun isOnboardingAlreadyHaveAccountSplashEnabled() = true
override fun isOnboardingSplashCarouselEnabled() = false
override fun isOnboardingSplashCarouselEnabled() = true
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");
* 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
* 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,
@ -14,11 +14,19 @@
* 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
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.lib.core.utils.compat.removeIfCompat
import im.vector.lib.core.utils.flow.tickerFlow
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
@ -49,7 +49,7 @@ private const val CHECK_INTERVAL = 2_000L
*/
@Singleton
class DecryptionFailureTracker @Inject constructor(
private val vectorAnalytics: VectorAnalytics,
private val analyticsTracker: AnalyticsTracker,
private val clock: Clock
) {
@ -136,7 +136,7 @@ class DecryptionFailureTracker @Inject constructor(
// for now we ignore events already reported even if displayed again?
.filter { alreadyReported.contains(it).not() }
.forEach { failedEventId ->
vectorAnalytics.capture(Error(failedEventId, Error.Domain.E2EE, aggregation.key))
analyticsTracker.capture(Error(failedEventId, Error.Domain.E2EE, aggregation.key))
alreadyReported.add(failedEventId)
}
}

View file

@ -16,11 +16,9 @@
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
interface VectorAnalytics {
interface VectorAnalytics : AnalyticsTracker {
/**
* 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
*/
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.
*/
data class CallEnded(
/**
* The duration of the call in milliseconds.
*/
val durationMs: Int,
/**
* Whether its a video call or not.
*/
val isVideo: Boolean,
/**
* Number of participants in the call.
*/
val numParticipants: Int,
/**
* Whether this user placed it.
*/
val placed: Boolean,
/**
* The duration of the call in milliseconds.
*/
val durationMs: Int,
/**
* Whether its a video call or not.
*/
val isVideo: Boolean,
/**
* Number of participants in the call.
*/
val numParticipants: Int,
/**
* Whether this user placed it.
*/
val placed: Boolean,
) : VectorAnalyticsEvent {
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.
*/
data class CallError(
/**
* Whether its a video call or not.
*/
val isVideo: Boolean,
/**
* Number of participants in the call.
*/
val numParticipants: Int,
/**
* Whether this user placed it.
*/
val placed: Boolean,
/**
* Whether its a video call or not.
*/
val isVideo: Boolean,
/**
* Number of participants in the call.
*/
val numParticipants: Int,
/**
* Whether this user placed it.
*/
val placed: Boolean,
) : VectorAnalyticsEvent {
override fun getName() = "CallError"

View file

@ -25,18 +25,18 @@ import im.vector.app.features.analytics.itf.VectorAnalyticsEvent
* Triggered when a call is started.
*/
data class CallStarted(
/**
* Whether its a video call or not.
*/
val isVideo: Boolean,
/**
* Number of participants in the call.
*/
val numParticipants: Int,
/**
* Whether this user placed it.
*/
val placed: Boolean,
/**
* Whether its a video call or not.
*/
val isVideo: Boolean,
/**
* Number of participants in the call.
*/
val numParticipants: Int,
/**
* Whether this user placed it.
*/
val placed: Boolean,
) : VectorAnalyticsEvent {
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.
*/
data class Click(
/**
* The index of the element, if its in a list of elements.
*/
val index: Int? = null,
/**
* The unique name of this element.
*/
val name: Name,
/**
* The index of the element, if its in a list of elements.
*/
val index: Int? = null,
/**
* The unique name of this element.
*/
val name: Name,
) : VectorAnalyticsEvent {
enum class Name {

View file

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

View file

@ -25,12 +25,12 @@ import im.vector.app.features.analytics.itf.VectorAnalyticsEvent
* Triggered when an error occurred
*/
data class Error(
/**
* Context - client defined, can be used for debugging
*/
val context: String? = null,
val domain: Domain,
val name: Name,
/**
* Context - client defined, can be used for debugging
*/
val context: String? = null,
val domain: Domain,
val name: Name,
) : VectorAnalyticsEvent {
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.
*/
data class JoinedRoom(
/**
* Whether the room is a DM.
*/
val isDM: Boolean,
/**
* The size of the room.
*/
val roomSize: RoomSize,
/**
* Whether the room is a DM.
*/
val isDM: Boolean,
/**
* The size of the room.
*/
val roomSize: RoomSize,
) : VectorAnalyticsEvent {
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.
*/
data class PerformanceTimer(
/**
* Client defined, can be used for debugging.
*/
val context: String? = null,
/**
* Client defined, an optional value to indicate how many items were handled during the operation.
*/
val itemCount: Int? = null,
/**
* The timer that is being reported.
*/
val name: Name,
/**
* The time reported by the timer in milliseconds.
*/
val timeMs: Int,
/**
* Client defined, can be used for debugging.
*/
val context: String? = null,
/**
* Client defined, an optional value to indicate how many items were
* handled during the operation.
*/
val itemCount: Int? = null,
/**
* The timer that is being reported.
*/
val name: Name,
/**
* The time reported by the timer in milliseconds.
*/
val timeMs: Int,
) : VectorAnalyticsEvent {
enum class Name {
@ -55,7 +56,8 @@ data class PerformanceTimer(
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,
@ -65,7 +67,8 @@ data class PerformanceTimer(
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,
@ -80,7 +83,8 @@ data class PerformanceTimer(
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,
}

View file

@ -25,28 +25,221 @@ import im.vector.app.features.analytics.itf.VectorAnalyticsScreen
* Triggered when the user changed screen
*/
data class Screen(
/**
* How long the screen was displayed for in milliseconds.
*/
val durationMs: Int? = null,
val screenName: ScreenName,
/**
* How long the screen was displayed for in milliseconds.
*/
val durationMs: Int? = null,
val screenName: ScreenName,
) : VectorAnalyticsScreen {
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,
/**
* The Home tab on iOS | possibly the same on Android? | The Home space
* on Web?
*/
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,
/**
* 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,
/**
* 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,
/**
* 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,
/**
* ?
*/
WebCompleteSecurity,
/**
* ?
*/
WebE2ESetup,
WebForgotPassword,
/**
* ?
*/
WebLoading,
WebLogin,
WebRegister,
/**
* ?
*/
WebSoftLogout,
WebWelcome,
/**
* The splash screen.
*/
Welcome,
}
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.Intent
import com.google.android.material.appbar.MaterialToolbar
import dagger.hilt.android.AndroidEntryPoint
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.databinding.ActivitySimpleBinding
import im.vector.app.features.themes.ActivityOtherThemes
import org.matrix.android.sdk.api.session.content.ContentAttachmentData
@AndroidEntryPoint
class AttachmentsPreviewActivity : VectorBaseActivity<ActivitySimpleBinding>(), ToolbarConfigurable {
class AttachmentsPreviewActivity : VectorBaseActivity<ActivitySimpleBinding>() {
companion object {
private const val EXTRA_FRAGMENT_ARGS = "EXTRA_FRAGMENT_ARGS"
@ -72,8 +70,4 @@ class AttachmentsPreviewActivity : VectorBaseActivity<ActivitySimpleBinding>(),
setResult(RESULT_OK, resultIntent)
finish()
}
override fun configure(toolbar: MaterialToolbar) {
configureToolbar(toolbar)
}
}

View file

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

View file

@ -17,6 +17,7 @@
package im.vector.app.features.call.dialpad
import android.content.ClipboardManager
import android.content.Context
import android.content.res.ColorStateList
import android.os.Bundle
import android.telephony.PhoneNumberFormattingTextWatcher
@ -37,6 +38,10 @@ import androidx.fragment.app.Fragment
import com.android.dialer.dialpadview.DialpadView
import com.android.dialer.dialpadview.DigitsEditText
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
class DialPadFragment : Fragment(), TextWatcher {
@ -53,6 +58,25 @@ class DialPadFragment : Fragment(), TextWatcher {
private var enableDelete = 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(
inflater: LayoutInflater,
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)
}
}.attach()
configureToolbar(views.callTransferToolbar)
setupToolbar(views.callTransferToolbar)
.allowBack()
views.callTransferToolbar.title = getString(R.string.call_transfer_title)
setupConnectAction()
}

View file

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

View file

@ -22,6 +22,9 @@ import androidx.lifecycle.LifecycleOwner
import im.vector.app.ActiveSessionDataSource
import im.vector.app.BuildConfig
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.audio.CallAudioManager
import im.vector.app.features.call.lookup.CallProtocolsChecker
@ -68,7 +71,8 @@ private val loggerTag = LoggerTag("WebRtcCallManager", LoggerTag.VOIP)
@Singleton
class WebRtcCallManager @Inject constructor(
private val context: Context,
private val activeSessionDataSource: ActiveSessionDataSource
private val activeSessionDataSource: ActiveSessionDataSource,
private val analyticsTracker: AnalyticsTracker
) : CallListener,
DefaultLifecycleObserver {
@ -237,6 +241,7 @@ class WebRtcCallManager @Inject constructor(
val currentCall = getCurrentCall().takeIf { it != call }
currentCall?.updateRemoteOnHold(onHold = true)
audioManager.setMode(if (call.mxCall.isVideoCall) CallAudioManager.Mode.VIDEO_CALL else CallAudioManager.Mode.AUDIO_CALL)
call.trackCallStarted()
this.currentCall.setAndNotify(call)
}
@ -245,6 +250,7 @@ class WebRtcCallManager @Inject constructor(
val webRtcCall = callsByCallId.remove(callId) ?: return Unit.also {
Timber.tag(loggerTag.value).v("On call ended for unknown call $callId")
}
webRtcCall.trackCallEnded()
CallService.onCallTerminated(context, callId, endCallReason, rejected)
callsByRoomId[webRtcCall.signalingRoomId]?.remove(webRtcCall)
callsByRoomId[webRtcCall.nativeRoomId]?.remove(webRtcCall)
@ -443,4 +449,28 @@ class WebRtcCallManager @Inject constructor(
}
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.session.identity.ThreePid
import timber.log.Timber
import javax.inject.Inject
object CommandParser {
class CommandParser @Inject constructor() {
/**
* Convert the text message into a Slash command.
@ -34,11 +35,9 @@ object CommandParser {
*/
fun parseSlashCommand(textMessage: CharSequence): ParsedCommand {
// check if it has the Slash marker
if (!textMessage.startsWith("/")) {
return ParsedCommand.ErrorNotACommand
return if (!textMessage.startsWith("/")) {
ParsedCommand.ErrorNotACommand
} else {
Timber.v("parseSlashCommand")
// "/" only
if (textMessage.length == 1) {
return ParsedCommand.ErrorEmptySlashCommand
@ -52,7 +51,7 @@ object CommandParser {
val messageParts = try {
textMessage.split("\\s+".toRegex()).dropLastWhile { it.isEmpty() }
} catch (e: Exception) {
Timber.e(e, "## manageSlashCommand() : split failed")
Timber.e(e, "## parseSlashCommand() : split failed")
null
}
@ -64,7 +63,7 @@ object CommandParser {
val slashCommand = messageParts.first()
val message = textMessage.substring(slashCommand.length).trim()
return when {
when {
Command.PLAIN.matches(slashCommand) -> {
if (message.isNotEmpty()) {
ParsedCommand.SendPlainText(message = message)

View file

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

View file

@ -67,7 +67,8 @@ class ContactsBookFragment @Inject constructor(
setupFilterView()
setupConsentView()
setupOnlyBoundContactsView()
setupCloseView()
setupToolbar(views.phoneBookToolbar)
.allowBack(useCross = true)
contactsBookViewModel.observeViewEvents {
when (it) {
is ContactsBookViewEvents.Failure -> showFailure(it.throwable)
@ -119,12 +120,6 @@ class ContactsBookFragment @Inject constructor(
views.phoneBookRecyclerView.configureWith(contactsBookController)
}
private fun setupCloseView() {
views.phoneBookClose.debouncedClicks {
sharedActionViewModel.post(UserListSharedAction.GoBack)
}
}
override fun invalidate() = withState(contactsBookViewModel) { state ->
views.phoneBookSearchForMatrixContacts.isVisible = state.filteredMappedContacts.isNotEmpty() && state.identityServerUrl != null && !state.userConsent
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.onPermissionDeniedSnackbar
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.userdirectory.UserListFragment
import im.vector.app.features.userdirectory.UserListFragmentArgs
@ -63,6 +64,7 @@ class CreateDirectRoomActivity : SimpleFragmentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
analyticsScreenName = Screen.ScreenName.StartChat
views.toolbar.visibility = View.GONE
sharedActionViewModel = viewModelProvider.get(UserListSharedActionViewModel::class.java)

View file

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

View file

@ -22,13 +22,13 @@ import com.airbnb.mvrx.Loading
import com.airbnb.mvrx.Success
import com.airbnb.mvrx.Uninitialized
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.loadingItem
import im.vector.app.core.resources.StringProvider
import im.vector.app.core.ui.list.ItemStyle
import im.vector.app.core.ui.list.genericItem
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.crypto.keysbackup.KeysBackupState
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 im.vector.app.R
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.StringProvider
import im.vector.app.core.utils.colorizeMatchingText
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.bottomSheetVerificationNoticeItem
import im.vector.lib.core.utils.epoxy.charsequence.EpoxyCharSequence
import im.vector.lib.core.utils.epoxy.charsequence.toEpoxyCharSequence
import javax.inject.Inject
class VerificationCancelController @Inject constructor(

View file

@ -19,13 +19,13 @@ package im.vector.app.features.crypto.verification.cancel
import com.airbnb.epoxy.EpoxyController
import im.vector.app.R
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.StringProvider
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.bottomSheetVerificationNoticeItem
import im.vector.app.features.html.EventHtmlRenderer
import im.vector.lib.core.utils.epoxy.charsequence.toEpoxyCharSequence
import javax.inject.Inject
class VerificationNotMeController @Inject constructor(

View file

@ -19,12 +19,12 @@ package im.vector.app.features.crypto.verification.choose
import com.airbnb.epoxy.EpoxyController
import im.vector.app.R
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.StringProvider
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.bottomSheetVerificationQrCodeItem
import im.vector.lib.core.utils.epoxy.charsequence.toEpoxyCharSequence
import javax.inject.Inject
class VerificationChooseMethodController @Inject constructor(

View file

@ -19,13 +19,13 @@ package im.vector.app.features.crypto.verification.conclusion
import com.airbnb.epoxy.EpoxyController
import im.vector.app.R
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.StringProvider
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.bottomSheetVerificationNoticeItem
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 javax.inject.Inject

View file

@ -21,7 +21,6 @@ import com.airbnb.mvrx.Fail
import com.airbnb.mvrx.Success
import im.vector.app.R
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.error.ErrorFormatter
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.bottomSheetVerificationWaitingItem
import im.vector.app.features.displayname.getBestName
import im.vector.lib.core.utils.epoxy.charsequence.toEpoxyCharSequence
import javax.inject.Inject
class VerificationEmojiCodeController @Inject constructor(

View file

@ -22,7 +22,7 @@ import com.airbnb.epoxy.EpoxyModelClass
import im.vector.app.R
import im.vector.app.core.epoxy.VectorEpoxyHolder
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.

View file

@ -18,12 +18,12 @@ package im.vector.app.features.crypto.verification.qrconfirmation
import com.airbnb.epoxy.EpoxyController
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.StringProvider
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.bottomSheetVerificationWaitingItem
import im.vector.lib.core.utils.epoxy.charsequence.toEpoxyCharSequence
import org.matrix.android.sdk.api.crypto.RoomEncryptionTrustLevel
import javax.inject.Inject

View file

@ -19,7 +19,6 @@ package im.vector.app.features.crypto.verification.qrconfirmation
import com.airbnb.epoxy.EpoxyController
import im.vector.app.R
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.StringProvider
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.bottomSheetVerificationNoticeItem
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 javax.inject.Inject

View file

@ -23,7 +23,6 @@ import com.airbnb.mvrx.Success
import com.airbnb.mvrx.Uninitialized
import im.vector.app.R
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.StringProvider
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.bottomSheetVerificationWaitingItem
import im.vector.app.features.displayname.getBestName
import im.vector.lib.core.utils.epoxy.charsequence.toEpoxyCharSequence
import javax.inject.Inject
class VerificationRequestController @Inject constructor(

View file

@ -18,11 +18,11 @@ package im.vector.app.features.devtools
import com.airbnb.epoxy.TypedEpoxyController
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.ui.list.genericFooterItem
import im.vector.app.features.form.formEditTextItem
import im.vector.app.features.form.formMultiLineEditTextItem
import im.vector.lib.core.utils.epoxy.charsequence.toEpoxyCharSequence
import javax.inject.Inject
class RoomDevToolSendFormController @Inject constructor(

View file

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

View file

@ -24,6 +24,7 @@ import android.os.Bundle
import android.os.Parcelable
import android.view.Menu
import android.view.MenuItem
import android.view.View
import androidx.core.view.GravityCompat
import androidx.core.view.isVisible
import androidx.drawerlayout.widget.DrawerLayout
@ -32,7 +33,6 @@ import androidx.fragment.app.FragmentManager
import androidx.lifecycle.lifecycleScope
import com.airbnb.mvrx.Mavericks
import com.airbnb.mvrx.viewModel
import com.google.android.material.appbar.MaterialToolbar
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import dagger.hilt.android.AndroidEntryPoint
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.registerStartForActivityResult
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.pushers.PushersManager
import im.vector.app.databinding.ActivityHomeBinding
import im.vector.app.features.MainActivity
import im.vector.app.features.MainActivityArgs
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.matrixto.MatrixToBottomSheet
import im.vector.app.features.navigation.Navigator
@ -96,7 +97,6 @@ data class HomeActivityArgs(
@AndroidEntryPoint
class HomeActivity :
VectorBaseActivity<ActivityHomeBinding>(),
ToolbarConfigurable,
NavigationInterceptor,
SpaceInviteBottomSheet.InteractionListener,
MatrixToBottomSheet.InteractionListener {
@ -104,6 +104,7 @@ class HomeActivity :
private lateinit var sharedActionViewModel: HomeSharedActionViewModel
private val homeActivityViewModel: HomeActivityViewModel by viewModel()
@Suppress("UNUSED")
private val analyticsAccountDataViewModel: AnalyticsAccountDataViewModel by viewModel()
@Suppress("UNUSED")
@ -164,6 +165,16 @@ class HomeActivity :
}
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) {
hideKeyboard()
}
@ -175,6 +186,7 @@ class HomeActivity :
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
analyticsScreenName = Screen.ScreenName.Home
supportFragmentManager.registerFragmentLifecycleCallbacks(fragmentLifecycleCallbacks, false)
FcmHelper.ensureFcmTokenIsRetrieved(this, pushManager, vectorPreferences.areNotificationEnabledForDevice())
sharedActionViewModel = viewModelProvider.get(HomeSharedActionViewModel::class.java)
@ -478,10 +490,6 @@ class HomeActivity :
serverBackupStatusViewModel.refreshRemoteStateIfNeeded()
}
override fun configure(toolbar: MaterialToolbar) {
configureToolbar(toolbar, false)
}
override fun getMenuRes() = R.menu.home
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.core.extensions.commitTransaction
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.VectorBaseFragment
import im.vector.app.core.resources.ColorProvider
@ -314,11 +313,9 @@ class HomeDetailFragment @Inject constructor(
}
private fun setupToolbar() {
val parentActivity = vectorBaseActivity
if (parentActivity is ToolbarConfigurable) {
parentActivity.configure(views.groupToolbar)
}
views.groupToolbar.title = ""
setupToolbar(views.groupToolbar)
.setTitle(null)
views.groupToolbarAvatarImageView.debouncedClicks {
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.utils.startSharePlainTextIntent
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.VectorSettingsActivity
import im.vector.app.features.spaces.SpaceListFragment
@ -97,6 +98,7 @@ class HomeDrawerFragment @Inject constructor(
views.homeDrawerInviteFriendButton.debouncedClicks {
session.permalinkService().createPermalink(sharedActionViewModel.session.myUserId)?.let { permalink ->
analyticsTracker.screen(Screen(screenName = Screen.ScreenName.MobileInviteFriends))
val text = getString(R.string.invite_friends_text, permalink)
startSharePlainTextIntent(

View file

@ -19,6 +19,7 @@ package im.vector.app.features.home.room.detail
import android.content.Context
import android.content.Intent
import android.os.Bundle
import android.view.View
import android.widget.Toast
import androidx.core.view.GravityCompat
import androidx.drawerlayout.widget.DrawerLayout
@ -27,15 +28,15 @@ import androidx.fragment.app.FragmentManager
import androidx.lifecycle.lifecycleScope
import com.airbnb.mvrx.Mavericks
import com.airbnb.mvrx.viewModel
import com.google.android.material.appbar.MaterialToolbar
import dagger.hilt.android.AndroidEntryPoint
import im.vector.app.core.extensions.endKeepScreenOn
import im.vector.app.core.extensions.hideKeyboard
import im.vector.app.core.extensions.keepScreenOn
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.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.detail.timeline.helper.VoiceMessagePlaybackTracker
import im.vector.app.features.matrixto.MatrixToBottomSheet
@ -50,7 +51,6 @@ import javax.inject.Inject
@AndroidEntryPoint
class RoomDetailActivity :
VectorBaseActivity<ActivityRoomDetailBinding>(),
ToolbarConfigurable,
MatrixToBottomSheet.InteractionListener {
override fun getBinding(): ActivityRoomDetailBinding {
@ -156,11 +156,17 @@ class RoomDetailActivity :
super.onDestroy()
}
override fun configure(toolbar: MaterialToolbar) {
configureToolbar(toolbar)
}
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) {
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.databinding.DialogReportContentBinding
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.AttachmentsHelper
import im.vector.app.features.attachments.ContactAttachment
@ -239,7 +241,8 @@ data class RoomDetailArgs(
val roomId: String,
val eventId: String? = null,
val sharedData: SharedData? = null,
val openShareSpaceForId: String? = null
val openShareSpaceForId: String? = null,
val switchToParentSpace: Boolean = false
) : Parcelable
class RoomDetailFragment @Inject constructor(
@ -337,6 +340,7 @@ class RoomDetailFragment @Inject constructor(
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
analyticsScreenName = Screen.ScreenName.Room
setFragmentResultListener(MigrateRoomBottomSheet.REQUEST_KEY) { _, bundle ->
bundle.getString(MigrateRoomBottomSheet.BUNDLE_KEY_REPLACEMENT_ROOM)?.let { replacementRoomId ->
roomDetailViewModel.handle(RoomDetailAction.RoomUpgradeSuccess(replacementRoomId))
@ -363,6 +367,7 @@ class RoomDetailFragment @Inject constructor(
keyboardStateUtils = KeyboardStateUtils(requireActivity())
lazyLoadedViews.bind(views)
setupToolbar(views.roomToolbar)
.allowBack()
setupRecyclerView()
setupComposer()
setupNotificationView()
@ -677,7 +682,7 @@ class RoomDetailFragment @Inject constructor(
*/
private fun EmojiPopup.Builder.setOnEmojiPopupDismissListenerLifecycleAware(action: () -> Unit): EmojiPopup.Builder {
return setOnEmojiPopupDismissListener {
if (lifecycle.currentState == Lifecycle.State.STARTED) {
if (lifecycle.currentState.isAtLeast(Lifecycle.State.STARTED)) {
action()
}
}
@ -1397,6 +1402,7 @@ class RoomDetailFragment @Inject constructor(
return
}
if (text.isNotBlank()) {
analyticsTracker.capture(Click(name = Click.Name.SendMessageButton))
// We collapse ASAP, if not there will be a slight annoying delay
views.composerLayout.collapse(true)
lockSendButton = true
@ -1511,7 +1517,7 @@ class RoomDetailFragment @Inject constructor(
views.roomToolbarSubtitleView.apply {
setTextOrHide(subtitle)
if (typingMessage.isNullOrBlank()) {
setTextColor(colorProvider.getColorFromAttribute(R.attr.vctr_content_primary))
setTextColor(colorProvider.getColorFromAttribute(R.attr.vctr_content_secondary))
setTypeface(null, Typeface.NORMAL)
} else {
setTextColor(colorProvider.getColorFromAttribute(R.attr.colorPrimary))

View file

@ -28,6 +28,7 @@ import com.airbnb.mvrx.Uninitialized
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import im.vector.app.AppStateHandler
import im.vector.app.BuildConfig
import im.vector.app.R
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.resources.StringProvider
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.extensions.toAnalyticsJoinedRoom
import im.vector.app.features.call.conference.ConferenceEvent
import im.vector.app.features.call.conference.JitsiActiveConferenceHolder
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.settings.VectorDataStore
import im.vector.app.features.settings.VectorPreferences
import im.vector.app.space
import im.vector.lib.core.utils.flow.chunk
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableSharedFlow
@ -112,9 +116,11 @@ class RoomDetailViewModel @AssistedInject constructor(
private val chatEffectManager: ChatEffectManager,
private val directRoomHelper: DirectRoomHelper,
private val jitsiService: JitsiService,
private val analyticsTracker: AnalyticsTracker,
private val activeConferenceHolder: JitsiActiveConferenceHolder,
private val decryptionFailureTracker: DecryptionFailureTracker,
timelineFactory: TimelineFactory
timelineFactory: TimelineFactory,
appStateHandler: AppStateHandler
) : VectorViewModel<RoomDetailViewState, RoomDetailAction, RoomDetailViewEvents>(initialState),
Timeline.Listener, ChatEffectManager.Delegate, CallProtocolsChecker.Listener {
@ -179,6 +185,24 @@ class RoomDetailViewModel @AssistedInject constructor(
if (OutboundSessionKeySharingStrategy.WhenEnteringRoom == BuildConfig.outboundSessionKeySharingStrategy && room.isEncrypted()) {
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() {
@ -709,7 +733,10 @@ class RoomDetailViewModel @AssistedInject constructor(
private fun handleAcceptInvite() {
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 isAllowedToSetupEncryption: Boolean = true,
val hasFailedSending: Boolean = false,
val jitsiState: JitsiState = JitsiState()
val jitsiState: JitsiState = JitsiState(),
val switchToParentSpace: Boolean = false
) : MavericksState {
constructor(args: RoomDetailArgs) : this(
roomId = args.roomId,
eventId = args.eventId,
// Also highlight the target event, if any
highlightedEventId = args.eventId
highlightedEventId = args.eventId,
switchToParentSpace = args.switchToParentSpace
)
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.platform.VectorViewModel
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.command.CommandParser
import im.vector.app.features.command.ParsedCommand
@ -66,8 +68,10 @@ class MessageComposerViewModel @AssistedInject constructor(
private val session: Session,
private val stringProvider: StringProvider,
private val vectorPreferences: VectorPreferences,
private val commandParser: CommandParser,
private val rainbowGenerator: RainbowGenerator,
private val voiceMessageHelper: VoiceMessageHelper,
private val analyticsTracker: AnalyticsTracker,
private val voicePlayerHelper: VoicePlayerHelper
) : VectorViewModel<MessageComposerViewState, MessageComposerAction, MessageComposerViewEvents>(initialState) {
@ -183,7 +187,7 @@ class MessageComposerViewModel @AssistedInject constructor(
withState { state ->
when (state.sendMode) {
is SendMode.Regular -> {
when (val slashCommandResult = CommandParser.parseSlashCommand(action.text)) {
when (val slashCommandResult = commandParser.parseSlashCommand(action.text)) {
is ParsedCommand.ErrorNotACommand -> {
// Send the text message to the room
room.sendTextMessage(action.text, autoMarkdown = action.autoMarkdown)
@ -520,6 +524,7 @@ class MessageComposerViewModel @AssistedInject constructor(
return@launch
}
session.getRoomSummary(command.roomAlias)
?.also { analyticsTracker.capture(it.toAnalyticsJoinedRoom()) }
?.roomId
?.let {
_viewEvents.post(MessageComposerViewEvents.JoinRoomCommandSuccess(it))

View file

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

View file

@ -26,12 +26,12 @@ import com.airbnb.epoxy.VisibilityState
import im.vector.app.R
import im.vector.app.core.date.DateFormatKind
import im.vector.app.core.date.VectorDateFormatter
import im.vector.app.core.epoxy.charsequence.toEpoxyCharSequence
import im.vector.app.core.epoxy.loadingItem
import im.vector.app.core.epoxy.noResultItem
import im.vector.app.core.resources.StringProvider
import im.vector.app.core.ui.list.GenericHeaderItem_
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.events.model.Content
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.VectorEpoxyHolder
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.extensions.setTextOrHide
import im.vector.app.features.displayname.getBestName
import im.vector.app.features.home.AvatarRenderer
import im.vector.lib.core.utils.epoxy.charsequence.EpoxyCharSequence
import org.matrix.android.sdk.api.util.MatrixItem
@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.bottomSheetQuickReactionsItem
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.resources.StringProvider
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.html.SpanUtils
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.failure.Failure
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