kopia lustrzana https://github.com/ryukoposting/Signal-Android
Implement UI and backend for sending story reactions.
Co-authored-by: Rashad Sookram <rashad@signal.org>fork-5.53.8
rodzic
7f4a12c179
commit
437c1e2f21
|
@ -27,6 +27,7 @@ import org.thoughtcrime.securesms.attachments.Attachment;
|
|||
import org.thoughtcrime.securesms.components.mention.MentionAnnotation;
|
||||
import org.thoughtcrime.securesms.conversation.colors.ChatColors;
|
||||
import org.thoughtcrime.securesms.database.model.Mention;
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.StoryTextPost;
|
||||
import org.thoughtcrime.securesms.mms.DecryptableStreamUriLoader.DecryptableUri;
|
||||
import org.thoughtcrime.securesms.mms.GlideRequests;
|
||||
import org.thoughtcrime.securesms.mms.Slide;
|
||||
|
@ -50,7 +51,8 @@ public class QuoteView extends FrameLayout implements RecipientForeverObserver {
|
|||
PREVIEW(0),
|
||||
OUTGOING(1),
|
||||
INCOMING(2),
|
||||
STORY_REPLY(3);
|
||||
STORY_REPLY(3),
|
||||
STORY_REPLY_PREVIEW(4);
|
||||
|
||||
private final int code;
|
||||
|
||||
|
@ -178,7 +180,7 @@ public class QuoteView extends FrameLayout implements RecipientForeverObserver {
|
|||
int radius = getResources().getDimensionPixelOffset(R.dimen.quote_corner_radius_preview);
|
||||
cornerMask.setTopLeftRadius(radius);
|
||||
cornerMask.setTopRightRadius(radius);
|
||||
} else if (messageType == MessageType.STORY_REPLY) {
|
||||
} else if (isStoryReply()) {
|
||||
thumbWidth = getResources().getDimensionPixelOffset(R.dimen.quote_story_thumb_width);
|
||||
thumbHeight = getResources().getDimensionPixelOffset(R.dimen.quote_story_thumb_height);
|
||||
}
|
||||
|
@ -250,9 +252,9 @@ public class QuoteView extends FrameLayout implements RecipientForeverObserver {
|
|||
|
||||
private void setQuoteAuthor(@NonNull Recipient author) {
|
||||
boolean outgoing = messageType != MessageType.INCOMING;
|
||||
boolean preview = messageType == MessageType.PREVIEW || messageType == MessageType.STORY_REPLY;
|
||||
boolean preview = messageType == MessageType.PREVIEW || messageType == MessageType.STORY_REPLY_PREVIEW;
|
||||
|
||||
if (messageType == MessageType.STORY_REPLY) {
|
||||
if (isStoryReply()) {
|
||||
authorView.setText(author.isSelf() ? getContext().getString(R.string.QuoteView_your_story)
|
||||
: getContext().getString(R.string.QuoteView_s_story, author.getDisplayName(getContext())));
|
||||
} else {
|
||||
|
@ -264,12 +266,26 @@ public class QuoteView extends FrameLayout implements RecipientForeverObserver {
|
|||
mainView.setBackgroundColor(ContextCompat.getColor(getContext(), preview ? R.color.quote_preview_background : R.color.quote_view_background));
|
||||
}
|
||||
|
||||
private void setQuoteText(@Nullable CharSequence body, @NonNull SlideDeck attachments) {
|
||||
boolean isTextStory = !attachments.containsMediaSlide() && messageType == MessageType.STORY_REPLY;
|
||||
private boolean isStoryReply() {
|
||||
return messageType == MessageType.STORY_REPLY || messageType == MessageType.STORY_REPLY_PREVIEW;
|
||||
}
|
||||
|
||||
private void setQuoteText(@Nullable CharSequence body, @NonNull SlideDeck attachments) {
|
||||
boolean isTextStory = !attachments.containsMediaSlide() && isStoryReply();
|
||||
|
||||
if (!TextUtils.isEmpty(body) || !attachments.containsMediaSlide()) {
|
||||
if (isTextStory && body != null) {
|
||||
try {
|
||||
bodyView.setText(StoryTextPostModel.parseFrom(body.toString()).getText());
|
||||
} catch (Exception e) {
|
||||
Log.w(TAG, "Could not parse body of text post.", e);
|
||||
bodyView.setText("");
|
||||
}
|
||||
} else {
|
||||
bodyView.setText(body == null ? "" : body);
|
||||
}
|
||||
|
||||
if (!isTextStory && (!TextUtils.isEmpty(body) || !attachments.containsMediaSlide())) {
|
||||
bodyView.setVisibility(VISIBLE);
|
||||
bodyView.setText(body == null ? "" : body);
|
||||
mediaDescriptionText.setVisibility(GONE);
|
||||
return;
|
||||
}
|
||||
|
@ -277,11 +293,6 @@ public class QuoteView extends FrameLayout implements RecipientForeverObserver {
|
|||
bodyView.setVisibility(GONE);
|
||||
mediaDescriptionText.setVisibility(VISIBLE);
|
||||
|
||||
if (isTextStory) {
|
||||
// TODO [alex] -- Media description.
|
||||
return;
|
||||
}
|
||||
|
||||
Slide audioSlide = attachments.getSlides().stream().filter(Slide::hasAudio).findFirst().orElse(null);
|
||||
Slide documentSlide = attachments.getSlides().stream().filter(Slide::hasDocument).findFirst().orElse(null);
|
||||
Slide imageSlide = attachments.getSlides().stream().filter(Slide::hasImage).findFirst().orElse(null);
|
||||
|
@ -314,7 +325,7 @@ public class QuoteView extends FrameLayout implements RecipientForeverObserver {
|
|||
}
|
||||
|
||||
private void setQuoteAttachment(@NonNull GlideRequests glideRequests, @NonNull CharSequence body, @NonNull SlideDeck slideDeck) {
|
||||
if (!attachments.containsMediaSlide() && messageType == MessageType.STORY_REPLY) {
|
||||
if (!attachments.containsMediaSlide() && isStoryReply()) {
|
||||
StoryTextPostModel model = StoryTextPostModel.parseFrom(body.toString());
|
||||
attachmentVideoOverlayView.setVisibility(GONE);
|
||||
attachmentContainerView.setVisibility(GONE);
|
||||
|
|
|
@ -81,6 +81,7 @@ import org.thoughtcrime.securesms.components.Outliner;
|
|||
import org.thoughtcrime.securesms.components.PlaybackSpeedToggleTextView;
|
||||
import org.thoughtcrime.securesms.components.QuoteView;
|
||||
import org.thoughtcrime.securesms.components.SharedContactView;
|
||||
import org.thoughtcrime.securesms.components.emoji.EmojiImageView;
|
||||
import org.thoughtcrime.securesms.components.emoji.EmojiTextView;
|
||||
import org.thoughtcrime.securesms.components.mention.MentionAnnotation;
|
||||
import org.thoughtcrime.securesms.contactshare.Contact;
|
||||
|
@ -190,6 +191,9 @@ public final class ConversationItem extends RelativeLayout implements BindableCo
|
|||
private AlertView alertView;
|
||||
protected ReactionsConversationView reactionsView;
|
||||
protected BadgeImageView badgeImageView;
|
||||
private View storyReactionLabelWrapper;
|
||||
private TextView storyReactionLabel;
|
||||
private EmojiImageView storyReactionEmoji;
|
||||
|
||||
private @NonNull Set<MultiselectPart> batchSelected = new HashSet<>();
|
||||
private @NonNull Outliner outliner = new Outliner();
|
||||
|
@ -271,28 +275,31 @@ public final class ConversationItem extends RelativeLayout implements BindableCo
|
|||
|
||||
initializeAttributes();
|
||||
|
||||
this.bodyText = findViewById(R.id.conversation_item_body);
|
||||
this.footer = findViewById(R.id.conversation_item_footer);
|
||||
this.stickerFooter = findViewById(R.id.conversation_item_sticker_footer);
|
||||
this.groupSender = findViewById(R.id.group_message_sender);
|
||||
this.alertView = findViewById(R.id.indicators_parent);
|
||||
this.contactPhoto = findViewById(R.id.contact_photo);
|
||||
this.contactPhotoHolder = findViewById(R.id.contact_photo_container);
|
||||
this.bodyBubble = findViewById(R.id.body_bubble);
|
||||
this.mediaThumbnailStub = new NullableStub<>(findViewById(R.id.image_view_stub));
|
||||
this.audioViewStub = new Stub<>(findViewById(R.id.audio_view_stub));
|
||||
this.documentViewStub = new Stub<>(findViewById(R.id.document_view_stub));
|
||||
this.sharedContactStub = new Stub<>(findViewById(R.id.shared_contact_view_stub));
|
||||
this.linkPreviewStub = new Stub<>(findViewById(R.id.link_preview_stub));
|
||||
this.stickerStub = new Stub<>(findViewById(R.id.sticker_view_stub));
|
||||
this.revealableStub = new Stub<>(findViewById(R.id.revealable_view_stub));
|
||||
this.callToActionStub = ViewUtil.findStubById(this, R.id.conversation_item_call_to_action_stub);
|
||||
this.groupSenderHolder = findViewById(R.id.group_sender_holder);
|
||||
this.quoteView = findViewById(R.id.quote_view);
|
||||
this.reply = findViewById(R.id.reply_icon_wrapper);
|
||||
this.replyIcon = findViewById(R.id.reply_icon);
|
||||
this.reactionsView = findViewById(R.id.reactions_view);
|
||||
this.badgeImageView = findViewById(R.id.badge);
|
||||
this.bodyText = findViewById(R.id.conversation_item_body);
|
||||
this.footer = findViewById(R.id.conversation_item_footer);
|
||||
this.stickerFooter = findViewById(R.id.conversation_item_sticker_footer);
|
||||
this.groupSender = findViewById(R.id.group_message_sender);
|
||||
this.alertView = findViewById(R.id.indicators_parent);
|
||||
this.contactPhoto = findViewById(R.id.contact_photo);
|
||||
this.contactPhotoHolder = findViewById(R.id.contact_photo_container);
|
||||
this.bodyBubble = findViewById(R.id.body_bubble);
|
||||
this.mediaThumbnailStub = new NullableStub<>(findViewById(R.id.image_view_stub));
|
||||
this.audioViewStub = new Stub<>(findViewById(R.id.audio_view_stub));
|
||||
this.documentViewStub = new Stub<>(findViewById(R.id.document_view_stub));
|
||||
this.sharedContactStub = new Stub<>(findViewById(R.id.shared_contact_view_stub));
|
||||
this.linkPreviewStub = new Stub<>(findViewById(R.id.link_preview_stub));
|
||||
this.stickerStub = new Stub<>(findViewById(R.id.sticker_view_stub));
|
||||
this.revealableStub = new Stub<>(findViewById(R.id.revealable_view_stub));
|
||||
this.callToActionStub = ViewUtil.findStubById(this, R.id.conversation_item_call_to_action_stub);
|
||||
this.groupSenderHolder = findViewById(R.id.group_sender_holder);
|
||||
this.quoteView = findViewById(R.id.quote_view);
|
||||
this.reply = findViewById(R.id.reply_icon_wrapper);
|
||||
this.replyIcon = findViewById(R.id.reply_icon);
|
||||
this.reactionsView = findViewById(R.id.reactions_view);
|
||||
this.badgeImageView = findViewById(R.id.badge);
|
||||
this.storyReactionLabelWrapper = findViewById(R.id.story_reacted_label_holder);
|
||||
this.storyReactionLabel = findViewById(R.id.story_reacted_label);
|
||||
this.storyReactionEmoji = findViewById(R.id.story_reaction_emoji);
|
||||
|
||||
setOnClickListener(new ClickListener(null));
|
||||
|
||||
|
@ -355,6 +362,7 @@ public final class ConversationItem extends RelativeLayout implements BindableCo
|
|||
setMessageSpacing(context, messageRecord, previousMessageRecord, nextMessageRecord, groupThread);
|
||||
setReactions(messageRecord);
|
||||
setFooter(messageRecord, nextMessageRecord, locale, groupThread, hasWallpaper);
|
||||
setStoryReactionLabel(messageRecord);
|
||||
|
||||
if (audioViewStub.resolved()) {
|
||||
audioViewStub.get().setOnLongClickListener(passthroughClickListener);
|
||||
|
@ -454,6 +462,7 @@ public final class ConversationItem extends RelativeLayout implements BindableCo
|
|||
if (!updatingFooter &&
|
||||
getActiveFooter(messageRecord) == footer &&
|
||||
!hasAudio(messageRecord) &&
|
||||
!isStoryReaction(messageRecord) &&
|
||||
isFooterVisible(messageRecord, nextMessageRecord, groupThread) &&
|
||||
!bodyText.isJumbomoji() &&
|
||||
conversationMessage.getBottomButton() == null &&
|
||||
|
@ -493,8 +502,9 @@ public final class ConversationItem extends RelativeLayout implements BindableCo
|
|||
}
|
||||
}
|
||||
|
||||
if (!updatingFooter && ViewUtil.getTopMargin(footer) != defaultTopMargin) {
|
||||
ViewUtil.setTopMargin(footer, defaultTopMargin);
|
||||
int defaultTopMarginForRecord = getDefaultTopMarginForRecord(messageRecord, defaultTopMargin, defaultBottomMargin);
|
||||
if (!updatingFooter && ViewUtil.getTopMargin(footer) != defaultTopMarginForRecord) {
|
||||
ViewUtil.setTopMargin(footer, defaultTopMarginForRecord);
|
||||
ViewUtil.setBottomMargin(footer, defaultBottomMargin);
|
||||
needsMeasure = true;
|
||||
}
|
||||
|
@ -538,6 +548,14 @@ public final class ConversationItem extends RelativeLayout implements BindableCo
|
|||
}
|
||||
}
|
||||
|
||||
private int getDefaultTopMarginForRecord(@NonNull MessageRecord messageRecord, int defaultTopMargin, int defaultBottomMargin) {
|
||||
if (isStoryReaction(messageRecord)) {
|
||||
return defaultBottomMargin;
|
||||
} else {
|
||||
return defaultTopMargin;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRecipientChanged(@NonNull Recipient modified) {
|
||||
if (conversationRecipient.getId().equals(modified.getId())) {
|
||||
|
@ -837,6 +855,10 @@ public final class ConversationItem extends RelativeLayout implements BindableCo
|
|||
}
|
||||
}
|
||||
|
||||
private boolean isStoryReaction(MessageRecord messageRecord) {
|
||||
return MessageRecordUtil.isStoryReaction(messageRecord);
|
||||
}
|
||||
|
||||
private boolean isCaptionlessMms(MessageRecord messageRecord) {
|
||||
return MessageRecordUtil.isCaptionlessMms(messageRecord, context);
|
||||
}
|
||||
|
@ -914,7 +936,7 @@ public final class ConversationItem extends RelativeLayout implements BindableCo
|
|||
bodyText.setText(italics);
|
||||
bodyText.setVisibility(View.VISIBLE);
|
||||
bodyText.setOverflowText(null);
|
||||
} else if (isCaptionlessMms(messageRecord)) {
|
||||
} else if (isCaptionlessMms(messageRecord) || isStoryReaction(messageRecord)) {
|
||||
bodyText.setVisibility(View.GONE);
|
||||
} else {
|
||||
Spannable styledText = conversationMessage.getDisplayBody(getContext());
|
||||
|
@ -1524,6 +1546,34 @@ public final class ConversationItem extends RelativeLayout implements BindableCo
|
|||
}
|
||||
}
|
||||
|
||||
private void setStoryReactionLabel(@NonNull MessageRecord record) {
|
||||
if (isStoryReaction(record)) {
|
||||
storyReactionLabelWrapper.setVisibility(View.VISIBLE);
|
||||
storyReactionLabel.setTextColor(record.isOutgoing() ? colorizer.getOutgoingBodyTextColor(context) : ContextCompat.getColor(context, R.color.signal_text_primary));
|
||||
storyReactionLabel.setText(getStoryReactionLabelText(messageRecord));
|
||||
storyReactionEmoji.setImageEmoji(record.getBody());
|
||||
storyReactionEmoji.setVisibility(View.VISIBLE);
|
||||
} else if (storyReactionLabelWrapper != null) {
|
||||
storyReactionLabelWrapper.setVisibility(View.GONE);
|
||||
storyReactionEmoji.setVisibility(View.GONE);
|
||||
}
|
||||
}
|
||||
|
||||
private @NonNull String getStoryReactionLabelText(@NonNull MessageRecord messageRecord) {
|
||||
if (hasQuote(messageRecord)) {
|
||||
MmsMessageRecord mmsMessageRecord = (MmsMessageRecord) messageRecord;
|
||||
RecipientId author = mmsMessageRecord.getQuote().getAuthor();
|
||||
|
||||
if (author.equals(Recipient.self().getId())) {
|
||||
return context.getString(R.string.ConversationItem__s_dot_story, context.getString(R.string.QuoteView_you));
|
||||
} else {
|
||||
return context.getString(R.string.ConversationItem__s_dot_story, Recipient.resolved(author).getDisplayName(context));
|
||||
}
|
||||
} else {
|
||||
return context.getString(R.string.ConversationItem__reacted_to_a_story);
|
||||
}
|
||||
}
|
||||
|
||||
private boolean forceFooter(@NonNull MessageRecord messageRecord) {
|
||||
return hasAudio(messageRecord);
|
||||
}
|
||||
|
|
|
@ -2970,7 +2970,7 @@ public class ConversationParentFragment extends Fragment
|
|||
long expiresIn = TimeUnit.SECONDS.toMillis(recipient.get().getExpiresInSeconds());
|
||||
QuoteModel quote = result.isViewOnce() ? null : inputPanel.getQuote().orElse(null);
|
||||
List<Mention> mentions = new ArrayList<>(result.getMentions());
|
||||
OutgoingMediaMessage message = new OutgoingMediaMessage(recipient.get(), new SlideDeck(), result.getBody(), System.currentTimeMillis(), -1, expiresIn, result.isViewOnce(), distributionType, result.getStoryType(), null, quote, Collections.emptyList(), Collections.emptyList(), mentions);
|
||||
OutgoingMediaMessage message = new OutgoingMediaMessage(recipient.get(), new SlideDeck(), result.getBody(), System.currentTimeMillis(), -1, expiresIn, result.isViewOnce(), distributionType, result.getStoryType(), null, false, quote, Collections.emptyList(), Collections.emptyList(), mentions);
|
||||
OutgoingMediaMessage secureMessage = new OutgoingSecureMediaMessage(message);
|
||||
|
||||
final Context context = requireContext().getApplicationContext();
|
||||
|
@ -3046,7 +3046,7 @@ public class ConversationParentFragment extends Fragment
|
|||
}
|
||||
}
|
||||
|
||||
OutgoingMediaMessage outgoingMessageCandidate = new OutgoingMediaMessage(Recipient.resolved(recipientId), slideDeck, body, System.currentTimeMillis(), subscriptionId, expiresIn, viewOnce, distributionType, StoryType.NONE, null, quote, contacts, previews, mentions);
|
||||
OutgoingMediaMessage outgoingMessageCandidate = new OutgoingMediaMessage(Recipient.resolved(recipientId), slideDeck, body, System.currentTimeMillis(), subscriptionId, expiresIn, viewOnce, distributionType, StoryType.NONE, null, false, quote, contacts, previews, mentions);
|
||||
|
||||
final SettableFuture<Void> future = new SettableFuture<>();
|
||||
final Context context = requireContext().getApplicationContext();
|
||||
|
|
|
@ -1479,7 +1479,23 @@ public class MmsDatabase extends MessageDatabase {
|
|||
return new OutgoingExpirationUpdateMessage(recipient, timestamp, expiresIn);
|
||||
}
|
||||
|
||||
OutgoingMediaMessage message = new OutgoingMediaMessage(recipient, body, attachments, timestamp, subscriptionId, expiresIn, viewOnce, distributionType, storyType, parentStoryId, quote, contacts, previews, mentions, networkFailures, mismatches);
|
||||
OutgoingMediaMessage message = new OutgoingMediaMessage(recipient,
|
||||
body,
|
||||
attachments,
|
||||
timestamp,
|
||||
subscriptionId,
|
||||
expiresIn,
|
||||
viewOnce,
|
||||
distributionType,
|
||||
storyType,
|
||||
parentStoryId,
|
||||
Types.isStoryReaction(outboxType),
|
||||
quote,
|
||||
contacts,
|
||||
previews,
|
||||
mentions,
|
||||
networkFailures,
|
||||
mismatches);
|
||||
|
||||
if (Types.isSecureType(outboxType)) {
|
||||
return new OutgoingSecureMediaMessage(message);
|
||||
|
@ -1675,6 +1691,10 @@ public class MmsDatabase extends MessageDatabase {
|
|||
type |= Types.EXPIRATION_TIMER_UPDATE_BIT;
|
||||
}
|
||||
|
||||
if (retrieved.isStoryReaction()) {
|
||||
type |= Types.SPECIAL_TYPE_STORY_REACTION;
|
||||
}
|
||||
|
||||
return insertMessageInbox(retrieved, "", threadId, type);
|
||||
}
|
||||
|
||||
|
@ -1779,6 +1799,10 @@ public class MmsDatabase extends MessageDatabase {
|
|||
type |= Types.EXPIRATION_TIMER_UPDATE_BIT;
|
||||
}
|
||||
|
||||
if (message.isStoryReaction()) {
|
||||
type |= Types.SPECIAL_TYPE_STORY_REACTION;
|
||||
}
|
||||
|
||||
Map<RecipientId, EarlyReceiptCache.Receipt> earlyDeliveryReceipts = earlyDeliveryReceiptCache.remove(message.getSentTimeMillis());
|
||||
|
||||
ContentValues contentValues = new ContentValues();
|
||||
|
|
|
@ -45,20 +45,21 @@ public interface MmsSmsColumns {
|
|||
* {@link #TOTAL_MASK}.
|
||||
*
|
||||
* <pre>
|
||||
* _____________________________________ ENCRYPTION ({@link #ENCRYPTION_MASK})
|
||||
* | _____________________________ SECURE MESSAGE INFORMATION (no mask, but look at {@link #SECURE_MESSAGE_BIT})
|
||||
* | | ________________________ GROUPS (no mask, but look at {@link #GROUP_UPDATE_BIT})
|
||||
* | | | _________________ KEY_EXCHANGE ({@link #KEY_EXCHANGE_MASK})
|
||||
* | | | | _________ MESSAGE_ATTRIBUTES ({@link #MESSAGE_ATTRIBUTE_MASK})
|
||||
* | | | | | ____ BASE_TYPE ({@link #BASE_TYPE_MASK})
|
||||
* ___|___ _| _| ___|__ | __|_
|
||||
* | | | | | | | | | || |
|
||||
* 0000 0000 0000 0000 0000 0000 0000 0000
|
||||
* _____________________________________________ SPECIAL TYPES (Story reactions) ({@link #SPECIAL_TYPES_MASK}
|
||||
* | _____________________________________ ENCRYPTION ({@link #ENCRYPTION_MASK})
|
||||
* | | _____________________________ SECURE MESSAGE INFORMATION (no mask, but look at {@link #SECURE_MESSAGE_BIT})
|
||||
* | | | ________________________ GROUPS (no mask, but look at {@link #GROUP_UPDATE_BIT})
|
||||
* | | | | _________________ KEY_EXCHANGE ({@link #KEY_EXCHANGE_MASK})
|
||||
* | | | | | _________ MESSAGE_ATTRIBUTES ({@link #MESSAGE_ATTRIBUTE_MASK})
|
||||
* | | | | | | ____ BASE_TYPE ({@link #BASE_TYPE_MASK})
|
||||
* _| ___|___ _| _| ___|__ | __|_
|
||||
* | | | | | | | | | | | || |
|
||||
* 0000 0000 0000 0000 0000 0000 0000 0000 0000
|
||||
* </pre>
|
||||
*/
|
||||
public static class Types {
|
||||
|
||||
protected static final long TOTAL_MASK = 0xFFFFFFFF;
|
||||
protected static final long TOTAL_MASK = 0xFFFFFFFFFL;
|
||||
|
||||
// Base Types
|
||||
protected static final long BASE_TYPE_MASK = 0x1F;
|
||||
|
@ -134,6 +135,18 @@ public interface MmsSmsColumns {
|
|||
protected static final long ENCRYPTION_REMOTE_DUPLICATE_BIT = 0x04000000;
|
||||
protected static final long ENCRYPTION_REMOTE_LEGACY_BIT = 0x02000000;
|
||||
|
||||
// Special message types
|
||||
public static final long SPECIAL_TYPES_MASK = 0xF00000000L;
|
||||
public static final long SPECIAL_TYPE_STORY_REACTION = 0x100000000L;
|
||||
|
||||
public static boolean isSpecialType(long type) {
|
||||
return (type & SPECIAL_TYPES_MASK) != 0L;
|
||||
}
|
||||
|
||||
public static boolean isStoryReaction(long type) {
|
||||
return (type & SPECIAL_TYPE_STORY_REACTION) == SPECIAL_TYPE_STORY_REACTION;
|
||||
}
|
||||
|
||||
public static boolean isDraftMessageType(long type) {
|
||||
return (type & BASE_TYPE_MASK) == BASE_DRAFT_TYPE;
|
||||
}
|
||||
|
|
|
@ -317,7 +317,14 @@ public final class PushGroupSendJob extends PushSendJob {
|
|||
.filter(r -> r.getStoriesCapability() == Recipient.Capability.SUPPORTED)
|
||||
.collect(java.util.stream.Collectors.toList());
|
||||
|
||||
groupMessageBuilder.withStoryContext(new SignalServiceDataMessage.StoryContext(recipient.requireServiceId(), storyRecord.getDateSent()));
|
||||
SignalServiceDataMessage.StoryContext storyContext = new SignalServiceDataMessage.StoryContext(recipient.requireServiceId(), storyRecord.getDateSent());
|
||||
groupMessageBuilder.withStoryContext(storyContext);
|
||||
|
||||
Optional<SignalServiceDataMessage.Reaction> reaction = getStoryReactionFor(message, storyContext);
|
||||
if (reaction.isPresent()) {
|
||||
groupMessageBuilder.withReaction(reaction.get());
|
||||
groupMessageBuilder.withBody(null);
|
||||
}
|
||||
} catch (NoSuchMessageException e) {
|
||||
// The story has probably expired
|
||||
// TODO [stories] check what should happen in this case
|
||||
|
|
|
@ -206,9 +206,9 @@ public class PushMediaSendJob extends PushSendJob {
|
|||
SignalServiceMessageSender messageSender = ApplicationDependencies.getSignalServiceMessageSender();
|
||||
SignalServiceAddress address = RecipientUtil.toSignalServiceAddress(context, messageRecipient);
|
||||
List<Attachment> attachments = Stream.of(message.getAttachments()).filterNot(Attachment::isSticker).toList();
|
||||
List<SignalServiceAttachment> serviceAttachments = getAttachmentPointersFor(attachments);
|
||||
Optional<byte[]> profileKey = getProfileKey(messageRecipient);
|
||||
Optional<SignalServiceDataMessage.Sticker> sticker = getStickerFor(message);
|
||||
List<SignalServiceAttachment> serviceAttachments = getAttachmentPointersFor(attachments);
|
||||
Optional<byte[]> profileKey = getProfileKey(messageRecipient);
|
||||
Optional<SignalServiceDataMessage.Sticker> sticker = getStickerFor(message);
|
||||
List<SharedContact> sharedContacts = getSharedContactsFor(message);
|
||||
List<SignalServicePreview> previews = getPreviewsFor(message);
|
||||
SignalServiceDataMessage.Builder mediaMessageBuilder = SignalServiceDataMessage.newBuilder()
|
||||
|
@ -226,7 +226,15 @@ public class PushMediaSendJob extends PushSendJob {
|
|||
if (message.getParentStoryId() != null) {
|
||||
try {
|
||||
MessageRecord storyRecord = SignalDatabase.mms().getMessageRecord(message.getParentStoryId().asMessageId().getId());
|
||||
mediaMessageBuilder.withStoryContext(new SignalServiceDataMessage.StoryContext(address.getServiceId(), storyRecord.getDateSent()));
|
||||
|
||||
SignalServiceDataMessage.StoryContext storyContext = new SignalServiceDataMessage.StoryContext(address.getServiceId(), storyRecord.getDateSent());
|
||||
mediaMessageBuilder.withStoryContext(storyContext);
|
||||
|
||||
Optional<SignalServiceDataMessage.Reaction> reaction = getStoryReactionFor(message, storyContext);
|
||||
if (reaction.isPresent()) {
|
||||
mediaMessageBuilder.withReaction(reaction.get());
|
||||
mediaMessageBuilder.withBody(null);
|
||||
}
|
||||
} catch (NoSuchMessageException e) {
|
||||
// The story has probably expired
|
||||
// TODO [stories] check what should happen in this case
|
||||
|
|
|
@ -375,6 +375,17 @@ public abstract class PushSendJob extends SendJob {
|
|||
}
|
||||
}
|
||||
|
||||
protected Optional<SignalServiceDataMessage.Reaction> getStoryReactionFor(@NonNull OutgoingMediaMessage message, @NonNull SignalServiceDataMessage.StoryContext storyContext) {
|
||||
if (message.isStoryReaction()) {
|
||||
return Optional.of(new SignalServiceDataMessage.Reaction(
|
||||
message.getBody(),
|
||||
false,
|
||||
new SignalServiceAddress(storyContext.getAuthorServiceId()), storyContext.getSentTimestamp()));
|
||||
} else {
|
||||
return Optional.empty();
|
||||
}
|
||||
}
|
||||
|
||||
List<SharedContact> getSharedContactsFor(OutgoingMediaMessage mediaMessage) {
|
||||
List<SharedContact> sharedContacts = new LinkedList<>();
|
||||
|
||||
|
|
|
@ -225,6 +225,7 @@ class MediaSelectionRepository(context: Context) {
|
|||
ThreadDatabase.DistributionTypes.DEFAULT,
|
||||
storyType,
|
||||
null,
|
||||
false,
|
||||
null,
|
||||
emptyList(),
|
||||
emptyList(),
|
||||
|
|
|
@ -82,6 +82,7 @@ class TextStoryPostSendRepository(context: Context) {
|
|||
ThreadDatabase.DistributionTypes.DEFAULT,
|
||||
storyType.toTextStoryType(),
|
||||
null,
|
||||
false,
|
||||
null,
|
||||
emptyList(),
|
||||
listOfNotNull(linkPreview),
|
||||
|
|
|
@ -840,6 +840,7 @@ public final class MessageContentProcessor {
|
|||
receivedTime,
|
||||
StoryType.NONE,
|
||||
null,
|
||||
false,
|
||||
-1,
|
||||
expiresInSeconds * 1000L,
|
||||
true,
|
||||
|
@ -873,9 +874,15 @@ public final class MessageContentProcessor {
|
|||
return null;
|
||||
}
|
||||
|
||||
private @Nullable MessageId handleReaction(@NonNull SignalServiceContent content, @NonNull SignalServiceDataMessage message, @NonNull Recipient senderRecipient) {
|
||||
private @Nullable MessageId handleReaction(@NonNull SignalServiceContent content, @NonNull SignalServiceDataMessage message, @NonNull Recipient senderRecipient) throws StorageFailedException {
|
||||
log(content.getTimestamp(), "Handle reaction for message " + message.getReaction().get().getTargetSentTimestamp());
|
||||
|
||||
if (content.getStoryMessage().isPresent()) {
|
||||
log(content.getTimestamp(), "Reaction has a story context. Treating as a story reaction.");
|
||||
handleStoryReaction(content, message, senderRecipient);
|
||||
return null;
|
||||
}
|
||||
|
||||
SignalServiceDataMessage.Reaction reaction = message.getReaction().get();
|
||||
|
||||
if (!EmojiUtil.isEmoji(reaction.getEmoji())) {
|
||||
|
@ -1350,6 +1357,7 @@ public final class MessageContentProcessor {
|
|||
System.currentTimeMillis(),
|
||||
storyType,
|
||||
null,
|
||||
false,
|
||||
-1,
|
||||
0,
|
||||
false,
|
||||
|
@ -1448,6 +1456,85 @@ public final class MessageContentProcessor {
|
|||
return Base64.encodeBytes(builder.build().toByteArray());
|
||||
}
|
||||
|
||||
private void handleStoryReaction(@NonNull SignalServiceContent content, @NonNull SignalServiceDataMessage message, @NonNull Recipient senderRecipient) throws StorageFailedException {
|
||||
log(content.getTimestamp(), "Story reaction.");
|
||||
|
||||
if (!Stories.isFeatureAvailable()) {
|
||||
warn(content.getTimestamp(), "Dropping unsupported story reaction.");
|
||||
return;
|
||||
}
|
||||
|
||||
SignalServiceDataMessage.Reaction reaction = message.getReaction().get();
|
||||
|
||||
if (!EmojiUtil.isEmoji(reaction.getEmoji())) {
|
||||
warn(content.getTimestamp(), "Story reaction text is not a valid emoji! Ignoring the message.");
|
||||
return;
|
||||
}
|
||||
|
||||
SignalServiceDataMessage.StoryContext storyContext = message.getStoryContext().get();
|
||||
|
||||
MessageDatabase database = SignalDatabase.mms();
|
||||
database.beginTransaction();
|
||||
|
||||
try {
|
||||
RecipientId storyAuthorRecipient = RecipientId.from(storyContext.getAuthorServiceId(), null);
|
||||
ParentStoryId parentStoryId;
|
||||
QuoteModel quoteModel = null;
|
||||
try {
|
||||
MessageId storyMessageId = database.getStoryId(storyAuthorRecipient, storyContext.getSentTimestamp());
|
||||
|
||||
if (message.getGroupContext().isPresent()) {
|
||||
parentStoryId = new ParentStoryId.GroupReply(storyMessageId.getId());
|
||||
} else {
|
||||
MmsMessageRecord story = (MmsMessageRecord) database.getMessageRecord(storyMessageId.getId());
|
||||
|
||||
if (!story.getStoryType().isStoryWithReplies()) {
|
||||
warn(content.getTimestamp(), "Story has reactions disabled. Dropping reaction.");
|
||||
return;
|
||||
}
|
||||
|
||||
parentStoryId = new ParentStoryId.DirectReply(storyMessageId.getId());
|
||||
quoteModel = new QuoteModel(storyContext.getSentTimestamp(), storyAuthorRecipient, "", false, story.getSlideDeck().asAttachments(), Collections.emptyList());
|
||||
}
|
||||
} catch (NoSuchMessageException e) {
|
||||
warn(content.getTimestamp(), "Couldn't find story for reaction.", e);
|
||||
return;
|
||||
}
|
||||
|
||||
IncomingMediaMessage mediaMessage = new IncomingMediaMessage(senderRecipient.getId(),
|
||||
content.getTimestamp(),
|
||||
content.getServerReceivedTimestamp(),
|
||||
System.currentTimeMillis(),
|
||||
StoryType.NONE,
|
||||
parentStoryId,
|
||||
true,
|
||||
-1,
|
||||
0,
|
||||
false,
|
||||
false,
|
||||
content.isNeedsReceipt(),
|
||||
Optional.of(reaction.getEmoji()),
|
||||
Optional.ofNullable(GroupUtil.getGroupContextIfPresent(content)),
|
||||
Optional.empty(),
|
||||
Optional.ofNullable(quoteModel),
|
||||
Optional.empty(),
|
||||
Optional.empty(),
|
||||
Optional.empty(),
|
||||
Optional.empty(),
|
||||
content.getServerUuid());
|
||||
|
||||
Optional<InsertResult> insertResult = database.insertSecureDecryptedMessageInbox(mediaMessage, -1);
|
||||
|
||||
if (insertResult.isPresent()) {
|
||||
database.setTransactionSuccessful();
|
||||
}
|
||||
} catch (MmsException e) {
|
||||
throw new StorageFailedException(e, content.getSender().getIdentifier(), content.getSenderDevice());
|
||||
} finally {
|
||||
database.endTransaction();
|
||||
}
|
||||
}
|
||||
|
||||
private void handleStoryReply(@NonNull SignalServiceContent content, @NonNull SignalServiceDataMessage message, @NonNull Recipient senderRecipient) throws StorageFailedException {
|
||||
log(content.getTimestamp(), "Story reply.");
|
||||
|
||||
|
@ -1492,6 +1579,7 @@ public final class MessageContentProcessor {
|
|||
System.currentTimeMillis(),
|
||||
StoryType.NONE,
|
||||
parentStoryId,
|
||||
false,
|
||||
-1,
|
||||
0,
|
||||
false,
|
||||
|
@ -1549,6 +1637,7 @@ public final class MessageContentProcessor {
|
|||
receivedTime,
|
||||
StoryType.NONE,
|
||||
null,
|
||||
false,
|
||||
-1,
|
||||
TimeUnit.SECONDS.toMillis(message.getExpiresInSeconds()),
|
||||
false,
|
||||
|
@ -1655,6 +1744,7 @@ public final class MessageContentProcessor {
|
|||
ThreadDatabase.DistributionTypes.DEFAULT,
|
||||
StoryType.NONE,
|
||||
null,
|
||||
false,
|
||||
quote.orElse(null),
|
||||
sharedContacts.orElse(Collections.emptyList()),
|
||||
previews.orElse(Collections.emptyList()),
|
||||
|
@ -1849,6 +1939,7 @@ public final class MessageContentProcessor {
|
|||
ThreadDatabase.DistributionTypes.DEFAULT,
|
||||
StoryType.NONE,
|
||||
null,
|
||||
false,
|
||||
null,
|
||||
Collections.emptyList(),
|
||||
Collections.emptyList(),
|
||||
|
|
|
@ -22,6 +22,7 @@ class IncomingMediaMessage(
|
|||
val isPushMessage: Boolean = false,
|
||||
val storyType: StoryType = StoryType.NONE,
|
||||
val parentStoryId: ParentStoryId? = null,
|
||||
val isStoryReaction: Boolean = false,
|
||||
val sentTimeMillis: Long,
|
||||
val serverTimeMillis: Long,
|
||||
val receivedTimeMillis: Long,
|
||||
|
@ -86,6 +87,7 @@ class IncomingMediaMessage(
|
|||
receivedTimeMillis: Long,
|
||||
storyType: StoryType,
|
||||
parentStoryId: ParentStoryId?,
|
||||
isStoryReaction: Boolean,
|
||||
subscriptionId: Int,
|
||||
expiresIn: Long,
|
||||
expirationUpdate: Boolean,
|
||||
|
@ -107,6 +109,7 @@ class IncomingMediaMessage(
|
|||
isPushMessage = true,
|
||||
storyType = storyType,
|
||||
parentStoryId = parentStoryId,
|
||||
isStoryReaction = isStoryReaction,
|
||||
sentTimeMillis = sentTimeMillis,
|
||||
serverTimeMillis = serverTimeMillis,
|
||||
receivedTimeMillis = receivedTimeMillis,
|
||||
|
|
|
@ -19,6 +19,7 @@ public class OutgoingExpirationUpdateMessage extends OutgoingSecureMediaMessage
|
|||
false,
|
||||
StoryType.NONE,
|
||||
null,
|
||||
false,
|
||||
null,
|
||||
Collections.emptyList(),
|
||||
Collections.emptyList(),
|
||||
|
|
|
@ -41,6 +41,7 @@ public final class OutgoingGroupUpdateMessage extends OutgoingSecureMediaMessage
|
|||
viewOnce,
|
||||
StoryType.NONE,
|
||||
null,
|
||||
false,
|
||||
quote,
|
||||
contacts,
|
||||
previews,
|
||||
|
|
|
@ -33,6 +33,7 @@ public class OutgoingMediaMessage {
|
|||
private final QuoteModel outgoingQuote;
|
||||
private final StoryType storyType;
|
||||
private final ParentStoryId parentStoryId;
|
||||
private final boolean isStoryReaction;
|
||||
|
||||
private final Set<NetworkFailure> networkFailures = new HashSet<>();
|
||||
private final Set<IdentityKeyMismatch> identityKeyMismatches = new HashSet<>();
|
||||
|
@ -50,6 +51,7 @@ public class OutgoingMediaMessage {
|
|||
int distributionType,
|
||||
@NonNull StoryType storyType,
|
||||
@Nullable ParentStoryId parentStoryId,
|
||||
boolean isStoryReaction,
|
||||
@Nullable QuoteModel outgoingQuote,
|
||||
@NonNull List<Contact> contacts,
|
||||
@NonNull List<LinkPreview> linkPreviews,
|
||||
|
@ -68,6 +70,7 @@ public class OutgoingMediaMessage {
|
|||
this.outgoingQuote = outgoingQuote;
|
||||
this.storyType = storyType;
|
||||
this.parentStoryId = parentStoryId;
|
||||
this.isStoryReaction = isStoryReaction;
|
||||
|
||||
this.contacts.addAll(contacts);
|
||||
this.linkPreviews.addAll(linkPreviews);
|
||||
|
@ -86,6 +89,7 @@ public class OutgoingMediaMessage {
|
|||
int distributionType,
|
||||
@NonNull StoryType storyType,
|
||||
@Nullable ParentStoryId parentStoryId,
|
||||
boolean isStoryReaction,
|
||||
@Nullable QuoteModel outgoingQuote,
|
||||
@NonNull List<Contact> contacts,
|
||||
@NonNull List<LinkPreview> linkPreviews,
|
||||
|
@ -101,6 +105,7 @@ public class OutgoingMediaMessage {
|
|||
distributionType,
|
||||
storyType,
|
||||
parentStoryId,
|
||||
isStoryReaction,
|
||||
outgoingQuote,
|
||||
contacts,
|
||||
linkPreviews,
|
||||
|
@ -121,6 +126,7 @@ public class OutgoingMediaMessage {
|
|||
this.outgoingQuote = that.outgoingQuote;
|
||||
this.storyType = that.storyType;
|
||||
this.parentStoryId = that.parentStoryId;
|
||||
this.isStoryReaction = that.isStoryReaction;
|
||||
|
||||
this.identityKeyMismatches.addAll(that.identityKeyMismatches);
|
||||
this.networkFailures.addAll(that.networkFailures);
|
||||
|
@ -141,6 +147,7 @@ public class OutgoingMediaMessage {
|
|||
distributionType,
|
||||
storyType,
|
||||
parentStoryId,
|
||||
isStoryReaction,
|
||||
outgoingQuote,
|
||||
contacts,
|
||||
linkPreviews,
|
||||
|
@ -202,6 +209,10 @@ public class OutgoingMediaMessage {
|
|||
return parentStoryId;
|
||||
}
|
||||
|
||||
public boolean isStoryReaction() {
|
||||
return isStoryReaction;
|
||||
}
|
||||
|
||||
public @Nullable QuoteModel getOutgoingQuote() {
|
||||
return outgoingQuote;
|
||||
}
|
||||
|
|
|
@ -25,12 +25,13 @@ public class OutgoingSecureMediaMessage extends OutgoingMediaMessage {
|
|||
boolean viewOnce,
|
||||
@NonNull StoryType storyType,
|
||||
@Nullable ParentStoryId parentStoryId,
|
||||
boolean isStoryReaction,
|
||||
@Nullable QuoteModel quote,
|
||||
@NonNull List<Contact> contacts,
|
||||
@NonNull List<LinkPreview> previews,
|
||||
@NonNull List<Mention> mentions)
|
||||
{
|
||||
super(recipient, body, attachments, sentTimeMillis, -1, expiresIn, viewOnce, distributionType, storyType, parentStoryId, quote, contacts, previews, mentions, Collections.emptySet(), Collections.emptySet());
|
||||
super(recipient, body, attachments, sentTimeMillis, -1, expiresIn, viewOnce, distributionType, storyType, parentStoryId, isStoryReaction, quote, contacts, previews, mentions, Collections.emptySet(), Collections.emptySet());
|
||||
}
|
||||
|
||||
public OutgoingSecureMediaMessage(OutgoingMediaMessage base) {
|
||||
|
@ -53,6 +54,7 @@ public class OutgoingSecureMediaMessage extends OutgoingMediaMessage {
|
|||
isViewOnce(),
|
||||
getStoryType(),
|
||||
getParentStoryId(),
|
||||
isStoryReaction(),
|
||||
getOutgoingQuote(),
|
||||
getSharedContacts(),
|
||||
getLinkPreviews(),
|
||||
|
@ -69,6 +71,7 @@ public class OutgoingSecureMediaMessage extends OutgoingMediaMessage {
|
|||
isViewOnce(),
|
||||
getStoryType(),
|
||||
getParentStoryId(),
|
||||
isStoryReaction(),
|
||||
getOutgoingQuote(),
|
||||
getSharedContacts(),
|
||||
getLinkPreviews(),
|
||||
|
|
|
@ -89,6 +89,7 @@ public class RemoteReplyReceiver extends BroadcastReceiver {
|
|||
0,
|
||||
StoryType.NONE,
|
||||
null,
|
||||
false,
|
||||
null,
|
||||
Collections.emptyList(),
|
||||
Collections.emptyList(),
|
||||
|
|
|
@ -198,6 +198,7 @@ public final class MultiShareSender {
|
|||
ThreadDatabase.DistributionTypes.DEFAULT,
|
||||
storyType,
|
||||
null,
|
||||
false,
|
||||
null,
|
||||
Collections.emptyList(),
|
||||
multiShareArgs.getLinkPreview() != null ? Collections.singletonList(multiShareArgs.getLinkPreview())
|
||||
|
@ -221,6 +222,7 @@ public final class MultiShareSender {
|
|||
ThreadDatabase.DistributionTypes.DEFAULT,
|
||||
StoryType.NONE,
|
||||
null,
|
||||
false,
|
||||
null,
|
||||
Collections.emptyList(),
|
||||
multiShareArgs.getLinkPreview() != null ? Collections.singletonList(multiShareArgs.getLinkPreview())
|
||||
|
|
|
@ -25,6 +25,8 @@ data class StoryTextPostModel(
|
|||
messageDigest.update(storyTextPost.toByteArray())
|
||||
}
|
||||
|
||||
val text: String = storyTextPost.body
|
||||
|
||||
companion object {
|
||||
@JvmStatic
|
||||
fun parseFrom(body: String): StoryTextPostModel {
|
||||
|
|
|
@ -3,13 +3,10 @@ package org.thoughtcrime.securesms.stories.viewer.reply.composer
|
|||
import android.animation.AnimatorSet
|
||||
import android.animation.ObjectAnimator
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.Dialog
|
||||
import android.content.Context
|
||||
import android.util.AttributeSet
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.constraintlayout.widget.ConstraintLayout
|
||||
import androidx.core.animation.addListener
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.components.emoji.EmojiImageView
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
|
@ -24,13 +21,12 @@ class StoryReactionBar @JvmOverloads constructor(
|
|||
|
||||
private var animatorSet: AnimatorSet? = null
|
||||
|
||||
private val emojiVerticalTranslation = context.resources.getDimensionPixelSize(R.dimen.reaction_scrubber_anim_start_translation_y)
|
||||
|
||||
init {
|
||||
inflate(context, R.layout.stories_reaction_bar, this)
|
||||
alpha = 0f
|
||||
setBackgroundResource(R.drawable.conversation_reaction_overlay_background)
|
||||
}
|
||||
|
||||
private val background: View = findViewById(R.id.conversation_reaction_scrubber_background)
|
||||
private val emojiViews: List<EmojiImageView> = listOf(
|
||||
findViewById(R.id.reaction_1),
|
||||
findViewById(R.id.reaction_2),
|
||||
|
@ -55,10 +51,14 @@ class StoryReactionBar @JvmOverloads constructor(
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
setOnClickListener {
|
||||
callback?.onTouchOutsideOfReactionBar()
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("Recycle")
|
||||
fun show() {
|
||||
fun animateIn() {
|
||||
visible = true
|
||||
|
||||
animatorSet?.cancel()
|
||||
|
@ -67,7 +67,7 @@ class StoryReactionBar @JvmOverloads constructor(
|
|||
playTogether(
|
||||
emojiViews.flatMap {
|
||||
listOf(ObjectAnimator.ofFloat(it, View.ALPHA, 1f), ObjectAnimator.ofFloat(it, View.TRANSLATION_Y, 0f))
|
||||
} + ObjectAnimator.ofFloat(background, View.ALPHA, 1f)
|
||||
} + ObjectAnimator.ofFloat(this@StoryReactionBar, View.ALPHA, 1f)
|
||||
)
|
||||
|
||||
start()
|
||||
|
@ -75,58 +75,16 @@ class StoryReactionBar @JvmOverloads constructor(
|
|||
}
|
||||
|
||||
private fun onEmojiSelected(emoji: String) {
|
||||
// TODO [stories] -- Animation / Haptics
|
||||
hide()
|
||||
callback?.onReactionSelected(emoji)
|
||||
}
|
||||
|
||||
private fun onOpenReactionPicker() {
|
||||
// TODO [stories] -- Animation / Haptics
|
||||
hide()
|
||||
callback?.onOpenReactionPicker()
|
||||
}
|
||||
|
||||
@SuppressLint("Recycle")
|
||||
private fun hide() {
|
||||
animatorSet?.cancel()
|
||||
animatorSet = AnimatorSet().apply {
|
||||
|
||||
playTogether(
|
||||
emojiViews.flatMap {
|
||||
listOf(
|
||||
ObjectAnimator.ofFloat(it, View.ALPHA, 0f),
|
||||
ObjectAnimator.ofFloat(it, View.TRANSLATION_Y, emojiVerticalTranslation.toFloat())
|
||||
)
|
||||
} + ObjectAnimator.ofFloat(background, View.ALPHA, 0f)
|
||||
)
|
||||
|
||||
addListener(onEnd = {
|
||||
visible = false
|
||||
})
|
||||
start()
|
||||
}
|
||||
}
|
||||
|
||||
interface Callback {
|
||||
fun onTouchOutsideOfReactionBar()
|
||||
fun onReactionSelected(emoji: String)
|
||||
fun onOpenReactionPicker()
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun installIntoBottomSheet(context: Context, dialog: Dialog): StoryReactionBar {
|
||||
val container: ViewGroup = dialog.findViewById(R.id.container)
|
||||
|
||||
val oldReactionBar: StoryReactionBar? = container.findViewById(R.id.reaction_bar)
|
||||
if (oldReactionBar != null) {
|
||||
return oldReactionBar
|
||||
}
|
||||
|
||||
val reactionBar = StoryReactionBar(context)
|
||||
|
||||
reactionBar.id = R.id.reaction_bar
|
||||
|
||||
container.addView(reactionBar)
|
||||
return reactionBar
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -30,11 +30,11 @@ class StoryReplyComposer @JvmOverloads constructor(
|
|||
|
||||
private val inputAwareLayout: InputAwareLayout
|
||||
private val quoteView: QuoteView
|
||||
private val reactionButton: View
|
||||
private val privacyChrome: TextView
|
||||
private val emojiDrawerToggle: EmojiToggle
|
||||
private val emojiDrawer: MediaKeyboard
|
||||
|
||||
val reactionButton: View
|
||||
val input: ComposeText
|
||||
|
||||
var isRequestingEmojiDrawer: Boolean = false
|
||||
|
|
|
@ -17,10 +17,12 @@ import org.thoughtcrime.securesms.keyboard.KeyboardPage
|
|||
import org.thoughtcrime.securesms.keyboard.KeyboardPagerViewModel
|
||||
import org.thoughtcrime.securesms.keyboard.emoji.EmojiKeyboardPageFragment
|
||||
import org.thoughtcrime.securesms.keyboard.emoji.search.EmojiSearchFragment
|
||||
import org.thoughtcrime.securesms.reactions.any.ReactWithAnyEmojiBottomSheetDialogFragment
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
import org.thoughtcrime.securesms.stories.viewer.page.StoryViewerPageViewModel
|
||||
import org.thoughtcrime.securesms.stories.viewer.reply.composer.StoryReactionBar
|
||||
import org.thoughtcrime.securesms.stories.viewer.reply.composer.StoryReplyComposer
|
||||
import org.thoughtcrime.securesms.util.FragmentDialogs.displayInDialogAboveAnchor
|
||||
import org.thoughtcrime.securesms.util.LifecycleDisposable
|
||||
import org.thoughtcrime.securesms.util.ViewUtil
|
||||
|
||||
|
@ -31,7 +33,8 @@ class StoryDirectReplyDialogFragment :
|
|||
KeyboardEntryDialogFragment(R.layout.stories_reply_to_story_fragment),
|
||||
EmojiKeyboardPageFragment.Callback,
|
||||
EmojiEventListener,
|
||||
EmojiSearchFragment.Callback {
|
||||
EmojiSearchFragment.Callback,
|
||||
ReactWithAnyEmojiBottomSheetDialogFragment.Callback {
|
||||
|
||||
private val lifecycleDisposable = LifecycleDisposable()
|
||||
|
||||
|
@ -49,7 +52,7 @@ class StoryDirectReplyDialogFragment :
|
|||
ownerProducer = { requireParentFragment() }
|
||||
)
|
||||
|
||||
private lateinit var input: StoryReplyComposer
|
||||
private lateinit var composer: StoryReplyComposer
|
||||
|
||||
private val storyId: Long
|
||||
get() = requireArguments().getLong(ARG_STORY_ID)
|
||||
|
@ -60,14 +63,12 @@ class StoryDirectReplyDialogFragment :
|
|||
override val withDim: Boolean = true
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
val reactionBar: StoryReactionBar = view.findViewById(R.id.reaction_bar)
|
||||
|
||||
lifecycleDisposable.bindTo(viewLifecycleOwner)
|
||||
|
||||
input = view.findViewById(R.id.input)
|
||||
input.callback = object : StoryReplyComposer.Callback {
|
||||
composer = view.findViewById(R.id.input)
|
||||
composer.callback = object : StoryReplyComposer.Callback {
|
||||
override fun onSendActionClicked() {
|
||||
lifecycleDisposable += viewModel.send(input.consumeInput().first)
|
||||
lifecycleDisposable += viewModel.sendReply(composer.consumeInput().first)
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe {
|
||||
Toast.makeText(requireContext(), R.string.StoryDirectReplyDialogFragment__reply_sent, Toast.LENGTH_LONG).show()
|
||||
|
@ -76,7 +77,26 @@ class StoryDirectReplyDialogFragment :
|
|||
}
|
||||
|
||||
override fun onPickReactionClicked() {
|
||||
reactionBar.show()
|
||||
displayInDialogAboveAnchor(composer.reactionButton, R.layout.stories_reaction_bar_layout) { dialog, view ->
|
||||
view.findViewById<StoryReactionBar>(R.id.reaction_bar).apply {
|
||||
callback = object : StoryReactionBar.Callback {
|
||||
override fun onTouchOutsideOfReactionBar() {
|
||||
dialog.dismiss()
|
||||
}
|
||||
|
||||
override fun onReactionSelected(emoji: String) {
|
||||
dialog.dismiss()
|
||||
sendReaction(emoji)
|
||||
}
|
||||
|
||||
override fun onOpenReactionPicker() {
|
||||
dialog.dismiss()
|
||||
ReactWithAnyEmojiBottomSheetDialogFragment.createForStory().show(childFragmentManager, null)
|
||||
}
|
||||
}
|
||||
animateIn()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onInitializeEmojiDrawer(mediaKeyboard: MediaKeyboard) {
|
||||
|
@ -89,11 +109,11 @@ class StoryDirectReplyDialogFragment :
|
|||
|
||||
viewModel.state.observe(viewLifecycleOwner) { state ->
|
||||
if (state.recipient != null) {
|
||||
input.displayPrivacyChrome(state.recipient)
|
||||
composer.displayPrivacyChrome(state.recipient)
|
||||
}
|
||||
|
||||
if (state.storyRecord != null) {
|
||||
input.setQuote(state.storyRecord as MediaMmsMessageRecord)
|
||||
composer.setQuote(state.storyRecord as MediaMmsMessageRecord)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -101,21 +121,21 @@ class StoryDirectReplyDialogFragment :
|
|||
override fun onResume() {
|
||||
super.onResume()
|
||||
|
||||
ViewUtil.focusAndShowKeyboard(input)
|
||||
ViewUtil.focusAndShowKeyboard(composer)
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
super.onPause()
|
||||
|
||||
ViewUtil.hideKeyboard(requireContext(), input)
|
||||
ViewUtil.hideKeyboard(requireContext(), composer)
|
||||
}
|
||||
|
||||
override fun openEmojiSearch() {
|
||||
input.openEmojiSearch()
|
||||
composer.openEmojiSearch()
|
||||
}
|
||||
|
||||
override fun onKeyboardHidden() {
|
||||
if (!input.isRequestingEmojiDrawer) {
|
||||
if (!composer.isRequestingEmojiDrawer) {
|
||||
super.onKeyboardHidden()
|
||||
}
|
||||
}
|
||||
|
@ -141,12 +161,28 @@ class StoryDirectReplyDialogFragment :
|
|||
}
|
||||
|
||||
override fun onEmojiSelected(emoji: String?) {
|
||||
input.onEmojiSelected(emoji)
|
||||
composer.onEmojiSelected(emoji)
|
||||
}
|
||||
|
||||
override fun closeEmojiSearch() {
|
||||
input.closeEmojiSearch()
|
||||
composer.closeEmojiSearch()
|
||||
}
|
||||
|
||||
override fun onKeyEvent(keyEvent: KeyEvent?) = Unit
|
||||
|
||||
override fun onReactWithAnyEmojiDialogDismissed() = Unit
|
||||
|
||||
override fun onReactWithAnyEmojiSelected(emoji: String) {
|
||||
sendReaction(emoji)
|
||||
}
|
||||
|
||||
private fun sendReaction(emoji: String) {
|
||||
lifecycleDisposable += viewModel.sendReaction(emoji)
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe {
|
||||
// TODO [alex] -- Reaction explosion animation instead of toast.
|
||||
Toast.makeText(requireContext(), R.string.StoryDirectReplyDialogFragment__reaction_sent, Toast.LENGTH_LONG).show()
|
||||
dismissAllowingStateLoss()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -25,7 +25,7 @@ class StoryDirectReplyRepository(context: Context) {
|
|||
}.subscribeOn(Schedulers.io())
|
||||
}
|
||||
|
||||
fun send(storyId: Long, groupDirectReplyRecipientId: RecipientId?, charSequence: CharSequence): Completable {
|
||||
fun send(storyId: Long, groupDirectReplyRecipientId: RecipientId?, charSequence: CharSequence, isReaction: Boolean): Completable {
|
||||
return Completable.create { emitter ->
|
||||
val message = SignalDatabase.mms.getMessageRecord(storyId) as MediaMmsMessageRecord
|
||||
val (recipient, threadId) = if (groupDirectReplyRecipientId == null) {
|
||||
|
@ -35,14 +35,10 @@ class StoryDirectReplyRepository(context: Context) {
|
|||
resolved to SignalDatabase.threads.getOrCreateThreadIdFor(resolved)
|
||||
}
|
||||
|
||||
val quoteAuthor: Recipient = if (message.isOutgoing) {
|
||||
Recipient.self()
|
||||
} else {
|
||||
message.individualRecipient
|
||||
}
|
||||
|
||||
if (!quoteAuthor.serviceId.isPresent || !quoteAuthor.e164.isPresent) {
|
||||
throw AssertionError("Bad quote author.")
|
||||
val quoteAuthor: Recipient = when {
|
||||
groupDirectReplyRecipientId != null -> message.recipient
|
||||
message.isOutgoing -> Recipient.self()
|
||||
else -> message.individualRecipient
|
||||
}
|
||||
|
||||
MessageSender.send(
|
||||
|
@ -58,6 +54,7 @@ class StoryDirectReplyRepository(context: Context) {
|
|||
0,
|
||||
StoryType.NONE,
|
||||
ParentStoryId.DirectReply(storyId),
|
||||
isReaction,
|
||||
QuoteModel(message.dateSent, quoteAuthor.id, message.body, false, message.slideDeck.asAttachments(), null),
|
||||
emptyList(),
|
||||
emptyList(),
|
||||
|
|
|
@ -33,8 +33,12 @@ class StoryDirectReplyViewModel(
|
|||
}
|
||||
}
|
||||
|
||||
fun send(charSequence: CharSequence): Completable {
|
||||
return repository.send(storyId, groupDirectReplyRecipientId, charSequence)
|
||||
fun sendReply(charSequence: CharSequence): Completable {
|
||||
return repository.send(storyId, groupDirectReplyRecipientId, charSequence, false)
|
||||
}
|
||||
|
||||
fun sendReaction(emoji: CharSequence): Completable {
|
||||
return repository.send(storyId, groupDirectReplyRecipientId, emoji, true)
|
||||
}
|
||||
|
||||
override fun onCleared() {
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
package org.thoughtcrime.securesms.stories.viewer.reply.group
|
||||
|
||||
import android.database.Cursor
|
||||
import org.signal.paging.PagedDataSource
|
||||
import org.thoughtcrime.securesms.conversation.ConversationMessage
|
||||
import org.thoughtcrime.securesms.database.MmsDatabase
|
||||
import org.thoughtcrime.securesms.database.MmsSmsColumns
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||
import org.thoughtcrime.securesms.database.model.MessageRecord
|
||||
import org.thoughtcrime.securesms.database.model.MmsMessageRecord
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
|
||||
|
@ -20,7 +20,7 @@ class StoryGroupReplyDataSource(private val parentStoryId: Long) : PagedDataSour
|
|||
cursor.moveToPosition(start - 1)
|
||||
val reader = MmsDatabase.Reader(cursor)
|
||||
while (cursor.moveToNext() && cursor.position < start + length) {
|
||||
results.add(readRowFromRecord(reader.current))
|
||||
results.add(readRowFromRecord(reader.current as MmsMessageRecord))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -35,21 +35,30 @@ class StoryGroupReplyDataSource(private val parentStoryId: Long) : PagedDataSour
|
|||
return data.key
|
||||
}
|
||||
|
||||
private fun readRowFromRecord(record: MessageRecord): StoryGroupReplyItemData {
|
||||
return readMessageRecordFromCursor(record)
|
||||
private fun readRowFromRecord(record: MmsMessageRecord): StoryGroupReplyItemData {
|
||||
return if (MmsSmsColumns.Types.isStoryReaction(record.type)) {
|
||||
readReactionFromRecord(record)
|
||||
} else {
|
||||
readTextFromRecord(record)
|
||||
}
|
||||
}
|
||||
|
||||
private fun readReactionFromCursor(cursor: Cursor): StoryGroupReplyItemData {
|
||||
throw NotImplementedError("TODO -- Need to know what the special story reaction record looks like.")
|
||||
}
|
||||
|
||||
private fun readMessageRecordFromCursor(messageRecord: MessageRecord): StoryGroupReplyItemData {
|
||||
private fun readReactionFromRecord(record: MmsMessageRecord): StoryGroupReplyItemData {
|
||||
return StoryGroupReplyItemData(
|
||||
key = StoryGroupReplyItemData.Key.Text(messageRecord.id),
|
||||
sender = if (messageRecord.isOutgoing) Recipient.self() else messageRecord.individualRecipient,
|
||||
sentAtMillis = messageRecord.dateSent,
|
||||
key = StoryGroupReplyItemData.Key.Reaction(record.id),
|
||||
sender = if (record.isOutgoing) Recipient.self() else record.individualRecipient,
|
||||
sentAtMillis = record.dateSent,
|
||||
replyBody = StoryGroupReplyItemData.ReplyBody.Reaction(record.body)
|
||||
)
|
||||
}
|
||||
|
||||
private fun readTextFromRecord(record: MmsMessageRecord): StoryGroupReplyItemData {
|
||||
return StoryGroupReplyItemData(
|
||||
key = StoryGroupReplyItemData.Key.Text(record.id),
|
||||
sender = if (record.isOutgoing) Recipient.self() else record.individualRecipient,
|
||||
sentAtMillis = record.dateSent,
|
||||
replyBody = StoryGroupReplyItemData.ReplyBody.Text(
|
||||
ConversationMessage.ConversationMessageFactory.createWithUnresolvedData(ApplicationDependencies.getApplication(), messageRecord)
|
||||
ConversationMessage.ConversationMessageFactory.createWithUnresolvedData(ApplicationDependencies.getApplication(), record)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
|
|
@ -32,6 +32,7 @@ import org.thoughtcrime.securesms.stories.viewer.reply.StoryViewsAndRepliesPager
|
|||
import org.thoughtcrime.securesms.stories.viewer.reply.composer.StoryReactionBar
|
||||
import org.thoughtcrime.securesms.stories.viewer.reply.composer.StoryReplyComposer
|
||||
import org.thoughtcrime.securesms.util.DeleteDialog
|
||||
import org.thoughtcrime.securesms.util.FragmentDialogs.displayInDialogAboveAnchor
|
||||
import org.thoughtcrime.securesms.util.LifecycleDisposable
|
||||
import org.thoughtcrime.securesms.util.Projection
|
||||
import org.thoughtcrime.securesms.util.ServiceUtil
|
||||
|
@ -49,7 +50,6 @@ class StoryGroupReplyFragment :
|
|||
StoryViewsAndRepliesPagerChild,
|
||||
BottomSheetBehaviorDelegate,
|
||||
StoryReplyComposer.Callback,
|
||||
StoryReactionBar.Callback,
|
||||
EmojiKeyboardCallback,
|
||||
ReactWithAnyEmojiBottomSheetDialogFragment.Callback {
|
||||
|
||||
|
@ -79,12 +79,10 @@ class StoryGroupReplyFragment :
|
|||
|
||||
private lateinit var recyclerView: RecyclerView
|
||||
private lateinit var composer: StoryReplyComposer
|
||||
private lateinit var reactionBar: StoryReactionBar
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
recyclerView = view.findViewById(R.id.recycler)
|
||||
composer = view.findViewById(R.id.composer)
|
||||
reactionBar = view.findViewById(R.id.reaction_bar)
|
||||
|
||||
lifecycleDisposable.bindTo(viewLifecycleOwner)
|
||||
|
||||
|
@ -98,7 +96,6 @@ class StoryGroupReplyFragment :
|
|||
StoryGroupReplyItem.register(adapter)
|
||||
|
||||
composer.callback = this
|
||||
reactionBar.callback = this
|
||||
|
||||
onPageSelected(findListener<StoryViewsAndRepliesPagerParent>()?.selectedChild ?: StoryViewsAndRepliesPagerParent.Child.REPLIES)
|
||||
|
||||
|
@ -189,7 +186,6 @@ class StoryGroupReplyFragment :
|
|||
val inputProjection = Projection.relativeToViewRoot(composer, null)
|
||||
val parentProjection = Projection.relativeToViewRoot(bottomSheet.parent as ViewGroup, null)
|
||||
composer.translationY = (parentProjection.height + parentProjection.y - (inputProjection.y + inputProjection.height))
|
||||
reactionBar.translationY = composer.translationY
|
||||
inputProjection.release()
|
||||
parentProjection.release()
|
||||
}
|
||||
|
@ -204,23 +200,38 @@ class StoryGroupReplyFragment :
|
|||
}
|
||||
|
||||
override fun onPickReactionClicked() {
|
||||
reactionBar.show()
|
||||
displayInDialogAboveAnchor(composer.reactionButton, R.layout.stories_reaction_bar_layout) { dialog, view ->
|
||||
view.findViewById<StoryReactionBar>(R.id.reaction_bar).apply {
|
||||
callback = object : StoryReactionBar.Callback {
|
||||
override fun onTouchOutsideOfReactionBar() {
|
||||
dialog.dismiss()
|
||||
}
|
||||
|
||||
override fun onReactionSelected(emoji: String) {
|
||||
dialog.dismiss()
|
||||
sendReaction(emoji)
|
||||
}
|
||||
|
||||
override fun onOpenReactionPicker() {
|
||||
dialog.dismiss()
|
||||
ReactWithAnyEmojiBottomSheetDialogFragment.createForStory().show(childFragmentManager, null)
|
||||
}
|
||||
}
|
||||
animateIn()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onEmojiSelected(emoji: String?) {
|
||||
composer.onEmojiSelected(emoji)
|
||||
}
|
||||
|
||||
override fun onReactionSelected(emoji: String) {
|
||||
private fun sendReaction(emoji: String) {
|
||||
lifecycleDisposable += StoryGroupReplySender.sendReaction(requireContext(), storyId, emoji).subscribe()
|
||||
}
|
||||
|
||||
override fun onKeyEvent(keyEvent: KeyEvent?) = Unit
|
||||
|
||||
override fun onOpenReactionPicker() {
|
||||
ReactWithAnyEmojiBottomSheetDialogFragment.createForStory().show(childFragmentManager, null)
|
||||
}
|
||||
|
||||
override fun onInitializeEmojiDrawer(mediaKeyboard: MediaKeyboard) {
|
||||
keyboardPagerViewModel.setOnlyPage(KeyboardPage.EMOJI)
|
||||
mediaKeyboard.setFragmentManager(childFragmentManager)
|
||||
|
@ -238,7 +249,7 @@ class StoryGroupReplyFragment :
|
|||
}
|
||||
|
||||
override fun onReactWithAnyEmojiSelected(emoji: String) {
|
||||
onReactionSelected(emoji)
|
||||
sendReaction(emoji)
|
||||
}
|
||||
|
||||
override fun onHeightChanged(height: Int) {
|
||||
|
|
|
@ -14,7 +14,16 @@ import org.thoughtcrime.securesms.sms.MessageSender
|
|||
* Stateless message sender for Story Group replies and reactions.
|
||||
*/
|
||||
object StoryGroupReplySender {
|
||||
|
||||
fun sendReply(context: Context, storyId: Long, body: CharSequence, mentions: List<Mention>): Completable {
|
||||
return sendInternal(context, storyId, body, mentions, false)
|
||||
}
|
||||
|
||||
fun sendReaction(context: Context, storyId: Long, emoji: String): Completable {
|
||||
return sendInternal(context, storyId, emoji, emptyList(), true)
|
||||
}
|
||||
|
||||
private fun sendInternal(context: Context, storyId: Long, body: CharSequence, mentions: List<Mention>, isReaction: Boolean): Completable {
|
||||
return Completable.create {
|
||||
|
||||
val message = SignalDatabase.mms.getMessageRecord(storyId)
|
||||
|
@ -33,6 +42,7 @@ object StoryGroupReplySender {
|
|||
0,
|
||||
StoryType.NONE,
|
||||
ParentStoryId.GroupReply(message.id),
|
||||
isReaction,
|
||||
null,
|
||||
emptyList(),
|
||||
emptyList(),
|
||||
|
@ -48,9 +58,4 @@ object StoryGroupReplySender {
|
|||
}
|
||||
}.subscribeOn(Schedulers.io())
|
||||
}
|
||||
|
||||
fun sendReaction(context: Context, storyId: Long, emoji: String): Completable {
|
||||
// TODO [stories]
|
||||
return Completable.complete()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,13 +1,10 @@
|
|||
package org.thoughtcrime.securesms.stories.viewer.text
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.graphics.Color
|
||||
import android.graphics.drawable.ColorDrawable
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.widget.TextView
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.viewModels
|
||||
import org.signal.core.util.DimensionUnit
|
||||
|
@ -17,9 +14,8 @@ import org.thoughtcrime.securesms.mediapreview.MediaPreviewFragment
|
|||
import org.thoughtcrime.securesms.stories.StoryTextPostView
|
||||
import org.thoughtcrime.securesms.stories.viewer.page.StoryPost
|
||||
import org.thoughtcrime.securesms.util.CommunicationActions
|
||||
import org.thoughtcrime.securesms.util.Projection
|
||||
import org.thoughtcrime.securesms.util.FragmentDialogs.displayInDialogAboveAnchor
|
||||
import org.thoughtcrime.securesms.util.fragments.requireListener
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
class StoryTextPostPreviewFragment : Fragment(R.layout.stories_text_post_preview_fragment) {
|
||||
|
||||
|
@ -85,29 +81,7 @@ class StoryTextPostPreviewFragment : Fragment(R.layout.stories_text_post_preview
|
|||
|
||||
contentView.layout(0, 0, contentView.measuredWidth, contentView.measuredHeight)
|
||||
|
||||
val alertDialog = AlertDialog.Builder(requireContext())
|
||||
.setView(contentView)
|
||||
.create()
|
||||
|
||||
alertDialog.window!!.attributes = alertDialog.window!!.attributes.apply {
|
||||
val rootProjection = Projection.relativeToViewRoot(view.rootView, null)
|
||||
val viewProjection = Projection.relativeToViewRoot(view, null).translateY(view.translationY)
|
||||
|
||||
val dialogBottom = rootProjection.height / 2f + contentView.measuredHeight / 2f
|
||||
val linkPreviewViewTop = viewProjection.y
|
||||
|
||||
rootProjection.release()
|
||||
viewProjection.release()
|
||||
|
||||
val delta = linkPreviewViewTop - dialogBottom
|
||||
this.y = delta.roundToInt()
|
||||
}
|
||||
alertDialog.window!!.setDimAmount(0f)
|
||||
alertDialog.window!!.setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT))
|
||||
alertDialog.setOnDismissListener {
|
||||
requireListener<Callback>().setIsDisplayingLinkPreviewTooltip(false)
|
||||
}
|
||||
alertDialog.show()
|
||||
displayInDialogAboveAnchor(view, contentView, windowDim = 0f)
|
||||
}
|
||||
|
||||
interface Callback {
|
||||
|
|
|
@ -0,0 +1,72 @@
|
|||
package org.thoughtcrime.securesms.util
|
||||
|
||||
import android.content.DialogInterface
|
||||
import android.graphics.Color
|
||||
import android.graphics.drawable.ColorDrawable
|
||||
import android.view.Gravity
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.annotation.LayoutRes
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.fragment.app.Fragment
|
||||
import org.thoughtcrime.securesms.stories.viewer.text.StoryTextPostPreviewFragment
|
||||
import org.thoughtcrime.securesms.util.fragments.requireListener
|
||||
|
||||
/**
|
||||
* Helper functions to display custom views in AlertDialogs anchored to the top of the specified view.
|
||||
*/
|
||||
object FragmentDialogs {
|
||||
|
||||
fun Fragment.displayInDialogAboveAnchor(
|
||||
anchorView: View,
|
||||
@LayoutRes contentLayoutId: Int,
|
||||
windowDim: Float = -1f,
|
||||
onShow: (DialogInterface, View) -> Unit = { _, _ -> }
|
||||
): DialogInterface {
|
||||
val contentView = LayoutInflater.from(anchorView.context).inflate(contentLayoutId, requireView() as ViewGroup, false)
|
||||
|
||||
contentView.measure(
|
||||
View.MeasureSpec.makeMeasureSpec(contentView.layoutParams.width, View.MeasureSpec.EXACTLY),
|
||||
View.MeasureSpec.makeMeasureSpec(contentView.layoutParams.height, View.MeasureSpec.EXACTLY)
|
||||
)
|
||||
|
||||
contentView.layout(0, 0, contentView.measuredWidth, contentView.measuredHeight)
|
||||
|
||||
return displayInDialogAboveAnchor(anchorView, contentView, windowDim, onShow)
|
||||
}
|
||||
|
||||
fun Fragment.displayInDialogAboveAnchor(
|
||||
anchorView: View,
|
||||
contentView: View,
|
||||
windowDim: Float = -1f,
|
||||
onShow: (DialogInterface, View) -> Unit = { _, _ -> }
|
||||
): DialogInterface {
|
||||
val alertDialog = AlertDialog.Builder(requireContext())
|
||||
.setView(contentView)
|
||||
.create()
|
||||
|
||||
alertDialog.window!!.attributes = alertDialog.window!!.attributes.apply {
|
||||
val viewProjection = Projection.relativeToViewRoot(anchorView, null).translateY(anchorView.translationY)
|
||||
this.y = (viewProjection.y - contentView.height).toInt()
|
||||
this.gravity = Gravity.TOP
|
||||
|
||||
viewProjection.release()
|
||||
}
|
||||
|
||||
if (windowDim >= 0f) {
|
||||
alertDialog.window!!.setDimAmount(windowDim)
|
||||
}
|
||||
|
||||
alertDialog.window!!.setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT))
|
||||
alertDialog.setOnDismissListener {
|
||||
requireListener<StoryTextPostPreviewFragment.Callback>().setIsDisplayingLinkPreviewTooltip(false)
|
||||
}
|
||||
|
||||
alertDialog.setOnShowListener { onShow(alertDialog, contentView) }
|
||||
|
||||
alertDialog.show()
|
||||
|
||||
return alertDialog
|
||||
}
|
||||
}
|
|
@ -4,6 +4,7 @@ package org.thoughtcrime.securesms.util
|
|||
|
||||
import android.content.Context
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.database.MmsSmsColumns
|
||||
import org.thoughtcrime.securesms.database.model.MediaMmsMessageRecord
|
||||
import org.thoughtcrime.securesms.database.model.MessageRecord
|
||||
import org.thoughtcrime.securesms.database.model.MmsMessageRecord
|
||||
|
@ -36,6 +37,9 @@ fun MessageRecord.isCaptionlessMms(context: Context): Boolean =
|
|||
fun MessageRecord.hasThumbnail(): Boolean =
|
||||
isMms && (this as MmsMessageRecord).slideDeck.thumbnailSlide != null
|
||||
|
||||
fun MessageRecord.isStoryReaction(): Boolean =
|
||||
isMms && MmsSmsColumns.Types.isStoryReaction((this as MmsMessageRecord).type)
|
||||
|
||||
fun MessageRecord.isBorderless(context: Context): Boolean {
|
||||
return isCaptionlessMms(context) &&
|
||||
hasThumbnail() &&
|
||||
|
|
|
@ -235,6 +235,18 @@
|
|||
|
||||
</org.thoughtcrime.securesms.conversation.ConversationItemBodyBubble>
|
||||
|
||||
<org.thoughtcrime.securesms.components.emoji.EmojiImageView
|
||||
android:id="@+id/story_reaction_emoji"
|
||||
android:layout_width="32dp"
|
||||
android:layout_height="32dp"
|
||||
android:layout_alignEnd="@id/body_bubble"
|
||||
android:layout_alignBottom="@id/body_bubble"
|
||||
android:layout_marginEnd="32dp"
|
||||
android:layout_marginBottom="24dp"
|
||||
android:visibility="gone"
|
||||
tools:src="@drawable/ic_emoji"
|
||||
tools:visibility="visible" />
|
||||
|
||||
<org.thoughtcrime.securesms.components.AlertView
|
||||
android:id="@+id/indicators_parent"
|
||||
android:layout_width="wrap_content"
|
||||
|
|
|
@ -5,13 +5,13 @@
|
|||
android:id="@+id/conversation_item"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingEnd="@dimen/conversation_individual_right_gutter"
|
||||
android:clipChildren="false"
|
||||
android:clipToPadding="false"
|
||||
android:focusable="true"
|
||||
android:nextFocusLeft="@id/container"
|
||||
android:nextFocusRight="@id/embedded_text_editor"
|
||||
android:orientation="horizontal">
|
||||
android:orientation="horizontal"
|
||||
android:paddingEnd="@dimen/conversation_individual_right_gutter">
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/reply_icon_wrapper"
|
||||
|
@ -26,9 +26,9 @@
|
|||
android:id="@+id/reply_icon"
|
||||
android:layout_width="@dimen/conversation_item_reply_size"
|
||||
android:layout_height="@dimen/conversation_item_reply_size"
|
||||
android:layout_gravity="center"
|
||||
android:padding="9dp"
|
||||
android:tint="@color/signal_icon_tint_primary"
|
||||
android:layout_gravity="center"
|
||||
app:srcCompat="@drawable/ic_reply_24" />
|
||||
|
||||
</FrameLayout>
|
||||
|
@ -46,6 +46,31 @@
|
|||
android:orientation="vertical"
|
||||
tools:backgroundTint="@color/core_grey_05">
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/story_reacted_label_holder"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="7dp"
|
||||
android:layout_marginBottom="-3dp"
|
||||
android:orientation="horizontal"
|
||||
android:paddingStart="@dimen/message_bubble_horizontal_padding"
|
||||
android:paddingEnd="@dimen/message_bubble_horizontal_padding"
|
||||
android:visibility="gone"
|
||||
tools:visibility="visible">
|
||||
|
||||
<org.thoughtcrime.securesms.components.emoji.EmojiTextView
|
||||
android:id="@+id/story_reacted_label"
|
||||
style="@style/TextAppearance.Signal.Subtitle"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginEnd="4sp"
|
||||
android:ellipsize="end"
|
||||
android:maxLines="1"
|
||||
android:textColor="@color/signal_text_secondary"
|
||||
tools:text="Reacted to your story" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<org.thoughtcrime.securesms.components.QuoteView
|
||||
android:id="@+id/quote_view"
|
||||
android:layout_width="wrap_content"
|
||||
|
@ -127,8 +152,8 @@
|
|||
android:textColor="@color/conversation_item_sent_text_primary_color"
|
||||
android:textColorLink="@color/conversation_item_sent_text_primary_color"
|
||||
app:emoji_maxLength="1000"
|
||||
app:scaleEmojis="true"
|
||||
app:measureLastLine="true"
|
||||
app:scaleEmojis="true"
|
||||
tools:text="Mango pickle lorem ipsum" />
|
||||
|
||||
<org.thoughtcrime.securesms.components.ConversationItemFooter
|
||||
|
@ -167,6 +192,18 @@
|
|||
|
||||
</org.thoughtcrime.securesms.conversation.ConversationItemBodyBubble>
|
||||
|
||||
<org.thoughtcrime.securesms.components.emoji.EmojiImageView
|
||||
android:id="@+id/story_reaction_emoji"
|
||||
android:layout_width="32dp"
|
||||
android:layout_height="32dp"
|
||||
android:layout_alignEnd="@id/body_bubble"
|
||||
android:layout_alignBottom="@id/body_bubble"
|
||||
android:layout_marginEnd="32dp"
|
||||
android:layout_marginBottom="24dp"
|
||||
android:visibility="gone"
|
||||
tools:src="@drawable/ic_emoji"
|
||||
tools:visibility="visible" />
|
||||
|
||||
<org.thoughtcrime.securesms.components.AlertView
|
||||
android:id="@+id/indicators_parent"
|
||||
android:layout_width="wrap_content"
|
||||
|
@ -174,8 +211,8 @@
|
|||
android:layout_alignParentEnd="true"
|
||||
android:layout_alignParentBottom="true"
|
||||
android:layout_marginStart="8dp"
|
||||
android:padding="8dp"
|
||||
android:orientation="vertical" />
|
||||
android:orientation="vertical"
|
||||
android:padding="8dp" />
|
||||
|
||||
<org.thoughtcrime.securesms.reactions.ReactionsConversationView
|
||||
android:id="@+id/reactions_view"
|
||||
|
|
|
@ -36,10 +36,4 @@
|
|||
android:layout_height="wrap_content"
|
||||
app:layout_constraintBottom_toBottomOf="parent" />
|
||||
|
||||
<org.thoughtcrime.securesms.stories.viewer.reply.composer.StoryReactionBar
|
||||
android:id="@+id/reaction_bar"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content" />
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
@ -2,135 +2,106 @@
|
|||
<merge xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
android:layout_width="@dimen/reaction_scrubber_width"
|
||||
android:layout_height="?actionBarSize"
|
||||
tools:parentTag="androidx.constraintlayout.widget.ConstraintLayout">
|
||||
|
||||
<View
|
||||
android:id="@+id/conversation_reaction_scrubber_background"
|
||||
android:layout_width="@dimen/reaction_scrubber_width"
|
||||
android:layout_height="?attr/actionBarSize"
|
||||
android:layout_marginTop="40dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:layout_marginBottom="56dp"
|
||||
<org.thoughtcrime.securesms.components.emoji.EmojiImageView
|
||||
android:id="@+id/reaction_1"
|
||||
android:layout_width="32dp"
|
||||
android:layout_height="48dp"
|
||||
android:alpha="0"
|
||||
android:background="@drawable/conversation_reaction_overlay_background"
|
||||
android:elevation="4dp"
|
||||
android:translationY="@dimen/reaction_scrubber_anim_start_translation_y"
|
||||
app:forceJumbo="true"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toStartOf="@id/reaction_2"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
tools:alpha="1"
|
||||
tools:translationY="0dp" />
|
||||
|
||||
<org.thoughtcrime.securesms.components.emoji.EmojiImageView
|
||||
android:id="@+id/reaction_2"
|
||||
android:layout_width="32dp"
|
||||
android:layout_height="48dp"
|
||||
android:alpha="0"
|
||||
android:translationY="@dimen/reaction_scrubber_anim_start_translation_y"
|
||||
app:forceJumbo="true"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toStartOf="@id/reaction_3"
|
||||
app:layout_constraintStart_toEndOf="@id/reaction_1"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
tools:alpha="1"
|
||||
tools:translationY="0dp" />
|
||||
|
||||
<org.thoughtcrime.securesms.components.emoji.EmojiImageView
|
||||
android:id="@+id/reaction_3"
|
||||
android:layout_width="32dp"
|
||||
android:layout_height="48dp"
|
||||
android:alpha="0"
|
||||
android:translationY="@dimen/reaction_scrubber_anim_start_translation_y"
|
||||
app:forceJumbo="true"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toStartOf="@id/reaction_4"
|
||||
app:layout_constraintStart_toEndOf="@id/reaction_2"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
tools:alpha="1"
|
||||
tools:translationY="0dp" />
|
||||
|
||||
<org.thoughtcrime.securesms.components.emoji.EmojiImageView
|
||||
android:id="@+id/reaction_4"
|
||||
android:layout_width="32dp"
|
||||
android:layout_height="48dp"
|
||||
android:alpha="0"
|
||||
android:translationY="@dimen/reaction_scrubber_anim_start_translation_y"
|
||||
app:forceJumbo="true"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toStartOf="@id/reaction_5"
|
||||
app:layout_constraintStart_toEndOf="@id/reaction_3"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
tools:alpha="1"
|
||||
tools:translationY="0dp" />
|
||||
|
||||
<org.thoughtcrime.securesms.components.emoji.EmojiImageView
|
||||
android:id="@+id/reaction_5"
|
||||
android:layout_width="32dp"
|
||||
android:layout_height="48dp"
|
||||
android:alpha="0"
|
||||
android:translationY="@dimen/reaction_scrubber_anim_start_translation_y"
|
||||
app:forceJumbo="true"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toStartOf="@id/reaction_6"
|
||||
app:layout_constraintStart_toEndOf="@id/reaction_4"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
tools:alpha="1"
|
||||
tools:translationY="0dp" />
|
||||
|
||||
<org.thoughtcrime.securesms.components.emoji.EmojiImageView
|
||||
android:id="@+id/reaction_6"
|
||||
android:layout_width="32dp"
|
||||
android:layout_height="48dp"
|
||||
android:alpha="0"
|
||||
android:translationY="@dimen/reaction_scrubber_anim_start_translation_y"
|
||||
app:forceJumbo="true"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toStartOf="@id/reaction_7"
|
||||
app:layout_constraintStart_toEndOf="@id/reaction_5"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
tools:alpha="1"
|
||||
tools:translationY="0dp" />
|
||||
|
||||
<org.thoughtcrime.securesms.components.emoji.EmojiImageView
|
||||
android:id="@+id/reaction_7"
|
||||
android:layout_width="32dp"
|
||||
android:layout_height="48dp"
|
||||
android:alpha="0"
|
||||
android:translationY="@dimen/reaction_scrubber_anim_start_translation_y"
|
||||
app:forceJumbo="true"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
tools:alpha="1" />
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:id="@+id/conversation_reaction_scrubber_foreground"
|
||||
android:layout_width="@dimen/reaction_scrubber_width"
|
||||
android:layout_height="@dimen/conversation_reaction_scrubber_height"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:layout_marginBottom="16dp"
|
||||
android:clipToPadding="false"
|
||||
android:elevation="4dp"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent">
|
||||
|
||||
<org.thoughtcrime.securesms.components.emoji.EmojiImageView
|
||||
android:id="@+id/reaction_1"
|
||||
android:layout_width="32dp"
|
||||
android:layout_height="48dp"
|
||||
android:alpha="0"
|
||||
android:translationY="@dimen/reaction_scrubber_anim_start_translation_y"
|
||||
app:forceJumbo="true"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toStartOf="@id/reaction_2"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
tools:alpha="1"
|
||||
tools:translationY="0dp" />
|
||||
|
||||
<org.thoughtcrime.securesms.components.emoji.EmojiImageView
|
||||
android:id="@+id/reaction_2"
|
||||
android:layout_width="32dp"
|
||||
android:layout_height="48dp"
|
||||
android:alpha="0"
|
||||
android:translationY="@dimen/reaction_scrubber_anim_start_translation_y"
|
||||
app:forceJumbo="true"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toStartOf="@id/reaction_3"
|
||||
app:layout_constraintStart_toEndOf="@id/reaction_1"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
tools:alpha="1"
|
||||
tools:translationY="0dp" />
|
||||
|
||||
<org.thoughtcrime.securesms.components.emoji.EmojiImageView
|
||||
android:id="@+id/reaction_3"
|
||||
android:layout_width="32dp"
|
||||
android:layout_height="48dp"
|
||||
android:alpha="0"
|
||||
android:translationY="@dimen/reaction_scrubber_anim_start_translation_y"
|
||||
app:forceJumbo="true"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toStartOf="@id/reaction_4"
|
||||
app:layout_constraintStart_toEndOf="@id/reaction_2"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
tools:alpha="1"
|
||||
tools:translationY="0dp" />
|
||||
|
||||
<org.thoughtcrime.securesms.components.emoji.EmojiImageView
|
||||
android:id="@+id/reaction_4"
|
||||
android:layout_width="32dp"
|
||||
android:layout_height="48dp"
|
||||
android:alpha="0"
|
||||
android:translationY="@dimen/reaction_scrubber_anim_start_translation_y"
|
||||
app:forceJumbo="true"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toStartOf="@id/reaction_5"
|
||||
app:layout_constraintStart_toEndOf="@id/reaction_3"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
tools:alpha="1"
|
||||
tools:translationY="0dp" />
|
||||
|
||||
<org.thoughtcrime.securesms.components.emoji.EmojiImageView
|
||||
android:id="@+id/reaction_5"
|
||||
android:layout_width="32dp"
|
||||
android:layout_height="48dp"
|
||||
android:alpha="0"
|
||||
android:translationY="@dimen/reaction_scrubber_anim_start_translation_y"
|
||||
app:forceJumbo="true"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toStartOf="@id/reaction_6"
|
||||
app:layout_constraintStart_toEndOf="@id/reaction_4"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
tools:alpha="1"
|
||||
tools:translationY="0dp" />
|
||||
|
||||
<org.thoughtcrime.securesms.components.emoji.EmojiImageView
|
||||
android:id="@+id/reaction_6"
|
||||
android:layout_width="32dp"
|
||||
android:layout_height="48dp"
|
||||
android:alpha="0"
|
||||
android:translationY="@dimen/reaction_scrubber_anim_start_translation_y"
|
||||
app:forceJumbo="true"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toStartOf="@id/reaction_7"
|
||||
app:layout_constraintStart_toEndOf="@id/reaction_5"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
tools:alpha="1"
|
||||
tools:translationY="0dp" />
|
||||
|
||||
<org.thoughtcrime.securesms.components.emoji.EmojiImageView
|
||||
android:id="@+id/reaction_7"
|
||||
android:layout_width="32dp"
|
||||
android:layout_height="48dp"
|
||||
android:alpha="0"
|
||||
android:translationY="@dimen/reaction_scrubber_anim_start_translation_y"
|
||||
app:forceJumbo="true"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@id/reaction_6"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
tools:alpha="1"
|
||||
tools:translationY="0dp" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
app:layout_constraintStart_toEndOf="@id/reaction_6"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
tools:alpha="1"
|
||||
tools:translationY="0dp" />
|
||||
|
||||
</merge>
|
|
@ -0,0 +1,13 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="?actionBarSize"
|
||||
android:maxHeight="?actionBarSize">
|
||||
|
||||
<org.thoughtcrime.securesms.stories.viewer.reply.composer.StoryReactionBar
|
||||
android:id="@+id/reaction_bar"
|
||||
android:layout_width="@dimen/reaction_scrubber_width"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_gravity="end"
|
||||
android:layout_marginEnd="16dp" />
|
||||
</FrameLayout>
|
|
@ -49,7 +49,7 @@
|
|||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:message_type="story_reply"
|
||||
app:message_type="story_reply_preview"
|
||||
app:quote_colorPrimary="@color/signal_text_primary"
|
||||
app:quote_colorSecondary="@color/signal_text_primary"
|
||||
tools:visibility="visible" />
|
||||
|
@ -91,8 +91,7 @@
|
|||
android:layout_width="48dp"
|
||||
android:layout_height="48dp"
|
||||
android:layout_marginEnd="6dp"
|
||||
android:layout_marginBottom="8dp"
|
||||
app:layout_constraintBottom_toTopOf="@id/emoji_drawer_stub"
|
||||
app:layout_constraintBottom_toBottomOf="@id/bubble"
|
||||
app:layout_constraintEnd_toEndOf="parent">
|
||||
|
||||
<ImageView
|
||||
|
@ -103,7 +102,8 @@
|
|||
android:background="?attr/selectableItemBackgroundBorderless"
|
||||
android:contentDescription="@string/StoryReplyComposer__react_to_this_story"
|
||||
android:scaleType="centerInside"
|
||||
app:srcCompat="@drawable/ic_add_reaction_outline_24" />
|
||||
app:srcCompat="@drawable/ic_add_reaction_outline_24"
|
||||
app:tint="@color/signal_icon_tint_primary" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/reply"
|
||||
|
|
|
@ -8,9 +8,4 @@
|
|||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent" />
|
||||
|
||||
<org.thoughtcrime.securesms.stories.viewer.reply.composer.StoryReactionBar
|
||||
android:id="@+id/reaction_bar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent" />
|
||||
|
||||
</org.thoughtcrime.securesms.components.InputAwareLayout>
|
|
@ -176,6 +176,7 @@
|
|||
<enum name="outgoing" value="1" />
|
||||
<enum name="incoming" value="2" />
|
||||
<enum name="story_reply" value="3" />
|
||||
<enum name="story_reply_preview" value="4" />
|
||||
</attr>
|
||||
<attr name="quote_colorPrimary" format="color" />
|
||||
<attr name="quote_colorSecondary" format="color" />
|
||||
|
|
|
@ -4606,6 +4606,8 @@
|
|||
<string name="StoryViewerPageFragment__see_more">… See More</string>
|
||||
<!-- Displayed in toast after sending a direct reply -->
|
||||
<string name="StoryDirectReplyDialogFragment__reply_sent">Reply sent</string>
|
||||
<!-- Displayed in toast after sending a direct reaction -->
|
||||
<string name="StoryDirectReplyDialogFragment__reaction_sent">Reaction sent</string>
|
||||
<!-- Displayed in the viewer when a story is no longer available -->
|
||||
<string name="StorySlateView__this_story_is_no_longer_available">This story is no longer available.</string>
|
||||
<!-- Displayed in the viewer when the network is not available -->
|
||||
|
@ -4622,6 +4624,11 @@
|
|||
<!-- Label for a button in a notification at the bottom of the chat list to turn off censorship circumvention -->
|
||||
<string name="TurnOffCircumventionMegaphone_turn_off">Turn off</string>
|
||||
|
||||
<!-- Conversation Item label for reactions to a story -->
|
||||
<string name="ConversationItem__s_dot_story">%1$s · Story</string>
|
||||
<!-- Conversation Item label for reactions to an unavailable story -->
|
||||
<string name="ConversationItem__reacted_to_a_story">Reacted to a story</string>
|
||||
|
||||
<!-- endregion -->
|
||||
<!-- Content description for expand contacts chevron -->
|
||||
<string name="ExpandModel__view_more">View more</string>
|
||||
|
|
|
@ -49,6 +49,8 @@ import org.thoughtcrime.securesms.database.MmsSmsColumns.Types.OUTGOING_VIDEO_CA
|
|||
import org.thoughtcrime.securesms.database.MmsSmsColumns.Types.PROFILE_CHANGE_TYPE
|
||||
import org.thoughtcrime.securesms.database.MmsSmsColumns.Types.PUSH_MESSAGE_BIT
|
||||
import org.thoughtcrime.securesms.database.MmsSmsColumns.Types.SECURE_MESSAGE_BIT
|
||||
import org.thoughtcrime.securesms.database.MmsSmsColumns.Types.SPECIAL_TYPES_MASK
|
||||
import org.thoughtcrime.securesms.database.MmsSmsColumns.Types.SPECIAL_TYPE_STORY_REACTION
|
||||
import org.thoughtcrime.securesms.database.MmsSmsColumns.Types.UNSUPPORTED_MESSAGE_TYPE
|
||||
|
||||
object MessageBitmaskColumnTransformer : ColumnTransformer {
|
||||
|
@ -108,6 +110,8 @@ object MessageBitmaskColumnTransformer : ColumnTransformer {
|
|||
isChangeNumber:${type == CHANGE_NUMBER_TYPE}
|
||||
isBoostRequest:${type == BOOST_REQUEST_TYPE}
|
||||
isGroupV2LeaveOnly:${type and GROUP_V2_LEAVE_BITS == GROUP_V2_LEAVE_BITS}
|
||||
isSpecialType:${type and SPECIAL_TYPES_MASK != 0L}
|
||||
isStoryReaction:${type and SPECIAL_TYPE_STORY_REACTION == SPECIAL_TYPE_STORY_REACTION}
|
||||
""".trimIndent()
|
||||
|
||||
return "$type<br><br>" + describe.replace(Regex("is[A-Z][A-Za-z0-9]*:false\n?"), "").replace("\n", "<br>")
|
||||
|
|
|
@ -39,6 +39,7 @@ object TestMms {
|
|||
distributionType,
|
||||
storyType,
|
||||
null,
|
||||
false,
|
||||
null,
|
||||
emptyList(),
|
||||
emptyList(),
|
||||
|
|
Ładowanie…
Reference in New Issue