diff --git a/CHANGES.md b/CHANGES.md index 0481ec1af6..cac9ab2608 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -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) ======================================= diff --git a/changelog.d/2965.bugfix b/changelog.d/2965.bugfix new file mode 100644 index 0000000000..6c60d5bd49 --- /dev/null +++ b/changelog.d/2965.bugfix @@ -0,0 +1 @@ +Do not show typing notification of ignored users. diff --git a/changelog.d/7475.bugfix b/changelog.d/7475.bugfix new file mode 100644 index 0000000000..7d0c3866a0 --- /dev/null +++ b/changelog.d/7475.bugfix @@ -0,0 +1 @@ +[Push Notifications, Threads] - quick reply to threaded notification now sent to thread except main timeline diff --git a/changelog.d/7746.feature b/changelog.d/7746.feature new file mode 100644 index 0000000000..6732d50b9c --- /dev/null +++ b/changelog.d/7746.feature @@ -0,0 +1 @@ +[Rich text editor] Add support for links diff --git a/changelog.d/7784.bugfix b/changelog.d/7784.bugfix new file mode 100644 index 0000000000..107da01877 --- /dev/null +++ b/changelog.d/7784.bugfix @@ -0,0 +1 @@ +[Session manager] Other sessions list: filter option is displayed when selection mode is enabled diff --git a/changelog.d/7786.bugfix b/changelog.d/7786.bugfix new file mode 100644 index 0000000000..60a4a324d4 --- /dev/null +++ b/changelog.d/7786.bugfix @@ -0,0 +1 @@ +[Session manager] Other sessions: Filter bottom sheet cut in landscape mode diff --git a/changelog.d/7790.bugfix b/changelog.d/7790.bugfix new file mode 100644 index 0000000000..7390f92b32 --- /dev/null +++ b/changelog.d/7790.bugfix @@ -0,0 +1 @@ +Automatically show keyboard after learn more bottom sheet is dismissed diff --git a/changelog.d/7792.bugfix b/changelog.d/7792.bugfix new file mode 100644 index 0000000000..d5c80a0825 --- /dev/null +++ b/changelog.d/7792.bugfix @@ -0,0 +1 @@ +[Session Manager] Other sessions list: cannot select/deselect session by a long press when in select mode diff --git a/changelog.d/7794.bugfix b/changelog.d/7794.bugfix new file mode 100644 index 0000000000..54cd93728b --- /dev/null +++ b/changelog.d/7794.bugfix @@ -0,0 +1 @@ +Fix current session ip address visibility diff --git a/changelog.d/7795.feature b/changelog.d/7795.feature new file mode 100644 index 0000000000..50c7e2a8cc --- /dev/null +++ b/changelog.d/7795.feature @@ -0,0 +1 @@ +[Session manager] Security recommendations cards: whole view should be tappable diff --git a/changelog.d/7798.bugfix b/changelog.d/7798.bugfix new file mode 100644 index 0000000000..4289f9ee96 --- /dev/null +++ b/changelog.d/7798.bugfix @@ -0,0 +1 @@ +Device Manager UI review fixes diff --git a/changelog.d/7821.misc b/changelog.d/7821.misc new file mode 100644 index 0000000000..3cb73d1b8a --- /dev/null +++ b/changelog.d/7821.misc @@ -0,0 +1 @@ +[Voice Broadcast] Replace the player timeline \ No newline at end of file diff --git a/changelog.d/7836.misc b/changelog.d/7836.misc new file mode 100644 index 0000000000..7086e57d5a --- /dev/null +++ b/changelog.d/7836.misc @@ -0,0 +1 @@ +Increase session manager test coverage diff --git a/dependencies.gradle b/dependencies.gradle index dd8e6bb11c..b81c1c2017 100644 --- a/dependencies.gradle +++ b/dependencies.gradle @@ -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 : [ diff --git a/fastlane/metadata/android/en-US/changelogs/40105130.txt b/fastlane/metadata/android/en-US/changelogs/40105130.txt new file mode 100644 index 0000000000..91c25cf053 --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/40105130.txt @@ -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 diff --git a/fastlane/metadata/android/en-US/changelogs/40105140.txt b/fastlane/metadata/android/en-US/changelogs/40105140.txt new file mode 100644 index 0000000000..91c25cf053 --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/40105140.txt @@ -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 diff --git a/library/ui-strings/src/main/res/values/strings.xml b/library/ui-strings/src/main/res/values/strings.xml index 736db70fb7..1477a77a8d 100644 --- a/library/ui-strings/src/main/res/values/strings.xml +++ b/library/ui-strings/src/main/res/values/strings.xml @@ -420,6 +420,7 @@ Got it Select all Deselect all + Yes, Stop Copied to clipboard @@ -3128,6 +3129,8 @@ You are already recording a voice broadcast. Please end your current voice broadcast to start a new one. %1$s left + Stop live broadcasting? + 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. Anyone in %s will be able to find and join this room - no need to manually invite everyone. You’ll be able to change this in room settings anytime. Anyone in a parent space will be able to find and join this room - no need to manually invite everyone. You’ll be able to change this in room settings anytime. @@ -3343,7 +3346,7 @@ Consider signing out from old sessions (%1$d day or more) that you don’t use anymore. Consider signing out from old sessions (%1$d days or more) that you don’t use anymore. - Current Session + Current session Session Device @@ -3484,13 +3487,19 @@ Confirm Please ensure that you know the origin of this code. By linking devices, you will provide someone with full access to your account. - + Apply bold format Apply italic format Apply strikethrough format Apply underline format + Set link Toggle full screen mode + Text + Link + Create a link + Edit link + In reply to sent a file. diff --git a/matrix-sdk-android/build.gradle b/matrix-sdk-android/build.gradle index 9d717af857..d972df84c2 100644 --- a/matrix-sdk-android/build.gradle +++ b/matrix-sdk-android/build.gradle @@ -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()}\"" diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/room/RoomTypingUsersHandler.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/room/RoomTypingUsersHandler.kt index 54bb63753c..519112b1b7 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/room/RoomTypingUsersHandler.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/room/RoomTypingUsersHandler.kt @@ -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( diff --git a/tools/emojis/emoji_picker_datasource_formatted.json b/tools/emojis/emoji_picker_datasource_formatted.json index c00bd10371..0dcf9ccb25 100644 --- a/tools/emojis/emoji_picker_datasource_formatted.json +++ b/tools/emojis/emoji_picker_datasource_formatted.json @@ -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": { diff --git a/vector-app/build.gradle b/vector-app/build.gradle index 6dc96b4a4d..e8de0cd27e 100644 --- a/vector-app/build.gradle +++ b/vector-app/build.gradle @@ -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' diff --git a/vector-app/src/main/AndroidManifest.xml b/vector-app/src/main/AndroidManifest.xml index bde68d8351..8c7fc618b0 100644 --- a/vector-app/src/main/AndroidManifest.xml +++ b/vector-app/src/main/AndroidManifest.xml @@ -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" diff --git a/vector/src/main/java/im/vector/app/core/di/ActiveSessionHolder.kt b/vector/src/main/java/im/vector/app/core/di/ActiveSessionHolder.kt index fead1e15b1..e698f65bf4 100644 --- a/vector/src/main/java/im/vector/app/core/di/ActiveSessionHolder.kt +++ b/vector/src/main/java/im/vector/app/core/di/ActiveSessionHolder.kt @@ -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) } } diff --git a/vector/src/main/java/im/vector/app/core/di/MavericksViewModelModule.kt b/vector/src/main/java/im/vector/app/core/di/MavericksViewModelModule.kt index f1f2389b9c..09150e1708 100644 --- a/vector/src/main/java/im/vector/app/core/di/MavericksViewModelModule.kt +++ b/vector/src/main/java/im/vector/app/core/di/MavericksViewModelModule.kt @@ -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<*, *> } diff --git a/vector/src/main/java/im/vector/app/core/platform/VectorBaseDialogFragment.kt b/vector/src/main/java/im/vector/app/core/platform/VectorBaseDialogFragment.kt new file mode 100644 index 0000000000..5a817b989e --- /dev/null +++ b/vector/src/main/java/im/vector/app/core/platform/VectorBaseDialogFragment.kt @@ -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 : 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 VectorViewModel<*, *, T>.observeViewEvents(observer: (T) -> Unit) { + viewEvents + .stream() + .onEach { + observer(it) + } + .launchIn(viewLifecycleOwner.lifecycleScope) + } +} diff --git a/vector/src/main/java/im/vector/app/core/pushers/VectorPushHandler.kt b/vector/src/main/java/im/vector/app/core/pushers/VectorPushHandler.kt index 0d2cd56995..04c71e0412 100644 --- a/vector/src/main/java/im/vector/app/core/pushers/VectorPushHandler.kt +++ b/vector/src/main/java/im/vector/app/core/pushers/VectorPushHandler.kt @@ -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") diff --git a/vector/src/main/java/im/vector/app/features/MainActivity.kt b/vector/src/main/java/im/vector/app/features/MainActivity.kt index c197cfccf3..8ce375122e 100644 --- a/vector/src/main/java/im/vector/app/features/MainActivity.kt +++ b/vector/src/main/java/im/vector/app/features/MainActivity.kt @@ -174,12 +174,15 @@ class MainActivity : VectorBaseActivity(), UnlockedActivity private fun handleAppStarted() { if (intent.hasExtra(EXTRA_NEXT_INTENT)) { // Start the next Activity + startSyncing() val nextIntent = intent.getParcelableExtraCompat(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(), 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() diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailAction.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailAction.kt index faee8f652c..9fe296e1f9 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailAction.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailAction.kt @@ -127,6 +127,7 @@ sealed class RoomDetailAction : VectorViewModelAction { object Pause : Recording() object Resume : Recording() object Stop : Recording() + object StopConfirmed : Recording() } sealed class Listening : VoiceBroadcastAction() { diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewEvents.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewEvents.kt index 399d5e0abe..77dd826cfe 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewEvents.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewEvents.kt @@ -71,6 +71,8 @@ sealed class RoomDetailViewEvents : VectorViewEvents { object DisplayEnableIntegrationsWarning : RoomDetailViewEvents() + object DisplayPromptToStopVoiceBroadcast : RoomDetailViewEvents() + data class OpenStickerPicker(val widget: Widget) : RoomDetailViewEvents() object OpenIntegrationManager : RoomDetailViewEvents() diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt index a3a2355d66..3d211b2d82 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt @@ -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( diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineViewModel.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineViewModel.kt index 89a199001c..43b22f9b64 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineViewModel.kt @@ -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() diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerFragment.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerFragment.kt index d56ea8b733..4849e20b6d 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerFragment.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerFragment.kt @@ -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(), 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(), 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(), 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) + } } } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerView.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerView.kt index 44fcf22d4a..b68f4046c8 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerView.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerView.kt @@ -45,4 +45,5 @@ interface Callback : ComposerEditText.Callback { fun onAddAttachment() fun onExpandOrCompactChange() fun onFullScreenModeChanged() + fun onSetLink(isTextSupported: Boolean, initialLink: String?) } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/RichTextComposerLayout.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/RichTextComposerLayout.kt index d69fe8edeb..543210e006 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/RichTextComposerLayout.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/RichTextComposerLayout.kt @@ -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,8 +232,25 @@ 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.setOnTouchListener { v, event -> diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/link/SetLinkAction.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/link/SetLinkAction.kt new file mode 100644 index 0000000000..5cc31022ea --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/link/SetLinkAction.kt @@ -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() +} diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/link/SetLinkFragment.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/link/SetLinkFragment.kt new file mode 100644 index 0000000000..008a8017ee --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/link/SetLinkFragment.kt @@ -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() { + + @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() +} diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/link/SetLinkSharedActionViewModel.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/link/SetLinkSharedActionViewModel.kt new file mode 100644 index 0000000000..fb9f3f0d5b --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/link/SetLinkSharedActionViewModel.kt @@ -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() + +sealed interface SetLinkSharedAction : VectorSharedAction { + data class Set( + val link: String, + ) : SetLinkSharedAction + + data class Insert( + val text: String, + val link: String, + ) : SetLinkSharedAction + + object Remove : SetLinkSharedAction +} diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/link/SetLinkViewEvents.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/link/SetLinkViewEvents.kt new file mode 100644 index 0000000000..cd42651c22 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/link/SetLinkViewEvents.kt @@ -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() +} diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/link/SetLinkViewModel.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/link/SetLinkViewModel.kt new file mode 100644 index 0000000000..9a5b5cd8dd --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/link/SetLinkViewModel.kt @@ -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(initialState) { + + @AssistedFactory + interface Factory : MavericksAssistedViewModelFactory { + override fun create(initialState: SetLinkViewState): SetLinkViewModel + } + + companion object : MavericksViewModelFactory 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)) + } +} diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/link/SetLinkViewState.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/link/SetLinkViewState.kt new file mode 100644 index 0000000000..ea61f7eb72 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/link/SetLinkViewState.kt @@ -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 +} diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageVoiceBroadcastListeningItem.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageVoiceBroadcastListeningItem.kt index 1aa3178ce5..b788d79214 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageVoiceBroadcastListeningItem.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageVoiceBroadcastListeningItem.kt @@ -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(R.id.fastBackwardButton) val fastForwardButton by bind(R.id.fastForwardButton) val seekBar by bind(R.id.seekBar) - val durationView by bind(R.id.playbackDuration) + val remainingTimeView by bind(R.id.remainingTime) + val elapsedTimeView by bind(R.id.elapsedTime) val broadcasterNameMetadata by bind(R.id.broadcasterNameMetadata) val voiceBroadcastMetadata by bind(R.id.voiceBroadcastMetadata) val listenersCountMetadata by bind(R.id.listenersCountMetadata) diff --git a/vector/src/main/java/im/vector/app/features/notifications/NotificationBroadcastReceiver.kt b/vector/src/main/java/im/vector/app/features/notifications/NotificationBroadcastReceiver.kt index 455f4778e8..e231686c27 100644 --- a/vector/src/main/java/im/vector/app/features/notifications/NotificationBroadcastReceiver.kt +++ b/vector/src/main/java/im/vector/app/features/notifications/NotificationBroadcastReceiver.kt @@ -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?) { - room.sendService().sendTextMessage(message) + 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" } } diff --git a/vector/src/main/java/im/vector/app/features/notifications/NotificationUtils.kt b/vector/src/main/java/im/vector/app/features/notifications/NotificationUtils.kt index 7bf78bdb95..5b3a244137 100755 --- a/vector/src/main/java/im/vector/app/features/notifications/NotificationUtils.kt +++ b/vector/src/main/java/im/vector/app/features/notifications/NotificationUtils.kt @@ -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(), diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/VectorSettingsDevicesFragment.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/VectorSettingsDevicesFragment.kt index 4b93ac397e..d3847bfd9a 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/VectorSettingsDevicesFragment.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/VectorSettingsDevicesFragment.kt @@ -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 ) diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/VectorSettingsDevicesViewNavigator.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/VectorSettingsDevicesViewNavigator.kt index d4b3345fea..bcfa1c30db 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/VectorSettingsDevicesViewNavigator.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/VectorSettingsDevicesViewNavigator.kt @@ -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) ) } diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/details/SessionDetailsHeaderItem.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/details/SessionDetailsHeaderItem.kt index ff6ce3faad..f76c21da8e 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/details/SessionDetailsHeaderItem.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/details/SessionDetailsHeaderItem.kt @@ -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(R.layout.item_session_details_header) { diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/list/SecurityRecommendationView.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/list/SecurityRecommendationView.kt index 07202274ad..2a43a9aade 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/list/SecurityRecommendationView.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/list/SecurityRecommendationView.kt @@ -53,6 +53,9 @@ class SecurityRecommendationView @JvmOverloads constructor( setImage(it) } + setOnClickListener { + callback?.onViewAllClicked() + } views.recommendationViewAllButton.setOnClickListener { callback?.onViewAllClicked() } diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/list/SessionInfoView.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/list/SessionInfoView.kt index eecec72b0a..5d2daf2941 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/list/SessionInfoView.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/list/SessionInfoView.kt @@ -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) { diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/list/SessionInfoViewState.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/list/SessionInfoViewState.kt index 5d3c4b4f4b..6c7ca809ea 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/list/SessionInfoViewState.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/list/SessionInfoViewState.kt @@ -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, ) diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/more/SessionLearnMoreBottomSheet.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/more/SessionLearnMoreBottomSheet.kt index 22ca06eb1e..502d9abca3 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/more/SessionLearnMoreBottomSheet.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/more/SessionLearnMoreBottomSheet.kt @@ -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 Unit)? = null + override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): BottomSheetSessionLearnMoreBinding { return BottomSheetSessionLearnMoreBinding.inflate(inflater, container, false) } @@ -57,6 +60,11 @@ class SessionLearnMoreBottomSheet : VectorBaseBottomSheetDialogFragment super.invalidate() views.bottomSheetSessionLearnMoreTitle.text = viewState.title @@ -65,11 +73,12 @@ class SessionLearnMoreBottomSheet : VectorBaseBottomSheetDialogFragment 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, 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)) } } diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewFragment.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewFragment.kt index f3df0cced0..399f99201b 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewFragment.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewFragment.kt @@ -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) diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/rename/RenameSessionFragment.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/rename/RenameSessionFragment.kt index 2f671492e3..d2cbbbdee5 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/rename/RenameSessionFragment.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/rename/RenameSessionFragment.kt @@ -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() { diff --git a/vector/src/main/java/im/vector/app/features/start/StartAppViewModel.kt b/vector/src/main/java/im/vector/app/features/start/StartAppViewModel.kt index 4b2e7bef98..6eb8bf2e80 100644 --- a/vector/src/main/java/im/vector/app/features/start/StartAppViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/start/StartAppViewModel.kt @@ -63,7 +63,7 @@ class StartAppViewModel @AssistedInject constructor( } private suspend fun eagerlyInitializeSession() { - sessionHolder.getOrInitializeSession(startSync = true) + sessionHolder.getOrInitializeSession() } private fun handleLongProcessing() { diff --git a/vector/src/main/java/im/vector/app/features/themes/ThemeUtils.kt b/vector/src/main/java/im/vector/app/features/themes/ThemeUtils.kt index b5c7b162d8..3c902d162e 100644 --- a/vector/src/main/java/im/vector/app/features/themes/ThemeUtils.kt +++ b/vector/src/main/java/im/vector/app/features/themes/ThemeUtils.kt @@ -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 + } } diff --git a/vector/src/main/res/drawable/ic_composer_link.xml b/vector/src/main/res/drawable/ic_composer_link.xml new file mode 100644 index 0000000000..6d0f731ed9 --- /dev/null +++ b/vector/src/main/res/drawable/ic_composer_link.xml @@ -0,0 +1,12 @@ + + + diff --git a/vector/src/main/res/layout/bottom_sheet_device_manager_filter.xml b/vector/src/main/res/layout/bottom_sheet_device_manager_filter.xml index a7987e70b5..fd66aec1ea 100644 --- a/vector/src/main/res/layout/bottom_sheet_device_manager_filter.xml +++ b/vector/src/main/res/layout/bottom_sheet_device_manager_filter.xml @@ -2,9 +2,7 @@ + android:orientation="vertical"> - + android:layout_height="match_parent" + android:clipToPadding="false" + android:paddingHorizontal="24dp" + android:paddingBottom="32dp" + android:scrollbarStyle="outsideOverlay"> - + android:paddingTop="24dp" + android:showDividers="none"> - + - + - + - + - + - + - + + + + + diff --git a/vector/src/main/res/layout/fragment_other_sessions.xml b/vector/src/main/res/layout/fragment_other_sessions.xml index 62384b7ee1..ce289bd125 100644 --- a/vector/src/main/res/layout/fragment_other_sessions.xml +++ b/vector/src/main/res/layout/fragment_other_sessions.xml @@ -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" diff --git a/vector/src/main/res/layout/fragment_session_overview.xml b/vector/src/main/res/layout/fragment_session_overview.xml index 1c59abfd12..4719357802 100644 --- a/vector/src/main/res/layout/fragment_session_overview.xml +++ b/vector/src/main/res/layout/fragment_session_overview.xml @@ -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" diff --git a/vector/src/main/res/layout/fragment_set_link.xml b/vector/src/main/res/layout/fragment_set_link.xml new file mode 100644 index 0000000000..36b3421253 --- /dev/null +++ b/vector/src/main/res/layout/fragment_set_link.xml @@ -0,0 +1,117 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + +