Implement UI and backend for sending story reactions.

Co-authored-by: Rashad Sookram <rashad@signal.org>
fork-5.53.8
Alex Hart 2022-03-16 13:44:54 -03:00 zatwierdzone przez Cody Henthorne
rodzic 7f4a12c179
commit 437c1e2f21
41 zmienionych plików z 689 dodań i 343 usunięć

Wyświetl plik

@ -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);

Wyświetl plik

@ -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);
}

Wyświetl plik

@ -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();

Wyświetl plik

@ -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();

Wyświetl plik

@ -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;
}

Wyświetl plik

@ -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

Wyświetl plik

@ -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

Wyświetl plik

@ -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<>();

Wyświetl plik

@ -225,6 +225,7 @@ class MediaSelectionRepository(context: Context) {
ThreadDatabase.DistributionTypes.DEFAULT,
storyType,
null,
false,
null,
emptyList(),
emptyList(),

Wyświetl plik

@ -82,6 +82,7 @@ class TextStoryPostSendRepository(context: Context) {
ThreadDatabase.DistributionTypes.DEFAULT,
storyType.toTextStoryType(),
null,
false,
null,
emptyList(),
listOfNotNull(linkPreview),

Wyświetl plik

@ -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(),

Wyświetl plik

@ -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,

Wyświetl plik

@ -19,6 +19,7 @@ public class OutgoingExpirationUpdateMessage extends OutgoingSecureMediaMessage
false,
StoryType.NONE,
null,
false,
null,
Collections.emptyList(),
Collections.emptyList(),

Wyświetl plik

@ -41,6 +41,7 @@ public final class OutgoingGroupUpdateMessage extends OutgoingSecureMediaMessage
viewOnce,
StoryType.NONE,
null,
false,
quote,
contacts,
previews,

Wyświetl plik

@ -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;
}

Wyświetl plik

@ -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(),

Wyświetl plik

@ -89,6 +89,7 @@ public class RemoteReplyReceiver extends BroadcastReceiver {
0,
StoryType.NONE,
null,
false,
null,
Collections.emptyList(),
Collections.emptyList(),

Wyświetl plik

@ -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())

Wyświetl plik

@ -25,6 +25,8 @@ data class StoryTextPostModel(
messageDigest.update(storyTextPost.toByteArray())
}
val text: String = storyTextPost.body
companion object {
@JvmStatic
fun parseFrom(body: String): StoryTextPostModel {

Wyświetl plik

@ -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
}
}
}

Wyświetl plik

@ -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

Wyświetl plik

@ -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()
}
}
}

Wyświetl plik

@ -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(),

Wyświetl plik

@ -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() {

Wyświetl plik

@ -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)
)
)
}

Wyświetl plik

@ -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) {

Wyświetl plik

@ -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()
}
}

Wyświetl plik

@ -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 {

Wyświetl plik

@ -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
}
}

Wyświetl plik

@ -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() &&

Wyświetl plik

@ -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"

Wyświetl plik

@ -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"

Wyświetl plik

@ -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>

Wyświetl plik

@ -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>

Wyświetl plik

@ -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>

Wyświetl plik

@ -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"

Wyświetl plik

@ -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>

Wyświetl plik

@ -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" />

Wyświetl plik

@ -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>

Wyświetl plik

@ -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>")

Wyświetl plik

@ -39,6 +39,7 @@ object TestMms {
distributionType,
storyType,
null,
false,
null,
emptyList(),
emptyList(),