mirror of
https://github.com/element-hq/element-android
synced 2024-11-24 18:35:40 +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.RoomDetailFragment
|
||||||
import im.vector.riotx.features.home.room.detail.readreceipts.DisplayReadReceiptsBottomSheet
|
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.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.filtered.FilteredRoomsActivity
|
||||||
import im.vector.riotx.features.home.room.list.RoomListFragment
|
import im.vector.riotx.features.home.room.list.RoomListFragment
|
||||||
import im.vector.riotx.features.invite.VectorInviteView
|
import im.vector.riotx.features.invite.VectorInviteView
|
||||||
|
@ -103,12 +105,10 @@ interface ScreenComponent {
|
||||||
|
|
||||||
fun inject(messageActionsBottomSheet: MessageActionsBottomSheet)
|
fun inject(messageActionsBottomSheet: MessageActionsBottomSheet)
|
||||||
|
|
||||||
fun inject(viewReactionBottomSheet: ViewReactionBottomSheet)
|
fun inject(viewReactionsBottomSheet: ViewReactionsBottomSheet)
|
||||||
|
|
||||||
fun inject(viewEditHistoryBottomSheet: ViewEditHistoryBottomSheet)
|
fun inject(viewEditHistoryBottomSheet: ViewEditHistoryBottomSheet)
|
||||||
|
|
||||||
fun inject(messageMenuFragment: MessageMenuFragment)
|
|
||||||
|
|
||||||
fun inject(vectorSettingsActivity: VectorSettingsActivity)
|
fun inject(vectorSettingsActivity: VectorSettingsActivity)
|
||||||
|
|
||||||
fun inject(createRoomFragment: CreateRoomFragment)
|
fun inject(createRoomFragment: CreateRoomFragment)
|
||||||
|
@ -135,8 +135,6 @@ interface ScreenComponent {
|
||||||
|
|
||||||
fun inject(sasVerificationIncomingFragment: SASVerificationIncomingFragment)
|
fun inject(sasVerificationIncomingFragment: SASVerificationIncomingFragment)
|
||||||
|
|
||||||
fun inject(quickReactionFragment: QuickReactionFragment)
|
|
||||||
|
|
||||||
fun inject(emojiReactionPickerActivity: EmojiReactionPickerActivity)
|
fun inject(emojiReactionPickerActivity: EmojiReactionPickerActivity)
|
||||||
|
|
||||||
fun inject(loginActivity: LoginActivity)
|
fun inject(loginActivity: LoginActivity)
|
||||||
|
|
|
@ -13,7 +13,7 @@
|
||||||
* See the License for the specific language governing permissions and
|
* See the License for the specific language governing permissions and
|
||||||
* limitations under the License.
|
* 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.content.Context
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
@ -24,7 +24,6 @@ import com.airbnb.mvrx.MvRxViewModelStore
|
||||||
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
|
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
|
||||||
import im.vector.riotx.core.di.DaggerScreenComponent
|
import im.vector.riotx.core.di.DaggerScreenComponent
|
||||||
import im.vector.riotx.core.di.ScreenComponent
|
import im.vector.riotx.core.di.ScreenComponent
|
||||||
import im.vector.riotx.core.platform.VectorBaseActivity
|
|
||||||
import java.util.*
|
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.readreceipts.DisplayReadReceiptsBottomSheet
|
||||||
import im.vector.riotx.features.home.room.detail.timeline.TimelineEventController
|
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.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.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.EventHtmlRenderer
|
||||||
import im.vector.riotx.features.html.PillImageSpan
|
import im.vector.riotx.features.html.PillImageSpan
|
||||||
import im.vector.riotx.features.invite.VectorInviteView
|
import im.vector.riotx.features.invite.VectorInviteView
|
||||||
|
@ -917,7 +919,7 @@ class RoomDetailFragment :
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onLongClickOnReactionPill(informationData: MessageInformationData, reaction: String) {
|
override fun onLongClickOnReactionPill(informationData: MessageInformationData, reaction: String) {
|
||||||
ViewReactionBottomSheet.newInstance(roomDetailArgs.roomId, informationData)
|
ViewReactionsBottomSheet.newInstance(roomDetailArgs.roomId, informationData)
|
||||||
.show(requireActivity().supportFragmentManager, "DISPLAY_REACTIONS")
|
.show(requireActivity().supportFragmentManager, "DISPLAY_REACTIONS")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -970,7 +972,7 @@ class RoomDetailFragment :
|
||||||
startActivityForResult(EmojiReactionPickerActivity.intent(requireContext(), action.eventId), REACTION_SELECT_REQUEST_CODE)
|
startActivityForResult(EmojiReactionPickerActivity.intent(requireContext(), action.eventId), REACTION_SELECT_REQUEST_CODE)
|
||||||
}
|
}
|
||||||
is SimpleAction.ViewReactions -> {
|
is SimpleAction.ViewReactions -> {
|
||||||
ViewReactionBottomSheet.newInstance(roomDetailArgs.roomId, action.messageInformationData)
|
ViewReactionsBottomSheet.newInstance(roomDetailArgs.roomId, action.messageInformationData)
|
||||||
.show(requireActivity().supportFragmentManager, "DISPLAY_REACTIONS")
|
.show(requireActivity().supportFragmentManager, "DISPLAY_REACTIONS")
|
||||||
}
|
}
|
||||||
is SimpleAction.Copy -> {
|
is SimpleAction.Copy -> {
|
||||||
|
|
|
@ -30,7 +30,7 @@ import com.airbnb.mvrx.MvRx
|
||||||
import com.airbnb.mvrx.args
|
import com.airbnb.mvrx.args
|
||||||
import im.vector.riotx.R
|
import im.vector.riotx.R
|
||||||
import im.vector.riotx.core.di.ScreenComponent
|
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 im.vector.riotx.features.home.room.detail.timeline.item.ReadReceiptData
|
||||||
import kotlinx.android.parcel.Parcelize
|
import kotlinx.android.parcel.Parcelize
|
||||||
import kotlinx.android.synthetic.main.bottom_sheet_epoxylist_with_title.*
|
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,93 +21,54 @@ import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import android.widget.FrameLayout
|
import android.widget.FrameLayout
|
||||||
import android.widget.ImageView
|
|
||||||
import android.widget.TextView
|
|
||||||
import androidx.core.view.isVisible
|
|
||||||
import androidx.lifecycle.ViewModelProviders
|
import androidx.lifecycle.ViewModelProviders
|
||||||
import butterknife.BindView
|
|
||||||
import butterknife.ButterKnife
|
|
||||||
import com.airbnb.mvrx.MvRx
|
|
||||||
import com.airbnb.mvrx.fragmentViewModel
|
import com.airbnb.mvrx.fragmentViewModel
|
||||||
import com.airbnb.mvrx.withState
|
import com.airbnb.mvrx.withState
|
||||||
import com.google.android.material.bottomsheet.BottomSheetBehavior
|
import com.google.android.material.bottomsheet.BottomSheetBehavior
|
||||||
import com.google.android.material.bottomsheet.BottomSheetDialog
|
import com.google.android.material.bottomsheet.BottomSheetDialog
|
||||||
|
import im.vector.riotx.EmojiCompatFontProvider
|
||||||
import im.vector.riotx.R
|
import im.vector.riotx.R
|
||||||
import im.vector.riotx.core.di.ScreenComponent
|
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.AvatarRenderer
|
||||||
import im.vector.riotx.features.home.room.detail.timeline.item.MessageInformationData
|
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
|
import javax.inject.Inject
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Bottom sheet fragment that shows a message preview with list of contextual actions
|
* 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 val viewModel: MessageActionsViewModel by fragmentViewModel(MessageActionsViewModel::class)
|
||||||
|
|
||||||
|
private lateinit var messageActionsEpoxyController: MessageActionsEpoxyController
|
||||||
private lateinit var actionHandlerModel: ActionsHandler
|
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) {
|
override fun injectWith(screenComponent: ScreenComponent) {
|
||||||
screenComponent.inject(this)
|
screenComponent.inject(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
||||||
val view = inflater.inflate(R.layout.bottom_sheet_message_actions, container, false)
|
return inflater.inflate(R.layout.bottom_sheet_generic_recycler_epoxy, container, false)
|
||||||
ButterKnife.bind(this, view)
|
|
||||||
return view
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onActivityCreated(savedInstanceState: Bundle?) {
|
override fun onActivityCreated(savedInstanceState: Bundle?) {
|
||||||
super.onActivityCreated(savedInstanceState)
|
super.onActivityCreated(savedInstanceState)
|
||||||
actionHandlerModel = ViewModelProviders.of(requireActivity()).get(ActionsHandler::class.java)
|
actionHandlerModel = ViewModelProviders.of(requireActivity()).get(ActionsHandler::class.java)
|
||||||
|
|
||||||
val cfm = childFragmentManager
|
messageActionsEpoxyController = MessageActionsEpoxyController(requireContext(), avatarRenderer, fontProvider)
|
||||||
var menuActionFragment = cfm.findFragmentByTag("MenuActionFragment") as? MessageMenuFragment
|
bottomSheetEpoxyRecyclerView.setController(messageActionsEpoxyController)
|
||||||
if (menuActionFragment == null) {
|
messageActionsEpoxyController.listener = this
|
||||||
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) {
|
override fun didSelectMenuAction(simpleAction: SimpleAction) {
|
||||||
actionHandlerModel.fireAction(simpleAction)
|
actionHandlerModel.fireAction(simpleAction)
|
||||||
dismiss()
|
dismiss()
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
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 onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||||
val dialog = super.onCreateDialog(savedInstanceState)
|
val dialog = super.onCreateDialog(savedInstanceState)
|
||||||
|
@ -124,32 +85,7 @@ class MessageActionsBottomSheet : VectorBaseBottomSheetDialogFragment() {
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun invalidate() = withState(viewModel) {
|
override fun invalidate() = withState(viewModel) {
|
||||||
val body = viewModel.resolveBody(it)
|
messageActionsEpoxyController.setData(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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
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 dagger.Lazy
|
||||||
import im.vector.matrix.android.api.session.Session
|
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.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.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.MessageTextContent
|
||||||
import im.vector.matrix.android.api.session.room.model.message.MessageType
|
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.TimelineEvent
|
||||||
import im.vector.matrix.android.api.session.room.timeline.getLastMessageContent
|
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.RxRoom
|
||||||
import im.vector.matrix.rx.unwrap
|
import im.vector.matrix.rx.unwrap
|
||||||
|
import im.vector.riotx.R
|
||||||
import im.vector.riotx.core.extensions.canReact
|
import im.vector.riotx.core.extensions.canReact
|
||||||
import im.vector.riotx.core.platform.VectorViewModel
|
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.format.NoticeEventFormatter
|
||||||
import im.vector.riotx.features.home.room.detail.timeline.item.MessageInformationData
|
import im.vector.riotx.features.home.room.detail.timeline.item.MessageInformationData
|
||||||
import im.vector.riotx.features.html.EventHtmlRenderer
|
import im.vector.riotx.features.html.EventHtmlRenderer
|
||||||
import java.text.SimpleDateFormat
|
import java.text.SimpleDateFormat
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Quick reactions state
|
||||||
|
*/
|
||||||
|
data class ToggleState(
|
||||||
|
val reaction: String,
|
||||||
|
val isSelected: Boolean
|
||||||
|
)
|
||||||
|
|
||||||
data class MessageActionState(
|
data class MessageActionState(
|
||||||
val roomId: String,
|
val roomId: String,
|
||||||
val eventId: String,
|
val eventId: String,
|
||||||
val informationData: MessageInformationData,
|
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 {
|
) : MvRxState {
|
||||||
|
|
||||||
constructor(args: TimelineEventFragmentArgs) : this(roomId = args.roomId, eventId = args.eventId, informationData = args.informationData)
|
constructor(args: TimelineEventFragmentArgs) : this(roomId = args.roomId, eventId = args.eventId, informationData = args.informationData)
|
||||||
|
@ -49,17 +70,92 @@ data class MessageActionState(
|
||||||
|
|
||||||
fun senderName(): String = informationData.memberName?.toString() ?: ""
|
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 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()) {
|
return when (timelineEvent()?.root?.getClearType()) {
|
||||||
EventType.MESSAGE -> {
|
EventType.MESSAGE -> {
|
||||||
val messageContent: MessageContent? = timelineEvent()?.getLastMessageContent()
|
val messageContent: MessageContent? = timelineEvent()?.getLastMessageContent()
|
||||||
if (messageContent is MessageTextContent && messageContent.format == MessageType.FORMAT_MATRIX_HTML) {
|
if (messageContent is MessageTextContent && messageContent.format == MessageType.FORMAT_MATRIX_HTML) {
|
||||||
eventHtmlRenderer?.render(messageContent.formattedBody
|
eventHtmlRenderer.get().render(messageContent.formattedBody
|
||||||
?: messageContent.body)
|
?: messageContent.body)
|
||||||
} else {
|
} else {
|
||||||
messageContent?.body
|
messageContent?.body
|
||||||
|
@ -72,54 +168,177 @@ data class MessageActionState(
|
||||||
EventType.CALL_INVITE,
|
EventType.CALL_INVITE,
|
||||||
EventType.CALL_HANGUP,
|
EventType.CALL_HANGUP,
|
||||||
EventType.CALL_ANSWER -> {
|
EventType.CALL_ANSWER -> {
|
||||||
timelineEvent()?.let { noticeEventFormatter?.format(it) }
|
timelineEvent()?.let { noticeEventFormatter.format(it) }
|
||||||
}
|
}
|
||||||
else -> null
|
else -> null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
private fun actionsForEvent(optionalEvent: Optional<TimelineEvent>): List<SimpleAction> {
|
||||||
* Information related to an event and used to display preview in contextual bottomsheet.
|
val event = optionalEvent.getOrNull() ?: return emptyList()
|
||||||
*/
|
|
||||||
class MessageActionsViewModel @AssistedInject constructor(@Assisted
|
|
||||||
initialState: MessageActionState,
|
|
||||||
private val eventHtmlRenderer: Lazy<EventHtmlRenderer>,
|
|
||||||
session: Session,
|
|
||||||
private val noticeEventFormatter: NoticeEventFormatter
|
|
||||||
) : VectorViewModel<MessageActionState>(initialState) {
|
|
||||||
|
|
||||||
private val eventId = initialState.eventId
|
val messageContent: MessageContent? = event.annotations?.editSummary?.aggregatedContent.toModel()
|
||||||
private val room = session.getRoom(initialState.roomId)
|
?: event.root.getClearContent().toModel()
|
||||||
|
val type = messageContent?.type
|
||||||
|
|
||||||
@AssistedInject.Factory
|
return arrayListOf<SimpleAction>().apply {
|
||||||
interface Factory {
|
if (event.root.sendState.hasFailed()) {
|
||||||
fun create(initialState: MessageActionState): MessageActionsViewModel
|
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? {
|
if (canRedact(event, session.myUserId)) {
|
||||||
val fragment: MessageActionsBottomSheet = (viewModelContext as FragmentViewModelContext).fragment()
|
add(SimpleAction.Delete(eventId))
|
||||||
return fragment.messageActionViewModelFactory.create(state)
|
}
|
||||||
|
|
||||||
|
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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
init {
|
add(SimpleAction.ViewSource(event.root.toContentStringWithIndent()))
|
||||||
observeEvent()
|
if (event.isEncrypted()) {
|
||||||
|
val decryptedContent = event.root.toClearContentStringWithIndent()
|
||||||
|
?: stringProvider.getString(R.string.encryption_information_decryption_error)
|
||||||
|
add(SimpleAction.ViewDecryptedSource(decryptedContent))
|
||||||
}
|
}
|
||||||
|
add(SimpleAction.CopyPermalink(eventId))
|
||||||
|
|
||||||
private fun observeEvent() {
|
if (session.myUserId != event.root.senderId && event.root.getClearType() == EventType.MESSAGE) {
|
||||||
if (room == null) return
|
// not sent by me
|
||||||
RxRoom(room)
|
add(SimpleAction.Flag(eventId))
|
||||||
.liveTimelineEvent(eventId)
|
}
|
||||||
.unwrap()
|
}
|
||||||
.execute {
|
|
||||||
copy(timelineEvent = it)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun resolveBody(state: MessageActionState): CharSequence? {
|
private fun canCancel(@Suppress("UNUSED_PARAMETER") event: TimelineEvent): Boolean {
|
||||||
return state.messageBody(eventHtmlRenderer.get(), noticeEventFormatter)
|
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,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
|
* See the License for the specific language governing permissions and
|
||||||
* limitations under the License.
|
* 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.os.Bundle
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
|
@ -29,6 +29,8 @@ import com.airbnb.mvrx.fragmentViewModel
|
||||||
import com.airbnb.mvrx.withState
|
import com.airbnb.mvrx.withState
|
||||||
import im.vector.riotx.R
|
import im.vector.riotx.R
|
||||||
import im.vector.riotx.core.di.ScreenComponent
|
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.home.room.detail.timeline.item.MessageInformationData
|
||||||
import im.vector.riotx.features.html.EventHtmlRenderer
|
import im.vector.riotx.features.html.EventHtmlRenderer
|
||||||
import kotlinx.android.synthetic.main.bottom_sheet_epoxylist_with_title.*
|
import kotlinx.android.synthetic.main.bottom_sheet_epoxylist_with_title.*
|
|
@ -13,7 +13,7 @@
|
||||||
* See the License for the specific language governing permissions and
|
* See the License for the specific language governing permissions and
|
||||||
* limitations under the License.
|
* 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.content.Context
|
||||||
import android.text.Spannable
|
import android.text.Spannable
|
||||||
|
@ -41,7 +41,7 @@ import timber.log.Timber
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Epoxy controller for reaction event list
|
* Epoxy controller for edit history list
|
||||||
*/
|
*/
|
||||||
class ViewEditHistoryEpoxyController(private val context: Context,
|
class ViewEditHistoryEpoxyController(private val context: Context,
|
||||||
val dateFormatter: VectorDateFormatter,
|
val dateFormatter: VectorDateFormatter,
|
|
@ -13,7 +13,7 @@
|
||||||
* See the License for the specific language governing permissions and
|
* See the License for the specific language governing permissions and
|
||||||
* limitations under the License.
|
* 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.airbnb.mvrx.*
|
||||||
import com.squareup.inject.assisted.Assisted
|
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.matrix.android.internal.crypto.algorithms.olm.OlmDecryptionResult
|
||||||
import im.vector.riotx.core.platform.VectorViewModel
|
import im.vector.riotx.core.platform.VectorViewModel
|
||||||
import im.vector.riotx.core.date.VectorDateFormatter
|
import im.vector.riotx.core.date.VectorDateFormatter
|
||||||
|
import im.vector.riotx.features.home.room.detail.timeline.action.TimelineEventFragmentArgs
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
|
|
@ -14,7 +14,7 @@
|
||||||
* limitations under the License.
|
* 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 android.widget.TextView
|
||||||
import androidx.core.view.isVisible
|
import androidx.core.view.isVisible
|
|
@ -14,7 +14,7 @@
|
||||||
* limitations under the License.
|
* 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.os.Bundle
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
|
@ -30,6 +30,8 @@ import com.airbnb.mvrx.fragmentViewModel
|
||||||
import com.airbnb.mvrx.withState
|
import com.airbnb.mvrx.withState
|
||||||
import im.vector.riotx.R
|
import im.vector.riotx.R
|
||||||
import im.vector.riotx.core.di.ScreenComponent
|
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.home.room.detail.timeline.item.MessageInformationData
|
||||||
import kotlinx.android.synthetic.main.bottom_sheet_epoxylist_with_title.*
|
import kotlinx.android.synthetic.main.bottom_sheet_epoxylist_with_title.*
|
||||||
import javax.inject.Inject
|
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
|
* 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)
|
@BindView(R.id.bottom_sheet_display_reactions_list)
|
||||||
lateinit var epoxyRecyclerView: EpoxyRecyclerView
|
lateinit var epoxyRecyclerView: EpoxyRecyclerView
|
||||||
|
@ -72,7 +74,7 @@ class ViewReactionBottomSheet : VectorBaseBottomSheetDialogFragment() {
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
fun newInstance(roomId: String, informationData: MessageInformationData): ViewReactionBottomSheet {
|
fun newInstance(roomId: String, informationData: MessageInformationData): ViewReactionsBottomSheet {
|
||||||
val args = Bundle()
|
val args = Bundle()
|
||||||
val parcelableArgs = TimelineEventFragmentArgs(
|
val parcelableArgs = TimelineEventFragmentArgs(
|
||||||
informationData.eventId,
|
informationData.eventId,
|
||||||
|
@ -80,7 +82,7 @@ class ViewReactionBottomSheet : VectorBaseBottomSheetDialogFragment() {
|
||||||
informationData
|
informationData
|
||||||
)
|
)
|
||||||
args.putParcelable(MvRx.KEY_ARG, parcelableArgs)
|
args.putParcelable(MvRx.KEY_ARG, parcelableArgs)
|
||||||
return ViewReactionBottomSheet().apply { arguments = args }
|
return ViewReactionsBottomSheet().apply { arguments = args }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -14,7 +14,7 @@
|
||||||
* limitations under the License.
|
* 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.epoxy.TypedEpoxyController
|
||||||
import com.airbnb.mvrx.Fail
|
import com.airbnb.mvrx.Fail
|
|
@ -14,7 +14,7 @@
|
||||||
* limitations under the License.
|
* 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.Async
|
||||||
import com.airbnb.mvrx.FragmentViewModelContext
|
import com.airbnb.mvrx.FragmentViewModelContext
|
||||||
|
@ -30,6 +30,7 @@ import im.vector.matrix.rx.RxRoom
|
||||||
import im.vector.matrix.rx.unwrap
|
import im.vector.matrix.rx.unwrap
|
||||||
import im.vector.riotx.core.platform.VectorViewModel
|
import im.vector.riotx.core.platform.VectorViewModel
|
||||||
import im.vector.riotx.core.date.VectorDateFormatter
|
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.Observable
|
||||||
import io.reactivex.Single
|
import io.reactivex.Single
|
||||||
|
|
||||||
|
@ -53,7 +54,7 @@ data class ReactionInfo(
|
||||||
/**
|
/**
|
||||||
* Used to display the list of members that reacted to a given event
|
* 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,
|
initialState: DisplayReactionsViewState,
|
||||||
private val session: Session,
|
private val session: Session,
|
||||||
private val dateFormatter: VectorDateFormatter
|
private val dateFormatter: VectorDateFormatter
|
||||||
|
@ -66,14 +67,14 @@ class ViewReactionViewModel @AssistedInject constructor(@Assisted
|
||||||
|
|
||||||
@AssistedInject.Factory
|
@AssistedInject.Factory
|
||||||
interface 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? {
|
override fun create(viewModelContext: ViewModelContext, state: DisplayReactionsViewState): ViewReactionsViewModel? {
|
||||||
val fragment: ViewReactionBottomSheet = (viewModelContext as FragmentViewModelContext).fragment()
|
val fragment: ViewReactionsBottomSheet = (viewModelContext as FragmentViewModelContext).fragment()
|
||||||
return fragment.viewReactionViewModelFactory.create(state)
|
return fragment.viewReactionsViewModelFactory.create(state)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -25,7 +25,6 @@
|
||||||
android:orientation="vertical"
|
android:orientation="vertical"
|
||||||
android:scrollbars="vertical"
|
android:scrollbars="vertical"
|
||||||
tools:itemCount="15"
|
tools:itemCount="15"
|
||||||
tools:listitem="@layout/item_simple_reaction_info">
|
tools:listitem="@layout/item_simple_reaction_info" />
|
||||||
|
|
||||||
</com.airbnb.epoxy.EpoxyRecyclerView>
|
|
||||||
</LinearLayout>
|
</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"
|
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="wrap_content"
|
||||||
android:clickable="true"
|
android:clickable="true"
|
||||||
android:focusable="true"
|
android:focusable="true"
|
||||||
android:foreground="?attr/selectableItemBackground"
|
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