mirror of
https://github.com/oxen-io/session-android.git
synced 2025-02-20 06:48:27 +00:00
Merge branch 'ui' of https://github.com/oxen-io/session-android into ui
This commit is contained in:
commit
64a70d0555
@ -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,
|
||||||
|
@ -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
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
|
@ -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);
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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 {
|
||||||
|
@ -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
|
||||||
|
@ -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) }
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -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()
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -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()
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -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()
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -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()
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -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()
|
||||||
}
|
}
|
@ -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()
|
||||||
}
|
}
|
||||||
|
@ -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>)
|
||||||
}
|
}
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
@ -37,7 +37,7 @@ class ControlMessageView : LinearLayout {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun recycle() {
|
fun recycle() {
|
||||||
// TODO: Implement
|
|
||||||
}
|
}
|
||||||
// endregion
|
// endregion
|
||||||
}
|
}
|
@ -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
|
||||||
}
|
}
|
@ -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
|
||||||
}
|
}
|
@ -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")
|
||||||
|
}
|
||||||
}
|
}
|
@ -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
|
||||||
|
@ -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 {
|
||||||
|
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -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
|
||||||
|
|
||||||
|
}
|
@ -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
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -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()
|
||||||
|
}
|
||||||
|
}
|
@ -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;
|
||||||
|
@ -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?)
|
||||||
}
|
}
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -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) {
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
19
app/src/main/res/layout/album_thumbnail_1.xml
Normal file
19
app/src/main/res/layout/album_thumbnail_1.xml
Normal 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>
|
@ -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"
|
||||||
|
@ -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"
|
||||||
|
@ -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"
|
||||||
|
@ -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"
|
||||||
|
@ -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" />
|
||||||
|
@ -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>
|
@ -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>
|
@ -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>
|
@ -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
|
||||||
|
@ -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"
|
||||||
|
@ -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
|
||||||
|
@ -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"
|
||||||
|
@ -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">  Read More</string>
|
<string name="ConversationItem_read_more">Read More</string>
|
||||||
<string name="ConversationItem_download_more">  Download More</string>
|
<string name="ConversationItem_download_more">  Download More</string>
|
||||||
<string name="ConversationItem_pending">  Pending</string>
|
<string name="ConversationItem_pending">  Pending</string>
|
||||||
<!-- ConversationActivity -->
|
<!-- ConversationActivity -->
|
||||||
|
@ -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">  إقرأ المزيد</string>
|
<string name="ConversationItem_read_more">إقرأ المزيد</string>
|
||||||
<string name="ConversationItem_download_more">  تنزيل المزيد</string>
|
<string name="ConversationItem_download_more">  تنزيل المزيد</string>
|
||||||
<string name="ConversationItem_pending">  في الإنتظار</string>
|
<string name="ConversationItem_pending">  في الإنتظار</string>
|
||||||
<!-- ConversationActivity -->
|
<!-- ConversationActivity -->
|
||||||
|
@ -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">  Read More</string>
|
<string name="ConversationItem_read_more">Read More</string>
|
||||||
<string name="ConversationItem_download_more">  Download More</string>
|
<string name="ConversationItem_download_more">  Download More</string>
|
||||||
<string name="ConversationItem_pending">  Pending</string>
|
<string name="ConversationItem_pending">  Pending</string>
|
||||||
<!-- ConversationActivity -->
|
<!-- ConversationActivity -->
|
||||||
|
@ -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">  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 -->
|
||||||
|
@ -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">  Read More</string>
|
<string name="ConversationItem_read_more">Read More</string>
|
||||||
<string name="ConversationItem_download_more">  Download More</string>
|
<string name="ConversationItem_download_more">  Download More</string>
|
||||||
<string name="ConversationItem_pending">  Pending</string>
|
<string name="ConversationItem_pending">  Pending</string>
|
||||||
<!-- ConversationActivity -->
|
<!-- ConversationActivity -->
|
||||||
|
@ -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">  Տեսնել Ավելին</string>
|
<string name="ConversationItem_read_more">Տեսնել Ավելին</string>
|
||||||
<string name="ConversationItem_download_more">  Բեռնել Ավելին</string>
|
<string name="ConversationItem_download_more">  Բեռնել Ավելին</string>
|
||||||
<string name="ConversationItem_pending">  Հերթագրված է</string>
|
<string name="ConversationItem_pending">  Հերթագրված է</string>
|
||||||
<!-- ConversationActivity -->
|
<!-- ConversationActivity -->
|
||||||
|
@ -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">  Read More</string>
|
<string name="ConversationItem_read_more">Read More</string>
|
||||||
<string name="ConversationItem_download_more">  Download More</string>
|
<string name="ConversationItem_download_more">  Download More</string>
|
||||||
<string name="ConversationItem_pending">  Pending</string>
|
<string name="ConversationItem_pending">  Pending</string>
|
||||||
<!-- ConversationActivity -->
|
<!-- ConversationActivity -->
|
||||||
|
@ -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">  Read More</string>
|
<string name="ConversationItem_read_more">Read More</string>
|
||||||
<string name="ConversationItem_download_more">  Download More</string>
|
<string name="ConversationItem_download_more">  Download More</string>
|
||||||
<string name="ConversationItem_pending">  Pending</string>
|
<string name="ConversationItem_pending">  Pending</string>
|
||||||
<!-- ConversationActivity -->
|
<!-- ConversationActivity -->
|
||||||
|
@ -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">  Read More</string>
|
<string name="ConversationItem_read_more">Read More</string>
|
||||||
<string name="ConversationItem_download_more">  Download More</string>
|
<string name="ConversationItem_download_more">  Download More</string>
|
||||||
<string name="ConversationItem_pending">  Pending</string>
|
<string name="ConversationItem_pending">  Pending</string>
|
||||||
<!-- ConversationActivity -->
|
<!-- ConversationActivity -->
|
||||||
|
@ -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">  Read More</string>
|
<string name="ConversationItem_read_more">Read More</string>
|
||||||
<string name="ConversationItem_download_more">  Download More</string>
|
<string name="ConversationItem_download_more">  Download More</string>
|
||||||
<string name="ConversationItem_pending">  Pending</string>
|
<string name="ConversationItem_pending">  Pending</string>
|
||||||
<!-- ConversationActivity -->
|
<!-- ConversationActivity -->
|
||||||
|
@ -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">  Read More</string>
|
<string name="ConversationItem_read_more">Read More</string>
|
||||||
<string name="ConversationItem_download_more">  Download More</string>
|
<string name="ConversationItem_download_more">  Download More</string>
|
||||||
<string name="ConversationItem_pending">  Pending</string>
|
<string name="ConversationItem_pending">  Pending</string>
|
||||||
<!-- ConversationActivity -->
|
<!-- ConversationActivity -->
|
||||||
|
@ -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">  Read More</string>
|
<string name="ConversationItem_read_more">Read More</string>
|
||||||
<string name="ConversationItem_download_more">  Download More</string>
|
<string name="ConversationItem_download_more">  Download More</string>
|
||||||
<string name="ConversationItem_pending">  Pending</string>
|
<string name="ConversationItem_pending">  Pending</string>
|
||||||
<!-- ConversationActivity -->
|
<!-- ConversationActivity -->
|
||||||
|
@ -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">  Read More</string>
|
<string name="ConversationItem_read_more">Read More</string>
|
||||||
<string name="ConversationItem_download_more">  Download More</string>
|
<string name="ConversationItem_download_more">  Download More</string>
|
||||||
<string name="ConversationItem_pending">  Pending</string>
|
<string name="ConversationItem_pending">  Pending</string>
|
||||||
<!-- ConversationActivity -->
|
<!-- ConversationActivity -->
|
||||||
|
@ -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">  Read More</string>
|
<string name="ConversationItem_read_more">Read More</string>
|
||||||
<string name="ConversationItem_download_more">  Download More</string>
|
<string name="ConversationItem_download_more">  Download More</string>
|
||||||
<string name="ConversationItem_pending">  Pending</string>
|
<string name="ConversationItem_pending">  Pending</string>
|
||||||
<!-- ConversationActivity -->
|
<!-- ConversationActivity -->
|
||||||
|
@ -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">  Read More</string>
|
<string name="ConversationItem_read_more">Read More</string>
|
||||||
<string name="ConversationItem_download_more">  Download More</string>
|
<string name="ConversationItem_download_more">  Download More</string>
|
||||||
<string name="ConversationItem_pending">  Pending</string>
|
<string name="ConversationItem_pending">  Pending</string>
|
||||||
<!-- ConversationActivity -->
|
<!-- ConversationActivity -->
|
||||||
|
@ -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">  Read More</string>
|
<string name="ConversationItem_read_more">Read More</string>
|
||||||
<string name="ConversationItem_download_more">  Download More</string>
|
<string name="ConversationItem_download_more">  Download More</string>
|
||||||
<string name="ConversationItem_pending">  Pending</string>
|
<string name="ConversationItem_pending">  Pending</string>
|
||||||
<!-- ConversationActivity -->
|
<!-- ConversationActivity -->
|
||||||
|
@ -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">  Read More</string>
|
<string name="ConversationItem_read_more">Read More</string>
|
||||||
<string name="ConversationItem_download_more">  Download More</string>
|
<string name="ConversationItem_download_more">  Download More</string>
|
||||||
<string name="ConversationItem_pending">  Pending</string>
|
<string name="ConversationItem_pending">  Pending</string>
|
||||||
<!-- ConversationActivity -->
|
<!-- ConversationActivity -->
|
||||||
|
@ -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">  Read More</string>
|
<string name="ConversationItem_read_more">Read More</string>
|
||||||
<string name="ConversationItem_download_more">  Download More</string>
|
<string name="ConversationItem_download_more">  Download More</string>
|
||||||
<string name="ConversationItem_pending">  Pending</string>
|
<string name="ConversationItem_pending">  Pending</string>
|
||||||
<!-- ConversationActivity -->
|
<!-- ConversationActivity -->
|
||||||
|
@ -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">  Ler Mais</string>
|
<string name="ConversationItem_read_more">Ler Mais</string>
|
||||||
<string name="ConversationItem_download_more">  Fazer Download de Mais</string>
|
<string name="ConversationItem_download_more">  Fazer Download de Mais</string>
|
||||||
<string name="ConversationItem_pending">  Pendendo</string>
|
<string name="ConversationItem_pending">  Pendendo</string>
|
||||||
<!-- ConversationActivity -->
|
<!-- ConversationActivity -->
|
||||||
|
@ -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">  Read More</string>
|
<string name="ConversationItem_read_more">Read More</string>
|
||||||
<string name="ConversationItem_download_more">  Download More</string>
|
<string name="ConversationItem_download_more">  Download More</string>
|
||||||
<string name="ConversationItem_pending">  Pending</string>
|
<string name="ConversationItem_pending">  Pending</string>
|
||||||
<!-- ConversationActivity -->
|
<!-- ConversationActivity -->
|
||||||
|
@ -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">  Read More</string>
|
<string name="ConversationItem_read_more">Read More</string>
|
||||||
<string name="ConversationItem_download_more">  Download More</string>
|
<string name="ConversationItem_download_more">  Download More</string>
|
||||||
<string name="ConversationItem_pending">  Pending</string>
|
<string name="ConversationItem_pending">  Pending</string>
|
||||||
<!-- ConversationActivity -->
|
<!-- ConversationActivity -->
|
||||||
|
@ -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">  Read More</string>
|
<string name="ConversationItem_read_more">Read More</string>
|
||||||
<string name="ConversationItem_download_more">  Download More</string>
|
<string name="ConversationItem_download_more">  Download More</string>
|
||||||
<string name="ConversationItem_pending">  Pending</string>
|
<string name="ConversationItem_pending">  Pending</string>
|
||||||
<!-- ConversationActivity -->
|
<!-- ConversationActivity -->
|
||||||
|
@ -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">  Read More</string>
|
<string name="ConversationItem_read_more">Read More</string>
|
||||||
<string name="ConversationItem_download_more">  Download More</string>
|
<string name="ConversationItem_download_more">  Download More</string>
|
||||||
<string name="ConversationItem_pending">  Pending</string>
|
<string name="ConversationItem_pending">  Pending</string>
|
||||||
<!-- ConversationActivity -->
|
<!-- ConversationActivity -->
|
||||||
|
@ -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">  Read More</string>
|
<string name="ConversationItem_read_more">Read More</string>
|
||||||
<string name="ConversationItem_download_more">  Download More</string>
|
<string name="ConversationItem_download_more">  Download More</string>
|
||||||
<string name="ConversationItem_pending">  Pending</string>
|
<string name="ConversationItem_pending">  Pending</string>
|
||||||
<!-- ConversationActivity -->
|
<!-- ConversationActivity -->
|
||||||
|
@ -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">  Read More</string>
|
<string name="ConversationItem_read_more">Read More</string>
|
||||||
<string name="ConversationItem_download_more">  Download More</string>
|
<string name="ConversationItem_download_more">  Download More</string>
|
||||||
<string name="ConversationItem_pending">  Pending</string>
|
<string name="ConversationItem_pending">  Pending</string>
|
||||||
<!-- ConversationActivity -->
|
<!-- ConversationActivity -->
|
||||||
|
@ -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">  Read More</string>
|
<string name="ConversationItem_read_more">Read More</string>
|
||||||
<string name="ConversationItem_download_more">  Download More</string>
|
<string name="ConversationItem_download_more">  Download More</string>
|
||||||
<string name="ConversationItem_pending">  Pending</string>
|
<string name="ConversationItem_pending">  Pending</string>
|
||||||
<!-- ConversationActivity -->
|
<!-- ConversationActivity -->
|
||||||
|
@ -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">  Read More</string>
|
<string name="ConversationItem_read_more">Read More</string>
|
||||||
<string name="ConversationItem_download_more">  Tải thêm</string>
|
<string name="ConversationItem_download_more">  Tải thêm</string>
|
||||||
<string name="ConversationItem_pending">  Đang chờ</string>
|
<string name="ConversationItem_pending">  Đang chờ</string>
|
||||||
<!-- ConversationActivity -->
|
<!-- ConversationActivity -->
|
||||||
|
@ -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">  Read More</string>
|
<string name="ConversationItem_read_more">Read More</string>
|
||||||
<string name="ConversationItem_download_more">  Download More</string>
|
<string name="ConversationItem_download_more">  Download More</string>
|
||||||
<string name="ConversationItem_pending">  Pending</string>
|
<string name="ConversationItem_pending">  Pending</string>
|
||||||
<!-- ConversationActivity -->
|
<!-- ConversationActivity -->
|
||||||
|
@ -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">  Read More</string>
|
<string name="ConversationItem_read_more">Read More</string>
|
||||||
<string name="ConversationItem_download_more">  Download More</string>
|
<string name="ConversationItem_download_more">  Download More</string>
|
||||||
<string name="ConversationItem_pending">  Pending</string>
|
<string name="ConversationItem_pending">  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>
|
||||||
|
@ -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"
|
||||||
|
@ -34,7 +34,7 @@ class DataExtractionNotification() : ControlMessage() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
internal constructor(kind: Kind) : this() {
|
constructor(kind: Kind) : this() {
|
||||||
this.kind = kind
|
this.kind = kind
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
@ -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)
|
||||||
|
}
|
||||||
}
|
}
|
Loading…
x
Reference in New Issue
Block a user