diff --git a/.drone.yml b/.drone.yml index c7613596714..9123b04a06c 100644 --- a/.drone.yml +++ b/.drone.yml @@ -51,7 +51,7 @@ steps: commands: - emulator -avd android -no-snapshot -gpu swiftshader_indirect -no-window -no-audio -skin 500x833 & - scripts/wait_for_emulator.sh - - ./gradlew --console=plain testGplayDebugUnitTest connectedGplayDebugAndroidTest + - ./gradlew --console=plain --stacktrace testGplayDebugUnitTest connectedGplayDebugAndroidTest services: - name: server @@ -81,6 +81,6 @@ trigger: - pull_request --- kind: signature -hmac: cf0c19e54fa45d1ee226f5f05202a32329b90aaf46711ea073c566a4c4a8a6c5 +hmac: d3f0eb5c71a3a463a52789aa577b3ca742616a8d966ac90c187774179693f5ea ... diff --git a/app/src/main/java/com/nextcloud/talk/chat/ChatActivity.kt b/app/src/main/java/com/nextcloud/talk/chat/ChatActivity.kt index dc66a09531a..7731e32f1f4 100644 --- a/app/src/main/java/com/nextcloud/talk/chat/ChatActivity.kt +++ b/app/src/main/java/com/nextcloud/talk/chat/ChatActivity.kt @@ -137,8 +137,12 @@ import com.nextcloud.talk.api.NcApiCoroutines import com.nextcloud.talk.application.NextcloudTalkApplication import com.nextcloud.talk.chat.data.model.ChatMessage import com.nextcloud.talk.chat.data.model.FileParameters +import com.nextcloud.talk.chat.ui.MessageActionsBottomSheet import com.nextcloud.talk.chat.ui.ProfileModalBottomSheet import com.nextcloud.talk.chat.ui.ShowReactionsModalBottomSheet +import com.nextcloud.talk.chat.ui.TempMessageActionsBottomSheet +import com.nextcloud.talk.chat.ui.buildMessageActionsState +import com.nextcloud.talk.data.database.model.SendStatus import com.nextcloud.talk.chat.ui.model.MessageTypeContent import com.nextcloud.talk.chat.viewmodels.ChatViewModel import com.nextcloud.talk.chat.viewmodels.MessageInputViewModel @@ -183,9 +187,7 @@ import com.nextcloud.talk.ui.chat.ChatViewState import com.nextcloud.talk.ui.dialog.DateTimeCompose import com.nextcloud.talk.ui.dialog.FileAttachmentPreviewFragment import com.nextcloud.talk.ui.dialog.GetPinnedOptionsDialog -import com.nextcloud.talk.ui.dialog.MessageActionsDialog import com.nextcloud.talk.ui.dialog.SaveToStorageDialogFragment -import com.nextcloud.talk.ui.dialog.TempMessageActionsDialog import com.nextcloud.talk.ui.theme.LocalMessageUtils import com.nextcloud.talk.ui.theme.LocalOpenGraphFetcher import com.nextcloud.talk.ui.theme.LocalViewThemeUtils @@ -834,6 +836,87 @@ class ChatActivity : ) } } + + val messageActionsMessageId by chatViewModel.messageActionsMessageId.collectAsStateWithLifecycle() + val messageActionsMessage by produceState(null, messageActionsMessageId) { + value = messageActionsMessageId?.let { id -> chatViewModel.getMessageById(id).first() } + } + val isOnline by networkMonitor.isOnline.collectAsStateWithLifecycle() + messageActionsMessage?.let { msg -> + if (msg.isTemporary) { + val sendingFailed = msg.sendStatus == SendStatus.FAILED + TempMessageActionsBottomSheet( + showResend = sendingFailed && isOnline, + showEdit = sendingFailed || !isOnline, + showDelete = sendingFailed || !isOnline, + onResend = { + chatViewModel.resendMessage( + conversationUser!!.getCredentials(), + ApiUtils.getUrlForChat(chatApiVersion, conversationUser!!.baseUrl!!, roomToken), + msg + ) + }, + onEdit = { messageInputViewModel.edit(msg) }, + onDelete = { chatViewModel.deleteTempMessage(msg) }, + onCopy = { copyMessage(msg) }, + onDismiss = { chatViewModel.dismissMessageActions() } + ) + } else { + conversationUser?.let { user -> + MessageActionsBottomSheet( + actionsState = buildMessageActionsState( + message = msg, + user = user, + conversation = currentConversation, + hasChatPermission = participantPermissions.hasChatPermission(), + hasReactPermission = participantPermissions.hasReactPermission(), + spreedCapabilities = spreedCapabilities, + isOnline = isOnline, + dateUtils = dateUtils, + conversationThreadId = conversationThreadId + ), + onEmojiClick = { emoji -> + if (msg.reactionsSelf?.contains(emoji) == true) { + chatViewModel.deleteReaction(roomToken, msg, emoji) + } else { + chatViewModel.addReaction(roomToken, msg, emoji) + } + }, + onReply = { + if (msg.isThread && conversationThreadId == null) { + openThread(msg) + } else { + messageInputViewModel.reply(msg) + } + }, + onReplyPrivately = { replyPrivately(msg) }, + onOpenThread = { msg.threadId?.let { openThread(it) } }, + onForward = { forwardMessage(msg) }, + onEdit = { messageInputViewModel.edit(msg) }, + onCopy = { copyMessage(msg) }, + onMarkAsUnread = { markAsUnread(msg) }, + onRemind = { remindMeLater(msg) }, + onPin = { pinMessage(msg) }, + onUnpin = { unPinMessage(msg) }, + onTranslate = { translateMessage(msg) }, + onShareToNote = { shareToNotes(msg) }, + onShare = { + if (msg.getCalculateMessageType() == + ChatMessage.MessageType.SINGLE_NC_ATTACHMENT_MESSAGE + ) { + checkIfSharable(msg) + } else { + msg.message?.let { shareMessageText(it) } + } + }, + onSave = { checkIfSaveable(msg) }, + onOpenInFiles = { openInFilesApp(msg) }, + onDelete = { deleteMessage(msg) }, + onDismiss = { chatViewModel.dismissMessageActions() } + ) + } + } + } } } } @@ -3776,24 +3859,8 @@ class ChatActivity : } private fun openMessageActionsDialog(message: ChatMessage) { - if (message.isTemporary) { - TempMessageActionsDialog( - this, - message - ).show() - } else if (hasVisibleItems(message) && - !isSystemMessage(message) - ) { - MessageActionsDialog( - this, - message, - conversationUser, - currentConversation, - isShowMessageDeletionButton(message), - participantPermissions.hasChatPermission(), - participantPermissions.hasReactPermission(), - spreedCapabilities - ).show() + if (message.isTemporary || (hasVisibleItems(message) && !isSystemMessage(message))) { + chatViewModel.showMessageActions(message.jsonMessageId.toLong()) } } diff --git a/app/src/main/java/com/nextcloud/talk/chat/data/model/BlurHashDecoder.kt b/app/src/main/java/com/nextcloud/talk/chat/data/model/BlurHashDecoder.kt index 41eeeec168e..46140bccf28 100644 --- a/app/src/main/java/com/nextcloud/talk/chat/data/model/BlurHashDecoder.kt +++ b/app/src/main/java/com/nextcloud/talk/chat/data/model/BlurHashDecoder.kt @@ -17,6 +17,8 @@ import kotlin.math.PI import kotlin.math.cos import kotlin.math.pow import kotlin.math.withSign +import androidx.core.graphics.createBitmap +import androidx.core.graphics.set private const val BLURHASH_CHARS = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz#\$%*+,-.:;=?@[]^_{|}~" @@ -137,10 +139,10 @@ internal object BlurHashDecoder { return Color.rgb(linearToSrgb(r), linearToSrgb(g), linearToSrgb(b)) } - val bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888) + val bitmap = createBitmap(width, height) for (y in 0 until height) { for (x in 0 until width) { - bitmap.setPixel(x, y, computePixel(x, y)) + bitmap[x, y] = computePixel(x, y) } } return bitmap diff --git a/app/src/main/java/com/nextcloud/talk/chat/ui/MessageActionsBottomSheet.kt b/app/src/main/java/com/nextcloud/talk/chat/ui/MessageActionsBottomSheet.kt new file mode 100644 index 00000000000..8ec37b1f7f4 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/chat/ui/MessageActionsBottomSheet.kt @@ -0,0 +1,899 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package com.nextcloud.talk.chat.ui + +import android.annotation.SuppressLint +import android.content.Context +import android.content.res.Configuration +import android.os.Handler +import android.os.Looper +import android.view.MotionEvent +import android.view.inputmethod.InputMethodManager +import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.BoxWithConstraints +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.navigationBarsPadding +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.AlertDialog +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.ModalBottomSheet +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.material3.darkColorScheme +import androidx.compose.material3.lightColorScheme +import androidx.compose.material3.rememberModalBottomSheetState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.RectangleShape +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalView +import androidx.compose.ui.res.dimensionResource +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.sp +import androidx.compose.ui.viewinterop.AndroidView +import androidx.core.content.ContextCompat +import com.nextcloud.talk.R +import com.nextcloud.talk.chat.data.model.ChatMessage +import com.nextcloud.talk.data.user.model.User +import com.nextcloud.talk.models.domain.ConversationModel +import com.nextcloud.talk.models.json.capabilities.SpreedCapability +import com.nextcloud.talk.models.json.conversations.ConversationEnums +import com.nextcloud.talk.utils.CapabilitiesUtil +import com.nextcloud.talk.utils.CapabilitiesUtil.hasSpreedFeatureCapability +import com.nextcloud.talk.utils.ConversationUtils +import com.nextcloud.talk.utils.DateConstants +import com.nextcloud.talk.utils.DateUtils +import com.nextcloud.talk.utils.SpreedFeatures +import com.vanniktech.emoji.Emoji +import com.vanniktech.emoji.EmojiEditText +import com.vanniktech.emoji.EmojiPopup +import com.vanniktech.emoji.installDisableKeyboardInput +import com.vanniktech.emoji.installForceSingleEmoji +import com.vanniktech.emoji.recent.RecentEmojiManager +import com.vanniktech.emoji.search.SearchEmojiManager +import java.util.Date + +private const val TAG = "MessageActionsSheet" +private const val AGE_THRESHOLD_FOR_EDIT_MESSAGE = 86400000L +private const val AGE_THRESHOLD_FOR_DELETE_MESSAGE = 21600000L +private const val EMOJI_POPUP_TOGGLE_DELAY = 200L +private const val MAX_RECENTS = 8 +private const val ACTOR_BOTS = "bots" + +private val emojiSearchKeywords = mapOf( + "๐Ÿ‘" to "thumbsup", + "๐Ÿ‘Ž" to "thumbsdown", + "โค๏ธ" to "heart", + "๐Ÿ˜‚" to "joy", + "๐Ÿ˜•" to "confused", + "๐Ÿ˜ข" to "cry", + "๐Ÿ™" to "pray", + "๐Ÿ”ฅ" to "fire" +) + +private fun buildEmojiList(recentEmojiManager: RecentEmojiManager): List { + val recentEmojis = recentEmojiManager.getRecentEmojis() + val searchEmojiManager = SearchEmojiManager() + val initialKeywords = listOf("thumbsup", "thumbsdown", "heart", "joy", "confused", "cry", "pray", "fire") + val initialEmojisFromSearch = mutableSetOf() + initialKeywords.forEach { keyword -> + val results = searchEmojiManager.search(keyword) + if (results.isNotEmpty()) { + initialEmojisFromSearch.add(results[0].component1()) + recentEmojiManager.addEmoji(results[0].component1()) + } + if (initialEmojisFromSearch.size >= MAX_RECENTS) return@forEach + } + return (recentEmojis + initialEmojisFromSearch).distinct().take(MAX_RECENTS).map { it.unicode } +} + +data class MessageActionsState( + val showEmojiBar: Boolean, + val selfReactions: Set, + val showEditInfo: Boolean, + val lastEditedBy: String, + val lastEditedAt: String, + val showReply: Boolean, + val showReplyPrivately: Boolean, + val showOpenThread: Boolean, + val showForward: Boolean, + val showEdit: Boolean, + val showCopy: Boolean, + val showMarkAsUnread: Boolean, + val showRemind: Boolean, + val showPin: Boolean, + val isPinned: Boolean, + val showTranslate: Boolean, + val showShareToNote: Boolean, + val showShare: Boolean, + val showSave: Boolean, + val showOpenInFiles: Boolean, + val showDelete: Boolean +) + +@Suppress("LongParameterList", "CyclomaticComplexMethod", "LongMethod") +internal fun buildMessageActionsState( + message: ChatMessage, + user: User?, + conversation: ConversationModel?, + hasChatPermission: Boolean, + hasReactPermission: Boolean, + spreedCapabilities: SpreedCapability, + isOnline: Boolean, + dateUtils: DateUtils, + conversationThreadId: Long? +): MessageActionsState { + val messageType = message.getCalculateMessageType() + val messageHasFileAttachment = ChatMessage.MessageType.SINGLE_NC_ATTACHMENT_MESSAGE == messageType + val messageHasRegularText = ChatMessage.MessageType.REGULAR_TEXT_MESSAGE == messageType && !message.isDeleted + val messageHasCaptions = messageHasFileAttachment && message.message != "{file}" && !message.isDeleted + + val isOlderThanTwentyFourHours = message.createdAt + .before(Date(System.currentTimeMillis() - AGE_THRESHOLD_FOR_EDIT_MESSAGE)) + val isOlderThanSixHours = message.createdAt + .before(Date(System.currentTimeMillis() - AGE_THRESHOLD_FOR_DELETE_MESSAGE)) + + val isUserAllowedByPrivileges = if (user == null) { + false + } else if (message.actorId == user.userId) { + true + } else if (conversation != null) { + ConversationUtils.canModerate(conversation, spreedCapabilities) + } else { + false + } + + val isNoTimeLimitOnNoteToSelf = + hasSpreedFeatureCapability(spreedCapabilities, SpreedFeatures.EDIT_MESSAGES_NOTE_TO_SELF) && + conversation?.type == ConversationEnums.ConversationType.NOTE_TO_SELF + val isMessageBotOneToOne = message.actorType == ACTOR_BOTS && + (message.isOneToOneConversation || message.isFormerOneToOneConversation) && + !isOlderThanTwentyFourHours + val messageIsEditable = hasSpreedFeatureCapability(spreedCapabilities, SpreedFeatures.EDIT_MESSAGES) && + (messageHasRegularText || messageHasCaptions) && + !isOlderThanTwentyFourHours && + isUserAllowedByPrivileges + val isMessageEditable = isNoTimeLimitOnNoteToSelf || messageIsEditable || isMessageBotOneToOne + + val hasDeleteMessagesUnlimitedCapability = + hasSpreedFeatureCapability(spreedCapabilities, SpreedFeatures.DELETE_MESSAGES_UNLIMITED) + val showMessageDeletionButton = when { + !isUserAllowedByPrivileges -> false + !hasDeleteMessagesUnlimitedCapability && isOlderThanSixHours -> false + message.systemMessageType != ChatMessage.SystemMessageType.DUMMY -> false + message.isDeleted -> false + !hasSpreedFeatureCapability(spreedCapabilities, SpreedFeatures.DELETE_MESSAGES) -> false + !hasChatPermission -> false + else -> true + } + + val canPin = message.isOneToOneConversation || + (conversation != null && ConversationUtils.isParticipantOwnerOrModerator(conversation)) + val isConversationReadOnly = ConversationEnums.ConversationReadOnlyState.CONVERSATION_READ_ONLY == + conversation?.conversationReadOnlyState + val isReactable = !message.isCommandMessage && !message.isDeletedCommentMessage && !message.isDeleted + + val showEmojiBar = hasSpreedFeatureCapability(spreedCapabilities, SpreedFeatures.REACTIONS) && + hasReactPermission && + !isConversationReadOnly && + isReactable + + val editedTimestamp = message.lastEditTimestamp ?: 0L + val showEditInfo = editedTimestamp != 0L && !message.isDeleted + val lastEditedBy = if (showEditInfo) { + message.lastEditActorDisplayName ?: "" + } else { + "" + } + val lastEditedAt = if (showEditInfo) { + dateUtils.getLocalDateTimeStringFromTimestamp(editedTimestamp * DateConstants.SECOND_DIVIDER) + } else { + "" + } + + val hasUserId = user?.userId?.isNotEmpty() == true && user.userId != "?" + val hasUserActorId = message.actorType == "users" && message.actorId != conversation?.actorId + + return MessageActionsState( + showEmojiBar = showEmojiBar, + selfReactions = message.reactionsSelf?.toSet() ?: emptySet(), + showEditInfo = showEditInfo, + lastEditedBy = lastEditedBy, + lastEditedAt = lastEditedAt, + showReply = message.replyable && hasChatPermission, + showReplyPrivately = message.replyable && + hasUserId && + hasUserActorId && + conversation?.type != ConversationEnums.ConversationType.ROOM_TYPE_ONE_TO_ONE_CALL && + isOnline, + showOpenThread = message.isThread && conversationThreadId == null, + showForward = ChatMessage.MessageType.REGULAR_TEXT_MESSAGE == messageType && + !(message.isDeletedCommentMessage || message.isDeleted) && + isOnline, + showEdit = isMessageEditable, + showCopy = !message.isDeleted, + showMarkAsUnread = hasSpreedFeatureCapability(spreedCapabilities, SpreedFeatures.CHAT_READ_MARKER) && + ChatMessage.MessageType.SYSTEM_MESSAGE != messageType && + isOnline, + showRemind = !message.isDeleted && + hasSpreedFeatureCapability(spreedCapabilities, SpreedFeatures.REMIND_ME_LATER) && + isOnline, + showPin = !message.isDeleted && + hasSpreedFeatureCapability(spreedCapabilities, SpreedFeatures.PINNED_MESSAGES) && + isOnline && + canPin, + isPinned = conversation?.lastPinnedId == message.jsonMessageId.toLong(), + showTranslate = !message.isDeleted && + ChatMessage.MessageType.REGULAR_TEXT_MESSAGE == messageType && + CapabilitiesUtil.isTranslationsSupported(spreedCapabilities) && + isOnline, + showShareToNote = !message.isDeleted && + !ConversationUtils.isNoteToSelfConversation(conversation) && + isOnline, + showShare = messageHasFileAttachment || (messageHasRegularText && isOnline), + showSave = messageHasFileAttachment, + showOpenInFiles = messageHasFileAttachment && isOnline, + showDelete = showMessageDeletionButton && isOnline + ) +} + +@Suppress("LongParameterList") +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun MessageActionsBottomSheet( + actionsState: MessageActionsState, + onEmojiClick: (String) -> Unit, + onReply: () -> Unit, + onReplyPrivately: () -> Unit, + onOpenThread: () -> Unit, + onForward: () -> Unit, + onEdit: () -> Unit, + onCopy: () -> Unit, + onMarkAsUnread: () -> Unit, + onRemind: () -> Unit, + onPin: () -> Unit, + onUnpin: () -> Unit, + onTranslate: () -> Unit, + onShareToNote: () -> Unit, + onShare: () -> Unit, + onSave: () -> Unit, + onOpenInFiles: () -> Unit, + onDelete: () -> Unit, + onDismiss: () -> Unit +) { + val sheetState = rememberModalBottomSheetState() + ModalBottomSheet( + onDismissRequest = onDismiss, + sheetState = sheetState, + containerColor = MaterialTheme.colorScheme.surfaceContainerLow + ) { + MessageActionsSheetContent( + actionsState = actionsState, + onEmojiClick = onEmojiClick, + onReply = onReply, + onReplyPrivately = onReplyPrivately, + onOpenThread = onOpenThread, + onForward = onForward, + onEdit = onEdit, + onCopy = onCopy, + onMarkAsUnread = onMarkAsUnread, + onRemind = onRemind, + onPin = onPin, + onUnpin = onUnpin, + onTranslate = onTranslate, + onShareToNote = onShareToNote, + onShare = onShare, + onSave = onSave, + onOpenInFiles = onOpenInFiles, + onDelete = onDelete, + onDismiss = onDismiss + ) + } +} + +@Suppress("LongParameterList", "LongMethod", "CyclomaticComplexMethod") +@Composable +internal fun MessageActionsSheetContent( + actionsState: MessageActionsState, + onEmojiClick: (String) -> Unit, + onReply: () -> Unit, + onReplyPrivately: () -> Unit, + onOpenThread: () -> Unit, + onForward: () -> Unit, + onEdit: () -> Unit, + onCopy: () -> Unit, + onMarkAsUnread: () -> Unit, + onRemind: () -> Unit, + onPin: () -> Unit, + onUnpin: () -> Unit, + onTranslate: () -> Unit, + onShareToNote: () -> Unit, + onShare: () -> Unit, + onSave: () -> Unit, + onOpenInFiles: () -> Unit, + onDelete: () -> Unit, + onDismiss: () -> Unit +) { + var actionsVisible by remember { mutableStateOf(true) } + var showDeleteConfirmation by remember { mutableStateOf(false) } + + Column( + modifier = Modifier + .fillMaxWidth() + .navigationBarsPadding() + ) { + if (actionsState.showEmojiBar) { + EmojiBar( + selfReactions = actionsState.selfReactions, + onEmojiClick = { emoji -> + onEmojiClick(emoji) + onDismiss() + }, + onPickerShown = { actionsVisible = false }, + onPickerDismissed = { actionsVisible = true } + ) + } + + if (actionsVisible) { + Column( + modifier = Modifier + .fillMaxWidth() + .verticalScroll(rememberScrollState()) + ) { + if (actionsState.showEditInfo) { + EditedInfo( + editedBy = actionsState.lastEditedBy, + editedAt = actionsState.lastEditedAt + ) + } + if (actionsState.showReply) { + MessageActionItem( + iconRes = R.drawable.ic_reply, + text = stringResource(R.string.nc_reply), + onClick = { + onReply() + onDismiss() + } + ) + } + if (actionsState.showReplyPrivately) { + MessageActionItem( + iconRes = R.drawable.ic_reply, + text = stringResource(R.string.nc_reply_privately), + onClick = { + onReplyPrivately() + onDismiss() + } + ) + } + if (actionsState.showOpenThread) { + MessageActionItem( + iconRes = R.drawable.outline_forum_24, + text = stringResource(R.string.open_thread), + onClick = { + onOpenThread() + onDismiss() + } + ) + } + if (actionsState.showForward) { + MessageActionItem( + iconRes = R.drawable.forward_24, + text = stringResource(R.string.nc_forward_message), + onClick = { + onForward() + onDismiss() + } + ) + } + if (actionsState.showEdit) { + MessageActionItem( + iconRes = R.drawable.ic_edit_24, + text = stringResource(R.string.nc_edit_message), + onClick = { + onEdit() + onDismiss() + } + ) + } + if (actionsState.showCopy) { + MessageActionItem( + iconRes = R.drawable.ic_content_copy, + text = stringResource(R.string.nc_copy_message), + onClick = { + onCopy() + onDismiss() + } + ) + } + if (actionsState.showMarkAsUnread) { + MessageActionItem( + iconRes = R.drawable.ic_mark_chat_unread_24px, + text = stringResource(R.string.nc_mark_as_unread), + onClick = { + onMarkAsUnread() + onDismiss() + } + ) + } + if (actionsState.showRemind) { + MessageActionItem( + iconRes = R.drawable.ic_timer_black_24dp, + text = stringResource(R.string.nc_remind), + onClick = { + onRemind() + onDismiss() + } + ) + } + if (actionsState.showPin) { + if (actionsState.isPinned) { + MessageActionItem( + iconRes = R.drawable.keep_off_24px, + text = stringResource(R.string.unpin_message), + onClick = { + onUnpin() + onDismiss() + } + ) + } else { + MessageActionItem( + iconRes = R.drawable.keep_24px, + text = stringResource(R.string.pin_message), + onClick = { + onPin() + onDismiss() + } + ) + } + } + if (actionsState.showTranslate) { + MessageActionItem( + iconRes = R.drawable.ic_baseline_translate_24, + text = stringResource(R.string.translate), + onClick = { + onTranslate() + onDismiss() + } + ) + } + if (actionsState.showShareToNote) { + MessageActionItem( + iconRes = R.drawable.ic_edit_note_24, + text = stringResource(R.string.add_to_notes), + onClick = { + onShareToNote() + onDismiss() + } + ) + } + if (actionsState.showShare) { + MessageActionItem( + iconRes = R.drawable.ic_share_action, + text = stringResource(R.string.share), + onClick = { + onShare() + onDismiss() + } + ) + } + if (actionsState.showSave) { + MessageActionItem( + iconRes = R.drawable.baseline_download_24, + text = stringResource(R.string.nc_save_message), + onClick = { + onSave() + onDismiss() + } + ) + } + if (actionsState.showOpenInFiles) { + MessageActionItem( + iconRes = R.drawable.ic_exit_to_app_black_24dp, + text = stringResource(R.string.open_in_files_app), + onClick = { + onOpenInFiles() + onDismiss() + } + ) + } + if (actionsState.showDelete) { + MessageActionItem( + iconRes = R.drawable.ic_delete, + text = stringResource(R.string.nc_delete), + onClick = { showDeleteConfirmation = true } + ) + } + } + } + } + + if (showDeleteConfirmation) { + DeleteConfirmationDialog( + onConfirm = { + onDelete() + onDismiss() + }, + onDismiss = { showDeleteConfirmation = false } + ) + } +} + +@Composable +internal fun EmojiBar( + selfReactions: Set, + onEmojiClick: (String) -> Unit, + onPickerShown: () -> Unit, + onPickerDismissed: () -> Unit +) { + val context = LocalContext.current + val recentEmojiManager = remember(context) { RecentEmojiManager(context, MAX_RECENTS) } + val emojis: List = remember(context) { buildEmojiList(recentEmojiManager) } + + val startPadding = dimensionResource(R.dimen.standard_padding) + val emojiButtonSize = dimensionResource(R.dimen.reaction_bottom_sheet_layout_size) + val emojiSpacing = dimensionResource(R.dimen.standard_quarter_margin) + val moreButtonWidth = dimensionResource(R.dimen.activity_row_layout_height) + + BoxWithConstraints(modifier = Modifier.fillMaxWidth()) { + // Slot width per emoji when using Arrangement.spacedBy(emojiSpacing): + // each emoji contributes its own size plus one spacing gap before the next item. + // Available space = maxWidth - startPadding - moreButtonWidth. + // N โ‰ค (available) / (emojiButtonSize + emojiSpacing) + val slotWidth = emojiButtonSize + emojiSpacing + val availableForEmojis = maxWidth - startPadding - moreButtonWidth + val maxVisible = maxOf(0, (availableForEmojis / slotWidth).toInt()) + + Row( + modifier = Modifier.padding(start = startPadding), + horizontalArrangement = Arrangement.spacedBy(emojiSpacing), + verticalAlignment = Alignment.CenterVertically + ) { + emojis.take(maxVisible).forEach { unicodeEmoji -> + EmojiButton( + emoji = unicodeEmoji, + isSelected = selfReactions.contains(unicodeEmoji), + onClick = { + onEmojiClick(unicodeEmoji) + val keyword = emojiSearchKeywords[unicodeEmoji] ?: "" + val results = SearchEmojiManager().search(keyword) + if (results.isNotEmpty()) { + recentEmojiManager.addEmoji(results[0].component1()) + recentEmojiManager.persist() + } + } + ) + } + MoreEmojiButton( + onEmojiSelected = onEmojiClick, + onPickerShown = onPickerShown, + onPickerDismissed = onPickerDismissed + ) + } + } +} + +@Composable +private fun EmojiButton(emoji: String, isSelected: Boolean, onClick: () -> Unit) { + val size = dimensionResource(R.dimen.reaction_bottom_sheet_layout_size) + Surface( + onClick = onClick, + shape = CircleShape, + color = if (isSelected) MaterialTheme.colorScheme.secondaryContainer else Color.Transparent, + modifier = Modifier.size(size) + ) { + Row( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.Center + ) { + Text(text = emoji, fontSize = 20.sp) + } + } +} + +@SuppressLint("ClickableViewAccessibility") +@Suppress("LongMethod") +@Composable +private fun MoreEmojiButton( + onEmojiSelected: (String) -> Unit, + onPickerShown: () -> Unit, + onPickerDismissed: () -> Unit +) { + val context = LocalContext.current + val rootView = LocalView.current + val popupRef = remember { mutableStateOf(null) } + + DisposableEffect(Unit) { + onDispose { popupRef.value?.dismiss() } + } + + AndroidView( + factory = { ctx -> + EmojiEditText(ctx).apply { + setBackgroundColor(android.graphics.Color.TRANSPARENT) + val drawable = ContextCompat.getDrawable(ctx, R.drawable.ic_dots_horizontal) + setCompoundDrawablesWithIntrinsicBounds(null, null, drawable, null) + contentDescription = ctx.getString(R.string.emoji_more) + + val emojiPopup = EmojiPopup( + rootView = rootView, + editText = this, + onEmojiPopupShownListener = { + clearFocus() + onPickerShown() + }, + onEmojiClickListener = { emoji -> + popupRef.value?.dismiss() + onEmojiSelected(emoji.unicode) + }, + onEmojiPopupDismissListener = { + clearFocus() + val imm = ctx.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager + imm.hideSoftInputFromWindow(windowToken, 0) + onPickerDismissed() + } + ) + installDisableKeyboardInput(emojiPopup) + installForceSingleEmoji() + setOnTouchListener { _, event -> + if (event.action == MotionEvent.ACTION_DOWN) { + val popup = emojiPopup + if (popup.isShowing) { + popup.dismiss() + } else { + popup.show() + // workaround for first-open bug (see issue #1914) + Handler(Looper.getMainLooper()).postDelayed( + { + popup.dismiss() + popup.show() + }, + EMOJI_POPUP_TOGGLE_DELAY + ) + } + } + true + } + popupRef.value = emojiPopup + } + }, + modifier = Modifier + .width(dimensionResource(R.dimen.activity_row_layout_height)) + .height(dimensionResource(R.dimen.activity_row_layout_height)) + ) +} + +@Composable +internal fun MessageActionItem(iconRes: Int, text: String, onClick: () -> Unit) { + TextButton( + onClick = onClick, + modifier = Modifier + .fillMaxWidth() + .height(dimensionResource(R.dimen.bottom_sheet_item_height)), + shape = RectangleShape, + contentPadding = PaddingValues(horizontal = dimensionResource(R.dimen.standard_dialog_padding)), + colors = ButtonDefaults.textButtonColors(contentColor = MaterialTheme.colorScheme.onSurface) + ) { + Icon( + painter = painterResource(iconRes), + contentDescription = null, + tint = MaterialTheme.colorScheme.onSurfaceVariant + ) + Spacer(modifier = Modifier.width(dimensionResource(R.dimen.standard_dialog_padding))) + Text( + text = text, + modifier = Modifier.weight(1f), + style = MaterialTheme.typography.bodyLarge, + textAlign = TextAlign.Start + ) + } +} + +@Composable +private fun EditedInfo(editedBy: String, editedAt: String) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding( + start = dimensionResource(R.dimen.standard_dialog_padding), + end = dimensionResource(R.dimen.standard_dialog_padding), + top = dimensionResource(R.dimen.standard_half_margin) + ) + ) { + if (editedBy.isNotEmpty()) { + Text( + text = editedBy, + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + if (editedAt.isNotEmpty()) { + Text( + text = editedAt, + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + } +} + +@Composable +private fun DeleteConfirmationDialog(onConfirm: () -> Unit, onDismiss: () -> Unit) { + AlertDialog( + onDismissRequest = onDismiss, + icon = { + Icon( + painter = painterResource(R.drawable.ic_delete_black_24dp), + contentDescription = null + ) + }, + title = { Text(stringResource(R.string.nc_delete_message)) }, + text = { Text(stringResource(R.string.message_delete_are_you_sure)) }, + confirmButton = { + TextButton(onClick = onConfirm) { + Text(stringResource(R.string.nc_delete)) + } + }, + dismissButton = { + TextButton(onClick = onDismiss) { + Text(stringResource(R.string.nc_cancel)) + } + } + ) +} + +@Preview(showBackground = true, name = "Light โ€” emoji bar") +@Preview(showBackground = true, name = "Dark โ€” emoji bar", uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +private fun PreviewEmojiBar() { + val colorScheme = if (isSystemInDarkTheme()) darkColorScheme() else lightColorScheme() + MaterialTheme(colorScheme = colorScheme) { + Surface(color = MaterialTheme.colorScheme.surfaceContainerLow) { + EmojiBar( + selfReactions = setOf("๐Ÿ‘", "โค๏ธ"), + onEmojiClick = {}, + onPickerShown = {}, + onPickerDismissed = {} + ) + } + } +} + +@Preview(showBackground = true, name = "Light โ€” actions") +@Preview(showBackground = true, name = "Dark โ€” actions", uiMode = Configuration.UI_MODE_NIGHT_YES) +@Preview(showBackground = true, name = "RTL ยท Arabic", locale = "ar") +@Composable +private fun PreviewMessageActionsSheetContent() { + val colorScheme = if (isSystemInDarkTheme()) darkColorScheme() else lightColorScheme() + val previewState = MessageActionsState( + showEmojiBar = true, + selfReactions = setOf("๐Ÿ‘", "โค๏ธ"), + showEditInfo = true, + lastEditedBy = "Alice", + lastEditedAt = "12:30", + showReply = true, + showReplyPrivately = true, + showOpenThread = false, + showForward = true, + showEdit = true, + showCopy = true, + showMarkAsUnread = true, + showRemind = true, + showPin = true, + isPinned = false, + showTranslate = true, + showShareToNote = true, + showShare = true, + showSave = false, + showOpenInFiles = false, + showDelete = true + ) + MaterialTheme(colorScheme = colorScheme) { + Surface(color = MaterialTheme.colorScheme.surfaceContainerLow) { + MessageActionsSheetContent( + actionsState = previewState, + onEmojiClick = {}, + onReply = {}, + onReplyPrivately = {}, + onOpenThread = {}, + onForward = {}, + onEdit = {}, + onCopy = {}, + onMarkAsUnread = {}, + onRemind = {}, + onPin = {}, + onUnpin = {}, + onTranslate = {}, + onShareToNote = {}, + onShare = {}, + onSave = {}, + onOpenInFiles = {}, + onDelete = {}, + onDismiss = {} + ) + } + } +} + +@Preview(showBackground = true, name = "Light โ€” pinned") +@Composable +private fun PreviewMessageActionsSheetPinned() { + MaterialTheme(colorScheme = lightColorScheme()) { + Surface(color = MaterialTheme.colorScheme.surfaceContainerLow) { + MessageActionsSheetContent( + actionsState = MessageActionsState( + showEmojiBar = false, + selfReactions = emptySet(), + showEditInfo = false, + lastEditedBy = "", + lastEditedAt = "", + showReply = false, + showReplyPrivately = false, + showOpenThread = false, + showForward = false, + showEdit = false, + showCopy = true, + showMarkAsUnread = false, + showRemind = false, + showPin = true, + isPinned = true, + showTranslate = false, + showShareToNote = false, + showShare = false, + showSave = false, + showOpenInFiles = false, + showDelete = false + ), + onEmojiClick = {}, + onReply = {}, + onReplyPrivately = {}, + onOpenThread = {}, + onForward = {}, + onEdit = {}, + onCopy = {}, + onMarkAsUnread = {}, + onRemind = {}, + onPin = {}, + onUnpin = {}, + onTranslate = {}, + onShareToNote = {}, + onShare = {}, + onSave = {}, + onOpenInFiles = {}, + onDelete = {}, + onDismiss = {} + ) + } + } +} diff --git a/app/src/main/java/com/nextcloud/talk/chat/ui/TempMessageActionsBottomSheet.kt b/app/src/main/java/com/nextcloud/talk/chat/ui/TempMessageActionsBottomSheet.kt new file mode 100644 index 00000000000..43bfd91b186 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/chat/ui/TempMessageActionsBottomSheet.kt @@ -0,0 +1,137 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package com.nextcloud.talk.chat.ui + +import android.content.res.Configuration +import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.navigationBarsPadding +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.ModalBottomSheet +import androidx.compose.material3.Surface +import androidx.compose.material3.darkColorScheme +import androidx.compose.material3.lightColorScheme +import androidx.compose.material3.rememberModalBottomSheetState +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import com.nextcloud.talk.R + +@Suppress("LongParameterList") +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun TempMessageActionsBottomSheet( + showResend: Boolean, + showEdit: Boolean, + showDelete: Boolean, + onResend: () -> Unit, + onEdit: () -> Unit, + onDelete: () -> Unit, + onCopy: () -> Unit, + onDismiss: () -> Unit +) { + val sheetState = rememberModalBottomSheetState() + ModalBottomSheet( + onDismissRequest = onDismiss, + sheetState = sheetState, + containerColor = MaterialTheme.colorScheme.surfaceContainerLow + ) { + TempMessageActionsContent( + showResend = showResend, + showEdit = showEdit, + showDelete = showDelete, + onResend = onResend, + onEdit = onEdit, + onDelete = onDelete, + onCopy = onCopy, + onDismiss = onDismiss + ) + } +} + +@Suppress("LongParameterList") +@Composable +internal fun TempMessageActionsContent( + showResend: Boolean, + showEdit: Boolean, + showDelete: Boolean, + onResend: () -> Unit, + onEdit: () -> Unit, + onDelete: () -> Unit, + onCopy: () -> Unit, + onDismiss: () -> Unit +) { + Column( + modifier = Modifier + .fillMaxWidth() + .navigationBarsPadding() + ) { + if (showResend) { + MessageActionItem( + iconRes = R.drawable.ic_send_24px, + text = stringResource(R.string.resend_message), + onClick = { + onResend() + onDismiss() + } + ) + } + MessageActionItem( + iconRes = R.drawable.ic_content_copy, + text = stringResource(R.string.nc_copy_message), + onClick = { + onCopy() + onDismiss() + } + ) + if (showEdit) { + MessageActionItem( + iconRes = R.drawable.ic_edit_24, + text = stringResource(R.string.nc_edit_message), + onClick = { + onEdit() + onDismiss() + } + ) + } + if (showDelete) { + MessageActionItem( + iconRes = R.drawable.ic_delete, + text = stringResource(R.string.nc_delete_message), + onClick = { + onDelete() + onDismiss() + } + ) + } + } +} + +@Preview(showBackground = true, name = "Light โ€” all items") +@Preview(showBackground = true, name = "Dark โ€” all items", uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +private fun PreviewTempMessageActionsContent() { + val colorScheme = if (isSystemInDarkTheme()) darkColorScheme() else lightColorScheme() + MaterialTheme(colorScheme = colorScheme) { + Surface(color = MaterialTheme.colorScheme.surfaceContainerLow) { + TempMessageActionsContent( + showResend = true, + showEdit = true, + showDelete = true, + onResend = {}, + onEdit = {}, + onDelete = {}, + onCopy = {}, + onDismiss = {} + ) + } + } +} diff --git a/app/src/main/java/com/nextcloud/talk/chat/viewmodels/ChatViewModel.kt b/app/src/main/java/com/nextcloud/talk/chat/viewmodels/ChatViewModel.kt index 84ff0d116bd..b9cb8978942 100644 --- a/app/src/main/java/com/nextcloud/talk/chat/viewmodels/ChatViewModel.kt +++ b/app/src/main/java/com/nextcloud/talk/chat/viewmodels/ChatViewModel.kt @@ -312,6 +312,17 @@ class ChatViewModel @AssistedInject constructor( _profileSheetMessageId.value = null } + private val _messageActionsMessageId = MutableStateFlow(null) + val messageActionsMessageId: StateFlow = _messageActionsMessageId + + fun showMessageActions(messageId: Long) { + _messageActionsMessageId.value = messageId + } + + fun dismissMessageActions() { + _messageActionsMessageId.value = null + } + val getLastCommonReadFlow = chatRepository.lastCommonReadFlow val isLoadingFlow = chatRepository.isLoadingFlow diff --git a/app/src/main/java/com/nextcloud/talk/ui/dialog/MessageActionsDialog.kt b/app/src/main/java/com/nextcloud/talk/ui/dialog/MessageActionsDialog.kt deleted file mode 100644 index 2aba2d01daa..00000000000 --- a/app/src/main/java/com/nextcloud/talk/ui/dialog/MessageActionsDialog.kt +++ /dev/null @@ -1,647 +0,0 @@ -/* - * Nextcloud Talk - Android Client - * - * SPDX-FileCopyrightText: 2022 Marcel Hibbe - * SPDX-FileCopyrightText: 2022 Andy Scherzinger - * SPDX-License-Identifier: GPL-3.0-or-later - */ -package com.nextcloud.talk.ui.dialog - -import android.annotation.SuppressLint -import android.content.Context -import android.os.Bundle -import android.os.Handler -import android.os.Looper -import android.util.Log -import android.view.MotionEvent -import android.view.View -import android.view.ViewGroup -import android.view.inputmethod.InputMethodManager -import androidx.appcompat.app.AlertDialog -import androidx.appcompat.content.res.AppCompatResources -import androidx.lifecycle.lifecycleScope -import autodagger.AutoInjector -import com.google.android.material.bottomsheet.BottomSheetBehavior -import com.google.android.material.bottomsheet.BottomSheetDialog -import com.google.android.material.dialog.MaterialAlertDialogBuilder -import com.nextcloud.talk.R -import com.nextcloud.talk.application.NextcloudTalkApplication -import com.nextcloud.talk.chat.ChatActivity -import com.nextcloud.talk.chat.data.model.ChatMessage -import com.nextcloud.talk.data.network.NetworkMonitor -import com.nextcloud.talk.data.user.model.User -import com.nextcloud.talk.databinding.DialogMessageActionsBinding -import com.nextcloud.talk.models.domain.ConversationModel -import com.nextcloud.talk.models.json.capabilities.SpreedCapability -import com.nextcloud.talk.models.json.conversations.ConversationEnums -import com.nextcloud.talk.repositories.reactions.ReactionsRepository -import com.nextcloud.talk.ui.theme.ViewThemeUtils -import com.nextcloud.talk.utils.ApiUtils -import com.nextcloud.talk.utils.CapabilitiesUtil -import com.nextcloud.talk.utils.CapabilitiesUtil.hasSpreedFeatureCapability -import com.nextcloud.talk.utils.ConversationUtils -import com.nextcloud.talk.utils.DateConstants -import com.nextcloud.talk.utils.DateUtils -import com.nextcloud.talk.utils.SpreedFeatures -import com.vanniktech.emoji.Emoji -import com.vanniktech.emoji.EmojiPopup -import com.vanniktech.emoji.EmojiTextView -import com.vanniktech.emoji.installDisableKeyboardInput -import com.vanniktech.emoji.installForceSingleEmoji -import com.vanniktech.emoji.recent.RecentEmojiManager -import com.vanniktech.emoji.search.SearchEmojiManager -import kotlinx.coroutines.launch -import java.util.Date -import javax.inject.Inject - -@AutoInjector(NextcloudTalkApplication::class) -@Suppress("LongParameterList", "TooManyFunctions") -class MessageActionsDialog( - private val chatActivity: ChatActivity, - private val message: ChatMessage, - private val user: User?, - private val currentConversation: ConversationModel?, - private val showMessageDeletionButton: Boolean, - private val hasChatPermission: Boolean, - private val hasReactPermission: Boolean, - private val spreedCapabilities: SpreedCapability -) : BottomSheetDialog(chatActivity) { - - @Inject - lateinit var viewThemeUtils: ViewThemeUtils - - @Inject - lateinit var reactionsRepository: ReactionsRepository - - @Inject - lateinit var dateUtils: DateUtils - - @Inject - lateinit var networkMonitor: NetworkMonitor - - private lateinit var dialogMessageActionsBinding: DialogMessageActionsBinding - - private lateinit var popup: EmojiPopup - - private val messageHasFileAttachment = - ChatMessage.MessageType.SINGLE_NC_ATTACHMENT_MESSAGE == message.getCalculateMessageType() - - private val messageHasRegularText = ChatMessage.MessageType.REGULAR_TEXT_MESSAGE == message - .getCalculateMessageType() && - !message.isDeleted - - private val isOlderThanTwentyFourHours = message - .createdAt - .before(Date(System.currentTimeMillis() - AGE_THRESHOLD_FOR_EDIT_MESSAGE)) - - private val canPin = message.isOneToOneConversation || - ConversationUtils.isParticipantOwnerOrModerator(currentConversation!!) - private val isUserAllowedToEdit = chatActivity.userAllowedByPrivilages(message) - private var isNoTimeLimitOnNoteToSelf = hasSpreedFeatureCapability( - spreedCapabilities, - SpreedFeatures - .EDIT_MESSAGES_NOTE_TO_SELF - ) && - currentConversation?.type == ConversationEnums.ConversationType.NOTE_TO_SELF - - private val isMessageBotOneToOne = (message.actorType == ACTOR_BOTS) && - ( - message.isOneToOneConversation || - message.isFormerOneToOneConversation - ) && - !isOlderThanTwentyFourHours - - private val messageHasCaptions = messageHasFileAttachment && message.message != "{file}" && !message.isDeleted - - private var messageIsEditable = hasSpreedFeatureCapability( - spreedCapabilities, - SpreedFeatures.EDIT_MESSAGES - ) && - (messageHasRegularText || messageHasCaptions) && - !isOlderThanTwentyFourHours && - isUserAllowedToEdit - - private val isMessageEditable = isNoTimeLimitOnNoteToSelf || messageIsEditable || isMessageBotOneToOne - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - NextcloudTalkApplication.sharedApplication?.componentApplication?.inject(this) - - dialogMessageActionsBinding = DialogMessageActionsBinding.inflate(layoutInflater) - setContentView(dialogMessageActionsBinding.root) - window?.setLayout(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT) - - viewThemeUtils.material.colorBottomSheetBackground(dialogMessageActionsBinding.root) - viewThemeUtils.material.colorBottomSheetDragHandle(dialogMessageActionsBinding.bottomSheetDragHandle) - initEmojiBar(hasReactPermission) - initMenuItemCopy(!message.isDeleted) - initMenuItems(networkMonitor.isOnline.value) - } - - private fun initMenuItems(isOnline: Boolean) { - this.lifecycleScope.launch { - initMenuItemTranslate( - !message.isDeleted && - ChatMessage.MessageType.REGULAR_TEXT_MESSAGE == message.getCalculateMessageType() && - CapabilitiesUtil.isTranslationsSupported(spreedCapabilities) && - isOnline - ) - initMenuEditorDetails(message.lastEditTimestamp!! != 0L && !message.isDeleted) - initMenuReplyToMessage(message.replyable && hasChatPermission) - initMenuReplyPrivately( - message.replyable && - hasUserId(user) && - hasUserActorId(message) && - currentConversation?.type != ConversationEnums.ConversationType.ROOM_TYPE_ONE_TO_ONE_CALL && - isOnline - ) - initMenuOpenThread(message.isThread && chatActivity.conversationThreadId == null) - initMenuEditMessage(isMessageEditable) - initMenuDeleteMessage(showMessageDeletionButton && isOnline) - initMenuForwardMessage( - ChatMessage.MessageType.REGULAR_TEXT_MESSAGE == message.getCalculateMessageType() && - !(message.isDeletedCommentMessage || message.isDeleted) && - isOnline - ) - initMenuRemindMessage( - !message.isDeleted && - hasSpreedFeatureCapability(spreedCapabilities, SpreedFeatures.REMIND_ME_LATER) && - isOnline - ) - initMenuPinMessage( - !message.isDeleted && - hasSpreedFeatureCapability(spreedCapabilities, SpreedFeatures.PINNED_MESSAGES) && - isOnline && - canPin - ) - initMenuMarkAsUnread( - hasSpreedFeatureCapability(spreedCapabilities, SpreedFeatures.CHAT_READ_MARKER) && - ChatMessage.MessageType.SYSTEM_MESSAGE != message.getCalculateMessageType() && - isOnline - ) - initMenuShare(messageHasFileAttachment || messageHasRegularText && isOnline) - initMenuItemOpenNcApp( - ChatMessage.MessageType.SINGLE_NC_ATTACHMENT_MESSAGE == message.getCalculateMessageType() && - isOnline - ) - initMenuItemSave(message.getCalculateMessageType() == ChatMessage.MessageType.SINGLE_NC_ATTACHMENT_MESSAGE) - initMenuAddToNote( - !message.isDeleted && - !ConversationUtils.isNoteToSelfConversation(currentConversation) && - networkMonitor.isOnline.value - ) - } - } - - override fun onStart() { - super.onStart() - val bottomSheet = findViewById(R.id.design_bottom_sheet) - val behavior = BottomSheetBehavior.from(bottomSheet as View) - behavior.state = BottomSheetBehavior.STATE_COLLAPSED - } - - private fun hasUserId(user: User?): Boolean = user?.userId?.isNotEmpty() == true && user.userId != "?" - - private fun hasUserActorId(message: ChatMessage): Boolean = - message.actorType == "users" && - message.actorId != currentConversation?.actorId - - @SuppressLint("ClickableViewAccessibility") - private fun initEmojiMore() { - dialogMessageActionsBinding.emojiMore.setOnTouchListener { v, event -> - if (event.action == MotionEvent.ACTION_DOWN) { - toggleEmojiPopup() - } - true - } - - popup = EmojiPopup( - rootView = dialogMessageActionsBinding.root, - editText = dialogMessageActionsBinding.emojiMore, - onEmojiPopupShownListener = { - dialogMessageActionsBinding.emojiMore.clearFocus() - dialogMessageActionsBinding.messageActions.visibility = View.GONE - }, - onEmojiClickListener = { - popup.dismiss() - clickOnEmoji(message, it.unicode) - }, - onEmojiPopupDismissListener = { - dialogMessageActionsBinding.emojiMore.clearFocus() - dialogMessageActionsBinding.messageActions.visibility = View.VISIBLE - - val imm: InputMethodManager = context.getSystemService(Context.INPUT_METHOD_SERVICE) as - InputMethodManager - imm.hideSoftInputFromWindow(dialogMessageActionsBinding.emojiMore.windowToken, 0) - } - ) - dialogMessageActionsBinding.emojiMore.installDisableKeyboardInput(popup) - dialogMessageActionsBinding.emojiMore.installForceSingleEmoji() - } - - /* - This method is a hacky workaround to avoid bug #1914 - As the bug happens only for the very first time when the popup is opened, - it is closed after some milliseconds and opened again. - */ - private fun toggleEmojiPopup() { - if (popup.isShowing) { - popup.dismiss() - } else { - popup.show() - Handler(Looper.getMainLooper()).postDelayed( - { - popup.dismiss() - popup.show() - }, - DELAY - ) - } - } - - private fun initEmojiBar(hasReactPermission: Boolean) { - if (hasSpreedFeatureCapability(spreedCapabilities, SpreedFeatures.REACTIONS) && - hasReactPermission && - isConversationWritable() && - isReactableMessageType(message) - ) { - val recentEmojiManager = RecentEmojiManager(context, MAX_RECENTS) - val recentEmojis = recentEmojiManager.getRecentEmojis() - val searchEmojiManager = SearchEmojiManager() - - val initialSearchKeywords = listOf( - "thumbsup", - "thumbsdown", - "heart", - "joy", - "confused", - "cry", - "pray", - "fire" - ) - val initialEmojisFromSearch = mutableSetOf() - - initialSearchKeywords.forEach { keyword -> - val searchResults = searchEmojiManager.search(keyword) - if (searchResults.isNotEmpty()) { - initialEmojisFromSearch.add(searchResults[ZERO_INDEX].component1()) - recentEmojiManager.addEmoji(searchResults[ZERO_INDEX].component1()) - } - if (initialEmojisFromSearch.size >= MAX_RECENTS) { - return@forEach - } - } - val combinedEmojis = (recentEmojis + initialEmojisFromSearch).toList().distinct().take(MAX_RECENTS) - - setupEmojiView(combinedEmojis, recentEmojiManager) - - dialogMessageActionsBinding.emojiMore.setOnClickListener { - dismiss() - } - initEmojiMore() - dialogMessageActionsBinding.emojiBar.visibility = View.VISIBLE - } else { - dialogMessageActionsBinding.emojiBar.visibility = View.GONE - } - } - - private fun setupEmojiView(combinedEmojis: List, recentEmojiManager: RecentEmojiManager) { - val emojiSearchKeywords = mapOf( - "๐Ÿ‘" to "thumbsup", - "๐Ÿ‘Ž" to "thumbsdown", - "โค๏ธ" to "heart", - "๐Ÿ˜‚" to "joy", - "๐Ÿ˜•" to "confused", - "๐Ÿ˜ข" to "cry", - "๐Ÿ™" to "pray", - "๐Ÿ”ฅ" to "fire" - ) - - val emojiTextViews = listOf( - dialogMessageActionsBinding.emojiThumbsUp, - dialogMessageActionsBinding.emojiThumbsDown, - dialogMessageActionsBinding.emojiHeart, - dialogMessageActionsBinding.emojiLaugh, - dialogMessageActionsBinding.emojiConfused, - dialogMessageActionsBinding.emojiCry, - dialogMessageActionsBinding.emojiPray, - dialogMessageActionsBinding.emojiFire - ) - - emojiTextViews.forEachIndexed { index, textView -> - val emoji = combinedEmojis.getOrNull(index)?.unicode - if (emoji != null) { - textView.text = emoji - checkAndSetEmojiSelfReaction(textView) - textView.setOnClickListener { - clickOnEmoji(message, emoji) - val keyword = emojiSearchKeywords[emoji] ?: "" - val result = SearchEmojiManager().search(keyword) - if (result.isNotEmpty()) { - recentEmojiManager.addEmoji(result[ZERO_INDEX].component1()) - recentEmojiManager.persist() - } - } - textView.visibility = View.VISIBLE - } else { - textView.visibility = View.GONE - } - } - } - - private fun isConversationWritable(): Boolean = - ConversationEnums.ConversationReadOnlyState.CONVERSATION_READ_ONLY != - currentConversation?.conversationReadOnlyState - - private fun isReactableMessageType(message: ChatMessage): Boolean = - !(message.isCommandMessage || message.isDeletedCommentMessage || message.isDeleted) - - private fun checkAndSetEmojiSelfReaction(emoji: EmojiTextView) { - if (emoji.text?.toString() != null && message.reactionsSelf?.contains(emoji.text?.toString()) == true) { - viewThemeUtils.talk.setCheckedBackground(emoji) - } - } - - private fun initMenuMarkAsUnread(visible: Boolean) { - if (visible) { - dialogMessageActionsBinding.menuMarkAsUnread.setOnClickListener { - chatActivity.markAsUnread(message) - dismiss() - } - } - - dialogMessageActionsBinding.menuMarkAsUnread.visibility = getVisibility(visible) - } - - private fun initMenuForwardMessage(visible: Boolean) { - if (visible) { - dialogMessageActionsBinding.menuForwardMessage.setOnClickListener { - chatActivity.forwardMessage(message) - dismiss() - } - } - - dialogMessageActionsBinding.menuForwardMessage.visibility = getVisibility(visible) - } - - private fun initMenuRemindMessage(visible: Boolean) { - if (visible) { - dialogMessageActionsBinding.menuNotifyMessage.setOnClickListener { - chatActivity.remindMeLater(message) - dismiss() - } - } - - dialogMessageActionsBinding.menuNotifyMessage.visibility = getVisibility(visible) - } - - private fun initMenuPinMessage(visible: Boolean) { - if (visible) { - dialogMessageActionsBinding.menuPinMessage.setOnClickListener { - if (currentConversation?.lastPinnedId == message.jsonMessageId.toLong()) { - chatActivity.unPinMessage(message) - } else { - chatActivity.pinMessage(message) - } - dismiss() - } - - if (currentConversation?.lastPinnedId == message.jsonMessageId.toLong()) { - dialogMessageActionsBinding.menuPinMessageText.text = context.getString(R.string.unpin_message) - val unpinnedDrawable = AppCompatResources.getDrawable(context, R.drawable.keep_off_24px) - dialogMessageActionsBinding.menuPinMessageIcon.setImageDrawable(unpinnedDrawable) - } - } - - dialogMessageActionsBinding.menuPinMessage.visibility = getVisibility(visible) - } - - private fun initMenuDeleteMessage(visible: Boolean) { - if (visible) { - dialogMessageActionsBinding.menuDeleteMessage.setOnClickListener { - val areYouSure = context.resources.getString(R.string.message_delete_are_you_sure) - val deleteMessage = context.resources.getString(R.string.nc_delete_message) - val delete = context.resources.getString(R.string.nc_delete) - val cancel = context.resources.getString(R.string.nc_cancel) - val builder = MaterialAlertDialogBuilder(context) - builder - .setIcon( - viewThemeUtils.dialog - .colorMaterialAlertDialogIcon(context, R.drawable.ic_delete_black_24dp) - ) - .setMessage(areYouSure) - .setTitle(deleteMessage) - .setPositiveButton(delete) { dialog, which -> - chatActivity.deleteMessage(message) - dismiss() - } - .setNegativeButton(cancel) { dialog, which -> - // unused atm - } - .let { dialogBuilder -> - viewThemeUtils.dialog - .colorMaterialAlertDialogBackground(context, dialogBuilder) - } - - val dialog: AlertDialog = builder.create() - dialog.setOnShowListener { - viewThemeUtils.platform.colorTextButtons( - dialog.getButton(BUTTON_POSITIVE), - dialog.getButton(BUTTON_NEGATIVE) - ) - } - dialog.show() - } - } - dialogMessageActionsBinding.menuDeleteMessage.visibility = getVisibility(visible) - } - - private fun initMenuEditMessage(visible: Boolean) { - dialogMessageActionsBinding.menuEditMessage.setOnClickListener { - chatActivity.messageInputViewModel.edit(message) - dismiss() - } - - dialogMessageActionsBinding.menuEditMessage.visibility = getVisibility(visible) - } - - private fun initMenuReplyPrivately(visible: Boolean) { - if (visible) { - dialogMessageActionsBinding.menuReplyPrivately.setOnClickListener { - chatActivity.replyPrivately(message) - dismiss() - } - } - - dialogMessageActionsBinding.menuReplyPrivately.visibility = getVisibility(visible) - } - - private fun initMenuOpenThread(visible: Boolean) { - if (visible) { - dialogMessageActionsBinding.menuOpenThread.setOnClickListener { - message.threadId?.let { - chatActivity.openThread(it) - dismiss() - } - } - } - - dialogMessageActionsBinding.menuOpenThread.visibility = getVisibility(visible) - } - - private fun initMenuReplyToMessage(visible: Boolean) { - if (visible) { - dialogMessageActionsBinding.menuReplyToMessage.setOnClickListener { - if (message.isThread && chatActivity.conversationThreadId == null) { - chatActivity.openThread(message) - } else { - chatActivity.messageInputViewModel.reply(message) - } - dismiss() - } - } - - dialogMessageActionsBinding.menuReplyToMessage.visibility = getVisibility(visible) - } - - private fun initMenuEditorDetails(showEditorDetails: Boolean) { - if (showEditorDetails) { - val editedTime = dateUtils.getLocalDateTimeStringFromTimestamp( - message.lastEditTimestamp!! * - DateConstants.SECOND_DIVIDER - ) - val lastEditorName = message.lastEditActorDisplayName ?: "" - val editorName = String.format( - context.getString(R.string.message_last_edited_by), - lastEditorName - ) - dialogMessageActionsBinding.editorName.text = editorName - dialogMessageActionsBinding.editedTime.text = editedTime - } - dialogMessageActionsBinding.menuMessageEditedInfo.visibility = getVisibility(showEditorDetails) - } - - private fun initMenuItemCopy(visible: Boolean) { - if (visible) { - dialogMessageActionsBinding.menuCopyMessage.setOnClickListener { - chatActivity.copyMessage(message) - dismiss() - } - } - - dialogMessageActionsBinding.menuCopyMessage.visibility = getVisibility(visible) - } - - private fun initMenuItemTranslate(visible: Boolean) { - if (visible) { - dialogMessageActionsBinding.menuTranslateMessage.setOnClickListener { - chatActivity.translateMessage(message) - dismiss() - } - } - - dialogMessageActionsBinding.menuTranslateMessage.visibility = getVisibility(visible) - } - - private fun initMenuShare(visible: Boolean) { - if (messageHasFileAttachment) { - dialogMessageActionsBinding.menuShare.setOnClickListener { - chatActivity.checkIfSharable(message) - dismiss() - } - } - if (messageHasRegularText) { - dialogMessageActionsBinding.menuShare.setOnClickListener { - message.message?.let { messageText -> chatActivity.shareMessageText(messageText) } - dismiss() - } - } - dialogMessageActionsBinding.menuShare.visibility = getVisibility(visible) - } - - private fun initMenuItemOpenNcApp(visible: Boolean) { - if (visible) { - dialogMessageActionsBinding.menuOpenInNcApp.setOnClickListener { - chatActivity.openInFilesApp(message) - dismiss() - } - } - - dialogMessageActionsBinding.menuOpenInNcApp.visibility = getVisibility(visible) - } - - private fun initMenuItemSave(visible: Boolean) { - if (visible) { - dialogMessageActionsBinding.menuSaveMessage.setOnClickListener { - chatActivity.checkIfSaveable(message) - dismiss() - } - } - dialogMessageActionsBinding.menuSaveMessage.visibility = getVisibility(visible) - } - - private fun initMenuAddToNote(visible: Boolean) { - if (visible) { - dialogMessageActionsBinding.menuShareToNote.setOnClickListener { - chatActivity.shareToNotes(message) - dismiss() - } - } - dialogMessageActionsBinding.menuShareToNote.visibility = getVisibility(visible) - } - - private fun getVisibility(visible: Boolean): Int = - if (visible) { - View.VISIBLE - } else { - View.GONE - } - - @Suppress("Detekt.TooGenericExceptionCaught") - private fun clickOnEmoji(message: ChatMessage, emoji: String) { - val credentials = ApiUtils.getCredentials(user!!.username, user.token) - val url = ApiUtils.getUrlForMessageReaction( - baseUrl = user.baseUrl!!, - roomToken = currentConversation!!.token, - messageId = message.jsonMessageId.toString() - ) - - chatActivity.lifecycleScope.launch { - try { - if (message.reactionsSelf?.contains(emoji) == true) { - reactionsRepository.deleteReaction( - credentials, - user.id!!, - url, - currentConversation.token, - message, - emoji - ) - } else { - reactionsRepository.addReaction( - credentials, - user.id!!, - url, - currentConversation.token, - message, - emoji - ) - } - } catch (e: Exception) { - Log.e(TAG, "clickOnEmoji error", e) - } finally { - dismiss() - } - } - } - - companion object { - private val TAG = MessageActionsDialog::class.java.simpleName - private const val ACTOR_LENGTH = 6 - private const val DELAY: Long = 200 - private const val AGE_THRESHOLD_FOR_EDIT_MESSAGE: Long = 86400000 - private const val ACTOR_BOTS = "bots" - private const val ZERO_INDEX = 0 - private const val MAX_RECENTS = 8 - } -} diff --git a/app/src/main/java/com/nextcloud/talk/ui/dialog/TempMessageActionsDialog.kt b/app/src/main/java/com/nextcloud/talk/ui/dialog/TempMessageActionsDialog.kt deleted file mode 100644 index 23930d78d0f..00000000000 --- a/app/src/main/java/com/nextcloud/talk/ui/dialog/TempMessageActionsDialog.kt +++ /dev/null @@ -1,130 +0,0 @@ -/* - * Nextcloud Talk - Android Client - * - * SPDX-FileCopyrightText: 2022 Marcel Hibbe - * SPDX-FileCopyrightText: 2022 Andy Scherzinger - * SPDX-License-Identifier: GPL-3.0-or-later - */ -package com.nextcloud.talk.ui.dialog - -import android.os.Bundle -import android.view.View -import android.view.ViewGroup -import androidx.lifecycle.lifecycleScope -import autodagger.AutoInjector -import com.google.android.material.bottomsheet.BottomSheetBehavior -import com.google.android.material.bottomsheet.BottomSheetDialog -import com.nextcloud.talk.R -import com.nextcloud.talk.application.NextcloudTalkApplication -import com.nextcloud.talk.chat.ChatActivity -import com.nextcloud.talk.chat.data.model.ChatMessage -import com.nextcloud.talk.data.database.model.SendStatus -import com.nextcloud.talk.data.network.NetworkMonitor -import com.nextcloud.talk.databinding.DialogTempMessageActionsBinding -import com.nextcloud.talk.ui.theme.ViewThemeUtils -import com.nextcloud.talk.utils.ApiUtils -import com.nextcloud.talk.utils.DateUtils -import kotlinx.coroutines.launch -import javax.inject.Inject - -@AutoInjector(NextcloudTalkApplication::class) -class TempMessageActionsDialog(private val chatActivity: ChatActivity, private val message: ChatMessage) : - BottomSheetDialog(chatActivity) { - - @Inject - lateinit var viewThemeUtils: ViewThemeUtils - - @Inject - lateinit var dateUtils: DateUtils - - @Inject - lateinit var networkMonitor: NetworkMonitor - - private lateinit var binding: DialogTempMessageActionsBinding - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - NextcloudTalkApplication.sharedApplication?.componentApplication?.inject(this) - - binding = DialogTempMessageActionsBinding.inflate(layoutInflater) - setContentView(binding.root) - window?.setLayout(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT) - - viewThemeUtils.material.colorBottomSheetBackground(binding.root) - viewThemeUtils.material.colorBottomSheetDragHandle(binding.bottomSheetDragHandle) - initMenuItems() - } - - private fun initMenuItems() { - this.lifecycleScope.launch { - val sendingFailed = message.sendStatus == SendStatus.FAILED - initResendMessage(sendingFailed && networkMonitor.isOnline.value) - initMenuEditMessage(sendingFailed || !networkMonitor.isOnline.value) - initMenuDeleteMessage(sendingFailed || !networkMonitor.isOnline.value) - initMenuItemCopy() - } - } - - override fun onStart() { - super.onStart() - val bottomSheet = findViewById(R.id.design_bottom_sheet) - val behavior = BottomSheetBehavior.from(bottomSheet as View) - behavior.state = BottomSheetBehavior.STATE_COLLAPSED - } - - private fun initResendMessage(visible: Boolean) { - if (visible) { - binding.menuResendMessage.setOnClickListener { - chatActivity.chatViewModel.resendMessage( - chatActivity.conversationUser!!.getCredentials(), - ApiUtils.getUrlForChat( - chatActivity.chatApiVersion, - chatActivity.conversationUser!!.baseUrl!!, - chatActivity.roomToken - ), - message - ) - dismiss() - } - } - binding.menuResendMessage.visibility = getVisibility(visible) - } - - private fun initMenuDeleteMessage(visible: Boolean) { - if (visible) { - binding.menuDeleteMessage.setOnClickListener { - chatActivity.chatViewModel.deleteTempMessage(message) - dismiss() - } - } - binding.menuDeleteMessage.visibility = getVisibility(visible) - } - - private fun initMenuEditMessage(visible: Boolean) { - if (visible) { - binding.menuEditMessage.setOnClickListener { - chatActivity.messageInputViewModel.edit(message) - dismiss() - } - } - binding.menuEditMessage.visibility = getVisibility(visible) - } - - private fun initMenuItemCopy() { - binding.menuCopyMessage.setOnClickListener { - chatActivity.copyMessage(message) - dismiss() - } - } - - private fun getVisibility(visible: Boolean): Int = - if (visible) { - View.VISIBLE - } else { - View.GONE - } - - companion object { - private val TAG = TempMessageActionsDialog::class.java.simpleName - } -} diff --git a/app/src/main/res/layout/dialog_message_actions.xml b/app/src/main/res/layout/dialog_message_actions.xml deleted file mode 100644 index 17a797119ef..00000000000 --- a/app/src/main/res/layout/dialog_message_actions.xml +++ /dev/null @@ -1,682 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/app/src/main/res/layout/dialog_temp_message_actions.xml b/app/src/main/res/layout/dialog_temp_message_actions.xml deleted file mode 100644 index c7dee424c2d..00000000000 --- a/app/src/main/res/layout/dialog_temp_message_actions.xml +++ /dev/null @@ -1,165 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index b2cdf70b851..27c5c8591df 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -431,14 +431,7 @@ How to translate with transifex: Mute all notifications Appear offline ๐Ÿ˜ƒ - ๐Ÿ‘ - ๐Ÿ‘Ž - โค๏ธ - ๐Ÿ˜ฏ - ๐Ÿ˜ข - ๐Ÿ™ - ๐Ÿ”ฅ - More emojis + More emojis Don\'t clear Today 15 minutes @@ -496,7 +489,6 @@ How to translate with transifex: Failed to send message Failed to send message: Add attachment - Recent See %d similar message See %d similar messages @@ -739,7 +731,6 @@ How to translate with transifex: 999+ - Open main menu Failed to save %1$s Selected %1$s (%2$d) @@ -875,7 +866,6 @@ How to translate with transifex: You are not allowed to activate audio! You are not allowed to activate video! Scroll to bottom - This message is too old to be shown Translate Translation From @@ -910,16 +900,13 @@ How to translate with transifex: Caption Retrieval failed Languages could not be retrieved - Edit message Edit Update message - Cancel editing Messages older than 24 hours can not be edited Conversation is read only Edit message (edited) Silent message - Conversation not found Add to Notes Edited by admin Cancel editing diff --git a/scripts/analysis/lint-results.txt b/scripts/analysis/lint-results.txt index a0c1e5f3efa..e5e2d4d6476 100644 --- a/scripts/analysis/lint-results.txt +++ b/scripts/analysis/lint-results.txt @@ -1,2 +1,2 @@ DO NOT TOUCH; GENERATED BY DRONE - Lint Report: 89 warnings + Lint Report: 87 warnings