Merge branch 'develop' into feature/bca/rust_flavor

This commit is contained in:
valere 2022-12-26 09:29:03 +01:00
commit 76fa1bfee5
78 changed files with 1216 additions and 156 deletions

View file

@ -1,3 +1,19 @@
Changes in Element v1.5.14 (2022-12-20)
=======================================
Bugfixes 🐛
----------
- ActiveSessionHolder is not supposed to start syncing. Instead, the MainActivity does it, if necessary. Fixes a race condition when clearing cache.
Changes in Element v1.5.13 (2022-12-19)
=======================================
Bugfixes 🐛
----------
- Add `largeHeap=true` in the manifest since we are seeing more crashes (OOM) when handling sync response.
Changes in Element v1.5.12 (2022-12-15) Changes in Element v1.5.12 (2022-12-15)
======================================= =======================================

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

@ -0,0 +1 @@
Do not show typing notification of ignored users.

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

@ -0,0 +1 @@
[Push Notifications, Threads] - quick reply to threaded notification now sent to thread except main timeline

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

@ -0,0 +1 @@
[Rich text editor] Add support for links

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

@ -0,0 +1 @@
[Session manager] Other sessions list: filter option is displayed when selection mode is enabled

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

@ -0,0 +1 @@
[Session manager] Other sessions: Filter bottom sheet cut in landscape mode

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

@ -0,0 +1 @@
Automatically show keyboard after learn more bottom sheet is dismissed

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

@ -0,0 +1 @@
[Session Manager] Other sessions list: cannot select/deselect session by a long press when in select mode

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

@ -0,0 +1 @@
Fix current session ip address visibility

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

@ -0,0 +1 @@
[Session manager] Security recommendations cards: whole view should be tappable

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

@ -0,0 +1 @@
Device Manager UI review fixes

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

@ -0,0 +1 @@
[Voice Broadcast] Replace the player timeline

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

@ -0,0 +1 @@
Increase session manager test coverage

View file

@ -83,7 +83,7 @@ ext.libs = [
'appdistributionApi' : "com.google.firebase:firebase-appdistribution-api-ktx:$appDistribution", 'appdistributionApi' : "com.google.firebase:firebase-appdistribution-api-ktx:$appDistribution",
'appdistribution' : "com.google.firebase:firebase-appdistribution:$appDistribution", 'appdistribution' : "com.google.firebase:firebase-appdistribution:$appDistribution",
// Phone number https://github.com/google/libphonenumber // Phone number https://github.com/google/libphonenumber
'phonenumber' : "com.googlecode.libphonenumber:libphonenumber:8.13.1" 'phonenumber' : "com.googlecode.libphonenumber:libphonenumber:8.13.3"
], ],
dagger : [ dagger : [
'dagger' : "com.google.dagger:dagger:$dagger", 'dagger' : "com.google.dagger:dagger:$dagger",
@ -98,7 +98,7 @@ ext.libs = [
], ],
element : [ element : [
'opusencoder' : "io.element.android:opusencoder:1.1.0", 'opusencoder' : "io.element.android:opusencoder:1.1.0",
'wysiwyg' : "io.element.android:wysiwyg:0.9.0" 'wysiwyg' : "io.element.android:wysiwyg:0.10.0"
], ],
squareup : [ squareup : [
'moshi' : "com.squareup.moshi:moshi:$moshi", 'moshi' : "com.squareup.moshi:moshi:$moshi",
@ -129,7 +129,7 @@ ext.libs = [
'mavericksTesting' : "com.airbnb.android:mavericks-testing:$mavericks" 'mavericksTesting' : "com.airbnb.android:mavericks-testing:$mavericks"
], ],
maplibre : [ maplibre : [
'androidSdk' : "org.maplibre.gl:android-sdk:9.5.2", 'androidSdk' : "org.maplibre.gl:android-sdk:9.6.0",
'pluginAnnotation' : "org.maplibre.gl:android-plugin-annotation-v9:1.0.0" 'pluginAnnotation' : "org.maplibre.gl:android-plugin-annotation-v9:1.0.0"
], ],
mockk : [ mockk : [

View file

@ -0,0 +1,2 @@
Main changes in this version: Thread are now enabled by default.
Full changelog: https://github.com/vector-im/element-android/releases

View file

@ -0,0 +1,2 @@
Main changes in this version: Thread are now enabled by default.
Full changelog: https://github.com/vector-im/element-android/releases

View file

@ -420,6 +420,7 @@
<string name="action_got_it">Got it</string> <string name="action_got_it">Got it</string>
<string name="action_select_all">Select all</string> <string name="action_select_all">Select all</string>
<string name="action_deselect_all">Deselect all</string> <string name="action_deselect_all">Deselect all</string>
<string name="action_stop">Yes, Stop</string>
<string name="copied_to_clipboard">Copied to clipboard</string> <string name="copied_to_clipboard">Copied to clipboard</string>
@ -3128,6 +3129,8 @@
<string name="error_voice_broadcast_already_in_progress_message">You are already recording a voice broadcast. Please end your current voice broadcast to start a new one.</string> <string name="error_voice_broadcast_already_in_progress_message">You are already recording a voice broadcast. Please end your current voice broadcast to start a new one.</string>
<!-- Examples of usage: 6h 15min 30sec left / 15min 30sec left / 30sec left --> <!-- Examples of usage: 6h 15min 30sec left / 15min 30sec left / 30sec left -->
<string name="voice_broadcast_recording_time_left">%1$s left</string> <string name="voice_broadcast_recording_time_left">%1$s left</string>
<string name="stop_voice_broadcast_dialog_title">Stop live broadcasting?</string>
<string name="stop_voice_broadcast_content">Are you sure you want to stop your live broadcast? This will end the broadcast and the full recording will be available in the room.</string>
<string name="upgrade_room_for_restricted">Anyone in %s will be able to find and join this room - no need to manually invite everyone. Youll be able to change this in room settings anytime.</string> <string name="upgrade_room_for_restricted">Anyone in %s will be able to find and join this room - no need to manually invite everyone. Youll be able to change this in room settings anytime.</string>
<string name="upgrade_room_for_restricted_no_param">Anyone in a parent space will be able to find and join this room - no need to manually invite everyone. Youll be able to change this in room settings anytime.</string> <string name="upgrade_room_for_restricted_no_param">Anyone in a parent space will be able to find and join this room - no need to manually invite everyone. Youll be able to change this in room settings anytime.</string>
@ -3343,7 +3346,7 @@
<item quantity="one">Consider signing out from old sessions (%1$d day or more) that you dont use anymore.</item> <item quantity="one">Consider signing out from old sessions (%1$d day or more) that you dont use anymore.</item>
<item quantity="other">Consider signing out from old sessions (%1$d days or more) that you dont use anymore.</item> <item quantity="other">Consider signing out from old sessions (%1$d days or more) that you dont use anymore.</item>
</plurals> </plurals>
<string name="device_manager_current_session_title">Current Session</string> <string name="device_manager_current_session_title">Current session</string>
<string name="device_manager_session_title">Session</string> <string name="device_manager_session_title">Session</string>
<string name="device_manager_device_title">Device</string> <string name="device_manager_device_title">Device</string>
<!-- Examples: Last activity Yesterday at 6PM, Last activity Aug 31 at 5:47PM --> <!-- Examples: Last activity Yesterday at 6PM, Last activity Aug 31 at 5:47PM -->
@ -3484,13 +3487,19 @@
<string name="qr_code_login_confirm_security_code">Confirm</string> <string name="qr_code_login_confirm_security_code">Confirm</string>
<string name="qr_code_login_confirm_security_code_description">Please ensure that you know the origin of this code. By linking devices, you will provide someone with full access to your account.</string> <string name="qr_code_login_confirm_security_code_description">Please ensure that you know the origin of this code. By linking devices, you will provide someone with full access to your account.</string>
<!-- WYSIWYG Composer --> <!-- Rich text editor -->
<string name="rich_text_editor_format_bold">Apply bold format</string> <string name="rich_text_editor_format_bold">Apply bold format</string>
<string name="rich_text_editor_format_italic">Apply italic format</string> <string name="rich_text_editor_format_italic">Apply italic format</string>
<string name="rich_text_editor_format_strikethrough">Apply strikethrough format</string> <string name="rich_text_editor_format_strikethrough">Apply strikethrough format</string>
<string name="rich_text_editor_format_underline">Apply underline format</string> <string name="rich_text_editor_format_underline">Apply underline format</string>
<string name="rich_text_editor_link">Set link</string>
<string name="rich_text_editor_full_screen_toggle">Toggle full screen mode</string> <string name="rich_text_editor_full_screen_toggle">Toggle full screen mode</string>
<string name="set_link_text">Text</string>
<string name="set_link_link">Link</string>
<string name="set_link_create">Create a link</string>
<string name="set_link_edit">Edit link</string>
<!-- ReplyTo events --> <!-- ReplyTo events -->
<string name="message_reply_to_prefix">In reply to</string> <string name="message_reply_to_prefix">In reply to</string>
<string name="message_reply_to_sender_sent_file">sent a file.</string> <string name="message_reply_to_sender_sent_file">sent a file.</string>

View file

@ -63,7 +63,7 @@ android {
// that the app's state is completely cleared between tests. // that the app's state is completely cleared between tests.
testInstrumentationRunnerArguments clearPackageData: 'true' testInstrumentationRunnerArguments clearPackageData: 'true'
buildConfigField "String", "SDK_VERSION", "\"1.5.14\"" buildConfigField "String", "SDK_VERSION", "\"1.5.16\""
buildConfigField "String", "GIT_SDK_REVISION", "\"${gitRevision()}\"" buildConfigField "String", "GIT_SDK_REVISION", "\"${gitRevision()}\""
buildConfigField "String", "GIT_SDK_REVISION_UNIX_DATE", "\"${gitRevisionUnixDate()}\"" buildConfigField "String", "GIT_SDK_REVISION_UNIX_DATE", "\"${gitRevisionUnixDate()}\""

View file

@ -18,6 +18,7 @@ package org.matrix.android.sdk.internal.session.sync.handler.room
import io.realm.Realm import io.realm.Realm
import org.matrix.android.sdk.api.session.room.sender.SenderInfo import org.matrix.android.sdk.api.session.room.sender.SenderInfo
import org.matrix.android.sdk.internal.database.model.IgnoredUserEntity
import org.matrix.android.sdk.internal.di.UserId import org.matrix.android.sdk.internal.di.UserId
import org.matrix.android.sdk.internal.session.room.membership.RoomMemberHelper import org.matrix.android.sdk.internal.session.room.membership.RoomMemberHelper
import org.matrix.android.sdk.internal.session.typing.DefaultTypingUsersTracker import org.matrix.android.sdk.internal.session.typing.DefaultTypingUsersTracker
@ -30,8 +31,15 @@ internal class RoomTypingUsersHandler @Inject constructor(
// TODO This could be handled outside of the Realm transaction. Use the new aggregator? // TODO This could be handled outside of the Realm transaction. Use the new aggregator?
fun handle(realm: Realm, roomId: String, ephemeralResult: RoomSyncHandler.EphemeralResult?) { fun handle(realm: Realm, roomId: String, ephemeralResult: RoomSyncHandler.EphemeralResult?) {
val typingUserIds = ephemeralResult?.typingUserIds
if (typingUserIds.isNullOrEmpty()) {
typingUsersTracker.setTypingUsersFromRoom(roomId, emptyList())
return
}
// Filter ignored users and current user
val filteredUserIds = realm.where(IgnoredUserEntity::class.java).findAll().map { it.userId } + userId
val roomMemberHelper = RoomMemberHelper(realm, roomId) val roomMemberHelper = RoomMemberHelper(realm, roomId)
val typingIds = ephemeralResult?.typingUserIds?.filter { it != userId }.orEmpty() val typingIds = typingUserIds.filter { it !in filteredUserIds }
val senderInfo = typingIds.map { userId -> val senderInfo = typingIds.map { userId ->
val roomMemberSummaryEntity = roomMemberHelper.getLastRoomMember(userId) val roomMemberSummaryEntity = roomMemberHelper.getLastRoomMember(userId)
SenderInfo( SenderInfo(

View file

@ -3013,7 +3013,11 @@
"begging", "begging",
"mercy", "mercy",
"puppy eyes", "puppy eyes",
"face" "face",
"cry",
"tears",
"sad",
"grievance"
] ]
}, },
"face-holding-back-tears": { "face-holding-back-tears": {
@ -3060,9 +3064,7 @@
"fearful", "fearful",
"scared", "scared",
"terrified", "terrified",
"nervous", "nervous"
"oops",
"huh"
] ]
}, },
"anxious-face-with-sweat": { "anxious-face-with-sweat": {

View file

@ -37,7 +37,7 @@ ext.versionMinor = 5
// Note: even values are reserved for regular release, odd values for hotfix release. // Note: even values are reserved for regular release, odd values for hotfix release.
// When creating a hotfix, you should decrease the value, since the current value // When creating a hotfix, you should decrease the value, since the current value
// is the value for the next regular release. // is the value for the next regular release.
ext.versionPatch = 14 ext.versionPatch = 16
static def getGitTimestamp() { static def getGitTimestamp() {
def cmd = 'git show -s --format=%ct' def cmd = 'git show -s --format=%ct'

View file

@ -10,6 +10,7 @@
android:hasFragileUserData="true" android:hasFragileUserData="true"
android:icon="@mipmap/ic_launcher" android:icon="@mipmap/ic_launcher"
android:label="@string/app_name" android:label="@string/app_name"
android:largeHeap="true"
android:networkSecurityConfig="@xml/network_security_config" android:networkSecurityConfig="@xml/network_security_config"
android:resizeableActivity="true" android:resizeableActivity="true"
android:roundIcon="@mipmap/ic_launcher_round" android:roundIcon="@mipmap/ic_launcher_round"

View file

@ -18,7 +18,6 @@ package im.vector.app.core.di
import android.content.Context import android.content.Context
import im.vector.app.ActiveSessionDataSource import im.vector.app.ActiveSessionDataSource
import im.vector.app.core.extensions.startSyncing
import im.vector.app.core.pushers.UnregisterUnifiedPushUseCase import im.vector.app.core.pushers.UnregisterUnifiedPushUseCase
import im.vector.app.core.services.GuardServiceStarter import im.vector.app.core.services.GuardServiceStarter
import im.vector.app.core.session.ConfigureAndStartSessionUseCase import im.vector.app.core.session.ConfigureAndStartSessionUseCase
@ -72,7 +71,7 @@ class ActiveSessionHolder @Inject constructor(
suspend fun clearActiveSession() { suspend fun clearActiveSession() {
// Do some cleanup first // Do some cleanup first
getSafeActiveSession(startSync = false)?.let { getSafeActiveSession()?.let {
Timber.w("clearActiveSession of ${it.myUserId}") Timber.w("clearActiveSession of ${it.myUserId}")
it.callSignalingService().removeCallListener(callManager) it.callSignalingService().removeCallListener(callManager)
it.removeListener(sessionListener) it.removeListener(sessionListener)
@ -93,8 +92,8 @@ class ActiveSessionHolder @Inject constructor(
return activeSessionReference.get() != null || authenticationService.hasAuthenticatedSessions() return activeSessionReference.get() != null || authenticationService.hasAuthenticatedSessions()
} }
fun getSafeActiveSession(startSync: Boolean = true): Session? { fun getSafeActiveSession(): Session? {
return runBlocking { getOrInitializeSession(startSync = startSync) } return runBlocking { getOrInitializeSession() }
} }
fun getActiveSession(): Session { fun getActiveSession(): Session {
@ -102,16 +101,11 @@ class ActiveSessionHolder @Inject constructor(
?: throw IllegalStateException("You should authenticate before using this") ?: throw IllegalStateException("You should authenticate before using this")
} }
suspend fun getOrInitializeSession(startSync: Boolean): Session? { suspend fun getOrInitializeSession(): Session? {
return activeSessionReference.get() return activeSessionReference.get()
?.also {
if (startSync && !it.syncService().isSyncThreadAlive()) {
it.startSyncing(applicationContext)
}
}
?: sessionInitializer.tryInitialize(readCurrentSession = { activeSessionReference.get() }) { session -> ?: sessionInitializer.tryInitialize(readCurrentSession = { activeSessionReference.get() }) { session ->
setActiveSession(session) setActiveSession(session)
configureAndStartSessionUseCase.execute(session, startSyncing = startSync) configureAndStartSessionUseCase.execute(session, startSyncing = false)
} }
} }

View file

@ -45,6 +45,7 @@ import im.vector.app.features.home.UserColorAccountDataViewModel
import im.vector.app.features.home.room.breadcrumbs.BreadcrumbsViewModel import im.vector.app.features.home.room.breadcrumbs.BreadcrumbsViewModel
import im.vector.app.features.home.room.detail.TimelineViewModel import im.vector.app.features.home.room.detail.TimelineViewModel
import im.vector.app.features.home.room.detail.composer.MessageComposerViewModel import im.vector.app.features.home.room.detail.composer.MessageComposerViewModel
import im.vector.app.features.home.room.detail.composer.link.SetLinkViewModel
import im.vector.app.features.home.room.detail.search.SearchViewModel import im.vector.app.features.home.room.detail.search.SearchViewModel
import im.vector.app.features.home.room.detail.timeline.action.MessageActionsViewModel import im.vector.app.features.home.room.detail.timeline.action.MessageActionsViewModel
import im.vector.app.features.home.room.detail.timeline.edithistory.ViewEditHistoryViewModel import im.vector.app.features.home.room.detail.timeline.edithistory.ViewEditHistoryViewModel
@ -695,4 +696,9 @@ interface MavericksViewModelModule {
fun vectorSettingsNotificationPreferenceViewModelFactory( fun vectorSettingsNotificationPreferenceViewModelFactory(
factory: VectorSettingsNotificationPreferenceViewModel.Factory factory: VectorSettingsNotificationPreferenceViewModel.Factory
): MavericksAssistedViewModelFactory<*, *> ): MavericksAssistedViewModelFactory<*, *>
@Binds
@IntoMap
@MavericksViewModelKey(SetLinkViewModel::class)
fun setLinkViewModelFactory(factory: SetLinkViewModel.Factory): MavericksAssistedViewModelFactory<*, *>
} }

View file

@ -0,0 +1,155 @@
/*
* Copyright 2019 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.app.core.platform
import android.content.Context
import android.os.Bundle
import android.os.Parcelable
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.annotation.CallSuper
import androidx.fragment.app.DialogFragment
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.lifecycleScope
import androidx.viewbinding.ViewBinding
import com.airbnb.mvrx.MavericksView
import dagger.hilt.android.EntryPointAccessors
import im.vector.app.R
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.features.analytics.AnalyticsTracker
import im.vector.app.features.analytics.plan.MobileScreen
import im.vector.app.features.themes.ThemeUtils
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import reactivecircus.flowbinding.android.view.clicks
import timber.log.Timber
/**
* Add Mavericks capabilities, handle DI and bindings.
*/
abstract class VectorBaseDialogFragment<VB : ViewBinding> : DialogFragment(), MavericksView {
/* ==========================================================================================
* Analytics
* ========================================================================================== */
protected var analyticsScreenName: MobileScreen.ScreenName? = null
protected lateinit var analyticsTracker: AnalyticsTracker
/* ==========================================================================================
* View
* ========================================================================================== */
private var _binding: VB? = null
// This property is only valid between onCreateView and onDestroyView.
protected val views: VB
get() = _binding!!
abstract fun getBinding(inflater: LayoutInflater, container: ViewGroup?): VB
/* ==========================================================================================
* View model
* ========================================================================================== */
private lateinit var viewModelFactory: ViewModelProvider.Factory
protected val activityViewModelProvider
get() = ViewModelProvider(requireActivity(), viewModelFactory)
protected val fragmentViewModelProvider
get() = ViewModelProvider(this, viewModelFactory)
val vectorBaseActivity: VectorBaseActivity<*> by lazy {
activity as VectorBaseActivity<*>
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setStyle(STYLE_NORMAL, ThemeUtils.getApplicationThemeRes(requireContext()))
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
_binding = getBinding(inflater, container)
return views.root
}
@CallSuper
override fun onDestroyView() {
_binding = null
super.onDestroyView()
}
@CallSuper
override fun onDestroy() {
super.onDestroy()
}
override fun onAttach(context: Context) {
val activityEntryPoint = EntryPointAccessors.fromActivity(vectorBaseActivity, ActivityEntryPoint::class.java)
viewModelFactory = activityEntryPoint.viewModelFactory()
val singletonEntryPoint = context.singletonEntryPoint()
analyticsTracker = singletonEntryPoint.analyticsTracker()
super.onAttach(context)
}
override fun onResume() {
super.onResume()
Timber.i("onResume BottomSheet ${javaClass.simpleName}")
analyticsScreenName?.let {
analyticsTracker.screen(MobileScreen(screenName = it))
}
}
override fun onStart() {
super.onStart()
// This ensures that invalidate() is called for static screens that don't
// subscribe to a ViewModel.
postInvalidate()
requireDialog().window?.setWindowAnimations(R.style.Animation_AppCompat_Dialog)
}
protected fun setArguments(args: Parcelable? = null) {
arguments = args.toMvRxBundle()
}
/* ==========================================================================================
* Views
* ========================================================================================== */
protected fun View.debouncedClicks(onClicked: () -> Unit) {
clicks()
.onEach { onClicked() }
.launchIn(viewLifecycleOwner.lifecycleScope)
}
/* ==========================================================================================
* ViewEvents
* ========================================================================================== */
protected fun <T : VectorViewEvents> VectorViewModel<*, *, T>.observeViewEvents(observer: (T) -> Unit) {
viewEvents
.stream()
.onEach {
observer(it)
}
.launchIn(viewLifecycleOwner.lifecycleScope)
}
}

View file

@ -118,7 +118,7 @@ class VectorPushHandler @Inject constructor(
Timber.tag(loggerTag.value).d("## handleInternal()") Timber.tag(loggerTag.value).d("## handleInternal()")
} }
val session = activeSessionHolder.getOrInitializeSession(startSync = false) val session = activeSessionHolder.getOrInitializeSession()
if (session == null) { if (session == null) {
Timber.tag(loggerTag.value).w("## Can't sync from push, no current session") Timber.tag(loggerTag.value).w("## Can't sync from push, no current session")

View file

@ -174,12 +174,15 @@ class MainActivity : VectorBaseActivity<ActivityMainBinding>(), UnlockedActivity
private fun handleAppStarted() { private fun handleAppStarted() {
if (intent.hasExtra(EXTRA_NEXT_INTENT)) { if (intent.hasExtra(EXTRA_NEXT_INTENT)) {
// Start the next Activity // Start the next Activity
startSyncing()
val nextIntent = intent.getParcelableExtraCompat<Intent>(EXTRA_NEXT_INTENT) val nextIntent = intent.getParcelableExtraCompat<Intent>(EXTRA_NEXT_INTENT)
startIntentAndFinish(nextIntent) startIntentAndFinish(nextIntent)
} else if (intent.hasExtra(EXTRA_INIT_SESSION)) { } else if (intent.hasExtra(EXTRA_INIT_SESSION)) {
startSyncing()
setResult(RESULT_OK) setResult(RESULT_OK)
finish() finish()
} else if (intent.action == ACTION_ROOM_DETAILS_FROM_SHORTCUT) { } else if (intent.action == ACTION_ROOM_DETAILS_FROM_SHORTCUT) {
startSyncing()
val roomId = intent.getStringExtra(EXTRA_ROOM_ID) val roomId = intent.getStringExtra(EXTRA_ROOM_ID)
if (roomId?.isNotEmpty() == true) { if (roomId?.isNotEmpty() == true) {
navigator.openRoom(this, roomId, trigger = ViewRoom.Trigger.Shortcut) navigator.openRoom(this, roomId, trigger = ViewRoom.Trigger.Shortcut)
@ -194,11 +197,16 @@ class MainActivity : VectorBaseActivity<ActivityMainBinding>(), UnlockedActivity
if (args.clearCache || args.clearCredentials) { if (args.clearCache || args.clearCredentials) {
doCleanUp() doCleanUp()
} else { } else {
startSyncing()
startNextActivityAndFinish() startNextActivityAndFinish()
} }
} }
} }
private fun startSyncing() {
activeSessionHolder.getSafeActiveSession()?.startSyncing(this)
}
private fun clearNotifications() { private fun clearNotifications() {
// Dismiss all notifications // Dismiss all notifications
notificationDrawerManager.clearAllEvents() notificationDrawerManager.clearAllEvents()

View file

@ -127,6 +127,7 @@ sealed class RoomDetailAction : VectorViewModelAction {
object Pause : Recording() object Pause : Recording()
object Resume : Recording() object Resume : Recording()
object Stop : Recording() object Stop : Recording()
object StopConfirmed : Recording()
} }
sealed class Listening : VoiceBroadcastAction() { sealed class Listening : VoiceBroadcastAction() {

View file

@ -71,6 +71,8 @@ sealed class RoomDetailViewEvents : VectorViewEvents {
object DisplayEnableIntegrationsWarning : RoomDetailViewEvents() object DisplayEnableIntegrationsWarning : RoomDetailViewEvents()
object DisplayPromptToStopVoiceBroadcast : RoomDetailViewEvents()
data class OpenStickerPicker(val widget: Widget) : RoomDetailViewEvents() data class OpenStickerPicker(val widget: Widget) : RoomDetailViewEvents()
object OpenIntegrationManager : RoomDetailViewEvents() object OpenIntegrationManager : RoomDetailViewEvents()

View file

@ -413,6 +413,7 @@ class TimelineFragment :
is RoomDetailViewEvents.DisplayAndAcceptCall -> acceptIncomingCall(it) is RoomDetailViewEvents.DisplayAndAcceptCall -> acceptIncomingCall(it)
RoomDetailViewEvents.RoomReplacementStarted -> handleRoomReplacement() RoomDetailViewEvents.RoomReplacementStarted -> handleRoomReplacement()
RoomDetailViewEvents.OpenElementCallWidget -> handleOpenElementCallWidget() RoomDetailViewEvents.OpenElementCallWidget -> handleOpenElementCallWidget()
RoomDetailViewEvents.DisplayPromptToStopVoiceBroadcast -> displayPromptToStopVoiceBroadcast()
} }
} }
@ -2005,6 +2006,20 @@ class TimelineFragment :
} }
} }
private fun displayPromptToStopVoiceBroadcast() {
ConfirmationDialogBuilder
.show(
activity = requireActivity(),
askForReason = false,
confirmationRes = R.string.stop_voice_broadcast_content,
positiveRes = R.string.action_stop,
reasonHintRes = 0,
titleRes = R.string.stop_voice_broadcast_dialog_title
) {
timelineViewModel.handle(RoomDetailAction.VoiceBroadcastAction.Recording.StopConfirmed)
}
}
override fun onTapToReturnToCall() { override fun onTapToReturnToCall() {
callManager.getCurrentCall()?.let { call -> callManager.getCurrentCall()?.let { call ->
VectorCallActivity.newIntent( VectorCallActivity.newIntent(

View file

@ -634,7 +634,8 @@ class TimelineViewModel @AssistedInject constructor(
} }
VoiceBroadcastAction.Recording.Pause -> voiceBroadcastHelper.pauseVoiceBroadcast(room.roomId) VoiceBroadcastAction.Recording.Pause -> voiceBroadcastHelper.pauseVoiceBroadcast(room.roomId)
VoiceBroadcastAction.Recording.Resume -> voiceBroadcastHelper.resumeVoiceBroadcast(room.roomId) VoiceBroadcastAction.Recording.Resume -> voiceBroadcastHelper.resumeVoiceBroadcast(room.roomId)
VoiceBroadcastAction.Recording.Stop -> voiceBroadcastHelper.stopVoiceBroadcast(room.roomId) VoiceBroadcastAction.Recording.Stop -> _viewEvents.post(RoomDetailViewEvents.DisplayPromptToStopVoiceBroadcast)
VoiceBroadcastAction.Recording.StopConfirmed -> voiceBroadcastHelper.stopVoiceBroadcast(room.roomId)
is VoiceBroadcastAction.Listening.PlayOrResume -> voiceBroadcastHelper.playOrResumePlayback(action.voiceBroadcast) is VoiceBroadcastAction.Listening.PlayOrResume -> voiceBroadcastHelper.playOrResumePlayback(action.voiceBroadcast)
VoiceBroadcastAction.Listening.Pause -> voiceBroadcastHelper.pausePlayback() VoiceBroadcastAction.Listening.Pause -> voiceBroadcastHelper.pausePlayback()
VoiceBroadcastAction.Listening.Stop -> voiceBroadcastHelper.stopPlayback() VoiceBroadcastAction.Listening.Stop -> voiceBroadcastHelper.stopPlayback()

View file

@ -80,6 +80,9 @@ import im.vector.app.features.home.room.detail.AutoCompleter
import im.vector.app.features.home.room.detail.RoomDetailAction import im.vector.app.features.home.room.detail.RoomDetailAction
import im.vector.app.features.home.room.detail.RoomDetailAction.VoiceBroadcastAction import im.vector.app.features.home.room.detail.RoomDetailAction.VoiceBroadcastAction
import im.vector.app.features.home.room.detail.TimelineViewModel import im.vector.app.features.home.room.detail.TimelineViewModel
import im.vector.app.features.home.room.detail.composer.link.SetLinkFragment
import im.vector.app.features.home.room.detail.composer.link.SetLinkSharedAction
import im.vector.app.features.home.room.detail.composer.link.SetLinkSharedActionViewModel
import im.vector.app.features.home.room.detail.composer.voice.VoiceMessageRecorderView import im.vector.app.features.home.room.detail.composer.voice.VoiceMessageRecorderView
import im.vector.app.features.home.room.detail.timeline.action.MessageSharedActionViewModel import im.vector.app.features.home.room.detail.timeline.action.MessageSharedActionViewModel
import im.vector.app.features.home.room.detail.upgrade.MigrateRoomBottomSheet import im.vector.app.features.home.room.detail.upgrade.MigrateRoomBottomSheet
@ -147,6 +150,7 @@ class MessageComposerFragment : VectorBaseFragment<FragmentComposerBinding>(), A
private lateinit var sharedActionViewModel: MessageSharedActionViewModel private lateinit var sharedActionViewModel: MessageSharedActionViewModel
private val attachmentViewModel: AttachmentTypeSelectorViewModel by fragmentViewModel() private val attachmentViewModel: AttachmentTypeSelectorViewModel by fragmentViewModel()
private val attachmentActionsViewModel: AttachmentTypeSelectorSharedActionViewModel by viewModels() private val attachmentActionsViewModel: AttachmentTypeSelectorSharedActionViewModel by viewModels()
private val setLinkActionsViewModel: SetLinkSharedActionViewModel by viewModels()
private val composer: MessageComposerView get() { private val composer: MessageComposerView get() {
return if (vectorPreferences.isRichTextEditorEnabled()) { return if (vectorPreferences.isRichTextEditorEnabled()) {
@ -212,6 +216,14 @@ class MessageComposerFragment : VectorBaseFragment<FragmentComposerBinding>(), A
.onEach { onTypeSelected(it.attachmentType) } .onEach { onTypeSelected(it.attachmentType) }
.launchIn(lifecycleScope) .launchIn(lifecycleScope)
setLinkActionsViewModel.stream()
.onEach { when (it) {
is SetLinkSharedAction.Insert -> views.richTextComposerLayout.insertLink(it.link, it.text)
is SetLinkSharedAction.Set -> views.richTextComposerLayout.setLink(it.link)
SetLinkSharedAction.Remove -> views.richTextComposerLayout.removeLink()
} }
.launchIn(lifecycleScope)
messageComposerViewModel.stateFlow.map { it.isFullScreen } messageComposerViewModel.stateFlow.map { it.isFullScreen }
.distinctUntilChanged() .distinctUntilChanged()
.onEach { isFullScreen -> .onEach { isFullScreen ->
@ -385,6 +397,10 @@ class MessageComposerFragment : VectorBaseFragment<FragmentComposerBinding>(), A
override fun onFullScreenModeChanged() = withState(messageComposerViewModel) { state -> override fun onFullScreenModeChanged() = withState(messageComposerViewModel) { state ->
messageComposerViewModel.handle(MessageComposerAction.SetFullScreen(!state.isFullScreen)) messageComposerViewModel.handle(MessageComposerAction.SetFullScreen(!state.isFullScreen))
} }
override fun onSetLink(isTextSupported: Boolean, initialLink: String?) {
SetLinkFragment.show(isTextSupported, initialLink, childFragmentManager)
}
} }
} }

View file

@ -45,4 +45,5 @@ interface Callback : ComposerEditText.Callback {
fun onAddAttachment() fun onAddAttachment()
fun onExpandOrCompactChange() fun onExpandOrCompactChange()
fun onFullScreenModeChanged() fun onFullScreenModeChanged()
fun onSetLink(isTextSupported: Boolean, initialLink: String?)
} }

View file

@ -49,6 +49,7 @@ import im.vector.app.databinding.ComposerRichTextLayoutBinding
import im.vector.app.databinding.ViewRichTextMenuButtonBinding import im.vector.app.databinding.ViewRichTextMenuButtonBinding
import io.element.android.wysiwyg.EditorEditText import io.element.android.wysiwyg.EditorEditText
import io.element.android.wysiwyg.inputhandlers.models.InlineFormat import io.element.android.wysiwyg.inputhandlers.models.InlineFormat
import io.element.android.wysiwyg.inputhandlers.models.LinkAction
import io.element.android.wysiwyg.utils.RustErrorCollector import io.element.android.wysiwyg.utils.RustErrorCollector
import uniffi.wysiwyg_composer.ActionState import uniffi.wysiwyg_composer.ActionState
import uniffi.wysiwyg_composer.ComposerAction import uniffi.wysiwyg_composer.ComposerAction
@ -231,8 +232,25 @@ internal class RichTextComposerLayout @JvmOverloads constructor(
addRichTextMenuItem(R.drawable.ic_composer_strikethrough, R.string.rich_text_editor_format_strikethrough, ComposerAction.STRIKE_THROUGH) { addRichTextMenuItem(R.drawable.ic_composer_strikethrough, R.string.rich_text_editor_format_strikethrough, ComposerAction.STRIKE_THROUGH) {
views.richTextComposerEditText.toggleInlineFormat(InlineFormat.StrikeThrough) views.richTextComposerEditText.toggleInlineFormat(InlineFormat.StrikeThrough)
} }
addRichTextMenuItem(R.drawable.ic_composer_link, R.string.rich_text_editor_link, ComposerAction.LINK) {
views.richTextComposerEditText.getLinkAction()?.let {
when (it) {
LinkAction.InsertLink -> callback?.onSetLink(isTextSupported = true, initialLink = null)
is LinkAction.SetLink -> callback?.onSetLink(isTextSupported = false, initialLink = it.currentLink)
}
}
}
} }
fun setLink(link: String?) =
views.richTextComposerEditText.setLink(link)
fun insertLink(link: String, text: String) =
views.richTextComposerEditText.insertLink(link, text)
fun removeLink() =
views.richTextComposerEditText.removeLink()
@SuppressLint("ClickableViewAccessibility") @SuppressLint("ClickableViewAccessibility")
private fun disallowParentInterceptTouchEvent(view: View) { private fun disallowParentInterceptTouchEvent(view: View) {
view.setOnTouchListener { v, event -> view.setOnTouchListener { v, event ->

View file

@ -0,0 +1,30 @@
/*
* Copyright (c) 2022 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.app.features.home.room.detail.composer.link
import im.vector.app.core.platform.VectorViewModelAction
sealed class SetLinkAction : VectorViewModelAction {
data class LinkChanged(
val newLink: String
) : SetLinkAction()
data class Save(
val link: String,
val text: String,
) : SetLinkAction()
}

View file

@ -0,0 +1,131 @@
/*
* Copyright (c) 2021 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.app.features.home.room.detail.composer.link
import android.os.Bundle
import android.os.Parcelable
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.core.view.isGone
import androidx.fragment.app.FragmentManager
import androidx.fragment.app.viewModels
import androidx.lifecycle.lifecycleScope
import com.airbnb.mvrx.args
import com.airbnb.mvrx.fragmentViewModel
import com.airbnb.mvrx.withState
import dagger.hilt.android.AndroidEntryPoint
import im.vector.app.R
import im.vector.app.core.platform.VectorBaseDialogFragment
import im.vector.app.databinding.FragmentSetLinkBinding
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.parcelize.Parcelize
import reactivecircus.flowbinding.android.widget.textChanges
@AndroidEntryPoint
class SetLinkFragment :
VectorBaseDialogFragment<FragmentSetLinkBinding>() {
@Parcelize
data class Args(
val isTextSupported: Boolean,
val initialLink: String?,
) : Parcelable
private val viewModel: SetLinkViewModel by fragmentViewModel()
private val sharedActionViewModel: SetLinkSharedActionViewModel by viewModels(
ownerProducer = { requireParentFragment() }
)
private val args: Args by args()
override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentSetLinkBinding {
return FragmentSetLinkBinding.inflate(inflater, container, false)
}
companion object {
fun show(isTextSupported: Boolean, initialLink: String?, fragmentManager: FragmentManager) =
SetLinkFragment().apply {
setArguments(Args(isTextSupported, initialLink))
}.show(fragmentManager, "SetLinkBottomSheet")
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
views.link.setText(args.initialLink)
views.link.textChanges()
.onEach {
viewModel.handle(SetLinkAction.LinkChanged(it.toString()))
}
.launchIn(viewLifecycleOwner.lifecycleScope)
views.save.debouncedClicks {
viewModel.handle(
SetLinkAction.Save(
link = views.link.text.toString(),
text = views.text.text.toString(),
)
)
}
views.cancel.debouncedClicks(::onCancel)
views.remove.debouncedClicks(::onRemove)
viewModel.observeViewEvents {
when (it) {
is SetLinkViewEvents.SavedLinkAndText -> handleInsert(link = it.link, text = it.text)
is SetLinkViewEvents.SavedLink -> handleSet(link = it.link)
}
}
views.toolbar.setNavigationOnClickListener {
dismiss()
}
}
override fun invalidate() = withState(viewModel) { viewState ->
views.toolbar.title = getString(
if (viewState.initialLink != null) {
R.string.set_link_edit
} else {
R.string.set_link_create
}
)
views.remove.isGone = !viewState.removeVisible
views.save.isEnabled = viewState.saveEnabled
views.textLayout.isGone = !viewState.isTextSupported
}
private fun handleInsert(link: String, text: String) {
sharedActionViewModel.post(SetLinkSharedAction.Insert(text, link))
dismiss()
}
private fun handleSet(link: String) {
sharedActionViewModel.post(SetLinkSharedAction.Set(link))
dismiss()
}
private fun onRemove() {
sharedActionViewModel.post(SetLinkSharedAction.Remove)
dismiss()
}
private fun onCancel() = dismiss()
}

View file

@ -0,0 +1,37 @@
/*
* Copyright (c) 2022 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
package im.vector.app.features.home.room.detail.composer.link
import im.vector.app.core.platform.VectorSharedAction
import im.vector.app.core.platform.VectorSharedActionViewModel
import javax.inject.Inject
class SetLinkSharedActionViewModel @Inject constructor() :
VectorSharedActionViewModel<SetLinkSharedAction>()
sealed interface SetLinkSharedAction : VectorSharedAction {
data class Set(
val link: String,
) : SetLinkSharedAction
data class Insert(
val text: String,
val link: String,
) : SetLinkSharedAction
object Remove : SetLinkSharedAction
}

View file

@ -0,0 +1,31 @@
/*
* Copyright (c) 2021 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.app.features.home.room.detail.composer.link
import im.vector.app.core.platform.VectorViewEvents
sealed class SetLinkViewEvents : VectorViewEvents {
data class SavedLink(
val link: String,
) : SetLinkViewEvents()
data class SavedLinkAndText(
val link: String,
val text: String,
) : SetLinkViewEvents()
}

View file

@ -0,0 +1,55 @@
/*
* Copyright (c) 2021 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.app.features.home.room.detail.composer.link
import com.airbnb.mvrx.MavericksViewModelFactory
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import im.vector.app.core.di.MavericksAssistedViewModelFactory
import im.vector.app.core.di.hiltMavericksViewModelFactory
import im.vector.app.core.platform.VectorViewModel
class SetLinkViewModel @AssistedInject constructor(
@Assisted private val initialState: SetLinkViewState,
) : VectorViewModel<SetLinkViewState, SetLinkAction, SetLinkViewEvents>(initialState) {
@AssistedFactory
interface Factory : MavericksAssistedViewModelFactory<SetLinkViewModel, SetLinkViewState> {
override fun create(initialState: SetLinkViewState): SetLinkViewModel
}
companion object : MavericksViewModelFactory<SetLinkViewModel, SetLinkViewState> by hiltMavericksViewModelFactory()
override fun handle(action: SetLinkAction) = when (action) {
is SetLinkAction.LinkChanged -> handleLinkChanged(action.newLink)
is SetLinkAction.Save -> handleSave(action.link, action.text)
}
private fun handleLinkChanged(newLink: String) = setState {
copy(saveEnabled = newLink != initialLink.orEmpty())
}
private fun handleSave(
link: String,
text: String
) = if (initialState.isTextSupported) {
_viewEvents.post(SetLinkViewEvents.SavedLinkAndText(link, text))
} else {
_viewEvents.post(SetLinkViewEvents.SavedLink(link))
}
}

View file

@ -0,0 +1,34 @@
/*
* Copyright (c) 2021 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.app.features.home.room.detail.composer.link
import com.airbnb.mvrx.MavericksState
data class SetLinkViewState(
val isTextSupported: Boolean,
val initialLink: String?,
val saveEnabled: Boolean,
) : MavericksState {
constructor(args: SetLinkFragment.Args) : this(
isTextSupported = args.isTextSupported,
initialLink = args.initialLink,
saveEnabled = false,
)
val removeVisible = initialLink != null
}

View file

@ -122,10 +122,14 @@ abstract class MessageVoiceBroadcastListeningItem : AbsMessageVoiceBroadcastItem
private fun bindSeekBar(holder: Holder) { private fun bindSeekBar(holder: Holder) {
with(holder) { with(holder) {
durationView.text = formatPlaybackTime(duration) remainingTimeView.text = formatRemainingTime(duration)
elapsedTimeView.text = formatPlaybackTime(0)
seekBar.max = duration seekBar.max = duration
seekBar.setOnSeekBarChangeListener(object : SeekBar.OnSeekBarChangeListener { seekBar.setOnSeekBarChangeListener(object : SeekBar.OnSeekBarChangeListener {
override fun onProgressChanged(seekBar: SeekBar, progress: Int, fromUser: Boolean) = Unit override fun onProgressChanged(seekBar: SeekBar, progress: Int, fromUser: Boolean) {
remainingTimeView.text = formatRemainingTime(duration - progress)
elapsedTimeView.text = formatPlaybackTime(progress)
}
override fun onStartTrackingTouch(seekBar: SeekBar) { override fun onStartTrackingTouch(seekBar: SeekBar) {
isUserSeeking = true isUserSeeking = true
@ -156,6 +160,7 @@ abstract class MessageVoiceBroadcastListeningItem : AbsMessageVoiceBroadcastItem
} }
private fun formatPlaybackTime(time: Int) = DateUtils.formatElapsedTime((time / 1000).toLong()) private fun formatPlaybackTime(time: Int) = DateUtils.formatElapsedTime((time / 1000).toLong())
private fun formatRemainingTime(time: Int) = if (time < 1000) formatPlaybackTime(time) else String.format("-%s", formatPlaybackTime(time))
override fun unbind(holder: Holder) { override fun unbind(holder: Holder) {
super.unbind(holder) super.unbind(holder)
@ -177,7 +182,8 @@ abstract class MessageVoiceBroadcastListeningItem : AbsMessageVoiceBroadcastItem
val fastBackwardButton by bind<ImageButton>(R.id.fastBackwardButton) val fastBackwardButton by bind<ImageButton>(R.id.fastBackwardButton)
val fastForwardButton by bind<ImageButton>(R.id.fastForwardButton) val fastForwardButton by bind<ImageButton>(R.id.fastForwardButton)
val seekBar by bind<SeekBar>(R.id.seekBar) val seekBar by bind<SeekBar>(R.id.seekBar)
val durationView by bind<TextView>(R.id.playbackDuration) val remainingTimeView by bind<TextView>(R.id.remainingTime)
val elapsedTimeView by bind<TextView>(R.id.elapsedTime)
val broadcasterNameMetadata by bind<VoiceBroadcastMetadataView>(R.id.broadcasterNameMetadata) val broadcasterNameMetadata by bind<VoiceBroadcastMetadataView>(R.id.broadcasterNameMetadata)
val voiceBroadcastMetadata by bind<VoiceBroadcastMetadataView>(R.id.voiceBroadcastMetadata) val voiceBroadcastMetadata by bind<VoiceBroadcastMetadataView>(R.id.voiceBroadcastMetadata)
val listenersCountMetadata by bind<VoiceBroadcastMetadataView>(R.id.listenersCountMetadata) val listenersCountMetadata by bind<VoiceBroadcastMetadataView>(R.id.listenersCountMetadata)

View file

@ -118,6 +118,7 @@ class NotificationBroadcastReceiver : BroadcastReceiver() {
private fun handleSmartReply(intent: Intent, context: Context) { private fun handleSmartReply(intent: Intent, context: Context) {
val message = getReplyMessage(intent) val message = getReplyMessage(intent)
val roomId = intent.getStringExtra(KEY_ROOM_ID) val roomId = intent.getStringExtra(KEY_ROOM_ID)
val threadId = intent.getStringExtra(KEY_THREAD_ID)
if (message.isNullOrBlank() || roomId.isNullOrBlank()) { if (message.isNullOrBlank() || roomId.isNullOrBlank()) {
// ignore this event // ignore this event
@ -126,13 +127,20 @@ class NotificationBroadcastReceiver : BroadcastReceiver() {
} }
activeSessionHolder.getActiveSession().let { session -> activeSessionHolder.getActiveSession().let { session ->
session.getRoom(roomId)?.let { room -> session.getRoom(roomId)?.let { room ->
sendMatrixEvent(message, session, room, context) sendMatrixEvent(message, threadId, session, room, context)
} }
} }
} }
private fun sendMatrixEvent(message: String, session: Session, room: Room, context: Context?) { private fun sendMatrixEvent(message: String, threadId: String?, session: Session, room: Room, context: Context?) {
room.sendService().sendTextMessage(message) if (threadId != null) {
room.relationService().replyInThread(
rootThreadEventId = threadId,
replyInThreadText = message,
)
} else {
room.sendService().sendTextMessage(message)
}
// Create a new event to be displayed in the notification drawer, right now // Create a new event to be displayed in the notification drawer, right now
@ -148,7 +156,7 @@ class NotificationBroadcastReceiver : BroadcastReceiver() {
body = message, body = message,
imageUriString = null, imageUriString = null,
roomId = room.roomId, roomId = room.roomId,
threadId = null, // needs to be changed: https://github.com/vector-im/element-android/issues/7475 threadId = threadId,
roomName = room.roomSummary()?.displayName ?: room.roomId, roomName = room.roomSummary()?.displayName ?: room.roomId,
roomIsDirect = room.roomSummary()?.isDirect == true, roomIsDirect = room.roomSummary()?.isDirect == true,
outGoingMessage = true, outGoingMessage = true,
@ -223,6 +231,7 @@ class NotificationBroadcastReceiver : BroadcastReceiver() {
companion object { companion object {
const val KEY_ROOM_ID = "roomID" const val KEY_ROOM_ID = "roomID"
const val KEY_THREAD_ID = "threadID"
const val KEY_TEXT_REPLY = "key_text_reply" const val KEY_TEXT_REPLY = "key_text_reply"
} }
} }

View file

@ -657,7 +657,7 @@ class NotificationUtils @Inject constructor(
// Quick reply // Quick reply
if (!roomInfo.hasSmartReplyError) { if (!roomInfo.hasSmartReplyError) {
buildQuickReplyIntent(roomInfo.roomId, senderDisplayNameForReplyCompat)?.let { replyPendingIntent -> buildQuickReplyIntent(roomInfo.roomId, threadId, senderDisplayNameForReplyCompat)?.let { replyPendingIntent ->
val remoteInput = RemoteInput.Builder(NotificationBroadcastReceiver.KEY_TEXT_REPLY) val remoteInput = RemoteInput.Builder(NotificationBroadcastReceiver.KEY_TEXT_REPLY)
.setLabel(stringProvider.getString(R.string.action_quick_reply)) .setLabel(stringProvider.getString(R.string.action_quick_reply))
.build() .build()
@ -892,13 +892,17 @@ class NotificationUtils @Inject constructor(
However, for Android devices running Marshmallow and below (API level 23 and below), However, for Android devices running Marshmallow and below (API level 23 and below),
it will be more appropriate to use an activity. Since you have to provide your own UI. it will be more appropriate to use an activity. Since you have to provide your own UI.
*/ */
private fun buildQuickReplyIntent(roomId: String, senderName: String?): PendingIntent? { private fun buildQuickReplyIntent(roomId: String, threadId: String?, senderName: String?): PendingIntent? {
val intent: Intent val intent: Intent
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
intent = Intent(context, NotificationBroadcastReceiver::class.java) intent = Intent(context, NotificationBroadcastReceiver::class.java)
intent.action = actionIds.smartReply intent.action = actionIds.smartReply
intent.data = createIgnoredUri(roomId) intent.data = createIgnoredUri(roomId)
intent.putExtra(NotificationBroadcastReceiver.KEY_ROOM_ID, roomId) intent.putExtra(NotificationBroadcastReceiver.KEY_ROOM_ID, roomId)
threadId?.let {
intent.putExtra(NotificationBroadcastReceiver.KEY_THREAD_ID, it)
}
return PendingIntent.getBroadcast( return PendingIntent.getBroadcast(
context, context,
clock.epochMillis().toInt(), clock.epochMillis().toInt(),

View file

@ -223,7 +223,6 @@ class VectorSettingsDevicesFragment :
override fun onViewAllClicked() { override fun onViewAllClicked() {
viewNavigator.navigateToOtherSessions( viewNavigator.navigateToOtherSessions(
requireActivity(), requireActivity(),
R.string.device_manager_header_section_security_recommendations_title,
DeviceManagerFilterType.UNVERIFIED, DeviceManagerFilterType.UNVERIFIED,
excludeCurrentDevice = true excludeCurrentDevice = true
) )
@ -233,7 +232,6 @@ class VectorSettingsDevicesFragment :
override fun onViewAllClicked() { override fun onViewAllClicked() {
viewNavigator.navigateToOtherSessions( viewNavigator.navigateToOtherSessions(
requireActivity(), requireActivity(),
R.string.device_manager_header_section_security_recommendations_title,
DeviceManagerFilterType.INACTIVE, DeviceManagerFilterType.INACTIVE,
excludeCurrentDevice = true excludeCurrentDevice = true
) )
@ -447,7 +445,6 @@ class VectorSettingsDevicesFragment :
override fun onViewAllOtherSessionsClicked() { override fun onViewAllOtherSessionsClicked() {
viewNavigator.navigateToOtherSessions( viewNavigator.navigateToOtherSessions(
context = requireActivity(), context = requireActivity(),
titleResourceId = R.string.device_manager_sessions_other_title,
defaultFilter = DeviceManagerFilterType.ALL_SESSIONS, defaultFilter = DeviceManagerFilterType.ALL_SESSIONS,
excludeCurrentDevice = true excludeCurrentDevice = true
) )

View file

@ -31,12 +31,11 @@ class VectorSettingsDevicesViewNavigator @Inject constructor() {
fun navigateToOtherSessions( fun navigateToOtherSessions(
context: Context, context: Context,
titleResourceId: Int,
defaultFilter: DeviceManagerFilterType, defaultFilter: DeviceManagerFilterType,
excludeCurrentDevice: Boolean, excludeCurrentDevice: Boolean,
) { ) {
context.startActivity( context.startActivity(
OtherSessionsActivity.newIntent(context, titleResourceId, defaultFilter, excludeCurrentDevice) OtherSessionsActivity.newIntent(context, defaultFilter, excludeCurrentDevice)
) )
} }

View file

@ -27,7 +27,7 @@ import im.vector.app.core.epoxy.VectorEpoxyHolder
import im.vector.app.core.epoxy.VectorEpoxyModel import im.vector.app.core.epoxy.VectorEpoxyModel
import im.vector.app.core.utils.DimensionConverter import im.vector.app.core.utils.DimensionConverter
private const val EXTRA_TOP_MARGIN_DP = 48 private const val EXTRA_TOP_MARGIN_DP = 32
@EpoxyModelClass @EpoxyModelClass
abstract class SessionDetailsHeaderItem : VectorEpoxyModel<SessionDetailsHeaderItem.Holder>(R.layout.item_session_details_header) { abstract class SessionDetailsHeaderItem : VectorEpoxyModel<SessionDetailsHeaderItem.Holder>(R.layout.item_session_details_header) {

View file

@ -53,6 +53,9 @@ class SecurityRecommendationView @JvmOverloads constructor(
setImage(it) setImage(it)
} }
setOnClickListener {
callback?.onViewAllClicked()
}
views.recommendationViewAllButton.setOnClickListener { views.recommendationViewAllButton.setOnClickListener {
callback?.onViewAllClicked() callback?.onViewAllClicked()
} }

View file

@ -75,7 +75,7 @@ class SessionInfoView @JvmOverloads constructor(
renderDeviceLastSeenDetails( renderDeviceLastSeenDetails(
sessionInfoViewState.deviceFullInfo.isInactive, sessionInfoViewState.deviceFullInfo.isInactive,
sessionInfoViewState.deviceFullInfo.deviceInfo, sessionInfoViewState.deviceFullInfo.deviceInfo,
sessionInfoViewState.isLastSeenDetailsVisible, sessionInfoViewState.isLastActivityVisible,
sessionInfoViewState.isShowingIpAddress, sessionInfoViewState.isShowingIpAddress,
dateFormatter, dateFormatter,
drawableProvider, drawableProvider,
@ -197,7 +197,7 @@ class SessionInfoView @JvmOverloads constructor(
} else { } else {
views.sessionInfoLastActivityTextView.isGone = true views.sessionInfoLastActivityTextView.isGone = true
} }
views.sessionInfoLastIPAddressTextView.setTextOrHide(deviceInfo.lastSeenIp?.takeIf { isLastSeenDetailsVisible && isShowingIpAddress }) views.sessionInfoLastIPAddressTextView.setTextOrHide(deviceInfo.lastSeenIp?.takeIf { isShowingIpAddress })
} }
private fun renderDetailsButton(isDetailsButtonVisible: Boolean) { private fun renderDetailsButton(isDetailsButtonVisible: Boolean) {

View file

@ -24,6 +24,6 @@ data class SessionInfoViewState(
val isVerifyButtonVisible: Boolean = true, val isVerifyButtonVisible: Boolean = true,
val isDetailsButtonVisible: Boolean = true, val isDetailsButtonVisible: Boolean = true,
val isLearnMoreLinkVisible: Boolean = false, val isLearnMoreLinkVisible: Boolean = false,
val isLastSeenDetailsVisible: Boolean = false, val isLastActivityVisible: Boolean = false,
val isShowingIpAddress: Boolean = false, val isShowingIpAddress: Boolean = false,
) )

View file

@ -16,6 +16,7 @@
package im.vector.app.features.settings.devices.v2.more package im.vector.app.features.settings.devices.v2.more
import android.content.DialogInterface
import android.os.Bundle import android.os.Bundle
import android.os.Parcelable import android.os.Parcelable
import android.view.LayoutInflater import android.view.LayoutInflater
@ -42,6 +43,8 @@ class SessionLearnMoreBottomSheet : VectorBaseBottomSheetDialogFragment<BottomSh
override val showExpanded = true override val showExpanded = true
var onDismiss: (() -> Unit)? = null
override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): BottomSheetSessionLearnMoreBinding { override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): BottomSheetSessionLearnMoreBinding {
return BottomSheetSessionLearnMoreBinding.inflate(inflater, container, false) return BottomSheetSessionLearnMoreBinding.inflate(inflater, container, false)
} }
@ -57,6 +60,11 @@ class SessionLearnMoreBottomSheet : VectorBaseBottomSheetDialogFragment<BottomSh
} }
} }
override fun onDismiss(dialog: DialogInterface) {
super.onDismiss(dialog)
onDismiss?.invoke()
}
override fun invalidate() = withState(viewModel) { viewState -> override fun invalidate() = withState(viewModel) { viewState ->
super.invalidate() super.invalidate()
views.bottomSheetSessionLearnMoreTitle.text = viewState.title views.bottomSheetSessionLearnMoreTitle.text = viewState.title
@ -65,11 +73,12 @@ class SessionLearnMoreBottomSheet : VectorBaseBottomSheetDialogFragment<BottomSh
companion object { companion object {
fun show(fragmentManager: FragmentManager, args: Args) { fun show(fragmentManager: FragmentManager, args: Args): SessionLearnMoreBottomSheet {
val bottomSheet = SessionLearnMoreBottomSheet() val bottomSheet = SessionLearnMoreBottomSheet()
bottomSheet.isCancelable = true bottomSheet.isCancelable = true
bottomSheet.setArguments(args) bottomSheet.setArguments(args)
bottomSheet.show(fragmentManager, "SessionLearnMoreBottomSheet") bottomSheet.show(fragmentManager, "SessionLearnMoreBottomSheet")
return bottomSheet
} }
} }
} }

View file

@ -20,7 +20,6 @@ import android.content.Context
import android.content.Intent import android.content.Intent
import android.os.Bundle import android.os.Bundle
import android.view.View import android.view.View
import androidx.annotation.StringRes
import com.airbnb.mvrx.Mavericks import com.airbnb.mvrx.Mavericks
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import im.vector.app.core.extensions.addFragment import im.vector.app.core.extensions.addFragment
@ -48,13 +47,11 @@ class OtherSessionsActivity : SimpleFragmentActivity() {
companion object { companion object {
fun newIntent( fun newIntent(
context: Context, context: Context,
@StringRes
titleResourceId: Int,
defaultFilter: DeviceManagerFilterType, defaultFilter: DeviceManagerFilterType,
excludeCurrentDevice: Boolean, excludeCurrentDevice: Boolean,
): Intent { ): Intent {
return Intent(context, OtherSessionsActivity::class.java).apply { return Intent(context, OtherSessionsActivity::class.java).apply {
putExtra(Mavericks.KEY_ARG, OtherSessionsArgs(titleResourceId, defaultFilter, excludeCurrentDevice)) putExtra(Mavericks.KEY_ARG, OtherSessionsArgs(defaultFilter, excludeCurrentDevice))
} }
} }
} }

View file

@ -17,14 +17,11 @@
package im.vector.app.features.settings.devices.v2.othersessions package im.vector.app.features.settings.devices.v2.othersessions
import android.os.Parcelable import android.os.Parcelable
import androidx.annotation.StringRes
import im.vector.app.features.settings.devices.v2.filter.DeviceManagerFilterType import im.vector.app.features.settings.devices.v2.filter.DeviceManagerFilterType
import kotlinx.parcelize.Parcelize import kotlinx.parcelize.Parcelize
@Parcelize @Parcelize
data class OtherSessionsArgs( data class OtherSessionsArgs(
@StringRes
val titleResourceId: Int,
val defaultFilter: DeviceManagerFilterType, val defaultFilter: DeviceManagerFilterType,
val excludeCurrentDevice: Boolean, val excludeCurrentDevice: Boolean,
) : Parcelable ) : Parcelable

View file

@ -182,7 +182,9 @@ class OtherSessionsFragment :
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
setupToolbar(views.otherSessionsToolbar).setTitle(args.titleResourceId).allowBack() setupToolbar(views.otherSessionsToolbar)
.setTitle(R.string.device_manager_sessions_other_title)
.allowBack()
observeViewEvents() observeViewEvents()
initFilterView() initFilterView()
} }
@ -225,6 +227,7 @@ class OtherSessionsFragment :
override fun invalidate() = withState(viewModel) { state -> override fun invalidate() = withState(viewModel) { state ->
updateLoading(state.isLoading) updateLoading(state.isLoading)
updateFilterView(state.isSelectModeEnabled)
if (state.devices is Success) { if (state.devices is Success) {
val devices = state.devices.invoke() val devices = state.devices.invoke()
renderDevices(devices, state.currentFilter, state.isShowingIpAddress) renderDevices(devices, state.currentFilter, state.isShowingIpAddress)
@ -240,13 +243,17 @@ class OtherSessionsFragment :
} }
} }
private fun updateFilterView(isSelectModeEnabled: Boolean) {
views.otherSessionsFilterFrameLayout.isVisible = isSelectModeEnabled.not()
}
private fun updateToolbar(devices: List<DeviceFullInfo>, isSelectModeEnabled: Boolean) { private fun updateToolbar(devices: List<DeviceFullInfo>, isSelectModeEnabled: Boolean) {
invalidateOptionsMenu() invalidateOptionsMenu()
val title = if (isSelectModeEnabled) { val title = if (isSelectModeEnabled) {
val selection = devices.count { it.isSelected } val selection = devices.count { it.isSelected }
stringProvider.getQuantityString(R.plurals.x_selected, selection, selection) stringProvider.getQuantityString(R.plurals.x_selected, selection, selection)
} else { } else {
getString(args.titleResourceId) getString(R.string.device_manager_sessions_other_title)
} }
toolbar?.title = title toolbar?.title = title
} }
@ -341,6 +348,8 @@ class OtherSessionsFragment :
override fun onOtherSessionLongClicked(deviceId: String) = withState(viewModel) { state -> override fun onOtherSessionLongClicked(deviceId: String) = withState(viewModel) { state ->
if (!state.isSelectModeEnabled) { if (!state.isSelectModeEnabled) {
enableSelectMode(true, deviceId) enableSelectMode(true, deviceId)
} else {
viewModel.handle(OtherSessionsAction.ToggleSelectionForDevice(deviceId))
} }
} }

View file

@ -224,7 +224,7 @@ class SessionOverviewFragment :
isVerifyButtonVisible = isCurrentSession || viewState.isCurrentSessionTrusted, isVerifyButtonVisible = isCurrentSession || viewState.isCurrentSessionTrusted,
isDetailsButtonVisible = false, isDetailsButtonVisible = false,
isLearnMoreLinkVisible = deviceInfo.roomEncryptionTrustLevel != RoomEncryptionTrustLevel.Default, isLearnMoreLinkVisible = deviceInfo.roomEncryptionTrustLevel != RoomEncryptionTrustLevel.Default,
isLastSeenDetailsVisible = !isCurrentSession, isLastActivityVisible = !isCurrentSession,
isShowingIpAddress = viewState.isShowingIpAddress, isShowingIpAddress = viewState.isShowingIpAddress,
) )
views.sessionOverviewInfo.render(infoViewState, dateFormatter, drawableProvider, colorProvider, stringProvider) views.sessionOverviewInfo.render(infoViewState, dateFormatter, drawableProvider, colorProvider, stringProvider)

View file

@ -20,6 +20,7 @@ import android.os.Bundle
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.view.ViewTreeObserver
import androidx.core.widget.doOnTextChanged import androidx.core.widget.doOnTextChanged
import com.airbnb.mvrx.fragmentViewModel import com.airbnb.mvrx.fragmentViewModel
import com.airbnb.mvrx.withState import com.airbnb.mvrx.withState
@ -62,12 +63,24 @@ class RenameSessionFragment :
} }
private fun initEditText() { private fun initEditText() {
views.renameSessionEditText.showKeyboard(andRequestFocus = true) showKeyboard()
views.renameSessionEditText.doOnTextChanged { text, _, _, _ -> views.renameSessionEditText.doOnTextChanged { text, _, _, _ ->
viewModel.handle(RenameSessionAction.EditLocally(text.toString())) viewModel.handle(RenameSessionAction.EditLocally(text.toString()))
} }
} }
private fun showKeyboard() {
val focusChangeListener = object : ViewTreeObserver.OnWindowFocusChangeListener {
override fun onWindowFocusChanged(hasFocus: Boolean) {
if (hasFocus) {
views.renameSessionEditText.showKeyboard(andRequestFocus = true)
}
views.renameSessionEditText.viewTreeObserver.removeOnWindowFocusChangeListener(this)
}
}
views.renameSessionEditText.viewTreeObserver.addOnWindowFocusChangeListener(focusChangeListener)
}
private fun initSaveButton() { private fun initSaveButton() {
views.renameSessionSave.debouncedClicks { views.renameSessionSave.debouncedClicks {
viewModel.handle(RenameSessionAction.SaveModifications) viewModel.handle(RenameSessionAction.SaveModifications)
@ -89,7 +102,9 @@ class RenameSessionFragment :
title = getString(R.string.device_manager_learn_more_session_rename_title), title = getString(R.string.device_manager_learn_more_session_rename_title),
description = getString(R.string.device_manager_learn_more_session_rename), description = getString(R.string.device_manager_learn_more_session_rename),
) )
SessionLearnMoreBottomSheet.show(childFragmentManager, args) SessionLearnMoreBottomSheet
.show(childFragmentManager, args)
.onDismiss = { showKeyboard() }
} }
private fun observeViewEvents() { private fun observeViewEvents() {

View file

@ -63,7 +63,7 @@ class StartAppViewModel @AssistedInject constructor(
} }
private suspend fun eagerlyInitializeSession() { private suspend fun eagerlyInitializeSession() {
sessionHolder.getOrInitializeSession(startSync = true) sessionHolder.getOrInitializeSession()
} }
private fun handleLongProcessing() { private fun handleLongProcessing() {

View file

@ -24,6 +24,7 @@ import android.graphics.drawable.Drawable
import android.util.TypedValue import android.util.TypedValue
import androidx.annotation.AttrRes import androidx.annotation.AttrRes
import androidx.annotation.ColorInt import androidx.annotation.ColorInt
import androidx.annotation.StyleRes
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.core.content.edit import androidx.core.content.edit
import androidx.core.graphics.drawable.DrawableCompat import androidx.core.graphics.drawable.DrawableCompat
@ -113,19 +114,16 @@ object ThemeUtils {
*/ */
fun setApplicationTheme(context: Context, aTheme: String) { fun setApplicationTheme(context: Context, aTheme: String) {
currentTheme.set(aTheme) currentTheme.set(aTheme)
context.setTheme( context.setTheme(themeToRes(context, aTheme))
when (aTheme) {
SYSTEM_THEME_VALUE -> if (isSystemDarkTheme(context.resources)) R.style.Theme_Vector_Dark else R.style.Theme_Vector_Light
THEME_DARK_VALUE -> R.style.Theme_Vector_Dark
THEME_BLACK_VALUE -> R.style.Theme_Vector_Black
else -> R.style.Theme_Vector_Light
}
)
// Clear the cache // Clear the cache
mColorByAttr.clear() mColorByAttr.clear()
} }
@StyleRes
fun getApplicationThemeRes(context: Context) =
themeToRes(context, currentTheme.get())
/** /**
* Set the activity theme according to the selected one. Default is Light, so if this is the current * Set the activity theme according to the selected one. Default is Light, so if this is the current
* theme, the theme is not changed. * theme, the theme is not changed.
@ -200,4 +198,13 @@ object ThemeUtils {
DrawableCompat.setTint(tinted, color) DrawableCompat.setTint(tinted, color)
return tinted return tinted
} }
@StyleRes
private fun themeToRes(context: Context, theme: String): Int =
when (theme) {
SYSTEM_THEME_VALUE -> if (isSystemDarkTheme(context.resources)) R.style.Theme_Vector_Dark else R.style.Theme_Vector_Light
THEME_DARK_VALUE -> R.style.Theme_Vector_Dark
THEME_BLACK_VALUE -> R.style.Theme_Vector_Black
else -> R.style.Theme_Vector_Light
}
} }

View file

@ -0,0 +1,12 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="44dp"
android:height="44dp"
android:viewportWidth="44"
android:viewportHeight="44">
<path
android:pathData="M22.566,16.151L23.101,15.616C24.577,14.14 26.956,14.126 28.415,15.585C29.874,17.044 29.86,19.423 28.383,20.899L25.844,23.438C24.368,24.915 21.989,24.929 20.53,23.47M21.434,27.849L20.899,28.383C19.423,29.86 17.044,29.874 15.585,28.415C14.126,26.956 14.14,24.577 15.616,23.101L18.156,20.562C19.632,19.086 22.011,19.071 23.47,20.53"
android:strokeWidth="1.5"
android:fillColor="#00000000"
android:strokeColor="#8D97A5"
android:strokeLineCap="round"/>
</vector>

View file

@ -2,9 +2,7 @@
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:orientation="vertical" android:orientation="vertical">
android:paddingHorizontal="24dp"
android:paddingBottom="32dp">
<View <View
android:layout_width="36dp" android:layout_width="36dp"
@ -18,75 +16,102 @@
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginTop="12dp" android:layout_marginTop="12dp"
android:paddingHorizontal="24dp"
android:text="@string/device_manager_filter_bottom_sheet_title" /> android:text="@string/device_manager_filter_bottom_sheet_title" />
<RadioGroup <ScrollView
android:id="@+id/filterOptionsRadioGroup"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="match_parent"
android:layout_marginTop="24dp" android:clipToPadding="false"
android:layoutDirection="rtl" android:paddingHorizontal="24dp"
android:showDividers="none"> android:paddingBottom="32dp"
android:scrollbarStyle="outsideOverlay">
<RadioButton <RadioGroup
android:id="@+id/filterOptionAllSessionsRadioButton" android:id="@+id/filterOptionsRadioGroup"
style="@style/TextAppearance.Vector.Subtitle.Medium.DevicesManagement"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:checked="true" android:paddingTop="24dp"
android:minHeight="0dp" android:showDividers="none">
android:text="@string/device_manager_filter_option_all_sessions" />
<RadioButton <RadioButton
android:id="@+id/filterOptionVerifiedRadioButton" android:id="@+id/filterOptionAllSessionsRadioButton"
style="@style/TextAppearance.Vector.Subtitle.Medium.DevicesManagement" style="@style/TextAppearance.Vector.Subtitle.Medium.DevicesManagement"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginTop="24dp" android:layout_gravity="start"
android:minHeight="0dp" android:background="?android:selectableItemBackground"
android:text="@string/device_manager_filter_option_verified" /> android:button="@null"
android:checked="true"
android:drawableEnd="?android:attr/listChoiceIndicatorSingle"
android:minHeight="0dp"
android:text="@string/device_manager_filter_option_all_sessions"
android:textAlignment="textStart" />
<TextView <RadioButton
android:id="@+id/filterOptionVerifiedTextView" android:id="@+id/filterOptionVerifiedRadioButton"
style="@style/TextAppearance.Vector.Body.DevicesManagement" style="@style/TextAppearance.Vector.Subtitle.Medium.DevicesManagement"
android:layout_width="wrap_content" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_gravity="end" android:layout_gravity="start"
android:text="@string/device_manager_filter_option_verified_description" /> android:layout_marginTop="24dp"
android:background="?android:selectableItemBackground"
android:button="@null"
android:drawableEnd="?android:attr/listChoiceIndicatorSingle"
android:minHeight="0dp"
android:text="@string/device_manager_filter_option_verified"
android:textAlignment="textStart" />
<RadioButton <TextView
android:id="@+id/filterOptionUnverifiedRadioButton" android:id="@+id/filterOptionVerifiedTextView"
style="@style/TextAppearance.Vector.Subtitle.Medium.DevicesManagement" style="@style/TextAppearance.Vector.Body.DevicesManagement"
android:layout_width="match_parent" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginTop="16dp" android:text="@string/device_manager_filter_option_verified_description" />
android:minHeight="0dp"
android:text="@string/device_manager_filter_option_unverified" />
<TextView <RadioButton
android:id="@+id/filterOptionUnverifiedTextView" android:id="@+id/filterOptionUnverifiedRadioButton"
style="@style/TextAppearance.Vector.Body.DevicesManagement" style="@style/TextAppearance.Vector.Subtitle.Medium.DevicesManagement"
android:layout_width="wrap_content" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_gravity="end" android:layout_gravity="start"
android:text="@string/device_manager_filter_option_unverified_description" /> android:layout_marginTop="16dp"
android:background="?android:selectableItemBackground"
android:button="@null"
android:drawableEnd="?android:attr/listChoiceIndicatorSingle"
android:minHeight="0dp"
android:text="@string/device_manager_filter_option_unverified"
android:textAlignment="textStart" />
<RadioButton <TextView
android:id="@+id/filterOptionInactiveRadioButton" android:id="@+id/filterOptionUnverifiedTextView"
style="@style/TextAppearance.Vector.Subtitle.Medium.DevicesManagement" style="@style/TextAppearance.Vector.Body.DevicesManagement"
android:layout_width="match_parent" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginTop="16dp" android:text="@string/device_manager_filter_option_unverified_description" />
android:minHeight="0dp"
android:text="@string/device_manager_filter_option_inactive" />
<TextView <RadioButton
android:id="@+id/filterOptionInactiveTextView" android:id="@+id/filterOptionInactiveRadioButton"
style="@style/TextAppearance.Vector.Body.DevicesManagement" style="@style/TextAppearance.Vector.Subtitle.Medium.DevicesManagement"
android:layout_width="wrap_content" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_gravity="end" /> android:layout_gravity="start"
android:layout_marginTop="16dp"
android:background="?android:selectableItemBackground"
android:button="@null"
android:drawableEnd="?android:attr/listChoiceIndicatorSingle"
android:minHeight="0dp"
android:text="@string/device_manager_filter_option_inactive"
android:textAlignment="textStart" />
</RadioGroup> <TextView
android:id="@+id/filterOptionInactiveTextView"
style="@style/TextAppearance.Vector.Body.DevicesManagement"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
</RadioGroup>
</ScrollView>
</LinearLayout> </LinearLayout>

View file

@ -69,7 +69,7 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginTop="?attr/actionBarSize" android:layout_marginTop="?attr/actionBarSize"
android:layout_marginBottom="32dp" android:layout_marginBottom="16dp"
app:layout_collapseMode="parallax" app:layout_collapseMode="parallax"
app:sessionsListHeaderDescription="@string/device_manager_sessions_other_description" app:sessionsListHeaderDescription="@string/device_manager_sessions_other_description"
app:sessionsListHeaderHasLearnMoreLink="false" app:sessionsListHeaderHasLearnMoreLink="false"
@ -81,7 +81,7 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginStart="16dp" android:layout_marginStart="16dp"
android:layout_marginTop="?attr/actionBarSize" android:layout_marginTop="?attr/actionBarSize"
android:layout_marginBottom="32dp" android:layout_marginBottom="16dp"
android:paddingTop="20dp" android:paddingTop="20dp"
android:visibility="gone" android:visibility="gone"
app:layout_collapseMode="parallax" app:layout_collapseMode="parallax"

View file

@ -47,6 +47,7 @@
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginHorizontal="8dp" android:layout_marginHorizontal="8dp"
android:layout_marginTop="4dp"
android:text="@string/device_manager_session_overview_signout" android:text="@string/device_manager_session_overview_signout"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0" app:layout_constraintHorizontal_bias="0"

View file

@ -0,0 +1,117 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.google.android.material.appbar.AppBarLayout
android:id="@+id/appBarLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<com.google.android.material.appbar.MaterialToolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?actionBarSize"
app:navigationIcon="@drawable/ic_x_18dp"
app:title="@string/set_link_create" />
</com.google.android.material.appbar.AppBarLayout>
<ScrollView
android:layout_width="match_parent"
android:layout_height="0dp"
app:layout_constraintBottom_toTopOf="@id/save"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHeight_min="100dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/appBarLayout">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="16dp">
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/textLayout"
style="@style/Widget.Vector.TextInputLayout.Form"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp"
android:hint="@string/set_link_text">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/text"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/linkLayout"
style="@style/Widget.Vector.TextInputLayout.Form"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp"
android:hint="@string/set_link_link">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/link"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="textUri" />
</com.google.android.material.textfield.TextInputLayout>
</LinearLayout>
</ScrollView>
<Button
android:id="@+id/save"
style="@style/Widget.Vector.Button.CallToAction"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="16dp"
android:height="56dp"
android:text="@string/action_save"
android:textAllCaps="false"
app:iconGravity="textStart"
app:layout_constraintBottom_toTopOf="@id/remove"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" />
<Button
android:id="@+id/remove"
style="@style/Widget.Vector.Button.Destructive"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="16dp"
android:height="56dp"
android:text="@string/action_remove"
android:textAllCaps="false"
android:visibility="gone"
app:iconGravity="textStart"
app:layout_constraintBottom_toTopOf="@id/cancel"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
tools:visibility="visible" />
<Button
android:id="@+id/cancel"
style="@style/Widget.Vector.Button.Outlined"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="16dp"
android:layout_marginBottom="8dp"
android:height="56dp"
android:text="@string/action_cancel"
android:textAllCaps="false"
app:iconGravity="textStart"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -75,7 +75,7 @@
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginHorizontal="16dp" android:layout_marginHorizontal="16dp"
android:layout_marginVertical="16dp" android:layout_marginVertical="4dp"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/deviceListHeaderCurrentSession" /> app:layout_constraintTop_toBottomOf="@id/deviceListHeaderCurrentSession" />

View file

@ -5,9 +5,15 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:foreground="?selectableItemBackground" android:foreground="?selectableItemBackground"
android:paddingHorizontal="8dp"
android:paddingTop="8dp"> android:paddingTop="8dp">
<androidx.constraintlayout.widget.Guideline
android:id="@+id/startGuideline"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
app:layout_constraintGuide_begin="8dp" />
<View <View
android:id="@+id/otherSessionItemBackground" android:id="@+id/otherSessionItemBackground"
android:layout_width="0dp" android:layout_width="0dp"
@ -29,7 +35,7 @@
android:contentDescription="@string/a11y_device_manager_device_type_mobile" android:contentDescription="@string/a11y_device_manager_device_type_mobile"
android:padding="8dp" android:padding="8dp"
app:layout_constraintBottom_toBottomOf="@id/otherSessionItemBackground" app:layout_constraintBottom_toBottomOf="@id/otherSessionItemBackground"
app:layout_constraintStart_toStartOf="@id/otherSessionItemBackground" app:layout_constraintStart_toStartOf="@id/startGuideline"
app:layout_constraintTop_toTopOf="@id/otherSessionItemBackground" app:layout_constraintTop_toTopOf="@id/otherSessionItemBackground"
tools:src="@drawable/ic_device_type_mobile" /> tools:src="@drawable/ic_device_type_mobile" />
@ -52,8 +58,8 @@
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginStart="16dp" android:layout_marginStart="16dp"
android:layout_marginEnd="8dp"
android:layout_marginTop="8dp" android:layout_marginTop="8dp"
android:layout_marginEnd="8dp"
android:ellipsize="end" android:ellipsize="end"
android:lines="1" android:lines="1"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
@ -89,7 +95,7 @@
android:id="@+id/otherSessionSeparator" android:id="@+id/otherSessionSeparator"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="1dp" android:layout_height="1dp"
android:layout_marginTop="8dp" android:layout_marginTop="16dp"
android:background="?vctr_content_quinary" android:background="?vctr_content_quinary"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="@id/otherSessionNameTextView" app:layout_constraintStart_toStartOf="@id/otherSessionNameTextView"

View file

@ -9,7 +9,7 @@
android:id="@+id/sessionDetailsContentTitle" android:id="@+id/sessionDetailsContentTitle"
style="@style/TextAppearance.Vector.Body.DevicesManagement" style="@style/TextAppearance.Vector.Body.DevicesManagement"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="0dp"
android:layout_marginStart="@dimen/layout_horizontal_margin" android:layout_marginStart="@dimen/layout_horizontal_margin"
app:layout_constraintBottom_toTopOf="@id/sessionDetailsContentDivider" app:layout_constraintBottom_toTopOf="@id/sessionDetailsContentDivider"
app:layout_constraintEnd_toStartOf="@id/sessionDetailsContentDescription" app:layout_constraintEnd_toStartOf="@id/sessionDetailsContentDescription"
@ -22,14 +22,14 @@
style="@style/TextAppearance.Vector.Body" style="@style/TextAppearance.Vector.Body"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginStart="8dp" android:layout_marginStart="12dp"
android:layout_marginEnd="@dimen/layout_horizontal_margin" android:layout_marginEnd="@dimen/layout_horizontal_margin"
android:gravity="end" android:gravity="end"
app:layout_constraintBottom_toTopOf="@id/sessionDetailsContentDivider" app:layout_constraintBottom_toTopOf="@id/sessionDetailsContentDivider"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/sessionDetailsContentTitle" app:layout_constraintStart_toEndOf="@id/sessionDetailsContentTitle"
app:layout_constraintTop_toTopOf="@id/sessionDetailsContentTop" app:layout_constraintTop_toTopOf="@id/sessionDetailsContentTop"
tools:text="Element Web: Firefox" /> tools:text="app.element.io: Firefox on macOS" />
<View <View
android:id="@+id/sessionDetailsContentDivider" android:id="@+id/sessionDetailsContentDivider"

View file

@ -140,27 +140,40 @@
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginTop="24dp" android:layout_marginTop="24dp"
android:layout_marginEnd="6dp"
android:paddingStart="0dp" android:paddingStart="0dp"
android:paddingEnd="0dp" android:paddingEnd="0dp"
android:progressDrawable="@drawable/bg_seek_bar" android:progressDrawable="@drawable/bg_seek_bar"
android:thumbTint="?vctr_content_secondary"
android:thumbOffset="3dp" android:thumbOffset="3dp"
app:layout_constraintBottom_toBottomOf="parent" android:thumbTint="?vctr_content_secondary"
app:layout_constraintEnd_toStartOf="@id/playbackDuration" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/controllerButtonsFlow" app:layout_constraintTop_toBottomOf="@id/controllerButtonsFlow"
tools:progress="0" /> tools:progress="50" />
<TextView <TextView
android:id="@+id/playbackDuration" android:id="@+id/elapsedTime"
style="@style/Widget.Vector.TextView.Caption" style="@style/Widget.Vector.TextView.Caption"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginStart="4dp"
android:layout_marginTop="-3dp"
android:textColor="?vctr_content_tertiary"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/seekBar"
tools:ignore="NegativeMargin"
tools:text="0:11" />
<TextView
android:id="@+id/remainingTime"
style="@style/Widget.Vector.TextView.Caption"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="-3dp"
android:layout_marginEnd="4dp"
android:textColor="?vctr_content_tertiary" android:textColor="?vctr_content_tertiary"
app:layout_constraintBottom_toBottomOf="@id/seekBar"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="@id/seekBar" app:layout_constraintTop_toBottomOf="@id/seekBar"
tools:text="0:23" /> tools:ignore="NegativeMargin"
tools:text="-0:12" />
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -3,7 +3,8 @@
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent"> android:layout_height="match_parent"
android:paddingBottom="8dp">
<androidx.recyclerview.widget.RecyclerView <androidx.recyclerview.widget.RecyclerView
android:id="@+id/otherSessionsRecyclerView" android:id="@+id/otherSessionsRecyclerView"
@ -19,8 +20,9 @@
style="@style/Widget.Vector.Button.Text" style="@style/Widget.Vector.Button.Text"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginStart="72dp"
android:layout_marginTop="4dp"
android:padding="0dp" android:padding="0dp"
android:layout_marginStart="16dp"
app:layout_constraintStart_toStartOf="@id/otherSessionsRecyclerView" app:layout_constraintStart_toStartOf="@id/otherSessionsRecyclerView"
app:layout_constraintTop_toBottomOf="@id/otherSessionsRecyclerView" app:layout_constraintTop_toBottomOf="@id/otherSessionsRecyclerView"
tools:text="@string/device_manager_other_sessions_view_all" /> tools:text="@string/device_manager_other_sessions_view_all" />

View file

@ -5,6 +5,7 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:background="@drawable/bg_current_session" android:background="@drawable/bg_current_session"
android:foreground="?attr/selectableItemBackground"
android:paddingHorizontal="16dp" android:paddingHorizontal="16dp"
android:paddingTop="16dp" android:paddingTop="16dp"
android:paddingBottom="8dp"> android:paddingBottom="8dp">

View file

@ -6,7 +6,7 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:background="@drawable/bg_current_session" android:background="@drawable/bg_current_session"
android:paddingHorizontal="24dp" android:paddingHorizontal="24dp"
android:paddingBottom="16dp"> android:paddingBottom="8dp">
<ImageView <ImageView
android:id="@+id/sessionInfoDeviceTypeImageView" android:id="@+id/sessionInfoDeviceTypeImageView"

View file

@ -24,7 +24,7 @@
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginHorizontal="@dimen/layout_horizontal_margin" android:layout_marginHorizontal="@dimen/layout_horizontal_margin"
android:layout_marginTop="18.5dp" android:layout_marginTop="@dimen/layout_vertical_margin"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/sessions_list_header_title" app:layout_constraintTop_toBottomOf="@id/sessions_list_header_title"

File diff suppressed because one or more lines are too long

View file

@ -63,6 +63,11 @@ class DeleteMatrixClientInfoUseCaseTest {
// Given // Given
val error = Exception() val error = Exception()
givenSetMatrixClientInfoFails(error) givenSetMatrixClientInfoFails(error)
val expectedClientInfoToBeSet = MatrixClientInfoContent(
name = "",
version = "",
url = "",
)
// When // When
val result = deleteMatrixClientInfoUseCase.execute() val result = deleteMatrixClientInfoUseCase.execute()
@ -70,6 +75,12 @@ class DeleteMatrixClientInfoUseCaseTest {
// Then // Then
result.isFailure shouldBe true result.isFailure shouldBe true
result.exceptionOrNull() shouldBeEqualTo error result.exceptionOrNull() shouldBeEqualTo error
coVerify {
fakeSetMatrixClientInfoUseCase.execute(
fakeActiveSessionHolder.fakeSession,
expectedClientInfoToBeSet
)
}
} }
private fun givenSetMatrixClientInfoSucceeds() { private fun givenSetMatrixClientInfoSucceeds() {

View file

@ -0,0 +1,157 @@
/*
* Copyright (c) 2022 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.app.features.home.room.detail.composer.link
import com.airbnb.mvrx.test.MavericksTestRule
import im.vector.app.test.test
import im.vector.app.test.testDispatcher
import org.junit.Rule
import org.junit.Test
class SetLinkViewModelTest {
@get:Rule
val mavericksTestRule = MavericksTestRule(testDispatcher = testDispatcher)
companion object {
const val link = "https://matrix.org"
const val newLink = "https://matrix.org/new"
const val text = "Matrix"
}
private val fragmentArgs = SetLinkFragment.Args(
isTextSupported = true,
initialLink = link
)
private fun createViewModel(
args: SetLinkFragment.Args
) = SetLinkViewModel(
initialState = SetLinkViewState(args),
)
@Test
fun `given no initial link, then remove button is hidden`() {
val viewModel = createViewModel(
fragmentArgs
.copy(initialLink = null)
)
val viewModelTest = viewModel.test()
viewModelTest
.assertLatestState { !it.removeVisible }
.finish()
}
@Test
fun `given no initial link, when link changed, then remove button is still hidden`() {
val viewModel = createViewModel(
fragmentArgs.copy(initialLink = null)
)
val viewModelTest = viewModel.test()
viewModel.handle(SetLinkAction.LinkChanged(newLink))
viewModelTest
.assertLatestState { !it.removeVisible }
.finish()
}
@Test
fun `when link is unchanged, it disables the save button`() {
val viewModel = createViewModel(
fragmentArgs
.copy(initialLink = link)
)
val viewModelTest = viewModel.test()
viewModelTest
.assertLatestState { !it.saveEnabled }
.finish()
}
@Test
fun `when link is changed, it enables the save button`() {
val viewModel = createViewModel(
fragmentArgs.copy(initialLink = link)
)
val viewModelTest = viewModel.test()
viewModel.handle(SetLinkAction.LinkChanged(newLink))
viewModelTest
.assertLatestState { it.saveEnabled }
.finish()
}
@Test
fun `given no initial link, when link is changed to empty, it disables the save button`() {
val viewModel = createViewModel(
fragmentArgs.copy(initialLink = null)
)
val viewModelTest = viewModel.test()
viewModel.handle(SetLinkAction.LinkChanged(""))
viewModelTest
.assertLatestState {
!it.saveEnabled
}
.finish()
}
@Test
fun `given text is supported, when saved, it emits the right event`() {
val viewModel = createViewModel(
fragmentArgs.copy(isTextSupported = true)
)
val viewModelTest = viewModel.test()
viewModel.handle(
SetLinkAction.Save(link = newLink, text = text)
)
viewModelTest
.assertEvent {
it == SetLinkViewEvents.SavedLinkAndText(
link = newLink,
text = text,
)
}
.finish()
}
@Test
fun `given text is not supported, when saved, it emits the right event`() {
val viewModel = createViewModel(
fragmentArgs.copy(isTextSupported = false)
)
val viewModelTest = viewModel.test()
viewModel.handle(
SetLinkAction.Save(link = newLink, text = text)
)
viewModelTest
.assertEvent {
it == SetLinkViewEvents.SavedLink(link = newLink)
}
.finish()
}
}

View file

@ -53,6 +53,8 @@ import org.matrix.android.sdk.api.session.crypto.crosssigning.DeviceTrustLevel
import org.matrix.android.sdk.api.session.crypto.model.CryptoDeviceInfo import org.matrix.android.sdk.api.session.crypto.model.CryptoDeviceInfo
import org.matrix.android.sdk.api.session.crypto.model.DeviceInfo import org.matrix.android.sdk.api.session.crypto.model.DeviceInfo
import org.matrix.android.sdk.api.session.crypto.model.RoomEncryptionTrustLevel import org.matrix.android.sdk.api.session.crypto.model.RoomEncryptionTrustLevel
import org.matrix.android.sdk.api.session.crypto.verification.VerificationTransaction
import org.matrix.android.sdk.api.session.crypto.verification.VerificationTxState
import org.matrix.android.sdk.api.session.uia.DefaultBaseAuth import org.matrix.android.sdk.api.session.uia.DefaultBaseAuth
private const val A_CURRENT_DEVICE_ID = "current-device-id" private const val A_CURRENT_DEVICE_ID = "current-device-id"
@ -76,6 +78,10 @@ class DevicesViewModelTest {
private val fakeVectorPreferences = FakeVectorPreferences() private val fakeVectorPreferences = FakeVectorPreferences()
private val toggleIpAddressVisibilityUseCase = mockk<ToggleIpAddressVisibilityUseCase>() private val toggleIpAddressVisibilityUseCase = mockk<ToggleIpAddressVisibilityUseCase>()
private val verifiedTransaction = mockk<VerificationTransaction>().apply {
every { state } returns VerificationTxState.Verified
}
private fun createViewModel(): DevicesViewModel { private fun createViewModel(): DevicesViewModel {
return DevicesViewModel( return DevicesViewModel(
initialState = DevicesViewState(), initialState = DevicesViewState(),
@ -374,6 +380,18 @@ class DevicesViewModelTest {
viewModelTest.finish() viewModelTest.finish()
} }
@Test
fun `given the view model when a verified transaction is updated then device list is refreshed`() {
// Given
val viewModel = createViewModel()
// When
viewModel.transactionUpdated(verifiedTransaction)
// Then
verify { viewModel.refreshDeviceList() }
}
private fun givenCurrentSessionCrossSigningInfo(): CurrentSessionCrossSigningInfo { private fun givenCurrentSessionCrossSigningInfo(): CurrentSessionCrossSigningInfo {
val currentSessionCrossSigningInfo = mockk<CurrentSessionCrossSigningInfo>() val currentSessionCrossSigningInfo = mockk<CurrentSessionCrossSigningInfo>()
every { currentSessionCrossSigningInfo.deviceId } returns A_CURRENT_DEVICE_ID every { currentSessionCrossSigningInfo.deviceId } returns A_CURRENT_DEVICE_ID

View file

@ -0,0 +1,53 @@
/*
* Copyright (c) 2022 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.app.features.settings.devices.v2
import im.vector.app.test.fakes.FakeVectorPreferences
import org.junit.Test
class ToggleIpAddressVisibilityUseCaseTest {
private val fakeVectorPreferences = FakeVectorPreferences()
private val toggleIpAddressVisibilityUseCase = ToggleIpAddressVisibilityUseCase(
vectorPreferences = fakeVectorPreferences.instance,
)
@Test
fun `given ip addresses are currently visible then then visibility is set as false`() {
// Given
fakeVectorPreferences.givenShowIpAddressInSessionManagerScreens(true)
// When
toggleIpAddressVisibilityUseCase.execute()
// Then
fakeVectorPreferences.verifySetIpAddressVisibilityInDeviceManagerScreens(false)
}
@Test
fun `given ip addresses are currently not visible then then visibility is set as true`() {
// Given
fakeVectorPreferences.givenShowIpAddressInSessionManagerScreens(false)
// When
toggleIpAddressVisibilityUseCase.execute()
// Then
fakeVectorPreferences.verifySetIpAddressVisibilityInDeviceManagerScreens(true)
}
}

View file

@ -31,7 +31,6 @@ import org.junit.Before
import org.junit.Test import org.junit.Test
private const val A_SESSION_ID = "session_id" private const val A_SESSION_ID = "session_id"
private const val A_TITLE_RESOURCE_ID = 1234
private val A_DEFAULT_FILTER = DeviceManagerFilterType.INACTIVE private val A_DEFAULT_FILTER = DeviceManagerFilterType.INACTIVE
class VectorSettingsDevicesViewNavigatorTest { class VectorSettingsDevicesViewNavigatorTest {
@ -67,11 +66,11 @@ class VectorSettingsDevicesViewNavigatorTest {
@Test @Test
fun `given an intent when navigating to other sessions list then it starts the correct activity`() { fun `given an intent when navigating to other sessions list then it starts the correct activity`() {
// Given // Given
val intent = givenIntentForOtherSessions(A_TITLE_RESOURCE_ID, A_DEFAULT_FILTER, true) val intent = givenIntentForOtherSessions(A_DEFAULT_FILTER, true)
context.givenStartActivity(intent) context.givenStartActivity(intent)
// When // When
vectorSettingsDevicesViewNavigator.navigateToOtherSessions(context.instance, A_TITLE_RESOURCE_ID, A_DEFAULT_FILTER, true) vectorSettingsDevicesViewNavigator.navigateToOtherSessions(context.instance, A_DEFAULT_FILTER, true)
// Then // Then
context.verifyStartActivity(intent) context.verifyStartActivity(intent)
@ -96,9 +95,9 @@ class VectorSettingsDevicesViewNavigatorTest {
return intent return intent
} }
private fun givenIntentForOtherSessions(titleResourceId: Int, defaultFilter: DeviceManagerFilterType, excludeCurrentDevice: Boolean): Intent { private fun givenIntentForOtherSessions(defaultFilter: DeviceManagerFilterType, excludeCurrentDevice: Boolean): Intent {
val intent = mockk<Intent>() val intent = mockk<Intent>()
every { OtherSessionsActivity.newIntent(context.instance, titleResourceId, defaultFilter, excludeCurrentDevice) } returns intent every { OtherSessionsActivity.newIntent(context.instance, defaultFilter, excludeCurrentDevice) } returns intent
return intent return intent
} }

View file

@ -48,7 +48,6 @@ import org.junit.Rule
import org.junit.Test import org.junit.Test
import org.matrix.android.sdk.api.session.uia.DefaultBaseAuth import org.matrix.android.sdk.api.session.uia.DefaultBaseAuth
private const val A_TITLE_RES_ID = 1
private const val A_DEVICE_ID_1 = "device-id-1" private const val A_DEVICE_ID_1 = "device-id-1"
private const val A_DEVICE_ID_2 = "device-id-2" private const val A_DEVICE_ID_2 = "device-id-2"
private const val A_PASSWORD = "password" private const val A_PASSWORD = "password"
@ -59,7 +58,6 @@ class OtherSessionsViewModelTest {
val mavericksTestRule = MavericksTestRule(testDispatcher = testDispatcher) val mavericksTestRule = MavericksTestRule(testDispatcher = testDispatcher)
private val defaultArgs = OtherSessionsArgs( private val defaultArgs = OtherSessionsArgs(
titleResourceId = A_TITLE_RES_ID,
defaultFilter = DeviceManagerFilterType.ALL_SESSIONS, defaultFilter = DeviceManagerFilterType.ALL_SESSIONS,
excludeCurrentDevice = false, excludeCurrentDevice = false,
) )

View file

@ -77,4 +77,12 @@ class FakeVectorPreferences {
fun givenIsBackgroundSyncEnabled(isEnabled: Boolean) { fun givenIsBackgroundSyncEnabled(isEnabled: Boolean) {
every { instance.isBackgroundSyncEnabled() } returns isEnabled every { instance.isBackgroundSyncEnabled() } returns isEnabled
} }
fun givenShowIpAddressInSessionManagerScreens(show: Boolean) {
every { instance.showIpAddressInSessionManagerScreens() } returns show
}
fun verifySetIpAddressVisibilityInDeviceManagerScreens(isVisible: Boolean) {
verify { instance.setIpAddressVisibilityInDeviceManagerScreens(isVisible) }
}
} }