All Downloads are FREE. Search and download functionalities are using the official Maven repository.

main.com.sceyt.chatuikit.presentation.components.channel.input.MessageInputView.kt Maven / Gradle / Ivy

package com.sceyt.chatuikit.presentation.components.channel.input

import android.content.Context
import android.text.Editable
import android.text.TextWatcher
import android.util.AttributeSet
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.EditText
import android.widget.ImageView
import android.widget.Toast
import androidx.annotation.DrawableRes
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.core.view.isInvisible
import androidx.core.view.isVisible
import androidx.core.widget.doAfterTextChanged
import com.google.gson.Gson
import com.sceyt.chat.models.attachment.Attachment
import com.sceyt.chat.models.message.Message
import com.sceyt.chatuikit.R
import com.sceyt.chatuikit.SceytChatUIKit
import com.sceyt.chatuikit.data.models.channels.ChannelTypeEnum
import com.sceyt.chatuikit.data.models.channels.DraftMessage
import com.sceyt.chatuikit.data.models.channels.SceytChannel
import com.sceyt.chatuikit.data.models.channels.SceytMember
import com.sceyt.chatuikit.data.models.messages.AttachmentTypeEnum
import com.sceyt.chatuikit.data.models.messages.LinkPreviewDetails
import com.sceyt.chatuikit.data.models.messages.SceytMessage
import com.sceyt.chatuikit.databinding.SceytDisableMessageInputBinding
import com.sceyt.chatuikit.databinding.SceytMessageInputViewBinding
import com.sceyt.chatuikit.extensions.asComponentActivity
import com.sceyt.chatuikit.extensions.customToastSnackBar
import com.sceyt.chatuikit.extensions.empty
import com.sceyt.chatuikit.extensions.getScope
import com.sceyt.chatuikit.extensions.getString
import com.sceyt.chatuikit.extensions.hideSoftInput
import com.sceyt.chatuikit.extensions.isEqualsVideoOrImage
import com.sceyt.chatuikit.extensions.notAutoCorrectable
import com.sceyt.chatuikit.extensions.setBackgroundTint
import com.sceyt.chatuikit.extensions.setTextAndMoveSelectionEnd
import com.sceyt.chatuikit.extensions.showSoftInput
import com.sceyt.chatuikit.formatters.attributes.DraftMessageBodyFormatterAttributes
import com.sceyt.chatuikit.media.audio.AudioPlayerHelper
import com.sceyt.chatuikit.media.audio.AudioRecorderHelper
import com.sceyt.chatuikit.persistence.extensions.getChannelType
import com.sceyt.chatuikit.persistence.extensions.isPeerBlocked
import com.sceyt.chatuikit.persistence.lazyVar
import com.sceyt.chatuikit.presentation.common.DebounceHelper
import com.sceyt.chatuikit.presentation.common.SceytDialog
import com.sceyt.chatuikit.presentation.components.channel.input.adapters.attachments.AttachmentItem
import com.sceyt.chatuikit.presentation.components.channel.input.adapters.attachments.AttachmentsAdapter
import com.sceyt.chatuikit.presentation.components.channel.input.adapters.attachments.AttachmentsViewHolderFactory
import com.sceyt.chatuikit.presentation.components.channel.input.components.MentionUsersListView
import com.sceyt.chatuikit.presentation.components.channel.input.data.InputState
import com.sceyt.chatuikit.presentation.components.channel.input.data.InputState.Text
import com.sceyt.chatuikit.presentation.components.channel.input.data.InputState.Voice
import com.sceyt.chatuikit.presentation.components.channel.input.data.SearchResult
import com.sceyt.chatuikit.presentation.components.channel.input.format.BodyStyleRange
import com.sceyt.chatuikit.presentation.components.channel.input.helpers.MessageToSendHelper
import com.sceyt.chatuikit.presentation.components.channel.input.link.SingleLinkDetailsProvider
import com.sceyt.chatuikit.presentation.components.channel.input.listeners.MessageInputActionCallback
import com.sceyt.chatuikit.presentation.components.channel.input.listeners.action.InputActionsListener
import com.sceyt.chatuikit.presentation.components.channel.input.listeners.action.InputActionsListenerImpl
import com.sceyt.chatuikit.presentation.components.channel.input.listeners.click.AttachmentClickListeners
import com.sceyt.chatuikit.presentation.components.channel.input.listeners.click.MessageInputClickListeners
import com.sceyt.chatuikit.presentation.components.channel.input.listeners.click.MessageInputClickListenersImpl
import com.sceyt.chatuikit.presentation.components.channel.input.listeners.click.SelectFileTypePopupClickListeners
import com.sceyt.chatuikit.presentation.components.channel.input.listeners.click.SelectFileTypePopupClickListenersImpl
import com.sceyt.chatuikit.presentation.components.channel.input.listeners.event.InputEventsListener
import com.sceyt.chatuikit.presentation.components.channel.input.listeners.event.InputEventsListenerImpl
import com.sceyt.chatuikit.presentation.components.channel.input.mention.Mention
import com.sceyt.chatuikit.presentation.components.channel.input.mention.MentionAnnotation
import com.sceyt.chatuikit.presentation.components.channel.input.mention.MentionUserHelper
import com.sceyt.chatuikit.presentation.components.channel.input.mention.MentionValidatorWatcher
import com.sceyt.chatuikit.presentation.components.channel.input.mention.MessageBodyStyleHelper
import com.sceyt.chatuikit.presentation.components.channel.input.mention.query.InlineQuery
import com.sceyt.chatuikit.presentation.components.channel.input.mention.query.InlineQueryChangedListener
import com.sceyt.chatuikit.presentation.components.channel.messages.dialogs.ChooseFileTypeDialog
import com.sceyt.chatuikit.presentation.components.picker.BottomSheetMediaPicker
import com.sceyt.chatuikit.presentation.custom_views.voice_recorder.AudioMetadata
import com.sceyt.chatuikit.presentation.custom_views.voice_recorder.RecordingListener
import com.sceyt.chatuikit.presentation.custom_views.voice_recorder.VoiceRecordPlaybackView
import com.sceyt.chatuikit.presentation.custom_views.voice_recorder.VoiceRecorderView
import com.sceyt.chatuikit.shared.helpers.picker.FilePickerHelper
import com.sceyt.chatuikit.shared.helpers.picker.PickType
import com.sceyt.chatuikit.styles.input.InputCoverStyle
import com.sceyt.chatuikit.styles.input.InputSelectedMediaStyle
import com.sceyt.chatuikit.styles.input.MessageInputStyle
import com.sceyt.chatuikit.styles.input.MessageSearchControlsStyle
import com.vanniktech.ui.animateToGone
import com.vanniktech.ui.animateToVisible
import kotlinx.coroutines.Job
import kotlinx.coroutines.MainScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import java.io.File

@Suppress("MemberVisibilityCanBePrivate")
class MessageInputView @JvmOverloads constructor(
        context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
) : ConstraintLayout(context, attrs, defStyleAttr), MessageInputClickListeners.ClickListeners,
        SelectFileTypePopupClickListeners.ClickListeners, InputEventsListener.InputEventListeners,
        InputActionsListener.InputActionListeners {

    private lateinit var attachmentsAdapter: AttachmentsAdapter
    private var attachmentsViewHolderFactory by lazyVar { AttachmentsViewHolderFactory(context, style) }
    private var allAttachments = mutableListOf()
    private val binding: SceytMessageInputViewBinding
    private var style: MessageInputStyle
    private var clickListeners = MessageInputClickListenersImpl(this)
    private var eventListeners = InputEventsListenerImpl(this)
    private var actionListeners = InputActionsListenerImpl(this)
    private var selectFileTypePopupClickListeners = SelectFileTypePopupClickListenersImpl(this)
    private var filePickerHelper: FilePickerHelper? = null
    private val typingDebounceHelper by lazy { DebounceHelper(100, getScope()) }
    private var typingTimeoutJob: Job? = null
    private var inputState = Voice
    private var disabledInputByGesture: Boolean = false
    private var voiceRecorderView: VoiceRecorderView? = null
    private var mentionUsersListView: MentionUsersListView? = null
    private var inputTextWatcher: TextWatcher? = null
    private var messageInputActionCallback: MessageInputActionCallback? = null
    private val messageToSendHelper by lazy { MessageToSendHelper(context, actionListeners) }
    private val linkDetailsProvider by lazy { SingleLinkDetailsProvider(context, getScope()) }
    private val audioRecorderHelper: AudioRecorderHelper by lazy { AudioRecorderHelper(getScope(), context) }
    var enableVoiceRecord = true
        private set
    var enableSendAttachment = true
        private set
    var enableMention = true
        private set
    var isInputHidden = false
        private set
    var isInMultiSelectMode = false
        private set
    var isInSearchMode = false
        private set
    var editMessage: SceytMessage? = null
        private set
    var replyMessage: SceytMessage? = null
        private set
    var replyThreadMessageId: Long? = null
        private set
    var linkDetails: LinkPreviewDetails? = null
        private set

    init {
        binding = SceytMessageInputViewBinding.inflate(LayoutInflater.from(context), this)
        style = MessageInputStyle.Builder(context, attrs).build()

        if (!isInEditMode)
            filePickerHelper = FilePickerHelper(context.asComponentActivity())

        init()
    }

    private fun init() {
        with(binding) {
            applyStyle()
            setOnClickListeners()
            voiceRecordPlaybackView.setStyle(style.voiceRecordPlaybackViewStyle)
            messageActionsView.setStyle(style)
            linkPreviewView.setStyle(style)
            addInoutListeners()
            determineInputState()
            addInputTextWatcher()
            setupAttachmentsList()
            if (enableVoiceRecord) {
                // Init SceytVoiceMessageRecorderView outside of post, because it's using permission launcher
                val voiceRecorderView = VoiceRecorderView(context).also { it.setStyle(style) }
                post {
                    onStateChanged(inputState)
                    (parent as? ViewGroup)?.let { parentView ->
                        val index = parentView.indexOfChild(this@MessageInputView)
                        parentView.addView(voiceRecorderView.apply {
                            setRecordingListener()
                            [email protected] = this
                            [email protected]?.setRecorderHeight(binding.layoutInput.height)
                            isVisible = canShowRecorderView()
                        }, index + 1, LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT))
                    }
                }
            }
        }
    }

    private fun addInputTextWatcher() {
        inputTextWatcher = binding.messageInput.doAfterTextChanged { text ->
            onInputChanged(text)
        }
    }

    private fun onInputChanged(text: Editable?) {
        if (isRecording())
            return

        determineInputState()

        typingTimeoutJob?.cancel()
        typingTimeoutJob = MainScope().launch {
            delay(2000)
            actionListeners.sendTyping(false)
        }

        typingDebounceHelper.submit {
            actionListeners.sendTyping(text.isNullOrBlank().not())
            updateDraftMessage()
            tryToLoadLinkPreview(text)
        }
    }

    private fun hideAndReleaseLinkPreview() {
        linkDetails = null
        binding.linkPreviewView.hideLinkDetails()
    }

    private fun updateDraftMessage() {
        val replyOrEditMessage = replyMessage ?: editMessage
        val isReply = replyMessage != null
        with(binding.messageInput) {
            actionListeners.updateDraftMessage(text, mentions, styling, replyOrEditMessage, isReply)
        }
    }

    private fun tryToLoadLinkPreview(text: Editable?) {
        if (text.isNullOrBlank()) {
            hideAndReleaseLinkPreview()
            linkDetailsProvider.cancel()
        } else {
            linkDetails = null
            binding.linkPreviewView.hideLinkDetailsWithTimeout()
            linkDetailsProvider.loadLinkDetails(text.toString(), detailsCallback = {
                if (it != null) {
                    binding.linkPreviewView.showLinkDetails(it)
                    linkDetails = it
                } else hideAndReleaseLinkPreview()
            }, imageSizeCallback = { size ->
                linkDetails = linkDetails?.copy(imageWidth = size.width, imageHeight = size.height)
            }, thumbCallback = {
                linkDetails = linkDetails?.copy(thumb = it)
            })
        }
    }

    private fun SceytMessageInputViewBinding.setOnClickListeners() {
        messageActionsView.setClickListener(clickListeners)
        linkPreviewView.setClickListener(clickListeners)

        icSendMessage.setOnClickListener {
            when (inputState) {
                Text -> clickListeners.onSendMsgClick(it)
                Voice -> clickListeners.onVoiceClick(it)
            }
        }

        icSendMessage.setOnLongClickListener {
            if (inputState == Voice) clickListeners.onVoiceLongClick(it)
            return@setOnLongClickListener true
        }

        icAddAttachments.setOnClickListener {
            clickListeners.onAddAttachmentClick(it)
        }

        btnJoin.setOnClickListener {
            clickListeners.onJoinClick()
        }

        btnClearChat.setOnClickListener {
            clickListeners.onClearChatClick()
        }

        layoutSearchControl.icDown.setOnClickListener {
            clickListeners.onScrollToNextMessageClick()
        }

        layoutSearchControl.icUp.setOnClickListener {
            clickListeners.onScrollToPreviousMessageClick()
        }
    }

    private fun addInoutListeners() {
        binding.messageInput.setInlineQueryChangedListener(object : InlineQueryChangedListener {
            override fun onQueryChanged(inlineQuery: InlineQuery) {
                when (inlineQuery) {
                    is InlineQuery.Mention -> {
                        if (enableMention)
                            eventListeners.onMentionUsersListener(inlineQuery.query)
                    }

                    else -> setMentionList(emptyList())
                }
            }
        })

        binding.messageInput.setStylingChangedListener(::updateDraftMessage)
    }

    private fun sendMessage() {
        val body = binding.messageInput.text
        if (body.isNullOrBlank() && allAttachments.isEmpty() && editMessage?.attachments.isNullOrEmpty()) {
            if (isEditingMessage())
                customToastSnackBar(this, context.getString(R.string.sceyt_empty_message_body_message))
            return
        }

        closeReplyOrEditView {
            messageToSendHelper.sendMessage(allAttachments, body, editMessage, replyMessage,
                replyThreadMessageId, linkDetails)
            reset(clearInput = true, closeLinkPreview = true)
        }
    }

    private fun isEditingMessage() = editMessage != null

    private fun tryToSendRecording(file: File?, amplitudes: IntArray, duration: Int) {
        file ?: run {
            finishRecording()
            return
        }
        val metadata = Gson().toJson(AudioMetadata(amplitudes, duration))
        createAttachmentWithPaths(file.path, metadata = metadata,
            attachmentType = AttachmentTypeEnum.Voice.value).getOrNull(0)?.let {
            allAttachments.add(it)
            sendMessage()
        } ?: finishRecording()
    }

    private fun finishRecording() {
        binding.layoutInput.isVisible = true
        showVoiceRecorder()
        binding.voiceRecordPlaybackView.isVisible = false
        binding.messageInput.setText(empty)
        determineInputState()
        binding.messageInput.requestFocus()
    }

    private fun canShowRecorderView() = !disabledInputByGesture && !isInputHidden && inputState == Voice

    private fun VoiceRecorderView.setRecordingListener() {
        setListener(object : RecordingListener {
            override fun onRecordingStarted() {
                val directoryToSaveRecording = context.filesDir.path + "/Audio"
                AudioPlayerHelper.pauseAll()
                audioRecorderHelper.startRecording(directoryToSaveRecording) {}
                binding.layoutInput.isInvisible = true
                voiceRecorderView?.keepScreenOn = true
            }

            override fun onRecordingCompleted(shouldShowPreview: Boolean) {
                audioRecorderHelper.stopRecording { isTooShort, file, duration, amplitudes ->
                    if (isTooShort) {
                        finishRecording()
                        return@stopRecording
                    }
                    if (shouldShowPreview) {
                        showRecordPreview(file, amplitudes, duration)
                    } else {
                        finishRecording()
                        tryToSendRecording(file, amplitudes.toIntArray(), duration)
                    }
                    voiceRecorderView?.keepScreenOn = false
                }
            }

            override fun onRecordingCanceled() {
                audioRecorderHelper.cancelRecording()
                finishRecording()
                voiceRecorderView?.keepScreenOn = false
            }
        })
    }

    private fun showRecordPreview(file: File?, amplitudes: Array, duration: Int) {
        file ?: return
        val metadata = AudioMetadata(amplitudes.toIntArray(), duration)
        binding.voiceRecordPlaybackView.init(file, metadata, object : VoiceRecordPlaybackView.VoiceRecordPlaybackListeners {
            override fun onDeleteVoiceRecord() {
                file.deleteOnExit()
                finishRecording()
            }

            override fun onSendVoiceMessage() {
                tryToSendRecording(file, amplitudes.toIntArray(), duration)
                finishRecording()
            }
        })
        voiceRecorderView?.isVisible = false
        binding.voiceRecordPlaybackView.isVisible = true
    }

    private fun handleAttachmentClick() {
        ChooseFileTypeDialog(context).setChooseListener { chooseType ->
            when (chooseType) {
                PickType.Gallery -> selectFileTypePopupClickListeners.onGalleryClick()
                PickType.Photo -> selectFileTypePopupClickListeners.onTakePhotoClick()
                PickType.Video -> selectFileTypePopupClickListeners.onTakeVideoClick()
                PickType.File -> selectFileTypePopupClickListeners.onFileClick()
            }
        }.show()
    }

    private fun SceytMessageInputViewBinding.applyStyle() {
        layoutInput.setBackgroundColor(style.backgroundColor)
        viewAttachments.setBackgroundColor(style.dividerColor)
        divider.setBackgroundColor(style.dividerColor)
        icSendMessage.setBackgroundTint(style.sendIconBackgroundColor)
        icAddAttachments.setImageDrawable(style.attachmentIcon)
        enableVoiceRecord = style.enableVoiceRecord
        enableSendAttachment = style.enableSendAttachment
        enableMention = style.enableMention
        style.inputStyle.apply(messageInput, null)
        style.joinButtonStyle.apply(btnJoin)
        style.clearChatTextStyle.apply(btnClearChat)
        applySelectedMediaStyle(style.selectedMediaStyle)
        applySearchResultStyle(style.messageSearchControlsStyle)
        layoutInputCover.applyInputCoverStyle(style.inputCoverStyle)
        icAddAttachments.isVisible = enableSendAttachment
        if (isInEditMode) {
            icSendMessage.setImageDrawable(if (enableVoiceRecord)
                style.voiceRecordIcon else style.sendMessageIcon)
        }
    }

    private fun SceytMessageInputViewBinding.applySelectedMediaStyle(style: InputSelectedMediaStyle) {
        rvAttachments.setBackgroundColor(style.backgroundColor)
    }

    private fun SceytMessageInputViewBinding.applySearchResultStyle(style: MessageSearchControlsStyle) {
        layoutSearchControl.root.setBackgroundColor(style.backgroundColor)
        layoutSearchControl.icDown.setImageDrawable(style.previousIcon)
        layoutSearchControl.icUp.setImageDrawable(style.nextIcon)
        style.resultTextStyle.apply(layoutSearchControl.tvResult)
    }

    private fun SceytDisableMessageInputBinding.applyInputCoverStyle(style: InputCoverStyle) {
        root.setBackgroundColor(style.backgroundColor)
        divider.setBackgroundColor(style.dividerColor)
        style.textStyle.apply(tvMessage)
    }

    private fun determineInputState() {
        if (!isEnabledInput() || isInMultiSelectMode || isInSearchMode || isInEditMode)
            return

        val showVoiceIcon = enableVoiceRecord && binding.messageInput.text?.trim().isNullOrEmpty() && allAttachments.isEmpty()
                && !binding.voiceRecordPlaybackView.isShowing && !isEditingMessage()
        val newState = if (showVoiceIcon) Voice else Text
        if (inputState != newState)
            onStateChanged(newState)
        inputState = newState

        binding.icSendMessage.isInvisible = showVoiceIcon
        binding.icAddAttachments.isVisible = enableSendAttachment && !isEditingMessage()
        binding.viewAttachments.isVisible = allAttachments.isNotEmpty()
        if (showVoiceIcon) {
            showVoiceRecorder()
        } else hideAndStopVoiceRecorder()
    }

    private fun isEnabledInput() = !disabledInputByGesture && !isInputHidden

    private fun addAttachments(attachments: List) {
        binding.viewAttachments.isVisible = true
        allAttachments.addAll(attachments)
        attachmentsAdapter.addItems(attachments.map { AttachmentItem(it) })
        determineInputState()
    }

    private fun setupAttachmentsList() {
        attachmentsAdapter = AttachmentsAdapter(
            data = allAttachments.map { AttachmentItem(it) },
            factory = attachmentsViewHolderFactory.also {
                it.setClickListener(AttachmentClickListeners.RemoveAttachmentClickListener { _, item ->
                    clickListeners.onRemoveAttachmentClick(item)
                })
            })
        binding.rvAttachments.adapter = attachmentsAdapter
    }

    private fun showHideJoinButton(show: Boolean) {
        isInputHidden = show
        binding.btnJoin.isVisible = show
        binding.layoutInput.isVisible = !disabledInputByGesture && !show
        binding.layoutInputCover.root.isVisible = disabledInputByGesture && !show
        voiceRecorderView?.isVisible = canShowRecorderView()
    }

    private fun checkIsExistAttachment(path: String?): Boolean {
        return allAttachments.map { it.filePath }.contains(path)
    }

    private fun closeReplyOrEditView(readyCb: (() -> Unit?)? = null) {
        if (replyMessage == null && editMessage == null)
            readyCb?.invoke()
        else binding.messageActionsView.close(readyCb)
    }

    private fun closeLinkDetailsView(readyCb: (() -> Unit?)? = null) {
        if (linkDetails == null)
            readyCb?.invoke()
        else binding.linkPreviewView.hideLinkDetails(readyCb)
    }

    private fun hideInputWithMessage(message: String, @DrawableRes startIcon: Int) {
        with(binding) {
            layoutInputCover.apply {
                tvMessage.text = message
                icStateIcon.setImageResource(startIcon)
                icStateIcon.isVisible = startIcon != 0
                root.isVisible = true
            }
            layoutInput.isInvisible = true
            messageInput.setText(empty)
            btnJoin.isVisible = false
            viewAttachments.isVisible = false
            hideAndStopVoiceRecorder()
        }
    }

    private fun showInput() {
        if (isRecording())
            binding.layoutInput.isInvisible = true
        else
            binding.layoutInput.isVisible = true

        binding.layoutInputCover.root.isVisible = false
        determineInputState()
    }

    private fun showVoiceRecorder() {
        if (canShowRecorderView())
            voiceRecorderView?.isVisible = true
    }

    private fun hideAndStopVoiceRecorder() {
        voiceRecorderView?.isVisible = false
        voiceRecorderView?.forceStopRecording()
    }

    private fun initMentionUsersContainer() {
        if (mentionUsersListView == null)
            (parent as? ViewGroup)?.addView(MentionUsersListView(context).apply {
                setStyle(style.mentionUsersListStyle)
                mentionUsersListView = initWithMessageInputView(this@MessageInputView).also {
                    setUserClickListener {
                        clickListeners.onSelectedUserToMentionClick(it)
                    }
                }
            })
    }

    private fun onStateChanged(newState: InputState) {
        eventListeners.onInputStateChanged(binding.icSendMessage, newState)
    }

    private fun initInputWithEditMessage(message: SceytMessage) {
        with(binding) {
            var body = MessageBodyStyleHelper.buildOnlyTextStyles(message.body, message.bodyAttributes)
            if (!message.mentionedUsers.isNullOrEmpty()) {
                val data = MentionUserHelper.getMentionsIndexed(context, message.bodyAttributes, message.mentionedUsers)
                body = MentionAnnotation.setMentionAnnotations(body, data)
            }
            messageInput.setText(body)
            messageInput.text?.let { text -> messageInput.setSelection(text.length) }
            context.showSoftInput(messageInput)
        }
    }

    internal fun editMessage(message: SceytMessage, initWithDraft: Boolean) {
        checkIfRecordingAndConfirm {
            replyMessage = null
            editMessage = message
            determineInputState()
            if (!initWithDraft)
                initInputWithEditMessage(message)
            binding.messageActionsView.editMessage(message)
            if (!initWithDraft)
                updateDraftMessage()
        }
    }

    internal fun replyMessage(message: SceytMessage, initWithDraft: Boolean) {
        checkIfRecordingAndConfirm {
            editMessage = null
            replyMessage = message
            binding.messageActionsView.replyMessage(message)

            if (!initWithDraft) {
                context.showSoftInput(binding.messageInput)
                updateDraftMessage()
            }
        }
    }

    internal fun setReplyInThreadMessageId(messageId: Long?) {
        replyThreadMessageId = messageId
    }

    internal fun setDraftMessage(draftMessage: DraftMessage?) {
        if (draftMessage == null || draftMessage.body.isNullOrEmpty())
            return
        var body: CharSequence
        binding.messageInput.removeTextChangedListener(inputTextWatcher)
        with(binding.messageInput) {
            body = style.draftMessageBodyFormatterAttributes.format(context,
                from = DraftMessageBodyFormatterAttributes(
                    message = draftMessage,
                    mentionTextStyle = style.mentionTextStyle,
                    mentionUserNameFormatter = style.mentionUserNameFormatter
                ))
            if (!draftMessage.mentionUsers.isNullOrEmpty()) {
                val data = MentionUserHelper.getMentionsIndexed(context, draftMessage.bodyAttributes,
                    draftMessage.mentionUsers)
                body = MentionAnnotation.setMentionAnnotations(body, data)
            }
            setTextAndMoveSelectionEnd(body)

            if (draftMessage.replyOrEditMessage != null)
                if (draftMessage.isReply)
                    replyMessage(draftMessage.replyOrEditMessage, initWithDraft = true)
                else editMessage(draftMessage.replyOrEditMessage, initWithDraft = true)
        }
        determineInputState()
        addInputTextWatcher()
    }

    internal fun checkIsParticipant(channel: SceytChannel) {
        when (channel.getChannelType()) {
            ChannelTypeEnum.Public -> {
                if (channel.userRole.isNullOrBlank()) {
                    showHideJoinButton(true)
                } else showHideJoinButton(false)
            }

            ChannelTypeEnum.Direct -> {
                val isBlockedPeer = channel.isPeerBlocked()
                with(binding) {
                    if (isBlockedPeer) {
                        viewAttachments.isVisible = false
                        messageActionsView.isVisible = false
                    }
                    isInputHidden = if (isBlockedPeer) {
                        hideInputWithMessage(getString(R.string.sceyt_you_blocked_this_user), R.drawable.sceyt_ic_warning)
                        true
                    } else {
                        if (disabledInputByGesture.not())
                            showInput()
                        false
                    }
                }
            }

            else -> return
        }
    }

    internal fun joinSuccess() {
        showHideJoinButton(false)
    }

    internal fun onChannelLeft() {
        showHideJoinButton(true)
    }

    internal fun getEventListeners() = eventListeners

    internal fun onSearchMessagesResult(data: SearchResult) {
        with(binding.layoutSearchControl) {
            val hasResult = data.messages.isNotEmpty()
            tvResult.text = if (hasResult)
                "${data.currentIndex + 1} ${getString(R.string.sceyt_of)} ${data.messages.size}"
            else getString(R.string.sceyt_not_found)
            icDown.isEnabled = hasResult && data.currentIndex > 0
            icUp.isEnabled = hasResult && data.currentIndex < data.messages.lastIndex
        }
    }

    private fun setInitialStateSearchMessagesResult() {
        with(binding.layoutSearchControl) {
            tvResult.text = empty
            icDown.isEnabled = false
            icUp.isEnabled = false
        }
    }

    internal fun setInputActionsCallback(callback: MessageInputActionCallback) {
        messageInputActionCallback = callback
    }

    @SuppressWarnings("WeakerAccess")
    fun checkIfRecordingAndConfirm(onConfirm: () -> Unit) {
        if (isRecording()) {
            SceytDialog.showDialog(context, R.string.sceyt_stop_recording,
                R.string.sceyt_stop_recording_desc, R.string.sceyt_discard, positiveCb = {
                    stopRecording()
                    onConfirm()
                })
        } else onConfirm()
    }

    @SuppressWarnings("WeakerAccess")
    fun createAttachmentWithPaths(vararg filePath: String,
                                  metadata: String = "",
                                  attachmentType: String? = null): MutableList {
        val attachments = mutableListOf()
        for (path in filePath) {
            if (checkIsExistAttachment(path))
                continue

            val attachment = messageToSendHelper.buildAttachment(path, metadata = metadata,
                attachmentType = attachmentType)
            if (attachment != null) {
                attachments.add(attachment)
            } else
                Toast.makeText(context, "\"${File(path).name}\" ${getString(R.string.sceyt_unsupported_file_format)}", Toast.LENGTH_SHORT).show()
        }
        return attachments
    }

    fun addAttachment(vararg filePath: String) {
        val attachments = createAttachmentWithPaths(*filePath)
        addAttachments(attachments)
    }

    @SuppressWarnings("WeakerAccess")
    fun reset(clearInput: Boolean, closeLinkPreview: Boolean) {
        if (clearInput)
            binding.messageInput.text = null
        closeReplyOrEditView()
        if (closeLinkPreview)
            closeLinkDetailsView()
        editMessage = null
        replyMessage = null
        allAttachments.clear()
        attachmentsAdapter.clear()
        determineInputState()
        updateDraftMessage()
    }

    @Suppress("Unused")
    fun disableInputWithMessage(message: String, @DrawableRes startIcon: Int = R.drawable.sceyt_ic_warning) {
        disabledInputByGesture = true
        hideInputWithMessage(message, startIcon)
    }

    @Suppress("unused")
    fun enableInput() {
        disabledInputByGesture = false
        if (!isInputHidden)
            showInput()
    }

    fun setMentionValidator(validator: MentionValidatorWatcher.MentionValidator) {
        binding.messageInput.setMentionValidator(validator)
    }

    @Suppress("unused")
    fun enableDisableMention(enable: Boolean) {
        enableMention = enable
    }

    @SuppressWarnings("WeakerAccess")
    fun stopRecording() {
        voiceRecorderView?.forceStopRecording()
    }

    fun isEmpty() = binding.messageInput.text.isNullOrBlank() && allAttachments.isEmpty()

    @SuppressWarnings("WeakerAccess")
    fun isRecording() = voiceRecorderView?.isRecording == true

    fun getComposedMessage() = binding.messageInput.text

    fun getInputCover() = binding.layoutInputCover

    val inputEditText: EditText get() = binding.messageInput

    fun setClickListener(listener: MessageInputClickListeners) {
        clickListeners.setListener(listener)
    }

    fun setCustomClickListener(listener: MessageInputClickListenersImpl) {
        clickListeners = listener
    }

    fun setActionListener(listener: InputActionsListener) {
        actionListeners.setListener(listener)
    }

    fun setCustomActionListener(listener: InputActionsListenerImpl) {
        actionListeners = listener
    }

    @Suppress("unused")
    fun setEventListener(listener: InputEventsListener) {
        eventListeners.setListener(listener)
    }

    fun setCustomEventListener(listener: InputEventsListenerImpl) {
        eventListeners = listener
    }

    @Suppress("unused")
    fun setCustomSelectFileTypePopupClickListener(listener: SelectFileTypePopupClickListenersImpl) {
        selectFileTypePopupClickListeners = listener
    }

    fun setCustomAttachmentViewHolderFactory(factory: AttachmentsViewHolderFactory) {
        attachmentsViewHolderFactory = factory
    }

    fun setSaveUrlsPlace(savePathsTo: MutableSet) {
        filePickerHelper?.setSaveUrlsPlace(savePathsTo)
    }

    fun setMentionList(data: List) {
        if (data.isEmpty() && mentionUsersListView == null) return
        initMentionUsersContainer()
        mentionUsersListView?.setMentionList(data.toSet().take(30))
    }

    override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
        super.onSizeChanged(w, h, oldw, oldh)
        mentionUsersListView?.onInputSizeChanged(h)
    }

    override fun onSendMsgClick(view: View) {
        sendMessage()
    }

    override fun onAddAttachmentClick(view: View) {
        handleAttachmentClick()
    }

    override fun onVoiceClick(view: View) {

    }

    override fun onVoiceLongClick(view: View) {

    }

    override fun onCancelReplyMessageViewClick(view: View) {
        closeReplyOrEditView()
        reset(replyMessage == null, false)
    }

    override fun onCancelLinkPreviewClick(view: View) {
        closeLinkDetailsView()
        linkDetails = linkDetails?.copy(hideDetails = true)
    }

    override fun onRemoveAttachmentClick(item: AttachmentItem) {
        attachmentsAdapter.removeItem(item)
        allAttachments.remove(item.attachment)
        binding.viewAttachments.isVisible = allAttachments.isNotEmpty()
        determineInputState()
    }

    override fun onJoinClick() {
        messageInputActionCallback?.join()
    }

    private fun getPickerListener(): BottomSheetMediaPicker.PickerListener {
        return BottomSheetMediaPicker.PickerListener {
            addAttachment(*it.map { mediaData -> mediaData.realPath }.toTypedArray())
            // Remove attachments that are not in the picker result
            allAttachments.filter { item ->
                item.type.isEqualsVideoOrImage() && it.none { mediaData -> mediaData.realPath == item.filePath }
            }.forEach { attachment ->
                val item = AttachmentItem(attachment)
                attachmentsAdapter.removeItem(item)
                allAttachments.remove(attachment)
            }
        }
    }

    override fun onAttachedToWindow() {
        super.onAttachedToWindow()
        BottomSheetMediaPicker.pickerListener?.let {
            BottomSheetMediaPicker.pickerListener = getPickerListener()
        }
    }

    override fun onWindowFocusChanged(hasWindowFocus: Boolean) {
        if (!hasWindowFocus) {
            binding.messageInput.clearFocus()
            hideSoftInput()
        }
    }

    // Choose file type popup listeners
    override fun onGalleryClick() {
        binding.messageInput.clearFocus()
        filePickerHelper?.openMediaPicker(
            pickerListener = getPickerListener(),
            selections = allAttachments.map { it.filePath }.toTypedArray(),
            maxSelectCount = SceytChatUIKit.config.attachmentSelectionLimit
        )
    }

    override fun onTakePhotoClick() {
        filePickerHelper?.takePicture {
            addAttachment(it)
        }
    }

    override fun onTakeVideoClick() {
        filePickerHelper?.takeVideo {
            addAttachment(it)
        }
    }

    override fun onFileClick() {
        filePickerHelper?.chooseMultipleFiles(allowMultiple = true) {
            addAttachment(*it.toTypedArray())
        }
    }

    override fun onInputStateChanged(sendImage: ImageView, state: InputState) {
        val iconResId = if (state == Voice) style.voiceRecordIcon
        else style.sendMessageIcon
        binding.icSendMessage.setImageDrawable(iconResId)
    }

    // Input actions listeners
    override fun sendMessage(message: Message, linkDetails: LinkPreviewDetails?) {
        messageInputActionCallback?.sendMessage(message, linkDetails)
    }

    override fun sendMessages(message: List, linkDetails: LinkPreviewDetails?) {
        messageInputActionCallback?.sendMessages(message, linkDetails)
    }

    override fun sendEditMessage(message: SceytMessage, linkDetails: LinkPreviewDetails?) {
        messageInputActionCallback?.sendEditMessage(message, linkDetails)
    }

    override fun sendTyping(typing: Boolean) {
        messageInputActionCallback?.sendTyping(typing)
    }

    override fun updateDraftMessage(text: Editable?, mentionUserIds: List, styling: List?, replyOrEditMessage: SceytMessage?, isReply: Boolean) {
        messageInputActionCallback?.updateDraftMessage(text, mentionUserIds, styling, replyOrEditMessage, isReply)
    }

    override fun onMentionUsersListener(query: String) {
        messageInputActionCallback?.mention(query)
    }

    override fun onClearChatClick() {
        messageInputActionCallback?.clearChat()
    }

    override fun onScrollToNextMessageClick() {
        messageInputActionCallback?.scrollToNext()
    }

    override fun onScrollToPreviousMessageClick() {
        messageInputActionCallback?.scrollToPrev()
    }

    override fun onSelectedUserToMentionClick(member: SceytMember) {
        val name = style.mentionUserNameFormatter.format(context, member.user).notAutoCorrectable()
        binding.messageInput.replaceTextWithMention(name, member.id)
    }

    override fun onMultiselectModeListener(isMultiselectMode: Boolean) {
        with(binding) {
            isInMultiSelectMode = isMultiselectMode
            showHideInputOnModeChange(isMultiselectMode)
            if (isMultiselectMode) {
                btnClearChat.animateToVisible(150)
            } else
                btnClearChat.animateToGone(150)
        }
    }

    override fun onSearchModeChangeListener(inSearchMode: Boolean) {
        with(binding) {
            isInSearchMode = inSearchMode
            showHideInputOnModeChange(inSearchMode)
            if (inSearchMode) {
                setInitialStateSearchMessagesResult()
                layoutSearchControl.root.animateToVisible(150)
            } else
                layoutSearchControl.root.animateToGone(150)
        }
    }

    private fun showHideInputOnModeChange(isInSelectMode: Boolean) {
        with(binding) {
            layoutInput.isInvisible = isInSelectMode
            viewAttachments.isVisible = !isInSelectMode && allAttachments.isNotEmpty()
            if (isInSelectMode) {
                hideAndStopVoiceRecorder()
                closeReplyOrEditView()
                closeLinkDetailsView()
            } else {
                replyMessage?.let { replyMessage(it, initWithDraft = true) }
                editMessage?.let { editMessage(it, initWithDraft = true) }
                linkDetails?.let {
                    linkPreviewView.showLinkDetails(it)
                }
                determineInputState()
            }
        }
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy