This commit is contained in:
Ryan Zhao 2021-06-29 16:03:10 +10:00
commit 64a70d0555
81 changed files with 1672 additions and 518 deletions

View File

@ -143,8 +143,8 @@ dependencies {
testImplementation 'org.robolectric:shadows-multidex:4.2' testImplementation 'org.robolectric:shadows-multidex:4.2'
} }
def canonicalVersionCode = 182 def canonicalVersionCode = 183
def canonicalVersionName = "1.10.13" def canonicalVersionName = "1.11.0"
def postFixSize = 10 def postFixSize = 10
def abiPostFix = ['armeabi-v7a' : 1, def abiPostFix = ['armeabi-v7a' : 1,

View File

@ -64,10 +64,12 @@ import org.session.libsignal.utilities.Log;
import org.thoughtcrime.securesms.components.MediaView; import org.thoughtcrime.securesms.components.MediaView;
import org.thoughtcrime.securesms.database.MediaDatabase.MediaRecord; import org.thoughtcrime.securesms.database.MediaDatabase.MediaRecord;
import org.thoughtcrime.securesms.database.loaders.PagingMediaLoader; import org.thoughtcrime.securesms.database.loaders.PagingMediaLoader;
import org.thoughtcrime.securesms.database.model.MmsMessageRecord;
import org.thoughtcrime.securesms.mediapreview.MediaPreviewViewModel; import org.thoughtcrime.securesms.mediapreview.MediaPreviewViewModel;
import org.thoughtcrime.securesms.mediapreview.MediaRailAdapter; import org.thoughtcrime.securesms.mediapreview.MediaRailAdapter;
import org.thoughtcrime.securesms.mms.GlideApp; import org.thoughtcrime.securesms.mms.GlideApp;
import org.thoughtcrime.securesms.mms.GlideRequests; import org.thoughtcrime.securesms.mms.GlideRequests;
import org.thoughtcrime.securesms.mms.Slide;
import org.thoughtcrime.securesms.permissions.Permissions; import org.thoughtcrime.securesms.permissions.Permissions;
import org.thoughtcrime.securesms.util.AttachmentUtil; import org.thoughtcrime.securesms.util.AttachmentUtil;
import org.thoughtcrime.securesms.util.DateUtils; import org.thoughtcrime.securesms.util.DateUtils;
@ -116,6 +118,22 @@ public class MediaPreviewActivity extends PassphraseRequiredActionBarActivity im
private int restartItem = -1; private int restartItem = -1;
public static Intent getPreviewIntent(Context context, Slide slide, MmsMessageRecord mms) {
Intent previewIntent = null;
if (MediaPreviewActivity.isContentTypeSupported(slide.getContentType()) && slide.getUri() != null) {
previewIntent = new Intent(context, MediaPreviewActivity.class);
previewIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
.setDataAndType(slide.getUri(), slide.getContentType())
.putExtra(ADDRESS_EXTRA, mms.getRecipient().getAddress())
.putExtra(OUTGOING_EXTRA, mms.isOutgoing())
.putExtra(DATE_EXTRA, mms.getTimestamp())
.putExtra(SIZE_EXTRA, slide.asAttachment().getSize())
.putExtra(CAPTION_EXTRA, slide.getCaption().orNull())
.putExtra(LEFT_IS_RECENT_EXTRA, false);
}
return previewIntent;
}
@SuppressWarnings("ConstantConditions") @SuppressWarnings("ConstantConditions")
@Override @Override

View File

@ -23,6 +23,7 @@ import com.google.android.exoplayer2.DefaultRenderersFactory;
import com.google.android.exoplayer2.ExoPlaybackException; import com.google.android.exoplayer2.ExoPlaybackException;
import com.google.android.exoplayer2.ExoPlayerFactory; import com.google.android.exoplayer2.ExoPlayerFactory;
import com.google.android.exoplayer2.LoadControl; import com.google.android.exoplayer2.LoadControl;
import com.google.android.exoplayer2.PlaybackParameters;
import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.Player;
import com.google.android.exoplayer2.SimpleExoPlayer; import com.google.android.exoplayer2.SimpleExoPlayer;
import com.google.android.exoplayer2.audio.AudioAttributes; import com.google.android.exoplayer2.audio.AudioAttributes;
@ -103,9 +104,9 @@ public class AudioSlidePlayer implements SensorEventListener {
} }
private void play(final double progress, boolean earpiece) throws IOException { private void play(final double progress, boolean earpiece) throws IOException {
if (this.mediaPlayer != null) return; if (this.mediaPlayer != null) { stop(); }
LoadControl loadControl = new DefaultLoadControl.Builder().setBufferDurationsMs(Integer.MAX_VALUE, Integer.MAX_VALUE, Integer.MAX_VALUE, Integer.MAX_VALUE).createDefaultLoadControl(); LoadControl loadControl = new DefaultLoadControl.Builder().setBufferDurationsMs(Integer.MAX_VALUE, Integer.MAX_VALUE, Integer.MAX_VALUE, Integer.MAX_VALUE).createDefaultLoadControl();
this.mediaPlayer = ExoPlayerFactory.newSimpleInstance(context, new DefaultRenderersFactory(context), new DefaultTrackSelector(), loadControl); this.mediaPlayer = ExoPlayerFactory.newSimpleInstance(context, new DefaultRenderersFactory(context), new DefaultTrackSelector(), loadControl);
this.audioAttachmentServer = new AttachmentServer(context, slide.asAttachment()); this.audioAttachmentServer = new AttachmentServer(context, slide.asAttachment());
this.startTime = System.currentTimeMillis(); this.startTime = System.currentTimeMillis();
@ -184,8 +185,6 @@ public class AudioSlidePlayer implements SensorEventListener {
public void onPlayerError(ExoPlaybackException error) { public void onPlayerError(ExoPlaybackException error) {
Log.w(TAG, "MediaPlayer Error: " + error); Log.w(TAG, "MediaPlayer Error: " + error);
Toast.makeText(context, R.string.AudioSlidePlayer_error_playing_audio, Toast.LENGTH_SHORT).show();
synchronized (AudioSlidePlayer.this) { synchronized (AudioSlidePlayer.this) {
mediaPlayer = null; mediaPlayer = null;
@ -267,8 +266,17 @@ public class AudioSlidePlayer implements SensorEventListener {
return slide; return slide;
} }
public Long getDuration() {
if (mediaPlayer == null) { return 0L; }
return mediaPlayer.getDuration();
}
private Pair<Double, Integer> getProgress() { public Double getProgress() {
if (mediaPlayer == null) { return 0.0; }
return (double) mediaPlayer.getCurrentPosition() / (double) mediaPlayer.getDuration();
}
private Pair<Double, Integer> getProgressTuple() {
if (mediaPlayer == null || mediaPlayer.getCurrentPosition() <= 0 || mediaPlayer.getDuration() <= 0) { if (mediaPlayer == null || mediaPlayer.getCurrentPosition() <= 0 || mediaPlayer.getDuration() <= 0) {
return new Pair<>(0D, 0); return new Pair<>(0D, 0);
} else { } else {
@ -277,6 +285,16 @@ public class AudioSlidePlayer implements SensorEventListener {
} }
} }
public float getPlaybackSpeed() {
if (mediaPlayer == null) { return 1.0f; }
return mediaPlayer.getPlaybackParameters().speed;
}
public void setPlaybackSpeed(float speed) {
if (mediaPlayer == null) { return; }
mediaPlayer.setPlaybackParameters(new PlaybackParameters(speed));
}
private void notifyOnStart() { private void notifyOnStart() {
Util.runOnMain(() -> getListener().onPlayerStart(AudioSlidePlayer.this)); Util.runOnMain(() -> getListener().onPlayerStart(AudioSlidePlayer.this));
} }
@ -383,7 +401,7 @@ public class AudioSlidePlayer implements SensorEventListener {
return; return;
} }
Pair<Double, Integer> progress = player.getProgress(); Pair<Double, Integer> progress = player.getProgressTuple();
player.notifyOnProgress(progress.first, progress.second); player.notifyOnProgress(progress.first, progress.second);
sendEmptyMessageDelayed(0, 50); sendEmptyMessageDelayed(0, 50);
} }

View File

@ -1,27 +1,27 @@
package org.thoughtcrime.securesms.components; package org.thoughtcrime.securesms.components;
import android.content.Context; import android.content.Context;
import androidx.annotation.ColorInt;
import androidx.annotation.IdRes;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import android.util.AttributeSet; import android.util.AttributeSet;
import android.view.ViewGroup; import android.view.ViewGroup;
import android.widget.FrameLayout; import android.widget.FrameLayout;
import android.widget.TextView; import android.widget.TextView;
import network.loki.messenger.R; import androidx.annotation.ColorInt;
import androidx.annotation.IdRes;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.thoughtcrime.securesms.conversation.v2.utilities.ThumbnailView; import org.session.libsession.utilities.Stub;
import org.thoughtcrime.securesms.conversation.v2.utilities.KThumbnailView;
import org.thoughtcrime.securesms.mms.GlideRequests; import org.thoughtcrime.securesms.mms.GlideRequests;
import org.thoughtcrime.securesms.mms.Slide; import org.thoughtcrime.securesms.mms.Slide;
import org.thoughtcrime.securesms.mms.SlideClickListener; import org.thoughtcrime.securesms.mms.SlideClickListener;
import org.thoughtcrime.securesms.mms.SlidesClickedListener; import org.thoughtcrime.securesms.mms.SlidesClickedListener;
import org.session.libsession.utilities.Stub;
import java.util.List; import java.util.List;
import network.loki.messenger.R;
public class AlbumThumbnailView extends FrameLayout { public class AlbumThumbnailView extends FrameLayout {
private @Nullable SlideClickListener thumbnailClickListener; private @Nullable SlideClickListener thumbnailClickListener;
@ -53,8 +53,8 @@ public class AlbumThumbnailView extends FrameLayout {
private void initialize() { private void initialize() {
inflate(getContext(), R.layout.album_thumbnail_view, this); inflate(getContext(), R.layout.album_thumbnail_view, this);
albumCellContainer = findViewById(R.id.album_cell_container); albumCellContainer = findViewById(R.id.albumCellContainer);
transferControls = new Stub<>(findViewById(R.id.album_transfer_controls_stub)); transferControls = new Stub<>(findViewById(R.id.albumTransferControlsStub));
} }
public void setSlides(@NonNull GlideRequests glideRequests, @NonNull List<Slide> slides, boolean showControls) { public void setSlides(@NonNull GlideRequests glideRequests, @NonNull List<Slide> slides, boolean showControls) {
@ -149,9 +149,8 @@ public class AlbumThumbnailView extends FrameLayout {
} }
private void setSlide(@NonNull GlideRequests glideRequests, @NonNull Slide slide, @IdRes int id) { private void setSlide(@NonNull GlideRequests glideRequests, @NonNull Slide slide, @IdRes int id) {
ThumbnailView cell = findViewById(id); KThumbnailView cell = findViewById(id);
cell.setImageResource(glideRequests, slide, false, false); cell.setImageResource(glideRequests, slide, false);
cell.setLoadIndicatorVisibile(slide.isInProgress());
cell.setThumbnailClickListener(defaultThumbnailClickListener); cell.setThumbnailClickListener(defaultThumbnailClickListener);
cell.setOnLongClickListener(defaultLongClickListener); cell.setOnLongClickListener(defaultLongClickListener);
} }

View File

@ -65,15 +65,10 @@ public class ConversationItemThumbnail extends FrameLayout {
if (attrs != null) { if (attrs != null) {
TypedArray typedArray = getContext().getTheme().obtainStyledAttributes(attrs, R.styleable.ConversationItemThumbnail, 0, 0); TypedArray typedArray = getContext().getTheme().obtainStyledAttributes(attrs, R.styleable.ConversationItemThumbnail, 0, 0);
thumbnail.setBounds(typedArray.getDimensionPixelSize(R.styleable.ConversationItemThumbnail_conversationThumbnail_minWidth, 0),
typedArray.getDimensionPixelSize(R.styleable.ConversationItemThumbnail_conversationThumbnail_maxWidth, 0),
typedArray.getDimensionPixelSize(R.styleable.ConversationItemThumbnail_conversationThumbnail_minHeight, 0),
typedArray.getDimensionPixelSize(R.styleable.ConversationItemThumbnail_conversationThumbnail_maxHeight, 0));
typedArray.recycle(); typedArray.recycle();
} }
} }
@SuppressWarnings("SuspiciousNameCombination")
@Override @Override
protected void dispatchDraw(Canvas canvas) { protected void dispatchDraw(Canvas canvas) {
super.dispatchDraw(canvas); super.dispatchDraw(canvas);

View File

@ -29,7 +29,6 @@ public class OutlinedThumbnailView extends ThumbnailView {
outliner = new Outliner(); outliner = new Outliner();
outliner.setColor(ThemeUtil.getThemedColor(getContext(), R.attr.conversation_item_image_outline_color)); outliner.setColor(ThemeUtil.getThemedColor(getContext(), R.attr.conversation_item_image_outline_color));
setRadius(0);
setWillNotDraw(false); setWillNotDraw(false);
} }

View File

@ -102,7 +102,6 @@ import org.session.libsession.utilities.recipients.RecipientModifiedListener;
import org.session.libsession.utilities.ExpirationUtil; import org.session.libsession.utilities.ExpirationUtil;
import org.session.libsession.utilities.GroupUtil; import org.session.libsession.utilities.GroupUtil;
import org.session.libsession.utilities.MediaTypes; import org.session.libsession.utilities.MediaTypes;
import org.session.libsession.utilities.SSKEnvironment;
import org.session.libsession.utilities.ServiceUtil; import org.session.libsession.utilities.ServiceUtil;
import org.session.libsession.utilities.TextSecurePreferences; import org.session.libsession.utilities.TextSecurePreferences;
import org.session.libsession.utilities.Util; import org.session.libsession.utilities.Util;
@ -165,8 +164,8 @@ import org.thoughtcrime.securesms.loki.views.MentionCandidateSelectionView;
import org.thoughtcrime.securesms.loki.views.ProfilePictureView; import org.thoughtcrime.securesms.loki.views.ProfilePictureView;
import org.thoughtcrime.securesms.mediasend.Media; import org.thoughtcrime.securesms.mediasend.Media;
import org.thoughtcrime.securesms.mediasend.MediaSendActivity; import org.thoughtcrime.securesms.mediasend.MediaSendActivity;
import org.thoughtcrime.securesms.mms.AttachmentManager; import org.thoughtcrime.securesms.conversation.v2.utilities.AttachmentManager;
import org.thoughtcrime.securesms.mms.AttachmentManager.MediaType; import org.thoughtcrime.securesms.conversation.v2.utilities.AttachmentManager.MediaType;
import org.thoughtcrime.securesms.mms.AudioSlide; import org.thoughtcrime.securesms.mms.AudioSlide;
import org.thoughtcrime.securesms.mms.GifSlide; import org.thoughtcrime.securesms.mms.GifSlide;
import org.thoughtcrime.securesms.mms.GlideApp; import org.thoughtcrime.securesms.mms.GlideApp;
@ -422,9 +421,9 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
return; return;
} }
if (!org.thoughtcrime.securesms.util.Util.isEmpty(composeText) || attachmentManager.isAttachmentPresent()) { if (!org.thoughtcrime.securesms.util.Util.isEmpty(composeText)) {
saveDraft(); saveDraft();
attachmentManager.clear(glideRequests, false); attachmentManager.clear();
silentlySetComposeText(""); silentlySetComposeText("");
} }
@ -1424,9 +1423,9 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
case AttachmentTypeSelector.ADD_SOUND: case AttachmentTypeSelector.ADD_SOUND:
AttachmentManager.selectAudio(this, PICK_AUDIO); break; AttachmentManager.selectAudio(this, PICK_AUDIO); break;
case AttachmentTypeSelector.ADD_CONTACT_INFO: case AttachmentTypeSelector.ADD_CONTACT_INFO:
AttachmentManager.selectContactInfo(this, PICK_CONTACT); break; break;
case AttachmentTypeSelector.ADD_LOCATION: case AttachmentTypeSelector.ADD_LOCATION:
AttachmentManager.selectLocation(this, PICK_LOCATION); break; break;
case AttachmentTypeSelector.TAKE_PHOTO: case AttachmentTypeSelector.TAKE_PHOTO:
attachmentManager.capturePhoto(this, TAKE_PHOTO); break; attachmentManager.capturePhoto(this, TAKE_PHOTO); break;
case AttachmentTypeSelector.ADD_GIF: case AttachmentTypeSelector.ADD_GIF:
@ -1620,7 +1619,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
private String getMessage() throws InvalidMessageException { private String getMessage() throws InvalidMessageException {
String result = composeText.getTextTrimmed(); String result = composeText.getTextTrimmed();
if (result.length() < 1 && !attachmentManager.isAttachmentPresent()) throw new InvalidMessageException(); if (result.length() < 1) throw new InvalidMessageException();
for (Mention mention : mentions) { for (Mention mention : mentions) {
try { try {
int startIndex = result.indexOf("@" + mention.getDisplayName()); int startIndex = result.indexOf("@" + mention.getDisplayName());
@ -1723,7 +1722,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
String message = getMessage(); String message = getMessage();
boolean initiating = threadId == -1; boolean initiating = threadId == -1;
boolean needsSplit = message.length() > characterCalculator.calculateCharacters(message).maxPrimaryMessageSize; boolean needsSplit = message.length() > characterCalculator.calculateCharacters(message).maxPrimaryMessageSize;
boolean isMediaMessage = attachmentManager.isAttachmentPresent() || boolean isMediaMessage = false ||
// recipient.isGroupRecipient() || // recipient.isGroupRecipient() ||
inputPanel.getQuote().isPresent() || inputPanel.getQuote().isPresent() ||
linkPreviewViewModel.hasLinkPreview() || linkPreviewViewModel.hasLinkPreview() ||
@ -1785,7 +1784,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
ApplicationContext.getInstance(context).getTypingStatusSender().onTypingStopped(threadId); ApplicationContext.getInstance(context).getTypingStatusSender().onTypingStopped(threadId);
inputPanel.clearQuote(); inputPanel.clearQuote();
attachmentManager.clear(glideRequests, false); attachmentManager.clear();
silentlySetComposeText(""); silentlySetComposeText("");
final long id = fragment.stageOutgoingMessage(outgoingMessage); final long id = fragment.stageOutgoingMessage(outgoingMessage);
@ -1859,7 +1858,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
return; return;
} }
if (composeText.getText().length() == 0 && !attachmentManager.isAttachmentPresent()) { if (composeText.getText().length() == 0) {
buttonToggle.display(attachButton); buttonToggle.display(attachButton);
quickAttachmentToggle.show(); quickAttachmentToggle.show();
inlineAttachmentToggle.hide(); inlineAttachmentToggle.hide();
@ -1867,7 +1866,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
buttonToggle.display(sendButton); buttonToggle.display(sendButton);
quickAttachmentToggle.hide(); quickAttachmentToggle.hide();
if (!attachmentManager.isAttachmentPresent() && !linkPreviewViewModel.hasLinkPreview()) { if (!linkPreviewViewModel.hasLinkPreview()) {
inlineAttachmentToggle.show(); inlineAttachmentToggle.show();
} else { } else {
inlineAttachmentToggle.hide(); inlineAttachmentToggle.hide();
@ -1876,7 +1875,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
} }
private void updateLinkPreviewState() { private void updateLinkPreviewState() {
if (TextSecurePreferences.isLinkPreviewsEnabled(this) && !attachmentManager.isAttachmentPresent()) { if (TextSecurePreferences.isLinkPreviewsEnabled(this)) {
linkPreviewViewModel.onEnabled(); linkPreviewViewModel.onEnabled();
linkPreviewViewModel.onTextChanged(this, composeText.getTextTrimmed(), composeText.getSelectionStart(), composeText.getSelectionEnd()); linkPreviewViewModel.onTextChanged(this, composeText.getTextTrimmed(), composeText.getSelectionStart(), composeText.getSelectionEnd());
} else { } else {

View File

@ -1,17 +1,27 @@
package org.thoughtcrime.securesms.conversation.v2 package org.thoughtcrime.securesms.conversation.v2
import android.Manifest
import android.animation.FloatEvaluator import android.animation.FloatEvaluator
import android.animation.ValueAnimator import android.animation.ValueAnimator
import android.content.Context
import android.content.ClipData
import android.content.ClipboardManager
import android.content.Intent import android.content.Intent
import android.content.res.Resources import android.content.res.Resources
import android.database.Cursor import android.database.Cursor
import android.graphics.Rect import android.graphics.Rect
import android.graphics.Typeface import android.graphics.Typeface
import android.os.Bundle import android.os.Bundle
import android.net.Uri
import android.os.*
import android.text.TextUtils
import android.util.Log import android.util.Log
import android.util.Pair
import android.util.TypedValue import android.util.TypedValue
import android.view.* import android.view.*
import android.widget.RelativeLayout import android.widget.RelativeLayout
import android.widget.Toast
import androidx.appcompat.app.AlertDialog
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.lifecycle.Observer import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.ViewModelProvider
@ -20,6 +30,7 @@ import androidx.loader.app.LoaderManager
import androidx.loader.content.Loader import androidx.loader.content.Loader
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import com.annimon.stream.Stream
import kotlinx.android.synthetic.main.activity_conversation_v2.* import kotlinx.android.synthetic.main.activity_conversation_v2.*
import kotlinx.android.synthetic.main.activity_conversation_v2.view.* import kotlinx.android.synthetic.main.activity_conversation_v2.view.*
import kotlinx.android.synthetic.main.activity_conversation_v2_action_bar.* import kotlinx.android.synthetic.main.activity_conversation_v2_action_bar.*
@ -29,47 +40,74 @@ import kotlinx.android.synthetic.main.view_input_bar.view.*
import kotlinx.android.synthetic.main.view_input_bar_recording.* import kotlinx.android.synthetic.main.view_input_bar_recording.*
import kotlinx.android.synthetic.main.view_input_bar_recording.view.* import kotlinx.android.synthetic.main.view_input_bar_recording.view.*
import network.loki.messenger.R import network.loki.messenger.R
import nl.komponents.kovenant.ui.failUi
import nl.komponents.kovenant.ui.successUi import nl.komponents.kovenant.ui.successUi
import org.session.libsession.messaging.MessagingModuleConfiguration
import org.session.libsession.messaging.contacts.Contact import org.session.libsession.messaging.contacts.Contact
import org.session.libsession.messaging.mentions.Mention import org.session.libsession.messaging.mentions.Mention
import org.session.libsession.messaging.mentions.MentionsManager import org.session.libsession.messaging.mentions.MentionsManager
import org.session.libsession.messaging.messages.control.DataExtractionNotification
import org.session.libsession.messaging.messages.signal.OutgoingMediaMessage
import org.session.libsession.messaging.messages.signal.OutgoingTextMessage import org.session.libsession.messaging.messages.signal.OutgoingTextMessage
import org.session.libsession.messaging.messages.visible.OpenGroupInvitation
import org.session.libsession.messaging.messages.visible.VisibleMessage import org.session.libsession.messaging.messages.visible.VisibleMessage
import org.session.libsession.messaging.open_groups.OpenGroupAPIV2 import org.session.libsession.messaging.open_groups.OpenGroupAPIV2
import org.session.libsession.messaging.sending_receiving.MessageSender import org.session.libsession.messaging.sending_receiving.MessageSender
import org.session.libsession.utilities.Address import org.session.libsession.utilities.Address
import org.session.libsession.utilities.TextSecurePreferences import org.session.libsession.utilities.TextSecurePreferences
import org.session.libsession.utilities.concurrent.SimpleTask import org.session.libsession.utilities.concurrent.SimpleTask
import org.session.libsession.messaging.sending_receiving.MessageSender.send
import org.session.libsession.messaging.sending_receiving.attachments.Attachment
import org.session.libsession.messaging.sending_receiving.link_preview.LinkPreview
import org.session.libsession.messaging.sending_receiving.quotes.QuoteModel
import org.session.libsession.utilities.Address.Companion.fromSerialized
import org.session.libsession.utilities.MediaTypes
import org.session.libsession.utilities.recipients.Recipient
import org.session.libsignal.utilities.ListenableFuture
import org.session.libsignal.utilities.ThreadUtils
import org.thoughtcrime.securesms.ApplicationContext import org.thoughtcrime.securesms.ApplicationContext
import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity
import org.thoughtcrime.securesms.audio.AudioRecorder
import org.thoughtcrime.securesms.contactshare.SimpleTextWatcher import org.thoughtcrime.securesms.contactshare.SimpleTextWatcher
import org.thoughtcrime.securesms.conversation.ConversationFragment
import org.thoughtcrime.securesms.conversation.v2.dialogs.* import org.thoughtcrime.securesms.conversation.v2.dialogs.*
import org.thoughtcrime.securesms.conversation.v2.input_bar.InputBarButton import org.thoughtcrime.securesms.conversation.v2.input_bar.InputBarButton
import org.thoughtcrime.securesms.conversation.v2.input_bar.InputBarDelegate import org.thoughtcrime.securesms.conversation.v2.input_bar.InputBarDelegate
import org.thoughtcrime.securesms.conversation.v2.input_bar.InputBarRecordingViewDelegate import org.thoughtcrime.securesms.conversation.v2.input_bar.InputBarRecordingViewDelegate
import org.thoughtcrime.securesms.conversation.v2.input_bar.mentions.MentionCandidatesView import org.thoughtcrime.securesms.conversation.v2.input_bar.mentions.MentionCandidatesView
import org.thoughtcrime.securesms.conversation.v2.menus.ConversationActionModeCallback import org.thoughtcrime.securesms.conversation.v2.menus.ConversationActionModeCallback
import org.thoughtcrime.securesms.conversation.v2.menus.ConversationActionModeCallbackDelegate
import org.thoughtcrime.securesms.conversation.v2.menus.ConversationMenuHelper import org.thoughtcrime.securesms.conversation.v2.menus.ConversationMenuHelper
import org.thoughtcrime.securesms.conversation.v2.messages.VisibleMessageView import org.thoughtcrime.securesms.conversation.v2.messages.VisibleMessageView
import org.thoughtcrime.securesms.conversation.v2.search.SearchBottomBar import org.thoughtcrime.securesms.conversation.v2.search.SearchBottomBar
import org.thoughtcrime.securesms.conversation.v2.search.SearchViewModel import org.thoughtcrime.securesms.conversation.v2.search.SearchViewModel
import org.thoughtcrime.securesms.conversation.v2.utilities.AttachmentManager
import org.thoughtcrime.securesms.database.DatabaseFactory import org.thoughtcrime.securesms.database.DatabaseFactory
import org.thoughtcrime.securesms.database.DraftDatabase import org.thoughtcrime.securesms.database.DraftDatabase
import org.thoughtcrime.securesms.database.DraftDatabase.Drafts import org.thoughtcrime.securesms.database.DraftDatabase.Drafts
import org.thoughtcrime.securesms.database.model.MessageRecord import org.thoughtcrime.securesms.database.model.MessageRecord
import org.thoughtcrime.securesms.database.model.MmsMessageRecord
import org.thoughtcrime.securesms.giph.ui.GiphyActivity import org.thoughtcrime.securesms.giph.ui.GiphyActivity
import org.thoughtcrime.securesms.linkpreview.LinkPreviewRepository import org.thoughtcrime.securesms.linkpreview.LinkPreviewRepository
import org.thoughtcrime.securesms.linkpreview.LinkPreviewUtil
import org.thoughtcrime.securesms.linkpreview.LinkPreviewViewModel import org.thoughtcrime.securesms.linkpreview.LinkPreviewViewModel
import org.thoughtcrime.securesms.linkpreview.LinkPreviewViewModel.LinkPreviewState import org.thoughtcrime.securesms.linkpreview.LinkPreviewViewModel.LinkPreviewState
import org.thoughtcrime.securesms.loki.utilities.ActivityDispatcher
import org.thoughtcrime.securesms.loki.utilities.push
import org.thoughtcrime.securesms.loki.activities.SelectContactsActivity
import org.thoughtcrime.securesms.loki.activities.SelectContactsActivity.Companion.selectedContactsKey
import org.thoughtcrime.securesms.loki.utilities.MentionUtilities
import org.thoughtcrime.securesms.loki.utilities.toPx import org.thoughtcrime.securesms.loki.utilities.toPx
import org.thoughtcrime.securesms.mediasend.Media import org.thoughtcrime.securesms.mediasend.Media
import org.thoughtcrime.securesms.mediasend.MediaSendActivity import org.thoughtcrime.securesms.mediasend.MediaSendActivity
import org.thoughtcrime.securesms.mms.* import org.thoughtcrime.securesms.mms.*
import org.thoughtcrime.securesms.notifications.MarkReadReceiver import org.thoughtcrime.securesms.notifications.MarkReadReceiver
import org.thoughtcrime.securesms.permissions.Permissions
import org.thoughtcrime.securesms.util.DateUtils import org.thoughtcrime.securesms.util.DateUtils
import org.thoughtcrime.securesms.util.MediaUtil import org.thoughtcrime.securesms.util.MediaUtil
import org.thoughtcrime.securesms.util.SaveAttachmentTask
import org.w3c.dom.Text
import java.util.* import java.util.*
import java.util.concurrent.ExecutionException
import kotlin.math.* import kotlin.math.*
// Some things that seemingly belong to the input bar (e.g. the voice message recording UI) are actually // Some things that seemingly belong to the input bar (e.g. the voice message recording UI) are actually
@ -77,7 +115,8 @@ import kotlin.math.*
// price we pay is a bit of back and forth between the input bar and the conversation activity. // price we pay is a bit of back and forth between the input bar and the conversation activity.
class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDelegate, class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDelegate,
InputBarRecordingViewDelegate, AttachmentManager.AttachmentListener, SearchBottomBar.EventListener { InputBarRecordingViewDelegate, AttachmentManager.AttachmentListener, ActivityDispatcher,
ConversationActionModeCallbackDelegate, SearchBottomBar.EventListener {
private val screenWidth = Resources.getSystem().displayMetrics.widthPixels private val screenWidth = Resources.getSystem().displayMetrics.widthPixels
private var searchViewModel: SearchViewModel? = null private var searchViewModel: SearchViewModel? = null
private var linkPreviewViewModel: LinkPreviewViewModel? = null private var linkPreviewViewModel: LinkPreviewViewModel? = null
@ -85,6 +124,9 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
private var actionMode: ActionMode? = null private var actionMode: ActionMode? = null
private var unreadCount = 0 private var unreadCount = 0
// Attachments // Attachments
private val audioRecorder = AudioRecorder(this)
private val stopAudioHandler = Handler(Looper.getMainLooper())
private val stopVoiceMessageRecordingTask = Runnable { sendVoiceMessage() }
private val attachmentManager by lazy { AttachmentManager(this, this) } private val attachmentManager by lazy { AttachmentManager(this, this) }
private var isLockViewExpanded = false private var isLockViewExpanded = false
private var isShowingAttachmentOptions = false private var isShowingAttachmentOptions = false
@ -98,16 +140,13 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
private val layoutManager: LinearLayoutManager private val layoutManager: LinearLayoutManager
get() { return conversationRecyclerView.layoutManager as LinearLayoutManager } get() { return conversationRecyclerView.layoutManager as LinearLayoutManager }
// TODO: Selected message background color
// TODO: Overflow menu background + text color
private val adapter by lazy { private val adapter by lazy {
val cursor = DatabaseFactory.getMmsSmsDatabase(this).getConversation(threadID) val cursor = DatabaseFactory.getMmsSmsDatabase(this).getConversation(threadID)
val adapter = ConversationAdapter( val adapter = ConversationAdapter(
this, this,
cursor, cursor,
onItemPress = { message, position, view -> onItemPress = { message, position, view, rawRect ->
handlePress(message, position, view) handlePress(message, position, view, rawRect)
}, },
onItemSwipeToReply = { message, position -> onItemSwipeToReply = { message, position ->
handleSwipeToReply(message, position) handleSwipeToReply(message, position)
@ -138,6 +177,7 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
const val TAKE_PHOTO = 7 const val TAKE_PHOTO = 7
const val PICK_GIF = 10 const val PICK_GIF = 10
const val PICK_FROM_LIBRARY = 12 const val PICK_FROM_LIBRARY = 12
const val INVITE_CONTACTS = 124
} }
// endregion // endregion
@ -165,6 +205,28 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
markAllAsRead() markAllAsRead()
} }
override fun onResume() {
super.onResume()
ApplicationContext.getInstance(this).messageNotifier.setVisibleThread(threadID)
}
override fun onPause() {
super.onPause()
ApplicationContext.getInstance(this).messageNotifier.setVisibleThread(-1)
}
override fun getSystemService(name: String): Any? {
if (name == ActivityDispatcher.SERVICE) {
return this
}
return super.getSystemService(name)
}
override fun dispatchIntent(body: (Context) -> Intent?) {
val intent = body(this) ?: return
push(intent, false)
}
private fun setUpRecyclerView() { private fun setUpRecyclerView() {
conversationRecyclerView.adapter = adapter conversationRecyclerView.adapter = adapter
val layoutManager = LinearLayoutManager(this, LinearLayoutManager.VERTICAL, true) val layoutManager = LinearLayoutManager(this, LinearLayoutManager.VERTICAL, true)
@ -351,8 +413,19 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
} }
override fun inputBarEditTextContentChanged(newContent: CharSequence) { override fun inputBarEditTextContentChanged(newContent: CharSequence) {
linkPreviewViewModel?.onTextChanged(this, inputBar.text, 0, 0) if (TextSecurePreferences.isLinkPreviewsEnabled(this)) {
linkPreviewViewModel?.onTextChanged(this, inputBar.text, 0, 0)
}
showOrHideMentionCandidatesIfNeeded(newContent) showOrHideMentionCandidatesIfNeeded(newContent)
if (LinkPreviewUtil.findWhitelistedUrls(newContent.toString()).isNotEmpty()
&& !TextSecurePreferences.isLinkPreviewsEnabled(this) && !TextSecurePreferences.hasSeenLinkPreviewSuggestionDialog(this)) {
LinkPreviewDialog {
setUpLinkPreviewObserver()
linkPreviewViewModel?.onEnabled()
linkPreviewViewModel?.onTextChanged(this, inputBar.text, 0, 0)
}.show(supportFragmentManager, "Link Preview Dialog")
TextSecurePreferences.setHasSeenLinkPreviewSuggestionDialog(this)
}
} }
private fun showOrHideMentionCandidatesIfNeeded(text: CharSequence) { private fun showOrHideMentionCandidatesIfNeeded(text: CharSequence) {
@ -536,16 +609,16 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
// region Interaction // region Interaction
override fun onOptionsItemSelected(item: MenuItem): Boolean { override fun onOptionsItemSelected(item: MenuItem): Boolean {
// TODO: Implement return ConversationMenuHelper.onOptionItemSelected(this, item, thread)
return super.onOptionsItemSelected(item)
} }
// `position` is the adapter position; not the visual position // `position` is the adapter position; not the visual position
private fun handlePress(message: MessageRecord, position: Int, view: VisibleMessageView) { private fun handlePress(message: MessageRecord, position: Int, view: VisibleMessageView, rawRect: Rect) {
val actionMode = this.actionMode val actionMode = this.actionMode
if (actionMode != null) { if (actionMode != null) {
adapter.toggleSelection(message, position) adapter.toggleSelection(message, position)
val actionModeCallback = ConversationActionModeCallback(adapter, threadID, this) val actionModeCallback = ConversationActionModeCallback(adapter, threadID, this)
actionModeCallback.delegate = this
actionModeCallback.updateActionModeMenu(actionMode.menu) actionModeCallback.updateActionModeMenu(actionMode.menu)
if (adapter.selectedItems.isEmpty()) { if (adapter.selectedItems.isEmpty()) {
actionMode.finish() actionMode.finish()
@ -556,19 +629,20 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
// We have to use onContentClick (rather than a click listener directly on // We have to use onContentClick (rather than a click listener directly on
// the view) so as to not interfere with all the other gestures. Do not add // the view) so as to not interfere with all the other gestures. Do not add
// onClickListeners directly to message content views. // onClickListeners directly to message content views.
view.onContentClick() view.onContentClick(rawRect)
} }
} }
// `position` is the adapter position; not the visual position // `position` is the adapter position; not the visual position
private fun handleSwipeToReply(message: MessageRecord, position: Int) { private fun handleSwipeToReply(message: MessageRecord, position: Int) {
inputBar.draftQuote(message) inputBar.draftQuote(message, glide)
} }
// `position` is the adapter position; not the visual position // `position` is the adapter position; not the visual position
private fun handleLongPress(message: MessageRecord, position: Int) { private fun handleLongPress(message: MessageRecord, position: Int) {
val actionMode = this.actionMode val actionMode = this.actionMode
val actionModeCallback = ConversationActionModeCallback(adapter, threadID, this) val actionModeCallback = ConversationActionModeCallback(adapter, threadID, this)
actionModeCallback.delegate = this
if (actionMode == null) { // Nothing should be selected if this is the case if (actionMode == null) { // Nothing should be selected if this is the case
adapter.toggleSelection(message, position) adapter.toggleSelection(message, position)
this.actionMode = if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.M) { this.actionMode = if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.M) {
@ -621,10 +695,20 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
} }
override fun onMicrophoneButtonUp(event: MotionEvent) { override fun onMicrophoneButtonUp(event: MotionEvent) {
if (isValidLockViewLocation(event.rawX.roundToInt(), event.rawY.roundToInt())) { val x = event.rawX.roundToInt()
val y = event.rawY.roundToInt()
if (isValidLockViewLocation(x, y)) {
inputBarRecordingView.lock() inputBarRecordingView.lock()
} else { } else {
hideVoiceMessageUI() val recordButtonOverlay = inputBarRecordingView.recordButtonOverlay
val location = IntArray(2) { 0 }
recordButtonOverlay.getLocationOnScreen(location)
val hitRect = Rect(location[0], location[1], location[0] + recordButtonOverlay.width, location[1] + recordButtonOverlay.height)
if (hitRect.contains(x, y)) {
sendVoiceMessage()
} else {
cancelVoiceMessage()
}
} }
} }
@ -639,7 +723,8 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
} }
private fun unblock() { private fun unblock() {
// TODO: Implement if (!thread.isContactRecipient) { return }
DatabaseFactory.getRecipientDatabase(this).setBlocked(thread, false)
} }
private fun handleMentionSelected(mention: Mention) { private fun handleMentionSelected(mention: Mention) {
@ -654,7 +739,19 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
this.previousText = newText this.previousText = newText
} }
override fun send() { override fun sendMessage() {
if (thread.isContactRecipient && thread.isBlocked) {
BlockedDialog(thread).show(supportFragmentManager, "Blocked Dialog")
return
}
if (inputBar.linkPreview != null || inputBar.quote != null) {
sendAttachments(listOf(), getMessageBody(), inputBar.quote, inputBar.linkPreview)
} else {
sendTextOnlyMessage()
}
}
private fun sendTextOnlyMessage() {
// Create the message // Create the message
val message = VisibleMessage() val message = VisibleMessage()
message.sentTimestamp = System.currentTimeMillis() message.sentTimestamp = System.currentTimeMillis()
@ -662,6 +759,8 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
val outgoingTextMessage = OutgoingTextMessage.from(message, thread) val outgoingTextMessage = OutgoingTextMessage.from(message, thread)
// Clear the input bar // Clear the input bar
inputBar.text = "" inputBar.text = ""
inputBar.cancelQuoteDraft()
inputBar.cancelLinkPreviewDraft()
// Clear mentions // Clear mentions
previousText = "" previousText = ""
currentMentionStartIndex = -1 currentMentionStartIndex = -1
@ -674,6 +773,36 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
ApplicationContext.getInstance(this).typingStatusSender.onTypingStopped(threadID) ApplicationContext.getInstance(this).typingStatusSender.onTypingStopped(threadID)
} }
private fun sendAttachments(attachments: List<Attachment>, body: String?, quotedMessage: MessageRecord? = null, linkPreview: LinkPreview? = null) {
// Create the message
val message = VisibleMessage()
message.sentTimestamp = System.currentTimeMillis()
message.text = body
val quote = quotedMessage?.let {
val quotedAttachments = (it as? MmsMessageRecord)?.slideDeck?.asAttachments() ?: listOf()
QuoteModel(it.dateSent, it.individualRecipient.address, it.body, false, quotedAttachments)
}
val outgoingTextMessage = OutgoingMediaMessage.from(message, thread, attachments, quote, linkPreview)
// Clear the input bar
inputBar.text = ""
inputBar.cancelQuoteDraft()
inputBar.cancelLinkPreviewDraft()
// Clear mentions
previousText = ""
currentMentionStartIndex = -1
mentions.clear()
// Reset the attachment manager
attachmentManager.clear()
// Reset attachments button if needed
if (isShowingAttachmentOptions) { toggleAttachmentOptions() }
// Put the message in the database
message.id = DatabaseFactory.getMmsDatabase(this).insertMessageOutbox(outgoingTextMessage, threadID, false) { }
// Send it
MessageSender.send(message, thread.address, attachments, quote, linkPreview)
// Send a typing stopped message
ApplicationContext.getInstance(this).typingStatusSender.onTypingStopped(threadID)
}
private fun showGIFPicker() { private fun showGIFPicker() {
AttachmentManager.selectGif(this, ConversationActivityV2.PICK_GIF) AttachmentManager.selectGif(this, ConversationActivityV2.PICK_GIF)
} }
@ -691,27 +820,42 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
} }
override fun onAttachmentChanged() { override fun onAttachmentChanged() {
// Do nothing
} }
override fun onActivityResult(requestCode: Int, resultCode: Int, intent: Intent?) { override fun onActivityResult(requestCode: Int, resultCode: Int, intent: Intent?) {
super.onActivityResult(requestCode, resultCode, intent) super.onActivityResult(requestCode, resultCode, intent)
intent ?: return val mediaPreppedListener = object : ListenableFuture.Listener<Boolean> {
override fun onSuccess(result: Boolean?) {
sendAttachments(attachmentManager.buildSlideDeck().asAttachments(), null)
}
override fun onFailure(e: ExecutionException?) {
Toast.makeText(this@ConversationActivityV2, R.string.activity_conversation_attachment_prep_failed, Toast.LENGTH_LONG).show()
}
}
when (requestCode) { when (requestCode) {
PICK_DOCUMENT -> { PICK_DOCUMENT -> {
val data = intent.data ?: return val uri = intent?.data ?: return
prepMediaForSending(uri, AttachmentManager.MediaType.DOCUMENT).addListener(mediaPreppedListener)
} }
TAKE_PHOTO -> { TAKE_PHOTO -> {
if (resultCode != RESULT_OK) { return }
val uri = attachmentManager.captureUri ?: return val uri = attachmentManager.captureUri ?: return
prepMediaForSending(uri, AttachmentManager.MediaType.IMAGE).addListener(mediaPreppedListener)
} }
PICK_GIF -> { PICK_GIF -> {
val data = intent.data ?: return intent ?: return
val uri = intent.data ?: return
val type = AttachmentManager.MediaType.GIF val type = AttachmentManager.MediaType.GIF
val width = intent.getIntExtra(GiphyActivity.EXTRA_WIDTH, 0) val width = intent.getIntExtra(GiphyActivity.EXTRA_WIDTH, 0)
val height = intent.getIntExtra(GiphyActivity.EXTRA_HEIGHT, 0) val height = intent.getIntExtra(GiphyActivity.EXTRA_HEIGHT, 0)
prepMediaForSending(uri, type, width, height).addListener(mediaPreppedListener)
} }
PICK_FROM_LIBRARY -> { PICK_FROM_LIBRARY -> {
val message = intent.getStringExtra(MediaSendActivity.EXTRA_MESSAGE) intent ?: return
val body = intent.getStringExtra(MediaSendActivity.EXTRA_MESSAGE)
val media = intent.getParcelableArrayListExtra<Media>(MediaSendActivity.EXTRA_MEDIA) ?: return val media = intent.getParcelableArrayListExtra<Media>(MediaSendActivity.EXTRA_MEDIA) ?: return
val slideDeck = SlideDeck() val slideDeck = SlideDeck()
for (item in media) { for (item in media) {
@ -730,9 +874,221 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
} }
} }
} }
sendAttachments(slideDeck.asAttachments(), body)
}
INVITE_CONTACTS -> {
if (!thread.isOpenGroupRecipient) { return }
val extras = intent?.extras ?: return
if (!intent.hasExtra(SelectContactsActivity.selectedContactsKey)) { return }
val selectedContacts = extras.getStringArray(selectedContactsKey)!!
val openGroup = DatabaseFactory.getLokiThreadDatabase(this).getOpenGroupChat(threadID)
for (contact in selectedContacts) {
val recipient = Recipient.from(this, fromSerialized(contact), true)
val message = VisibleMessage()
message.sentTimestamp = System.currentTimeMillis()
val openGroupInvitation = OpenGroupInvitation()
openGroupInvitation.name = openGroup!!.name
openGroupInvitation.url = openGroup!!.joinURL
message.openGroupInvitation = openGroupInvitation
val outgoingTextMessage = OutgoingTextMessage.fromOpenGroupInvitation(openGroupInvitation, recipient, message.sentTimestamp)
DatabaseFactory.getSmsDatabase(this).insertMessageOutbox(-1, outgoingTextMessage, message.sentTimestamp!!)
MessageSender.send(message, recipient.address)
}
} }
} }
} }
private fun prepMediaForSending(uri: Uri, type: AttachmentManager.MediaType): ListenableFuture<Boolean> {
return prepMediaForSending(uri, type, null, null)
}
private fun prepMediaForSending(uri: Uri, type: AttachmentManager.MediaType, width: Int?, height: Int?): ListenableFuture<Boolean> {
return attachmentManager.setMedia(glide, uri, type, MediaConstraints.getPushMediaConstraints(), width ?: 0, height ?: 0)
}
override fun startRecordingVoiceMessage() {
showVoiceMessageUI()
window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
audioRecorder.startRecording()
stopAudioHandler.postDelayed(stopVoiceMessageRecordingTask, 60000) // Limit voice messages to 1 minute each
}
override fun sendVoiceMessage() {
hideVoiceMessageUI()
window.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
val future = audioRecorder.stopRecording()
stopAudioHandler.removeCallbacks(stopVoiceMessageRecordingTask)
future.addListener(object : ListenableFuture.Listener<Pair<Uri?, Long?>> {
override fun onSuccess(result: Pair<Uri?, Long?>) {
val audioSlide = AudioSlide(this@ConversationActivityV2, result.first, result.second!!, MediaTypes.AUDIO_AAC, true)
val slideDeck = SlideDeck()
slideDeck.addSlide(audioSlide)
sendAttachments(slideDeck.asAttachments(), null)
}
override fun onFailure(e: ExecutionException) {
Toast.makeText(this@ConversationActivityV2, R.string.ConversationActivity_unable_to_record_audio, Toast.LENGTH_LONG).show()
}
})
}
override fun cancelVoiceMessage() {
hideVoiceMessageUI()
window.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
audioRecorder.stopRecording()
stopAudioHandler.removeCallbacks(stopVoiceMessageRecordingTask)
}
override fun deleteMessages(messages: Set<MessageRecord>) {
val messageCount = messages.size
val messageDataProvider = MessagingModuleConfiguration.shared.messageDataProvider
val messageDB = DatabaseFactory.getLokiMessageDatabase(this@ConversationActivityV2)
val builder = AlertDialog.Builder(this)
builder.setTitle(resources.getQuantityString(R.plurals.ConversationFragment_delete_selected_messages, messageCount, messageCount))
builder.setMessage(resources.getQuantityString(R.plurals.ConversationFragment_this_will_permanently_delete_all_n_selected_messages, messageCount, messageCount))
builder.setCancelable(true)
val openGroup = DatabaseFactory.getLokiThreadDatabase(this).getOpenGroupChat(threadID)
builder.setPositiveButton(R.string.delete) { _, _ ->
if (openGroup != null) {
val messageServerIDs = mutableMapOf<Long, MessageRecord>()
for (message in messages) {
val messageServerID = messageDB.getServerID(message.id, !message.isMms) ?: continue
messageServerIDs[messageServerID] = message
}
for ((messageServerID, message) in messageServerIDs) {
OpenGroupAPIV2.deleteMessage(messageServerID, openGroup.room, openGroup.server)
.success {
messageDataProvider.deleteMessage(message.id, !message.isMms)
}.failUi { error ->
Toast.makeText(this@ConversationActivityV2, "Couldn't delete message due to error: $error", Toast.LENGTH_LONG).show()
}
}
} else {
ThreadUtils.queue {
for (message in messages) {
if (message.isMms) {
DatabaseFactory.getMmsDatabase(this@ConversationActivityV2).delete(message.id)
} else {
DatabaseFactory.getSmsDatabase(this@ConversationActivityV2).deleteMessage(message.id)
}
}
}
}
endActionMode()
}
builder.setNegativeButton(android.R.string.cancel) { dialog, _ ->
dialog.dismiss()
endActionMode()
}
builder.show()
}
override fun banUser(messages: Set<MessageRecord>) {
val builder = AlertDialog.Builder(this)
val sessionID = messages.first().individualRecipient.address.toString()
builder.setTitle(R.string.ConversationFragment_ban_selected_user)
builder.setMessage("This will ban the selected user from this room. It won't ban them from other rooms. The selected user won't know that they've been banned.")
builder.setCancelable(true)
val openGroup = DatabaseFactory.getLokiThreadDatabase(this).getOpenGroupChat(threadID)!!
builder.setPositiveButton(R.string.ban) { _, _ ->
OpenGroupAPIV2.ban(sessionID, openGroup.room, openGroup.server).successUi {
Toast.makeText(this@ConversationActivityV2, "Successfully banned user", Toast.LENGTH_LONG).show()
}.failUi { error ->
Toast.makeText(this@ConversationActivityV2, "Couldn't ban user due to error: $error", Toast.LENGTH_LONG).show()
}
endActionMode()
}
builder.setNegativeButton(android.R.string.cancel) { dialog, _ ->
dialog.dismiss()
endActionMode()
}
builder.show()
}
override fun copyMessages(messages: Set<MessageRecord>) {
val sortedMessages = messages.sortedBy { it.dateSent }
val builder = StringBuilder()
for (message in sortedMessages) {
val body = MentionUtilities.highlightMentions(message.body, message.threadId, this)
if (TextUtils.isEmpty(body)) { continue }
val formattedTimestamp = DateUtils.getExtendedRelativeTimeSpanString(this, Locale.getDefault(), message.timestamp)
builder.append("$formattedTimestamp: $body").append('\n')
}
if (builder.isNotEmpty() && builder[builder.length - 1] == '\n') {
builder.deleteCharAt(builder.length - 1)
}
val result = builder.toString()
if (TextUtils.isEmpty(result)) { return }
val manager = getSystemService(CLIPBOARD_SERVICE) as ClipboardManager
manager.setPrimaryClip(ClipData.newPlainText("Message Content", result))
Toast.makeText(this, R.string.copied_to_clipboard, Toast.LENGTH_SHORT).show()
endActionMode()
}
override fun copySessionID(messages: Set<MessageRecord>) {
val sessionID = messages.first().individualRecipient.address.toString()
val clip = ClipData.newPlainText("Session ID", sessionID)
val manager = getSystemService(CLIPBOARD_SERVICE) as ClipboardManager
manager.setPrimaryClip(clip)
Toast.makeText(this, R.string.copied_to_clipboard, Toast.LENGTH_SHORT).show()
endActionMode()
}
override fun resendMessage(messages: Set<MessageRecord>) {
// TODO: Implement
}
override fun saveAttachment(messages: Set<MessageRecord>) {
val message = messages.first() as MmsMessageRecord
SaveAttachmentTask.showWarningDialog(this, { _, _ ->
Permissions.with(this)
.request(Manifest.permission.WRITE_EXTERNAL_STORAGE)
.maxSdkVersion(Build.VERSION_CODES.P)
.withPermanentDenialDialog(getString(R.string.MediaPreviewActivity_signal_needs_the_storage_permission_in_order_to_write_to_external_storage_but_it_has_been_permanently_denied))
.onAnyDenied {
endActionMode()
Toast.makeText(this@ConversationActivityV2, R.string.MediaPreviewActivity_unable_to_write_to_external_storage_without_permission, Toast.LENGTH_LONG).show()
}
.onAllGranted {
endActionMode()
val attachments: List<SaveAttachmentTask.Attachment?> = Stream.of(message.slideDeck.slides)
.filter { s: Slide -> s.uri != null && (s.hasImage() || s.hasVideo() || s.hasAudio() || s.hasDocument()) }
.map { s: Slide -> SaveAttachmentTask.Attachment(s.uri!!, s.contentType, message.dateReceived, s.fileName.orNull()) }
.toList()
if (attachments.isNotEmpty()) {
val saveTask = SaveAttachmentTask(this)
saveTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, *attachments.toTypedArray())
if (!message.isOutgoing) {
sendMediaSavedNotification()
}
return@onAllGranted
}
Toast.makeText(this,
resources.getQuantityString(R.plurals.ConversationFragment_error_while_saving_attachments_to_sd_card, 1),
Toast.LENGTH_LONG).show()
}
.execute()
})
}
override fun reply(messages: Set<MessageRecord>) {
inputBar.draftQuote(messages.first(), glide)
endActionMode()
}
private fun sendMediaSavedNotification() {
if (thread.isGroupRecipient) { return }
val timestamp = System.currentTimeMillis()
val kind = DataExtractionNotification.Kind.MediaSaved(timestamp)
val message = DataExtractionNotification(kind)
MessageSender.send(message, thread.address)
}
private fun endActionMode() {
actionMode?.finish()
actionMode = null
}
// endregion // endregion
// region General // region General

View File

@ -2,6 +2,7 @@ package org.thoughtcrime.securesms.conversation.v2
import android.content.Context import android.content.Context
import android.database.Cursor import android.database.Cursor
import android.graphics.Rect
import android.view.ViewGroup import android.view.ViewGroup
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.recyclerview.widget.RecyclerView.ViewHolder import androidx.recyclerview.widget.RecyclerView.ViewHolder
@ -13,7 +14,7 @@ import org.thoughtcrime.securesms.database.DatabaseFactory
import org.thoughtcrime.securesms.database.model.MessageRecord import org.thoughtcrime.securesms.database.model.MessageRecord
import org.thoughtcrime.securesms.mms.GlideRequests import org.thoughtcrime.securesms.mms.GlideRequests
class ConversationAdapter(context: Context, cursor: Cursor, private val onItemPress: (MessageRecord, Int, VisibleMessageView) -> Unit, class ConversationAdapter(context: Context, cursor: Cursor, private val onItemPress: (MessageRecord, Int, VisibleMessageView, Rect) -> Unit,
private val onItemSwipeToReply: (MessageRecord, Int) -> Unit, private val onItemLongPress: (MessageRecord, Int) -> Unit, private val onItemSwipeToReply: (MessageRecord, Int) -> Unit, private val onItemLongPress: (MessageRecord, Int) -> Unit,
private val glide: GlideRequests) private val glide: GlideRequests)
: CursorRecyclerViewAdapter<ViewHolder>(context, cursor) { : CursorRecyclerViewAdapter<ViewHolder>(context, cursor) {
@ -69,7 +70,7 @@ class ConversationAdapter(context: Context, cursor: Cursor, private val onItemPr
view.messageTimestampTextView.isVisible = isSelected view.messageTimestampTextView.isVisible = isSelected
val position = viewHolder.adapterPosition val position = viewHolder.adapterPosition
view.bind(message, getMessageBefore(position, cursor), getMessageAfter(position, cursor), glide, searchQuery) view.bind(message, getMessageBefore(position, cursor), getMessageAfter(position, cursor), glide, searchQuery)
view.onPress = { onItemPress(message, viewHolder.adapterPosition, view) } view.onPress = { rawX, rawY -> onItemPress(message, viewHolder.adapterPosition, view, Rect(rawX, rawY, rawX, rawY)) }
view.onSwipeToReply = { onItemSwipeToReply(message, viewHolder.adapterPosition) } view.onSwipeToReply = { onItemSwipeToReply(message, viewHolder.adapterPosition) }
view.onLongPress = { onItemLongPress(message, viewHolder.adapterPosition) } view.onLongPress = { onItemLongPress(message, viewHolder.adapterPosition) }
} }

View File

@ -0,0 +1,172 @@
package org.thoughtcrime.securesms.conversation.v2.components
import android.content.Context
import android.graphics.Canvas
import android.graphics.Rect
import android.util.AttributeSet
import android.view.LayoutInflater
import android.view.ViewGroup
import android.widget.FrameLayout
import android.widget.TextView
import androidx.core.view.children
import androidx.core.view.isVisible
import kotlinx.android.synthetic.main.album_thumbnail_view.view.*
import network.loki.messenger.R
import org.session.libsession.utilities.ViewUtil
import org.thoughtcrime.securesms.MediaPreviewActivity
import org.thoughtcrime.securesms.components.CornerMask
import org.thoughtcrime.securesms.conversation.v2.utilities.KThumbnailView
import org.thoughtcrime.securesms.database.model.MmsMessageRecord
import org.thoughtcrime.securesms.loki.utilities.ActivityDispatcher
import org.thoughtcrime.securesms.longmessage.LongMessageActivity
import org.thoughtcrime.securesms.mms.GlideRequests
import org.thoughtcrime.securesms.mms.Slide
import kotlin.math.roundToInt
class AlbumThumbnailView : FrameLayout {
companion object {
const val MAX_ALBUM_DISPLAY_SIZE = 5
}
// region Lifecycle
constructor(context: Context) : super(context) {
initialize()
}
constructor(context: Context, attrs: AttributeSet) : super(context, attrs) {
initialize()
}
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr) {
initialize()
}
private val cornerMask by lazy { CornerMask(this) }
private var slides: List<Slide> = listOf()
private var slideSize: Int = 0
private fun initialize() {
LayoutInflater.from(context).inflate(R.layout.album_thumbnail_view, this)
}
override fun dispatchDraw(canvas: Canvas?) {
super.dispatchDraw(canvas)
cornerMask.mask(canvas)
}
// endregion
// region Interaction
fun calculateHitObject(rawRect: Rect, mms: MmsMessageRecord) {
// Z-check in specific order
val testRect = Rect()
// test "Read More"
albumCellBodyTextReadMore.getGlobalVisibleRect(testRect)
if (Rect.intersects(rawRect, testRect)) {
// dispatch to activity view
ActivityDispatcher.get(context)?.dispatchIntent { context ->
LongMessageActivity.getIntent(context, mms.recipient.address, mms.getId(), true)
}
return
}
// test each album child
albumCellContainer.findViewById<ViewGroup>(R.id.album_thumbnail_root)?.children?.forEachIndexed { index, child ->
child.getGlobalVisibleRect(testRect)
if (Rect.intersects(rawRect, testRect)) {
// hit intersects with this particular child
val slide = slides.getOrNull(index) ?: return
// only open to downloaded images
if (slide.isInProgress) return
ActivityDispatcher.get(context)?.dispatchIntent { context ->
MediaPreviewActivity.getPreviewIntent(context, slide, mms)
}
}
}
}
fun bind(glideRequests: GlideRequests, message: MmsMessageRecord,
isStart: Boolean, isEnd: Boolean) {
slides = message.slideDeck.thumbnailSlides
if (slides.isEmpty()) {
// this should never be encountered because it's checked by parent
return
}
calculateRadius(isStart, isEnd, message.isOutgoing)
// recreate cell views if different size to what we have already (for recycling)
if (slides.size != this.slideSize) {
albumCellContainer.removeAllViews()
LayoutInflater.from(context).inflate(layoutRes(slides.size), albumCellContainer)
val overflowed = slides.size > MAX_ALBUM_DISPLAY_SIZE
albumCellContainer.findViewById<TextView>(R.id.album_cell_overflow_text)?.let { overflowText ->
// overflowText will be null if !overflowed
overflowText.isVisible = overflowed // more than max album size
overflowText.text = context.getString(R.string.AlbumThumbnailView_plus, slides.size - MAX_ALBUM_DISPLAY_SIZE)
}
this.slideSize = slides.size
}
// iterate binding
slides.take(5).forEachIndexed { position, slide ->
val thumbnailView = getThumbnailView(position)
thumbnailView.setImageResource(glideRequests, slide, isPreview = false)
}
albumCellBodyParent.isVisible = message.body.isNotEmpty()
albumCellBodyText.text = message.body
post {
// post to await layout of text
albumCellBodyText.layout?.let { layout ->
val maxEllipsis = (0 until layout.lineCount).maxByOrNull { lineNum -> layout.getEllipsisCount(lineNum) }
?: 0
// show read more text if at least one line is ellipsized
ViewUtil.setPaddingTop(albumCellBodyTextParent, if (maxEllipsis > 0) resources.getDimension(R.dimen.small_spacing).roundToInt() else resources.getDimension(R.dimen.medium_spacing).roundToInt())
albumCellBodyTextReadMore.isVisible = maxEllipsis > 0
}
}
}
// endregion
fun layoutRes(slideCount: Int) = when (slideCount) {
1 -> R.layout.album_thumbnail_1 // single
2 -> R.layout.album_thumbnail_2// two sidebyside
3 -> R.layout.album_thumbnail_3// three stacked
4 -> R.layout.album_thumbnail_4// four square
5 -> R.layout.album_thumbnail_5//
else -> R.layout.album_thumbnail_many// five or more
}
fun getThumbnailView(position: Int): KThumbnailView = when (position) {
0 -> albumCellContainer.findViewById<ViewGroup>(R.id.albumCellContainer).findViewById(R.id.album_cell_1)
1 -> albumCellContainer.findViewById<ViewGroup>(R.id.albumCellContainer).findViewById(R.id.album_cell_2)
2 -> albumCellContainer.findViewById<ViewGroup>(R.id.albumCellContainer).findViewById(R.id.album_cell_3)
3 -> albumCellContainer.findViewById<ViewGroup>(R.id.albumCellContainer).findViewById(R.id.album_cell_4)
4 -> albumCellContainer.findViewById<ViewGroup>(R.id.albumCellContainer).findViewById(R.id.album_cell_5)
else -> throw Exception("Can't get thumbnail view for non-existent thumbnail at position: $position")
}
fun calculateRadius(isStart: Boolean, isEnd: Boolean, outgoing: Boolean) {
val roundedDimen = context.resources.getDimension(R.dimen.message_corner_radius).toInt()
val collapsedDimen = context.resources.getDimension(R.dimen.message_corner_collapse_radius).toInt()
val (startTop, endTop, startBottom, endBottom) = when {
// single message, consistent dimen
isStart && isEnd -> intArrayOf(roundedDimen, roundedDimen, roundedDimen, roundedDimen)
// start of message cluster, collapsed BL
isStart -> intArrayOf(roundedDimen, roundedDimen, collapsedDimen, roundedDimen)
// end of message cluster, collapsed TL
isEnd -> intArrayOf(collapsedDimen, roundedDimen, roundedDimen, roundedDimen)
// else in the middle, no rounding left side
else -> intArrayOf(collapsedDimen, roundedDimen, collapsedDimen, roundedDimen)
}
// TL, TR, BR, BL (CW direction)
cornerMask.setRadii(
if (!outgoing) startTop else endTop, // TL
if (!outgoing) endTop else startTop, // TR
if (!outgoing) endBottom else startBottom, // BR
if (!outgoing) startBottom else endBottom // BL
)
}
}

View File

@ -36,6 +36,7 @@ class BlockedDialog(private val recipient: Recipient) : BaseDialog() {
} }
private fun unblock() { private fun unblock() {
// TODO: Implement DatabaseFactory.getRecipientDatabase(requireContext()).setBlocked(recipient, false)
dismiss()
} }
} }

View File

@ -9,14 +9,16 @@ import androidx.appcompat.app.AlertDialog
import kotlinx.android.synthetic.main.dialog_join_open_group.view.* import kotlinx.android.synthetic.main.dialog_join_open_group.view.*
import network.loki.messenger.R import network.loki.messenger.R
import org.session.libsession.messaging.open_groups.OpenGroupV2 import org.session.libsession.messaging.open_groups.OpenGroupV2
import org.session.libsession.utilities.OpenGroupUrlParser
import org.thoughtcrime.securesms.conversation.v2.utilities.BaseDialog import org.thoughtcrime.securesms.conversation.v2.utilities.BaseDialog
import org.thoughtcrime.securesms.loki.api.OpenGroupManager
import org.thoughtcrime.securesms.loki.protocol.MultiDeviceProtocol
/** Shown upon tapping an open group invitation. */ /** Shown upon tapping an open group invitation. */
class JoinOpenGroupDialog(private val openGroup: OpenGroupV2) : BaseDialog() { class JoinOpenGroupDialog(private val name: String, private val url: String) : BaseDialog() {
override fun setContentView(builder: AlertDialog.Builder) { override fun setContentView(builder: AlertDialog.Builder) {
val contentView = LayoutInflater.from(requireContext()).inflate(R.layout.dialog_join_open_group, null) val contentView = LayoutInflater.from(requireContext()).inflate(R.layout.dialog_join_open_group, null)
val name = openGroup.name
val title = resources.getString(R.string.dialog_join_open_group_title, name) val title = resources.getString(R.string.dialog_join_open_group_title, name)
contentView.joinOpenGroupTitleTextView.text = title contentView.joinOpenGroupTitleTextView.text = title
val explanation = resources.getString(R.string.dialog_join_open_group_explanation, name) val explanation = resources.getString(R.string.dialog_join_open_group_explanation, name)
@ -30,6 +32,9 @@ class JoinOpenGroupDialog(private val openGroup: OpenGroupV2) : BaseDialog() {
} }
private fun join() { private fun join() {
// TODO: Implement val openGroup = OpenGroupUrlParser.parseUrl(url)
OpenGroupManager.add(openGroup.server, openGroup.room, openGroup.serverPublicKey, requireContext())
MultiDeviceProtocol.forceSyncConfigurationNowIfNeeded(requireContext())
dismiss()
} }
} }

View File

@ -4,11 +4,12 @@ import android.view.LayoutInflater
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import kotlinx.android.synthetic.main.dialog_link_preview.view.* import kotlinx.android.synthetic.main.dialog_link_preview.view.*
import network.loki.messenger.R import network.loki.messenger.R
import org.session.libsession.utilities.TextSecurePreferences
import org.thoughtcrime.securesms.conversation.v2.utilities.BaseDialog import org.thoughtcrime.securesms.conversation.v2.utilities.BaseDialog
/** Shown the first time the user inputs a URL that could generate a link preview, to /** Shown the first time the user inputs a URL that could generate a link preview, to
* let them know that Session offers the ability to send and receive link previews. */ * let them know that Session offers the ability to send and receive link previews. */
class LinkPreviewDialog() : BaseDialog() { class LinkPreviewDialog(private val onEnabled: () -> Unit) : BaseDialog() {
override fun setContentView(builder: AlertDialog.Builder) { override fun setContentView(builder: AlertDialog.Builder) {
val contentView = LayoutInflater.from(requireContext()).inflate(R.layout.dialog_link_preview, null) val contentView = LayoutInflater.from(requireContext()).inflate(R.layout.dialog_link_preview, null)
@ -18,6 +19,8 @@ class LinkPreviewDialog() : BaseDialog() {
} }
private fun enable() { private fun enable() {
// TODO: Implement TextSecurePreferences.setLinkPreviewsEnabled(requireContext(), true)
dismiss()
onEnabled()
} }
} }

View File

@ -1,10 +1,13 @@
package org.thoughtcrime.securesms.conversation.v2.dialogs package org.thoughtcrime.securesms.conversation.v2.dialogs
import android.content.Intent
import android.graphics.Typeface import android.graphics.Typeface
import android.net.Uri
import android.text.Spannable import android.text.Spannable
import android.text.SpannableStringBuilder import android.text.SpannableStringBuilder
import android.text.style.StyleSpan import android.text.style.StyleSpan
import android.view.LayoutInflater import android.view.LayoutInflater
import android.widget.Toast
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import kotlinx.android.synthetic.main.dialog_open_url.view.* import kotlinx.android.synthetic.main.dialog_open_url.view.*
import network.loki.messenger.R import network.loki.messenger.R
@ -26,6 +29,12 @@ class OpenURLDialog(private val url: String) : BaseDialog() {
} }
private fun open() { private fun open() {
// TODO: Implement try {
val intent = Intent(Intent.ACTION_VIEW, Uri.parse(url))
requireContext().startActivity(intent)
} catch (e: Exception) {
Toast.makeText(context, R.string.invalid_url, Toast.LENGTH_SHORT).show()
}
dismiss()
} }
} }

View File

@ -8,7 +8,6 @@ import android.view.MotionEvent
import android.widget.RelativeLayout import android.widget.RelativeLayout
import androidx.core.view.isVisible import androidx.core.view.isVisible
import kotlinx.android.synthetic.main.view_input_bar.view.* import kotlinx.android.synthetic.main.view_input_bar.view.*
import kotlinx.android.synthetic.main.view_quote.view.*
import network.loki.messenger.R import network.loki.messenger.R
import org.session.libsession.messaging.sending_receiving.link_preview.LinkPreview import org.session.libsession.messaging.sending_receiving.link_preview.LinkPreview
import org.thoughtcrime.securesms.conversation.v2.components.LinkPreviewDraftView import org.thoughtcrime.securesms.conversation.v2.components.LinkPreviewDraftView
@ -30,6 +29,8 @@ class InputBar : RelativeLayout, InputBarEditTextDelegate, QuoteViewDelegate, Li
private var linkPreviewDraftView: LinkPreviewDraftView? = null private var linkPreviewDraftView: LinkPreviewDraftView? = null
var delegate: InputBarDelegate? = null var delegate: InputBarDelegate? = null
var additionalContentHeight = 0 var additionalContentHeight = 0
var quote: MessageRecord? = null
var linkPreview: LinkPreview? = null
var text: String var text: String
get() { return inputBarEditText.text.toString() } get() { return inputBarEditText.text.toString() }
@ -53,7 +54,7 @@ class InputBar : RelativeLayout, InputBarEditTextDelegate, QuoteViewDelegate, Li
// Microphone button // Microphone button
microphoneOrSendButtonContainer.addView(microphoneButton) microphoneOrSendButtonContainer.addView(microphoneButton)
microphoneButton.layoutParams = RelativeLayout.LayoutParams(RelativeLayout.LayoutParams.MATCH_PARENT, RelativeLayout.LayoutParams.MATCH_PARENT) microphoneButton.layoutParams = RelativeLayout.LayoutParams(RelativeLayout.LayoutParams.MATCH_PARENT, RelativeLayout.LayoutParams.MATCH_PARENT)
microphoneButton.onLongPress = { showVoiceMessageUI() } microphoneButton.onLongPress = { startRecordingVoiceMessage() }
microphoneButton.onMove = { delegate?.onMicrophoneButtonMove(it) } microphoneButton.onMove = { delegate?.onMicrophoneButtonMove(it) }
microphoneButton.onCancel = { delegate?.onMicrophoneButtonCancel(it) } microphoneButton.onCancel = { delegate?.onMicrophoneButtonCancel(it) }
microphoneButton.onUp = { delegate?.onMicrophoneButtonUp(it) } microphoneButton.onUp = { delegate?.onMicrophoneButtonUp(it) }
@ -61,7 +62,7 @@ class InputBar : RelativeLayout, InputBarEditTextDelegate, QuoteViewDelegate, Li
microphoneOrSendButtonContainer.addView(sendButton) microphoneOrSendButtonContainer.addView(sendButton)
sendButton.layoutParams = RelativeLayout.LayoutParams(RelativeLayout.LayoutParams.MATCH_PARENT, RelativeLayout.LayoutParams.MATCH_PARENT) sendButton.layoutParams = RelativeLayout.LayoutParams(RelativeLayout.LayoutParams.MATCH_PARENT, RelativeLayout.LayoutParams.MATCH_PARENT)
sendButton.isVisible = false sendButton.isVisible = false
sendButton.onUp = { delegate?.send() } sendButton.onUp = { delegate?.sendMessage() }
// Edit text // Edit text
inputBarEditText.imeOptions = inputBarEditText.imeOptions or 16777216 // Always use incognito keyboard inputBarEditText.imeOptions = inputBarEditText.imeOptions or 16777216 // Always use incognito keyboard
inputBarEditText.delegate = this inputBarEditText.delegate = this
@ -93,14 +94,16 @@ class InputBar : RelativeLayout, InputBarEditTextDelegate, QuoteViewDelegate, Li
delegate?.toggleAttachmentOptions() delegate?.toggleAttachmentOptions()
} }
private fun showVoiceMessageUI() { private fun startRecordingVoiceMessage() {
delegate?.showVoiceMessageUI() delegate?.startRecordingVoiceMessage()
} }
// Drafting quotes and drafting link previews is mutually exclusive, i.e. you can't draft // Drafting quotes and drafting link previews is mutually exclusive, i.e. you can't draft
// a quote and a link preview at the same time. // a quote and a link preview at the same time.
fun draftQuote(message: MessageRecord) { fun draftQuote(message: MessageRecord, glide: GlideRequests) {
quote = message
linkPreview = null
linkPreviewDraftView = null linkPreviewDraftView = null
inputBarAdditionalContentContainer.removeAllViews() inputBarAdditionalContentContainer.removeAllViews()
val quoteView = QuoteView(context, QuoteView.Mode.Draft) val quoteView = QuoteView(context, QuoteView.Mode.Draft)
@ -112,7 +115,7 @@ class InputBar : RelativeLayout, InputBarEditTextDelegate, QuoteViewDelegate, Li
// here to get the layout right. // here to get the layout right.
val maxContentWidth = (screenWidth - 2 * resources.getDimension(R.dimen.medium_spacing) - toPx(16, resources) - toPx(30, resources)).roundToInt() val maxContentWidth = (screenWidth - 2 * resources.getDimension(R.dimen.medium_spacing) - toPx(16, resources) - toPx(30, resources)).roundToInt()
quoteView.bind(message.individualRecipient.address.toString(), message.body, attachments, quoteView.bind(message.individualRecipient.address.toString(), message.body, attachments,
message.recipient, true, maxContentWidth, message.isOpenGroupInvitation, message.threadId) message.recipient, true, maxContentWidth, message.isOpenGroupInvitation, message.threadId, glide)
// The 6 DP below is the padding the quote view applies to itself, which isn't included in the // The 6 DP below is the padding the quote view applies to itself, which isn't included in the
// intrinsic height calculation. // intrinsic height calculation.
val quoteViewIntrinsicHeight = quoteView.getIntrinsicHeight(maxContentWidth) + toPx(6, resources) val quoteViewIntrinsicHeight = quoteView.getIntrinsicHeight(maxContentWidth) + toPx(6, resources)
@ -122,6 +125,7 @@ class InputBar : RelativeLayout, InputBarEditTextDelegate, QuoteViewDelegate, Li
} }
override fun cancelQuoteDraft() { override fun cancelQuoteDraft() {
quote = null
inputBarAdditionalContentContainer.removeAllViews() inputBarAdditionalContentContainer.removeAllViews()
val newHeight = max(inputBarEditText.height + 2 * vMargin, minHeight) val newHeight = max(inputBarEditText.height + 2 * vMargin, minHeight)
additionalContentHeight = 0 additionalContentHeight = 0
@ -129,6 +133,7 @@ class InputBar : RelativeLayout, InputBarEditTextDelegate, QuoteViewDelegate, Li
} }
fun draftLinkPreview() { fun draftLinkPreview() {
quote = null
val linkPreviewDraftHeight = toPx(88, resources) val linkPreviewDraftHeight = toPx(88, resources)
inputBarAdditionalContentContainer.removeAllViews() inputBarAdditionalContentContainer.removeAllViews()
val linkPreviewDraftView = LinkPreviewDraftView(context) val linkPreviewDraftView = LinkPreviewDraftView(context)
@ -141,11 +146,14 @@ class InputBar : RelativeLayout, InputBarEditTextDelegate, QuoteViewDelegate, Li
} }
fun updateLinkPreviewDraft(glide: GlideRequests, linkPreview: LinkPreview) { fun updateLinkPreviewDraft(glide: GlideRequests, linkPreview: LinkPreview) {
this.linkPreview = linkPreview
val linkPreviewDraftView = this.linkPreviewDraftView ?: return val linkPreviewDraftView = this.linkPreviewDraftView ?: return
linkPreviewDraftView.update(glide, linkPreview) linkPreviewDraftView.update(glide, linkPreview)
} }
override fun cancelLinkPreviewDraft() { override fun cancelLinkPreviewDraft() {
if (quote != null) { return }
linkPreview = null
inputBarAdditionalContentContainer.removeAllViews() inputBarAdditionalContentContainer.removeAllViews()
val newHeight = max(inputBarEditText.height + 2 * vMargin, minHeight) val newHeight = max(inputBarEditText.height + 2 * vMargin, minHeight)
additionalContentHeight = 0 additionalContentHeight = 0
@ -160,8 +168,9 @@ interface InputBarDelegate {
fun inputBarEditTextContentChanged(newContent: CharSequence) fun inputBarEditTextContentChanged(newContent: CharSequence)
fun toggleAttachmentOptions() fun toggleAttachmentOptions()
fun showVoiceMessageUI() fun showVoiceMessageUI()
fun startRecordingVoiceMessage()
fun onMicrophoneButtonMove(event: MotionEvent) fun onMicrophoneButtonMove(event: MotionEvent)
fun onMicrophoneButtonCancel(event: MotionEvent) fun onMicrophoneButtonCancel(event: MotionEvent)
fun onMicrophoneButtonUp(event: MotionEvent) fun onMicrophoneButtonUp(event: MotionEvent)
fun send() fun sendMessage()
} }

View File

@ -134,10 +134,14 @@ class InputBarRecordingView : RelativeLayout {
} }
fadeInAnimation.start() fadeInAnimation.start()
recordButtonOverlayImageView.setImageDrawable(ResourcesCompat.getDrawable(resources, R.drawable.ic_arrow_up, context.theme)) recordButtonOverlayImageView.setImageDrawable(ResourcesCompat.getDrawable(resources, R.drawable.ic_arrow_up, context.theme))
recordButtonOverlay.setOnClickListener { delegate?.sendVoiceMessage() }
inputBarCancelButton.setOnClickListener { delegate?.cancelVoiceMessage() }
} }
} }
interface InputBarRecordingViewDelegate { interface InputBarRecordingViewDelegate {
fun handleVoiceMessageUIHidden() fun handleVoiceMessageUIHidden()
fun sendVoiceMessage()
fun cancelVoiceMessage()
} }

View File

@ -10,9 +10,11 @@ import org.session.libsession.utilities.TextSecurePreferences
import org.thoughtcrime.securesms.conversation.v2.ConversationAdapter import org.thoughtcrime.securesms.conversation.v2.ConversationAdapter
import org.thoughtcrime.securesms.database.DatabaseFactory import org.thoughtcrime.securesms.database.DatabaseFactory
import org.thoughtcrime.securesms.database.model.MediaMmsMessageRecord import org.thoughtcrime.securesms.database.model.MediaMmsMessageRecord
import org.thoughtcrime.securesms.database.model.MessageRecord
class ConversationActionModeCallback(private val adapter: ConversationAdapter, private val threadID: Long, class ConversationActionModeCallback(private val adapter: ConversationAdapter, private val threadID: Long,
private val context: Context) : ActionMode.Callback { private val context: Context) : ActionMode.Callback {
var delegate: ConversationActionModeCallbackDelegate? = null
override fun onCreateActionMode(mode: ActionMode, menu: Menu): Boolean { override fun onCreateActionMode(mode: ActionMode, menu: Menu): Boolean {
val inflater = mode.menuInflater val inflater = mode.menuInflater
@ -44,8 +46,6 @@ class ConversationActionModeCallback(private val adapter: ConversationAdapter, p
if (selectedUsers.size > 1) { return false } if (selectedUsers.size > 1) { return false }
return OpenGroupAPIV2.isUserModerator(userPublicKey, openGroup.room, openGroup.server) return OpenGroupAPIV2.isUserModerator(userPublicKey, openGroup.room, openGroup.server)
} }
// Message info
menu.findItem(R.id.menu_context_details).isVisible = (selectedItems.size == 1)
// Delete message // Delete message
menu.findItem(R.id.menu_context_delete_message).isVisible = userCanDeleteSelectedItems() menu.findItem(R.id.menu_context_delete_message).isVisible = userCanDeleteSelectedItems()
// Ban user // Ban user
@ -70,6 +70,16 @@ class ConversationActionModeCallback(private val adapter: ConversationAdapter, p
} }
override fun onActionItemClicked(mode: ActionMode, item: MenuItem): Boolean { override fun onActionItemClicked(mode: ActionMode, item: MenuItem): Boolean {
val selectedItems = adapter.selectedItems
when (item.itemId) {
R.id.menu_context_delete_message -> delegate?.deleteMessages(selectedItems)
R.id.menu_context_ban_user -> delegate?.banUser(selectedItems)
R.id.menu_context_copy -> delegate?.copyMessages(selectedItems)
R.id.menu_context_copy_public_key -> delegate?.copySessionID(selectedItems)
R.id.menu_context_resend -> delegate?.resendMessage(selectedItems)
R.id.menu_context_save_attachment -> delegate?.saveAttachment(selectedItems)
R.id.menu_context_reply -> delegate?.reply(selectedItems)
}
return true return true
} }
@ -77,4 +87,15 @@ class ConversationActionModeCallback(private val adapter: ConversationAdapter, p
adapter.selectedItems.clear() adapter.selectedItems.clear()
adapter.notifyDataSetChanged() adapter.notifyDataSetChanged()
} }
}
interface ConversationActionModeCallbackDelegate {
fun deleteMessages(messages: Set<MessageRecord>)
fun banUser(messages: Set<MessageRecord>)
fun copyMessages(messages: Set<MessageRecord>)
fun copySessionID(messages: Set<MessageRecord>)
fun resendMessage(messages: Set<MessageRecord>)
fun saveAttachment(messages: Set<MessageRecord>)
fun reply(messages: Set<MessageRecord>)
} }

View File

@ -1,26 +1,49 @@
package org.thoughtcrime.securesms.conversation.v2.menus package org.thoughtcrime.securesms.conversation.v2.menus
import android.annotation.SuppressLint
import android.content.ClipData
import android.content.ClipboardManager
import android.content.Context import android.content.Context
import android.content.Intent
import android.graphics.BitmapFactory
import android.graphics.PorterDuff import android.graphics.PorterDuff
import android.graphics.PorterDuffColorFilter import android.graphics.PorterDuffColorFilter
import android.os.AsyncTask
import android.view.Menu import android.view.Menu
import android.view.MenuInflater import android.view.MenuInflater
import android.view.MenuItem import android.view.MenuItem
import android.view.View import android.view.View
import android.widget.ImageView import android.widget.ImageView
import android.widget.TextView import android.widget.TextView
import android.widget.Toast
import androidx.annotation.ColorInt import androidx.annotation.ColorInt
import androidx.appcompat.widget.SearchView import androidx.appcompat.widget.SearchView
import androidx.appcompat.widget.SearchView.OnQueryTextListener import androidx.appcompat.widget.SearchView.OnQueryTextListener
import androidx.lifecycle.ViewModelProvider
import kotlinx.android.synthetic.main.activity_conversation_v2.* import kotlinx.android.synthetic.main.activity_conversation_v2.*
import kotlinx.android.synthetic.main.session_logo_action_bar_content.* import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.pm.ShortcutInfoCompat
import androidx.core.content.pm.ShortcutManagerCompat
import androidx.core.graphics.drawable.IconCompat
import network.loki.messenger.R import network.loki.messenger.R
import org.session.libsession.messaging.messages.control.ExpirationTimerUpdate
import org.session.libsession.messaging.sending_receiving.MessageSender
import org.session.libsession.messaging.sending_receiving.leave
import org.session.libsession.utilities.ExpirationUtil import org.session.libsession.utilities.ExpirationUtil
import org.session.libsession.utilities.GroupUtil.doubleDecodeGroupID
import org.session.libsession.utilities.TextSecurePreferences
import org.session.libsession.utilities.recipients.Recipient import org.session.libsession.utilities.recipients.Recipient
import org.thoughtcrime.securesms.conversation.v2.ConversationActivityV2 import org.thoughtcrime.securesms.conversation.v2.ConversationActivityV2
import org.thoughtcrime.securesms.conversation.v2.search.SearchViewModel import org.session.libsignal.utilities.guava.Optional
import org.session.libsignal.utilities.toHexString
import org.thoughtcrime.securesms.*
import org.thoughtcrime.securesms.database.DatabaseFactory
import org.thoughtcrime.securesms.loki.activities.EditClosedGroupActivity
import org.thoughtcrime.securesms.loki.activities.EditClosedGroupActivity.Companion.groupIDKey
import org.thoughtcrime.securesms.loki.activities.SelectContactsActivity
import org.thoughtcrime.securesms.loki.utilities.getColorWithID import org.thoughtcrime.securesms.loki.utilities.getColorWithID
import org.thoughtcrime.securesms.util.BitmapUtil
import java.io.IOException
object ConversationMenuHelper { object ConversationMenuHelper {
@ -69,6 +92,7 @@ object ConversationMenuHelper {
} else { } else {
inflater.inflate(R.menu.menu_conversation_unmuted, menu) inflater.inflate(R.menu.menu_conversation_unmuted, menu)
} }
// Search // Search
val searchViewItem = menu.findItem(R.id.menu_search) val searchViewItem = menu.findItem(R.id.menu_search)
val searchView = searchViewItem.actionView as SearchView val searchView = searchViewItem.actionView as SearchView
@ -114,4 +138,195 @@ object ConversationMenuHelper {
} }
}) })
} }
fun onOptionItemSelected(context: Context, item: MenuItem, thread: Recipient): Boolean {
when (item.itemId) {
R.id.menu_view_all_media -> { showAllMedia(context, thread) }
R.id.menu_search -> { search(context) }
R.id.menu_add_shortcut -> { addShortcut(context, thread) }
R.id.menu_expiring_messages -> { showExpiringMessagesDialog(context, thread) }
R.id.menu_expiring_messages_off -> { showExpiringMessagesDialog(context, thread) }
R.id.menu_unblock -> { unblock(context, thread) }
R.id.menu_block -> { block(context, thread) }
R.id.menu_copy_session_id -> { copySessionID(context, thread) }
R.id.menu_edit_group -> { editClosedGroup(context, thread) }
R.id.menu_leave_group -> { leaveClosedGroup(context, thread) }
R.id.menu_invite_to_open_group -> { inviteContacts(context, thread) }
R.id.menu_unmute_notifications -> { unmute(context, thread) }
R.id.menu_mute_notifications -> { mute(context, thread) }
}
return true
}
private fun showAllMedia(context: Context, thread: Recipient) {
val intent = Intent(context, MediaOverviewActivity::class.java)
intent.putExtra(MediaOverviewActivity.ADDRESS_EXTRA, thread.address)
val activity = context as AppCompatActivity
activity.startActivity(intent)
}
private fun search(context: Context) {
Toast.makeText(context, "Not yet implemented", Toast.LENGTH_LONG).show() // TODO: Implement
}
@SuppressLint("StaticFieldLeak")
private fun addShortcut(context: Context, thread: Recipient) {
object : AsyncTask<Void?, Void?, IconCompat?>() {
override fun doInBackground(vararg params: Void?): IconCompat? {
var icon: IconCompat? = null
val contactPhoto = thread.contactPhoto
if (contactPhoto != null) {
try {
var bitmap = BitmapFactory.decodeStream(contactPhoto.openInputStream(context))
bitmap = BitmapUtil.createScaledBitmap(bitmap, 300, 300)
icon = IconCompat.createWithAdaptiveBitmap(bitmap)
} catch (e: IOException) {
// Do nothing
}
}
if (icon == null) {
icon = IconCompat.createWithResource(context, if (thread.isGroupRecipient) R.mipmap.ic_group_shortcut else R.mipmap.ic_person_shortcut)
}
return icon
}
override fun onPostExecute(icon: IconCompat?) {
val name = Optional.fromNullable<String>(thread.name)
.or(Optional.fromNullable<String>(thread.profileName))
.or(thread.toShortString())
val shortcutInfo = ShortcutInfoCompat.Builder(context, thread.address.serialize() + '-' + System.currentTimeMillis())
.setShortLabel(name)
.setIcon(icon)
.setIntent(ShortcutLauncherActivity.createIntent(context, thread.address))
.build()
if (ShortcutManagerCompat.requestPinShortcut(context, shortcutInfo, null)) {
Toast.makeText(context, context.resources.getString(R.string.ConversationActivity_added_to_home_screen), Toast.LENGTH_LONG).show()
}
}
}.execute()
}
private fun showExpiringMessagesDialog(context: Context, thread: Recipient) {
if (thread.isClosedGroupRecipient) {
val group = DatabaseFactory.getGroupDatabase(context).getGroup(thread.address.toGroupString()).orNull()
if (group?.isActive == false) { return }
}
ExpirationDialog.show(context, thread.expireMessages) { expirationTime: Int ->
DatabaseFactory.getRecipientDatabase(context).setExpireMessages(thread, expirationTime)
val message = ExpirationTimerUpdate(expirationTime)
message.recipient = thread.address.serialize()
message.sentTimestamp = System.currentTimeMillis()
val expiringMessageManager = ApplicationContext.getInstance(context).expiringMessageManager
expiringMessageManager.setExpirationTimer(message)
MessageSender.send(message, thread.address)
val activity = context as AppCompatActivity
activity.invalidateOptionsMenu()
}
}
private fun unblock(context: Context, thread: Recipient) {
if (!thread.isContactRecipient) { return }
val title = R.string.ConversationActivity_unblock_this_contact_question
val message = R.string.ConversationActivity_you_will_once_again_be_able_to_receive_messages_and_calls_from_this_contact
AlertDialog.Builder(context)
.setTitle(title)
.setMessage(message)
.setNegativeButton(android.R.string.cancel, null)
.setPositiveButton(R.string.ConversationActivity_unblock) { _, _ ->
DatabaseFactory.getRecipientDatabase(context)
.setBlocked(thread, false)
}.show()
}
private fun block(context: Context, thread: Recipient) {
if (!thread.isContactRecipient) { return }
val title = R.string.RecipientPreferenceActivity_block_this_contact_question
val message = R.string.RecipientPreferenceActivity_you_will_no_longer_receive_messages_and_calls_from_this_contact
AlertDialog.Builder(context)
.setTitle(title)
.setMessage(message)
.setNegativeButton(android.R.string.cancel, null)
.setPositiveButton(R.string.RecipientPreferenceActivity_block) { _, _ ->
DatabaseFactory.getRecipientDatabase(context)
.setBlocked(thread, true)
}.show()
}
private fun copySessionID(context: Context, thread: Recipient) {
if (!thread.isContactRecipient) { return }
val sessionID = thread.address.toString()
val clip = ClipData.newPlainText("Session ID", sessionID)
val activity = context as AppCompatActivity
val manager = activity.getSystemService(PassphraseRequiredActionBarActivity.CLIPBOARD_SERVICE) as ClipboardManager
manager.setPrimaryClip(clip)
Toast.makeText(context, R.string.copied_to_clipboard, Toast.LENGTH_SHORT).show()
}
private fun editClosedGroup(context: Context, thread: Recipient) {
if (!thread.isClosedGroupRecipient) { return }
val intent = Intent(context, EditClosedGroupActivity::class.java)
val groupID: String = thread.address.toGroupString()
intent.putExtra(groupIDKey, groupID)
context.startActivity(intent)
}
private fun leaveClosedGroup(context: Context, thread: Recipient) {
if (!thread.isClosedGroupRecipient) { return }
val builder = AlertDialog.Builder(context)
builder.setTitle(context.resources.getString(R.string.ConversationActivity_leave_group))
builder.setCancelable(true)
val group = DatabaseFactory.getGroupDatabase(context).getGroup(thread.address.toGroupString()).orNull()
val admins = group.admins
val sessionID = TextSecurePreferences.getLocalNumber(context)
val isCurrentUserAdmin = admins.any { it.toString() == sessionID }
val message = if (isCurrentUserAdmin) {
"Because you are the creator of this group it will be deleted for everyone. This cannot be undone."
} else {
context.resources.getString(R.string.ConversationActivity_are_you_sure_you_want_to_leave_this_group)
}
builder.setMessage(message)
builder.setPositiveButton(R.string.yes) { _, _ ->
var groupPublicKey: String?
var isClosedGroup: Boolean
try {
groupPublicKey = doubleDecodeGroupID(thread.address.toString()).toHexString()
isClosedGroup = DatabaseFactory.getLokiAPIDatabase(context).isClosedGroup(groupPublicKey)
} catch (e: IOException) {
groupPublicKey = null
isClosedGroup = false
}
try {
if (isClosedGroup) {
MessageSender.leave(groupPublicKey!!, true)
// TODO: Disable input?
} else {
Toast.makeText(context, R.string.ConversationActivity_error_leaving_group, Toast.LENGTH_LONG).show()
}
} catch (e: Exception) {
Toast.makeText(context, R.string.ConversationActivity_error_leaving_group, Toast.LENGTH_LONG).show()
}
}
builder.setNegativeButton(R.string.no, null)
builder.show()
}
private fun inviteContacts(context: Context, thread: Recipient) {
if (!thread.isOpenGroupRecipient) { return }
val intent = Intent(context, SelectContactsActivity::class.java)
val activity = context as AppCompatActivity
activity.startActivityForResult(intent, ConversationActivityV2.INVITE_CONTACTS)
}
private fun unmute(context: Context, thread: Recipient) {
thread.setMuted(0)
DatabaseFactory.getRecipientDatabase(context).setMuted(thread, 0)
}
private fun mute(context: Context, thread: Recipient) {
MuteDialog.show(context) { until: Long ->
thread.setMuted(until)
DatabaseFactory.getRecipientDatabase(context).setMuted(thread, until)
}
}
} }

View File

@ -37,7 +37,7 @@ class ControlMessageView : LinearLayout {
} }
fun recycle() { fun recycle() {
// TODO: Implement
} }
// endregion // endregion
} }

View File

@ -30,9 +30,5 @@ class DocumentView : LinearLayout {
documentTitleTextView.setTextColor(textColor) documentTitleTextView.setTextColor(textColor)
documentViewIconImageView.imageTintList = ColorStateList.valueOf(textColor) documentViewIconImageView.imageTintList = ColorStateList.valueOf(textColor)
} }
fun recycle() {
// TODO: Implement
}
// endregion // endregion
} }

View File

@ -1,17 +1,23 @@
package org.thoughtcrime.securesms.conversation.v2.messages package org.thoughtcrime.securesms.conversation.v2.messages
import android.content.Context import android.content.Context
import android.content.Intent
import android.graphics.Canvas import android.graphics.Canvas
import android.graphics.drawable.Drawable import android.graphics.drawable.Drawable
import android.net.Uri
import android.util.AttributeSet import android.util.AttributeSet
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.ViewOutlineProvider import android.view.ViewOutlineProvider
import android.widget.LinearLayout import android.widget.LinearLayout
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.res.ResourcesCompat import androidx.core.content.res.ResourcesCompat
import kotlinx.android.synthetic.main.view_link_preview.view.* import kotlinx.android.synthetic.main.view_link_preview.view.*
import network.loki.messenger.R import network.loki.messenger.R
import org.thoughtcrime.securesms.components.CornerMask import org.thoughtcrime.securesms.components.CornerMask
import org.thoughtcrime.securesms.conversation.v2.dialogs.OpenURLDialog
import org.thoughtcrime.securesms.conversation.v2.utilities.MessageBubbleUtilities import org.thoughtcrime.securesms.conversation.v2.utilities.MessageBubbleUtilities
import org.thoughtcrime.securesms.database.model.MessageRecord
import org.thoughtcrime.securesms.database.model.MmsMessageRecord import org.thoughtcrime.securesms.database.model.MmsMessageRecord
import org.thoughtcrime.securesms.loki.utilities.UiModeUtilities import org.thoughtcrime.securesms.loki.utilities.UiModeUtilities
import org.thoughtcrime.securesms.mms.GlideRequests import org.thoughtcrime.securesms.mms.GlideRequests
@ -19,6 +25,7 @@ import org.thoughtcrime.securesms.mms.ImageSlide
class LinkPreviewView : LinearLayout { class LinkPreviewView : LinearLayout {
private val cornerMask by lazy { CornerMask(this) } private val cornerMask by lazy { CornerMask(this) }
private var url: String? = null
// region Lifecycle // region Lifecycle
constructor(context: Context) : super(context) { initialize() } constructor(context: Context) : super(context) { initialize() }
@ -32,11 +39,9 @@ class LinkPreviewView : LinearLayout {
// region Updating // region Updating
fun bind(message: MmsMessageRecord, glide: GlideRequests, isStartOfMessageCluster: Boolean, isEndOfMessageCluster: Boolean, searchQuery: String?) { fun bind(message: MmsMessageRecord, glide: GlideRequests, isStartOfMessageCluster: Boolean, isEndOfMessageCluster: Boolean, searchQuery: String?) {
mainLinkPreviewContainer.background = background
mainLinkPreviewContainer.outlineProvider = ViewOutlineProvider.BACKGROUND
mainLinkPreviewContainer.clipToOutline = true
// Thumbnail
val linkPreview = message.linkPreviews.first() val linkPreview = message.linkPreviews.first()
url = linkPreview.url
// Thumbnail
if (linkPreview.getThumbnail().isPresent) { if (linkPreview.getThumbnail().isPresent) {
// This internally fetches the thumbnail // This internally fetches the thumbnail
thumbnailImageView.setImageResource(glide, ImageSlide(context, linkPreview.getThumbnail().get()), false, false) thumbnailImageView.setImageResource(glide, ImageSlide(context, linkPreview.getThumbnail().get()), false, false)
@ -64,9 +69,13 @@ class LinkPreviewView : LinearLayout {
super.dispatchDraw(canvas) super.dispatchDraw(canvas)
cornerMask.mask(canvas) cornerMask.mask(canvas)
} }
// endregion
fun recycle() { // region Interaction
// TODO: Implement fun openURL() {
val url = this.url ?: return
val activity = context as AppCompatActivity
OpenURLDialog(url).show(activity.supportFragmentManager, "Open URL Dialog")
} }
// endregion // endregion
} }

View File

@ -5,13 +5,17 @@ import android.util.AttributeSet
import android.view.LayoutInflater import android.view.LayoutInflater
import android.widget.LinearLayout import android.widget.LinearLayout
import androidx.annotation.ColorInt import androidx.annotation.ColorInt
import androidx.appcompat.app.AppCompatActivity
import kotlinx.android.synthetic.main.view_open_group_invitation.view.* import kotlinx.android.synthetic.main.view_open_group_invitation.view.*
import network.loki.messenger.R import network.loki.messenger.R
import org.session.libsession.messaging.open_groups.OpenGroupV2
import org.session.libsession.messaging.utilities.UpdateMessageData import org.session.libsession.messaging.utilities.UpdateMessageData
import org.session.libsession.utilities.OpenGroupUrlParser import org.session.libsession.utilities.OpenGroupUrlParser
import org.thoughtcrime.securesms.conversation.v2.dialogs.JoinOpenGroupDialog
import org.thoughtcrime.securesms.database.model.MessageRecord import org.thoughtcrime.securesms.database.model.MessageRecord
class OpenGroupInvitationView : LinearLayout { class OpenGroupInvitationView : LinearLayout {
private var data: UpdateMessageData.Kind.OpenGroupInvitation? = null
constructor(context: Context): super(context) { initialize() } constructor(context: Context): super(context) { initialize() }
constructor(context: Context, attrs: AttributeSet?): super(context, attrs) { initialize() } constructor(context: Context, attrs: AttributeSet?): super(context, attrs) { initialize() }
@ -25,6 +29,7 @@ class OpenGroupInvitationView : LinearLayout {
// FIXME: This is a really weird approach... // FIXME: This is a really weird approach...
val umd = UpdateMessageData.fromJSON(message.body)!! val umd = UpdateMessageData.fromJSON(message.body)!!
val data = umd.kind as UpdateMessageData.Kind.OpenGroupInvitation val data = umd.kind as UpdateMessageData.Kind.OpenGroupInvitation
this.data = data
val iconID = if (message.isOutgoing) R.drawable.ic_globe else R.drawable.ic_plus val iconID = if (message.isOutgoing) R.drawable.ic_globe else R.drawable.ic_plus
openGroupInvitationIconImageView.setImageResource(iconID) openGroupInvitationIconImageView.setImageResource(iconID)
openGroupTitleTextView.text = data.groupName openGroupTitleTextView.text = data.groupName
@ -33,4 +38,10 @@ class OpenGroupInvitationView : LinearLayout {
openGroupJoinMessageTextView.setTextColor(textColor) openGroupJoinMessageTextView.setTextColor(textColor)
openGroupURLTextView.setTextColor(textColor) openGroupURLTextView.setTextColor(textColor)
} }
fun joinOpenGroup() {
val data = data ?: return
val activity = context as AppCompatActivity
JoinOpenGroupDialog(data.groupName, data.groupUrl).show(activity.supportFragmentManager, "Join Open Group Dialog")
}
} }

View File

@ -1,5 +1,6 @@
package org.thoughtcrime.securesms.conversation.v2.messages package org.thoughtcrime.securesms.conversation.v2.messages
import android.content.ContentResolver
import android.content.Context import android.content.Context
import android.content.res.ColorStateList import android.content.res.ColorStateList
import android.content.res.Resources import android.content.res.Resources
@ -13,6 +14,8 @@ import androidx.core.content.res.ResourcesCompat
import androidx.core.text.toSpannable import androidx.core.text.toSpannable
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.core.view.marginStart import androidx.core.view.marginStart
import com.google.android.exoplayer2.util.MimeTypes
import kotlinx.android.synthetic.main.view_link_preview.view.*
import kotlinx.android.synthetic.main.view_quote.view.* import kotlinx.android.synthetic.main.view_quote.view.*
import network.loki.messenger.R import network.loki.messenger.R
import org.session.libsession.messaging.contacts.Contact import org.session.libsession.messaging.contacts.Contact
@ -22,7 +25,10 @@ import org.thoughtcrime.securesms.conversation.v2.utilities.TextUtilities
import org.thoughtcrime.securesms.database.DatabaseFactory import org.thoughtcrime.securesms.database.DatabaseFactory
import org.thoughtcrime.securesms.database.model.MessageRecord import org.thoughtcrime.securesms.database.model.MessageRecord
import org.thoughtcrime.securesms.loki.utilities.* import org.thoughtcrime.securesms.loki.utilities.*
import org.thoughtcrime.securesms.mms.GlideRequests
import org.thoughtcrime.securesms.mms.ImageSlide
import org.thoughtcrime.securesms.mms.SlideDeck import org.thoughtcrime.securesms.mms.SlideDeck
import org.thoughtcrime.securesms.util.MediaUtil
import kotlin.math.max import kotlin.math.max
import kotlin.math.min import kotlin.math.min
import kotlin.math.roundToInt import kotlin.math.roundToInt
@ -104,7 +110,7 @@ class QuoteView : LinearLayout {
// region Updating // region Updating
fun bind(authorPublicKey: String, body: String?, attachments: SlideDeck?, thread: Recipient, fun bind(authorPublicKey: String, body: String?, attachments: SlideDeck?, thread: Recipient,
isOutgoingMessage: Boolean, maxContentWidth: Int, isOpenGroupInvitation: Boolean, threadID: Long) { isOutgoingMessage: Boolean, maxContentWidth: Int, isOpenGroupInvitation: Boolean, threadID: Long, glide: GlideRequests) {
val contactDB = DatabaseFactory.getSessionContactDatabase(context) val contactDB = DatabaseFactory.getSessionContactDatabase(context)
// Reduce the max body text view line count to 2 if this is a group thread because // Reduce the max body text view line count to 2 if this is a group thread because
// we'll be showing the author text view and we don't want the overall quote view height // we'll be showing the author text view and we don't want the overall quote view height
@ -136,15 +142,24 @@ class QuoteView : LinearLayout {
val backgroundColorID = if (UiModeUtilities.isDayUiMode(context)) R.color.black else R.color.accent val backgroundColorID = if (UiModeUtilities.isDayUiMode(context)) R.color.black else R.color.accent
val backgroundColor = ResourcesCompat.getColor(resources, backgroundColorID, context.theme) val backgroundColor = ResourcesCompat.getColor(resources, backgroundColorID, context.theme)
quoteViewAttachmentPreviewContainer.backgroundTintList = ColorStateList.valueOf(backgroundColor) quoteViewAttachmentPreviewContainer.backgroundTintList = ColorStateList.valueOf(backgroundColor)
quoteViewAttachmentPreviewImageView.isVisible = false
quoteViewAttachmentThumbnailImageView.isVisible = false
if (attachments.audioSlide != null) { if (attachments.audioSlide != null) {
quoteViewAttachmentPreviewImageView.setImageResource(R.drawable.ic_microphone) quoteViewAttachmentPreviewImageView.setImageResource(R.drawable.ic_microphone)
quoteViewAttachmentPreviewImageView.isVisible = true
quoteViewBodyTextView.text = resources.getString(R.string.Slide_audio) quoteViewBodyTextView.text = resources.getString(R.string.Slide_audio)
} else if (attachments.documentSlide != null) { } else if (attachments.documentSlide != null) {
quoteViewAttachmentPreviewImageView.setImageResource(R.drawable.ic_document_large_light) quoteViewAttachmentPreviewImageView.setImageResource(R.drawable.ic_document_large_light)
quoteViewAttachmentPreviewImageView.isVisible = true
quoteViewBodyTextView.text = resources.getString(R.string.document) quoteViewBodyTextView.text = resources.getString(R.string.document)
} else if (attachments.thumbnailSlide != null) {
val slide = attachments.thumbnailSlide!!
// This internally fetches the thumbnail
quoteViewAttachmentThumbnailImageView.radius = toPx(4, resources)
quoteViewAttachmentThumbnailImageView.setImageResource(glide, slide, false, false)
quoteViewAttachmentThumbnailImageView.isVisible = true
quoteViewBodyTextView.text = if (MediaUtil.isVideo(slide.asAttachment())) resources.getString(R.string.Slide_video) else resources.getString(R.string.Slide_image)
} }
// TODO: Link previews
// TODO: Images/video
} }
mainQuoteViewContainer.layoutParams = LayoutParams(LayoutParams.WRAP_CONTENT, getIntrinsicHeight(maxContentWidth)) mainQuoteViewContainer.layoutParams = LayoutParams(LayoutParams.WRAP_CONTENT, getIntrinsicHeight(maxContentWidth))
val quoteViewMainContentContainerLayoutParams = quoteViewMainContentContainer.layoutParams as RelativeLayout.LayoutParams val quoteViewMainContentContainerLayoutParams = quoteViewMainContentContainer.layoutParams as RelativeLayout.LayoutParams

View File

@ -2,6 +2,7 @@ package org.thoughtcrime.securesms.conversation.v2.messages
import android.content.Context import android.content.Context
import android.graphics.Color import android.graphics.Color
import android.graphics.Rect
import android.graphics.drawable.Drawable import android.graphics.drawable.Drawable
import android.text.style.BackgroundColorSpan import android.text.style.BackgroundColorSpan
import android.text.style.ForegroundColorSpan import android.text.style.ForegroundColorSpan
@ -11,6 +12,7 @@ import android.util.TypedValue
import android.view.LayoutInflater import android.view.LayoutInflater
import android.widget.LinearLayout import android.widget.LinearLayout
import android.widget.TextView import android.widget.TextView
import android.widget.Toast
import androidx.annotation.ColorInt import androidx.annotation.ColorInt
import androidx.annotation.DrawableRes import androidx.annotation.DrawableRes
import androidx.core.content.res.ResourcesCompat import androidx.core.content.res.ResourcesCompat
@ -22,6 +24,7 @@ import network.loki.messenger.R
import org.session.libsession.utilities.ThemeUtil import org.session.libsession.utilities.ThemeUtil
import org.session.libsession.utilities.ViewUtil import org.session.libsession.utilities.ViewUtil
import org.session.libsession.utilities.recipients.Recipient import org.session.libsession.utilities.recipients.Recipient
import org.thoughtcrime.securesms.conversation.v2.components.AlbumThumbnailView
import org.thoughtcrime.securesms.components.emoji.EmojiTextView import org.thoughtcrime.securesms.components.emoji.EmojiTextView
import org.thoughtcrime.securesms.database.model.MessageRecord import org.thoughtcrime.securesms.database.model.MessageRecord
import org.thoughtcrime.securesms.database.model.MmsMessageRecord import org.thoughtcrime.securesms.database.model.MmsMessageRecord
@ -33,7 +36,8 @@ import java.util.*
import kotlin.math.roundToInt import kotlin.math.roundToInt
class VisibleMessageContentView : LinearLayout { class VisibleMessageContentView : LinearLayout {
var onContentClick: (() -> Unit)? = null var onContentClick: ((rawRect: Rect) -> Unit)? = null
var onContentDoubleTap: (() -> Unit)? = null
// region Lifecycle // region Lifecycle
constructor(context: Context) : super(context) { initialize() } constructor(context: Context) : super(context) { initialize() }
@ -58,10 +62,12 @@ class VisibleMessageContentView : LinearLayout {
// Body // Body
mainContainer.removeAllViews() mainContainer.removeAllViews()
onContentClick = null onContentClick = null
onContentDoubleTap = null
if (message is MmsMessageRecord && message.linkPreviews.isNotEmpty()) { if (message is MmsMessageRecord && message.linkPreviews.isNotEmpty()) {
val linkPreviewView = LinkPreviewView(context) val linkPreviewView = LinkPreviewView(context)
linkPreviewView.bind(message, glide, isStartOfMessageCluster, isEndOfMessageCluster, searchQuery) linkPreviewView.bind(message, glide, isStartOfMessageCluster, isEndOfMessageCluster, searchQuery)
mainContainer.addView(linkPreviewView) mainContainer.addView(linkPreviewView)
onContentClick = { linkPreviewView.openURL() }
// Body text view is inside the link preview for layout convenience // Body text view is inside the link preview for layout convenience
} else if (message is MmsMessageRecord && message.quote != null) { } else if (message is MmsMessageRecord && message.quote != null) {
val quote = message.quote!! val quote = message.quote!!
@ -71,7 +77,7 @@ class VisibleMessageContentView : LinearLayout {
// here to get the layout right. // here to get the layout right.
val maxContentWidth = (maxWidth - 2 * resources.getDimension(R.dimen.medium_spacing) - toPx(16, resources)).roundToInt() val maxContentWidth = (maxWidth - 2 * resources.getDimension(R.dimen.medium_spacing) - toPx(16, resources)).roundToInt()
quoteView.bind(quote.author.toString(), quote.text, quote.attachment, thread, quoteView.bind(quote.author.toString(), quote.text, quote.attachment, thread,
message.isOutgoing, maxContentWidth, message.isOpenGroupInvitation, message.threadId) message.isOutgoing, maxContentWidth, message.isOpenGroupInvitation, message.threadId, glide)
mainContainer.addView(quoteView) mainContainer.addView(quoteView)
val bodyTextView = VisibleMessageContentView.getBodyTextView(context, message, searchQuery) val bodyTextView = VisibleMessageContentView.getBodyTextView(context, message, searchQuery)
ViewUtil.setPaddingTop(bodyTextView, 0) ViewUtil.setPaddingTop(bodyTextView, 0)
@ -83,21 +89,32 @@ class VisibleMessageContentView : LinearLayout {
// We have to use onContentClick (rather than a click listener directly on the voice // We have to use onContentClick (rather than a click listener directly on the voice
// message view) so as to not interfere with all the other gestures. // message view) so as to not interfere with all the other gestures.
onContentClick = { voiceMessageView.togglePlayback() } onContentClick = { voiceMessageView.togglePlayback() }
onContentDoubleTap = { voiceMessageView.handleDoubleTap() }
} else if (message is MmsMessageRecord && message.slideDeck.documentSlide != null) { } else if (message is MmsMessageRecord && message.slideDeck.documentSlide != null) {
val documentView = DocumentView(context) val documentView = DocumentView(context)
documentView.bind(message, VisibleMessageContentView.getTextColor(context, message)) documentView.bind(message, VisibleMessageContentView.getTextColor(context, message))
mainContainer.addView(documentView) mainContainer.addView(documentView)
} else if (message is MmsMessageRecord && message.slideDeck.asAttachments().isNotEmpty()) { } else if (message is MmsMessageRecord && message.slideDeck.asAttachments().isNotEmpty()) {
val dummyTextView = TextView(context) val albumThumbnailView = AlbumThumbnailView(context)
dummyTextView.text = "asifuygaihsfo" mainContainer.addView(albumThumbnailView)
mainContainer.addView(dummyTextView) // isStart and isEnd of cluster needed for calculating the mask for full bubble image groups
// bind after add view because views are inflated and calculated during bind
albumThumbnailView.bind(
glideRequests = glide,
message = message,
isStart = isStartOfMessageCluster,
isEnd = isEndOfMessageCluster
)
onContentClick = { albumThumbnailView.calculateHitObject(it, message) }
} else if (message.isOpenGroupInvitation) { } else if (message.isOpenGroupInvitation) {
val openGroupInvitationView = OpenGroupInvitationView(context) val openGroupInvitationView = OpenGroupInvitationView(context)
openGroupInvitationView.bind(message, VisibleMessageContentView.getTextColor(context, message)) openGroupInvitationView.bind(message, VisibleMessageContentView.getTextColor(context, message))
mainContainer.addView(openGroupInvitationView) mainContainer.addView(openGroupInvitationView)
onContentClick = { openGroupInvitationView.joinOpenGroup() }
} else { } else {
val bodyTextView = VisibleMessageContentView.getBodyTextView(context, message, searchQuery) val bodyTextView = VisibleMessageContentView.getBodyTextView(context, message, searchQuery)
mainContainer.addView(bodyTextView) mainContainer.addView(bodyTextView)
onContentClick = { openURLIfNeeded(message) }
} }
} }
@ -121,6 +138,12 @@ class VisibleMessageContentView : LinearLayout {
} }
// endregion // endregion
// region Interaction
private fun openURLIfNeeded(message: MessageRecord) {
Toast.makeText(context, "Not yet implemented", Toast.LENGTH_LONG).show()
}
// endregion
// region Convenience // region Convenience
companion object { companion object {

View File

@ -4,21 +4,15 @@ import android.content.Context
import android.content.res.Resources import android.content.res.Resources
import android.graphics.Canvas import android.graphics.Canvas
import android.graphics.Rect import android.graphics.Rect
import android.graphics.Region
import android.graphics.drawable.ColorDrawable import android.graphics.drawable.ColorDrawable
import android.os.Build import android.os.Build
import android.os.Handler import android.os.Handler
import android.os.Looper import android.os.Looper
import android.util.AttributeSet import android.util.AttributeSet
import android.util.Log
import android.view.* import android.view.*
import android.widget.LinearLayout import android.widget.LinearLayout
import androidx.annotation.ColorRes
import androidx.annotation.DrawableRes
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.core.graphics.withClip
import androidx.core.view.isVisible import androidx.core.view.isVisible
import kotlinx.android.synthetic.main.view_conversation.view.*
import kotlinx.android.synthetic.main.view_visible_message.view.* import kotlinx.android.synthetic.main.view_visible_message.view.*
import kotlinx.android.synthetic.main.view_visible_message.view.profilePictureView import kotlinx.android.synthetic.main.view_visible_message.view.profilePictureView
import network.loki.messenger.R import network.loki.messenger.R
@ -27,7 +21,6 @@ import org.session.libsession.messaging.open_groups.OpenGroupAPIV2
import org.session.libsession.utilities.ViewUtil import org.session.libsession.utilities.ViewUtil
import org.thoughtcrime.securesms.database.DatabaseFactory import org.thoughtcrime.securesms.database.DatabaseFactory
import org.thoughtcrime.securesms.database.model.MessageRecord import org.thoughtcrime.securesms.database.model.MessageRecord
import org.thoughtcrime.securesms.loki.utilities.disableClipping
import org.thoughtcrime.securesms.loki.utilities.getColorWithID import org.thoughtcrime.securesms.loki.utilities.getColorWithID
import org.thoughtcrime.securesms.loki.utilities.toDp import org.thoughtcrime.securesms.loki.utilities.toDp
import org.thoughtcrime.securesms.loki.utilities.toPx import org.thoughtcrime.securesms.loki.utilities.toPx
@ -46,11 +39,13 @@ class VisibleMessageView : LinearLayout {
private var dx = 0.0f private var dx = 0.0f
private var previousTranslationX = 0.0f private var previousTranslationX = 0.0f
private val gestureHandler = Handler(Looper.getMainLooper()) private val gestureHandler = Handler(Looper.getMainLooper())
private var pressCallback: Runnable? = null
private var longPressCallback: Runnable? = null private var longPressCallback: Runnable? = null
private var onDownTimestamp = 0L private var onDownTimestamp = 0L
private var onDoubleTap: (() -> Unit)? = null
var snIsSelected = false var snIsSelected = false
set(value) { field = value; handleIsSelectedChanged()} set(value) { field = value; handleIsSelectedChanged()}
var onPress: (() -> Unit)? = null var onPress: ((rawX: Int, rawY: Int) -> Unit)? = null
var onSwipeToReply: (() -> Unit)? = null var onSwipeToReply: (() -> Unit)? = null
var onLongPress: (() -> Unit)? = null var onLongPress: (() -> Unit)? = null
@ -58,6 +53,7 @@ class VisibleMessageView : LinearLayout {
const val swipeToReplyThreshold = 80.0f // dp const val swipeToReplyThreshold = 80.0f // dp
const val longPressMovementTreshold = 10.0f // dp const val longPressMovementTreshold = 10.0f // dp
const val longPressDurationThreshold = 250L // ms const val longPressDurationThreshold = 250L // ms
const val maxDoubleTapInterval = 200L
} }
// region Lifecycle // region Lifecycle
@ -143,6 +139,7 @@ class VisibleMessageView : LinearLayout {
if (profilePictureContainer.visibility != View.GONE) { maxWidth -= profilePictureContainer.width } if (profilePictureContainer.visibility != View.GONE) { maxWidth -= profilePictureContainer.width }
// Populate content view // Populate content view
messageContentView.bind(message, isStartOfMessageCluster, isEndOfMessageCluster, glide, maxWidth, thread, searchQuery) messageContentView.bind(message, isStartOfMessageCluster, isEndOfMessageCluster, glide, maxWidth, thread, searchQuery)
onDoubleTap = { messageContentView.onContentDoubleTap?.invoke() }
} }
private fun setMessageSpacing(isStartOfMessageCluster: Boolean, isEndOfMessageCluster: Boolean) { private fun setMessageSpacing(isStartOfMessageCluster: Boolean, isEndOfMessageCluster: Boolean) {
@ -195,7 +192,7 @@ class VisibleMessageView : LinearLayout {
val spacing = context.resources.getDimensionPixelSize(R.dimen.small_spacing) val spacing = context.resources.getDimensionPixelSize(R.dimen.small_spacing)
val threshold = VisibleMessageView.swipeToReplyThreshold val threshold = VisibleMessageView.swipeToReplyThreshold
val iconSize = toPx(24, context.resources) val iconSize = toPx(24, context.resources)
val bottomVOffset = paddingBottom + (messageContentView.height - iconSize) / 2 val bottomVOffset = paddingBottom + messageStatusImageView.height + (messageContentView.height - iconSize) / 2
swipeToReplyIconRect.left = messageContentContainer.right + spacing swipeToReplyIconRect.left = messageContentContainer.right + spacing
swipeToReplyIconRect.top = height - bottomVOffset - iconSize swipeToReplyIconRect.top = height - bottomVOffset - iconSize
swipeToReplyIconRect.right = messageContentContainer.right + iconSize + spacing swipeToReplyIconRect.right = messageContentContainer.right + iconSize + spacing
@ -272,7 +269,18 @@ class VisibleMessageView : LinearLayout {
onSwipeToReply?.invoke() onSwipeToReply?.invoke()
} else if ((Date().time - onDownTimestamp) < VisibleMessageView.longPressDurationThreshold) { } else if ((Date().time - onDownTimestamp) < VisibleMessageView.longPressDurationThreshold) {
longPressCallback?.let { gestureHandler.removeCallbacks(it) } longPressCallback?.let { gestureHandler.removeCallbacks(it) }
onPress?.invoke() val pressCallback = this.pressCallback
if (pressCallback != null) {
// If we're here and pressCallback isn't null, it means that we tapped again within
// maxDoubleTapInterval ms and we should count this as a double tap
gestureHandler.removeCallbacks(pressCallback)
this.pressCallback = null
onDoubleTap?.invoke()
} else {
val newPressCallback = Runnable { onPress(event.rawX.toInt(), event.rawY.toInt()) }
this.pressCallback = newPressCallback
gestureHandler.postDelayed(newPressCallback, VisibleMessageView.maxDoubleTapInterval)
}
} }
resetPosition() resetPosition()
} }
@ -297,8 +305,13 @@ class VisibleMessageView : LinearLayout {
onLongPress?.invoke() onLongPress?.invoke()
} }
fun onContentClick() { fun onContentClick(rawRect: Rect) {
messageContentView.onContentClick?.invoke() messageContentView.onContentClick?.invoke(rawRect)
}
private fun onPress(rawX: Int, rawY: Int) {
onPress?.invoke(rawX, rawY)
pressCallback = null
} }
// endregion // endregion
} }

View File

@ -2,31 +2,29 @@ package org.thoughtcrime.securesms.conversation.v2.messages
import android.content.Context import android.content.Context
import android.graphics.Canvas import android.graphics.Canvas
import android.graphics.drawable.Drawable
import android.os.Handler
import android.os.Looper
import android.util.AttributeSet import android.util.AttributeSet
import android.view.LayoutInflater import android.util.Log
import android.view.ViewOutlineProvider import android.view.*
import android.widget.LinearLayout import android.widget.LinearLayout
import android.widget.RelativeLayout import android.widget.RelativeLayout
import androidx.core.view.isVisible import androidx.core.view.isVisible
import kotlinx.android.synthetic.main.view_voice_message.view.* import kotlinx.android.synthetic.main.view_voice_message.view.*
import network.loki.messenger.R import network.loki.messenger.R
import org.thoughtcrime.securesms.audio.AudioSlidePlayer
import org.thoughtcrime.securesms.components.CornerMask import org.thoughtcrime.securesms.components.CornerMask
import org.thoughtcrime.securesms.conversation.v2.utilities.MessageBubbleUtilities import org.thoughtcrime.securesms.conversation.v2.utilities.MessageBubbleUtilities
import org.thoughtcrime.securesms.database.model.MmsMessageRecord import org.thoughtcrime.securesms.database.model.MmsMessageRecord
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
import kotlin.math.roundToInt import kotlin.math.roundToInt
import kotlin.math.roundToLong
class VoiceMessageView : LinearLayout { class VoiceMessageView : LinearLayout, AudioSlidePlayer.Listener {
private val snHandler = Handler(Looper.getMainLooper())
private val cornerMask by lazy { CornerMask(this) } private val cornerMask by lazy { CornerMask(this) }
private var runnable: Runnable? = null private var isPlaying = false
private var mockIsPlaying = false private var progress = 0.0
private var mockProgress = 0L private var duration = 0L
set(value) { field = value; handleProgressChanged() } private var player: AudioSlidePlayer? = null
private var mockDuration = 12000L private var isPreparing = false
// region Lifecycle // region Lifecycle
constructor(context: Context) : super(context) { initialize() } constructor(context: Context) : super(context) { initialize() }
@ -36,14 +34,18 @@ class VoiceMessageView : LinearLayout {
private fun initialize() { private fun initialize() {
LayoutInflater.from(context).inflate(R.layout.view_voice_message, this) LayoutInflater.from(context).inflate(R.layout.view_voice_message, this)
voiceMessageViewDurationTextView.text = String.format("%01d:%02d", voiceMessageViewDurationTextView.text = String.format("%01d:%02d",
TimeUnit.MILLISECONDS.toMinutes(mockDuration), TimeUnit.MILLISECONDS.toMinutes(0),
TimeUnit.MILLISECONDS.toSeconds(mockDuration)) TimeUnit.MILLISECONDS.toSeconds(0))
} }
// endregion // endregion
// region Updating // region Updating
fun bind(message: MmsMessageRecord, isStartOfMessageCluster: Boolean, isEndOfMessageCluster: Boolean) { fun bind(message: MmsMessageRecord, isStartOfMessageCluster: Boolean, isEndOfMessageCluster: Boolean) {
val audio = message.slideDeck.audioSlide!! val audio = message.slideDeck.audioSlide!!
val player = AudioSlidePlayer.createFor(context, audio, this)
this.player = player
isPreparing = true
player.play(0.0)
voiceMessageViewLoader.isVisible = audio.isPendingDownload voiceMessageViewLoader.isVisible = audio.isPendingDownload
val cornerRadii = MessageBubbleUtilities.calculateRadii(context, isStartOfMessageCluster, isEndOfMessageCluster, message.isOutgoing) val cornerRadii = MessageBubbleUtilities.calculateRadii(context, isStartOfMessageCluster, isEndOfMessageCluster, message.isOutgoing)
cornerMask.setTopLeftRadius(cornerRadii[0]) cornerMask.setTopLeftRadius(cornerRadii[0])
@ -52,43 +54,59 @@ class VoiceMessageView : LinearLayout {
cornerMask.setBottomLeftRadius(cornerRadii[3]) cornerMask.setBottomLeftRadius(cornerRadii[3])
} }
private fun handleProgressChanged() { override fun onPlayerStart(player: AudioSlidePlayer) {
if (!isPreparing) { return }
isPreparing = false
duration = player.duration
voiceMessageViewDurationTextView.text = String.format("%01d:%02d", voiceMessageViewDurationTextView.text = String.format("%01d:%02d",
TimeUnit.MILLISECONDS.toMinutes(mockDuration - mockProgress), TimeUnit.MILLISECONDS.toMinutes(duration),
TimeUnit.MILLISECONDS.toSeconds(mockDuration - mockProgress)) TimeUnit.MILLISECONDS.toSeconds(duration))
player.stop()
}
override fun onPlayerProgress(player: AudioSlidePlayer, progress: Double, unused: Long) {
if (progress == 1.0) {
togglePlayback()
handleProgressChanged(0.0)
} else {
handleProgressChanged(progress)
}
}
private fun handleProgressChanged(progress: Double) {
this.progress = progress
voiceMessageViewDurationTextView.text = String.format("%01d:%02d",
TimeUnit.MILLISECONDS.toMinutes(duration - (progress * duration.toDouble()).roundToLong()),
TimeUnit.MILLISECONDS.toSeconds(duration - (progress * duration.toDouble()).roundToLong()))
val layoutParams = progressView.layoutParams as RelativeLayout.LayoutParams val layoutParams = progressView.layoutParams as RelativeLayout.LayoutParams
val fraction = mockProgress.toFloat() / mockDuration.toFloat() layoutParams.width = (width.toFloat() * progress.toFloat()).roundToInt()
layoutParams.width = (width.toFloat() * fraction).roundToInt()
progressView.layoutParams = layoutParams progressView.layoutParams = layoutParams
} }
override fun onPlayerStop(player: AudioSlidePlayer) { }
override fun dispatchDraw(canvas: Canvas) { override fun dispatchDraw(canvas: Canvas) {
super.dispatchDraw(canvas) super.dispatchDraw(canvas)
cornerMask.mask(canvas) cornerMask.mask(canvas)
} }
fun recycle() {
// TODO: Implement
}
// endregion // endregion
// region Interaction // region Interaction
fun togglePlayback() { fun togglePlayback() {
mockIsPlaying = !mockIsPlaying val player = this.player ?: return
val iconID = if (mockIsPlaying) R.drawable.exo_icon_pause else R.drawable.exo_icon_play isPlaying = !isPlaying
val iconID = if (isPlaying) R.drawable.exo_icon_pause else R.drawable.exo_icon_play
voiceMessagePlaybackImageView.setImageResource(iconID) voiceMessagePlaybackImageView.setImageResource(iconID)
if (mockIsPlaying) { if (isPlaying) {
updateProgress() player.play(progress)
} else { } else {
runnable?.let { snHandler.removeCallbacks(it) } player.stop()
} }
} }
private fun updateProgress() { fun handleDoubleTap() {
mockProgress += 20L val player = this.player ?: return
val runnable = Runnable { updateProgress() } player.playbackSpeed = if (player.playbackSpeed == 1.0f) 1.5f else 1.0f
this.runnable = runnable
snHandler.postDelayed(runnable, 20L)
} }
// endregion // endregion
} }

View File

@ -14,7 +14,7 @@
* You should have received a copy of the GNU General Public License * You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>. * along with this program. If not, see <http://www.gnu.org/licenses/>.
*/ */
package org.thoughtcrime.securesms.mms; package org.thoughtcrime.securesms.conversation.v2.utilities;
import android.Manifest; import android.Manifest;
import android.annotation.SuppressLint; import android.annotation.SuppressLint;
@ -23,29 +23,31 @@ import android.content.ActivityNotFoundException;
import android.content.Context; import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.database.Cursor; import android.database.Cursor;
import android.graphics.PorterDuff;
import android.net.Uri; import android.net.Uri;
import android.os.AsyncTask; import android.os.AsyncTask;
import android.provider.ContactsContract;
import android.provider.MediaStore; import android.provider.MediaStore;
import android.provider.OpenableColumns; import android.provider.OpenableColumns;
import android.text.TextUtils; import android.text.TextUtils;
import android.util.Pair; import android.util.Pair;
import android.view.View;
import android.widget.Toast; import android.widget.Toast;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import org.thoughtcrime.securesms.MediaPreviewActivity;
import org.thoughtcrime.securesms.loki.views.MessageAudioView;
import org.thoughtcrime.securesms.components.DocumentView;
import org.thoughtcrime.securesms.components.RemovableEditableMediaView;
import org.thoughtcrime.securesms.conversation.v2.utilities.ThumbnailView;
import org.session.libsignal.utilities.NoExternalStorageException; import org.session.libsignal.utilities.NoExternalStorageException;
import org.thoughtcrime.securesms.giph.ui.GiphyActivity; import org.thoughtcrime.securesms.giph.ui.GiphyActivity;
import org.session.libsignal.utilities.Log; import org.session.libsignal.utilities.Log;
import org.thoughtcrime.securesms.mediasend.MediaSendActivity; import org.thoughtcrime.securesms.mediasend.MediaSendActivity;
import org.thoughtcrime.securesms.mms.AudioSlide;
import org.thoughtcrime.securesms.mms.DocumentSlide;
import org.thoughtcrime.securesms.mms.GifSlide;
import org.thoughtcrime.securesms.mms.GlideRequests;
import org.thoughtcrime.securesms.mms.ImageSlide;
import org.thoughtcrime.securesms.mms.MediaConstraints;
import org.thoughtcrime.securesms.mms.PartAuthority;
import org.thoughtcrime.securesms.mms.Slide;
import org.thoughtcrime.securesms.mms.SlideDeck;
import org.thoughtcrime.securesms.mms.VideoSlide;
import org.thoughtcrime.securesms.permissions.Permissions; import org.thoughtcrime.securesms.permissions.Permissions;
import org.thoughtcrime.securesms.providers.BlobProvider; import org.thoughtcrime.securesms.providers.BlobProvider;
import org.session.libsignal.utilities.ExternalStorageUtil; import org.session.libsignal.utilities.ExternalStorageUtil;
@ -53,13 +55,8 @@ import org.thoughtcrime.securesms.util.FileProviderUtil;
import org.thoughtcrime.securesms.util.MediaUtil; import org.thoughtcrime.securesms.util.MediaUtil;
import org.session.libsignal.utilities.guava.Optional; import org.session.libsignal.utilities.guava.Optional;
import org.session.libsession.messaging.sending_receiving.attachments.Attachment;
import org.session.libsession.utilities.recipients.Recipient; import org.session.libsession.utilities.recipients.Recipient;
import org.session.libsession.utilities.ThemeUtil;
import org.session.libsession.utilities.ViewUtil;
import org.session.libsession.utilities.Stub;
import org.session.libsignal.utilities.ListenableFuture; import org.session.libsignal.utilities.ListenableFuture;
import org.session.libsignal.utilities.ListenableFuture.Listener;
import org.session.libsignal.utilities.SettableFuture; import org.session.libsignal.utilities.SettableFuture;
import java.io.File; import java.io.File;
@ -67,26 +64,18 @@ import java.io.IOException;
import java.util.Iterator; import java.util.Iterator;
import java.util.LinkedList; import java.util.LinkedList;
import java.util.List; import java.util.List;
import java.util.concurrent.ExecutionException;
import network.loki.messenger.R; import network.loki.messenger.R;
import static android.provider.MediaStore.EXTRA_OUTPUT; import static android.provider.MediaStore.EXTRA_OUTPUT;
public class AttachmentManager { public class AttachmentManager {
private final static String TAG = AttachmentManager.class.getSimpleName(); private final static String TAG = AttachmentManager.class.getSimpleName();
private final @NonNull Context context; private final @NonNull Context context;
private final @NonNull Stub<View> attachmentViewStub;
private final @NonNull AttachmentListener attachmentListener; private final @NonNull AttachmentListener attachmentListener;
private RemovableEditableMediaView removableMediaView;
private ThumbnailView thumbnail;
private MessageAudioView audioView;
private DocumentView documentView;
private @NonNull List<Uri> garbage = new LinkedList<>(); private @NonNull List<Uri> garbage = new LinkedList<>();
private @NonNull Optional<Slide> slide = Optional.absent(); private @NonNull Optional<Slide> slide = Optional.absent();
private @Nullable Uri captureUri; private @Nullable Uri captureUri;
@ -94,51 +83,12 @@ public class AttachmentManager {
public AttachmentManager(@NonNull Activity activity, @NonNull AttachmentListener listener) { public AttachmentManager(@NonNull Activity activity, @NonNull AttachmentListener listener) {
this.context = activity; this.context = activity;
this.attachmentListener = listener; this.attachmentListener = listener;
this.attachmentViewStub = ViewUtil.findStubById(activity, R.id.attachment_editor_stub);
} }
private void inflateStub() { public void clear() {
if (!attachmentViewStub.resolved()) { markGarbage(getSlideUri());
View root = attachmentViewStub.get(); slide = Optional.absent();
attachmentListener.onAttachmentChanged();
this.thumbnail = ViewUtil.findById(root, R.id.attachment_thumbnail);
this.audioView = ViewUtil.findById(root, R.id.attachment_audio);
this.documentView = ViewUtil.findById(root, R.id.attachment_document);
this.removableMediaView = ViewUtil.findById(root, R.id.removable_media_view);
removableMediaView.setRemoveClickListener(new RemoveButtonListener());
thumbnail.setOnClickListener(new ThumbnailClickListener());
documentView.getBackground().setColorFilter(ThemeUtil.getThemedColor(context, R.attr.conversation_item_bubble_background), PorterDuff.Mode.MULTIPLY);
}
}
public void clear(@NonNull GlideRequests glideRequests, boolean animate) {
if (attachmentViewStub.resolved()) {
if (animate) {
ViewUtil.fadeOut(attachmentViewStub.get(), 200).addListener(new Listener<Boolean>() {
@Override
public void onSuccess(Boolean result) {
thumbnail.clear(glideRequests);
attachmentViewStub.get().setVisibility(View.GONE);
attachmentListener.onAttachmentChanged();
}
@Override
public void onFailure(ExecutionException e) {
}
});
} else {
thumbnail.clear(glideRequests);
attachmentViewStub.get().setVisibility(View.GONE);
attachmentListener.onAttachmentChanged();
}
markGarbage(getSlideUri());
slide = Optional.absent();
audioView.cleanup();
}
} }
public void cleanup() { public void cleanup() {
@ -190,16 +140,12 @@ public class AttachmentManager {
final int width, final int width,
final int height) final int height)
{ {
inflateStub();
final SettableFuture<Boolean> result = new SettableFuture<>(); final SettableFuture<Boolean> result = new SettableFuture<>();
new AsyncTask<Void, Void, Slide>() { new AsyncTask<Void, Void, Slide>() {
@Override @Override
protected void onPreExecute() { protected void onPreExecute() {
thumbnail.clear(glideRequests);
thumbnail.showProgressSpinner();
attachmentViewStub.get().setVisibility(View.VISIBLE);
} }
@Override @Override
@ -222,35 +168,12 @@ public class AttachmentManager {
@Override @Override
protected void onPostExecute(@Nullable final Slide slide) { protected void onPostExecute(@Nullable final Slide slide) {
if (slide == null) { if (slide == null) {
attachmentViewStub.get().setVisibility(View.GONE);
Toast.makeText(context,
R.string.ConversationActivity_sorry_there_was_an_error_setting_your_attachment,
Toast.LENGTH_SHORT).show();
result.set(false); result.set(false);
} else if (!areConstraintsSatisfied(context, slide, constraints)) { } else if (!areConstraintsSatisfied(context, slide, constraints)) {
attachmentViewStub.get().setVisibility(View.GONE);
Toast.makeText(context,
R.string.ConversationActivity_attachment_exceeds_size_limits,
Toast.LENGTH_SHORT).show();
result.set(false); result.set(false);
} else { } else {
setSlide(slide); setSlide(slide);
attachmentViewStub.get().setVisibility(View.VISIBLE); result.set(true);
if (slide.hasAudio()) {
audioView.setAudio((AudioSlide) slide, false);
removableMediaView.display(audioView, false);
result.set(true);
} else if (slide.hasDocument()) {
documentView.setDocument((DocumentSlide) slide, false);
removableMediaView.display(documentView, false);
result.set(true);
} else {
Attachment attachment = slide.asAttachment();
result.deferTo(thumbnail.setImageResource(glideRequests, slide, false, true, attachment.getWidth(), attachment.getHeight()));
removableMediaView.display(thumbnail, mediaType == MediaType.IMAGE);
}
attachmentListener.onAttachmentChanged(); attachmentListener.onAttachmentChanged();
} }
} }
@ -317,11 +240,8 @@ public class AttachmentManager {
return result; return result;
} }
public boolean isAttachmentPresent() { public @NonNull
return attachmentViewStub.resolved() && attachmentViewStub.get().getVisibility() == View.VISIBLE; SlideDeck buildSlideDeck() {
}
public @NonNull SlideDeck buildSlideDeck() {
SlideDeck deck = new SlideDeck(); SlideDeck deck = new SlideDeck();
if (slide.isPresent()) deck.addSlide(slide.get()); if (slide.isPresent()) deck.addSlide(slide.get());
return deck; return deck;
@ -333,43 +253,16 @@ public class AttachmentManager {
public static void selectGallery(Activity activity, int requestCode, @NonNull Recipient recipient, @NonNull String body) { public static void selectGallery(Activity activity, int requestCode, @NonNull Recipient recipient, @NonNull String body) {
Permissions.with(activity) Permissions.with(activity)
.request(Manifest.permission.READ_EXTERNAL_STORAGE) .request(Manifest.permission.READ_EXTERNAL_STORAGE)
.withPermanentDenialDialog(activity.getString(R.string.AttachmentManager_signal_requires_the_external_storage_permission_in_order_to_attach_photos_videos_or_audio)) .withPermanentDenialDialog(activity.getString(R.string.AttachmentManager_signal_requires_the_external_storage_permission_in_order_to_attach_photos_videos_or_audio))
.onAllGranted(() -> activity.startActivityForResult(MediaSendActivity.buildGalleryIntent(activity, recipient, body), requestCode)) .onAllGranted(() -> activity.startActivityForResult(MediaSendActivity.buildGalleryIntent(activity, recipient, body), requestCode))
.execute(); .execute();
} }
public static void selectAudio(Activity activity, int requestCode) { public static void selectAudio(Activity activity, int requestCode) {
selectMediaType(activity, "audio/*", null, requestCode); selectMediaType(activity, "audio/*", null, requestCode);
} }
public static void selectContactInfo(Activity activity, int requestCode) {
Permissions.with(activity)
.request(Manifest.permission.WRITE_CONTACTS)
.withPermanentDenialDialog(activity.getString(R.string.AttachmentManager_signal_requires_contacts_permission_in_order_to_attach_contact_information))
.onAllGranted(() -> {
Intent intent = new Intent(Intent.ACTION_PICK, ContactsContract.Contacts.CONTENT_URI);
activity.startActivityForResult(intent, requestCode);
})
.execute();
}
public static void selectLocation(Activity activity, int requestCode) {
/* Loki - Enable again once we have location sharing
Permissions.with(activity)
.request(Manifest.permission.ACCESS_FINE_LOCATION, Manifest.permission.ACCESS_COARSE_LOCATION)
.withPermanentDenialDialog(activity.getString(R.string.AttachmentManager_signal_requires_location_information_in_order_to_attach_a_location))
.onAllGranted(() -> {
try {
activity.startActivityForResult(new PlacePicker.IntentBuilder().build(activity), requestCode);
} catch (GooglePlayServicesRepairableException | GooglePlayServicesNotAvailableException e) {
Log.w(TAG, e);
}
})
.execute();
*/
}
public static void selectGif(Activity activity, int requestCode) { public static void selectGif(Activity activity, int requestCode) {
Intent intent = new Intent(activity, GiphyActivity.class); Intent intent = new Intent(activity, GiphyActivity.class);
intent.putExtra(GiphyActivity.EXTRA_IS_MMS, false); intent.putExtra(GiphyActivity.EXTRA_IS_MMS, false);
@ -386,28 +279,25 @@ public class AttachmentManager {
public void capturePhoto(Activity activity, int requestCode) { public void capturePhoto(Activity activity, int requestCode) {
Permissions.with(activity) Permissions.with(activity)
.request(Manifest.permission.CAMERA) .request(Manifest.permission.CAMERA)
.withPermanentDenialDialog(activity.getString(R.string.AttachmentManager_signal_requires_the_camera_permission_in_order_to_take_photos_but_it_has_been_permanently_denied)) .withPermanentDenialDialog(activity.getString(R.string.AttachmentManager_signal_requires_the_camera_permission_in_order_to_take_photos_but_it_has_been_permanently_denied))
.onAllGranted(() -> { .onAllGranted(() -> {
try { try {
File captureFile = File.createTempFile( File captureFile = File.createTempFile("conversation-capture", ".jpg", ExternalStorageUtil.getImageDir(activity));
"conversation-capture", Uri captureUri = FileProviderUtil.getUriFor(context, captureFile);
".jpg", Intent captureIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
ExternalStorageUtil.getImageDir(activity)); captureIntent.putExtra(EXTRA_OUTPUT, captureUri);
Uri captureUri = FileProviderUtil.getUriFor(context, captureFile); captureIntent.setFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
Intent captureIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE); if (captureIntent.resolveActivity(activity.getPackageManager()) != null) {
captureIntent.putExtra(EXTRA_OUTPUT, captureUri); Log.d(TAG, "captureUri path is " + captureUri.getPath());
captureIntent.setFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION); this.captureUri = captureUri;
if (captureIntent.resolveActivity(activity.getPackageManager()) != null) { activity.startActivityForResult(captureIntent, requestCode);
Log.d(TAG, "captureUri path is " + captureUri.getPath()); }
this.captureUri = captureUri; } catch (IOException | NoExternalStorageException e) {
activity.startActivityForResult(captureIntent, requestCode); throw new RuntimeException("Error creating image capture intent.", e);
} }
} catch (IOException | NoExternalStorageException e) { })
throw new RuntimeException("Error creating image capture intent.", e); .execute();
}
})
.execute();
} }
private static void selectMediaType(Activity activity, @NonNull String type, @Nullable String[] extraMimeType, int requestCode) { private static void selectMediaType(Activity activity, @NonNull String type, @Nullable String[] extraMimeType, int requestCode) {
@ -445,34 +335,6 @@ public class AttachmentManager {
constraints.canResize(slide.asAttachment()); constraints.canResize(slide.asAttachment());
} }
private void previewImageDraft(final @NonNull Slide slide) {
if (MediaPreviewActivity.isContentTypeSupported(slide.getContentType()) && slide.getUri() != null) {
Intent intent = new Intent(context, MediaPreviewActivity.class);
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
intent.putExtra(MediaPreviewActivity.SIZE_EXTRA, slide.asAttachment().getSize());
intent.putExtra(MediaPreviewActivity.CAPTION_EXTRA, slide.getCaption().orNull());
intent.putExtra(MediaPreviewActivity.OUTGOING_EXTRA, true);
intent.setDataAndType(slide.getUri(), slide.getContentType());
context.startActivity(intent);
}
}
private class ThumbnailClickListener implements View.OnClickListener {
@Override
public void onClick(View v) {
if (slide.isPresent()) previewImageDraft(slide.get());
}
}
private class RemoveButtonListener implements View.OnClickListener {
@Override
public void onClick(View v) {
cleanup();
clear(GlideApp.with(context.getApplicationContext()), true);
}
}
public interface AttachmentListener { public interface AttachmentListener {
void onAttachmentChanged(); void onAttachmentChanged();
} }
@ -513,6 +375,5 @@ public class AttachmentManager {
return DOCUMENT; return DOCUMENT;
} }
} }
} }

View File

@ -0,0 +1,197 @@
package org.thoughtcrime.securesms.conversation.v2.utilities
import android.content.Context
import android.graphics.Bitmap
import android.graphics.drawable.Drawable
import android.net.Uri
import android.util.AttributeSet
import android.view.View
import android.widget.FrameLayout
import androidx.core.view.isVisible
import com.bumptech.glide.load.engine.DiskCacheStrategy
import com.bumptech.glide.load.resource.bitmap.CenterCrop
import com.bumptech.glide.load.resource.bitmap.RoundedCorners
import com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions
import com.bumptech.glide.request.RequestOptions
import kotlinx.android.synthetic.main.thumbnail_view.view.*
import network.loki.messenger.R
import org.session.libsession.messaging.sending_receiving.attachments.AttachmentTransferProgress
import org.session.libsession.utilities.Util.equals
import org.session.libsignal.utilities.ListenableFuture
import org.session.libsignal.utilities.SettableFuture
import org.thoughtcrime.securesms.components.GlideBitmapListeningTarget
import org.thoughtcrime.securesms.components.GlideDrawableListeningTarget
import org.thoughtcrime.securesms.mms.*
import org.thoughtcrime.securesms.mms.DecryptableStreamUriLoader.DecryptableUri
open class KThumbnailView: FrameLayout {
companion object {
private const val WIDTH = 0
private const val HEIGHT = 1
}
// region Lifecycle
constructor(context: Context) : super(context) { initialize(null) }
constructor(context: Context, attrs: AttributeSet) : super(context, attrs) { initialize(attrs) }
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr) { initialize(attrs) }
private val image by lazy { thumbnail_image }
private val playOverlay by lazy { play_overlay }
private val captionIcon by lazy { thumbnail_caption_icon }
val loadIndicator: View by lazy { thumbnail_load_indicator }
private val dimensDelegate = ThumbnailDimensDelegate()
var thumbnailClickListener: SlideClickListener? = null
private var slide: Slide? = null
private var radius: Int = 0
private fun initialize(attrs: AttributeSet?) {
inflate(context, R.layout.thumbnail_view, this)
if (attrs != null) {
val typedArray = context.theme.obtainStyledAttributes(attrs, R.styleable.ThumbnailView, 0, 0)
dimensDelegate.setBounds(typedArray.getDimensionPixelSize(R.styleable.ThumbnailView_minWidth, 0),
typedArray.getDimensionPixelSize(R.styleable.ThumbnailView_minHeight, 0),
typedArray.getDimensionPixelSize(R.styleable.ThumbnailView_maxWidth, 0),
typedArray.getDimensionPixelSize(R.styleable.ThumbnailView_maxHeight, 0))
radius = typedArray.getDimensionPixelSize(R.styleable.ThumbnailView_thumbnail_radius, 0)
typedArray.recycle()
}
}
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
val adjustedDimens = dimensDelegate.resourceSize()
if (adjustedDimens[WIDTH] == 0 && adjustedDimens[HEIGHT] == 0) {
return super.onMeasure(widthMeasureSpec, heightMeasureSpec)
}
val finalWidth: Int = adjustedDimens[WIDTH] + paddingLeft + paddingRight
val finalHeight: Int = adjustedDimens[HEIGHT] + paddingTop + paddingBottom
super.onMeasure(
MeasureSpec.makeMeasureSpec(finalWidth, MeasureSpec.EXACTLY),
MeasureSpec.makeMeasureSpec(finalHeight, MeasureSpec.EXACTLY)
)
}
private fun getDefaultWidth() = maxOf(layoutParams?.width ?: 0, 0)
private fun getDefaultHeight() = maxOf(layoutParams?.height ?: 0, 0)
// endregion
// region Interaction
fun setImageResource(glide: GlideRequests, slide: Slide, isPreview: Boolean): ListenableFuture<Boolean> {
return setImageResource(glide, slide, isPreview, 0, 0)
}
fun setImageResource(glide: GlideRequests, slide: Slide,
isPreview: Boolean, naturalWidth: Int,
naturalHeight: Int): ListenableFuture<Boolean> {
val currentSlide = this.slide
playOverlay.isVisible = (slide.thumbnailUri != null && slide.hasPlayOverlay() &&
(slide.transferState == AttachmentTransferProgress.TRANSFER_PROGRESS_DONE || isPreview))
if (equals(currentSlide, slide)) {
// don't re-load slide
return SettableFuture(false)
}
if (currentSlide != null && currentSlide.fastPreflightId != null && currentSlide.fastPreflightId == slide.fastPreflightId) {
// not reloading slide for fast preflight
this.slide = slide
}
this.slide = slide
captionIcon.isVisible = slide.caption.isPresent
loadIndicator.isVisible = slide.isInProgress
dimensDelegate.setDimens(naturalWidth, naturalHeight)
invalidate()
val result = SettableFuture<Boolean>()
when {
slide.thumbnailUri != null -> {
buildThumbnailGlideRequest(glide, slide).into(GlideDrawableListeningTarget(image, result))
}
slide.hasPlaceholder() -> {
buildPlaceholderGlideRequest(glide, slide).into(GlideBitmapListeningTarget(image, result))
}
else -> {
glide.clear(image)
result.set(false)
}
}
return result
}
fun buildThumbnailGlideRequest(glide: GlideRequests, slide: Slide): GlideRequest<Drawable> {
val dimens = dimensDelegate.resourceSize()
val request = glide.load(DecryptableUri(slide.thumbnailUri!!))
.diskCacheStrategy(DiskCacheStrategy.RESOURCE)
.let { request ->
if (dimens[WIDTH] == 0 || dimens[HEIGHT] == 0) {
request.override(getDefaultWidth(), getDefaultHeight())
} else {
request.override(dimens[WIDTH], dimens[HEIGHT])
}
}
.transition(DrawableTransitionOptions.withCrossFade())
.centerCrop()
return if (slide.isInProgress) request else request.apply(RequestOptions.errorOf(R.drawable.ic_missing_thumbnail_picture))
}
fun buildPlaceholderGlideRequest(glide: GlideRequests, slide: Slide): GlideRequest<Bitmap> {
val dimens = dimensDelegate.resourceSize()
return glide.asBitmap()
.load(slide.getPlaceholderRes(context.theme))
.diskCacheStrategy(DiskCacheStrategy.NONE)
.let { request ->
if (dimens[WIDTH] == 0 || dimens[HEIGHT] == 0) {
request.override(getDefaultWidth(), getDefaultHeight())
} else {
request.override(dimens[WIDTH], dimens[HEIGHT])
}
}
.fitCenter()
}
open fun clear(glideRequests: GlideRequests) {
glideRequests.clear(image)
slide = null
}
fun setImageResource(glideRequests: GlideRequests, uri: Uri): ListenableFuture<Boolean> {
val future = SettableFuture<Boolean>()
var request: GlideRequest<Drawable> = glideRequests.load(DecryptableUri(uri))
.diskCacheStrategy(DiskCacheStrategy.NONE)
.transition(DrawableTransitionOptions.withCrossFade())
request = if (radius > 0) {
request.transforms(CenterCrop(), RoundedCorners(radius))
} else {
request.transforms(CenterCrop())
}
request.into(GlideDrawableListeningTarget(image, future))
return future
}
// endregion
}

View File

@ -0,0 +1,91 @@
package org.thoughtcrime.securesms.conversation.v2.utilities
class ThumbnailDimensDelegate {
companion object {
// dimens array constants
private const val WIDTH = 0
private const val HEIGHT = 1
private const val DIMENS_ARRAY_SIZE = 2
// bounds array constants
private const val MIN_WIDTH = 0
private const val MIN_HEIGHT = 1
private const val MAX_WIDTH = 2
private const val MAX_HEIGHT = 3
private const val BOUNDS_ARRAY_SIZE = 4
// const zero int array
private val EMPTY_DIMENS = intArrayOf(0,0)
}
private val measured: IntArray = IntArray(DIMENS_ARRAY_SIZE)
private val dimens: IntArray = IntArray(DIMENS_ARRAY_SIZE)
private val bounds: IntArray = IntArray(BOUNDS_ARRAY_SIZE)
fun resourceSize(): IntArray {
if (dimens.all { it == 0 }) {
// dimens are (0, 0), don't go any further
return EMPTY_DIMENS
}
val naturalWidth = dimens[WIDTH].toDouble()
val naturalHeight = dimens[HEIGHT].toDouble()
val minWidth = dimens[MIN_WIDTH]
val maxWidth = dimens[MAX_WIDTH]
val minHeight = dimens[MIN_HEIGHT]
val maxHeight = dimens[MAX_HEIGHT]
// calculate actual measured
var measuredWidth: Double = naturalWidth
var measuredHeight: Double = naturalHeight
val widthInBounds = measuredWidth >= minWidth && measuredWidth <= maxWidth
val heightInBounds = measuredHeight >= minHeight && measuredHeight <= maxHeight
if (!widthInBounds || !heightInBounds) {
val minWidthRatio: Double = naturalWidth / minWidth
val maxWidthRatio: Double = naturalWidth / maxWidth
val minHeightRatio: Double = naturalHeight / minHeight
val maxHeightRatio: Double = naturalHeight / maxHeight
if (maxWidthRatio > 1 || maxHeightRatio > 1) {
if (maxWidthRatio >= maxHeightRatio) {
measuredWidth /= maxWidthRatio
measuredHeight /= maxWidthRatio
} else {
measuredWidth /= maxHeightRatio
measuredHeight /= maxHeightRatio
}
measuredWidth = Math.max(measuredWidth, minWidth.toDouble())
measuredHeight = Math.max(measuredHeight, minHeight.toDouble())
} else if (minWidthRatio < 1 || minHeightRatio < 1) {
if (minWidthRatio <= minHeightRatio) {
measuredWidth /= minWidthRatio
measuredHeight /= minWidthRatio
} else {
measuredWidth /= minHeightRatio
measuredHeight /= minHeightRatio
}
measuredWidth = Math.min(measuredWidth, maxWidth.toDouble())
measuredHeight = Math.min(measuredHeight, maxHeight.toDouble())
}
}
measured[WIDTH] = measuredWidth.toInt()
measured[HEIGHT] = measuredHeight.toInt()
return measured
}
fun setBounds(minWidth: Int, minHeight: Int, maxWidth: Int, maxHeight: Int) {
bounds[MIN_WIDTH] = minWidth
bounds[MIN_HEIGHT] = minHeight
bounds[MAX_WIDTH] = maxWidth
bounds[MAX_HEIGHT] = maxHeight
}
fun setDimens(width: Int, height: Int) {
dimens[WIDTH] = width
dimens[HEIGHT] = height
}
}

View File

@ -0,0 +1,64 @@
package org.thoughtcrime.securesms.conversation.v2.utilities
import android.animation.ValueAnimator
import android.content.Context
import android.graphics.Canvas
import android.graphics.Interpolator
import android.graphics.Paint
import android.graphics.Rect
import android.os.SystemClock
import android.util.AttributeSet
import android.util.Log
import android.view.View
import android.view.animation.AccelerateDecelerateInterpolator
import android.view.animation.Animation
import android.view.animation.AnimationSet
import android.view.animation.AnimationUtils
import androidx.core.content.res.ResourcesCompat
import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView
import network.loki.messenger.R
import kotlin.math.sin
class ThumbnailProgressBar: View {
constructor(context: Context) : super(context)
constructor(context: Context, attrs: AttributeSet) : super(context, attrs)
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr)
private val firstX: Double
get() = sin(SystemClock.elapsedRealtime() / 300.0) * 1.5
private val secondX: Double
get() = sin(SystemClock.elapsedRealtime() / 300.0 + (Math.PI/4)) * 1.5
private val paint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
style = Paint.Style.FILL
color = ResourcesCompat.getColor(resources, R.color.accent, null)
}
private val objectRect = Rect()
private val drawingRect = Rect()
override fun dispatchDraw(canvas: Canvas?) {
if (canvas == null) return
getDrawingRect(objectRect)
drawingRect.set(objectRect)
val coercedFX = firstX
val coercedSX = secondX
val firstMeasuredX = objectRect.left + (objectRect.width() * coercedFX)
val secondMeasuredX = objectRect.left + (objectRect.width() * coercedSX)
drawingRect.set(
(if (firstMeasuredX < secondMeasuredX) firstMeasuredX else secondMeasuredX).toInt(),
objectRect.top,
(if (firstMeasuredX < secondMeasuredX) secondMeasuredX else firstMeasuredX).toInt(),
objectRect.bottom
)
canvas.drawRect(drawingRect, paint)
invalidate()
}
}

View File

@ -97,6 +97,7 @@ public class ThumbnailView extends FrameLayout {
bounds[MAX_WIDTH] = typedArray.getDimensionPixelSize(R.styleable.ThumbnailView_maxWidth, 0); bounds[MAX_WIDTH] = typedArray.getDimensionPixelSize(R.styleable.ThumbnailView_maxWidth, 0);
bounds[MIN_HEIGHT] = typedArray.getDimensionPixelSize(R.styleable.ThumbnailView_minHeight, 0); bounds[MIN_HEIGHT] = typedArray.getDimensionPixelSize(R.styleable.ThumbnailView_minHeight, 0);
bounds[MAX_HEIGHT] = typedArray.getDimensionPixelSize(R.styleable.ThumbnailView_maxHeight, 0); bounds[MAX_HEIGHT] = typedArray.getDimensionPixelSize(R.styleable.ThumbnailView_maxHeight, 0);
radius = typedArray.getDimensionPixelSize(R.styleable.ThumbnailView_thumbnail_radius, 0);
typedArray.recycle(); typedArray.recycle();
} else { } else {
radius = 0; radius = 0;

View File

@ -1,6 +1,10 @@
package org.thoughtcrime.securesms.loki.utilities package org.thoughtcrime.securesms.loki.utilities
import android.annotation.SuppressLint
import android.app.Activity
import android.content.Context
import android.content.Intent import android.content.Intent
import android.os.Bundle
import android.view.View import android.view.View
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.widget.Toolbar import androidx.appcompat.widget.Toolbar
@ -52,4 +56,13 @@ fun AppCompatActivity.show(intent: Intent, isForResult: Boolean = false) {
startActivity(intent) startActivity(intent)
} }
overridePendingTransition(R.anim.slide_from_bottom, R.anim.fade_scale_out) overridePendingTransition(R.anim.slide_from_bottom, R.anim.fade_scale_out)
}
interface ActivityDispatcher {
companion object {
const val SERVICE = "ActivityDispatcher_SERVICE"
@SuppressLint("WrongConstant")
fun get(context: Context) = context.getSystemService(SERVICE) as? ActivityDispatcher
}
fun dispatchIntent(body: (Context)->Intent?)
} }

View File

@ -2,36 +2,20 @@ package org.thoughtcrime.securesms.longmessage;
import android.content.Context; import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.graphics.PorterDuff;
import android.os.Bundle; import android.os.Bundle;
import android.text.SpannableString;
import android.text.method.LinkMovementMethod; import android.text.method.LinkMovementMethod;
import android.text.style.URLSpan;
import android.text.util.Linkify;
import android.util.TypedValue;
import android.view.MenuItem; import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView; import android.widget.TextView;
import android.widget.Toast; import android.widget.Toast;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.lifecycle.ViewModelProvider; import androidx.lifecycle.ViewModelProvider;
import com.annimon.stream.Stream;
import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity;
import org.thoughtcrime.securesms.components.ConversationItemFooter;
import org.thoughtcrime.securesms.linkpreview.LinkPreviewUtil;
import org.session.libsession.utilities.Address; import org.session.libsession.utilities.Address;
import org.session.libsession.utilities.recipients.Recipient;
import org.session.libsession.utilities.TextSecurePreferences;
import org.session.libsession.utilities.ThemeUtil;
import org.session.libsession.utilities.Util; import org.session.libsession.utilities.Util;
import org.session.libsession.utilities.Stub; import org.session.libsession.utilities.recipients.Recipient;
import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity;
import java.util.Locale; import org.thoughtcrime.securesms.loki.utilities.MentionUtilities;
import network.loki.messenger.R; import network.loki.messenger.R;
@ -43,8 +27,7 @@ public class LongMessageActivity extends PassphraseRequiredActionBarActivity {
private static final int MAX_DISPLAY_LENGTH = 64 * 1024; private static final int MAX_DISPLAY_LENGTH = 64 * 1024;
private Stub<ViewGroup> sentBubble; private TextView textBody;
private Stub<ViewGroup> receivedBubble;
private LongMessageViewModel viewModel; private LongMessageViewModel viewModel;
@ -60,9 +43,7 @@ public class LongMessageActivity extends PassphraseRequiredActionBarActivity {
protected void onCreate(Bundle savedInstanceState, boolean ready) { protected void onCreate(Bundle savedInstanceState, boolean ready) {
super.onCreate(savedInstanceState, ready); super.onCreate(savedInstanceState, ready);
setContentView(R.layout.longmessage_activity); setContentView(R.layout.longmessage_activity);
textBody = findViewById(R.id.longmessage_text);
sentBubble = new Stub<>(findViewById(R.id.longmessage_sent_stub));
receivedBubble = new Stub<>(findViewById(R.id.longmessage_received_stub));
initViewModel(getIntent().getLongExtra(KEY_MESSAGE_ID, -1), getIntent().getBooleanExtra(KEY_IS_MMS, false)); initViewModel(getIntent().getLongExtra(KEY_MESSAGE_ID, -1), getIntent().getBooleanExtra(KEY_IS_MMS, false));
} }
@ -93,36 +74,19 @@ public class LongMessageActivity extends PassphraseRequiredActionBarActivity {
return; return;
} }
if (message.get().getMessageRecord().isOutgoing()) { if (message.get().getMessageRecord().isOutgoing()) {
getSupportActionBar().setTitle(getString(R.string.LongMessageActivity_your_message)); getSupportActionBar().setTitle(getString(R.string.LongMessageActivity_your_message));
} else { } else {
Recipient recipient = message.get().getMessageRecord().getRecipient(); Recipient recipient = message.get().getMessageRecord().getRecipient();
String name = Util.getFirstNonEmpty(recipient.getName(), recipient.getProfileName(), recipient.getAddress().serialize()) ; String name = Util.getFirstNonEmpty(recipient.getName(), recipient.getProfileName(), recipient.getAddress().serialize());
getSupportActionBar().setTitle(getString(R.string.LongMessageActivity_message_from_s, name)); getSupportActionBar().setTitle(getString(R.string.LongMessageActivity_message_from_s, name));
} }
ViewGroup bubble; String trimmedBody = getTrimmedBody(message.get().getFullBody());
String mentionBody = MentionUtilities.highlightMentions(trimmedBody, message.get().getMessageRecord().getThreadId(), this);
if (message.get().getMessageRecord().isOutgoing()) { textBody.setText(mentionBody);
bubble = sentBubble.get(); textBody.setMovementMethod(LinkMovementMethod.getInstance());
bubble.getBackground().setColorFilter(ThemeUtil.getThemedColor(this, R.attr.message_sent_background_color), PorterDuff.Mode.MULTIPLY);
} else {
bubble = receivedBubble.get();
bubble.getBackground().setColorFilter(ThemeUtil.getThemedColor(this, R.attr.message_received_background_color), PorterDuff.Mode.MULTIPLY);
}
TextView text = bubble.findViewById(R.id.longmessage_text);
ConversationItemFooter footer = bubble.findViewById(R.id.longmessage_footer);
String trimmedBody = getTrimmedBody(message.get().getFullBody());
SpannableString styledBody = linkifyMessageBody(new SpannableString(trimmedBody));
bubble.setVisibility(View.VISIBLE);
text.setText(styledBody);
text.setMovementMethod(LinkMovementMethod.getInstance());
text.setTextSize(TypedValue.COMPLEX_UNIT_SP, TextSecurePreferences.getMessageBodyTextSize(this));
footer.setMessageRecord(message.get().getMessageRecord(), Locale.getDefault());
}); });
} }
@ -131,15 +95,4 @@ public class LongMessageActivity extends PassphraseRequiredActionBarActivity {
: text.substring(0, MAX_DISPLAY_LENGTH); : text.substring(0, MAX_DISPLAY_LENGTH);
} }
private SpannableString linkifyMessageBody(SpannableString messageBody) {
int linkPattern = Linkify.WEB_URLS | Linkify.EMAIL_ADDRESSES | Linkify.PHONE_NUMBERS;
boolean hasLinks = Linkify.addLinks(messageBody, linkPattern);
if (hasLinks) {
Stream.of(messageBody.getSpans(0, messageBody.length(), URLSpan.class))
.filterNot(url -> LinkPreviewUtil.isLegalUrl(url.getURL()))
.forEach(messageBody::removeSpan);
}
return messageBody;
}
} }

View File

@ -31,7 +31,6 @@ import org.session.libsession.utilities.MediaTypes;
import org.thoughtcrime.securesms.database.AttachmentDatabase; import org.thoughtcrime.securesms.database.AttachmentDatabase;
import org.thoughtcrime.securesms.util.ResUtil; import org.thoughtcrime.securesms.util.ResUtil;
public class AudioSlide extends Slide { public class AudioSlide extends Slide {
public AudioSlide(Context context, Uri uri, long dataSize, boolean voiceNote) { public AudioSlide(Context context, Uri uri, long dataSize, boolean voiceNote) {

View File

@ -103,7 +103,6 @@ public class AppProtectionPreferenceFragment extends CorrectedPreferenceFragment
TextSecurePreferences.setScreenLockTimeout(getContext(), 0); TextSecurePreferences.setScreenLockTimeout(getContext(), 0);
} else { } else {
long timeoutSeconds = TimeUnit.MILLISECONDS.toSeconds(duration); long timeoutSeconds = TimeUnit.MILLISECONDS.toSeconds(duration);
// long timeoutSeconds = Math.max(TimeUnit.MILLISECONDS.toSeconds(duration), 60);
TextSecurePreferences.setScreenLockTimeout(getContext(), timeoutSeconds); TextSecurePreferences.setScreenLockTimeout(getContext(), timeoutSeconds);
} }
@ -117,7 +116,6 @@ public class AppProtectionPreferenceFragment extends CorrectedPreferenceFragment
private class ReadReceiptToggleListener implements Preference.OnPreferenceChangeListener { private class ReadReceiptToggleListener implements Preference.OnPreferenceChangeListener {
@Override @Override
public boolean onPreferenceChange(Preference preference, Object newValue) { public boolean onPreferenceChange(Preference preference, Object newValue) {
boolean enabled = (boolean)newValue;
return true; return true;
} }
} }
@ -138,21 +136,6 @@ public class AppProtectionPreferenceFragment extends CorrectedPreferenceFragment
private class LinkPreviewToggleListener implements Preference.OnPreferenceChangeListener { private class LinkPreviewToggleListener implements Preference.OnPreferenceChangeListener {
@Override @Override
public boolean onPreferenceChange(Preference preference, Object newValue) { public boolean onPreferenceChange(Preference preference, Object newValue) {
boolean enabled = (boolean)newValue;
if (enabled) {
AlertDialog.Builder builder = new AlertDialog.Builder(requireContext());
builder.setTitle("Enable Link Previews?");
builder.setMessage("You will not have full metadata protection when sending or receiving link previews.");
builder.setPositiveButton("OK", (dialog, which) -> dialog.dismiss());
builder.setNegativeButton("Cancel", (dialog, which) -> {
TextSecurePreferences.setLinkPreviewsEnabled(requireContext(), false);
((SwitchPreferenceCompat)AppProtectionPreferenceFragment.this.findPreference(TextSecurePreferences.LINK_PREVIEWS)).setChecked(false);
dialog.dismiss();
});
builder.create().show();
}
return true; return true;
} }
} }

View File

@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/album_thumbnail_root"
android:layout_width="@dimen/media_bubble_default_dimens"
android:layout_height="@dimen/media_bubble_default_dimens">
<org.thoughtcrime.securesms.conversation.v2.utilities.KThumbnailView
android:id="@+id/album_cell_1"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:minWidth="@dimen/media_bubble_min_width"
app:maxWidth="@dimen/media_bubble_max_width"
app:minHeight="@dimen/media_bubble_min_height"
app:maxHeight="@dimen/media_bubble_max_height"
app:thumbnail_radius="1dp"/>
</FrameLayout>

View File

@ -7,13 +7,13 @@
android:layout_width="@dimen/album_total_width" android:layout_width="@dimen/album_total_width"
android:layout_height="@dimen/album_2_total_height"> android:layout_height="@dimen/album_2_total_height">
<org.thoughtcrime.securesms.conversation.v2.utilities.ThumbnailView <org.thoughtcrime.securesms.conversation.v2.utilities.KThumbnailView
android:id="@+id/album_cell_1" android:id="@+id/album_cell_1"
android:layout_width="@dimen/album_2_cell_width" android:layout_width="@dimen/album_2_cell_width"
android:layout_height="@dimen/album_2_total_height" android:layout_height="@dimen/album_2_total_height"
app:thumbnail_radius="0dp"/> app:thumbnail_radius="0dp"/>
<org.thoughtcrime.securesms.conversation.v2.utilities.ThumbnailView <org.thoughtcrime.securesms.conversation.v2.utilities.KThumbnailView
android:id="@+id/album_cell_2" android:id="@+id/album_cell_2"
android:layout_width="@dimen/album_2_cell_width" android:layout_width="@dimen/album_2_cell_width"
android:layout_height="@dimen/album_2_total_height" android:layout_height="@dimen/album_2_total_height"

View File

@ -6,20 +6,20 @@
android:layout_width="@dimen/album_total_width" android:layout_width="@dimen/album_total_width"
android:layout_height="@dimen/album_3_total_height"> android:layout_height="@dimen/album_3_total_height">
<org.thoughtcrime.securesms.conversation.v2.utilities.ThumbnailView <org.thoughtcrime.securesms.conversation.v2.utilities.KThumbnailView
android:id="@+id/album_cell_1" android:id="@+id/album_cell_1"
android:layout_width="@dimen/album_3_cell_width_big" android:layout_width="@dimen/album_3_cell_width_big"
android:layout_height="@dimen/album_3_total_height" android:layout_height="@dimen/album_3_total_height"
app:thumbnail_radius="0dp"/> app:thumbnail_radius="0dp"/>
<org.thoughtcrime.securesms.conversation.v2.utilities.ThumbnailView <org.thoughtcrime.securesms.conversation.v2.utilities.KThumbnailView
android:id="@+id/album_cell_2" android:id="@+id/album_cell_2"
android:layout_width="@dimen/album_3_cell_size_small" android:layout_width="@dimen/album_3_cell_size_small"
android:layout_height="@dimen/album_3_cell_size_small" android:layout_height="@dimen/album_3_cell_size_small"
android:layout_gravity="right|end|top" android:layout_gravity="right|end|top"
app:thumbnail_radius="0dp"/> app:thumbnail_radius="0dp"/>
<org.thoughtcrime.securesms.conversation.v2.utilities.ThumbnailView <org.thoughtcrime.securesms.conversation.v2.utilities.KThumbnailView
android:id="@+id/album_cell_3" android:id="@+id/album_cell_3"
android:layout_width="@dimen/album_3_cell_size_small" android:layout_width="@dimen/album_3_cell_size_small"
android:layout_height="@dimen/album_3_cell_size_small" android:layout_height="@dimen/album_3_cell_size_small"

View File

@ -6,27 +6,27 @@
android:layout_width="@dimen/album_total_width" android:layout_width="@dimen/album_total_width"
android:layout_height="@dimen/album_4_total_height"> android:layout_height="@dimen/album_4_total_height">
<org.thoughtcrime.securesms.conversation.v2.utilities.ThumbnailView <org.thoughtcrime.securesms.conversation.v2.utilities.KThumbnailView
android:id="@+id/album_cell_1" android:id="@+id/album_cell_1"
android:layout_width="@dimen/album_4_cell_size" android:layout_width="@dimen/album_4_cell_size"
android:layout_height="@dimen/album_4_cell_size" android:layout_height="@dimen/album_4_cell_size"
app:thumbnail_radius="0dp"/> app:thumbnail_radius="0dp"/>
<org.thoughtcrime.securesms.conversation.v2.utilities.ThumbnailView <org.thoughtcrime.securesms.conversation.v2.utilities.KThumbnailView
android:id="@+id/album_cell_2" android:id="@+id/album_cell_2"
android:layout_width="@dimen/album_4_cell_size" android:layout_width="@dimen/album_4_cell_size"
android:layout_height="@dimen/album_4_cell_size" android:layout_height="@dimen/album_4_cell_size"
android:layout_gravity="right|end|top" android:layout_gravity="right|end|top"
app:thumbnail_radius="0dp"/> app:thumbnail_radius="0dp"/>
<org.thoughtcrime.securesms.conversation.v2.utilities.ThumbnailView <org.thoughtcrime.securesms.conversation.v2.utilities.KThumbnailView
android:id="@+id/album_cell_3" android:id="@+id/album_cell_3"
android:layout_width="@dimen/album_4_cell_size" android:layout_width="@dimen/album_4_cell_size"
android:layout_height="@dimen/album_4_cell_size" android:layout_height="@dimen/album_4_cell_size"
android:layout_gravity="left|start|bottom" android:layout_gravity="left|start|bottom"
app:thumbnail_radius="0dp"/> app:thumbnail_radius="0dp"/>
<org.thoughtcrime.securesms.conversation.v2.utilities.ThumbnailView <org.thoughtcrime.securesms.conversation.v2.utilities.KThumbnailView
android:id="@+id/album_cell_4" android:id="@+id/album_cell_4"
android:layout_width="@dimen/album_4_cell_size" android:layout_width="@dimen/album_4_cell_size"
android:layout_height="@dimen/album_4_cell_size" android:layout_height="@dimen/album_4_cell_size"

View File

@ -6,34 +6,34 @@
android:layout_width="@dimen/album_total_width" android:layout_width="@dimen/album_total_width"
android:layout_height="@dimen/album_5_total_height"> android:layout_height="@dimen/album_5_total_height">
<org.thoughtcrime.securesms.conversation.v2.utilities.ThumbnailView <org.thoughtcrime.securesms.conversation.v2.utilities.KThumbnailView
android:id="@+id/album_cell_1" android:id="@+id/album_cell_1"
android:layout_width="@dimen/album_5_cell_size_big" android:layout_width="@dimen/album_5_cell_size_big"
android:layout_height="@dimen/album_5_cell_size_big" android:layout_height="@dimen/album_5_cell_size_big"
app:thumbnail_radius="0dp"/> app:thumbnail_radius="0dp"/>
<org.thoughtcrime.securesms.conversation.v2.utilities.ThumbnailView <org.thoughtcrime.securesms.conversation.v2.utilities.KThumbnailView
android:id="@+id/album_cell_2" android:id="@+id/album_cell_2"
android:layout_width="@dimen/album_5_cell_size_big" android:layout_width="@dimen/album_5_cell_size_big"
android:layout_height="@dimen/album_5_cell_size_big" android:layout_height="@dimen/album_5_cell_size_big"
android:layout_gravity="right|end|top" android:layout_gravity="right|end|top"
app:thumbnail_radius="0dp"/> app:thumbnail_radius="0dp"/>
<org.thoughtcrime.securesms.conversation.v2.utilities.ThumbnailView <org.thoughtcrime.securesms.conversation.v2.utilities.KThumbnailView
android:id="@+id/album_cell_3" android:id="@+id/album_cell_3"
android:layout_width="@dimen/album_5_cell_size_small" android:layout_width="@dimen/album_5_cell_size_small"
android:layout_height="@dimen/album_5_cell_size_small" android:layout_height="@dimen/album_5_cell_size_small"
android:layout_gravity="left|start|bottom" android:layout_gravity="left|start|bottom"
app:thumbnail_radius="0dp"/> app:thumbnail_radius="0dp"/>
<org.thoughtcrime.securesms.conversation.v2.utilities.ThumbnailView <org.thoughtcrime.securesms.conversation.v2.utilities.KThumbnailView
android:id="@+id/album_cell_4" android:id="@+id/album_cell_4"
android:layout_width="@dimen/album_5_cell_size_small" android:layout_width="@dimen/album_5_cell_size_small"
android:layout_height="@dimen/album_5_cell_size_small" android:layout_height="@dimen/album_5_cell_size_small"
android:layout_gravity="center_horizontal|bottom" android:layout_gravity="center_horizontal|bottom"
app:thumbnail_radius="0dp"/> app:thumbnail_radius="0dp"/>
<org.thoughtcrime.securesms.conversation.v2.utilities.ThumbnailView <org.thoughtcrime.securesms.conversation.v2.utilities.KThumbnailView
android:id="@+id/album_cell_5" android:id="@+id/album_cell_5"
android:layout_width="@dimen/album_5_cell_size_small" android:layout_width="@dimen/album_5_cell_size_small"
android:layout_height="@dimen/album_5_cell_size_small" android:layout_height="@dimen/album_5_cell_size_small"

View File

@ -7,27 +7,27 @@
android:layout_width="@dimen/album_total_width" android:layout_width="@dimen/album_total_width"
android:layout_height="@dimen/album_5_total_height"> android:layout_height="@dimen/album_5_total_height">
<org.thoughtcrime.securesms.conversation.v2.utilities.ThumbnailView <org.thoughtcrime.securesms.conversation.v2.utilities.KThumbnailView
android:id="@+id/album_cell_1" android:id="@+id/album_cell_1"
android:layout_width="@dimen/album_5_cell_size_big" android:layout_width="@dimen/album_5_cell_size_big"
android:layout_height="@dimen/album_5_cell_size_big" android:layout_height="@dimen/album_5_cell_size_big"
app:thumbnail_radius="0dp"/> app:thumbnail_radius="0dp"/>
<org.thoughtcrime.securesms.conversation.v2.utilities.ThumbnailView <org.thoughtcrime.securesms.conversation.v2.utilities.KThumbnailView
android:id="@+id/album_cell_2" android:id="@+id/album_cell_2"
android:layout_width="@dimen/album_5_cell_size_big" android:layout_width="@dimen/album_5_cell_size_big"
android:layout_height="@dimen/album_5_cell_size_big" android:layout_height="@dimen/album_5_cell_size_big"
android:layout_gravity="right|end|top" android:layout_gravity="right|end|top"
app:thumbnail_radius="0dp"/> app:thumbnail_radius="0dp"/>
<org.thoughtcrime.securesms.conversation.v2.utilities.ThumbnailView <org.thoughtcrime.securesms.conversation.v2.utilities.KThumbnailView
android:id="@+id/album_cell_3" android:id="@+id/album_cell_3"
android:layout_width="@dimen/album_5_cell_size_small" android:layout_width="@dimen/album_5_cell_size_small"
android:layout_height="@dimen/album_5_cell_size_small" android:layout_height="@dimen/album_5_cell_size_small"
android:layout_gravity="left|start|bottom" android:layout_gravity="left|start|bottom"
app:thumbnail_radius="0dp"/> app:thumbnail_radius="0dp"/>
<org.thoughtcrime.securesms.conversation.v2.utilities.ThumbnailView <org.thoughtcrime.securesms.conversation.v2.utilities.KThumbnailView
android:id="@+id/album_cell_4" android:id="@+id/album_cell_4"
android:layout_width="@dimen/album_5_cell_size_small" android:layout_width="@dimen/album_5_cell_size_small"
android:layout_height="@dimen/album_5_cell_size_small" android:layout_height="@dimen/album_5_cell_size_small"
@ -39,7 +39,7 @@
android:layout_height="@dimen/album_5_cell_size_small" android:layout_height="@dimen/album_5_cell_size_small"
android:layout_gravity="right|end|bottom"> android:layout_gravity="right|end|bottom">
<org.thoughtcrime.securesms.conversation.v2.utilities.ThumbnailView <org.thoughtcrime.securesms.conversation.v2.utilities.KThumbnailView
android:id="@+id/album_cell_5" android:id="@+id/album_cell_5"
android:layout_height="match_parent" android:layout_height="match_parent"
android:layout_width="match_parent" android:layout_width="match_parent"
@ -51,7 +51,7 @@
android:layout_height="match_parent" android:layout_height="match_parent"
android:layout_width="match_parent" android:layout_width="match_parent"
android:gravity="center" android:gravity="center"
android:textSize="28dp" android:textSize="@dimen/text_size"
android:textColor="@color/core_white" android:textColor="@color/core_white"
android:background="@color/transparent_black_40" android:background="@color/transparent_black_40"
tools:text="+2" /> tools:text="+2" />

View File

@ -1,21 +1,79 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<merge <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools"
android:orientation="vertical" xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="wrap_content" android:layout_width="match_parent"
android:layout_height="wrap_content"> android:layout_height="match_parent">
<FrameLayout <FrameLayout
android:id="@+id/album_cell_container" android:id="@+id/albumCellContainer"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:background="?conversation_background"/> />
<ViewStub <ViewStub
android:id="@+id/album_transfer_controls_stub" android:layout_alignTop="@+id/albumCellContainer"
android:layout_alignStart="@+id/albumCellContainer"
android:layout_alignEnd="@+id/albumCellContainer"
android:layout_alignBottom="@+id/albumCellContainer"
android:id="@+id/albumTransferControlsStub"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_gravity="center" android:layout_gravity="center"
android:layout="@layout/transfer_controls_stub" /> android:layout="@layout/transfer_controls_stub" />
</merge> <androidx.constraintlayout.widget.ConstraintLayout
android:layout_alignTop="@+id/albumCellContainer"
android:layout_alignStart="@+id/albumCellContainer"
android:layout_alignEnd="@+id/albumCellContainer"
android:layout_alignBottom="@+id/albumCellContainer"
tools:visibility="visible"
android:visibility="gone"
android:layout_gravity="bottom"
android:id="@+id/albumCellBodyParent"
android:layout_width="0dp"
android:layout_height="wrap_content">
<ImageView
android:id="@+id/albumCellShade"
android:src="@drawable/image_shade"
android:layout_width="match_parent"
android:layout_height="0dp"
app:layout_constraintTop_toTopOf="@+id/albumCellBodyTextParent"
app:layout_constraintBottom_toBottomOf="parent"
/>
<LinearLayout
android:id="@+id/albumCellBodyTextParent"
android:padding="@dimen/medium_spacing"
android:layout_width="match_parent"
android:layout_height="0dp"
app:layout_constraintBottom_toTopOf="@+id/albumCellBodyTextReadMore"
android:orientation="horizontal">
<View
android:layout_width="@dimen/accent_line_thickness"
android:layout_height="match_parent"
android:background="@color/accent"/>
<TextView
android:maxLines="4"
android:ellipsize="end"
android:id="@+id/albumCellBodyText"
android:textColor="@color/core_white"
android:layout_marginStart="@dimen/small_spacing"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
</LinearLayout>
<TextView
tools:visibility="visible"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
android:id="@+id/albumCellBodyTextReadMore"
android:textColor="@color/core_white"
android:paddingHorizontal="@dimen/medium_spacing"
android:paddingBottom="@dimen/medium_spacing"
android:textStyle="bold"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/ConversationItem_read_more"/>
</androidx.constraintlayout.widget.ConstraintLayout>
</RelativeLayout>

View File

@ -1,34 +1,35 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" <RelativeLayout
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools" xmlns:app="http://schemas.android.com/apk/res-auto"
android:orientation="vertical" xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent" android:orientation="vertical"
android:layout_height="wrap_content" android:layout_width="match_parent"
android:layout_gravity="center" android:layout_height="wrap_content"
android:gravity="center"> android:layout_gravity="center"
android:gravity="center">
<cn.carbswang.android.numberpickerview.library.NumberPickerView <cn.carbswang.android.numberpickerview.library.NumberPickerView
android:id="@+id/expiration_number_picker" android:id="@+id/expiration_number_picker"
android:layout_alignParentTop="true" android:layout_alignParentTop="true"
app:npv_WrapSelectorWheel="false" app:npv_WrapSelectorWheel="false"
app:npv_DividerColor="#cbc8ea" app:npv_DividerColor="#cbc8ea"
app:npv_TextColorNormal="?conversation_number_picker_text_color_normal" app:npv_TextColorNormal="@color/text"
app:npv_TextColorSelected="?conversation_number_picker_text_color_selected" app:npv_TextColorSelected="@color/text"
app:npv_ItemPaddingVertical="20dp" app:npv_ItemPaddingVertical="20dp"
app:npv_TextColorHint="@color/grey_600" app:npv_TextColorHint="@color/grey_600"
app:npv_TextSizeNormal="16sp" app:npv_TextSizeNormal="16sp"
app:npv_TextSizeSelected="16sp" app:npv_TextSizeSelected="16sp"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content"/> android:layout_height="wrap_content"/>
<TextView android:id="@+id/expiration_details"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@id/expiration_number_picker"
android:minLines="3"
android:padding="20dp"
tools:text="Your messages will not expire."/>
<TextView
android:id="@+id/expiration_details"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@id/expiration_number_picker"
android:minLines="3"
android:padding="20dp"
tools:text="Your messages will not expire."/>
</RelativeLayout> </RelativeLayout>

View File

@ -4,23 +4,18 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent"> android:layout_height="match_parent">
<FrameLayout <LinearLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="16dp"> android:padding="16dp">
<ViewStub <TextView
android:id="@+id/longmessage_sent_stub" android:textSize="@dimen/text_size"
android:id="@+id/longmessage_text"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"/>
android:layout="@layout/longmessage_bubble_sent"/>
<ViewStub </LinearLayout>
android:id="@+id/longmessage_received_stub"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout="@layout/longmessage_bubble_received"/>
</FrameLayout>
</ScrollView> </ScrollView>

View File

@ -22,15 +22,12 @@
android:src="@drawable/ic_caption_28" android:src="@drawable/ic_caption_28"
android:visibility="gone" /> android:visibility="gone" />
<ProgressBar <org.thoughtcrime.securesms.conversation.v2.utilities.ThumbnailProgressBar
android:id="@+id/thumbnail_load_indicator" android:id="@+id/thumbnail_load_indicator"
android:layout_width="32dp" android:layout_width="match_parent"
android:layout_height="32dp" android:layout_height="@dimen/accent_line_thickness"
android:layout_gravity="center" android:layout_gravity="bottom"
android:indeterminate="true"
android:visibility="gone" android:visibility="gone"
android:indeterminateTint="@android:color/white"
android:indeterminateTintMode="src_in"
tools:visibility="visible" /> tools:visibility="visible" />
<FrameLayout <FrameLayout

View File

@ -128,6 +128,7 @@
<!-- The actual record button overlay --> <!-- The actual record button overlay -->
<RelativeLayout <RelativeLayout
android:id="@+id/recordButtonOverlay"
android:layout_width="80dp" android:layout_width="80dp"
android:layout_height="80dp" android:layout_height="80dp"
android:layout_alignParentEnd="true" android:layout_alignParentEnd="true"

View File

@ -32,6 +32,13 @@
android:scaleType="centerInside" android:scaleType="centerInside"
android:src="@drawable/ic_microphone" /> android:src="@drawable/ic_microphone" />
<org.thoughtcrime.securesms.conversation.v2.utilities.ThumbnailView
android:id="@+id/quoteViewAttachmentThumbnailImageView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scaleType="centerCrop"
android:visibility="gone" />
</RelativeLayout> </RelativeLayout>
<LinearLayout <LinearLayout

View File

@ -32,11 +32,6 @@
android:id="@+id/menu_context_resend" android:id="@+id/menu_context_resend"
app:showAsAction="never" /> app:showAsAction="never" />
<item
android:title="@string/conversation_context__menu_message_details"
android:id="@+id/menu_context_details"
app:showAsAction="never" />
<item <item
android:title="@string/conversation_context__menu_ban_user" android:title="@string/conversation_context__menu_ban_user"
android:id="@+id/menu_context_ban_user" android:id="@+id/menu_context_ban_user"

View File

@ -54,7 +54,7 @@
<string name="ConversationItem_click_to_approve_unencrypted">Send failed, tap for unsecured fallback</string> <string name="ConversationItem_click_to_approve_unencrypted">Send failed, tap for unsecured fallback</string>
<string name="ConversationItem_unable_to_open_media">Can\'t find an app able to open this media.</string> <string name="ConversationItem_unable_to_open_media">Can\'t find an app able to open this media.</string>
<string name="ConversationItem_copied_text">Copied %s</string> <string name="ConversationItem_copied_text">Copied %s</string>
<string name="ConversationItem_read_more">&#160; Read More</string> <string name="ConversationItem_read_more">Read More</string>
<string name="ConversationItem_download_more">&#160; Download More</string> <string name="ConversationItem_download_more">&#160; Download More</string>
<string name="ConversationItem_pending">&#160; Pending</string> <string name="ConversationItem_pending">&#160; Pending</string>
<!-- ConversationActivity --> <!-- ConversationActivity -->

View File

@ -62,7 +62,7 @@
<string name="ConversationItem_click_to_approve_unencrypted">فشل الإرسال، انقر للرد الغير آمن</string> <string name="ConversationItem_click_to_approve_unencrypted">فشل الإرسال، انقر للرد الغير آمن</string>
<string name="ConversationItem_unable_to_open_media">لم يتم العثور على تطبيق قادر على فتح الملف.</string> <string name="ConversationItem_unable_to_open_media">لم يتم العثور على تطبيق قادر على فتح الملف.</string>
<string name="ConversationItem_copied_text">تم نسخ %s</string> <string name="ConversationItem_copied_text">تم نسخ %s</string>
<string name="ConversationItem_read_more">&#160; إقرأ المزيد</string> <string name="ConversationItem_read_more">إقرأ المزيد</string>
<string name="ConversationItem_download_more">&#160; تنزيل المزيد</string> <string name="ConversationItem_download_more">&#160; تنزيل المزيد</string>
<string name="ConversationItem_pending">&#160; في الإنتظار</string> <string name="ConversationItem_pending">&#160; في الإنتظار</string>
<!-- ConversationActivity --> <!-- ConversationActivity -->

View File

@ -58,7 +58,7 @@
<string name="ConversationItem_click_to_approve_unencrypted">Send failed, tap for unsecured fallback</string> <string name="ConversationItem_click_to_approve_unencrypted">Send failed, tap for unsecured fallback</string>
<string name="ConversationItem_unable_to_open_media">Can\'t find an app able to open this media.</string> <string name="ConversationItem_unable_to_open_media">Can\'t find an app able to open this media.</string>
<string name="ConversationItem_copied_text">Copied %s</string> <string name="ConversationItem_copied_text">Copied %s</string>
<string name="ConversationItem_read_more">&#160; Read More</string> <string name="ConversationItem_read_more">Read More</string>
<string name="ConversationItem_download_more">&#160; Download More</string> <string name="ConversationItem_download_more">&#160; Download More</string>
<string name="ConversationItem_pending">&#160; Pending</string> <string name="ConversationItem_pending">&#160; Pending</string>
<!-- ConversationActivity --> <!-- ConversationActivity -->

View File

@ -54,7 +54,7 @@
<string name="ConversationItem_click_to_approve_unencrypted">Sendado malsukcesis, tuŝeti por nesekura retropaŝo</string> <string name="ConversationItem_click_to_approve_unencrypted">Sendado malsukcesis, tuŝeti por nesekura retropaŝo</string>
<string name="ConversationItem_unable_to_open_media">Ne povas trovi aplikaĵon, kiu kapablas malfermi ĉi tiun aŭdvidaĵon.</string> <string name="ConversationItem_unable_to_open_media">Ne povas trovi aplikaĵon, kiu kapablas malfermi ĉi tiun aŭdvidaĵon.</string>
<string name="ConversationItem_copied_text">Kopiinta %s</string> <string name="ConversationItem_copied_text">Kopiinta %s</string>
<string name="ConversationItem_read_more">&#160; Legi pli</string> <string name="ConversationItem_read_more">Legi pli</string>
<string name="ConversationItem_download_more">  Elŝuti pli</string> <string name="ConversationItem_download_more">  Elŝuti pli</string>
<string name="ConversationItem_pending">  Pritraktota</string> <string name="ConversationItem_pending">  Pritraktota</string>
<!-- ConversationActivity --> <!-- ConversationActivity -->

View File

@ -56,7 +56,7 @@
<string name="ConversationItem_click_to_approve_unencrypted">Send failed, tap for unsecured fallback</string> <string name="ConversationItem_click_to_approve_unencrypted">Send failed, tap for unsecured fallback</string>
<string name="ConversationItem_unable_to_open_media">Can\'t find an app able to open this media.</string> <string name="ConversationItem_unable_to_open_media">Can\'t find an app able to open this media.</string>
<string name="ConversationItem_copied_text">Copied %s</string> <string name="ConversationItem_copied_text">Copied %s</string>
<string name="ConversationItem_read_more">&#160; Read More</string> <string name="ConversationItem_read_more">Read More</string>
<string name="ConversationItem_download_more">&#160; Download More</string> <string name="ConversationItem_download_more">&#160; Download More</string>
<string name="ConversationItem_pending">&#160; Pending</string> <string name="ConversationItem_pending">&#160; Pending</string>
<!-- ConversationActivity --> <!-- ConversationActivity -->

View File

@ -54,7 +54,7 @@
<string name="ConversationItem_click_to_approve_unencrypted">Չի ուղարկվում, սեղմեք անապահով այլընտրանքի համար</string> <string name="ConversationItem_click_to_approve_unencrypted">Չի ուղարկվում, սեղմեք անապահով այլընտրանքի համար</string>
<string name="ConversationItem_unable_to_open_media">Չհաջողվեց գտնել հավելված, որը ճանաչում է այս մեդիայի տեսակը։</string> <string name="ConversationItem_unable_to_open_media">Չհաջողվեց գտնել հավելված, որը ճանաչում է այս մեդիայի տեսակը։</string>
<string name="ConversationItem_copied_text">«%s»֊ը պատճենվել է</string> <string name="ConversationItem_copied_text">«%s»֊ը պատճենվել է</string>
<string name="ConversationItem_read_more">&#160; Տեսնել Ավելին</string> <string name="ConversationItem_read_more">Տեսնել Ավելին</string>
<string name="ConversationItem_download_more">&#160; Բեռնել Ավելին</string> <string name="ConversationItem_download_more">&#160; Բեռնել Ավելին</string>
<string name="ConversationItem_pending">&#160; Հերթագրված է</string> <string name="ConversationItem_pending">&#160; Հերթագրված է</string>
<!-- ConversationActivity --> <!-- ConversationActivity -->

View File

@ -52,7 +52,7 @@
<string name="ConversationItem_click_to_approve_unencrypted">Send failed, tap for unsecured fallback</string> <string name="ConversationItem_click_to_approve_unencrypted">Send failed, tap for unsecured fallback</string>
<string name="ConversationItem_unable_to_open_media">Can\'t find an app able to open this media.</string> <string name="ConversationItem_unable_to_open_media">Can\'t find an app able to open this media.</string>
<string name="ConversationItem_copied_text">Copied %s</string> <string name="ConversationItem_copied_text">Copied %s</string>
<string name="ConversationItem_read_more">&#160; Read More</string> <string name="ConversationItem_read_more">Read More</string>
<string name="ConversationItem_download_more">&#160; Download More</string> <string name="ConversationItem_download_more">&#160; Download More</string>
<string name="ConversationItem_pending">&#160; Pending</string> <string name="ConversationItem_pending">&#160; Pending</string>
<!-- ConversationActivity --> <!-- ConversationActivity -->

View File

@ -54,7 +54,7 @@
<string name="ConversationItem_click_to_approve_unencrypted">Send failed, tap for unsecured fallback</string> <string name="ConversationItem_click_to_approve_unencrypted">Send failed, tap for unsecured fallback</string>
<string name="ConversationItem_unable_to_open_media">Can\'t find an app able to open this media.</string> <string name="ConversationItem_unable_to_open_media">Can\'t find an app able to open this media.</string>
<string name="ConversationItem_copied_text">Copied %s</string> <string name="ConversationItem_copied_text">Copied %s</string>
<string name="ConversationItem_read_more">&#160; Read More</string> <string name="ConversationItem_read_more">Read More</string>
<string name="ConversationItem_download_more">&#160; Download More</string> <string name="ConversationItem_download_more">&#160; Download More</string>
<string name="ConversationItem_pending">&#160; Pending</string> <string name="ConversationItem_pending">&#160; Pending</string>
<!-- ConversationActivity --> <!-- ConversationActivity -->

View File

@ -52,7 +52,7 @@
<string name="ConversationItem_click_to_approve_unencrypted">Send failed, tap for unsecured fallback</string> <string name="ConversationItem_click_to_approve_unencrypted">Send failed, tap for unsecured fallback</string>
<string name="ConversationItem_unable_to_open_media">미디어를 열 수 있는 앱이 없음</string> <string name="ConversationItem_unable_to_open_media">미디어를 열 수 있는 앱이 없음</string>
<string name="ConversationItem_copied_text">복사됨 %s</string> <string name="ConversationItem_copied_text">복사됨 %s</string>
<string name="ConversationItem_read_more">&#160; Read More</string> <string name="ConversationItem_read_more">Read More</string>
<string name="ConversationItem_download_more">&#160; Download More</string> <string name="ConversationItem_download_more">&#160; Download More</string>
<string name="ConversationItem_pending">&#160; Pending</string> <string name="ConversationItem_pending">&#160; Pending</string>
<!-- ConversationActivity --> <!-- ConversationActivity -->

View File

@ -52,7 +52,7 @@
<string name="ConversationItem_click_to_approve_unencrypted">Send failed, tap for unsecured fallback</string> <string name="ConversationItem_click_to_approve_unencrypted">Send failed, tap for unsecured fallback</string>
<string name="ConversationItem_unable_to_open_media">Can\'t find an app able to open this media.</string> <string name="ConversationItem_unable_to_open_media">Can\'t find an app able to open this media.</string>
<string name="ConversationItem_copied_text">Copied %s</string> <string name="ConversationItem_copied_text">Copied %s</string>
<string name="ConversationItem_read_more">&#160; Read More</string> <string name="ConversationItem_read_more">Read More</string>
<string name="ConversationItem_download_more">&#160; Download More</string> <string name="ConversationItem_download_more">&#160; Download More</string>
<string name="ConversationItem_pending">&#160; Pending</string> <string name="ConversationItem_pending">&#160; Pending</string>
<!-- ConversationActivity --> <!-- ConversationActivity -->

View File

@ -56,7 +56,7 @@
<string name="ConversationItem_click_to_approve_unencrypted">Send failed, tap for unsecured fallback</string> <string name="ConversationItem_click_to_approve_unencrypted">Send failed, tap for unsecured fallback</string>
<string name="ConversationItem_unable_to_open_media">Can\'t find an app able to open this media.</string> <string name="ConversationItem_unable_to_open_media">Can\'t find an app able to open this media.</string>
<string name="ConversationItem_copied_text">Copied %s</string> <string name="ConversationItem_copied_text">Copied %s</string>
<string name="ConversationItem_read_more">&#160; Read More</string> <string name="ConversationItem_read_more">Read More</string>
<string name="ConversationItem_download_more">&#160; Download More</string> <string name="ConversationItem_download_more">&#160; Download More</string>
<string name="ConversationItem_pending">&#160; Pending</string> <string name="ConversationItem_pending">&#160; Pending</string>
<!-- ConversationActivity --> <!-- ConversationActivity -->

View File

@ -54,7 +54,7 @@
<string name="ConversationItem_click_to_approve_unencrypted">Send failed, tap for unsecured fallback</string> <string name="ConversationItem_click_to_approve_unencrypted">Send failed, tap for unsecured fallback</string>
<string name="ConversationItem_unable_to_open_media">ရုပ်သံ ကိုဖွင့်နိုင်သည့် app မရှိပါ။ </string> <string name="ConversationItem_unable_to_open_media">ရုပ်သံ ကိုဖွင့်နိုင်သည့် app မရှိပါ။ </string>
<string name="ConversationItem_copied_text">ကူးပြီးပါပြီ %s </string> <string name="ConversationItem_copied_text">ကူးပြီးပါပြီ %s </string>
<string name="ConversationItem_read_more">&#160; Read More</string> <string name="ConversationItem_read_more">Read More</string>
<string name="ConversationItem_download_more">&#160; Download More</string> <string name="ConversationItem_download_more">&#160; Download More</string>
<string name="ConversationItem_pending">&#160; Pending</string> <string name="ConversationItem_pending">&#160; Pending</string>
<!-- ConversationActivity --> <!-- ConversationActivity -->

View File

@ -54,7 +54,7 @@
<string name="ConversationItem_click_to_approve_unencrypted">Send failed, tap for unsecured fallback</string> <string name="ConversationItem_click_to_approve_unencrypted">Send failed, tap for unsecured fallback</string>
<string name="ConversationItem_unable_to_open_media">Can\'t find an app able to open this media.</string> <string name="ConversationItem_unable_to_open_media">Can\'t find an app able to open this media.</string>
<string name="ConversationItem_copied_text">Copied %s</string> <string name="ConversationItem_copied_text">Copied %s</string>
<string name="ConversationItem_read_more">&#160; Read More</string> <string name="ConversationItem_read_more">Read More</string>
<string name="ConversationItem_download_more">&#160; Download More</string> <string name="ConversationItem_download_more">&#160; Download More</string>
<string name="ConversationItem_pending">&#160; Pending</string> <string name="ConversationItem_pending">&#160; Pending</string>
<!-- ConversationActivity --> <!-- ConversationActivity -->

View File

@ -52,7 +52,7 @@
<string name="ConversationItem_click_to_approve_unencrypted">Send failed, tap for unsecured fallback</string> <string name="ConversationItem_click_to_approve_unencrypted">Send failed, tap for unsecured fallback</string>
<string name="ConversationItem_unable_to_open_media">Can\'t find an app able to open this media.</string> <string name="ConversationItem_unable_to_open_media">Can\'t find an app able to open this media.</string>
<string name="ConversationItem_copied_text">Copied %s</string> <string name="ConversationItem_copied_text">Copied %s</string>
<string name="ConversationItem_read_more">&#160; Read More</string> <string name="ConversationItem_read_more">Read More</string>
<string name="ConversationItem_download_more">&#160; Download More</string> <string name="ConversationItem_download_more">&#160; Download More</string>
<string name="ConversationItem_pending">&#160; Pending</string> <string name="ConversationItem_pending">&#160; Pending</string>
<!-- ConversationActivity --> <!-- ConversationActivity -->

View File

@ -54,7 +54,7 @@
<string name="ConversationItem_click_to_approve_unencrypted">Send failed, tap for unsecured fallback</string> <string name="ConversationItem_click_to_approve_unencrypted">Send failed, tap for unsecured fallback</string>
<string name="ConversationItem_unable_to_open_media">Can\'t find an app able to open this media.</string> <string name="ConversationItem_unable_to_open_media">Can\'t find an app able to open this media.</string>
<string name="ConversationItem_copied_text">Copied %s</string> <string name="ConversationItem_copied_text">Copied %s</string>
<string name="ConversationItem_read_more">&#160; Read More</string> <string name="ConversationItem_read_more">Read More</string>
<string name="ConversationItem_download_more">&#160; Download More</string> <string name="ConversationItem_download_more">&#160; Download More</string>
<string name="ConversationItem_pending">&#160; Pending</string> <string name="ConversationItem_pending">&#160; Pending</string>
<!-- ConversationActivity --> <!-- ConversationActivity -->

View File

@ -54,7 +54,7 @@
<string name="ConversationItem_click_to_approve_unencrypted">Mana rirkachu, ashtawan yachankapak munashpaka kaypi llapipay</string> <string name="ConversationItem_click_to_approve_unencrypted">Mana rirkachu, ashtawan yachankapak munashpaka kaypi llapipay</string>
<string name="ConversationItem_unable_to_open_media">Kayta paskankapakka mana aplicacionta taririnchu.</string> <string name="ConversationItem_unable_to_open_media">Kayta paskankapakka mana aplicacionta taririnchu.</string>
<string name="ConversationItem_copied_text">Chayshinayarka %s</string> <string name="ConversationItem_copied_text">Chayshinayarka %s</string>
<string name="ConversationItem_read_more">&#160; Read More</string> <string name="ConversationItem_read_more">Read More</string>
<string name="ConversationItem_download_more">&#160; Download More</string> <string name="ConversationItem_download_more">&#160; Download More</string>
<string name="ConversationItem_pending">&#160; Pending</string> <string name="ConversationItem_pending">&#160; Pending</string>
<!-- ConversationActivity --> <!-- ConversationActivity -->

View File

@ -54,7 +54,7 @@
<string name="ConversationItem_click_to_approve_unencrypted">Send failed, tap for unsecured fallback</string> <string name="ConversationItem_click_to_approve_unencrypted">Send failed, tap for unsecured fallback</string>
<string name="ConversationItem_unable_to_open_media">Can\'t find an app able to open this media.</string> <string name="ConversationItem_unable_to_open_media">Can\'t find an app able to open this media.</string>
<string name="ConversationItem_copied_text">Copied %s</string> <string name="ConversationItem_copied_text">Copied %s</string>
<string name="ConversationItem_read_more">&#160; Read More</string> <string name="ConversationItem_read_more">Read More</string>
<string name="ConversationItem_download_more">&#160; Download More</string> <string name="ConversationItem_download_more">&#160; Download More</string>
<string name="ConversationItem_pending">&#160; Pending</string> <string name="ConversationItem_pending">&#160; Pending</string>
<!-- ConversationActivity --> <!-- ConversationActivity -->

View File

@ -54,7 +54,7 @@
<string name="ConversationItem_click_to_approve_unencrypted">Send failed, tap for unsecured fallback</string> <string name="ConversationItem_click_to_approve_unencrypted">Send failed, tap for unsecured fallback</string>
<string name="ConversationItem_unable_to_open_media">Can\'t find an app able to open this media.</string> <string name="ConversationItem_unable_to_open_media">Can\'t find an app able to open this media.</string>
<string name="ConversationItem_copied_text">Copied %s</string> <string name="ConversationItem_copied_text">Copied %s</string>
<string name="ConversationItem_read_more">&#160; Read More</string> <string name="ConversationItem_read_more">Read More</string>
<string name="ConversationItem_download_more">&#160; Download More</string> <string name="ConversationItem_download_more">&#160; Download More</string>
<string name="ConversationItem_pending">&#160; Pending</string> <string name="ConversationItem_pending">&#160; Pending</string>
<!-- ConversationActivity --> <!-- ConversationActivity -->

View File

@ -54,7 +54,7 @@
<string name="ConversationItem_click_to_approve_unencrypted">Falha no envio, toque para fallback inseguro</string> <string name="ConversationItem_click_to_approve_unencrypted">Falha no envio, toque para fallback inseguro</string>
<string name="ConversationItem_unable_to_open_media">Não é possível encontrar um app para abrir esta mídia.</string> <string name="ConversationItem_unable_to_open_media">Não é possível encontrar um app para abrir esta mídia.</string>
<string name="ConversationItem_copied_text">Copiou %s</string> <string name="ConversationItem_copied_text">Copiou %s</string>
<string name="ConversationItem_read_more">&#160; Ler Mais</string> <string name="ConversationItem_read_more">Ler Mais</string>
<string name="ConversationItem_download_more">&#160; Fazer Download de Mais</string> <string name="ConversationItem_download_more">&#160; Fazer Download de Mais</string>
<string name="ConversationItem_pending">&#160; Pendendo</string> <string name="ConversationItem_pending">&#160; Pendendo</string>
<!-- ConversationActivity --> <!-- ConversationActivity -->

View File

@ -58,7 +58,7 @@
<string name="ConversationItem_click_to_approve_unencrypted">Send failed, tap for unsecured fallback</string> <string name="ConversationItem_click_to_approve_unencrypted">Send failed, tap for unsecured fallback</string>
<string name="ConversationItem_unable_to_open_media">Can\'t find an app able to open this media.</string> <string name="ConversationItem_unable_to_open_media">Can\'t find an app able to open this media.</string>
<string name="ConversationItem_copied_text">Copied %s</string> <string name="ConversationItem_copied_text">Copied %s</string>
<string name="ConversationItem_read_more">&#160; Read More</string> <string name="ConversationItem_read_more">Read More</string>
<string name="ConversationItem_download_more">&#160; Download More</string> <string name="ConversationItem_download_more">&#160; Download More</string>
<string name="ConversationItem_pending">&#160; Pending</string> <string name="ConversationItem_pending">&#160; Pending</string>
<!-- ConversationActivity --> <!-- ConversationActivity -->

View File

@ -56,7 +56,7 @@
<string name="ConversationItem_click_to_approve_unencrypted">Слање није успело, тапните за необезбеђену одступницу</string> <string name="ConversationItem_click_to_approve_unencrypted">Слање није успело, тапните за необезбеђену одступницу</string>
<string name="ConversationItem_unable_to_open_media">Нема апликације која може да отвори овај медијум.</string> <string name="ConversationItem_unable_to_open_media">Нема апликације која може да отвори овај медијум.</string>
<string name="ConversationItem_copied_text">Копирана %s</string> <string name="ConversationItem_copied_text">Копирана %s</string>
<string name="ConversationItem_read_more">&#160; Read More</string> <string name="ConversationItem_read_more">Read More</string>
<string name="ConversationItem_download_more">&#160; Download More</string> <string name="ConversationItem_download_more">&#160; Download More</string>
<string name="ConversationItem_pending">&#160; Pending</string> <string name="ConversationItem_pending">&#160; Pending</string>
<!-- ConversationActivity --> <!-- ConversationActivity -->

View File

@ -54,7 +54,7 @@
<string name="ConversationItem_click_to_approve_unencrypted">imefeli kutuma, gusa kwa isiyo salama ili kuanguka nyuma</string> <string name="ConversationItem_click_to_approve_unencrypted">imefeli kutuma, gusa kwa isiyo salama ili kuanguka nyuma</string>
<string name="ConversationItem_unable_to_open_media">Haiwezi kupata programu inayoweza kufungua media hii.</string> <string name="ConversationItem_unable_to_open_media">Haiwezi kupata programu inayoweza kufungua media hii.</string>
<string name="ConversationItem_copied_text">nakala 1%s</string> <string name="ConversationItem_copied_text">nakala 1%s</string>
<string name="ConversationItem_read_more">&#160; Read More</string> <string name="ConversationItem_read_more">Read More</string>
<string name="ConversationItem_download_more">&#160; Download More</string> <string name="ConversationItem_download_more">&#160; Download More</string>
<string name="ConversationItem_pending">&#160; Pending</string> <string name="ConversationItem_pending">&#160; Pending</string>
<!-- ConversationActivity --> <!-- ConversationActivity -->

View File

@ -54,7 +54,7 @@
<string name="ConversationItem_click_to_approve_unencrypted">Send failed, tap for unsecured fallback</string> <string name="ConversationItem_click_to_approve_unencrypted">Send failed, tap for unsecured fallback</string>
<string name="ConversationItem_unable_to_open_media">Can\'t find an app able to open this media.</string> <string name="ConversationItem_unable_to_open_media">Can\'t find an app able to open this media.</string>
<string name="ConversationItem_copied_text">Copied %s</string> <string name="ConversationItem_copied_text">Copied %s</string>
<string name="ConversationItem_read_more">&#160; Read More</string> <string name="ConversationItem_read_more">Read More</string>
<string name="ConversationItem_download_more">&#160; Download More</string> <string name="ConversationItem_download_more">&#160; Download More</string>
<string name="ConversationItem_pending">&#160; Pending</string> <string name="ConversationItem_pending">&#160; Pending</string>
<!-- ConversationActivity --> <!-- ConversationActivity -->

View File

@ -54,7 +54,7 @@
<string name="ConversationItem_click_to_approve_unencrypted">పంపడం విఫలమైంది, అసురక్షిత తిరిగి పొందడం కోసం నొక్కండి</string> <string name="ConversationItem_click_to_approve_unencrypted">పంపడం విఫలమైంది, అసురక్షిత తిరిగి పొందడం కోసం నొక్కండి</string>
<string name="ConversationItem_unable_to_open_media">మీడియా ఎంచుకోవడానికి అనువర్తనం దొరకదు.</string> <string name="ConversationItem_unable_to_open_media">మీడియా ఎంచుకోవడానికి అనువర్తనం దొరకదు.</string>
<string name="ConversationItem_copied_text">ప్రతి తీసుకోబడింది %s</string> <string name="ConversationItem_copied_text">ప్రతి తీసుకోబడింది %s</string>
<string name="ConversationItem_read_more">&#160; Read More</string> <string name="ConversationItem_read_more">Read More</string>
<string name="ConversationItem_download_more">&#160; Download More</string> <string name="ConversationItem_download_more">&#160; Download More</string>
<string name="ConversationItem_pending">&#160; Pending</string> <string name="ConversationItem_pending">&#160; Pending</string>
<!-- ConversationActivity --> <!-- ConversationActivity -->

View File

@ -52,7 +52,7 @@
<string name="ConversationItem_click_to_approve_unencrypted">การส่งล้มเหลว แตะเพื่อกลับไปใช้วิธีที่ไม่ปลอดภัยแทน</string> <string name="ConversationItem_click_to_approve_unencrypted">การส่งล้มเหลว แตะเพื่อกลับไปใช้วิธีที่ไม่ปลอดภัยแทน</string>
<string name="ConversationItem_unable_to_open_media">ไม่พบแอปที่สามารถเปิดสื่อนี้</string> <string name="ConversationItem_unable_to_open_media">ไม่พบแอปที่สามารถเปิดสื่อนี้</string>
<string name="ConversationItem_copied_text">คัดลอกแล้ว %s</string> <string name="ConversationItem_copied_text">คัดลอกแล้ว %s</string>
<string name="ConversationItem_read_more">&#160; Read More</string> <string name="ConversationItem_read_more">Read More</string>
<string name="ConversationItem_download_more">&#160; Download More</string> <string name="ConversationItem_download_more">&#160; Download More</string>
<string name="ConversationItem_pending">&#160; Pending</string> <string name="ConversationItem_pending">&#160; Pending</string>
<!-- ConversationActivity --> <!-- ConversationActivity -->

View File

@ -54,7 +54,7 @@
<string name="ConversationItem_click_to_approve_unencrypted">Send failed, tap for unsecured fallback</string> <string name="ConversationItem_click_to_approve_unencrypted">Send failed, tap for unsecured fallback</string>
<string name="ConversationItem_unable_to_open_media">Can\'t find an app able to open this media.</string> <string name="ConversationItem_unable_to_open_media">Can\'t find an app able to open this media.</string>
<string name="ConversationItem_copied_text">Copied %s</string> <string name="ConversationItem_copied_text">Copied %s</string>
<string name="ConversationItem_read_more">&#160; Read More</string> <string name="ConversationItem_read_more">Read More</string>
<string name="ConversationItem_download_more">&#160; Download More</string> <string name="ConversationItem_download_more">&#160; Download More</string>
<string name="ConversationItem_pending">&#160; Pending</string> <string name="ConversationItem_pending">&#160; Pending</string>
<!-- ConversationActivity --> <!-- ConversationActivity -->

View File

@ -52,7 +52,7 @@
<string name="ConversationItem_click_to_approve_unencrypted">Send failed, tap for unsecured fallback</string> <string name="ConversationItem_click_to_approve_unencrypted">Send failed, tap for unsecured fallback</string>
<string name="ConversationItem_unable_to_open_media">Không tìm thấy ứng dụng để mở dữ liệu truyền thông này.</string> <string name="ConversationItem_unable_to_open_media">Không tìm thấy ứng dụng để mở dữ liệu truyền thông này.</string>
<string name="ConversationItem_copied_text">Đã sao chép %s</string> <string name="ConversationItem_copied_text">Đã sao chép %s</string>
<string name="ConversationItem_read_more">&#160; Read More</string> <string name="ConversationItem_read_more">Read More</string>
<string name="ConversationItem_download_more">&#160; Tải thêm</string> <string name="ConversationItem_download_more">&#160; Tải thêm</string>
<string name="ConversationItem_pending">&#160; Đang chờ</string> <string name="ConversationItem_pending">&#160; Đang chờ</string>
<!-- ConversationActivity --> <!-- ConversationActivity -->

View File

@ -54,7 +54,7 @@
<string name="ConversationItem_click_to_approve_unencrypted">Send failed, tap for unsecured fallback</string> <string name="ConversationItem_click_to_approve_unencrypted">Send failed, tap for unsecured fallback</string>
<string name="ConversationItem_unable_to_open_media">tosobodde kufuna app esobola kugulawo kiwandiiko kyo</string> <string name="ConversationItem_unable_to_open_media">tosobodde kufuna app esobola kugulawo kiwandiiko kyo</string>
<string name="ConversationItem_copied_text">Copied %s</string> <string name="ConversationItem_copied_text">Copied %s</string>
<string name="ConversationItem_read_more">&#160; Read More</string> <string name="ConversationItem_read_more">Read More</string>
<string name="ConversationItem_download_more">&#160; Download More</string> <string name="ConversationItem_download_more">&#160; Download More</string>
<string name="ConversationItem_pending">&#160; Pending</string> <string name="ConversationItem_pending">&#160; Pending</string>
<!-- ConversationActivity --> <!-- ConversationActivity -->

View File

@ -64,7 +64,7 @@
<string name="ConversationItem_click_to_approve_unencrypted">Send failed, tap for unsecured fallback</string> <string name="ConversationItem_click_to_approve_unencrypted">Send failed, tap for unsecured fallback</string>
<string name="ConversationItem_unable_to_open_media">Can\'t find an app able to open this media.</string> <string name="ConversationItem_unable_to_open_media">Can\'t find an app able to open this media.</string>
<string name="ConversationItem_copied_text">Copied %s</string> <string name="ConversationItem_copied_text">Copied %s</string>
<string name="ConversationItem_read_more">&#160; Read More</string> <string name="ConversationItem_read_more">Read More</string>
<string name="ConversationItem_download_more">&#160; Download More</string> <string name="ConversationItem_download_more">&#160; Download More</string>
<string name="ConversationItem_pending">&#160; Pending</string> <string name="ConversationItem_pending">&#160; Pending</string>
@ -872,4 +872,6 @@
<string name="dialog_download_button_title">Download</string> <string name="dialog_download_button_title">Download</string>
<string name="activity_conversation_blocked_banner_text">%s is blocked. Unblock them?</string> <string name="activity_conversation_blocked_banner_text">%s is blocked. Unblock them?</string>
<string name="activity_conversation_attachment_prep_failed">Failed to prepare attachment for sending.</string>
</resources> </resources>

View File

@ -23,7 +23,7 @@ class AttachmentDownloadJob(val attachmentID: Long, val databaseMessageID: Long)
} }
// Settings // Settings
override val maxFailureCount: Int = 20 override val maxFailureCount: Int = 100
companion object { companion object {
val KEY: String = "AttachmentDownloadJob" val KEY: String = "AttachmentDownloadJob"

View File

@ -34,7 +34,7 @@ class DataExtractionNotification() : ControlMessage() {
} }
} }
internal constructor(kind: Kind) : this() { constructor(kind: Kind) : this() {
this.kind = kind this.kind = kind
} }

View File

@ -33,7 +33,7 @@ class ExpirationTimerUpdate() : ControlMessage() {
} }
} }
internal constructor(duration: Int) : this() { constructor(duration: Int) : this() {
this.syncTarget = null this.syncTarget = null
this.duration = duration this.duration = duration
} }

View File

@ -779,4 +779,12 @@ object TextSecurePreferences {
fun setLastOpenDate(context: Context) { fun setLastOpenDate(context: Context) {
setLongPreference(context, LAST_OPEN_DATE, System.currentTimeMillis()) setLongPreference(context, LAST_OPEN_DATE, System.currentTimeMillis())
} }
fun hasSeenLinkPreviewSuggestionDialog(context: Context): Boolean {
return getBooleanPreference(context, "has_seen_link_preview_suggestion_dialog", false)
}
fun setHasSeenLinkPreviewSuggestionDialog(context: Context) {
setBooleanPreference(context, "has_seen_link_preview_suggestion_dialog", true)
}
} }