mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-02-26 04:33:36 +00:00
Add partial share and draft support to CFv2.
This commit is contained in:
@@ -191,6 +191,11 @@ public class ComposeText extends EmojiEditText {
|
||||
setHintWithChecks(hint);
|
||||
}
|
||||
|
||||
public void setDraftText(@Nullable CharSequence draftText) {
|
||||
setText("");
|
||||
append(draftText);
|
||||
}
|
||||
|
||||
public void appendInvite(String invite) {
|
||||
if (getText() == null) {
|
||||
return;
|
||||
|
||||
@@ -205,7 +205,7 @@ public class LinkPreviewView extends FrameLayout {
|
||||
if (showThumbnail && linkPreview.getThumbnail().isPresent()) {
|
||||
thumbnail.setVisibility(VISIBLE);
|
||||
thumbnailState.applyState(thumbnail);
|
||||
thumbnail.get().setImageResource(glideRequests, new ImageSlide(getContext(), linkPreview.getThumbnail().get()), type == TYPE_CONVERSATION, false);
|
||||
thumbnail.get().setImageResource(glideRequests, new ImageSlide(linkPreview.getThumbnail().get()), type == TYPE_CONVERSATION, false);
|
||||
thumbnail.get().showDownloadText(false);
|
||||
} else {
|
||||
thumbnail.setVisibility(GONE);
|
||||
|
||||
@@ -201,7 +201,7 @@ public final class TransferControlView extends FrameLayout {
|
||||
|
||||
private String getDownloadText(@NonNull List<Slide> slides) {
|
||||
if (slides.size() == 1) {
|
||||
return slides.get(0).getContentDescription();
|
||||
return slides.get(0).getContentDescription(getContext());
|
||||
} else {
|
||||
int downloadCount = Stream.of(slides).reduce(0, (count, slide) -> slide.getTransferState() != AttachmentTable.TRANSFER_PROGRESS_DONE ? count + 1 : count);
|
||||
return getContext().getResources().getQuantityString(R.plurals.TransferControlView_n_items, downloadCount, downloadCount);
|
||||
|
||||
@@ -17,6 +17,7 @@ import org.thoughtcrime.securesms.database.SignalDatabase;
|
||||
import org.thoughtcrime.securesms.database.ThreadTable;
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
||||
import org.thoughtcrime.securesms.mediasend.Media;
|
||||
import org.thoughtcrime.securesms.mms.SlideFactory;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
import org.thoughtcrime.securesms.stickers.StickerLocator;
|
||||
@@ -105,6 +106,9 @@ public class ConversationIntents {
|
||||
private final RecipientId recipientId;
|
||||
private final long threadId;
|
||||
private final String draftText;
|
||||
private final Uri draftMedia;
|
||||
private final String draftContentType;
|
||||
private final SlideFactory.MediaType draftMediaType;
|
||||
private final ArrayList<Media> media;
|
||||
private final StickerLocator stickerLocator;
|
||||
private final boolean isBorderless;
|
||||
@@ -124,6 +128,8 @@ public class ConversationIntents {
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
false,
|
||||
ThreadTable.DistributionTypes.DEFAULT,
|
||||
-1,
|
||||
@@ -137,6 +143,8 @@ public class ConversationIntents {
|
||||
return new Args(RecipientId.from(Objects.requireNonNull(arguments.getString(EXTRA_RECIPIENT))),
|
||||
arguments.getLong(EXTRA_THREAD_ID, -1),
|
||||
arguments.getString(EXTRA_TEXT),
|
||||
ConversationIntents.getIntentData(arguments),
|
||||
ConversationIntents.getIntentType(arguments),
|
||||
arguments.getParcelableArrayList(EXTRA_MEDIA),
|
||||
arguments.getParcelable(EXTRA_STICKER),
|
||||
arguments.getBoolean(EXTRA_BORDERLESS, false),
|
||||
@@ -152,6 +160,8 @@ public class ConversationIntents {
|
||||
private Args(@NonNull RecipientId recipientId,
|
||||
long threadId,
|
||||
@Nullable String draftText,
|
||||
@Nullable Uri draftMedia,
|
||||
@Nullable String draftContentType,
|
||||
@Nullable ArrayList<Media> media,
|
||||
@Nullable StickerLocator stickerLocator,
|
||||
boolean isBorderless,
|
||||
@@ -166,6 +176,8 @@ public class ConversationIntents {
|
||||
this.recipientId = recipientId;
|
||||
this.threadId = threadId;
|
||||
this.draftText = draftText;
|
||||
this.draftMedia = draftMedia;
|
||||
this.draftContentType = draftContentType;
|
||||
this.media = media;
|
||||
this.stickerLocator = stickerLocator;
|
||||
this.isBorderless = isBorderless;
|
||||
@@ -176,6 +188,7 @@ public class ConversationIntents {
|
||||
this.giftBadge = giftBadge;
|
||||
this.shareDataTimestamp = shareDataTimestamp;
|
||||
this.conversationScreenType = conversationScreenType;
|
||||
this.draftMediaType = SlideFactory.MediaType.from(draftContentType);
|
||||
}
|
||||
|
||||
public @NonNull RecipientId getRecipientId() {
|
||||
@@ -190,6 +203,18 @@ public class ConversationIntents {
|
||||
return draftText;
|
||||
}
|
||||
|
||||
public @Nullable Uri getDraftMedia() {
|
||||
return draftMedia;
|
||||
}
|
||||
|
||||
public @Nullable String getDraftContentType() {
|
||||
return draftContentType;
|
||||
}
|
||||
|
||||
public @Nullable SlideFactory.MediaType getDraftMediaType() {
|
||||
return draftMediaType;
|
||||
}
|
||||
|
||||
public @Nullable ArrayList<Media> getMedia() {
|
||||
return media;
|
||||
}
|
||||
@@ -237,6 +262,10 @@ public class ConversationIntents {
|
||||
public @NonNull ConversationScreenType getConversationScreenType() {
|
||||
return conversationScreenType;
|
||||
}
|
||||
|
||||
public boolean canInitializeFromDatabase() {
|
||||
return draftText == null && (draftMedia == null || ConversationIntents.isBubbleIntentUri(draftMedia) || ConversationIntents.isNotificationIntentUri(draftMedia)) && draftMediaType == null;
|
||||
}
|
||||
}
|
||||
|
||||
public final static class Builder {
|
||||
|
||||
@@ -1127,7 +1127,7 @@ public final class ConversationItem extends RelativeLayout implements BindableCo
|
||||
mediaThumbnailStub.require().setVisibility(VISIBLE);
|
||||
mediaThumbnailStub.require().setMinimumThumbnailWidth(readDimen(R.dimen.media_bubble_min_width_with_content));
|
||||
mediaThumbnailStub.require().setMaximumThumbnailHeight(readDimen(R.dimen.media_bubble_max_height));
|
||||
mediaThumbnailStub.require().setImageResource(glideRequests, Collections.singletonList(new ImageSlide(context, linkPreview.getThumbnail().get())), showControls, false);
|
||||
mediaThumbnailStub.require().setImageResource(glideRequests, Collections.singletonList(new ImageSlide(linkPreview.getThumbnail().get())), showControls, false);
|
||||
mediaThumbnailStub.require().setThumbnailClickListener(new LinkPreviewThumbnailClickListener());
|
||||
mediaThumbnailStub.require().setDownloadClickListener(downloadClickListener);
|
||||
mediaThumbnailStub.require().setOnLongClickListener(passthroughClickListener);
|
||||
|
||||
@@ -1,16 +1,22 @@
|
||||
package org.thoughtcrime.securesms.conversation.drafts
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.Color
|
||||
import android.net.Uri
|
||||
import android.text.Spannable
|
||||
import android.text.SpannableString
|
||||
import androidx.annotation.WorkerThread
|
||||
import com.bumptech.glide.load.engine.DiskCacheStrategy
|
||||
import io.reactivex.rxjava3.core.Maybe
|
||||
import io.reactivex.rxjava3.core.Single
|
||||
import io.reactivex.rxjava3.schedulers.Schedulers
|
||||
import org.signal.core.util.StreamUtil
|
||||
import org.signal.core.util.concurrent.SignalExecutors
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.thoughtcrime.securesms.components.location.SignalPlace
|
||||
import org.thoughtcrime.securesms.components.mention.MentionAnnotation
|
||||
import org.thoughtcrime.securesms.conversation.ConversationIntents
|
||||
import org.thoughtcrime.securesms.conversation.ConversationMessage
|
||||
import org.thoughtcrime.securesms.conversation.ConversationMessage.ConversationMessageFactory
|
||||
import org.thoughtcrime.securesms.conversation.MessageStyler
|
||||
@@ -27,28 +33,126 @@ import org.thoughtcrime.securesms.database.model.MessageId
|
||||
import org.thoughtcrime.securesms.database.model.MessageRecord
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.BodyRangeList
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
|
||||
import org.thoughtcrime.securesms.mediasend.Media
|
||||
import org.thoughtcrime.securesms.mms.DecryptableStreamUriLoader.DecryptableUri
|
||||
import org.thoughtcrime.securesms.mms.GifSlide
|
||||
import org.thoughtcrime.securesms.mms.GlideApp
|
||||
import org.thoughtcrime.securesms.mms.GlideRequests
|
||||
import org.thoughtcrime.securesms.mms.ImageSlide
|
||||
import org.thoughtcrime.securesms.mms.PartAuthority
|
||||
import org.thoughtcrime.securesms.mms.QuoteId
|
||||
import org.thoughtcrime.securesms.mms.Slide
|
||||
import org.thoughtcrime.securesms.mms.SlideFactory
|
||||
import org.thoughtcrime.securesms.mms.StickerSlide
|
||||
import org.thoughtcrime.securesms.providers.BlobProvider
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.util.Base64
|
||||
import org.thoughtcrime.securesms.util.MediaUtil
|
||||
import org.thoughtcrime.securesms.util.concurrent.SerialMonoLifoExecutor
|
||||
import org.thoughtcrime.securesms.util.hasTextSlide
|
||||
import org.thoughtcrime.securesms.util.requireTextSlide
|
||||
import java.io.IOException
|
||||
import java.util.concurrent.ExecutionException
|
||||
import java.util.concurrent.Executor
|
||||
import java.util.concurrent.TimeUnit
|
||||
import java.util.concurrent.TimeoutException
|
||||
|
||||
class DraftRepository(
|
||||
private val context: Context = ApplicationDependencies.getApplication(),
|
||||
private val threadTable: ThreadTable = SignalDatabase.threads,
|
||||
private val draftTable: DraftTable = SignalDatabase.drafts,
|
||||
private val saveDraftsExecutor: Executor = SerialMonoLifoExecutor(SignalExecutors.BOUNDED)
|
||||
private val saveDraftsExecutor: Executor = SerialMonoLifoExecutor(SignalExecutors.BOUNDED),
|
||||
private val conversationArguments: ConversationIntents.Args? = null
|
||||
) {
|
||||
|
||||
companion object {
|
||||
val TAG = Log.tag(DraftRepository::class.java)
|
||||
}
|
||||
|
||||
fun getShareOrDraftData(): Maybe<Pair<ShareOrDraftData, Drafts?>> {
|
||||
return Maybe.fromCallable<Pair<ShareOrDraftData, Drafts?>> { getShareOrDraftDataInternal() }
|
||||
.observeOn(Schedulers.io())
|
||||
}
|
||||
|
||||
private fun getShareOrDraftDataInternal(): Pair<ShareOrDraftData, Drafts?>? {
|
||||
val shareText = conversationArguments?.draftText
|
||||
val shareMedia = conversationArguments?.draftMedia
|
||||
val shareContentType = conversationArguments?.draftContentType
|
||||
val shareMediaType = conversationArguments?.draftMediaType
|
||||
val shareMediaList = conversationArguments?.media ?: emptyList()
|
||||
val stickerLocator = conversationArguments?.stickerLocator
|
||||
val borderless = conversationArguments?.isBorderless ?: false
|
||||
|
||||
if (stickerLocator != null && shareMedia != null) {
|
||||
val slide = StickerSlide(context, shareMedia, 0, stickerLocator, shareContentType!!)
|
||||
return ShareOrDraftData.SendSticker(slide) to null
|
||||
}
|
||||
|
||||
if (shareMedia != null && shareContentType != null && borderless) {
|
||||
val details = getKeyboardImageDetails(GlideApp.with(context), shareMedia)
|
||||
|
||||
if (details == null || !details.hasTransparency) {
|
||||
return ShareOrDraftData.SetMedia(shareMedia, shareMediaType!!, null) to null
|
||||
}
|
||||
|
||||
val slide: Slide? = if (MediaUtil.isGif(shareContentType)) {
|
||||
GifSlide(context, shareMedia, 0, details.width, details.height, true, null)
|
||||
} else if (MediaUtil.isImageType(shareContentType)) {
|
||||
ImageSlide(context, shareMedia, shareContentType, 0, details.width, details.height, true, null, null)
|
||||
} else {
|
||||
Log.w(TAG, "Attempting to send unsupported non-image via keyboard share")
|
||||
null
|
||||
}
|
||||
|
||||
return if (slide != null) ShareOrDraftData.SendKeyboardImage(slide) to null else null
|
||||
}
|
||||
|
||||
if (shareMediaList.isNotEmpty()) {
|
||||
return ShareOrDraftData.StartSendMedia(shareMediaList, shareText) to null
|
||||
}
|
||||
|
||||
if (shareMedia != null && shareMediaType != null) {
|
||||
return ShareOrDraftData.SetMedia(shareMedia, shareMediaType, shareText) to null
|
||||
}
|
||||
|
||||
if (shareText != null) {
|
||||
return ShareOrDraftData.SetText(shareText) to null
|
||||
}
|
||||
|
||||
if (conversationArguments?.canInitializeFromDatabase() == true) {
|
||||
val (drafts, updatedText) = loadDraftsInternal(conversationArguments.threadId)
|
||||
|
||||
val draftText: CharSequence? = drafts.firstOrNull { it.type == DraftTable.Draft.TEXT }?.let { updatedText ?: it.value }
|
||||
|
||||
val location: SignalPlace? = drafts.firstOrNull { it.type == DraftTable.Draft.LOCATION }?.let { SignalPlace.deserialize(it.value) }
|
||||
if (location != null) {
|
||||
return ShareOrDraftData.SetLocation(location, draftText) to drafts
|
||||
}
|
||||
|
||||
val audio: Uri? = drafts.firstOrNull { it.type == DraftTable.Draft.AUDIO }?.let { Uri.parse(it.value) }
|
||||
if (audio != null) {
|
||||
return ShareOrDraftData.SetMedia(audio, SlideFactory.MediaType.AUDIO, null) to drafts
|
||||
}
|
||||
|
||||
val quote: ConversationMessage? = drafts.firstOrNull { it.type == DraftTable.Draft.QUOTE }?.let { loadDraftQuoteInternal(it.value) }
|
||||
if (quote != null) {
|
||||
return ShareOrDraftData.SetQuote(quote, draftText) to drafts
|
||||
}
|
||||
|
||||
val messageEdit: ConversationMessage? = drafts.firstOrNull { it.type == DraftTable.Draft.MESSAGE_EDIT }?.let { loadDraftMessageEditInternal(it.value) }
|
||||
if (messageEdit != null) {
|
||||
return ShareOrDraftData.SetEditMessage(messageEdit) to drafts
|
||||
}
|
||||
|
||||
if (draftText != null) {
|
||||
return ShareOrDraftData.SetText(draftText) to drafts
|
||||
}
|
||||
}
|
||||
|
||||
// no share or draft
|
||||
return null
|
||||
}
|
||||
|
||||
fun deleteVoiceNoteDraftData(draft: DraftTable.Draft?) {
|
||||
if (draft != null) {
|
||||
SignalExecutors.BOUNDED.execute {
|
||||
@@ -57,11 +161,13 @@ class DraftRepository(
|
||||
}
|
||||
}
|
||||
|
||||
fun saveDrafts(recipient: Recipient, threadId: Long, distributionType: Int, drafts: Drafts) {
|
||||
fun saveDrafts(recipient: Recipient?, threadId: Long, distributionType: Int, drafts: Drafts) {
|
||||
require(threadId != -1L || recipient != null)
|
||||
|
||||
saveDraftsExecutor.execute {
|
||||
if (drafts.isNotEmpty()) {
|
||||
val actualThreadId = if (threadId == -1L) {
|
||||
threadTable.getOrCreateThreadIdFor(recipient, distributionType)
|
||||
threadTable.getOrCreateThreadIdFor(recipient!!, distributionType)
|
||||
} else {
|
||||
threadId
|
||||
}
|
||||
@@ -79,65 +185,109 @@ class DraftRepository(
|
||||
}
|
||||
}
|
||||
|
||||
@Deprecated("Not needed for CFv2")
|
||||
fun loadDrafts(threadId: Long): Single<DatabaseDraft> {
|
||||
return Single.fromCallable {
|
||||
val drafts: Drafts = draftTable.getDrafts(threadId)
|
||||
val bodyRangesDraft: DraftTable.Draft? = drafts.getDraftOfType(DraftTable.Draft.BODY_RANGES)
|
||||
val textDraft: DraftTable.Draft? = drafts.getDraftOfType(DraftTable.Draft.TEXT)
|
||||
var updatedText: Spannable? = null
|
||||
|
||||
if (textDraft != null && bodyRangesDraft != null) {
|
||||
val bodyRanges: BodyRangeList = BodyRangeList.parseFrom(Base64.decodeOrThrow(bodyRangesDraft.value))
|
||||
val mentions: List<Mention> = MentionUtil.bodyRangeListToMentions(bodyRanges)
|
||||
|
||||
val updated = MentionUtil.updateBodyAndMentionsWithDisplayNames(context, textDraft.value, mentions)
|
||||
|
||||
updatedText = SpannableString(updated.body)
|
||||
MentionAnnotation.setMentionAnnotations(updatedText, updated.mentions)
|
||||
MessageStyler.style(id = MessageStyler.DRAFT_ID, messageRanges = bodyRanges.adjustBodyRanges(updated.bodyAdjustments), span = updatedText, hideSpoilerText = false)
|
||||
}
|
||||
|
||||
DatabaseDraft(drafts, updatedText)
|
||||
loadDraftsInternal(threadId)
|
||||
}.subscribeOn(Schedulers.io())
|
||||
}
|
||||
|
||||
fun loadDraftQuote(serialized: String): Maybe<ConversationMessage> {
|
||||
return Maybe.fromCallable {
|
||||
val quoteId: QuoteId = QuoteId.deserialize(context, serialized) ?: return@fromCallable null
|
||||
val messageRecord: MessageRecord = SignalDatabase.messages.getMessageFor(quoteId.id, quoteId.author)?.let {
|
||||
if (it is MediaMmsMessageRecord) {
|
||||
it.withAttachments(context, SignalDatabase.attachments.getAttachmentsForMessage(it.id))
|
||||
} else {
|
||||
it
|
||||
}
|
||||
} ?: return@fromCallable null
|
||||
private fun loadDraftsInternal(threadId: Long): DatabaseDraft {
|
||||
val drafts: Drafts = draftTable.getDrafts(threadId)
|
||||
val bodyRangesDraft: DraftTable.Draft? = drafts.getDraftOfType(DraftTable.Draft.BODY_RANGES)
|
||||
val textDraft: DraftTable.Draft? = drafts.getDraftOfType(DraftTable.Draft.TEXT)
|
||||
var updatedText: Spannable? = null
|
||||
|
||||
val threadRecipient = requireNotNull(SignalDatabase.threads.getRecipientForThreadId(messageRecord.threadId))
|
||||
ConversationMessageFactory.createWithUnresolvedData(context, messageRecord, threadRecipient)
|
||||
if (textDraft != null && bodyRangesDraft != null) {
|
||||
val bodyRanges: BodyRangeList = BodyRangeList.parseFrom(Base64.decodeOrThrow(bodyRangesDraft.value))
|
||||
val mentions: List<Mention> = MentionUtil.bodyRangeListToMentions(bodyRanges)
|
||||
|
||||
val updated = MentionUtil.updateBodyAndMentionsWithDisplayNames(context, textDraft.value, mentions)
|
||||
|
||||
updatedText = SpannableString(updated.body)
|
||||
MentionAnnotation.setMentionAnnotations(updatedText, updated.mentions)
|
||||
MessageStyler.style(id = MessageStyler.DRAFT_ID, messageRanges = bodyRanges.adjustBodyRanges(updated.bodyAdjustments), span = updatedText, hideSpoilerText = false)
|
||||
}
|
||||
|
||||
return DatabaseDraft(drafts, updatedText)
|
||||
}
|
||||
|
||||
@Deprecated("Not needed for CFv2")
|
||||
fun loadDraftQuote(serialized: String): Maybe<ConversationMessage> {
|
||||
return Maybe.fromCallable { loadDraftQuoteInternal(serialized) }
|
||||
}
|
||||
|
||||
private fun loadDraftQuoteInternal(serialized: String): ConversationMessage? {
|
||||
val quoteId: QuoteId = QuoteId.deserialize(context, serialized) ?: return null
|
||||
val messageRecord: MessageRecord = SignalDatabase.messages.getMessageFor(quoteId.id, quoteId.author)?.let {
|
||||
if (it is MediaMmsMessageRecord) {
|
||||
it.withAttachments(context, SignalDatabase.attachments.getAttachmentsForMessage(it.id))
|
||||
} else {
|
||||
it
|
||||
}
|
||||
} ?: return null
|
||||
|
||||
val threadRecipient = requireNotNull(SignalDatabase.threads.getRecipientForThreadId(messageRecord.threadId))
|
||||
return ConversationMessageFactory.createWithUnresolvedData(context, messageRecord, threadRecipient)
|
||||
}
|
||||
|
||||
@Deprecated("Not needed for CFv2")
|
||||
fun loadDraftMessageEdit(serialized: String): Maybe<ConversationMessage> {
|
||||
return Maybe.fromCallable {
|
||||
val messageId = MessageId.deserialize(serialized)
|
||||
val messageRecord: MessageRecord = SignalDatabase.messages.getMessageRecordOrNull(messageId.id) ?: return@fromCallable null
|
||||
val threadRecipient: Recipient = requireNotNull(SignalDatabase.threads.getRecipientForThreadId(messageRecord.threadId))
|
||||
if (messageRecord.hasTextSlide()) {
|
||||
val textSlide = messageRecord.requireTextSlide()
|
||||
if (textSlide.uri != null) {
|
||||
try {
|
||||
PartAuthority.getAttachmentStream(context, textSlide.uri!!).use { stream ->
|
||||
val body = StreamUtil.readFullyAsString(stream)
|
||||
return@fromCallable ConversationMessageFactory.createWithUnresolvedData(context, messageRecord, body, threadRecipient)
|
||||
}
|
||||
} catch (e: IOException) {
|
||||
Log.e(TAG, "Failed to load text slide", e)
|
||||
return Maybe.fromCallable { loadDraftMessageEditInternal(serialized) }
|
||||
}
|
||||
|
||||
private fun loadDraftMessageEditInternal(serialized: String): ConversationMessage? {
|
||||
val messageId = MessageId.deserialize(serialized)
|
||||
val messageRecord: MessageRecord = SignalDatabase.messages.getMessageRecordOrNull(messageId.id) ?: return null
|
||||
val threadRecipient: Recipient = requireNotNull(SignalDatabase.threads.getRecipientForThreadId(messageRecord.threadId))
|
||||
if (messageRecord.hasTextSlide()) {
|
||||
val textSlide = messageRecord.requireTextSlide()
|
||||
if (textSlide.uri != null) {
|
||||
try {
|
||||
PartAuthority.getAttachmentStream(context, textSlide.uri!!).use { stream ->
|
||||
val body = StreamUtil.readFullyAsString(stream)
|
||||
return ConversationMessageFactory.createWithUnresolvedData(context, messageRecord, body, threadRecipient)
|
||||
}
|
||||
} catch (e: IOException) {
|
||||
Log.e(TAG, "Failed to load text slide", e)
|
||||
}
|
||||
}
|
||||
ConversationMessageFactory.createWithUnresolvedData(context, messageRecord, threadRecipient)
|
||||
}
|
||||
return ConversationMessageFactory.createWithUnresolvedData(context, messageRecord, threadRecipient)
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
private fun getKeyboardImageDetails(glideRequests: GlideRequests, uri: Uri): KeyboardImageDetails? {
|
||||
return try {
|
||||
val bitmap: Bitmap = glideRequests.asBitmap()
|
||||
.load(DecryptableUri(uri))
|
||||
.skipMemoryCache(true)
|
||||
.diskCacheStrategy(DiskCacheStrategy.NONE)
|
||||
.submit()
|
||||
.get(1000, TimeUnit.MILLISECONDS)
|
||||
val topLeft = bitmap.getPixel(0, 0)
|
||||
KeyboardImageDetails(bitmap.width, bitmap.height, Color.alpha(topLeft) < 255)
|
||||
} catch (e: InterruptedException) {
|
||||
null
|
||||
} catch (e: ExecutionException) {
|
||||
null
|
||||
} catch (e: TimeoutException) {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
data class DatabaseDraft(val drafts: Drafts, val updatedText: CharSequence?)
|
||||
|
||||
sealed interface ShareOrDraftData {
|
||||
data class SendSticker(val slide: Slide) : ShareOrDraftData
|
||||
data class SendKeyboardImage(val slide: Slide) : ShareOrDraftData
|
||||
data class StartSendMedia(val mediaList: List<Media>, val text: CharSequence?) : ShareOrDraftData
|
||||
data class SetMedia(val media: Uri, val mediaType: SlideFactory.MediaType, val text: CharSequence?) : ShareOrDraftData
|
||||
data class SetText(val text: CharSequence) : ShareOrDraftData
|
||||
data class SetLocation(val location: SignalPlace, val draftText: CharSequence?) : ShareOrDraftData
|
||||
data class SetQuote(val quote: ConversationMessage, val draftText: CharSequence?) : ShareOrDraftData
|
||||
data class SetEditMessage(val messageEdit: ConversationMessage) : ShareOrDraftData
|
||||
}
|
||||
|
||||
data class KeyboardImageDetails(val width: Int, val height: Int, val hasTransparency: Boolean)
|
||||
}
|
||||
|
||||
@@ -10,8 +10,10 @@ import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
* management pattern going forward for drafts.
|
||||
*/
|
||||
data class DraftState(
|
||||
val recipientId: RecipientId = RecipientId.UNKNOWN,
|
||||
@Deprecated("Not needed for CFv2")
|
||||
val recipientId: RecipientId? = null,
|
||||
val threadId: Long = -1,
|
||||
@Deprecated("Not needed for CFv2")
|
||||
val distributionType: Int = 0,
|
||||
val textDraft: DraftTable.Draft? = null,
|
||||
val bodyRangesDraft: DraftTable.Draft? = null,
|
||||
@@ -36,7 +38,7 @@ data class DraftState(
|
||||
}
|
||||
}
|
||||
|
||||
fun copyAndSetDrafts(threadId: Long, drafts: Drafts): DraftState {
|
||||
fun copyAndSetDrafts(threadId: Long = this.threadId, drafts: Drafts): DraftState {
|
||||
return copy(
|
||||
threadId = threadId,
|
||||
textDraft = drafts.getDraftOfType(DraftTable.Draft.TEXT),
|
||||
|
||||
@@ -25,10 +25,11 @@ import org.thoughtcrime.securesms.util.rx.RxStore
|
||||
* management pattern going forward for drafts.
|
||||
*/
|
||||
class DraftViewModel @JvmOverloads constructor(
|
||||
threadId: Long = -1,
|
||||
private val repository: DraftRepository = DraftRepository()
|
||||
) : ViewModel() {
|
||||
|
||||
private val store = RxStore(DraftState())
|
||||
private val store = RxStore(DraftState(threadId = threadId))
|
||||
|
||||
val state: Flowable<DraftState> = store.stateFlowable.observeOn(AndroidSchedulers.mainThread())
|
||||
|
||||
@@ -39,10 +40,12 @@ class DraftViewModel @JvmOverloads constructor(
|
||||
store.dispose()
|
||||
}
|
||||
|
||||
@Deprecated("Not needed for CFv2")
|
||||
fun setThreadId(threadId: Long) {
|
||||
store.update { it.copy(threadId = threadId) }
|
||||
}
|
||||
|
||||
@Deprecated("Not needed for CFv2")
|
||||
fun setDistributionType(distributionType: Int) {
|
||||
store.update { it.copy(distributionType = distributionType) }
|
||||
}
|
||||
@@ -64,6 +67,7 @@ class DraftViewModel @JvmOverloads constructor(
|
||||
}
|
||||
}
|
||||
|
||||
@Deprecated("Not needed for CFv2")
|
||||
fun onRecipientChanged(recipient: Recipient) {
|
||||
store.update { it.copy(recipientId = recipient.id) }
|
||||
}
|
||||
@@ -130,16 +134,17 @@ class DraftViewModel @JvmOverloads constructor(
|
||||
}
|
||||
}
|
||||
|
||||
fun onSendComplete(threadId: Long) {
|
||||
fun onSendComplete(threadId: Long = store.state.threadId) {
|
||||
repository.deleteVoiceNoteDraftData(store.state.voiceNoteDraft)
|
||||
store.update { saveDrafts(it.copyAndClearDrafts(threadId)) }
|
||||
}
|
||||
|
||||
private fun saveDrafts(state: DraftState): DraftState {
|
||||
repository.saveDrafts(Recipient.resolved(state.recipientId), state.threadId, state.distributionType, state.toDrafts())
|
||||
repository.saveDrafts(state.recipientId?.let { Recipient.resolved(it) }, state.threadId, state.distributionType, state.toDrafts())
|
||||
return state
|
||||
}
|
||||
|
||||
@Deprecated("Not needed for CFv2")
|
||||
fun loadDrafts(threadId: Long): Single<DraftRepository.DatabaseDraft> {
|
||||
return repository
|
||||
.loadDrafts(threadId)
|
||||
@@ -149,17 +154,30 @@ class DraftViewModel @JvmOverloads constructor(
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
}
|
||||
|
||||
@Deprecated("Not needed for CFv2")
|
||||
fun loadDraftQuote(serialized: String): Maybe<ConversationMessage> {
|
||||
return repository.loadDraftQuote(serialized)
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
}
|
||||
|
||||
@Deprecated("Not needed for CFv2")
|
||||
fun loadDraftEditMessage(serialized: String): Maybe<ConversationMessage> {
|
||||
return repository.loadDraftMessageEdit(serialized)
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
}
|
||||
|
||||
fun loadShareOrDraftData(): Maybe<DraftRepository.ShareOrDraftData> {
|
||||
return repository.getShareOrDraftData()
|
||||
.doOnSuccess { (_, drafts) ->
|
||||
if (drafts != null) {
|
||||
store.update { saveDrafts(it.copyAndSetDrafts(drafts = drafts)) }
|
||||
}
|
||||
}
|
||||
.map { (data, _) -> data }
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
}
|
||||
}
|
||||
|
||||
private fun String.toTextDraft(): Draft? {
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
/*
|
||||
* Copyright 2023 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.conversation.v2
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import androidx.activity.result.contract.ActivityResultContract
|
||||
import androidx.core.content.IntentCompat
|
||||
import androidx.fragment.app.Fragment
|
||||
import org.thoughtcrime.securesms.contactshare.Contact
|
||||
import org.thoughtcrime.securesms.contactshare.ContactShareEditActivity
|
||||
import org.thoughtcrime.securesms.conversation.MessageSendType
|
||||
import org.thoughtcrime.securesms.conversation.colors.ChatColors
|
||||
import org.thoughtcrime.securesms.mediasend.Media
|
||||
import org.thoughtcrime.securesms.mediasend.MediaSendActivityResult
|
||||
import org.thoughtcrime.securesms.mediasend.v2.MediaSelectionActivity
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
|
||||
/**
|
||||
* This encapsulates the logic for interacting with other activities used throughout a conversation. The gist
|
||||
* is to use this to launch the various activities and use the [Callbacks] to provide strongly-typed results.
|
||||
* It is intended to replace the need for [android.app.Activity.onActivityResult].
|
||||
*
|
||||
* Note, not all activity results will live here but this should handle most of the basic cases. More advance
|
||||
* usages like [AddToContactsContract] can be split out into their own [ActivityResultContract] implementations.
|
||||
*/
|
||||
class ConversationActivityResultContracts(fragment: Fragment, private val callbacks: Callbacks) {
|
||||
|
||||
private val contactShareLauncher = fragment.registerForActivityResult(ContactShareEditor) { contacts -> callbacks.onSendContacts(contacts) }
|
||||
private val mediaSelectionLauncher = fragment.registerForActivityResult(MediaSelection) { result -> callbacks.onMediaSend(result) }
|
||||
|
||||
fun launchContactShareEditor(uri: Uri, chatColors: ChatColors) {
|
||||
contactShareLauncher.launch(uri to chatColors)
|
||||
}
|
||||
|
||||
fun launchMediaEditor(mediaList: List<Media>, recipientId: RecipientId, text: CharSequence?) {
|
||||
mediaSelectionLauncher.launch(MediaSelectionInput(mediaList, recipientId, text))
|
||||
}
|
||||
|
||||
private object MediaSelection : ActivityResultContract<MediaSelectionInput, MediaSendActivityResult>() {
|
||||
override fun createIntent(context: Context, input: MediaSelectionInput): Intent {
|
||||
val (media, recipientId, text) = input
|
||||
return MediaSelectionActivity.editor(context, MessageSendType.SignalMessageSendType, media, recipientId, text)
|
||||
}
|
||||
|
||||
override fun parseResult(resultCode: Int, intent: Intent?): MediaSendActivityResult {
|
||||
return MediaSendActivityResult.fromData(intent!!)
|
||||
}
|
||||
}
|
||||
|
||||
private object ContactShareEditor : ActivityResultContract<Pair<Uri, ChatColors>, List<Contact>>() {
|
||||
override fun createIntent(context: Context, input: Pair<Uri, ChatColors>): Intent {
|
||||
val (uri, chatColors) = input
|
||||
return ContactShareEditActivity.getIntent(context, listOf(uri), chatColors.asSingleColor())
|
||||
}
|
||||
|
||||
override fun parseResult(resultCode: Int, intent: Intent?): List<Contact> {
|
||||
return intent?.let { IntentCompat.getParcelableArrayListExtra(intent, ContactShareEditActivity.KEY_CONTACTS, Contact::class.java) } ?: emptyList()
|
||||
}
|
||||
}
|
||||
|
||||
private data class MediaSelectionInput(val media: List<Media>, val recipientId: RecipientId, val text: CharSequence?)
|
||||
|
||||
interface Callbacks {
|
||||
fun onSendContacts(contacts: List<Contact>)
|
||||
fun onMediaSend(result: MediaSendActivityResult)
|
||||
}
|
||||
}
|
||||
@@ -98,6 +98,7 @@ import org.thoughtcrime.securesms.components.ProgressCardDialogFragmentArgs
|
||||
import org.thoughtcrime.securesms.components.ScrollToPositionDelegate
|
||||
import org.thoughtcrime.securesms.components.SendButton
|
||||
import org.thoughtcrime.securesms.components.ViewBinderDelegate
|
||||
import org.thoughtcrime.securesms.components.mention.MentionAnnotation
|
||||
import org.thoughtcrime.securesms.components.menu.ActionItem
|
||||
import org.thoughtcrime.securesms.components.menu.SignalBottomActionBar
|
||||
import org.thoughtcrime.securesms.components.recyclerview.SmoothScrollingLinearLayoutManager
|
||||
@@ -126,11 +127,15 @@ import org.thoughtcrime.securesms.conversation.ConversationReactionOverlay.OnHid
|
||||
import org.thoughtcrime.securesms.conversation.MarkReadHelper
|
||||
import org.thoughtcrime.securesms.conversation.MenuState
|
||||
import org.thoughtcrime.securesms.conversation.MessageSendType
|
||||
import org.thoughtcrime.securesms.conversation.MessageStyler.getStyling
|
||||
import org.thoughtcrime.securesms.conversation.SelectedConversationModel
|
||||
import org.thoughtcrime.securesms.conversation.ShowAdminsBottomSheetDialog
|
||||
import org.thoughtcrime.securesms.conversation.colors.ChatColors
|
||||
import org.thoughtcrime.securesms.conversation.colors.Colorizer
|
||||
import org.thoughtcrime.securesms.conversation.colors.RecyclerViewColorizer
|
||||
import org.thoughtcrime.securesms.conversation.drafts.DraftRepository
|
||||
import org.thoughtcrime.securesms.conversation.drafts.DraftRepository.ShareOrDraftData
|
||||
import org.thoughtcrime.securesms.conversation.drafts.DraftViewModel
|
||||
import org.thoughtcrime.securesms.conversation.mutiselect.ConversationItemAnimator
|
||||
import org.thoughtcrime.securesms.conversation.mutiselect.MultiselectItemDecoration
|
||||
import org.thoughtcrime.securesms.conversation.mutiselect.MultiselectPart
|
||||
@@ -145,10 +150,13 @@ import org.thoughtcrime.securesms.conversation.v2.keyboard.AttachmentKeyboardFra
|
||||
import org.thoughtcrime.securesms.database.model.IdentityRecord
|
||||
import org.thoughtcrime.securesms.database.model.InMemoryMessageRecord
|
||||
import org.thoughtcrime.securesms.database.model.MediaMmsMessageRecord
|
||||
import org.thoughtcrime.securesms.database.model.Mention
|
||||
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.Quote
|
||||
import org.thoughtcrime.securesms.database.model.StickerRecord
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.BodyRangeList
|
||||
import org.thoughtcrime.securesms.databinding.V2ConversationFragmentBinding
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
|
||||
import org.thoughtcrime.securesms.giph.mp4.GiphyMp4ItemDecoration
|
||||
@@ -177,13 +185,18 @@ import org.thoughtcrime.securesms.mediapreview.MediaIntentFactory
|
||||
import org.thoughtcrime.securesms.mediapreview.MediaIntentFactory.create
|
||||
import org.thoughtcrime.securesms.mediapreview.MediaPreviewV2Activity
|
||||
import org.thoughtcrime.securesms.mediasend.Media
|
||||
import org.thoughtcrime.securesms.mediasend.MediaSendActivityResult
|
||||
import org.thoughtcrime.securesms.mediasend.v2.MediaSelectionActivity
|
||||
import org.thoughtcrime.securesms.messagedetails.MessageDetailsFragment
|
||||
import org.thoughtcrime.securesms.messagerequests.MessageRequestRepository
|
||||
import org.thoughtcrime.securesms.messagerequests.MessageRequestState
|
||||
import org.thoughtcrime.securesms.mms.AttachmentManager
|
||||
import org.thoughtcrime.securesms.mms.GlideApp
|
||||
import org.thoughtcrime.securesms.mms.MediaConstraints
|
||||
import org.thoughtcrime.securesms.mms.QuoteModel
|
||||
import org.thoughtcrime.securesms.mms.Slide
|
||||
import org.thoughtcrime.securesms.mms.SlideDeck
|
||||
import org.thoughtcrime.securesms.mms.SlideFactory
|
||||
import org.thoughtcrime.securesms.notifications.v2.ConversationId
|
||||
import org.thoughtcrime.securesms.payments.preferences.PaymentsActivity
|
||||
import org.thoughtcrime.securesms.permissions.Permissions
|
||||
@@ -208,10 +221,12 @@ import org.thoughtcrime.securesms.util.BottomSheetUtil
|
||||
import org.thoughtcrime.securesms.util.BubbleUtil
|
||||
import org.thoughtcrime.securesms.util.CommunicationActions
|
||||
import org.thoughtcrime.securesms.util.ContextUtil
|
||||
import org.thoughtcrime.securesms.util.Debouncer
|
||||
import org.thoughtcrime.securesms.util.DeleteDialog
|
||||
import org.thoughtcrime.securesms.util.DrawableUtil
|
||||
import org.thoughtcrime.securesms.util.FeatureFlags
|
||||
import org.thoughtcrime.securesms.util.FullscreenHelper
|
||||
import org.thoughtcrime.securesms.util.MediaUtil
|
||||
import org.thoughtcrime.securesms.util.PlayStoreUtil
|
||||
import org.thoughtcrime.securesms.util.SaveAttachmentUtil
|
||||
import org.thoughtcrime.securesms.util.SignalLocalMetrics
|
||||
@@ -232,6 +247,7 @@ import org.thoughtcrime.securesms.verify.VerifyIdentityActivity
|
||||
import org.thoughtcrime.securesms.wallpaper.ChatWallpaper
|
||||
import org.thoughtcrime.securesms.wallpaper.ChatWallpaperDimLevelUtil
|
||||
import java.util.Locale
|
||||
import java.util.Optional
|
||||
import java.util.concurrent.ExecutionException
|
||||
|
||||
/**
|
||||
@@ -284,14 +300,20 @@ class ConversationFragment : LoggingFragment(R.layout.v2_conversation_fragment)
|
||||
MessageRequestViewModel(args.threadId, conversationRecipientRepository, messageRequestRepository)
|
||||
}
|
||||
|
||||
private val draftViewModel: DraftViewModel by viewModel {
|
||||
DraftViewModel(threadId = args.threadId, repository = DraftRepository(conversationArguments = args))
|
||||
}
|
||||
|
||||
private val conversationTooltips = ConversationTooltips(this)
|
||||
private val colorizer = Colorizer()
|
||||
private val textDraftSaveDebouncer = Debouncer(500)
|
||||
|
||||
private lateinit var conversationOptionsMenuProvider: ConversationOptionsMenu.Provider
|
||||
private lateinit var layoutManager: LinearLayoutManager
|
||||
private lateinit var markReadHelper: MarkReadHelper
|
||||
private lateinit var giphyMp4ProjectionRecycler: GiphyMp4ProjectionRecycler
|
||||
private lateinit var addToContactsLauncher: ActivityResultLauncher<Intent>
|
||||
private lateinit var conversationActivityResultContracts: ConversationActivityResultContracts
|
||||
private lateinit var scrollToPositionDelegate: ScrollToPositionDelegate
|
||||
private lateinit var adapter: ConversationAdapterV2
|
||||
private lateinit var recyclerViewColorizer: RecyclerViewColorizer
|
||||
@@ -483,8 +505,6 @@ class ConversationFragment : LoggingFragment(R.layout.v2_conversation_fragment)
|
||||
addTextChangedListener(composeTextEventsListener)
|
||||
setOnClickListener(composeTextEventsListener)
|
||||
onFocusChangeListener = composeTextEventsListener
|
||||
|
||||
setMessageSendType(MessageSendType.SignalMessageSendType)
|
||||
}
|
||||
|
||||
sendButton.apply {
|
||||
@@ -553,6 +573,30 @@ class ConversationFragment : LoggingFragment(R.layout.v2_conversation_fragment)
|
||||
SwipeAvailabilityProvider(),
|
||||
this::handleReplyToMessage
|
||||
).attachToRecyclerView(binding.conversationItemRecycler)
|
||||
|
||||
draftViewModel.loadShareOrDraftData()
|
||||
.subscribeBy {
|
||||
when (it) {
|
||||
is ShareOrDraftData.SendKeyboardImage -> sendMessageWithoutComposeInput(slide = it.slide, clearCompose = false)
|
||||
is ShareOrDraftData.SendSticker -> sendMessageWithoutComposeInput(slide = it.slide, clearCompose = true)
|
||||
is ShareOrDraftData.SetEditMessage -> inputPanel.enterEditMessageMode(GlideApp.with(this), it.messageEdit, true)
|
||||
is ShareOrDraftData.SetLocation -> attachmentManager.setLocation(it.location, MediaConstraints.getPushMediaConstraints())
|
||||
is ShareOrDraftData.SetMedia -> {
|
||||
composeText.setDraftText(it.text)
|
||||
setMedia(it.media, it.mediaType)
|
||||
}
|
||||
is ShareOrDraftData.SetQuote -> {
|
||||
composeText.setDraftText(it.draftText)
|
||||
handleReplyToMessage(it.quote)
|
||||
}
|
||||
is ShareOrDraftData.SetText -> composeText.setDraftText(it.text)
|
||||
is ShareOrDraftData.StartSendMedia -> {
|
||||
val recipientId = viewModel.recipientSnapshot?.id ?: return@subscribeBy
|
||||
conversationActivityResultContracts.launchMediaEditor(it.mediaList, recipientId, it.text)
|
||||
}
|
||||
}
|
||||
}
|
||||
.addTo(disposables)
|
||||
}
|
||||
|
||||
private fun presentInputReadyState(inputReadyState: InputReadyState) {
|
||||
@@ -577,6 +621,8 @@ class ConversationFragment : LoggingFragment(R.layout.v2_conversation_fragment)
|
||||
} else {
|
||||
disabledInputView.clear()
|
||||
}
|
||||
|
||||
composeText.setMessageSendType(MessageSendType.SignalMessageSendType)
|
||||
}
|
||||
|
||||
private fun presentIdentityRecordsState(identityRecordsState: IdentityRecordsState) {
|
||||
@@ -599,6 +645,33 @@ class ConversationFragment : LoggingFragment(R.layout.v2_conversation_fragment)
|
||||
}
|
||||
}
|
||||
|
||||
private fun setMedia(uri: Uri, mediaType: SlideFactory.MediaType, width: Int = 0, height: Int = 0, borderless: Boolean = false, videoGif: Boolean = false) {
|
||||
val recipientId: RecipientId = viewModel.recipientSnapshot?.id ?: return
|
||||
|
||||
if (mediaType == SlideFactory.MediaType.VCARD) {
|
||||
conversationActivityResultContracts.launchContactShareEditor(uri, viewModel.recipientSnapshot!!.chatColors)
|
||||
} else if (mediaType == SlideFactory.MediaType.IMAGE || mediaType == SlideFactory.MediaType.GIF || mediaType == SlideFactory.MediaType.VIDEO) {
|
||||
val mimeType = MediaUtil.getMimeType(requireContext(), uri) ?: mediaType.toFallbackMimeType()
|
||||
val media = Media(
|
||||
uri,
|
||||
mimeType,
|
||||
0,
|
||||
width,
|
||||
height,
|
||||
0,
|
||||
0,
|
||||
borderless,
|
||||
videoGif,
|
||||
Optional.empty(),
|
||||
Optional.empty(),
|
||||
Optional.empty()
|
||||
)
|
||||
conversationActivityResultContracts.launchMediaEditor(listOf(media), recipientId, composeText.textTrimmed)
|
||||
} else {
|
||||
attachmentManager.setMedia(GlideApp.with(this), uri, mediaType, MediaConstraints.getPushMediaConstraints(), width, height)
|
||||
}
|
||||
}
|
||||
|
||||
private fun calculateCharactersRemaining() {
|
||||
val messageBody: String = binding.conversationInputPanel.embeddedTextEditor.textTrimmed.toString()
|
||||
val charactersLeftView: TextView = binding.conversationInputSpaceLeft
|
||||
@@ -620,6 +693,7 @@ class ConversationFragment : LoggingFragment(R.layout.v2_conversation_fragment)
|
||||
|
||||
private fun registerForResults() {
|
||||
addToContactsLauncher = registerForActivityResult(AddToContactsContract()) {}
|
||||
conversationActivityResultContracts = ConversationActivityResultContracts(this, ActivityResultCallbacks())
|
||||
}
|
||||
|
||||
private fun onRecipientChanged(recipient: Recipient) {
|
||||
@@ -881,26 +955,54 @@ class ConversationFragment : LoggingFragment(R.layout.v2_conversation_fragment)
|
||||
}
|
||||
}
|
||||
|
||||
private fun sendMessage(
|
||||
metricId: String? = null,
|
||||
scheduledDate: Long = -1,
|
||||
slideDeck: SlideDeck? = if (attachmentManager.isAttachmentPresent) attachmentManager.buildSlideDeck() else null
|
||||
private fun sendMessageWithoutComposeInput(
|
||||
slide: Slide? = null,
|
||||
contacts: List<Contact> = emptyList(),
|
||||
clearCompose: Boolean = true
|
||||
) {
|
||||
sendMessage(
|
||||
slideDeck = slide?.let { SlideDeck().apply { addSlide(slide) } },
|
||||
contacts = contacts,
|
||||
clearCompose = clearCompose,
|
||||
body = "",
|
||||
mentions = emptyList(),
|
||||
bodyRanges = null,
|
||||
messageToEdit = null,
|
||||
quote = null
|
||||
)
|
||||
}
|
||||
|
||||
private fun sendMessage(
|
||||
body: String = composeText.editableText.toString(),
|
||||
mentions: List<Mention> = composeText.mentions,
|
||||
bodyRanges: BodyRangeList? = composeText.styling,
|
||||
messageToEdit: MessageId? = inputPanel.editMessageId,
|
||||
quote: QuoteModel? = inputPanel.quote.orNull(),
|
||||
scheduledDate: Long = -1,
|
||||
slideDeck: SlideDeck? = if (attachmentManager.isAttachmentPresent) attachmentManager.buildSlideDeck() else null,
|
||||
contacts: List<Contact> = emptyList(),
|
||||
clearCompose: Boolean = true
|
||||
) {
|
||||
val metricId = viewModel.recipientSnapshot?.let { if (it.isGroup == true) SignalLocalMetrics.GroupMessageSend.start() else SignalLocalMetrics.IndividualMessageSend.start() }
|
||||
|
||||
val send: Completable = viewModel.sendMessage(
|
||||
metricId = metricId,
|
||||
body = composeText.editableText.toString(),
|
||||
body = body,
|
||||
slideDeck = slideDeck,
|
||||
scheduledDate = scheduledDate,
|
||||
messageToEdit = inputPanel.editMessageId,
|
||||
quote = inputPanel.quote.orNull(),
|
||||
mentions = composeText.mentions,
|
||||
bodyRanges = composeText.styling
|
||||
messageToEdit = messageToEdit,
|
||||
quote = quote,
|
||||
mentions = mentions,
|
||||
bodyRanges = bodyRanges,
|
||||
contacts = contacts
|
||||
)
|
||||
|
||||
disposables += send
|
||||
.doOnSubscribe {
|
||||
composeText.setText("")
|
||||
inputPanel.clearQuote()
|
||||
if (clearCompose) {
|
||||
composeText.setText("")
|
||||
inputPanel.clearQuote()
|
||||
}
|
||||
}
|
||||
.subscribeBy(
|
||||
onError = { t ->
|
||||
@@ -929,7 +1031,7 @@ class ConversationFragment : LoggingFragment(R.layout.v2_conversation_fragment)
|
||||
|
||||
// todo [cfv2] updateLinkPreviewState();
|
||||
|
||||
// todo [cfv2] draftViewModel.onSendComplete(threadId);
|
||||
draftViewModel.onSendComplete()
|
||||
|
||||
inputPanel.exitEditMessageMode()
|
||||
}
|
||||
@@ -2067,6 +2169,23 @@ class ConversationFragment : LoggingFragment(R.layout.v2_conversation_fragment)
|
||||
}
|
||||
// endregion Conversation Callbacks
|
||||
|
||||
//region Activity Results Callbacks
|
||||
|
||||
private inner class ActivityResultCallbacks : ConversationActivityResultContracts.Callbacks {
|
||||
override fun onSendContacts(contacts: List<Contact>) {
|
||||
sendMessageWithoutComposeInput(
|
||||
contacts = contacts,
|
||||
clearCompose = false
|
||||
)
|
||||
}
|
||||
|
||||
override fun onMediaSend(result: MediaSendActivityResult) {
|
||||
// TODO [cfv2] media send
|
||||
}
|
||||
}
|
||||
|
||||
//endregion
|
||||
|
||||
private class LastSeenPositionUpdater(
|
||||
val adapter: ConversationAdapterV2,
|
||||
val layoutManager: LinearLayoutManager,
|
||||
@@ -2282,13 +2401,7 @@ class ConversationFragment : LoggingFragment(R.layout.v2_conversation_fragment)
|
||||
|
||||
private inner class SendButtonListener : View.OnClickListener, OnEditorActionListener {
|
||||
override fun onClick(v: View) {
|
||||
val metricId = if (viewModel.recipientSnapshot?.isGroup == true) {
|
||||
SignalLocalMetrics.GroupMessageSend.start()
|
||||
} else {
|
||||
SignalLocalMetrics.IndividualMessageSend.start()
|
||||
}
|
||||
|
||||
sendMessage(metricId)
|
||||
sendMessage()
|
||||
}
|
||||
|
||||
override fun onEditorAction(v: TextView, actionId: Int, event: KeyEvent): Boolean {
|
||||
@@ -2312,6 +2425,9 @@ class ConversationFragment : LoggingFragment(R.layout.v2_conversation_fragment)
|
||||
ComposeText.CursorPositionChangedListener {
|
||||
|
||||
private var beforeLength = 0
|
||||
private var previousText = ""
|
||||
|
||||
var typingStatusEnabled = true
|
||||
|
||||
override fun onKey(v: View, keyCode: Int, event: KeyEvent): Boolean {
|
||||
if (event.action == KeyEvent.ACTION_DOWN) {
|
||||
@@ -2352,11 +2468,114 @@ class ConversationFragment : LoggingFragment(R.layout.v2_conversation_fragment)
|
||||
// todo [cfv2] linkPreviewViewModel.onTextChanged(requireContext(), composeText.getTextTrimmed().toString(), start, end);
|
||||
}
|
||||
|
||||
override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) = Unit
|
||||
override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) {
|
||||
handleSaveDraftOnTextChange(composeText.textTrimmed)
|
||||
handleTypingIndicatorOnTextChange(s.toString())
|
||||
}
|
||||
|
||||
private fun handleSaveDraftOnTextChange(text: CharSequence) {
|
||||
textDraftSaveDebouncer.publish {
|
||||
if (inputPanel.inEditMessageMode()) {
|
||||
draftViewModel.setMessageEditDraft(inputPanel.editMessageId!!, text.toString(), MentionAnnotation.getMentionsFromAnnotations(text), getStyling(text))
|
||||
} else {
|
||||
draftViewModel.setTextDraft(text.toString(), MentionAnnotation.getMentionsFromAnnotations(text), getStyling(text))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleTypingIndicatorOnTextChange(text: String) {
|
||||
val recipient = viewModel.recipientSnapshot
|
||||
|
||||
if (recipient == null || !typingStatusEnabled || recipient.isBlocked || recipient.isSelf) {
|
||||
return
|
||||
}
|
||||
|
||||
val typingStatusSender = ApplicationDependencies.getTypingStatusSender()
|
||||
if (text.length == 0) {
|
||||
typingStatusSender.onTypingStoppedWithNotify(args.threadId)
|
||||
} else if (text.length < previousText.length && previousText.contains(text)) {
|
||||
typingStatusSender.onTypingStopped(args.threadId)
|
||||
} else {
|
||||
typingStatusSender.onTypingStarted(args.threadId)
|
||||
}
|
||||
|
||||
previousText = text
|
||||
}
|
||||
}
|
||||
|
||||
//endregion Compose + Send Callbacks
|
||||
|
||||
//region Input Panel Callbacks
|
||||
|
||||
private inner class InputPanelListener : InputPanel.Listener {
|
||||
override fun onVoiceNoteDraftPlay(audioUri: Uri, progress: Double) {
|
||||
// TODO [cfv2] Not yet implemented
|
||||
}
|
||||
|
||||
override fun onVoiceNoteDraftSeekTo(audioUri: Uri, progress: Double) {
|
||||
// TODO [cfv2] Not yet implemented
|
||||
}
|
||||
|
||||
override fun onVoiceNoteDraftPause(audioUri: Uri) {
|
||||
// TODO [cfv2] Not yet implemented
|
||||
}
|
||||
|
||||
override fun onVoiceNoteDraftDelete(audioUri: Uri) {
|
||||
// TODO [cfv2] Not yet implemented
|
||||
}
|
||||
|
||||
override fun onRecorderStarted() {
|
||||
// TODO [cfv2] Not yet implemented
|
||||
}
|
||||
|
||||
override fun onRecorderLocked() {
|
||||
// TODO [cfv2] Not yet implemented
|
||||
}
|
||||
|
||||
override fun onRecorderFinished() {
|
||||
// TODO [cfv2] Not yet implemented
|
||||
}
|
||||
|
||||
override fun onRecorderCanceled(byUser: Boolean) {
|
||||
// TODO [cfv2] Not yet implemented
|
||||
}
|
||||
|
||||
override fun onRecorderPermissionRequired() {
|
||||
// TODO [cfv2] Not yet implemented
|
||||
}
|
||||
|
||||
override fun onEmojiToggle() {
|
||||
// TODO [cfv2] Not yet implemented
|
||||
}
|
||||
|
||||
override fun onLinkPreviewCanceled() {
|
||||
// TODO [cfv2] Not yet implemented
|
||||
}
|
||||
|
||||
override fun onStickerSuggestionSelected(sticker: StickerRecord) {
|
||||
// TODO [cfv2] Not yet implemented
|
||||
}
|
||||
|
||||
override fun onQuoteChanged(id: Long, author: RecipientId) {
|
||||
draftViewModel.setQuoteDraft(id, author)
|
||||
}
|
||||
|
||||
override fun onQuoteCleared() {
|
||||
draftViewModel.clearQuoteDraft()
|
||||
}
|
||||
|
||||
override fun onEnterEditMode() {
|
||||
// TODO [cfv2] Not yet implemented
|
||||
}
|
||||
|
||||
override fun onExitEditMode() {
|
||||
updateToggleButtonState()
|
||||
draftViewModel.deleteMessageEditDraft()
|
||||
}
|
||||
}
|
||||
|
||||
//endregion
|
||||
|
||||
//region Attachment + Media Keyboard
|
||||
|
||||
private inner class AttachmentManagerListener : AttachmentManager.AttachmentListener {
|
||||
@@ -2365,7 +2584,7 @@ class ConversationFragment : LoggingFragment(R.layout.v2_conversation_fragment)
|
||||
}
|
||||
|
||||
override fun onLocationRemoved() {
|
||||
// TODO [cfv2] implement
|
||||
draftViewModel.clearLocationDraft()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -169,7 +169,8 @@ class ConversationRepository(
|
||||
messageToEdit: MessageId?,
|
||||
quote: QuoteModel?,
|
||||
mentions: List<Mention>,
|
||||
bodyRanges: BodyRangeList?
|
||||
bodyRanges: BodyRangeList?,
|
||||
contacts: List<Contact>
|
||||
): Completable {
|
||||
val sendCompletable = Completable.create { emitter ->
|
||||
if (body.isEmpty() && slideDeck?.containsMediaSlide() != true) {
|
||||
@@ -193,7 +194,8 @@ class ConversationRepository(
|
||||
scheduledDate = scheduledDate,
|
||||
outgoingQuote = quote,
|
||||
messageToEdit = messageToEdit?.id ?: 0,
|
||||
mentions = mentions
|
||||
mentions = mentions,
|
||||
sharedContacts = contacts
|
||||
)
|
||||
|
||||
MessageSender.send(
|
||||
|
||||
@@ -28,6 +28,7 @@ import org.signal.core.util.concurrent.subscribeWithSubject
|
||||
import org.signal.core.util.orNull
|
||||
import org.signal.paging.ProxyPagingController
|
||||
import org.thoughtcrime.securesms.components.reminder.Reminder
|
||||
import org.thoughtcrime.securesms.contactshare.Contact
|
||||
import org.thoughtcrime.securesms.conversation.ConversationMessage
|
||||
import org.thoughtcrime.securesms.conversation.colors.GroupAuthorNameColorHelper
|
||||
import org.thoughtcrime.securesms.conversation.colors.NameColor
|
||||
@@ -42,6 +43,7 @@ import org.thoughtcrime.securesms.database.model.MmsMessageRecord
|
||||
import org.thoughtcrime.securesms.database.model.Quote
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.BodyRangeList
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
|
||||
import org.thoughtcrime.securesms.jobs.RetrieveProfileJob
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.messagerequests.MessageRequestRepository
|
||||
import org.thoughtcrime.securesms.messagerequests.MessageRequestState
|
||||
@@ -117,11 +119,6 @@ class ConversationViewModel(
|
||||
recipientSnapshot = it
|
||||
}
|
||||
|
||||
disposables += recipientRepository
|
||||
.conversationRecipient
|
||||
.skip(1) // We can safely skip the first emission since this is used for updating the header on future changes
|
||||
.subscribeBy { pagingController.onDataItemChanged(ConversationElementKey.threadHeader) }
|
||||
|
||||
disposables += repository.getConversationThreadState(threadId, requestedStartingPosition)
|
||||
.subscribeBy(onSuccess = {
|
||||
pagingController.set(it.items.controller)
|
||||
@@ -153,6 +150,18 @@ class ConversationViewModel(
|
||||
}
|
||||
}.subscribeOn(Schedulers.io()).subscribe()
|
||||
|
||||
recipientRepository
|
||||
.conversationRecipient
|
||||
.filter { it.isRegistered }
|
||||
.take(1)
|
||||
.subscribeBy { RetrieveProfileJob.enqueue(it.id) }
|
||||
.addTo(disposables)
|
||||
|
||||
disposables += recipientRepository
|
||||
.conversationRecipient
|
||||
.skip(1) // We can safely skip the first emission since this is used for updating the header on future changes
|
||||
.subscribeBy { pagingController.onDataItemChanged(ConversationElementKey.threadHeader) }
|
||||
|
||||
disposables += scrollButtonStateStore.update(
|
||||
repository.getMessageCounts(threadId)
|
||||
) { counts, state ->
|
||||
@@ -250,7 +259,8 @@ class ConversationViewModel(
|
||||
messageToEdit: MessageId?,
|
||||
quote: QuoteModel?,
|
||||
mentions: List<Mention>,
|
||||
bodyRanges: BodyRangeList?
|
||||
bodyRanges: BodyRangeList?,
|
||||
contacts: List<Contact>
|
||||
): Completable {
|
||||
return repository.sendMessage(
|
||||
threadId = threadId,
|
||||
@@ -262,7 +272,8 @@ class ConversationViewModel(
|
||||
messageToEdit = messageToEdit,
|
||||
quote = quote,
|
||||
mentions = mentions,
|
||||
bodyRanges = bodyRanges
|
||||
bodyRanges = bodyRanges,
|
||||
contacts = contacts
|
||||
).observeOn(AndroidSchedulers.mainThread())
|
||||
}
|
||||
|
||||
|
||||
@@ -57,15 +57,15 @@ public class AudioSlide extends Slide {
|
||||
}
|
||||
|
||||
public AudioSlide(Context context, Uri uri, long dataSize, boolean voiceNote) {
|
||||
super(context, constructAttachmentFromUri(context, uri, MediaUtil.AUDIO_UNSPECIFIED, dataSize, 0, 0, false, null, null, null, null, null, voiceNote, false, false, false));
|
||||
super(constructAttachmentFromUri(context, uri, MediaUtil.AUDIO_UNSPECIFIED, dataSize, 0, 0, false, null, null, null, null, null, voiceNote, false, false, false));
|
||||
}
|
||||
|
||||
public AudioSlide(Context context, Uri uri, long dataSize, String contentType, boolean voiceNote) {
|
||||
super(context, new UriAttachment(uri, contentType, AttachmentTable.TRANSFER_PROGRESS_STARTED, dataSize, 0, 0, null, null, voiceNote, false, false, false, null, null, null, null, null));
|
||||
super(new UriAttachment(uri, contentType, AttachmentTable.TRANSFER_PROGRESS_STARTED, dataSize, 0, 0, null, null, voiceNote, false, false, false, null, null, null, null, null));
|
||||
}
|
||||
|
||||
public AudioSlide(Context context, Attachment attachment) {
|
||||
super(context, attachment);
|
||||
super(attachment);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -85,7 +85,7 @@ public class AudioSlide extends Slide {
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public String getContentDescription() {
|
||||
public String getContentDescription(Context context) {
|
||||
return context.getString(R.string.Slide_audio);
|
||||
}
|
||||
|
||||
|
||||
@@ -13,14 +13,14 @@ import org.thoughtcrime.securesms.util.StorageUtil;
|
||||
public class DocumentSlide extends Slide {
|
||||
|
||||
public DocumentSlide(@NonNull Context context, @NonNull Attachment attachment) {
|
||||
super(context, attachment);
|
||||
super(attachment);
|
||||
}
|
||||
|
||||
public DocumentSlide(@NonNull Context context, @NonNull Uri uri,
|
||||
@NonNull String contentType, long size,
|
||||
@Nullable String fileName)
|
||||
{
|
||||
super(context, constructAttachmentFromUri(context, uri, contentType, size, 0, 0, true, StorageUtil.getCleanFileName(fileName), null, null, null, null, false, false, false, false));
|
||||
super(constructAttachmentFromUri(context, uri, contentType, size, 0, 0, true, StorageUtil.getCleanFileName(fileName), null, null, null, null, false, false, false, false));
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -12,8 +12,8 @@ public class GifSlide extends ImageSlide {
|
||||
|
||||
private final boolean borderless;
|
||||
|
||||
public GifSlide(Context context, Attachment attachment) {
|
||||
super(context, attachment);
|
||||
public GifSlide(Attachment attachment) {
|
||||
super(attachment);
|
||||
this.borderless = attachment.isBorderless();
|
||||
}
|
||||
|
||||
@@ -22,22 +22,22 @@ public class GifSlide extends ImageSlide {
|
||||
}
|
||||
|
||||
public GifSlide(Context context, Uri uri, long size, int width, int height, boolean borderless, @Nullable String caption) {
|
||||
super(context, constructAttachmentFromUri(context,
|
||||
uri,
|
||||
MediaUtil.IMAGE_GIF,
|
||||
size,
|
||||
width,
|
||||
height,
|
||||
true,
|
||||
null,
|
||||
caption,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
false,
|
||||
borderless,
|
||||
true,
|
||||
false));
|
||||
super(constructAttachmentFromUri(context,
|
||||
uri,
|
||||
MediaUtil.IMAGE_GIF,
|
||||
size,
|
||||
width,
|
||||
height,
|
||||
true,
|
||||
null,
|
||||
caption,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
false,
|
||||
borderless,
|
||||
true,
|
||||
false));
|
||||
|
||||
this.borderless = borderless;
|
||||
}
|
||||
|
||||
@@ -38,8 +38,8 @@ public class ImageSlide extends Slide {
|
||||
@SuppressWarnings("unused")
|
||||
private static final String TAG = Log.tag(ImageSlide.class);
|
||||
|
||||
public ImageSlide(@NonNull Context context, @NonNull Attachment attachment) {
|
||||
super(context, attachment);
|
||||
public ImageSlide(@NonNull Attachment attachment) {
|
||||
super(attachment);
|
||||
this.borderless = attachment.isBorderless();
|
||||
}
|
||||
|
||||
@@ -52,7 +52,7 @@ public class ImageSlide extends Slide {
|
||||
}
|
||||
|
||||
public ImageSlide(Context context, Uri uri, String contentType, long size, int width, int height, boolean borderless, @Nullable String caption, @Nullable BlurHash blurHash, @Nullable TransformProperties transformProperties) {
|
||||
super(context, constructAttachmentFromUri(context, uri, contentType, size, width, height, true, null, caption, null, blurHash, null, false, borderless, false, false, transformProperties));
|
||||
super(constructAttachmentFromUri(context, uri, contentType, size, width, height, true, null, caption, null, blurHash, null, false, borderless, false, false, transformProperties));
|
||||
this.borderless = borderless;
|
||||
}
|
||||
|
||||
@@ -78,7 +78,7 @@ public class ImageSlide extends Slide {
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public String getContentDescription() {
|
||||
public String getContentDescription(Context context) {
|
||||
return context.getString(R.string.Slide_image);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,12 +10,12 @@ import org.thoughtcrime.securesms.attachments.Attachment;
|
||||
public class MmsSlide extends ImageSlide {
|
||||
|
||||
public MmsSlide(@NonNull Context context, @NonNull Attachment attachment) {
|
||||
super(context, attachment);
|
||||
super(attachment);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public String getContentDescription() {
|
||||
public String getContentDescription(Context context) {
|
||||
return "MMS";
|
||||
}
|
||||
|
||||
|
||||
@@ -40,10 +40,8 @@ import java.util.Optional;
|
||||
public abstract class Slide {
|
||||
|
||||
protected final Attachment attachment;
|
||||
protected final Context context;
|
||||
|
||||
public Slide(@NonNull Context context, @NonNull Attachment attachment) {
|
||||
this.context = context;
|
||||
public Slide(@NonNull Attachment attachment) {
|
||||
this.attachment = attachment;
|
||||
}
|
||||
|
||||
@@ -122,7 +120,7 @@ public abstract class Slide {
|
||||
return hasVideo() && attachment.isVideoGif();
|
||||
}
|
||||
|
||||
public @NonNull String getContentDescription() { return ""; }
|
||||
public @NonNull String getContentDescription(@NonNull Context context) { return ""; }
|
||||
|
||||
public @NonNull Attachment asAttachment() {
|
||||
return attachment;
|
||||
|
||||
@@ -21,13 +21,13 @@ public class StickerSlide extends Slide {
|
||||
|
||||
private final StickerLocator stickerLocator;
|
||||
|
||||
public StickerSlide(@NonNull Context context, @NonNull Attachment attachment) {
|
||||
super(context, attachment);
|
||||
public StickerSlide(@NonNull Attachment attachment) {
|
||||
super(attachment);
|
||||
this.stickerLocator = Objects.requireNonNull(attachment.getSticker());
|
||||
}
|
||||
|
||||
public StickerSlide(Context context, Uri uri, long size, @NonNull StickerLocator stickerLocator, @NonNull String contentType) {
|
||||
super(context, constructAttachmentFromUri(context, uri, contentType, size, WIDTH, HEIGHT, true, null, null, stickerLocator, null, null, false, false, false, false));
|
||||
super(constructAttachmentFromUri(context, uri, contentType, size, WIDTH, HEIGHT, true, null, null, stickerLocator, null, null, false, false, false, false));
|
||||
this.stickerLocator = Objects.requireNonNull(attachment.getSticker());
|
||||
}
|
||||
|
||||
@@ -47,7 +47,7 @@ public class StickerSlide extends Slide {
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull String getContentDescription() {
|
||||
public @NonNull String getContentDescription(Context context) {
|
||||
return context.getString(R.string.Slide_sticker);
|
||||
}
|
||||
|
||||
|
||||
@@ -13,10 +13,10 @@ import org.thoughtcrime.securesms.util.MediaUtil;
|
||||
public class TextSlide extends Slide {
|
||||
|
||||
public TextSlide(@NonNull Context context, @NonNull Attachment attachment) {
|
||||
super(context, attachment);
|
||||
super(attachment);
|
||||
}
|
||||
|
||||
public TextSlide(@NonNull Context context, @NonNull Uri uri, @Nullable String filename, long size) {
|
||||
super(context, constructAttachmentFromUri(context, uri, MediaUtil.LONG_TEXT, size, 0, 0, true, filename, null, null, null, null, false, false, false, false));
|
||||
super(constructAttachmentFromUri(context, uri, MediaUtil.LONG_TEXT, size, 0, 0, true, filename, null, null, null, null, false, false, false, false));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,15 +38,15 @@ public class VideoSlide extends Slide {
|
||||
}
|
||||
|
||||
public VideoSlide(Context context, Uri uri, long dataSize, boolean gif, @Nullable String caption, @Nullable AttachmentTable.TransformProperties transformProperties) {
|
||||
super(context, constructAttachmentFromUri(context, uri, MediaUtil.VIDEO_UNSPECIFIED, dataSize, 0, 0, MediaUtil.hasVideoThumbnail(context, uri), null, caption, null, null, null, false, false, gif, false, transformProperties));
|
||||
super(constructAttachmentFromUri(context, uri, MediaUtil.VIDEO_UNSPECIFIED, dataSize, 0, 0, MediaUtil.hasVideoThumbnail(context, uri), null, caption, null, null, null, false, false, gif, false, transformProperties));
|
||||
}
|
||||
|
||||
public VideoSlide(Context context, Uri uri, long dataSize, boolean gif, int width, int height, @Nullable String caption, @Nullable AttachmentTable.TransformProperties transformProperties) {
|
||||
super(context, constructAttachmentFromUri(context, uri, MediaUtil.VIDEO_UNSPECIFIED, dataSize, width, height, MediaUtil.hasVideoThumbnail(context, uri), null, caption, null, null, null, false, false, gif, false, transformProperties));
|
||||
super(constructAttachmentFromUri(context, uri, MediaUtil.VIDEO_UNSPECIFIED, dataSize, width, height, MediaUtil.hasVideoThumbnail(context, uri), null, caption, null, null, null, false, false, gif, false, transformProperties));
|
||||
}
|
||||
|
||||
public VideoSlide(Context context, Attachment attachment) {
|
||||
super(context, attachment);
|
||||
super(attachment);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -75,7 +75,7 @@ public class VideoSlide extends Slide {
|
||||
}
|
||||
|
||||
@NonNull @Override
|
||||
public String getContentDescription() {
|
||||
public String getContentDescription(Context context) {
|
||||
return context.getString(R.string.Slide_video);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ import org.thoughtcrime.securesms.util.MediaUtil;
|
||||
public class ViewOnceSlide extends Slide {
|
||||
|
||||
public ViewOnceSlide(@NonNull Context context, @NonNull Attachment attachment) {
|
||||
super(context, attachment);
|
||||
super(attachment);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -116,7 +116,7 @@ class StoryLinkPreviewView @JvmOverloads constructor(
|
||||
|
||||
notImage.visible = false
|
||||
|
||||
val imageSlide: Slide? = linkPreview.thumbnail.map { ImageSlide(context, it) }.orElse(null)
|
||||
val imageSlide: Slide? = linkPreview.thumbnail.map { ImageSlide(it) }.orElse(null)
|
||||
if (imageSlide != null) {
|
||||
if (loadThumbnail) {
|
||||
future = image.setImageResource(
|
||||
|
||||
@@ -92,12 +92,12 @@ public class MediaUtil {
|
||||
|
||||
public static @NonNull Slide getSlideForAttachment(Context context, Attachment attachment) {
|
||||
if (attachment.isSticker()) {
|
||||
return new StickerSlide(context, attachment);
|
||||
return new StickerSlide(attachment);
|
||||
}
|
||||
|
||||
switch (getSlideTypeFromContentType(attachment.getContentType())) {
|
||||
case GIF : return new GifSlide(context, attachment);
|
||||
case IMAGE : return new ImageSlide(context, attachment);
|
||||
case GIF : return new GifSlide(attachment);
|
||||
case IMAGE : return new ImageSlide(attachment);
|
||||
case VIDEO : return new VideoSlide(context, attachment);
|
||||
case AUDIO : return new AudioSlide(context, attachment);
|
||||
case MMS : return new MmsSlide(context, attachment);
|
||||
|
||||
Reference in New Issue
Block a user