diff --git a/.github/workflows/triage-labelled.yml b/.github/workflows/triage-labelled.yml index f478d2bd7b..174e3c54c0 100644 --- a/.github/workflows/triage-labelled.yml +++ b/.github/workflows/triage-labelled.yml @@ -142,32 +142,6 @@ jobs: env: PROJECT_ID: "PN_kwDOAM0swc2KCw" GITHUB_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN }} - - move_threads_issues: - name: A-Threads to Thread board - runs-on: ubuntu-latest - # Skip in forks - if: > - github.repository == 'vector-im/element-android' && - contains(github.event.issue.labels.*.name, 'A-Threads') - steps: - - uses: octokit/graphql-action@v2.x - with: - headers: '{"GraphQL-Features": "projects_next_graphql"}' - query: | - mutation add_to_project($projectid:ID!,$contentid:ID!) { - addProjectNextItem(input:{projectId:$projectid contentId:$contentid}) { - projectNextItem { - id - } - } - } - projectid: ${{ env.PROJECT_ID }} - contentid: ${{ github.event.issue.node_id }} - env: - PROJECT_ID: "PN_kwDOAM0swc0rRA" - GITHUB_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN }} - move_message_bubbles_issues: name: A-Message-Bubbles to Message bubbles board runs-on: ubuntu-latest diff --git a/changelog.d/6646.misc b/changelog.d/6646.misc new file mode 100644 index 0000000000..0f2e3f0881 --- /dev/null +++ b/changelog.d/6646.misc @@ -0,0 +1 @@ +[App Layout] Obsolete settings are not shown when App Layout flag is enabled diff --git a/changelog.d/6754.bugfix b/changelog.d/6754.bugfix new file mode 100644 index 0000000000..e9f6960595 --- /dev/null +++ b/changelog.d/6754.bugfix @@ -0,0 +1 @@ +[App Layout] - space switcher now has empty state diff --git a/changelog.d/6835.feature b/changelog.d/6835.feature new file mode 100644 index 0000000000..e4e610f7e0 --- /dev/null +++ b/changelog.d/6835.feature @@ -0,0 +1 @@ +[App Layout] New empty states for home screen diff --git a/changelog.d/6876.feature b/changelog.d/6876.feature new file mode 100644 index 0000000000..12a2b78a1e --- /dev/null +++ b/changelog.d/6876.feature @@ -0,0 +1 @@ +[App Layout] - Invites now show empty screen after you reject last invite diff --git a/changelog.d/6989.bugfix b/changelog.d/6989.bugfix new file mode 100644 index 0000000000..cf740ca741 --- /dev/null +++ b/changelog.d/6989.bugfix @@ -0,0 +1 @@ +Catch race condition crash in voice recording diff --git a/changelog.d/7010.feature b/changelog.d/7010.feature new file mode 100644 index 0000000000..d6c7c85b3e --- /dev/null +++ b/changelog.d/7010.feature @@ -0,0 +1 @@ +Try to detect devices that lack Opus encoder support, use bundled libopus library for those. diff --git a/changelog.d/7016.wip b/changelog.d/7016.wip new file mode 100644 index 0000000000..6918991a86 --- /dev/null +++ b/changelog.d/7016.wip @@ -0,0 +1 @@ +[New Layout] Improves talkback accessibility diff --git a/docs/nightly_build.md b/docs/nightly_build.md index 7750e0466a..77cc676c7f 100644 --- a/docs/nightly_build.md +++ b/docs/nightly_build.md @@ -47,7 +47,7 @@ git checkout develop mv towncrier.toml towncrier.toml.bak sed 's/CHANGES\.md/CHANGES_NIGHTLY\.md/' towncrier.toml.bak > towncrier.toml rm towncrier.toml.bak -yes n | towncrier --version nightly +yes n | towncrier build --version nightly ./gradlew assembleGplayNightly appDistributionUploadGplayNightly $CI_GRADLE_ARG_PROPERTIES ``` diff --git a/library/ui-strings/src/main/res/values/strings.xml b/library/ui-strings/src/main/res/values/strings.xml index 8621fa7a4b..fd18ee8992 100644 --- a/library/ui-strings/src/main/res/values/strings.xml +++ b/library/ui-strings/src/main/res/values/strings.xml @@ -140,8 +140,10 @@ Start Chat Create Room Explore Rooms - Expand space children - Collapse space children + + Expand %s children + + Collapse %s children @@ -442,9 +444,16 @@ "System Alerts" Suggested Rooms + + No spaces yet. + Spaces are a new way to group rooms and people. Create a space to get started. + Invites + Nothing new. + This is where your new requests and invites will be. + Conversations Matrix contacts only @@ -3252,6 +3261,27 @@ Session Last activity %1$s + + %s\nis looking a little empty. + + Spaces are a new way to group rooms and people. Add an existing room, or create a new one, using the bottom-right button. + + Welcome to ${app_name},\n%s. + The all-in-one secure chat app for teams, friends and organisations. Create a chat, or join an existing room, to get started. + Nothing to report. + This is where your unread messages will show up, when you have some. + + Welcome to a new view! + + To simplify your ${app_name}, tabs are now optional. Manage them using the top-right menu. + Access Spaces + + Access your Spaces (bottom-right) faster and easier than ever before. + Give Feedback + + Tap top right to see the option to feedback. + Try it out + Filter All session Verified diff --git a/library/ui-styles/src/main/res/values-h720dp/dimens.xml b/library/ui-styles/src/main/res/values-h720dp/dimens.xml index 1a7791720d..2a7b12cf2f 100644 --- a/library/ui-styles/src/main/res/values-h720dp/dimens.xml +++ b/library/ui-styles/src/main/res/values-h720dp/dimens.xml @@ -2,4 +2,8 @@ 0.05 0.40 - \ No newline at end of file + + 16dp + 40dp + 46dp + diff --git a/library/ui-styles/src/main/res/values/dimens.xml b/library/ui-styles/src/main/res/values/dimens.xml index 53f1044a12..758dd6e978 100644 --- a/library/ui-styles/src/main/res/values/dimens.xml +++ b/library/ui-styles/src/main/res/values/dimens.xml @@ -74,4 +74,9 @@ 112dp + + + 8dp + 16dp + 28dp diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/CryptoStoreHelper.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/CryptoStoreHelper.kt index ba1afd4758..48cfbebe5b 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/CryptoStoreHelper.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/CryptoStoreHelper.kt @@ -21,6 +21,7 @@ import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore import org.matrix.android.sdk.internal.crypto.store.db.RealmCryptoStore import org.matrix.android.sdk.internal.crypto.store.db.RealmCryptoStoreModule import org.matrix.android.sdk.internal.crypto.store.db.mapper.CrossSigningKeysMapper +import org.matrix.android.sdk.internal.crypto.store.db.mapper.MyDeviceLastSeenInfoEntityMapper import org.matrix.android.sdk.internal.di.MoshiProvider import org.matrix.android.sdk.internal.util.time.DefaultClock import kotlin.random.Random @@ -37,6 +38,7 @@ internal class CryptoStoreHelper { userId = "userId_" + Random.nextInt(), deviceId = "deviceId_sample", clock = DefaultClock(), + myDeviceLastSeenInfoEntityMapper = MyDeviceLastSeenInfoEntityMapper() ) } } diff --git a/vector-app/build.gradle b/vector-app/build.gradle index 2368586bfb..a8262fde40 100644 --- a/vector-app/build.gradle +++ b/vector-app/build.gradle @@ -340,10 +340,6 @@ android { "-opt-in=kotlinx.coroutines.ExperimentalCoroutinesApi", ] } - - buildFeatures { - viewBinding true - } } dependencies { diff --git a/vector-app/src/gplay/AndroidManifest.xml b/vector-app/src/gplay/AndroidManifest.xml index bc74b9bb52..a5f0eae6be 100755 --- a/vector-app/src/gplay/AndroidManifest.xml +++ b/vector-app/src/gplay/AndroidManifest.xml @@ -1,7 +1,7 @@ - + + 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 bd105436f3..40484f57e8 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 @@ -53,6 +53,7 @@ import im.vector.app.features.home.room.detail.upgrade.MigrateRoomViewModel import im.vector.app.features.home.room.list.RoomListViewModel import im.vector.app.features.home.room.list.home.HomeRoomListViewModel import im.vector.app.features.home.room.list.home.invites.InvitesViewModel +import im.vector.app.features.home.room.list.home.release.ReleaseNotesViewModel import im.vector.app.features.homeserver.HomeServerCapabilitiesViewModel import im.vector.app.features.invite.InviteUsersToRoomViewModel import im.vector.app.features.location.LocationSharingViewModel @@ -626,6 +627,11 @@ interface MavericksViewModelModule { @MavericksViewModelKey(InvitesViewModel::class) fun invitesViewModel(factory: InvitesViewModel.Factory): MavericksAssistedViewModelFactory<*, *> + @Binds + @IntoMap + @MavericksViewModelKey(ReleaseNotesViewModel::class) + fun releaseNotesViewModel(factory: ReleaseNotesViewModel.Factory): MavericksAssistedViewModelFactory<*, *> + @Binds @IntoMap @MavericksViewModelKey(SessionOverviewViewModel::class) diff --git a/vector/src/main/java/im/vector/app/core/platform/StateView.kt b/vector/src/main/java/im/vector/app/core/platform/StateView.kt index 6f36787d0c..2fb99c705a 100755 --- a/vector/src/main/java/im/vector/app/core/platform/StateView.kt +++ b/vector/src/main/java/im/vector/app/core/platform/StateView.kt @@ -21,6 +21,7 @@ import android.graphics.drawable.Drawable import android.util.AttributeSet import android.view.View import android.widget.FrameLayout +import android.widget.ImageView import androidx.core.view.isVisible import im.vector.app.R import im.vector.app.core.extensions.updateConstraintSet @@ -36,7 +37,8 @@ class StateView @JvmOverloads constructor(context: Context, attrs: AttributeSet? val title: CharSequence? = null, val image: Drawable? = null, val isBigImage: Boolean = false, - val message: CharSequence? = null + val message: CharSequence? = null, + val imageScaleType: ImageView.ScaleType? = ImageView.ScaleType.FIT_CENTER, ) : State() data class Error(val message: CharSequence? = null) : State() @@ -79,6 +81,7 @@ class StateView @JvmOverloads constructor(context: Context, attrs: AttributeSet? is State.Content -> Unit is State.Loading -> Unit is State.Empty -> { + views.emptyImageView.scaleType = newState.imageScaleType views.emptyImageView.setImageDrawable(newState.image) views.emptyView.updateConstraintSet { it.constrainPercentHeight(R.id.emptyImageView, if (newState.isBigImage) 0.5f else 0.1f) diff --git a/vector/src/main/java/im/vector/app/features/analytics/plan/Interaction.kt b/vector/src/main/java/im/vector/app/features/analytics/plan/Interaction.kt index 6336faa74c..1df1b35439 100644 --- a/vector/src/main/java/im/vector/app/features/analytics/plan/Interaction.kt +++ b/vector/src/main/java/im/vector/app/features/analytics/plan/Interaction.kt @@ -40,6 +40,46 @@ data class Interaction( ) : VectorAnalyticsEvent { enum class Name { + /** + * User tapped the All filter in the All Chats filter tab. + */ + MobileAllChatsFilterAll, + + /** + * User tapped the Favourites filter in the All Chats filter tab. + */ + MobileAllChatsFilterFavourites, + + /** + * User tapped the People filter in the All Chats filter tab. + */ + MobileAllChatsFilterPeople, + + /** + * User tapped the Unreads filter in the All Chats filter tab. + */ + MobileAllChatsFilterUnreads, + + /** + * User disabled filters from the all chats layout settings. + */ + MobileAllChatsFiltersDisabled, + + /** + * User enabled filters from the all chats layout settings. + */ + MobileAllChatsFiltersEnabled, + + /** + * User disabled recents from the all chats layout settings. + */ + MobileAllChatsRecentsDisabled, + + /** + * User enabled recents from the all chats layout settings. + */ + MobileAllChatsRecentsEnabled, + /** * User tapped on Add to Home button on Room Details screen. */ @@ -60,6 +100,11 @@ data class Interaction( */ MobileRoomThreadSummaryItem, + /** + * User validated the creation of a new space. + */ + MobileSpaceCreationValidated, + /** * User tapped on the filter button on ThreadList screen. */ @@ -81,6 +126,12 @@ data class Interaction( */ SpacePanelSwitchSpace, + /** + * User tapped an unselected sub space from the space list -> space + * switching should occur. + */ + SpacePanelSwitchSubSpace, + /** * User clicked the create room button in the add existing room to space * dialog in Element Web/Desktop. diff --git a/vector/src/main/java/im/vector/app/features/analytics/plan/MobileScreen.kt b/vector/src/main/java/im/vector/app/features/analytics/plan/MobileScreen.kt index 3ce3dfb578..7ea41e2d78 100644 --- a/vector/src/main/java/im/vector/app/features/analytics/plan/MobileScreen.kt +++ b/vector/src/main/java/im/vector/app/features/analytics/plan/MobileScreen.kt @@ -43,6 +43,11 @@ data class MobileScreen( */ CreateRoom, + /** + * The screen shown to create a new space. + */ + CreateSpace, + /** * The confirmation screen shown before deactivating an account. */ @@ -78,6 +83,11 @@ data class MobileScreen( */ InviteFriends, + /** + * Room accessed via space bottom sheet list. + */ + Invites, + /** * The screen that displays the login flow (when the user already has an * account). @@ -261,6 +271,11 @@ data class MobileScreen( */ Sidebar, + /** + * Room accessed via space bottom sheet list. + */ + SpaceBottomSheet, + /** * Screen that displays the list of rooms and spaces of a space. */ diff --git a/vector/src/main/java/im/vector/app/features/analytics/plan/UserProperties.kt b/vector/src/main/java/im/vector/app/features/analytics/plan/UserProperties.kt index 77be2456cd..28732c9a42 100644 --- a/vector/src/main/java/im/vector/app/features/analytics/plan/UserProperties.kt +++ b/vector/src/main/java/im/vector/app/features/analytics/plan/UserProperties.kt @@ -44,6 +44,10 @@ data class UserProperties( * Whether the user has the people space enabled. */ val webMetaSpacePeopleEnabled: Boolean? = null, + /** + * The active filter in the All Chats screen. + */ + val allChatsActiveFilter: AllChatsActiveFilter? = null, /** * The selected messaging use case during the onboarding flow. */ @@ -80,6 +84,29 @@ data class UserProperties( WorkMessaging, } + enum class AllChatsActiveFilter { + + /** + * Filters are activated and All is selected. + */ + All, + + /** + * Filters are activated and Favourites is selected. + */ + Favourites, + + /** + * Filters are activated and People is selected. + */ + People, + + /** + * Filters are activated and Unreads is selected. + */ + Unreads, + } + fun getProperties(): Map? { return mutableMapOf().apply { webMetaSpaceFavouritesEnabled?.let { put("WebMetaSpaceFavouritesEnabled", it) } @@ -87,6 +114,7 @@ data class UserProperties( webMetaSpaceHomeEnabled?.let { put("WebMetaSpaceHomeEnabled", it) } webMetaSpaceOrphansEnabled?.let { put("WebMetaSpaceOrphansEnabled", it) } webMetaSpacePeopleEnabled?.let { put("WebMetaSpacePeopleEnabled", it) } + allChatsActiveFilter?.let { put("allChatsActiveFilter", it.name) } ftueUseCaseSelection?.let { put("ftueUseCaseSelection", it.name) } numFavouriteRooms?.let { put("numFavouriteRooms", it) } numSpaces?.let { put("numSpaces", it) } diff --git a/vector/src/main/java/im/vector/app/features/analytics/plan/ViewRoom.kt b/vector/src/main/java/im/vector/app/features/analytics/plan/ViewRoom.kt index f6a724304b..366979025a 100644 --- a/vector/src/main/java/im/vector/app/features/analytics/plan/ViewRoom.kt +++ b/vector/src/main/java/im/vector/app/features/analytics/plan/ViewRoom.kt @@ -110,6 +110,11 @@ data class ViewRoom( */ MobileSearchContactDetail, + /** + * Room accessed via space bottom sheet list. + */ + MobileSpaceBottomSheet, + /** * Room accessed via interacting with direct chat item in the space * contact detail screen. diff --git a/vector/src/main/java/im/vector/app/features/home/HomeActivity.kt b/vector/src/main/java/im/vector/app/features/home/HomeActivity.kt index 2a8390c93c..78b4364f38 100644 --- a/vector/src/main/java/im/vector/app/features/home/HomeActivity.kt +++ b/vector/src/main/java/im/vector/app/features/home/HomeActivity.kt @@ -60,6 +60,7 @@ import im.vector.app.features.disclaimer.showDisclaimerDialog import im.vector.app.features.home.room.list.actions.RoomListSharedAction import im.vector.app.features.home.room.list.actions.RoomListSharedActionViewModel import im.vector.app.features.home.room.list.home.layout.HomeLayoutSettingBottomDialogFragment +import im.vector.app.features.home.room.list.home.release.ReleaseNotesActivity import im.vector.app.features.matrixto.MatrixToBottomSheet import im.vector.app.features.matrixto.OriginOfMatrixTo import im.vector.app.features.navigation.Navigator @@ -268,6 +269,7 @@ class HomeActivity : } is HomeActivityViewEvents.OnCrossSignedInvalidated -> handleCrossSigningInvalidated(it) HomeActivityViewEvents.ShowAnalyticsOptIn -> handleShowAnalyticsOptIn() + HomeActivityViewEvents.ShowReleaseNotes -> handleShowReleaseNotes() HomeActivityViewEvents.NotifyUserForThreadsMigration -> handleNotifyUserForThreadsMigration() is HomeActivityViewEvents.MigrateThreads -> migrateThreadsIfNeeded(it.checkSession) } @@ -282,6 +284,10 @@ class HomeActivity : homeActivityViewModel.handle(HomeActivityViewActions.ViewStarted) } + private fun handleShowReleaseNotes() { + startActivity(Intent(this, ReleaseNotesActivity::class.java)) + } + private fun showSpaceSettings(spaceId: String) { // open bottom sheet SpaceSettingsMenuBottomSheet diff --git a/vector/src/main/java/im/vector/app/features/home/HomeActivityViewEvents.kt b/vector/src/main/java/im/vector/app/features/home/HomeActivityViewEvents.kt index 170550d5b4..e0b9e8ceb5 100644 --- a/vector/src/main/java/im/vector/app/features/home/HomeActivityViewEvents.kt +++ b/vector/src/main/java/im/vector/app/features/home/HomeActivityViewEvents.kt @@ -31,6 +31,7 @@ sealed interface HomeActivityViewEvents : VectorViewEvents { data class OnCrossSignedInvalidated(val userItem: MatrixItem.UserItem) : HomeActivityViewEvents object PromptToEnableSessionPush : HomeActivityViewEvents object ShowAnalyticsOptIn : HomeActivityViewEvents + object ShowReleaseNotes : HomeActivityViewEvents object NotifyUserForThreadsMigration : HomeActivityViewEvents data class MigrateThreads(val checkSession: Boolean) : HomeActivityViewEvents object StartRecoverySetupFlow : HomeActivityViewEvents diff --git a/vector/src/main/java/im/vector/app/features/home/HomeActivityViewModel.kt b/vector/src/main/java/im/vector/app/features/home/HomeActivityViewModel.kt index cfe76706a5..dd54285fb5 100644 --- a/vector/src/main/java/im/vector/app/features/home/HomeActivityViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/home/HomeActivityViewModel.kt @@ -26,11 +26,13 @@ import im.vector.app.core.di.ActiveSessionHolder import im.vector.app.core.di.MavericksAssistedViewModelFactory import im.vector.app.core.di.hiltMavericksViewModelFactory import im.vector.app.core.platform.VectorViewModel +import im.vector.app.features.VectorFeatures import im.vector.app.features.analytics.AnalyticsConfig import im.vector.app.features.analytics.AnalyticsTracker import im.vector.app.features.analytics.extensions.toAnalyticsType import im.vector.app.features.analytics.plan.Signup import im.vector.app.features.analytics.store.AnalyticsStore +import im.vector.app.features.home.room.list.home.release.ReleaseNotesPreferencesStore import im.vector.app.features.login.ReAuthHelper import im.vector.app.features.onboarding.AuthenticationDescription import im.vector.app.features.raw.wellknown.ElementWellKnown @@ -82,6 +84,8 @@ class HomeActivityViewModel @AssistedInject constructor( private val vectorPreferences: VectorPreferences, private val analyticsTracker: AnalyticsTracker, private val analyticsConfig: AnalyticsConfig, + private val releaseNotesPreferencesStore: ReleaseNotesPreferencesStore, + private val vectorFeatures: VectorFeatures, ) : VectorViewModel(initialState) { @AssistedFactory @@ -110,9 +114,27 @@ class HomeActivityViewModel @AssistedInject constructor( checkSessionPushIsOn() observeCrossSigningReset() observeAnalytics() + observeReleaseNotes() initThreadsMigration() } + private fun observeReleaseNotes() = withState { state -> + // we don't want to show release notes for new users or after relogin + if (state.authenticationDescription == null && vectorFeatures.isNewAppLayoutEnabled()) { + releaseNotesPreferencesStore.appLayoutOnboardingShown.onEach { isAppLayoutOnboardingShown -> + if (!isAppLayoutOnboardingShown) { + releaseNotesPreferencesStore.setAppLayoutOnboardingShown(true) + _viewEvents.post(HomeActivityViewEvents.ShowReleaseNotes) + } + }.launchIn(viewModelScope) + } else { + // we assume that users which came from auth flow either have seen updates already (relogin) or don't need them (new user) + viewModelScope.launch { + releaseNotesPreferencesStore.setAppLayoutOnboardingShown(true) + } + } + } + private fun observeAnalytics() { if (analyticsConfig.isEnabled) { analyticsStore.didAskUserConsentFlow diff --git a/vector/src/main/java/im/vector/app/features/home/NewHomeDetailFragment.kt b/vector/src/main/java/im/vector/app/features/home/NewHomeDetailFragment.kt index f47da1e6a0..12dee92430 100644 --- a/vector/src/main/java/im/vector/app/features/home/NewHomeDetailFragment.kt +++ b/vector/src/main/java/im/vector/app/features/home/NewHomeDetailFragment.kt @@ -58,14 +58,11 @@ import im.vector.app.features.settings.VectorSettingsActivity.Companion.EXTRA_DI import im.vector.app.features.spaces.SpaceListBottomSheet import im.vector.app.features.workers.signout.BannerState import im.vector.app.features.workers.signout.ServerBackupStatusViewModel -import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach -import kotlinx.coroutines.launch import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.crypto.model.DeviceInfo import org.matrix.android.sdk.api.session.room.model.RoomSummary -import org.matrix.android.sdk.api.util.toMatrixItem import javax.inject.Inject @AndroidEntryPoint @@ -322,12 +319,6 @@ class NewHomeDetailFragment : private fun setupToolbar() { setupToolbar(views.toolbar) - lifecycleScope.launch(Dispatchers.IO) { - session.userService().getUser(session.myUserId)?.let { user -> - avatarRenderer.render(user.toMatrixItem(), views.avatar) - } - } - views.collapsingToolbar.debouncedClicks(::openSpaceSettings) views.toolbar.debouncedClicks(::openSpaceSettings) @@ -373,9 +364,16 @@ class NewHomeDetailFragment : vectorPreferences.developerShowDebugInfo() ) + refreshAvatar() hasUnreadRooms = it.hasUnreadMessages } + private fun refreshAvatar() = withState(viewModel) { state -> + state.myMatrixItem?.let { user -> + avatarRenderer.render(user, views.avatar) + } + } + override fun onTapToReturnToCall() { callManager.getCurrentCall()?.let { call -> VectorCallActivity.newIntent( diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/AudioMessageHelper.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/AudioMessageHelper.kt index 2e150daee6..a5e899c672 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/AudioMessageHelper.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/AudioMessageHelper.kt @@ -48,7 +48,7 @@ class AudioMessageHelper @Inject constructor( ) { private var mediaPlayer: MediaPlayer? = null private var currentPlayingId: String? = null - private var voiceRecorder: VoiceRecorder = voiceRecorderProvider.provideVoiceRecorder() + private val voiceRecorder: VoiceRecorder by lazy { voiceRecorderProvider.provideVoiceRecorder() } private val amplitudeList = mutableListOf() @@ -79,18 +79,19 @@ class AudioMessageHelper @Inject constructor( } fun stopRecording(): MultiPickerAudioType? { - tryOrNull("Cannot stop media recording amplitude") { - stopRecordingAmplitudes() - } val voiceMessageFile = tryOrNull("Cannot stop media recorder!") { voiceRecorder.stopRecord() voiceRecorder.getVoiceMessageFile() } - try { + tryOrNull("Cannot stop media recording amplitude") { + stopRecordingAmplitudes() + } + + return try { voiceMessageFile?.let { val outputFileUri = FileProvider.getUriForFile(context, buildMeta.applicationId + ".fileProvider", it, "Voice message.${it.extension}") - return outputFileUri + outputFileUri .toMultiPickerAudioType(context) ?.apply { waveform = if (amplitudeList.size < 50) { @@ -99,10 +100,13 @@ class AudioMessageHelper @Inject constructor( amplitudeList.chunked(amplitudeList.size / 50) { items -> items.maxOrNull() ?: 0 } } } - } ?: return null + } } catch (e: FileNotFoundException) { Timber.e(e, "Cannot stop voice recording") - return null + null + } catch (e: RuntimeException) { + Timber.e(e, "Error while retrieving metadata") + null } } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerViewModel.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerViewModel.kt index ca86010915..7d67ec8c60 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerViewModel.kt @@ -883,7 +883,11 @@ class MessageComposerViewModel @AssistedInject constructor( } private fun handlePlayOrPauseRecordingPlayback() { - audioMessageHelper.startOrPauseRecordingPlayback() + try { + audioMessageHelper.startOrPauseRecordingPlayback() + } catch (failure: Throwable) { + _viewEvents.post(MessageComposerViewEvents.VoicePlaybackOrRecordingFailure(failure)) + } } fun endAllVoiceActions(deleteRecord: Boolean = true) { diff --git a/vector/src/main/java/im/vector/app/features/home/room/list/home/HomeRoomListFragment.kt b/vector/src/main/java/im/vector/app/features/home/room/list/home/HomeRoomListFragment.kt index 32635e3407..edb619cd90 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/list/home/HomeRoomListFragment.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/list/home/HomeRoomListFragment.kt @@ -199,11 +199,17 @@ class HomeRoomListFragment : ).also { controller -> controller.listener = this controller.onFilterChanged = ::onRoomFilterChanged + roomListViewModel.emptyStateFlow.onEach { emptyStateOptional -> + controller.submitEmptyStateData(emptyStateOptional.getOrNull()) + }.launchIn(lifecycleScope) section.filtersData.onEach { controller.submitFiltersData(it.getOrNull()) }.launchIn(lifecycleScope) section.list.observe(viewLifecycleOwner) { list -> controller.submitList(list) + if (list.isEmpty()) { + controller.requestForcedModelBuild() + } } }.adapter } diff --git a/vector/src/main/java/im/vector/app/features/home/room/list/home/HomeRoomListViewModel.kt b/vector/src/main/java/im/vector/app/features/home/room/list/home/HomeRoomListViewModel.kt index 425f309202..b52c4e0190 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/list/home/HomeRoomListViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/list/home/HomeRoomListViewModel.kt @@ -16,6 +16,7 @@ package im.vector.app.features.home.room.list.home +import android.widget.ImageView import androidx.lifecycle.map import androidx.paging.PagedList import arrow.core.toOption @@ -23,11 +24,14 @@ import com.airbnb.mvrx.MavericksViewModelFactory import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject +import im.vector.app.R import im.vector.app.SpaceStateHandler import im.vector.app.core.di.MavericksAssistedViewModelFactory import im.vector.app.core.di.hiltMavericksViewModelFactory import im.vector.app.core.platform.StateView import im.vector.app.core.platform.VectorViewModel +import im.vector.app.core.resources.DrawableProvider +import im.vector.app.core.resources.StringProvider import im.vector.app.features.home.room.list.home.filter.HomeRoomFilter import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableSharedFlow @@ -36,6 +40,7 @@ import kotlinx.coroutines.flow.asSharedFlow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onEach @@ -52,6 +57,7 @@ import org.matrix.android.sdk.api.session.room.RoomSortOrder import org.matrix.android.sdk.api.session.room.RoomSummaryQueryParams import org.matrix.android.sdk.api.session.room.UpdatableLivePageResult import org.matrix.android.sdk.api.session.room.model.Membership +import org.matrix.android.sdk.api.session.room.model.RoomSummary import org.matrix.android.sdk.api.session.room.model.tag.RoomTag import org.matrix.android.sdk.api.session.room.roomSummaryQueryParams import org.matrix.android.sdk.api.session.room.state.isPublic @@ -63,6 +69,8 @@ class HomeRoomListViewModel @AssistedInject constructor( private val session: Session, private val spaceStateHandler: SpaceStateHandler, private val preferencesStore: HomeLayoutPreferencesStore, + private val stringProvider: StringProvider, + private val drawableProvider: DrawableProvider, ) : VectorViewModel(initialState) { @AssistedFactory @@ -82,6 +90,10 @@ class HomeRoomListViewModel @AssistedInject constructor( private val _sections = MutableSharedFlow>(replay = 1) val sections = _sections.asSharedFlow() + private var currentFilter: HomeRoomFilter = HomeRoomFilter.ALL + private val _emptyStateFlow = MutableSharedFlow>(replay = 1) + val emptyStateFlow = _emptyStateFlow.asSharedFlow() + private var filteredPagedRoomSummariesLive: UpdatableLivePageResult? = null init { @@ -109,6 +121,7 @@ class HomeRoomListViewModel @AssistedInject constructor( } newSections.add(getFilteredRoomsSection()) + emitEmptyState() _sections.emit(newSections) setState { @@ -171,6 +184,7 @@ class HomeRoomListViewModel @AssistedInject constructor( liveResults.queryParams = liveResults.queryParams.copy( spaceFilter = selectedSpace?.roomId.toActiveSpaceOrNoFilter() ) + emitEmptyState() }.launchIn(viewModelScope) return HomeRoomSection.RoomSummaryData( @@ -179,27 +193,48 @@ class HomeRoomListViewModel @AssistedInject constructor( ) } + private fun emitEmptyState() { + viewModelScope.launch { + val emptyState = getEmptyStateData(currentFilter, spaceStateHandler.getCurrentSpace()) + _emptyStateFlow.emit(Optional.from(emptyState)) + } + } + private fun getFiltersDataFlow(): SharedFlow>> { val flow = MutableSharedFlow>>(replay = 1) - val favouritesFlow = session.flow() - .liveRoomSummaries( - RoomSummaryQueryParams.Builder().also { builder -> - builder.roomTagQueryFilter = RoomTagQueryFilter(true, null, null) - }.build() - ) - .map { it.isNotEmpty() } + val spaceFLow = spaceStateHandler.getSelectedSpaceFlow() .distinctUntilChanged() + .onStart { + emit(spaceStateHandler.getCurrentSpace().toOption()) + } - val dmsFLow = session.flow() - .liveRoomSummaries( - RoomSummaryQueryParams.Builder().also { builder -> - builder.memberships = listOf(Membership.JOIN) - builder.roomCategoryFilter = RoomCategoryFilter.ONLY_DM - }.build() - ) - .map { it.isNotEmpty() } - .distinctUntilChanged() + val favouritesFlow = + spaceFLow.flatMapLatest { selectedSpace -> + session.flow() + .liveRoomSummaries( + RoomSummaryQueryParams.Builder().also { builder -> + builder.spaceFilter = selectedSpace.orNull()?.roomId.toActiveSpaceOrNoFilter() + builder.roomTagQueryFilter = RoomTagQueryFilter(true, null, null) + }.build() + ) + } + .map { it.isNotEmpty() } + .distinctUntilChanged() + + val dmsFLow = + spaceFLow.flatMapLatest { selectedSpace -> + session.flow() + .liveRoomSummaries( + RoomSummaryQueryParams.Builder().also { builder -> + builder.spaceFilter = selectedSpace.orNull()?.roomId.toActiveSpaceOrNoFilter() + builder.memberships = listOf(Membership.JOIN) + builder.roomCategoryFilter = RoomCategoryFilter.ONLY_DM + }.build() + ) + } + .map { it.isNotEmpty() } + .distinctUntilChanged() combine(favouritesFlow, dmsFLow, preferencesStore.areFiltersEnabledFlow) { hasFavourite, hasDm, areFiltersEnabled -> Triple(hasFavourite, hasDm, areFiltersEnabled) @@ -250,6 +285,38 @@ class HomeRoomListViewModel @AssistedInject constructor( } } + private fun getEmptyStateData(filter: HomeRoomFilter, selectedSpace: RoomSummary?): StateView.State.Empty? { + return when (filter) { + HomeRoomFilter.ALL -> + if (selectedSpace != null) { + StateView.State.Empty( + title = stringProvider.getString(R.string.home_empty_space_no_rooms_title, selectedSpace.displayName), + message = stringProvider.getString(R.string.home_empty_space_no_rooms_message), + image = drawableProvider.getDrawable(R.drawable.ill_empty_space), + isBigImage = true + ) + } else { + val userName = session.userService().getUser(session.myUserId)?.displayName ?: "" + StateView.State.Empty( + title = stringProvider.getString(R.string.home_empty_no_rooms_title, userName), + message = stringProvider.getString(R.string.home_empty_no_rooms_message), + image = drawableProvider.getDrawable(R.drawable.ill_empty_all_chats), + isBigImage = true + ) + } + HomeRoomFilter.UNREADS -> + StateView.State.Empty( + title = stringProvider.getString(R.string.home_empty_no_unreads_title), + message = stringProvider.getString(R.string.home_empty_no_unreads_message), + image = drawableProvider.getDrawable(R.drawable.ill_empty_unreads), + isBigImage = true, + imageScaleType = ImageView.ScaleType.CENTER_INSIDE + ) + else -> + null + } + } + override fun handle(action: HomeRoomListAction) { when (action) { is HomeRoomListAction.SelectRoom -> handleSelectRoom(action) @@ -261,9 +328,12 @@ class HomeRoomListViewModel @AssistedInject constructor( } private fun handleChangeRoomFilter(action: HomeRoomListAction.ChangeRoomFilter) { + currentFilter = action.filter filteredPagedRoomSummariesLive?.let { liveResults -> liveResults.queryParams = getFilteredQueryParams(action.filter, liveResults.queryParams) } + + emitEmptyState() } fun isPublicRoom(roomId: String): Boolean { diff --git a/vector/src/main/java/im/vector/app/features/home/room/list/home/RoomListEmptyItem.kt b/vector/src/main/java/im/vector/app/features/home/room/list/home/RoomListEmptyItem.kt new file mode 100644 index 0000000000..f7b3262529 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/home/room/list/home/RoomListEmptyItem.kt @@ -0,0 +1,40 @@ +/* + * 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.list.home + +import com.airbnb.epoxy.EpoxyAttribute +import com.airbnb.epoxy.EpoxyModelClass +import im.vector.app.R +import im.vector.app.core.epoxy.VectorEpoxyHolder +import im.vector.app.core.epoxy.VectorEpoxyModel +import im.vector.app.core.platform.StateView + +@EpoxyModelClass +abstract class RoomListEmptyItem : VectorEpoxyModel(R.layout.item_state_view) { + + @EpoxyAttribute + lateinit var emptyData: StateView.State.Empty + + override fun bind(holder: Holder) { + super.bind(holder) + holder.stateView.state = emptyData + } + + class Holder : VectorEpoxyHolder() { + val stateView by bind(R.id.stateView) + } +} diff --git a/vector/src/main/java/im/vector/app/features/home/room/list/home/filter/HomeFilteredRoomsController.kt b/vector/src/main/java/im/vector/app/features/home/room/list/home/filter/HomeFilteredRoomsController.kt index 2d673bc089..789c9e9985 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/list/home/filter/HomeFilteredRoomsController.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/list/home/filter/HomeFilteredRoomsController.kt @@ -18,11 +18,13 @@ package im.vector.app.features.home.room.list.home.filter import com.airbnb.epoxy.EpoxyModel import com.airbnb.epoxy.paging.PagedListEpoxyController +import im.vector.app.core.platform.StateView import im.vector.app.core.utils.createUIHandler import im.vector.app.features.home.RoomListDisplayMode import im.vector.app.features.home.room.list.RoomListListener import im.vector.app.features.home.room.list.RoomSummaryItemFactory import im.vector.app.features.home.room.list.RoomSummaryItemPlaceHolder_ +import im.vector.app.features.home.room.list.home.roomListEmptyItem import org.matrix.android.sdk.api.session.room.members.ChangeMembershipState import org.matrix.android.sdk.api.session.room.model.RoomSummary @@ -44,6 +46,8 @@ class HomeFilteredRoomsController( var onFilterChanged: ((HomeRoomFilter) -> Unit)? = null private var filtersData: List? = null + private var emptyStateData: StateView.State.Empty? = null + private var currentState: StateView.State = StateView.State.Content override fun addModels(models: List>) { val host = this @@ -54,14 +58,29 @@ class HomeFilteredRoomsController( onFilterChangedListener(host.onFilterChanged) } } - super.addModels(models) + + if (models.isEmpty() && emptyStateData != null) { + emptyStateData?.let { emptyState -> + roomListEmptyItem { + id("state_item") + emptyData(emptyState) + } + currentState = emptyState + } + } else { + currentState = StateView.State.Content + super.addModels(models) + } + } + + fun submitEmptyStateData(state: StateView.State.Empty?) { + this.emptyStateData = state } fun submitFiltersData(data: List?) { this.filtersData = data requestForcedModelBuild() } - override fun buildItemModel(currentPosition: Int, item: RoomSummary?): EpoxyModel<*> { item ?: return RoomSummaryItemPlaceHolder_().apply { id(currentPosition) } return roomSummaryItemFactory.create(item, roomChangeMembershipStates.orEmpty(), emptySet(), RoomListDisplayMode.ROOMS, listener) diff --git a/vector/src/main/java/im/vector/app/features/home/room/list/home/invites/InvitesFragment.kt b/vector/src/main/java/im/vector/app/features/home/room/list/home/invites/InvitesFragment.kt index 74b46cec33..f557483289 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/list/home/invites/InvitesFragment.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/list/home/invites/InvitesFragment.kt @@ -20,15 +20,18 @@ import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import androidx.lifecycle.lifecycleScope import com.airbnb.mvrx.fragmentViewModel -import com.airbnb.mvrx.withState import dagger.hilt.android.AndroidEntryPoint import im.vector.app.core.extensions.configureWith +import im.vector.app.core.platform.StateView import im.vector.app.core.platform.VectorBaseFragment import im.vector.app.databinding.FragmentInvitesBinding import im.vector.app.features.analytics.plan.ViewRoom import im.vector.app.features.home.room.list.RoomListListener import im.vector.app.features.notifications.NotificationDrawerManager +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach import org.matrix.android.sdk.api.session.room.model.RoomSummary import org.matrix.android.sdk.api.session.room.model.SpaceChildInfo import javax.inject.Inject @@ -51,6 +54,8 @@ class InvitesFragment : VectorBaseFragment(), RoomListLi setupToolbar(views.invitesToolbar) .allowBack() + views.invitesStateView.contentView = views.invitesRecycler + views.invitesRecycler.configureWith(controller) controller.listener = this @@ -62,13 +67,31 @@ class InvitesFragment : VectorBaseFragment(), RoomListLi when (it) { is InvitesViewEvents.Failure -> showFailure(it.throwable) is InvitesViewEvents.OpenRoom -> handleOpenRoom(it.roomSummary, it.shouldCloseInviteView) - InvitesViewEvents.Close -> handleClose() } } - } - private fun handleClose() { - requireActivity().finish() + viewModel.invites.onEach { + when (it) { + is InvitesContentState.Content -> { + views.invitesStateView.state = StateView.State.Content + controller.submitList(it.content) + } + is InvitesContentState.Empty -> { + views.invitesStateView.state = StateView.State.Empty( + title = it.title, + image = it.image, + message = it.message + ) + } + is InvitesContentState.Error -> { + when (views.invitesStateView.state) { + StateView.State.Content -> showErrorInSnackbar(it.throwable) + else -> views.invitesStateView.state = StateView.State.Error(it.throwable.message) + } + } + InvitesContentState.Loading -> views.invitesStateView.state = StateView.State.Loading + } + }.launchIn(viewLifecycleOwner.lifecycleScope) } private fun handleOpenRoom(roomSummary: RoomSummary, shouldCloseInviteView: Boolean) { @@ -83,14 +106,6 @@ class InvitesFragment : VectorBaseFragment(), RoomListLi } } - override fun invalidate(): Unit = withState(viewModel) { state -> - super.invalidate() - - state.pagedList?.observe(viewLifecycleOwner) { list -> - controller.submitList(list) - } - } - override fun onRejectRoomInvitation(room: RoomSummary) { notificationDrawerManager.updateEvents { it.clearMemberShipNotificationForRoom(room.roomId) } viewModel.handle(InvitesAction.RejectInvitation(room)) diff --git a/vector/src/main/java/im/vector/app/features/home/room/list/home/invites/InvitesViewEvents.kt b/vector/src/main/java/im/vector/app/features/home/room/list/home/invites/InvitesViewEvents.kt index d68577cf95..21310592a4 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/list/home/invites/InvitesViewEvents.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/list/home/invites/InvitesViewEvents.kt @@ -22,5 +22,4 @@ import org.matrix.android.sdk.api.session.room.model.RoomSummary sealed class InvitesViewEvents : VectorViewEvents { data class Failure(val throwable: Throwable) : InvitesViewEvents() data class OpenRoom(val roomSummary: RoomSummary, val shouldCloseInviteView: Boolean) : InvitesViewEvents() - object Close : InvitesViewEvents() } diff --git a/vector/src/main/java/im/vector/app/features/home/room/list/home/invites/InvitesViewModel.kt b/vector/src/main/java/im/vector/app/features/home/room/list/home/invites/InvitesViewModel.kt index b0d854be66..b8034d2364 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/list/home/invites/InvitesViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/list/home/invites/InvitesViewModel.kt @@ -16,14 +16,25 @@ package im.vector.app.features.home.room.list.home.invites +import androidx.lifecycle.asFlow import androidx.paging.PagedList import com.airbnb.mvrx.MavericksViewModelFactory import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject +import im.vector.app.R import im.vector.app.core.di.MavericksAssistedViewModelFactory import im.vector.app.core.di.hiltMavericksViewModelFactory import im.vector.app.core.platform.VectorViewModel +import im.vector.app.core.resources.DrawableProvider +import im.vector.app.core.resources.StringProvider +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.asSharedFlow +import kotlinx.coroutines.flow.catch +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.flow.onStart import kotlinx.coroutines.launch import org.matrix.android.sdk.api.extensions.orFalse import org.matrix.android.sdk.api.session.Session @@ -36,6 +47,8 @@ import timber.log.Timber class InvitesViewModel @AssistedInject constructor( @Assisted val initialState: InvitesViewState, private val session: Session, + private val stringProvider: StringProvider, + private val drawableProvider: DrawableProvider ) : VectorViewModel(initialState) { private val pagedListConfig = PagedList.Config.Builder() @@ -52,6 +65,11 @@ class InvitesViewModel @AssistedInject constructor( companion object : MavericksViewModelFactory by hiltMavericksViewModelFactory() + private val _invites = MutableSharedFlow(replay = 1) + val invites = _invites.asSharedFlow() + + private var invitesCount = -1 + init { observeInvites() } @@ -72,8 +90,6 @@ class InvitesViewModel @AssistedInject constructor( return@withState } - val shouldCloseInviteView = state.pagedList?.value?.size == 1 - viewModelScope.launch { try { session.roomService().leaveRoom(roomId) @@ -81,9 +97,6 @@ class InvitesViewModel @AssistedInject constructor( // Instead, we wait for the room to be rejected // Known bug: if the user is invited again (after rejecting the first invitation), the loading will be displayed instead of the buttons. // If we update the state, the button will be displayed again, so it's not ideal... - if (shouldCloseInviteView) { - _viewEvents.post(InvitesViewEvents.Close) - } } catch (failure: Throwable) { // Notify the user _viewEvents.post(InvitesViewEvents.Failure(failure)) @@ -101,9 +114,7 @@ class InvitesViewModel @AssistedInject constructor( } // close invites view when navigate to a room from the last one invite - val shouldCloseInviteView = state.pagedList?.value?.size == 1 - - _viewEvents.post(InvitesViewEvents.OpenRoom(action.roomSummary, shouldCloseInviteView)) + val shouldCloseInviteView = invitesCount == 1 // quick echo setState { @@ -117,6 +128,8 @@ class InvitesViewModel @AssistedInject constructor( } ) } + + _viewEvents.post(InvitesViewEvents.OpenRoom(action.roomSummary, shouldCloseInviteView)) } private fun observeInvites() { @@ -129,8 +142,26 @@ class InvitesViewModel @AssistedInject constructor( sortOrder = RoomSortOrder.ACTIVITY ) - setState { - copy(pagedList = pagedList) - } + pagedList.asFlow() + .map { + if (it.isEmpty()) { + InvitesContentState.Empty( + title = stringProvider.getString(R.string.invites_empty_title), + image = drawableProvider.getDrawable(R.drawable.ic_invites_empty), + message = stringProvider.getString(R.string.invites_empty_message) + ) + } else { + invitesCount = it.loadedCount + InvitesContentState.Content(it) + } + } + .catch { + emit(InvitesContentState.Error(it)) + } + .onStart { + emit(InvitesContentState.Loading) + }.onEach { + _invites.emit(it) + }.launchIn(viewModelScope) } } diff --git a/vector/src/main/java/im/vector/app/features/home/room/list/home/invites/InvitesViewState.kt b/vector/src/main/java/im/vector/app/features/home/room/list/home/invites/InvitesViewState.kt index 708db29604..2f82c3fe76 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/list/home/invites/InvitesViewState.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/list/home/invites/InvitesViewState.kt @@ -16,13 +16,24 @@ package im.vector.app.features.home.room.list.home.invites -import androidx.lifecycle.LiveData +import android.graphics.drawable.Drawable import androidx.paging.PagedList import com.airbnb.mvrx.MavericksState import org.matrix.android.sdk.api.session.room.members.ChangeMembershipState import org.matrix.android.sdk.api.session.room.model.RoomSummary data class InvitesViewState( - val pagedList: LiveData>? = null, val roomMembershipChanges: Map = emptyMap(), ) : MavericksState + +sealed interface InvitesContentState { + object Loading : InvitesContentState + data class Empty( + val title: CharSequence, + val image: Drawable?, + val message: CharSequence + ) : InvitesContentState + + data class Content(val content: PagedList) : InvitesContentState + data class Error(val throwable: Throwable) : InvitesContentState +} diff --git a/vector/src/main/java/im/vector/app/features/home/room/list/home/recent/RecentRoomCarouselController.kt b/vector/src/main/java/im/vector/app/features/home/room/list/home/recent/RecentRoomCarouselController.kt index ebec912779..0c5d9a3533 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/list/home/recent/RecentRoomCarouselController.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/list/home/recent/RecentRoomCarouselController.kt @@ -23,6 +23,8 @@ import com.airbnb.epoxy.CarouselModelBuilder import com.airbnb.epoxy.EpoxyController import com.airbnb.epoxy.EpoxyModel import com.airbnb.epoxy.carousel +import com.google.android.material.color.MaterialColors +import im.vector.app.R import im.vector.app.features.home.AvatarRenderer import im.vector.app.features.home.room.list.RoomListListener import org.matrix.android.sdk.api.session.room.model.RoomSummary @@ -43,6 +45,12 @@ class RecentRoomCarouselController @Inject constructor( resources.displayMetrics ).toInt() + private val topPadding = TypedValue.applyDimension( + TypedValue.COMPLEX_UNIT_DIP, + 12f, + resources.displayMetrics + ).toInt() + private val itemSpacing = TypedValue.applyDimension( TypedValue.COMPLEX_UNIT_DIP, 24f, @@ -61,11 +69,15 @@ class RecentRoomCarouselController @Inject constructor( id("recents_carousel") padding(Carousel.Padding( host.hPadding, - 0, + host.topPadding, host.hPadding, 0, host.itemSpacing) ) + onBind { _, view, _ -> + val colorSurface = MaterialColors.getColor(view, R.attr.vctr_toolbar_background) + view.setBackgroundColor(colorSurface) + } withModelsFrom(data) { roomSummary -> val onClick = host.listener?.let { it::onRoomClicked } val onLongClick = host.listener?.let { it::onRoomLongClicked } diff --git a/vector/src/main/java/im/vector/app/features/home/room/list/home/release/ReleaseCarouselData.kt b/vector/src/main/java/im/vector/app/features/home/room/list/home/release/ReleaseCarouselData.kt new file mode 100644 index 0000000000..22431b0bf9 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/home/room/list/home/release/ReleaseCarouselData.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.list.home.release + +import androidx.annotation.DrawableRes +import androidx.annotation.StringRes + +class ReleaseCarouselData( + val items: List +) { + data class Item( + @StringRes val title: Int, + @StringRes val body: Int, + @DrawableRes val image: Int, + ) +} diff --git a/vector/src/main/java/im/vector/app/features/home/room/list/home/release/ReleaseCarouselItem.kt b/vector/src/main/java/im/vector/app/features/home/room/list/home/release/ReleaseCarouselItem.kt new file mode 100644 index 0000000000..49eb0761f7 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/home/room/list/home/release/ReleaseCarouselItem.kt @@ -0,0 +1,46 @@ +/* + * 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.list.home.release + +import android.widget.ImageView +import android.widget.TextView +import com.airbnb.epoxy.EpoxyAttribute +import com.airbnb.epoxy.EpoxyModelClass +import im.vector.app.R +import im.vector.app.core.epoxy.VectorEpoxyHolder +import im.vector.app.core.epoxy.VectorEpoxyModel + +@EpoxyModelClass +abstract class ReleaseCarouselItem : VectorEpoxyModel(R.layout.item_release_carousel) { + + @EpoxyAttribute + lateinit var item: ReleaseCarouselData.Item + + override fun bind(holder: Holder) { + super.bind(holder) + + holder.image.setImageResource(item.image) + holder.title.setText(item.title) + holder.body.setText(item.body) + } + + class Holder : VectorEpoxyHolder() { + val image by bind(R.id.carousel_item_image) + val title by bind(R.id.carousel_item_title) + val body by bind(R.id.carousel_item_body) + } +} diff --git a/vector/src/main/java/im/vector/app/features/home/room/list/home/release/ReleaseNotesAction.kt b/vector/src/main/java/im/vector/app/features/home/room/list/home/release/ReleaseNotesAction.kt new file mode 100644 index 0000000000..7a66d00589 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/home/room/list/home/release/ReleaseNotesAction.kt @@ -0,0 +1,28 @@ +/* + * 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.list.home.release + +import im.vector.app.core.platform.VectorViewModelAction + +sealed class ReleaseNotesAction : VectorViewModelAction { + data class NextPressed( + val isLastItemSelected: Boolean = false + ) : ReleaseNotesAction() + data class PageSelected( + val selectedPageIndex: Int = 0 + ) : ReleaseNotesAction() +} diff --git a/vector/src/main/java/im/vector/app/features/home/room/list/home/release/ReleaseNotesActivity.kt b/vector/src/main/java/im/vector/app/features/home/room/list/home/release/ReleaseNotesActivity.kt new file mode 100644 index 0000000000..c5cc55d7bb --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/home/room/list/home/release/ReleaseNotesActivity.kt @@ -0,0 +1,41 @@ +/* + * 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.list.home.release + +import dagger.hilt.android.AndroidEntryPoint +import im.vector.app.core.extensions.addFragment +import im.vector.app.core.platform.ScreenOrientationLocker +import im.vector.app.core.platform.VectorBaseActivity +import im.vector.app.databinding.ActivitySimpleBinding +import javax.inject.Inject + +@AndroidEntryPoint +class ReleaseNotesActivity : VectorBaseActivity() { + + @Inject lateinit var orientationLocker: ScreenOrientationLocker + + override fun getBinding() = ActivitySimpleBinding.inflate(layoutInflater) + + override fun getCoordinatorLayout() = views.coordinatorLayout + + override fun initUiAndData() { + orientationLocker.lockPhonesToPortrait(this) + if (isFirstCreation()) { + addFragment(views.simpleFragmentContainer, ReleaseNotesFragment::class.java) + } + } +} diff --git a/vector/src/main/java/im/vector/app/features/home/room/list/home/release/ReleaseNotesCarouselController.kt b/vector/src/main/java/im/vector/app/features/home/room/list/home/release/ReleaseNotesCarouselController.kt new file mode 100644 index 0000000000..22d2915c47 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/home/room/list/home/release/ReleaseNotesCarouselController.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.list.home.release + +import com.airbnb.epoxy.TypedEpoxyController +import javax.inject.Inject + +class ReleaseNotesCarouselController @Inject constructor() : TypedEpoxyController() { + override fun buildModels(data: ReleaseCarouselData) { + data.items.forEachIndexed { index, item -> + releaseCarouselItem { + id(index) + item(item) + } + } + } +} diff --git a/vector/src/main/java/im/vector/app/features/home/room/list/home/release/ReleaseNotesFragment.kt b/vector/src/main/java/im/vector/app/features/home/room/list/home/release/ReleaseNotesFragment.kt new file mode 100644 index 0000000000..6b86897dc8 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/home/room/list/home/release/ReleaseNotesFragment.kt @@ -0,0 +1,138 @@ +/* + * 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.list.home.release + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.lifecycle.DefaultLifecycleObserver +import androidx.lifecycle.LifecycleOwner +import androidx.viewpager2.widget.ViewPager2 +import com.airbnb.mvrx.fragmentViewModel +import com.google.android.material.tabs.TabLayoutMediator +import dagger.hilt.android.AndroidEntryPoint +import im.vector.app.R +import im.vector.app.core.epoxy.onClick +import im.vector.app.core.platform.VectorBaseFragment +import im.vector.app.databinding.BottomSheetReleaseNotesBinding +import javax.inject.Inject + +@AndroidEntryPoint +class ReleaseNotesFragment : VectorBaseFragment() { + + @Inject lateinit var carouselController: ReleaseNotesCarouselController + private var tabLayoutMediator: TabLayoutMediator? = null + + private val viewModel by fragmentViewModel(ReleaseNotesViewModel::class) + + override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): BottomSheetReleaseNotesBinding { + return BottomSheetReleaseNotesBinding.inflate(inflater, container, false) + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + val carouselAdapter = carouselController.adapter + views.releaseNotesCarousel.adapter = carouselAdapter + + tabLayoutMediator = TabLayoutMediator(views.releaseNotesCarouselIndicator, views.releaseNotesCarousel) { _, _ -> } + .also { it.attach() } + + val pageCallback = object : ViewPager2.OnPageChangeCallback() { + override fun onPageSelected(position: Int) { + viewModel.handle(ReleaseNotesAction.PageSelected(position)) + updateButtonText(position) + } + } + + viewLifecycleOwner.lifecycle.addObserver(object : DefaultLifecycleObserver { + override fun onCreate(owner: LifecycleOwner) { + views.releaseNotesCarousel.registerOnPageChangeCallback(pageCallback) + } + + override fun onDestroy(owner: LifecycleOwner) { + views.releaseNotesCarousel.unregisterOnPageChangeCallback(pageCallback) + } + }) + + carouselController.setData(createCarouselData()) + + views.releaseNotesBtnClose.onClick { close() } + views.releaseNotesButtonNext.onClick { + val isLastItemSelected = with(views.releaseNotesCarouselIndicator) { + selectedTabPosition == tabCount - 1 + } + viewModel.handle(ReleaseNotesAction.NextPressed(isLastItemSelected)) + } + + viewModel.observeViewEvents { + when (it) { + is ReleaseNotesViewEvents.SelectPage -> selectPage(it.index) + ReleaseNotesViewEvents.Close -> close() + } + } + } + + private fun createCarouselData(): ReleaseCarouselData { + return ReleaseCarouselData( + listOf( + ReleaseCarouselData.Item( + R.string.onboarding_new_app_layout_welcome_title, + R.string.onboarding_new_app_layout_welcome_message, + R.drawable.ill_app_layout_onboarding_rooms + ), + ReleaseCarouselData.Item( + R.string.onboarding_new_app_layout_spaces_title, + R.string.onboarding_new_app_layout_spaces_message, + R.drawable.ill_app_layout_onboarding_spaces + ), + ReleaseCarouselData.Item( + R.string.onboarding_new_app_layout_feedback_title, + R.string.onboarding_new_app_layout_feedback_message, + R.drawable.ill_app_layout_onboarding_rooms + ), + ) + ) + } + + private fun close() { + requireActivity().finish() + } + + private fun selectPage(index: Int) { + views.releaseNotesCarouselIndicator.selectTab(views.releaseNotesCarouselIndicator.getTabAt(index)) + updateButtonText(index) + } + + private fun updateButtonText(selectedIndex: Int) { + val isLastItem = selectedIndex == views.releaseNotesCarouselIndicator.tabCount - 1 + if (isLastItem) { + views.releaseNotesButtonNext.setText(R.string.onboarding_new_app_layout_button_try) + } else { + views.releaseNotesButtonNext.setText(R.string.action_next) + } + } + + override fun onDestroyView() { + tabLayoutMediator?.detach() + tabLayoutMediator = null + + views.releaseNotesCarousel.adapter = null + super.onDestroyView() + } +} diff --git a/vector/src/main/java/im/vector/app/features/home/room/list/home/release/ReleaseNotesPreferencesStore.kt b/vector/src/main/java/im/vector/app/features/home/room/list/home/release/ReleaseNotesPreferencesStore.kt new file mode 100644 index 0000000000..cefe107905 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/home/room/list/home/release/ReleaseNotesPreferencesStore.kt @@ -0,0 +1,48 @@ +/* + * 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.list.home.release + +import android.content.Context +import androidx.datastore.core.DataStore +import androidx.datastore.preferences.core.Preferences +import androidx.datastore.preferences.core.booleanPreferencesKey +import androidx.datastore.preferences.core.edit +import androidx.datastore.preferences.preferencesDataStore +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.map +import org.matrix.android.sdk.api.extensions.orFalse +import javax.inject.Inject + +private val Context.dataStore: DataStore by preferencesDataStore(name = "release_notes") + +class ReleaseNotesPreferencesStore @Inject constructor( + private val context: Context +) { + + private val isAppLayoutOnboardingShown = booleanPreferencesKey("SETTINGS_APP_LAYOUT_ONBOARDING_SHOWN") + + val appLayoutOnboardingShown: Flow = context.dataStore.data + .map { preferences -> preferences[isAppLayoutOnboardingShown].orFalse() } + .distinctUntilChanged() + + suspend fun setAppLayoutOnboardingShown(isShown: Boolean) { + context.dataStore.edit { settings -> + settings[isAppLayoutOnboardingShown] = isShown + } + } +} diff --git a/vector/src/main/java/im/vector/app/features/home/room/list/home/release/ReleaseNotesViewEvents.kt b/vector/src/main/java/im/vector/app/features/home/room/list/home/release/ReleaseNotesViewEvents.kt new file mode 100644 index 0000000000..7901a8b28f --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/home/room/list/home/release/ReleaseNotesViewEvents.kt @@ -0,0 +1,24 @@ +/* + * 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.list.home.release + +import im.vector.app.core.platform.VectorViewEvents + +sealed class ReleaseNotesViewEvents : VectorViewEvents { + object Close : ReleaseNotesViewEvents() + data class SelectPage(val index: Int) : ReleaseNotesViewEvents() +} diff --git a/vector/src/main/java/im/vector/app/features/home/room/list/home/release/ReleaseNotesViewModel.kt b/vector/src/main/java/im/vector/app/features/home/room/list/home/release/ReleaseNotesViewModel.kt new file mode 100644 index 0000000000..23e2364d0c --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/home/room/list/home/release/ReleaseNotesViewModel.kt @@ -0,0 +1,63 @@ +/* + * 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.list.home.release + +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.VectorDummyViewState +import im.vector.app.core.platform.VectorViewModel + +class ReleaseNotesViewModel @AssistedInject constructor( + @Assisted initialState: VectorDummyViewState, +) : VectorViewModel(initialState) { + + @AssistedFactory + interface Factory : MavericksAssistedViewModelFactory { + override fun create(initialState: VectorDummyViewState): ReleaseNotesViewModel + } + + companion object : MavericksViewModelFactory by hiltMavericksViewModelFactory() + + private var selectedPageIndex = 0 + + init { + _viewEvents.post(ReleaseNotesViewEvents.SelectPage(0)) + } + + override fun handle(action: ReleaseNotesAction) { + when (action) { + is ReleaseNotesAction.NextPressed -> handleNextPressed(action) + is ReleaseNotesAction.PageSelected -> handlePageSelected(action) + } + } + + private fun handlePageSelected(action: ReleaseNotesAction.PageSelected) { + selectedPageIndex = action.selectedPageIndex + } + + private fun handleNextPressed(action: ReleaseNotesAction.NextPressed) { + if (action.isLastItemSelected) { + _viewEvents.post(ReleaseNotesViewEvents.Close) + } else { + _viewEvents.post(ReleaseNotesViewEvents.SelectPage(++selectedPageIndex)) + } + } +} diff --git a/vector/src/main/java/im/vector/app/features/onboarding/OnboardingViewModel.kt b/vector/src/main/java/im/vector/app/features/onboarding/OnboardingViewModel.kt index 9661feb002..52c445f1fa 100644 --- a/vector/src/main/java/im/vector/app/features/onboarding/OnboardingViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/onboarding/OnboardingViewModel.kt @@ -627,7 +627,7 @@ class OnboardingViewModel @AssistedInject constructor( _viewEvents.post(OnboardingViewEvents.OnAccountCreated) } AuthenticationDescription.Login -> { - setState { copy(isLoading = false) } + setState { copy(isLoading = false, selectedAuthenticationState = SelectedAuthenticationState(authenticationDescription)) } _viewEvents.post(OnboardingViewEvents.OnAccountSignedIn) } } diff --git a/vector/src/main/java/im/vector/app/features/popup/PopupAlertManager.kt b/vector/src/main/java/im/vector/app/features/popup/PopupAlertManager.kt index 40ef6d819e..b1327f0caf 100644 --- a/vector/src/main/java/im/vector/app/features/popup/PopupAlertManager.kt +++ b/vector/src/main/java/im/vector/app/features/popup/PopupAlertManager.kt @@ -28,6 +28,7 @@ import im.vector.app.core.time.Clock import im.vector.app.core.utils.isAnimationEnabled import im.vector.app.features.MainActivity import im.vector.app.features.analytics.ui.consent.AnalyticsOptInActivity +import im.vector.app.features.home.room.list.home.release.ReleaseNotesActivity import im.vector.app.features.pin.PinActivity import im.vector.app.features.signout.hard.SignedOutActivity import im.vector.app.features.themes.ThemeUtils @@ -307,6 +308,7 @@ class PopupAlertManager @Inject constructor( activity !is PinActivity && activity !is SignedOutActivity && activity !is AnalyticsOptInActivity && + activity !is ReleaseNotesActivity && activity is VectorBaseActivity<*> && alert.shouldBeDisplayedIn.invoke(activity) } diff --git a/vector/src/main/java/im/vector/app/features/settings/VectorPreferences.kt b/vector/src/main/java/im/vector/app/features/settings/VectorPreferences.kt index cefbe64d9d..5e0cdf3baf 100755 --- a/vector/src/main/java/im/vector/app/features/settings/VectorPreferences.kt +++ b/vector/src/main/java/im/vector/app/features/settings/VectorPreferences.kt @@ -165,6 +165,11 @@ class VectorPreferences @Inject constructor( const val SETTINGS_LABS_AUTO_REPORT_UISI = "SETTINGS_LABS_AUTO_REPORT_UISI" const val SETTINGS_PREF_SPACE_SHOW_ALL_ROOM_IN_HOME = "SETTINGS_PREF_SPACE_SHOW_ALL_ROOM_IN_HOME" + /** + * This is not preference, but category on preferences screen which contains [SETTINGS_PREF_SPACE_SHOW_ALL_ROOM_IN_HOME]. + * Needed to show/hide this category, depending on visibility of [SETTINGS_PREF_SPACE_SHOW_ALL_ROOM_IN_HOME]. */ + const val SETTINGS_PREF_SPACE_CATEGORY = "SETTINGS_PREF_SPACE_CATEGORY" + private const val SETTINGS_DEVELOPER_MODE_PREFERENCE_KEY = "SETTINGS_DEVELOPER_MODE_PREFERENCE_KEY" private const val SETTINGS_LABS_SHOW_HIDDEN_EVENTS_PREFERENCE_KEY = "SETTINGS_LABS_SHOW_HIDDEN_EVENTS_PREFERENCE_KEY" private const val SETTINGS_LABS_ENABLE_SWIPE_TO_REPLY = "SETTINGS_LABS_ENABLE_SWIPE_TO_REPLY" diff --git a/vector/src/main/java/im/vector/app/features/settings/VectorSettingsLabsFragment.kt b/vector/src/main/java/im/vector/app/features/settings/VectorSettingsLabsFragment.kt index eb7864a89d..c1253f4ab4 100644 --- a/vector/src/main/java/im/vector/app/features/settings/VectorSettingsLabsFragment.kt +++ b/vector/src/main/java/im/vector/app/features/settings/VectorSettingsLabsFragment.kt @@ -27,6 +27,7 @@ import im.vector.app.R import im.vector.app.core.preference.VectorSwitchPreference import im.vector.app.features.MainActivity import im.vector.app.features.MainActivityArgs +import im.vector.app.features.VectorFeatures import im.vector.app.features.analytics.plan.MobileScreen import im.vector.app.features.home.room.threads.ThreadsManager import org.matrix.android.sdk.api.settings.LightweightSettingsStorage @@ -39,6 +40,7 @@ class VectorSettingsLabsFragment : @Inject lateinit var vectorPreferences: VectorPreferences @Inject lateinit var lightweightSettingsStorage: LightweightSettingsStorage @Inject lateinit var threadsManager: ThreadsManager + @Inject lateinit var vectorFeatures: VectorFeatures override var titleRes = R.string.room_settings_labs_pref_title override val preferenceXmlRes = R.xml.vector_settings_labs @@ -72,6 +74,10 @@ class VectorSettingsLabsFragment : true } } + + findPreference(VectorPreferences.SETTINGS_LABS_UNREAD_NOTIFICATIONS_AS_TAB)!!.let { + it.isVisible = !vectorFeatures.isNewAppLayoutEnabled() + } } /** diff --git a/vector/src/main/java/im/vector/app/features/settings/VectorSettingsPreferencesFragment.kt b/vector/src/main/java/im/vector/app/features/settings/VectorSettingsPreferencesFragment.kt index 0bd5316b8f..135c25cd8d 100644 --- a/vector/src/main/java/im/vector/app/features/settings/VectorSettingsPreferencesFragment.kt +++ b/vector/src/main/java/im/vector/app/features/settings/VectorSettingsPreferencesFragment.kt @@ -31,6 +31,7 @@ import im.vector.app.core.preference.VectorPreference import im.vector.app.core.preference.VectorSwitchPreference import im.vector.app.features.MainActivity import im.vector.app.features.MainActivityArgs +import im.vector.app.features.VectorFeatures import im.vector.app.features.analytics.plan.MobileScreen import im.vector.app.features.settings.font.FontScaleSettingActivity import im.vector.app.features.themes.ThemeUtils @@ -44,6 +45,7 @@ class VectorSettingsPreferencesFragment : @Inject lateinit var vectorPreferences: VectorPreferences @Inject lateinit var fontScalePreferences: FontScalePreferences + @Inject lateinit var vectorFeatures: VectorFeatures override var titleRes = R.string.settings_preferences override val preferenceXmlRes = R.xml.vector_settings_preferences @@ -99,6 +101,10 @@ class VectorSettingsPreferencesFragment : } } + findPreference(VectorPreferences.SETTINGS_PREF_SPACE_CATEGORY)!!.let { pref -> + pref.isVisible = !vectorFeatures.isNewAppLayoutEnabled() + } + // Url preview /* TODO Note: we keep the setting client side for now 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 03e2d2fd98..554001ad43 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 @@ -43,7 +43,6 @@ import im.vector.app.features.settings.devices.DeviceFullInfo import im.vector.app.features.settings.devices.DevicesAction import im.vector.app.features.settings.devices.DevicesViewEvents import im.vector.app.features.settings.devices.DevicesViewModel -import im.vector.app.features.settings.devices.v2.list.OtherSessionsController import im.vector.app.features.settings.devices.v2.list.OtherSessionsView import im.vector.app.features.settings.devices.v2.list.SESSION_IS_MARKED_AS_INACTIVE_AFTER_DAYS import im.vector.app.features.settings.devices.v2.list.SecurityRecommendationViewState @@ -129,11 +128,6 @@ class VectorSettingsDevicesFragment : private fun initOtherSessionsView() { views.deviceListOtherSessions.callback = this - views.deviceListOtherSessions.setCallback(object : OtherSessionsController.Callback { - override fun onItemClicked(deviceId: String) { - navigateToSessionOverview(deviceId) - } - }) } override fun onDestroyView() { @@ -264,6 +258,10 @@ class VectorSettingsDevicesFragment : } } + override fun onOtherSessionClicked(deviceId: String) { + navigateToSessionOverview(deviceId) + } + override fun onViewAllOtherSessionsClicked() { viewNavigator.navigateToOtherSessions(requireActivity()) } diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/list/OtherSessionsView.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/list/OtherSessionsView.kt index c6f8c02d22..bb61e0f595 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/list/OtherSessionsView.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/list/OtherSessionsView.kt @@ -32,9 +32,10 @@ class OtherSessionsView @JvmOverloads constructor( context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0 -) : ConstraintLayout(context, attrs, defStyleAttr) { +) : ConstraintLayout(context, attrs, defStyleAttr), OtherSessionsController.Callback { interface Callback { + fun onOtherSessionClicked(deviceId: String) fun onViewAllOtherSessionsClicked() } @@ -47,6 +48,8 @@ class OtherSessionsView @JvmOverloads constructor( inflate(context, R.layout.view_other_sessions, this) views = ViewOtherSessionsBinding.bind(this) + otherSessionsController.callback = this + views.otherSessionsViewAllButton.setOnClickListener { callback?.onViewAllOtherSessionsClicked() } @@ -58,13 +61,13 @@ class OtherSessionsView @JvmOverloads constructor( otherSessionsController.setData(devices) } - fun setCallback(callback: OtherSessionsController.Callback) { - otherSessionsController.callback = callback - } - override fun onDetachedFromWindow() { otherSessionsController.callback = null views.otherSessionsRecyclerView.cleanup() super.onDetachedFromWindow() } + + override fun onItemClicked(deviceId: String) { + callback?.onOtherSessionClicked(deviceId) + } } diff --git a/vector/src/main/java/im/vector/app/features/spaces/NewSpaceSummaryItem.kt b/vector/src/main/java/im/vector/app/features/spaces/NewSpaceSummaryItem.kt index e6e4aadb9c..d59de6ac99 100644 --- a/vector/src/main/java/im/vector/app/features/spaces/NewSpaceSummaryItem.kt +++ b/vector/src/main/java/im/vector/app/features/spaces/NewSpaceSummaryItem.kt @@ -58,7 +58,10 @@ abstract class NewSpaceSummaryItem : VectorEpoxyModel 0 holder.indent.updateLayoutParams { diff --git a/vector/src/main/java/im/vector/app/features/spaces/SpaceListFragment.kt b/vector/src/main/java/im/vector/app/features/spaces/SpaceListFragment.kt index ca9279cb37..0153d64df7 100644 --- a/vector/src/main/java/im/vector/app/features/spaces/SpaceListFragment.kt +++ b/vector/src/main/java/im/vector/app/features/spaces/SpaceListFragment.kt @@ -21,6 +21,7 @@ import android.view.HapticFeedbackConstants import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import androidx.core.view.isVisible import com.airbnb.epoxy.EpoxyTouchHelper import com.airbnb.mvrx.Loading import com.airbnb.mvrx.Success @@ -28,6 +29,7 @@ import com.airbnb.mvrx.Uninitialized import com.airbnb.mvrx.fragmentViewModel import com.airbnb.mvrx.withState import dagger.hilt.android.AndroidEntryPoint +import im.vector.app.core.epoxy.onClick import im.vector.app.core.extensions.cleanup import im.vector.app.core.extensions.configureWith import im.vector.app.core.platform.StateView @@ -71,6 +73,7 @@ class SpaceListFragment : homeActivitySharedActionViewModel = activityViewModelProvider[HomeSharedActionViewModel::class.java] roomListSharedActionViewModel = activityViewModelProvider[RoomListSharedActionViewModel::class.java] views.stateView.contentView = views.groupListView + views.spacesEmptyButton.onClick { onAddSpaceSelected() } setupSpaceController() observeViewEvents() } @@ -147,13 +150,22 @@ class SpaceListFragment : } override fun invalidate() = withState(viewModel) { state -> - when (state.asyncSpaces) { + when (val spaces = state.asyncSpaces) { Uninitialized, is Loading -> { views.stateView.state = StateView.State.Loading return@withState } - is Success -> views.stateView.state = StateView.State.Content + is Success -> { + views.stateView.state = StateView.State.Content + if (spaces.invoke().isEmpty()) { + views.spacesEmptyGroup.isVisible = true + views.groupListView.isVisible = false + } else { + views.spacesEmptyGroup.isVisible = false + views.groupListView.isVisible = true + } + } else -> Unit } diff --git a/vector/src/main/java/im/vector/app/features/voice/VoiceRecorderProvider.kt b/vector/src/main/java/im/vector/app/features/voice/VoiceRecorderProvider.kt index 28693ca287..1bf289fb4c 100644 --- a/vector/src/main/java/im/vector/app/features/voice/VoiceRecorderProvider.kt +++ b/vector/src/main/java/im/vector/app/features/voice/VoiceRecorderProvider.kt @@ -17,7 +17,10 @@ package im.vector.app.features.voice import android.content.Context +import android.media.MediaCodecList +import android.media.MediaFormat import android.os.Build +import androidx.annotation.VisibleForTesting import im.vector.app.features.VectorFeatures import kotlinx.coroutines.Dispatchers import javax.inject.Inject @@ -27,10 +30,21 @@ class VoiceRecorderProvider @Inject constructor( private val vectorFeatures: VectorFeatures, ) { fun provideVoiceRecorder(): VoiceRecorder { - return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q && vectorFeatures.forceUsageOfOpusEncoder().not()) { - VoiceRecorderQ(context) - } else { + return if (useFallbackRecorder()) { VoiceRecorderL(context, Dispatchers.IO) + } else { + VoiceRecorderQ(context) } } + + private fun useFallbackRecorder(): Boolean { + return Build.VERSION.SDK_INT < Build.VERSION_CODES.Q || !hasOpusEncoder() || vectorFeatures.forceUsageOfOpusEncoder() + } + + @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) + internal fun hasOpusEncoder(): Boolean { + val codecList = MediaCodecList(MediaCodecList.ALL_CODECS) + val format = MediaFormat.createAudioFormat(MediaFormat.MIMETYPE_AUDIO_OPUS, 48000, 1) + return codecList.findEncoderForFormat(format) != null + } } diff --git a/vector/src/main/res/drawable-hdpi/ill_app_layout_onboarding_rooms.webp b/vector/src/main/res/drawable-hdpi/ill_app_layout_onboarding_rooms.webp new file mode 100644 index 0000000000..5ac890e617 Binary files /dev/null and b/vector/src/main/res/drawable-hdpi/ill_app_layout_onboarding_rooms.webp differ diff --git a/vector/src/main/res/drawable-hdpi/ill_app_layout_onboarding_spaces.webp b/vector/src/main/res/drawable-hdpi/ill_app_layout_onboarding_spaces.webp new file mode 100644 index 0000000000..35f2a04236 Binary files /dev/null and b/vector/src/main/res/drawable-hdpi/ill_app_layout_onboarding_spaces.webp differ diff --git a/vector/src/main/res/drawable-hdpi/ill_empty_all_chats.webp b/vector/src/main/res/drawable-hdpi/ill_empty_all_chats.webp new file mode 100644 index 0000000000..6f5211b17e Binary files /dev/null and b/vector/src/main/res/drawable-hdpi/ill_empty_all_chats.webp differ diff --git a/vector/src/main/res/drawable-hdpi/ill_empty_space.webp b/vector/src/main/res/drawable-hdpi/ill_empty_space.webp new file mode 100644 index 0000000000..b33fe7937c Binary files /dev/null and b/vector/src/main/res/drawable-hdpi/ill_empty_space.webp differ diff --git a/vector/src/main/res/drawable-hdpi/ill_empty_unreads.webp b/vector/src/main/res/drawable-hdpi/ill_empty_unreads.webp new file mode 100644 index 0000000000..ce94823cc7 Binary files /dev/null and b/vector/src/main/res/drawable-hdpi/ill_empty_unreads.webp differ diff --git a/vector/src/main/res/drawable-mdpi/ill_app_layout_onboarding_rooms.webp b/vector/src/main/res/drawable-mdpi/ill_app_layout_onboarding_rooms.webp new file mode 100644 index 0000000000..07ece0d947 Binary files /dev/null and b/vector/src/main/res/drawable-mdpi/ill_app_layout_onboarding_rooms.webp differ diff --git a/vector/src/main/res/drawable-mdpi/ill_app_layout_onboarding_spaces.webp b/vector/src/main/res/drawable-mdpi/ill_app_layout_onboarding_spaces.webp new file mode 100644 index 0000000000..a77bce20e8 Binary files /dev/null and b/vector/src/main/res/drawable-mdpi/ill_app_layout_onboarding_spaces.webp differ diff --git a/vector/src/main/res/drawable-mdpi/ill_empty_all_chats.webp b/vector/src/main/res/drawable-mdpi/ill_empty_all_chats.webp new file mode 100644 index 0000000000..41c83c6b50 Binary files /dev/null and b/vector/src/main/res/drawable-mdpi/ill_empty_all_chats.webp differ diff --git a/vector/src/main/res/drawable-mdpi/ill_empty_space.webp b/vector/src/main/res/drawable-mdpi/ill_empty_space.webp new file mode 100644 index 0000000000..379bf44b63 Binary files /dev/null and b/vector/src/main/res/drawable-mdpi/ill_empty_space.webp differ diff --git a/vector/src/main/res/drawable-mdpi/ill_empty_unreads.webp b/vector/src/main/res/drawable-mdpi/ill_empty_unreads.webp new file mode 100644 index 0000000000..4209c0591d Binary files /dev/null and b/vector/src/main/res/drawable-mdpi/ill_empty_unreads.webp differ diff --git a/vector/src/main/res/drawable-xhdpi/ill_app_layout_onboarding_rooms.webp b/vector/src/main/res/drawable-xhdpi/ill_app_layout_onboarding_rooms.webp new file mode 100644 index 0000000000..e86bd5fe6b Binary files /dev/null and b/vector/src/main/res/drawable-xhdpi/ill_app_layout_onboarding_rooms.webp differ diff --git a/vector/src/main/res/drawable-xhdpi/ill_app_layout_onboarding_spaces.webp b/vector/src/main/res/drawable-xhdpi/ill_app_layout_onboarding_spaces.webp new file mode 100644 index 0000000000..9b5b9fa607 Binary files /dev/null and b/vector/src/main/res/drawable-xhdpi/ill_app_layout_onboarding_spaces.webp differ diff --git a/vector/src/main/res/drawable-xhdpi/ill_empty_all_chats.webp b/vector/src/main/res/drawable-xhdpi/ill_empty_all_chats.webp new file mode 100644 index 0000000000..c8374d1160 Binary files /dev/null and b/vector/src/main/res/drawable-xhdpi/ill_empty_all_chats.webp differ diff --git a/vector/src/main/res/drawable-xhdpi/ill_empty_space.webp b/vector/src/main/res/drawable-xhdpi/ill_empty_space.webp new file mode 100644 index 0000000000..c6d83f16c7 Binary files /dev/null and b/vector/src/main/res/drawable-xhdpi/ill_empty_space.webp differ diff --git a/vector/src/main/res/drawable-xhdpi/ill_empty_unreads.webp b/vector/src/main/res/drawable-xhdpi/ill_empty_unreads.webp new file mode 100644 index 0000000000..14d2dbdf9a Binary files /dev/null and b/vector/src/main/res/drawable-xhdpi/ill_empty_unreads.webp differ diff --git a/vector/src/main/res/drawable-xxhdpi/ill_app_layout_onboarding_rooms.webp b/vector/src/main/res/drawable-xxhdpi/ill_app_layout_onboarding_rooms.webp new file mode 100644 index 0000000000..f95909eaae Binary files /dev/null and b/vector/src/main/res/drawable-xxhdpi/ill_app_layout_onboarding_rooms.webp differ diff --git a/vector/src/main/res/drawable-xxhdpi/ill_app_layout_onboarding_spaces.webp b/vector/src/main/res/drawable-xxhdpi/ill_app_layout_onboarding_spaces.webp new file mode 100644 index 0000000000..03e31dc9d6 Binary files /dev/null and b/vector/src/main/res/drawable-xxhdpi/ill_app_layout_onboarding_spaces.webp differ diff --git a/vector/src/main/res/drawable-xxhdpi/ill_empty_all_chats.webp b/vector/src/main/res/drawable-xxhdpi/ill_empty_all_chats.webp new file mode 100644 index 0000000000..fc19311faf Binary files /dev/null and b/vector/src/main/res/drawable-xxhdpi/ill_empty_all_chats.webp differ diff --git a/vector/src/main/res/drawable-xxhdpi/ill_empty_space.webp b/vector/src/main/res/drawable-xxhdpi/ill_empty_space.webp new file mode 100644 index 0000000000..18b26b82ff Binary files /dev/null and b/vector/src/main/res/drawable-xxhdpi/ill_empty_space.webp differ diff --git a/vector/src/main/res/drawable-xxhdpi/ill_empty_unreads.webp b/vector/src/main/res/drawable-xxhdpi/ill_empty_unreads.webp new file mode 100644 index 0000000000..17127018ba Binary files /dev/null and b/vector/src/main/res/drawable-xxhdpi/ill_empty_unreads.webp differ diff --git a/vector/src/main/res/drawable-xxxhdpi/ill_app_layout_onboarding_rooms.webp b/vector/src/main/res/drawable-xxxhdpi/ill_app_layout_onboarding_rooms.webp new file mode 100644 index 0000000000..cee3cf512c Binary files /dev/null and b/vector/src/main/res/drawable-xxxhdpi/ill_app_layout_onboarding_rooms.webp differ diff --git a/vector/src/main/res/drawable-xxxhdpi/ill_app_layout_onboarding_spaces.webp b/vector/src/main/res/drawable-xxxhdpi/ill_app_layout_onboarding_spaces.webp new file mode 100644 index 0000000000..b4bf421be5 Binary files /dev/null and b/vector/src/main/res/drawable-xxxhdpi/ill_app_layout_onboarding_spaces.webp differ diff --git a/vector/src/main/res/drawable-xxxhdpi/ill_empty_all_chats.webp b/vector/src/main/res/drawable-xxxhdpi/ill_empty_all_chats.webp new file mode 100644 index 0000000000..e020c33543 Binary files /dev/null and b/vector/src/main/res/drawable-xxxhdpi/ill_empty_all_chats.webp differ diff --git a/vector/src/main/res/drawable-xxxhdpi/ill_empty_space.webp b/vector/src/main/res/drawable-xxxhdpi/ill_empty_space.webp new file mode 100644 index 0000000000..2c11bbafa0 Binary files /dev/null and b/vector/src/main/res/drawable-xxxhdpi/ill_empty_space.webp differ diff --git a/vector/src/main/res/drawable-xxxhdpi/ill_empty_unreads.webp b/vector/src/main/res/drawable-xxxhdpi/ill_empty_unreads.webp new file mode 100644 index 0000000000..278fbfac0b Binary files /dev/null and b/vector/src/main/res/drawable-xxxhdpi/ill_empty_unreads.webp differ diff --git a/vector/src/main/res/drawable/ic_invites_empty.xml b/vector/src/main/res/drawable/ic_invites_empty.xml new file mode 100644 index 0000000000..79908ff380 --- /dev/null +++ b/vector/src/main/res/drawable/ic_invites_empty.xml @@ -0,0 +1,14 @@ + + + + diff --git a/vector/src/main/res/layout/activity_home.xml b/vector/src/main/res/layout/activity_home.xml index 9899c15aa6..698aab2340 100644 --- a/vector/src/main/res/layout/activity_home.xml +++ b/vector/src/main/res/layout/activity_home.xml @@ -28,4 +28,4 @@ android:layout_height="match_parent" android:layout_gravity="start" /> - \ No newline at end of file + diff --git a/vector/src/main/res/layout/bottom_sheet_release_notes.xml b/vector/src/main/res/layout/bottom_sheet_release_notes.xml new file mode 100644 index 0000000000..1d14c2c4a2 --- /dev/null +++ b/vector/src/main/res/layout/bottom_sheet_release_notes.xml @@ -0,0 +1,55 @@ + + + + + + + +