Merge branch 'feature/mna/session-overview-screen' into feature/ons/device_manager_filter

* feature/mna/session-overview-screen: (57 commits)
  Fix missing mapper in CryptoStoreHelper for tests
  Fix unused string warning
  Update unit tests
  Rendering inactive status in SessionInfoView
  Adding comment with examples of some parametrized strings
  Fix post rebase
  Fixing wrong copyright title
  Adding last seen details + fix observation of wrong deviceId in ViewModel
  Adding learn more link in verification status details
  Unit tests for computing trust level of device
  Unit tests for GetCurrentSessionCrossSigningInfoUseCase
  Updating existing unit tests
  Navigation from other session item
  Show info in overview screen
  Renaming CurrentSessionView into SessionInfoView to be more generic
  Introducing some reusable usecases
  Adding unit tests for viewModel
  Adding unit tests for mapper
  Adding unit tests for the new use case
  Adding use case to get full device info for a given device id
  ...

# Conflicts:
#	library/ui-strings/src/main/res/values/strings.xml
#	vector/src/main/AndroidManifest.xml
#	vector/src/main/java/im/vector/app/core/di/MavericksViewModelModule.kt
#	vector/src/main/java/im/vector/app/features/settings/devices/v2/VectorSettingsDevicesFragment.kt
#	vector/src/main/java/im/vector/app/features/settings/devices/v2/VectorSettingsDevicesViewNavigator.kt
This commit is contained in:
Onuray Sahin 2022-09-07 16:33:35 +03:00
commit 8dcbd3710d
112 changed files with 1210 additions and 145 deletions

View file

@ -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

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

@ -0,0 +1 @@
[App Layout] Obsolete settings are not shown when App Layout flag is enabled

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

@ -0,0 +1 @@
[App Layout] - space switcher now has empty state

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

@ -0,0 +1 @@
[App Layout] New empty states for home screen

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

@ -0,0 +1 @@
[App Layout] - Invites now show empty screen after you reject last invite

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

@ -0,0 +1 @@
Catch race condition crash in voice recording

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

@ -0,0 +1 @@
Try to detect devices that lack Opus encoder support, use bundled libopus library for those.

1
changelog.d/7016.wip Normal file
View file

@ -0,0 +1 @@
[New Layout] Improves talkback accessibility

View file

@ -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
```

View file

@ -140,8 +140,10 @@
<string name="start_chat">Start Chat</string>
<string name="create_room">Create Room</string>
<string name="explore_rooms">Explore Rooms</string>
<string name="a11y_expand_space_children">Expand space children</string>
<string name="a11y_collapse_space_children">Collapse space children</string>
<!-- Note to translators: %s refers to the space whose children is being expanded -->
<string name="a11y_expand_space_children">Expand %s children</string>
<!-- Note to translators: %s refers to the space whose children is being collapsed -->
<string name="a11y_collapse_space_children">Collapse %s children</string>
<!-- Last seen time -->
@ -442,9 +444,16 @@
<string name="system_alerts_header">"System Alerts"</string>
<string name="suggested_header">Suggested Rooms</string>
<!-- Space List fragment -->
<string name="space_list_empty_title">No spaces yet.</string>
<string name="space_list_empty_message">Spaces are a new way to group rooms and people. Create a space to get started.</string>
<!-- Invites fragment -->
<string name="invites_title">Invites</string>
<string name="invites_empty_title">Nothing new.</string>
<string name="invites_empty_message">This is where your new requests and invites will be.</string>
<!-- People fragment -->
<string name="direct_chats_header">Conversations</string>
<string name="matrix_only_filter">Matrix contacts only</string>
@ -3252,6 +3261,27 @@
<string name="device_manager_session_title">Session</string>
<!-- Examples: Last activity Yesterday at 6PM, Last activity Aug 31 at 5:47PM -->
<string name="device_manager_session_last_activity">Last activity %1$s</string>
<!-- Note to translators: %s will be replaces with selected space name -->
<string name="home_empty_space_no_rooms_title">%s\nis looking a little empty.</string>
<!-- Note to translators: for RTL languages, Spaces will be at the bottom left. Please translate "bottom-left" instead of "bottom-right". Thanks!-->
<string name="home_empty_space_no_rooms_message">Spaces are a new way to group rooms and people. Add an existing room, or create a new one, using the bottom-right button.</string>
<!-- Note to translators: %s will be replaces with current user displayname -->
<string name="home_empty_no_rooms_title">Welcome to ${app_name},\n%s.</string>
<string name="home_empty_no_rooms_message">The all-in-one secure chat app for teams, friends and organisations. Create a chat, or join an existing room, to get started.</string>
<string name="home_empty_no_unreads_title">Nothing to report.</string>
<string name="home_empty_no_unreads_message">This is where your unread messages will show up, when you have some.</string>
<string name="onboarding_new_app_layout_welcome_title">Welcome to a new view!</string>
<!-- Note to translators: for RTL languages, menu will be at the bottom left. Please translate "bottom-left" instead of "bottom-right". Thanks!-->
<string name="onboarding_new_app_layout_welcome_message">To simplify your ${app_name}, tabs are now optional. Manage them using the top-right menu.</string>
<string name="onboarding_new_app_layout_spaces_title">Access Spaces</string>
<!-- Note to translators: for RTL languages, Spaces will be at the bottom left. Please translate "bottom-left" instead of "bottom-right". Thanks!-->
<string name="onboarding_new_app_layout_spaces_message">Access your Spaces (bottom-right) faster and easier than ever before.</string>
<string name="onboarding_new_app_layout_feedback_title">Give Feedback</string>
<!-- Note to translators: for RTL languages, context menu will be at top left corner instead of top right corner. Thanks!-->
<string name="onboarding_new_app_layout_feedback_message">Tap top right to see the option to feedback.</string>
<string name="onboarding_new_app_layout_button_try">Try it out</string>
<string name="device_manager_filter_bottom_sheet_title">Filter</string>
<string name="device_manager_filter_option_all_sessions">All session</string>
<string name="device_manager_filter_option_verified">Verified</string>

View file

@ -2,4 +2,8 @@
<resources>
<item name="ftue_auth_carousel_item_spacing" format="float" type="dimen">0.05</item>
<item name="ftue_auth_carousel_item_image_height" format="float" type="dimen">0.40</item>
</resources>
<dimen name="release_notes_vertical_margin_small">16dp</dimen>
<dimen name="release_notes_vertical_margin">40dp</dimen>
<dimen name="release_notes_vertical_margin_large">46dp</dimen>
</resources>

View file

@ -74,4 +74,9 @@
<!-- Material 3 -->
<dimen name="collapsing_toolbar_layout_medium_size">112dp</dimen>
<dimen name="release_notes_vertical_margin_small">8dp</dimen>
<dimen name="release_notes_vertical_margin">16dp</dimen>
<dimen name="release_notes_vertical_margin_large">28dp</dimen>
</resources>

View file

@ -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()
)
}
}

View file

@ -340,10 +340,6 @@ android {
"-opt-in=kotlinx.coroutines.ExperimentalCoroutinesApi",
]
}
buildFeatures {
viewBinding true
}
}
dependencies {

View file

@ -1,7 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application android:name="im.vector.app.VectorApplication">
<application>
<!-- Firebase components -->
<meta-data

View file

@ -4,6 +4,7 @@
package="im.vector.application">
<application
android:name="im.vector.app.VectorApplication"
android:allowBackup="false"
android:hasFragileUserData="true"
android:icon="@mipmap/ic_launcher"

View file

@ -78,17 +78,10 @@ android {
productFlavors {
gplay {
dimension "store"
isDefault = true
buildConfigField "String", "SHORT_FLAVOR_DESCRIPTION", "\"G\""
buildConfigField "String", "FLAVOR_DESCRIPTION", "\"GooglePlay\""
}
fdroid {
dimension "store"
buildConfigField "String", "SHORT_FLAVOR_DESCRIPTION", "\"F\""
buildConfigField "String", "FLAVOR_DESCRIPTION", "\"FDroid\""
}
}
@ -264,7 +257,7 @@ dependencies {
// UnifiedPush
implementation 'com.github.UnifiedPush:android-connector:2.0.1'
// UnifiedPush gplay flavor only
gplayImplementation('com.github.UnifiedPush:android-embedded_fcm_distributor:2.1.2') {
gplayImplementation('com.github.UnifiedPush:android-embedded_fcm_distributor:2.1.3') {
exclude group: 'com.google.firebase', module: 'firebase-core'
exclude group: 'com.google.firebase', module: 'firebase-analytics'
exclude group: 'com.google.firebase', module: 'firebase-measurement-connector'

View file

@ -20,6 +20,8 @@ import android.os.Build
import androidx.test.platform.app.InstrumentationRegistry
import im.vector.app.AndroidVersionTestOverrider
import im.vector.app.features.DefaultVectorFeatures
import io.mockk.every
import io.mockk.spyk
import org.amshove.kluent.shouldBeInstanceOf
import org.junit.After
import org.junit.Test
@ -27,7 +29,7 @@ import org.junit.Test
class VoiceRecorderProviderTests {
private val context = InstrumentationRegistry.getInstrumentation().targetContext
private val provider = VoiceRecorderProvider(context, DefaultVectorFeatures())
private val provider = spyk(VoiceRecorderProvider(context, DefaultVectorFeatures()))
@After
fun tearDown() {
@ -35,11 +37,19 @@ class VoiceRecorderProviderTests {
}
@Test
fun provideVoiceRecorderOnAndroidQReturnsQRecorder() {
fun provideVoiceRecorderOnAndroidQAndCodecReturnsQRecorder() {
AndroidVersionTestOverrider.override(Build.VERSION_CODES.Q)
every { provider.hasOpusEncoder() } returns true
provider.provideVoiceRecorder().shouldBeInstanceOf(VoiceRecorderQ::class)
}
@Test
fun provideVoiceRecorderOnAndroidQButNoCodecReturnsLRecorder() {
AndroidVersionTestOverrider.override(Build.VERSION_CODES.Q)
every { provider.hasOpusEncoder() } returns false
provider.provideVoiceRecorder().shouldBeInstanceOf(VoiceRecorderL::class)
}
@Test
fun provideVoiceRecorderOnOlderAndroidVersionReturnsLRecorder() {
AndroidVersionTestOverrider.override(Build.VERSION_CODES.LOLLIPOP)

View file

@ -338,6 +338,7 @@
<activity android:name=".features.settings.font.FontScaleSettingActivity"/>
<activity android:name=".features.call.dialpad.PstnDialActivity" />
<activity android:name=".features.home.room.list.home.invites.InvitesActivity"/>
<activity android:name=".features.home.room.list.home.release.ReleaseNotesActivity"/>
<activity android:name=".features.settings.devices.v2.overview.SessionOverviewActivity"/>
<activity android:name=".features.settings.devices.v2.othersessions.OtherSessionsActivity" />

View file

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

View file

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

View file

@ -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.

View file

@ -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.
*/

View file

@ -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<String, Any>? {
return mutableMapOf<String, Any>().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) }

View file

@ -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.

View file

@ -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

View file

@ -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

View file

@ -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<HomeActivityViewState, HomeActivityViewActions, HomeActivityViewEvents>(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

View file

@ -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(

View file

@ -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<Int>()
@ -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
}
}

View file

@ -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) {

View file

@ -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
}

View file

@ -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<HomeRoomListViewState, HomeRoomListAction, HomeRoomListViewEvents>(initialState) {
@AssistedFactory
@ -82,6 +90,10 @@ class HomeRoomListViewModel @AssistedInject constructor(
private val _sections = MutableSharedFlow<Set<HomeRoomSection>>(replay = 1)
val sections = _sections.asSharedFlow()
private var currentFilter: HomeRoomFilter = HomeRoomFilter.ALL
private val _emptyStateFlow = MutableSharedFlow<Optional<StateView.State.Empty>>(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<Optional<List<HomeRoomFilter>>> {
val flow = MutableSharedFlow<Optional<List<HomeRoomFilter>>>(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 {

View file

@ -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<RoomListEmptyItem.Holder>(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<StateView>(R.id.stateView)
}
}

View file

@ -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<HomeRoomFilter>? = null
private var emptyStateData: StateView.State.Empty? = null
private var currentState: StateView.State = StateView.State.Content
override fun addModels(models: List<EpoxyModel<*>>) {
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<HomeRoomFilter>?) {
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)

View file

@ -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<FragmentInvitesBinding>(), RoomListLi
setupToolbar(views.invitesToolbar)
.allowBack()
views.invitesStateView.contentView = views.invitesRecycler
views.invitesRecycler.configureWith(controller)
controller.listener = this
@ -62,13 +67,31 @@ class InvitesFragment : VectorBaseFragment<FragmentInvitesBinding>(), 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<FragmentInvitesBinding>(), 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))

View file

@ -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()
}

View file

@ -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<InvitesViewState, InvitesAction, InvitesViewEvents>(initialState) {
private val pagedListConfig = PagedList.Config.Builder()
@ -52,6 +65,11 @@ class InvitesViewModel @AssistedInject constructor(
companion object : MavericksViewModelFactory<InvitesViewModel, InvitesViewState> by hiltMavericksViewModelFactory()
private val _invites = MutableSharedFlow<InvitesContentState>(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)
}
}

View file

@ -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<PagedList<RoomSummary>>? = null,
val roomMembershipChanges: Map<String, ChangeMembershipState> = 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<RoomSummary>) : InvitesContentState
data class Error(val throwable: Throwable) : InvitesContentState
}

View file

@ -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 }

View file

@ -0,0 +1,30 @@
/*
* Copyright (c) 2022 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.app.features.home.room.list.home.release
import androidx.annotation.DrawableRes
import androidx.annotation.StringRes
class ReleaseCarouselData(
val items: List<Item>
) {
data class Item(
@StringRes val title: Int,
@StringRes val body: Int,
@DrawableRes val image: Int,
)
}

View file

@ -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<ReleaseCarouselItem.Holder>(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<ImageView>(R.id.carousel_item_image)
val title by bind<TextView>(R.id.carousel_item_title)
val body by bind<TextView>(R.id.carousel_item_body)
}
}

View file

@ -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()
}

View file

@ -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<ActivitySimpleBinding>() {
@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)
}
}
}

View file

@ -0,0 +1,31 @@
/*
* Copyright (c) 2021 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.app.features.home.room.list.home.release
import com.airbnb.epoxy.TypedEpoxyController
import javax.inject.Inject
class ReleaseNotesCarouselController @Inject constructor() : TypedEpoxyController<ReleaseCarouselData>() {
override fun buildModels(data: ReleaseCarouselData) {
data.items.forEachIndexed { index, item ->
releaseCarouselItem {
id(index)
item(item)
}
}
}
}

View file

@ -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<BottomSheetReleaseNotesBinding>() {
@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()
}
}

View file

@ -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<Preferences> 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<Boolean> = context.dataStore.data
.map { preferences -> preferences[isAppLayoutOnboardingShown].orFalse() }
.distinctUntilChanged()
suspend fun setAppLayoutOnboardingShown(isShown: Boolean) {
context.dataStore.edit { settings ->
settings[isAppLayoutOnboardingShown] = isShown
}
}
}

View file

@ -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()
}

View file

@ -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<VectorDummyViewState, ReleaseNotesAction, ReleaseNotesViewEvents>(initialState) {
@AssistedFactory
interface Factory : MavericksAssistedViewModelFactory<ReleaseNotesViewModel, VectorDummyViewState> {
override fun create(initialState: VectorDummyViewState): ReleaseNotesViewModel
}
companion object : MavericksViewModelFactory<ReleaseNotesViewModel, VectorDummyViewState> 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))
}
}
}

View file

@ -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)
}
}

View file

@ -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)
}

View file

@ -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"

View file

@ -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<VectorSwitchPreference>(VectorPreferences.SETTINGS_LABS_UNREAD_NOTIFICATIONS_AS_TAB)!!.let {
it.isVisible = !vectorFeatures.isNewAppLayoutEnabled()
}
}
/**

View file

@ -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<Preference>(VectorPreferences.SETTINGS_PREF_SPACE_CATEGORY)!!.let { pref ->
pref.isVisible = !vectorFeatures.isNewAppLayoutEnabled()
}
// Url preview
/*
TODO Note: we keep the setting client side for now

View file

@ -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())
}

View file

@ -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)
}
}

View file

@ -58,7 +58,10 @@ abstract class NewSpaceSummaryItem : VectorEpoxyModel<NewSpaceSummaryItem.Holder
holder.chevron.setOnClickListener(onToggleExpandListener)
holder.chevron.isVisible = hasChildren
holder.chevron.setImageResource(if (expanded) R.drawable.ic_expand_more else R.drawable.ic_arrow_right)
holder.chevron.contentDescription = context.getString(if (expanded) R.string.a11y_collapse_space_children else R.string.a11y_expand_space_children)
holder.chevron.contentDescription = context.getString(
if (expanded) R.string.a11y_collapse_space_children else R.string.a11y_expand_space_children,
matrixItem.displayName,
)
avatarRenderer.render(matrixItem, holder.avatar)
holder.unreadCounter.render(countState)

View file

@ -50,6 +50,7 @@ abstract class NewSubSpaceSummaryItem : VectorEpoxyModel<NewSubSpaceSummaryItem.
override fun bind(holder: Holder) {
super.bind(holder)
val context = holder.root.context
holder.root.onClick(onSubSpaceSelectedListener)
holder.name.text = matrixItem.displayName
holder.root.isChecked = selected
@ -63,6 +64,10 @@ abstract class NewSubSpaceSummaryItem : VectorEpoxyModel<NewSubSpaceSummaryItem.
)
holder.chevron.onClick(onToggleExpandListener)
holder.chevron.isVisible = hasChildren
holder.chevron.contentDescription = context.getString(
if (expanded) R.string.a11y_collapse_space_children else R.string.a11y_expand_space_children,
matrixItem.displayName,
)
holder.indent.isVisible = indent > 0
holder.indent.updateLayoutParams {

View file

@ -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
}

View file

@ -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
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 794 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 556 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2 KiB

View file

@ -0,0 +1,14 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="60dp"
android:height="60dp"
android:viewportWidth="60"
android:viewportHeight="60">
<path
android:pathData="M30,30m-30,0a30,30 0,1 1,60 0a30,30 0,1 1,-60 0"
android:fillColor="#E3E8F0"/>
<path
android:pathData="M25.665,33.544L15.229,23.209L29.236,13.398C29.993,12.868 31.007,12.868 31.764,13.398L45.771,23.209L35.247,33.631L33.851,32.446C31.93,30.816 29.11,30.778 27.145,32.355L25.665,33.544ZM22.439,36.134L14,42.91V27.777L22.439,36.134ZM47,27.777V43.606L38.393,36.301L47,27.777ZM31.177,35.566L43.47,46H16.714L29.733,35.546C30.156,35.208 30.765,35.216 31.177,35.566Z"
android:strokeWidth="2"
android:fillColor="#737D8C"
android:strokeColor="#737D8C"/>
</vector>

View file

@ -28,4 +28,4 @@
android:layout_height="match_parent"
android:layout_gravity="start" />
</androidx.drawerlayout.widget.DrawerLayout>
</androidx.drawerlayout.widget.DrawerLayout>

View file

@ -0,0 +1,55 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_gravity="bottom"
android:background="?colorSurface">
<androidx.appcompat.widget.AppCompatImageButton
android:id="@+id/release_notes_btn_close"
android:layout_width="48dp"
android:layout_height="48dp"
android:background="@null"
android:src="@drawable/ic_close_24dp"
android:tint="?vctr_content_secondary"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent"/>
<com.google.android.material.tabs.TabLayout
android:id="@+id/release_notes_carousel_indicator"
android:layout_width="match_parent"
android:layout_height="36dp"
android:layout_marginBottom="@dimen/release_notes_vertical_margin_small"
android:background="@null"
app:layout_constraintBottom_toTopOf="@id/releaseNotesButtonNext"
app:tabBackground="@drawable/indicator_onboarding_carousel_selector"
app:tabGravity="center"
app:tabIndicatorHeight="0dp"
app:tabPaddingEnd="8dp"
app:tabPaddingStart="8dp" />
<Button
android:id="@+id/releaseNotesButtonNext"
style="@style/Widget.Vector.Button.Login"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginHorizontal="24dp"
android:layout_marginBottom="@dimen/release_notes_vertical_margin"
android:textAllCaps="true"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
tools:text="@string/action_next" />
<androidx.viewpager2.widget.ViewPager2
android:id="@+id/release_notes_carousel"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_marginTop="@dimen/release_notes_vertical_margin_small"
android:layout_marginBottom="@dimen/release_notes_vertical_margin"
app:layout_constraintBottom_toTopOf="@id/release_notes_carousel_indicator"
app:layout_constraintTop_toBottomOf="@id/release_notes_btn_close" />
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -20,17 +20,24 @@
</com.google.android.material.appbar.AppBarLayout>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/invites_recycler"
<im.vector.app.core.platform.StateView
android:id="@+id/invites_state_view"
android:layout_width="0dp"
android:layout_height="0dp"
android:fastScrollEnabled="true"
android:overScrollMode="always"
android:scrollbars="vertical"
app:layout_behavior="@string/appbar_scrolling_view_behavior"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/appBarLayout" />
app:layout_constraintTop_toBottomOf="@id/appBarLayout">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/invites_recycler"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fastScrollEnabled="true"
android:overScrollMode="always"
android:scrollbars="vertical" />
</im.vector.app.core.platform.StateView>
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -39,7 +39,7 @@
android:layout_width="match_parent"
android:layout_height="0dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintTop_toBottomOf="@id/currentCallsView">
app:layout_constraintTop_toBottomOf="@id/syncStateView">
<com.google.android.material.appbar.AppBarLayout
android:id="@+id/appBarLayout"
@ -127,7 +127,6 @@
android:layout_height="wrap_content"
android:layout_marginEnd="16dp"
android:layout_marginBottom="16dp"
android:accessibilityTraversalBefore="@id/roomListView"
android:contentDescription="@string/a11y_create_message"
android:src="@drawable/ic_new_chat"
android:visibility="gone"

View file

@ -12,4 +12,48 @@
android:overScrollMode="always"
tools:listitem="@layout/item_space" />
<LinearLayout
android:id="@+id/spaces_empty_group"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:orientation="vertical"
android:padding="@dimen/layout_horizontal_margin"
android:visibility="gone"
tools:visibility="visible">
<TextView
android:id="@+id/spaces_empty_title"
style="@style/Widget.Vector.TextView.Subtitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:gravity="center"
android:text="@string/space_list_empty_title"
android:textColor="?vctr_content_primary"
android:textStyle="bold" />
<TextView
android:id="@+id/spaces_empty_message"
style="@style/Widget.Vector.TextView.Body"
android:layout_width="220dp"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_marginTop="20dp"
android:gravity="center"
android:text="@string/space_list_empty_message"
android:textColor="?vctr_content_secondary" />
<Button
android:id="@+id/spaces_empty_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_marginTop="8dp"
android:minWidth="190dp"
android:text="@string/create_space" />
</LinearLayout>
</im.vector.app.core.platform.StateView>

View file

@ -50,7 +50,7 @@
android:layout_height="1dp"
android:layout_marginStart="22dp"
android:layout_marginEnd="16dp"
android:background="?vctr_list_separator_system"
android:background="?vctr_list_separator"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" />

View file

@ -5,7 +5,7 @@
android:id="@+id/recentRoot"
android:layout_width="60dp"
android:layout_height="wrap_content"
android:background="?android:colorBackground"
android:background="?vctr_toolbar_background"
android:clickable="true"
android:focusable="true"
android:foreground="?attr/selectableItemBackground"
@ -50,6 +50,7 @@
android:layout_marginTop="4dp"
android:layout_marginBottom="16dp"
android:ellipsize="end"
android:importantForAccessibility="no"
android:lines="1"
android:textColor="?vctr_content_primary"
app:layout_constraintBottom_toBottomOf="parent"

View file

@ -0,0 +1,63 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_gravity="bottom">
<androidx.constraintlayout.widget.Guideline
android:id="@+id/splashCarouselGutterStart"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:orientation="vertical"
app:layout_constraintGuide_percent="@dimen/ftue_auth_gutter_start_percent" />
<androidx.constraintlayout.widget.Guideline
android:id="@+id/splashCarouselGutterEnd"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:orientation="vertical"
app:layout_constraintGuide_percent="@dimen/ftue_auth_gutter_end_percent" />
<ImageView
android:id="@+id/carousel_item_image"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:scaleType="centerInside"
android:layout_marginBottom="@dimen/release_notes_vertical_margin_large"
android:contentDescription="@null"
app:layout_constraintStart_toStartOf="@id/splashCarouselGutterStart"
app:layout_constraintEnd_toEndOf="@id/splashCarouselGutterStart"
app:layout_constraintBottom_toTopOf="@id/carousel_item_title"
tools:src="@drawable/ill_app_layout_onboarding_rooms"/>
<TextView
android:id="@+id/carousel_item_title"
style="@style/Widget.Vector.TextView.Title"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp"
android:gravity="center"
android:textColor="?vctr_content_primary"
android:maxLines="2"
app:layout_constraintBottom_toTopOf="@id/carousel_item_body"
app:layout_constraintEnd_toEndOf="@id/splashCarouselGutterEnd"
app:layout_constraintStart_toStartOf="@id/splashCarouselGutterStart"
tools:text="@string/onboarding_new_app_layout_welcome_title" />
<TextView
android:id="@+id/carousel_item_body"
style="@style/Widget.Vector.TextView.Subtitle"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:gravity="center"
android:textColor="?vctr_content_secondary"
android:maxLines="3"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="@id/splashCarouselGutterEnd"
app:layout_constraintStart_toStartOf="@id/splashCarouselGutterStart"
tools:text="@string/onboarding_new_app_layout_welcome_message" />
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<im.vector.app.core.platform.StateView xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/stateView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?android:colorBackground">
</im.vector.app.core.platform.StateView>

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.2 KiB

Some files were not shown because too many files have changed in this diff Show more