From a26e9594302c1503a2797af7ae5074a6904f048d Mon Sep 17 00:00:00 2001 From: Ganard Date: Tue, 4 Feb 2020 16:35:04 +0100 Subject: [PATCH 01/33] Attachements: introduce structure for preview, cropping and compressing --- build.gradle | 2 + vector/build.gradle | 1 + vector/src/main/AndroidManifest.xml | 5 ++ .../im/vector/riotx/core/di/FragmentModule.kt | 7 ++ .../preview/AttachmentPreviewController.kt | 30 +++++++ .../preview/AttachmentPreviewItem.kt | 58 +++++++++++++ .../preview/AttachmentsPreviewAction.kt | 22 +++++ .../preview/AttachmentsPreviewActivity.kt | 54 ++++++++++++ .../preview/AttachmentsPreviewFragment.kt | 67 +++++++++++++++ .../preview/AttachmentsPreviewViewEvents.kt | 22 +++++ .../preview/AttachmentsPreviewViewModel.kt | 47 ++++++++++ .../preview/AttachmentsPreviewViewState.kt | 28 ++++++ .../layout/fragment_attachments_preview.xml | 86 +++++++++++++++++++ .../res/layout/item_attachment_preview.xml | 19 ++++ 14 files changed, 448 insertions(+) create mode 100644 vector/src/main/java/im/vector/riotx/features/attachments/preview/AttachmentPreviewController.kt create mode 100644 vector/src/main/java/im/vector/riotx/features/attachments/preview/AttachmentPreviewItem.kt create mode 100644 vector/src/main/java/im/vector/riotx/features/attachments/preview/AttachmentsPreviewAction.kt create mode 100644 vector/src/main/java/im/vector/riotx/features/attachments/preview/AttachmentsPreviewActivity.kt create mode 100644 vector/src/main/java/im/vector/riotx/features/attachments/preview/AttachmentsPreviewFragment.kt create mode 100644 vector/src/main/java/im/vector/riotx/features/attachments/preview/AttachmentsPreviewViewEvents.kt create mode 100644 vector/src/main/java/im/vector/riotx/features/attachments/preview/AttachmentsPreviewViewModel.kt create mode 100644 vector/src/main/java/im/vector/riotx/features/attachments/preview/AttachmentsPreviewViewState.kt create mode 100644 vector/src/main/res/layout/fragment_attachments_preview.xml create mode 100644 vector/src/main/res/layout/item_attachment_preview.xml diff --git a/build.gradle b/build.gradle index a2fac55175..7f80eaf5fe 100644 --- a/build.gradle +++ b/build.gradle @@ -34,6 +34,8 @@ allprojects { includeGroupByRegex "com\\.github\\.jaiselrahman" // And monarchy includeGroupByRegex "com\\.github\\.Zhuinden" + // And ucrop + includeGroupByRegex "com\\.github\\.yalantis" } } maven { diff --git a/vector/build.gradle b/vector/build.gradle index 0517482904..7352adc712 100644 --- a/vector/build.gradle +++ b/vector/build.gradle @@ -341,6 +341,7 @@ dependencies { implementation "com.github.bumptech.glide:glide:$glide_version" kapt "com.github.bumptech.glide:compiler:$glide_version" implementation 'com.danikula:videocache:2.7.1' + implementation 'com.github.yalantis:ucrop:2.2.4' // Badge for compatibility implementation 'me.leolin:ShortcutBadger:1.1.22@aar' diff --git a/vector/src/main/AndroidManifest.xml b/vector/src/main/AndroidManifest.xml index 3207ab257a..f908880e6f 100644 --- a/vector/src/main/AndroidManifest.xml +++ b/vector/src/main/AndroidManifest.xml @@ -134,6 +134,11 @@ + + + () { + + override fun buildModels(data: AttachmentsPreviewViewState) { + data.attachments.forEach { + + } + } +} diff --git a/vector/src/main/java/im/vector/riotx/features/attachments/preview/AttachmentPreviewItem.kt b/vector/src/main/java/im/vector/riotx/features/attachments/preview/AttachmentPreviewItem.kt new file mode 100644 index 0000000000..03ac160d62 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/attachments/preview/AttachmentPreviewItem.kt @@ -0,0 +1,58 @@ +/* + * Copyright 2020 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.riotx.features.attachments.preview + +import android.view.View +import android.widget.ImageView +import com.airbnb.epoxy.EpoxyAttribute +import com.airbnb.epoxy.EpoxyModelClass +import com.bumptech.glide.Glide +import com.bumptech.glide.request.RequestOptions +import im.vector.matrix.android.api.session.content.ContentAttachmentData +import im.vector.riotx.R +import im.vector.riotx.core.epoxy.VectorEpoxyHolder +import im.vector.riotx.core.epoxy.VectorEpoxyModel +import kotlinx.android.synthetic.main.item_attachment_preview.view.* + +@EpoxyModelClass(layout = R.layout.item_attachment_preview) +abstract class AttachmentPreviewItem : VectorEpoxyModel() { + + @EpoxyAttribute lateinit var attachment: ContentAttachmentData + @EpoxyAttribute var clickListener: View.OnClickListener? = null + + override fun bind(holder: Holder) { + holder.view.setOnClickListener(clickListener) + // If name is empty, use userId as name and force it being centered + val mimeType = attachment.mimeType + val path = attachment.path + if (mimeType != null && (mimeType.startsWith("image") || mimeType.startsWith("video"))) { + Glide.with(holder.view.context) + .asBitmap() + .load(path) + .apply(RequestOptions().frame(0)) + .into(holder.imageView) + } else { + holder.imageView.attachmentPreviewImageView.setImageResource(R.drawable.filetype_attachment) + holder.imageView.attachmentPreviewImageView.scaleType = ImageView.ScaleType.FIT_CENTER + } + } + + class Holder : VectorEpoxyHolder() { + val imageView by bind(R.id.attachmentPreviewImageView) + } +} diff --git a/vector/src/main/java/im/vector/riotx/features/attachments/preview/AttachmentsPreviewAction.kt b/vector/src/main/java/im/vector/riotx/features/attachments/preview/AttachmentsPreviewAction.kt new file mode 100644 index 0000000000..61fb41a774 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/attachments/preview/AttachmentsPreviewAction.kt @@ -0,0 +1,22 @@ +/* + * Copyright 2020 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.riotx.features.attachments.preview + +import im.vector.riotx.core.platform.VectorViewModelAction + +sealed class AttachmentsPreviewAction : VectorViewModelAction diff --git a/vector/src/main/java/im/vector/riotx/features/attachments/preview/AttachmentsPreviewActivity.kt b/vector/src/main/java/im/vector/riotx/features/attachments/preview/AttachmentsPreviewActivity.kt new file mode 100644 index 0000000000..509be251c3 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/attachments/preview/AttachmentsPreviewActivity.kt @@ -0,0 +1,54 @@ +/* + * Copyright 2020 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.riotx.features.attachments.preview + +import android.content.Context +import android.content.Intent +import androidx.appcompat.widget.Toolbar +import im.vector.riotx.R +import im.vector.riotx.core.extensions.addFragment +import im.vector.riotx.core.platform.ToolbarConfigurable +import im.vector.riotx.core.platform.VectorBaseActivity + +class AttachmentsPreviewActivity : VectorBaseActivity(), ToolbarConfigurable { + + companion object { + + private const val EXTRA_FRAGMENT_ARGS = "EXTRA_FRAGMENT_ARGS" + + fun newIntent(context: Context, args: AttachmentsPreviewArgs): Intent { + return Intent(context, AttachmentsPreviewArgs::class.java).apply { + putExtra(EXTRA_FRAGMENT_ARGS, args) + } + } + } + + override fun getLayoutRes() = R.layout.activity_simple + + override fun initUiAndData() { + if (isFirstCreation()) { + val fragmentArgs: AttachmentsPreviewArgs = intent?.extras?.getParcelable(EXTRA_FRAGMENT_ARGS) + ?: return + addFragment(R.id.simpleFragmentContainer, AttachmentsPreviewFragment::class.java, fragmentArgs) + } + } + + override fun configure(toolbar: Toolbar) { + configureToolbar(toolbar) + } +} diff --git a/vector/src/main/java/im/vector/riotx/features/attachments/preview/AttachmentsPreviewFragment.kt b/vector/src/main/java/im/vector/riotx/features/attachments/preview/AttachmentsPreviewFragment.kt new file mode 100644 index 0000000000..6e99758c6d --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/attachments/preview/AttachmentsPreviewFragment.kt @@ -0,0 +1,67 @@ +/* + * Copyright 2020 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.riotx.features.attachments.preview + +import android.os.Bundle +import android.os.Parcelable +import android.view.View +import com.airbnb.mvrx.args +import com.airbnb.mvrx.fragmentViewModel +import com.airbnb.mvrx.withState +import im.vector.matrix.android.api.session.content.ContentAttachmentData +import im.vector.riotx.R +import im.vector.riotx.core.extensions.cleanup +import im.vector.riotx.core.extensions.configureWith +import im.vector.riotx.core.platform.VectorBaseFragment +import kotlinx.android.parcel.Parcelize +import kotlinx.android.synthetic.main.fragment_attachments_preview.* +import javax.inject.Inject + +@Parcelize +data class AttachmentsPreviewArgs( + val attachments: List +) : Parcelable + +class AttachmentsPreviewFragment @Inject constructor( + val viewModelFactory: AttachmentsPreviewViewModel.Factory, + private val attachmentPreviewController: AttachmentPreviewController +) : VectorBaseFragment() { + + private val fragmentArgs: AttachmentsPreviewArgs by args() + private val viewModel: AttachmentsPreviewViewModel by fragmentViewModel() + + override fun getLayoutResId() = R.layout.fragment_attachments_preview + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + setupRecyclerView() + } + + override fun onDestroyView() { + super.onDestroyView() + attachmentPreviewerList.cleanup() + } + + private fun setupRecyclerView() { + attachmentPreviewerList.configureWith(attachmentPreviewController, hasFixedSize = true) + } + + override fun invalidate() = withState(viewModel) { state -> + + } +} diff --git a/vector/src/main/java/im/vector/riotx/features/attachments/preview/AttachmentsPreviewViewEvents.kt b/vector/src/main/java/im/vector/riotx/features/attachments/preview/AttachmentsPreviewViewEvents.kt new file mode 100644 index 0000000000..12a9d9aa24 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/attachments/preview/AttachmentsPreviewViewEvents.kt @@ -0,0 +1,22 @@ +/* + * Copyright 2020 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.riotx.features.attachments.preview + +import im.vector.riotx.core.platform.VectorViewEvents + +sealed class AttachmentsPreviewViewEvents : VectorViewEvents diff --git a/vector/src/main/java/im/vector/riotx/features/attachments/preview/AttachmentsPreviewViewModel.kt b/vector/src/main/java/im/vector/riotx/features/attachments/preview/AttachmentsPreviewViewModel.kt new file mode 100644 index 0000000000..28a0aaebd6 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/attachments/preview/AttachmentsPreviewViewModel.kt @@ -0,0 +1,47 @@ +/* + * Copyright 2020 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.riotx.features.attachments.preview + +import com.airbnb.mvrx.FragmentViewModelContext +import com.airbnb.mvrx.MvRxViewModelFactory +import com.airbnb.mvrx.ViewModelContext +import com.squareup.inject.assisted.Assisted +import com.squareup.inject.assisted.AssistedInject +import im.vector.riotx.core.platform.VectorViewModel + +class AttachmentsPreviewViewModel @AssistedInject constructor(@Assisted initialState: AttachmentsPreviewViewState) + : VectorViewModel(initialState) { + + @AssistedInject.Factory + interface Factory { + fun create(initialState: AttachmentsPreviewViewState): AttachmentsPreviewViewModel + } + + companion object : MvRxViewModelFactory { + + @JvmStatic + override fun create(viewModelContext: ViewModelContext, state: AttachmentsPreviewViewState): AttachmentsPreviewViewModel? { + val fragment: AttachmentsPreviewFragment = (viewModelContext as FragmentViewModelContext).fragment() + return fragment.viewModelFactory.create(state) + } + } + + override fun handle(action: AttachmentsPreviewAction) { + //TODO + } +} diff --git a/vector/src/main/java/im/vector/riotx/features/attachments/preview/AttachmentsPreviewViewState.kt b/vector/src/main/java/im/vector/riotx/features/attachments/preview/AttachmentsPreviewViewState.kt new file mode 100644 index 0000000000..f92abb97ea --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/attachments/preview/AttachmentsPreviewViewState.kt @@ -0,0 +1,28 @@ +/* + * Copyright 2020 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.riotx.features.attachments.preview + +import com.airbnb.mvrx.MvRxState +import im.vector.matrix.android.api.session.content.ContentAttachmentData + +data class AttachmentsPreviewViewState( + val attachments: List +) : MvRxState { + + constructor(args: AttachmentsPreviewArgs) : this(attachments = args.attachments) +} diff --git a/vector/src/main/res/layout/fragment_attachments_preview.xml b/vector/src/main/res/layout/fragment_attachments_preview.xml new file mode 100644 index 0000000000..60d311b96d --- /dev/null +++ b/vector/src/main/res/layout/fragment_attachments_preview.xml @@ -0,0 +1,86 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/vector/src/main/res/layout/item_attachment_preview.xml b/vector/src/main/res/layout/item_attachment_preview.xml new file mode 100644 index 0000000000..3c769d17d9 --- /dev/null +++ b/vector/src/main/res/layout/item_attachment_preview.xml @@ -0,0 +1,19 @@ + + + + + + \ No newline at end of file From 41f1ec5d880995afbd0eeb145f3ae1f283f4de96 Mon Sep 17 00:00:00 2001 From: ganfra Date: Wed, 5 Feb 2020 11:24:18 +0100 Subject: [PATCH 02/33] Attachments: preview with pager mode --- .../preview/AttachmentPreviewController.kt | 30 ------- .../preview/AttachmentPreviewControllers.kt | 54 ++++++++++++ .../preview/AttachmentPreviewItem.kt | 58 ------------- .../preview/AttachmentPreviewItems.kt | 82 ++++++++++++++++++ .../preview/AttachmentsPreviewActivity.kt | 10 ++- .../preview/AttachmentsPreviewFragment.kt | 33 +++++-- .../home/room/detail/RoomDetailFragment.kt | 5 +- .../layout/fragment_attachments_preview.xml | 85 ++++--------------- .../layout/item_attachment_big_preview.xml | 12 +++ ... => item_attachment_miniature_preview.xml} | 9 +- 10 files changed, 209 insertions(+), 169 deletions(-) delete mode 100644 vector/src/main/java/im/vector/riotx/features/attachments/preview/AttachmentPreviewController.kt create mode 100644 vector/src/main/java/im/vector/riotx/features/attachments/preview/AttachmentPreviewControllers.kt delete mode 100644 vector/src/main/java/im/vector/riotx/features/attachments/preview/AttachmentPreviewItem.kt create mode 100644 vector/src/main/java/im/vector/riotx/features/attachments/preview/AttachmentPreviewItems.kt create mode 100644 vector/src/main/res/layout/item_attachment_big_preview.xml rename vector/src/main/res/layout/{item_attachment_preview.xml => item_attachment_miniature_preview.xml} (67%) diff --git a/vector/src/main/java/im/vector/riotx/features/attachments/preview/AttachmentPreviewController.kt b/vector/src/main/java/im/vector/riotx/features/attachments/preview/AttachmentPreviewController.kt deleted file mode 100644 index 273fd4272c..0000000000 --- a/vector/src/main/java/im/vector/riotx/features/attachments/preview/AttachmentPreviewController.kt +++ /dev/null @@ -1,30 +0,0 @@ -/* - * Copyright 2020 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.riotx.features.attachments.preview - -import com.airbnb.epoxy.TypedEpoxyController -import javax.inject.Inject - -class AttachmentPreviewController @Inject constructor() : TypedEpoxyController() { - - override fun buildModels(data: AttachmentsPreviewViewState) { - data.attachments.forEach { - - } - } -} diff --git a/vector/src/main/java/im/vector/riotx/features/attachments/preview/AttachmentPreviewControllers.kt b/vector/src/main/java/im/vector/riotx/features/attachments/preview/AttachmentPreviewControllers.kt new file mode 100644 index 0000000000..9cef33d402 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/attachments/preview/AttachmentPreviewControllers.kt @@ -0,0 +1,54 @@ +/* + * Copyright 2020 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.riotx.features.attachments.preview + +import com.airbnb.epoxy.TypedEpoxyController +import im.vector.matrix.android.api.session.content.ContentAttachmentData +import javax.inject.Inject + +class AttachmentBigPreviewController @Inject constructor() : TypedEpoxyController() { + + override fun buildModels(data: AttachmentsPreviewViewState) { + data.attachments.forEach { + attachmentBigPreviewItem { + id(it.path) + attachment(it) + } + } + } +} + +class AttachmentMiniaturePreviewController @Inject constructor() : TypedEpoxyController() { + + interface Callback { + fun onAttachmentClicked(contentAttachmentData: ContentAttachmentData) + } + + var callback: Callback? = null + + override fun buildModels(data: AttachmentsPreviewViewState) { + data.attachments.forEach { + attachmentMiniaturePreviewItem { + id(it.path) + attachment(it) + clickListener { _ -> + callback?.onAttachmentClicked(it) + } + } + } + } +} diff --git a/vector/src/main/java/im/vector/riotx/features/attachments/preview/AttachmentPreviewItem.kt b/vector/src/main/java/im/vector/riotx/features/attachments/preview/AttachmentPreviewItem.kt deleted file mode 100644 index 03ac160d62..0000000000 --- a/vector/src/main/java/im/vector/riotx/features/attachments/preview/AttachmentPreviewItem.kt +++ /dev/null @@ -1,58 +0,0 @@ -/* - * Copyright 2020 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.riotx.features.attachments.preview - -import android.view.View -import android.widget.ImageView -import com.airbnb.epoxy.EpoxyAttribute -import com.airbnb.epoxy.EpoxyModelClass -import com.bumptech.glide.Glide -import com.bumptech.glide.request.RequestOptions -import im.vector.matrix.android.api.session.content.ContentAttachmentData -import im.vector.riotx.R -import im.vector.riotx.core.epoxy.VectorEpoxyHolder -import im.vector.riotx.core.epoxy.VectorEpoxyModel -import kotlinx.android.synthetic.main.item_attachment_preview.view.* - -@EpoxyModelClass(layout = R.layout.item_attachment_preview) -abstract class AttachmentPreviewItem : VectorEpoxyModel() { - - @EpoxyAttribute lateinit var attachment: ContentAttachmentData - @EpoxyAttribute var clickListener: View.OnClickListener? = null - - override fun bind(holder: Holder) { - holder.view.setOnClickListener(clickListener) - // If name is empty, use userId as name and force it being centered - val mimeType = attachment.mimeType - val path = attachment.path - if (mimeType != null && (mimeType.startsWith("image") || mimeType.startsWith("video"))) { - Glide.with(holder.view.context) - .asBitmap() - .load(path) - .apply(RequestOptions().frame(0)) - .into(holder.imageView) - } else { - holder.imageView.attachmentPreviewImageView.setImageResource(R.drawable.filetype_attachment) - holder.imageView.attachmentPreviewImageView.scaleType = ImageView.ScaleType.FIT_CENTER - } - } - - class Holder : VectorEpoxyHolder() { - val imageView by bind(R.id.attachmentPreviewImageView) - } -} diff --git a/vector/src/main/java/im/vector/riotx/features/attachments/preview/AttachmentPreviewItems.kt b/vector/src/main/java/im/vector/riotx/features/attachments/preview/AttachmentPreviewItems.kt new file mode 100644 index 0000000000..24d7b312ec --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/attachments/preview/AttachmentPreviewItems.kt @@ -0,0 +1,82 @@ +/* + * Copyright 2020 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.riotx.features.attachments.preview + +import android.view.View +import android.widget.ImageView +import com.airbnb.epoxy.EpoxyAttribute +import com.airbnb.epoxy.EpoxyModelClass +import com.bumptech.glide.Glide +import com.bumptech.glide.request.RequestOptions +import im.vector.matrix.android.api.session.content.ContentAttachmentData +import im.vector.riotx.R +import im.vector.riotx.core.epoxy.VectorEpoxyHolder +import im.vector.riotx.core.epoxy.VectorEpoxyModel + +abstract class AttachmentPreviewItem : VectorEpoxyModel() { + + abstract val attachment: ContentAttachmentData + + override fun bind(holder: H) { + val path = attachment.path + if (attachment.type == ContentAttachmentData.Type.VIDEO || attachment.type == ContentAttachmentData.Type.IMAGE) { + Glide.with(holder.view.context) + .asBitmap() + .load(path) + .apply(RequestOptions().frame(0)) + .into(holder.imageView) + } else { + holder.imageView.setImageResource(R.drawable.filetype_attachment) + holder.imageView.scaleType = ImageView.ScaleType.FIT_CENTER + } + } + + abstract class Holder : VectorEpoxyHolder() { + abstract val imageView: ImageView + } +} + +@EpoxyModelClass(layout = R.layout.item_attachment_miniature_preview) +abstract class AttachmentMiniaturePreviewItem : AttachmentPreviewItem() { + + @EpoxyAttribute override lateinit var attachment: ContentAttachmentData + @EpoxyAttribute + var clickListener: View.OnClickListener? = null + + override fun bind(holder: Holder) { + super.bind(holder) + holder.view.setOnClickListener(clickListener) + } + + class Holder : AttachmentPreviewItem.Holder() { + override val imageView: ImageView + get() = miniatureImageView + private val miniatureImageView by bind(R.id.attachmentMiniatureImageView) + } +} + +@EpoxyModelClass(layout = R.layout.item_attachment_big_preview) +abstract class AttachmentBigPreviewItem : AttachmentPreviewItem() { + + @EpoxyAttribute override lateinit var attachment: ContentAttachmentData + + class Holder : AttachmentPreviewItem.Holder() { + override val imageView: ImageView + get() = bigImageView + private val bigImageView by bind(R.id.attachmentBigImageView) + } +} diff --git a/vector/src/main/java/im/vector/riotx/features/attachments/preview/AttachmentsPreviewActivity.kt b/vector/src/main/java/im/vector/riotx/features/attachments/preview/AttachmentsPreviewActivity.kt index 509be251c3..d8d19cd550 100644 --- a/vector/src/main/java/im/vector/riotx/features/attachments/preview/AttachmentsPreviewActivity.kt +++ b/vector/src/main/java/im/vector/riotx/features/attachments/preview/AttachmentsPreviewActivity.kt @@ -19,6 +19,8 @@ package im.vector.riotx.features.attachments.preview import android.content.Context import android.content.Intent +import android.os.Bundle +import android.view.WindowManager import androidx.appcompat.widget.Toolbar import im.vector.riotx.R import im.vector.riotx.core.extensions.addFragment @@ -30,9 +32,10 @@ class AttachmentsPreviewActivity : VectorBaseActivity(), ToolbarConfigurable { companion object { private const val EXTRA_FRAGMENT_ARGS = "EXTRA_FRAGMENT_ARGS" + const val REQUEST_CODE = 55 fun newIntent(context: Context, args: AttachmentsPreviewArgs): Intent { - return Intent(context, AttachmentsPreviewArgs::class.java).apply { + return Intent(context, AttachmentsPreviewActivity::class.java).apply { putExtra(EXTRA_FRAGMENT_ARGS, args) } } @@ -40,6 +43,11 @@ class AttachmentsPreviewActivity : VectorBaseActivity(), ToolbarConfigurable { override fun getLayoutRes() = R.layout.activity_simple + override fun onCreate(savedInstanceState: Bundle?) { + window.setFlags(WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS, WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS) + super.onCreate(savedInstanceState) + } + override fun initUiAndData() { if (isFirstCreation()) { val fragmentArgs: AttachmentsPreviewArgs = intent?.extras?.getParcelable(EXTRA_FRAGMENT_ARGS) diff --git a/vector/src/main/java/im/vector/riotx/features/attachments/preview/AttachmentsPreviewFragment.kt b/vector/src/main/java/im/vector/riotx/features/attachments/preview/AttachmentsPreviewFragment.kt index 6e99758c6d..99280c3830 100644 --- a/vector/src/main/java/im/vector/riotx/features/attachments/preview/AttachmentsPreviewFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/attachments/preview/AttachmentsPreviewFragment.kt @@ -20,13 +20,14 @@ package im.vector.riotx.features.attachments.preview import android.os.Bundle import android.os.Parcelable import android.view.View +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.PagerSnapHelper import com.airbnb.mvrx.args import com.airbnb.mvrx.fragmentViewModel import com.airbnb.mvrx.withState import im.vector.matrix.android.api.session.content.ContentAttachmentData import im.vector.riotx.R import im.vector.riotx.core.extensions.cleanup -import im.vector.riotx.core.extensions.configureWith import im.vector.riotx.core.platform.VectorBaseFragment import kotlinx.android.parcel.Parcelize import kotlinx.android.synthetic.main.fragment_attachments_preview.* @@ -39,8 +40,9 @@ data class AttachmentsPreviewArgs( class AttachmentsPreviewFragment @Inject constructor( val viewModelFactory: AttachmentsPreviewViewModel.Factory, - private val attachmentPreviewController: AttachmentPreviewController -) : VectorBaseFragment() { + private val attachmentMiniaturePreviewController: AttachmentMiniaturePreviewController, + private val attachmentBigPreviewController: AttachmentBigPreviewController +) : VectorBaseFragment(), AttachmentMiniaturePreviewController.Callback { private val fragmentArgs: AttachmentsPreviewArgs by args() private val viewModel: AttachmentsPreviewViewModel by fragmentViewModel() @@ -49,19 +51,36 @@ class AttachmentsPreviewFragment @Inject constructor( override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - setupRecyclerView() + setupRecyclerViews() + setupToolbar(attachmentPreviewerToolbar) } override fun onDestroyView() { super.onDestroyView() - attachmentPreviewerList.cleanup() + attachmentPreviewerMiniatureList.cleanup() } - private fun setupRecyclerView() { - attachmentPreviewerList.configureWith(attachmentPreviewController, hasFixedSize = true) + private fun setupRecyclerViews() { + attachmentPreviewerMiniatureList.layoutManager = LinearLayoutManager(context, LinearLayoutManager.HORIZONTAL, false) + attachmentPreviewerMiniatureList.setHasFixedSize(true) + attachmentPreviewerMiniatureList.adapter = attachmentMiniaturePreviewController.adapter + attachmentMiniaturePreviewController.callback = this + + attachmentPreviewerBigList.layoutManager = LinearLayoutManager(context, LinearLayoutManager.HORIZONTAL, false) + val snapHelper = PagerSnapHelper() + snapHelper.attachToRecyclerView(attachmentPreviewerBigList) + attachmentPreviewerBigList.setHasFixedSize(true) + attachmentPreviewerBigList.adapter = attachmentBigPreviewController.adapter + } override fun invalidate() = withState(viewModel) { state -> + attachmentMiniaturePreviewController.setData(state) + attachmentBigPreviewController.setData(state) + } + + override fun onAttachmentClicked(contentAttachmentData: ContentAttachmentData) { } + } diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt index 5cb3024712..71ab8f2852 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt @@ -116,6 +116,8 @@ import im.vector.riotx.core.utils.toast import im.vector.riotx.features.attachments.AttachmentTypeSelectorView import im.vector.riotx.features.attachments.AttachmentsHelper import im.vector.riotx.features.attachments.ContactAttachment +import im.vector.riotx.features.attachments.preview.AttachmentsPreviewActivity +import im.vector.riotx.features.attachments.preview.AttachmentsPreviewArgs import im.vector.riotx.features.command.Command import im.vector.riotx.features.crypto.util.toImageRes import im.vector.riotx.features.crypto.verification.VerificationBottomSheet @@ -1337,7 +1339,8 @@ class RoomDetailFragment @Inject constructor( // AttachmentsHelper.Callback override fun onContentAttachmentsReady(attachments: List) { - roomDetailViewModel.handle(RoomDetailAction.SendMedia(attachments)) + val intent = AttachmentsPreviewActivity.newIntent(requireContext(), AttachmentsPreviewArgs(attachments)) + startActivityForResult(intent, AttachmentsPreviewActivity.REQUEST_CODE) } override fun onAttachmentsProcessFailed() { diff --git a/vector/src/main/res/layout/fragment_attachments_preview.xml b/vector/src/main/res/layout/fragment_attachments_preview.xml index 60d311b96d..5acc8835b0 100644 --- a/vector/src/main/res/layout/fragment_attachments_preview.xml +++ b/vector/src/main/res/layout/fragment_attachments_preview.xml @@ -1,57 +1,32 @@ - + android:layout_height="match_parent" + xmlns:tools="http://schemas.android.com/tools"> - + + - - - - - - - - - - - - - - - - + diff --git a/vector/src/main/res/layout/item_attachment_big_preview.xml b/vector/src/main/res/layout/item_attachment_big_preview.xml new file mode 100644 index 0000000000..3705965520 --- /dev/null +++ b/vector/src/main/res/layout/item_attachment_big_preview.xml @@ -0,0 +1,12 @@ + + + + + + \ No newline at end of file diff --git a/vector/src/main/res/layout/item_attachment_preview.xml b/vector/src/main/res/layout/item_attachment_miniature_preview.xml similarity index 67% rename from vector/src/main/res/layout/item_attachment_preview.xml rename to vector/src/main/res/layout/item_attachment_miniature_preview.xml index 3c769d17d9..23dfcf381b 100644 --- a/vector/src/main/res/layout/item_attachment_preview.xml +++ b/vector/src/main/res/layout/item_attachment_miniature_preview.xml @@ -1,19 +1,18 @@ - - \ No newline at end of file + \ No newline at end of file From 64717872327339e9d44752dad3319dd645e587d5 Mon Sep 17 00:00:00 2001 From: ganfra Date: Wed, 5 Feb 2020 17:54:40 +0100 Subject: [PATCH 03/33] Share: start managing multi selection and warning --- vector/src/main/AndroidManifest.xml | 8 +++- .../home/room/detail/RoomDetailFragment.kt | 5 ++- .../features/home/room/list/RoomListAction.kt | 5 ++- .../home/room/list/RoomListFragment.kt | 38 +++++++++++++------ .../home/room/list/RoomListViewEvents.kt | 3 +- .../home/room/list/RoomListViewModel.kt | 31 ++++++++++++++- .../home/room/list/RoomListViewState.kt | 4 +- .../home/room/list/RoomSummaryController.kt | 15 +++++--- .../home/room/list/RoomSummaryItem.kt | 23 ++++++++++- .../home/room/list/RoomSummaryItemFactory.kt | 9 +++-- .../features/share/IncomingShareActivity.kt | 1 + .../res/layout/activity_incoming_share.xml | 1 - .../main/res/layout/fragment_room_list.xml | 13 +++++++ vector/src/main/res/layout/item_room.xml | 34 ++++++++++++----- vector/src/main/res/values/strings_riotX.xml | 1 + 15 files changed, 151 insertions(+), 40 deletions(-) diff --git a/vector/src/main/AndroidManifest.xml b/vector/src/main/AndroidManifest.xml index f908880e6f..d1873b650e 100644 --- a/vector/src/main/AndroidManifest.xml +++ b/vector/src/main/AndroidManifest.xml @@ -88,7 +88,13 @@ - + + + diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt index 71ab8f2852..1caada8ce6 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt @@ -154,6 +154,7 @@ import io.reactivex.android.schedulers.AndroidSchedulers import io.reactivex.schedulers.Schedulers import kotlinx.android.parcel.Parcelize import kotlinx.android.synthetic.main.fragment_room_detail.* +import kotlinx.android.synthetic.main.merge_composer_layout.* import kotlinx.android.synthetic.main.merge_composer_layout.view.* import kotlinx.android.synthetic.main.merge_overlay_waiting_view.* import org.commonmark.parser.Parser @@ -300,7 +301,9 @@ class RoomDetailFragment @Inject constructor( super.onActivityCreated(savedInstanceState) if (savedInstanceState == null) { when (val sharedData = roomDetailArgs.sharedData) { - is SharedData.Text -> roomDetailViewModel.handle(RoomDetailAction.SendMessage(sharedData.text, false)) + is SharedData.Text -> { + roomDetailViewModel.handle(RoomDetailAction.ExitSpecialMode(composerLayout.text.toString())) + } is SharedData.Attachments -> roomDetailViewModel.handle(RoomDetailAction.SendMedia(sharedData.attachmentData)) null -> Timber.v("No share data to process") } diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomListAction.kt b/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomListAction.kt index 9db7374169..801bb48aed 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomListAction.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomListAction.kt @@ -16,17 +16,20 @@ package im.vector.riotx.features.home.room.list +import im.vector.matrix.android.api.session.content.ContentAttachmentData import im.vector.matrix.android.api.session.room.model.RoomSummary import im.vector.matrix.android.api.session.room.notification.RoomNotificationState import im.vector.riotx.core.platform.VectorViewModelAction +import im.vector.riotx.features.share.SharedData sealed class RoomListAction : VectorViewModelAction { - data class SelectRoom(val roomSummary: RoomSummary) : RoomListAction() + data class SelectRoom(val roomSummary: RoomSummary, val enableMultiSelect: Boolean) : RoomListAction() data class ToggleCategory(val category: RoomCategory) : RoomListAction() data class AcceptInvitation(val roomSummary: RoomSummary) : RoomListAction() data class RejectInvitation(val roomSummary: RoomSummary) : RoomListAction() data class FilterWith(val filter: String) : RoomListAction() data class ChangeRoomNotificationState(val roomId: String, val notificationState: RoomNotificationState) : RoomListAction() data class LeaveRoom(val roomId: String) : RoomListAction() + data class ShareToSelectedRooms(val sharedData: SharedData, val optionalMessage: String? = null): RoomListAction() object MarkAllRoomsRead : RoomListAction() } diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomListFragment.kt b/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomListFragment.kt index 5a32f4b8b3..8bb8eeb75c 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomListFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomListFragment.kt @@ -106,10 +106,15 @@ class RoomListFragment @Inject constructor( when (it) { is RoomListViewEvents.Loading -> showLoading(it.message) is RoomListViewEvents.Failure -> showFailure(it.throwable) - is RoomListViewEvents.SelectRoom -> openSelectedRoom(it) + is RoomListViewEvents.SelectRoom -> handleSelectRoom(it) }.exhaustive } + sendShareButton.setOnClickListener { _ -> + roomListViewModel.handle(RoomListAction.ShareToSelectedRooms(roomListParams.sharedData!!)) + requireActivity().finish() + } + createChatFabMenu.listener = this sharedActionViewModel @@ -131,12 +136,19 @@ class RoomListFragment @Inject constructor( super.onDestroyView() } - private fun openSelectedRoom(event: RoomListViewEvents.SelectRoom) { + private fun handleSelectRoom(event: RoomListViewEvents.SelectRoom) { if (roomListParams.displayMode == RoomListDisplayMode.SHARE) { val sharedData = roomListParams.sharedData ?: return - navigator.openRoomForSharing(requireActivity(), event.roomId, sharedData) + AlertDialog.Builder(requireActivity()) + .setTitle(R.string.send_attachment) + .setMessage(getString(R.string.share_confirm_room, event.roomSummary.displayName)) + .setPositiveButton(R.string.send) { _, _ -> + navigator.openRoomForSharing(requireActivity(), event.roomSummary.roomId, sharedData) + } + .setNegativeButton(R.string.cancel, null) + .show() } else { - navigator.openRoom(requireActivity(), event.roomId) + navigator.openRoom(requireActivity(), event.roomSummary.roomId) } } @@ -256,7 +268,7 @@ class RoomListFragment @Inject constructor( is Fail -> renderFailure(state.asyncFilteredRooms.error) } roomController.update(state) - + sendShareButton.isVisible = state.multiSelectionEnabled // Mark all as read menu when (roomListParams.displayMode) { RoomListDisplayMode.HOME, @@ -338,22 +350,24 @@ class RoomListFragment @Inject constructor( if (createChatFabMenu.onBackPressed()) { return true } - return false } // RoomSummaryController.Callback ************************************************************** override fun onRoomClicked(room: RoomSummary) { - roomListViewModel.handle(RoomListAction.SelectRoom(room)) + roomListViewModel.handle(RoomListAction.SelectRoom(room, enableMultiSelect = false)) } override fun onRoomLongClicked(room: RoomSummary): Boolean { - roomController.onRoomLongClicked() - - RoomListQuickActionsBottomSheet - .newInstance(room.roomId, RoomListActionsArgs.Mode.FULL) - .show(childFragmentManager, "ROOM_LIST_QUICK_ACTIONS") + if (roomListParams.displayMode == RoomListDisplayMode.SHARE) { + roomListViewModel.handle(RoomListAction.SelectRoom(room, enableMultiSelect = true)) + } else { + roomController.onRoomLongClicked() + RoomListQuickActionsBottomSheet + .newInstance(room.roomId, RoomListActionsArgs.Mode.FULL) + .show(childFragmentManager, "ROOM_LIST_QUICK_ACTIONS") + } return true } diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomListViewEvents.kt b/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomListViewEvents.kt index 2e147293ec..1798174ef0 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomListViewEvents.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomListViewEvents.kt @@ -17,6 +17,7 @@ package im.vector.riotx.features.home.room.list +import im.vector.matrix.android.api.session.room.model.RoomSummary import im.vector.riotx.core.platform.VectorViewEvents /** @@ -26,5 +27,5 @@ sealed class RoomListViewEvents : VectorViewEvents { data class Loading(val message: CharSequence? = null) : RoomListViewEvents() data class Failure(val throwable: Throwable) : RoomListViewEvents() - data class SelectRoom(val roomId: String) : RoomListViewEvents() + data class SelectRoom(val roomSummary: RoomSummary) : RoomListViewEvents() } diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomListViewModel.kt b/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomListViewModel.kt index 22c18e9134..7ee641236c 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomListViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomListViewModel.kt @@ -25,9 +25,11 @@ import im.vector.matrix.android.api.session.Session import im.vector.matrix.android.api.session.room.model.Membership import im.vector.matrix.android.api.session.room.model.RoomSummary import im.vector.matrix.android.api.session.room.model.tag.RoomTag +import im.vector.riotx.core.extensions.exhaustive import im.vector.riotx.core.platform.VectorViewModel import im.vector.riotx.core.utils.DataSource import im.vector.riotx.features.home.RoomListDisplayMode +import im.vector.riotx.features.share.SharedData import io.reactivex.schedulers.Schedulers import timber.log.Timber import javax.inject.Inject @@ -67,13 +69,38 @@ class RoomListViewModel @Inject constructor(initialState: RoomListViewState, is RoomListAction.MarkAllRoomsRead -> handleMarkAllRoomsRead() is RoomListAction.LeaveRoom -> handleLeaveRoom(action) is RoomListAction.ChangeRoomNotificationState -> handleChangeNotificationMode(action) + is RoomListAction.ShareToSelectedRooms -> handleShareToSelectedRooms(action) + }.exhaustive + } + + private fun handleShareToSelectedRooms(action: RoomListAction.ShareToSelectedRooms) = withState { + val sharedData = action.sharedData + it.selectedRoomIds.forEach { roomId -> + val room = session.getRoom(roomId) + if (sharedData is SharedData.Text) { + room?.sendTextMessage(sharedData.text) + } else if (sharedData is SharedData.Attachments) { + room?.sendMedias(sharedData.attachmentData) + } } } // PRIVATE METHODS ***************************************************************************** - private fun handleSelectRoom(action: RoomListAction.SelectRoom) { - _viewEvents.post(RoomListViewEvents.SelectRoom(action.roomSummary.roomId)) + private fun handleSelectRoom(action: RoomListAction.SelectRoom) = withState { + if (it.multiSelectionEnabled) { + val selectedRooms = it.selectedRoomIds + val newSelectedRooms = if (selectedRooms.contains(action.roomSummary.roomId)) { + selectedRooms.minus(action.roomSummary.roomId) + } else { + selectedRooms.plus(action.roomSummary.roomId) + } + setState { copy(multiSelectionEnabled = newSelectedRooms.isNotEmpty(), selectedRoomIds = newSelectedRooms) } + } else if (action.enableMultiSelect) { + setState { copy(multiSelectionEnabled = true, selectedRoomIds = setOf(action.roomSummary.roomId)) } + } else { + _viewEvents.post(RoomListViewEvents.SelectRoom(action.roomSummary)) + } } private fun handleToggleCategory(action: RoomListAction.ToggleCategory) = setState { diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomListViewState.kt b/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomListViewState.kt index c127fa10e2..e660cc47c0 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomListViewState.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomListViewState.kt @@ -46,7 +46,9 @@ data class RoomListViewState( val isServerNoticeRoomsExpanded: Boolean = true, // For sharing val isRecentExpanded: Boolean = true, - val isOtherExpanded: Boolean = true + val isOtherExpanded: Boolean = true, + val selectedRoomIds: Set = emptySet(), + val multiSelectionEnabled: Boolean = false ) : MvRxState { constructor(args: RoomListParams) : this(displayMode = args.displayMode) diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomSummaryController.kt b/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomSummaryController.kt index c4afd442ab..eb05802d6e 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomSummaryController.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomSummaryController.kt @@ -77,7 +77,8 @@ class RoomSummaryController @Inject constructor(private val stringProvider: Stri viewState.joiningRoomsIds, viewState.joiningErrorRoomsIds, viewState.rejectingRoomsIds, - viewState.rejectingErrorRoomsIds) + viewState.rejectingErrorRoomsIds, + viewState.selectedRoomIds) addFilterFooter(viewState) } @@ -85,7 +86,6 @@ class RoomSummaryController @Inject constructor(private val stringProvider: Stri private fun buildShareRooms(viewState: RoomListViewState) { var hasResult = false val roomSummaries = viewState.asyncFilteredRooms() - roomListNameFilter.filter = viewState.roomFilter roomSummaries?.forEach { (category, summaries) -> @@ -105,7 +105,8 @@ class RoomSummaryController @Inject constructor(private val stringProvider: Stri emptySet(), emptySet(), emptySet(), - emptySet() + emptySet(), + viewState.selectedRoomIds ) } } @@ -131,7 +132,8 @@ class RoomSummaryController @Inject constructor(private val stringProvider: Stri viewState.joiningRoomsIds, viewState.joiningErrorRoomsIds, viewState.rejectingRoomsIds, - viewState.rejectingErrorRoomsIds) + viewState.rejectingErrorRoomsIds, + viewState.selectedRoomIds) // Never set showHelp to true for invitation if (category != RoomCategory.INVITE) { showHelp = userPreferencesProvider.shouldShowLongClickOnRoomHelp() @@ -196,10 +198,11 @@ class RoomSummaryController @Inject constructor(private val stringProvider: Stri joiningRoomsIds: Set, joiningErrorRoomsIds: Set, rejectingRoomsIds: Set, - rejectingErrorRoomsIds: Set) { + rejectingErrorRoomsIds: Set, + selectedRoomIds: Set) { summaries.forEach { roomSummary -> roomSummaryItemFactory - .create(roomSummary, joiningRoomsIds, joiningErrorRoomsIds, rejectingRoomsIds, rejectingErrorRoomsIds, listener) + .create(roomSummary, joiningRoomsIds, joiningErrorRoomsIds, rejectingRoomsIds, rejectingErrorRoomsIds,selectedRoomIds, listener) .addTo(this) } } diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomSummaryItem.kt b/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomSummaryItem.kt index 652740c0b7..3d4ce6b7ff 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomSummaryItem.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomSummaryItem.kt @@ -16,14 +16,17 @@ package im.vector.riotx.features.home.room.list +import android.view.HapticFeedbackConstants import android.view.View import android.view.ViewGroup import android.widget.ImageView import android.widget.TextView +import androidx.core.content.ContextCompat import androidx.core.view.isInvisible import androidx.core.view.isVisible import com.airbnb.epoxy.EpoxyAttribute import com.airbnb.epoxy.EpoxyModelClass +import com.amulyakhare.textdrawable.TextDrawable import im.vector.matrix.android.api.crypto.RoomEncryptionTrustLevel import im.vector.matrix.android.api.util.MatrixItem import im.vector.riotx.R @@ -48,11 +51,15 @@ abstract class RoomSummaryItem : VectorEpoxyModel() { @EpoxyAttribute var showHighlighted: Boolean = false @EpoxyAttribute var itemLongClickListener: View.OnLongClickListener? = null @EpoxyAttribute var itemClickListener: View.OnClickListener? = null + @EpoxyAttribute var showSelected: Boolean = false override fun bind(holder: Holder) { super.bind(holder) holder.rootView.setOnClickListener(itemClickListener) - holder.rootView.setOnLongClickListener(itemLongClickListener) + holder.rootView.setOnLongClickListener { + it.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS); + itemLongClickListener?.onLongClick(it) ?: false + } holder.titleView.text = matrixItem.getBestName() holder.lastEventTimeView.text = lastEventTime holder.lastEventView.text = lastFormattedEvent @@ -64,6 +71,19 @@ abstract class RoomSummaryItem : VectorEpoxyModel() { avatarRenderer.render(matrixItem, holder.avatarImageView) holder.roomAvatarDecorationImageView.isVisible = encryptionTrustLevel != null holder.roomAvatarDecorationImageView.setImageResource(encryptionTrustLevel.toImageRes()) + renderSelection(holder, showSelected) + } + + private fun renderSelection(holder: Holder, isSelected: Boolean) { + if (isSelected) { + holder.avatarCheckedImageView.visibility = View.VISIBLE + val backgroundColor = ContextCompat.getColor(holder.view.context, R.color.riotx_accent) + val backgroundDrawable = TextDrawable.builder().buildRound("", backgroundColor) + holder.avatarImageView.setImageDrawable(backgroundDrawable) + } else { + holder.avatarCheckedImageView.visibility = View.GONE + avatarRenderer.render(matrixItem, holder.avatarImageView) + } } class Holder : VectorEpoxyHolder() { @@ -74,6 +94,7 @@ abstract class RoomSummaryItem : VectorEpoxyModel() { val typingView by bind(R.id.roomTypingView) val draftView by bind(R.id.roomDraftBadge) val lastEventTimeView by bind(R.id.roomLastEventTimeView) + val avatarCheckedImageView by bind(R.id.roomAvatarCheckedImageView) val avatarImageView by bind(R.id.roomAvatarImageView) val roomAvatarDecorationImageView by bind(R.id.roomAvatarDecorationImageView) val rootView by bind(R.id.itemRoomLayout) diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomSummaryItemFactory.kt b/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomSummaryItemFactory.kt index d224ccec47..b61f8fcbe5 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomSummaryItemFactory.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomSummaryItemFactory.kt @@ -36,7 +36,6 @@ import javax.inject.Inject class RoomSummaryItemFactory @Inject constructor(private val displayableEventFormatter: DisplayableEventFormatter, private val dateFormatter: VectorDateFormatter, - private val colorProvider: ColorProvider, private val stringProvider: StringProvider, private val typingHelper: TypingHelper, private val session: Session, @@ -47,10 +46,11 @@ class RoomSummaryItemFactory @Inject constructor(private val displayableEventFor joiningErrorRoomsIds: Set, rejectingRoomsIds: Set, rejectingErrorRoomsIds: Set, + selectedRoomIds: Set, listener: RoomSummaryController.Listener?): VectorEpoxyModel<*> { return when (roomSummary.membership) { Membership.INVITE -> createInvitationItem(roomSummary, joiningRoomsIds, joiningErrorRoomsIds, rejectingRoomsIds, rejectingErrorRoomsIds, listener) - else -> createRoomItem(roomSummary, listener) + else -> createRoomItem(roomSummary, selectedRoomIds, listener) } } @@ -82,10 +82,10 @@ class RoomSummaryItemFactory @Inject constructor(private val displayableEventFor .listener { listener?.onRoomClicked(roomSummary) } } - private fun createRoomItem(roomSummary: RoomSummary, listener: RoomSummaryController.Listener?): VectorEpoxyModel<*> { + private fun createRoomItem(roomSummary: RoomSummary, selectedRoomIds: Set, listener: RoomSummaryController.Listener?): VectorEpoxyModel<*> { val unreadCount = roomSummary.notificationCount val showHighlighted = roomSummary.highlightCount > 0 - + val showSelected = selectedRoomIds.contains(roomSummary.roomId) var latestFormattedEvent: CharSequence = "" var latestEventTime: CharSequence = "" val latestEvent = roomSummary.latestPreviewableEvent @@ -119,6 +119,7 @@ class RoomSummaryItemFactory @Inject constructor(private val displayableEventFor .typingString(typingString) .lastFormattedEvent(latestFormattedEvent) .showHighlighted(showHighlighted) + .showSelected(showSelected) .unreadNotificationCount(unreadCount) .hasUnreadMessage(roomSummary.hasUnreadMessages) .hasDraft(roomSummary.userDrafts.isNotEmpty()) diff --git a/vector/src/main/java/im/vector/riotx/features/share/IncomingShareActivity.kt b/vector/src/main/java/im/vector/riotx/features/share/IncomingShareActivity.kt index 3669a51937..b9a9bdc103 100644 --- a/vector/src/main/java/im/vector/riotx/features/share/IncomingShareActivity.kt +++ b/vector/src/main/java/im/vector/riotx/features/share/IncomingShareActivity.kt @@ -19,6 +19,7 @@ package im.vector.riotx.features.share import android.content.ClipDescription import android.content.Intent import android.os.Bundle +import android.view.Window import android.widget.Toast import androidx.appcompat.widget.SearchView import com.airbnb.mvrx.viewModel diff --git a/vector/src/main/res/layout/activity_incoming_share.xml b/vector/src/main/res/layout/activity_incoming_share.xml index 986a852b5a..031e1b958e 100644 --- a/vector/src/main/res/layout/activity_incoming_share.xml +++ b/vector/src/main/res/layout/activity_incoming_share.xml @@ -13,7 +13,6 @@ style="@style/VectorToolbarStyle" android:layout_width="0dp" android:layout_height="?attr/actionBarSize" - android:elevation="4dp" app:contentInsetStart="0dp" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" diff --git a/vector/src/main/res/layout/fragment_room_list.xml b/vector/src/main/res/layout/fragment_room_list.xml index 2c828c8397..54e6db72e7 100644 --- a/vector/src/main/res/layout/fragment_room_list.xml +++ b/vector/src/main/res/layout/fragment_room_list.xml @@ -55,4 +55,17 @@ tools:layout_marginEnd="144dp" tools:visibility="visible" /> + + diff --git a/vector/src/main/res/layout/item_room.xml b/vector/src/main/res/layout/item_room.xml index 3ae16ddca9..05a2ff4ca9 100644 --- a/vector/src/main/res/layout/item_room.xml +++ b/vector/src/main/res/layout/item_room.xml @@ -21,21 +21,37 @@ app:layout_constraintTop_toTopOf="parent" tools:visibility="visible" /> - + app:layout_constraintTop_toTopOf="parent"> + + + + + + diff --git a/vector/src/main/res/values/strings_riotX.xml b/vector/src/main/res/values/strings_riotX.xml index 75790348de..8799d56ce6 100644 --- a/vector/src/main/res/values/strings_riotX.xml +++ b/vector/src/main/res/values/strings_riotX.xml @@ -21,6 +21,7 @@ Remove… + Do you want to send this attachment to %1$s? From b7a7aa2f153ed9d48844d560562cd7b76ff9ec9a Mon Sep 17 00:00:00 2001 From: Ganard Date: Thu, 6 Feb 2020 18:42:18 +0100 Subject: [PATCH 04/33] Attachment: continue working on preview screen --- .../session/content/ContentAttachmentData.kt | 1 + vector/src/main/AndroidManifest.xml | 5 +- .../riotx/core/utils/SnapHelperUtils.kt | 76 +++++++++++ .../features/attachments/AttachmentsMapper.kt | 12 +- .../attachments/ContentAttachmentData.kt | 32 +++++ .../preview/AttachmentPreviewControllers.kt | 10 +- .../preview/AttachmentsPreviewAction.kt | 8 +- .../preview/AttachmentsPreviewActivity.kt | 16 ++- .../preview/AttachmentsPreviewFragment.kt | 129 ++++++++++++++++-- .../preview/AttachmentsPreviewViewModel.kt | 35 ++++- .../preview/AttachmentsPreviewViewState.kt | 3 +- .../home/room/detail/RoomDetailFragment.kt | 19 ++- .../features/themes/ActivityOtherThemes.kt | 6 + .../layout/fragment_attachments_preview.xml | 42 ++++-- .../res/menu/vector_attachments_preview.xml | 19 +++ .../src/main/res/values-v21/theme_common.xml | 16 +++ vector/src/main/res/values/theme_common.xml | 2 + 17 files changed, 385 insertions(+), 46 deletions(-) create mode 100644 vector/src/main/java/im/vector/riotx/core/utils/SnapHelperUtils.kt create mode 100644 vector/src/main/java/im/vector/riotx/features/attachments/ContentAttachmentData.kt create mode 100755 vector/src/main/res/menu/vector_attachments_preview.xml create mode 100644 vector/src/main/res/values-v21/theme_common.xml diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/content/ContentAttachmentData.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/content/ContentAttachmentData.kt index 0d8ef2c52b..48dff4e56d 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/content/ContentAttachmentData.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/content/ContentAttachmentData.kt @@ -29,6 +29,7 @@ data class ContentAttachmentData( val width: Long? = 0, val exifOrientation: Int = ExifInterface.ORIENTATION_UNDEFINED, val name: String? = null, + val queryUri: String, val path: String, val mimeType: String?, val type: Type diff --git a/vector/src/main/AndroidManifest.xml b/vector/src/main/AndroidManifest.xml index d1873b650e..9d7495ef23 100644 --- a/vector/src/main/AndroidManifest.xml +++ b/vector/src/main/AndroidManifest.xml @@ -143,7 +143,10 @@ - + + diff --git a/vector/src/main/java/im/vector/riotx/core/utils/SnapHelperUtils.kt b/vector/src/main/java/im/vector/riotx/core/utils/SnapHelperUtils.kt new file mode 100644 index 0000000000..70831730b4 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/core/utils/SnapHelperUtils.kt @@ -0,0 +1,76 @@ +/* + * Copyright (c) 2020 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.riotx.core.utils + +import androidx.recyclerview.widget.RecyclerView +import androidx.recyclerview.widget.SnapHelper + +interface OnSnapPositionChangeListener { + + fun onSnapPositionChange(position: Int) +} + +fun RecyclerView.attachSnapHelperWithListener( + snapHelper: SnapHelper, + behavior: SnapOnScrollListener.Behavior = SnapOnScrollListener.Behavior.NOTIFY_ON_SCROLL_STATE_IDLE, + onSnapPositionChangeListener: OnSnapPositionChangeListener) { + snapHelper.attachToRecyclerView(this) + val snapOnScrollListener = SnapOnScrollListener(snapHelper, behavior, onSnapPositionChangeListener) + addOnScrollListener(snapOnScrollListener) +} + +fun SnapHelper.getSnapPosition(recyclerView: RecyclerView): Int { + val layoutManager = recyclerView.layoutManager ?: return RecyclerView.NO_POSITION + val snapView = findSnapView(layoutManager) ?: return RecyclerView.NO_POSITION + return layoutManager.getPosition(snapView) +} + +class SnapOnScrollListener( + private val snapHelper: SnapHelper, + var behavior: Behavior = Behavior.NOTIFY_ON_SCROLL, + var onSnapPositionChangeListener: OnSnapPositionChangeListener? = null +) : RecyclerView.OnScrollListener() { + + enum class Behavior { + NOTIFY_ON_SCROLL, + NOTIFY_ON_SCROLL_STATE_IDLE + } + + private var snapPosition = RecyclerView.NO_POSITION + + override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) { + if (behavior == Behavior.NOTIFY_ON_SCROLL) { + maybeNotifySnapPositionChange(recyclerView) + } + } + + override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) { + if (behavior == Behavior.NOTIFY_ON_SCROLL_STATE_IDLE + && newState == RecyclerView.SCROLL_STATE_IDLE) { + maybeNotifySnapPositionChange(recyclerView) + } + } + + private fun maybeNotifySnapPositionChange(recyclerView: RecyclerView) { + val snapPosition = snapHelper.getSnapPosition(recyclerView) + val snapPositionChanged = this.snapPosition != snapPosition + if (snapPositionChanged) { + onSnapPositionChangeListener?.onSnapPositionChange(snapPosition) + this.snapPosition = snapPosition + } + } +} diff --git a/vector/src/main/java/im/vector/riotx/features/attachments/AttachmentsMapper.kt b/vector/src/main/java/im/vector/riotx/features/attachments/AttachmentsMapper.kt index 4b51c548a7..c0009a7e49 100644 --- a/vector/src/main/java/im/vector/riotx/features/attachments/AttachmentsMapper.kt +++ b/vector/src/main/java/im/vector/riotx/features/attachments/AttachmentsMapper.kt @@ -37,7 +37,8 @@ fun ChosenFile.toContentAttachmentData(): ContentAttachmentData { type = mapType(), size = size, date = createdAt?.time ?: System.currentTimeMillis(), - name = displayName + name = displayName, + queryUri = queryUri ) } @@ -50,7 +51,8 @@ fun ChosenAudio.toContentAttachmentData(): ContentAttachmentData { size = size, date = createdAt?.time ?: System.currentTimeMillis(), name = displayName, - duration = duration + duration = duration, + queryUri = queryUri ) } @@ -74,7 +76,8 @@ fun ChosenImage.toContentAttachmentData(): ContentAttachmentData { height = height.toLong(), width = width.toLong(), exifOrientation = orientation, - date = createdAt?.time ?: System.currentTimeMillis() + date = createdAt?.time ?: System.currentTimeMillis(), + queryUri = queryUri ) } @@ -89,6 +92,7 @@ fun ChosenVideo.toContentAttachmentData(): ContentAttachmentData { height = height.toLong(), width = width.toLong(), duration = duration, - name = displayName + name = displayName, + queryUri = queryUri ) } diff --git a/vector/src/main/java/im/vector/riotx/features/attachments/ContentAttachmentData.kt b/vector/src/main/java/im/vector/riotx/features/attachments/ContentAttachmentData.kt new file mode 100644 index 0000000000..ecd979f900 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/attachments/ContentAttachmentData.kt @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2020 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.riotx.features.attachments + +import im.vector.matrix.android.api.session.content.ContentAttachmentData + +fun ContentAttachmentData.isPreviewable(): Boolean { + return type == ContentAttachmentData.Type.IMAGE || type == ContentAttachmentData.Type.VIDEO +} + +fun List.filterPreviewables(): List { + return filter { it.isPreviewable() } +} + +fun List.filterNonPreviewables(): List { + return filter { it.isPreviewable().not() } +} + diff --git a/vector/src/main/java/im/vector/riotx/features/attachments/preview/AttachmentPreviewControllers.kt b/vector/src/main/java/im/vector/riotx/features/attachments/preview/AttachmentPreviewControllers.kt index 9cef33d402..966bfe26cd 100644 --- a/vector/src/main/java/im/vector/riotx/features/attachments/preview/AttachmentPreviewControllers.kt +++ b/vector/src/main/java/im/vector/riotx/features/attachments/preview/AttachmentPreviewControllers.kt @@ -35,18 +35,18 @@ class AttachmentBigPreviewController @Inject constructor() : TypedEpoxyControlle class AttachmentMiniaturePreviewController @Inject constructor() : TypedEpoxyController() { interface Callback { - fun onAttachmentClicked(contentAttachmentData: ContentAttachmentData) + fun onAttachmentClicked(position: Int, contentAttachmentData: ContentAttachmentData) } var callback: Callback? = null override fun buildModels(data: AttachmentsPreviewViewState) { - data.attachments.forEach { + data.attachments.forEachIndexed { index, contentAttachmentData -> attachmentMiniaturePreviewItem { - id(it.path) - attachment(it) + id(contentAttachmentData.path) + attachment(contentAttachmentData) clickListener { _ -> - callback?.onAttachmentClicked(it) + callback?.onAttachmentClicked(index, contentAttachmentData) } } } diff --git a/vector/src/main/java/im/vector/riotx/features/attachments/preview/AttachmentsPreviewAction.kt b/vector/src/main/java/im/vector/riotx/features/attachments/preview/AttachmentsPreviewAction.kt index 61fb41a774..a016b71fab 100644 --- a/vector/src/main/java/im/vector/riotx/features/attachments/preview/AttachmentsPreviewAction.kt +++ b/vector/src/main/java/im/vector/riotx/features/attachments/preview/AttachmentsPreviewAction.kt @@ -19,4 +19,10 @@ package im.vector.riotx.features.attachments.preview import im.vector.riotx.core.platform.VectorViewModelAction -sealed class AttachmentsPreviewAction : VectorViewModelAction +sealed class AttachmentsPreviewAction : VectorViewModelAction { + object RemoveCurrentAttachment : AttachmentsPreviewAction() + data class SetCurrentAttachment(val index: Int): AttachmentsPreviewAction() + data class UpdatePathOfCurrentAttachment(val newPath: String): AttachmentsPreviewAction() + + +} diff --git a/vector/src/main/java/im/vector/riotx/features/attachments/preview/AttachmentsPreviewActivity.kt b/vector/src/main/java/im/vector/riotx/features/attachments/preview/AttachmentsPreviewActivity.kt index d8d19cd550..70dc0734ed 100644 --- a/vector/src/main/java/im/vector/riotx/features/attachments/preview/AttachmentsPreviewActivity.kt +++ b/vector/src/main/java/im/vector/riotx/features/attachments/preview/AttachmentsPreviewActivity.kt @@ -19,19 +19,20 @@ package im.vector.riotx.features.attachments.preview import android.content.Context import android.content.Intent -import android.os.Bundle -import android.view.WindowManager import androidx.appcompat.widget.Toolbar +import im.vector.matrix.android.api.session.content.ContentAttachmentData import im.vector.riotx.R import im.vector.riotx.core.extensions.addFragment import im.vector.riotx.core.platform.ToolbarConfigurable import im.vector.riotx.core.platform.VectorBaseActivity +import im.vector.riotx.features.themes.ActivityOtherThemes class AttachmentsPreviewActivity : VectorBaseActivity(), ToolbarConfigurable { companion object { private const val EXTRA_FRAGMENT_ARGS = "EXTRA_FRAGMENT_ARGS" + const val RESULT_NAME = "ATTACHMENTS_PREVIEW_RESULT" const val REQUEST_CODE = 55 fun newIntent(context: Context, args: AttachmentsPreviewArgs): Intent { @@ -39,15 +40,16 @@ class AttachmentsPreviewActivity : VectorBaseActivity(), ToolbarConfigurable { putExtra(EXTRA_FRAGMENT_ARGS, args) } } + + fun getOutput(intent: Intent): List { + return intent.getParcelableArrayListExtra(RESULT_NAME) + } } + override fun getOtherThemes() = ActivityOtherThemes.AttachmentsPreview + override fun getLayoutRes() = R.layout.activity_simple - override fun onCreate(savedInstanceState: Bundle?) { - window.setFlags(WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS, WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS) - super.onCreate(savedInstanceState) - } - override fun initUiAndData() { if (isFirstCreation()) { val fragmentArgs: AttachmentsPreviewArgs = intent?.extras?.getParcelable(EXTRA_FRAGMENT_ARGS) diff --git a/vector/src/main/java/im/vector/riotx/features/attachments/preview/AttachmentsPreviewFragment.kt b/vector/src/main/java/im/vector/riotx/features/attachments/preview/AttachmentsPreviewFragment.kt index 99280c3830..e13e7c6b60 100644 --- a/vector/src/main/java/im/vector/riotx/features/attachments/preview/AttachmentsPreviewFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/attachments/preview/AttachmentsPreviewFragment.kt @@ -17,24 +17,41 @@ package im.vector.riotx.features.attachments.preview +import android.app.Activity.RESULT_CANCELED +import android.app.Activity.RESULT_OK +import android.content.Intent import android.os.Bundle import android.os.Parcelable +import android.view.MenuItem import android.view.View +import android.view.ViewGroup +import android.widget.Toast +import androidx.core.net.toUri +import androidx.core.view.ViewCompat +import androidx.core.view.updateLayoutParams +import androidx.core.view.updatePadding import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.PagerSnapHelper import com.airbnb.mvrx.args import com.airbnb.mvrx.fragmentViewModel import com.airbnb.mvrx.withState +import com.yalantis.ucrop.UCrop import im.vector.matrix.android.api.session.content.ContentAttachmentData import im.vector.riotx.R import im.vector.riotx.core.extensions.cleanup import im.vector.riotx.core.platform.VectorBaseFragment +import im.vector.riotx.core.utils.OnSnapPositionChangeListener +import im.vector.riotx.core.utils.SnapOnScrollListener +import im.vector.riotx.core.utils.attachSnapHelperWithListener import kotlinx.android.parcel.Parcelize import kotlinx.android.synthetic.main.fragment_attachments_preview.* +import timber.log.Timber +import java.io.File import javax.inject.Inject @Parcelize data class AttachmentsPreviewArgs( + val roomId: String, val attachments: List ) : Parcelable @@ -51,13 +68,107 @@ class AttachmentsPreviewFragment @Inject constructor( override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) + applyInsets() setupRecyclerViews() setupToolbar(attachmentPreviewerToolbar) + attachmentPreviewerSendButton.setOnClickListener { + setResultAndFinish() + } } + override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { + if (resultCode == RESULT_OK) { + if (requestCode == UCrop.REQUEST_CROP && data != null) { + Timber.v("Crop success") + handleCropResult(data) + } + } + if (resultCode == UCrop.RESULT_ERROR) { + Timber.v("Crop error") + } + } + + override fun onOptionsItemSelected(item: MenuItem): Boolean { + return when (item.itemId) { + R.id.attachmentsPreviewRemoveAction -> { + handleRemoveAction() + true + } + R.id.attachmentsPreviewEditAction -> { + handleEditAction() + true + } + else -> { + super.onOptionsItemSelected(item) + } + } + } + + override fun getMenuRes() = R.menu.vector_attachments_preview + override fun onDestroyView() { super.onDestroyView() attachmentPreviewerMiniatureList.cleanup() + attachmentPreviewerBigList.cleanup() + } + + override fun invalidate() = withState(viewModel) { state -> + if (state.attachments.isEmpty()) { + requireActivity().setResult(RESULT_CANCELED) + requireActivity().finish() + } else { + attachmentMiniaturePreviewController.setData(state) + attachmentBigPreviewController.setData(state) + attachmentPreviewerBigList.scrollToPosition(state.currentAttachmentIndex) + attachmentPreviewerMiniatureList.scrollToPosition(state.currentAttachmentIndex) + } + } + + override fun onAttachmentClicked(position: Int, contentAttachmentData: ContentAttachmentData) { + viewModel.handle(AttachmentsPreviewAction.SetCurrentAttachment(position)) + } + + private fun setResultAndFinish() = withState(viewModel) { + val resultIntent = Intent().apply { + putParcelableArrayListExtra(AttachmentsPreviewActivity.RESULT_NAME, ArrayList(it.attachments)) + } + requireActivity().setResult(RESULT_OK, resultIntent) + requireActivity().finish() + } + + private fun applyInsets() { + view?.systemUiVisibility = View.SYSTEM_UI_FLAG_LAYOUT_STABLE or View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION + ViewCompat.setOnApplyWindowInsetsListener(attachmentPreviewerBottomContainer) { v, insets -> + v.updatePadding(bottom = insets.systemWindowInsetBottom) + insets + } + ViewCompat.setOnApplyWindowInsetsListener(attachmentPreviewerToolbar) { v, insets -> + v.updateLayoutParams { + topMargin = insets.systemWindowInsetTop + } + insets + } + } + + private fun handleCropResult(result: Intent) { + val resultPath = UCrop.getOutput(result)?.path + if (resultPath != null) { + viewModel.handle(AttachmentsPreviewAction.UpdatePathOfCurrentAttachment(resultPath)) + } else { + Toast.makeText(requireContext(), "Cannot retrieve cropped value", Toast.LENGTH_SHORT).show() + } + } + + private fun handleRemoveAction() { + viewModel.handle(AttachmentsPreviewAction.RemoveCurrentAttachment) + } + + private fun handleEditAction() = withState(viewModel) { + val currentAttachment = it.attachments.getOrNull(it.currentAttachmentIndex) ?: return@withState + val destinationFile = File(requireContext().cacheDir, "${currentAttachment.name}_edited_image_${System.currentTimeMillis()}") + UCrop.of(currentAttachment.queryUri.toUri(), destinationFile.toUri()) + .withOptions(UCrop.Options()) + .start(requireContext(), this) } private fun setupRecyclerViews() { @@ -67,20 +178,12 @@ class AttachmentsPreviewFragment @Inject constructor( attachmentMiniaturePreviewController.callback = this attachmentPreviewerBigList.layoutManager = LinearLayoutManager(context, LinearLayoutManager.HORIZONTAL, false) - val snapHelper = PagerSnapHelper() - snapHelper.attachToRecyclerView(attachmentPreviewerBigList) + attachmentPreviewerBigList.attachSnapHelperWithListener(PagerSnapHelper(), SnapOnScrollListener.Behavior.NOTIFY_ON_SCROLL_STATE_IDLE, object : OnSnapPositionChangeListener { + override fun onSnapPositionChange(position: Int) { + viewModel.handle(AttachmentsPreviewAction.SetCurrentAttachment(position)) + } + }) attachmentPreviewerBigList.setHasFixedSize(true) attachmentPreviewerBigList.adapter = attachmentBigPreviewController.adapter - } - - override fun invalidate() = withState(viewModel) { state -> - attachmentMiniaturePreviewController.setData(state) - attachmentBigPreviewController.setData(state) - } - - override fun onAttachmentClicked(contentAttachmentData: ContentAttachmentData) { - - } - } diff --git a/vector/src/main/java/im/vector/riotx/features/attachments/preview/AttachmentsPreviewViewModel.kt b/vector/src/main/java/im/vector/riotx/features/attachments/preview/AttachmentsPreviewViewModel.kt index 28a0aaebd6..b3be853eea 100644 --- a/vector/src/main/java/im/vector/riotx/features/attachments/preview/AttachmentsPreviewViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/attachments/preview/AttachmentsPreviewViewModel.kt @@ -17,12 +17,15 @@ package im.vector.riotx.features.attachments.preview +import androidx.lifecycle.viewModelScope import com.airbnb.mvrx.FragmentViewModelContext import com.airbnb.mvrx.MvRxViewModelFactory import com.airbnb.mvrx.ViewModelContext import com.squareup.inject.assisted.Assisted import com.squareup.inject.assisted.AssistedInject +import im.vector.riotx.core.extensions.exhaustive import im.vector.riotx.core.platform.VectorViewModel +import kotlinx.coroutines.launch class AttachmentsPreviewViewModel @AssistedInject constructor(@Assisted initialState: AttachmentsPreviewViewState) : VectorViewModel(initialState) { @@ -42,6 +45,36 @@ class AttachmentsPreviewViewModel @AssistedInject constructor(@Assisted initialS } override fun handle(action: AttachmentsPreviewAction) { - //TODO + when (action) { + is AttachmentsPreviewAction.SetCurrentAttachment -> handleSetCurrentAttachment(action) + is AttachmentsPreviewAction.UpdatePathOfCurrentAttachment -> handleUpdatePathOfCurrentAttachment(action) + AttachmentsPreviewAction.RemoveCurrentAttachment -> handleRemoveCurrentAttachment() + }.exhaustive + } + + private fun handleRemoveCurrentAttachment() = withState { + val currentAttachment = it.attachments.getOrNull(it.currentAttachmentIndex) ?: return@withState + val attachments = it.attachments.minusElement(currentAttachment) + val newAttachmentIndex = it.currentAttachmentIndex.coerceAtMost(attachments.size - 1) + setState { + copy(attachments = attachments, currentAttachmentIndex = newAttachmentIndex) + } + } + + private fun handleUpdatePathOfCurrentAttachment(action: AttachmentsPreviewAction.UpdatePathOfCurrentAttachment) = withState { + val attachments = it.attachments.mapIndexed { index, contentAttachmentData -> + if (index == it.currentAttachmentIndex) { + contentAttachmentData.copy(path = action.newPath) + } else { + contentAttachmentData + } + } + setState { + copy(attachments = attachments) + } + } + + private fun handleSetCurrentAttachment(action: AttachmentsPreviewAction.SetCurrentAttachment) = setState { + copy(currentAttachmentIndex = action.index) } } diff --git a/vector/src/main/java/im/vector/riotx/features/attachments/preview/AttachmentsPreviewViewState.kt b/vector/src/main/java/im/vector/riotx/features/attachments/preview/AttachmentsPreviewViewState.kt index f92abb97ea..feb1a32210 100644 --- a/vector/src/main/java/im/vector/riotx/features/attachments/preview/AttachmentsPreviewViewState.kt +++ b/vector/src/main/java/im/vector/riotx/features/attachments/preview/AttachmentsPreviewViewState.kt @@ -21,7 +21,8 @@ import com.airbnb.mvrx.MvRxState import im.vector.matrix.android.api.session.content.ContentAttachmentData data class AttachmentsPreviewViewState( - val attachments: List + val attachments: List, + val currentAttachmentIndex: Int = 0 ) : MvRxState { constructor(args: AttachmentsPreviewArgs) : this(attachments = args.attachments) diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt index 1caada8ce6..f63f82ca26 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt @@ -116,6 +116,8 @@ import im.vector.riotx.core.utils.toast import im.vector.riotx.features.attachments.AttachmentTypeSelectorView import im.vector.riotx.features.attachments.AttachmentsHelper import im.vector.riotx.features.attachments.ContactAttachment +import im.vector.riotx.features.attachments.filterNonPreviewables +import im.vector.riotx.features.attachments.filterPreviewables import im.vector.riotx.features.attachments.preview.AttachmentsPreviewActivity import im.vector.riotx.features.attachments.preview.AttachmentsPreviewArgs import im.vector.riotx.features.command.Command @@ -502,7 +504,11 @@ class RoomDetailFragment @Inject constructor( val hasBeenHandled = attachmentsHelper.onActivityResult(requestCode, resultCode, data) if (!hasBeenHandled && resultCode == RESULT_OK && data != null) { when (requestCode) { - REACTION_SELECT_REQUEST_CODE -> { + AttachmentsPreviewActivity.REQUEST_CODE -> { + val sendData = AttachmentsPreviewActivity.getOutput(data) + roomDetailViewModel.handle(RoomDetailAction.SendMedia(sendData)) + } + REACTION_SELECT_REQUEST_CODE -> { val (eventId, reaction) = EmojiReactionPickerActivity.getOutput(data) ?: return roomDetailViewModel.handle(RoomDetailAction.SendReaction(eventId, reaction)) } @@ -1342,8 +1348,15 @@ class RoomDetailFragment @Inject constructor( // AttachmentsHelper.Callback override fun onContentAttachmentsReady(attachments: List) { - val intent = AttachmentsPreviewActivity.newIntent(requireContext(), AttachmentsPreviewArgs(attachments)) - startActivityForResult(intent, AttachmentsPreviewActivity.REQUEST_CODE) + val previewable = attachments.filterPreviewables() + val nonPreviewable = attachments.filterNonPreviewables() + if (nonPreviewable.isNotEmpty()) { + roomDetailViewModel.handle(RoomDetailAction.SendMedia(nonPreviewable)) + } + if (previewable.isNotEmpty()) { + val intent = AttachmentsPreviewActivity.newIntent(requireContext(), AttachmentsPreviewArgs(roomDetailArgs.roomId, previewable)) + startActivityForResult(intent, AttachmentsPreviewActivity.REQUEST_CODE) + } } override fun onAttachmentsProcessFailed() { diff --git a/vector/src/main/java/im/vector/riotx/features/themes/ActivityOtherThemes.kt b/vector/src/main/java/im/vector/riotx/features/themes/ActivityOtherThemes.kt index 411069e9ca..b37c1a4818 100644 --- a/vector/src/main/java/im/vector/riotx/features/themes/ActivityOtherThemes.kt +++ b/vector/src/main/java/im/vector/riotx/features/themes/ActivityOtherThemes.kt @@ -32,4 +32,10 @@ sealed class ActivityOtherThemes(@StyleRes val dark: Int, R.style.AppTheme_Black, R.style.AppTheme_Status ) + + object AttachmentsPreview : ActivityOtherThemes( + R.style.AppTheme_AttachmentsPreview, + R.style.AppTheme_AttachmentsPreview, + R.style.AppTheme_AttachmentsPreview + ) } diff --git a/vector/src/main/res/layout/fragment_attachments_preview.xml b/vector/src/main/res/layout/fragment_attachments_preview.xml index 5acc8835b0..bcb5d682c4 100644 --- a/vector/src/main/res/layout/fragment_attachments_preview.xml +++ b/vector/src/main/res/layout/fragment_attachments_preview.xml @@ -1,37 +1,59 @@ + android:layout_height="match_parent"> + app:layout_constraintTop_toTopOf="parent" + tools:listitem="@layout/item_attachment_big_preview" /> - + app:layout_constraintStart_toStartOf="parent"> + + + + + + diff --git a/vector/src/main/res/menu/vector_attachments_preview.xml b/vector/src/main/res/menu/vector_attachments_preview.xml new file mode 100755 index 0000000000..4adf9dcff9 --- /dev/null +++ b/vector/src/main/res/menu/vector_attachments_preview.xml @@ -0,0 +1,19 @@ + + + + + + + + diff --git a/vector/src/main/res/values-v21/theme_common.xml b/vector/src/main/res/values-v21/theme_common.xml new file mode 100644 index 0000000000..155c73acdc --- /dev/null +++ b/vector/src/main/res/values-v21/theme_common.xml @@ -0,0 +1,16 @@ + + + + + + + + + \ No newline at end of file diff --git a/vector/src/main/res/values/theme_common.xml b/vector/src/main/res/values/theme_common.xml index 159b2ecb55..151d97c097 100644 --- a/vector/src/main/res/values/theme_common.xml +++ b/vector/src/main/res/values/theme_common.xml @@ -8,4 +8,6 @@ @color/primary_color_dark + -