mirror of
https://github.com/SchildiChat/SchildiChat-android.git
synced 2024-11-27 03:49:04 +03:00
Rework message menu bottom sheet: remove sub Fragment and use Epoxy
- Also move some class to some dedicated package
This commit is contained in:
parent
723a007c39
commit
1dacfa6744
33 changed files with 872 additions and 876 deletions
|
@ -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)
|
||||
|
|
|
@ -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.*
|
||||
|
||||
/**
|
|
@ -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 -> {
|
||||
|
|
|
@ -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.*
|
||||
|
|
|
@ -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<BottomSheetItemAction.Holder>() {
|
||||
|
||||
@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<ImageView>(R.id.action_icon)
|
||||
val text by bind<TextView>(R.id.action_title)
|
||||
}
|
||||
}
|
|
@ -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<BottomSheetItemMessagePreview.Holder>() {
|
||||
|
||||
@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<ImageView>(R.id.bottom_sheet_message_preview_avatar)
|
||||
val sender by bind<TextView>(R.id.bottom_sheet_message_preview_sender)
|
||||
val body by bind<TextView>(R.id.bottom_sheet_message_preview_body)
|
||||
val timestamp by bind<TextView>(R.id.bottom_sheet_message_preview_timestamp)
|
||||
}
|
||||
}
|
|
@ -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<BottomSheetItemQuickReactions.Holder>() {
|
||||
|
||||
@EpoxyAttribute
|
||||
lateinit var fontProvider: EmojiCompatFontProvider
|
||||
@EpoxyAttribute
|
||||
lateinit var texts: List<String>
|
||||
@EpoxyAttribute
|
||||
lateinit var selecteds: List<Boolean>
|
||||
@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<TextView>(R.id.quickReaction0)
|
||||
private val quickReaction1 by bind<TextView>(R.id.quickReaction1)
|
||||
private val quickReaction2 by bind<TextView>(R.id.quickReaction2)
|
||||
private val quickReaction3 by bind<TextView>(R.id.quickReaction3)
|
||||
private val quickReaction4 by bind<TextView>(R.id.quickReaction4)
|
||||
private val quickReaction5 by bind<TextView>(R.id.quickReaction5)
|
||||
private val quickReaction6 by bind<TextView>(R.id.quickReaction6)
|
||||
private val quickReaction7 by bind<TextView>(R.id.quickReaction7)
|
||||
|
||||
val textViews
|
||||
get() = listOf(
|
||||
quickReaction0,
|
||||
quickReaction1,
|
||||
quickReaction2,
|
||||
quickReaction3,
|
||||
quickReaction4,
|
||||
quickReaction5,
|
||||
quickReaction6,
|
||||
quickReaction7
|
||||
)
|
||||
}
|
||||
|
||||
interface Listener {
|
||||
fun didSelect(emoji: String, selected: Boolean)
|
||||
}
|
||||
}
|
|
@ -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<BottomSheetItemSendState.Holder>() {
|
||||
|
||||
@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<View>(R.id.messageStatusProgress)
|
||||
val text by bind<TextView>(R.id.messageStatusText)
|
||||
}
|
||||
}
|
|
@ -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<BottomSheetItemSeparator.Holder>() {
|
||||
|
||||
class Holder : VectorEpoxyHolder()
|
||||
}
|
|
@ -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 {
|
||||
|
|
|
@ -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<MessageActionState>() {
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
|
@ -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<TimelineEvent> = Uninitialized
|
||||
val timelineEvent: Async<TimelineEvent> = Uninitialized,
|
||||
val messageBody: CharSequence? = null,
|
||||
// For quick reactions
|
||||
val quickStates: Async<List<ToggleState>> = Uninitialized,
|
||||
// For actions
|
||||
val actions: Async<List<SimpleAction>> = 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<EventHtmlRenderer>,
|
||||
private val session: Session,
|
||||
private val noticeEventFormatter: NoticeEventFormatter,
|
||||
private val stringProvider: StringProvider
|
||||
) : VectorViewModel<MessageActionState>(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<MessageActionsViewModel, MessageActionState> {
|
||||
|
||||
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<TimelineEvent>): 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<EventHtmlRenderer>,
|
||||
session: Session,
|
||||
private val noticeEventFormatter: NoticeEventFormatter
|
||||
) : VectorViewModel<MessageActionState>(initialState) {
|
||||
private fun actionsForEvent(optionalEvent: Optional<TimelineEvent>): List<SimpleAction> {
|
||||
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<SimpleAction>().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<MessageActionsViewModel, MessageActionState> {
|
||||
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<MessageContent>()
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<ImageView>(R.id.action_icon)?.setImageResource(action.iconResId)
|
||||
findViewById<TextView>(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
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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<List<SimpleAction>> = 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<MessageMenuState>(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<MessageMenuViewModel, MessageMenuState> {
|
||||
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<TimelineEvent>): List<SimpleAction> {
|
||||
val event = optionalEvent.getOrNull() ?: return emptyList()
|
||||
|
||||
val messageContent: MessageContent? = event.annotations?.editSummary?.aggregatedContent.toModel()
|
||||
?: event.root.getClearContent().toModel()
|
||||
val type = messageContent?.type
|
||||
|
||||
return arrayListOf<SimpleAction>().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<MessageContent>()
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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<TextView>
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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<List<ToggleState>> = 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<QuickReactionState>(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<QuickReactionViewModel, QuickReactionState> {
|
||||
|
||||
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))
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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.*
|
|
@ -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,
|
|
@ -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.*
|
||||
|
|
@ -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
|
|
@ -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 }
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
|
@ -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<DisplayReactionsViewState>(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<ViewReactionViewModel, DisplayReactionsViewState> {
|
||||
companion object : MvRxViewModelFactory<ViewReactionsViewModel, DisplayReactionsViewState> {
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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" />
|
||||
|
||||
</com.airbnb.epoxy.EpoxyRecyclerView>
|
||||
</LinearLayout>
|
|
@ -0,0 +1,22 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
|
||||
<!-- Min height, else the recycler view is not rendered -->
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:minHeight="400dp"
|
||||
android:orientation="vertical">
|
||||
|
||||
<com.airbnb.epoxy.EpoxyRecyclerView
|
||||
android:id="@+id/bottomSheetEpoxyRecyclerView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:layout_weight="1"
|
||||
android:fadeScrollbars="false"
|
||||
android:orientation="vertical"
|
||||
android:scrollbars="vertical"
|
||||
tools:itemCount="5"
|
||||
tools:listitem="@layout/item_bottom_sheet_action" />
|
||||
|
||||
</LinearLayout>
|
|
@ -1,149 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.core.widget.NestedScrollView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
app:layout_behavior="@string/bottom_sheet_behavior">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical">
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:id="@+id/bottom_sheet_message_preview"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/bottom_sheet_message_preview_avatar"
|
||||
android:layout_width="60dp"
|
||||
android:layout_height="60dp"
|
||||
android:layout_margin="@dimen/layout_horizontal_margin"
|
||||
android:adjustViewBounds="true"
|
||||
android:background="@drawable/circle"
|
||||
android:contentDescription="@string/avatar"
|
||||
android:scaleType="centerCrop"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintVertical_bias="0"
|
||||
tools:src="@tools:sample/avatars" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/bottom_sheet_message_preview_sender"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="start"
|
||||
android:layout_marginStart="8dp"
|
||||
android:layout_marginLeft="8dp"
|
||||
android:layout_marginEnd="@dimen/layout_horizontal_margin"
|
||||
android:layout_marginRight="@dimen/layout_horizontal_margin"
|
||||
android:ellipsize="end"
|
||||
android:fontFamily="sans-serif-bold"
|
||||
android:singleLine="true"
|
||||
android:textColor="?riotx_text_primary"
|
||||
android:textSize="14sp"
|
||||
android:textStyle="bold"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@id/bottom_sheet_message_preview_avatar"
|
||||
app:layout_constraintTop_toTopOf="@id/bottom_sheet_message_preview_avatar"
|
||||
tools:text="@tools:sample/full_names" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/bottom_sheet_message_preview_body"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="start"
|
||||
android:layout_marginStart="8dp"
|
||||
android:layout_marginLeft="8dp"
|
||||
android:layout_marginEnd="@dimen/layout_horizontal_margin"
|
||||
android:layout_marginRight="@dimen/layout_horizontal_margin"
|
||||
android:layout_marginBottom="4dp"
|
||||
android:ellipsize="end"
|
||||
android:maxLines="3"
|
||||
android:textColor="?riotx_text_secondary"
|
||||
android:textIsSelectable="false"
|
||||
android:textSize="14sp"
|
||||
app:layout_constraintBottom_toTopOf="@id/bottom_sheet_message_preview_timestamp"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@id/bottom_sheet_message_preview_avatar"
|
||||
app:layout_constraintTop_toBottomOf="@id/bottom_sheet_message_preview_sender"
|
||||
tools:text="Quis harum id autem cumque consequatur laboriosam aliquam sed. Sint accusamus dignissimos nobis ullam earum debitis aspernatur. Sint accusamus dignissimos nobis ullam earum debitis aspernatur. " />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/bottom_sheet_message_preview_timestamp"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginEnd="@dimen/layout_horizontal_margin"
|
||||
android:layout_marginRight="@dimen/layout_horizontal_margin"
|
||||
android:layout_marginBottom="8dp"
|
||||
android:textColor="?riotx_text_secondary"
|
||||
android:textSize="12sp"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/bottom_sheet_message_preview_body"
|
||||
tools:text="Friday 8pm" />
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/messageStatusInfo"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginBottom="4dp"
|
||||
android:layout_marginEnd="16dp">
|
||||
|
||||
<ProgressBar
|
||||
android:id="@+id/messageStatusProgress"
|
||||
style="?android:attr/progressBarStyleSmall"
|
||||
android:layout_width="12dp"
|
||||
android:layout_height="12dp"
|
||||
android:layout_gravity="center"
|
||||
android:layout_marginEnd="4dp"
|
||||
android:visibility="gone"
|
||||
tools:visibility="visible" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/messageStatusText"
|
||||
android:textColor="?riotx_text_secondary"
|
||||
android:textStyle="bold"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:layout_weight="1"
|
||||
android:drawableStart="@drawable/ic_warning_small"
|
||||
android:drawablePadding="4dp"
|
||||
tools:text="@string/unable_to_send_message" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<View
|
||||
android:id="@+id/quickReactTopDivider"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="1dp"
|
||||
android:background="?attr/vctr_list_divider_color" />
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/bottom_sheet_quick_reaction_container"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
tools:background="@android:color/holo_green_light"
|
||||
tools:layout_height="180dp" />
|
||||
|
||||
<View
|
||||
android:id="@+id/quickReactBottomDivider"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="1dp"
|
||||
android:background="?attr/vctr_list_divider_color" />
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/bottom_sheet_menu_container"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
tools:background="@android:color/holo_blue_dark"
|
||||
tools:layout_height="250dp" />
|
||||
|
||||
</LinearLayout>
|
||||
</androidx.core.widget.NestedScrollView>
|
|
@ -1,5 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:orientation="vertical" android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"/>
|
|
@ -2,7 +2,7 @@
|
|||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:clickable="true"
|
||||
android:focusable="true"
|
||||
android:foreground="?attr/selectableItemBackground"
|
6
vector/src/main/res/layout/item_bottom_sheet_divider.xml
Normal file
6
vector/src/main/res/layout/item_bottom_sheet_divider.xml
Normal file
|
@ -0,0 +1,6 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<View xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:id="@+id/quickReactTopDivider"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="1dp"
|
||||
android:background="?attr/vctr_list_divider_color" />
|
|
@ -0,0 +1,78 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:id="@+id/bottom_sheet_message_preview"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/bottom_sheet_message_preview_avatar"
|
||||
android:layout_width="60dp"
|
||||
android:layout_height="60dp"
|
||||
android:layout_margin="@dimen/layout_horizontal_margin"
|
||||
android:adjustViewBounds="true"
|
||||
android:background="@drawable/circle"
|
||||
android:contentDescription="@string/avatar"
|
||||
android:scaleType="centerCrop"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintVertical_bias="0"
|
||||
tools:src="@tools:sample/avatars" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/bottom_sheet_message_preview_sender"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="start"
|
||||
android:layout_marginStart="8dp"
|
||||
android:layout_marginLeft="8dp"
|
||||
android:layout_marginEnd="@dimen/layout_horizontal_margin"
|
||||
android:layout_marginRight="@dimen/layout_horizontal_margin"
|
||||
android:ellipsize="end"
|
||||
android:fontFamily="sans-serif-bold"
|
||||
android:singleLine="true"
|
||||
android:textColor="?riotx_text_primary"
|
||||
android:textSize="14sp"
|
||||
android:textStyle="bold"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@id/bottom_sheet_message_preview_avatar"
|
||||
app:layout_constraintTop_toTopOf="@id/bottom_sheet_message_preview_avatar"
|
||||
tools:text="@tools:sample/full_names" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/bottom_sheet_message_preview_body"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="start"
|
||||
android:layout_marginStart="8dp"
|
||||
android:layout_marginLeft="8dp"
|
||||
android:layout_marginEnd="@dimen/layout_horizontal_margin"
|
||||
android:layout_marginRight="@dimen/layout_horizontal_margin"
|
||||
android:layout_marginBottom="4dp"
|
||||
android:ellipsize="end"
|
||||
android:maxLines="3"
|
||||
android:textColor="?riotx_text_secondary"
|
||||
android:textIsSelectable="false"
|
||||
android:textSize="14sp"
|
||||
app:layout_constraintBottom_toTopOf="@id/bottom_sheet_message_preview_timestamp"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@id/bottom_sheet_message_preview_avatar"
|
||||
app:layout_constraintTop_toBottomOf="@id/bottom_sheet_message_preview_sender"
|
||||
tools:text="Quis harum id autem cumque consequatur laboriosam aliquam sed. Sint accusamus dignissimos nobis ullam earum debitis aspernatur. Sint accusamus dignissimos nobis ullam earum debitis aspernatur. " />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/bottom_sheet_message_preview_timestamp"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginEnd="@dimen/layout_horizontal_margin"
|
||||
android:layout_marginRight="@dimen/layout_horizontal_margin"
|
||||
android:layout_marginBottom="8dp"
|
||||
android:textColor="?riotx_text_secondary"
|
||||
android:textSize="12sp"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/bottom_sheet_message_preview_body"
|
||||
tools:text="Friday 8pm" />
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
@ -0,0 +1,33 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:id="@+id/messageStatusInfo"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:layout_marginBottom="4dp">
|
||||
|
||||
<ProgressBar
|
||||
android:id="@+id/messageStatusProgress"
|
||||
style="?android:attr/progressBarStyleSmall"
|
||||
android:layout_width="12dp"
|
||||
android:layout_height="12dp"
|
||||
android:layout_gravity="center"
|
||||
android:layout_marginEnd="4dp"
|
||||
android:visibility="gone"
|
||||
tools:visibility="visible" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/messageStatusText"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:layout_weight="1"
|
||||
android:drawableStart="@drawable/ic_warning_small"
|
||||
android:drawablePadding="4dp"
|
||||
android:textColor="?riotx_text_secondary"
|
||||
android:textStyle="bold"
|
||||
tools:text="@string/unable_to_send_message" />
|
||||
|
||||
</LinearLayout>
|
Loading…
Reference in a new issue