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: env:
PROJECT_ID: "PN_kwDOAM0swc2KCw" PROJECT_ID: "PN_kwDOAM0swc2KCw"
GITHUB_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN }} 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: move_message_bubbles_issues:
name: A-Message-Bubbles to Message bubbles board name: A-Message-Bubbles to Message bubbles board
runs-on: ubuntu-latest 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 mv towncrier.toml towncrier.toml.bak
sed 's/CHANGES\.md/CHANGES_NIGHTLY\.md/' towncrier.toml.bak > towncrier.toml sed 's/CHANGES\.md/CHANGES_NIGHTLY\.md/' towncrier.toml.bak > towncrier.toml
rm towncrier.toml.bak rm towncrier.toml.bak
yes n | towncrier --version nightly yes n | towncrier build --version nightly
./gradlew assembleGplayNightly appDistributionUploadGplayNightly $CI_GRADLE_ARG_PROPERTIES ./gradlew assembleGplayNightly appDistributionUploadGplayNightly $CI_GRADLE_ARG_PROPERTIES
``` ```

View File

@ -140,8 +140,10 @@
<string name="start_chat">Start Chat</string> <string name="start_chat">Start Chat</string>
<string name="create_room">Create Room</string> <string name="create_room">Create Room</string>
<string name="explore_rooms">Explore Rooms</string> <string name="explore_rooms">Explore Rooms</string>
<string name="a11y_expand_space_children">Expand space children</string> <!-- Note to translators: %s refers to the space whose children is being expanded -->
<string name="a11y_collapse_space_children">Collapse space children</string> <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 --> <!-- Last seen time -->
@ -442,9 +444,16 @@
<string name="system_alerts_header">"System Alerts"</string> <string name="system_alerts_header">"System Alerts"</string>
<string name="suggested_header">Suggested Rooms</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 --> <!-- Invites fragment -->
<string name="invites_title">Invites</string> <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 --> <!-- People fragment -->
<string name="direct_chats_header">Conversations</string> <string name="direct_chats_header">Conversations</string>
<string name="matrix_only_filter">Matrix contacts only</string> <string name="matrix_only_filter">Matrix contacts only</string>
@ -3252,6 +3261,27 @@
<string name="device_manager_session_title">Session</string> <string name="device_manager_session_title">Session</string>
<!-- Examples: Last activity Yesterday at 6PM, Last activity Aug 31 at 5:47PM --> <!-- Examples: Last activity Yesterday at 6PM, Last activity Aug 31 at 5:47PM -->
<string name="device_manager_session_last_activity">Last activity %1$s</string> <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_bottom_sheet_title">Filter</string>
<string name="device_manager_filter_option_all_sessions">All session</string> <string name="device_manager_filter_option_all_sessions">All session</string>
<string name="device_manager_filter_option_verified">Verified</string> <string name="device_manager_filter_option_verified">Verified</string>

View File

@ -2,4 +2,8 @@
<resources> <resources>
<item name="ftue_auth_carousel_item_spacing" format="float" type="dimen">0.05</item> <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> <item name="ftue_auth_carousel_item_image_height" format="float" type="dimen">0.40</item>
<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> </resources>

View File

@ -74,4 +74,9 @@
<!-- Material 3 --> <!-- Material 3 -->
<dimen name="collapsing_toolbar_layout_medium_size">112dp</dimen> <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> </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.RealmCryptoStore
import org.matrix.android.sdk.internal.crypto.store.db.RealmCryptoStoreModule 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.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.di.MoshiProvider
import org.matrix.android.sdk.internal.util.time.DefaultClock import org.matrix.android.sdk.internal.util.time.DefaultClock
import kotlin.random.Random import kotlin.random.Random
@ -37,6 +38,7 @@ internal class CryptoStoreHelper {
userId = "userId_" + Random.nextInt(), userId = "userId_" + Random.nextInt(),
deviceId = "deviceId_sample", deviceId = "deviceId_sample",
clock = DefaultClock(), clock = DefaultClock(),
myDeviceLastSeenInfoEntityMapper = MyDeviceLastSeenInfoEntityMapper()
) )
} }
} }

View File

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

View File

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

View File

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

View File

@ -78,17 +78,10 @@ android {
productFlavors { productFlavors {
gplay { gplay {
dimension "store" dimension "store"
isDefault = true
buildConfigField "String", "SHORT_FLAVOR_DESCRIPTION", "\"G\""
buildConfigField "String", "FLAVOR_DESCRIPTION", "\"GooglePlay\""
} }
fdroid { fdroid {
dimension "store" dimension "store"
buildConfigField "String", "SHORT_FLAVOR_DESCRIPTION", "\"F\""
buildConfigField "String", "FLAVOR_DESCRIPTION", "\"FDroid\""
} }
} }
@ -264,7 +257,7 @@ dependencies {
// UnifiedPush // UnifiedPush
implementation 'com.github.UnifiedPush:android-connector:2.0.1' implementation 'com.github.UnifiedPush:android-connector:2.0.1'
// UnifiedPush gplay flavor only // 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-core'
exclude group: 'com.google.firebase', module: 'firebase-analytics' exclude group: 'com.google.firebase', module: 'firebase-analytics'
exclude group: 'com.google.firebase', module: 'firebase-measurement-connector' 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 androidx.test.platform.app.InstrumentationRegistry
import im.vector.app.AndroidVersionTestOverrider import im.vector.app.AndroidVersionTestOverrider
import im.vector.app.features.DefaultVectorFeatures import im.vector.app.features.DefaultVectorFeatures
import io.mockk.every
import io.mockk.spyk
import org.amshove.kluent.shouldBeInstanceOf import org.amshove.kluent.shouldBeInstanceOf
import org.junit.After import org.junit.After
import org.junit.Test import org.junit.Test
@ -27,7 +29,7 @@ import org.junit.Test
class VoiceRecorderProviderTests { class VoiceRecorderProviderTests {
private val context = InstrumentationRegistry.getInstrumentation().targetContext private val context = InstrumentationRegistry.getInstrumentation().targetContext
private val provider = VoiceRecorderProvider(context, DefaultVectorFeatures()) private val provider = spyk(VoiceRecorderProvider(context, DefaultVectorFeatures()))
@After @After
fun tearDown() { fun tearDown() {
@ -35,11 +37,19 @@ class VoiceRecorderProviderTests {
} }
@Test @Test
fun provideVoiceRecorderOnAndroidQReturnsQRecorder() { fun provideVoiceRecorderOnAndroidQAndCodecReturnsQRecorder() {
AndroidVersionTestOverrider.override(Build.VERSION_CODES.Q) AndroidVersionTestOverrider.override(Build.VERSION_CODES.Q)
every { provider.hasOpusEncoder() } returns true
provider.provideVoiceRecorder().shouldBeInstanceOf(VoiceRecorderQ::class) 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 @Test
fun provideVoiceRecorderOnOlderAndroidVersionReturnsLRecorder() { fun provideVoiceRecorderOnOlderAndroidVersionReturnsLRecorder() {
AndroidVersionTestOverrider.override(Build.VERSION_CODES.LOLLIPOP) AndroidVersionTestOverrider.override(Build.VERSION_CODES.LOLLIPOP)

View File

@ -338,6 +338,7 @@
<activity android:name=".features.settings.font.FontScaleSettingActivity"/> <activity android:name=".features.settings.font.FontScaleSettingActivity"/>
<activity android:name=".features.call.dialpad.PstnDialActivity" /> <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.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.overview.SessionOverviewActivity"/>
<activity android:name=".features.settings.devices.v2.othersessions.OtherSessionsActivity" /> <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.RoomListViewModel
import im.vector.app.features.home.room.list.home.HomeRoomListViewModel 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.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.homeserver.HomeServerCapabilitiesViewModel
import im.vector.app.features.invite.InviteUsersToRoomViewModel import im.vector.app.features.invite.InviteUsersToRoomViewModel
import im.vector.app.features.location.LocationSharingViewModel import im.vector.app.features.location.LocationSharingViewModel
@ -626,6 +627,11 @@ interface MavericksViewModelModule {
@MavericksViewModelKey(InvitesViewModel::class) @MavericksViewModelKey(InvitesViewModel::class)
fun invitesViewModel(factory: InvitesViewModel.Factory): MavericksAssistedViewModelFactory<*, *> fun invitesViewModel(factory: InvitesViewModel.Factory): MavericksAssistedViewModelFactory<*, *>
@Binds
@IntoMap
@MavericksViewModelKey(ReleaseNotesViewModel::class)
fun releaseNotesViewModel(factory: ReleaseNotesViewModel.Factory): MavericksAssistedViewModelFactory<*, *>
@Binds @Binds
@IntoMap @IntoMap
@MavericksViewModelKey(SessionOverviewViewModel::class) @MavericksViewModelKey(SessionOverviewViewModel::class)

View File

@ -21,6 +21,7 @@ import android.graphics.drawable.Drawable
import android.util.AttributeSet import android.util.AttributeSet
import android.view.View import android.view.View
import android.widget.FrameLayout import android.widget.FrameLayout
import android.widget.ImageView
import androidx.core.view.isVisible import androidx.core.view.isVisible
import im.vector.app.R import im.vector.app.R
import im.vector.app.core.extensions.updateConstraintSet import im.vector.app.core.extensions.updateConstraintSet
@ -36,7 +37,8 @@ class StateView @JvmOverloads constructor(context: Context, attrs: AttributeSet?
val title: CharSequence? = null, val title: CharSequence? = null,
val image: Drawable? = null, val image: Drawable? = null,
val isBigImage: Boolean = false, val isBigImage: Boolean = false,
val message: CharSequence? = null val message: CharSequence? = null,
val imageScaleType: ImageView.ScaleType? = ImageView.ScaleType.FIT_CENTER,
) : State() ) : State()
data class Error(val message: CharSequence? = null) : 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.Content -> Unit
is State.Loading -> Unit is State.Loading -> Unit
is State.Empty -> { is State.Empty -> {
views.emptyImageView.scaleType = newState.imageScaleType
views.emptyImageView.setImageDrawable(newState.image) views.emptyImageView.setImageDrawable(newState.image)
views.emptyView.updateConstraintSet { views.emptyView.updateConstraintSet {
it.constrainPercentHeight(R.id.emptyImageView, if (newState.isBigImage) 0.5f else 0.1f) it.constrainPercentHeight(R.id.emptyImageView, if (newState.isBigImage) 0.5f else 0.1f)

View File

@ -40,6 +40,46 @@ data class Interaction(
) : VectorAnalyticsEvent { ) : VectorAnalyticsEvent {
enum class Name { 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. * User tapped on Add to Home button on Room Details screen.
*/ */
@ -60,6 +100,11 @@ data class Interaction(
*/ */
MobileRoomThreadSummaryItem, MobileRoomThreadSummaryItem,
/**
* User validated the creation of a new space.
*/
MobileSpaceCreationValidated,
/** /**
* User tapped on the filter button on ThreadList screen. * User tapped on the filter button on ThreadList screen.
*/ */
@ -81,6 +126,12 @@ data class Interaction(
*/ */
SpacePanelSwitchSpace, 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 * User clicked the create room button in the add existing room to space
* dialog in Element Web/Desktop. * dialog in Element Web/Desktop.

View File

@ -43,6 +43,11 @@ data class MobileScreen(
*/ */
CreateRoom, CreateRoom,
/**
* The screen shown to create a new space.
*/
CreateSpace,
/** /**
* The confirmation screen shown before deactivating an account. * The confirmation screen shown before deactivating an account.
*/ */
@ -78,6 +83,11 @@ data class MobileScreen(
*/ */
InviteFriends, InviteFriends,
/**
* Room accessed via space bottom sheet list.
*/
Invites,
/** /**
* The screen that displays the login flow (when the user already has an * The screen that displays the login flow (when the user already has an
* account). * account).
@ -261,6 +271,11 @@ data class MobileScreen(
*/ */
Sidebar, Sidebar,
/**
* Room accessed via space bottom sheet list.
*/
SpaceBottomSheet,
/** /**
* Screen that displays the list of rooms and spaces of a space. * 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. * Whether the user has the people space enabled.
*/ */
val webMetaSpacePeopleEnabled: Boolean? = null, 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. * The selected messaging use case during the onboarding flow.
*/ */
@ -80,6 +84,29 @@ data class UserProperties(
WorkMessaging, 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>? { fun getProperties(): Map<String, Any>? {
return mutableMapOf<String, Any>().apply { return mutableMapOf<String, Any>().apply {
webMetaSpaceFavouritesEnabled?.let { put("WebMetaSpaceFavouritesEnabled", it) } webMetaSpaceFavouritesEnabled?.let { put("WebMetaSpaceFavouritesEnabled", it) }
@ -87,6 +114,7 @@ data class UserProperties(
webMetaSpaceHomeEnabled?.let { put("WebMetaSpaceHomeEnabled", it) } webMetaSpaceHomeEnabled?.let { put("WebMetaSpaceHomeEnabled", it) }
webMetaSpaceOrphansEnabled?.let { put("WebMetaSpaceOrphansEnabled", it) } webMetaSpaceOrphansEnabled?.let { put("WebMetaSpaceOrphansEnabled", it) }
webMetaSpacePeopleEnabled?.let { put("WebMetaSpacePeopleEnabled", it) } webMetaSpacePeopleEnabled?.let { put("WebMetaSpacePeopleEnabled", it) }
allChatsActiveFilter?.let { put("allChatsActiveFilter", it.name) }
ftueUseCaseSelection?.let { put("ftueUseCaseSelection", it.name) } ftueUseCaseSelection?.let { put("ftueUseCaseSelection", it.name) }
numFavouriteRooms?.let { put("numFavouriteRooms", it) } numFavouriteRooms?.let { put("numFavouriteRooms", it) }
numSpaces?.let { put("numSpaces", it) } numSpaces?.let { put("numSpaces", it) }

View File

@ -110,6 +110,11 @@ data class ViewRoom(
*/ */
MobileSearchContactDetail, MobileSearchContactDetail,
/**
* Room accessed via space bottom sheet list.
*/
MobileSpaceBottomSheet,
/** /**
* Room accessed via interacting with direct chat item in the space * Room accessed via interacting with direct chat item in the space
* contact detail screen. * 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.RoomListSharedAction
import im.vector.app.features.home.room.list.actions.RoomListSharedActionViewModel 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.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.MatrixToBottomSheet
import im.vector.app.features.matrixto.OriginOfMatrixTo import im.vector.app.features.matrixto.OriginOfMatrixTo
import im.vector.app.features.navigation.Navigator import im.vector.app.features.navigation.Navigator
@ -268,6 +269,7 @@ class HomeActivity :
} }
is HomeActivityViewEvents.OnCrossSignedInvalidated -> handleCrossSigningInvalidated(it) is HomeActivityViewEvents.OnCrossSignedInvalidated -> handleCrossSigningInvalidated(it)
HomeActivityViewEvents.ShowAnalyticsOptIn -> handleShowAnalyticsOptIn() HomeActivityViewEvents.ShowAnalyticsOptIn -> handleShowAnalyticsOptIn()
HomeActivityViewEvents.ShowReleaseNotes -> handleShowReleaseNotes()
HomeActivityViewEvents.NotifyUserForThreadsMigration -> handleNotifyUserForThreadsMigration() HomeActivityViewEvents.NotifyUserForThreadsMigration -> handleNotifyUserForThreadsMigration()
is HomeActivityViewEvents.MigrateThreads -> migrateThreadsIfNeeded(it.checkSession) is HomeActivityViewEvents.MigrateThreads -> migrateThreadsIfNeeded(it.checkSession)
} }
@ -282,6 +284,10 @@ class HomeActivity :
homeActivityViewModel.handle(HomeActivityViewActions.ViewStarted) homeActivityViewModel.handle(HomeActivityViewActions.ViewStarted)
} }
private fun handleShowReleaseNotes() {
startActivity(Intent(this, ReleaseNotesActivity::class.java))
}
private fun showSpaceSettings(spaceId: String) { private fun showSpaceSettings(spaceId: String) {
// open bottom sheet // open bottom sheet
SpaceSettingsMenuBottomSheet SpaceSettingsMenuBottomSheet

View File

@ -31,6 +31,7 @@ sealed interface HomeActivityViewEvents : VectorViewEvents {
data class OnCrossSignedInvalidated(val userItem: MatrixItem.UserItem) : HomeActivityViewEvents data class OnCrossSignedInvalidated(val userItem: MatrixItem.UserItem) : HomeActivityViewEvents
object PromptToEnableSessionPush : HomeActivityViewEvents object PromptToEnableSessionPush : HomeActivityViewEvents
object ShowAnalyticsOptIn : HomeActivityViewEvents object ShowAnalyticsOptIn : HomeActivityViewEvents
object ShowReleaseNotes : HomeActivityViewEvents
object NotifyUserForThreadsMigration : HomeActivityViewEvents object NotifyUserForThreadsMigration : HomeActivityViewEvents
data class MigrateThreads(val checkSession: Boolean) : HomeActivityViewEvents data class MigrateThreads(val checkSession: Boolean) : HomeActivityViewEvents
object StartRecoverySetupFlow : 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.MavericksAssistedViewModelFactory
import im.vector.app.core.di.hiltMavericksViewModelFactory import im.vector.app.core.di.hiltMavericksViewModelFactory
import im.vector.app.core.platform.VectorViewModel 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.AnalyticsConfig
import im.vector.app.features.analytics.AnalyticsTracker import im.vector.app.features.analytics.AnalyticsTracker
import im.vector.app.features.analytics.extensions.toAnalyticsType import im.vector.app.features.analytics.extensions.toAnalyticsType
import im.vector.app.features.analytics.plan.Signup import im.vector.app.features.analytics.plan.Signup
import im.vector.app.features.analytics.store.AnalyticsStore 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.login.ReAuthHelper
import im.vector.app.features.onboarding.AuthenticationDescription import im.vector.app.features.onboarding.AuthenticationDescription
import im.vector.app.features.raw.wellknown.ElementWellKnown import im.vector.app.features.raw.wellknown.ElementWellKnown
@ -82,6 +84,8 @@ class HomeActivityViewModel @AssistedInject constructor(
private val vectorPreferences: VectorPreferences, private val vectorPreferences: VectorPreferences,
private val analyticsTracker: AnalyticsTracker, private val analyticsTracker: AnalyticsTracker,
private val analyticsConfig: AnalyticsConfig, private val analyticsConfig: AnalyticsConfig,
private val releaseNotesPreferencesStore: ReleaseNotesPreferencesStore,
private val vectorFeatures: VectorFeatures,
) : VectorViewModel<HomeActivityViewState, HomeActivityViewActions, HomeActivityViewEvents>(initialState) { ) : VectorViewModel<HomeActivityViewState, HomeActivityViewActions, HomeActivityViewEvents>(initialState) {
@AssistedFactory @AssistedFactory
@ -110,9 +114,27 @@ class HomeActivityViewModel @AssistedInject constructor(
checkSessionPushIsOn() checkSessionPushIsOn()
observeCrossSigningReset() observeCrossSigningReset()
observeAnalytics() observeAnalytics()
observeReleaseNotes()
initThreadsMigration() 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() { private fun observeAnalytics() {
if (analyticsConfig.isEnabled) { if (analyticsConfig.isEnabled) {
analyticsStore.didAskUserConsentFlow 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.spaces.SpaceListBottomSheet
import im.vector.app.features.workers.signout.BannerState import im.vector.app.features.workers.signout.BannerState
import im.vector.app.features.workers.signout.ServerBackupStatusViewModel import im.vector.app.features.workers.signout.ServerBackupStatusViewModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import org.matrix.android.sdk.api.session.Session 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.crypto.model.DeviceInfo
import org.matrix.android.sdk.api.session.room.model.RoomSummary import org.matrix.android.sdk.api.session.room.model.RoomSummary
import org.matrix.android.sdk.api.util.toMatrixItem
import javax.inject.Inject import javax.inject.Inject
@AndroidEntryPoint @AndroidEntryPoint
@ -322,12 +319,6 @@ class NewHomeDetailFragment :
private fun setupToolbar() { private fun setupToolbar() {
setupToolbar(views.toolbar) 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.collapsingToolbar.debouncedClicks(::openSpaceSettings)
views.toolbar.debouncedClicks(::openSpaceSettings) views.toolbar.debouncedClicks(::openSpaceSettings)
@ -373,9 +364,16 @@ class NewHomeDetailFragment :
vectorPreferences.developerShowDebugInfo() vectorPreferences.developerShowDebugInfo()
) )
refreshAvatar()
hasUnreadRooms = it.hasUnreadMessages hasUnreadRooms = it.hasUnreadMessages
} }
private fun refreshAvatar() = withState(viewModel) { state ->
state.myMatrixItem?.let { user ->
avatarRenderer.render(user, views.avatar)
}
}
override fun onTapToReturnToCall() { override fun onTapToReturnToCall() {
callManager.getCurrentCall()?.let { call -> callManager.getCurrentCall()?.let { call ->
VectorCallActivity.newIntent( VectorCallActivity.newIntent(

View File

@ -48,7 +48,7 @@ class AudioMessageHelper @Inject constructor(
) { ) {
private var mediaPlayer: MediaPlayer? = null private var mediaPlayer: MediaPlayer? = null
private var currentPlayingId: String? = 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>() private val amplitudeList = mutableListOf<Int>()
@ -79,18 +79,19 @@ class AudioMessageHelper @Inject constructor(
} }
fun stopRecording(): MultiPickerAudioType? { fun stopRecording(): MultiPickerAudioType? {
tryOrNull("Cannot stop media recording amplitude") {
stopRecordingAmplitudes()
}
val voiceMessageFile = tryOrNull("Cannot stop media recorder!") { val voiceMessageFile = tryOrNull("Cannot stop media recorder!") {
voiceRecorder.stopRecord() voiceRecorder.stopRecord()
voiceRecorder.getVoiceMessageFile() voiceRecorder.getVoiceMessageFile()
} }
try { tryOrNull("Cannot stop media recording amplitude") {
stopRecordingAmplitudes()
}
return try {
voiceMessageFile?.let { voiceMessageFile?.let {
val outputFileUri = FileProvider.getUriForFile(context, buildMeta.applicationId + ".fileProvider", it, "Voice message.${it.extension}") val outputFileUri = FileProvider.getUriForFile(context, buildMeta.applicationId + ".fileProvider", it, "Voice message.${it.extension}")
return outputFileUri outputFileUri
.toMultiPickerAudioType(context) .toMultiPickerAudioType(context)
?.apply { ?.apply {
waveform = if (amplitudeList.size < 50) { waveform = if (amplitudeList.size < 50) {
@ -99,10 +100,13 @@ class AudioMessageHelper @Inject constructor(
amplitudeList.chunked(amplitudeList.size / 50) { items -> items.maxOrNull() ?: 0 } amplitudeList.chunked(amplitudeList.size / 50) { items -> items.maxOrNull() ?: 0 }
} }
} }
} ?: return null }
} catch (e: FileNotFoundException) { } catch (e: FileNotFoundException) {
Timber.e(e, "Cannot stop voice recording") 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() { private fun handlePlayOrPauseRecordingPlayback() {
try {
audioMessageHelper.startOrPauseRecordingPlayback() audioMessageHelper.startOrPauseRecordingPlayback()
} catch (failure: Throwable) {
_viewEvents.post(MessageComposerViewEvents.VoicePlaybackOrRecordingFailure(failure))
}
} }
fun endAllVoiceActions(deleteRecord: Boolean = true) { fun endAllVoiceActions(deleteRecord: Boolean = true) {

View File

@ -199,11 +199,17 @@ class HomeRoomListFragment :
).also { controller -> ).also { controller ->
controller.listener = this controller.listener = this
controller.onFilterChanged = ::onRoomFilterChanged controller.onFilterChanged = ::onRoomFilterChanged
roomListViewModel.emptyStateFlow.onEach { emptyStateOptional ->
controller.submitEmptyStateData(emptyStateOptional.getOrNull())
}.launchIn(lifecycleScope)
section.filtersData.onEach { section.filtersData.onEach {
controller.submitFiltersData(it.getOrNull()) controller.submitFiltersData(it.getOrNull())
}.launchIn(lifecycleScope) }.launchIn(lifecycleScope)
section.list.observe(viewLifecycleOwner) { list -> section.list.observe(viewLifecycleOwner) { list ->
controller.submitList(list) controller.submitList(list)
if (list.isEmpty()) {
controller.requestForcedModelBuild()
}
} }
}.adapter }.adapter
} }

View File

@ -16,6 +16,7 @@
package im.vector.app.features.home.room.list.home package im.vector.app.features.home.room.list.home
import android.widget.ImageView
import androidx.lifecycle.map import androidx.lifecycle.map
import androidx.paging.PagedList import androidx.paging.PagedList
import arrow.core.toOption import arrow.core.toOption
@ -23,11 +24,14 @@ import com.airbnb.mvrx.MavericksViewModelFactory
import dagger.assisted.Assisted import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject import dagger.assisted.AssistedInject
import im.vector.app.R
import im.vector.app.SpaceStateHandler import im.vector.app.SpaceStateHandler
import im.vector.app.core.di.MavericksAssistedViewModelFactory import im.vector.app.core.di.MavericksAssistedViewModelFactory
import im.vector.app.core.di.hiltMavericksViewModelFactory import im.vector.app.core.di.hiltMavericksViewModelFactory
import im.vector.app.core.platform.StateView import im.vector.app.core.platform.StateView
import im.vector.app.core.platform.VectorViewModel 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 im.vector.app.features.home.room.list.home.filter.HomeRoomFilter
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableSharedFlow
@ -36,6 +40,7 @@ import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach 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.RoomSummaryQueryParams
import org.matrix.android.sdk.api.session.room.UpdatableLivePageResult 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.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.model.tag.RoomTag
import org.matrix.android.sdk.api.session.room.roomSummaryQueryParams import org.matrix.android.sdk.api.session.room.roomSummaryQueryParams
import org.matrix.android.sdk.api.session.room.state.isPublic import org.matrix.android.sdk.api.session.room.state.isPublic
@ -63,6 +69,8 @@ class HomeRoomListViewModel @AssistedInject constructor(
private val session: Session, private val session: Session,
private val spaceStateHandler: SpaceStateHandler, private val spaceStateHandler: SpaceStateHandler,
private val preferencesStore: HomeLayoutPreferencesStore, private val preferencesStore: HomeLayoutPreferencesStore,
private val stringProvider: StringProvider,
private val drawableProvider: DrawableProvider,
) : VectorViewModel<HomeRoomListViewState, HomeRoomListAction, HomeRoomListViewEvents>(initialState) { ) : VectorViewModel<HomeRoomListViewState, HomeRoomListAction, HomeRoomListViewEvents>(initialState) {
@AssistedFactory @AssistedFactory
@ -82,6 +90,10 @@ class HomeRoomListViewModel @AssistedInject constructor(
private val _sections = MutableSharedFlow<Set<HomeRoomSection>>(replay = 1) private val _sections = MutableSharedFlow<Set<HomeRoomSection>>(replay = 1)
val sections = _sections.asSharedFlow() 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 private var filteredPagedRoomSummariesLive: UpdatableLivePageResult? = null
init { init {
@ -109,6 +121,7 @@ class HomeRoomListViewModel @AssistedInject constructor(
} }
newSections.add(getFilteredRoomsSection()) newSections.add(getFilteredRoomsSection())
emitEmptyState()
_sections.emit(newSections) _sections.emit(newSections)
setState { setState {
@ -171,6 +184,7 @@ class HomeRoomListViewModel @AssistedInject constructor(
liveResults.queryParams = liveResults.queryParams.copy( liveResults.queryParams = liveResults.queryParams.copy(
spaceFilter = selectedSpace?.roomId.toActiveSpaceOrNoFilter() spaceFilter = selectedSpace?.roomId.toActiveSpaceOrNoFilter()
) )
emitEmptyState()
}.launchIn(viewModelScope) }.launchIn(viewModelScope)
return HomeRoomSection.RoomSummaryData( return HomeRoomSection.RoomSummaryData(
@ -179,25 +193,46 @@ 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>>> { private fun getFiltersDataFlow(): SharedFlow<Optional<List<HomeRoomFilter>>> {
val flow = MutableSharedFlow<Optional<List<HomeRoomFilter>>>(replay = 1) val flow = MutableSharedFlow<Optional<List<HomeRoomFilter>>>(replay = 1)
val favouritesFlow = session.flow() val spaceFLow = spaceStateHandler.getSelectedSpaceFlow()
.distinctUntilChanged()
.onStart {
emit(spaceStateHandler.getCurrentSpace().toOption())
}
val favouritesFlow =
spaceFLow.flatMapLatest { selectedSpace ->
session.flow()
.liveRoomSummaries( .liveRoomSummaries(
RoomSummaryQueryParams.Builder().also { builder -> RoomSummaryQueryParams.Builder().also { builder ->
builder.spaceFilter = selectedSpace.orNull()?.roomId.toActiveSpaceOrNoFilter()
builder.roomTagQueryFilter = RoomTagQueryFilter(true, null, null) builder.roomTagQueryFilter = RoomTagQueryFilter(true, null, null)
}.build() }.build()
) )
}
.map { it.isNotEmpty() } .map { it.isNotEmpty() }
.distinctUntilChanged() .distinctUntilChanged()
val dmsFLow = session.flow() val dmsFLow =
spaceFLow.flatMapLatest { selectedSpace ->
session.flow()
.liveRoomSummaries( .liveRoomSummaries(
RoomSummaryQueryParams.Builder().also { builder -> RoomSummaryQueryParams.Builder().also { builder ->
builder.spaceFilter = selectedSpace.orNull()?.roomId.toActiveSpaceOrNoFilter()
builder.memberships = listOf(Membership.JOIN) builder.memberships = listOf(Membership.JOIN)
builder.roomCategoryFilter = RoomCategoryFilter.ONLY_DM builder.roomCategoryFilter = RoomCategoryFilter.ONLY_DM
}.build() }.build()
) )
}
.map { it.isNotEmpty() } .map { it.isNotEmpty() }
.distinctUntilChanged() .distinctUntilChanged()
@ -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) { override fun handle(action: HomeRoomListAction) {
when (action) { when (action) {
is HomeRoomListAction.SelectRoom -> handleSelectRoom(action) is HomeRoomListAction.SelectRoom -> handleSelectRoom(action)
@ -261,9 +328,12 @@ class HomeRoomListViewModel @AssistedInject constructor(
} }
private fun handleChangeRoomFilter(action: HomeRoomListAction.ChangeRoomFilter) { private fun handleChangeRoomFilter(action: HomeRoomListAction.ChangeRoomFilter) {
currentFilter = action.filter
filteredPagedRoomSummariesLive?.let { liveResults -> filteredPagedRoomSummariesLive?.let { liveResults ->
liveResults.queryParams = getFilteredQueryParams(action.filter, liveResults.queryParams) liveResults.queryParams = getFilteredQueryParams(action.filter, liveResults.queryParams)
} }
emitEmptyState()
} }
fun isPublicRoom(roomId: String): Boolean { 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.EpoxyModel
import com.airbnb.epoxy.paging.PagedListEpoxyController import com.airbnb.epoxy.paging.PagedListEpoxyController
import im.vector.app.core.platform.StateView
import im.vector.app.core.utils.createUIHandler import im.vector.app.core.utils.createUIHandler
import im.vector.app.features.home.RoomListDisplayMode import im.vector.app.features.home.RoomListDisplayMode
import im.vector.app.features.home.room.list.RoomListListener 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.RoomSummaryItemFactory
import im.vector.app.features.home.room.list.RoomSummaryItemPlaceHolder_ 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.members.ChangeMembershipState
import org.matrix.android.sdk.api.session.room.model.RoomSummary import org.matrix.android.sdk.api.session.room.model.RoomSummary
@ -44,6 +46,8 @@ class HomeFilteredRoomsController(
var onFilterChanged: ((HomeRoomFilter) -> Unit)? = null var onFilterChanged: ((HomeRoomFilter) -> Unit)? = null
private var filtersData: List<HomeRoomFilter>? = 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<*>>) { override fun addModels(models: List<EpoxyModel<*>>) {
val host = this val host = this
@ -54,14 +58,29 @@ class HomeFilteredRoomsController(
onFilterChangedListener(host.onFilterChanged) onFilterChangedListener(host.onFilterChanged)
} }
} }
if (models.isEmpty() && emptyStateData != null) {
emptyStateData?.let { emptyState ->
roomListEmptyItem {
id("state_item")
emptyData(emptyState)
}
currentState = emptyState
}
} else {
currentState = StateView.State.Content
super.addModels(models) super.addModels(models)
} }
}
fun submitEmptyStateData(state: StateView.State.Empty?) {
this.emptyStateData = state
}
fun submitFiltersData(data: List<HomeRoomFilter>?) { fun submitFiltersData(data: List<HomeRoomFilter>?) {
this.filtersData = data this.filtersData = data
requestForcedModelBuild() requestForcedModelBuild()
} }
override fun buildItemModel(currentPosition: Int, item: RoomSummary?): EpoxyModel<*> { override fun buildItemModel(currentPosition: Int, item: RoomSummary?): EpoxyModel<*> {
item ?: return RoomSummaryItemPlaceHolder_().apply { id(currentPosition) } item ?: return RoomSummaryItemPlaceHolder_().apply { id(currentPosition) }
return roomSummaryItemFactory.create(item, roomChangeMembershipStates.orEmpty(), emptySet(), RoomListDisplayMode.ROOMS, listener) 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.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.lifecycle.lifecycleScope
import com.airbnb.mvrx.fragmentViewModel import com.airbnb.mvrx.fragmentViewModel
import com.airbnb.mvrx.withState
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import im.vector.app.core.extensions.configureWith 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.core.platform.VectorBaseFragment
import im.vector.app.databinding.FragmentInvitesBinding import im.vector.app.databinding.FragmentInvitesBinding
import im.vector.app.features.analytics.plan.ViewRoom import im.vector.app.features.analytics.plan.ViewRoom
import im.vector.app.features.home.room.list.RoomListListener import im.vector.app.features.home.room.list.RoomListListener
import im.vector.app.features.notifications.NotificationDrawerManager 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.RoomSummary
import org.matrix.android.sdk.api.session.room.model.SpaceChildInfo import org.matrix.android.sdk.api.session.room.model.SpaceChildInfo
import javax.inject.Inject import javax.inject.Inject
@ -51,6 +54,8 @@ class InvitesFragment : VectorBaseFragment<FragmentInvitesBinding>(), RoomListLi
setupToolbar(views.invitesToolbar) setupToolbar(views.invitesToolbar)
.allowBack() .allowBack()
views.invitesStateView.contentView = views.invitesRecycler
views.invitesRecycler.configureWith(controller) views.invitesRecycler.configureWith(controller)
controller.listener = this controller.listener = this
@ -62,13 +67,31 @@ class InvitesFragment : VectorBaseFragment<FragmentInvitesBinding>(), RoomListLi
when (it) { when (it) {
is InvitesViewEvents.Failure -> showFailure(it.throwable) is InvitesViewEvents.Failure -> showFailure(it.throwable)
is InvitesViewEvents.OpenRoom -> handleOpenRoom(it.roomSummary, it.shouldCloseInviteView) is InvitesViewEvents.OpenRoom -> handleOpenRoom(it.roomSummary, it.shouldCloseInviteView)
InvitesViewEvents.Close -> handleClose()
}
} }
} }
private fun handleClose() { viewModel.invites.onEach {
requireActivity().finish() 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) { 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) { override fun onRejectRoomInvitation(room: RoomSummary) {
notificationDrawerManager.updateEvents { it.clearMemberShipNotificationForRoom(room.roomId) } notificationDrawerManager.updateEvents { it.clearMemberShipNotificationForRoom(room.roomId) }
viewModel.handle(InvitesAction.RejectInvitation(room)) 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 { sealed class InvitesViewEvents : VectorViewEvents {
data class Failure(val throwable: Throwable) : InvitesViewEvents() data class Failure(val throwable: Throwable) : InvitesViewEvents()
data class OpenRoom(val roomSummary: RoomSummary, val shouldCloseInviteView: Boolean) : 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 package im.vector.app.features.home.room.list.home.invites
import androidx.lifecycle.asFlow
import androidx.paging.PagedList import androidx.paging.PagedList
import com.airbnb.mvrx.MavericksViewModelFactory import com.airbnb.mvrx.MavericksViewModelFactory
import dagger.assisted.Assisted import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject import dagger.assisted.AssistedInject
import im.vector.app.R
import im.vector.app.core.di.MavericksAssistedViewModelFactory import im.vector.app.core.di.MavericksAssistedViewModelFactory
import im.vector.app.core.di.hiltMavericksViewModelFactory import im.vector.app.core.di.hiltMavericksViewModelFactory
import im.vector.app.core.platform.VectorViewModel 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 kotlinx.coroutines.launch
import org.matrix.android.sdk.api.extensions.orFalse import org.matrix.android.sdk.api.extensions.orFalse
import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.Session
@ -36,6 +47,8 @@ import timber.log.Timber
class InvitesViewModel @AssistedInject constructor( class InvitesViewModel @AssistedInject constructor(
@Assisted val initialState: InvitesViewState, @Assisted val initialState: InvitesViewState,
private val session: Session, private val session: Session,
private val stringProvider: StringProvider,
private val drawableProvider: DrawableProvider
) : VectorViewModel<InvitesViewState, InvitesAction, InvitesViewEvents>(initialState) { ) : VectorViewModel<InvitesViewState, InvitesAction, InvitesViewEvents>(initialState) {
private val pagedListConfig = PagedList.Config.Builder() private val pagedListConfig = PagedList.Config.Builder()
@ -52,6 +65,11 @@ class InvitesViewModel @AssistedInject constructor(
companion object : MavericksViewModelFactory<InvitesViewModel, InvitesViewState> by hiltMavericksViewModelFactory() companion object : MavericksViewModelFactory<InvitesViewModel, InvitesViewState> by hiltMavericksViewModelFactory()
private val _invites = MutableSharedFlow<InvitesContentState>(replay = 1)
val invites = _invites.asSharedFlow()
private var invitesCount = -1
init { init {
observeInvites() observeInvites()
} }
@ -72,8 +90,6 @@ class InvitesViewModel @AssistedInject constructor(
return@withState return@withState
} }
val shouldCloseInviteView = state.pagedList?.value?.size == 1
viewModelScope.launch { viewModelScope.launch {
try { try {
session.roomService().leaveRoom(roomId) session.roomService().leaveRoom(roomId)
@ -81,9 +97,6 @@ class InvitesViewModel @AssistedInject constructor(
// Instead, we wait for the room to be rejected // 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. // 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 we update the state, the button will be displayed again, so it's not ideal...
if (shouldCloseInviteView) {
_viewEvents.post(InvitesViewEvents.Close)
}
} catch (failure: Throwable) { } catch (failure: Throwable) {
// Notify the user // Notify the user
_viewEvents.post(InvitesViewEvents.Failure(failure)) _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 // close invites view when navigate to a room from the last one invite
val shouldCloseInviteView = state.pagedList?.value?.size == 1 val shouldCloseInviteView = invitesCount == 1
_viewEvents.post(InvitesViewEvents.OpenRoom(action.roomSummary, shouldCloseInviteView))
// quick echo // quick echo
setState { setState {
@ -117,6 +128,8 @@ class InvitesViewModel @AssistedInject constructor(
} }
) )
} }
_viewEvents.post(InvitesViewEvents.OpenRoom(action.roomSummary, shouldCloseInviteView))
} }
private fun observeInvites() { private fun observeInvites() {
@ -129,8 +142,26 @@ class InvitesViewModel @AssistedInject constructor(
sortOrder = RoomSortOrder.ACTIVITY sortOrder = RoomSortOrder.ACTIVITY
) )
setState { pagedList.asFlow()
copy(pagedList = pagedList) .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 package im.vector.app.features.home.room.list.home.invites
import androidx.lifecycle.LiveData import android.graphics.drawable.Drawable
import androidx.paging.PagedList import androidx.paging.PagedList
import com.airbnb.mvrx.MavericksState import com.airbnb.mvrx.MavericksState
import org.matrix.android.sdk.api.session.room.members.ChangeMembershipState import org.matrix.android.sdk.api.session.room.members.ChangeMembershipState
import org.matrix.android.sdk.api.session.room.model.RoomSummary import org.matrix.android.sdk.api.session.room.model.RoomSummary
data class InvitesViewState( data class InvitesViewState(
val pagedList: LiveData<PagedList<RoomSummary>>? = null,
val roomMembershipChanges: Map<String, ChangeMembershipState> = emptyMap(), val roomMembershipChanges: Map<String, ChangeMembershipState> = emptyMap(),
) : MavericksState ) : 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.EpoxyController
import com.airbnb.epoxy.EpoxyModel import com.airbnb.epoxy.EpoxyModel
import com.airbnb.epoxy.carousel 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.AvatarRenderer
import im.vector.app.features.home.room.list.RoomListListener import im.vector.app.features.home.room.list.RoomListListener
import org.matrix.android.sdk.api.session.room.model.RoomSummary import org.matrix.android.sdk.api.session.room.model.RoomSummary
@ -43,6 +45,12 @@ class RecentRoomCarouselController @Inject constructor(
resources.displayMetrics resources.displayMetrics
).toInt() ).toInt()
private val topPadding = TypedValue.applyDimension(
TypedValue.COMPLEX_UNIT_DIP,
12f,
resources.displayMetrics
).toInt()
private val itemSpacing = TypedValue.applyDimension( private val itemSpacing = TypedValue.applyDimension(
TypedValue.COMPLEX_UNIT_DIP, TypedValue.COMPLEX_UNIT_DIP,
24f, 24f,
@ -61,11 +69,15 @@ class RecentRoomCarouselController @Inject constructor(
id("recents_carousel") id("recents_carousel")
padding(Carousel.Padding( padding(Carousel.Padding(
host.hPadding, host.hPadding,
0, host.topPadding,
host.hPadding, host.hPadding,
0, 0,
host.itemSpacing) host.itemSpacing)
) )
onBind { _, view, _ ->
val colorSurface = MaterialColors.getColor(view, R.attr.vctr_toolbar_background)
view.setBackgroundColor(colorSurface)
}
withModelsFrom(data) { roomSummary -> withModelsFrom(data) { roomSummary ->
val onClick = host.listener?.let { it::onRoomClicked } val onClick = host.listener?.let { it::onRoomClicked }
val onLongClick = host.listener?.let { it::onRoomLongClicked } 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) _viewEvents.post(OnboardingViewEvents.OnAccountCreated)
} }
AuthenticationDescription.Login -> { AuthenticationDescription.Login -> {
setState { copy(isLoading = false) } setState { copy(isLoading = false, selectedAuthenticationState = SelectedAuthenticationState(authenticationDescription)) }
_viewEvents.post(OnboardingViewEvents.OnAccountSignedIn) _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.core.utils.isAnimationEnabled
import im.vector.app.features.MainActivity import im.vector.app.features.MainActivity
import im.vector.app.features.analytics.ui.consent.AnalyticsOptInActivity 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.pin.PinActivity
import im.vector.app.features.signout.hard.SignedOutActivity import im.vector.app.features.signout.hard.SignedOutActivity
import im.vector.app.features.themes.ThemeUtils import im.vector.app.features.themes.ThemeUtils
@ -307,6 +308,7 @@ class PopupAlertManager @Inject constructor(
activity !is PinActivity && activity !is PinActivity &&
activity !is SignedOutActivity && activity !is SignedOutActivity &&
activity !is AnalyticsOptInActivity && activity !is AnalyticsOptInActivity &&
activity !is ReleaseNotesActivity &&
activity is VectorBaseActivity<*> && activity is VectorBaseActivity<*> &&
alert.shouldBeDisplayedIn.invoke(activity) 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_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" 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_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_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" 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.core.preference.VectorSwitchPreference
import im.vector.app.features.MainActivity import im.vector.app.features.MainActivity
import im.vector.app.features.MainActivityArgs 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.analytics.plan.MobileScreen
import im.vector.app.features.home.room.threads.ThreadsManager import im.vector.app.features.home.room.threads.ThreadsManager
import org.matrix.android.sdk.api.settings.LightweightSettingsStorage import org.matrix.android.sdk.api.settings.LightweightSettingsStorage
@ -39,6 +40,7 @@ class VectorSettingsLabsFragment :
@Inject lateinit var vectorPreferences: VectorPreferences @Inject lateinit var vectorPreferences: VectorPreferences
@Inject lateinit var lightweightSettingsStorage: LightweightSettingsStorage @Inject lateinit var lightweightSettingsStorage: LightweightSettingsStorage
@Inject lateinit var threadsManager: ThreadsManager @Inject lateinit var threadsManager: ThreadsManager
@Inject lateinit var vectorFeatures: VectorFeatures
override var titleRes = R.string.room_settings_labs_pref_title override var titleRes = R.string.room_settings_labs_pref_title
override val preferenceXmlRes = R.xml.vector_settings_labs override val preferenceXmlRes = R.xml.vector_settings_labs
@ -72,6 +74,10 @@ class VectorSettingsLabsFragment :
true 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.core.preference.VectorSwitchPreference
import im.vector.app.features.MainActivity import im.vector.app.features.MainActivity
import im.vector.app.features.MainActivityArgs 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.analytics.plan.MobileScreen
import im.vector.app.features.settings.font.FontScaleSettingActivity import im.vector.app.features.settings.font.FontScaleSettingActivity
import im.vector.app.features.themes.ThemeUtils import im.vector.app.features.themes.ThemeUtils
@ -44,6 +45,7 @@ class VectorSettingsPreferencesFragment :
@Inject lateinit var vectorPreferences: VectorPreferences @Inject lateinit var vectorPreferences: VectorPreferences
@Inject lateinit var fontScalePreferences: FontScalePreferences @Inject lateinit var fontScalePreferences: FontScalePreferences
@Inject lateinit var vectorFeatures: VectorFeatures
override var titleRes = R.string.settings_preferences override var titleRes = R.string.settings_preferences
override val preferenceXmlRes = R.xml.vector_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 // Url preview
/* /*
TODO Note: we keep the setting client side for now 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.DevicesAction
import im.vector.app.features.settings.devices.DevicesViewEvents import im.vector.app.features.settings.devices.DevicesViewEvents
import im.vector.app.features.settings.devices.DevicesViewModel 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.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.SESSION_IS_MARKED_AS_INACTIVE_AFTER_DAYS
import im.vector.app.features.settings.devices.v2.list.SecurityRecommendationViewState import im.vector.app.features.settings.devices.v2.list.SecurityRecommendationViewState
@ -129,11 +128,6 @@ class VectorSettingsDevicesFragment :
private fun initOtherSessionsView() { private fun initOtherSessionsView() {
views.deviceListOtherSessions.callback = this views.deviceListOtherSessions.callback = this
views.deviceListOtherSessions.setCallback(object : OtherSessionsController.Callback {
override fun onItemClicked(deviceId: String) {
navigateToSessionOverview(deviceId)
}
})
} }
override fun onDestroyView() { override fun onDestroyView() {
@ -264,6 +258,10 @@ class VectorSettingsDevicesFragment :
} }
} }
override fun onOtherSessionClicked(deviceId: String) {
navigateToSessionOverview(deviceId)
}
override fun onViewAllOtherSessionsClicked() { override fun onViewAllOtherSessionsClicked() {
viewNavigator.navigateToOtherSessions(requireActivity()) viewNavigator.navigateToOtherSessions(requireActivity())
} }

View File

@ -32,9 +32,10 @@ class OtherSessionsView @JvmOverloads constructor(
context: Context, context: Context,
attrs: AttributeSet? = null, attrs: AttributeSet? = null,
defStyleAttr: Int = 0 defStyleAttr: Int = 0
) : ConstraintLayout(context, attrs, defStyleAttr) { ) : ConstraintLayout(context, attrs, defStyleAttr), OtherSessionsController.Callback {
interface Callback { interface Callback {
fun onOtherSessionClicked(deviceId: String)
fun onViewAllOtherSessionsClicked() fun onViewAllOtherSessionsClicked()
} }
@ -47,6 +48,8 @@ class OtherSessionsView @JvmOverloads constructor(
inflate(context, R.layout.view_other_sessions, this) inflate(context, R.layout.view_other_sessions, this)
views = ViewOtherSessionsBinding.bind(this) views = ViewOtherSessionsBinding.bind(this)
otherSessionsController.callback = this
views.otherSessionsViewAllButton.setOnClickListener { views.otherSessionsViewAllButton.setOnClickListener {
callback?.onViewAllOtherSessionsClicked() callback?.onViewAllOtherSessionsClicked()
} }
@ -58,13 +61,13 @@ class OtherSessionsView @JvmOverloads constructor(
otherSessionsController.setData(devices) otherSessionsController.setData(devices)
} }
fun setCallback(callback: OtherSessionsController.Callback) {
otherSessionsController.callback = callback
}
override fun onDetachedFromWindow() { override fun onDetachedFromWindow() {
otherSessionsController.callback = null otherSessionsController.callback = null
views.otherSessionsRecyclerView.cleanup() views.otherSessionsRecyclerView.cleanup()
super.onDetachedFromWindow() 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.setOnClickListener(onToggleExpandListener)
holder.chevron.isVisible = hasChildren holder.chevron.isVisible = hasChildren
holder.chevron.setImageResource(if (expanded) R.drawable.ic_expand_more else R.drawable.ic_arrow_right) 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) avatarRenderer.render(matrixItem, holder.avatar)
holder.unreadCounter.render(countState) holder.unreadCounter.render(countState)

View File

@ -50,6 +50,7 @@ abstract class NewSubSpaceSummaryItem : VectorEpoxyModel<NewSubSpaceSummaryItem.
override fun bind(holder: Holder) { override fun bind(holder: Holder) {
super.bind(holder) super.bind(holder)
val context = holder.root.context
holder.root.onClick(onSubSpaceSelectedListener) holder.root.onClick(onSubSpaceSelectedListener)
holder.name.text = matrixItem.displayName holder.name.text = matrixItem.displayName
holder.root.isChecked = selected holder.root.isChecked = selected
@ -63,6 +64,10 @@ abstract class NewSubSpaceSummaryItem : VectorEpoxyModel<NewSubSpaceSummaryItem.
) )
holder.chevron.onClick(onToggleExpandListener) holder.chevron.onClick(onToggleExpandListener)
holder.chevron.isVisible = hasChildren 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.isVisible = indent > 0
holder.indent.updateLayoutParams { holder.indent.updateLayoutParams {

View File

@ -21,6 +21,7 @@ import android.view.HapticFeedbackConstants
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.core.view.isVisible
import com.airbnb.epoxy.EpoxyTouchHelper import com.airbnb.epoxy.EpoxyTouchHelper
import com.airbnb.mvrx.Loading import com.airbnb.mvrx.Loading
import com.airbnb.mvrx.Success import com.airbnb.mvrx.Success
@ -28,6 +29,7 @@ import com.airbnb.mvrx.Uninitialized
import com.airbnb.mvrx.fragmentViewModel import com.airbnb.mvrx.fragmentViewModel
import com.airbnb.mvrx.withState import com.airbnb.mvrx.withState
import dagger.hilt.android.AndroidEntryPoint 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.cleanup
import im.vector.app.core.extensions.configureWith import im.vector.app.core.extensions.configureWith
import im.vector.app.core.platform.StateView import im.vector.app.core.platform.StateView
@ -71,6 +73,7 @@ class SpaceListFragment :
homeActivitySharedActionViewModel = activityViewModelProvider[HomeSharedActionViewModel::class.java] homeActivitySharedActionViewModel = activityViewModelProvider[HomeSharedActionViewModel::class.java]
roomListSharedActionViewModel = activityViewModelProvider[RoomListSharedActionViewModel::class.java] roomListSharedActionViewModel = activityViewModelProvider[RoomListSharedActionViewModel::class.java]
views.stateView.contentView = views.groupListView views.stateView.contentView = views.groupListView
views.spacesEmptyButton.onClick { onAddSpaceSelected() }
setupSpaceController() setupSpaceController()
observeViewEvents() observeViewEvents()
} }
@ -147,13 +150,22 @@ class SpaceListFragment :
} }
override fun invalidate() = withState(viewModel) { state -> override fun invalidate() = withState(viewModel) { state ->
when (state.asyncSpaces) { when (val spaces = state.asyncSpaces) {
Uninitialized, Uninitialized,
is Loading -> { is Loading -> {
views.stateView.state = StateView.State.Loading views.stateView.state = StateView.State.Loading
return@withState 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 else -> Unit
} }

View File

@ -17,7 +17,10 @@
package im.vector.app.features.voice package im.vector.app.features.voice
import android.content.Context import android.content.Context
import android.media.MediaCodecList
import android.media.MediaFormat
import android.os.Build import android.os.Build
import androidx.annotation.VisibleForTesting
import im.vector.app.features.VectorFeatures import im.vector.app.features.VectorFeatures
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import javax.inject.Inject import javax.inject.Inject
@ -27,10 +30,21 @@ class VoiceRecorderProvider @Inject constructor(
private val vectorFeatures: VectorFeatures, private val vectorFeatures: VectorFeatures,
) { ) {
fun provideVoiceRecorder(): VoiceRecorder { fun provideVoiceRecorder(): VoiceRecorder {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q && vectorFeatures.forceUsageOfOpusEncoder().not()) { return if (useFallbackRecorder()) {
VoiceRecorderQ(context)
} else {
VoiceRecorderL(context, Dispatchers.IO) 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.0 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.0 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

@ -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> </com.google.android.material.appbar.AppBarLayout>
<androidx.recyclerview.widget.RecyclerView <im.vector.app.core.platform.StateView
android:id="@+id/invites_recycler" android:id="@+id/invites_state_view"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="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_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/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> </androidx.constraintlayout.widget.ConstraintLayout>

View File

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

View File

@ -12,4 +12,48 @@
android:overScrollMode="always" android:overScrollMode="always"
tools:listitem="@layout/item_space" /> 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> </im.vector.app.core.platform.StateView>

View File

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

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

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