diff --git a/.github/workflows/danger.yml b/.github/workflows/danger.yml
index 5698a696b6..30b6600c94 100644
--- a/.github/workflows/danger.yml
+++ b/.github/workflows/danger.yml
@@ -11,7 +11,7 @@ jobs:
- run: |
npm install --save-dev @babel/plugin-transform-flow-strip-types
- name: Danger
- uses: danger/danger-js@11.1.3
+ uses: danger/danger-js@11.1.4
with:
args: "--dangerfile tools/danger/dangerfile.js"
env:
diff --git a/.github/workflows/post-pr.yml b/.github/workflows/post-pr.yml
index b618519b0d..aac4fffa4e 100644
--- a/.github/workflows/post-pr.yml
+++ b/.github/workflows/post-pr.yml
@@ -52,7 +52,7 @@ jobs:
restore-keys: |
${{ runner.os }}-gradle-
- name: Start synapse server
- uses: michaelkaye/setup-matrix-synapse@v1.0.3
+ uses: michaelkaye/setup-matrix-synapse@v1.0.4
with:
uploadLogs: true
httpPort: 8080
diff --git a/.github/workflows/quality.yml b/.github/workflows/quality.yml
index 1692e2e281..9d9e8e76e8 100644
--- a/.github/workflows/quality.yml
+++ b/.github/workflows/quality.yml
@@ -66,7 +66,7 @@ jobs:
yarn add danger-plugin-lint-report --dev
- name: Danger lint
if: always()
- uses: danger/danger-js@11.1.3
+ uses: danger/danger-js@11.1.4
with:
args: "--dangerfile tools/danger/dangerfile-lint.js"
env:
diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml
index 1816fe3a78..bb16d8abe8 100644
--- a/.github/workflows/tests.yml
+++ b/.github/workflows/tests.yml
@@ -50,7 +50,7 @@ jobs:
- uses: actions/setup-python@v4
with:
python-version: 3.8
- - uses: michaelkaye/setup-matrix-synapse@v1.0.3
+ - uses: michaelkaye/setup-matrix-synapse@v1.0.4
with:
uploadLogs: true
httpPort: 8080
diff --git a/.github/workflows/triage-incoming.yml b/.github/workflows/triage-incoming.yml
index 6a22bf5223..4dadc25ab4 100644
--- a/.github/workflows/triage-incoming.yml
+++ b/.github/workflows/triage-incoming.yml
@@ -10,7 +10,7 @@ jobs:
# Skip in forks
if: github.repository == 'vector-im/element-android'
steps:
- - uses: alex-page/github-project-automation-plus@bb266ff4dde9242060e2d5418e120a133586d488
+ - uses: alex-page/github-project-automation-plus@1f8873e97e3c8f58161a323b7c568c1f623a1c4d
with:
project: Issue triage
column: Incoming
diff --git a/.github/workflows/triage-priority-bugs.yml b/.github/workflows/triage-priority-bugs.yml
index e762102226..07e73fe805 100644
--- a/.github/workflows/triage-priority-bugs.yml
+++ b/.github/workflows/triage-priority-bugs.yml
@@ -24,7 +24,7 @@ jobs:
contains(github.event.issue.labels.*.name, 'A11y') &&
contains(github.event.issue.labels.*.name, 'O-Frequent'))
steps:
- - uses: alex-page/github-project-automation-plus@bb266ff4dde9242060e2d5418e120a133586d488
+ - uses: alex-page/github-project-automation-plus@1f8873e97e3c8f58161a323b7c568c1f623a1c4d
with:
project: Android App Team
column: Important Issues & Topics (P1)
@@ -50,7 +50,7 @@ jobs:
contains(github.event.issue.labels.*.name, 'A11y') &&
contains(github.event.issue.labels.*.name, 'O-Frequent')))
steps:
- - uses: alex-page/github-project-automation-plus@bb266ff4dde9242060e2d5418e120a133586d488
+ - uses: alex-page/github-project-automation-plus@1f8873e97e3c8f58161a323b7c568c1f623a1c4d
with:
project: Crypto Team
column: Ready
diff --git a/.github/workflows/triage-unlabelled.yml b/.github/workflows/triage-unlabelled.yml
index 06df286d09..98d6579958 100644
--- a/.github/workflows/triage-unlabelled.yml
+++ b/.github/workflows/triage-unlabelled.yml
@@ -28,7 +28,7 @@ jobs:
echo "ALREADY_IN_BOARD=false" >> $GITHUB_ENV
fi
- name: Move issue
- uses: alex-page/github-project-automation-plus@bb266ff4dde9242060e2d5418e120a133586d488
+ uses: alex-page/github-project-automation-plus@1f8873e97e3c8f58161a323b7c568c1f623a1c4d
if: ${{ env.ALREADY_IN_BOARD == 'true' }}
with:
project: Issue triage
diff --git a/build.gradle b/build.gradle
index f162685d7d..6ccb83e703 100644
--- a/build.gradle
+++ b/build.gradle
@@ -33,7 +33,7 @@ buildscript {
classpath "org.jetbrains.dokka:dokka-gradle-plugin:1.7.20"
classpath "org.jetbrains.kotlinx:kotlinx-knit:0.4.0"
classpath 'com.jakewharton:butterknife-gradle-plugin:10.2.3'
- classpath 'app.cash.paparazzi:paparazzi-gradle-plugin:1.1.0'
+ classpath libs.squareup.paparazziPlugin
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files
}
@@ -45,7 +45,7 @@ plugins {
// Detekt
id "io.gitlab.arturbosch.detekt" version "1.21.0"
// Ksp
- id "com.google.devtools.ksp" version "1.7.20-1.0.6"
+ id "com.google.devtools.ksp" version "1.7.20-1.0.7"
// Dependency Analysis
id 'com.autonomousapps.dependency-analysis' version "1.13.1"
@@ -322,7 +322,7 @@ ext.initScreenshotTests = { project ->
if (hasScreenshots) {
project.apply plugin: 'app.cash.paparazzi'
}
- project.dependencies { testCompileOnly "app.cash.paparazzi:paparazzi:1.0.0" }
+ project.dependencies { testCompileOnly libs.squareup.paparazzi }
project.android.testOptions.unitTests.all {
def screenshotTestCapture = "**/*ScreenshotTest*"
if (hasScreenshots) {
diff --git a/changelog.d/7429.feature b/changelog.d/7429.feature
new file mode 100644
index 0000000000..9857452eca
--- /dev/null
+++ b/changelog.d/7429.feature
@@ -0,0 +1 @@
+Add new UI for selecting an attachment
diff --git a/dependencies.gradle b/dependencies.gradle
index f081e0a874..926b86afa1 100644
--- a/dependencies.gradle
+++ b/dependencies.gradle
@@ -1,5 +1,4 @@
ext.versions = [
-
'minSdk' : 21,
'compileSdk' : 33,
'targetSdk' : 33,
@@ -12,7 +11,7 @@ def gradle = "7.3.1"
def kotlin = "1.7.20"
def kotlinCoroutines = "1.6.4"
def dagger = "2.44"
-def appDistribution = "16.0.0-beta04"
+def appDistribution = "16.0.0-beta05"
def retrofit = "2.9.0"
def markwon = "4.6.2"
def moshi = "1.14.0"
@@ -27,22 +26,20 @@ def jjwt = "0.11.5"
// Temporary version to unblock #6929. Once 0.16.0 is released we should use it, and revert
// the whole commit which set version 0.16.0-SNAPSHOT
def vanniktechEmoji = "0.16.0-SNAPSHOT"
-
-def sentry = "6.4.3"
-
-def fragment = "1.5.3"
-
+def sentry = "6.6.0"
+def fragment = "1.5.4"
// Testing
def mockk = "1.12.3" // We need to use 1.12.3 to have mocking in androidTest until a new version is released: https://github.com/mockk/mockk/issues/819
def espresso = "3.4.0"
def androidxTest = "1.4.0"
def androidxOrchestrator = "1.4.1"
+def paparazzi = "1.1.0"
+
ext.libs = [
gradle : [
'gradlePlugin' : "com.android.tools.build:gradle:$gradle",
'kotlinPlugin' : "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin",
'hiltPlugin' : "com.google.dagger:hilt-android-gradle-plugin:$dagger"
-
],
jetbrains : [
'coroutinesCore' : "org.jetbrains.kotlinx:kotlinx-coroutines-core:$kotlinCoroutines",
@@ -55,7 +52,7 @@ ext.libs = [
'biometric' : "androidx.biometric:biometric:1.1.0",
'core' : "androidx.core:core-ktx:1.9.0",
'recyclerview' : "androidx.recyclerview:recyclerview:1.2.1",
- 'exifinterface' : "androidx.exifinterface:exifinterface:1.3.4",
+ 'exifinterface' : "androidx.exifinterface:exifinterface:1.3.5",
'fragmentKtx' : "androidx.fragment:fragment-ktx:$fragment",
'fragmentTesting' : "androidx.fragment:fragment-testing:$fragment",
'constraintLayout' : "androidx.constraintlayout:constraintlayout:2.1.4",
@@ -82,7 +79,7 @@ ext.libs = [
'transition' : "androidx.transition:transition:1.2.0",
],
google : [
- 'material' : "com.google.android.material:material:1.6.1",
+ 'material' : "com.google.android.material:material:1.7.0",
'appdistributionApi' : "com.google.firebase:firebase-appdistribution-api-ktx:$appDistribution",
'appdistribution' : "com.google.firebase:firebase-appdistribution:$appDistribution",
// Phone number https://github.com/google/libphonenumber
@@ -108,6 +105,8 @@ ext.libs = [
'moshiKt' : "com.squareup.moshi:moshi-kotlin:$moshi",
'moshiKotlin' : "com.squareup.moshi:moshi-kotlin-codegen:$moshi",
'moshiAdapters' : "com.squareup.moshi:moshi-adapters:$moshi",
+ 'paparazzi' : "app.cash.paparazzi:paparazzi:$paparazzi",
+ 'paparazziPlugin' : "app.cash.paparazzi:paparazzi-gradle-plugin:$paparazzi",
'retrofit' : "com.squareup.retrofit2:retrofit:$retrofit",
'retrofitMoshi' : "com.squareup.retrofit2:converter-moshi:$retrofit"
],
@@ -167,7 +166,7 @@ ext.libs = [
'sentryAndroid' : "io.sentry:sentry-android:$sentry"
],
tests : [
- 'kluent' : "org.amshove.kluent:kluent-android:1.68",
+ 'kluent' : "org.amshove.kluent:kluent-android:1.72",
'timberJunitRule' : "net.lachlanmckee:timber-junit-rule:1.0.1",
'junit' : "junit:junit:4.13.2",
]
diff --git a/library/ui-strings/src/main/res/values/strings.xml b/library/ui-strings/src/main/res/values/strings.xml
index ea9b4b5999..d5223a0638 100644
--- a/library/ui-strings/src/main/res/values/strings.xml
+++ b/library/ui-strings/src/main/res/values/strings.xml
@@ -3205,6 +3205,15 @@
Share location
Start a voice broadcast
+ Photo library
+ Stickers
+ Attachments
+ Voice broadcast
+ Polls
+ Location
+ Camera
+ Contact
+
Show less
- "%1$d more"
diff --git a/vector-app/build.gradle b/vector-app/build.gradle
index 1e8a8b3d3f..9a0bfdfa63 100644
--- a/vector-app/build.gradle
+++ b/vector-app/build.gradle
@@ -372,7 +372,7 @@ dependencies {
debugImplementation 'com.facebook.soloader:soloader:0.10.4'
debugImplementation "com.kgurgul.flipper:flipper-realm-android:2.2.0"
- gplayImplementation "com.google.android.gms:play-services-location:20.0.0"
+ gplayImplementation "com.google.android.gms:play-services-location:21.0.0"
// UnifiedPush gplay flavor only
gplayImplementation('com.google.firebase:firebase-messaging:23.1.0') {
exclude group: 'com.google.firebase', module: 'firebase-core'
diff --git a/vector/build.gradle b/vector/build.gradle
index 348eb18220..0b884f4d99 100644
--- a/vector/build.gradle
+++ b/vector/build.gradle
@@ -239,7 +239,7 @@ dependencies {
implementation libs.sentry.sentryAndroid
// UnifiedPush
- implementation 'com.github.UnifiedPush:android-connector:2.1.0'
+ implementation 'com.github.UnifiedPush:android-connector:2.1.1'
implementation "androidx.emoji2:emoji2:1.2.0"
diff --git a/vector/src/main/java/im/vector/app/core/di/MavericksViewModelModule.kt b/vector/src/main/java/im/vector/app/core/di/MavericksViewModelModule.kt
index 97590028d8..2242abb7aa 100644
--- a/vector/src/main/java/im/vector/app/core/di/MavericksViewModelModule.kt
+++ b/vector/src/main/java/im/vector/app/core/di/MavericksViewModelModule.kt
@@ -22,6 +22,7 @@ import dagger.hilt.InstallIn
import dagger.multibindings.IntoMap
import im.vector.app.features.analytics.accountdata.AnalyticsAccountDataViewModel
import im.vector.app.features.analytics.ui.consent.AnalyticsConsentViewModel
+import im.vector.app.features.attachments.AttachmentTypeSelectorViewModel
import im.vector.app.features.auth.ReAuthViewModel
import im.vector.app.features.call.VectorCallViewModel
import im.vector.app.features.call.conference.JitsiCallViewModel
@@ -677,4 +678,9 @@ interface MavericksViewModelModule {
@IntoMap
@MavericksViewModelKey(VectorSettingsLabsViewModel::class)
fun vectorSettingsLabsViewModelFactory(factory: VectorSettingsLabsViewModel.Factory): MavericksAssistedViewModelFactory<*, *>
+
+ @Binds
+ @IntoMap
+ @MavericksViewModelKey(AttachmentTypeSelectorViewModel::class)
+ fun attachmentTypeSelectorViewModelFactory(factory: AttachmentTypeSelectorViewModel.Factory): MavericksAssistedViewModelFactory<*, *>
}
diff --git a/vector/src/main/java/im/vector/app/core/ui/views/BottomSheetActionButton.kt b/vector/src/main/java/im/vector/app/core/ui/views/BottomSheetActionButton.kt
index a3e8b3780c..ca3e6a360a 100644
--- a/vector/src/main/java/im/vector/app/core/ui/views/BottomSheetActionButton.kt
+++ b/vector/src/main/java/im/vector/app/core/ui/views/BottomSheetActionButton.kt
@@ -38,6 +38,10 @@ class BottomSheetActionButton @JvmOverloads constructor(
) : FrameLayout(context, attrs, defStyleAttr) {
val views: ViewBottomSheetActionButtonBinding
+ override fun setOnClickListener(l: OnClickListener?) {
+ views.bottomSheetActionClickableZone.setOnClickListener(l)
+ }
+
var title: String? = null
set(value) {
field = value
diff --git a/vector/src/main/java/im/vector/app/features/attachments/AttachmentType.kt b/vector/src/main/java/im/vector/app/features/attachments/AttachmentType.kt
new file mode 100644
index 0000000000..f4b97b9f9c
--- /dev/null
+++ b/vector/src/main/java/im/vector/app/features/attachments/AttachmentType.kt
@@ -0,0 +1,37 @@
+/*
+ * Copyright (c) 2022 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package im.vector.app.features.attachments
+
+import im.vector.app.core.utils.PERMISSIONS_EMPTY
+import im.vector.app.core.utils.PERMISSIONS_FOR_FOREGROUND_LOCATION_SHARING
+import im.vector.app.core.utils.PERMISSIONS_FOR_PICKING_CONTACT
+import im.vector.app.core.utils.PERMISSIONS_FOR_TAKING_PHOTO
+import im.vector.app.core.utils.PERMISSIONS_FOR_VOICE_BROADCAST
+
+/**
+ * The all possible types to pick with their required permissions.
+ */
+enum class AttachmentType(val permissions: List) {
+ CAMERA(PERMISSIONS_FOR_TAKING_PHOTO),
+ GALLERY(PERMISSIONS_EMPTY),
+ FILE(PERMISSIONS_EMPTY),
+ STICKER(PERMISSIONS_EMPTY),
+ CONTACT(PERMISSIONS_FOR_PICKING_CONTACT),
+ POLL(PERMISSIONS_EMPTY),
+ LOCATION(PERMISSIONS_FOR_FOREGROUND_LOCATION_SHARING),
+ VOICE_BROADCAST(PERMISSIONS_FOR_VOICE_BROADCAST),
+}
diff --git a/vector/src/main/java/im/vector/app/features/attachments/AttachmentTypeSelectorBottomSheet.kt b/vector/src/main/java/im/vector/app/features/attachments/AttachmentTypeSelectorBottomSheet.kt
new file mode 100644
index 0000000000..af17800455
--- /dev/null
+++ b/vector/src/main/java/im/vector/app/features/attachments/AttachmentTypeSelectorBottomSheet.kt
@@ -0,0 +1,80 @@
+/*
+ * 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.attachments
+
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import androidx.core.view.isVisible
+import androidx.fragment.app.FragmentManager
+import androidx.fragment.app.viewModels
+import com.airbnb.mvrx.fragmentViewModel
+import com.airbnb.mvrx.parentFragmentViewModel
+import com.airbnb.mvrx.withState
+import dagger.hilt.android.AndroidEntryPoint
+import im.vector.app.core.platform.VectorBaseBottomSheetDialogFragment
+import im.vector.app.databinding.BottomSheetAttachmentTypeSelectorBinding
+import im.vector.app.features.home.room.detail.TimelineViewModel
+
+@AndroidEntryPoint
+class AttachmentTypeSelectorBottomSheet : VectorBaseBottomSheetDialogFragment() {
+
+ private val viewModel: AttachmentTypeSelectorViewModel by fragmentViewModel()
+ private val timelineViewModel: TimelineViewModel by parentFragmentViewModel()
+ private val sharedActionViewModel: AttachmentTypeSelectorSharedActionViewModel by viewModels(
+ ownerProducer = { requireParentFragment() }
+ )
+
+ override val showExpanded = true
+
+ override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): BottomSheetAttachmentTypeSelectorBinding {
+ return BottomSheetAttachmentTypeSelectorBinding.inflate(inflater, container, false)
+ }
+
+ override fun invalidate() = withState(viewModel, timelineViewModel) { viewState, timelineState ->
+ super.invalidate()
+ views.location.isVisible = viewState.isLocationVisible
+ views.voiceBroadcast.isVisible = viewState.isVoiceBroadcastVisible
+ views.poll.isVisible = !timelineState.isThreadTimeline()
+ }
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ super.onViewCreated(view, savedInstanceState)
+ views.gallery.debouncedClicks { onAttachmentSelected(AttachmentType.GALLERY) }
+ views.stickers.debouncedClicks { onAttachmentSelected(AttachmentType.STICKER) }
+ views.file.debouncedClicks { onAttachmentSelected(AttachmentType.FILE) }
+ views.voiceBroadcast.debouncedClicks { onAttachmentSelected(AttachmentType.VOICE_BROADCAST) }
+ views.poll.debouncedClicks { onAttachmentSelected(AttachmentType.POLL) }
+ views.location.debouncedClicks { onAttachmentSelected(AttachmentType.LOCATION) }
+ views.camera.debouncedClicks { onAttachmentSelected(AttachmentType.CAMERA) }
+ views.contact.debouncedClicks { onAttachmentSelected(AttachmentType.CONTACT) }
+ }
+
+ private fun onAttachmentSelected(attachmentType: AttachmentType) {
+ val action = AttachmentTypeSelectorSharedAction.SelectAttachmentTypeAction(attachmentType)
+ sharedActionViewModel.post(action)
+ dismiss()
+ }
+
+ companion object {
+ fun show(fragmentManager: FragmentManager) {
+ val bottomSheet = AttachmentTypeSelectorBottomSheet()
+ bottomSheet.show(fragmentManager, "AttachmentTypeSelectorBottomSheet")
+ }
+ }
+}
diff --git a/vector/src/main/java/im/vector/app/features/attachments/AttachmentTypeSelectorSharedActionViewModel.kt b/vector/src/main/java/im/vector/app/features/attachments/AttachmentTypeSelectorSharedActionViewModel.kt
new file mode 100644
index 0000000000..e02b10c54b
--- /dev/null
+++ b/vector/src/main/java/im/vector/app/features/attachments/AttachmentTypeSelectorSharedActionViewModel.kt
@@ -0,0 +1,30 @@
+/*
+ * Copyright (c) 2022 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+package im.vector.app.features.attachments
+
+import im.vector.app.core.platform.VectorSharedAction
+import im.vector.app.core.platform.VectorSharedActionViewModel
+import javax.inject.Inject
+
+class AttachmentTypeSelectorSharedActionViewModel @Inject constructor() :
+ VectorSharedActionViewModel()
+
+sealed interface AttachmentTypeSelectorSharedAction : VectorSharedAction {
+ data class SelectAttachmentTypeAction(
+ val attachmentType: AttachmentType
+ ) : AttachmentTypeSelectorSharedAction
+}
diff --git a/vector/src/main/java/im/vector/app/features/attachments/AttachmentTypeSelectorView.kt b/vector/src/main/java/im/vector/app/features/attachments/AttachmentTypeSelectorView.kt
index 8536b765d4..55805a0728 100644
--- a/vector/src/main/java/im/vector/app/features/attachments/AttachmentTypeSelectorView.kt
+++ b/vector/src/main/java/im/vector/app/features/attachments/AttachmentTypeSelectorView.kt
@@ -30,17 +30,11 @@ import android.view.animation.TranslateAnimation
import android.widget.ImageButton
import android.widget.LinearLayout
import android.widget.PopupWindow
-import androidx.annotation.StringRes
import androidx.appcompat.widget.TooltipCompat
import androidx.core.view.doOnNextLayout
import androidx.core.view.isVisible
import im.vector.app.R
import im.vector.app.core.epoxy.onClick
-import im.vector.app.core.utils.PERMISSIONS_EMPTY
-import im.vector.app.core.utils.PERMISSIONS_FOR_FOREGROUND_LOCATION_SHARING
-import im.vector.app.core.utils.PERMISSIONS_FOR_PICKING_CONTACT
-import im.vector.app.core.utils.PERMISSIONS_FOR_TAKING_PHOTO
-import im.vector.app.core.utils.PERMISSIONS_FOR_VOICE_BROADCAST
import im.vector.app.databinding.ViewAttachmentTypeSelectorBinding
import im.vector.app.features.attachments.AttachmentTypeSelectorView.Callback
import kotlin.math.max
@@ -59,7 +53,7 @@ class AttachmentTypeSelectorView(
) : PopupWindow(context) {
interface Callback {
- fun onTypeSelected(type: Type)
+ fun onTypeSelected(type: AttachmentType)
}
private val views: ViewAttachmentTypeSelectorBinding
@@ -69,14 +63,14 @@ class AttachmentTypeSelectorView(
init {
contentView = inflater.inflate(R.layout.view_attachment_type_selector, null, false)
views = ViewAttachmentTypeSelectorBinding.bind(contentView)
- views.attachmentGalleryButton.configure(Type.GALLERY)
- views.attachmentCameraButton.configure(Type.CAMERA)
- views.attachmentFileButton.configure(Type.FILE)
- views.attachmentStickersButton.configure(Type.STICKER)
- views.attachmentContactButton.configure(Type.CONTACT)
- views.attachmentPollButton.configure(Type.POLL)
- views.attachmentLocationButton.configure(Type.LOCATION)
- views.attachmentVoiceBroadcast.configure(Type.VOICE_BROADCAST)
+ views.attachmentGalleryButton.configure(AttachmentType.GALLERY)
+ views.attachmentCameraButton.configure(AttachmentType.CAMERA)
+ views.attachmentFileButton.configure(AttachmentType.FILE)
+ views.attachmentStickersButton.configure(AttachmentType.STICKER)
+ views.attachmentContactButton.configure(AttachmentType.CONTACT)
+ views.attachmentPollButton.configure(AttachmentType.POLL)
+ views.attachmentLocationButton.configure(AttachmentType.LOCATION)
+ views.attachmentVoiceBroadcast.configure(AttachmentType.VOICE_BROADCAST)
width = LinearLayout.LayoutParams.MATCH_PARENT
height = LinearLayout.LayoutParams.WRAP_CONTENT
animationStyle = 0
@@ -127,16 +121,16 @@ class AttachmentTypeSelectorView(
}
}
- fun setAttachmentVisibility(type: Type, isVisible: Boolean) {
+ fun setAttachmentVisibility(type: AttachmentType, isVisible: Boolean) {
when (type) {
- Type.CAMERA -> views.attachmentCameraButton
- Type.GALLERY -> views.attachmentGalleryButton
- Type.FILE -> views.attachmentFileButton
- Type.STICKER -> views.attachmentStickersButton
- Type.CONTACT -> views.attachmentContactButton
- Type.POLL -> views.attachmentPollButton
- Type.LOCATION -> views.attachmentLocationButton
- Type.VOICE_BROADCAST -> views.attachmentVoiceBroadcast
+ AttachmentType.CAMERA -> views.attachmentCameraButton
+ AttachmentType.GALLERY -> views.attachmentGalleryButton
+ AttachmentType.FILE -> views.attachmentFileButton
+ AttachmentType.STICKER -> views.attachmentStickersButton
+ AttachmentType.CONTACT -> views.attachmentContactButton
+ AttachmentType.POLL -> views.attachmentPollButton
+ AttachmentType.LOCATION -> views.attachmentLocationButton
+ AttachmentType.VOICE_BROADCAST -> views.attachmentVoiceBroadcast
}.let {
it.isVisible = isVisible
}
@@ -200,13 +194,13 @@ class AttachmentTypeSelectorView(
return Pair(x, y)
}
- private fun ImageButton.configure(type: Type): ImageButton {
+ private fun ImageButton.configure(type: AttachmentType): ImageButton {
this.setOnClickListener(TypeClickListener(type))
- TooltipCompat.setTooltipText(this, context.getString(type.tooltipRes))
+ TooltipCompat.setTooltipText(this, context.getString(attachmentTooltipLabels.getValue(type)))
return this
}
- private inner class TypeClickListener(private val type: Type) : View.OnClickListener {
+ private inner class TypeClickListener(private val type: AttachmentType) : View.OnClickListener {
override fun onClick(v: View) {
dismiss()
@@ -217,14 +211,18 @@ class AttachmentTypeSelectorView(
/**
* The all possible types to pick with their required permissions and tooltip resource.
*/
- enum class Type(val permissions: List, @StringRes val tooltipRes: Int) {
- CAMERA(PERMISSIONS_FOR_TAKING_PHOTO, R.string.tooltip_attachment_photo),
- GALLERY(PERMISSIONS_EMPTY, R.string.tooltip_attachment_gallery),
- FILE(PERMISSIONS_EMPTY, R.string.tooltip_attachment_file),
- STICKER(PERMISSIONS_EMPTY, R.string.tooltip_attachment_sticker),
- CONTACT(PERMISSIONS_FOR_PICKING_CONTACT, R.string.tooltip_attachment_contact),
- POLL(PERMISSIONS_EMPTY, R.string.tooltip_attachment_poll),
- LOCATION(PERMISSIONS_FOR_FOREGROUND_LOCATION_SHARING, R.string.tooltip_attachment_location),
- VOICE_BROADCAST(PERMISSIONS_FOR_VOICE_BROADCAST, R.string.tooltip_attachment_voice_broadcast),
+ private companion object {
+ private val attachmentTooltipLabels: Map = AttachmentType.values().associateWith {
+ when (it) {
+ AttachmentType.CAMERA -> R.string.tooltip_attachment_photo
+ AttachmentType.GALLERY -> R.string.tooltip_attachment_gallery
+ AttachmentType.FILE -> R.string.tooltip_attachment_file
+ AttachmentType.STICKER -> R.string.tooltip_attachment_sticker
+ AttachmentType.CONTACT -> R.string.tooltip_attachment_contact
+ AttachmentType.POLL -> R.string.tooltip_attachment_poll
+ AttachmentType.LOCATION -> R.string.tooltip_attachment_location
+ AttachmentType.VOICE_BROADCAST -> R.string.tooltip_attachment_voice_broadcast
+ }
+ }
}
}
diff --git a/vector/src/main/java/im/vector/app/features/attachments/AttachmentTypeSelectorViewModel.kt b/vector/src/main/java/im/vector/app/features/attachments/AttachmentTypeSelectorViewModel.kt
new file mode 100644
index 0000000000..fe6616e53a
--- /dev/null
+++ b/vector/src/main/java/im/vector/app/features/attachments/AttachmentTypeSelectorViewModel.kt
@@ -0,0 +1,59 @@
+/*
+ * 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.attachments
+
+import com.airbnb.mvrx.MavericksState
+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.EmptyAction
+import im.vector.app.core.platform.EmptyViewEvents
+import im.vector.app.core.platform.VectorViewModel
+import im.vector.app.features.VectorFeatures
+
+class AttachmentTypeSelectorViewModel @AssistedInject constructor(
+ @Assisted initialState: AttachmentTypeSelectorViewState,
+ private val vectorFeatures: VectorFeatures,
+) : VectorViewModel(initialState) {
+ @AssistedFactory
+ interface Factory : MavericksAssistedViewModelFactory {
+ override fun create(initialState: AttachmentTypeSelectorViewState): AttachmentTypeSelectorViewModel
+ }
+
+ companion object : MavericksViewModelFactory by hiltMavericksViewModelFactory()
+
+ override fun handle(action: EmptyAction) {
+ // do nothing
+ }
+
+ init {
+ setState {
+ copy(
+ isLocationVisible = vectorFeatures.isLocationSharingEnabled(),
+ isVoiceBroadcastVisible = vectorFeatures.isVoiceBroadcastEnabled(),
+ )
+ }
+ }
+}
+
+data class AttachmentTypeSelectorViewState(
+ val isLocationVisible: Boolean = false,
+ val isVoiceBroadcastVisible: Boolean = false,
+) : MavericksState
diff --git a/vector/src/main/java/im/vector/app/features/attachments/AttachmentsHelper.kt b/vector/src/main/java/im/vector/app/features/attachments/AttachmentsHelper.kt
index 1a8e10d102..9692777e15 100644
--- a/vector/src/main/java/im/vector/app/features/attachments/AttachmentsHelper.kt
+++ b/vector/src/main/java/im/vector/app/features/attachments/AttachmentsHelper.kt
@@ -54,7 +54,7 @@ class AttachmentsHelper(
private var captureUri: Uri? = null
// The pending type is set if we have to handle permission request. It must be restored if the activity gets killed.
- var pendingType: AttachmentTypeSelectorView.Type? = null
+ var pendingType: AttachmentType? = null
// Restorable
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerFragment.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerFragment.kt
index 55ec922a57..afa9c84353 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerFragment.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerFragment.kt
@@ -40,6 +40,7 @@ import androidx.core.text.buildSpannedString
import androidx.core.view.isGone
import androidx.core.view.isInvisible
import androidx.core.view.isVisible
+import androidx.fragment.app.viewModels
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import com.airbnb.mvrx.parentFragmentViewModel
@@ -63,6 +64,10 @@ import im.vector.app.core.utils.onPermissionDeniedDialog
import im.vector.app.core.utils.registerForPermissionsResult
import im.vector.app.databinding.FragmentComposerBinding
import im.vector.app.features.VectorFeatures
+import im.vector.app.features.attachments.AttachmentType
+import im.vector.app.features.attachments.AttachmentTypeSelectorBottomSheet
+import im.vector.app.features.attachments.AttachmentTypeSelectorSharedAction
+import im.vector.app.features.attachments.AttachmentTypeSelectorSharedActionViewModel
import im.vector.app.features.attachments.AttachmentTypeSelectorView
import im.vector.app.features.attachments.AttachmentsHelper
import im.vector.app.features.attachments.ContactAttachment
@@ -92,6 +97,7 @@ import im.vector.app.features.settings.VectorPreferences
import im.vector.app.features.share.SharedData
import im.vector.app.features.voice.VoiceFailure
import kotlinx.coroutines.flow.debounce
+import kotlinx.coroutines.flow.filterIsInstance
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach
@@ -161,6 +167,7 @@ class MessageComposerFragment : VectorBaseFragment(), A
private val timelineViewModel: TimelineViewModel by parentFragmentViewModel()
private val messageComposerViewModel: MessageComposerViewModel by parentFragmentViewModel()
private lateinit var sharedActionViewModel: MessageSharedActionViewModel
+ private val attachmentViewModel: AttachmentTypeSelectorSharedActionViewModel by viewModels()
private val composer: MessageComposerView get() {
return if (vectorPreferences.isRichTextEditorEnabled()) {
@@ -219,6 +226,11 @@ class MessageComposerFragment : VectorBaseFragment(), A
}
}
+ attachmentViewModel.stream()
+ .filterIsInstance()
+ .onEach { onTypeSelected(it.attachmentType) }
+ .launchIn(lifecycleScope)
+
if (savedInstanceState != null) {
handleShareData()
}
@@ -299,21 +311,25 @@ class MessageComposerFragment : VectorBaseFragment(), A
}
composer.callback = object : PlainTextComposerLayout.Callback {
override fun onAddAttachment() {
- if (!::attachmentTypeSelector.isInitialized) {
- attachmentTypeSelector = AttachmentTypeSelectorView(vectorBaseActivity, vectorBaseActivity.layoutInflater, this@MessageComposerFragment)
- attachmentTypeSelector.setAttachmentVisibility(
- AttachmentTypeSelectorView.Type.LOCATION,
- vectorFeatures.isLocationSharingEnabled(),
- )
- attachmentTypeSelector.setAttachmentVisibility(
- AttachmentTypeSelectorView.Type.POLL, !isThreadTimeLine()
- )
- attachmentTypeSelector.setAttachmentVisibility(
- AttachmentTypeSelectorView.Type.VOICE_BROADCAST,
- vectorPreferences.isVoiceBroadcastEnabled(), // TODO check user permission
- )
+ if (vectorPreferences.isRichTextEditorEnabled()) {
+ AttachmentTypeSelectorBottomSheet.show(childFragmentManager)
+ } else {
+ if (!::attachmentTypeSelector.isInitialized) {
+ attachmentTypeSelector = AttachmentTypeSelectorView(vectorBaseActivity, vectorBaseActivity.layoutInflater, this@MessageComposerFragment)
+ attachmentTypeSelector.setAttachmentVisibility(
+ AttachmentType.LOCATION,
+ vectorFeatures.isLocationSharingEnabled(),
+ )
+ attachmentTypeSelector.setAttachmentVisibility(
+ AttachmentType.POLL, !isThreadTimeLine()
+ )
+ attachmentTypeSelector.setAttachmentVisibility(
+ AttachmentType.VOICE_BROADCAST,
+ vectorPreferences.isVoiceBroadcastEnabled(), // TODO check user permission
+ )
+ }
+ attachmentTypeSelector.show(composer.attachmentButton)
}
- attachmentTypeSelector.show(composer.attachmentButton)
}
override fun onExpandOrCompactChange() {
@@ -662,20 +678,20 @@ class MessageComposerFragment : VectorBaseFragment(), A
}
}
- private fun launchAttachmentProcess(type: AttachmentTypeSelectorView.Type) {
+ private fun launchAttachmentProcess(type: AttachmentType) {
when (type) {
- AttachmentTypeSelectorView.Type.CAMERA -> attachmentsHelper.openCamera(
+ AttachmentType.CAMERA -> attachmentsHelper.openCamera(
activity = requireActivity(),
vectorPreferences = vectorPreferences,
cameraActivityResultLauncher = attachmentCameraActivityResultLauncher,
cameraVideoActivityResultLauncher = attachmentCameraVideoActivityResultLauncher
)
- AttachmentTypeSelectorView.Type.FILE -> attachmentsHelper.selectFile(attachmentFileActivityResultLauncher)
- AttachmentTypeSelectorView.Type.GALLERY -> attachmentsHelper.selectGallery(attachmentMediaActivityResultLauncher)
- AttachmentTypeSelectorView.Type.CONTACT -> attachmentsHelper.selectContact(attachmentContactActivityResultLauncher)
- AttachmentTypeSelectorView.Type.STICKER -> timelineViewModel.handle(RoomDetailAction.SelectStickerAttachment)
- AttachmentTypeSelectorView.Type.POLL -> navigator.openCreatePoll(requireContext(), roomId, null, PollMode.CREATE)
- AttachmentTypeSelectorView.Type.LOCATION -> {
+ AttachmentType.FILE -> attachmentsHelper.selectFile(attachmentFileActivityResultLauncher)
+ AttachmentType.GALLERY -> attachmentsHelper.selectGallery(attachmentMediaActivityResultLauncher)
+ AttachmentType.CONTACT -> attachmentsHelper.selectContact(attachmentContactActivityResultLauncher)
+ AttachmentType.STICKER -> timelineViewModel.handle(RoomDetailAction.SelectStickerAttachment)
+ AttachmentType.POLL -> navigator.openCreatePoll(requireContext(), roomId, null, PollMode.CREATE)
+ AttachmentType.LOCATION -> {
navigator
.openLocationSharing(
context = requireContext(),
@@ -685,11 +701,11 @@ class MessageComposerFragment : VectorBaseFragment(), A
locationOwnerId = session.myUserId
)
}
- AttachmentTypeSelectorView.Type.VOICE_BROADCAST -> timelineViewModel.handle(VoiceBroadcastAction.Recording.Start)
+ AttachmentType.VOICE_BROADCAST -> timelineViewModel.handle(VoiceBroadcastAction.Recording.Start)
}
}
- override fun onTypeSelected(type: AttachmentTypeSelectorView.Type) {
+ override fun onTypeSelected(type: AttachmentType) {
if (checkPermissions(type.permissions, requireActivity(), typeSelectedActivityResultLauncher)) {
launchAttachmentProcess(type)
} else {
diff --git a/vector/src/main/res/layout/bottom_sheet_attachment_type_selector.xml b/vector/src/main/res/layout/bottom_sheet_attachment_type_selector.xml
new file mode 100644
index 0000000000..79a60624cf
--- /dev/null
+++ b/vector/src/main/res/layout/bottom_sheet_attachment_type_selector.xml
@@ -0,0 +1,86 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/vector/src/test/java/im/vector/app/features/attachments/AttachmentTypeSelectorViewModelTest.kt b/vector/src/test/java/im/vector/app/features/attachments/AttachmentTypeSelectorViewModelTest.kt
new file mode 100644
index 0000000000..478f631c06
--- /dev/null
+++ b/vector/src/test/java/im/vector/app/features/attachments/AttachmentTypeSelectorViewModelTest.kt
@@ -0,0 +1,91 @@
+/*
+ * 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.attachments
+
+import com.airbnb.mvrx.test.MavericksTestRule
+import im.vector.app.test.fakes.FakeVectorFeatures
+import im.vector.app.test.test
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+
+internal class AttachmentTypeSelectorViewModelTest {
+
+ @get:Rule
+ val mavericksTestRule = MavericksTestRule()
+
+ private val fakeVectorFeatures = FakeVectorFeatures()
+ private val initialState = AttachmentTypeSelectorViewState()
+
+ @Before
+ fun setUp() {
+ // Disable all features by default
+ fakeVectorFeatures.givenLocationSharing(isEnabled = false)
+ fakeVectorFeatures.givenVoiceBroadcast(isEnabled = false)
+ }
+
+ @Test
+ fun `given features are not enabled, then options are not visible`() {
+ createViewModel()
+ .test()
+ .assertStates(
+ listOf(
+ initialState,
+ )
+ )
+ .finish()
+ }
+
+ @Test
+ fun `given location sharing is enabled, then location sharing option is visible`() {
+ fakeVectorFeatures.givenLocationSharing(isEnabled = true)
+
+ createViewModel()
+ .test()
+ .assertStates(
+ listOf(
+ initialState.copy(
+ isLocationVisible = true
+ ),
+ )
+ )
+ .finish()
+ }
+
+ @Test
+ fun `given voice broadcast is enabled, then voice broadcast option is visible`() {
+ fakeVectorFeatures.givenVoiceBroadcast(isEnabled = true)
+
+ createViewModel()
+ .test()
+ .assertStates(
+ listOf(
+ initialState.copy(
+ isVoiceBroadcastVisible = true
+ ),
+ )
+ )
+ .finish()
+ }
+
+ private fun createViewModel(): AttachmentTypeSelectorViewModel {
+ return AttachmentTypeSelectorViewModel(
+ initialState,
+ vectorFeatures = fakeVectorFeatures,
+ )
+ }
+}
diff --git a/vector/src/test/java/im/vector/app/test/fakes/FakeVectorFeatures.kt b/vector/src/test/java/im/vector/app/test/fakes/FakeVectorFeatures.kt
index 4e6b4fc3df..d989abc214 100644
--- a/vector/src/test/java/im/vector/app/test/fakes/FakeVectorFeatures.kt
+++ b/vector/src/test/java/im/vector/app/test/fakes/FakeVectorFeatures.kt
@@ -42,4 +42,12 @@ class FakeVectorFeatures : VectorFeatures by spyk() {
fun givenCombinedLoginDisabled() {
every { isOnboardingCombinedLoginEnabled() } returns false
}
+
+ fun givenLocationSharing(isEnabled: Boolean) {
+ every { isLocationSharingEnabled() } returns isEnabled
+ }
+
+ fun givenVoiceBroadcast(isEnabled: Boolean) {
+ every { isVoiceBroadcastEnabled() } returns isEnabled
+ }
}