diff --git a/app/src/main/java/one/mixin/android/ui/conversation/ConversationFragment.kt b/app/src/main/java/one/mixin/android/ui/conversation/ConversationFragment.kt index 71cd28e547..c763deff33 100644 --- a/app/src/main/java/one/mixin/android/ui/conversation/ConversationFragment.kt +++ b/app/src/main/java/one/mixin/android/ui/conversation/ConversationFragment.kt @@ -65,6 +65,7 @@ import com.uber.autodispose.autoDispose import dagger.hilt.android.AndroidEntryPoint import io.reactivex.android.schedulers.AndroidSchedulers import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job import kotlinx.coroutines.delay import kotlinx.coroutines.launch import kotlinx.coroutines.withContext @@ -250,7 +251,6 @@ import one.mixin.android.vo.giphy.Image import one.mixin.android.vo.isAppCard import one.mixin.android.vo.isAttachment import one.mixin.android.vo.isAudio -import one.mixin.android.vo.isData import one.mixin.android.vo.isImage import one.mixin.android.vo.isLive import one.mixin.android.vo.isSticker @@ -258,7 +258,6 @@ import one.mixin.android.vo.isText import one.mixin.android.vo.isTranscript import one.mixin.android.vo.mediaExists import one.mixin.android.vo.saveToLocal -import one.mixin.android.vo.supportSticker import one.mixin.android.vo.toApp import one.mixin.android.vo.toTranscript import one.mixin.android.vo.toUser @@ -281,6 +280,7 @@ import one.mixin.android.widget.ContentEditText import one.mixin.android.widget.DraggableRecyclerView import one.mixin.android.widget.LinearSmoothScrollerCustom import one.mixin.android.widget.MixinHeadersDecoration +import one.mixin.android.widget.ToolView import one.mixin.android.widget.buildBottomSheetView import one.mixin.android.widget.gallery.MimeType import one.mixin.android.widget.gallery.ui.GalleryActivity.Companion.IS_VIDEO @@ -423,36 +423,30 @@ class ConversationFragment() : } } - private fun checkPinMessage() { - if (messageAdapter.selectSet.valueAt(0).canNotPin()) { - binding.toolView.pinIv.visibility = GONE - } else { - messageAdapter.selectSet.valueAt(0).messageId.let { messageId -> - lifecycleScope.launch { - if (isGroup) { - val role = - withContext(Dispatchers.IO) { - chatViewModel.findParticipantById( - conversationId, - Session.getAccountId()!!, - )?.role - } - if (role != ParticipantRole.OWNER.name && role != ParticipantRole.ADMIN.name) { - binding.toolView.pinIv.visibility = GONE - return@launch - } - } - val pinMessage = chatViewModel.findPinMessageById(messageId) - if (pinMessage == null) { - binding.toolView.pinIv.tag = PinAction.PIN - binding.toolView.pinIv.setImageResource(R.drawable.ic_message_pin) - binding.toolView.pinIv.visibility = VISIBLE - } else { - binding.toolView.pinIv.tag = PinAction.UNPIN - binding.toolView.pinIv.setImageResource(R.drawable.ic_message_unpin) - binding.toolView.pinIv.visibility = VISIBLE - } + private var pinMessageJob: Job? = null + + private fun updateMessageSelection() { + pinMessageJob?.cancel() + val messages = messageAdapter.selectSet.toList() + binding.toolView.updateSelection(messages) + if (messages.isEmpty()) { + binding.toolView.fadeOut() + } else if (!binding.toolView.isVisible) { + binding.toolView.fadeIn() + } + messageAdapter.notifyItemRangeChanged(0, messageAdapter.itemCount) + val message = messages.singleOrNull() ?: return + if (message.canNotPin()) return + pinMessageJob = viewLifecycleOwner.lifecycleScope.launch { + if (isGroup) { + val role = withContext(Dispatchers.IO) { + chatViewModel.findParticipantById(conversationId, Session.getAccountId()!!)?.role } + if (role != ParticipantRole.OWNER.name && role != ParticipantRole.ADMIN.name) return@launch + } + val pinAction = if (chatViewModel.findPinMessageById(message.messageId) == null) PinAction.PIN else PinAction.UNPIN + if (messageAdapter.selectSet.size == 1 && messageAdapter.selectSet.valueAt(0).messageId == message.messageId) { + binding.toolView.updateSelection(messageAdapter.selectSet.toList(), pinAction) } } } @@ -469,92 +463,20 @@ class ConversationFragment() : } else { messageAdapter.removeSelect(messageItem) } - binding.toolView.countTv.text = messageAdapter.selectSet.size.toString() - when { - messageAdapter.selectSet.isEmpty() -> binding.toolView.fadeOut() - messageAdapter.selectSet.size == 1 -> { - try { - if (messageAdapter.selectSet.valueAt(0).isText()) { - binding.toolView.copyIv.visibility = VISIBLE - } else { - binding.toolView.copyIv.visibility = GONE - } - } catch (e: ArrayIndexOutOfBoundsException) { - binding.toolView.copyIv.visibility = GONE - } - if (messageAdapter.selectSet.valueAt(0).isData()) { - binding.toolView.shareIv.visibility = VISIBLE - } else { - binding.toolView.shareIv.visibility = GONE - } - if (messageAdapter.selectSet.valueAt(0).supportSticker()) { - binding.toolView.addStickerIv.visibility = VISIBLE - } else { - binding.toolView.addStickerIv.visibility = GONE - } - if (messageAdapter.selectSet.valueAt(0).canNotReply()) { - binding.toolView.replyIv.visibility = GONE - } else { - binding.toolView.replyIv.visibility = VISIBLE - } - checkPinMessage() - } - else -> { - binding.toolView.forwardIv.visibility = VISIBLE - binding.toolView.replyIv.visibility = GONE - binding.toolView.copyIv.visibility = GONE - binding.toolView.addStickerIv.visibility = GONE - binding.toolView.shareIv.visibility = GONE - binding.toolView.pinIv.visibility = GONE - } - } - if (messageAdapter.selectSet.size > 99 || messageAdapter.selectSet.any { it.canNotForward() }) { - binding.toolView.forwardIv.visibility = GONE - } else { - binding.toolView.forwardIv.visibility = VISIBLE - } - messageAdapter.notifyItemRangeChanged(0, messageAdapter.itemCount) + updateMessageSelection() } override fun onLongClick( messageItem: MessageItem, position: Int, ): Boolean { - val b = messageAdapter.addSelect(messageItem) - binding.toolView.countTv.text = messageAdapter.selectSet.size.toString() - if (b) { - if (messageItem.isText()) { - binding.toolView.copyIv.visibility = VISIBLE - } else { - binding.toolView.copyIv.visibility = GONE - } - if (messageItem.isData()) { - binding.toolView.shareIv.visibility = VISIBLE - } else { - binding.toolView.shareIv.visibility = GONE - } - - if (messageItem.supportSticker()) { - binding.toolView.addStickerIv.visibility = VISIBLE - } else { - binding.toolView.addStickerIv.visibility = GONE - } - - if (messageAdapter.selectSet.any { it.canNotForward() }) { - binding.toolView.forwardIv.visibility = GONE - } else { - binding.toolView.forwardIv.visibility = VISIBLE - } - if (messageAdapter.selectSet.any { it.canNotReply() }) { - binding.toolView.replyIv.visibility = GONE - } else { - binding.toolView.replyIv.visibility = VISIBLE - } - checkPinMessage() - messageAdapter.notifyItemRangeChanged(0, messageAdapter.itemCount) - binding.toolView.fadeIn() + val added = messageAdapter.addSelect(messageItem) + if (added) { + updateMessageSelection() + val anchor = binding.messageRv.findViewHolderForAdapterPosition(position)?.itemView ?: binding.toolView + binding.toolView.showMenu(anchor) } - return b + return added } @SuppressLint("MissingPermission") @@ -1250,6 +1172,7 @@ class ConversationFragment() : if (viewDestroyed()) return@launch lastReadMessage = chatViewModel.findLastMessage(conversationId) } + binding.toolView.dismissMenu() deleteDialog?.dismiss() super.onPause() paused = true @@ -1347,6 +1270,7 @@ class ConversationFragment() : } override fun onDestroyView() { + pinMessageJob?.cancel() audioFile?.deleteOnExit() audioFile = null super.onDestroyView() @@ -1424,7 +1348,126 @@ class ConversationFragment() : } } + private fun onMessageAction(action: ToolView.Action) { + if (messageAdapter.selectSet.isEmpty()) return + when (action) { + ToolView.Action.REPLY -> { + messageAdapter.selectSet.valueAt(0).let { + binding.chatControl.replyView.bind(it) + } + displayReplyView() + closeTool() + } + ToolView.Action.COPY -> { + try { + context?.getClipboardManager()?.setPrimaryClip( + ClipData.newPlainText(null, messageAdapter.selectSet.valueAt(0).content), + ) + toast(R.string.copied_to_clipboard) + } catch (_: ArrayIndexOutOfBoundsException) { + } + closeTool() + } + ToolView.Action.FORWARD -> { + showForwardDialog() + } + ToolView.Action.PIN, ToolView.Action.UNPIN -> { + val pinMessages = + messageAdapter.selectSet.map { + PinMessageData(it.messageId, it.conversationId, requireNotNull(it.type), it.content, nowInUtc()) + } + val pinAction = if (action == ToolView.Action.PIN) PinAction.PIN else PinAction.UNPIN + if (pinMessages.isEmpty()) { + return + } + lifecycleScope.launch { + chatViewModel.sendPinMessage( + conversationId, + sender, + pinAction, + pinMessages, + ) + toast( + if (pinAction == PinAction.PIN) { + R.string.Message_pinned + } else { + R.string.Message_unpinned + }, + ) + closeTool() + } + } + ToolView.Action.SHARE -> { + val messageItem = messageAdapter.selectSet.valueAt(0) + Intent().apply { + var uri: Uri? = + try { + messageItem.absolutePath()?.toUri() + } catch (e: NullPointerException) { + null + } + if (uri == null || uri.path == null) { + closeTool() + return + } + if (ContentResolver.SCHEME_CONTENT != uri.scheme) { + uri = requireContext().getUriForFile(File(uri.path!!)) + } + this.action = Intent.ACTION_SEND + putExtra(Intent.EXTRA_STREAM, uri) + addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) + val extraMimeTypes = arrayOf("text/plain", "audio/*", "image/*", "video/*") + putExtra(Intent.EXTRA_MIME_TYPES, extraMimeTypes) + type = "application/*" + + val resInfoList = requireContext().packageManager.queryIntentActivities(this, PackageManager.MATCH_DEFAULT_ONLY) + for (resolveInfo in resInfoList) { + val packageName = resolveInfo.activityInfo.packageName + requireContext().grantUriPermission(packageName, uri, Intent.FLAG_GRANT_READ_URI_PERMISSION) + } + try { + startActivity(Intent.createChooser(this, messageItem.mediaName)) + } catch (ignored: ActivityNotFoundException) { + } + } + closeTool() + } + ToolView.Action.ADD_STICKER -> { + val messageItem = messageAdapter.selectSet.valueAt(0) + messageItem.let { m -> + if (messageItem.isSticker() && m.stickerId != null) { + addSticker(m) + } else if (messageItem.isImage()) { + val url = m.absolutePath(requireContext()) + url?.let { + val uri = url.toUri() + val mimeType = getMimeType(uri, true) + if (mimeType?.isStickerSupport() == true) { + StickerActivity.show(requireContext(), url = it, showAdd = true) + closeTool() + } else { + toast(R.string.Invalid_sticker_format) + } + } + } + } + } + ToolView.Action.DELETE -> { + messageAdapter.selectSet.filter { it.isAudio() }.forEach { + if (AudioPlayer.isPlay(it.messageId)) { + AudioPlayer.pause() + } + } + deleteMessage(messageAdapter.selectSet.toList()) + closeTool() + } + ToolView.Action.SELECT -> binding.toolView.enterMultipleSelection() + } + } + private fun closeTool() { + pinMessageJob?.cancel() + binding.toolView.updateSelection(emptyList()) messageAdapter.selectSet.clear() if (!binding.messageRv.isComputingLayout) { messageAdapter.notifyItemRangeChanged(0, messageAdapter.itemCount) @@ -1628,128 +1671,11 @@ class ConversationFragment() : activity?.onBackPressedDispatcher?.onBackPressed() } } - binding.toolView.deleteIv.setOnClickListener { - messageAdapter.selectSet.filter { it.isAudio() }.forEach { - if (AudioPlayer.isPlay(it.messageId)) { - AudioPlayer.pause() - } - } - deleteMessage(messageAdapter.selectSet.toList()) - closeTool() - } + binding.toolView.onAction = ::onMessageAction binding.chatControl.replyView.replyCloseIv.setOnClickListener { binding.chatControl.replyView.messageItem = null binding.chatControl.replyView.animateHeight(53.dp, 0) } - binding.toolView.copyIv.setOnClickListener { - try { - context?.getClipboardManager()?.setPrimaryClip( - ClipData.newPlainText(null, messageAdapter.selectSet.valueAt(0).content), - ) - toast(R.string.copied_to_clipboard) - } catch (_: ArrayIndexOutOfBoundsException) { - } - closeTool() - } - binding.toolView.forwardIv.setOnClickListener { - showForwardDialog() - } - binding.toolView.addStickerIv.setOnClickListener { - if (messageAdapter.selectSet.isEmpty()) { - return@setOnClickListener - } - val messageItem = messageAdapter.selectSet.valueAt(0) - messageItem.let { m -> - if (messageItem.isSticker() && m.stickerId != null) { - addSticker(m) - } else if (messageItem.isImage()) { - val url = m.absolutePath(requireContext()) - url?.let { - val uri = url.toUri() - val mimeType = getMimeType(uri, true) - if (mimeType?.isStickerSupport() == true) { - StickerActivity.show(requireContext(), url = it, showAdd = true) - closeTool() - } else { - toast(R.string.Invalid_sticker_format) - } - } - } - } - } - - binding.toolView.replyIv.setOnClickListener { - if (messageAdapter.selectSet.isEmpty()) { - return@setOnClickListener - } - messageAdapter.selectSet.valueAt(0).let { - binding.chatControl.replyView.bind(it) - } - displayReplyView() - closeTool() - } - - binding.toolView.pinIv.setOnClickListener { - val pinMessages = - messageAdapter.selectSet.map { - PinMessageData(it.messageId, it.conversationId, requireNotNull(it.type), it.content, nowInUtc()) - } - val action = (binding.toolView.pinIv.tag as PinAction?) ?: PinAction.PIN - if (pinMessages.isEmpty()) { - return@setOnClickListener - } - lifecycleScope.launch { - chatViewModel.sendPinMessage( - conversationId, - sender, - (binding.toolView.pinIv.tag as PinAction?) ?: PinAction.PIN, - pinMessages, - ) - toast( - if (action == PinAction.PIN) { - R.string.Message_pinned - } else { - R.string.Message_unpinned - }, - ) - closeTool() - } - } - binding.toolView.shareIv.setOnClickListener { - val messageItem = messageAdapter.selectSet.valueAt(0) - Intent().apply { - var uri: Uri? = - try { - messageItem.absolutePath()?.toUri() - } catch (e: NullPointerException) { - null - } - if (uri == null || uri.path == null) { - closeTool() - return@setOnClickListener - } - if (ContentResolver.SCHEME_CONTENT != uri.scheme) { - uri = requireContext().getUriForFile(File(uri.path!!)) - } - action = Intent.ACTION_SEND - putExtra(Intent.EXTRA_STREAM, uri) - addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) - val extraMimeTypes = arrayOf("text/plain", "audio/*", "image/*", "video/*") - putExtra(Intent.EXTRA_MIME_TYPES, extraMimeTypes) - type = "application/*" - - val resInfoList = requireContext().packageManager.queryIntentActivities(this, PackageManager.MATCH_DEFAULT_ONLY) - for (resolveInfo in resInfoList) { - val packageName = resolveInfo.activityInfo.packageName - requireContext().grantUriPermission(packageName, uri, Intent.FLAG_GRANT_READ_URI_PERMISSION) - } - try { - startActivity(Intent.createChooser(this, messageItem.mediaName)) - } catch (ignored: ActivityNotFoundException) { - } - } - closeTool() - } binding.groupDesc.movementMethod = LinkMovementMethod() binding.groupDesc.addAutoLinkMode(AutoLinkMode.MODE_URL) diff --git a/app/src/main/java/one/mixin/android/ui/conversation/chathistory/ChatHistoryActivity.kt b/app/src/main/java/one/mixin/android/ui/conversation/chathistory/ChatHistoryActivity.kt index 2a5fc8b537..32f79c03cc 100644 --- a/app/src/main/java/one/mixin/android/ui/conversation/chathistory/ChatHistoryActivity.kt +++ b/app/src/main/java/one/mixin/android/ui/conversation/chathistory/ChatHistoryActivity.kt @@ -12,6 +12,7 @@ import android.net.Uri import android.os.Build import android.os.Bundle import android.view.ContextThemeWrapper +import android.view.Gravity import android.view.View import androidx.appcompat.widget.PopupMenu import androidx.core.view.children @@ -47,7 +48,6 @@ import one.mixin.android.extension.openEmail import one.mixin.android.extension.openMedia import one.mixin.android.extension.openPermissionSetting import one.mixin.android.extension.screenHeight -import one.mixin.android.extension.showIcon import one.mixin.android.extension.toast import one.mixin.android.job.AttachmentDownloadJob import one.mixin.android.job.ConvertVideoJob @@ -598,7 +598,8 @@ class ChatHistoryActivity : BaseActivity() { lifecycleScope.launch { val role = withContext(Dispatchers.IO) { conversationRepository.findParticipantById(conversationId, Session.getAccountId()!!) }?.role val isAdmin = role == ParticipantRole.OWNER.name || role == ParticipantRole.ADMIN.name - val popMenu = PopupMenu(this@ChatHistoryActivity, view) + val anchor = binding.recyclerView.findContainingItemView(view) ?: view + val popMenu = PopupMenu(this@ChatHistoryActivity, anchor, Gravity.END, 0, R.style.MessageActionPopup) popMenu.menuInflater.inflate( R.menu.chathistory, popMenu.menu, @@ -607,7 +608,7 @@ class ChatHistoryActivity : BaseActivity() { popMenu.menu.findItem(R.id.copy).isVisible = messageItem.isText() popMenu.menu.findItem(R.id.forward).isVisible = !isTranscript && !messageItem.isAppButtonGroup() - popMenu.showIcon() + popMenu.setForceShowIcon(true) popMenu.setOnMenuItemClickListener { when (it.itemId) { R.id.copy -> { diff --git a/app/src/main/java/one/mixin/android/widget/ToolView.kt b/app/src/main/java/one/mixin/android/widget/ToolView.kt index 24db045f2d..e68b892efe 100644 --- a/app/src/main/java/one/mixin/android/widget/ToolView.kt +++ b/app/src/main/java/one/mixin/android/widget/ToolView.kt @@ -1,99 +1,157 @@ -@file:Suppress("DEPRECATION") - package one.mixin.android.widget import android.content.Context -import android.graphics.Color -import android.graphics.drawable.ColorDrawable import android.util.AttributeSet import android.view.Gravity import android.view.LayoutInflater import android.view.View -import android.widget.PopupWindow import android.widget.RelativeLayout -import android.widget.TextView +import androidx.annotation.DrawableRes import androidx.annotation.StringRes -import androidx.core.widget.PopupWindowCompat +import androidx.appcompat.widget.PopupMenu +import androidx.core.view.isVisible import one.mixin.android.R import one.mixin.android.databinding.ViewToolBinding -import one.mixin.android.extension.dp +import one.mixin.android.vo.MessageItem +import one.mixin.android.vo.isData +import one.mixin.android.vo.isText +import one.mixin.android.vo.supportSticker import one.mixin.android.websocket.PinAction class ToolView constructor(context: Context, attrs: AttributeSet) : RelativeLayout(context, attrs) { + enum class Action(@StringRes val title: Int, @DrawableRes val icon: Int) { + REPLY(R.string.Reply, R.drawable.ic_reply), + COPY(R.string.Copy, R.drawable.ic_content_copy), + FORWARD(R.string.Forward, R.drawable.ic_msg_forward), + PIN(R.string.pin_title, R.drawable.ic_message_pin), + UNPIN(R.string.Unpin, R.drawable.ic_message_unpin), + SHARE(R.string.Share, R.drawable.ic_share), + ADD_STICKER(R.string.Add_Sticker, R.drawable.ic_sticker_add), + DELETE(R.string.Delete, R.drawable.ic_msg_delete), + SELECT(R.string.Select_Multiple_Messages, R.drawable.ic_check_black_24dp), + } + private val binding = ViewToolBinding.inflate(LayoutInflater.from(context), this, true) val closeIv = binding.closeIv - val copyIv = binding.copyIv - val countTv = binding.countTv - val deleteIv = binding.deleteIv - val addStickerIv = binding.addStickerIv - val replyIv = binding.replyIv - val forwardIv = binding.forwardIv - val shareIv = binding.shareIv - val pinIv = binding.pinIv + var onAction: ((Action) -> Unit)? = null + + private val actionViews = mapOf( + Action.REPLY to binding.replyIv, + Action.COPY to binding.copyIv, + Action.FORWARD to binding.forwardIv, + Action.PIN to binding.pinIv, + Action.SHARE to binding.shareIv, + Action.ADD_STICKER to binding.addStickerIv, + Action.DELETE to binding.deleteIv, + ) + private var actions = emptyList() + private var popupMenu: PopupMenu? = null + private var multipleSelection = false + private var selectionCount = 0 init { - closeIv.setOnLongClickListener { - showTip(it, R.string.Close) - true - } - copyIv.setOnLongClickListener { - showTip(it, R.string.Copy) - true - } - deleteIv.setOnLongClickListener { - showTip(it, R.string.Delete) - true + binding.moreIv.setOnClickListener { showMenu(it) } + binding.closeIv.tooltipText = context.getString(R.string.Close) + } + + fun updateSelection(messages: Collection, pinAction: PinAction? = null) { + selectionCount = messages.size + multipleSelection = messages.isNotEmpty() && (multipleSelection || messages.size > 1) + actions = messageSelectionActions(messages, pinAction) + updateToolbar() + if (actions.isEmpty() || multipleSelection) { + dismissMenu() + } else { + popupMenu?.menu?.let { menu -> + Action.entries.forEach { action -> + menu.findItem(action.ordinal + 1).isVisible = action in actions + } + } } - addStickerIv.setOnLongClickListener { - showTip(it, R.string.Add_Sticker) - true + } + + fun enterMultipleSelection() { + if (actions.isEmpty()) return + multipleSelection = true + dismissMenu() + updateToolbar() + } + + private fun updateToolbar() { + binding.countTv.text = if (multipleSelection) { + selectionCount.toString() + } else { + resources.getQuantityString(R.plurals.items_selected, selectionCount, selectionCount) } - forwardIv.setOnLongClickListener { - showTip(it, R.string.Forward) - true + binding.closeIv.setImageResource(if (multipleSelection) R.drawable.ic_msg_close else R.drawable.ic_arrow_back) + binding.moreIv.isVisible = !multipleSelection && actions.isNotEmpty() + actionViews.forEach { (action, view) -> + val currentAction = if (action == Action.PIN && Action.UNPIN in actions) Action.UNPIN else action + view.isVisible = multipleSelection && currentAction in actions + view.setImageResource(currentAction.icon) + view.contentDescription = context.getString(currentAction.title) + view.tooltipText = view.contentDescription + view.setOnClickListener { + if (currentAction in actions) onAction?.invoke(currentAction) + } } - replyIv.setOnLongClickListener { - showTip(it, R.string.Reply) - true + } + + fun showMenu(anchor: View) { + if (actions.isEmpty() || multipleSelection || !anchor.isAttachedToWindow) return + dismissMenu() + val popup = PopupMenu(context, anchor, Gravity.END, 0, R.style.MessageActionPopup) + popup.setForceShowIcon(true) + Action.entries.forEach { action -> + popup.menu.add(0, action.ordinal + 1, action.ordinal, action.title).apply { + setIcon(action.icon) + isVisible = action in actions + } } - shareIv.setOnLongClickListener { - showTip(it, R.string.Share) - true + popup.setOnMenuItemClickListener { item -> + val action = Action.entries.getOrNull(item.itemId - 1) + if (action == null || action !in actions) { + false + } else { + dismissMenu() + onAction?.invoke(action) + true + } } - pinIv.setOnLongClickListener { - showTip( - it, - if (it.tag == PinAction.PIN) { - R.string.pin_title - } else { - R.string.Unpin - }, - ) - true + popup.setOnDismissListener { + if (popupMenu === it) popupMenu = null } + popupMenu = popup + popup.show() } - private val tipView by lazy { - LayoutInflater.from(context).inflate(R.layout.view_tip, null, false) + fun dismissMenu() { + popupMenu?.dismiss() + popupMenu = null } - private val popupWindow by lazy { - PopupWindow(context).apply { - isOutsideTouchable = true - setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT)) - contentView = tipView - } + + override fun onDetachedFromWindow() { + dismissMenu() + super.onDetachedFromWindow() } +} - private fun showTip( - view: View, - @StringRes str: Int, - ) { - tipView.apply { - (this as TextView).setText(str) - setOnClickListener { - popupWindow.dismiss() - } +internal fun messageSelectionActions( + messages: Collection, + pinAction: PinAction? = null, +): List { + if (messages.isEmpty()) return emptyList() + val message = messages.singleOrNull() + return buildList { + if (message != null && !message.canNotReply()) add(ToolView.Action.REPLY) + if (message?.isText() == true) add(ToolView.Action.COPY) + if (messages.size <= 99 && messages.none { it.canNotForward() }) add(ToolView.Action.FORWARD) + if (message != null && !message.canNotPin() && pinAction != null) { + add(if (pinAction == PinAction.PIN) ToolView.Action.PIN else ToolView.Action.UNPIN) } - PopupWindowCompat.showAsDropDown(popupWindow, view, 0, 12.dp, Gravity.BOTTOM or Gravity.CENTER_HORIZONTAL) + if (message?.isData() == true) add(ToolView.Action.SHARE) + if (message?.supportSticker() == true) add(ToolView.Action.ADD_STICKER) + add(ToolView.Action.DELETE) + if (message != null) add(ToolView.Action.SELECT) } } diff --git a/app/src/main/res/drawable/bg_message_action_popup.xml b/app/src/main/res/drawable/bg_message_action_popup.xml new file mode 100644 index 0000000000..66088035b5 --- /dev/null +++ b/app/src/main/res/drawable/bg_message_action_popup.xml @@ -0,0 +1,4 @@ + + diff --git a/app/src/main/res/layout/view_tool.xml b/app/src/main/res/layout/view_tool.xml index 0e97cf62a5..9f2b9eedfb 100644 --- a/app/src/main/res/layout/view_tool.xml +++ b/app/src/main/res/layout/view_tool.xml @@ -16,7 +16,7 @@ android:padding="8dp" android:layout_marginStart="8dp" android:src="@drawable/ic_msg_close" - tools:ignore="ContentDescription" /> + android:contentDescription="@string/Close" /> + android:contentDescription="@string/Delete" /> + android:contentDescription="@string/Copy" /> + android:contentDescription="@string/Forward" /> + android:contentDescription="@string/Add_Sticker" /> + android:contentDescription="@string/Reply" /> + android:contentDescription="@string/pin_title" /> + android:contentDescription="@string/Share" /> + diff --git a/app/src/main/res/menu/chathistory.xml b/app/src/main/res/menu/chathistory.xml index 16180737fc..a6956b1f4e 100644 --- a/app/src/main/res/menu/chathistory.xml +++ b/app/src/main/res/menu/chathistory.xml @@ -1,15 +1,15 @@ - + - \ No newline at end of file + diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index 2452a2832d..cab5ab3666 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -971,6 +971,7 @@ 由 TIP 提供安全保障 安全 选择 + 多选 选择恢复联系人 发送 正在发送 diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml index fa0869138f..af543e4564 100644 --- a/app/src/main/res/values-zh-rTW/strings.xml +++ b/app/src/main/res/values-zh-rTW/strings.xml @@ -781,6 +781,7 @@ 搜尋中 安全 選擇 + 多選 選擇緊急聯絡人 傳送 按檔案傳送 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 5bf8c9d1a0..da34bd8d19 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -999,6 +999,7 @@ Secured by TIP Security Select + Select Multiple Messages Select Recovery Contact Send Sending diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index e511b177c9..7295b9fce3 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -493,6 +493,12 @@ uniform + +