forked from GitHub-Mirror/riotX-android
Merge branch 'develop' into feature/home_rework
This commit is contained in:
@ -88,7 +88,6 @@ abstract class VectorBaseActivity : BaseMvRxActivity() {
|
||||
}
|
||||
|
||||
protected fun Disposable.disposeOnDestroy(): Disposable {
|
||||
// TODO Ganfra: never disposed...
|
||||
uiDisposables.add(this)
|
||||
return this
|
||||
}
|
||||
@ -128,6 +127,8 @@ abstract class VectorBaseActivity : BaseMvRxActivity() {
|
||||
|
||||
unBinder?.unbind()
|
||||
unBinder = null
|
||||
|
||||
uiDisposables.dispose()
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
|
@ -20,11 +20,13 @@ import android.text.Spannable
|
||||
import com.otaliastudios.autocomplete.AutocompletePolicy
|
||||
|
||||
class CommandAutocompletePolicy : AutocompletePolicy {
|
||||
|
||||
var enabled: Boolean = true
|
||||
|
||||
override fun getQuery(text: Spannable): CharSequence {
|
||||
if (text.length > 0) {
|
||||
return text.substring(1, text.length)
|
||||
}
|
||||
|
||||
// Should not happen
|
||||
return ""
|
||||
}
|
||||
@ -34,7 +36,7 @@ class CommandAutocompletePolicy : AutocompletePolicy {
|
||||
|
||||
// Only if text which starts with '/' and without space
|
||||
override fun shouldShowPopup(text: Spannable?, cursorPos: Int): Boolean {
|
||||
return text?.startsWith("/") == true
|
||||
return enabled && text?.startsWith("/") == true
|
||||
&& !text.contains(" ")
|
||||
}
|
||||
|
||||
|
@ -72,9 +72,10 @@ class HomeModule {
|
||||
val timelineMediaSizeProvider = TimelineMediaSizeProvider()
|
||||
val colorProvider = ColorProvider(fragment.requireContext())
|
||||
val timelineDateFormatter = get<TimelineDateFormatter>()
|
||||
val messageItemFactory = MessageItemFactory(colorProvider, timelineMediaSizeProvider, timelineDateFormatter, eventHtmlRenderer, get())
|
||||
|
||||
val timelineItemFactory = TimelineItemFactory(
|
||||
messageItemFactory = MessageItemFactory(colorProvider, timelineMediaSizeProvider, timelineDateFormatter, eventHtmlRenderer),
|
||||
messageItemFactory = messageItemFactory,
|
||||
noticeItemFactory = NoticeItemFactory(noticeEventFormatter),
|
||||
defaultItemFactory = DefaultItemFactory()
|
||||
)
|
||||
|
@ -23,17 +23,21 @@ import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
|
||||
|
||||
sealed class RoomDetailActions {
|
||||
|
||||
data class SendMessage(val text: String) : RoomDetailActions()
|
||||
data class SendMessage(val text: String, val autoMarkdown: Boolean) : RoomDetailActions()
|
||||
data class SendMedia(val mediaFiles: List<MediaFile>) : RoomDetailActions()
|
||||
data class EventDisplayed(val event: TimelineEvent) : RoomDetailActions()
|
||||
data class LoadMore(val direction: Timeline.Direction) : RoomDetailActions()
|
||||
data class SendReaction(val reaction: String, val targetEventId: String) : RoomDetailActions()
|
||||
data class RedactAction(val targetEventId: String, val reason: String? = "") : RoomDetailActions()
|
||||
data class UndoReaction(val targetEventId: String, val key: String, val reason: String? = "") : RoomDetailActions()
|
||||
data class UpdateQuickReactAction(val targetEventId: String,val selectedReaction: String,val opposite: String) : RoomDetailActions()
|
||||
data class ShowEditHistoryAction(val event: String,val editAggregatedSummary: EditAggregatedSummary) : RoomDetailActions()
|
||||
data class UpdateQuickReactAction(val targetEventId: String, val selectedReaction: String, val opposite: String) : RoomDetailActions()
|
||||
data class ShowEditHistoryAction(val event: String, val editAggregatedSummary: EditAggregatedSummary) : RoomDetailActions()
|
||||
object AcceptInvite : RoomDetailActions()
|
||||
object RejectInvite : RoomDetailActions()
|
||||
|
||||
data class EnterEditMode(val eventId: String) : RoomDetailActions()
|
||||
data class EnterQuoteMode(val eventId: String) : RoomDetailActions()
|
||||
data class EnterReplyMode(val eventId: String) : RoomDetailActions()
|
||||
|
||||
|
||||
}
|
@ -40,6 +40,7 @@ import androidx.lifecycle.Observer
|
||||
import androidx.lifecycle.ViewModelProviders
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import butterknife.BindView
|
||||
import com.airbnb.epoxy.EpoxyVisibilityTracker
|
||||
import com.airbnb.mvrx.args
|
||||
import com.airbnb.mvrx.fragmentViewModel
|
||||
@ -53,6 +54,7 @@ import com.otaliastudios.autocomplete.Autocomplete
|
||||
import com.otaliastudios.autocomplete.AutocompleteCallback
|
||||
import com.otaliastudios.autocomplete.CharPolicy
|
||||
import im.vector.matrix.android.api.session.Session
|
||||
import im.vector.matrix.android.api.session.events.model.toModel
|
||||
import im.vector.matrix.android.api.session.room.model.EditAggregatedSummary
|
||||
import im.vector.matrix.android.api.session.room.model.Membership
|
||||
import im.vector.matrix.android.api.session.room.model.message.*
|
||||
@ -74,6 +76,7 @@ import im.vector.riotredesign.features.home.AvatarRenderer
|
||||
import im.vector.riotredesign.features.home.HomeModule
|
||||
import im.vector.riotredesign.features.home.HomePermalinkHandler
|
||||
import im.vector.riotredesign.features.home.room.detail.composer.TextComposerActions
|
||||
import im.vector.riotredesign.features.home.room.detail.composer.TextComposerView
|
||||
import im.vector.riotredesign.features.home.room.detail.composer.TextComposerViewModel
|
||||
import im.vector.riotredesign.features.home.room.detail.composer.TextComposerViewState
|
||||
import im.vector.riotredesign.features.home.room.detail.timeline.TimelineEventController
|
||||
@ -89,12 +92,17 @@ import im.vector.riotredesign.features.media.ImageMediaViewerActivity
|
||||
import im.vector.riotredesign.features.media.VideoContentRenderer
|
||||
import im.vector.riotredesign.features.media.VideoMediaViewerActivity
|
||||
import im.vector.riotredesign.features.reactions.EmojiReactionPickerActivity
|
||||
import im.vector.riotredesign.features.settings.PreferencesManager
|
||||
import kotlinx.android.parcel.Parcelize
|
||||
import kotlinx.android.synthetic.main.fragment_room_detail.*
|
||||
import kotlinx.android.synthetic.main.merge_composer_layout.view.*
|
||||
import org.commonmark.parser.Parser
|
||||
import org.koin.android.ext.android.inject
|
||||
import org.koin.android.scope.ext.android.bindScope
|
||||
import org.koin.android.scope.ext.android.getOrCreateScope
|
||||
import org.koin.core.parameter.parametersOf
|
||||
import ru.noties.markwon.Markwon
|
||||
import ru.noties.markwon.html.HtmlPlugin
|
||||
import timber.log.Timber
|
||||
import java.io.File
|
||||
|
||||
@ -132,13 +140,12 @@ class RoomDetailFragment :
|
||||
* @return the sanitized display name
|
||||
*/
|
||||
fun sanitizeDisplayname(displayName: String): String? {
|
||||
var displayName = displayName
|
||||
// sanity checks
|
||||
if (!TextUtils.isEmpty(displayName)) {
|
||||
val ircPattern = " (IRC)"
|
||||
|
||||
if (displayName.endsWith(ircPattern)) {
|
||||
displayName = displayName.substring(0, displayName.length - ircPattern.length)
|
||||
return displayName.substring(0, displayName.length - ircPattern.length)
|
||||
}
|
||||
}
|
||||
|
||||
@ -155,6 +162,7 @@ class RoomDetailFragment :
|
||||
private val roomDetailViewModel: RoomDetailViewModel by fragmentViewModel()
|
||||
private val textComposerViewModel: TextComposerViewModel by fragmentViewModel()
|
||||
private val timelineEventController: TimelineEventController by inject { parametersOf(this) }
|
||||
private val commandAutocompletePolicy = CommandAutocompletePolicy()
|
||||
private val autocompleteCommandPresenter: AutocompleteCommandPresenter by inject { parametersOf(this) }
|
||||
private val autocompleteUserPresenter: AutocompleteUserPresenter by inject { parametersOf(this) }
|
||||
private val homePermalinkHandler: HomePermalinkHandler by inject()
|
||||
@ -165,6 +173,9 @@ class RoomDetailFragment :
|
||||
|
||||
private lateinit var actionViewModel: ActionsHandler
|
||||
|
||||
@BindView(R.id.composerLayout)
|
||||
lateinit var composerLayout: TextComposerView
|
||||
|
||||
override fun onActivityCreated(savedInstanceState: Bundle?) {
|
||||
super.onActivityCreated(savedInstanceState)
|
||||
actionViewModel = ViewModelProviders.of(requireActivity()).get(ActionsHandler::class.java)
|
||||
@ -187,6 +198,77 @@ class RoomDetailFragment :
|
||||
actionViewModel.actionCommandEvent.observe(this, Observer {
|
||||
handleActions(it)
|
||||
})
|
||||
|
||||
roomDetailViewModel.selectSubscribe(
|
||||
RoomDetailViewState::sendMode,
|
||||
RoomDetailViewState::selectedEvent,
|
||||
RoomDetailViewState::roomId) { mode, event, roomId ->
|
||||
when (mode) {
|
||||
SendMode.REGULAR -> {
|
||||
commandAutocompletePolicy.enabled = true
|
||||
val uid = session.sessionParams.credentials.userId
|
||||
val meMember = session.getRoom(roomId)?.getRoomMember(uid)
|
||||
AvatarRenderer.render(meMember?.avatarUrl, uid, meMember?.displayName, composerLayout.composerAvatarImageView)
|
||||
composerLayout.collapse()
|
||||
}
|
||||
SendMode.EDIT,
|
||||
SendMode.QUOTE,
|
||||
SendMode.REPLY -> {
|
||||
commandAutocompletePolicy.enabled = false
|
||||
if (event == null) {
|
||||
//we should ignore? can this happen?
|
||||
Timber.e("Enter edit mode with no event selected")
|
||||
return@selectSubscribe
|
||||
}
|
||||
//switch to expanded bar
|
||||
composerLayout.composerRelatedMessageTitle.apply {
|
||||
text = event.senderName
|
||||
setTextColor(ContextCompat.getColor(requireContext(), AvatarRenderer.getColorFromUserId(event.root.sender
|
||||
?: "")))
|
||||
}
|
||||
|
||||
//TODO this is used at several places, find way to refactor?
|
||||
val messageContent: MessageContent? =
|
||||
event.annotations?.editSummary?.aggregatedContent?.toModel()
|
||||
?: event.root.content.toModel()
|
||||
val nonFormattedBody = messageContent?.body ?: ""
|
||||
var formattedBody: CharSequence? = null
|
||||
if (messageContent is MessageTextContent && messageContent.format == MessageType.FORMAT_MATRIX_HTML) {
|
||||
val parser = Parser.builder().build()
|
||||
val document = parser.parse(messageContent.formattedBody ?: messageContent.body)
|
||||
formattedBody = Markwon.builder(requireContext())
|
||||
.usePlugin(HtmlPlugin.create()).build().render(document)
|
||||
}
|
||||
composerLayout.composerRelatedMessageContent.text = formattedBody ?: nonFormattedBody
|
||||
|
||||
|
||||
if (mode == SendMode.EDIT) {
|
||||
//TODO if it's a reply we should trim the top part of message
|
||||
composerLayout.composerEditText.setText(nonFormattedBody)
|
||||
composerLayout.composerRelatedMessageActionIcon.setImageDrawable(ContextCompat.getDrawable(requireContext(), R.drawable.ic_edit))
|
||||
} else if (mode == SendMode.QUOTE) {
|
||||
composerLayout.composerEditText.setText("")
|
||||
composerLayout.composerRelatedMessageActionIcon.setImageDrawable(ContextCompat.getDrawable(requireContext(), R.drawable.ic_quote))
|
||||
} else if (mode == SendMode.REPLY) {
|
||||
composerLayout.composerEditText.setText("")
|
||||
composerLayout.composerRelatedMessageActionIcon.setImageDrawable(ContextCompat.getDrawable(requireContext(), R.drawable.ic_reply))
|
||||
}
|
||||
|
||||
AvatarRenderer.render(event.senderAvatar, event.root.sender
|
||||
?: "", event.senderName, composerLayout.composerRelatedMessageAvatar)
|
||||
|
||||
composerLayout.composerEditText.setSelection(composerLayout.composerEditText.text.length)
|
||||
composerLayout.expand {
|
||||
focusComposerAndShowKeyboard()
|
||||
}
|
||||
composerLayout.composerRelatedMessageCloseButton.setOnClickListener {
|
||||
composerLayout.composerEditText.setText("")
|
||||
roomDetailViewModel.resetSendMode()
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||
@ -233,8 +315,8 @@ class RoomDetailFragment :
|
||||
private fun setupComposer() {
|
||||
val elevation = 6f
|
||||
val backgroundDrawable = ColorDrawable(Color.WHITE)
|
||||
Autocomplete.on<Command>(composerEditText)
|
||||
.with(CommandAutocompletePolicy())
|
||||
Autocomplete.on<Command>(composerLayout.composerEditText)
|
||||
.with(commandAutocompletePolicy)
|
||||
.with(autocompleteCommandPresenter)
|
||||
.with(elevation)
|
||||
.with(backgroundDrawable)
|
||||
@ -253,7 +335,7 @@ class RoomDetailFragment :
|
||||
.build()
|
||||
|
||||
autocompleteUserPresenter.callback = this
|
||||
Autocomplete.on<User>(composerEditText)
|
||||
Autocomplete.on<User>(composerLayout.composerEditText)
|
||||
.with(CharPolicy('@', true))
|
||||
.with(autocompleteUserPresenter)
|
||||
.with(elevation)
|
||||
@ -281,7 +363,7 @@ class RoomDetailFragment :
|
||||
// Add the span
|
||||
val user = session.getUser(item.userId)
|
||||
val span = PillImageSpan(glideRequests, context!!, item.userId, user)
|
||||
span.bind(composerEditText)
|
||||
span.bind(composerLayout.composerEditText)
|
||||
|
||||
editable.setSpan(span, startIndex, startIndex + displayName.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
|
||||
|
||||
@ -293,16 +375,16 @@ class RoomDetailFragment :
|
||||
})
|
||||
.build()
|
||||
|
||||
sendButton.setOnClickListener {
|
||||
val textMessage = composerEditText.text.toString()
|
||||
composerLayout.sendButton.setOnClickListener {
|
||||
val textMessage = composerLayout.composerEditText.text.toString()
|
||||
if (textMessage.isNotBlank()) {
|
||||
roomDetailViewModel.process(RoomDetailActions.SendMessage(textMessage))
|
||||
roomDetailViewModel.process(RoomDetailActions.SendMessage(textMessage, PreferencesManager.isMarkdownEnabled(requireContext())))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun setupAttachmentButton() {
|
||||
attachmentButton.setOnClickListener {
|
||||
composerLayout.attachmentButton.setOnClickListener {
|
||||
val intent = Intent(requireContext(), FilePickerActivity::class.java)
|
||||
intent.putExtra(FilePickerActivity.CONFIGS, Configurations.Builder()
|
||||
.setCheckPermission(true)
|
||||
@ -386,6 +468,11 @@ class RoomDetailFragment :
|
||||
if (summary?.membership == Membership.JOIN) {
|
||||
timelineEventController.setTimeline(state.timeline)
|
||||
inviteView.visibility = View.GONE
|
||||
|
||||
val uid = session.sessionParams.credentials.userId
|
||||
val meMember = session.getRoom(state.roomId)?.getRoomMember(uid)
|
||||
AvatarRenderer.render(meMember?.avatarUrl, uid, meMember?.displayName, composerLayout.composerAvatarImageView)
|
||||
|
||||
} else if (summary?.membership == Membership.INVITE && inviter != null) {
|
||||
inviteView.visibility = View.VISIBLE
|
||||
inviteView.render(inviter, VectorInviteView.Mode.LARGE)
|
||||
@ -416,7 +503,7 @@ class RoomDetailFragment :
|
||||
is SendMessageResult.MessageSent,
|
||||
is SendMessageResult.SlashCommandHandled -> {
|
||||
// Clear composer
|
||||
composerEditText.text = null
|
||||
composerLayout.composerEditText.text = null
|
||||
}
|
||||
is SendMessageResult.SlashCommandError -> {
|
||||
displayCommandError(getString(R.string.command_problem_with_parameters, sendMessageResult.command.command))
|
||||
@ -586,6 +673,18 @@ class RoomDetailFragment :
|
||||
roomDetailViewModel.process(RoomDetailActions.UpdateQuickReactAction(eventId, clickedOn, opposite))
|
||||
}
|
||||
}
|
||||
MessageMenuViewModel.ACTION_EDIT -> {
|
||||
val eventId = actionData.data.toString()
|
||||
roomDetailViewModel.process(RoomDetailActions.EnterEditMode(eventId))
|
||||
}
|
||||
MessageMenuViewModel.ACTION_QUOTE -> {
|
||||
val eventId = actionData.data.toString()
|
||||
roomDetailViewModel.process(RoomDetailActions.EnterQuoteMode(eventId))
|
||||
}
|
||||
MessageMenuViewModel.ACTION_REPLY -> {
|
||||
val eventId = actionData.data.toString()
|
||||
roomDetailViewModel.process(RoomDetailActions.EnterReplyMode(eventId))
|
||||
}
|
||||
else -> {
|
||||
Toast.makeText(context, "Action ${actionData.actionId} not implemented", Toast.LENGTH_LONG).show()
|
||||
}
|
||||
@ -599,6 +698,7 @@ class RoomDetailFragment :
|
||||
*
|
||||
* @param text the text to insert.
|
||||
*/
|
||||
//TODO legacy, refactor
|
||||
private fun insertUserDisplayNameInTextEditor(text: String?) {
|
||||
//TODO move logic outside of fragment
|
||||
if (null != text) {
|
||||
@ -607,21 +707,21 @@ class RoomDetailFragment :
|
||||
val myDisplayName = session.getUser(session.sessionParams.credentials.userId)?.displayName
|
||||
if (TextUtils.equals(myDisplayName, text)) {
|
||||
// current user
|
||||
if (TextUtils.isEmpty(composerEditText.text)) {
|
||||
composerEditText.append(Command.EMOTE.command + " ")
|
||||
composerEditText.setSelection(composerEditText.text.length)
|
||||
if (TextUtils.isEmpty(composerLayout.composerEditText.text)) {
|
||||
composerLayout.composerEditText.append(Command.EMOTE.command + " ")
|
||||
composerLayout.composerEditText.setSelection(composerLayout.composerEditText.text.length)
|
||||
// vibrate = true
|
||||
}
|
||||
} else {
|
||||
// another user
|
||||
if (TextUtils.isEmpty(composerEditText.text)) {
|
||||
if (TextUtils.isEmpty(composerLayout.composerEditText.text)) {
|
||||
// Ensure displayName will not be interpreted as a Slash command
|
||||
if (text.startsWith("/")) {
|
||||
composerEditText.append("\\")
|
||||
composerLayout.composerEditText.append("\\")
|
||||
}
|
||||
composerEditText.append(sanitizeDisplayname(text)!! + ": ")
|
||||
composerLayout.composerEditText.append(sanitizeDisplayname(text)!! + ": ")
|
||||
} else {
|
||||
composerEditText.text.insert(composerEditText.selectionStart, sanitizeDisplayname(text)!! + " ")
|
||||
composerLayout.composerEditText.text.insert(composerLayout.composerEditText.selectionStart, sanitizeDisplayname(text)!! + " ")
|
||||
}
|
||||
|
||||
// vibrate = true
|
||||
@ -633,12 +733,16 @@ class RoomDetailFragment :
|
||||
// v.vibrate(100)
|
||||
// }
|
||||
// }
|
||||
composerEditText.requestFocus()
|
||||
val imm = context?.getSystemService(Context.INPUT_METHOD_SERVICE) as? InputMethodManager
|
||||
imm?.showSoftInput(composerEditText, InputMethodManager.SHOW_FORCED)
|
||||
focusComposerAndShowKeyboard()
|
||||
}
|
||||
}
|
||||
|
||||
private fun focusComposerAndShowKeyboard() {
|
||||
composerLayout.composerEditText.requestFocus()
|
||||
val imm = context?.getSystemService(Context.INPUT_METHOD_SERVICE) as? InputMethodManager
|
||||
imm?.showSoftInput(composerLayout.composerEditText, InputMethodManager.SHOW_IMPLICIT)
|
||||
}
|
||||
|
||||
fun showSnackWithMessage(message: String, duration: Int = Snackbar.LENGTH_SHORT) {
|
||||
val snack = Snackbar.make(view!!, message, duration)
|
||||
snack.view.setBackgroundColor(ContextCompat.getColor(requireContext(), R.color.notification_accent_color))
|
||||
|
@ -16,6 +16,7 @@
|
||||
|
||||
package im.vector.riotredesign.features.home.room.detail
|
||||
|
||||
import android.text.TextUtils
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import com.airbnb.mvrx.MvRxViewModelFactory
|
||||
@ -25,8 +26,11 @@ import com.jakewharton.rxrelay2.BehaviorRelay
|
||||
import im.vector.matrix.android.api.MatrixCallback
|
||||
import im.vector.matrix.android.api.session.Session
|
||||
import im.vector.matrix.android.api.session.content.ContentAttachmentData
|
||||
import im.vector.matrix.android.api.session.events.model.toModel
|
||||
import im.vector.matrix.android.api.session.room.model.Membership
|
||||
import im.vector.matrix.android.api.session.room.model.message.MessageContent
|
||||
import im.vector.matrix.android.api.session.room.model.message.MessageType
|
||||
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
|
||||
import im.vector.matrix.rx.rx
|
||||
import im.vector.riotredesign.R
|
||||
import im.vector.riotredesign.core.platform.VectorViewModel
|
||||
@ -35,11 +39,14 @@ import im.vector.riotredesign.features.command.CommandParser
|
||||
import im.vector.riotredesign.features.command.ParsedCommand
|
||||
import im.vector.riotredesign.features.home.room.detail.timeline.helper.TimelineDisplayableEvents
|
||||
import io.reactivex.rxkotlin.subscribeBy
|
||||
import org.commonmark.parser.Parser
|
||||
import org.commonmark.renderer.html.HtmlRenderer
|
||||
import org.koin.android.ext.android.get
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.*
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
|
||||
class RoomDetailViewModel(initialState: RoomDetailViewState,
|
||||
private val session: Session
|
||||
) : VectorViewModel<RoomDetailViewState>(initialState) {
|
||||
@ -83,9 +90,29 @@ class RoomDetailViewModel(initialState: RoomDetailViewState,
|
||||
is RoomDetailActions.UndoReaction -> handleUndoReact(action)
|
||||
is RoomDetailActions.UpdateQuickReactAction -> handleUpdateQuickReaction(action)
|
||||
is RoomDetailActions.ShowEditHistoryAction -> handleShowEditHistoryReaction(action)
|
||||
is RoomDetailActions.EnterEditMode -> handleEditAction(action)
|
||||
is RoomDetailActions.EnterQuoteMode -> handleQuoteAction(action)
|
||||
is RoomDetailActions.EnterReplyMode -> handleReplyAction(action)
|
||||
}
|
||||
}
|
||||
|
||||
fun enterEditMode(event: TimelineEvent) {
|
||||
setState {
|
||||
copy(
|
||||
sendMode = SendMode.EDIT,
|
||||
selectedEvent = event
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun resetSendMode() {
|
||||
setState {
|
||||
copy(
|
||||
sendMode = SendMode.REGULAR,
|
||||
selectedEvent = null
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private val _nonBlockingPopAlert = MutableLiveData<LiveEvent<Pair<Int, List<Any>>>>()
|
||||
val nonBlockingPopAlert: LiveData<LiveEvent<Pair<Int, List<Any>>>>
|
||||
@ -99,71 +126,145 @@ class RoomDetailViewModel(initialState: RoomDetailViewState,
|
||||
// PRIVATE METHODS *****************************************************************************
|
||||
|
||||
private fun handleSendMessage(action: RoomDetailActions.SendMessage) {
|
||||
// Handle slash command
|
||||
val slashCommandResult = CommandParser.parseSplashCommand(action.text)
|
||||
withState { state ->
|
||||
when (state.sendMode) {
|
||||
SendMode.REGULAR -> {
|
||||
val slashCommandResult = CommandParser.parseSplashCommand(action.text)
|
||||
|
||||
when (slashCommandResult) {
|
||||
is ParsedCommand.ErrorNotACommand -> {
|
||||
// Send the text message to the room
|
||||
room.sendTextMessage(action.text)
|
||||
_sendMessageResultLiveData.postValue(LiveEvent(SendMessageResult.MessageSent))
|
||||
}
|
||||
is ParsedCommand.ErrorSyntax -> {
|
||||
_sendMessageResultLiveData.postValue(LiveEvent(SendMessageResult.SlashCommandError(slashCommandResult.command)))
|
||||
}
|
||||
is ParsedCommand.ErrorEmptySlashCommand -> {
|
||||
_sendMessageResultLiveData.postValue(LiveEvent(SendMessageResult.SlashCommandUnknown("/")))
|
||||
}
|
||||
is ParsedCommand.ErrorUnknownSlashCommand -> {
|
||||
_sendMessageResultLiveData.postValue(LiveEvent(SendMessageResult.SlashCommandUnknown(slashCommandResult.slashCommand)))
|
||||
}
|
||||
is ParsedCommand.Invite -> {
|
||||
handleInviteSlashCommand(slashCommandResult)
|
||||
}
|
||||
is ParsedCommand.SetUserPowerLevel -> {
|
||||
// TODO
|
||||
_sendMessageResultLiveData.postValue(LiveEvent(SendMessageResult.SlashCommandNotImplemented))
|
||||
}
|
||||
is ParsedCommand.ClearScalarToken -> {
|
||||
// TODO
|
||||
_sendMessageResultLiveData.postValue(LiveEvent(SendMessageResult.SlashCommandNotImplemented))
|
||||
}
|
||||
is ParsedCommand.SetMarkdown -> {
|
||||
// TODO
|
||||
_sendMessageResultLiveData.postValue(LiveEvent(SendMessageResult.SlashCommandNotImplemented))
|
||||
}
|
||||
is ParsedCommand.UnbanUser -> {
|
||||
// TODO
|
||||
_sendMessageResultLiveData.postValue(LiveEvent(SendMessageResult.SlashCommandNotImplemented))
|
||||
}
|
||||
is ParsedCommand.BanUser -> {
|
||||
// TODO
|
||||
_sendMessageResultLiveData.postValue(LiveEvent(SendMessageResult.SlashCommandNotImplemented))
|
||||
}
|
||||
is ParsedCommand.KickUser -> {
|
||||
// TODO
|
||||
_sendMessageResultLiveData.postValue(LiveEvent(SendMessageResult.SlashCommandNotImplemented))
|
||||
}
|
||||
is ParsedCommand.JoinRoom -> {
|
||||
// TODO
|
||||
_sendMessageResultLiveData.postValue(LiveEvent(SendMessageResult.SlashCommandNotImplemented))
|
||||
}
|
||||
is ParsedCommand.PartRoom -> {
|
||||
// TODO
|
||||
_sendMessageResultLiveData.postValue(LiveEvent(SendMessageResult.SlashCommandNotImplemented))
|
||||
}
|
||||
is ParsedCommand.SendEmote -> {
|
||||
room.sendTextMessage(slashCommandResult.message, msgType = MessageType.MSGTYPE_EMOTE)
|
||||
_sendMessageResultLiveData.postValue(LiveEvent(SendMessageResult.SlashCommandHandled))
|
||||
}
|
||||
is ParsedCommand.ChangeTopic -> {
|
||||
handleChangeTopicSlashCommand(slashCommandResult)
|
||||
}
|
||||
is ParsedCommand.ChangeDisplayName -> {
|
||||
// TODO
|
||||
_sendMessageResultLiveData.postValue(LiveEvent(SendMessageResult.SlashCommandNotImplemented))
|
||||
when (slashCommandResult) {
|
||||
is ParsedCommand.ErrorNotACommand -> {
|
||||
// Send the text message to the room
|
||||
room.sendTextMessage(action.text, autoMarkdown = action.autoMarkdown)
|
||||
_sendMessageResultLiveData.postValue(LiveEvent(SendMessageResult.MessageSent))
|
||||
}
|
||||
is ParsedCommand.ErrorSyntax -> {
|
||||
_sendMessageResultLiveData.postValue(LiveEvent(SendMessageResult.SlashCommandError(slashCommandResult.command)))
|
||||
}
|
||||
is ParsedCommand.ErrorEmptySlashCommand -> {
|
||||
_sendMessageResultLiveData.postValue(LiveEvent(SendMessageResult.SlashCommandUnknown("/")))
|
||||
}
|
||||
is ParsedCommand.ErrorUnknownSlashCommand -> {
|
||||
_sendMessageResultLiveData.postValue(LiveEvent(SendMessageResult.SlashCommandUnknown(slashCommandResult.slashCommand)))
|
||||
}
|
||||
is ParsedCommand.Invite -> {
|
||||
handleInviteSlashCommand(slashCommandResult)
|
||||
}
|
||||
is ParsedCommand.SetUserPowerLevel -> {
|
||||
// TODO
|
||||
_sendMessageResultLiveData.postValue(LiveEvent(SendMessageResult.SlashCommandNotImplemented))
|
||||
}
|
||||
is ParsedCommand.ClearScalarToken -> {
|
||||
// TODO
|
||||
_sendMessageResultLiveData.postValue(LiveEvent(SendMessageResult.SlashCommandNotImplemented))
|
||||
}
|
||||
is ParsedCommand.SetMarkdown -> {
|
||||
// TODO
|
||||
_sendMessageResultLiveData.postValue(LiveEvent(SendMessageResult.SlashCommandNotImplemented))
|
||||
}
|
||||
is ParsedCommand.UnbanUser -> {
|
||||
// TODO
|
||||
_sendMessageResultLiveData.postValue(LiveEvent(SendMessageResult.SlashCommandNotImplemented))
|
||||
}
|
||||
is ParsedCommand.BanUser -> {
|
||||
// TODO
|
||||
_sendMessageResultLiveData.postValue(LiveEvent(SendMessageResult.SlashCommandNotImplemented))
|
||||
}
|
||||
is ParsedCommand.KickUser -> {
|
||||
// TODO
|
||||
_sendMessageResultLiveData.postValue(LiveEvent(SendMessageResult.SlashCommandNotImplemented))
|
||||
}
|
||||
is ParsedCommand.JoinRoom -> {
|
||||
// TODO
|
||||
_sendMessageResultLiveData.postValue(LiveEvent(SendMessageResult.SlashCommandNotImplemented))
|
||||
}
|
||||
is ParsedCommand.PartRoom -> {
|
||||
// TODO
|
||||
_sendMessageResultLiveData.postValue(LiveEvent(SendMessageResult.SlashCommandNotImplemented))
|
||||
}
|
||||
is ParsedCommand.SendEmote -> {
|
||||
room.sendTextMessage(slashCommandResult.message, msgType = MessageType.MSGTYPE_EMOTE)
|
||||
_sendMessageResultLiveData.postValue(LiveEvent(SendMessageResult.SlashCommandHandled))
|
||||
}
|
||||
is ParsedCommand.ChangeTopic -> {
|
||||
handleChangeTopicSlashCommand(slashCommandResult)
|
||||
}
|
||||
is ParsedCommand.ChangeDisplayName -> {
|
||||
// TODO
|
||||
_sendMessageResultLiveData.postValue(LiveEvent(SendMessageResult.SlashCommandNotImplemented))
|
||||
}
|
||||
}
|
||||
}
|
||||
SendMode.EDIT -> {
|
||||
room.editTextMessage(state.selectedEvent?.root?.eventId ?: "", action.text, action.autoMarkdown)
|
||||
setState {
|
||||
copy(
|
||||
sendMode = SendMode.REGULAR,
|
||||
selectedEvent = null
|
||||
)
|
||||
}
|
||||
_sendMessageResultLiveData.postValue(LiveEvent(SendMessageResult.MessageSent))
|
||||
}
|
||||
SendMode.QUOTE -> {
|
||||
val messageContent: MessageContent? =
|
||||
state.selectedEvent?.annotations?.editSummary?.aggregatedContent?.toModel()
|
||||
?: state.selectedEvent?.root?.content.toModel()
|
||||
val textMsg = messageContent?.body
|
||||
|
||||
val finalText = legacyRiotQuoteText(textMsg, action.text)
|
||||
|
||||
//TODO Refactor this, just temporary for quotes
|
||||
val parser = Parser.builder().build()
|
||||
val document = parser.parse(finalText)
|
||||
val renderer = HtmlRenderer.builder().build()
|
||||
val htmlText = renderer.render(document)
|
||||
if (TextUtils.equals(finalText, htmlText)) {
|
||||
room.sendTextMessage(finalText)
|
||||
} else {
|
||||
room.sendFormattedTextMessage(finalText, htmlText)
|
||||
}
|
||||
setState {
|
||||
copy(
|
||||
sendMode = SendMode.REGULAR,
|
||||
selectedEvent = null
|
||||
)
|
||||
}
|
||||
_sendMessageResultLiveData.postValue(LiveEvent(SendMessageResult.MessageSent))
|
||||
}
|
||||
SendMode.REPLY -> {
|
||||
state.selectedEvent?.let {
|
||||
room.replyToMessage(it.root, action.text)
|
||||
setState {
|
||||
copy(
|
||||
sendMode = SendMode.REGULAR,
|
||||
selectedEvent = null
|
||||
)
|
||||
}
|
||||
_sendMessageResultLiveData.postValue(LiveEvent(SendMessageResult.MessageSent))
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
// Handle slash command
|
||||
|
||||
}
|
||||
|
||||
private fun legacyRiotQuoteText(quotedText: String?, myText: String): String {
|
||||
val messageParagraphs = quotedText?.split("\n\n".toRegex())?.dropLastWhile { it.isEmpty() }?.toTypedArray()
|
||||
var quotedTextMsg = StringBuilder()
|
||||
if (messageParagraphs != null) {
|
||||
for (i in messageParagraphs.indices) {
|
||||
if (messageParagraphs[i].trim({ it <= ' ' }) != "") {
|
||||
quotedTextMsg.append("> ").append(messageParagraphs[i])
|
||||
}
|
||||
|
||||
if (i + 1 != messageParagraphs.size) {
|
||||
quotedTextMsg.append("\n\n")
|
||||
}
|
||||
}
|
||||
}
|
||||
val finalText = "$quotedTextMsg\n\n$myText"
|
||||
return finalText
|
||||
}
|
||||
|
||||
private fun handleShowEditHistoryReaction(action: RoomDetailActions.ShowEditHistoryAction) {
|
||||
@ -263,6 +364,34 @@ class RoomDetailViewModel(initialState: RoomDetailViewState,
|
||||
room.join(object : MatrixCallback<Unit> {})
|
||||
}
|
||||
|
||||
private fun handleEditAction(action: RoomDetailActions.EnterEditMode) {
|
||||
room.getTimeLineEvent(action.eventId)?.let {
|
||||
enterEditMode(it)
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleQuoteAction(action: RoomDetailActions.EnterQuoteMode) {
|
||||
room.getTimeLineEvent(action.eventId)?.let {
|
||||
setState {
|
||||
copy(
|
||||
sendMode = SendMode.QUOTE,
|
||||
selectedEvent = it
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleReplyAction(action: RoomDetailActions.EnterReplyMode) {
|
||||
room.getTimeLineEvent(action.eventId)?.let {
|
||||
setState {
|
||||
copy(
|
||||
sendMode = SendMode.REPLY,
|
||||
selectedEvent = it
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private fun observeEventDisplayedActions() {
|
||||
// We are buffering scroll events for one second
|
||||
|
@ -22,15 +22,33 @@ import com.airbnb.mvrx.Uninitialized
|
||||
import im.vector.matrix.android.api.session.room.model.RoomSummary
|
||||
import im.vector.matrix.android.api.session.room.timeline.Timeline
|
||||
import im.vector.matrix.android.api.session.room.timeline.TimelineData
|
||||
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
|
||||
import im.vector.matrix.android.api.session.user.model.User
|
||||
|
||||
/**
|
||||
* Describes the current send mode:
|
||||
* REGULAR: sends the text as a regular message
|
||||
* QUOTE: User is currently quoting a message
|
||||
* EDIT: User is currently editing an existing message
|
||||
*
|
||||
* Depending on the state the bottom toolbar will change (icons/preview/actions...)
|
||||
*/
|
||||
enum class SendMode {
|
||||
REGULAR,
|
||||
QUOTE,
|
||||
EDIT,
|
||||
REPLY
|
||||
}
|
||||
|
||||
data class RoomDetailViewState(
|
||||
val roomId: String,
|
||||
val eventId: String?,
|
||||
val timeline: Timeline? = null,
|
||||
val asyncInviter: Async<User> = Uninitialized,
|
||||
val asyncRoomSummary: Async<RoomSummary> = Uninitialized,
|
||||
val asyncTimelineData: Async<TimelineData> = Uninitialized
|
||||
val asyncTimelineData: Async<TimelineData> = Uninitialized,
|
||||
val sendMode: SendMode = SendMode.REGULAR,
|
||||
val selectedEvent: TimelineEvent? = null
|
||||
) : MvRxState {
|
||||
|
||||
constructor(args: RoomDetailArgs) : this(roomId = args.roomId, eventId = args.eventId)
|
||||
|
@ -0,0 +1,116 @@
|
||||
package im.vector.riotredesign.features.home.room.detail.composer
|
||||
|
||||
import android.content.Context
|
||||
import android.util.AttributeSet
|
||||
import android.view.ViewGroup
|
||||
import android.widget.EditText
|
||||
import android.widget.ImageButton
|
||||
import android.widget.ImageView
|
||||
import android.widget.TextView
|
||||
import androidx.constraintlayout.widget.ConstraintLayout
|
||||
import androidx.constraintlayout.widget.ConstraintSet
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.transition.AutoTransition
|
||||
import androidx.transition.Transition
|
||||
import androidx.transition.TransitionManager
|
||||
import butterknife.BindView
|
||||
import butterknife.ButterKnife
|
||||
import im.vector.riotredesign.R
|
||||
|
||||
|
||||
/**
|
||||
* Encapsulate the timeline composer UX.
|
||||
*
|
||||
*/
|
||||
class TextComposerView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null,
|
||||
defStyleAttr: Int = 0) : ConstraintLayout(context, attrs, defStyleAttr) {
|
||||
|
||||
@BindView(R.id.composer_related_message_sender)
|
||||
lateinit var composerRelatedMessageTitle: TextView
|
||||
@BindView(R.id.composer_related_message_preview)
|
||||
lateinit var composerRelatedMessageContent: TextView
|
||||
@BindView(R.id.composer_related_message_avatar_view)
|
||||
lateinit var composerRelatedMessageAvatar: ImageView
|
||||
@BindView(R.id.composer_related_message_action_image)
|
||||
lateinit var composerRelatedMessageActionIcon: ImageView
|
||||
@BindView(R.id.composer_related_message_close)
|
||||
lateinit var composerRelatedMessageCloseButton: ImageButton
|
||||
@BindView(R.id.composerEditText)
|
||||
lateinit var composerEditText: EditText
|
||||
@BindView(R.id.composer_avatar_view)
|
||||
lateinit var composerAvatarImageView: ImageView
|
||||
|
||||
var currentConstraintSetId: Int = -1
|
||||
|
||||
|
||||
init {
|
||||
inflate(context, R.layout.merge_composer_layout, this)
|
||||
ButterKnife.bind(this)
|
||||
collapse(false)
|
||||
}
|
||||
|
||||
|
||||
fun collapse(animate: Boolean = true, transitionComplete: (() -> Unit)? = null) {
|
||||
if (currentConstraintSetId == R.layout.constraint_set_composer_layout_compact) {
|
||||
//ignore we good
|
||||
return
|
||||
}
|
||||
currentConstraintSetId = R.layout.constraint_set_composer_layout_compact
|
||||
if (animate) {
|
||||
val transition = AutoTransition()
|
||||
// transition.duration = 5000
|
||||
transition.addListener(object : Transition.TransitionListener {
|
||||
|
||||
override fun onTransitionEnd(transition: Transition) {
|
||||
transitionComplete?.invoke()
|
||||
}
|
||||
|
||||
override fun onTransitionResume(transition: Transition) {}
|
||||
|
||||
override fun onTransitionPause(transition: Transition) {}
|
||||
|
||||
override fun onTransitionCancel(transition: Transition) {}
|
||||
|
||||
override fun onTransitionStart(transition: Transition) {}
|
||||
}
|
||||
)
|
||||
TransitionManager.beginDelayedTransition((parent as? ViewGroup ?: this), transition)
|
||||
}
|
||||
ConstraintSet().also {
|
||||
it.clone(context, currentConstraintSetId)
|
||||
it.applyTo(this)
|
||||
}
|
||||
}
|
||||
|
||||
fun expand(animate: Boolean = true, transitionComplete: (() -> Unit)? = null) {
|
||||
if (currentConstraintSetId == R.layout.constraint_set_composer_layout_expanded) {
|
||||
//ignore we good
|
||||
return
|
||||
}
|
||||
currentConstraintSetId = R.layout.constraint_set_composer_layout_expanded
|
||||
if (animate) {
|
||||
val transition = AutoTransition()
|
||||
// transition.duration = 5000
|
||||
transition.addListener(object : Transition.TransitionListener {
|
||||
|
||||
override fun onTransitionEnd(transition: Transition) {
|
||||
transitionComplete?.invoke()
|
||||
}
|
||||
|
||||
override fun onTransitionResume(transition: Transition) {}
|
||||
|
||||
override fun onTransitionPause(transition: Transition) {}
|
||||
|
||||
override fun onTransitionCancel(transition: Transition) {}
|
||||
|
||||
override fun onTransitionStart(transition: Transition) {}
|
||||
}
|
||||
)
|
||||
TransitionManager.beginDelayedTransition((parent as? ViewGroup ?: this), transition)
|
||||
}
|
||||
ConstraintSet().also {
|
||||
it.clone(context, currentConstraintSetId)
|
||||
it.applyTo(this)
|
||||
}
|
||||
}
|
||||
}
|
@ -21,8 +21,14 @@ import com.airbnb.mvrx.ViewModelContext
|
||||
import im.vector.matrix.android.api.session.Session
|
||||
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.MessageTextContent
|
||||
import im.vector.matrix.android.api.session.room.model.message.MessageType
|
||||
import im.vector.riotredesign.core.platform.VectorViewModel
|
||||
import org.commonmark.parser.Parser
|
||||
import org.commonmark.renderer.html.HtmlRenderer
|
||||
import org.koin.android.ext.android.get
|
||||
import ru.noties.markwon.Markwon
|
||||
import ru.noties.markwon.html.HtmlPlugin
|
||||
import timber.log.Timber
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.*
|
||||
@ -31,7 +37,7 @@ import java.util.*
|
||||
data class MessageActionState(
|
||||
val userId: String,
|
||||
val senderName: String,
|
||||
val messageBody: String,
|
||||
val messageBody: CharSequence,
|
||||
val ts: String?,
|
||||
val senderAvatarPath: String? = null)
|
||||
: MvRxState
|
||||
@ -54,10 +60,19 @@ class MessageActionsViewModel(initialState: MessageActionState) : VectorViewMode
|
||||
val messageContent: MessageContent? = event.annotations?.editSummary?.aggregatedContent?.toModel()
|
||||
?: event.root.content.toModel()
|
||||
val originTs = event.root.originServerTs
|
||||
var body: CharSequence = messageContent?.body ?: ""
|
||||
if (messageContent is MessageTextContent && messageContent.format == MessageType.FORMAT_MATRIX_HTML) {
|
||||
val parser = Parser.builder().build()
|
||||
val document = parser.parse(messageContent.formattedBody ?: messageContent.body)
|
||||
// val renderer = HtmlRenderer.builder().build()
|
||||
body = Markwon.builder(viewModelContext.activity)
|
||||
.usePlugin(HtmlPlugin.create()).build().render(document)
|
||||
// body = renderer.render(document)
|
||||
}
|
||||
MessageActionState(
|
||||
event.root.sender ?: "",
|
||||
parcel.informationData.memberName.toString(),
|
||||
messageContent?.body ?: "",
|
||||
body,
|
||||
dateFormat.format(Date(originTs ?: 0)),
|
||||
currentSession.contentUrlResolver().resolveFullSize(parcel.informationData.avatarUrl)
|
||||
)
|
||||
|
@ -50,16 +50,17 @@ class MessageMenuViewModel(initialState: MessageMenuState) : VectorViewModel<Mes
|
||||
val event = currentSession.getRoom(parcel.roomId)?.getTimeLineEvent(parcel.eventId)
|
||||
?: return null
|
||||
|
||||
val messageContent: MessageContent = event.root.content.toModel() ?: return null
|
||||
val messageContent: MessageContent = event.annotations?.editSummary?.aggregatedContent?.toModel()
|
||||
?: event.root.content.toModel() ?: return null
|
||||
val type = messageContent.type
|
||||
|
||||
if (event.sendState == SendState.UNSENT) {
|
||||
//Resend and Delete
|
||||
return MessageMenuState(
|
||||
listOf(
|
||||
SimpleAction(ACTION_RESEND, R.string.resend, R.drawable.ic_corner_down_right, event.root.eventId),
|
||||
SimpleAction(ACTION_RESEND, R.string.resend, R.drawable.ic_send, event.root.eventId),
|
||||
//TODO delete icon
|
||||
SimpleAction(ACTION_DELETE, R.string.delete, R.drawable.ic_material_delete, event.root.eventId)
|
||||
SimpleAction(ACTION_DELETE, R.string.delete, R.drawable.ic_delete, event.root.eventId)
|
||||
)
|
||||
)
|
||||
}
|
||||
@ -67,14 +68,29 @@ class MessageMenuViewModel(initialState: MessageMenuState) : VectorViewModel<Mes
|
||||
|
||||
//TODO determine if can copy, forward, reply, quote, report?
|
||||
val actions = ArrayList<SimpleAction>().apply {
|
||||
this.add(SimpleAction(ACTION_ADD_REACTION, R.string.message_add_reaction, R.drawable.ic_smile, event.root.eventId))
|
||||
|
||||
if (event.sendState == SendState.SENDING) {
|
||||
//TODO add cancel?
|
||||
return@apply
|
||||
}
|
||||
//TODO is downloading attachement?
|
||||
|
||||
this.add(SimpleAction(ACTION_ADD_REACTION, R.string.message_add_reaction, R.drawable.ic_add_reaction, event.root.eventId))
|
||||
if (canCopy(type)) {
|
||||
//TODO copy images? html? see ClipBoard
|
||||
this.add(SimpleAction(ACTION_COPY, R.string.copy, R.drawable.ic_copy, messageContent.body))
|
||||
}
|
||||
|
||||
if (canReply(event, messageContent)) {
|
||||
this.add(SimpleAction(ACTION_REPLY, R.string.reply, R.drawable.ic_reply, event.root.eventId))
|
||||
}
|
||||
|
||||
if (canEdit(event, currentSession.sessionParams.credentials.userId)) {
|
||||
this.add(SimpleAction(ACTION_EDIT, R.string.edit, R.drawable.ic_edit, event.root.eventId))
|
||||
}
|
||||
|
||||
if (canRedact(event, currentSession.sessionParams.credentials.userId)) {
|
||||
this.add(SimpleAction(ACTION_DELETE, R.string.delete, R.drawable.ic_material_delete, event.root.eventId))
|
||||
this.add(SimpleAction(ACTION_DELETE, R.string.delete, R.drawable.ic_delete, event.root.eventId))
|
||||
}
|
||||
|
||||
if (canQuote(event, messageContent)) {
|
||||
@ -82,9 +98,6 @@ class MessageMenuViewModel(initialState: MessageMenuState) : VectorViewModel<Mes
|
||||
this.add(SimpleAction(ACTION_QUOTE, R.string.quote, R.drawable.ic_quote, parcel.eventId))
|
||||
}
|
||||
|
||||
if (canReply(event, messageContent)) {
|
||||
this.add(SimpleAction(ACTION_REPLY, R.string.reply, R.drawable.ic_corner_down_right))
|
||||
}
|
||||
if (canShare(type)) {
|
||||
if (messageContent is MessageImageContent) {
|
||||
this.add(
|
||||
@ -96,8 +109,6 @@ class MessageMenuViewModel(initialState: MessageMenuState) : VectorViewModel<Mes
|
||||
//TODO
|
||||
}
|
||||
|
||||
//TODO is uploading
|
||||
//TODO is downloading
|
||||
|
||||
if (event.sendState == SendState.SENT) {
|
||||
|
||||
@ -159,6 +170,17 @@ class MessageMenuViewModel(initialState: MessageMenuState) : VectorViewModel<Mes
|
||||
return event.root.sender == myUserId
|
||||
}
|
||||
|
||||
private fun canEdit(event: TimelineEvent, myUserId: String): Boolean {
|
||||
//Only event of type Event.EVENT_TYPE_MESSAGE are supported for the moment
|
||||
if (event.root.type != EventType.MESSAGE) return false
|
||||
//TODO if user is admin or moderator
|
||||
val messageContent = event.root.content.toModel<MessageContent>()
|
||||
return event.root.sender == myUserId && (
|
||||
messageContent?.type == MessageType.MSGTYPE_TEXT
|
||||
|| messageContent?.type == MessageType.MSGTYPE_EMOTE
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
private fun canCopy(type: String): Boolean {
|
||||
return when (type) {
|
||||
@ -187,6 +209,7 @@ class MessageMenuViewModel(initialState: MessageMenuState) : VectorViewModel<Mes
|
||||
|
||||
const val ACTION_ADD_REACTION = "add_reaction"
|
||||
const val ACTION_COPY = "copy"
|
||||
const val ACTION_EDIT = "edit"
|
||||
const val ACTION_QUOTE = "quote"
|
||||
const val ACTION_REPLY = "reply"
|
||||
const val ACTION_SHARE = "share"
|
||||
|
@ -38,6 +38,7 @@ import im.vector.riotredesign.core.epoxy.VectorEpoxyModel
|
||||
import im.vector.riotredesign.core.extensions.localDateTime
|
||||
import im.vector.riotredesign.core.linkify.VectorLinkify
|
||||
import im.vector.riotredesign.core.resources.ColorProvider
|
||||
import im.vector.riotredesign.core.resources.StringProvider
|
||||
import im.vector.riotredesign.core.utils.DebouncedClickListener
|
||||
import im.vector.riotredesign.features.home.AvatarRenderer
|
||||
import im.vector.riotredesign.features.home.room.detail.timeline.TimelineEventController
|
||||
@ -52,7 +53,8 @@ import me.gujun.android.span.span
|
||||
class MessageItemFactory(private val colorProvider: ColorProvider,
|
||||
private val timelineMediaSizeProvider: TimelineMediaSizeProvider,
|
||||
private val timelineDateFormatter: TimelineDateFormatter,
|
||||
private val htmlRenderer: EventHtmlRenderer) {
|
||||
private val htmlRenderer: EventHtmlRenderer,
|
||||
private val stringProvider: StringProvider) {
|
||||
|
||||
fun create(event: TimelineEvent,
|
||||
nextEvent: TimelineEvent?,
|
||||
@ -88,7 +90,9 @@ class MessageItemFactory(private val colorProvider: ColorProvider,
|
||||
avatarUrl = avatarUrl,
|
||||
memberName = formattedMemberName,
|
||||
showInformation = showInformation,
|
||||
orderedReactionList = event.annotations?.reactionsSummary?.map { Triple(it.key, it.count, it.addedByMe) },
|
||||
orderedReactionList = event.annotations?.reactionsSummary?.map {
|
||||
ReactionInfoData(it.key, it.count, it.addedByMe, it.localEchoEvents.isEmpty())
|
||||
},
|
||||
hasBeenEdited = hasBeenEdited
|
||||
)
|
||||
|
||||
@ -100,11 +104,11 @@ class MessageItemFactory(private val colorProvider: ColorProvider,
|
||||
val messageContent: MessageContent =
|
||||
event.annotations?.editSummary?.aggregatedContent?.toModel()
|
||||
?: event.root.content.toModel()
|
||||
?: return null
|
||||
?: //Malformed content, we should echo something on screen
|
||||
return DefaultItem_().text(stringProvider.getString(R.string.malformed_message))
|
||||
|
||||
if (messageContent.relatesTo?.type == RelationType.REPLACE) {
|
||||
//TODO blank item or ignore??
|
||||
// ignore this event
|
||||
// ignore replace event, the targeted id is already edited
|
||||
return BlankItem_()
|
||||
}
|
||||
// val all = event.root.toContent()
|
||||
|
@ -17,8 +17,10 @@
|
||||
package im.vector.riotredesign.features.home.room.detail.timeline.helper
|
||||
|
||||
import im.vector.matrix.android.api.session.events.model.EventType
|
||||
import im.vector.matrix.android.api.session.events.model.RelationType
|
||||
import im.vector.matrix.android.api.session.events.model.toModel
|
||||
import im.vector.matrix.android.api.session.room.model.RoomMember
|
||||
import im.vector.matrix.android.api.session.room.model.message.MessageContent
|
||||
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
|
||||
import im.vector.riotredesign.core.extensions.localDateTime
|
||||
|
||||
@ -42,33 +44,44 @@ object TimelineDisplayableEvents {
|
||||
}
|
||||
|
||||
fun TimelineEvent.isDisplayable(): Boolean {
|
||||
return TimelineDisplayableEvents.DISPLAYABLE_TYPES.contains(root.type) && !root.content.isNullOrEmpty()
|
||||
}
|
||||
|
||||
fun List<TimelineEvent>.filterDisplayableEvents(): List<TimelineEvent> {
|
||||
return this.filter {
|
||||
it.isDisplayable()
|
||||
if (!TimelineDisplayableEvents.DISPLAYABLE_TYPES.contains(root.type)) {
|
||||
return false
|
||||
}
|
||||
if (root.content.isNullOrEmpty()) {
|
||||
return false
|
||||
}
|
||||
//Edits should be filtered out!
|
||||
if (EventType.MESSAGE == root.type
|
||||
&& root.content.toModel<MessageContent>()?.relatesTo?.type == RelationType.REPLACE) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
//
|
||||
//fun List<TimelineEvent>.filterDisplayableEvents(): List<TimelineEvent> {
|
||||
// return this.filter {
|
||||
// it.isDisplayable()
|
||||
// }
|
||||
//}
|
||||
|
||||
fun TimelineEvent.senderAvatar(): String? {
|
||||
// We might have no avatar when user leave, so we try to get it from prevContent
|
||||
return senderAvatar
|
||||
?: if (root.type == EventType.STATE_ROOM_MEMBER) {
|
||||
root.prevContent.toModel<RoomMember>()?.avatarUrl
|
||||
} else {
|
||||
null
|
||||
}
|
||||
?: if (root.type == EventType.STATE_ROOM_MEMBER) {
|
||||
root.prevContent.toModel<RoomMember>()?.avatarUrl
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
fun TimelineEvent.senderName(): String? {
|
||||
// We might have no senderName when user leave, so we try to get it from prevContent
|
||||
return senderName
|
||||
?: if (root.type == EventType.STATE_ROOM_MEMBER) {
|
||||
root.prevContent.toModel<RoomMember>()?.displayName
|
||||
} else {
|
||||
null
|
||||
}
|
||||
?: if (root.type == EventType.STATE_ROOM_MEMBER) {
|
||||
root.prevContent.toModel<RoomMember>()?.displayName
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
fun TimelineEvent.canBeMerged(): Boolean {
|
||||
|
@ -112,11 +112,12 @@ abstract class AbsMessageItem<H : AbsMessageItem.Holder> : BaseEventItem<H>() {
|
||||
(holder.reactionWrapper?.children?.elementAtOrNull(index) as? ReactionButton)?.let { reactionButton ->
|
||||
reactionButton.isVisible = true
|
||||
reactionButton.reactedListener = reactionClickListener
|
||||
reactionButton.setTag(R.id.messageBottomInfo, reaction.first)
|
||||
reactionButton.setTag(R.id.messageBottomInfo, reaction.key)
|
||||
idToRefInFlow.add(reactionButton.id)
|
||||
reactionButton.reactionString = reaction.first
|
||||
reactionButton.reactionCount = reaction.second
|
||||
reactionButton.setChecked(reaction.third)
|
||||
reactionButton.reactionString = reaction.key
|
||||
reactionButton.reactionCount = reaction.count
|
||||
reactionButton.setChecked(reaction.addedByMe)
|
||||
reactionButton.isEnabled = reaction.synced
|
||||
}
|
||||
}
|
||||
// Just setting the view as gone will break the FlowHelper (and invisible will take too much space),
|
||||
|
@ -16,9 +16,8 @@
|
||||
|
||||
package im.vector.riotredesign.features.home.room.detail.timeline.item
|
||||
|
||||
import im.vector.matrix.android.api.session.room.send.SendState
|
||||
|
||||
import android.os.Parcelable
|
||||
import im.vector.matrix.android.api.session.room.send.SendState
|
||||
import kotlinx.android.parcel.Parcelize
|
||||
|
||||
@Parcelize
|
||||
@ -31,6 +30,15 @@ data class MessageInformationData(
|
||||
val memberName: CharSequence? = null,
|
||||
val showInformation: Boolean = true,
|
||||
/*List of reactions (emoji,count,isSelected)*/
|
||||
var orderedReactionList: List<Triple<String,Int,Boolean>>? = null,
|
||||
var orderedReactionList: List<ReactionInfoData>? = null,
|
||||
var hasBeenEdited: Boolean = false
|
||||
) : Parcelable
|
||||
) : Parcelable
|
||||
|
||||
|
||||
@Parcelize
|
||||
data class ReactionInfoData(
|
||||
val key: String,
|
||||
val count: Int,
|
||||
val addedByMe: Boolean,
|
||||
val synced: Boolean
|
||||
) : Parcelable
|
||||
|
Reference in New Issue
Block a user