Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
102 changes: 14 additions & 88 deletions app/src/main/java/com/nextcloud/talk/chat/MessageInputFragment.kt
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@
package com.nextcloud.talk.chat

import android.content.res.Resources
import android.graphics.drawable.Drawable
import android.os.Build
import android.os.Bundle
import android.os.CountDownTimer
Expand Down Expand Up @@ -36,8 +35,8 @@ import android.widget.LinearLayout
import android.widget.PopupMenu
import android.widget.RelativeLayout
import android.widget.SeekBar
import androidx.appcompat.content.res.AppCompatResources
import androidx.appcompat.view.ContextThemeWrapper
import androidx.compose.material3.MaterialTheme
import androidx.core.graphics.drawable.toDrawable
import androidx.core.view.isVisible
import androidx.core.widget.doAfterTextChanged
Expand All @@ -48,11 +47,7 @@ import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import autodagger.AutoInjector
import coil.Coil.imageLoader
import coil.load
import coil.request.ImageRequest
import coil.target.Target
import coil.transform.CircleCropTransformation
import com.google.android.flexbox.FlexboxLayout
import com.google.android.material.button.MaterialButton
import com.google.android.material.snackbar.Snackbar
Expand All @@ -72,6 +67,7 @@ import com.nextcloud.talk.models.json.chat.ChatUtils
import com.nextcloud.talk.models.json.mention.Mention
import com.nextcloud.talk.models.json.signaling.NCSignalingMessage
import com.nextcloud.talk.presenters.MentionAutocompletePresenter
import com.nextcloud.talk.ui.CallStartedBanner
import com.nextcloud.talk.ui.MicInputCloud
import com.nextcloud.talk.ui.dialog.AttachmentDialog
import com.nextcloud.talk.ui.theme.ViewThemeUtils
Expand All @@ -81,12 +77,10 @@ import com.nextcloud.talk.utils.CapabilitiesUtil
import com.nextcloud.talk.utils.CharPolicy
import com.nextcloud.talk.utils.ConversationUtils
import com.nextcloud.talk.utils.DateUtils
import com.nextcloud.talk.utils.DisplayUtils
import com.nextcloud.talk.utils.EmojiTextInputEditText
import com.nextcloud.talk.utils.ImageEmojiEditText
import com.nextcloud.talk.utils.SpreedFeatures
import com.nextcloud.talk.utils.bundle.BundleKeys
import com.nextcloud.talk.utils.database.user.CurrentUserProviderOld
import com.nextcloud.talk.utils.message.MessageUtils
import com.nextcloud.talk.utils.text.Spans
import com.otaliastudios.autocomplete.Autocomplete
Expand All @@ -108,9 +102,6 @@ class MessageInputFragment : Fragment() {
@Inject
lateinit var userManager: UserManager

@Inject
lateinit var currentUserProvider: CurrentUserProviderOld

@Inject
lateinit var networkMonitor: NetworkMonitor

Expand All @@ -129,7 +120,6 @@ class MessageInputFragment : Fragment() {
private var mentionAutocomplete: Autocomplete<*>? = null
private var xcounter = 0f
private var ycounter = 0f
private var collapsed = false
private var hasScheduledMessages = false
private lateinit var spreedCapabilities: SpreedCapability
private var hasSharedText = false
Expand Down Expand Up @@ -295,46 +285,19 @@ class MessageInputFragment : Fragment() {
}.collect()
}

viewLifecycleOwner.lifecycleScope.launch {
chatActivity.chatViewModel.lastCallSystemMessage.collect {
if (it.shouldShow) {
binding.fragmentCallStarted.callAuthorChip.text = it.actorDisplayName
val user = currentUserProvider.currentUser.blockingGet()
val url: String = if (it.actorType == "guests" || it.actorType == "guest") {
ApiUtils.getUrlForGuestAvatar(user!!.baseUrl!!, it.actorDisplayName, true)
} else {
ApiUtils.getUrlForAvatar(
user!!.baseUrl!!,
it.actorId,
false,
darkMode = DisplayUtils.isDarkModeOn(requireContext())
)
}

val imageRequest: ImageRequest = ImageRequest.Builder(requireContext())
.data(url)
.crossfade(true)
.transformations(CircleCropTransformation())
.target(object : Target {
override fun onStart(placeholder: Drawable?) {
// unused atm
}

override fun onError(error: Drawable?) {
// unused atm
}

override fun onSuccess(result: Drawable) {
binding.fragmentCallStarted.callAuthorChip.chipIcon = result
}
})
.build()
binding.fragmentCallStarted.setContent {
MaterialTheme(colorScheme = viewThemeUtils.getColorScheme(requireContext())) {
CallStartedBanner(
viewThemeUtils = viewThemeUtils,
onJoinVideoCall = { chatActivity.joinVideoCall() },
onJoinAudioCall = { chatActivity.joinAudioCall() }
)
}
}

imageLoader(requireContext()).enqueue(imageRequest)
binding.fragmentCallStarted.root.visibility = View.VISIBLE
} else {
binding.fragmentCallStarted.root.visibility = View.GONE
}
viewLifecycleOwner.lifecycleScope.launch {
chatActivity.chatViewModel.hasCall.collect { hasCall ->
binding.fragmentCallStarted.visibility = if (hasCall) View.VISIBLE else View.GONE
}
}
}
Expand Down Expand Up @@ -565,36 +528,11 @@ class MessageInputFragment : Fragment() {
binding.fragmentMessageInputView.button?.contentDescription =
resources.getString(R.string.nc_description_send_message_button)

binding.fragmentCallStarted.joinAudioCall.setOnClickListener {
chatActivity.joinAudioCall()
}

binding.fragmentCallStarted.joinVideoCall.setOnClickListener {
chatActivity.joinVideoCall()
}

binding.fragmentCallStarted.callStartedCloseBtn.setOnClickListener {
collapsed = !collapsed
binding.fragmentCallStarted.callBtnLayout.visibility = if (collapsed) View.GONE else View.VISIBLE
setDropDown(collapsed)
}

binding.fragmentMessageInputView.findViewById<ImageButton>(R.id.cancelReplyButton)?.setOnClickListener {
cancelReply()
}
}

private fun setDropDown(collapsed: Boolean) {
val drawable = if (collapsed) {
AppCompatResources.getDrawable(requireContext(), R.drawable.ic_keyboard_arrow_up)
} else {
AppCompatResources.getDrawable(requireContext(), R.drawable.ic_keyboard_arrow_down)
}

binding.fragmentCallStarted.callStartedCloseBtn.setImageDrawable(drawable)
viewThemeUtils.platform.colorImageView(binding.fragmentCallStarted.callStartedCloseBtn, ColorRole.PRIMARY)
}

@Suppress("ClickableViewAccessibility", "CyclomaticComplexMethod", "LongMethod")
private fun initVoiceRecordButton() {
handleButtonsVisibility()
Expand Down Expand Up @@ -1191,18 +1129,6 @@ class MessageInputFragment : Fragment() {
viewThemeUtils.platform.colorImageView(it, ColorRole.PRIMARY)
}

binding.fragmentCallStarted.callStartedBackground.apply {
viewThemeUtils.talk.themeOutgoingMessageBubble(this, grouped = true, false)
}

binding.fragmentCallStarted.callAuthorChip.apply {
viewThemeUtils.material.colorChipBackground(this)
}

binding.fragmentCallStarted.callStartedCloseBtn.apply {
viewThemeUtils.platform.colorImageView(this, ColorRole.PRIMARY)
}

binding.fragmentMessageInputView.submitThreadButton.apply {
viewThemeUtils.platform.colorImageView(this, ColorRole.SECONDARY)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -704,10 +704,6 @@ class ChatMessageSyncer @Inject constructor(
ChatMessage.SystemMessageType.MESSAGE_EDITED ->
upsertParentMessage(target, messageJson)

ChatMessage.SystemMessageType.LOBBY_NONE,
ChatMessage.SystemMessageType.LOBBY_NON_MODERATORS,
ChatMessage.SystemMessageType.LOBBY_OPEN_TO_EVERYONE -> needsRoomRefresh = true

ChatMessage.SystemMessageType.CLEARED_CHAT -> {
// for lookIntoFuture just deleting everything would be fine.
// But lets say we did not open the chat for a while and in between it was cleared.
Expand All @@ -719,8 +715,21 @@ class ChatMessageSyncer @Inject constructor(
chatBlocksDao.deleteChatBlocksOlderThan(target.internalConversationId, messageJson.id)
}

ChatMessage.SystemMessageType.LOBBY_NONE,
ChatMessage.SystemMessageType.LOBBY_NON_MODERATORS,
ChatMessage.SystemMessageType.LOBBY_OPEN_TO_EVERYONE,
ChatMessage.SystemMessageType.MESSAGE_PINNED,
ChatMessage.SystemMessageType.MESSAGE_UNPINNED -> needsRoomRefresh = true
ChatMessage.SystemMessageType.MESSAGE_UNPINNED,
ChatMessage.SystemMessageType.CALL_STARTED,
ChatMessage.SystemMessageType.CALL_ENDED,
ChatMessage.SystemMessageType.CALL_ENDED_EVERYONE,
ChatMessage.SystemMessageType.CALL_MISSED,
ChatMessage.SystemMessageType.CALL_TRIED -> {
val messageAgeMillis = System.currentTimeMillis() - messageJson.timestamp * MILLIS_PER_SECOND
if (messageAgeMillis <= ROOM_REFRESH_MAX_AGE_MILLIS) {
needsRoomRefresh = true
}
}

else -> {}
}
Expand Down Expand Up @@ -837,6 +846,7 @@ class ChatMessageSyncer @Inject constructor(

private const val DEFAULT_MESSAGES_LIMIT = 100
private const val MILLIS_PER_SECOND = 1000L
private const val ROOM_REFRESH_MAX_AGE_MILLIS = 3 * 60 * 60 * 1000L // 3 hours
private const val CATCH_UP_COOLDOWN_MILLIS = 5_000L
private const val MAX_CATCH_UP_RUNS_PER_BURST = 3
private const val MAX_BACKLOG_ROUNDS = 5
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -468,7 +468,9 @@ class ChatViewModel @AssistedInject constructor(

private val nonNullUserFlow = currentUserFlow.filterNotNull()

private val conversationFlow: Flow<ConversationModel> =
// Unlike conversationFlow below, this is not deduped by lastReadMessage/lastCommonReadMessage,
// so it also reacts to fields those two ignore, e.g. hasCall (see [hasCall]).
private val rawConversationFlow: Flow<ConversationModel> =
nonNullUserFlow
.flatMapLatest { user ->
val userId = requireNotNull(user.id)
Expand All @@ -483,7 +485,9 @@ class ChatViewModel @AssistedInject constructor(
null
}
}
.distinctUntilChangedBy { it.lastReadMessage to it.lastCommonReadMessage }

private val conversationFlow: Flow<ConversationModel> =
rawConversationFlow.distinctUntilChangedBy { it.lastReadMessage to it.lastCommonReadMessage }

private val conversationAndUserFlow =
combine(conversationFlow, nonNullUserFlow) { c, u -> c to u }
Expand Down Expand Up @@ -1021,23 +1025,11 @@ class ChatViewModel @AssistedInject constructor(
val capabilities: SpreedCapability? = null
)

data class CallStartedIndicatorData(
val actorDisplayName: String,
val actorType: String,
val actorId: String,
val shouldShow: Boolean
)

private val _lastCallSystemMessage = MutableStateFlow<ChatMessage?>(null)

val lastCallSystemMessage = _lastCallSystemMessage.map { msg ->
CallStartedIndicatorData(
msg?.actorDisplayName ?: "",
msg?.actorType ?: "",
msg?.actorId ?: "",
msg != null
)
}
// The conversation's own hasCall is the server's authoritative answer to "is a call currently
// active" — unlike a scan over whatever chat message window happens to be loaded (which used to
// make the call-started banner reappear for calls that had long since ended, since a loaded
// window's contents don't necessarily reflect what's actually happening right now).
val hasCall: Flow<Boolean> = rawConversationFlow.map { it.hasCall }.distinctUntilChanged()

private val _callEndedSystemMessage = MutableSharedFlow<ChatMessage.SystemMessageType>(extraBufferCapacity = 1)
val callEndedSystemMessage: SharedFlow<ChatMessage.SystemMessageType>
Expand Down Expand Up @@ -1384,16 +1376,13 @@ class ChatViewModel @AssistedInject constructor(
val chatMessageMap = chatMessageList.associateBy { it.jsonMessageId }.toMutableMap()
val chatMessageIterator = chatMessageMap.iterator()

// Only drives the "conversation will be deleted after the call" warning (callEndedSystemMessage)
// — the call-started banner itself comes from hasCall / conversation.hasCall now.
chatMessageList.lastOrNull {
it.systemMessageType in
listOf(
ChatMessage.SystemMessageType.CALL_STARTED,
ChatMessage.SystemMessageType.CALL_JOINED,
ChatMessage.SystemMessageType.CALL_LEFT,
ChatMessage.SystemMessageType.CALL_ENDED,
ChatMessage.SystemMessageType.CALL_TRIED,
ChatMessage.SystemMessageType.CALL_ENDED_EVERYONE,
ChatMessage.SystemMessageType.CALL_MISSED
ChatMessage.SystemMessageType.CALL_ENDED_EVERYONE
)
}?.let { callMessage ->
processCallSystemMessage(callMessage)
Expand All @@ -1416,24 +1405,10 @@ class ChatViewModel @AssistedInject constructor(
val isInitialSnapshot = !hasSeenInitialCallSystemMessage
hasSeenInitialCallSystemMessage = true

when (recent.systemMessageType) {
ChatMessage.SystemMessageType.CALL_STARTED -> {
_lastCallSystemMessage.tryEmit(recent)
}
ChatMessage.SystemMessageType.CALL_ENDED,
ChatMessage.SystemMessageType.CALL_ENDED_EVERYONE -> {
_lastCallSystemMessage.tryEmit(null)
if (!isInitialSnapshot && lastNotifiedCallEndedMessageId != recent.jsonMessageId) {
_callEndedSystemMessage.tryEmit(recent.systemMessageType!!)
}
lastNotifiedCallEndedMessageId = recent.jsonMessageId
}
ChatMessage.SystemMessageType.CALL_MISSED,
ChatMessage.SystemMessageType.CALL_TRIED -> {
_lastCallSystemMessage.tryEmit(null)
}
else -> {}
if (!isInitialSnapshot && lastNotifiedCallEndedMessageId != recent.jsonMessageId) {
_callEndedSystemMessage.tryEmit(recent.systemMessageType!!)
}
lastNotifiedCallEndedMessageId = recent.jsonMessageId
}

private fun isInfoMessageAboutDeletion(currentMessage: MutableMap.MutableEntry<Int, ChatMessage>): Boolean =
Expand Down
Loading
Loading