Fix view-once sync and quote descriptions.
This commit is contained in:
parent
e2a48d1714
commit
fd7aa9ccfa
17 changed files with 87 additions and 38 deletions
|
@ -154,10 +154,9 @@ public class InputPanel extends LinearLayout
|
|||
long id,
|
||||
@NonNull Recipient author,
|
||||
@NonNull String body,
|
||||
@NonNull SlideDeck attachments,
|
||||
boolean isViewOnce)
|
||||
@NonNull SlideDeck attachments)
|
||||
{
|
||||
this.quoteView.setQuote(glideRequests, id, author, body, false, attachments, isViewOnce);
|
||||
this.quoteView.setQuote(glideRequests, id, author, body, false, attachments);
|
||||
this.quoteView.setVisibility(View.VISIBLE);
|
||||
|
||||
if (this.linkPreview.getVisibility() == View.VISIBLE) {
|
||||
|
|
|
@ -29,6 +29,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.util.MediaUtil;
|
||||
import org.thoughtcrime.securesms.util.ThemeUtil;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
|
||||
|
@ -149,8 +150,7 @@ public class QuoteView extends FrameLayout implements RecipientForeverObserver {
|
|||
@NonNull Recipient author,
|
||||
@Nullable String body,
|
||||
boolean originalMissing,
|
||||
@NonNull SlideDeck attachments,
|
||||
boolean isViewOnce)
|
||||
@NonNull SlideDeck attachments)
|
||||
{
|
||||
if (this.author != null) this.author.removeForeverObserver(this);
|
||||
|
||||
|
@ -161,7 +161,7 @@ public class QuoteView extends FrameLayout implements RecipientForeverObserver {
|
|||
|
||||
this.author.observeForever(this);
|
||||
setQuoteAuthor(author);
|
||||
setQuoteText(body, attachments, isViewOnce);
|
||||
setQuoteText(body, attachments);
|
||||
setQuoteAttachment(glideRequests, attachments);
|
||||
setQuoteMissingFooter(originalMissing);
|
||||
}
|
||||
|
@ -197,7 +197,7 @@ public class QuoteView extends FrameLayout implements RecipientForeverObserver {
|
|||
mainView.setBackgroundColor(author.getColor().toQuoteBackgroundColor(getContext(), outgoing));
|
||||
}
|
||||
|
||||
private void setQuoteText(@Nullable String body, @NonNull SlideDeck attachments, boolean isViewOnce) {
|
||||
private void setQuoteText(@Nullable String body, @NonNull SlideDeck attachments) {
|
||||
if (!TextUtils.isEmpty(body) || !attachments.containsMediaSlide()) {
|
||||
bodyView.setVisibility(VISIBLE);
|
||||
bodyView.setText(body == null ? "" : body);
|
||||
|
@ -213,9 +213,10 @@ public class QuoteView extends FrameLayout implements RecipientForeverObserver {
|
|||
List<Slide> imageSlides = Stream.of(attachments.getSlides()).filter(Slide::hasImage).limit(1).toList();
|
||||
List<Slide> videoSlides = Stream.of(attachments.getSlides()).filter(Slide::hasVideo).limit(1).toList();
|
||||
List<Slide> stickerSlides = Stream.of(attachments.getSlides()).filter(Slide::hasSticker).limit(1).toList();
|
||||
List<Slide> viewOnceSlides = Stream.of(attachments.getSlides()).filter(Slide::hasViewOnce).limit(1).toList();
|
||||
|
||||
// Given that most types have images, we specifically check images last
|
||||
if (isViewOnce) {
|
||||
if (!viewOnceSlides.isEmpty()) {
|
||||
mediaDescriptionText.setText(R.string.QuoteView_media);
|
||||
} else if (!audioSlides.isEmpty()) {
|
||||
mediaDescriptionText.setText(R.string.QuoteView_audio);
|
||||
|
@ -233,10 +234,14 @@ public class QuoteView extends FrameLayout implements RecipientForeverObserver {
|
|||
private void setQuoteAttachment(@NonNull GlideRequests glideRequests, @NonNull SlideDeck slideDeck) {
|
||||
List<Slide> imageVideoSlides = Stream.of(slideDeck.getSlides()).filter(s -> s.hasImage() || s.hasVideo() || s.hasSticker()).limit(1).toList();
|
||||
List<Slide> documentSlides = Stream.of(attachments.getSlides()).filter(Slide::hasDocument).limit(1).toList();
|
||||
List<Slide> viewOnceSlides = Stream.of(attachments.getSlides()).filter(Slide::hasViewOnce).limit(1).toList();
|
||||
|
||||
attachmentVideoOverlayView.setVisibility(GONE);
|
||||
|
||||
if (!imageVideoSlides.isEmpty() && imageVideoSlides.get(0).getThumbnailUri() != null) {
|
||||
if (!viewOnceSlides.isEmpty()) {
|
||||
thumbnailView.setVisibility(GONE);
|
||||
attachmentContainerView.setVisibility(GONE);
|
||||
} else if (!imageVideoSlides.isEmpty() && imageVideoSlides.get(0).getThumbnailUri() != null) {
|
||||
thumbnailView.setVisibility(VISIBLE);
|
||||
attachmentContainerView.setVisibility(GONE);
|
||||
dismissView.setBackgroundResource(R.drawable.dismiss_background);
|
||||
|
|
|
@ -2836,8 +2836,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
|||
messageRecord.getDateSent(),
|
||||
author,
|
||||
body,
|
||||
slideDeck,
|
||||
messageRecord.isViewOnce());
|
||||
slideDeck);
|
||||
|
||||
} else if (messageRecord.isMms() && !((MmsMessageRecord) messageRecord).getLinkPreviews().isEmpty()) {
|
||||
LinkPreview linkPreview = ((MmsMessageRecord) messageRecord).getLinkPreviews().get(0);
|
||||
|
@ -2851,8 +2850,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
|||
messageRecord.getDateSent(),
|
||||
author,
|
||||
messageRecord.getBody(),
|
||||
slideDeck,
|
||||
messageRecord.isViewOnce());
|
||||
slideDeck);
|
||||
} else {
|
||||
SlideDeck slideDeck = messageRecord.isMms() ? ((MmsMessageRecord) messageRecord).getSlideDeck() : new SlideDeck();
|
||||
|
||||
|
@ -2866,8 +2864,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
|||
messageRecord.getDateSent(),
|
||||
author,
|
||||
messageRecord.getBody(),
|
||||
slideDeck,
|
||||
messageRecord.isViewOnce());
|
||||
slideDeck);
|
||||
}
|
||||
|
||||
inputPanel.clickOnComposeInput();
|
||||
|
|
|
@ -1077,7 +1077,7 @@ public class ConversationFragment extends Fragment
|
|||
.withMimeType(thumbnailSlide.getContentType())
|
||||
.createForSingleSessionOnDisk(requireContext());
|
||||
|
||||
DatabaseFactory.getAttachmentDatabase(requireContext()).deleteAttachmentFilesForMessage(messageRecord.getId());
|
||||
DatabaseFactory.getAttachmentDatabase(requireContext()).deleteAttachmentFilesForViewOnceMessage(messageRecord.getId());
|
||||
|
||||
ApplicationContext.getInstance(requireContext())
|
||||
.getViewOnceMessageManager()
|
||||
|
@ -1095,7 +1095,7 @@ public class ConversationFragment extends Fragment
|
|||
} else {
|
||||
Log.w(TAG, "Failed to open view-once photo. Showing a toast and deleting the attachments for the message just in case.");
|
||||
Toast.makeText(requireContext(), R.string.ConversationFragment_failed_to_open_message, Toast.LENGTH_SHORT).show();
|
||||
SignalExecutors.BOUNDED.execute(() -> DatabaseFactory.getAttachmentDatabase(requireContext()).deleteAttachmentFilesForMessage(messageRecord.getId()));
|
||||
SignalExecutors.BOUNDED.execute(() -> DatabaseFactory.getAttachmentDatabase(requireContext()).deleteAttachmentFilesForViewOnceMessage(messageRecord.getId()));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
@ -855,7 +855,7 @@ public class ConversationItem extends LinearLayout implements BindableConversati
|
|||
if (current.isMms() && !current.isMmsNotification() && ((MediaMmsMessageRecord)current).getQuote() != null) {
|
||||
Quote quote = ((MediaMmsMessageRecord)current).getQuote();
|
||||
//noinspection ConstantConditions
|
||||
quoteView.setQuote(glideRequests, quote.getId(), Recipient.live(quote.getAuthor()).get(), quote.getText(), quote.isOriginalMissing(), quote.getAttachment(), messageRecord.isViewOnce());
|
||||
quoteView.setQuote(glideRequests, quote.getId(), Recipient.live(quote.getAuthor()).get(), quote.getText(), quote.isOriginalMissing(), quote.getAttachment());
|
||||
quoteView.setVisibility(View.VISIBLE);
|
||||
quoteView.getLayoutParams().width = ViewGroup.LayoutParams.WRAP_CONTENT;
|
||||
|
||||
|
|
|
@ -320,8 +320,8 @@ public class AttachmentDatabase extends Database {
|
|||
notifyAttachmentListeners();
|
||||
}
|
||||
|
||||
public void deleteAttachmentFilesForMessage(long mmsId) {
|
||||
Log.d(TAG, "[deleteAttachmentFilesForMessage] mmsId: " + mmsId);
|
||||
public void deleteAttachmentFilesForViewOnceMessage(long mmsId) {
|
||||
Log.d(TAG, "[deleteAttachmentFilesForViewOnceMessage] mmsId: " + mmsId);
|
||||
|
||||
SQLiteDatabase database = databaseHelper.getWritableDatabase();
|
||||
Cursor cursor = null;
|
||||
|
@ -355,6 +355,7 @@ public class AttachmentDatabase extends Database {
|
|||
values.put(HEIGHT, 0);
|
||||
values.put(TRANSFER_STATE, TRANSFER_PROGRESS_DONE);
|
||||
values.put(BLUR_HASH, (String) null);
|
||||
values.put(CONTENT_TYPE, MediaUtil.VIEW_ONCE);
|
||||
|
||||
database.update(TABLE_NAME, values, MMS_ID + " = ?", new String[] {mmsId + ""});
|
||||
notifyAttachmentListeners();
|
||||
|
@ -365,7 +366,6 @@ public class AttachmentDatabase extends Database {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
public void deleteAttachment(@NonNull AttachmentId id) {
|
||||
Log.d(TAG, "[deleteAttachment] attachmentId: " + id);
|
||||
|
||||
|
|
|
@ -145,7 +145,9 @@ public class ThreadRecord extends DisplayRecord {
|
|||
}
|
||||
|
||||
private String getViewOnceDescription(@NonNull Context context, @Nullable String contentType) {
|
||||
if (MediaUtil.isVideoType(contentType)) {
|
||||
if (MediaUtil.isViewOnceType(contentType)) {
|
||||
return context.getString(R.string.ThreadRecord_disappearing_media);
|
||||
} else if (MediaUtil.isVideoType(contentType)) {
|
||||
return context.getString(R.string.ThreadRecord_disappearing_video);
|
||||
} else {
|
||||
return context.getString(R.string.ThreadRecord_disappearing_photo);
|
||||
|
|
|
@ -12,7 +12,6 @@ import org.thoughtcrime.securesms.ApplicationContext;
|
|||
import org.thoughtcrime.securesms.attachments.Attachment;
|
||||
import org.thoughtcrime.securesms.crypto.UnidentifiedAccessUtil;
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
import org.thoughtcrime.securesms.database.GroupDatabase;
|
||||
import org.thoughtcrime.securesms.database.GroupReceiptDatabase.GroupReceiptInfo;
|
||||
import org.thoughtcrime.securesms.database.MmsDatabase;
|
||||
import org.thoughtcrime.securesms.database.NoSuchMessageException;
|
||||
|
@ -203,7 +202,7 @@ public class PushGroupSendJob extends PushSendJob {
|
|||
}
|
||||
|
||||
if (message.isViewOnce()) {
|
||||
DatabaseFactory.getAttachmentDatabase(context).deleteAttachmentFilesForMessage(messageId);
|
||||
DatabaseFactory.getAttachmentDatabase(context).deleteAttachmentFilesForViewOnceMessage(messageId);
|
||||
}
|
||||
} else if (!networkFailures.isEmpty()) {
|
||||
throw new RetryLaterException();
|
||||
|
|
|
@ -150,7 +150,7 @@ public class PushMediaSendJob extends PushSendJob {
|
|||
}
|
||||
|
||||
if (message.isViewOnce()) {
|
||||
DatabaseFactory.getAttachmentDatabase(context).deleteAttachmentFilesForMessage(messageId);
|
||||
DatabaseFactory.getAttachmentDatabase(context).deleteAttachmentFilesForViewOnceMessage(messageId);
|
||||
}
|
||||
|
||||
log(TAG, "Sent message: " + messageId);
|
||||
|
|
|
@ -656,7 +656,7 @@ public final class PushProcessMessageJob extends BaseJob {
|
|||
handleReaction(content, message.getMessage());
|
||||
threadId = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(getSyncMessageDestination(message));
|
||||
threadId = threadId != -1 ? threadId : null;
|
||||
} else if (message.getMessage().getAttachments().isPresent() || message.getMessage().getQuote().isPresent() || message.getMessage().getPreviews().isPresent() || message.getMessage().getSticker().isPresent()) {
|
||||
} else if (message.getMessage().getAttachments().isPresent() || message.getMessage().getQuote().isPresent() || message.getMessage().getPreviews().isPresent() || message.getMessage().getSticker().isPresent() || message.getMessage().isViewOnce()) {
|
||||
threadId = handleSynchronizeSentMediaMessage(message);
|
||||
} else {
|
||||
threadId = handleSynchronizeSentTextMessage(message);
|
||||
|
@ -742,7 +742,7 @@ public final class PushProcessMessageJob extends BaseJob {
|
|||
MessageRecord record = DatabaseFactory.getMmsSmsDatabase(context).getMessageFor(timestamp, author);
|
||||
|
||||
if (record != null && record.isMms()) {
|
||||
DatabaseFactory.getAttachmentDatabase(context).deleteAttachmentFilesForMessage(record.getId());
|
||||
DatabaseFactory.getAttachmentDatabase(context).deleteAttachmentFilesForViewOnceMessage(record.getId());
|
||||
}
|
||||
|
||||
MessageNotifier.setLastDesktopActivityTimestamp(envelopeTimestamp);
|
||||
|
@ -842,7 +842,8 @@ public final class PushProcessMessageJob extends BaseJob {
|
|||
Optional<List<Contact>> sharedContacts = getContacts(message.getMessage().getSharedContacts());
|
||||
Optional<List<LinkPreview>> previews = getLinkPreviews(message.getMessage().getPreviews(), message.getMessage().getBody().or(""));
|
||||
boolean viewOnce = message.getMessage().isViewOnce();
|
||||
List<Attachment> syncAttachments = viewOnce ? Collections.emptyList() : PointerAttachment.forPointers(message.getMessage().getAttachments());
|
||||
List<Attachment> syncAttachments = viewOnce ? Collections.singletonList(new TombstoneAttachment(MediaUtil.VIEW_ONCE, false))
|
||||
: PointerAttachment.forPointers(message.getMessage().getAttachments());
|
||||
|
||||
if (sticker.isPresent()) {
|
||||
syncAttachments.add(sticker.get());
|
||||
|
@ -1289,7 +1290,9 @@ public final class PushProcessMessageJob extends BaseJob {
|
|||
if (message.isMms()) {
|
||||
MmsMessageRecord mmsMessage = (MmsMessageRecord) message;
|
||||
|
||||
if (!mmsMessage.isViewOnce()) {
|
||||
if (mmsMessage.isViewOnce()) {
|
||||
attachments.add(new TombstoneAttachment(MediaUtil.VIEW_ONCE, true));
|
||||
} else {
|
||||
attachments = mmsMessage.getSlideDeck().asAttachments();
|
||||
|
||||
if (attachments.isEmpty()) {
|
||||
|
@ -1298,8 +1301,6 @@ public final class PushProcessMessageJob extends BaseJob {
|
|||
.map(lp -> lp.getThumbnail().get())
|
||||
.toList());
|
||||
}
|
||||
} else if (quote.get().getAttachments().size() > 0) {
|
||||
attachments.add(new TombstoneAttachment(quote.get().getAttachments().get(0).getContentType(), true));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -217,12 +217,15 @@ public abstract class PushSendJob extends SendJob {
|
|||
protected Optional<SignalServiceDataMessage.Quote> getQuoteFor(OutgoingMediaMessage message) {
|
||||
if (message.getOutgoingQuote() == null) return Optional.absent();
|
||||
|
||||
long quoteId = message.getOutgoingQuote().getId();
|
||||
String quoteBody = message.getOutgoingQuote().getText();
|
||||
RecipientId quoteAuthor = message.getOutgoingQuote().getAuthor();
|
||||
List<SignalServiceDataMessage.Quote.QuotedAttachment> quoteAttachments = new LinkedList<>();
|
||||
long quoteId = message.getOutgoingQuote().getId();
|
||||
String quoteBody = message.getOutgoingQuote().getText();
|
||||
RecipientId quoteAuthor = message.getOutgoingQuote().getAuthor();
|
||||
List<SignalServiceDataMessage.Quote.QuotedAttachment> quoteAttachments = new LinkedList<>();
|
||||
List<Attachment> filteredAttachments = Stream.of(message.getOutgoingQuote().getAttachments())
|
||||
.filterNot(a -> MediaUtil.isViewOnceType(a.getContentType()))
|
||||
.toList();
|
||||
|
||||
for (Attachment attachment : message.getOutgoingQuote().getAttachments()) {
|
||||
for (Attachment attachment : filteredAttachments) {
|
||||
BitmapUtil.ScaleResult thumbnailData = null;
|
||||
SignalServiceAttachment thumbnail = null;
|
||||
String thumbnailType = MediaUtil.IMAGE_JPEG;
|
||||
|
|
|
@ -105,6 +105,10 @@ public abstract class Slide {
|
|||
return false;
|
||||
}
|
||||
|
||||
public boolean hasViewOnce() {
|
||||
return false;
|
||||
}
|
||||
|
||||
public @NonNull String getContentDescription() { return ""; }
|
||||
|
||||
public @NonNull Attachment asAttachment() {
|
||||
|
|
|
@ -88,7 +88,7 @@ public class SlideDeck {
|
|||
|
||||
public boolean containsMediaSlide() {
|
||||
for (Slide slide : slides) {
|
||||
if (slide.hasImage() || slide.hasVideo() || slide.hasAudio() || slide.hasDocument() || slide.hasSticker()) {
|
||||
if (slide.hasImage() || slide.hasVideo() || slide.hasAudio() || slide.hasDocument() || slide.hasSticker() || slide.hasViewOnce()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,28 @@
|
|||
package org.thoughtcrime.securesms.mms;
|
||||
|
||||
|
||||
import android.content.Context;
|
||||
import android.net.Uri;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import org.thoughtcrime.securesms.attachments.Attachment;
|
||||
import org.thoughtcrime.securesms.util.MediaUtil;
|
||||
|
||||
/**
|
||||
* Slide used for attachments with contentType {@link MediaUtil#VIEW_ONCE}.
|
||||
* Attachments will only get this type *after* they've been viewed, or if they were synced from a
|
||||
* linked device. Incoming unviewed messages will have the appropriate image/video contentType.
|
||||
*/
|
||||
public class ViewOnceSlide extends Slide {
|
||||
|
||||
public ViewOnceSlide(@NonNull Context context, @NonNull Attachment attachment) {
|
||||
super(context, attachment);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasViewOnce() {
|
||||
return true;
|
||||
}
|
||||
}
|
|
@ -54,7 +54,7 @@ public class ViewOnceMessageManager extends TimedEventManager<ViewOnceExpiration
|
|||
@Override
|
||||
protected void executeEvent(@NonNull ViewOnceExpirationInfo event) {
|
||||
Log.i(TAG, "Deleting attachments for message " + event.getMessageId());
|
||||
attachmentDatabase.deleteAttachmentFilesForMessage(event.getMessageId());
|
||||
attachmentDatabase.deleteAttachmentFilesForViewOnceMessage(event.getMessageId());
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
|
|
|
@ -34,6 +34,7 @@ import org.thoughtcrime.securesms.mms.Slide;
|
|||
import org.thoughtcrime.securesms.mms.StickerSlide;
|
||||
import org.thoughtcrime.securesms.mms.TextSlide;
|
||||
import org.thoughtcrime.securesms.mms.VideoSlide;
|
||||
import org.thoughtcrime.securesms.mms.ViewOnceSlide;
|
||||
import org.thoughtcrime.securesms.providers.BlobProvider;
|
||||
|
||||
import java.io.FileNotFoundException;
|
||||
|
@ -56,6 +57,7 @@ public class MediaUtil {
|
|||
public static final String VIDEO_UNSPECIFIED = "video/*";
|
||||
public static final String VCARD = "text/x-vcard";
|
||||
public static final String LONG_TEXT = "text/x-signal-plain";
|
||||
public static final String VIEW_ONCE = "application/x-signal-view-once";
|
||||
|
||||
public static SlideType getSlideTypeFromContentType(@NonNull String contentType) {
|
||||
if (isGif(contentType)) {
|
||||
|
@ -70,6 +72,8 @@ public class MediaUtil {
|
|||
return SlideType.MMS;
|
||||
} else if (isLongTextType(contentType)) {
|
||||
return SlideType.LONG_TEXT;
|
||||
} else if (isViewOnceType(contentType)) {
|
||||
return SlideType.VIEW_ONCE;
|
||||
} else {
|
||||
return SlideType.DOCUMENT;
|
||||
}
|
||||
|
@ -87,6 +91,7 @@ public class MediaUtil {
|
|||
case AUDIO : return new AudioSlide(context, attachment);
|
||||
case MMS : return new MmsSlide(context, attachment);
|
||||
case LONG_TEXT : return new TextSlide(context, attachment);
|
||||
case VIEW_ONCE : return new ViewOnceSlide(context, attachment);
|
||||
case DOCUMENT : return new DocumentSlide(context, attachment);
|
||||
default : throw new AssertionError();
|
||||
}
|
||||
|
@ -269,6 +274,10 @@ public class MediaUtil {
|
|||
return (null != contentType) && contentType.equals(LONG_TEXT);
|
||||
}
|
||||
|
||||
public static boolean isViewOnceType(String contentType) {
|
||||
return (null != contentType) && contentType.equals(VIEW_ONCE);
|
||||
}
|
||||
|
||||
public static boolean hasVideoThumbnail(Uri uri) {
|
||||
if (BlobProvider.isAuthority(uri) && MediaUtil.isVideo(BlobProvider.getMimeType(uri)) && Build.VERSION.SDK_INT >= 23) {
|
||||
return true;
|
||||
|
@ -373,6 +382,7 @@ public class MediaUtil {
|
|||
AUDIO,
|
||||
MMS,
|
||||
LONG_TEXT,
|
||||
VIEW_ONCE,
|
||||
DOCUMENT
|
||||
}
|
||||
}
|
||||
|
|
|
@ -819,6 +819,7 @@
|
|||
<string name="ThreadRecord_sticker">Sticker</string>
|
||||
<string name="ThreadRecord_disappearing_photo">Disappearing photo</string>
|
||||
<string name="ThreadRecord_disappearing_video">Disappearing video</string>
|
||||
<string name="ThreadRecord_disappearing_media">Disappearing media</string>
|
||||
<string name="ThreadRecord_s_is_on_signal">%s is on Signal!</string>
|
||||
<string name="ThreadRecord_disappearing_messages_disabled">Disappearing messages disabled</string>
|
||||
<string name="ThreadRecord_disappearing_message_time_updated_to_s">Disappearing message time set to %s</string>
|
||||
|
|
Loading…
Add table
Reference in a new issue