Add support for inline video playback of gifs in Conversation.

This commit is contained in:
Alex Hart
2021-04-20 15:12:35 -03:00
committed by Greyson Parrelli
parent 32d79ead15
commit 281630e751
45 changed files with 1364 additions and 408 deletions

View File

@@ -112,6 +112,7 @@ import org.thoughtcrime.securesms.components.HidingLinearLayout;
import org.thoughtcrime.securesms.components.InputAwareLayout;
import org.thoughtcrime.securesms.components.InputPanel;
import org.thoughtcrime.securesms.components.KeyboardAwareLinearLayout.OnKeyboardShownListener;
import org.thoughtcrime.securesms.components.MaskView;
import org.thoughtcrime.securesms.components.SendButton;
import org.thoughtcrime.securesms.components.TooltipPopup;
import org.thoughtcrime.securesms.components.TypingStatusSender;
@@ -3420,7 +3421,7 @@ public class ConversationActivity extends PassphraseRequiredActivity
}
@Override
public void handleReaction(@NonNull View maskTarget,
public void handleReaction(@NonNull MaskView.MaskTarget maskTarget,
@NonNull MessageRecord messageRecord,
@NonNull Toolbar.OnMenuItemClickListener toolbarListener,
@NonNull ConversationReactionOverlay.OnHideListener onHideListener)
@@ -3451,7 +3452,7 @@ public class ConversationActivity extends PassphraseRequiredActivity
}
@Override
public void handleReactionDetails(@NonNull View maskTarget) {
public void handleReactionDetails(@NonNull MaskView.MaskTarget maskTarget) {
reactionDelegate.showMask(maskTarget, titleView.getMeasuredHeight(), inputAreaHeight());
}

View File

@@ -36,12 +36,18 @@ import androidx.recyclerview.widget.DiffUtil;
import androidx.recyclerview.widget.ListAdapter;
import androidx.recyclerview.widget.RecyclerView;
import com.google.android.exoplayer2.source.MediaSource;
import org.signal.core.util.ThreadUtil;
import org.signal.core.util.logging.Log;
import org.signal.paging.PagingController;
import org.thoughtcrime.securesms.BindableConversationItem;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.components.MaskView;
import org.thoughtcrime.securesms.database.model.MessageRecord;
import org.thoughtcrime.securesms.giph.mp4.GiphyMp4Playable;
import org.thoughtcrime.securesms.giph.mp4.GiphyMp4PlaybackPolicyEnforcer;
import org.thoughtcrime.securesms.giph.mp4.GiphyMp4Projection;
import org.thoughtcrime.securesms.mms.GlideRequests;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.util.CachedInflater;
@@ -50,6 +56,7 @@ import org.thoughtcrime.securesms.util.StickyHeaderDecoration;
import org.thoughtcrime.securesms.util.ThemeUtil;
import org.thoughtcrime.securesms.util.Util;
import org.thoughtcrime.securesms.util.ViewUtil;
import org.thoughtcrime.securesms.video.exo.AttachmentMediaSourceFactory;
import org.whispersystems.libsignal.util.guava.Optional;
import java.security.MessageDigest;
@@ -100,11 +107,12 @@ public class ConversationAdapter
private final Locale locale;
private final Recipient recipient;
private final Set<ConversationMessage> selected;
private final List<ConversationMessage> fastRecords;
private final Set<Long> releasedFastRecords;
private final Calendar calendar;
private final MessageDigest digest;
private final Set<ConversationMessage> selected;
private final List<ConversationMessage> fastRecords;
private final Set<Long> releasedFastRecords;
private final Calendar calendar;
private final MessageDigest digest;
private final AttachmentMediaSourceFactory attachmentMediaSourceFactory;
private String searchQuery;
private ConversationMessage recordToPulse;
@@ -113,12 +121,14 @@ public class ConversationAdapter
private PagingController pagingController;
private boolean hasWallpaper;
private boolean isMessageRequestAccepted;
private ConversationMessage inlineContent;
ConversationAdapter(@NonNull LifecycleOwner lifecycleOwner,
@NonNull GlideRequests glideRequests,
@NonNull Locale locale,
@Nullable ItemClickListener clickListener,
@NonNull Recipient recipient)
@NonNull Recipient recipient,
@NonNull AttachmentMediaSourceFactory attachmentMediaSourceFactory)
{
super(new DiffUtil.ItemCallback<ConversationMessage>() {
@Override
@@ -134,17 +144,18 @@ public class ConversationAdapter
this.lifecycleOwner = lifecycleOwner;
this.glideRequests = glideRequests;
this.locale = locale;
this.clickListener = clickListener;
this.recipient = recipient;
this.selected = new HashSet<>();
this.fastRecords = new ArrayList<>();
this.releasedFastRecords = new HashSet<>();
this.calendar = Calendar.getInstance();
this.digest = getMessageDigestOrThrow();
this.hasWallpaper = recipient.hasWallpaper();
this.isMessageRequestAccepted = true;
this.glideRequests = glideRequests;
this.locale = locale;
this.clickListener = clickListener;
this.recipient = recipient;
this.selected = new HashSet<>();
this.fastRecords = new ArrayList<>();
this.releasedFastRecords = new HashSet<>();
this.calendar = Calendar.getInstance();
this.digest = getMessageDigestOrThrow();
this.hasWallpaper = recipient.hasWallpaper();
this.isMessageRequestAccepted = true;
this.attachmentMediaSourceFactory = attachmentMediaSourceFactory;
setHasStableIds(true);
}
@@ -257,7 +268,9 @@ public class ConversationAdapter
searchQuery,
conversationMessage == recordToPulse,
hasWallpaper,
isMessageRequestAccepted);
isMessageRequestAccepted,
attachmentMediaSourceFactory,
conversationMessage == inlineContent);
if (conversationMessage == recordToPulse) {
recordToPulse = null;
@@ -610,7 +623,14 @@ public class ConversationAdapter
}
}
static class ConversationViewHolder extends RecyclerView.ViewHolder {
public void playInlineContent(@Nullable ConversationMessage conversationMessage) {
if (this.inlineContent != conversationMessage) {
this.inlineContent = conversationMessage;
notifyDataSetChanged();
}
}
final static class ConversationViewHolder extends RecyclerView.ViewHolder implements GiphyMp4Playable {
public ConversationViewHolder(final @NonNull View itemView) {
super(itemView);
}
@@ -618,6 +638,36 @@ public class ConversationAdapter
public BindableConversationItem getBindable() {
return (BindableConversationItem) itemView;
}
@Override
public void showProjectionArea() {
getBindable().showProjectionArea();
}
@Override
public void hideProjectionArea() {
getBindable().hideProjectionArea();
}
@Override
public @Nullable MediaSource getMediaSource() {
return getBindable().getMediaSource();
}
@Override
public @Nullable GiphyMp4PlaybackPolicyEnforcer getPlaybackPolicyEnforcer() {
return getBindable().getPlaybackPolicyEnforcer();
}
@NonNull
public @Override GiphyMp4Projection getProjection(@NonNull RecyclerView recyclerView) {
return getBindable().getProjection(recyclerView);
}
@Override
public boolean canPlayContent() {
return getBindable().canPlayContent();
}
}
static class StickyHeaderViewHolder extends RecyclerView.ViewHolder {
@@ -688,6 +738,6 @@ public class ConversationAdapter
interface ItemClickListener extends BindableConversationItem.EventListener {
void onItemClick(ConversationMessage item);
void onItemLongClick(View maskTarget, ConversationMessage item);
void onItemLongClick(View itemView, ConversationMessage item);
}
}

View File

@@ -76,6 +76,7 @@ import org.thoughtcrime.securesms.VerifyIdentityActivity;
import org.thoughtcrime.securesms.attachments.Attachment;
import org.thoughtcrime.securesms.components.ConversationScrollToView;
import org.thoughtcrime.securesms.components.ConversationTypingView;
import org.thoughtcrime.securesms.components.MaskView;
import org.thoughtcrime.securesms.components.TooltipPopup;
import org.thoughtcrime.securesms.components.TypingStatusRepository;
import org.thoughtcrime.securesms.components.recyclerview.SmoothScrollingLinearLayoutManager;
@@ -97,6 +98,10 @@ import org.thoughtcrime.securesms.database.model.MessageRecord;
import org.thoughtcrime.securesms.database.model.MmsMessageRecord;
import org.thoughtcrime.securesms.database.model.ReactionRecord;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.giph.mp4.GiphyMp4PlaybackPolicy;
import org.thoughtcrime.securesms.giph.mp4.GiphyMp4ProjectionPlayerHolder;
import org.thoughtcrime.securesms.giph.mp4.GiphyMp4PlaybackController;
import org.thoughtcrime.securesms.giph.mp4.GiphyMp4ProjectionRecycler;
import org.thoughtcrime.securesms.groups.GroupId;
import org.thoughtcrime.securesms.groups.GroupMigrationMembershipChange;
import org.thoughtcrime.securesms.groups.ui.invitesandrequests.invite.GroupLinkInviteFriendsBottomSheetDialogFragment;
@@ -148,6 +153,7 @@ import org.thoughtcrime.securesms.util.WindowUtil;
import org.thoughtcrime.securesms.util.concurrent.SimpleTask;
import org.thoughtcrime.securesms.util.task.ProgressDialogAsyncTask;
import org.thoughtcrime.securesms.util.views.AdaptiveActionsToolbar;
import org.thoughtcrime.securesms.video.exo.AttachmentMediaSourceFactory;
import org.thoughtcrime.securesms.wallpaper.ChatWallpaper;
import org.whispersystems.libsignal.util.guava.Optional;
@@ -177,6 +183,7 @@ public class ConversationFragment extends LoggingFragment {
private boolean isReacting;
private ActionMode actionMode;
private Locale locale;
private FrameLayout videoContainer;
private RecyclerView list;
private RecyclerView.ItemDecoration lastSeenDecoration;
private RecyclerView.ItemDecoration inlineDateDecoration;
@@ -204,6 +211,8 @@ public class ConversationFragment extends LoggingFragment {
private View toolbarShadow;
private Stopwatch startupStopwatch;
private GiphyMp4ProjectionRecycler giphyMp4ProjectionRecycler;
public static void prepare(@NonNull Context context) {
FrameLayout parent = new FrameLayout(context);
parent.setLayoutParams(new FrameLayout.LayoutParams(FrameLayout.LayoutParams.MATCH_PARENT, FrameLayout.LayoutParams.WRAP_CONTENT));
@@ -226,6 +235,7 @@ public class ConversationFragment extends LoggingFragment {
@Override
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle bundle) {
final View view = inflater.inflate(R.layout.conversation_fragment, container, false);
videoContainer = view.findViewById(R.id.video_container);
list = view.findViewById(android.R.id.list);
composeDivider = view.findViewById(R.id.compose_divider);
@@ -250,13 +260,16 @@ public class ConversationFragment extends LoggingFragment {
typingView = (ConversationTypingView) inflater.inflate(R.layout.conversation_typing_view, container, false);
giphyMp4ProjectionRecycler = initializeGiphyMp4();
new ConversationItemSwipeCallback(
conversationMessage -> actionMode == null &&
MenuState.canReplyToMessage(recipient.get(),
MenuState.isActionMessage(conversationMessage.getMessageRecord()),
conversationMessage.getMessageRecord(),
messageRequestViewModel.shouldShowMessageRequest()),
this::handleReplyMessage
this::handleReplyMessage,
giphyMp4ProjectionRecycler
).attachToRecyclerView(list);
setupListLayoutListeners();
@@ -297,6 +310,26 @@ public class ConversationFragment extends LoggingFragment {
return view;
}
private @NonNull GiphyMp4ProjectionRecycler initializeGiphyMp4() {
int maxPlayback = GiphyMp4PlaybackPolicy.maxSimultaneousPlaybackInConversation();
List<GiphyMp4ProjectionPlayerHolder> holders = GiphyMp4ProjectionPlayerHolder.injectVideoViews(requireContext(),
getViewLifecycleOwner().getLifecycle(),
videoContainer,
maxPlayback);
GiphyMp4ProjectionRecycler callback = new GiphyMp4ProjectionRecycler(holders);
GiphyMp4PlaybackController.attach(list, callback, maxPlayback);
return callback;
}
private @NonNull MaskView.MaskTarget getMaskTarget(@NonNull View itemView) {
int adapterPosition = list.getChildAdapterPosition(itemView);
View videoPlayer = giphyMp4ProjectionRecycler.getVideoPlayerAtAdapterPosition(adapterPosition);
return new ConversationItemMaskTarget((ConversationItem) itemView, videoPlayer);
}
private void setupListLayoutListeners() {
list.addOnLayoutChangeListener((v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom) -> setListVerticalTranslation());
@@ -557,7 +590,7 @@ public class ConversationFragment extends LoggingFragment {
private void initializeListAdapter() {
if (this.recipient != null && this.threadId != -1) {
Log.d(TAG, "Initializing adapter for " + recipient.getId());
ConversationAdapter adapter = new ConversationAdapter(this, GlideApp.with(this), locale, selectionClickListener, this.recipient.get());
ConversationAdapter adapter = new ConversationAdapter(this, GlideApp.with(this), locale, selectionClickListener, this.recipient.get(), new AttachmentMediaSourceFactory(requireContext()));
adapter.setPagingController(conversationViewModel.getPagingController());
list.setAdapter(adapter);
setInlineDateDecoration(adapter);
@@ -1191,14 +1224,14 @@ public class ConversationFragment extends LoggingFragment {
void onMessageActionToolbarOpened();
void onForwardClicked();
void onMessageRequest(@NonNull MessageRequestViewModel viewModel);
void handleReaction(@NonNull View maskTarget,
void handleReaction(@NonNull MaskView.MaskTarget maskTarget,
@NonNull MessageRecord messageRecord,
@NonNull Toolbar.OnMenuItemClickListener toolbarListener,
@NonNull ConversationReactionOverlay.OnHideListener onHideListener);
void onCursorChanged();
void onListVerticalTranslationChanged(float translationY);
void onMessageWithErrorClicked(@NonNull MessageRecord messageRecord);
void handleReactionDetails(@NonNull View maskTarget);
void handleReactionDetails(@NonNull MaskView.MaskTarget maskTarget);
}
private class ConversationScrollListener extends OnScrollListener {
@@ -1288,7 +1321,7 @@ public class ConversationFragment extends LoggingFragment {
}
@Override
public void onItemLongClick(View maskTarget, ConversationMessage conversationMessage) {
public void onItemLongClick(View itemView, ConversationMessage conversationMessage) {
if (actionMode != null) return;
@@ -1304,7 +1337,7 @@ public class ConversationFragment extends LoggingFragment {
{
isReacting = true;
list.setLayoutFrozen(true);
listener.handleReaction(maskTarget, messageRecord, new ReactionsToolbarListener(conversationMessage), () -> {
listener.handleReaction(getMaskTarget(itemView), messageRecord, new ReactionsToolbarListener(conversationMessage), () -> {
isReacting = false;
list.setLayoutFrozen(false);
WindowUtil.setLightStatusBarFromTheme(requireActivity());
@@ -1452,7 +1485,7 @@ public class ConversationFragment extends LoggingFragment {
public void onReactionClicked(@NonNull View reactionTarget, long messageId, boolean isMms) {
if (getContext() == null) return;
listener.handleReactionDetails(reactionTarget);
listener.handleReactionDetails(getMaskTarget(reactionTarget));
ReactionsBottomSheetDialogFragment.create(messageId, isMms).show(requireFragmentManager(), null);
}
@@ -1571,6 +1604,11 @@ public class ConversationFragment extends LoggingFragment {
refreshList();
}
}
@Override
public void onPlayInlineContent(ConversationMessage conversationMessage) {
getListAdapter().playInlineContent(conversationMessage);
}
}
public void refreshList() {

View File

@@ -55,8 +55,10 @@ import androidx.annotation.Nullable;
import androidx.appcompat.app.AlertDialog;
import androidx.core.content.ContextCompat;
import androidx.lifecycle.LifecycleOwner;
import androidx.recyclerview.widget.RecyclerView;
import com.annimon.stream.Stream;
import com.google.android.exoplayer2.source.MediaSource;
import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.BindableConversationItem;
@@ -70,6 +72,7 @@ import org.thoughtcrime.securesms.components.AvatarImageView;
import org.thoughtcrime.securesms.components.BorderlessImageView;
import org.thoughtcrime.securesms.components.ConversationItemFooter;
import org.thoughtcrime.securesms.components.ConversationItemThumbnail;
import org.thoughtcrime.securesms.components.CornerMask;
import org.thoughtcrime.securesms.components.DocumentView;
import org.thoughtcrime.securesms.components.LinkPreviewView;
import org.thoughtcrime.securesms.components.Outliner;
@@ -87,6 +90,9 @@ import org.thoughtcrime.securesms.database.model.MessageRecord;
import org.thoughtcrime.securesms.database.model.MmsMessageRecord;
import org.thoughtcrime.securesms.database.model.Quote;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.giph.mp4.GiphyMp4PlaybackPolicy;
import org.thoughtcrime.securesms.giph.mp4.GiphyMp4PlaybackPolicyEnforcer;
import org.thoughtcrime.securesms.giph.mp4.GiphyMp4Projection;
import org.thoughtcrime.securesms.jobs.AttachmentDownloadJob;
import org.thoughtcrime.securesms.jobs.MmsDownloadJob;
import org.thoughtcrime.securesms.jobs.MmsSendJob;
@@ -100,6 +106,7 @@ import org.thoughtcrime.securesms.mms.Slide;
import org.thoughtcrime.securesms.mms.SlideClickListener;
import org.thoughtcrime.securesms.mms.SlidesClickedListener;
import org.thoughtcrime.securesms.mms.TextSlide;
import org.thoughtcrime.securesms.mms.VideoSlide;
import org.thoughtcrime.securesms.reactions.ReactionsConversationView;
import org.thoughtcrime.securesms.recipients.LiveRecipient;
import org.thoughtcrime.securesms.recipients.Recipient;
@@ -120,6 +127,7 @@ import org.thoughtcrime.securesms.util.Util;
import org.thoughtcrime.securesms.util.VibrateUtil;
import org.thoughtcrime.securesms.util.ViewUtil;
import org.thoughtcrime.securesms.util.views.Stub;
import org.thoughtcrime.securesms.video.exo.AttachmentMediaSourceFactory;
import org.whispersystems.libsignal.util.guava.Optional;
import java.util.ArrayList;
@@ -198,9 +206,13 @@ public final class ConversationItem extends RelativeLayout implements BindableCo
private final LinkPreviewClickListener linkPreviewClickListener = new LinkPreviewClickListener();
private final ViewOnceMessageClickListener revealableClickListener = new ViewOnceMessageClickListener();
private final UrlClickListener urlClickListener = new UrlClickListener();
private final Rect thumbnailMaskingRect = new Rect();
private final Context context;
private MediaSource mediaSource;
private boolean canPlayContent;
public ConversationItem(Context context) {
this(context, null);
}
@@ -260,7 +272,9 @@ public final class ConversationItem extends RelativeLayout implements BindableCo
@Nullable String searchQuery,
boolean pulse,
boolean hasWallpaper,
boolean isMessageRequestAccepted)
boolean isMessageRequestAccepted,
@NonNull AttachmentMediaSourceFactory attachmentMediaSourceFactory,
boolean allowedToPlayInline)
{
if (this.recipient != null) this.recipient.removeForeverObserver(this);
if (this.conversationRecipient != null) this.conversationRecipient.removeForeverObserver(this);
@@ -275,13 +289,15 @@ public final class ConversationItem extends RelativeLayout implements BindableCo
this.conversationRecipient = conversationRecipient.live();
this.groupThread = conversationRecipient.isGroup();
this.recipient = messageRecord.getIndividualRecipient().live();
this.canPlayContent = false;
this.mediaSource = null;
this.recipient.observeForever(this);
this.conversationRecipient.observeForever(this);
setGutterSizes(messageRecord, groupThread);
setMessageShape(messageRecord, previousMessageRecord, nextMessageRecord, groupThread);
setMediaAttributes(messageRecord, previousMessageRecord, nextMessageRecord, groupThread, hasWallpaper, isMessageRequestAccepted);
setMediaAttributes(messageRecord, previousMessageRecord, nextMessageRecord, groupThread, hasWallpaper, isMessageRequestAccepted, attachmentMediaSourceFactory, allowedToPlayInline);
setBodyText(messageRecord, searchQuery, isMessageRequestAccepted);
setBubbleState(messageRecord, hasWallpaper);
setInteractionState(conversationMessage, pulse);
@@ -675,12 +691,14 @@ public final class ConversationItem extends RelativeLayout implements BindableCo
}
}
private void setMediaAttributes(@NonNull MessageRecord messageRecord,
@NonNull Optional<MessageRecord> previousRecord,
@NonNull Optional<MessageRecord> nextRecord,
boolean isGroupThread,
boolean hasWallpaper,
boolean messageRequestAccepted)
private void setMediaAttributes(@NonNull MessageRecord messageRecord,
@NonNull Optional<MessageRecord> previousRecord,
@NonNull Optional<MessageRecord> nextRecord,
boolean isGroupThread,
boolean hasWallpaper,
boolean messageRequestAccepted,
@Nullable AttachmentMediaSourceFactory attachmentMediaSourceFactory,
boolean allowedToPlayInline)
{
boolean showControls = !messageRecord.isFailed();
@@ -865,6 +883,16 @@ public final class ConversationItem extends RelativeLayout implements BindableCo
ViewUtil.updateLayoutParamsIfNonNull(groupSenderHolder, ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);
footer.setVisibility(VISIBLE);
if (attachmentMediaSourceFactory != null &&
thumbnailSlides.size() == 1 &&
thumbnailSlides.get(0).isVideoGif() &&
thumbnailSlides.get(0) instanceof VideoSlide)
{
canPlayContent = GiphyMp4PlaybackPolicy.autoplay() || allowedToPlayInline;
mediaSource = attachmentMediaSourceFactory.createMediaSource(Objects.requireNonNull(thumbnailSlides.get(0).getUri()));
}
} else {
if (mediaThumbnailStub.resolved()) mediaThumbnailStub.get().setVisibility(View.GONE);
if (audioViewStub.resolved()) audioViewStub.get().setVisibility(View.GONE);
@@ -1399,6 +1427,68 @@ public final class ConversationItem extends RelativeLayout implements BindableCo
return span;
}
@Override
public void showProjectionArea() {
if (mediaThumbnailStub != null && mediaThumbnailStub.resolved()) {
mediaThumbnailStub.get().showThumbnailView();
bodyBubble.setMask(null);
}
}
@Override
public void hideProjectionArea() {
if (mediaThumbnailStub != null && mediaThumbnailStub.resolved()) {
mediaThumbnailStub.get().hideThumbnailView();
mediaThumbnailStub.get().getDrawingRect(thumbnailMaskingRect);
bodyBubble.setMask(thumbnailMaskingRect);
}
}
@Override
public @Nullable MediaSource getMediaSource() {
return mediaSource;
}
@Override
public @Nullable GiphyMp4PlaybackPolicyEnforcer getPlaybackPolicyEnforcer() {
if (GiphyMp4PlaybackPolicy.autoplay()) {
return null;
} else {
return new GiphyMp4PlaybackPolicyEnforcer(() -> {
eventListener.onPlayInlineContent(null);
});
}
}
@Override
public int getAdapterPosition() {
throw new UnsupportedOperationException("Do not delegate to this method");
}
@Override
public @NonNull GiphyMp4Projection getProjection(@NonNull RecyclerView recyclerView) {
return GiphyMp4Projection.forView(recyclerView, mediaThumbnailStub.get(), mediaThumbnailStub.get().getCornerMask())
.translateX(bodyBubble.getTranslationX());
}
@Override
public boolean canPlayContent() {
return mediaThumbnailStub != null && canPlayContent;
}
public @NonNull Rect getThumbnailMaskingRect(@NonNull ViewGroup parent) {
Rect rect = new Rect();
rect.set(thumbnailMaskingRect);
parent.offsetDescendantRectToMyCoords(mediaThumbnailStub.get(), rect);
return rect;
}
public @NonNull CornerMask getThumbnailCornerMask(@NonNull View view) {
return new CornerMask(view, mediaThumbnailStub.get().getCornerMask());
}
private class SharedContactEventListener implements SharedContactView.EventListener {
@Override
public void onAddToContactsClicked(@NonNull Contact contact) {
@@ -1526,6 +1616,8 @@ public final class ConversationItem extends RelativeLayout implements BindableCo
public void onClick(final View v, final Slide slide) {
if (shouldInterceptClicks(messageRecord) || !batchSelected.isEmpty()) {
performClick();
} else if (!canPlayContent && mediaSource != null && eventListener != null) {
eventListener.onPlayInlineContent(conversationMessage);
} else if (MediaPreviewActivity.isContentTypeSupported(slide.getContentType()) && slide.getUri() != null) {
Intent intent = new Intent(context, MediaPreviewActivity.class);
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);

View File

@@ -2,6 +2,8 @@ package org.thoughtcrime.securesms.conversation;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.util.AttributeSet;
import android.widget.LinearLayout;
@@ -19,6 +21,9 @@ public class ConversationItemBodyBubble extends LinearLayout {
@Nullable private List<Outliner> outliners = Collections.emptyList();
@Nullable private OnSizeChangedListener sizeChangedListener;
private MaskDrawable maskDrawable;
private Rect mask;
public ConversationItemBodyBubble(Context context) {
super(context);
}
@@ -39,6 +44,18 @@ public class ConversationItemBodyBubble extends LinearLayout {
this.sizeChangedListener = listener;
}
@Override
public void setBackground(Drawable background) {
maskDrawable = new MaskDrawable(background);
maskDrawable.setMask(mask);
super.setBackground(maskDrawable);
}
public void setMask(@Nullable Rect mask) {
this.mask = mask;
maskDrawable.setMask(mask);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);

View File

@@ -0,0 +1,59 @@
package org.thoughtcrime.securesms.conversation;
import android.graphics.Canvas;
import android.graphics.Color;
import android.view.View;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.recyclerview.widget.RecyclerView;
import org.thoughtcrime.securesms.components.CornerMask;
import org.thoughtcrime.securesms.components.MaskView;
import org.thoughtcrime.securesms.giph.mp4.GiphyMp4Projection;
import java.util.Arrays;
import java.util.List;
public final class ConversationItemMaskTarget extends MaskView.MaskTarget {
private final ConversationItem conversationItem;
private final View videoContainer;
public ConversationItemMaskTarget(@NonNull ConversationItem conversationItem,
@Nullable View videoContainer)
{
super(conversationItem);
this.conversationItem = conversationItem;
this.videoContainer = videoContainer;
}
@Override
protected @NonNull List<View> getAllTargets() {
if (videoContainer == null) {
return super.getAllTargets();
} else {
return Arrays.asList(conversationItem, videoContainer);
}
}
@Override
protected void draw(@NonNull Canvas canvas) {
super.draw(canvas);
if (videoContainer == null) {
return;
}
GiphyMp4Projection projection = conversationItem.getProjection((RecyclerView) conversationItem.getParent());
CornerMask cornerMask = projection.getCornerMask();
canvas.clipRect(conversationItem.bodyBubble.getLeft(),
conversationItem.bodyBubble.getTop(),
conversationItem.bodyBubble.getRight(),
conversationItem.bodyBubble.getTop() + projection.getHeight());
canvas.drawColor(Color.BLACK);
cornerMask.mask(canvas);
}
}

View File

@@ -11,6 +11,8 @@ import androidx.annotation.NonNull;
import androidx.recyclerview.widget.ItemTouchHelper;
import androidx.recyclerview.widget.RecyclerView;
import org.thoughtcrime.securesms.giph.mp4.GiphyMp4DisplayUpdater;
import org.thoughtcrime.securesms.giph.mp4.GiphyMp4Playable;
import org.thoughtcrime.securesms.util.AccessibilityUtil;
import org.thoughtcrime.securesms.util.ServiceUtil;
@@ -28,14 +30,17 @@ class ConversationItemSwipeCallback extends ItemTouchHelper.SimpleCallback {
private final SwipeAvailabilityProvider swipeAvailabilityProvider;
private final ConversationItemTouchListener itemTouchListener;
private final OnSwipeListener onSwipeListener;
private final GiphyMp4DisplayUpdater giphyMp4DisplayUpdater;
ConversationItemSwipeCallback(@NonNull SwipeAvailabilityProvider swipeAvailabilityProvider,
@NonNull OnSwipeListener onSwipeListener)
@NonNull OnSwipeListener onSwipeListener,
@NonNull GiphyMp4DisplayUpdater giphyMp4DisplayUpdater)
{
super(0, ItemTouchHelper.END);
this.itemTouchListener = new ConversationItemTouchListener(this::updateLatestDownCoordinate);
this.swipeAvailabilityProvider = swipeAvailabilityProvider;
this.onSwipeListener = onSwipeListener;
this.giphyMp4DisplayUpdater = giphyMp4DisplayUpdater;
this.shouldTriggerSwipeFeedback = true;
this.canTriggerSwipe = true;
}
@@ -88,12 +93,14 @@ class ConversationItemSwipeCallback extends ItemTouchHelper.SimpleCallback {
if (actionState == ItemTouchHelper.ACTION_STATE_SWIPE && isCorrectSwipeDir) {
ConversationSwipeAnimationHelper.update((ConversationItem) viewHolder.itemView, Math.abs(dx), sign);
updateVideoPlayer(recyclerView, viewHolder);
handleSwipeFeedback((ConversationItem) viewHolder.itemView, Math.abs(dx));
if (canTriggerSwipe) {
setTouchListener(recyclerView, viewHolder, Math.abs(dx));
}
} else if (actionState == ItemTouchHelper.ACTION_STATE_IDLE || dx == 0) {
ConversationSwipeAnimationHelper.update((ConversationItem) viewHolder.itemView, 0, 1);
updateVideoPlayer(recyclerView, viewHolder);
}
if (dx == 0) {
@@ -102,6 +109,12 @@ class ConversationItemSwipeCallback extends ItemTouchHelper.SimpleCallback {
}
}
private void updateVideoPlayer(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder) {
if (viewHolder instanceof GiphyMp4Playable) {
giphyMp4DisplayUpdater.updateDisplay(recyclerView, (GiphyMp4Playable) viewHolder);
}
}
private void handleSwipeFeedback(@NonNull ConversationItem item, float dx) {
if (dx > SWIPE_SUCCESS_DX && shouldTriggerSwipeFeedback) {
vibrate(item.getContext());
@@ -134,7 +147,7 @@ class ConversationItemSwipeCallback extends ItemTouchHelper.SimpleCallback {
case MotionEvent.ACTION_CANCEL:
swipeBack = true;
shouldTriggerSwipeFeedback = false;
resetProgressIfAnimationsDisabled(viewHolder);
resetProgressIfAnimationsDisabled(recyclerView, viewHolder);
break;
}
return false;
@@ -156,11 +169,12 @@ class ConversationItemSwipeCallback extends ItemTouchHelper.SimpleCallback {
recyclerView.cancelPendingInputEvents();
}
private static void resetProgressIfAnimationsDisabled(RecyclerView.ViewHolder viewHolder) {
private void resetProgressIfAnimationsDisabled(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder) {
if (AccessibilityUtil.areAnimationsDisabled(viewHolder.itemView.getContext())) {
ConversationSwipeAnimationHelper.update((ConversationItem) viewHolder.itemView,
0f,
getSignFromDirection(viewHolder.itemView));
updateVideoPlayer(recyclerView, viewHolder);
}
}

View File

@@ -8,6 +8,7 @@ import android.view.View;
import androidx.annotation.NonNull;
import androidx.appcompat.widget.Toolbar;
import org.thoughtcrime.securesms.components.MaskView;
import org.thoughtcrime.securesms.database.model.MessageRecord;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.util.views.Stub;
@@ -38,7 +39,7 @@ final class ConversationReactionDelegate {
}
void show(@NonNull Activity activity,
@NonNull View maskTarget,
@NonNull MaskView.MaskTarget maskTarget,
@NonNull Recipient conversationRecipient,
@NonNull MessageRecord messageRecord,
int maskPaddingBottom)
@@ -46,7 +47,7 @@ final class ConversationReactionDelegate {
resolveOverlay().show(activity, maskTarget, conversationRecipient, messageRecord, maskPaddingBottom, lastSeenDownPoint);
}
void showMask(@NonNull View maskTarget, int maskPaddingTop, int maskPaddingBottom) {
void showMask(@NonNull MaskView.MaskTarget maskTarget, int maskPaddingTop, int maskPaddingBottom) {
resolveOverlay().showMask(maskTarget, maskPaddingTop, maskPaddingBottom);
}

View File

@@ -145,7 +145,7 @@ public final class ConversationReactionOverlay extends RelativeLayout {
}
public void show(@NonNull Activity activity,
@NonNull View maskTarget,
@NonNull MaskView.MaskTarget maskTarget,
@NonNull Recipient conversationRecipient,
@NonNull MessageRecord messageRecord,
int maskPaddingBottom,
@@ -209,7 +209,7 @@ public final class ConversationReactionOverlay extends RelativeLayout {
}
}
public void showMask(@NonNull View maskTarget, int maskPaddingTop, int maskPaddingBottom) {
public void showMask(@NonNull MaskView.MaskTarget maskTarget, int maskPaddingTop, int maskPaddingBottom) {
maskView.setPadding(0, maskPaddingTop, 0, maskPaddingBottom);
maskView.setTarget(maskTarget);

View File

@@ -16,6 +16,7 @@ import androidx.core.content.ContextCompat;
import androidx.lifecycle.LifecycleOwner;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.Observer;
import androidx.recyclerview.widget.RecyclerView;
import com.google.android.material.button.MaterialButton;
@@ -29,6 +30,7 @@ import org.thoughtcrime.securesms.database.model.GroupCallUpdateDetailsUtil;
import org.thoughtcrime.securesms.database.model.LiveUpdateMessage;
import org.thoughtcrime.securesms.database.model.MessageRecord;
import org.thoughtcrime.securesms.database.model.UpdateDescription;
import org.thoughtcrime.securesms.giph.mp4.GiphyMp4Projection;
import org.thoughtcrime.securesms.mms.GlideRequests;
import org.thoughtcrime.securesms.recipients.LiveRecipient;
import org.thoughtcrime.securesms.recipients.Recipient;
@@ -40,6 +42,7 @@ import org.thoughtcrime.securesms.util.Util;
import org.thoughtcrime.securesms.util.ViewUtil;
import org.thoughtcrime.securesms.util.concurrent.ListenableFuture;
import org.thoughtcrime.securesms.util.livedata.LiveDataUtil;
import org.thoughtcrime.securesms.video.exo.AttachmentMediaSourceFactory;
import org.whispersystems.libsignal.util.guava.Optional;
import java.util.Collection;
@@ -102,7 +105,9 @@ public final class ConversationUpdateItem extends FrameLayout
@Nullable String searchQuery,
boolean pulseMention,
boolean hasWallpaper,
boolean isMessageRequestAccepted)
boolean isMessageRequestAccepted,
@NonNull AttachmentMediaSourceFactory attachmentMediaSourceFactory,
boolean allowedToPlayInline)
{
this.batchSelected = batchSelected;
@@ -182,6 +187,30 @@ public final class ConversationUpdateItem extends FrameLayout
public void unbind() {
}
@Override
public void showProjectionArea() {
}
@Override
public void hideProjectionArea() {
throw new UnsupportedOperationException("Call makes no sense for a conversation update item");
}
@Override
public int getAdapterPosition() {
throw new UnsupportedOperationException("Don't delegate to this method.");
}
@Override
public @NonNull GiphyMp4Projection getProjection(@NonNull RecyclerView recyclerView) {
throw new UnsupportedOperationException("ConversationUpdateItems cannot be projected into.");
}
@Override
public boolean canPlayContent() {
return false;
}
static final class RecipientObserverManager {
private final Observer<Recipient> recipientObserver;

View File

@@ -0,0 +1,90 @@
package org.thoughtcrime.securesms.conversation;
import android.graphics.Canvas;
import android.graphics.ColorFilter;
import android.graphics.Path;
import android.graphics.Rect;
import android.graphics.RectF;
import android.graphics.Region;
import android.graphics.drawable.Drawable;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
/**
* Drawable which lets you punch a hole through another drawable.
*/
public final class MaskDrawable extends Drawable {
private final RectF bounds = new RectF();
private final Path clipPath = new Path();
private Rect clipRect;
private float[] clipPathRadii;
private final Drawable wrapped;
public MaskDrawable(@NonNull Drawable wrapped) {
this.wrapped = wrapped;
}
@Override
public void draw(@NonNull Canvas canvas) {
if (clipRect == null) {
wrapped.draw(canvas);
return;
}
canvas.save();
if (clipPathRadii != null) {
clipPath.reset();
bounds.set(clipRect);
clipPath.addRoundRect(bounds, clipPathRadii, Path.Direction.CW);
canvas.clipPath(clipPath, Region.Op.DIFFERENCE);
} else {
canvas.clipRect(clipRect, Region.Op.DIFFERENCE);
}
wrapped.draw(canvas);
canvas.restore();
}
@Override
public void setAlpha(int alpha) {
wrapped.setAlpha(alpha);
}
@Override
public void setColorFilter(@Nullable ColorFilter colorFilter) {
wrapped.setColorFilter(colorFilter);
}
@Override
public int getOpacity() {
return wrapped.getOpacity();
}
@Override
public void setBounds(int left, int top, int right, int bottom) {
super.setBounds(left, top, right, bottom);
wrapped.setBounds(left, top, right, bottom);
}
@Override
public boolean getPadding(@NonNull Rect padding) {
return wrapped.getPadding(padding);
}
public void setMask(@Nullable Rect mask) {
this.clipRect = new Rect(mask);
invalidateSelf();
}
public void setCorners(@Nullable float[] clipPathRadii) {
this.clipPathRadii = clipPathRadii;
invalidateSelf();
}
}