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

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",
'appdistribution' : "com.google.firebase:firebase-appdistribution:$appDistribution",
// 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' : "com.google.dagger:dagger:$dagger",
@ -98,7 +98,7 @@ ext.libs = [
],
element : [
'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 : [
'moshi' : "com.squareup.moshi:moshi:$moshi",
@ -129,7 +129,7 @@ ext.libs = [
'mavericksTesting' : "com.airbnb.android:mavericks-testing:$mavericks"
],
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"
],
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_select_all">Select 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>
@ -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>
<!-- 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="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_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="other">Consider signing out from old sessions (%1$d days or more) that you dont use anymore.</item>
</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_device_title">Device</string>
<!-- 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_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_italic">Apply italic 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_link">Set link</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 -->
<string name="message_reply_to_prefix">In reply to</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.
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_UNIX_DATE", "\"${gitRevisionUnixDate()}\""

View file

@ -18,6 +18,7 @@ package org.matrix.android.sdk.internal.session.sync.handler.room
import io.realm.Realm
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.session.room.membership.RoomMemberHelper
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?
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 typingIds = ephemeralResult?.typingUserIds?.filter { it != userId }.orEmpty()
val typingIds = typingUserIds.filter { it !in filteredUserIds }
val senderInfo = typingIds.map { userId ->
val roomMemberSummaryEntity = roomMemberHelper.getLastRoomMember(userId)
SenderInfo(

View file

@ -3013,7 +3013,11 @@
"begging",
"mercy",
"puppy eyes",
"face"
"face",
"cry",
"tears",
"sad",
"grievance"
]
},
"face-holding-back-tears": {
@ -3060,9 +3064,7 @@
"fearful",
"scared",
"terrified",
"nervous",
"oops",
"huh"
"nervous"
]
},
"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.
// When creating a hotfix, you should decrease the value, since the current value
// is the value for the next regular release.
ext.versionPatch = 14
ext.versionPatch = 16
static def getGitTimestamp() {
def cmd = 'git show -s --format=%ct'

View file

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

View file

@ -18,7 +18,6 @@ package im.vector.app.core.di
import android.content.Context
import im.vector.app.ActiveSessionDataSource
import im.vector.app.core.extensions.startSyncing
import im.vector.app.core.pushers.UnregisterUnifiedPushUseCase
import im.vector.app.core.services.GuardServiceStarter
import im.vector.app.core.session.ConfigureAndStartSessionUseCase
@ -72,7 +71,7 @@ class ActiveSessionHolder @Inject constructor(
suspend fun clearActiveSession() {
// Do some cleanup first
getSafeActiveSession(startSync = false)?.let {
getSafeActiveSession()?.let {
Timber.w("clearActiveSession of ${it.myUserId}")
it.callSignalingService().removeCallListener(callManager)
it.removeListener(sessionListener)
@ -93,8 +92,8 @@ class ActiveSessionHolder @Inject constructor(
return activeSessionReference.get() != null || authenticationService.hasAuthenticatedSessions()
}
fun getSafeActiveSession(startSync: Boolean = true): Session? {
return runBlocking { getOrInitializeSession(startSync = startSync) }
fun getSafeActiveSession(): Session? {
return runBlocking { getOrInitializeSession() }
}
fun getActiveSession(): Session {
@ -102,16 +101,11 @@ class ActiveSessionHolder @Inject constructor(
?: throw IllegalStateException("You should authenticate before using this")
}
suspend fun getOrInitializeSession(startSync: Boolean): Session? {
suspend fun getOrInitializeSession(): Session? {
return activeSessionReference.get()
?.also {
if (startSync && !it.syncService().isSyncThreadAlive()) {
it.startSyncing(applicationContext)
}
}
?: sessionInitializer.tryInitialize(readCurrentSession = { activeSessionReference.get() }) { 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.detail.TimelineViewModel
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.timeline.action.MessageActionsViewModel
import im.vector.app.features.home.room.detail.timeline.edithistory.ViewEditHistoryViewModel
@ -695,4 +696,9 @@ interface MavericksViewModelModule {
fun vectorSettingsNotificationPreferenceViewModelFactory(
factory: VectorSettingsNotificationPreferenceViewModel.Factory
): 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()")
}
val session = activeSessionHolder.getOrInitializeSession(startSync = false)
val session = activeSessionHolder.getOrInitializeSession()
if (session == null) {
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() {
if (intent.hasExtra(EXTRA_NEXT_INTENT)) {
// Start the next Activity
startSyncing()
val nextIntent = intent.getParcelableExtraCompat<Intent>(EXTRA_NEXT_INTENT)
startIntentAndFinish(nextIntent)
} else if (intent.hasExtra(EXTRA_INIT_SESSION)) {
startSyncing()
setResult(RESULT_OK)
finish()
} else if (intent.action == ACTION_ROOM_DETAILS_FROM_SHORTCUT) {
startSyncing()
val roomId = intent.getStringExtra(EXTRA_ROOM_ID)
if (roomId?.isNotEmpty() == true) {
navigator.openRoom(this, roomId, trigger = ViewRoom.Trigger.Shortcut)
@ -194,11 +197,16 @@ class MainActivity : VectorBaseActivity<ActivityMainBinding>(), UnlockedActivity
if (args.clearCache || args.clearCredentials) {
doCleanUp()
} else {
startSyncing()
startNextActivityAndFinish()
}
}
}
private fun startSyncing() {
activeSessionHolder.getSafeActiveSession()?.startSyncing(this)
}
private fun clearNotifications() {
// Dismiss all notifications
notificationDrawerManager.clearAllEvents()

View file

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

View file

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

View file

@ -413,6 +413,7 @@ class TimelineFragment :
is RoomDetailViewEvents.DisplayAndAcceptCall -> acceptIncomingCall(it)
RoomDetailViewEvents.RoomReplacementStarted -> handleRoomReplacement()
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() {
callManager.getCurrentCall()?.let { call ->
VectorCallActivity.newIntent(

View file

@ -634,7 +634,8 @@ class TimelineViewModel @AssistedInject constructor(
}
VoiceBroadcastAction.Recording.Pause -> voiceBroadcastHelper.pauseVoiceBroadcast(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)
VoiceBroadcastAction.Listening.Pause -> voiceBroadcastHelper.pausePlayback()
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.VoiceBroadcastAction
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.timeline.action.MessageSharedActionViewModel
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 val attachmentViewModel: AttachmentTypeSelectorViewModel by fragmentViewModel()
private val attachmentActionsViewModel: AttachmentTypeSelectorSharedActionViewModel by viewModels()
private val setLinkActionsViewModel: SetLinkSharedActionViewModel by viewModels()
private val composer: MessageComposerView get() {
return if (vectorPreferences.isRichTextEditorEnabled()) {
@ -212,6 +216,14 @@ class MessageComposerFragment : VectorBaseFragment<FragmentComposerBinding>(), A
.onEach { onTypeSelected(it.attachmentType) }
.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 }
.distinctUntilChanged()
.onEach { isFullScreen ->
@ -385,6 +397,10 @@ class MessageComposerFragment : VectorBaseFragment<FragmentComposerBinding>(), A
override fun onFullScreenModeChanged() = withState(messageComposerViewModel) { state ->
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 onExpandOrCompactChange()
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 io.element.android.wysiwyg.EditorEditText
import io.element.android.wysiwyg.inputhandlers.models.InlineFormat
import io.element.android.wysiwyg.inputhandlers.models.LinkAction
import io.element.android.wysiwyg.utils.RustErrorCollector
import uniffi.wysiwyg_composer.ActionState
import uniffi.wysiwyg_composer.ComposerAction
@ -231,7 +232,24 @@ internal class RichTextComposerLayout @JvmOverloads constructor(
addRichTextMenuItem(R.drawable.ic_composer_strikethrough, R.string.rich_text_editor_format_strikethrough, ComposerAction.STRIKE_THROUGH) {
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")
private fun disallowParentInterceptTouchEvent(view: View) {

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) {
with(holder) {
durationView.text = formatPlaybackTime(duration)
remainingTimeView.text = formatRemainingTime(duration)
elapsedTimeView.text = formatPlaybackTime(0)
seekBar.max = duration
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) {
isUserSeeking = true
@ -156,6 +160,7 @@ abstract class MessageVoiceBroadcastListeningItem : AbsMessageVoiceBroadcastItem
}
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) {
super.unbind(holder)
@ -177,7 +182,8 @@ abstract class MessageVoiceBroadcastListeningItem : AbsMessageVoiceBroadcastItem
val fastBackwardButton by bind<ImageButton>(R.id.fastBackwardButton)
val fastForwardButton by bind<ImageButton>(R.id.fastForwardButton)
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 voiceBroadcastMetadata by bind<VoiceBroadcastMetadataView>(R.id.voiceBroadcastMetadata)
val listenersCountMetadata by bind<VoiceBroadcastMetadataView>(R.id.listenersCountMetadata)

View file

@ -118,6 +118,7 @@ class NotificationBroadcastReceiver : BroadcastReceiver() {
private fun handleSmartReply(intent: Intent, context: Context) {
val message = getReplyMessage(intent)
val roomId = intent.getStringExtra(KEY_ROOM_ID)
val threadId = intent.getStringExtra(KEY_THREAD_ID)
if (message.isNullOrBlank() || roomId.isNullOrBlank()) {
// ignore this event
@ -126,13 +127,20 @@ class NotificationBroadcastReceiver : BroadcastReceiver() {
}
activeSessionHolder.getActiveSession().let { session ->
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?) {
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
@ -148,7 +156,7 @@ class NotificationBroadcastReceiver : BroadcastReceiver() {
body = message,
imageUriString = null,
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,
roomIsDirect = room.roomSummary()?.isDirect == true,
outGoingMessage = true,
@ -223,6 +231,7 @@ class NotificationBroadcastReceiver : BroadcastReceiver() {
companion object {
const val KEY_ROOM_ID = "roomID"
const val KEY_THREAD_ID = "threadID"
const val KEY_TEXT_REPLY = "key_text_reply"
}
}

View file

@ -657,7 +657,7 @@ class NotificationUtils @Inject constructor(
// Quick reply
if (!roomInfo.hasSmartReplyError) {
buildQuickReplyIntent(roomInfo.roomId, senderDisplayNameForReplyCompat)?.let { replyPendingIntent ->
buildQuickReplyIntent(roomInfo.roomId, threadId, senderDisplayNameForReplyCompat)?.let { replyPendingIntent ->
val remoteInput = RemoteInput.Builder(NotificationBroadcastReceiver.KEY_TEXT_REPLY)
.setLabel(stringProvider.getString(R.string.action_quick_reply))
.build()
@ -892,13 +892,17 @@ class NotificationUtils @Inject constructor(
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.
*/
private fun buildQuickReplyIntent(roomId: String, senderName: String?): PendingIntent? {
private fun buildQuickReplyIntent(roomId: String, threadId: String?, senderName: String?): PendingIntent? {
val intent: Intent
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
intent = Intent(context, NotificationBroadcastReceiver::class.java)
intent.action = actionIds.smartReply
intent.data = createIgnoredUri(roomId)
intent.putExtra(NotificationBroadcastReceiver.KEY_ROOM_ID, roomId)
threadId?.let {
intent.putExtra(NotificationBroadcastReceiver.KEY_THREAD_ID, it)
}
return PendingIntent.getBroadcast(
context,
clock.epochMillis().toInt(),

View file

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

View file

@ -31,12 +31,11 @@ class VectorSettingsDevicesViewNavigator @Inject constructor() {
fun navigateToOtherSessions(
context: Context,
titleResourceId: Int,
defaultFilter: DeviceManagerFilterType,
excludeCurrentDevice: Boolean,
) {
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.utils.DimensionConverter
private const val EXTRA_TOP_MARGIN_DP = 48
private const val EXTRA_TOP_MARGIN_DP = 32
@EpoxyModelClass
abstract class SessionDetailsHeaderItem : VectorEpoxyModel<SessionDetailsHeaderItem.Holder>(R.layout.item_session_details_header) {

View file

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

View file

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

View file

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

View file

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

View file

@ -20,7 +20,6 @@ import android.content.Context
import android.content.Intent
import android.os.Bundle
import android.view.View
import androidx.annotation.StringRes
import com.airbnb.mvrx.Mavericks
import dagger.hilt.android.AndroidEntryPoint
import im.vector.app.core.extensions.addFragment
@ -48,13 +47,11 @@ class OtherSessionsActivity : SimpleFragmentActivity() {
companion object {
fun newIntent(
context: Context,
@StringRes
titleResourceId: Int,
defaultFilter: DeviceManagerFilterType,
excludeCurrentDevice: Boolean,
): Intent {
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
import android.os.Parcelable
import androidx.annotation.StringRes
import im.vector.app.features.settings.devices.v2.filter.DeviceManagerFilterType
import kotlinx.parcelize.Parcelize
@Parcelize
data class OtherSessionsArgs(
@StringRes
val titleResourceId: Int,
val defaultFilter: DeviceManagerFilterType,
val excludeCurrentDevice: Boolean,
) : Parcelable

View file

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

View file

@ -224,7 +224,7 @@ class SessionOverviewFragment :
isVerifyButtonVisible = isCurrentSession || viewState.isCurrentSessionTrusted,
isDetailsButtonVisible = false,
isLearnMoreLinkVisible = deviceInfo.roomEncryptionTrustLevel != RoomEncryptionTrustLevel.Default,
isLastSeenDetailsVisible = !isCurrentSession,
isLastActivityVisible = !isCurrentSession,
isShowingIpAddress = viewState.isShowingIpAddress,
)
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.View
import android.view.ViewGroup
import android.view.ViewTreeObserver
import androidx.core.widget.doOnTextChanged
import com.airbnb.mvrx.fragmentViewModel
import com.airbnb.mvrx.withState
@ -62,12 +63,24 @@ class RenameSessionFragment :
}
private fun initEditText() {
views.renameSessionEditText.showKeyboard(andRequestFocus = true)
showKeyboard()
views.renameSessionEditText.doOnTextChanged { text, _, _, _ ->
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() {
views.renameSessionSave.debouncedClicks {
viewModel.handle(RenameSessionAction.SaveModifications)
@ -89,7 +102,9 @@ class RenameSessionFragment :
title = getString(R.string.device_manager_learn_more_session_rename_title),
description = getString(R.string.device_manager_learn_more_session_rename),
)
SessionLearnMoreBottomSheet.show(childFragmentManager, args)
SessionLearnMoreBottomSheet
.show(childFragmentManager, args)
.onDismiss = { showKeyboard() }
}
private fun observeViewEvents() {

View file

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

View file

@ -24,6 +24,7 @@ import android.graphics.drawable.Drawable
import android.util.TypedValue
import androidx.annotation.AttrRes
import androidx.annotation.ColorInt
import androidx.annotation.StyleRes
import androidx.core.content.ContextCompat
import androidx.core.content.edit
import androidx.core.graphics.drawable.DrawableCompat
@ -113,19 +114,16 @@ object ThemeUtils {
*/
fun setApplicationTheme(context: Context, aTheme: String) {
currentTheme.set(aTheme)
context.setTheme(
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
}
)
context.setTheme(themeToRes(context, aTheme))
// Clear the cache
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
* theme, the theme is not changed.
@ -200,4 +198,13 @@ object ThemeUtils {
DrawableCompat.setTint(tinted, color)
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"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:paddingHorizontal="24dp"
android:paddingBottom="32dp">
android:orientation="vertical">
<View
android:layout_width="36dp"
@ -18,14 +16,22 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:paddingHorizontal="24dp"
android:text="@string/device_manager_filter_bottom_sheet_title" />
<ScrollView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipToPadding="false"
android:paddingHorizontal="24dp"
android:paddingBottom="32dp"
android:scrollbarStyle="outsideOverlay">
<RadioGroup
android:id="@+id/filterOptionsRadioGroup"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="24dp"
android:layoutDirection="rtl"
android:paddingTop="24dp"
android:showDividers="none">
<RadioButton
@ -33,25 +39,34 @@
style="@style/TextAppearance.Vector.Subtitle.Medium.DevicesManagement"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="start"
android:background="?android:selectableItemBackground"
android:button="@null"
android:checked="true"
android:drawableEnd="?android:attr/listChoiceIndicatorSingle"
android:minHeight="0dp"
android:text="@string/device_manager_filter_option_all_sessions" />
android:text="@string/device_manager_filter_option_all_sessions"
android:textAlignment="textStart" />
<RadioButton
android:id="@+id/filterOptionVerifiedRadioButton"
style="@style/TextAppearance.Vector.Subtitle.Medium.DevicesManagement"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="start"
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:text="@string/device_manager_filter_option_verified"
android:textAlignment="textStart" />
<TextView
android:id="@+id/filterOptionVerifiedTextView"
style="@style/TextAppearance.Vector.Body.DevicesManagement"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="end"
android:text="@string/device_manager_filter_option_verified_description" />
<RadioButton
@ -59,16 +74,20 @@
style="@style/TextAppearance.Vector.Subtitle.Medium.DevicesManagement"
android:layout_width="match_parent"
android:layout_height="wrap_content"
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_unverified" />
android:text="@string/device_manager_filter_option_unverified"
android:textAlignment="textStart" />
<TextView
android:id="@+id/filterOptionUnverifiedTextView"
style="@style/TextAppearance.Vector.Body.DevicesManagement"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="end"
android:text="@string/device_manager_filter_option_unverified_description" />
<RadioButton
@ -76,17 +95,23 @@
style="@style/TextAppearance.Vector.Subtitle.Medium.DevicesManagement"
android:layout_width="match_parent"
android:layout_height="wrap_content"
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:text="@string/device_manager_filter_option_inactive"
android:textAlignment="textStart" />
<TextView
android:id="@+id/filterOptionInactiveTextView"
style="@style/TextAppearance.Vector.Body.DevicesManagement"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="end" />
android:layout_height="wrap_content" />
</RadioGroup>
</ScrollView>
</LinearLayout>

View file

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

View file

@ -47,6 +47,7 @@
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginHorizontal="8dp"
android:layout_marginTop="4dp"
android:text="@string/device_manager_session_overview_signout"
app:layout_constraintEnd_toEndOf="parent"
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_height="wrap_content"
android:layout_marginHorizontal="16dp"
android:layout_marginVertical="16dp"
android:layout_marginVertical="4dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/deviceListHeaderCurrentSession" />

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -24,7 +24,7 @@
android:layout_width="0dp"
android:layout_height="wrap_content"
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_constraintStart_toStartOf="parent"
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
val error = Exception()
givenSetMatrixClientInfoFails(error)
val expectedClientInfoToBeSet = MatrixClientInfoContent(
name = "",
version = "",
url = "",
)
// When
val result = deleteMatrixClientInfoUseCase.execute()
@ -70,6 +75,12 @@ class DeleteMatrixClientInfoUseCaseTest {
// Then
result.isFailure shouldBe true
result.exceptionOrNull() shouldBeEqualTo error
coVerify {
fakeSetMatrixClientInfoUseCase.execute(
fakeActiveSessionHolder.fakeSession,
expectedClientInfoToBeSet
)
}
}
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.DeviceInfo
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
private const val A_CURRENT_DEVICE_ID = "current-device-id"
@ -76,6 +78,10 @@ class DevicesViewModelTest {
private val fakeVectorPreferences = FakeVectorPreferences()
private val toggleIpAddressVisibilityUseCase = mockk<ToggleIpAddressVisibilityUseCase>()
private val verifiedTransaction = mockk<VerificationTransaction>().apply {
every { state } returns VerificationTxState.Verified
}
private fun createViewModel(): DevicesViewModel {
return DevicesViewModel(
initialState = DevicesViewState(),
@ -374,6 +380,18 @@ class DevicesViewModelTest {
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 {
val currentSessionCrossSigningInfo = mockk<CurrentSessionCrossSigningInfo>()
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
private const val A_SESSION_ID = "session_id"
private const val A_TITLE_RESOURCE_ID = 1234
private val A_DEFAULT_FILTER = DeviceManagerFilterType.INACTIVE
class VectorSettingsDevicesViewNavigatorTest {
@ -67,11 +66,11 @@ class VectorSettingsDevicesViewNavigatorTest {
@Test
fun `given an intent when navigating to other sessions list then it starts the correct activity`() {
// Given
val intent = givenIntentForOtherSessions(A_TITLE_RESOURCE_ID, A_DEFAULT_FILTER, true)
val intent = givenIntentForOtherSessions(A_DEFAULT_FILTER, true)
context.givenStartActivity(intent)
// When
vectorSettingsDevicesViewNavigator.navigateToOtherSessions(context.instance, A_TITLE_RESOURCE_ID, A_DEFAULT_FILTER, true)
vectorSettingsDevicesViewNavigator.navigateToOtherSessions(context.instance, A_DEFAULT_FILTER, true)
// Then
context.verifyStartActivity(intent)
@ -96,9 +95,9 @@ class VectorSettingsDevicesViewNavigatorTest {
return intent
}
private fun givenIntentForOtherSessions(titleResourceId: Int, defaultFilter: DeviceManagerFilterType, excludeCurrentDevice: Boolean): Intent {
private fun givenIntentForOtherSessions(defaultFilter: DeviceManagerFilterType, excludeCurrentDevice: Boolean): 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
}

View file

@ -48,7 +48,6 @@ import org.junit.Rule
import org.junit.Test
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_2 = "device-id-2"
private const val A_PASSWORD = "password"
@ -59,7 +58,6 @@ class OtherSessionsViewModelTest {
val mavericksTestRule = MavericksTestRule(testDispatcher = testDispatcher)
private val defaultArgs = OtherSessionsArgs(
titleResourceId = A_TITLE_RES_ID,
defaultFilter = DeviceManagerFilterType.ALL_SESSIONS,
excludeCurrentDevice = false,
)

View file

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