kopia lustrzana https://github.com/ryukoposting/Signal-Android
Add send/recv/render support for text stories.
rodzic
3a2e8b9b19
commit
ff8d7fa6c2
|
@ -34,6 +34,7 @@ import org.thoughtcrime.securesms.mms.SlideDeck;
|
|||
import org.thoughtcrime.securesms.recipients.LiveRecipient;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientForeverObserver;
|
||||
import org.thoughtcrime.securesms.stories.StoryTextPostModel;
|
||||
import org.thoughtcrime.securesms.util.MediaUtil;
|
||||
import org.thoughtcrime.securesms.util.Projection;
|
||||
import org.thoughtcrime.securesms.util.ThemeUtil;
|
||||
|
@ -209,7 +210,7 @@ public class QuoteView extends FrameLayout implements RecipientForeverObserver {
|
|||
this.author.observeForever(this);
|
||||
setQuoteAuthor(author);
|
||||
setQuoteText(body, attachments);
|
||||
setQuoteAttachment(glideRequests, attachments);
|
||||
setQuoteAttachment(glideRequests, body, attachments);
|
||||
setQuoteMissingFooter(originalMissing);
|
||||
|
||||
if (Build.VERSION.SDK_INT < 21 && messageType == MessageType.INCOMING && chatColors != null) {
|
||||
|
@ -264,7 +265,9 @@ public class QuoteView extends FrameLayout implements RecipientForeverObserver {
|
|||
}
|
||||
|
||||
private void setQuoteText(@Nullable CharSequence body, @NonNull SlideDeck attachments) {
|
||||
if (!TextUtils.isEmpty(body) || !attachments.containsMediaSlide()) {
|
||||
boolean isTextStory = !attachments.containsMediaSlide() && messageType == MessageType.STORY_REPLY;
|
||||
|
||||
if (!isTextStory && (!TextUtils.isEmpty(body) || !attachments.containsMediaSlide())) {
|
||||
bodyView.setVisibility(VISIBLE);
|
||||
bodyView.setText(body == null ? "" : body);
|
||||
mediaDescriptionText.setVisibility(GONE);
|
||||
|
@ -274,6 +277,11 @@ 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);
|
||||
|
@ -305,7 +313,20 @@ public class QuoteView extends FrameLayout implements RecipientForeverObserver {
|
|||
}
|
||||
}
|
||||
|
||||
private void setQuoteAttachment(@NonNull GlideRequests glideRequests, @NonNull SlideDeck slideDeck) {
|
||||
private void setQuoteAttachment(@NonNull GlideRequests glideRequests, @NonNull CharSequence body, @NonNull SlideDeck slideDeck) {
|
||||
if (!attachments.containsMediaSlide() && messageType == MessageType.STORY_REPLY) {
|
||||
StoryTextPostModel model = StoryTextPostModel.parseFrom(body.toString());
|
||||
attachmentVideoOverlayView.setVisibility(GONE);
|
||||
attachmentContainerView.setVisibility(GONE);
|
||||
thumbnailView.setVisibility(VISIBLE);
|
||||
glideRequests.load(model)
|
||||
.centerCrop()
|
||||
.override(thumbWidth, thumbHeight)
|
||||
.diskCacheStrategy(DiskCacheStrategy.RESOURCE)
|
||||
.into(thumbnailView);
|
||||
return;
|
||||
}
|
||||
|
||||
Slide imageVideoSlide = slideDeck.getSlides().stream().filter(s -> s.hasImage() || s.hasVideo() || s.hasSticker()).findFirst().orElse(null);
|
||||
Slide documentSlide = slideDeck.getSlides().stream().filter(Slide::hasDocument).findFirst().orElse(null);
|
||||
Slide viewOnceSlide = slideDeck.getSlides().stream().filter(Slide::hasViewOnce).findFirst().orElse(null);
|
||||
|
|
|
@ -34,6 +34,7 @@ import org.thoughtcrime.securesms.mms.GlideRequests;
|
|||
import org.thoughtcrime.securesms.mms.Slide;
|
||||
import org.thoughtcrime.securesms.mms.SlideClickListener;
|
||||
import org.thoughtcrime.securesms.mms.SlidesClickedListener;
|
||||
import org.thoughtcrime.securesms.stories.StoryTextPostModel;
|
||||
import org.thoughtcrime.securesms.util.MediaUtil;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
import org.thoughtcrime.securesms.util.ViewUtil;
|
||||
|
@ -391,6 +392,31 @@ public class ThumbnailView extends FrameLayout {
|
|||
return future;
|
||||
}
|
||||
|
||||
public ListenableFuture<Boolean> setImageResource(@NonNull GlideRequests glideRequests, @NonNull StoryTextPostModel model, int width, int height) {
|
||||
SettableFuture<Boolean> future = new SettableFuture<>();
|
||||
|
||||
if (transferControls.isPresent()) getTransferControls().setVisibility(View.GONE);
|
||||
|
||||
GlideRequest request = glideRequests.load(model)
|
||||
.diskCacheStrategy(DiskCacheStrategy.NONE)
|
||||
.transition(withCrossFade());
|
||||
|
||||
if (width > 0 && height > 0) {
|
||||
request = request.override(width, height);
|
||||
}
|
||||
|
||||
if (radius > 0) {
|
||||
request = request.transforms(new CenterCrop(), new RoundedCorners(radius));
|
||||
} else {
|
||||
request = request.transforms(new CenterCrop());
|
||||
}
|
||||
|
||||
request.into(new GlideDrawableListeningTarget(image, future));
|
||||
blurhash.setImageDrawable(null);
|
||||
|
||||
return future;
|
||||
}
|
||||
|
||||
public void setThumbnailClickListener(SlideClickListener listener) {
|
||||
this.thumbnailClickListener = listener;
|
||||
}
|
||||
|
|
|
@ -5,6 +5,7 @@ import androidx.core.util.Consumer
|
|||
import io.reactivex.rxjava3.core.Single
|
||||
import org.signal.core.util.concurrent.SignalExecutors
|
||||
import org.thoughtcrime.securesms.contacts.paged.ContactSearchKey
|
||||
import org.thoughtcrime.securesms.contacts.paged.RecipientSearchKey
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||
import org.thoughtcrime.securesms.database.ThreadDatabase
|
||||
import org.thoughtcrime.securesms.database.identity.IdentityRecordList
|
||||
|
@ -30,7 +31,7 @@ class MultiselectForwardRepository(context: Context) {
|
|||
fun checkForBadIdentityRecords(contactSearchKeys: Set<ContactSearchKey>, consumer: Consumer<List<IdentityRecord>>) {
|
||||
SignalExecutors.BOUNDED.execute {
|
||||
val recipients: List<Recipient> = contactSearchKeys
|
||||
.filterIsInstance<ContactSearchKey.KnownRecipient>()
|
||||
.filterIsInstance<RecipientSearchKey>()
|
||||
.map { Recipient.resolved(it.recipientId) }
|
||||
val identityRecordList: IdentityRecordList = ApplicationDependencies.getProtocolStore().aci().identities().getIdentityRecords(recipients)
|
||||
|
||||
|
|
|
@ -17,11 +17,33 @@ enum class StoryType(val code: Int) {
|
|||
/**
|
||||
* User cannot send replies to this story.
|
||||
*/
|
||||
STORY_WITHOUT_REPLIES(2);
|
||||
STORY_WITHOUT_REPLIES(2),
|
||||
|
||||
val isStory get() = this == STORY_WITH_REPLIES || this == STORY_WITHOUT_REPLIES
|
||||
/**
|
||||
* Text story that allows replies
|
||||
*/
|
||||
TEXT_STORY_WITH_REPLIES(3),
|
||||
|
||||
val isStoryWithReplies get() = this == STORY_WITH_REPLIES
|
||||
/**
|
||||
* Text story that does not allow replies
|
||||
*/
|
||||
TEXT_STORY_WITHOUT_REPLIES(4);
|
||||
|
||||
val isStory get() = this != NONE
|
||||
|
||||
val isStoryWithReplies get() = this == STORY_WITH_REPLIES || this == TEXT_STORY_WITH_REPLIES
|
||||
|
||||
val isTextStory get() = this == TEXT_STORY_WITHOUT_REPLIES || this == TEXT_STORY_WITH_REPLIES
|
||||
|
||||
fun toTextStoryType(): StoryType {
|
||||
return when (this) {
|
||||
NONE -> NONE
|
||||
STORY_WITH_REPLIES -> TEXT_STORY_WITH_REPLIES
|
||||
STORY_WITHOUT_REPLIES -> TEXT_STORY_WITHOUT_REPLIES
|
||||
TEXT_STORY_WITH_REPLIES -> TEXT_STORY_WITH_REPLIES
|
||||
TEXT_STORY_WITHOUT_REPLIES -> TEXT_STORY_WITHOUT_REPLIES
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
@JvmStatic
|
||||
|
@ -29,8 +51,20 @@ enum class StoryType(val code: Int) {
|
|||
return when (code) {
|
||||
1 -> STORY_WITH_REPLIES
|
||||
2 -> STORY_WITHOUT_REPLIES
|
||||
3 -> TEXT_STORY_WITH_REPLIES
|
||||
4 -> TEXT_STORY_WITHOUT_REPLIES
|
||||
else -> NONE
|
||||
}
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun withReplies(isTextStory: Boolean): StoryType {
|
||||
return if (isTextStory) TEXT_STORY_WITH_REPLIES else STORY_WITH_REPLIES
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun withoutReplies(isTextStory: Boolean): StoryType {
|
||||
return if (isTextStory) TEXT_STORY_WITHOUT_REPLIES else STORY_WITHOUT_REPLIES
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,6 +3,7 @@ package org.thoughtcrime.securesms.fonts
|
|||
import android.graphics.Typeface
|
||||
import androidx.annotation.DrawableRes
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.StoryTextPost
|
||||
|
||||
/**
|
||||
* Describes which font the user wishes to render content in.
|
||||
|
@ -13,4 +14,18 @@ enum class TextFont(@DrawableRes val icon: Int, val fallbackFamily: String, val
|
|||
SERIF(R.drawable.ic_font_serif, "serif", Typeface.NORMAL, false),
|
||||
SCRIPT(R.drawable.ic_font_script, "serif", Typeface.BOLD, false),
|
||||
CONDENSED(R.drawable.ic_font_condensed, "sans-serif", Typeface.BOLD, true);
|
||||
|
||||
companion object {
|
||||
fun fromStyle(style: StoryTextPost.Style): TextFont {
|
||||
return when (style) {
|
||||
StoryTextPost.Style.DEFAULT -> REGULAR
|
||||
StoryTextPost.Style.REGULAR -> REGULAR
|
||||
StoryTextPost.Style.BOLD -> BOLD
|
||||
StoryTextPost.Style.SERIF -> SERIF
|
||||
StoryTextPost.Style.SCRIPT -> SCRIPT
|
||||
StoryTextPost.Style.CONDENSED -> CONDENSED
|
||||
StoryTextPost.Style.UNRECOGNIZED -> REGULAR
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -24,6 +24,7 @@ import org.thoughtcrime.securesms.jobmanager.JobLogger;
|
|||
import org.thoughtcrime.securesms.jobmanager.JobManager;
|
||||
import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint;
|
||||
import org.thoughtcrime.securesms.messages.GroupSendUtil;
|
||||
import org.thoughtcrime.securesms.messages.StorySendUtil;
|
||||
import org.thoughtcrime.securesms.mms.MmsException;
|
||||
import org.thoughtcrime.securesms.mms.OutgoingMediaMessage;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
|
@ -59,11 +60,11 @@ public final class PushDistributionListSendJob extends PushSendJob {
|
|||
|
||||
public PushDistributionListSendJob(long messageId, @NonNull RecipientId destination, boolean hasMedia) {
|
||||
this(new Parameters.Builder()
|
||||
.setQueue(destination.toQueueKey(hasMedia))
|
||||
.addConstraint(NetworkConstraint.KEY)
|
||||
.setLifespan(TimeUnit.DAYS.toMillis(1))
|
||||
.setMaxAttempts(Parameters.UNLIMITED)
|
||||
.build(),
|
||||
.setQueue(destination.toQueueKey(hasMedia))
|
||||
.addConstraint(NetworkConstraint.KEY)
|
||||
.setLifespan(TimeUnit.DAYS.toMillis(1))
|
||||
.setMaxAttempts(Parameters.UNLIMITED)
|
||||
.build(),
|
||||
messageId);
|
||||
|
||||
}
|
||||
|
@ -147,7 +148,7 @@ public final class PushDistributionListSendJob extends PushSendJob {
|
|||
List<Recipient> target;
|
||||
|
||||
if (!existingNetworkFailures.isEmpty()) target = Stream.of(existingNetworkFailures).map(nf -> nf.getRecipientId(context)).distinct().map(Recipient::resolved).toList();
|
||||
else target = Stream.of(getFullRecipients(listRecipient.requireDistributionListId(), messageId)).distinctBy(Recipient::getId).toList();
|
||||
else target = Stream.of(getFullRecipients(listRecipient.requireDistributionListId(), messageId)).distinctBy(Recipient::getId).toList();
|
||||
|
||||
List<SendMessageResult> results = deliver(message, target);
|
||||
Log.i(TAG, JobLogger.format(this, "Finished send."));
|
||||
|
@ -173,10 +174,15 @@ public final class PushDistributionListSendJob extends PushSendJob {
|
|||
|
||||
List<Attachment> attachments = Stream.of(message.getAttachments()).filterNot(Attachment::isSticker).toList();
|
||||
List<SignalServiceAttachment> attachmentPointers = getAttachmentPointersFor(attachments);
|
||||
boolean isRecipientUpdate = Stream.of(SignalDatabase.groupReceipts().getGroupReceiptInfo(messageId))
|
||||
.anyMatch(info -> info.getStatus() > GroupReceiptDatabase.STATUS_UNDELIVERED);
|
||||
boolean isRecipientUpdate = Stream.of(SignalDatabase.groupReceipts().getGroupReceiptInfo(messageId))
|
||||
.anyMatch(info -> info.getStatus() > GroupReceiptDatabase.STATUS_UNDELIVERED);
|
||||
|
||||
SignalServiceStoryMessage storyMessage = SignalServiceStoryMessage.forFileAttachment(Recipient.self().getProfileKey(), null, attachmentPointers.get(0), message.getStoryType().isStoryWithReplies());
|
||||
final SignalServiceStoryMessage storyMessage;
|
||||
if (message.getStoryType().isTextStory()) {
|
||||
storyMessage = SignalServiceStoryMessage.forTextAttachment(Recipient.self().getProfileKey(), null, StorySendUtil.deserializeBodyToStoryTextAttachment(message, this::getPreviewsFor), message.getStoryType().isStoryWithReplies());
|
||||
} else {
|
||||
storyMessage = SignalServiceStoryMessage.forFileAttachment(Recipient.self().getProfileKey(), null, attachmentPointers.get(0), message.getStoryType().isStoryWithReplies());
|
||||
}
|
||||
return GroupSendUtil.sendStoryMessage(context, message.getRecipient().requireDistributionListId(), destinations, isRecipientUpdate, new MessageId(messageId, true), message.getSentTimeMillis(), storyMessage);
|
||||
} catch (ServerRejectedException e) {
|
||||
throw new UndeliverableMessageException(e);
|
||||
|
|
|
@ -32,6 +32,7 @@ import org.thoughtcrime.securesms.jobmanager.JobLogger;
|
|||
import org.thoughtcrime.securesms.jobmanager.JobManager;
|
||||
import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint;
|
||||
import org.thoughtcrime.securesms.messages.GroupSendUtil;
|
||||
import org.thoughtcrime.securesms.messages.StorySendUtil;
|
||||
import org.thoughtcrime.securesms.mms.MessageGroupContext;
|
||||
import org.thoughtcrime.securesms.mms.MmsException;
|
||||
import org.thoughtcrime.securesms.mms.OutgoingGroupUpdateMessage;
|
||||
|
@ -249,7 +250,12 @@ public final class PushGroupSendJob extends PushSendJob {
|
|||
.withRevision(v2GroupProperties.getGroupRevision())
|
||||
.build();
|
||||
|
||||
SignalServiceStoryMessage storyMessage = SignalServiceStoryMessage.forFileAttachment(Recipient.self().getProfileKey(), groupContext, attachmentPointers.get(0), message.getStoryType().isStoryWithReplies());
|
||||
final SignalServiceStoryMessage storyMessage;
|
||||
if (message.getStoryType().isTextStory()) {
|
||||
storyMessage = SignalServiceStoryMessage.forTextAttachment(Recipient.self().getProfileKey(), groupContext, StorySendUtil.deserializeBodyToStoryTextAttachment(message, this::getPreviewsFor), message.getStoryType().isStoryWithReplies());
|
||||
} else {
|
||||
storyMessage = SignalServiceStoryMessage.forFileAttachment(Recipient.self().getProfileKey(), groupContext, attachmentPointers.get(0), message.getStoryType().isStoryWithReplies());
|
||||
}
|
||||
|
||||
return GroupSendUtil.sendGroupStoryMessage(context, groupId.requireV2(), destinations, isRecipientUpdate, new MessageId(messageId, true), message.getSentTimeMillis(), storyMessage);
|
||||
} else {
|
||||
|
|
|
@ -9,16 +9,16 @@ object TextStoryBackgroundColors {
|
|||
id = ChatColors.Id.NotSet,
|
||||
linearGradient = ChatColors.LinearGradient(
|
||||
degrees = 191.41f,
|
||||
colors = intArrayOf(0xFFF53844.toInt(), 0xFFF33845.toInt(), 0xFFEC3848.toInt(), 0xFFE2384C.toInt(), 0xFFD63851.toInt(), 0xFFC73857.toInt(), 0xFFB6385E.toInt(), 0xFFA43866.toInt(), 0xFF93376D.toInt(), 0xFF813775.toInt(), 0xFF70377C.toInt(), 0xFF613782.toInt(), 0xFF553787.toInt(), 0xFF4B378B.toInt(), 0xFF44378E.toInt(), 0xFF42378F.toInt()),
|
||||
positions = floatArrayOf(0.2109f, 0.2168f, 0.2339f, 0.2611f, 0.2975f, 0.3418f, 0.3932f, 0.4506f, 0.5129f, 0.5791f, 0.6481f, 0.719f, 0.7907f, 0.8621f, 0.9322f, 1.0f)
|
||||
colors = intArrayOf(0xFFF53844.toInt(), 0xFF42378F.toInt()),
|
||||
positions = floatArrayOf(0f, 1.0f)
|
||||
)
|
||||
),
|
||||
ChatColors.forGradient(
|
||||
id = ChatColors.Id.NotSet,
|
||||
linearGradient = ChatColors.LinearGradient(
|
||||
degrees = 192.04f,
|
||||
colors = intArrayOf(0xFFF04CE6.toInt(), 0xFFEE4BE6.toInt(), 0xFFE54AE5.toInt(), 0xFFD949E5.toInt(), 0xFFC946E4.toInt(), 0xFFB644E3.toInt(), 0xFFA141E3.toInt(), 0xFF8B3FE2.toInt(), 0xFF743CE1.toInt(), 0xFF5E39E0.toInt(), 0xFF4936DF.toInt(), 0xFF3634DE.toInt(), 0xFF2632DD.toInt(), 0xFF1930DD.toInt(), 0xFF112FDD.toInt(), 0xFF0E2FDD.toInt()),
|
||||
positions = floatArrayOf(0.0f, 0.0807f, 0.1554f, 0.225f, 0.2904f, 0.3526f, 0.4125f, 0.471f, 0.529f, 0.5875f, 0.6474f, 0.7096f, 0.775f, 0.8446f, 0.9193f, 1.0f)
|
||||
colors = intArrayOf(0xFFF04CE6.toInt(), 0xFF0E2FDD.toInt()),
|
||||
positions = floatArrayOf(0.0f, 1.0f)
|
||||
),
|
||||
),
|
||||
ChatColors.forGradient(
|
||||
|
@ -33,16 +33,16 @@ object TextStoryBackgroundColors {
|
|||
id = ChatColors.Id.NotSet,
|
||||
linearGradient = ChatColors.LinearGradient(
|
||||
degrees = 180f,
|
||||
colors = intArrayOf(0xFF0093E9.toInt(), 0xFF0294E9.toInt(), 0xFF0696E7.toInt(), 0xFF0D99E5.toInt(), 0xFF169EE3.toInt(), 0xFF21A3E0.toInt(), 0xFF2DA8DD.toInt(), 0xFF3AAEDA.toInt(), 0xFF46B5D6.toInt(), 0xFF53BBD3.toInt(), 0xFF5FC0D0.toInt(), 0xFF6AC5CD.toInt(), 0xFF73CACB.toInt(), 0xFF7ACDC9.toInt(), 0xFF7ECFC7.toInt(), 0xFF80D0C7.toInt()),
|
||||
positions = floatArrayOf(0.0f, 0.0807f, 0.1554f, 0.225f, 0.2904f, 0.3526f, 0.4125f, 0.471f, 0.529f, 0.5875f, 0.6474f, 0.7096f, 0.775f, 0.8446f, 0.9193f, 1.0f)
|
||||
colors = intArrayOf(0xFF0093E9.toInt(), 0xFF80D0C7.toInt()),
|
||||
positions = floatArrayOf(0.0f, 1.0f)
|
||||
)
|
||||
),
|
||||
ChatColors.forGradient(
|
||||
id = ChatColors.Id.NotSet,
|
||||
linearGradient = ChatColors.LinearGradient(
|
||||
degrees = 180f,
|
||||
colors = intArrayOf(0xFF65CDAC.toInt(), 0xFF64CDAB.toInt(), 0xFF60CBA8.toInt(), 0xFF5BC8A3.toInt(), 0xFF55C49D.toInt(), 0xFF4DC096.toInt(), 0xFF45BB8F.toInt(), 0xFF3CB687.toInt(), 0xFF33B17F.toInt(), 0xFF2AAC76.toInt(), 0xFF21A76F.toInt(), 0xFF1AA268.toInt(), 0xFF139F62.toInt(), 0xFF0E9C5E.toInt(), 0xFF0B9A5B.toInt(), 0xFF0A995A.toInt()),
|
||||
positions = floatArrayOf(0.0f, 0.0807f, 0.1554f, 0.225f, 0.2904f, 0.3526f, 0.4125f, 0.471f, 0.529f, 0.5875f, 0.6474f, 0.7096f, 0.775f, 0.8446f, 0.9193f, 1.0f)
|
||||
colors = intArrayOf(0xFF65CDAC.toInt(), 0xFF0A995A.toInt()),
|
||||
positions = floatArrayOf(0.0f, 1.0f)
|
||||
)
|
||||
),
|
||||
ChatColors.forColor(
|
||||
|
|
|
@ -17,6 +17,7 @@ import org.thoughtcrime.securesms.contacts.HeaderAction
|
|||
import org.thoughtcrime.securesms.contacts.paged.ContactSearchConfiguration
|
||||
import org.thoughtcrime.securesms.contacts.paged.ContactSearchKey
|
||||
import org.thoughtcrime.securesms.contacts.paged.ContactSearchMediator
|
||||
import org.thoughtcrime.securesms.conversation.ui.error.SafetyNumberChangeDialog
|
||||
import org.thoughtcrime.securesms.linkpreview.LinkPreviewViewModel
|
||||
import org.thoughtcrime.securesms.mediasend.v2.stories.ChooseGroupStoryBottomSheet
|
||||
import org.thoughtcrime.securesms.mediasend.v2.stories.ChooseStoryTypeBottomSheet
|
||||
|
@ -45,7 +46,7 @@ class TextStoryPostSendFragment : Fragment(R.layout.stories_send_text_post_fragm
|
|||
|
||||
private val viewModel: TextStoryPostSendViewModel by viewModels(
|
||||
factoryProducer = {
|
||||
TextStoryPostSendViewModel.Factory(TextStoryPostSendRepository())
|
||||
TextStoryPostSendViewModel.Factory(TextStoryPostSendRepository(requireContext()))
|
||||
}
|
||||
)
|
||||
|
||||
|
@ -99,6 +100,10 @@ class TextStoryPostSendFragment : Fragment(R.layout.stories_send_text_post_fragm
|
|||
}
|
||||
}
|
||||
|
||||
disposables += viewModel.untrustedIdentities.subscribe {
|
||||
SafetyNumberChangeDialog.show(childFragmentManager, it)
|
||||
}
|
||||
|
||||
searchField.doAfterTextChanged {
|
||||
contactSearchMediator.onFilterChanged(it?.toString())
|
||||
}
|
||||
|
@ -158,9 +163,8 @@ class TextStoryPostSendFragment : Fragment(R.layout.stories_send_text_post_fragm
|
|||
shareConfirmButton.isEnabled = false
|
||||
|
||||
val textStoryPostCreationState = creationViewModel.state.value
|
||||
val linkPreviewState = linkPreviewViewModel.linkPreviewState.value
|
||||
|
||||
viewModel.onSend(contactSearchMediator.getSelectedContacts(), textStoryPostCreationState!!, linkPreviewState!!)
|
||||
viewModel.onSend(contactSearchMediator.getSelectedContacts(), textStoryPostCreationState!!, linkPreviewViewModel.onSend().firstOrNull())
|
||||
}
|
||||
|
||||
private fun animateInSelection() {
|
||||
|
|
|
@ -1,12 +1,30 @@
|
|||
package org.thoughtcrime.securesms.mediasend.v2.text.send
|
||||
|
||||
import io.reactivex.rxjava3.core.Completable
|
||||
import android.content.Context
|
||||
import io.reactivex.rxjava3.core.Single
|
||||
import io.reactivex.rxjava3.schedulers.Schedulers
|
||||
import org.signal.core.util.ThreadUtil
|
||||
import org.thoughtcrime.securesms.contacts.paged.ContactSearchKey
|
||||
import org.thoughtcrime.securesms.contacts.paged.RecipientSearchKey
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||
import org.thoughtcrime.securesms.database.ThreadDatabase
|
||||
import org.thoughtcrime.securesms.database.identity.IdentityRecordList
|
||||
import org.thoughtcrime.securesms.database.model.StoryType
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.StoryTextPost
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
|
||||
import org.thoughtcrime.securesms.fonts.TextFont
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.linkpreview.LinkPreview
|
||||
import org.thoughtcrime.securesms.mediasend.v2.text.TextStoryPostCreationState
|
||||
import org.thoughtcrime.securesms.mms.OutgoingMediaMessage
|
||||
import org.thoughtcrime.securesms.mms.OutgoingSecureMediaMessage
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.sms.MessageSender
|
||||
import org.thoughtcrime.securesms.util.Base64
|
||||
|
||||
class TextStoryPostSendRepository {
|
||||
class TextStoryPostSendRepository(context: Context) {
|
||||
|
||||
private val context = context.applicationContext
|
||||
|
||||
fun isFirstSendToStory(shareContacts: Set<ContactSearchKey>): Boolean {
|
||||
if (SignalStore.storyValues().userHasAddedToAStory) {
|
||||
|
@ -16,8 +34,92 @@ class TextStoryPostSendRepository {
|
|||
return shareContacts.any { it is ContactSearchKey.Story }
|
||||
}
|
||||
|
||||
fun send(contactSearchKey: Set<ContactSearchKey>, textStoryPostCreationState: TextStoryPostCreationState, linkPreview: LinkPreview?): Completable {
|
||||
// TODO [stories] -- Implementation once we know what text post messages look like.
|
||||
return Completable.complete()
|
||||
fun send(contactSearchKey: Set<ContactSearchKey>, textStoryPostCreationState: TextStoryPostCreationState, linkPreview: LinkPreview?): Single<TextStoryPostSendResult> {
|
||||
return checkForBadIdentityRecords(contactSearchKey).flatMap { result ->
|
||||
if (result is TextStoryPostSendResult.Success) {
|
||||
performSend(contactSearchKey, textStoryPostCreationState, linkPreview)
|
||||
} else {
|
||||
Single.just(result)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun checkForBadIdentityRecords(contactSearchKeys: Set<ContactSearchKey>): Single<TextStoryPostSendResult> {
|
||||
return Single.fromCallable {
|
||||
val recipients: List<Recipient> = contactSearchKeys
|
||||
.filterIsInstance<RecipientSearchKey>()
|
||||
.map { Recipient.resolved(it.recipientId) }
|
||||
val identityRecordList: IdentityRecordList = ApplicationDependencies.getProtocolStore().aci().identities().getIdentityRecords(recipients)
|
||||
|
||||
if (identityRecordList.untrustedRecords.isNotEmpty()) {
|
||||
TextStoryPostSendResult.UntrustedRecordsError(identityRecordList.untrustedRecords)
|
||||
} else {
|
||||
TextStoryPostSendResult.Success
|
||||
}
|
||||
}.subscribeOn(Schedulers.io())
|
||||
}
|
||||
|
||||
private fun performSend(contactSearchKey: Set<ContactSearchKey>, textStoryPostCreationState: TextStoryPostCreationState, linkPreview: LinkPreview?): Single<TextStoryPostSendResult> {
|
||||
return Single.fromCallable {
|
||||
val messages: MutableList<OutgoingSecureMediaMessage> = mutableListOf()
|
||||
|
||||
for (contact in contactSearchKey) {
|
||||
val recipient = Recipient.resolved(contact.requireShareContact().recipientId.get())
|
||||
val isStory = contact is ContactSearchKey.Story || recipient.isDistributionList
|
||||
|
||||
if (isStory && recipient.isActiveGroup) {
|
||||
SignalDatabase.groups.markDisplayAsStory(recipient.requireGroupId())
|
||||
}
|
||||
|
||||
val storyType: StoryType = when {
|
||||
recipient.isDistributionList -> SignalDatabase.distributionLists.getStoryType(recipient.requireDistributionListId())
|
||||
isStory -> StoryType.STORY_WITH_REPLIES
|
||||
else -> StoryType.NONE
|
||||
}
|
||||
|
||||
val message = OutgoingMediaMessage(
|
||||
recipient,
|
||||
serializeTextStoryState(textStoryPostCreationState),
|
||||
emptyList(),
|
||||
System.currentTimeMillis(),
|
||||
-1,
|
||||
0,
|
||||
false,
|
||||
ThreadDatabase.DistributionTypes.DEFAULT,
|
||||
storyType.toTextStoryType(),
|
||||
null,
|
||||
null,
|
||||
emptyList(),
|
||||
listOfNotNull(linkPreview),
|
||||
emptyList(),
|
||||
mutableSetOf(),
|
||||
mutableSetOf()
|
||||
)
|
||||
|
||||
messages.add(OutgoingSecureMediaMessage(message))
|
||||
ThreadUtil.sleep(5)
|
||||
}
|
||||
|
||||
MessageSender.sendMediaBroadcast(context, messages, emptyList())
|
||||
TextStoryPostSendResult.Success
|
||||
}
|
||||
}
|
||||
|
||||
private fun serializeTextStoryState(textStoryPostCreationState: TextStoryPostCreationState): String {
|
||||
val builder = StoryTextPost.newBuilder()
|
||||
|
||||
builder.body = textStoryPostCreationState.body.toString()
|
||||
builder.background = textStoryPostCreationState.backgroundColor.serialize()
|
||||
builder.style = when (textStoryPostCreationState.textFont) {
|
||||
TextFont.REGULAR -> StoryTextPost.Style.REGULAR
|
||||
TextFont.BOLD -> StoryTextPost.Style.BOLD
|
||||
TextFont.SERIF -> StoryTextPost.Style.SERIF
|
||||
TextFont.SCRIPT -> StoryTextPost.Style.SCRIPT
|
||||
TextFont.CONDENSED -> StoryTextPost.Style.CONDENSED
|
||||
}
|
||||
builder.textBackgroundColor = textStoryPostCreationState.textBackgroundColor
|
||||
builder.textForegroundColor = textStoryPostCreationState.textForegroundColor
|
||||
|
||||
return Base64.encodeBytes(builder.build().toByteArray())
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,8 @@
|
|||
package org.thoughtcrime.securesms.mediasend.v2.text.send
|
||||
|
||||
import org.thoughtcrime.securesms.database.model.IdentityRecord
|
||||
|
||||
sealed class TextStoryPostSendResult {
|
||||
object Success : TextStoryPostSendResult()
|
||||
data class UntrustedRecordsError(val untrustedRecords: List<IdentityRecord>) : TextStoryPostSendResult()
|
||||
}
|
|
@ -3,20 +3,25 @@ package org.thoughtcrime.securesms.mediasend.v2.text.send
|
|||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import io.reactivex.rxjava3.core.Observable
|
||||
import io.reactivex.rxjava3.disposables.CompositeDisposable
|
||||
import io.reactivex.rxjava3.kotlin.plusAssign
|
||||
import io.reactivex.rxjava3.kotlin.subscribeBy
|
||||
import io.reactivex.rxjava3.subjects.PublishSubject
|
||||
import org.thoughtcrime.securesms.contacts.paged.ContactSearchKey
|
||||
import org.thoughtcrime.securesms.linkpreview.LinkPreviewViewModel
|
||||
import org.thoughtcrime.securesms.database.model.IdentityRecord
|
||||
import org.thoughtcrime.securesms.linkpreview.LinkPreview
|
||||
import org.thoughtcrime.securesms.mediasend.v2.text.TextStoryPostCreationState
|
||||
import org.thoughtcrime.securesms.util.livedata.Store
|
||||
|
||||
class TextStoryPostSendViewModel(private val repository: TextStoryPostSendRepository) : ViewModel() {
|
||||
|
||||
private val store = Store(TextStoryPostSendState.INIT)
|
||||
private val untrustedIdentitySubject = PublishSubject.create<List<IdentityRecord>>()
|
||||
private val disposables = CompositeDisposable()
|
||||
|
||||
val state: LiveData<TextStoryPostSendState> = store.stateLiveData
|
||||
val untrustedIdentities: Observable<List<IdentityRecord>> = untrustedIdentitySubject
|
||||
|
||||
override fun onCleared() {
|
||||
disposables.clear()
|
||||
|
@ -36,14 +41,22 @@ class TextStoryPostSendViewModel(private val repository: TextStoryPostSendReposi
|
|||
}
|
||||
}
|
||||
|
||||
fun onSend(contactSearchKeys: Set<ContactSearchKey>, textStoryPostCreationState: TextStoryPostCreationState, linkPreviewState: LinkPreviewViewModel.LinkPreviewState) {
|
||||
fun onSend(contactSearchKeys: Set<ContactSearchKey>, textStoryPostCreationState: TextStoryPostCreationState, linkPreview: LinkPreview?) {
|
||||
store.update {
|
||||
TextStoryPostSendState.SENDING
|
||||
}
|
||||
|
||||
disposables += repository.send(contactSearchKeys, textStoryPostCreationState, linkPreviewState.linkPreview.orNull()).subscribeBy(
|
||||
onComplete = {
|
||||
store.update { TextStoryPostSendState.SENT }
|
||||
disposables += repository.send(contactSearchKeys, textStoryPostCreationState, linkPreview).subscribeBy(
|
||||
onSuccess = {
|
||||
when (it) {
|
||||
is TextStoryPostSendResult.Success -> {
|
||||
store.update { TextStoryPostSendState.SENT }
|
||||
}
|
||||
is TextStoryPostSendResult.UntrustedRecordsError -> {
|
||||
untrustedIdentitySubject.onNext(it.untrustedRecords)
|
||||
store.update { TextStoryPostSendState.INIT }
|
||||
}
|
||||
}
|
||||
},
|
||||
onError = {
|
||||
// TODO [stories] -- Error of some sort.
|
||||
|
|
|
@ -52,6 +52,9 @@ import org.thoughtcrime.securesms.database.model.ReactionRecord;
|
|||
import org.thoughtcrime.securesms.database.model.StickerRecord;
|
||||
import org.thoughtcrime.securesms.database.model.StoryType;
|
||||
import org.thoughtcrime.securesms.database.model.ThreadRecord;
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.ChatColor;
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.StoryTextPost;
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.StoryTextPostOrBuilder;
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.groups.BadGroupIdException;
|
||||
import org.thoughtcrime.securesms.groups.GroupChangeBusyException;
|
||||
|
@ -116,6 +119,7 @@ import org.thoughtcrime.securesms.sms.OutgoingTextMessage;
|
|||
import org.thoughtcrime.securesms.stickers.StickerLocator;
|
||||
import org.thoughtcrime.securesms.storage.StorageSyncHelper;
|
||||
import org.thoughtcrime.securesms.stories.Stories;
|
||||
import org.thoughtcrime.securesms.util.Base64;
|
||||
import org.thoughtcrime.securesms.util.FeatureFlags;
|
||||
import org.thoughtcrime.securesms.util.GroupUtil;
|
||||
import org.thoughtcrime.securesms.util.Hex;
|
||||
|
@ -139,6 +143,7 @@ import org.whispersystems.signalservice.api.messages.SignalServiceGroupV2;
|
|||
import org.whispersystems.signalservice.api.messages.SignalServicePreview;
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceReceiptMessage;
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceStoryMessage;
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceTextAttachment;
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceTypingMessage;
|
||||
import org.whispersystems.signalservice.api.messages.calls.AnswerMessage;
|
||||
import org.whispersystems.signalservice.api.messages.calls.BusyMessage;
|
||||
|
@ -165,10 +170,12 @@ import org.whispersystems.signalservice.api.messages.shared.SharedContact;
|
|||
import org.whispersystems.signalservice.api.payments.Money;
|
||||
import org.whispersystems.signalservice.api.push.DistributionId;
|
||||
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
|
||||
import org.whispersystems.signalservice.api.util.OptionalUtil;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.security.SecureRandom;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
|
@ -1333,9 +1340,9 @@ public final class MessageContentProcessor {
|
|||
try {
|
||||
final StoryType storyType;
|
||||
if (message.getAllowsReplies().or(false)) {
|
||||
storyType = StoryType.STORY_WITH_REPLIES;
|
||||
storyType = StoryType.withReplies(message.getTextAttachment().isPresent());
|
||||
} else {
|
||||
storyType = StoryType.STORY_WITHOUT_REPLIES;
|
||||
storyType = StoryType.withoutReplies(message.getTextAttachment().isPresent());
|
||||
}
|
||||
|
||||
IncomingMediaMessage mediaMessage = new IncomingMediaMessage(senderRecipient.getId(),
|
||||
|
@ -1349,12 +1356,15 @@ public final class MessageContentProcessor {
|
|||
false,
|
||||
false,
|
||||
content.isNeedsReceipt(),
|
||||
Optional.absent(),
|
||||
message.getTextAttachment().transform(this::serializeTextAttachment),
|
||||
Optional.fromNullable(GroupUtil.getGroupContextIfPresent(content)),
|
||||
message.getFileAttachment().transform(Collections::singletonList),
|
||||
Optional.absent(),
|
||||
Optional.absent(),
|
||||
Optional.absent(),
|
||||
getLinkPreviews(OptionalUtil.flatMap(message.getTextAttachment(),
|
||||
t -> t.getPreview().transform(Collections::singletonList)),
|
||||
"",
|
||||
true),
|
||||
Optional.absent(),
|
||||
Optional.absent(),
|
||||
content.getServerUuid());
|
||||
|
@ -1382,6 +1392,64 @@ public final class MessageContentProcessor {
|
|||
}
|
||||
}
|
||||
|
||||
private @NonNull String serializeTextAttachment(@NonNull SignalServiceTextAttachment textAttachment) {
|
||||
StoryTextPost.Builder builder = StoryTextPost.newBuilder();
|
||||
|
||||
if (textAttachment.getText().isPresent()) {
|
||||
builder.setBody(textAttachment.getText().get());
|
||||
}
|
||||
|
||||
if (textAttachment.getStyle().isPresent()) {
|
||||
switch (textAttachment.getStyle().get()) {
|
||||
case DEFAULT:
|
||||
builder.setStyle(StoryTextPost.Style.DEFAULT);
|
||||
break;
|
||||
case REGULAR:
|
||||
builder.setStyle(StoryTextPost.Style.REGULAR);
|
||||
break;
|
||||
case BOLD:
|
||||
builder.setStyle(StoryTextPost.Style.BOLD);
|
||||
break;
|
||||
case SERIF:
|
||||
builder.setStyle(StoryTextPost.Style.SERIF);
|
||||
break;
|
||||
case SCRIPT:
|
||||
builder.setStyle(StoryTextPost.Style.SCRIPT);
|
||||
break;
|
||||
case CONDENSED:
|
||||
builder.setStyle(StoryTextPost.Style.CONDENSED);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (textAttachment.getTextBackgroundColor().isPresent()) {
|
||||
builder.setTextBackgroundColor(textAttachment.getTextBackgroundColor().get());
|
||||
}
|
||||
|
||||
if (textAttachment.getTextForegroundColor().isPresent()) {
|
||||
builder.setTextForegroundColor(textAttachment.getTextForegroundColor().get());
|
||||
}
|
||||
|
||||
ChatColor.Builder chatColorBuilder = ChatColor.newBuilder();
|
||||
if (textAttachment.getBackgroundColor().isPresent()) {
|
||||
chatColorBuilder.setSingleColor(ChatColor.SingleColor.newBuilder().setColor(textAttachment.getBackgroundColor().get()));
|
||||
} else if (textAttachment.getBackgroundGradient().isPresent()) {
|
||||
SignalServiceTextAttachment.Gradient gradient = textAttachment.getBackgroundGradient().get();
|
||||
ChatColor.LinearGradient.Builder linearGradientBuilder = ChatColor.LinearGradient.newBuilder();
|
||||
|
||||
linearGradientBuilder.setRotation(gradient.getAngle().or(0).floatValue());
|
||||
linearGradientBuilder.addColors(gradient.getStartColor().get());
|
||||
linearGradientBuilder.addColors(gradient.getEndColor().get());
|
||||
linearGradientBuilder.addAllPositions(Arrays.asList(0f, 1f));
|
||||
|
||||
chatColorBuilder.setLinearGradient(linearGradientBuilder);
|
||||
}
|
||||
|
||||
builder.setBackground(chatColorBuilder);
|
||||
|
||||
return Base64.encodeBytes(builder.build().toByteArray());
|
||||
}
|
||||
|
||||
private void handleStoryReply(@NonNull SignalServiceContent content, @NonNull SignalServiceDataMessage message, @NonNull Recipient senderRecipient) throws StorageFailedException {
|
||||
log(content.getTimestamp(), "Story reply.");
|
||||
|
||||
|
@ -1473,7 +1541,7 @@ public final class MessageContentProcessor {
|
|||
try {
|
||||
Optional<QuoteModel> quote = getValidatedQuote(message.getQuote());
|
||||
Optional<List<Contact>> sharedContacts = getContacts(message.getSharedContacts());
|
||||
Optional<List<LinkPreview>> linkPreviews = getLinkPreviews(message.getPreviews(), message.getBody().or(""));
|
||||
Optional<List<LinkPreview>> linkPreviews = getLinkPreviews(message.getPreviews(), message.getBody().or(""), false);
|
||||
Optional<List<Mention>> mentions = getMentions(message.getMentions());
|
||||
Optional<Attachment> sticker = getStickerAttachment(message.getSticker());
|
||||
|
||||
|
@ -1569,7 +1637,7 @@ public final class MessageContentProcessor {
|
|||
Optional<QuoteModel> quote = getValidatedQuote(message.getMessage().getQuote());
|
||||
Optional<Attachment> sticker = getStickerAttachment(message.getMessage().getSticker());
|
||||
Optional<List<Contact>> sharedContacts = getContacts(message.getMessage().getSharedContacts());
|
||||
Optional<List<LinkPreview>> previews = getLinkPreviews(message.getMessage().getPreviews(), message.getMessage().getBody().or(""));
|
||||
Optional<List<LinkPreview>> previews = getLinkPreviews(message.getMessage().getPreviews(), message.getMessage().getBody().or(""), false);
|
||||
Optional<List<Mention>> mentions = getMentions(message.getMessage().getMentions());
|
||||
boolean viewOnce = message.getMessage().isViewOnce();
|
||||
List<Attachment> syncAttachments = viewOnce ? Collections.singletonList(new TombstoneAttachment(MediaUtil.VIEW_ONCE, false))
|
||||
|
@ -2314,7 +2382,7 @@ public final class MessageContentProcessor {
|
|||
return Optional.of(contacts);
|
||||
}
|
||||
|
||||
private Optional<List<LinkPreview>> getLinkPreviews(Optional<List<SignalServicePreview>> previews, @NonNull String message) {
|
||||
private Optional<List<LinkPreview>> getLinkPreviews(Optional<List<SignalServicePreview>> previews, @NonNull String message, boolean isStoryEmbed) {
|
||||
if (!previews.isPresent() || previews.get().isEmpty()) return Optional.absent();
|
||||
|
||||
List<LinkPreview> linkPreviews = new ArrayList<>(previews.get().size());
|
||||
|
@ -2329,7 +2397,7 @@ public final class MessageContentProcessor {
|
|||
boolean presentInBody = url.isPresent() && urlsInMessage.containsUrl(url.get());
|
||||
boolean validDomain = url.isPresent() && LinkPreviewUtil.isValidPreviewUrl(url.get());
|
||||
|
||||
if (hasTitle && presentInBody && validDomain) {
|
||||
if (hasTitle && (presentInBody || isStoryEmbed) && validDomain) {
|
||||
LinkPreview linkPreview = new LinkPreview(url.get(), title.or(""), description.or(""), preview.getDate(), thumbnail);
|
||||
linkPreviews.add(linkPreview);
|
||||
} else {
|
||||
|
|
|
@ -0,0 +1,58 @@
|
|||
package org.thoughtcrime.securesms.messages
|
||||
|
||||
import com.google.protobuf.InvalidProtocolBufferException
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.StoryTextPost
|
||||
import org.thoughtcrime.securesms.mms.OutgoingMediaMessage
|
||||
import org.thoughtcrime.securesms.util.Base64
|
||||
import org.whispersystems.libsignal.util.guava.Optional
|
||||
import org.whispersystems.signalservice.api.messages.SignalServicePreview
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceTextAttachment
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
object StorySendUtil {
|
||||
@JvmStatic
|
||||
@Throws(InvalidProtocolBufferException::class)
|
||||
fun deserializeBodyToStoryTextAttachment(message: OutgoingMediaMessage, getPreviewsFor: (OutgoingMediaMessage) -> List<SignalServicePreview>): SignalServiceTextAttachment {
|
||||
val storyTextPost = StoryTextPost.parseFrom(Base64.decode(message.body))
|
||||
val preview = if (message.linkPreviews.isEmpty()) {
|
||||
Optional.absent()
|
||||
} else {
|
||||
Optional.of(getPreviewsFor(message)[0])
|
||||
}
|
||||
|
||||
return if (storyTextPost.background.hasLinearGradient()) {
|
||||
SignalServiceTextAttachment.forGradientBackground(
|
||||
Optional.fromNullable(storyTextPost.body),
|
||||
Optional.fromNullable(getStyle(storyTextPost.style)),
|
||||
Optional.of(storyTextPost.textForegroundColor),
|
||||
Optional.of(storyTextPost.textBackgroundColor),
|
||||
preview,
|
||||
SignalServiceTextAttachment.Gradient(
|
||||
Optional.of(storyTextPost.background.linearGradient.getColors(0)),
|
||||
Optional.of(storyTextPost.background.linearGradient.getColors(1)),
|
||||
Optional.of(storyTextPost.background.linearGradient.rotation.roundToInt())
|
||||
)
|
||||
)
|
||||
} else {
|
||||
SignalServiceTextAttachment.forSolidBackground(
|
||||
Optional.fromNullable(storyTextPost.body),
|
||||
Optional.fromNullable(getStyle(storyTextPost.style)),
|
||||
Optional.of(storyTextPost.textForegroundColor),
|
||||
Optional.of(storyTextPost.textBackgroundColor),
|
||||
preview,
|
||||
storyTextPost.background.singleColor.color
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun getStyle(style: StoryTextPost.Style): SignalServiceTextAttachment.Style {
|
||||
return when (style) {
|
||||
StoryTextPost.Style.REGULAR -> SignalServiceTextAttachment.Style.REGULAR
|
||||
StoryTextPost.Style.BOLD -> SignalServiceTextAttachment.Style.BOLD
|
||||
StoryTextPost.Style.SERIF -> SignalServiceTextAttachment.Style.SERIF
|
||||
StoryTextPost.Style.SCRIPT -> SignalServiceTextAttachment.Style.SCRIPT
|
||||
StoryTextPost.Style.CONDENSED -> SignalServiceTextAttachment.Style.CONDENSED
|
||||
else -> SignalServiceTextAttachment.Style.DEFAULT
|
||||
}
|
||||
}
|
||||
}
|
|
@ -41,6 +41,7 @@ import org.thoughtcrime.securesms.mms.AttachmentStreamUriLoader.AttachmentModel;
|
|||
import org.thoughtcrime.securesms.mms.DecryptableStreamUriLoader.DecryptableUri;
|
||||
import org.thoughtcrime.securesms.stickers.StickerRemoteUri;
|
||||
import org.thoughtcrime.securesms.stickers.StickerRemoteUriLoader;
|
||||
import org.thoughtcrime.securesms.stories.StoryTextPostModel;
|
||||
import org.thoughtcrime.securesms.util.ConversationShortcutPhoto;
|
||||
|
||||
import java.io.File;
|
||||
|
@ -78,7 +79,9 @@ public class SignalGlideComponents implements RegisterGlideComponents {
|
|||
registry.register(APNGDecoder.class, Drawable.class, new ApngFrameDrawableTranscoder());
|
||||
|
||||
registry.prepend(BlurHash.class, Bitmap.class, new BlurHashResourceDecoder());
|
||||
registry.prepend(StoryTextPostModel.class, Bitmap.class, new StoryTextPostModel.Decoder());
|
||||
|
||||
registry.append(StoryTextPostModel.class, StoryTextPostModel.class, UnitModelLoader.Factory.getInstance());
|
||||
registry.append(ConversationShortcutPhoto.class, Bitmap.class, new ConversationShortcutPhoto.Loader.Factory(context));
|
||||
registry.append(ContactPhoto.class, InputStream.class, new ContactPhotoLoader.Factory(context));
|
||||
registry.append(DecryptableUri.class, InputStream.class, new DecryptableStreamUriLoader.Factory(context));
|
||||
|
|
|
@ -44,6 +44,7 @@ import org.thoughtcrime.securesms.database.SmsDatabase;
|
|||
import org.thoughtcrime.securesms.database.ThreadDatabase;
|
||||
import org.thoughtcrime.securesms.database.model.MessageId;
|
||||
import org.thoughtcrime.securesms.database.model.MessageRecord;
|
||||
import org.thoughtcrime.securesms.database.model.MmsMessageRecord;
|
||||
import org.thoughtcrime.securesms.database.model.ReactionRecord;
|
||||
import org.thoughtcrime.securesms.database.model.SmsMessageRecord;
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
|
@ -85,7 +86,9 @@ import java.util.Collection;
|
|||
import java.util.Collections;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public class MessageSender {
|
||||
|
||||
|
@ -229,6 +232,7 @@ public class MessageSender {
|
|||
|
||||
attachmentDatabase.updateMessageId(preUploadAttachmentIds, primaryMessageId);
|
||||
messageIds.add(primaryMessageId);
|
||||
uploadLinkPreviews(primaryMessage, primaryMessageId, jobManager, preUploadJobIds, messageDependsOnIds);
|
||||
|
||||
if (messages.size() > 0) {
|
||||
List<OutgoingSecureMediaMessage> secondaryMessages = messages.subList(1, messages.size());
|
||||
|
@ -257,6 +261,7 @@ public class MessageSender {
|
|||
|
||||
attachmentDatabase.updateMessageId(attachmentIds, messageId);
|
||||
messageIds.add(messageId);
|
||||
uploadLinkPreviews(secondaryMessage, messageId, jobManager, preUploadJobIds, messageDependsOnIds);
|
||||
}
|
||||
|
||||
for (int i = 0; i < attachmentCopies.size(); i++) {
|
||||
|
@ -291,6 +296,31 @@ public class MessageSender {
|
|||
}
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
private static void uploadLinkPreviews(@NonNull OutgoingMediaMessage mediaMessage,
|
||||
long outgoingMessageId,
|
||||
@NonNull JobManager jobManager,
|
||||
@NonNull List<String> preUploadJobIds,
|
||||
@NonNull List<String> messageDependsOnIds)
|
||||
{
|
||||
if (!mediaMessage.getLinkPreviews().isEmpty()) {
|
||||
try {
|
||||
MmsMessageRecord freshRecord = (MmsMessageRecord) SignalDatabase.mms().getMessageRecord(outgoingMessageId);
|
||||
freshRecord.getLinkPreviews()
|
||||
.stream()
|
||||
.map(LinkPreview::getAttachmentId)
|
||||
.filter(Objects::nonNull)
|
||||
.forEach(previewId -> {
|
||||
Job job = new AttachmentUploadJob(previewId);
|
||||
jobManager.add(job, preUploadJobIds);
|
||||
messageDependsOnIds.add(job.getId());
|
||||
});
|
||||
} catch (NoSuchMessageException e) {
|
||||
throw new AssertionError("Cannot fetch record we just inserted");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return A result if the attachment was enqueued, or null if it failed to enqueue or shouldn't
|
||||
* be enqueued (like in the case of a local self-send).
|
||||
|
|
|
@ -33,13 +33,11 @@ class StoryLinkPreviewView @JvmOverloads constructor(
|
|||
private val title: TextView = findViewById(R.id.link_preview_title)
|
||||
private val url: TextView = findViewById(R.id.link_preview_url)
|
||||
|
||||
fun bind(linkPreviewState: LinkPreviewViewModel.LinkPreviewState, hiddenVisibility: Int = View.INVISIBLE) {
|
||||
if (linkPreviewState.linkPreview.isPresent) {
|
||||
fun bind(linkPreview: LinkPreview?, hiddenVisibility: Int = View.INVISIBLE) {
|
||||
if (linkPreview != null) {
|
||||
visibility = View.VISIBLE
|
||||
isClickable = true
|
||||
|
||||
val linkPreview: LinkPreview = linkPreviewState.linkPreview.get()
|
||||
|
||||
val corners = DimensionUnit.DP.toPixels(18f).toInt()
|
||||
image.setCorners(corners, corners, corners, corners)
|
||||
|
||||
|
@ -60,6 +58,10 @@ class StoryLinkPreviewView @JvmOverloads constructor(
|
|||
}
|
||||
}
|
||||
|
||||
fun bind(linkPreviewState: LinkPreviewViewModel.LinkPreviewState, hiddenVisibility: Int = View.INVISIBLE) {
|
||||
bind(linkPreviewState.linkPreview.orNull(), hiddenVisibility)
|
||||
}
|
||||
|
||||
private fun formatUrl(linkPreview: LinkPreview) {
|
||||
var domain: String? = null
|
||||
|
||||
|
|
|
@ -0,0 +1,60 @@
|
|||
package org.thoughtcrime.securesms.stories
|
||||
|
||||
import android.graphics.Bitmap
|
||||
import android.view.View
|
||||
import androidx.core.graphics.scale
|
||||
import androidx.core.view.drawToBitmap
|
||||
import com.bumptech.glide.load.Key
|
||||
import com.bumptech.glide.load.Options
|
||||
import com.bumptech.glide.load.ResourceDecoder
|
||||
import com.bumptech.glide.load.engine.Resource
|
||||
import com.bumptech.glide.load.resource.SimpleResource
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.StoryTextPost
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
|
||||
import org.thoughtcrime.securesms.util.Base64
|
||||
import java.security.MessageDigest
|
||||
|
||||
/**
|
||||
* Glide model to render a StoryTextPost as a bitmap
|
||||
*/
|
||||
data class StoryTextPostModel(
|
||||
private val storyTextPost: StoryTextPost
|
||||
) : Key {
|
||||
|
||||
override fun updateDiskCacheKey(messageDigest: MessageDigest) {
|
||||
messageDigest.update(storyTextPost.toByteArray())
|
||||
}
|
||||
|
||||
companion object {
|
||||
@JvmStatic
|
||||
fun parseFrom(body: String): StoryTextPostModel {
|
||||
return StoryTextPostModel(StoryTextPost.parseFrom(Base64.decode(body)))
|
||||
}
|
||||
}
|
||||
|
||||
class Decoder : ResourceDecoder<StoryTextPostModel, Bitmap> {
|
||||
|
||||
companion object {
|
||||
private const val RENDER_WIDTH = 1080
|
||||
private const val RENDER_HEIGHT = 1920
|
||||
}
|
||||
|
||||
override fun handles(source: StoryTextPostModel, options: Options): Boolean = true
|
||||
|
||||
override fun decode(source: StoryTextPostModel, width: Int, height: Int, options: Options): Resource<Bitmap> {
|
||||
val view = StoryTextPostView(ApplicationDependencies.getApplication())
|
||||
|
||||
view.measure(View.MeasureSpec.makeMeasureSpec(RENDER_WIDTH, View.MeasureSpec.EXACTLY), View.MeasureSpec.makeMeasureSpec(RENDER_HEIGHT, View.MeasureSpec.EXACTLY))
|
||||
view.layout(0, 0, view.measuredWidth, view.measuredHeight)
|
||||
|
||||
view.bindFromStoryTextPost(source.storyTextPost)
|
||||
|
||||
view.measure(View.MeasureSpec.makeMeasureSpec(RENDER_WIDTH, View.MeasureSpec.EXACTLY), View.MeasureSpec.makeMeasureSpec(RENDER_HEIGHT, View.MeasureSpec.EXACTLY))
|
||||
view.layout(0, 0, view.measuredWidth, view.measuredHeight)
|
||||
|
||||
val bitmap = view.drawToBitmap().scale(width, height)
|
||||
|
||||
return SimpleResource(bitmap)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -6,6 +6,7 @@ import android.graphics.Typeface
|
|||
import android.graphics.drawable.Drawable
|
||||
import android.util.AttributeSet
|
||||
import android.util.TypedValue
|
||||
import android.view.View
|
||||
import android.widget.ImageView
|
||||
import android.widget.TextView
|
||||
import androidx.annotation.ColorInt
|
||||
|
@ -18,12 +19,18 @@ import androidx.core.widget.doAfterTextChanged
|
|||
import com.airbnb.lottie.SimpleColorFilter
|
||||
import org.signal.core.util.DimensionUnit
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.conversation.colors.ChatColors
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.StoryTextPost
|
||||
import org.thoughtcrime.securesms.fonts.Fonts
|
||||
import org.thoughtcrime.securesms.fonts.TextFont
|
||||
import org.thoughtcrime.securesms.linkpreview.LinkPreview
|
||||
import org.thoughtcrime.securesms.linkpreview.LinkPreviewViewModel
|
||||
import org.thoughtcrime.securesms.mediasend.v2.text.TextAlignment
|
||||
import org.thoughtcrime.securesms.mediasend.v2.text.TextStoryPostCreationState
|
||||
import org.thoughtcrime.securesms.mediasend.v2.text.TextStoryScale
|
||||
import org.thoughtcrime.securesms.util.ViewUtil
|
||||
import org.thoughtcrime.securesms.util.visible
|
||||
import java.util.Locale
|
||||
|
||||
class StoryTextPostView @JvmOverloads constructor(
|
||||
context: Context,
|
||||
|
@ -116,10 +123,8 @@ class StoryTextPostView @JvmOverloads constructor(
|
|||
|
||||
setPostBackground(state.backgroundColor.chatBubbleMask)
|
||||
setText(
|
||||
if (state.body.isEmpty()) {
|
||||
state.body.ifEmpty {
|
||||
context.getString(R.string.TextStoryPostCreationFragment__tap_to_add_text)
|
||||
} else {
|
||||
state.body
|
||||
},
|
||||
state.body.isEmpty()
|
||||
)
|
||||
|
@ -134,6 +139,32 @@ class StoryTextPostView @JvmOverloads constructor(
|
|||
postAdjustLinkPreviewTranslationY()
|
||||
}
|
||||
|
||||
fun bindFromStoryTextPost(storyTextPost: StoryTextPost) {
|
||||
linkPreviewView.visible = false
|
||||
|
||||
textAlignment = TextAlignment.CENTER
|
||||
|
||||
setPostBackground(ChatColors.forChatColor(ChatColors.Id.NotSet, storyTextPost.background).chatBubbleMask)
|
||||
setText(storyTextPost.body, false)
|
||||
setTextColor(storyTextPost.textForegroundColor)
|
||||
setTextBackgroundColor(storyTextPost.textBackgroundColor)
|
||||
setTextGravity(TextAlignment.CENTER)
|
||||
|
||||
when (val fontResult = Fonts.resolveFont(context, Locale.getDefault(), TextFont.fromStyle(storyTextPost.style))) {
|
||||
is Fonts.FontResult.Immediate -> setTypeface(fontResult.typeface)
|
||||
is Fonts.FontResult.Async -> setTypeface(fontResult.future.get())
|
||||
}
|
||||
|
||||
hideCloseButton()
|
||||
|
||||
postAdjustTextTranslationX(TextAlignment.CENTER)
|
||||
postAdjustLinkPreviewTranslationY()
|
||||
}
|
||||
|
||||
fun bindLinkPreview(linkPreview: LinkPreview?) {
|
||||
linkPreviewView.bind(linkPreview, View.GONE)
|
||||
}
|
||||
|
||||
fun bindLinkPreviewState(linkPreviewState: LinkPreviewViewModel.LinkPreviewState, hiddenVisibility: Int) {
|
||||
linkPreviewView.bind(linkPreviewState, hiddenVisibility)
|
||||
}
|
||||
|
@ -145,7 +176,7 @@ class StoryTextPostView @JvmOverloads constructor(
|
|||
}
|
||||
}
|
||||
|
||||
fun postAdjustTextTranslationX(textAlignment: TextAlignment) {
|
||||
private fun postAdjustTextTranslationX(textAlignment: TextAlignment) {
|
||||
doOnNextLayout {
|
||||
adjustTextTranslationX(textAlignment)
|
||||
}
|
||||
|
@ -159,6 +190,10 @@ class StoryTextPostView @JvmOverloads constructor(
|
|||
linkPreviewView.setOnCloseClickListener(onClickListener)
|
||||
}
|
||||
|
||||
fun setLinkPreviewClickListener(onClickListener: OnClickListener?) {
|
||||
linkPreviewView.setOnClickListener(onClickListener)
|
||||
}
|
||||
|
||||
fun showPostContent() {
|
||||
textView.alpha = 1f
|
||||
linkPreviewView.alpha = 1f
|
||||
|
|
|
@ -10,9 +10,12 @@ import org.thoughtcrime.securesms.avatar.view.AvatarView
|
|||
import org.thoughtcrime.securesms.components.ThumbnailView
|
||||
import org.thoughtcrime.securesms.components.settings.PreferenceModel
|
||||
import org.thoughtcrime.securesms.database.model.MediaMmsMessageRecord
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.StoryTextPost
|
||||
import org.thoughtcrime.securesms.mms.GlideApp
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.stories.StoryTextPostModel
|
||||
import org.thoughtcrime.securesms.stories.dialogs.StoryContextMenu
|
||||
import org.thoughtcrime.securesms.util.Base64
|
||||
import org.thoughtcrime.securesms.util.DateUtils
|
||||
import org.thoughtcrime.securesms.util.adapter.mapping.LayoutFactory
|
||||
import org.thoughtcrime.securesms.util.adapter.mapping.MappingAdapter
|
||||
|
@ -80,11 +83,23 @@ object StoriesLandingItem {
|
|||
val record = model.data.primaryStory.messageRecord as MediaMmsMessageRecord
|
||||
|
||||
avatarView.setStoryRingFromState(model.data.storyViewState)
|
||||
storyPreview.setImageResource(GlideApp.with(storyPreview), record.slideDeck.thumbnailSlide!!, false, true)
|
||||
|
||||
if (record.storyType.isTextStory) {
|
||||
storyPreview.setImageResource(GlideApp.with(storyPreview), StoryTextPostModel(StoryTextPost.parseFrom(Base64.decode(record.body))), 0, 0)
|
||||
} else if (record.slideDeck.thumbnailSlide != null) {
|
||||
storyPreview.setImageResource(GlideApp.with(storyPreview), record.slideDeck.thumbnailSlide!!, false, true)
|
||||
} else {
|
||||
storyPreview.clear(GlideApp.with(storyPreview))
|
||||
}
|
||||
|
||||
if (model.data.secondaryStory != null) {
|
||||
val secondaryRecord = model.data.secondaryStory.messageRecord as MediaMmsMessageRecord
|
||||
storyMulti.setImageResource(GlideApp.with(storyPreview), secondaryRecord.slideDeck.thumbnailSlide!!, false, true)
|
||||
|
||||
if (secondaryRecord.storyType.isTextStory) {
|
||||
storyMulti.setImageResource(GlideApp.with(storyPreview), StoryTextPostModel(StoryTextPost.parseFrom(Base64.decode(secondaryRecord.body))), 0, 0)
|
||||
} else {
|
||||
storyMulti.setImageResource(GlideApp.with(storyPreview), secondaryRecord.slideDeck.thumbnailSlide!!, false, true)
|
||||
}
|
||||
storyMulti.visible = true
|
||||
} else {
|
||||
storyMulti.visible = false
|
||||
|
|
|
@ -10,7 +10,11 @@ import org.thoughtcrime.securesms.components.menu.SignalContextMenu
|
|||
import org.thoughtcrime.securesms.components.settings.PreferenceModel
|
||||
import org.thoughtcrime.securesms.conversation.ConversationMessage
|
||||
import org.thoughtcrime.securesms.database.model.MmsMessageRecord
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.StoryTextPost
|
||||
import org.thoughtcrime.securesms.mms.GlideApp
|
||||
import org.thoughtcrime.securesms.mms.Slide
|
||||
import org.thoughtcrime.securesms.stories.StoryTextPostModel
|
||||
import org.thoughtcrime.securesms.util.Base64
|
||||
import org.thoughtcrime.securesms.util.DateUtils
|
||||
import org.thoughtcrime.securesms.util.adapter.mapping.LayoutFactory
|
||||
import org.thoughtcrime.securesms.util.adapter.mapping.MappingAdapter
|
||||
|
@ -55,8 +59,13 @@ object MyStoriesItem {
|
|||
viewCount.text = context.resources.getQuantityString(R.plurals.MyStories__d_views, model.distributionStory.messageRecord.readReceiptCount, model.distributionStory.messageRecord.readReceiptCount)
|
||||
date.text = DateUtils.getBriefRelativeTimeSpanString(context, Locale.getDefault(), model.distributionStory.messageRecord.dateSent)
|
||||
|
||||
val thumbnail = (model.distributionStory.messageRecord as MmsMessageRecord).slideDeck.thumbnailSlide
|
||||
if (thumbnail != null) {
|
||||
val record: MmsMessageRecord = model.distributionStory.messageRecord as MmsMessageRecord
|
||||
val thumbnail: Slide? = record.slideDeck.thumbnailSlide
|
||||
|
||||
@Suppress("CascadeIf")
|
||||
if (record.storyType.isTextStory) {
|
||||
storyPreview.setImageResource(GlideApp.with(storyPreview), StoryTextPostModel(StoryTextPost.parseFrom(Base64.decode(record.body))), 0, 0)
|
||||
} else if (thumbnail != null) {
|
||||
storyPreview.setImageResource(GlideApp.with(itemView), thumbnail, false, true)
|
||||
} else {
|
||||
storyPreview.clear(GlideApp.with(itemView))
|
||||
|
|
|
@ -1,8 +1,11 @@
|
|||
package org.thoughtcrime.securesms.stories.viewer.page
|
||||
|
||||
import android.net.Uri
|
||||
import org.thoughtcrime.securesms.attachments.Attachment
|
||||
import org.thoughtcrime.securesms.conversation.ConversationMessage
|
||||
import org.thoughtcrime.securesms.database.AttachmentDatabase
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.util.MediaUtil
|
||||
|
||||
/**
|
||||
* Each story is made up of a collection of posts
|
||||
|
@ -15,7 +18,24 @@ class StoryPost(
|
|||
val viewCount: Int,
|
||||
val replyCount: Int,
|
||||
val dateInMilliseconds: Long,
|
||||
val attachment: Attachment,
|
||||
val content: Content,
|
||||
val conversationMessage: ConversationMessage,
|
||||
val allowsReplies: Boolean
|
||||
)
|
||||
) {
|
||||
sealed class Content(val uri: Uri?) {
|
||||
class AttachmentContent(val attachment: Attachment) : Content(attachment.uri) {
|
||||
override val transferState: Int = attachment.transferState
|
||||
|
||||
override fun isVideo(): Boolean = MediaUtil.isVideo(attachment)
|
||||
}
|
||||
class TextContent(uri: Uri, val recordId: Long) : Content(uri) {
|
||||
override val transferState: Int = AttachmentDatabase.TRANSFER_PROGRESS_DONE
|
||||
|
||||
override fun isVideo(): Boolean = false
|
||||
}
|
||||
|
||||
abstract val transferState: Int
|
||||
|
||||
abstract fun isVideo(): Boolean
|
||||
}
|
||||
}
|
||||
|
|
|
@ -46,12 +46,12 @@ import org.thoughtcrime.securesms.stories.viewer.StoryViewerViewModel
|
|||
import org.thoughtcrime.securesms.stories.viewer.reply.direct.StoryDirectReplyDialogFragment
|
||||
import org.thoughtcrime.securesms.stories.viewer.reply.group.StoryGroupReplyBottomSheetDialogFragment
|
||||
import org.thoughtcrime.securesms.stories.viewer.reply.tabs.StoryViewsAndRepliesDialogFragment
|
||||
import org.thoughtcrime.securesms.stories.viewer.text.StoryTextPostPreviewFragment
|
||||
import org.thoughtcrime.securesms.stories.viewer.views.StoryViewsBottomSheetDialogFragment
|
||||
import org.thoughtcrime.securesms.util.AvatarUtil
|
||||
import org.thoughtcrime.securesms.util.BottomSheetUtil
|
||||
import org.thoughtcrime.securesms.util.DateUtils
|
||||
import org.thoughtcrime.securesms.util.LifecycleDisposable
|
||||
import org.thoughtcrime.securesms.util.MediaUtil
|
||||
import org.thoughtcrime.securesms.util.fragments.requireListener
|
||||
import org.thoughtcrime.securesms.util.views.TouchInterceptingFrameLayout
|
||||
import org.thoughtcrime.securesms.util.visible
|
||||
|
@ -59,7 +59,12 @@ import java.util.Locale
|
|||
import java.util.concurrent.TimeUnit
|
||||
import kotlin.math.abs
|
||||
|
||||
class StoryViewerPageFragment : Fragment(R.layout.stories_viewer_fragment_page), MediaPreviewFragment.Events, MultiselectForwardBottomSheet.Callback, StorySlateView.Callback {
|
||||
class StoryViewerPageFragment :
|
||||
Fragment(R.layout.stories_viewer_fragment_page),
|
||||
MediaPreviewFragment.Events,
|
||||
MultiselectForwardBottomSheet.Callback,
|
||||
StorySlateView.Callback,
|
||||
StoryTextPostPreviewFragment.Callback {
|
||||
|
||||
private lateinit var progressBar: SegmentedProgressBar
|
||||
private lateinit var storySlate: StorySlateView
|
||||
|
@ -140,7 +145,7 @@ class StoryViewerPageFragment : Fragment(R.layout.stories_viewer_fragment_page),
|
|||
)
|
||||
)
|
||||
|
||||
cardWrapper.setOnInterceptTouchEventListener { storySlate.state == StorySlateView.State.HIDDEN }
|
||||
cardWrapper.setOnInterceptTouchEventListener { storySlate.state == StorySlateView.State.HIDDEN && childFragmentManager.findFragmentById(R.id.story_content_container) !is StoryTextPostPreviewFragment }
|
||||
cardWrapper.setOnTouchListener { _, event ->
|
||||
val result = gestureDetector.onTouchEvent(event)
|
||||
if (event.actionMasked == MotionEvent.ACTION_DOWN) {
|
||||
|
@ -176,8 +181,8 @@ class StoryViewerPageFragment : Fragment(R.layout.stories_viewer_fragment_page),
|
|||
}
|
||||
|
||||
override fun onRequestSegmentProgressPercentage(): Float? {
|
||||
val attachmentUri = if (viewModel.hasPost() && MediaUtil.isVideo(viewModel.getPost().attachment)) {
|
||||
viewModel.getPost().attachment.uri
|
||||
val attachmentUri = if (viewModel.hasPost() && viewModel.getPost().content.isVideo()) {
|
||||
viewModel.getPost().content.uri
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
@ -228,7 +233,7 @@ class StoryViewerPageFragment : Fragment(R.layout.stories_viewer_fragment_page),
|
|||
|
||||
val durations: Map<Int, Long> = state.posts
|
||||
.mapIndexed { index, storyPost ->
|
||||
index to if (MediaUtil.isVideo(storyPost.attachment)) -1L else TimeUnit.SECONDS.toMillis(5)
|
||||
index to if (storyPost.content.isVideo()) -1L else TimeUnit.SECONDS.toMillis(5)
|
||||
}
|
||||
.toMap()
|
||||
|
||||
|
@ -336,7 +341,7 @@ class StoryViewerPageFragment : Fragment(R.layout.stories_viewer_fragment_page),
|
|||
|
||||
private fun resumeProgress() {
|
||||
if (progressBar.segmentCount != 0 && viewModel.hasPost()) {
|
||||
val postUri = viewModel.getPost().attachment.uri
|
||||
val postUri = viewModel.getPost().content.uri
|
||||
if (postUri != null) {
|
||||
progressBar.start()
|
||||
videoControlsDelegate.resume(postUri)
|
||||
|
@ -385,12 +390,12 @@ class StoryViewerPageFragment : Fragment(R.layout.stories_viewer_fragment_page),
|
|||
|
||||
private fun presentStory(post: StoryPost, index: Int) {
|
||||
val fragment = childFragmentManager.findFragmentById(R.id.story_content_container)
|
||||
if (fragment != null && fragment.requireArguments().getParcelable<Uri>(MediaPreviewFragment.DATA_URI) == post.attachment.uri) {
|
||||
if (fragment != null && fragment.requireArguments().getParcelable<Uri>(MediaPreviewFragment.DATA_URI) == post.content.uri) {
|
||||
progressBar.setPosition(index)
|
||||
return
|
||||
}
|
||||
|
||||
if (post.attachment.uri == null) {
|
||||
if (post.content.uri == null) {
|
||||
progressBar.setPosition(index)
|
||||
progressBar.invalidate()
|
||||
} else {
|
||||
|
@ -404,7 +409,7 @@ class StoryViewerPageFragment : Fragment(R.layout.stories_viewer_fragment_page),
|
|||
}
|
||||
|
||||
private fun presentSlate(post: StoryPost) {
|
||||
when (post.attachment.transferState) {
|
||||
when (post.content.transferState) {
|
||||
AttachmentDatabase.TRANSFER_PROGRESS_DONE -> {
|
||||
storySlate.moveToState(StorySlateView.State.HIDDEN, post.id)
|
||||
viewModel.setIsDisplayingSlate(false)
|
||||
|
@ -448,7 +453,12 @@ class StoryViewerPageFragment : Fragment(R.layout.stories_viewer_fragment_page),
|
|||
|
||||
@SuppressLint("SetTextI18n")
|
||||
private fun presentCaption(caption: TextView, largeCaption: TextView, largeCaptionOverlay: View, storyPost: StoryPost) {
|
||||
val displayBody = storyPost.conversationMessage.getDisplayBody(requireContext())
|
||||
val displayBody = if (storyPost.content is StoryPost.Content.AttachmentContent) {
|
||||
storyPost.conversationMessage.getDisplayBody(requireContext())
|
||||
} else {
|
||||
""
|
||||
}
|
||||
|
||||
caption.text = displayBody
|
||||
largeCaption.text = displayBody
|
||||
caption.visible = displayBody.isNotEmpty()
|
||||
|
@ -556,7 +566,14 @@ class StoryViewerPageFragment : Fragment(R.layout.stories_viewer_fragment_page),
|
|||
}
|
||||
|
||||
private fun createFragmentForPost(storyPost: StoryPost): Fragment {
|
||||
return MediaPreviewFragment.newInstance(storyPost.attachment, false)
|
||||
return when (storyPost.content) {
|
||||
is StoryPost.Content.AttachmentContent -> MediaPreviewFragment.newInstance(storyPost.content.attachment, false)
|
||||
is StoryPost.Content.TextContent -> StoryTextPostPreviewFragment.create(storyPost.content)
|
||||
}
|
||||
}
|
||||
|
||||
override fun setIsDisplayingLinkPreviewTooltip(isDisplayingLinkPreviewTooltip: Boolean) {
|
||||
viewModel.setIsDisplayingLinkPreviewTooltip(isDisplayingLinkPreviewTooltip)
|
||||
}
|
||||
|
||||
override fun getVideoControlsDelegate(): VideoControlsDelegate {
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package org.thoughtcrime.securesms.stories.viewer.page
|
||||
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import io.reactivex.rxjava3.core.Completable
|
||||
import io.reactivex.rxjava3.core.Observable
|
||||
import io.reactivex.rxjava3.schedulers.Schedulers
|
||||
|
@ -71,7 +72,7 @@ class StoryViewerPageRepository(context: Context) {
|
|||
viewCount = record.viewedReceiptCount,
|
||||
replyCount = SignalDatabase.mms.getNumberOfStoryReplies(record.id),
|
||||
dateInMilliseconds = record.dateSent,
|
||||
attachment = (record as MmsMessageRecord).slideDeck.firstSlide!!.asAttachment(),
|
||||
content = getContent(record as MmsMessageRecord),
|
||||
conversationMessage = ConversationMessage.ConversationMessageFactory.createWithUnresolvedData(context, record),
|
||||
allowsReplies = record.storyType.isStoryWithReplies
|
||||
)
|
||||
|
@ -125,9 +126,11 @@ class StoryViewerPageRepository(context: Context) {
|
|||
}
|
||||
|
||||
fun forceDownload(post: StoryPost) {
|
||||
ApplicationDependencies.getJobManager().add(
|
||||
AttachmentDownloadJob(post.id, (post.attachment as DatabaseAttachment).attachmentId, true)
|
||||
)
|
||||
if (post.content is StoryPost.Content.AttachmentContent) {
|
||||
ApplicationDependencies.getJobManager().add(
|
||||
AttachmentDownloadJob(post.id, (post.content.attachment as DatabaseAttachment).attachmentId, true)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun getStoryPostsFor(recipientId: RecipientId): Observable<List<StoryPost>> {
|
||||
|
@ -167,4 +170,17 @@ class StoryViewerPageRepository(context: Context) {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun getContent(record: MmsMessageRecord): StoryPost.Content {
|
||||
return if (record.storyType.isTextStory) {
|
||||
StoryPost.Content.TextContent(
|
||||
uri = Uri.parse("story_text_post://${record.id}"),
|
||||
recordId = record.id
|
||||
)
|
||||
} else {
|
||||
StoryPost.Content.AttachmentContent(
|
||||
attachment = record.slideDeck.asAttachments().first()
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -158,6 +158,10 @@ class StoryViewerPageViewModel(
|
|||
storyViewerPlaybackStore.update { it.copy(areSegmentsInitialized = areSegmentsInitialized) }
|
||||
}
|
||||
|
||||
fun setIsDisplayingLinkPreviewTooltip(isDisplayingLinkPreviewTooltip: Boolean) {
|
||||
storyViewerPlaybackStore.update { it.copy(isDisplayingLinkPreviewTooltip = isDisplayingLinkPreviewTooltip) }
|
||||
}
|
||||
|
||||
private fun resolveSwipeToReplyState(state: StoryViewerPageState, index: Int = state.selectedPostIndex): StoryViewerPageState.ReplyState {
|
||||
if (index !in state.posts.indices) {
|
||||
return StoryViewerPageState.ReplyState.NONE
|
||||
|
|
|
@ -12,7 +12,8 @@ data class StoryViewerPlaybackState(
|
|||
val isUserScrollingParent: Boolean = false,
|
||||
val isSelectedPage: Boolean = false,
|
||||
val isDisplayingSlate: Boolean = false,
|
||||
val isFragmentResumed: Boolean = false
|
||||
val isFragmentResumed: Boolean = false,
|
||||
val isDisplayingLinkPreviewTooltip: Boolean = false
|
||||
) {
|
||||
val isPaused: Boolean = !areSegmentsInitialized ||
|
||||
isUserTouching ||
|
||||
|
@ -26,5 +27,6 @@ data class StoryViewerPlaybackState(
|
|||
isUserScrollingParent ||
|
||||
!isSelectedPage ||
|
||||
isDisplayingSlate ||
|
||||
!isFragmentResumed
|
||||
!isFragmentResumed ||
|
||||
isDisplayingLinkPreviewTooltip
|
||||
}
|
||||
|
|
|
@ -92,7 +92,7 @@ class StoryReplyComposer @JvmOverloads constructor(
|
|||
GlideApp.with(this),
|
||||
messageRecord.dateSent,
|
||||
messageRecord.recipient,
|
||||
null,
|
||||
messageRecord.body,
|
||||
false,
|
||||
messageRecord.slideDeck,
|
||||
null
|
||||
|
|
|
@ -37,7 +37,7 @@ class StoryDirectReplyDialogFragment :
|
|||
|
||||
private val viewModel: StoryDirectReplyViewModel by viewModels(
|
||||
factoryProducer = {
|
||||
StoryDirectReplyViewModel.Factory(storyId, recipientId, StoryDirectReplyRepository())
|
||||
StoryDirectReplyViewModel.Factory(storyId, recipientId, StoryDirectReplyRepository(requireContext()))
|
||||
}
|
||||
)
|
||||
|
||||
|
|
|
@ -15,7 +15,9 @@ import org.thoughtcrime.securesms.recipients.Recipient
|
|||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
import org.thoughtcrime.securesms.sms.MessageSender
|
||||
|
||||
class StoryDirectReplyRepository {
|
||||
class StoryDirectReplyRepository(context: Context) {
|
||||
|
||||
private val context = context.applicationContext
|
||||
|
||||
fun getStoryPost(storyId: Long): Single<MessageRecord> {
|
||||
return Single.fromCallable {
|
||||
|
@ -23,7 +25,7 @@ class StoryDirectReplyRepository {
|
|||
}.subscribeOn(Schedulers.io())
|
||||
}
|
||||
|
||||
fun send(context: Context, storyId: Long, groupDirectReplyRecipientId: RecipientId?, charSequence: CharSequence): Completable {
|
||||
fun send(storyId: Long, groupDirectReplyRecipientId: RecipientId?, charSequence: CharSequence): Completable {
|
||||
return Completable.create { emitter ->
|
||||
val message = SignalDatabase.mms.getMessageRecord(storyId) as MediaMmsMessageRecord
|
||||
val (recipient, threadId) = if (groupDirectReplyRecipientId == null) {
|
||||
|
@ -56,7 +58,7 @@ class StoryDirectReplyRepository {
|
|||
0,
|
||||
StoryType.NONE,
|
||||
ParentStoryId.DirectReply(storyId),
|
||||
QuoteModel(message.dateSent, quoteAuthor.id, "", false, message.slideDeck.asAttachments(), null),
|
||||
QuoteModel(message.dateSent, quoteAuthor.id, message.body, false, message.slideDeck.asAttachments(), null),
|
||||
emptyList(),
|
||||
emptyList(),
|
||||
emptyList(),
|
||||
|
|
|
@ -1,26 +1,21 @@
|
|||
package org.thoughtcrime.securesms.stories.viewer.reply.direct
|
||||
|
||||
import android.content.Context
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import io.reactivex.rxjava3.core.Completable
|
||||
import io.reactivex.rxjava3.disposables.CompositeDisposable
|
||||
import io.reactivex.rxjava3.kotlin.plusAssign
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
import org.thoughtcrime.securesms.util.livedata.Store
|
||||
|
||||
class StoryDirectReplyViewModel(
|
||||
context: Context,
|
||||
private val storyId: Long,
|
||||
private val groupDirectReplyRecipientId: RecipientId?,
|
||||
private val repository: StoryDirectReplyRepository
|
||||
) : ViewModel() {
|
||||
|
||||
private val context = context.applicationContext
|
||||
|
||||
private val store = Store(StoryDirectReplyState())
|
||||
private val disposables = CompositeDisposable()
|
||||
|
||||
|
@ -39,7 +34,7 @@ class StoryDirectReplyViewModel(
|
|||
}
|
||||
|
||||
fun send(charSequence: CharSequence): Completable {
|
||||
return repository.send(context, storyId, groupDirectReplyRecipientId, charSequence)
|
||||
return repository.send(storyId, groupDirectReplyRecipientId, charSequence)
|
||||
}
|
||||
|
||||
override fun onCleared() {
|
||||
|
@ -54,7 +49,7 @@ class StoryDirectReplyViewModel(
|
|||
) : ViewModelProvider.Factory {
|
||||
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
|
||||
return modelClass.cast(
|
||||
StoryDirectReplyViewModel(ApplicationDependencies.getApplication(), storyId, groupDirectReplyRecipientId, repository)
|
||||
StoryDirectReplyViewModel(storyId, groupDirectReplyRecipientId, repository)
|
||||
) as T
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,116 @@
|
|||
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
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.linkpreview.LinkPreview
|
||||
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.fragments.requireListener
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
class StoryTextPostPreviewFragment : Fragment(R.layout.stories_text_post_preview_fragment) {
|
||||
|
||||
companion object {
|
||||
private const val STORY_ID = "STORY_ID"
|
||||
|
||||
fun create(content: StoryPost.Content.TextContent): Fragment {
|
||||
return StoryTextPostPreviewFragment().apply {
|
||||
arguments = Bundle().apply {
|
||||
putParcelable(MediaPreviewFragment.DATA_URI, content.uri)
|
||||
putLong(STORY_ID, content.recordId)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val viewModel: StoryTextPostViewModel by viewModels(
|
||||
factoryProducer = {
|
||||
StoryTextPostViewModel.Factory(requireArguments().getLong(STORY_ID), StoryTextPostRepository())
|
||||
}
|
||||
)
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
val storyTextPostView: StoryTextPostView = view.findViewById(R.id.story_text_post)
|
||||
|
||||
viewModel.state.observe(viewLifecycleOwner) { state ->
|
||||
when (state.loadState) {
|
||||
StoryTextPostState.LoadState.INIT -> Unit
|
||||
StoryTextPostState.LoadState.LOADED -> {
|
||||
storyTextPostView.bindFromStoryTextPost(state.storyTextPost!!)
|
||||
storyTextPostView.bindLinkPreview(state.linkPreview)
|
||||
|
||||
if (state.linkPreview != null) {
|
||||
storyTextPostView.setLinkPreviewClickListener {
|
||||
showLinkPreviewTooltip(it, state.linkPreview)
|
||||
}
|
||||
} else {
|
||||
storyTextPostView.setLinkPreviewClickListener(null)
|
||||
}
|
||||
}
|
||||
StoryTextPostState.LoadState.FAILED -> {
|
||||
requireListener<MediaPreviewFragment.Events>().mediaNotAvailable()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("AlertDialogBuilderUsage")
|
||||
private fun showLinkPreviewTooltip(view: View, linkPreview: LinkPreview) {
|
||||
requireListener<Callback>().setIsDisplayingLinkPreviewTooltip(true)
|
||||
|
||||
val contentView = LayoutInflater.from(requireContext()).inflate(R.layout.stories_link_popup, null, false)
|
||||
|
||||
contentView.findViewById<TextView>(R.id.url).text = linkPreview.url
|
||||
contentView.setOnClickListener {
|
||||
CommunicationActions.openBrowserLink(requireContext(), linkPreview.url)
|
||||
}
|
||||
|
||||
contentView.measure(
|
||||
View.MeasureSpec.makeMeasureSpec(DimensionUnit.DP.toPixels(275f).toInt(), View.MeasureSpec.EXACTLY),
|
||||
0
|
||||
)
|
||||
|
||||
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()
|
||||
}
|
||||
|
||||
interface Callback {
|
||||
fun setIsDisplayingLinkPreviewTooltip(isDisplayingLinkPreviewTooltip: Boolean)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
package org.thoughtcrime.securesms.stories.viewer.text
|
||||
|
||||
import io.reactivex.rxjava3.core.Single
|
||||
import io.reactivex.rxjava3.schedulers.Schedulers
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||
import org.thoughtcrime.securesms.database.model.MmsMessageRecord
|
||||
|
||||
class StoryTextPostRepository {
|
||||
fun getRecord(recordId: Long): Single<MmsMessageRecord> {
|
||||
return Single.fromCallable {
|
||||
SignalDatabase.mms.getMessageRecord(recordId) as MmsMessageRecord
|
||||
}.subscribeOn(Schedulers.io())
|
||||
}
|
||||
}
|
|
@ -0,0 +1,16 @@
|
|||
package org.thoughtcrime.securesms.stories.viewer.text
|
||||
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.StoryTextPost
|
||||
import org.thoughtcrime.securesms.linkpreview.LinkPreview
|
||||
|
||||
data class StoryTextPostState(
|
||||
val storyTextPost: StoryTextPost? = null,
|
||||
val linkPreview: LinkPreview? = null,
|
||||
val loadState: LoadState = LoadState.INIT
|
||||
) {
|
||||
enum class LoadState {
|
||||
INIT,
|
||||
LOADED,
|
||||
FAILED
|
||||
}
|
||||
}
|
|
@ -0,0 +1,50 @@
|
|||
package org.thoughtcrime.securesms.stories.viewer.text
|
||||
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import io.reactivex.rxjava3.disposables.CompositeDisposable
|
||||
import io.reactivex.rxjava3.kotlin.plusAssign
|
||||
import io.reactivex.rxjava3.kotlin.subscribeBy
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.StoryTextPost
|
||||
import org.thoughtcrime.securesms.util.Base64
|
||||
import org.thoughtcrime.securesms.util.livedata.Store
|
||||
|
||||
class StoryTextPostViewModel(recordId: Long, repository: StoryTextPostRepository) : ViewModel() {
|
||||
|
||||
private val store = Store(StoryTextPostState())
|
||||
private val disposables = CompositeDisposable()
|
||||
|
||||
val state: LiveData<StoryTextPostState> = store.stateLiveData
|
||||
|
||||
init {
|
||||
disposables += repository.getRecord(recordId).subscribeBy(
|
||||
onSuccess = { record ->
|
||||
store.update { state ->
|
||||
state.copy(
|
||||
storyTextPost = StoryTextPost.parseFrom(Base64.decode(record.body)),
|
||||
linkPreview = record.linkPreviews.firstOrNull(),
|
||||
loadState = StoryTextPostState.LoadState.LOADED
|
||||
)
|
||||
}
|
||||
},
|
||||
onError = {
|
||||
store.update { state ->
|
||||
state.copy(
|
||||
loadState = StoryTextPostState.LoadState.FAILED
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
override fun onCleared() {
|
||||
disposables.clear()
|
||||
}
|
||||
|
||||
class Factory(private val recordId: Long, private val repository: StoryTextPostRepository) : ViewModelProvider.Factory {
|
||||
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
|
||||
return modelClass.cast(StoryTextPostViewModel(recordId, repository)) as T
|
||||
}
|
||||
}
|
||||
}
|
|
@ -86,9 +86,9 @@ public final class FeatureFlags {
|
|||
private static final String DONOR_BADGES = "android.donorBadges.6";
|
||||
private static final String DONOR_BADGES_DISPLAY = "android.donorBadges.display.4";
|
||||
private static final String CDSH = "android.cdsh";
|
||||
private static final String STORIES = "android.stories";
|
||||
private static final String STORIES = "android.stories.2";
|
||||
private static final String STORIES_TEXT_FUNCTIONS = "android.stories.text.functions";
|
||||
private static final String STORIES_TEXT_POSTS = "android.stories.text.posts";
|
||||
private static final String STORIES_TEXT_POSTS = "android.stories.text.posts.2";
|
||||
private static final String HARDWARE_AEC_BLOCKLIST_MODELS = "android.calling.hardwareAecBlockList";
|
||||
private static final String SOFTWARE_AEC_BLOCKLIST_MODELS = "android.calling.softwareAecBlockList";
|
||||
private static final String USE_HARDWARE_AEC_IF_OLD = "android.calling.useHardwareAecIfOlderThanApi29";
|
||||
|
|
|
@ -204,4 +204,21 @@ message CustomAvatar {
|
|||
Vector vector = 2;
|
||||
Photo photo = 3;
|
||||
}
|
||||
}
|
||||
|
||||
message StoryTextPost {
|
||||
enum Style {
|
||||
DEFAULT = 0;
|
||||
REGULAR = 1;
|
||||
BOLD = 2;
|
||||
SERIF = 3;
|
||||
SCRIPT = 4;
|
||||
CONDENSED = 5;
|
||||
}
|
||||
|
||||
string body = 1;
|
||||
Style style = 2;
|
||||
int32 textForegroundColor = 3;
|
||||
int32 textBackgroundColor = 4;
|
||||
ChatColor background = 5;
|
||||
}
|
|
@ -0,0 +1,77 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout 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="275dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:clipChildren="false"
|
||||
android:clipToPadding="false"
|
||||
android:paddingBottom="5dp">
|
||||
|
||||
<View
|
||||
android:id="@+id/bottom_arrow"
|
||||
android:layout_width="24dp"
|
||||
android:layout_height="24dp"
|
||||
android:layout_marginTop="24dp"
|
||||
android:background="@color/core_black"
|
||||
android:rotation="45"
|
||||
app:layout_constraintBottom_toBottomOf="@id/bubble"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/bubble" />
|
||||
|
||||
<androidx.cardview.widget.CardView
|
||||
android:id="@+id/bubble"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:alpha="0.9"
|
||||
app:cardBackgroundColor="@color/core_black"
|
||||
app:cardCornerRadius="12dp"
|
||||
app:cardElevation="0dp"
|
||||
app:layout_constraintBottom_toBottomOf="@id/url"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="@id/visit_link" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/visit_link"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:maxLines="1"
|
||||
android:paddingHorizontal="16dp"
|
||||
android:paddingTop="14dp"
|
||||
android:text="@string/StoriesLinkPopup__visit_link"
|
||||
android:textAppearance="@style/TextAppearance.Signal.Body2.Bold"
|
||||
android:textColor="@color/core_white"
|
||||
app:layout_constraintEnd_toStartOf="@id/chevron"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/url"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:ellipsize="end"
|
||||
android:maxLines="1"
|
||||
android:paddingHorizontal="16dp"
|
||||
android:paddingTop="1dp"
|
||||
android:paddingBottom="15dp"
|
||||
android:textAppearance="@style/TextAppearance.Signal.Caption"
|
||||
android:textColor="@color/core_white"
|
||||
app:layout_constraintEnd_toStartOf="@id/chevron"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/visit_link"
|
||||
tools:text="https://www.signal.org/blog/entry/20/20/20/20/20" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/chevron"
|
||||
android:layout_width="16dp"
|
||||
android:layout_height="16dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:importantForAccessibility="no"
|
||||
app:layout_constraintBottom_toBottomOf="@id/bubble"
|
||||
app:layout_constraintEnd_toEndOf="@id/bubble"
|
||||
app:layout_constraintTop_toTopOf="@id/bubble"
|
||||
app:srcCompat="@drawable/exo_ic_chevron_right" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
@ -8,7 +8,7 @@
|
|||
android:paddingEnd="@dimen/dsl_settings_gutter"
|
||||
android:background="?selectableItemBackground">
|
||||
|
||||
<org.thoughtcrime.securesms.components.ThumbnailView
|
||||
<org.thoughtcrime.securesms.components.OutlinedThumbnailView
|
||||
android:id="@+id/story"
|
||||
android:layout_width="56dp"
|
||||
android:layout_height="84dp"
|
||||
|
@ -17,6 +17,8 @@
|
|||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:otv_cornerRadius="12dp"
|
||||
app:otv_strokeColor="@color/signal_background_primary"
|
||||
tools:background="@color/red" />
|
||||
|
||||
<org.thoughtcrime.securesms.components.emoji.EmojiTextView
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<org.thoughtcrime.securesms.stories.StoryTextPostView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:id="@+id/story_text_post"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent" />
|
|
@ -4614,6 +4614,7 @@
|
|||
<!-- endregion -->
|
||||
<!-- Content description for expand contacts chevron -->
|
||||
<string name="ExpandModel__view_more">View more</string>
|
||||
<string name="StoriesLinkPopup__visit_link">Visit link</string>
|
||||
|
||||
<!-- EOF -->
|
||||
|
||||
|
|
Ładowanie…
Reference in New Issue