mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-02-24 11:45:28 +00:00
Use context menu when selecting a message in chat.
This commit is contained in:
committed by
Cody Henthorne
parent
d254d24d77
commit
e4d43ade93
@@ -0,0 +1,91 @@
|
||||
package org.thoughtcrime.securesms.components.menu
|
||||
|
||||
import android.os.Build
|
||||
import android.view.View
|
||||
import android.widget.ImageView
|
||||
import android.widget.TextView
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.util.adapter.mapping.LayoutFactory
|
||||
import org.thoughtcrime.securesms.util.adapter.mapping.MappingAdapter
|
||||
import org.thoughtcrime.securesms.util.adapter.mapping.MappingModel
|
||||
import org.thoughtcrime.securesms.util.adapter.mapping.MappingViewHolder
|
||||
|
||||
/**
|
||||
* Handles the setup and display of actions shown in a context menu.
|
||||
*/
|
||||
class ContextMenuList(recyclerView: RecyclerView, onItemClick: () -> Unit) {
|
||||
|
||||
private val mappingAdapter = MappingAdapter().apply {
|
||||
registerFactory(DisplayItem::class.java, LayoutFactory({ ItemViewHolder(it, onItemClick) }, R.layout.signal_context_menu_item))
|
||||
}
|
||||
|
||||
init {
|
||||
recyclerView.apply {
|
||||
adapter = mappingAdapter
|
||||
layoutManager = LinearLayoutManager(context)
|
||||
itemAnimator = null
|
||||
}
|
||||
}
|
||||
|
||||
fun setItems(items: List<ActionItem>) {
|
||||
mappingAdapter.submitList(items.toAdapterItems())
|
||||
}
|
||||
|
||||
private fun List<ActionItem>.toAdapterItems(): List<DisplayItem> {
|
||||
return this.mapIndexed { index, item ->
|
||||
val displayType: DisplayType = when {
|
||||
this.size == 1 -> DisplayType.ONLY
|
||||
index == 0 -> DisplayType.TOP
|
||||
index == this.size - 1 -> DisplayType.BOTTOM
|
||||
else -> DisplayType.MIDDLE
|
||||
}
|
||||
|
||||
DisplayItem(item, displayType)
|
||||
}
|
||||
}
|
||||
|
||||
private data class DisplayItem(
|
||||
val item: ActionItem,
|
||||
val displayType: DisplayType
|
||||
) : MappingModel<DisplayItem> {
|
||||
override fun areItemsTheSame(newItem: DisplayItem): Boolean {
|
||||
return this == newItem
|
||||
}
|
||||
|
||||
override fun areContentsTheSame(newItem: DisplayItem): Boolean {
|
||||
return this == newItem
|
||||
}
|
||||
}
|
||||
|
||||
private enum class DisplayType {
|
||||
TOP, BOTTOM, MIDDLE, ONLY
|
||||
}
|
||||
|
||||
private class ItemViewHolder(
|
||||
itemView: View,
|
||||
private val onItemClick: () -> Unit,
|
||||
) : MappingViewHolder<DisplayItem>(itemView) {
|
||||
val icon: ImageView = itemView.findViewById(R.id.signal_context_menu_item_icon)
|
||||
val title: TextView = itemView.findViewById(R.id.signal_context_menu_item_title)
|
||||
|
||||
override fun bind(model: DisplayItem) {
|
||||
icon.setImageResource(model.item.iconRes)
|
||||
title.text = model.item.title
|
||||
itemView.setOnClickListener {
|
||||
model.item.action.run()
|
||||
onItemClick()
|
||||
}
|
||||
|
||||
if (Build.VERSION.SDK_INT >= 21) {
|
||||
when (model.displayType) {
|
||||
DisplayType.TOP -> itemView.setBackgroundResource(R.drawable.signal_context_menu_item_background_top)
|
||||
DisplayType.BOTTOM -> itemView.setBackgroundResource(R.drawable.signal_context_menu_item_background_bottom)
|
||||
DisplayType.MIDDLE -> itemView.setBackgroundResource(R.drawable.signal_context_menu_item_background_middle)
|
||||
DisplayType.ONLY -> itemView.setBackgroundResource(R.drawable.signal_context_menu_item_background_only)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -6,18 +6,10 @@ import android.os.Build
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.ImageView
|
||||
import android.widget.PopupWindow
|
||||
import android.widget.TextView
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.util.ViewUtil
|
||||
import org.thoughtcrime.securesms.util.adapter.mapping.Factory
|
||||
import org.thoughtcrime.securesms.util.adapter.mapping.MappingAdapter
|
||||
import org.thoughtcrime.securesms.util.adapter.mapping.MappingModel
|
||||
import org.thoughtcrime.securesms.util.adapter.mapping.MappingViewHolder
|
||||
|
||||
/**
|
||||
* A custom context menu that will show next to an anchor view and display several options. Basically a PopupMenu with custom UI and positioning rules.
|
||||
@@ -42,9 +34,10 @@ class SignalContextMenu private constructor(
|
||||
|
||||
val context: Context = anchor.context
|
||||
|
||||
val mappingAdapter = MappingAdapter().apply {
|
||||
registerFactory(DisplayItem::class.java, ItemViewHolderFactory())
|
||||
}
|
||||
private val contextMenuList = ContextMenuList(
|
||||
recyclerView = contentView.findViewById(R.id.signal_context_menu_list),
|
||||
onItemClick = { dismiss() },
|
||||
)
|
||||
|
||||
init {
|
||||
setBackgroundDrawable(ContextCompat.getDrawable(context, R.drawable.signal_context_menu_background))
|
||||
@@ -59,13 +52,7 @@ class SignalContextMenu private constructor(
|
||||
elevation = 20f
|
||||
}
|
||||
|
||||
contentView.findViewById<RecyclerView>(R.id.signal_context_menu_list).apply {
|
||||
adapter = mappingAdapter
|
||||
layoutManager = LinearLayoutManager(context)
|
||||
itemAnimator = null
|
||||
}
|
||||
|
||||
mappingAdapter.submitList(items.toAdapterItems())
|
||||
contextMenuList.setItems(items)
|
||||
}
|
||||
|
||||
private fun show() {
|
||||
@@ -97,7 +84,7 @@ class SignalContextMenu private constructor(
|
||||
offsetY = baseOffsetY
|
||||
} else if (menuTopBound > screenTopBound) {
|
||||
offsetY = -(anchorRect.height() + contentView.measuredHeight + baseOffsetY)
|
||||
mappingAdapter.submitList(items.reversed().toAdapterItems())
|
||||
contextMenuList.setItems(items.reversed())
|
||||
} else {
|
||||
offsetY = -((anchorRect.height() / 2) + (contentView.measuredHeight / 2) + baseOffsetY)
|
||||
}
|
||||
@@ -122,65 +109,6 @@ class SignalContextMenu private constructor(
|
||||
showAsDropDown(anchor, offsetX, offsetY)
|
||||
}
|
||||
|
||||
private fun List<ActionItem>.toAdapterItems(): List<DisplayItem> {
|
||||
return this.mapIndexed { index, item ->
|
||||
val displayType: DisplayType = when {
|
||||
this.size == 1 -> DisplayType.ONLY
|
||||
index == 0 -> DisplayType.TOP
|
||||
index == this.size - 1 -> DisplayType.BOTTOM
|
||||
else -> DisplayType.MIDDLE
|
||||
}
|
||||
|
||||
DisplayItem(item, displayType)
|
||||
}
|
||||
}
|
||||
|
||||
private data class DisplayItem(
|
||||
val item: ActionItem,
|
||||
val displayType: DisplayType
|
||||
) : MappingModel<DisplayItem> {
|
||||
override fun areItemsTheSame(newItem: DisplayItem): Boolean {
|
||||
return this == newItem
|
||||
}
|
||||
|
||||
override fun areContentsTheSame(newItem: DisplayItem): Boolean {
|
||||
return this == newItem
|
||||
}
|
||||
}
|
||||
|
||||
private enum class DisplayType {
|
||||
TOP, BOTTOM, MIDDLE, ONLY
|
||||
}
|
||||
|
||||
private inner class ItemViewHolder(itemView: View) : MappingViewHolder<DisplayItem>(itemView) {
|
||||
val icon: ImageView = itemView.findViewById(R.id.signal_context_menu_item_icon)
|
||||
val title: TextView = itemView.findViewById(R.id.signal_context_menu_item_title)
|
||||
|
||||
override fun bind(model: DisplayItem) {
|
||||
icon.setImageResource(model.item.iconRes)
|
||||
title.text = model.item.title
|
||||
itemView.setOnClickListener {
|
||||
model.item.action.run()
|
||||
dismiss()
|
||||
}
|
||||
|
||||
if (Build.VERSION.SDK_INT >= 21) {
|
||||
when (model.displayType) {
|
||||
DisplayType.TOP -> itemView.setBackgroundResource(R.drawable.signal_context_menu_item_background_top)
|
||||
DisplayType.BOTTOM -> itemView.setBackgroundResource(R.drawable.signal_context_menu_item_background_bottom)
|
||||
DisplayType.MIDDLE -> itemView.setBackgroundResource(R.drawable.signal_context_menu_item_background_middle)
|
||||
DisplayType.ONLY -> itemView.setBackgroundResource(R.drawable.signal_context_menu_item_background_only)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private inner class ItemViewHolderFactory : Factory<DisplayItem> {
|
||||
override fun createViewHolder(parent: ViewGroup): MappingViewHolder<DisplayItem> {
|
||||
return ItemViewHolder(LayoutInflater.from(parent.context).inflate(R.layout.signal_context_menu_item, parent, false))
|
||||
}
|
||||
}
|
||||
|
||||
enum class HorizontalPosition {
|
||||
START, END
|
||||
}
|
||||
|
||||
@@ -191,6 +191,27 @@ public class VoiceNoteMediaController implements DefaultLifecycleObserver {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Tells the Media service to resume playback of a given audio slide. If the audio slide is not
|
||||
* currently paused, playback will be started from the beginning.
|
||||
*
|
||||
* @param audioSlideUri The Uri of the desired audio slide
|
||||
* @param messageId The Message id of the given audio slide
|
||||
*/
|
||||
public void resumePlayback(@NonNull Uri audioSlideUri, long messageId) {
|
||||
if (isCurrentTrack(audioSlideUri)) {
|
||||
getMediaController().getTransportControls().play();
|
||||
} else {
|
||||
Bundle extras = new Bundle();
|
||||
extras.putLong(EXTRA_MESSAGE_ID, messageId);
|
||||
extras.putLong(EXTRA_THREAD_ID, -1L);
|
||||
extras.putDouble(EXTRA_PROGRESS, 0.0);
|
||||
extras.putBoolean(EXTRA_PLAY_SINGLE, true);
|
||||
|
||||
getMediaController().getTransportControls().playFromUri(audioSlideUri, extras);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Pauses playback if the given audio slide is playing.
|
||||
*
|
||||
|
||||
@@ -706,6 +706,11 @@ public class ConversationAdapter
|
||||
return getBindable().canPlayContent();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean shouldProjectContent() {
|
||||
return getBindable().shouldProjectContent();
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull ProjectionList getColorizerProjections(@NonNull ViewGroup coordinateRoot) {
|
||||
return getBindable().getColorizerProjections(coordinateRoot);
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
package org.thoughtcrime.securesms.conversation
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Build
|
||||
import android.view.Gravity
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.PopupWindow
|
||||
import androidx.core.content.ContextCompat
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.components.menu.ActionItem
|
||||
import org.thoughtcrime.securesms.components.menu.ContextMenuList
|
||||
|
||||
/**
|
||||
* The context menu shown after long pressing a message in ConversationActivity.
|
||||
*/
|
||||
class ConversationContextMenu(private val anchor: View, items: List<ActionItem>) : PopupWindow(
|
||||
LayoutInflater.from(anchor.context).inflate(R.layout.signal_context_menu, null),
|
||||
ViewGroup.LayoutParams.WRAP_CONTENT,
|
||||
ViewGroup.LayoutParams.WRAP_CONTENT,
|
||||
) {
|
||||
|
||||
val context: Context = anchor.context
|
||||
|
||||
private val contextMenuList = ContextMenuList(
|
||||
recyclerView = contentView.findViewById(R.id.signal_context_menu_list),
|
||||
onItemClick = { dismiss() },
|
||||
)
|
||||
|
||||
init {
|
||||
setBackgroundDrawable(ContextCompat.getDrawable(context, R.drawable.signal_context_menu_background))
|
||||
|
||||
isFocusable = false
|
||||
isOutsideTouchable = true
|
||||
|
||||
if (Build.VERSION.SDK_INT >= 21) {
|
||||
elevation = 20f
|
||||
}
|
||||
|
||||
contextMenuList.setItems(items)
|
||||
|
||||
contentView.measure(
|
||||
View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED),
|
||||
View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED)
|
||||
)
|
||||
}
|
||||
|
||||
fun getMaxWidth(): Int = contentView.measuredWidth
|
||||
fun getMaxHeight(): Int = contentView.measuredHeight
|
||||
|
||||
fun show(offsetX: Int, offsetY: Int) {
|
||||
showAsDropDown(anchor, offsetX, offsetY, Gravity.TOP or Gravity.START)
|
||||
}
|
||||
}
|
||||
@@ -24,6 +24,7 @@ import android.annotation.SuppressLint;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.res.Configuration;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.Color;
|
||||
import android.graphics.Rect;
|
||||
import android.net.Uri;
|
||||
@@ -148,6 +149,7 @@ import org.thoughtcrime.securesms.stickers.StickerPackPreviewActivity;
|
||||
import org.thoughtcrime.securesms.util.CachedInflater;
|
||||
import org.thoughtcrime.securesms.util.CommunicationActions;
|
||||
import org.thoughtcrime.securesms.util.HtmlUtil;
|
||||
import org.thoughtcrime.securesms.util.MessageRecordUtil;
|
||||
import org.thoughtcrime.securesms.util.RemoteDeleteUtil;
|
||||
import org.thoughtcrime.securesms.util.SaveAttachmentTask;
|
||||
import org.thoughtcrime.securesms.util.SignalLocalMetrics;
|
||||
@@ -180,7 +182,6 @@ import java.util.Set;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
|
||||
import kotlin.Unit;
|
||||
import kotlin.jvm.functions.Function1;
|
||||
|
||||
@SuppressLint("StaticFieldLeak")
|
||||
public class ConversationFragment extends LoggingFragment implements MultiselectForwardFragment.Callback {
|
||||
@@ -196,7 +197,6 @@ public class ConversationFragment extends LoggingFragment implements Multiselect
|
||||
|
||||
private LiveRecipient recipient;
|
||||
private long threadId;
|
||||
private boolean isReacting;
|
||||
private ActionMode actionMode;
|
||||
private Locale locale;
|
||||
private FrameLayout videoContainer;
|
||||
@@ -768,7 +768,7 @@ public class ConversationFragment extends LoggingFragment implements Multiselect
|
||||
}
|
||||
|
||||
if (menuState.shouldShowSaveAttachmentAction()) {
|
||||
items.add(new ActionItem(R.drawable.ic_save_24, getResources().getString(R.string.conversation_selection__menu_save), () -> {
|
||||
items.add(new ActionItem(R.drawable.ic_save_24_tinted, getResources().getString(R.string.conversation_selection__menu_save), () -> {
|
||||
handleSaveAttachment((MediaMmsMessageRecord) getSelectedConversationMessage().getMessageRecord());
|
||||
actionMode.finish();
|
||||
}));
|
||||
@@ -810,19 +810,21 @@ public class ConversationFragment extends LoggingFragment implements Multiselect
|
||||
ViewUtil.animateIn(bottomActionBar, bottomActionBar.getEnterAnimation());
|
||||
listener.onBottomActionBarVisibilityChanged(View.VISIBLE);
|
||||
|
||||
ViewKt.doOnPreDraw(bottomActionBar, new Function1<View, Unit>() {
|
||||
@Override public Unit invoke(View view) {
|
||||
if (view.getHeight() == 0 && view.getVisibility() == View.VISIBLE) {
|
||||
ViewKt.doOnPreDraw(bottomActionBar, this);
|
||||
return Unit.INSTANCE;
|
||||
bottomActionBar.getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
|
||||
@Override
|
||||
public boolean onPreDraw() {
|
||||
if (bottomActionBar.getHeight() == 0 && bottomActionBar.getVisibility() == View.VISIBLE) {
|
||||
return false;
|
||||
}
|
||||
|
||||
int bottomPadding = view.getHeight() + (int) DimensionUnit.DP.toPixels(18);
|
||||
bottomActionBar.getViewTreeObserver().removeOnPreDrawListener(this);
|
||||
|
||||
int bottomPadding = bottomActionBar.getHeight() + (int) DimensionUnit.DP.toPixels(18);
|
||||
list.setPadding(list.getPaddingLeft(), list.getPaddingTop(), list.getPaddingRight(), bottomPadding);
|
||||
|
||||
list.scrollBy(0, -(bottomPadding - additionalScrollOffset));
|
||||
|
||||
return Unit.INSTANCE;
|
||||
return false;
|
||||
}
|
||||
});
|
||||
} else {
|
||||
@@ -1314,12 +1316,14 @@ public class ConversationFragment extends LoggingFragment implements Multiselect
|
||||
void onForwardClicked();
|
||||
void onMessageRequest(@NonNull MessageRequestViewModel viewModel);
|
||||
void handleReaction(@NonNull ConversationMessage conversationMessage,
|
||||
@NonNull Toolbar.OnMenuItemClickListener toolbarListener,
|
||||
@NonNull ConversationReactionOverlay.OnActionSelectedListener onActionSelectedListener,
|
||||
@NonNull SelectedConversationModel selectedConversationModel,
|
||||
@NonNull ConversationReactionOverlay.OnHideListener onHideListener);
|
||||
void onCursorChanged();
|
||||
void onMessageWithErrorClicked(@NonNull MessageRecord messageRecord);
|
||||
void onVoiceNotePause(@NonNull Uri uri);
|
||||
void onVoiceNotePlay(@NonNull Uri uri, long messageId, double progress);
|
||||
void onVoiceNoteResume(@NonNull Uri uri, long messageId);
|
||||
void onVoiceNoteSeekTo(@NonNull Uri uri, double progress);
|
||||
void onVoiceNotePlaybackSpeedChanged(@NonNull Uri uri, float speed);
|
||||
void onRegisterVoiceNoteCallbacks(@NonNull Observer<VoiceNotePlaybackState> onPlaybackStartObserver);
|
||||
@@ -1430,16 +1434,63 @@ public class ConversationFragment extends LoggingFragment implements Multiselect
|
||||
multiselectItemDecoration.setFocusedItem(new MultiselectPart.Message(item.getConversationMessage()));
|
||||
list.invalidateItemDecorations();
|
||||
|
||||
isReacting = true;
|
||||
reactionsShade.setVisibility(View.VISIBLE);
|
||||
list.setLayoutFrozen(true);
|
||||
listener.handleReaction(item.getConversationMessage(), new ReactionsToolbarListener(item.getConversationMessage()), () -> {
|
||||
isReacting = false;
|
||||
reactionsShade.setVisibility(View.GONE);
|
||||
list.setLayoutFrozen(false);
|
||||
WindowUtil.setLightStatusBarFromTheme(requireActivity());
|
||||
clearFocusedItem();
|
||||
});
|
||||
|
||||
if (itemView instanceof ConversationItem) {
|
||||
Uri audioUri = getAudioUriForLongClick(messageRecord);
|
||||
if (audioUri != null) {
|
||||
listener.onVoiceNotePause(audioUri);
|
||||
}
|
||||
|
||||
Bitmap videoBitmap = null;
|
||||
int childAdapterPosition = list.getChildAdapterPosition(itemView);
|
||||
|
||||
final GiphyMp4ProjectionPlayerHolder mp4Holder;
|
||||
if (childAdapterPosition != RecyclerView.NO_POSITION) {
|
||||
mp4Holder = giphyMp4ProjectionRecycler.getCurrentHolder(childAdapterPosition);
|
||||
if (mp4Holder != null) {
|
||||
mp4Holder.pause();
|
||||
videoBitmap = mp4Holder.getBitmap();
|
||||
mp4Holder.hide();
|
||||
}
|
||||
} else {
|
||||
mp4Holder = null;
|
||||
}
|
||||
|
||||
ConversationItem conversationItem = (ConversationItem) itemView;
|
||||
Bitmap bitmap = ConversationItemSelection.snapshotView(conversationItem, list, messageRecord, videoBitmap);
|
||||
|
||||
final ConversationItemBodyBubble bodyBubble = conversationItem.bodyBubble;
|
||||
SelectedConversationModel selectedConversationModel = new SelectedConversationModel(bitmap,
|
||||
itemView.getX(),
|
||||
itemView.getY() + list.getTranslationY(),
|
||||
bodyBubble.getX(),
|
||||
bodyBubble.getWidth(),
|
||||
audioUri,
|
||||
messageRecord.isOutgoing());
|
||||
|
||||
bodyBubble.setVisibility(View.INVISIBLE);
|
||||
|
||||
listener.handleReaction(item.getConversationMessage(), new ReactionsToolbarListener(item.getConversationMessage()), selectedConversationModel, () -> {
|
||||
reactionsShade.setVisibility(View.GONE);
|
||||
list.setLayoutFrozen(false);
|
||||
|
||||
if (selectedConversationModel.getAudioUri() != null) {
|
||||
listener.onVoiceNoteResume(selectedConversationModel.getAudioUri(), messageRecord.getId());
|
||||
}
|
||||
|
||||
WindowUtil.setLightStatusBarFromTheme(requireActivity());
|
||||
clearFocusedItem();
|
||||
|
||||
if (mp4Holder != null) {
|
||||
mp4Holder.show();
|
||||
mp4Holder.resume();
|
||||
}
|
||||
|
||||
bodyBubble.setVisibility(View.VISIBLE);
|
||||
});
|
||||
}
|
||||
} else {
|
||||
clearFocusedItem();
|
||||
((ConversationAdapter) list.getAdapter()).toggleSelection(item);
|
||||
@@ -1449,6 +1500,20 @@ public class ConversationFragment extends LoggingFragment implements Multiselect
|
||||
}
|
||||
}
|
||||
|
||||
@Nullable private Uri getAudioUriForLongClick(@NonNull MessageRecord messageRecord) {
|
||||
VoiceNotePlaybackState playbackState = listener.getVoiceNoteMediaController().getVoiceNotePlaybackState().getValue();
|
||||
if (playbackState == null || !playbackState.isPlaying()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!MessageRecordUtil.hasAudio(messageRecord) || !messageRecord.isMms()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
Uri messageUri = ((MmsMessageRecord) messageRecord).getSlideDeck().getAudioSlide().getUri();
|
||||
return playbackState.getUri().equals(messageUri) ? messageUri : null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onQuoteClicked(MmsMessageRecord messageRecord) {
|
||||
if (messageRecord.getQuote() == null) {
|
||||
@@ -1867,7 +1932,7 @@ public class ConversationFragment extends LoggingFragment implements Multiselect
|
||||
}
|
||||
}
|
||||
|
||||
private class ReactionsToolbarListener implements Toolbar.OnMenuItemClickListener {
|
||||
private class ReactionsToolbarListener implements ConversationReactionOverlay.OnActionSelectedListener {
|
||||
|
||||
private final ConversationMessage conversationMessage;
|
||||
|
||||
@@ -1876,16 +1941,32 @@ public class ConversationFragment extends LoggingFragment implements Multiselect
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onMenuItemClick(MenuItem item) {
|
||||
switch (item.getItemId()) {
|
||||
case R.id.action_info: handleDisplayDetails(conversationMessage); return true;
|
||||
case R.id.action_delete: handleDeleteMessages(conversationMessage.getMultiselectCollection().toSet()); return true;
|
||||
case R.id.action_copy: handleCopyMessage(conversationMessage.getMultiselectCollection().toSet()); return true;
|
||||
case R.id.action_reply: handleReplyMessage(conversationMessage); return true;
|
||||
case R.id.action_multiselect: handleEnterMultiSelect(conversationMessage); return true;
|
||||
case R.id.action_forward: handleForwardMessageParts(conversationMessage.getMultiselectCollection().toSet()); return true;
|
||||
case R.id.action_download: handleSaveAttachment((MediaMmsMessageRecord) conversationMessage.getMessageRecord()); return true;
|
||||
default: return false;
|
||||
public void onActionSelected(@NonNull ConversationReactionOverlay.Action action) {
|
||||
switch (action) {
|
||||
case REPLY:
|
||||
handleReplyMessage(conversationMessage);
|
||||
break;
|
||||
case FORWARD:
|
||||
handleForwardMessageParts(conversationMessage.getMultiselectCollection().toSet());
|
||||
break;
|
||||
case RESEND:
|
||||
handleResendMessage(conversationMessage.getMessageRecord());
|
||||
break;
|
||||
case DOWNLOAD:
|
||||
handleSaveAttachment((MediaMmsMessageRecord) conversationMessage.getMessageRecord());
|
||||
break;
|
||||
case COPY:
|
||||
handleCopyMessage(conversationMessage.getMultiselectCollection().toSet());
|
||||
break;
|
||||
case MULTISELECT:
|
||||
handleEnterMultiSelect(conversationMessage);
|
||||
break;
|
||||
case VIEW_INFO:
|
||||
handleDisplayDetails(conversationMessage);
|
||||
break;
|
||||
case DELETE:
|
||||
handleDeleteMessages(conversationMessage.getMultiselectCollection().toSet());
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -160,6 +160,9 @@ public final class ConversationItem extends RelativeLayout implements BindableCo
|
||||
|
||||
private static final Rect SWIPE_RECT = new Rect();
|
||||
|
||||
public static final float LONG_PRESS_SCALE_FACTOR = 0.95f;
|
||||
private static final int SHRINK_BUBBLE_DELAY_MILLIS = 100;
|
||||
|
||||
private ConversationMessage conversationMessage;
|
||||
private MessageRecord messageRecord;
|
||||
private Optional<MessageRecord> nextMessageRecord;
|
||||
@@ -224,6 +227,21 @@ public final class ConversationItem extends RelativeLayout implements BindableCo
|
||||
private float lastYDownRelativeToThis;
|
||||
private ProjectionList colorizerProjections = new ProjectionList(3);
|
||||
|
||||
private final Runnable shrinkBubble = new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
bodyBubble.animate()
|
||||
.scaleX(LONG_PRESS_SCALE_FACTOR)
|
||||
.scaleY(LONG_PRESS_SCALE_FACTOR)
|
||||
.setUpdateListener(animation -> {
|
||||
View parent = (View) getParent();
|
||||
if (parent != null) {
|
||||
parent.invalidate();
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
public ConversationItem(Context context) {
|
||||
this(context, null);
|
||||
}
|
||||
@@ -343,6 +361,20 @@ public final class ConversationItem extends RelativeLayout implements BindableCo
|
||||
setGroupAuthorColor(messageRecord, hasWallpaper, colorizer);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean dispatchTouchEvent(MotionEvent ev) {
|
||||
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
|
||||
getHandler().postDelayed(shrinkBubble, SHRINK_BUBBLE_DELAY_MILLIS);
|
||||
} else {
|
||||
getHandler().removeCallbacks(shrinkBubble);
|
||||
bodyBubble.animate()
|
||||
.scaleX(1.0f)
|
||||
.scaleY(1.0f);
|
||||
}
|
||||
|
||||
return super.dispatchTouchEvent(ev);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onInterceptTouchEvent(MotionEvent ev) {
|
||||
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
|
||||
@@ -1737,7 +1769,11 @@ public final class ConversationItem extends RelativeLayout implements BindableCo
|
||||
@Override
|
||||
public @NonNull Projection getGiphyMp4PlayableProjection(@NonNull ViewGroup recyclerView) {
|
||||
if (mediaThumbnailStub != null && mediaThumbnailStub.isResolvable()) {
|
||||
return Projection.relativeToParent(recyclerView, mediaThumbnailStub.require(), mediaThumbnailStub.require().getCorners())
|
||||
ConversationItemThumbnail thumbnail = mediaThumbnailStub.require();
|
||||
return Projection.relativeToParent(recyclerView, thumbnail, thumbnail.getCorners())
|
||||
.scale(bodyBubble.getScaleX())
|
||||
.translateX(Util.halfOffsetFromScale(thumbnail.getWidth(), bodyBubble.getScaleX()))
|
||||
.translateY(Util.halfOffsetFromScale(thumbnail.getHeight(), bodyBubble.getScaleY()))
|
||||
.translateY(getTranslationY())
|
||||
.translateX(bodyBubble.getTranslationX())
|
||||
.translateX(getTranslationX());
|
||||
@@ -1754,6 +1790,11 @@ public final class ConversationItem extends RelativeLayout implements BindableCo
|
||||
return mediaThumbnailStub != null && mediaThumbnailStub.isResolvable() && canPlayContent;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean shouldProjectContent() {
|
||||
return canPlayContent() && bodyBubble.getVisibility() == VISIBLE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull ProjectionList getColorizerProjections(@NonNull ViewGroup coordinateRoot) {
|
||||
colorizerProjections.clear();
|
||||
@@ -1761,34 +1802,70 @@ public final class ConversationItem extends RelativeLayout implements BindableCo
|
||||
if (messageRecord.isOutgoing() &&
|
||||
!hasNoBubble(messageRecord) &&
|
||||
!messageRecord.isRemoteDelete() &&
|
||||
bodyBubbleCorners != null)
|
||||
bodyBubbleCorners != null &&
|
||||
bodyBubble.getVisibility() == VISIBLE)
|
||||
{
|
||||
Projection bodyBubbleToRoot = Projection.relativeToParent(coordinateRoot, bodyBubble, bodyBubbleCorners).translateX(bodyBubble.getTranslationX());
|
||||
Projection videoToBubble = bodyBubble.getVideoPlayerProjection();
|
||||
|
||||
float translationX = Util.halfOffsetFromScale(bodyBubble.getWidth(), bodyBubble.getScaleX());
|
||||
float translationY = Util.halfOffsetFromScale(bodyBubble.getHeight(), bodyBubble.getScaleY());
|
||||
|
||||
if (videoToBubble != null) {
|
||||
Projection videoToRoot = Projection.translateFromDescendantToParentCoords(videoToBubble, bodyBubble, coordinateRoot);
|
||||
colorizerProjections.addAll(Projection.getCapAndTail(bodyBubbleToRoot, videoToRoot));
|
||||
|
||||
List<Projection> projections = Projection.getCapAndTail(bodyBubbleToRoot, videoToRoot);
|
||||
if (!projections.isEmpty()) {
|
||||
projections.get(0)
|
||||
.scale(bodyBubble.getScaleX())
|
||||
.translateX(translationX)
|
||||
.translateY(translationY);
|
||||
projections.get(1)
|
||||
.scale(bodyBubble.getScaleX())
|
||||
.translateX(translationX)
|
||||
.translateY(-translationY);
|
||||
}
|
||||
|
||||
colorizerProjections.addAll(projections);
|
||||
} else {
|
||||
colorizerProjections.add(bodyBubbleToRoot);
|
||||
colorizerProjections.add(
|
||||
bodyBubbleToRoot.scale(bodyBubble.getScaleX())
|
||||
.translateX(translationX)
|
||||
.translateY(translationY)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (messageRecord.isOutgoing() &&
|
||||
hasNoBubble(messageRecord) &&
|
||||
hasWallpaper)
|
||||
hasWallpaper &&
|
||||
bodyBubble.getVisibility() == VISIBLE)
|
||||
{
|
||||
Projection footerProjection = getActiveFooter(messageRecord).getProjection(coordinateRoot);
|
||||
ConversationItemFooter footer = getActiveFooter(messageRecord);
|
||||
Projection footerProjection = footer.getProjection(coordinateRoot);
|
||||
if (footerProjection != null) {
|
||||
colorizerProjections.add(footerProjection.translateX(bodyBubble.getTranslationX()));
|
||||
colorizerProjections.add(
|
||||
footerProjection.translateX(bodyBubble.getTranslationX())
|
||||
.scale(bodyBubble.getScaleX())
|
||||
.translateX(Util.halfOffsetFromScale(footer.getWidth(), bodyBubble.getScaleX()))
|
||||
.translateY(-Util.halfOffsetFromScale(footer.getHeight(), bodyBubble.getScaleY()))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (!messageRecord.isOutgoing() &&
|
||||
hasQuote(messageRecord) &&
|
||||
quoteView != null)
|
||||
quoteView != null &&
|
||||
bodyBubble.getVisibility() == VISIBLE)
|
||||
{
|
||||
bodyBubble.setQuoteViewProjection(quoteView.getProjection(bodyBubble));
|
||||
colorizerProjections.add(quoteView.getProjection(coordinateRoot).translateX(bodyBubble.getTranslationX() + this.getTranslationX()));
|
||||
|
||||
float bubbleOffsetFromScale = Util.halfOffsetFromScale(bodyBubble.getHeight(), bodyBubble.getScaleY());
|
||||
Projection cProj = quoteView.getProjection(coordinateRoot)
|
||||
.translateX(bodyBubble.getTranslationX() + this.getTranslationX() + Util.halfOffsetFromScale(quoteView.getWidth(), bodyBubble.getScaleX()))
|
||||
.translateY(bubbleOffsetFromScale - quoteView.getY() + (quoteView.getY() * bodyBubble.getScaleY()))
|
||||
.scale(bodyBubble.getScaleX());
|
||||
colorizerProjections.add(cProj);
|
||||
}
|
||||
|
||||
for (int i = 0; i < colorizerProjections.size(); i++) {
|
||||
|
||||
@@ -0,0 +1,106 @@
|
||||
package org.thoughtcrime.securesms.conversation
|
||||
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.Canvas
|
||||
import android.graphics.Path
|
||||
import android.view.View
|
||||
import androidx.core.graphics.applyCanvas
|
||||
import androidx.core.graphics.createBitmap
|
||||
import androidx.core.graphics.withClip
|
||||
import androidx.core.graphics.withTranslation
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import org.thoughtcrime.securesms.database.model.MessageRecord
|
||||
import org.thoughtcrime.securesms.util.hasNoBubble
|
||||
|
||||
object ConversationItemSelection {
|
||||
|
||||
@JvmStatic
|
||||
fun snapshotView(
|
||||
conversationItem: ConversationItem,
|
||||
list: RecyclerView,
|
||||
messageRecord: MessageRecord,
|
||||
videoBitmap: Bitmap?,
|
||||
): Bitmap {
|
||||
val isOutgoing = messageRecord.isOutgoing
|
||||
val hasNoBubble = messageRecord.hasNoBubble(conversationItem.context)
|
||||
|
||||
return snapshotMessage(
|
||||
conversationItem = conversationItem,
|
||||
list = list,
|
||||
videoBitmap = videoBitmap,
|
||||
drawConversationItem = !isOutgoing || hasNoBubble,
|
||||
)
|
||||
}
|
||||
|
||||
private fun snapshotMessage(
|
||||
conversationItem: ConversationItem,
|
||||
list: RecyclerView,
|
||||
videoBitmap: Bitmap?,
|
||||
drawConversationItem: Boolean,
|
||||
): Bitmap {
|
||||
val initialReactionVisibility = conversationItem.reactionsView.visibility
|
||||
if (initialReactionVisibility == View.VISIBLE) {
|
||||
conversationItem.reactionsView.visibility = View.INVISIBLE
|
||||
}
|
||||
|
||||
val originalScale = conversationItem.bodyBubble.scaleX
|
||||
conversationItem.bodyBubble.scaleX = 1.0f
|
||||
conversationItem.bodyBubble.scaleY = 1.0f
|
||||
|
||||
val projections = conversationItem.getColorizerProjections(list)
|
||||
|
||||
val path = Path()
|
||||
|
||||
val yTranslation = -conversationItem.y
|
||||
|
||||
val mp4Projection = conversationItem.getGiphyMp4PlayableProjection(list)
|
||||
var scaledVideoBitmap = videoBitmap
|
||||
if (videoBitmap != null) {
|
||||
scaledVideoBitmap = Bitmap.createScaledBitmap(
|
||||
videoBitmap,
|
||||
(videoBitmap.width / originalScale).toInt(),
|
||||
(videoBitmap.height / originalScale).toInt(),
|
||||
true
|
||||
)
|
||||
|
||||
mp4Projection.translateY(yTranslation)
|
||||
mp4Projection.applyToPath(path)
|
||||
}
|
||||
|
||||
projections.use {
|
||||
it.forEach { p ->
|
||||
p.translateY(yTranslation)
|
||||
p.applyToPath(path)
|
||||
}
|
||||
}
|
||||
|
||||
val distanceToBubbleBottom = conversationItem.bodyBubble.height + conversationItem.bodyBubble.y.toInt()
|
||||
return createBitmap(conversationItem.width, distanceToBubbleBottom).applyCanvas {
|
||||
if (drawConversationItem) {
|
||||
draw(conversationItem)
|
||||
}
|
||||
|
||||
withClip(path) {
|
||||
withTranslation(y = yTranslation) {
|
||||
list.draw(this)
|
||||
|
||||
if (scaledVideoBitmap != null) {
|
||||
drawBitmap(scaledVideoBitmap, mp4Projection.x, mp4Projection.y - yTranslation, null)
|
||||
}
|
||||
}
|
||||
}
|
||||
}.also {
|
||||
mp4Projection.release()
|
||||
conversationItem.reactionsView.visibility = initialReactionVisibility
|
||||
conversationItem.bodyBubble.scaleX = originalScale
|
||||
conversationItem.bodyBubble.scaleY = originalScale
|
||||
}
|
||||
}
|
||||
|
||||
private fun Canvas.draw(conversationItem: ConversationItem) {
|
||||
val bodyBubble = conversationItem.bodyBubble
|
||||
withTranslation(bodyBubble.x, bodyBubble.y) {
|
||||
bodyBubble.draw(this@draw)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3664,12 +3664,13 @@ public class ConversationParentFragment extends Fragment
|
||||
|
||||
@Override
|
||||
public void handleReaction(@NonNull ConversationMessage conversationMessage,
|
||||
@NonNull Toolbar.OnMenuItemClickListener toolbarListener,
|
||||
@NonNull ConversationReactionOverlay.OnActionSelectedListener onActionSelectedListener,
|
||||
@NonNull SelectedConversationModel selectedConversationModel,
|
||||
@NonNull ConversationReactionOverlay.OnHideListener onHideListener)
|
||||
{
|
||||
reactionDelegate.setOnToolbarItemClickedListener(toolbarListener);
|
||||
reactionDelegate.setOnActionSelectedListener(onActionSelectedListener);
|
||||
reactionDelegate.setOnHideListener(onHideListener);
|
||||
reactionDelegate.show(requireActivity(), recipient.get(), conversationMessage, groupViewModel.isNonAdminInAnnouncementGroup());
|
||||
reactionDelegate.show(requireActivity(), recipient.get(), conversationMessage, groupViewModel.isNonAdminInAnnouncementGroup(), selectedConversationModel);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -3697,6 +3698,11 @@ public class ConversationParentFragment extends Fragment
|
||||
voiceNoteMediaController.startConsecutivePlayback(uri, messageId, progress);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onVoiceNoteResume(@NonNull Uri uri, long messageId) {
|
||||
voiceNoteMediaController.resumePlayback(uri, messageId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onVoiceNoteSeekTo(@NonNull Uri uri, double progress) {
|
||||
voiceNoteMediaController.seekToPosition(uri, progress);
|
||||
|
||||
@@ -5,7 +5,6 @@ import android.graphics.PointF;
|
||||
import android.view.MotionEvent;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.appcompat.widget.Toolbar;
|
||||
|
||||
import org.thoughtcrime.securesms.database.model.MessageRecord;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
@@ -24,7 +23,7 @@ final class ConversationReactionDelegate {
|
||||
private final PointF lastSeenDownPoint = new PointF();
|
||||
|
||||
private ConversationReactionOverlay.OnReactionSelectedListener onReactionSelectedListener;
|
||||
private Toolbar.OnMenuItemClickListener onToolbarItemClickedListener;
|
||||
private ConversationReactionOverlay.OnActionSelectedListener onActionSelectedListener;
|
||||
private ConversationReactionOverlay.OnHideListener onHideListener;
|
||||
|
||||
ConversationReactionDelegate(@NonNull Stub<ConversationReactionOverlay> overlayStub) {
|
||||
@@ -38,9 +37,10 @@ final class ConversationReactionDelegate {
|
||||
void show(@NonNull Activity activity,
|
||||
@NonNull Recipient conversationRecipient,
|
||||
@NonNull ConversationMessage conversationMessage,
|
||||
boolean isNonAdminInAnnouncementGroup)
|
||||
boolean isNonAdminInAnnouncementGroup,
|
||||
@NonNull SelectedConversationModel selectedConversationModel)
|
||||
{
|
||||
resolveOverlay().show(activity, conversationRecipient, conversationMessage, lastSeenDownPoint, isNonAdminInAnnouncementGroup);
|
||||
resolveOverlay().show(activity, conversationRecipient, conversationMessage, lastSeenDownPoint, isNonAdminInAnnouncementGroup, selectedConversationModel);
|
||||
}
|
||||
|
||||
void hide() {
|
||||
@@ -59,11 +59,11 @@ final class ConversationReactionDelegate {
|
||||
}
|
||||
}
|
||||
|
||||
void setOnToolbarItemClickedListener(@NonNull Toolbar.OnMenuItemClickListener onToolbarItemClickedListener) {
|
||||
this.onToolbarItemClickedListener = onToolbarItemClickedListener;
|
||||
void setOnActionSelectedListener(@NonNull ConversationReactionOverlay.OnActionSelectedListener onActionSelectedListener) {
|
||||
this.onActionSelectedListener = onActionSelectedListener;
|
||||
|
||||
if (overlayStub.resolved()) {
|
||||
overlayStub.get().setOnToolbarItemClickedListener(onToolbarItemClickedListener);
|
||||
overlayStub.get().setOnActionSelectedListener(onActionSelectedListener);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -99,7 +99,7 @@ final class ConversationReactionDelegate {
|
||||
overlay.requestFitSystemWindows();
|
||||
|
||||
overlay.setOnHideListener(onHideListener);
|
||||
overlay.setOnToolbarItemClickedListener(onToolbarItemClickedListener);
|
||||
overlay.setOnActionSelectedListener(onActionSelectedListener);
|
||||
overlay.setOnReactionSelectedListener(onReactionSelectedListener);
|
||||
|
||||
return overlay;
|
||||
|
||||
@@ -2,34 +2,41 @@ package org.thoughtcrime.securesms.conversation;
|
||||
|
||||
import android.animation.Animator;
|
||||
import android.animation.AnimatorSet;
|
||||
import android.animation.ObjectAnimator;
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.PointF;
|
||||
import android.graphics.Rect;
|
||||
import android.graphics.drawable.BitmapDrawable;
|
||||
import android.os.Build;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.HapticFeedbackConstants;
|
||||
import android.view.MenuItem;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.Window;
|
||||
import android.view.animation.DecelerateInterpolator;
|
||||
import android.view.animation.Interpolator;
|
||||
import android.widget.RelativeLayout;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.widget.Toolbar;
|
||||
import androidx.annotation.RequiresApi;
|
||||
import androidx.constraintlayout.widget.ConstraintLayout;
|
||||
import androidx.constraintlayout.widget.ConstraintSet;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import androidx.core.view.ViewKt;
|
||||
import androidx.vectordrawable.graphics.drawable.AnimatorInflaterCompat;
|
||||
|
||||
import com.annimon.stream.Stream;
|
||||
|
||||
import org.signal.core.util.DimensionUnit;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.animation.AnimationCompleteListener;
|
||||
import org.thoughtcrime.securesms.components.emoji.EmojiImageView;
|
||||
import org.thoughtcrime.securesms.components.emoji.EmojiUtil;
|
||||
import org.thoughtcrime.securesms.components.menu.ActionItem;
|
||||
import org.thoughtcrime.securesms.database.model.MessageRecord;
|
||||
import org.thoughtcrime.securesms.database.model.ReactionRecord;
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
||||
@@ -39,12 +46,15 @@ import org.thoughtcrime.securesms.util.Util;
|
||||
import org.thoughtcrime.securesms.util.ViewUtil;
|
||||
import org.thoughtcrime.securesms.util.WindowUtil;
|
||||
|
||||
import java.util.LinkedList;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import kotlin.Unit;
|
||||
|
||||
public final class ConversationReactionOverlay extends RelativeLayout {
|
||||
|
||||
private static final Interpolator INTERPOLATOR = new DecelerateInterpolator();
|
||||
private static final Interpolator INTERPOLATOR = new DecelerateInterpolator();
|
||||
private static final long TRANSITION_Y_DURATION = 150;
|
||||
|
||||
private final Rect emojiViewGlobalRect = new Rect();
|
||||
private final Rect emojiStripViewBounds = new Rect();
|
||||
@@ -54,23 +64,29 @@ public final class ConversationReactionOverlay extends RelativeLayout {
|
||||
private final Boundary verticalScrubBoundary = new Boundary();
|
||||
private final PointF deadzoneTouchPoint = new PointF();
|
||||
|
||||
private Activity activity;
|
||||
private Recipient conversationRecipient;
|
||||
private MessageRecord messageRecord;
|
||||
private OverlayState overlayState = OverlayState.HIDDEN;
|
||||
private boolean isNonAdminInAnnouncementGroup;
|
||||
private Activity activity;
|
||||
private Recipient conversationRecipient;
|
||||
private MessageRecord messageRecord;
|
||||
private SelectedConversationModel selectedConversationModel;
|
||||
private OverlayState overlayState = OverlayState.HIDDEN;
|
||||
private boolean isNonAdminInAnnouncementGroup;
|
||||
|
||||
private boolean downIsOurs;
|
||||
private boolean isToolbarTouch;
|
||||
private int selected = -1;
|
||||
private int customEmojiIndex;
|
||||
private int originalStatusBarColor;
|
||||
private int originalNavigationBarColor;
|
||||
|
||||
private View dropdownAnchor;
|
||||
private View toolbarShade;
|
||||
private View inputShade;
|
||||
private View conversationItem;
|
||||
private View backgroundView;
|
||||
private ConstraintLayout foregroundView;
|
||||
private View selectedView;
|
||||
private EmojiImageView[] emojiViews;
|
||||
private Toolbar toolbar;
|
||||
|
||||
private ConversationContextMenu contextMenu;
|
||||
|
||||
private float touchDownDeadZoneSize;
|
||||
private float distanceFromTouchDownPointToTopOfScrubberDeadZone;
|
||||
@@ -78,21 +94,18 @@ public final class ConversationReactionOverlay extends RelativeLayout {
|
||||
private int scrubberDistanceFromTouchDown;
|
||||
private int scrubberHeight;
|
||||
private int scrubberWidth;
|
||||
private int actionBarHeight;
|
||||
private int selectedVerticalTranslation;
|
||||
private int scrubberHorizontalMargin;
|
||||
private int animationEmojiStartDelayFactor;
|
||||
private int statusBarHeight;
|
||||
|
||||
private OnReactionSelectedListener onReactionSelectedListener;
|
||||
private Toolbar.OnMenuItemClickListener onToolbarItemClickedListener;
|
||||
private OnActionSelectedListener onActionSelectedListener;
|
||||
private OnHideListener onHideListener;
|
||||
|
||||
private AnimatorSet revealAnimatorSet = new AnimatorSet();
|
||||
private AnimatorSet revealMaskAnimatorSet = new AnimatorSet();
|
||||
private AnimatorSet hideAnimatorSet = new AnimatorSet();
|
||||
private AnimatorSet hideAllButMaskAnimatorSet = new AnimatorSet();
|
||||
private AnimatorSet hideMaskAnimatorSet = new AnimatorSet();
|
||||
private AnimatorSet revealAnimatorSet = new AnimatorSet();
|
||||
private AnimatorSet hideAnimatorSet = new AnimatorSet();
|
||||
private List<Animator> hideAnimators;
|
||||
|
||||
public ConversationReactionOverlay(@NonNull Context context) {
|
||||
super(context);
|
||||
@@ -106,13 +119,13 @@ public final class ConversationReactionOverlay extends RelativeLayout {
|
||||
protected void onFinishInflate() {
|
||||
super.onFinishInflate();
|
||||
|
||||
backgroundView = findViewById(R.id.conversation_reaction_scrubber_background);
|
||||
foregroundView = findViewById(R.id.conversation_reaction_scrubber_foreground);
|
||||
selectedView = findViewById(R.id.conversation_reaction_current_selection_indicator);
|
||||
toolbar = findViewById(R.id.conversation_reaction_toolbar);
|
||||
|
||||
toolbar.setOnMenuItemClickListener(this::handleToolbarItemClicked);
|
||||
toolbar.setNavigationOnClickListener(view -> hide());
|
||||
dropdownAnchor = findViewById(R.id.dropdown_anchor);
|
||||
toolbarShade = findViewById(R.id.toolbar_shade);
|
||||
inputShade = findViewById(R.id.input_shade);
|
||||
conversationItem = findViewById(R.id.conversation_item);
|
||||
backgroundView = findViewById(R.id.conversation_reaction_scrubber_background);
|
||||
foregroundView = findViewById(R.id.conversation_reaction_scrubber_foreground);
|
||||
selectedView = findViewById(R.id.conversation_reaction_current_selection_indicator);
|
||||
|
||||
emojiViews = new EmojiImageView[] { findViewById(R.id.reaction_1),
|
||||
findViewById(R.id.reaction_2),
|
||||
@@ -131,7 +144,6 @@ public final class ConversationReactionOverlay extends RelativeLayout {
|
||||
scrubberDistanceFromTouchDown = getResources().getDimensionPixelOffset(R.dimen.conversation_reaction_scrubber_distance);
|
||||
scrubberHeight = getResources().getDimensionPixelOffset(R.dimen.conversation_reaction_scrubber_height);
|
||||
scrubberWidth = getResources().getDimensionPixelOffset(R.dimen.reaction_scrubber_width);
|
||||
actionBarHeight = (int) ThemeUtil.getThemedDimen(getContext(), R.attr.actionBarSize);
|
||||
selectedVerticalTranslation = getResources().getDimensionPixelOffset(R.dimen.conversation_reaction_scrub_vertical_translation);
|
||||
scrubberHorizontalMargin = getResources().getDimensionPixelOffset(R.dimen.conversation_reaction_scrub_horizontal_margin);
|
||||
|
||||
@@ -144,7 +156,8 @@ public final class ConversationReactionOverlay extends RelativeLayout {
|
||||
@NonNull Recipient conversationRecipient,
|
||||
@NonNull ConversationMessage conversationMessage,
|
||||
@NonNull PointF lastSeenDownPoint,
|
||||
boolean isNonAdminInAnnouncementGroup)
|
||||
boolean isNonAdminInAnnouncementGroup,
|
||||
@NonNull SelectedConversationModel selectedConversationModel)
|
||||
{
|
||||
if (overlayState != OverlayState.HIDDEN) {
|
||||
return;
|
||||
@@ -152,11 +165,11 @@ public final class ConversationReactionOverlay extends RelativeLayout {
|
||||
|
||||
this.messageRecord = conversationMessage.getMessageRecord();
|
||||
this.conversationRecipient = conversationRecipient;
|
||||
this.selectedConversationModel = selectedConversationModel;
|
||||
this.isNonAdminInAnnouncementGroup = isNonAdminInAnnouncementGroup;
|
||||
overlayState = OverlayState.UNINITAILIZED;
|
||||
selected = -1;
|
||||
|
||||
setupToolbarMenuItems(conversationMessage);
|
||||
setupSelectedEmoji();
|
||||
|
||||
if (Build.VERSION.SDK_INT >= 21) {
|
||||
@@ -166,63 +179,260 @@ public final class ConversationReactionOverlay extends RelativeLayout {
|
||||
statusBarHeight = ViewUtil.getStatusBarHeight(this);
|
||||
}
|
||||
|
||||
final float scrubberTranslationY = Math.max(-scrubberDistanceFromTouchDown + actionBarHeight,
|
||||
lastSeenDownPoint.y - scrubberHeight - scrubberDistanceFromTouchDown - statusBarHeight);
|
||||
ViewGroup.LayoutParams layoutParams = inputShade.getLayoutParams();
|
||||
layoutParams.height = activity.findViewById(R.id.bottom_panel).getHeight();
|
||||
inputShade.setLayoutParams(layoutParams);
|
||||
|
||||
final float halfWidth = scrubberWidth / 2f + scrubberHorizontalMargin;
|
||||
final float screenWidth = getResources().getDisplayMetrics().widthPixels;
|
||||
final float downX = ViewUtil.isLtr(this) ? lastSeenDownPoint.x : screenWidth - lastSeenDownPoint.x;
|
||||
final float scrubberTranslationX = Util.clamp(downX - halfWidth,
|
||||
scrubberHorizontalMargin,
|
||||
screenWidth + scrubberHorizontalMargin - halfWidth * 2) * (ViewUtil.isLtr(this) ? 1 : -1);
|
||||
toolbarShade.setVisibility(VISIBLE);
|
||||
inputShade.setVisibility(VISIBLE);
|
||||
|
||||
backgroundView.setTranslationX(scrubberTranslationX);
|
||||
backgroundView.setTranslationY(scrubberTranslationY);
|
||||
Bitmap conversationItemSnapshot = selectedConversationModel.getBitmap();
|
||||
|
||||
foregroundView.setTranslationX(scrubberTranslationX);
|
||||
foregroundView.setTranslationY(scrubberTranslationY);
|
||||
conversationItem.setLayoutParams(new LayoutParams(conversationItemSnapshot.getWidth(), conversationItemSnapshot.getHeight()));
|
||||
conversationItem.setBackground(new BitmapDrawable(getResources(), conversationItemSnapshot));
|
||||
|
||||
verticalScrubBoundary.update(lastSeenDownPoint.y - distanceFromTouchDownPointToTopOfScrubberDeadZone,
|
||||
lastSeenDownPoint.y + distanceFromTouchDownPointToBottomOfScrubberDeadZone);
|
||||
float initialX = selectedConversationModel.getBitmapX();
|
||||
boolean isMessageOnLeft = selectedConversationModel.isOutgoing() ^ ViewUtil.isLtr(this);
|
||||
|
||||
conversationItem.setX(initialX);
|
||||
conversationItem.setY(selectedConversationModel.getBitmapY() - statusBarHeight);
|
||||
|
||||
conversationItem.setScaleX(ConversationItem.LONG_PRESS_SCALE_FACTOR);
|
||||
conversationItem.setScaleY(ConversationItem.LONG_PRESS_SCALE_FACTOR);
|
||||
|
||||
setVisibility(View.INVISIBLE);
|
||||
|
||||
ViewKt.doOnLayout(this, v -> {
|
||||
showAfterLayout(activity, conversationMessage, lastSeenDownPoint, isMessageOnLeft);
|
||||
return Unit.INSTANCE;
|
||||
});
|
||||
}
|
||||
|
||||
private void showAfterLayout(@NonNull Activity activity,
|
||||
@NonNull ConversationMessage conversationMessage,
|
||||
@NonNull PointF lastSeenDownPoint,
|
||||
boolean isMessageOnLeft) {
|
||||
contextMenu = new ConversationContextMenu(dropdownAnchor, getMenuActionItems(conversationMessage));
|
||||
|
||||
Bitmap conversationItemSnapshot = selectedConversationModel.getBitmap();
|
||||
boolean isWideLayout = contextMenu.getMaxWidth() + scrubberWidth < getWidth();
|
||||
|
||||
int bubbleWidth = selectedConversationModel.getBubbleWidth();
|
||||
|
||||
float endX = selectedConversationModel.getBitmapX();
|
||||
float endY = conversationItem.getY();
|
||||
float endApparentTop = endY;
|
||||
float endScale = 1f;
|
||||
|
||||
float menuPadding = DimensionUnit.DP.toPixels(12f);
|
||||
int reactionBarHeight = backgroundView.getHeight();
|
||||
|
||||
float reactionBarBackgroundY;
|
||||
|
||||
if (isWideLayout) {
|
||||
boolean everythingFitsVertically = scrubberHeight + conversationItemSnapshot.getHeight() < getHeight();
|
||||
if (everythingFitsVertically) {
|
||||
boolean reactionBarFitsAboveItem = conversationItem.getY() > scrubberHeight;
|
||||
|
||||
if (reactionBarFitsAboveItem) {
|
||||
reactionBarBackgroundY = conversationItem.getY() - menuPadding - reactionBarHeight;
|
||||
} else {
|
||||
endY = reactionBarHeight + menuPadding;
|
||||
reactionBarBackgroundY = 0f;
|
||||
}
|
||||
} else {
|
||||
float spaceAvailableForItem = getHeight() - reactionBarHeight - menuPadding;
|
||||
|
||||
endScale = spaceAvailableForItem / conversationItem.getHeight();
|
||||
endY = reactionBarHeight + menuPadding - Util.halfOffsetFromScale(conversationItemSnapshot.getHeight(), endScale);
|
||||
reactionBarBackgroundY = 0f;
|
||||
}
|
||||
} else {
|
||||
boolean everythingFitsVertically = contextMenu.getMaxHeight() + conversationItemSnapshot.getHeight() + menuPadding + reactionBarHeight < getHeight();
|
||||
|
||||
if (everythingFitsVertically) {
|
||||
float itemBottom = selectedConversationModel.getBitmapY() + conversationItemSnapshot.getHeight();
|
||||
boolean menuFitsBelowItem = itemBottom + menuPadding + contextMenu.getMaxHeight() <= getHeight();
|
||||
|
||||
if (menuFitsBelowItem) {
|
||||
reactionBarBackgroundY = conversationItem.getY() - menuPadding - reactionBarHeight;
|
||||
|
||||
if (reactionBarBackgroundY < 0) {
|
||||
endY = backgroundView.getHeight();
|
||||
reactionBarBackgroundY = 0f;
|
||||
}
|
||||
} else {
|
||||
endY = getHeight() - contextMenu.getMaxHeight() - menuPadding - conversationItemSnapshot.getHeight();
|
||||
reactionBarBackgroundY = endY - menuPadding - reactionBarHeight;
|
||||
}
|
||||
|
||||
endApparentTop = endY;
|
||||
} else if (reactionBarHeight + contextMenu.getMaxHeight() + menuPadding < getHeight()) {
|
||||
float spaceAvailableForItem = (float) getHeight() - contextMenu.getMaxHeight() - menuPadding - reactionBarHeight;
|
||||
|
||||
endScale = spaceAvailableForItem / conversationItemSnapshot.getHeight();
|
||||
endY = reactionBarHeight - Util.halfOffsetFromScale(conversationItemSnapshot.getHeight(), endScale);
|
||||
reactionBarBackgroundY = 0f;
|
||||
endApparentTop = reactionBarHeight;
|
||||
} else {
|
||||
contextMenu.setHeight(contextMenu.getMaxHeight() / 2);
|
||||
|
||||
int menuHeight = contextMenu.getHeight();
|
||||
boolean fitsVertically = menuHeight + conversationItem.getHeight() + menuPadding + reactionBarHeight < getHeight();
|
||||
|
||||
if (fitsVertically) {
|
||||
endY = getHeight() - menuHeight - menuPadding - conversationItemSnapshot.getHeight();
|
||||
reactionBarBackgroundY = endY - reactionBarHeight;
|
||||
endApparentTop = endY;
|
||||
} else {
|
||||
float spaceAvailableForItem = (float) getHeight() - menuHeight - menuPadding - reactionBarHeight;
|
||||
|
||||
endScale = spaceAvailableForItem / conversationItemSnapshot.getHeight();
|
||||
endY = reactionBarHeight - Util.halfOffsetFromScale(conversationItemSnapshot.getHeight(), endScale);
|
||||
reactionBarBackgroundY = 0f;
|
||||
endApparentTop = reactionBarHeight;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
reactionBarBackgroundY = Math.max(reactionBarBackgroundY, -statusBarHeight);
|
||||
|
||||
hideAnimatorSet.end();
|
||||
toolbar.setVisibility(VISIBLE);
|
||||
hideAnimatorSet = newHideAnimatorSet();
|
||||
setVisibility(View.VISIBLE);
|
||||
revealAnimatorSet.start();
|
||||
|
||||
if (Build.VERSION.SDK_INT >= 21) {
|
||||
this.activity = activity;
|
||||
originalStatusBarColor = activity.getWindow().getStatusBarColor();
|
||||
WindowUtil.setStatusBarColor(activity.getWindow(), ContextCompat.getColor(getContext(), R.color.action_mode_status_bar));
|
||||
updateSystemUiOnShow(activity);
|
||||
}
|
||||
|
||||
if (!ThemeUtil.isDarkTheme(getContext())) {
|
||||
WindowUtil.setLightStatusBar(activity.getWindow());
|
||||
}
|
||||
float scrubberX;
|
||||
if (isMessageOnLeft) {
|
||||
scrubberX = scrubberHorizontalMargin;
|
||||
} else {
|
||||
scrubberX = getWidth() - scrubberWidth - scrubberHorizontalMargin;
|
||||
}
|
||||
|
||||
foregroundView.setX(scrubberX);
|
||||
foregroundView.setY(reactionBarBackgroundY + reactionBarHeight / 2f - foregroundView.getHeight() / 2f);
|
||||
|
||||
backgroundView.setX(scrubberX);
|
||||
backgroundView.setY(reactionBarBackgroundY);
|
||||
|
||||
verticalScrubBoundary.update(reactionBarBackgroundY,
|
||||
lastSeenDownPoint.y + distanceFromTouchDownPointToBottomOfScrubberDeadZone);
|
||||
|
||||
updateBoundsOnLayoutChanged();
|
||||
|
||||
revealAnimatorSet.start();
|
||||
|
||||
if (isWideLayout) {
|
||||
float scrubberRight = scrubberX + scrubberWidth;
|
||||
float offsetX = isMessageOnLeft ? scrubberRight + menuPadding : scrubberX - contextMenu.getMaxWidth() - menuPadding;
|
||||
contextMenu.show((int) offsetX, (int) Math.min(backgroundView.getY(), getHeight() - contextMenu.getMaxHeight()));
|
||||
} else {
|
||||
float contentX = selectedConversationModel.getContentX();
|
||||
float offsetX = isMessageOnLeft ? contentX : - contextMenu.getMaxWidth() + contentX + bubbleWidth;
|
||||
|
||||
float menuTop = endApparentTop + (conversationItemSnapshot.getHeight() * endScale);
|
||||
contextMenu.show((int) offsetX, (int) (menuTop + menuPadding));
|
||||
}
|
||||
|
||||
conversationItem.animate()
|
||||
.x(endX)
|
||||
.scaleX(endScale)
|
||||
.scaleY(endScale);
|
||||
|
||||
conversationItem.animate()
|
||||
.y(endY)
|
||||
.setDuration(TRANSITION_Y_DURATION);
|
||||
}
|
||||
|
||||
@RequiresApi(api = 21)
|
||||
private void updateSystemUiOnShow(@NonNull Activity activity) {
|
||||
Window window = activity.getWindow();
|
||||
int barColor = ContextCompat.getColor(getContext(), R.color.conversation_item_selected_system_ui);
|
||||
|
||||
originalStatusBarColor = window.getStatusBarColor();
|
||||
WindowUtil.setStatusBarColor(window, barColor);
|
||||
|
||||
originalNavigationBarColor = window.getNavigationBarColor();
|
||||
WindowUtil.setNavigationBarColor(window, barColor);
|
||||
|
||||
if (!ThemeUtil.isDarkTheme(getContext())) {
|
||||
WindowUtil.clearLightStatusBar(window);
|
||||
}
|
||||
}
|
||||
|
||||
public void hide() {
|
||||
hideInternal(hideAnimatorSet, onHideListener);
|
||||
hideInternal(onHideListener);
|
||||
}
|
||||
|
||||
public void hideForReactWithAny() {
|
||||
hideInternal(hideAnimatorSet, null);
|
||||
hideInternal(onHideListener);
|
||||
}
|
||||
|
||||
private void hideInternal(@NonNull AnimatorSet hideAnimatorSet, @Nullable OnHideListener onHideListener) {
|
||||
private void hideInternal(@Nullable OnHideListener onHideListener) {
|
||||
overlayState = OverlayState.HIDDEN;
|
||||
|
||||
int duration = getContext().getResources().getInteger(R.integer.reaction_scrubber_reveal_duration);
|
||||
|
||||
List<Animator> hides = new ArrayList<>(hideAnimators);
|
||||
|
||||
ObjectAnimator itemScaleXAnim = new ObjectAnimator();
|
||||
itemScaleXAnim.setProperty(View.SCALE_X);
|
||||
itemScaleXAnim.setFloatValues(1f);
|
||||
itemScaleXAnim.setTarget(conversationItem);
|
||||
itemScaleXAnim.setDuration(duration);
|
||||
hides.add(itemScaleXAnim);
|
||||
|
||||
ObjectAnimator itemScaleYAnim = new ObjectAnimator();
|
||||
itemScaleYAnim.setProperty(View.SCALE_Y);
|
||||
itemScaleYAnim.setFloatValues(1f);
|
||||
itemScaleYAnim.setTarget(conversationItem);
|
||||
itemScaleYAnim.setDuration(duration);
|
||||
hides.add(itemScaleYAnim);
|
||||
|
||||
ObjectAnimator itemXAnim = new ObjectAnimator();
|
||||
itemXAnim.setProperty(View.X);
|
||||
itemXAnim.setFloatValues(selectedConversationModel.getBitmapX());
|
||||
itemXAnim.setTarget(conversationItem);
|
||||
itemXAnim.setDuration(duration);
|
||||
hides.add(itemXAnim);
|
||||
|
||||
ObjectAnimator itemYAnim = new ObjectAnimator();
|
||||
itemYAnim.setProperty(View.Y);
|
||||
itemYAnim.setFloatValues(selectedConversationModel.getBitmapY() - statusBarHeight);
|
||||
itemYAnim.setTarget(conversationItem);
|
||||
itemYAnim.setDuration(TRANSITION_Y_DURATION);
|
||||
hides.add(itemYAnim);
|
||||
|
||||
hideAnimatorSet.playTogether(hides);
|
||||
|
||||
revealAnimatorSet.end();
|
||||
hideAnimatorSet.start();
|
||||
|
||||
if (Build.VERSION.SDK_INT >= 21 && activity != null) {
|
||||
WindowUtil.setStatusBarColor(activity.getWindow(), originalStatusBarColor);
|
||||
WindowUtil.clearLightStatusBar(activity.getWindow());
|
||||
activity = null;
|
||||
}
|
||||
hideAnimatorSet.addListener(new AnimationCompleteListener() {
|
||||
@Override public void onAnimationEnd(Animator animation) {
|
||||
hideAnimatorSet.removeListener(this);
|
||||
|
||||
if (onHideListener != null) {
|
||||
onHideListener.onHide();
|
||||
toolbarShade.setVisibility(INVISIBLE);
|
||||
inputShade.setVisibility(INVISIBLE);
|
||||
|
||||
if (Build.VERSION.SDK_INT >= 21 && activity != null) {
|
||||
WindowUtil.setStatusBarColor(activity.getWindow(), originalStatusBarColor);
|
||||
WindowUtil.setNavigationBarColor(activity.getWindow(), originalNavigationBarColor);
|
||||
activity = null;
|
||||
}
|
||||
|
||||
if (onHideListener != null) {
|
||||
onHideListener.onHide();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (contextMenu != null) {
|
||||
contextMenu.dismiss();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -238,6 +448,10 @@ public final class ConversationReactionOverlay extends RelativeLayout {
|
||||
protected void onLayout(boolean changed, int l, int t, int r, int b) {
|
||||
super.onLayout(changed, l, t, r, b);
|
||||
|
||||
updateBoundsOnLayoutChanged();
|
||||
}
|
||||
|
||||
private void updateBoundsOnLayoutChanged() {
|
||||
backgroundView.getGlobalVisibleRect(emojiStripViewBounds);
|
||||
emojiViews[0].getGlobalVisibleRect(emojiViewGlobalRect);
|
||||
emojiStripViewBounds.left = getStart(emojiViewGlobalRect);
|
||||
@@ -300,24 +514,10 @@ public final class ConversationReactionOverlay extends RelativeLayout {
|
||||
}
|
||||
}
|
||||
|
||||
if (isToolbarTouch) {
|
||||
if (motionEvent.getAction() == MotionEvent.ACTION_CANCEL || motionEvent.getAction() == MotionEvent.ACTION_UP) {
|
||||
isToolbarTouch = false;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
switch (motionEvent.getAction()) {
|
||||
case MotionEvent.ACTION_DOWN:
|
||||
selected = getSelectedIndexViaDownEvent(motionEvent);
|
||||
|
||||
if (selected == -1) {
|
||||
if (motionEvent.getY() < toolbar.getHeight() + statusBarHeight) {
|
||||
isToolbarTouch = true;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
deadzoneTouchPoint.set(motionEvent.getX(), motionEvent.getY());
|
||||
overlayState = OverlayState.DEADZONE;
|
||||
downIsOurs = true;
|
||||
@@ -454,8 +654,8 @@ public final class ConversationReactionOverlay extends RelativeLayout {
|
||||
this.onReactionSelectedListener = onReactionSelectedListener;
|
||||
}
|
||||
|
||||
public void setOnToolbarItemClickedListener(@Nullable Toolbar.OnMenuItemClickListener onToolbarItemClickedListener) {
|
||||
this.onToolbarItemClickedListener = onToolbarItemClickedListener;
|
||||
public void setOnActionSelectedListener(@Nullable OnActionSelectedListener onActionSelectedListener) {
|
||||
this.onActionSelectedListener = onActionSelectedListener;
|
||||
}
|
||||
|
||||
public void setOnHideListener(@Nullable OnHideListener onHideListener) {
|
||||
@@ -474,24 +674,48 @@ public final class ConversationReactionOverlay extends RelativeLayout {
|
||||
.orElse(null);
|
||||
}
|
||||
|
||||
private void setupToolbarMenuItems(@NonNull ConversationMessage conversationMessage) {
|
||||
private @NonNull List<ActionItem> getMenuActionItems(@NonNull ConversationMessage conversationMessage) {
|
||||
MenuState menuState = MenuState.getMenuState(conversationRecipient, conversationMessage.getMultiselectCollection().toSet(), false, isNonAdminInAnnouncementGroup);
|
||||
|
||||
toolbar.getMenu().findItem(R.id.action_copy).setVisible(menuState.shouldShowCopyAction());
|
||||
toolbar.getMenu().findItem(R.id.action_download).setVisible(menuState.shouldShowSaveAttachmentAction());
|
||||
toolbar.getMenu().findItem(R.id.action_forward).setVisible(menuState.shouldShowForwardAction());
|
||||
toolbar.getMenu().findItem(R.id.action_reply).setVisible(menuState.shouldShowReplyAction());
|
||||
}
|
||||
List<ActionItem> items = new ArrayList<>();
|
||||
|
||||
private boolean handleToolbarItemClicked(@NonNull MenuItem menuItem) {
|
||||
|
||||
hide();
|
||||
|
||||
if (onToolbarItemClickedListener == null) {
|
||||
return false;
|
||||
if (menuState.shouldShowReplyAction()) {
|
||||
items.add(new ActionItem(R.drawable.ic_reply_24_tinted, getResources().getString(R.string.conversation_selection__menu_reply), () -> handleActionItemClicked(Action.REPLY)));
|
||||
}
|
||||
|
||||
return onToolbarItemClickedListener.onMenuItemClick(menuItem);
|
||||
if (menuState.shouldShowForwardAction()) {
|
||||
items.add(new ActionItem(R.drawable.ic_forward_24_tinted, getResources().getString(R.string.conversation_selection__menu_forward), () -> handleActionItemClicked(Action.FORWARD)));
|
||||
}
|
||||
|
||||
if (menuState.shouldShowResendAction()) {
|
||||
items.add(new ActionItem(R.drawable.ic_retry_24, getResources().getString(R.string.conversation_selection__menu_resend_message), () -> handleActionItemClicked(Action.RESEND)));
|
||||
}
|
||||
|
||||
if (menuState.shouldShowSaveAttachmentAction()) {
|
||||
items.add(new ActionItem(R.drawable.ic_save_24_tinted, getResources().getString(R.string.conversation_selection__menu_save), () -> handleActionItemClicked(Action.DOWNLOAD)));
|
||||
}
|
||||
|
||||
if (menuState.shouldShowCopyAction()) {
|
||||
items.add(new ActionItem(R.drawable.ic_copy_24_tinted, getResources().getString(R.string.conversation_selection__menu_copy), () -> handleActionItemClicked(Action.COPY)));
|
||||
}
|
||||
|
||||
items.add(new ActionItem(R.drawable.ic_select_24_tinted, getResources().getString(R.string.conversation_selection__menu_multi_select), () -> handleActionItemClicked(Action.MULTISELECT)));
|
||||
items.add(new ActionItem(R.drawable.ic_info_tinted_24, getResources().getString(R.string.conversation_selection__menu_message_details), () -> handleActionItemClicked(Action.VIEW_INFO)));
|
||||
items.add(new ActionItem(R.drawable.ic_delete_tinted_24, getResources().getString(R.string.conversation_selection__menu_delete), () -> handleActionItemClicked(Action.DELETE)));
|
||||
|
||||
return items;
|
||||
}
|
||||
|
||||
private void handleActionItemClicked(@NonNull Action action) {
|
||||
hideInternal(() -> {
|
||||
if (onHideListener != null) {
|
||||
onHideListener.onHide();
|
||||
}
|
||||
|
||||
if (onActionSelectedListener != null) {
|
||||
onActionSelectedListener.onActionSelected(action);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void initAnimators() {
|
||||
@@ -521,64 +745,47 @@ public final class ConversationReactionOverlay extends RelativeLayout {
|
||||
selectedRevealAnim.setDuration(duration);
|
||||
reveals.add(selectedRevealAnim);
|
||||
|
||||
Animator toolbarRevealAnim = AnimatorInflaterCompat.loadAnimator(getContext(), android.R.animator.fade_in);
|
||||
toolbarRevealAnim.setTarget(toolbar);
|
||||
toolbarRevealAnim.setDuration(duration);
|
||||
reveals.add(toolbarRevealAnim);
|
||||
|
||||
revealAnimatorSet.setInterpolator(INTERPOLATOR);
|
||||
revealAnimatorSet.playTogether(reveals);
|
||||
|
||||
revealMaskAnimatorSet.setInterpolator(INTERPOLATOR);
|
||||
revealMaskAnimatorSet.playTogether(overlayRevealAnim);
|
||||
|
||||
List<Animator> hides = Stream.of(emojiViews)
|
||||
.mapIndexed((idx, v) -> {
|
||||
Animator anim = AnimatorInflaterCompat.loadAnimator(getContext(), R.animator.reactions_scrubber_hide);
|
||||
anim.setTarget(v);
|
||||
anim.setStartDelay(idx * animationEmojiStartDelayFactor);
|
||||
return anim;
|
||||
})
|
||||
.toList();
|
||||
hideAnimators = Stream.of(emojiViews)
|
||||
.mapIndexed((idx, v) -> {
|
||||
Animator anim = AnimatorInflaterCompat.loadAnimator(getContext(), R.animator.reactions_scrubber_hide);
|
||||
anim.setTarget(v);
|
||||
anim.setStartDelay(idx * animationEmojiStartDelayFactor);
|
||||
return anim;
|
||||
})
|
||||
.toList();
|
||||
|
||||
Animator overlayHideAnim = AnimatorInflaterCompat.loadAnimator(getContext(), android.R.animator.fade_out);
|
||||
overlayHideAnim.setDuration(duration);
|
||||
hideAnimators.add(overlayHideAnim);
|
||||
|
||||
Animator backgroundHideAnim = AnimatorInflaterCompat.loadAnimator(getContext(), android.R.animator.fade_out);
|
||||
backgroundHideAnim.setTarget(backgroundView);
|
||||
backgroundHideAnim.setDuration(duration);
|
||||
hides.add(backgroundHideAnim);
|
||||
hideAnimators.add(backgroundHideAnim);
|
||||
|
||||
Animator selectedHideAnim = AnimatorInflaterCompat.loadAnimator(getContext(), android.R.animator.fade_out);
|
||||
selectedHideAnim.setTarget(selectedView);
|
||||
selectedHideAnim.setDuration(duration);
|
||||
hides.add(selectedHideAnim);
|
||||
hideAnimators.add(selectedHideAnim);
|
||||
|
||||
Animator toolbarHideAnim = AnimatorInflaterCompat.loadAnimator(getContext(), android.R.animator.fade_out);
|
||||
toolbarHideAnim.setTarget(toolbar);
|
||||
toolbarHideAnim.setDuration(duration);
|
||||
hides.add(toolbarHideAnim);
|
||||
hideAnimatorSet = newHideAnimatorSet();
|
||||
}
|
||||
|
||||
AnimationCompleteListener hideListener = new AnimationCompleteListener() {
|
||||
private @NonNull AnimatorSet newHideAnimatorSet() {
|
||||
AnimatorSet set = new AnimatorSet();
|
||||
|
||||
set.addListener(new AnimationCompleteListener() {
|
||||
@Override
|
||||
public void onAnimationEnd(Animator animation) {
|
||||
setVisibility(View.GONE);
|
||||
}
|
||||
};
|
||||
});
|
||||
set.setInterpolator(INTERPOLATOR);
|
||||
|
||||
List<Animator> hideAllAnimators = new LinkedList<>(hides);
|
||||
hideAllAnimators.add(overlayHideAnim);
|
||||
|
||||
hideAnimatorSet.addListener(hideListener);
|
||||
hideAnimatorSet.setInterpolator(INTERPOLATOR);
|
||||
hideAnimatorSet.playTogether(hideAllAnimators);
|
||||
|
||||
hideAllButMaskAnimatorSet.setInterpolator(INTERPOLATOR);
|
||||
hideAllButMaskAnimatorSet.playTogether(hides);
|
||||
|
||||
hideMaskAnimatorSet.addListener(hideListener);
|
||||
hideMaskAnimatorSet.setInterpolator(INTERPOLATOR);
|
||||
hideMaskAnimatorSet.playTogether(overlayHideAnim);
|
||||
return set;
|
||||
}
|
||||
|
||||
public interface OnHideListener {
|
||||
@@ -590,6 +797,10 @@ public final class ConversationReactionOverlay extends RelativeLayout {
|
||||
void onCustomReactionSelected(@NonNull MessageRecord messageRecord, boolean hasAddedCustomEmoji);
|
||||
}
|
||||
|
||||
public interface OnActionSelectedListener {
|
||||
void onActionSelected(@NonNull Action action);
|
||||
}
|
||||
|
||||
private static class Boundary {
|
||||
private float min;
|
||||
private float max;
|
||||
@@ -621,4 +832,15 @@ public final class ConversationReactionOverlay extends RelativeLayout {
|
||||
SCRUB,
|
||||
TAP
|
||||
}
|
||||
|
||||
public enum Action {
|
||||
REPLY,
|
||||
FORWARD,
|
||||
RESEND,
|
||||
DOWNLOAD,
|
||||
COPY,
|
||||
MULTISELECT,
|
||||
VIEW_INFO,
|
||||
DELETE,
|
||||
}
|
||||
}
|
||||
@@ -220,6 +220,11 @@ public final class ConversationUpdateItem extends FrameLayout
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean shouldProjectContent() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull ProjectionList getColorizerProjections(@NonNull ViewGroup coordinateRoot) {
|
||||
return EMPTY_PROJECTION_LIST;
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
package org.thoughtcrime.securesms.conversation
|
||||
|
||||
import android.graphics.Bitmap
|
||||
import android.net.Uri
|
||||
|
||||
/**
|
||||
* Contains information on a single selected conversation item. This is used when transitioning
|
||||
* between selected and unselected states.
|
||||
*/
|
||||
data class SelectedConversationModel(
|
||||
val bitmap: Bitmap,
|
||||
val bitmapX: Float,
|
||||
val bitmapY: Float,
|
||||
val contentX: Float,
|
||||
val bubbleWidth: Int,
|
||||
val audioUri: Uri? = null,
|
||||
val isOutgoing: Boolean,
|
||||
)
|
||||
@@ -79,6 +79,7 @@ class RecyclerViewColorizer(private val recyclerView: RecyclerView) {
|
||||
}
|
||||
|
||||
private val colorPaint = Paint()
|
||||
private val outOfBoundsPaint = Paint()
|
||||
|
||||
override fun getItemOffsets(outRect: Rect, view: View, parent: RecyclerView, state: RecyclerView.State) {
|
||||
outRect.setEmpty()
|
||||
@@ -122,6 +123,8 @@ class RecyclerViewColorizer(private val recyclerView: RecyclerView) {
|
||||
mask.setXfermode(colorPaint.xfermode)
|
||||
mask.setBounds(0, 0, parent.width, parent.height)
|
||||
mask.draw(canvas)
|
||||
|
||||
outOfBoundsPaint.color = chatColors.getColors().last()
|
||||
} else {
|
||||
colorPaint.color = chatColors.asSingleColor()
|
||||
canvas.drawRect(
|
||||
@@ -131,7 +134,13 @@ class RecyclerViewColorizer(private val recyclerView: RecyclerView) {
|
||||
parent.height.toFloat(),
|
||||
colorPaint
|
||||
)
|
||||
|
||||
outOfBoundsPaint.color = chatColors.asSingleColor()
|
||||
}
|
||||
|
||||
canvas.drawRect(
|
||||
0f, parent.height.toFloat(), parent.width.toFloat(), parent.height * 2f, outOfBoundsPaint
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -362,7 +362,7 @@ class MultiselectItemDecoration(
|
||||
}
|
||||
}
|
||||
|
||||
if (child.canPlayContent()) {
|
||||
if (child.canPlayContent() && child.shouldProjectContent()) {
|
||||
val mp4GifProjection = child.getGiphyMp4PlayableProjection(child.rootView as ViewGroup)
|
||||
path.op(mp4GifProjection.path, Path.Op.DIFFERENCE)
|
||||
mp4GifProjection.release()
|
||||
|
||||
@@ -51,4 +51,10 @@ public interface GiphyMp4Playable {
|
||||
* Specifies whether the content can start playing.
|
||||
*/
|
||||
boolean canPlayContent();
|
||||
|
||||
/**
|
||||
* Specifies whether the projection from {@link #getGiphyMp4PlayableProjection(ViewGroup)} should
|
||||
* be used to project into a view.
|
||||
*/
|
||||
boolean shouldProjectContent();
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package org.thoughtcrime.securesms.giph.mp4;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.Bitmap;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
@@ -98,10 +99,18 @@ public final class GiphyMp4ProjectionPlayerHolder implements Player.Listener, De
|
||||
container.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
public void pause() {
|
||||
player.pause();
|
||||
}
|
||||
|
||||
public void show() {
|
||||
container.setVisibility(View.VISIBLE);
|
||||
}
|
||||
|
||||
public void resume() {
|
||||
player.play();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPlaybackStateChanged(int playbackState) {
|
||||
if (playbackState == Player.STATE_READY) {
|
||||
@@ -177,4 +186,8 @@ public final class GiphyMp4ProjectionPlayerHolder implements Player.Listener, De
|
||||
public void setCorners(@Nullable Projection.Corners corners) {
|
||||
player.setCorners(corners);
|
||||
}
|
||||
|
||||
public @Nullable Bitmap getBitmap() {
|
||||
return player.getBitmap();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -121,7 +121,7 @@ public final class GiphyMp4ProjectionRecycler implements GiphyMp4PlaybackControl
|
||||
}
|
||||
}
|
||||
|
||||
private @Nullable GiphyMp4ProjectionPlayerHolder getCurrentHolder(int adapterPosition) {
|
||||
public @Nullable GiphyMp4ProjectionPlayerHolder getCurrentHolder(int adapterPosition) {
|
||||
if (playing.get(adapterPosition) != null) {
|
||||
return playing.get(adapterPosition);
|
||||
} else if (notPlaying.get(adapterPosition) != null) {
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
package org.thoughtcrime.securesms.giph.mp4;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.Canvas;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.TextureView;
|
||||
import android.view.View;
|
||||
import android.widget.FrameLayout;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
@@ -103,6 +106,12 @@ public final class GiphyMp4VideoPlayer extends FrameLayout implements DefaultLif
|
||||
}
|
||||
}
|
||||
|
||||
void pause() {
|
||||
if (exoPlayer != null) {
|
||||
exoPlayer.pause();
|
||||
}
|
||||
}
|
||||
|
||||
void stop() {
|
||||
if (exoPlayer != null) {
|
||||
exoPlayer.stop();
|
||||
@@ -122,4 +131,13 @@ public final class GiphyMp4VideoPlayer extends FrameLayout implements DefaultLif
|
||||
void setResizeMode(@AspectRatioFrameLayout.ResizeMode int resizeMode) {
|
||||
exoView.setResizeMode(resizeMode);
|
||||
}
|
||||
|
||||
@Nullable Bitmap getBitmap() {
|
||||
final View view = exoView.getVideoSurfaceView();
|
||||
if (view instanceof TextureView) {
|
||||
return ((TextureView) view).getBitmap();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -89,6 +89,11 @@ final class GiphyMp4ViewHolder extends MappingViewHolder<GiphyImage> implements
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean shouldProjectContent() {
|
||||
return true;
|
||||
}
|
||||
|
||||
private void loadPlaceholderImage(@NonNull GiphyImage giphyImage) {
|
||||
GlideApp.with(itemView)
|
||||
.load(new ChunkedImageUrl(giphyImage.getStillUrl()))
|
||||
|
||||
@@ -16,13 +16,11 @@ import androidx.annotation.Nullable;
|
||||
import androidx.lifecycle.LifecycleOwner;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import com.annimon.stream.Stream;
|
||||
import com.google.android.exoplayer2.MediaItem;
|
||||
|
||||
import org.signal.core.util.ThreadUtil;
|
||||
import org.signal.core.util.concurrent.SignalExecutors;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.conversation.ClipProjectionDrawable;
|
||||
import org.thoughtcrime.securesms.conversation.ConversationItem;
|
||||
import org.thoughtcrime.securesms.conversation.ConversationMessage;
|
||||
import org.thoughtcrime.securesms.conversation.colors.Colorizable;
|
||||
@@ -41,10 +39,7 @@ import org.whispersystems.libsignal.util.guava.Optional;
|
||||
import java.sql.Date;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
final class MessageHeaderViewHolder extends RecyclerView.ViewHolder implements GiphyMp4Playable, Colorizable {
|
||||
private final TextView sentDate;
|
||||
@@ -250,6 +245,11 @@ final class MessageHeaderViewHolder extends RecyclerView.ViewHolder implements G
|
||||
return conversationItem.canPlayContent();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean shouldProjectContent() {
|
||||
return conversationItem.shouldProjectContent();
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull ProjectionList getColorizerProjections(@NonNull ViewGroup coordinateRoot) {
|
||||
return conversationItem.getColorizerProjections(coordinateRoot);
|
||||
|
||||
@@ -114,6 +114,14 @@ public final class Projection {
|
||||
return set(x, y + yTranslation, width, height, corners);
|
||||
}
|
||||
|
||||
public @NonNull Projection scale(float scale) {
|
||||
Corners newCorners = new Corners(this.corners.topLeft * scale,
|
||||
this.corners.topRight * scale,
|
||||
this.corners.bottomRight * scale,
|
||||
this.corners.bottomLeft * scale);
|
||||
return set(x, y, (int) (width * scale), (int) (height * scale), newCorners);
|
||||
}
|
||||
|
||||
public static @NonNull Projection relativeToParent(@NonNull ViewGroup parent, @NonNull View view, @Nullable Corners corners) {
|
||||
Rect viewBounds = new Rect();
|
||||
|
||||
|
||||
@@ -444,6 +444,15 @@ public class Util {
|
||||
return Math.min(Math.max(value, min), max);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns half of the difference between the given length, and the length when scaled by the
|
||||
* given scale.
|
||||
*/
|
||||
public static float halfOffsetFromScale(int length, float scale) {
|
||||
float scaledLength = length * scale;
|
||||
return (length - scaledLength) / 2;
|
||||
}
|
||||
|
||||
public static @Nullable String readTextFromClipboard(@NonNull Context context) {
|
||||
{
|
||||
ClipboardManager clipboardManager = (ClipboardManager)context.getSystemService(Context.CLIPBOARD_SERVICE);
|
||||
|
||||
@@ -35,6 +35,12 @@ public final class WindowUtil {
|
||||
setSystemUiFlags(window, View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR);
|
||||
}
|
||||
|
||||
public static void setNavigationBarColor(@NonNull Window window, @ColorInt int color) {
|
||||
if (Build.VERSION.SDK_INT < 21) return;
|
||||
|
||||
window.setNavigationBarColor(color);
|
||||
}
|
||||
|
||||
public static void setLightStatusBarFromTheme(@NonNull Activity activity) {
|
||||
if (Build.VERSION.SDK_INT < 23) return;
|
||||
|
||||
|
||||
@@ -1,91 +0,0 @@
|
||||
package org.thoughtcrime.securesms.util.views;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.res.TypedArray;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuItem;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.widget.Toolbar;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.util.ViewUtil;
|
||||
|
||||
/**
|
||||
* AdaptiveActionsToolbar behaves like a normal {@link Toolbar} except in that it ignores the
|
||||
* showAsAlways attributes of menu items added via menu inflation, opting for an adaptive algorithm
|
||||
* instead. This algorithm will display as many icons as it can up to a specific percentage of the
|
||||
* screen.
|
||||
*
|
||||
* Each ActionView icon is expected to occupy 48dp of space, including padding. Items are stacked one
|
||||
* after the next with no margins.
|
||||
*
|
||||
* This view can be customized via attributes:
|
||||
*
|
||||
* aat_max_shown -- controls the max number of items to display.
|
||||
* aat_percent_for_actions -- controls the max percent of screen width the buttons can occupy.
|
||||
*/
|
||||
public class AdaptiveActionsToolbar extends Toolbar {
|
||||
|
||||
private static final int NAVIGATION_DP = 56;
|
||||
private static final int ACTION_VIEW_WIDTH_DP = 48;
|
||||
private static final int OVERFLOW_VIEW_WIDTH_DP = 36;
|
||||
|
||||
private int maxShown;
|
||||
|
||||
public AdaptiveActionsToolbar(@NonNull Context context) {
|
||||
this(context, null);
|
||||
}
|
||||
|
||||
public AdaptiveActionsToolbar(@NonNull Context context, @Nullable AttributeSet attrs) {
|
||||
this(context, attrs, R.attr.toolbarStyle);
|
||||
}
|
||||
|
||||
public AdaptiveActionsToolbar(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
|
||||
super(context, attrs, defStyleAttr);
|
||||
|
||||
TypedArray array = context.obtainStyledAttributes(attrs, R.styleable.AdaptiveActionsToolbar);
|
||||
|
||||
maxShown = array.getInteger(R.styleable.AdaptiveActionsToolbar_aat_max_shown, 100);
|
||||
|
||||
array.recycle();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
|
||||
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
|
||||
adjustMenuActions(getMenu(), maxShown, getMeasuredWidth());
|
||||
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
|
||||
}
|
||||
|
||||
public static void adjustMenuActions(@NonNull Menu menu, int maxToShow, int toolbarWidthPx) {
|
||||
int menuSize = 0;
|
||||
|
||||
for (int i = 0; i < menu.size(); i++) {
|
||||
if (menu.getItem(i).isVisible()) {
|
||||
menuSize++;
|
||||
}
|
||||
}
|
||||
|
||||
int widthAllowed = toolbarWidthPx - ViewUtil.dpToPx(NAVIGATION_DP);
|
||||
int nItemsToShow = Math.min(maxToShow, widthAllowed / ViewUtil.dpToPx(ACTION_VIEW_WIDTH_DP));
|
||||
|
||||
if (nItemsToShow < menuSize) {
|
||||
widthAllowed -= ViewUtil.dpToPx(OVERFLOW_VIEW_WIDTH_DP);
|
||||
}
|
||||
|
||||
nItemsToShow = Math.min(maxToShow, widthAllowed / ViewUtil.dpToPx(ACTION_VIEW_WIDTH_DP));
|
||||
|
||||
for (int i = 0; i < menu.size(); i++) {
|
||||
MenuItem item = menu.getItem(i);
|
||||
if (item.isVisible() && nItemsToShow > 0) {
|
||||
item.setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS);
|
||||
nItemsToShow--;
|
||||
} else {
|
||||
item.setShowAsAction(MenuItem.SHOW_AS_ACTION_NEVER);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
9
app/src/main/res/drawable-night/ic_select_24_tinted.xml
Normal file
9
app/src/main/res/drawable-night/ic_select_24_tinted.xml
Normal file
@@ -0,0 +1,9 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:pathData="M12,1C9.8244,1 7.6977,1.6451 5.8887,2.8538C4.0798,4.0625 2.6699,5.7805 1.8373,7.7905C1.0048,9.8005 0.7869,12.0122 1.2114,14.146C1.6358,16.2798 2.6834,18.2398 4.2218,19.7782C5.7602,21.3166 7.7202,22.3642 9.854,22.7886C11.9878,23.2131 14.1995,22.9952 16.2095,22.1627C18.2195,21.3301 19.9375,19.9202 21.1462,18.1113C22.3549,16.3023 23,14.1756 23,12C23,9.0826 21.8411,6.2847 19.7782,4.2218C17.7153,2.1589 14.9174,1 12,1V1ZM10.232,17L6.166,12.934L7.227,11.874L10.227,14.874L16.768,8.333L17.834,9.4L10.232,17Z"
|
||||
android:fillColor="@color/signal_icon_tint_primary"/>
|
||||
</vector>
|
||||
4
app/src/main/res/drawable/ic_retry_24.xml
Normal file
4
app/src/main/res/drawable/ic_retry_24.xml
Normal file
@@ -0,0 +1,4 @@
|
||||
<vector android:height="24dp" android:viewportHeight="20"
|
||||
android:viewportWidth="20" android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path android:fillColor="@color/signal_icon_tint_primary" android:pathData="M14.189,10.329l-1.4,-1.385 -1.061,1.061 3.9,3.884 3.865,-3.866 -1.06,-1.06 -1.055,1.055L16.5,11.344V10a7.75,7.75 0,1 0,-2.27 5.48l-1.061,-1.061A6.249,6.249 0,1 1,15 10v1.671Z"/>
|
||||
</vector>
|
||||
9
app/src/main/res/drawable/ic_save_24_tinted.xml
Normal file
9
app/src/main/res/drawable/ic_save_24_tinted.xml
Normal file
@@ -0,0 +1,9 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:pathData="M22,10V19C22,19.7956 21.6839,20.5587 21.1213,21.1213C20.5587,21.6839 19.7956,22 19,22H5C4.2043,22 3.4413,21.6839 2.8787,21.1213C2.3161,20.5587 2,19.7956 2,19V10C2,9.2044 2.3161,8.4413 2.8787,7.8787C3.4413,7.3161 4.2043,7 5,7H9.75V8.5H5C4.6022,8.5 4.2206,8.658 3.9393,8.9393C3.658,9.2206 3.5,9.6022 3.5,10V19C3.5,19.3978 3.658,19.7794 3.9393,20.0607C4.2206,20.342 4.6022,20.5 5,20.5H19C19.3978,20.5 19.7794,20.342 20.0607,20.0607C20.342,19.7794 20.5,19.3978 20.5,19V10C20.5,9.6022 20.342,9.2206 20.0607,8.9393C19.7794,8.658 19.3978,8.5 19,8.5H14.25V7H19C19.7956,7 20.5587,7.3161 21.1213,7.8787C21.6839,8.4413 22,9.2044 22,10ZM15.419,11.47L13.586,13.3L12.766,14.448V2H11.266V14.45L10.524,13.411L8.581,11.485L7.525,12.55L12.012,17L16.479,12.532L15.419,11.47Z"
|
||||
android:fillColor="@color/signal_icon_tint_primary"/>
|
||||
</vector>
|
||||
9
app/src/main/res/drawable/ic_select_24_tinted.xml
Normal file
9
app/src/main/res/drawable/ic_select_24_tinted.xml
Normal file
@@ -0,0 +1,9 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:pathData="M12,2.5C13.8789,2.5 15.7156,3.0572 17.2779,4.101C18.8402,5.1449 20.0578,6.6286 20.7769,8.3645C21.4959,10.1004 21.684,12.0105 21.3175,13.8534C20.9509,15.6962 20.0461,17.3889 18.7175,18.7175C17.3889,20.0461 15.6962,20.9509 13.8534,21.3175C12.0105,21.684 10.1004,21.4959 8.3645,20.7769C6.6286,20.0578 5.1449,18.8402 4.101,17.2779C3.0572,15.7157 2.5,13.8789 2.5,12C2.5029,9.4813 3.5047,7.0667 5.2857,5.2857C7.0667,3.5047 9.4813,2.5029 12,2.5V2.5ZM12,1C9.8244,1 7.6977,1.6451 5.8887,2.8538C4.0798,4.0625 2.6699,5.7805 1.8373,7.7905C1.0048,9.8005 0.7869,12.0122 1.2114,14.146C1.6358,16.2798 2.6834,18.2398 4.2218,19.7782C5.7602,21.3166 7.7202,22.3642 9.854,22.7886C11.9878,23.2131 14.1995,22.9952 16.2095,22.1627C18.2195,21.3301 19.9375,19.9202 21.1462,18.1113C22.3549,16.3023 23,14.1756 23,12C23,9.0826 21.8411,6.2847 19.7782,4.2218C17.7153,2.1589 14.9174,1 12,1V1ZM17.834,9.4L16.773,8.338L10.232,14.879L7.232,11.879L6.171,12.939L10.232,17L17.834,9.4Z"
|
||||
android:fillColor="@color/signal_icon_tint_primary"/>
|
||||
</vector>
|
||||
@@ -1,12 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<org.thoughtcrime.securesms.util.views.AdaptiveActionsToolbar xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="?attr/actionBarSize"
|
||||
android:background="@color/signal_background_secondary"
|
||||
app:contentInsetStart="0dp"
|
||||
app:contentInsetStartWithNavigation="48sp"
|
||||
app:menu="@menu/conversation_reactions_long_press_menu"
|
||||
app:navigationIcon="@drawable/ic_x_tinted">
|
||||
|
||||
</org.thoughtcrime.securesms.util.views.AdaptiveActionsToolbar>
|
||||
@@ -13,9 +13,31 @@
|
||||
android:visibility="gone"
|
||||
tools:visibility="visible">
|
||||
|
||||
<include
|
||||
android:id="@+id/conversation_reaction_toolbar"
|
||||
layout="@layout/conversation_reaction_long_press_toolbar" />
|
||||
<Space
|
||||
android:id="@+id/dropdown_anchor"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:layout_alignParentLeft="true"
|
||||
tools:ignore="RtlHardcoded" />
|
||||
|
||||
<View
|
||||
android:id="@+id/toolbar_shade"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="?actionBarSize"
|
||||
android:background="@color/reactions_screen_shade_color"
|
||||
android:layout_alignParentTop="true" />
|
||||
|
||||
<View
|
||||
android:id="@+id/input_shade"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="48dp"
|
||||
android:background="@color/reactions_screen_shade_color"
|
||||
android:layout_alignParentBottom="true" />
|
||||
|
||||
<View
|
||||
android:id="@+id/conversation_item"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content" />
|
||||
|
||||
<View
|
||||
android:id="@+id/conversation_reaction_scrubber_background"
|
||||
|
||||
@@ -1,47 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<menu xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
<item
|
||||
android:id="@+id/action_info"
|
||||
android:icon="@drawable/ic_info_white_24"
|
||||
android:title="@string/conversation_context__menu_message_details"
|
||||
app:iconTint="@color/signal_icon_tint_primary"
|
||||
app:showAsAction="always" />
|
||||
<item
|
||||
android:id="@+id/action_delete"
|
||||
android:icon="@drawable/ic_trash_24"
|
||||
android:title="@string/conversation_context__menu_delete_message"
|
||||
app:iconTint="@color/signal_icon_tint_primary"
|
||||
app:showAsAction="always" />
|
||||
<item
|
||||
android:id="@+id/action_copy"
|
||||
android:icon="@drawable/ic_copy_24"
|
||||
android:title="@string/conversation_context__menu_copy_text"
|
||||
app:iconTint="@color/signal_icon_tint_primary"
|
||||
app:showAsAction="always" />
|
||||
<item
|
||||
android:id="@+id/action_reply"
|
||||
android:icon="@drawable/ic_reply_24"
|
||||
android:title="@string/conversation_context__menu_reply_to_message"
|
||||
app:iconTint="@color/signal_icon_tint_primary"
|
||||
app:showAsAction="always" />
|
||||
<item
|
||||
android:visible="false"
|
||||
android:id="@+id/action_download"
|
||||
android:icon="@drawable/ic_save_24"
|
||||
android:title="@string/conversation_context_image__save_attachment"
|
||||
app:iconTint="@color/signal_icon_tint_primary"
|
||||
app:showAsAction="always" />
|
||||
<item
|
||||
android:id="@+id/action_multiselect"
|
||||
android:icon="@drawable/ic_select_24"
|
||||
android:title="@string/conversation_context__reaction_multi_select"
|
||||
app:iconTint="@color/signal_icon_tint_primary"
|
||||
app:showAsAction="always" />
|
||||
<item
|
||||
android:id="@+id/action_forward"
|
||||
android:icon="@drawable/ic_forward_24"
|
||||
android:title="@string/conversation_context__menu_forward_message"
|
||||
app:iconTint="@color/signal_icon_tint_primary"
|
||||
app:showAsAction="ifRoom" />
|
||||
</menu>
|
||||
@@ -127,6 +127,7 @@
|
||||
<color name="reactions_pill_text_color">@color/core_grey_35</color>
|
||||
<color name="reactions_pill_selected_text_color">@color/core_grey_15</color>
|
||||
<color name="reactions_screen_shade_color">@color/transparent_black_60</color>
|
||||
<color name="reactions_status_bar_shade">#070707</color>
|
||||
|
||||
<color name="recipient_contact_button_color">@color/core_grey_80</color>
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<color name="action_mode_status_bar">@color/signal_background_secondary</color>
|
||||
<color name="conversation_item_selected_system_ui">@color/reactions_status_bar_shade</color>
|
||||
</resources>
|
||||
@@ -251,10 +251,6 @@
|
||||
<attr name="rcv_outgoing" format="boolean" />
|
||||
</declare-styleable>
|
||||
|
||||
<declare-styleable name="AdaptiveActionsToolbar">
|
||||
<attr name="aat_max_shown" format="integer" />
|
||||
</declare-styleable>
|
||||
|
||||
<declare-styleable name="VideoThumbnailsRangeSelectorView">
|
||||
<attr name="thumbWidth" format="dimension" />
|
||||
<attr name="thumbColorEdited" format="color" />
|
||||
|
||||
@@ -30,7 +30,7 @@
|
||||
|
||||
<color name="conversation_compose_divider">#32000000</color>
|
||||
|
||||
<color name="action_mode_status_bar">@color/core_grey_60</color>
|
||||
<color name="conversation_item_selected_system_ui">@color/core_grey_60</color>
|
||||
<color name="touch_highlight">#400099cc</color>
|
||||
|
||||
<color name="device_link_item_background_light">#ffffffff</color>
|
||||
|
||||
@@ -127,6 +127,7 @@
|
||||
<color name="reactions_pill_text_color">@color/core_grey_60</color>
|
||||
<color name="reactions_pill_selected_text_color">@color/core_grey_75</color>
|
||||
<color name="reactions_screen_shade_color">@color/transparent_black_40</color>
|
||||
<color name="reactions_status_bar_shade">#999999</color>
|
||||
|
||||
<color name="recipient_contact_button_color">@color/core_grey_02</color>
|
||||
|
||||
|
||||
@@ -2775,22 +2775,6 @@
|
||||
<string name="conversation_callable_secure__menu_video">Signal video call</string>
|
||||
|
||||
<!-- conversation_context -->
|
||||
<!-- Button to view detailed information for a message -->
|
||||
<string name="conversation_context__menu_message_details">Info</string>
|
||||
<!-- Button to copy a message\'s text to the clipboard -->
|
||||
<string name="conversation_context__menu_copy_text">Copy</string>
|
||||
<!-- Button to delete a message -->
|
||||
<string name="conversation_context__menu_delete_message">Delete</string>
|
||||
<!-- Button to forward a message to another person or group chat -->
|
||||
<string name="conversation_context__menu_forward_message">Forward</string>
|
||||
<!-- Button to retry sending a message -->
|
||||
<string name="conversation_context__menu_resend_message">Resend message</string>
|
||||
<!-- Button to reply to a message -->
|
||||
<string name="conversation_context__menu_reply_to_message">Reply</string>
|
||||
|
||||
<!-- conversation_context_reaction -->
|
||||
<!-- Button to select a message and enter selection mode -->
|
||||
<string name="conversation_context__reaction_multi_select">Select multiple</string>
|
||||
|
||||
<!-- Heading which shows how many messages are currently selected -->
|
||||
<plurals name="conversation_context__s_selected">
|
||||
@@ -2818,6 +2802,10 @@
|
||||
<string name="conversation_selection__menu_reply">Reply</string>
|
||||
<!-- Button to save a message attachment (image, file etc.) -->
|
||||
<string name="conversation_selection__menu_save">Save</string>
|
||||
<!-- Button to retry sending a message -->
|
||||
<string name="conversation_selection__menu_resend_message">Resend</string>
|
||||
<!-- Button to select a message and enter selection mode -->
|
||||
<string name="conversation_selection__menu_multi_select">Select</string>
|
||||
|
||||
<!-- conversation_expiring_on -->
|
||||
|
||||
|
||||
Reference in New Issue
Block a user