From 1dacfa6744142e90a4df1eae73e0fd52a54ed782 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 10 Oct 2019 14:36:10 +0200 Subject: [PATCH] Rework message menu bottom sheet: remove sub Fragment and use Epoxy - Also move some class to some dedicated package --- .../vector/riotx/core/di/ScreenComponent.kt | 8 +- .../VectorBaseBottomSheetDialogFragment.kt | 3 +- .../home/room/detail/RoomDetailFragment.kt | 6 +- .../DisplayReadReceiptsBottomSheet.kt | 2 +- .../timeline/action/BottomSheetItemAction.kt | 55 ++++ .../action/BottomSheetItemMessagePreview.kt | 59 ++++ .../action/BottomSheetItemQuickReactions.kt | 80 +++++ .../action/BottomSheetItemSendState.kt | 52 +++ .../action/BottomSheetItemSeparator.kt | 27 ++ .../action/MessageActionsBottomSheet.kt | 98 +----- .../action/MessageActionsEpoxyController.kt | 104 ++++++ .../action/MessageActionsViewModel.kt | 297 +++++++++++++++--- .../timeline/action/MessageMenuFragment.kt | 104 ------ .../timeline/action/MessageMenuViewModel.kt | 279 ---------------- .../timeline/action/QuickReactionFragment.kt | 89 ------ .../timeline/action/QuickReactionViewModel.kt | 96 ------ .../detail/timeline/action/SimpleAction.kt | 43 +++ .../ViewEditHistoryBottomSheet.kt | 4 +- .../ViewEditHistoryEpoxyController.kt | 4 +- .../ViewEditHistoryViewModel.kt | 3 +- .../ReactionInfoSimpleItem.kt | 2 +- .../ViewReactionsBottomSheet.kt} | 14 +- .../ViewReactionsEpoxyController.kt | 2 +- .../ViewReactionsViewModel.kt} | 19 +- .../bottom_sheet_epoxylist_with_title.xml | 3 +- .../bottom_sheet_generic_recycler_epoxy.xml | 22 ++ .../layout/bottom_sheet_message_actions.xml | 149 --------- .../main/res/layout/fragment_message_menu.xml | 5 - ...ction.xml => item_bottom_sheet_action.xml} | 2 +- .../res/layout/item_bottom_sheet_divider.xml | 6 + .../item_bottom_sheet_message_preview.xml | 78 +++++ .../item_bottom_sheet_message_status.xml | 33 ++ ...l => item_bottom_sheet_quick_reaction.xml} | 0 33 files changed, 872 insertions(+), 876 deletions(-) rename vector/src/main/java/im/vector/riotx/{features/home/room/detail/timeline/action => core/platform}/VectorBaseBottomSheetDialogFragment.kt (95%) create mode 100644 vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/BottomSheetItemAction.kt create mode 100644 vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/BottomSheetItemMessagePreview.kt create mode 100644 vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/BottomSheetItemQuickReactions.kt create mode 100644 vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/BottomSheetItemSendState.kt create mode 100644 vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/BottomSheetItemSeparator.kt create mode 100644 vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/MessageActionsEpoxyController.kt delete mode 100644 vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/MessageMenuFragment.kt delete mode 100644 vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/MessageMenuViewModel.kt delete mode 100644 vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/QuickReactionFragment.kt delete mode 100644 vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/QuickReactionViewModel.kt create mode 100644 vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/SimpleAction.kt rename vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/{action => edithistory}/ViewEditHistoryBottomSheet.kt (93%) rename vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/{action => edithistory}/ViewEditHistoryEpoxyController.kt (98%) rename vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/{action => edithistory}/ViewEditHistoryViewModel.kt (96%) rename vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/{action => reactions}/ReactionInfoSimpleItem.kt (96%) rename vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/{action/ViewReactionBottomSheet.kt => reactions/ViewReactionsBottomSheet.kt} (82%) rename vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/{action => reactions}/ViewReactionsEpoxyController.kt (96%) rename vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/{action/ViewReactionViewModel.kt => reactions/ViewReactionsViewModel.kt} (83%) create mode 100644 vector/src/main/res/layout/bottom_sheet_generic_recycler_epoxy.xml delete mode 100644 vector/src/main/res/layout/bottom_sheet_message_actions.xml delete mode 100644 vector/src/main/res/layout/fragment_message_menu.xml rename vector/src/main/res/layout/{adapter_item_action.xml => item_bottom_sheet_action.xml} (97%) create mode 100644 vector/src/main/res/layout/item_bottom_sheet_divider.xml create mode 100644 vector/src/main/res/layout/item_bottom_sheet_message_preview.xml create mode 100644 vector/src/main/res/layout/item_bottom_sheet_message_status.xml rename vector/src/main/res/layout/{adapter_item_action_quick_reaction.xml => item_bottom_sheet_quick_reaction.xml} (100%) diff --git a/vector/src/main/java/im/vector/riotx/core/di/ScreenComponent.kt b/vector/src/main/java/im/vector/riotx/core/di/ScreenComponent.kt index 3b18d3042e..2cbc3d2a8b 100644 --- a/vector/src/main/java/im/vector/riotx/core/di/ScreenComponent.kt +++ b/vector/src/main/java/im/vector/riotx/core/di/ScreenComponent.kt @@ -42,6 +42,8 @@ import im.vector.riotx.features.home.group.GroupListFragment import im.vector.riotx.features.home.room.detail.RoomDetailFragment import im.vector.riotx.features.home.room.detail.readreceipts.DisplayReadReceiptsBottomSheet import im.vector.riotx.features.home.room.detail.timeline.action.* +import im.vector.riotx.features.home.room.detail.timeline.edithistory.ViewEditHistoryBottomSheet +import im.vector.riotx.features.home.room.detail.timeline.reactions.ViewReactionsBottomSheet import im.vector.riotx.features.home.room.filtered.FilteredRoomsActivity import im.vector.riotx.features.home.room.list.RoomListFragment import im.vector.riotx.features.invite.VectorInviteView @@ -103,12 +105,10 @@ interface ScreenComponent { fun inject(messageActionsBottomSheet: MessageActionsBottomSheet) - fun inject(viewReactionBottomSheet: ViewReactionBottomSheet) + fun inject(viewReactionsBottomSheet: ViewReactionsBottomSheet) fun inject(viewEditHistoryBottomSheet: ViewEditHistoryBottomSheet) - fun inject(messageMenuFragment: MessageMenuFragment) - fun inject(vectorSettingsActivity: VectorSettingsActivity) fun inject(createRoomFragment: CreateRoomFragment) @@ -135,8 +135,6 @@ interface ScreenComponent { fun inject(sasVerificationIncomingFragment: SASVerificationIncomingFragment) - fun inject(quickReactionFragment: QuickReactionFragment) - fun inject(emojiReactionPickerActivity: EmojiReactionPickerActivity) fun inject(loginActivity: LoginActivity) diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/VectorBaseBottomSheetDialogFragment.kt b/vector/src/main/java/im/vector/riotx/core/platform/VectorBaseBottomSheetDialogFragment.kt similarity index 95% rename from vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/VectorBaseBottomSheetDialogFragment.kt rename to vector/src/main/java/im/vector/riotx/core/platform/VectorBaseBottomSheetDialogFragment.kt index 026cb3ba1c..892f7b0daa 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/VectorBaseBottomSheetDialogFragment.kt +++ b/vector/src/main/java/im/vector/riotx/core/platform/VectorBaseBottomSheetDialogFragment.kt @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package im.vector.riotx.features.home.room.detail.timeline.action +package im.vector.riotx.core.platform import android.content.Context import android.os.Bundle @@ -24,7 +24,6 @@ import com.airbnb.mvrx.MvRxViewModelStore import com.google.android.material.bottomsheet.BottomSheetDialogFragment import im.vector.riotx.core.di.DaggerScreenComponent import im.vector.riotx.core.di.ScreenComponent -import im.vector.riotx.core.platform.VectorBaseActivity import java.util.* /** 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 f09fbff09f..7447bf92e6 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 @@ -94,7 +94,9 @@ import im.vector.riotx.features.home.room.detail.composer.TextComposerViewState import im.vector.riotx.features.home.room.detail.readreceipts.DisplayReadReceiptsBottomSheet import im.vector.riotx.features.home.room.detail.timeline.TimelineEventController import im.vector.riotx.features.home.room.detail.timeline.action.* +import im.vector.riotx.features.home.room.detail.timeline.edithistory.ViewEditHistoryBottomSheet import im.vector.riotx.features.home.room.detail.timeline.item.* +import im.vector.riotx.features.home.room.detail.timeline.reactions.ViewReactionsBottomSheet import im.vector.riotx.features.html.EventHtmlRenderer import im.vector.riotx.features.html.PillImageSpan import im.vector.riotx.features.invite.VectorInviteView @@ -917,7 +919,7 @@ class RoomDetailFragment : } override fun onLongClickOnReactionPill(informationData: MessageInformationData, reaction: String) { - ViewReactionBottomSheet.newInstance(roomDetailArgs.roomId, informationData) + ViewReactionsBottomSheet.newInstance(roomDetailArgs.roomId, informationData) .show(requireActivity().supportFragmentManager, "DISPLAY_REACTIONS") } @@ -970,7 +972,7 @@ class RoomDetailFragment : startActivityForResult(EmojiReactionPickerActivity.intent(requireContext(), action.eventId), REACTION_SELECT_REQUEST_CODE) } is SimpleAction.ViewReactions -> { - ViewReactionBottomSheet.newInstance(roomDetailArgs.roomId, action.messageInformationData) + ViewReactionsBottomSheet.newInstance(roomDetailArgs.roomId, action.messageInformationData) .show(requireActivity().supportFragmentManager, "DISPLAY_REACTIONS") } is SimpleAction.Copy -> { diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/readreceipts/DisplayReadReceiptsBottomSheet.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/readreceipts/DisplayReadReceiptsBottomSheet.kt index 510fc1e26f..80539d73e4 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/readreceipts/DisplayReadReceiptsBottomSheet.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/readreceipts/DisplayReadReceiptsBottomSheet.kt @@ -30,7 +30,7 @@ import com.airbnb.mvrx.MvRx import com.airbnb.mvrx.args import im.vector.riotx.R import im.vector.riotx.core.di.ScreenComponent -import im.vector.riotx.features.home.room.detail.timeline.action.VectorBaseBottomSheetDialogFragment +import im.vector.riotx.core.platform.VectorBaseBottomSheetDialogFragment import im.vector.riotx.features.home.room.detail.timeline.item.ReadReceiptData import kotlinx.android.parcel.Parcelize import kotlinx.android.synthetic.main.bottom_sheet_epoxylist_with_title.* diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/BottomSheetItemAction.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/BottomSheetItemAction.kt new file mode 100644 index 0000000000..8ee7460d53 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/BottomSheetItemAction.kt @@ -0,0 +1,55 @@ +/* + * Copyright 2019 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.home.room.detail.timeline.action + +import android.view.View +import android.widget.ImageView +import android.widget.TextView +import androidx.annotation.DrawableRes +import com.airbnb.epoxy.EpoxyAttribute +import com.airbnb.epoxy.EpoxyModelClass +import im.vector.riotx.R +import im.vector.riotx.core.epoxy.VectorEpoxyHolder +import im.vector.riotx.core.epoxy.VectorEpoxyModel + +/** + * A action for bottom sheet. + */ +@EpoxyModelClass(layout = R.layout.item_bottom_sheet_action) +abstract class BottomSheetItemAction : VectorEpoxyModel() { + + @EpoxyAttribute + @DrawableRes + var iconRes: Int = 0 + @EpoxyAttribute + var textRes: Int = 0 + @EpoxyAttribute + lateinit var listener: View.OnClickListener + + override fun bind(holder: Holder) { + holder.view.setOnClickListener { + listener.onClick(it) + } + + holder.icon.setImageResource(iconRes) + holder.text.setText(textRes) + } + + class Holder : VectorEpoxyHolder() { + val icon by bind(R.id.action_icon) + val text by bind(R.id.action_title) + } +} diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/BottomSheetItemMessagePreview.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/BottomSheetItemMessagePreview.kt new file mode 100644 index 0000000000..d37aa43770 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/BottomSheetItemMessagePreview.kt @@ -0,0 +1,59 @@ +/* + * Copyright 2019 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.home.room.detail.timeline.action + +import android.widget.ImageView +import android.widget.TextView +import com.airbnb.epoxy.EpoxyAttribute +import com.airbnb.epoxy.EpoxyModelClass +import im.vector.riotx.R +import im.vector.riotx.core.epoxy.VectorEpoxyHolder +import im.vector.riotx.core.epoxy.VectorEpoxyModel +import im.vector.riotx.core.extensions.setTextOrHide +import im.vector.riotx.features.home.AvatarRenderer +import im.vector.riotx.features.home.room.detail.timeline.item.MessageInformationData + +/** + * A message preview for bottom sheet. + */ +@EpoxyModelClass(layout = R.layout.item_bottom_sheet_message_preview) +abstract class BottomSheetItemMessagePreview : VectorEpoxyModel() { + + @EpoxyAttribute + lateinit var avatarRenderer: AvatarRenderer + @EpoxyAttribute + lateinit var informationData: MessageInformationData + @EpoxyAttribute + var senderName: String? = null + @EpoxyAttribute + lateinit var body: CharSequence + @EpoxyAttribute + var time: CharSequence? = null + + override fun bind(holder: Holder) { + avatarRenderer.render(informationData.avatarUrl, informationData.senderId, senderName, holder.avatar) + holder.sender.setTextOrHide(senderName) + holder.body.text = body + holder.timestamp.setTextOrHide(time) + } + + class Holder : VectorEpoxyHolder() { + val avatar by bind(R.id.bottom_sheet_message_preview_avatar) + val sender by bind(R.id.bottom_sheet_message_preview_sender) + val body by bind(R.id.bottom_sheet_message_preview_body) + val timestamp by bind(R.id.bottom_sheet_message_preview_timestamp) + } +} diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/BottomSheetItemQuickReactions.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/BottomSheetItemQuickReactions.kt new file mode 100644 index 0000000000..3aafa7c974 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/BottomSheetItemQuickReactions.kt @@ -0,0 +1,80 @@ +/* + * Copyright 2019 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.home.room.detail.timeline.action + +import android.graphics.Typeface +import android.widget.TextView +import com.airbnb.epoxy.EpoxyAttribute +import com.airbnb.epoxy.EpoxyModelClass +import im.vector.riotx.EmojiCompatFontProvider +import im.vector.riotx.R +import im.vector.riotx.core.epoxy.VectorEpoxyHolder +import im.vector.riotx.core.epoxy.VectorEpoxyModel + +/** + * A quick reaction list for bottom sheet. + */ +@EpoxyModelClass(layout = R.layout.item_bottom_sheet_quick_reaction) +abstract class BottomSheetItemQuickReactions : VectorEpoxyModel() { + + @EpoxyAttribute + lateinit var fontProvider: EmojiCompatFontProvider + @EpoxyAttribute + lateinit var texts: List + @EpoxyAttribute + lateinit var selecteds: List + @EpoxyAttribute + var listener: Listener? = null + + override fun bind(holder: Holder) { + holder.textViews.forEachIndexed { index, textView -> + textView.typeface = fontProvider.typeface ?: Typeface.DEFAULT + textView.text = texts[index] + textView.alpha = if (selecteds[index]) 0.2f else 1f + + textView.setOnClickListener { + listener?.didSelect(texts[index], !selecteds[index]) + } + } + } + + class Holder : VectorEpoxyHolder() { + private val quickReaction0 by bind(R.id.quickReaction0) + private val quickReaction1 by bind(R.id.quickReaction1) + private val quickReaction2 by bind(R.id.quickReaction2) + private val quickReaction3 by bind(R.id.quickReaction3) + private val quickReaction4 by bind(R.id.quickReaction4) + private val quickReaction5 by bind(R.id.quickReaction5) + private val quickReaction6 by bind(R.id.quickReaction6) + private val quickReaction7 by bind(R.id.quickReaction7) + + val textViews + get() = listOf( + quickReaction0, + quickReaction1, + quickReaction2, + quickReaction3, + quickReaction4, + quickReaction5, + quickReaction6, + quickReaction7 + ) + } + + interface Listener { + fun didSelect(emoji: String, selected: Boolean) + } +} diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/BottomSheetItemSendState.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/BottomSheetItemSendState.kt new file mode 100644 index 0000000000..86a5512349 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/BottomSheetItemSendState.kt @@ -0,0 +1,52 @@ +/* + * Copyright 2019 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.home.room.detail.timeline.action + +import android.view.View +import android.widget.TextView +import androidx.annotation.DrawableRes +import androidx.core.view.isVisible +import com.airbnb.epoxy.EpoxyAttribute +import com.airbnb.epoxy.EpoxyModelClass +import im.vector.riotx.R +import im.vector.riotx.core.epoxy.VectorEpoxyHolder +import im.vector.riotx.core.epoxy.VectorEpoxyModel + +/** + * A send state for bottom sheet. + */ +@EpoxyModelClass(layout = R.layout.item_bottom_sheet_message_status) +abstract class BottomSheetItemSendState : VectorEpoxyModel() { + + @EpoxyAttribute + var showProgress: Boolean = false + @EpoxyAttribute + lateinit var text: CharSequence + @EpoxyAttribute + @DrawableRes + var drawableStart: Int = 0 + + override fun bind(holder: Holder) { + holder.progress.isVisible = showProgress + holder.text.setCompoundDrawablesWithIntrinsicBounds(drawableStart, 0, 0, 0) + holder.text.text = text + } + + class Holder : VectorEpoxyHolder() { + val progress by bind(R.id.messageStatusProgress) + val text by bind(R.id.messageStatusText) + } +} diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/BottomSheetItemSeparator.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/BottomSheetItemSeparator.kt new file mode 100644 index 0000000000..f09f68b714 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/BottomSheetItemSeparator.kt @@ -0,0 +1,27 @@ +/* + * Copyright 2019 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.home.room.detail.timeline.action + +import com.airbnb.epoxy.EpoxyModelClass +import im.vector.riotx.R +import im.vector.riotx.core.epoxy.VectorEpoxyHolder +import im.vector.riotx.core.epoxy.VectorEpoxyModel + +@EpoxyModelClass(layout = R.layout.item_bottom_sheet_divider) +abstract class BottomSheetItemSeparator : VectorEpoxyModel() { + + class Holder : VectorEpoxyHolder() +} diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/MessageActionsBottomSheet.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/MessageActionsBottomSheet.kt index 11f3207e32..39116b59b5 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/MessageActionsBottomSheet.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/MessageActionsBottomSheet.kt @@ -21,92 +21,53 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.widget.FrameLayout -import android.widget.ImageView -import android.widget.TextView -import androidx.core.view.isVisible import androidx.lifecycle.ViewModelProviders -import butterknife.BindView -import butterknife.ButterKnife -import com.airbnb.mvrx.MvRx import com.airbnb.mvrx.fragmentViewModel import com.airbnb.mvrx.withState import com.google.android.material.bottomsheet.BottomSheetBehavior import com.google.android.material.bottomsheet.BottomSheetDialog +import im.vector.riotx.EmojiCompatFontProvider import im.vector.riotx.R import im.vector.riotx.core.di.ScreenComponent +import im.vector.riotx.core.platform.VectorBaseBottomSheetDialogFragment import im.vector.riotx.features.home.AvatarRenderer import im.vector.riotx.features.home.room.detail.timeline.item.MessageInformationData -import kotlinx.android.synthetic.main.bottom_sheet_message_actions.* +import kotlinx.android.synthetic.main.bottom_sheet_generic_recycler_epoxy.* import javax.inject.Inject /** * Bottom sheet fragment that shows a message preview with list of contextual actions - * (Includes fragments for quick reactions and list of actions) */ -class MessageActionsBottomSheet : VectorBaseBottomSheetDialogFragment() { +class MessageActionsBottomSheet : VectorBaseBottomSheetDialogFragment(), MessageActionsEpoxyController.MessageActionsEpoxyControllerListener { + @Inject lateinit var messageActionViewModelFactory: MessageActionsViewModel.Factory + @Inject lateinit var avatarRenderer: AvatarRenderer + @Inject lateinit var fontProvider: EmojiCompatFontProvider - @Inject - lateinit var messageActionViewModelFactory: MessageActionsViewModel.Factory - @Inject - lateinit var avatarRenderer: AvatarRenderer private val viewModel: MessageActionsViewModel by fragmentViewModel(MessageActionsViewModel::class) + private lateinit var messageActionsEpoxyController: MessageActionsEpoxyController private lateinit var actionHandlerModel: ActionsHandler - @BindView(R.id.bottom_sheet_message_preview_avatar) - lateinit var senderAvatarImageView: ImageView - - @BindView(R.id.bottom_sheet_message_preview_sender) - lateinit var senderNameTextView: TextView - - @BindView(R.id.bottom_sheet_message_preview_timestamp) - lateinit var messageTimestampText: TextView - - @BindView(R.id.bottom_sheet_message_preview_body) - lateinit var messageBodyTextView: TextView - override fun injectWith(screenComponent: ScreenComponent) { screenComponent.inject(this) } override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { - val view = inflater.inflate(R.layout.bottom_sheet_message_actions, container, false) - ButterKnife.bind(this, view) - return view + return inflater.inflate(R.layout.bottom_sheet_generic_recycler_epoxy, container, false) } override fun onActivityCreated(savedInstanceState: Bundle?) { super.onActivityCreated(savedInstanceState) actionHandlerModel = ViewModelProviders.of(requireActivity()).get(ActionsHandler::class.java) - val cfm = childFragmentManager - var menuActionFragment = cfm.findFragmentByTag("MenuActionFragment") as? MessageMenuFragment - if (menuActionFragment == null) { - menuActionFragment = MessageMenuFragment.newInstance(arguments!!.get(MvRx.KEY_ARG) as TimelineEventFragmentArgs) - cfm.beginTransaction() - .replace(R.id.bottom_sheet_menu_container, menuActionFragment, "MenuActionFragment") - .commit() - } - menuActionFragment.interactionListener = object : MessageMenuFragment.InteractionListener { - override fun didSelectMenuAction(simpleAction: SimpleAction) { - actionHandlerModel.fireAction(simpleAction) - dismiss() - } - } + messageActionsEpoxyController = MessageActionsEpoxyController(requireContext(), avatarRenderer, fontProvider) + bottomSheetEpoxyRecyclerView.setController(messageActionsEpoxyController) + messageActionsEpoxyController.listener = this + } - var quickReactionFragment = cfm.findFragmentByTag("QuickReaction") as? QuickReactionFragment - if (quickReactionFragment == null) { - quickReactionFragment = QuickReactionFragment.newInstance(arguments!!.get(MvRx.KEY_ARG) as TimelineEventFragmentArgs) - cfm.beginTransaction() - .replace(R.id.bottom_sheet_quick_reaction_container, quickReactionFragment, "QuickReaction") - .commit() - } - quickReactionFragment.interactionListener = object : QuickReactionFragment.InteractionListener { - override fun didQuickReactWith(clickedOn: String, add: Boolean, eventId: String) { - actionHandlerModel.fireAction(SimpleAction.QuickReact(eventId, clickedOn, add)) - dismiss() - } - } + override fun didSelectMenuAction(simpleAction: SimpleAction) { + actionHandlerModel.fireAction(simpleAction) + dismiss() } override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { @@ -124,32 +85,7 @@ class MessageActionsBottomSheet : VectorBaseBottomSheetDialogFragment() { } override fun invalidate() = withState(viewModel) { - val body = viewModel.resolveBody(it) - if (body != null) { - bottom_sheet_message_preview.isVisible = true - senderNameTextView.text = it.senderName() - messageBodyTextView.text = body - messageTimestampText.text = it.time() - avatarRenderer.render(it.informationData.avatarUrl, it.informationData.senderId, it.senderName(), senderAvatarImageView) - } else { - bottom_sheet_message_preview.isVisible = false - } - quickReactBottomDivider.isVisible = it.canReact() - bottom_sheet_quick_reaction_container.isVisible = it.canReact() - if (it.informationData.sendState.isSending()) { - messageStatusInfo.isVisible = true - messageStatusProgress.isVisible = true - messageStatusText.text = getString(R.string.event_status_sending_message) - messageStatusText.setCompoundDrawables(null, null, null, null) - } else if (it.informationData.sendState.hasFailed()) { - messageStatusInfo.isVisible = true - messageStatusProgress.isVisible = false - messageStatusText.setCompoundDrawablesRelativeWithIntrinsicBounds(R.drawable.ic_warning_small, 0, 0, 0) - messageStatusText.text = getString(R.string.unable_to_send_message) - } else { - messageStatusInfo.isVisible = false - } - return@withState + messageActionsEpoxyController.setData(it) } companion object { diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/MessageActionsEpoxyController.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/MessageActionsEpoxyController.kt new file mode 100644 index 0000000000..1de7302820 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/MessageActionsEpoxyController.kt @@ -0,0 +1,104 @@ +/* + * Copyright 2019 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.home.room.detail.timeline.action + +import android.content.Context +import android.view.View +import com.airbnb.epoxy.TypedEpoxyController +import com.airbnb.mvrx.Success +import im.vector.riotx.EmojiCompatFontProvider +import im.vector.riotx.R +import im.vector.riotx.features.home.AvatarRenderer + +/** + * Epoxy controller for message action list + */ +class MessageActionsEpoxyController(private val context: Context, + private val avatarRenderer: AvatarRenderer, + private val fontProvider: EmojiCompatFontProvider) : TypedEpoxyController() { + + var listener: MessageActionsEpoxyControllerListener? = null + + override fun buildModels(state: MessageActionState) { + // Message preview + val body = state.messageBody + if (body != null) { + bottomSheetItemMessagePreview { + id("preview") + avatarRenderer(avatarRenderer) + informationData(state.informationData) + senderName(state.senderName()) + body(body) + time(state.time()) + } + } + + // Send state + if (state.informationData.sendState.isSending()) { + bottomSheetItemSendState { + id("send_state") + showProgress(true) + text(context.getString(R.string.event_status_sending_message)) + } + } else if (state.informationData.sendState.hasFailed()) { + bottomSheetItemSendState { + id("send_state") + showProgress(false) + text(context.getString(R.string.unable_to_send_message)) + drawableStart(R.drawable.ic_warning_small) + } + } + + // Quick reactions + if (state.canReact() && state.quickStates is Success) { + // Separator + bottomSheetItemSeparator { + id("reaction_separator") + } + + bottomSheetItemQuickReactions { + id("quick_reaction") + fontProvider(fontProvider) + texts(state.quickStates()?.map { it.reaction }.orEmpty()) + selecteds(state.quickStates()?.map { it.isSelected }.orEmpty()) + listener(object : BottomSheetItemQuickReactions.Listener { + override fun didSelect(emoji: String, selected: Boolean) { + listener?.didSelectMenuAction(SimpleAction.QuickReact(state.eventId, emoji, selected)) + } + }) + } + } + + // Separator + bottomSheetItemSeparator { + id("actions_separator") + } + + // Action + state.actions()?.forEachIndexed { index, action -> + bottomSheetItemAction { + id("action_$index") + iconRes(action.iconResId) + textRes(action.titleRes) + listener(View.OnClickListener { listener?.didSelectMenuAction(action) }) + } + } + } + + interface MessageActionsEpoxyControllerListener { + fun didSelectMenuAction(simpleAction: SimpleAction) + } +} diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/MessageActionsViewModel.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/MessageActionsViewModel.kt index c80c7c5f15..cc1237a555 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/MessageActionsViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/MessageActionsViewModel.kt @@ -21,26 +21,47 @@ import com.squareup.inject.assisted.AssistedInject import dagger.Lazy import im.vector.matrix.android.api.session.Session import im.vector.matrix.android.api.session.events.model.EventType +import im.vector.matrix.android.api.session.events.model.isTextMessage +import im.vector.matrix.android.api.session.events.model.toModel import im.vector.matrix.android.api.session.room.model.message.MessageContent +import im.vector.matrix.android.api.session.room.model.message.MessageImageContent import im.vector.matrix.android.api.session.room.model.message.MessageTextContent import im.vector.matrix.android.api.session.room.model.message.MessageType +import im.vector.matrix.android.api.session.room.send.SendState import im.vector.matrix.android.api.session.room.timeline.TimelineEvent import im.vector.matrix.android.api.session.room.timeline.getLastMessageContent +import im.vector.matrix.android.api.session.room.timeline.hasBeenEdited +import im.vector.matrix.android.api.util.Optional import im.vector.matrix.rx.RxRoom import im.vector.matrix.rx.unwrap +import im.vector.riotx.R import im.vector.riotx.core.extensions.canReact import im.vector.riotx.core.platform.VectorViewModel +import im.vector.riotx.core.resources.StringProvider import im.vector.riotx.features.home.room.detail.timeline.format.NoticeEventFormatter import im.vector.riotx.features.home.room.detail.timeline.item.MessageInformationData import im.vector.riotx.features.html.EventHtmlRenderer import java.text.SimpleDateFormat import java.util.* +/** + * Quick reactions state + */ +data class ToggleState( + val reaction: String, + val isSelected: Boolean +) + data class MessageActionState( val roomId: String, val eventId: String, val informationData: MessageInformationData, - val timelineEvent: Async = Uninitialized + val timelineEvent: Async = Uninitialized, + val messageBody: CharSequence? = null, + // For quick reactions + val quickStates: Async> = Uninitialized, + // For actions + val actions: Async> = Uninitialized ) : MvRxState { constructor(args: TimelineEventFragmentArgs) : this(roomId = args.roomId, eventId = args.eventId, informationData = args.informationData) @@ -49,18 +70,93 @@ data class MessageActionState( fun senderName(): String = informationData.memberName?.toString() ?: "" - fun time(): String? = timelineEvent()?.root?.originServerTs?.let { dateFormat.format(Date(it)) } - ?: "" + fun time(): String? = timelineEvent()?.root?.originServerTs?.let { dateFormat.format(Date(it)) } ?: "" fun canReact() = timelineEvent()?.canReact() == true +} - fun messageBody(eventHtmlRenderer: EventHtmlRenderer?, noticeEventFormatter: NoticeEventFormatter?): CharSequence? { +/** + * Information related to an event and used to display preview in contextual bottomsheet. + */ +class MessageActionsViewModel @AssistedInject constructor(@Assisted + initialState: MessageActionState, + private val eventHtmlRenderer: Lazy, + private val session: Session, + private val noticeEventFormatter: NoticeEventFormatter, + private val stringProvider: StringProvider +) : VectorViewModel(initialState) { + + private val eventId = initialState.eventId + private val informationData = initialState.informationData + private val room = session.getRoom(initialState.roomId) + + @AssistedInject.Factory + interface Factory { + fun create(initialState: MessageActionState): MessageActionsViewModel + } + + companion object : MvRxViewModelFactory { + + val quickEmojis = listOf("👍", "👎", "😄", "🎉", "😕", "❤️", "🚀", "👀") + + override fun create(viewModelContext: ViewModelContext, state: MessageActionState): MessageActionsViewModel? { + val fragment: MessageActionsBottomSheet = (viewModelContext as FragmentViewModelContext).fragment() + return fragment.messageActionViewModelFactory.create(state) + } + } + + init { + observeEvent() + observeReactions() + observeEventAction() + } + + private fun observeEvent() { + if (room == null) return + RxRoom(room) + .liveTimelineEvent(eventId) + .unwrap() + .execute { + copy( + timelineEvent = it, + messageBody = computeMessageBody(it) + ) + } + } + + private fun observeEventAction() { + if (room == null) return + RxRoom(room) + .liveTimelineEvent(eventId) + .map { + actionsForEvent(it) + } + .execute { + copy(actions = it) + } + } + + private fun observeReactions() { + if (room == null) return + RxRoom(room) + .liveAnnotationSummary(eventId) + .map { annotations -> + quickEmojis.map { emoji -> + ToggleState(emoji, annotations.getOrNull()?.reactionsSummary?.firstOrNull { it.key == emoji }?.addedByMe ?: false) + } + } + .execute { + copy(quickStates = it) + } + } + + private fun computeMessageBody(timelineEvent: Async): CharSequence? { return when (timelineEvent()?.root?.getClearType()) { EventType.MESSAGE -> { val messageContent: MessageContent? = timelineEvent()?.getLastMessageContent() if (messageContent is MessageTextContent && messageContent.format == MessageType.FORMAT_MATRIX_HTML) { - eventHtmlRenderer?.render(messageContent.formattedBody - ?: messageContent.body) + eventHtmlRenderer.get().render(messageContent.formattedBody + ?: messageContent.body) } else { messageContent?.body } @@ -72,54 +168,177 @@ data class MessageActionState( EventType.CALL_INVITE, EventType.CALL_HANGUP, EventType.CALL_ANSWER -> { - timelineEvent()?.let { noticeEventFormatter?.format(it) } + timelineEvent()?.let { noticeEventFormatter.format(it) } } else -> null } } -} -/** - * Information related to an event and used to display preview in contextual bottomsheet. - */ -class MessageActionsViewModel @AssistedInject constructor(@Assisted - initialState: MessageActionState, - private val eventHtmlRenderer: Lazy, - session: Session, - private val noticeEventFormatter: NoticeEventFormatter -) : VectorViewModel(initialState) { + private fun actionsForEvent(optionalEvent: Optional): List { + val event = optionalEvent.getOrNull() ?: return emptyList() - private val eventId = initialState.eventId - private val room = session.getRoom(initialState.roomId) + val messageContent: MessageContent? = event.annotations?.editSummary?.aggregatedContent.toModel() + ?: event.root.getClearContent().toModel() + val type = messageContent?.type - @AssistedInject.Factory - interface Factory { - fun create(initialState: MessageActionState): MessageActionsViewModel - } + return arrayListOf().apply { + if (event.root.sendState.hasFailed()) { + if (canRetry(event)) { + add(SimpleAction.Resend(eventId)) + } + add(SimpleAction.Remove(eventId)) + } else if (event.root.sendState.isSending()) { + // TODO is uploading attachment? + if (canCancel(event)) { + add(SimpleAction.Cancel(eventId)) + } + } else { + if (!event.root.isRedacted()) { + if (canReply(event, messageContent)) { + add(SimpleAction.Reply(eventId)) + } - companion object : MvRxViewModelFactory { + if (canEdit(event, session.myUserId)) { + add(SimpleAction.Edit(eventId)) + } - override fun create(viewModelContext: ViewModelContext, state: MessageActionState): MessageActionsViewModel? { - val fragment: MessageActionsBottomSheet = (viewModelContext as FragmentViewModelContext).fragment() - return fragment.messageActionViewModelFactory.create(state) + if (canRedact(event, session.myUserId)) { + add(SimpleAction.Delete(eventId)) + } + + if (canCopy(type)) { + // TODO copy images? html? see ClipBoard + add(SimpleAction.Copy(messageContent!!.body)) + } + + if (event.canReact()) { + add(SimpleAction.AddReaction(eventId)) + } + + if (canQuote(event, messageContent)) { + add(SimpleAction.Quote(eventId)) + } + + if (canViewReactions(event)) { + add(SimpleAction.ViewReactions(informationData)) + } + + if (event.hasBeenEdited()) { + add(SimpleAction.ViewEditHistory(informationData)) + } + + if (canShare(type)) { + if (messageContent is MessageImageContent) { + session.contentUrlResolver().resolveFullSize(messageContent.url)?.let { url -> + add(SimpleAction.Share(url)) + } + } + // TODO + } + + if (event.root.sendState == SendState.SENT) { + // TODO Can be redacted + + // TODO sent by me or sufficient power level + } + } + + add(SimpleAction.ViewSource(event.root.toContentStringWithIndent())) + if (event.isEncrypted()) { + val decryptedContent = event.root.toClearContentStringWithIndent() + ?: stringProvider.getString(R.string.encryption_information_decryption_error) + add(SimpleAction.ViewDecryptedSource(decryptedContent)) + } + add(SimpleAction.CopyPermalink(eventId)) + + if (session.myUserId != event.root.senderId && event.root.getClearType() == EventType.MESSAGE) { + // not sent by me + add(SimpleAction.Flag(eventId)) + } + } } } - init { - observeEvent() + private fun canCancel(@Suppress("UNUSED_PARAMETER") event: TimelineEvent): Boolean { + return false } - private fun observeEvent() { - if (room == null) return - RxRoom(room) - .liveTimelineEvent(eventId) - .unwrap() - .execute { - copy(timelineEvent = it) - } + private fun canReply(event: TimelineEvent, messageContent: MessageContent?): Boolean { + // Only event of type Event.EVENT_TYPE_MESSAGE are supported for the moment + if (event.root.getClearType() != EventType.MESSAGE) return false + return when (messageContent?.type) { + MessageType.MSGTYPE_TEXT, + MessageType.MSGTYPE_NOTICE, + MessageType.MSGTYPE_EMOTE, + MessageType.MSGTYPE_IMAGE, + MessageType.MSGTYPE_VIDEO, + MessageType.MSGTYPE_AUDIO, + MessageType.MSGTYPE_FILE -> true + else -> false + } } - fun resolveBody(state: MessageActionState): CharSequence? { - return state.messageBody(eventHtmlRenderer.get(), noticeEventFormatter) + private fun canQuote(event: TimelineEvent, messageContent: MessageContent?): Boolean { + // Only event of type Event.EVENT_TYPE_MESSAGE are supported for the moment + if (event.root.getClearType() != EventType.MESSAGE) return false + return when (messageContent?.type) { + MessageType.MSGTYPE_TEXT, + MessageType.MSGTYPE_NOTICE, + MessageType.MSGTYPE_EMOTE, + MessageType.FORMAT_MATRIX_HTML, + MessageType.MSGTYPE_LOCATION -> { + true + } + else -> false + } + } + + private fun canRedact(event: TimelineEvent, myUserId: String): Boolean { + // Only event of type Event.EVENT_TYPE_MESSAGE are supported for the moment + if (event.root.getClearType() != EventType.MESSAGE) return false + // TODO if user is admin or moderator + return event.root.senderId == myUserId + } + + private fun canRetry(event: TimelineEvent): Boolean { + return event.root.sendState.hasFailed() && event.root.isTextMessage() + } + + private fun canViewReactions(event: TimelineEvent): Boolean { + // Only event of type Event.EVENT_TYPE_MESSAGE are supported for the moment + if (event.root.getClearType() != EventType.MESSAGE) return false + // TODO if user is admin or moderator + return event.annotations?.reactionsSummary?.isNotEmpty() ?: false + } + + private fun canEdit(event: TimelineEvent, myUserId: String): Boolean { + // Only event of type Event.EVENT_TYPE_MESSAGE are supported for the moment + if (event.root.getClearType() != EventType.MESSAGE) return false + // TODO if user is admin or moderator + val messageContent = event.root.getClearContent().toModel() + return event.root.senderId == myUserId && ( + messageContent?.type == MessageType.MSGTYPE_TEXT + || messageContent?.type == MessageType.MSGTYPE_EMOTE + ) + } + + private fun canCopy(type: String?): Boolean { + return when (type) { + MessageType.MSGTYPE_TEXT, + MessageType.MSGTYPE_NOTICE, + MessageType.MSGTYPE_EMOTE, + MessageType.FORMAT_MATRIX_HTML, + MessageType.MSGTYPE_LOCATION -> true + else -> false + } + } + + private fun canShare(type: String?): Boolean { + return when (type) { + MessageType.MSGTYPE_IMAGE, + MessageType.MSGTYPE_AUDIO, + MessageType.MSGTYPE_VIDEO -> true + else -> false + } } } diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/MessageMenuFragment.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/MessageMenuFragment.kt deleted file mode 100644 index 2eec705eea..0000000000 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/MessageMenuFragment.kt +++ /dev/null @@ -1,104 +0,0 @@ -/* - * Copyright 2019 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.home.room.detail.timeline.action - -import android.os.Bundle -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import android.widget.FrameLayout -import android.widget.ImageView -import android.widget.LinearLayout -import android.widget.TextView -import com.airbnb.mvrx.MvRx -import com.airbnb.mvrx.fragmentViewModel -import com.airbnb.mvrx.withState -import im.vector.riotx.R -import im.vector.riotx.core.di.ScreenComponent -import im.vector.riotx.core.platform.VectorBaseFragment -import im.vector.riotx.features.themes.ThemeUtils -import javax.inject.Inject - -/** - * Fragment showing the list of available contextual action for a given message. - */ -class MessageMenuFragment : VectorBaseFragment() { - - @Inject lateinit var messageMenuViewModelFactory: MessageMenuViewModel.Factory - private val viewModel: MessageMenuViewModel by fragmentViewModel(MessageMenuViewModel::class) - private var addSeparators = false - var interactionListener: InteractionListener? = null - - override fun injectWith(injector: ScreenComponent) { - injector.inject(this) - } - - override fun getLayoutResId() = R.layout.fragment_message_menu - - override fun invalidate() = withState(viewModel) { state -> - - val linearLayout = view as? LinearLayout - if (linearLayout != null) { - val inflater = LayoutInflater.from(linearLayout.context) - linearLayout.removeAllViews() - var insertIndex = 0 - val actions = state.actions() - actions?.forEachIndexed { index, action -> - inflateActionView(action, inflater, linearLayout)?.let { - it.setOnClickListener { - interactionListener?.didSelectMenuAction(action) - } - linearLayout.addView(it, insertIndex) - insertIndex++ - if (addSeparators) { - if (index < actions.size - 1) { - linearLayout.addView(inflateSeparatorView(), insertIndex) - insertIndex++ - } - } - } - } - } - } - - private fun inflateActionView(action: SimpleAction, inflater: LayoutInflater, container: ViewGroup?): View? { - return inflater.inflate(R.layout.adapter_item_action, container, false)?.apply { - findViewById(R.id.action_icon)?.setImageResource(action.iconResId) - findViewById(R.id.action_title)?.setText(action.titleRes) - } - } - - private fun inflateSeparatorView(): View { - val frame = FrameLayout(requireContext()) - frame.setBackgroundColor(ThemeUtils.getColor(requireContext(), R.attr.vctr_list_divider_color)) - frame.layoutParams = LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, requireContext().resources.displayMetrics.density.toInt()) - return frame - } - - interface InteractionListener { - fun didSelectMenuAction(simpleAction: SimpleAction) - } - - companion object { - fun newInstance(pa: TimelineEventFragmentArgs): MessageMenuFragment { - val args = Bundle() - args.putParcelable(MvRx.KEY_ARG, pa) - val fragment = MessageMenuFragment() - fragment.arguments = args - return fragment - } - } -} diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/MessageMenuViewModel.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/MessageMenuViewModel.kt deleted file mode 100644 index 14d730044a..0000000000 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/MessageMenuViewModel.kt +++ /dev/null @@ -1,279 +0,0 @@ -/* - * Copyright 2019 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.home.room.detail.timeline.action - -import androidx.annotation.DrawableRes -import androidx.annotation.StringRes -import com.airbnb.mvrx.* -import com.squareup.inject.assisted.Assisted -import com.squareup.inject.assisted.AssistedInject -import im.vector.matrix.android.api.session.Session -import im.vector.matrix.android.api.session.events.model.EventType -import im.vector.matrix.android.api.session.events.model.isTextMessage -import im.vector.matrix.android.api.session.events.model.toModel -import im.vector.matrix.android.api.session.room.model.message.MessageContent -import im.vector.matrix.android.api.session.room.model.message.MessageImageContent -import im.vector.matrix.android.api.session.room.model.message.MessageType -import im.vector.matrix.android.api.session.room.send.SendState -import im.vector.matrix.android.api.session.room.timeline.TimelineEvent -import im.vector.matrix.android.api.session.room.timeline.hasBeenEdited -import im.vector.matrix.android.api.util.Optional -import im.vector.matrix.rx.RxRoom -import im.vector.riotx.R -import im.vector.riotx.core.extensions.canReact -import im.vector.riotx.core.platform.VectorViewModel -import im.vector.riotx.core.resources.StringProvider -import im.vector.riotx.features.home.room.detail.timeline.item.MessageInformationData - -sealed class SimpleAction(@StringRes val titleRes: Int, @DrawableRes val iconResId: Int) { - data class AddReaction(val eventId: String) : SimpleAction(R.string.message_add_reaction, R.drawable.ic_add_reaction) - data class Copy(val content: String) : SimpleAction(R.string.copy, R.drawable.ic_copy) - data class Edit(val eventId: String) : SimpleAction(R.string.edit, R.drawable.ic_edit) - data class Quote(val eventId: String) : SimpleAction(R.string.quote, R.drawable.ic_quote) - data class Reply(val eventId: String) : SimpleAction(R.string.reply, R.drawable.ic_reply) - data class Share(val imageUrl: String) : SimpleAction(R.string.share, R.drawable.ic_share) - data class Resend(val eventId: String) : SimpleAction(R.string.global_retry, R.drawable.ic_refresh_cw) - data class Remove(val eventId: String) : SimpleAction(R.string.remove, R.drawable.ic_trash) - data class Delete(val eventId: String) : SimpleAction(R.string.delete, R.drawable.ic_delete) - data class Cancel(val eventId: String) : SimpleAction(R.string.cancel, R.drawable.ic_close_round) - data class ViewSource(val content: String) : SimpleAction(R.string.view_source, R.drawable.ic_view_source) - data class ViewDecryptedSource(val content: String) : SimpleAction(R.string.view_decrypted_source, R.drawable.ic_view_source) - data class CopyPermalink(val eventId: String) : SimpleAction(R.string.permalink, R.drawable.ic_permalink) - data class Flag(val eventId: String) : SimpleAction(R.string.report_content, R.drawable.ic_flag) - data class QuickReact(val eventId: String, val clickedOn: String, val add: Boolean) : SimpleAction(0, 0) - data class ViewReactions(val messageInformationData: MessageInformationData) : SimpleAction(R.string.message_view_reaction, R.drawable.ic_view_reactions) - data class ViewEditHistory(val messageInformationData: MessageInformationData) : - SimpleAction(R.string.message_view_edit_history, R.drawable.ic_view_edit_history) -} - -data class MessageMenuState( - val roomId: String, - val eventId: String, - val informationData: MessageInformationData, - val actions: Async> = Uninitialized -) : MvRxState { - - constructor(args: TimelineEventFragmentArgs) : this(roomId = args.roomId, eventId = args.eventId, informationData = args.informationData) -} - -/** - * Manages list actions for a given message (copy / paste / forward...) - */ -class MessageMenuViewModel @AssistedInject constructor(@Assisted initialState: MessageMenuState, - private val session: Session, - private val stringProvider: StringProvider) : VectorViewModel(initialState) { - - @AssistedInject.Factory - interface Factory { - fun create(initialState: MessageMenuState): MessageMenuViewModel - } - - private val room = session.getRoom(initialState.roomId) - ?: throw IllegalStateException("Shouldn't use this ViewModel without a room") - - private val eventId = initialState.eventId - private val informationData: MessageInformationData = initialState.informationData - - companion object : MvRxViewModelFactory { - override fun create(viewModelContext: ViewModelContext, state: MessageMenuState): MessageMenuViewModel? { - val fragment: MessageMenuFragment = (viewModelContext as FragmentViewModelContext).fragment() - return fragment.messageMenuViewModelFactory.create(state) - } - } - - init { - observeEvent() - } - - private fun observeEvent() { - RxRoom(room) - .liveTimelineEvent(eventId) - .map { - actionsForEvent(it) - } - .execute { - copy(actions = it) - } - } - - private fun actionsForEvent(optionalEvent: Optional): List { - val event = optionalEvent.getOrNull() ?: return emptyList() - - val messageContent: MessageContent? = event.annotations?.editSummary?.aggregatedContent.toModel() - ?: event.root.getClearContent().toModel() - val type = messageContent?.type - - return arrayListOf().apply { - if (event.root.sendState.hasFailed()) { - if (canRetry(event)) { - add(SimpleAction.Resend(eventId)) - } - add(SimpleAction.Remove(eventId)) - } else if (event.root.sendState.isSending()) { - // TODO is uploading attachment? - if (canCancel(event)) { - add(SimpleAction.Cancel(eventId)) - } - } else { - if (!event.root.isRedacted()) { - if (canReply(event, messageContent)) { - add(SimpleAction.Reply(eventId)) - } - - if (canEdit(event, session.myUserId)) { - add(SimpleAction.Edit(eventId)) - } - - if (canRedact(event, session.myUserId)) { - add(SimpleAction.Delete(eventId)) - } - - if (canCopy(type)) { - // TODO copy images? html? see ClipBoard - add(SimpleAction.Copy(messageContent!!.body)) - } - - if (event.canReact()) { - add(SimpleAction.AddReaction(eventId)) - } - - if (canQuote(event, messageContent)) { - add(SimpleAction.Quote(eventId)) - } - - if (canViewReactions(event)) { - add(SimpleAction.ViewReactions(informationData)) - } - - if (event.hasBeenEdited()) { - add(SimpleAction.ViewEditHistory(informationData)) - } - - if (canShare(type)) { - if (messageContent is MessageImageContent) { - session.contentUrlResolver().resolveFullSize(messageContent.url)?.let { url -> - add(SimpleAction.Share(url)) - } - } - // TODO - } - - if (event.root.sendState == SendState.SENT) { - // TODO Can be redacted - - // TODO sent by me or sufficient power level - } - } - - add(SimpleAction.ViewSource(event.root.toContentStringWithIndent())) - if (event.isEncrypted()) { - val decryptedContent = event.root.toClearContentStringWithIndent() - ?: stringProvider.getString(R.string.encryption_information_decryption_error) - add(SimpleAction.ViewDecryptedSource(decryptedContent)) - } - add(SimpleAction.CopyPermalink(eventId)) - - if (session.myUserId != event.root.senderId && event.root.getClearType() == EventType.MESSAGE) { - // not sent by me - add(SimpleAction.Flag(eventId)) - } - } - } - } - - private fun canCancel(@Suppress("UNUSED_PARAMETER") event: TimelineEvent): Boolean { - return false - } - - private fun canReply(event: TimelineEvent, messageContent: MessageContent?): Boolean { - // Only event of type Event.EVENT_TYPE_MESSAGE are supported for the moment - if (event.root.getClearType() != EventType.MESSAGE) return false - return when (messageContent?.type) { - MessageType.MSGTYPE_TEXT, - MessageType.MSGTYPE_NOTICE, - MessageType.MSGTYPE_EMOTE, - MessageType.MSGTYPE_IMAGE, - MessageType.MSGTYPE_VIDEO, - MessageType.MSGTYPE_AUDIO, - MessageType.MSGTYPE_FILE -> true - else -> false - } - } - - private fun canQuote(event: TimelineEvent, messageContent: MessageContent?): Boolean { - // Only event of type Event.EVENT_TYPE_MESSAGE are supported for the moment - if (event.root.getClearType() != EventType.MESSAGE) return false - return when (messageContent?.type) { - MessageType.MSGTYPE_TEXT, - MessageType.MSGTYPE_NOTICE, - MessageType.MSGTYPE_EMOTE, - MessageType.FORMAT_MATRIX_HTML, - MessageType.MSGTYPE_LOCATION -> { - true - } - else -> false - } - } - - private fun canRedact(event: TimelineEvent, myUserId: String): Boolean { - // Only event of type Event.EVENT_TYPE_MESSAGE are supported for the moment - if (event.root.getClearType() != EventType.MESSAGE) return false - // TODO if user is admin or moderator - return event.root.senderId == myUserId - } - - private fun canRetry(event: TimelineEvent): Boolean { - return event.root.sendState.hasFailed() && event.root.isTextMessage() - } - - private fun canViewReactions(event: TimelineEvent): Boolean { - // Only event of type Event.EVENT_TYPE_MESSAGE are supported for the moment - if (event.root.getClearType() != EventType.MESSAGE) return false - // TODO if user is admin or moderator - return event.annotations?.reactionsSummary?.isNotEmpty() ?: false - } - - private fun canEdit(event: TimelineEvent, myUserId: String): Boolean { - // Only event of type Event.EVENT_TYPE_MESSAGE are supported for the moment - if (event.root.getClearType() != EventType.MESSAGE) return false - // TODO if user is admin or moderator - val messageContent = event.root.getClearContent().toModel() - return event.root.senderId == myUserId && ( - messageContent?.type == MessageType.MSGTYPE_TEXT - || messageContent?.type == MessageType.MSGTYPE_EMOTE - ) - } - - private fun canCopy(type: String?): Boolean { - return when (type) { - MessageType.MSGTYPE_TEXT, - MessageType.MSGTYPE_NOTICE, - MessageType.MSGTYPE_EMOTE, - MessageType.FORMAT_MATRIX_HTML, - MessageType.MSGTYPE_LOCATION -> true - else -> false - } - } - - private fun canShare(type: String?): Boolean { - return when (type) { - MessageType.MSGTYPE_IMAGE, - MessageType.MSGTYPE_AUDIO, - MessageType.MSGTYPE_VIDEO -> true - else -> false - } - } -} diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/QuickReactionFragment.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/QuickReactionFragment.kt deleted file mode 100644 index cabb4c113f..0000000000 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/QuickReactionFragment.kt +++ /dev/null @@ -1,89 +0,0 @@ -/* - * Copyright 2019 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.home.room.detail.timeline.action - -import android.graphics.Typeface -import android.os.Bundle -import android.view.View -import android.widget.TextView -import com.airbnb.mvrx.MvRx -import com.airbnb.mvrx.fragmentViewModel -import com.airbnb.mvrx.withState -import im.vector.riotx.EmojiCompatFontProvider -import im.vector.riotx.R -import im.vector.riotx.core.di.ScreenComponent -import im.vector.riotx.core.platform.VectorBaseFragment -import kotlinx.android.synthetic.main.adapter_item_action_quick_reaction.* -import javax.inject.Inject - -/** - * Quick Reaction Fragment (agree / like reactions) - */ -class QuickReactionFragment : VectorBaseFragment() { - - private val viewModel: QuickReactionViewModel by fragmentViewModel(QuickReactionViewModel::class) - - var interactionListener: InteractionListener? = null - - @Inject lateinit var fontProvider: EmojiCompatFontProvider - @Inject lateinit var quickReactionViewModelFactory: QuickReactionViewModel.Factory - - override fun getLayoutResId() = R.layout.adapter_item_action_quick_reaction - - override fun injectWith(injector: ScreenComponent) { - injector.inject(this) - } - - private lateinit var textViews: List - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - textViews = listOf(quickReaction0, quickReaction1, quickReaction2, quickReaction3, - quickReaction4, quickReaction5, quickReaction6, quickReaction7) - textViews.forEachIndexed { index, textView -> - textView.typeface = fontProvider.typeface ?: Typeface.DEFAULT - textView.setOnClickListener { - viewModel.didSelect(index) - } - } - } - - override fun invalidate() = withState(viewModel) { - val quickReactionsStates = it.quickStates() ?: return@withState - quickReactionsStates.forEachIndexed { index, qs -> - textViews[index].text = qs.reaction - textViews[index].alpha = if (qs.isSelected) 0.2f else 1f - } - - if (it.result != null) { - interactionListener?.didQuickReactWith(it.result.reaction, it.result.isSelected, it.eventId) - } - } - - interface InteractionListener { - fun didQuickReactWith(clickedOn: String, add: Boolean, eventId: String) - } - - companion object { - fun newInstance(pa: TimelineEventFragmentArgs): QuickReactionFragment { - val args = Bundle() - args.putParcelable(MvRx.KEY_ARG, pa) - val fragment = QuickReactionFragment() - fragment.arguments = args - return fragment - } - } -} diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/QuickReactionViewModel.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/QuickReactionViewModel.kt deleted file mode 100644 index edcfd8e28c..0000000000 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/QuickReactionViewModel.kt +++ /dev/null @@ -1,96 +0,0 @@ -/* - * Copyright 2019 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.home.room.detail.timeline.action - -import com.airbnb.mvrx.* -import com.squareup.inject.assisted.Assisted -import com.squareup.inject.assisted.AssistedInject -import im.vector.matrix.android.api.session.Session -import im.vector.matrix.rx.RxRoom -import im.vector.riotx.core.platform.VectorViewModel -import im.vector.riotx.features.home.room.detail.timeline.item.MessageInformationData - -/** - * Quick reactions state, it's a toggle with 3rd state - */ -data class ToggleState( - val reaction: String, - val isSelected: Boolean -) - -data class QuickReactionState( - val roomId: String, - val eventId: String, - val informationData: MessageInformationData, - val quickStates: Async> = Uninitialized, - val result: ToggleState? = null - /** Pair of 'clickedOn' and current toggles state*/ -) : MvRxState { - - constructor(args: TimelineEventFragmentArgs) : this(roomId = args.roomId, eventId = args.eventId, informationData = args.informationData) -} - -/** - * Quick reaction view model - */ -class QuickReactionViewModel @AssistedInject constructor(@Assisted initialState: QuickReactionState, - private val session: Session) : VectorViewModel(initialState) { - - @AssistedInject.Factory - interface Factory { - fun create(initialState: QuickReactionState): QuickReactionViewModel - } - - private val room = session.getRoom(initialState.roomId) - private val eventId = initialState.eventId - - companion object : MvRxViewModelFactory { - - val quickEmojis = listOf("👍", "👎", "😄", "🎉", "😕", "❤️", "🚀", "👀") - - override fun create(viewModelContext: ViewModelContext, state: QuickReactionState): QuickReactionViewModel? { - val fragment: QuickReactionFragment = (viewModelContext as FragmentViewModelContext).fragment() - return fragment.quickReactionViewModelFactory.create(state) - } - } - - init { - observeReactions() - } - - private fun observeReactions() { - if (room == null) return - RxRoom(room) - .liveAnnotationSummary(eventId) - .map { annotations -> - quickEmojis.map { emoji -> - ToggleState(emoji, annotations.getOrNull()?.reactionsSummary?.firstOrNull { it.key == emoji }?.addedByMe - ?: false) - } - } - .execute { - copy(quickStates = it) - } - } - - fun didSelect(index: Int) = withState { - val selectedReaction = it.quickStates()?.get(index) ?: return@withState - val isSelected = selectedReaction.isSelected - setState { - copy(result = ToggleState(selectedReaction.reaction, !isSelected)) - } - } -} diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/SimpleAction.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/SimpleAction.kt new file mode 100644 index 0000000000..9ba1bbb212 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/SimpleAction.kt @@ -0,0 +1,43 @@ +/* + * Copyright 2019 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.home.room.detail.timeline.action + +import androidx.annotation.DrawableRes +import androidx.annotation.StringRes +import im.vector.riotx.R +import im.vector.riotx.features.home.room.detail.timeline.item.MessageInformationData + +sealed class SimpleAction(@StringRes val titleRes: Int, @DrawableRes val iconResId: Int) { + data class AddReaction(val eventId: String) : SimpleAction(R.string.message_add_reaction, R.drawable.ic_add_reaction) + data class Copy(val content: String) : SimpleAction(R.string.copy, R.drawable.ic_copy) + data class Edit(val eventId: String) : SimpleAction(R.string.edit, R.drawable.ic_edit) + data class Quote(val eventId: String) : SimpleAction(R.string.quote, R.drawable.ic_quote) + data class Reply(val eventId: String) : SimpleAction(R.string.reply, R.drawable.ic_reply) + data class Share(val imageUrl: String) : SimpleAction(R.string.share, R.drawable.ic_share) + data class Resend(val eventId: String) : SimpleAction(R.string.global_retry, R.drawable.ic_refresh_cw) + data class Remove(val eventId: String) : SimpleAction(R.string.remove, R.drawable.ic_trash) + data class Delete(val eventId: String) : SimpleAction(R.string.delete, R.drawable.ic_delete) + data class Cancel(val eventId: String) : SimpleAction(R.string.cancel, R.drawable.ic_close_round) + data class ViewSource(val content: String) : SimpleAction(R.string.view_source, R.drawable.ic_view_source) + data class ViewDecryptedSource(val content: String) : SimpleAction(R.string.view_decrypted_source, R.drawable.ic_view_source) + data class CopyPermalink(val eventId: String) : SimpleAction(R.string.permalink, R.drawable.ic_permalink) + data class Flag(val eventId: String) : SimpleAction(R.string.report_content, R.drawable.ic_flag) + data class QuickReact(val eventId: String, val clickedOn: String, val add: Boolean) : SimpleAction(0, 0) + data class ViewReactions(val messageInformationData: MessageInformationData) : SimpleAction(R.string.message_view_reaction, R.drawable.ic_view_reactions) + data class ViewEditHistory(val messageInformationData: MessageInformationData) : + SimpleAction(R.string.message_view_edit_history, R.drawable.ic_view_edit_history) +} diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/ViewEditHistoryBottomSheet.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/edithistory/ViewEditHistoryBottomSheet.kt similarity index 93% rename from vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/ViewEditHistoryBottomSheet.kt rename to vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/edithistory/ViewEditHistoryBottomSheet.kt index 5fefb36e29..acdef2058d 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/ViewEditHistoryBottomSheet.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/edithistory/ViewEditHistoryBottomSheet.kt @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package im.vector.riotx.features.home.room.detail.timeline.action +package im.vector.riotx.features.home.room.detail.timeline.edithistory import android.os.Bundle import android.view.LayoutInflater @@ -29,6 +29,8 @@ import com.airbnb.mvrx.fragmentViewModel import com.airbnb.mvrx.withState import im.vector.riotx.R import im.vector.riotx.core.di.ScreenComponent +import im.vector.riotx.core.platform.VectorBaseBottomSheetDialogFragment +import im.vector.riotx.features.home.room.detail.timeline.action.TimelineEventFragmentArgs import im.vector.riotx.features.home.room.detail.timeline.item.MessageInformationData import im.vector.riotx.features.html.EventHtmlRenderer import kotlinx.android.synthetic.main.bottom_sheet_epoxylist_with_title.* diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/ViewEditHistoryEpoxyController.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/edithistory/ViewEditHistoryEpoxyController.kt similarity index 98% rename from vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/ViewEditHistoryEpoxyController.kt rename to vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/edithistory/ViewEditHistoryEpoxyController.kt index 288f001651..d36e98f67c 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/ViewEditHistoryEpoxyController.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/edithistory/ViewEditHistoryEpoxyController.kt @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package im.vector.riotx.features.home.room.detail.timeline.action +package im.vector.riotx.features.home.room.detail.timeline.edithistory import android.content.Context import android.text.Spannable @@ -41,7 +41,7 @@ import timber.log.Timber import java.util.* /** - * Epoxy controller for reaction event list + * Epoxy controller for edit history list */ class ViewEditHistoryEpoxyController(private val context: Context, val dateFormatter: VectorDateFormatter, diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/ViewEditHistoryViewModel.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/edithistory/ViewEditHistoryViewModel.kt similarity index 96% rename from vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/ViewEditHistoryViewModel.kt rename to vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/edithistory/ViewEditHistoryViewModel.kt index 890fbe60e5..e2b976b273 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/ViewEditHistoryViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/edithistory/ViewEditHistoryViewModel.kt @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package im.vector.riotx.features.home.room.detail.timeline.action +package im.vector.riotx.features.home.room.detail.timeline.edithistory import com.airbnb.mvrx.* import com.squareup.inject.assisted.Assisted @@ -28,6 +28,7 @@ import im.vector.matrix.android.api.session.room.model.message.isReply import im.vector.matrix.android.internal.crypto.algorithms.olm.OlmDecryptionResult import im.vector.riotx.core.platform.VectorViewModel import im.vector.riotx.core.date.VectorDateFormatter +import im.vector.riotx.features.home.room.detail.timeline.action.TimelineEventFragmentArgs import timber.log.Timber import java.util.* diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/ReactionInfoSimpleItem.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/reactions/ReactionInfoSimpleItem.kt similarity index 96% rename from vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/ReactionInfoSimpleItem.kt rename to vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/reactions/ReactionInfoSimpleItem.kt index e1d03d93fc..39392324aa 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/ReactionInfoSimpleItem.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/reactions/ReactionInfoSimpleItem.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package im.vector.riotx.features.home.room.detail.timeline.action +package im.vector.riotx.features.home.room.detail.timeline.reactions import android.widget.TextView import androidx.core.view.isVisible diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/ViewReactionBottomSheet.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/reactions/ViewReactionsBottomSheet.kt similarity index 82% rename from vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/ViewReactionBottomSheet.kt rename to vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/reactions/ViewReactionsBottomSheet.kt index b4eba4bbec..deb2a84818 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/ViewReactionBottomSheet.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/reactions/ViewReactionsBottomSheet.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package im.vector.riotx.features.home.room.detail.timeline.action +package im.vector.riotx.features.home.room.detail.timeline.reactions import android.os.Bundle import android.view.LayoutInflater @@ -30,6 +30,8 @@ import com.airbnb.mvrx.fragmentViewModel import com.airbnb.mvrx.withState import im.vector.riotx.R import im.vector.riotx.core.di.ScreenComponent +import im.vector.riotx.core.platform.VectorBaseBottomSheetDialogFragment +import im.vector.riotx.features.home.room.detail.timeline.action.TimelineEventFragmentArgs import im.vector.riotx.features.home.room.detail.timeline.item.MessageInformationData import kotlinx.android.synthetic.main.bottom_sheet_epoxylist_with_title.* import javax.inject.Inject @@ -37,11 +39,11 @@ import javax.inject.Inject /** * Bottom sheet displaying list of reactions for a given event ordered by timestamp */ -class ViewReactionBottomSheet : VectorBaseBottomSheetDialogFragment() { +class ViewReactionsBottomSheet : VectorBaseBottomSheetDialogFragment() { - private val viewModel: ViewReactionViewModel by fragmentViewModel(ViewReactionViewModel::class) + private val viewModel: ViewReactionsViewModel by fragmentViewModel(ViewReactionsViewModel::class) - @Inject lateinit var viewReactionViewModelFactory: ViewReactionViewModel.Factory + @Inject lateinit var viewReactionsViewModelFactory: ViewReactionsViewModel.Factory @BindView(R.id.bottom_sheet_display_reactions_list) lateinit var epoxyRecyclerView: EpoxyRecyclerView @@ -72,7 +74,7 @@ class ViewReactionBottomSheet : VectorBaseBottomSheetDialogFragment() { } companion object { - fun newInstance(roomId: String, informationData: MessageInformationData): ViewReactionBottomSheet { + fun newInstance(roomId: String, informationData: MessageInformationData): ViewReactionsBottomSheet { val args = Bundle() val parcelableArgs = TimelineEventFragmentArgs( informationData.eventId, @@ -80,7 +82,7 @@ class ViewReactionBottomSheet : VectorBaseBottomSheetDialogFragment() { informationData ) args.putParcelable(MvRx.KEY_ARG, parcelableArgs) - return ViewReactionBottomSheet().apply { arguments = args } + return ViewReactionsBottomSheet().apply { arguments = args } } } } diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/ViewReactionsEpoxyController.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/reactions/ViewReactionsEpoxyController.kt similarity index 96% rename from vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/ViewReactionsEpoxyController.kt rename to vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/reactions/ViewReactionsEpoxyController.kt index 6b1c099261..7fd2edcbfe 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/ViewReactionsEpoxyController.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/reactions/ViewReactionsEpoxyController.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package im.vector.riotx.features.home.room.detail.timeline.action +package im.vector.riotx.features.home.room.detail.timeline.reactions import com.airbnb.epoxy.TypedEpoxyController import com.airbnb.mvrx.Fail diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/ViewReactionViewModel.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/reactions/ViewReactionsViewModel.kt similarity index 83% rename from vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/ViewReactionViewModel.kt rename to vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/reactions/ViewReactionsViewModel.kt index a3611edd87..208e126022 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/ViewReactionViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/reactions/ViewReactionsViewModel.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package im.vector.riotx.features.home.room.detail.timeline.action +package im.vector.riotx.features.home.room.detail.timeline.reactions import com.airbnb.mvrx.Async import com.airbnb.mvrx.FragmentViewModelContext @@ -30,6 +30,7 @@ import im.vector.matrix.rx.RxRoom import im.vector.matrix.rx.unwrap import im.vector.riotx.core.platform.VectorViewModel import im.vector.riotx.core.date.VectorDateFormatter +import im.vector.riotx.features.home.room.detail.timeline.action.TimelineEventFragmentArgs import io.reactivex.Observable import io.reactivex.Single @@ -53,10 +54,10 @@ data class ReactionInfo( /** * Used to display the list of members that reacted to a given event */ -class ViewReactionViewModel @AssistedInject constructor(@Assisted +class ViewReactionsViewModel @AssistedInject constructor(@Assisted initialState: DisplayReactionsViewState, - private val session: Session, - private val dateFormatter: VectorDateFormatter + private val session: Session, + private val dateFormatter: VectorDateFormatter ) : VectorViewModel(initialState) { private val roomId = initialState.roomId @@ -66,14 +67,14 @@ class ViewReactionViewModel @AssistedInject constructor(@Assisted @AssistedInject.Factory interface Factory { - fun create(initialState: DisplayReactionsViewState): ViewReactionViewModel + fun create(initialState: DisplayReactionsViewState): ViewReactionsViewModel } - companion object : MvRxViewModelFactory { + companion object : MvRxViewModelFactory { - override fun create(viewModelContext: ViewModelContext, state: DisplayReactionsViewState): ViewReactionViewModel? { - val fragment: ViewReactionBottomSheet = (viewModelContext as FragmentViewModelContext).fragment() - return fragment.viewReactionViewModelFactory.create(state) + override fun create(viewModelContext: ViewModelContext, state: DisplayReactionsViewState): ViewReactionsViewModel? { + val fragment: ViewReactionsBottomSheet = (viewModelContext as FragmentViewModelContext).fragment() + return fragment.viewReactionsViewModelFactory.create(state) } } diff --git a/vector/src/main/res/layout/bottom_sheet_epoxylist_with_title.xml b/vector/src/main/res/layout/bottom_sheet_epoxylist_with_title.xml index 9b3ffb26a3..0cc2e16ed9 100644 --- a/vector/src/main/res/layout/bottom_sheet_epoxylist_with_title.xml +++ b/vector/src/main/res/layout/bottom_sheet_epoxylist_with_title.xml @@ -25,7 +25,6 @@ android:orientation="vertical" android:scrollbars="vertical" tools:itemCount="15" - tools:listitem="@layout/item_simple_reaction_info"> + tools:listitem="@layout/item_simple_reaction_info" /> - \ No newline at end of file diff --git a/vector/src/main/res/layout/bottom_sheet_generic_recycler_epoxy.xml b/vector/src/main/res/layout/bottom_sheet_generic_recycler_epoxy.xml new file mode 100644 index 0000000000..5ca952272f --- /dev/null +++ b/vector/src/main/res/layout/bottom_sheet_generic_recycler_epoxy.xml @@ -0,0 +1,22 @@ + + + + + + + + \ No newline at end of file diff --git a/vector/src/main/res/layout/bottom_sheet_message_actions.xml b/vector/src/main/res/layout/bottom_sheet_message_actions.xml deleted file mode 100644 index c7d4f5ac8e..0000000000 --- a/vector/src/main/res/layout/bottom_sheet_message_actions.xml +++ /dev/null @@ -1,149 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/vector/src/main/res/layout/fragment_message_menu.xml b/vector/src/main/res/layout/fragment_message_menu.xml deleted file mode 100644 index 4538ac935c..0000000000 --- a/vector/src/main/res/layout/fragment_message_menu.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - \ No newline at end of file diff --git a/vector/src/main/res/layout/adapter_item_action.xml b/vector/src/main/res/layout/item_bottom_sheet_action.xml similarity index 97% rename from vector/src/main/res/layout/adapter_item_action.xml rename to vector/src/main/res/layout/item_bottom_sheet_action.xml index 03d2f81115..44145e7bf5 100644 --- a/vector/src/main/res/layout/adapter_item_action.xml +++ b/vector/src/main/res/layout/item_bottom_sheet_action.xml @@ -2,7 +2,7 @@ + diff --git a/vector/src/main/res/layout/item_bottom_sheet_message_preview.xml b/vector/src/main/res/layout/item_bottom_sheet_message_preview.xml new file mode 100644 index 0000000000..a688f38d0f --- /dev/null +++ b/vector/src/main/res/layout/item_bottom_sheet_message_preview.xml @@ -0,0 +1,78 @@ + + + + + + + + + + + diff --git a/vector/src/main/res/layout/item_bottom_sheet_message_status.xml b/vector/src/main/res/layout/item_bottom_sheet_message_status.xml new file mode 100644 index 0000000000..10c129cf58 --- /dev/null +++ b/vector/src/main/res/layout/item_bottom_sheet_message_status.xml @@ -0,0 +1,33 @@ + + + + + + + + diff --git a/vector/src/main/res/layout/adapter_item_action_quick_reaction.xml b/vector/src/main/res/layout/item_bottom_sheet_quick_reaction.xml similarity index 100% rename from vector/src/main/res/layout/adapter_item_action_quick_reaction.xml rename to vector/src/main/res/layout/item_bottom_sheet_quick_reaction.xml