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

View File

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

View File

@ -64,10 +64,12 @@ import org.session.libsignal.utilities.Log;
import org.thoughtcrime.securesms.components.MediaView;
import org.thoughtcrime.securesms.database.MediaDatabase.MediaRecord;
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.MediaRailAdapter;
import org.thoughtcrime.securesms.mms.GlideApp;
import org.thoughtcrime.securesms.mms.GlideRequests;
import org.thoughtcrime.securesms.mms.Slide;
import org.thoughtcrime.securesms.permissions.Permissions;
import org.thoughtcrime.securesms.util.AttachmentUtil;
import org.thoughtcrime.securesms.util.DateUtils;
@ -116,6 +118,22 @@ public class MediaPreviewActivity extends PassphraseRequiredActionBarActivity im
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")
@Override

View File

@ -23,6 +23,7 @@ import com.google.android.exoplayer2.DefaultRenderersFactory;
import com.google.android.exoplayer2.ExoPlaybackException;
import com.google.android.exoplayer2.ExoPlayerFactory;
import com.google.android.exoplayer2.LoadControl;
import com.google.android.exoplayer2.PlaybackParameters;
import com.google.android.exoplayer2.Player;
import com.google.android.exoplayer2.SimpleExoPlayer;
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 {
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.audioAttachmentServer = new AttachmentServer(context, slide.asAttachment());
this.startTime = System.currentTimeMillis();
@ -184,8 +185,6 @@ public class AudioSlidePlayer implements SensorEventListener {
public void onPlayerError(ExoPlaybackException error) {
Log.w(TAG, "MediaPlayer Error: " + error);
Toast.makeText(context, R.string.AudioSlidePlayer_error_playing_audio, Toast.LENGTH_SHORT).show();
synchronized (AudioSlidePlayer.this) {
mediaPlayer = null;
@ -267,8 +266,17 @@ public class AudioSlidePlayer implements SensorEventListener {
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) {
return new Pair<>(0D, 0);
} 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() {
Util.runOnMain(() -> getListener().onPlayerStart(AudioSlidePlayer.this));
}
@ -383,7 +401,7 @@ public class AudioSlidePlayer implements SensorEventListener {
return;
}
Pair<Double, Integer> progress = player.getProgress();
Pair<Double, Integer> progress = player.getProgressTuple();
player.notifyOnProgress(progress.first, progress.second);
sendEmptyMessageDelayed(0, 50);
}

View File

@ -1,27 +1,27 @@
package org.thoughtcrime.securesms.components;
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.view.ViewGroup;
import android.widget.FrameLayout;
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.Slide;
import org.thoughtcrime.securesms.mms.SlideClickListener;
import org.thoughtcrime.securesms.mms.SlidesClickedListener;
import org.session.libsession.utilities.Stub;
import java.util.List;
import network.loki.messenger.R;
public class AlbumThumbnailView extends FrameLayout {
private @Nullable SlideClickListener thumbnailClickListener;
@ -53,8 +53,8 @@ public class AlbumThumbnailView extends FrameLayout {
private void initialize() {
inflate(getContext(), R.layout.album_thumbnail_view, this);
albumCellContainer = findViewById(R.id.album_cell_container);
transferControls = new Stub<>(findViewById(R.id.album_transfer_controls_stub));
albumCellContainer = findViewById(R.id.albumCellContainer);
transferControls = new Stub<>(findViewById(R.id.albumTransferControlsStub));
}
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) {
ThumbnailView cell = findViewById(id);
cell.setImageResource(glideRequests, slide, false, false);
cell.setLoadIndicatorVisibile(slide.isInProgress());
KThumbnailView cell = findViewById(id);
cell.setImageResource(glideRequests, slide, false);
cell.setThumbnailClickListener(defaultThumbnailClickListener);
cell.setOnLongClickListener(defaultLongClickListener);
}

View File

@ -65,15 +65,10 @@ public class ConversationItemThumbnail extends FrameLayout {
if (attrs != null) {
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();
}
}
@SuppressWarnings("SuspiciousNameCombination")
@Override
protected void dispatchDraw(Canvas canvas) {
super.dispatchDraw(canvas);

View File

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

View File

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

View File

@ -1,17 +1,27 @@
package org.thoughtcrime.securesms.conversation.v2
import android.Manifest
import android.animation.FloatEvaluator
import android.animation.ValueAnimator
import android.content.Context
import android.content.ClipData
import android.content.ClipboardManager
import android.content.Intent
import android.content.res.Resources
import android.database.Cursor
import android.graphics.Rect
import android.graphics.Typeface
import android.os.Bundle
import android.net.Uri
import android.os.*
import android.text.TextUtils
import android.util.Log
import android.util.Pair
import android.util.TypedValue
import android.view.*
import android.widget.RelativeLayout
import android.widget.Toast
import androidx.appcompat.app.AlertDialog
import androidx.core.view.isVisible
import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelProvider
@ -20,6 +30,7 @@ import androidx.loader.app.LoaderManager
import androidx.loader.content.Loader
import androidx.recyclerview.widget.LinearLayoutManager
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.view.*
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.view.*
import network.loki.messenger.R
import nl.komponents.kovenant.ui.failUi
import nl.komponents.kovenant.ui.successUi
import org.session.libsession.messaging.MessagingModuleConfiguration
import org.session.libsession.messaging.contacts.Contact
import org.session.libsession.messaging.mentions.Mention
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.visible.OpenGroupInvitation
import org.session.libsession.messaging.messages.visible.VisibleMessage
import org.session.libsession.messaging.open_groups.OpenGroupAPIV2
import org.session.libsession.messaging.sending_receiving.MessageSender
import org.session.libsession.utilities.Address
import org.session.libsession.utilities.TextSecurePreferences
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.PassphraseRequiredActionBarActivity
import org.thoughtcrime.securesms.audio.AudioRecorder
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.input_bar.InputBarButton
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.mentions.MentionCandidatesView
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.messages.VisibleMessageView
import org.thoughtcrime.securesms.conversation.v2.search.SearchBottomBar
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.DraftDatabase
import org.thoughtcrime.securesms.database.DraftDatabase.Drafts
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.linkpreview.LinkPreviewRepository
import org.thoughtcrime.securesms.linkpreview.LinkPreviewUtil
import org.thoughtcrime.securesms.linkpreview.LinkPreviewViewModel
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.mediasend.Media
import org.thoughtcrime.securesms.mediasend.MediaSendActivity
import org.thoughtcrime.securesms.mms.*
import org.thoughtcrime.securesms.notifications.MarkReadReceiver
import org.thoughtcrime.securesms.permissions.Permissions
import org.thoughtcrime.securesms.util.DateUtils
import org.thoughtcrime.securesms.util.MediaUtil
import org.thoughtcrime.securesms.util.SaveAttachmentTask
import org.w3c.dom.Text
import java.util.*
import java.util.concurrent.ExecutionException
import kotlin.math.*
// 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.
class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDelegate,
InputBarRecordingViewDelegate, AttachmentManager.AttachmentListener, SearchBottomBar.EventListener {
InputBarRecordingViewDelegate, AttachmentManager.AttachmentListener, ActivityDispatcher,
ConversationActionModeCallbackDelegate, SearchBottomBar.EventListener {
private val screenWidth = Resources.getSystem().displayMetrics.widthPixels
private var searchViewModel: SearchViewModel? = null
private var linkPreviewViewModel: LinkPreviewViewModel? = null
@ -85,6 +124,9 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
private var actionMode: ActionMode? = null
private var unreadCount = 0
// 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 var isLockViewExpanded = false
private var isShowingAttachmentOptions = false
@ -98,16 +140,13 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
private val layoutManager: LinearLayoutManager
get() { return conversationRecyclerView.layoutManager as LinearLayoutManager }
// TODO: Selected message background color
// TODO: Overflow menu background + text color
private val adapter by lazy {
val cursor = DatabaseFactory.getMmsSmsDatabase(this).getConversation(threadID)
val adapter = ConversationAdapter(
this,
cursor,
onItemPress = { message, position, view ->
handlePress(message, position, view)
onItemPress = { message, position, view, rawRect ->
handlePress(message, position, view, rawRect)
},
onItemSwipeToReply = { message, position ->
handleSwipeToReply(message, position)
@ -138,6 +177,7 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
const val TAKE_PHOTO = 7
const val PICK_GIF = 10
const val PICK_FROM_LIBRARY = 12
const val INVITE_CONTACTS = 124
}
// endregion
@ -165,6 +205,28 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
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() {
conversationRecyclerView.adapter = adapter
val layoutManager = LinearLayoutManager(this, LinearLayoutManager.VERTICAL, true)
@ -351,8 +413,19 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
}
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)
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) {
@ -536,16 +609,16 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
// region Interaction
override fun onOptionsItemSelected(item: MenuItem): Boolean {
// TODO: Implement
return super.onOptionsItemSelected(item)
return ConversationMenuHelper.onOptionItemSelected(this, item, thread)
}
// `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
if (actionMode != null) {
adapter.toggleSelection(message, position)
val actionModeCallback = ConversationActionModeCallback(adapter, threadID, this)
actionModeCallback.delegate = this
actionModeCallback.updateActionModeMenu(actionMode.menu)
if (adapter.selectedItems.isEmpty()) {
actionMode.finish()
@ -556,19 +629,20 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
// 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
// onClickListeners directly to message content views.
view.onContentClick()
view.onContentClick(rawRect)
}
}
// `position` is the adapter position; not the visual position
private fun handleSwipeToReply(message: MessageRecord, position: Int) {
inputBar.draftQuote(message)
inputBar.draftQuote(message, glide)
}
// `position` is the adapter position; not the visual position
private fun handleLongPress(message: MessageRecord, position: Int) {
val actionMode = this.actionMode
val actionModeCallback = ConversationActionModeCallback(adapter, threadID, this)
actionModeCallback.delegate = this
if (actionMode == null) { // Nothing should be selected if this is the case
adapter.toggleSelection(message, position)
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) {
if (isValidLockViewLocation(event.rawX.roundToInt(), event.rawY.roundToInt())) {
val x = event.rawX.roundToInt()
val y = event.rawY.roundToInt()
if (isValidLockViewLocation(x, y)) {
inputBarRecordingView.lock()
} 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() {
// TODO: Implement
if (!thread.isContactRecipient) { return }
DatabaseFactory.getRecipientDatabase(this).setBlocked(thread, false)
}
private fun handleMentionSelected(mention: Mention) {
@ -654,7 +739,19 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
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
val message = VisibleMessage()
message.sentTimestamp = System.currentTimeMillis()
@ -662,6 +759,8 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
val outgoingTextMessage = OutgoingTextMessage.from(message, thread)
// Clear the input bar
inputBar.text = ""
inputBar.cancelQuoteDraft()
inputBar.cancelLinkPreviewDraft()
// Clear mentions
previousText = ""
currentMentionStartIndex = -1
@ -674,6 +773,36 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
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() {
AttachmentManager.selectGif(this, ConversationActivityV2.PICK_GIF)
}
@ -691,27 +820,42 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
}
override fun onAttachmentChanged() {
// Do nothing
}
override fun onActivityResult(requestCode: Int, resultCode: Int, intent: 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) {
PICK_DOCUMENT -> {
val data = intent.data ?: return
val uri = intent?.data ?: return
prepMediaForSending(uri, AttachmentManager.MediaType.DOCUMENT).addListener(mediaPreppedListener)
}
TAKE_PHOTO -> {
if (resultCode != RESULT_OK) { return }
val uri = attachmentManager.captureUri ?: return
prepMediaForSending(uri, AttachmentManager.MediaType.IMAGE).addListener(mediaPreppedListener)
}
PICK_GIF -> {
val data = intent.data ?: return
intent ?: return
val uri = intent.data ?: return
val type = AttachmentManager.MediaType.GIF
val width = intent.getIntExtra(GiphyActivity.EXTRA_WIDTH, 0)
val height = intent.getIntExtra(GiphyActivity.EXTRA_HEIGHT, 0)
prepMediaForSending(uri, type, width, height).addListener(mediaPreppedListener)
}
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 slideDeck = SlideDeck()
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
// region General

View File

@ -2,6 +2,7 @@ package org.thoughtcrime.securesms.conversation.v2
import android.content.Context
import android.database.Cursor
import android.graphics.Rect
import android.view.ViewGroup
import androidx.core.view.isVisible
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.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 glide: GlideRequests)
: CursorRecyclerViewAdapter<ViewHolder>(context, cursor) {
@ -69,7 +70,7 @@ class ConversationAdapter(context: Context, cursor: Cursor, private val onItemPr
view.messageTimestampTextView.isVisible = isSelected
val position = viewHolder.adapterPosition
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.onLongPress = { onItemLongPress(message, viewHolder.adapterPosition) }
}

View File

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

View File

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

View File

@ -9,14 +9,16 @@ import androidx.appcompat.app.AlertDialog
import kotlinx.android.synthetic.main.dialog_join_open_group.view.*
import network.loki.messenger.R
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.loki.api.OpenGroupManager
import org.thoughtcrime.securesms.loki.protocol.MultiDeviceProtocol
/** 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) {
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)
contentView.joinOpenGroupTitleTextView.text = title
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() {
// TODO: Implement
val openGroup = OpenGroupUrlParser.parseUrl(url)
OpenGroupManager.add(openGroup.server, openGroup.room, openGroup.serverPublicKey, requireContext())
MultiDeviceProtocol.forceSyncConfigurationNowIfNeeded(requireContext())
dismiss()
}
}

View File

@ -4,11 +4,12 @@ import android.view.LayoutInflater
import androidx.appcompat.app.AlertDialog
import kotlinx.android.synthetic.main.dialog_link_preview.view.*
import network.loki.messenger.R
import org.session.libsession.utilities.TextSecurePreferences
import org.thoughtcrime.securesms.conversation.v2.utilities.BaseDialog
/** 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. */
class LinkPreviewDialog() : BaseDialog() {
class LinkPreviewDialog(private val onEnabled: () -> Unit) : BaseDialog() {
override fun setContentView(builder: AlertDialog.Builder) {
val contentView = LayoutInflater.from(requireContext()).inflate(R.layout.dialog_link_preview, null)
@ -18,6 +19,8 @@ class LinkPreviewDialog() : BaseDialog() {
}
private fun enable() {
// TODO: Implement
TextSecurePreferences.setLinkPreviewsEnabled(requireContext(), true)
dismiss()
onEnabled()
}
}

View File

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

View File

@ -8,7 +8,6 @@ import android.view.MotionEvent
import android.widget.RelativeLayout
import androidx.core.view.isVisible
import kotlinx.android.synthetic.main.view_input_bar.view.*
import kotlinx.android.synthetic.main.view_quote.view.*
import network.loki.messenger.R
import org.session.libsession.messaging.sending_receiving.link_preview.LinkPreview
import org.thoughtcrime.securesms.conversation.v2.components.LinkPreviewDraftView
@ -30,6 +29,8 @@ class InputBar : RelativeLayout, InputBarEditTextDelegate, QuoteViewDelegate, Li
private var linkPreviewDraftView: LinkPreviewDraftView? = null
var delegate: InputBarDelegate? = null
var additionalContentHeight = 0
var quote: MessageRecord? = null
var linkPreview: LinkPreview? = null
var text: String
get() { return inputBarEditText.text.toString() }
@ -53,7 +54,7 @@ class InputBar : RelativeLayout, InputBarEditTextDelegate, QuoteViewDelegate, Li
// Microphone button
microphoneOrSendButtonContainer.addView(microphoneButton)
microphoneButton.layoutParams = RelativeLayout.LayoutParams(RelativeLayout.LayoutParams.MATCH_PARENT, RelativeLayout.LayoutParams.MATCH_PARENT)
microphoneButton.onLongPress = { showVoiceMessageUI() }
microphoneButton.onLongPress = { startRecordingVoiceMessage() }
microphoneButton.onMove = { delegate?.onMicrophoneButtonMove(it) }
microphoneButton.onCancel = { delegate?.onMicrophoneButtonCancel(it) }
microphoneButton.onUp = { delegate?.onMicrophoneButtonUp(it) }
@ -61,7 +62,7 @@ class InputBar : RelativeLayout, InputBarEditTextDelegate, QuoteViewDelegate, Li
microphoneOrSendButtonContainer.addView(sendButton)
sendButton.layoutParams = RelativeLayout.LayoutParams(RelativeLayout.LayoutParams.MATCH_PARENT, RelativeLayout.LayoutParams.MATCH_PARENT)
sendButton.isVisible = false
sendButton.onUp = { delegate?.send() }
sendButton.onUp = { delegate?.sendMessage() }
// Edit text
inputBarEditText.imeOptions = inputBarEditText.imeOptions or 16777216 // Always use incognito keyboard
inputBarEditText.delegate = this
@ -93,14 +94,16 @@ class InputBar : RelativeLayout, InputBarEditTextDelegate, QuoteViewDelegate, Li
delegate?.toggleAttachmentOptions()
}
private fun showVoiceMessageUI() {
delegate?.showVoiceMessageUI()
private fun startRecordingVoiceMessage() {
delegate?.startRecordingVoiceMessage()
}
// 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.
fun draftQuote(message: MessageRecord) {
fun draftQuote(message: MessageRecord, glide: GlideRequests) {
quote = message
linkPreview = null
linkPreviewDraftView = null
inputBarAdditionalContentContainer.removeAllViews()
val quoteView = QuoteView(context, QuoteView.Mode.Draft)
@ -112,7 +115,7 @@ class InputBar : RelativeLayout, InputBarEditTextDelegate, QuoteViewDelegate, Li
// here to get the layout right.
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,
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
// intrinsic height calculation.
val quoteViewIntrinsicHeight = quoteView.getIntrinsicHeight(maxContentWidth) + toPx(6, resources)
@ -122,6 +125,7 @@ class InputBar : RelativeLayout, InputBarEditTextDelegate, QuoteViewDelegate, Li
}
override fun cancelQuoteDraft() {
quote = null
inputBarAdditionalContentContainer.removeAllViews()
val newHeight = max(inputBarEditText.height + 2 * vMargin, minHeight)
additionalContentHeight = 0
@ -129,6 +133,7 @@ class InputBar : RelativeLayout, InputBarEditTextDelegate, QuoteViewDelegate, Li
}
fun draftLinkPreview() {
quote = null
val linkPreviewDraftHeight = toPx(88, resources)
inputBarAdditionalContentContainer.removeAllViews()
val linkPreviewDraftView = LinkPreviewDraftView(context)
@ -141,11 +146,14 @@ class InputBar : RelativeLayout, InputBarEditTextDelegate, QuoteViewDelegate, Li
}
fun updateLinkPreviewDraft(glide: GlideRequests, linkPreview: LinkPreview) {
this.linkPreview = linkPreview
val linkPreviewDraftView = this.linkPreviewDraftView ?: return
linkPreviewDraftView.update(glide, linkPreview)
}
override fun cancelLinkPreviewDraft() {
if (quote != null) { return }
linkPreview = null
inputBarAdditionalContentContainer.removeAllViews()
val newHeight = max(inputBarEditText.height + 2 * vMargin, minHeight)
additionalContentHeight = 0
@ -160,8 +168,9 @@ interface InputBarDelegate {
fun inputBarEditTextContentChanged(newContent: CharSequence)
fun toggleAttachmentOptions()
fun showVoiceMessageUI()
fun startRecordingVoiceMessage()
fun onMicrophoneButtonMove(event: MotionEvent)
fun onMicrophoneButtonCancel(event: MotionEvent)
fun onMicrophoneButtonUp(event: MotionEvent)
fun send()
fun sendMessage()
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,5 +1,6 @@
package org.thoughtcrime.securesms.conversation.v2.messages
import android.content.ContentResolver
import android.content.Context
import android.content.res.ColorStateList
import android.content.res.Resources
@ -13,6 +14,8 @@ import androidx.core.content.res.ResourcesCompat
import androidx.core.text.toSpannable
import androidx.core.view.isVisible
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 network.loki.messenger.R
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.model.MessageRecord
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.util.MediaUtil
import kotlin.math.max
import kotlin.math.min
import kotlin.math.roundToInt
@ -104,7 +110,7 @@ class QuoteView : LinearLayout {
// region Updating
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)
// 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
@ -136,15 +142,24 @@ class QuoteView : LinearLayout {
val backgroundColorID = if (UiModeUtilities.isDayUiMode(context)) R.color.black else R.color.accent
val backgroundColor = ResourcesCompat.getColor(resources, backgroundColorID, context.theme)
quoteViewAttachmentPreviewContainer.backgroundTintList = ColorStateList.valueOf(backgroundColor)
quoteViewAttachmentPreviewImageView.isVisible = false
quoteViewAttachmentThumbnailImageView.isVisible = false
if (attachments.audioSlide != null) {
quoteViewAttachmentPreviewImageView.setImageResource(R.drawable.ic_microphone)
quoteViewAttachmentPreviewImageView.isVisible = true
quoteViewBodyTextView.text = resources.getString(R.string.Slide_audio)
} else if (attachments.documentSlide != null) {
quoteViewAttachmentPreviewImageView.setImageResource(R.drawable.ic_document_large_light)
quoteViewAttachmentPreviewImageView.isVisible = true
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))
val quoteViewMainContentContainerLayoutParams = quoteViewMainContentContainer.layoutParams as RelativeLayout.LayoutParams

View File

@ -2,6 +2,7 @@ package org.thoughtcrime.securesms.conversation.v2.messages
import android.content.Context
import android.graphics.Color
import android.graphics.Rect
import android.graphics.drawable.Drawable
import android.text.style.BackgroundColorSpan
import android.text.style.ForegroundColorSpan
@ -11,6 +12,7 @@ import android.util.TypedValue
import android.view.LayoutInflater
import android.widget.LinearLayout
import android.widget.TextView
import android.widget.Toast
import androidx.annotation.ColorInt
import androidx.annotation.DrawableRes
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.ViewUtil
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.database.model.MessageRecord
import org.thoughtcrime.securesms.database.model.MmsMessageRecord
@ -33,7 +36,8 @@ import java.util.*
import kotlin.math.roundToInt
class VisibleMessageContentView : LinearLayout {
var onContentClick: (() -> Unit)? = null
var onContentClick: ((rawRect: Rect) -> Unit)? = null
var onContentDoubleTap: (() -> Unit)? = null
// region Lifecycle
constructor(context: Context) : super(context) { initialize() }
@ -58,10 +62,12 @@ class VisibleMessageContentView : LinearLayout {
// Body
mainContainer.removeAllViews()
onContentClick = null
onContentDoubleTap = null
if (message is MmsMessageRecord && message.linkPreviews.isNotEmpty()) {
val linkPreviewView = LinkPreviewView(context)
linkPreviewView.bind(message, glide, isStartOfMessageCluster, isEndOfMessageCluster, searchQuery)
mainContainer.addView(linkPreviewView)
onContentClick = { linkPreviewView.openURL() }
// Body text view is inside the link preview for layout convenience
} else if (message is MmsMessageRecord && message.quote != null) {
val quote = message.quote!!
@ -71,7 +77,7 @@ class VisibleMessageContentView : LinearLayout {
// here to get the layout right.
val maxContentWidth = (maxWidth - 2 * resources.getDimension(R.dimen.medium_spacing) - toPx(16, resources)).roundToInt()
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)
val bodyTextView = VisibleMessageContentView.getBodyTextView(context, message, searchQuery)
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
// message view) so as to not interfere with all the other gestures.
onContentClick = { voiceMessageView.togglePlayback() }
onContentDoubleTap = { voiceMessageView.handleDoubleTap() }
} else if (message is MmsMessageRecord && message.slideDeck.documentSlide != null) {
val documentView = DocumentView(context)
documentView.bind(message, VisibleMessageContentView.getTextColor(context, message))
mainContainer.addView(documentView)
} else if (message is MmsMessageRecord && message.slideDeck.asAttachments().isNotEmpty()) {
val dummyTextView = TextView(context)
dummyTextView.text = "asifuygaihsfo"
mainContainer.addView(dummyTextView)
val albumThumbnailView = AlbumThumbnailView(context)
mainContainer.addView(albumThumbnailView)
// 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) {
val openGroupInvitationView = OpenGroupInvitationView(context)
openGroupInvitationView.bind(message, VisibleMessageContentView.getTextColor(context, message))
mainContainer.addView(openGroupInvitationView)
onContentClick = { openGroupInvitationView.joinOpenGroup() }
} else {
val bodyTextView = VisibleMessageContentView.getBodyTextView(context, message, searchQuery)
mainContainer.addView(bodyTextView)
onContentClick = { openURLIfNeeded(message) }
}
}
@ -121,6 +138,12 @@ class VisibleMessageContentView : LinearLayout {
}
// endregion
// region Interaction
private fun openURLIfNeeded(message: MessageRecord) {
Toast.makeText(context, "Not yet implemented", Toast.LENGTH_LONG).show()
}
// endregion
// region Convenience
companion object {

View File

@ -4,21 +4,15 @@ import android.content.Context
import android.content.res.Resources
import android.graphics.Canvas
import android.graphics.Rect
import android.graphics.Region
import android.graphics.drawable.ColorDrawable
import android.os.Build
import android.os.Handler
import android.os.Looper
import android.util.AttributeSet
import android.util.Log
import android.view.*
import android.widget.LinearLayout
import androidx.annotation.ColorRes
import androidx.annotation.DrawableRes
import androidx.core.content.ContextCompat
import androidx.core.graphics.withClip
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.profilePictureView
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.thoughtcrime.securesms.database.DatabaseFactory
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.toDp
import org.thoughtcrime.securesms.loki.utilities.toPx
@ -46,11 +39,13 @@ class VisibleMessageView : LinearLayout {
private var dx = 0.0f
private var previousTranslationX = 0.0f
private val gestureHandler = Handler(Looper.getMainLooper())
private var pressCallback: Runnable? = null
private var longPressCallback: Runnable? = null
private var onDownTimestamp = 0L
private var onDoubleTap: (() -> Unit)? = null
var snIsSelected = false
set(value) { field = value; handleIsSelectedChanged()}
var onPress: (() -> Unit)? = null
var onPress: ((rawX: Int, rawY: Int) -> Unit)? = null
var onSwipeToReply: (() -> Unit)? = null
var onLongPress: (() -> Unit)? = null
@ -58,6 +53,7 @@ class VisibleMessageView : LinearLayout {
const val swipeToReplyThreshold = 80.0f // dp
const val longPressMovementTreshold = 10.0f // dp
const val longPressDurationThreshold = 250L // ms
const val maxDoubleTapInterval = 200L
}
// region Lifecycle
@ -143,6 +139,7 @@ class VisibleMessageView : LinearLayout {
if (profilePictureContainer.visibility != View.GONE) { maxWidth -= profilePictureContainer.width }
// Populate content view
messageContentView.bind(message, isStartOfMessageCluster, isEndOfMessageCluster, glide, maxWidth, thread, searchQuery)
onDoubleTap = { messageContentView.onContentDoubleTap?.invoke() }
}
private fun setMessageSpacing(isStartOfMessageCluster: Boolean, isEndOfMessageCluster: Boolean) {
@ -195,7 +192,7 @@ class VisibleMessageView : LinearLayout {
val spacing = context.resources.getDimensionPixelSize(R.dimen.small_spacing)
val threshold = VisibleMessageView.swipeToReplyThreshold
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.top = height - bottomVOffset - iconSize
swipeToReplyIconRect.right = messageContentContainer.right + iconSize + spacing
@ -272,7 +269,18 @@ class VisibleMessageView : LinearLayout {
onSwipeToReply?.invoke()
} else if ((Date().time - onDownTimestamp) < VisibleMessageView.longPressDurationThreshold) {
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()
}
@ -297,8 +305,13 @@ class VisibleMessageView : LinearLayout {
onLongPress?.invoke()
}
fun onContentClick() {
messageContentView.onContentClick?.invoke()
fun onContentClick(rawRect: Rect) {
messageContentView.onContentClick?.invoke(rawRect)
}
private fun onPress(rawX: Int, rawY: Int) {
onPress?.invoke(rawX, rawY)
pressCallback = null
}
// endregion
}

View File

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

View File

@ -14,7 +14,7 @@
* You should have received a copy of the GNU General Public License
* 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.annotation.SuppressLint;
@ -23,29 +23,31 @@ import android.content.ActivityNotFoundException;
import android.content.Context;
import android.content.Intent;
import android.database.Cursor;
import android.graphics.PorterDuff;
import android.net.Uri;
import android.os.AsyncTask;
import android.provider.ContactsContract;
import android.provider.MediaStore;
import android.provider.OpenableColumns;
import android.text.TextUtils;
import android.util.Pair;
import android.view.View;
import android.widget.Toast;
import androidx.annotation.NonNull;
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.thoughtcrime.securesms.giph.ui.GiphyActivity;
import org.session.libsignal.utilities.Log;
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.providers.BlobProvider;
import org.session.libsignal.utilities.ExternalStorageUtil;
@ -53,13 +55,8 @@ import org.thoughtcrime.securesms.util.FileProviderUtil;
import org.thoughtcrime.securesms.util.MediaUtil;
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.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.Listener;
import org.session.libsignal.utilities.SettableFuture;
import java.io.File;
@ -67,26 +64,18 @@ import java.io.IOException;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.concurrent.ExecutionException;
import network.loki.messenger.R;
import static android.provider.MediaStore.EXTRA_OUTPUT;
public class AttachmentManager {
private final static String TAG = AttachmentManager.class.getSimpleName();
private final @NonNull Context context;
private final @NonNull Stub<View> attachmentViewStub;
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 Optional<Slide> slide = Optional.absent();
private @Nullable Uri captureUri;
@ -94,51 +83,12 @@ public class AttachmentManager {
public AttachmentManager(@NonNull Activity activity, @NonNull AttachmentListener listener) {
this.context = activity;
this.attachmentListener = listener;
this.attachmentViewStub = ViewUtil.findStubById(activity, R.id.attachment_editor_stub);
}
private void inflateStub() {
if (!attachmentViewStub.resolved()) {
View root = attachmentViewStub.get();
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 clear() {
markGarbage(getSlideUri());
slide = Optional.absent();
attachmentListener.onAttachmentChanged();
}
public void cleanup() {
@ -190,16 +140,12 @@ public class AttachmentManager {
final int width,
final int height)
{
inflateStub();
final SettableFuture<Boolean> result = new SettableFuture<>();
new AsyncTask<Void, Void, Slide>() {
@Override
protected void onPreExecute() {
thumbnail.clear(glideRequests);
thumbnail.showProgressSpinner();
attachmentViewStub.get().setVisibility(View.VISIBLE);
}
@Override
@ -222,35 +168,12 @@ public class AttachmentManager {
@Override
protected void onPostExecute(@Nullable final Slide slide) {
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);
} 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);
} else {
setSlide(slide);
attachmentViewStub.get().setVisibility(View.VISIBLE);
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);
}
result.set(true);
attachmentListener.onAttachmentChanged();
}
}
@ -317,11 +240,8 @@ public class AttachmentManager {
return result;
}
public boolean isAttachmentPresent() {
return attachmentViewStub.resolved() && attachmentViewStub.get().getVisibility() == View.VISIBLE;
}
public @NonNull SlideDeck buildSlideDeck() {
public @NonNull
SlideDeck buildSlideDeck() {
SlideDeck deck = new SlideDeck();
if (slide.isPresent()) deck.addSlide(slide.get());
return deck;
@ -333,43 +253,16 @@ public class AttachmentManager {
public static void selectGallery(Activity activity, int requestCode, @NonNull Recipient recipient, @NonNull String body) {
Permissions.with(activity)
.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))
.onAllGranted(() -> activity.startActivityForResult(MediaSendActivity.buildGalleryIntent(activity, recipient, body), requestCode))
.execute();
.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))
.onAllGranted(() -> activity.startActivityForResult(MediaSendActivity.buildGalleryIntent(activity, recipient, body), requestCode))
.execute();
}
public static void selectAudio(Activity activity, int 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) {
Intent intent = new Intent(activity, GiphyActivity.class);
intent.putExtra(GiphyActivity.EXTRA_IS_MMS, false);
@ -386,28 +279,25 @@ public class AttachmentManager {
public void capturePhoto(Activity activity, int requestCode) {
Permissions.with(activity)
.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))
.onAllGranted(() -> {
try {
File captureFile = File.createTempFile(
"conversation-capture",
".jpg",
ExternalStorageUtil.getImageDir(activity));
Uri captureUri = FileProviderUtil.getUriFor(context, captureFile);
Intent captureIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
captureIntent.putExtra(EXTRA_OUTPUT, captureUri);
captureIntent.setFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
if (captureIntent.resolveActivity(activity.getPackageManager()) != null) {
Log.d(TAG, "captureUri path is " + captureUri.getPath());
this.captureUri = captureUri;
activity.startActivityForResult(captureIntent, requestCode);
}
} catch (IOException | NoExternalStorageException e) {
throw new RuntimeException("Error creating image capture intent.", e);
}
})
.execute();
.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))
.onAllGranted(() -> {
try {
File captureFile = File.createTempFile("conversation-capture", ".jpg", ExternalStorageUtil.getImageDir(activity));
Uri captureUri = FileProviderUtil.getUriFor(context, captureFile);
Intent captureIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
captureIntent.putExtra(EXTRA_OUTPUT, captureUri);
captureIntent.setFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
if (captureIntent.resolveActivity(activity.getPackageManager()) != null) {
Log.d(TAG, "captureUri path is " + captureUri.getPath());
this.captureUri = captureUri;
activity.startActivityForResult(captureIntent, requestCode);
}
} catch (IOException | NoExternalStorageException e) {
throw new RuntimeException("Error creating image capture intent.", e);
}
})
.execute();
}
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());
}
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 {
void onAttachmentChanged();
}
@ -513,6 +375,5 @@ public class AttachmentManager {
return DOCUMENT;
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -2,36 +2,20 @@ package org.thoughtcrime.securesms.longmessage;
import android.content.Context;
import android.content.Intent;
import android.graphics.PorterDuff;
import android.os.Bundle;
import android.text.SpannableString;
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.View;
import android.view.ViewGroup;
import android.widget.TextView;
import android.widget.Toast;
import androidx.annotation.NonNull;
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.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.Stub;
import java.util.Locale;
import org.session.libsession.utilities.recipients.Recipient;
import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity;
import org.thoughtcrime.securesms.loki.utilities.MentionUtilities;
import network.loki.messenger.R;
@ -43,8 +27,7 @@ public class LongMessageActivity extends PassphraseRequiredActionBarActivity {
private static final int MAX_DISPLAY_LENGTH = 64 * 1024;
private Stub<ViewGroup> sentBubble;
private Stub<ViewGroup> receivedBubble;
private TextView textBody;
private LongMessageViewModel viewModel;
@ -60,9 +43,7 @@ public class LongMessageActivity extends PassphraseRequiredActionBarActivity {
protected void onCreate(Bundle savedInstanceState, boolean ready) {
super.onCreate(savedInstanceState, ready);
setContentView(R.layout.longmessage_activity);
sentBubble = new Stub<>(findViewById(R.id.longmessage_sent_stub));
receivedBubble = new Stub<>(findViewById(R.id.longmessage_received_stub));
textBody = findViewById(R.id.longmessage_text);
initViewModel(getIntent().getLongExtra(KEY_MESSAGE_ID, -1), getIntent().getBooleanExtra(KEY_IS_MMS, false));
}
@ -93,36 +74,19 @@ public class LongMessageActivity extends PassphraseRequiredActionBarActivity {
return;
}
if (message.get().getMessageRecord().isOutgoing()) {
getSupportActionBar().setTitle(getString(R.string.LongMessageActivity_your_message));
} else {
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));
}
ViewGroup bubble;
String trimmedBody = getTrimmedBody(message.get().getFullBody());
String mentionBody = MentionUtilities.highlightMentions(trimmedBody, message.get().getMessageRecord().getThreadId(), this);
if (message.get().getMessageRecord().isOutgoing()) {
bubble = sentBubble.get();
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());
textBody.setText(mentionBody);
textBody.setMovementMethod(LinkMovementMethod.getInstance());
});
}
@ -131,15 +95,4 @@ public class LongMessageActivity extends PassphraseRequiredActionBarActivity {
: text.substring(0, MAX_DISPLAY_LENGTH);
}
private SpannableString linkifyMessageBody(SpannableString messageBody) {
int linkPattern = Linkify.WEB_URLS | Linkify.EMAIL_ADDRESSES | Linkify.PHONE_NUMBERS;
boolean hasLinks = Linkify.addLinks(messageBody, linkPattern);
if (hasLinks) {
Stream.of(messageBody.getSpans(0, messageBody.length(), URLSpan.class))
.filterNot(url -> LinkPreviewUtil.isLegalUrl(url.getURL()))
.forEach(messageBody::removeSpan);
}
return messageBody;
}
}

View File

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

View File

@ -103,7 +103,6 @@ public class AppProtectionPreferenceFragment extends CorrectedPreferenceFragment
TextSecurePreferences.setScreenLockTimeout(getContext(), 0);
} else {
long timeoutSeconds = TimeUnit.MILLISECONDS.toSeconds(duration);
// long timeoutSeconds = Math.max(TimeUnit.MILLISECONDS.toSeconds(duration), 60);
TextSecurePreferences.setScreenLockTimeout(getContext(), timeoutSeconds);
}
@ -117,7 +116,6 @@ public class AppProtectionPreferenceFragment extends CorrectedPreferenceFragment
private class ReadReceiptToggleListener implements Preference.OnPreferenceChangeListener {
@Override
public boolean onPreferenceChange(Preference preference, Object newValue) {
boolean enabled = (boolean)newValue;
return true;
}
}
@ -138,21 +136,6 @@ public class AppProtectionPreferenceFragment extends CorrectedPreferenceFragment
private class LinkPreviewToggleListener implements Preference.OnPreferenceChangeListener {
@Override
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;
}
}

View File

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

View File

@ -7,13 +7,13 @@
android:layout_width="@dimen/album_total_width"
android:layout_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:layout_width="@dimen/album_2_cell_width"
android:layout_height="@dimen/album_2_total_height"
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:layout_width="@dimen/album_2_cell_width"
android:layout_height="@dimen/album_2_total_height"

View File

@ -6,20 +6,20 @@
android:layout_width="@dimen/album_total_width"
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:layout_width="@dimen/album_3_cell_width_big"
android:layout_height="@dimen/album_3_total_height"
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:layout_width="@dimen/album_3_cell_size_small"
android:layout_height="@dimen/album_3_cell_size_small"
android:layout_gravity="right|end|top"
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:layout_width="@dimen/album_3_cell_size_small"
android:layout_height="@dimen/album_3_cell_size_small"

View File

@ -6,27 +6,27 @@
android:layout_width="@dimen/album_total_width"
android:layout_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:layout_width="@dimen/album_4_cell_size"
android:layout_height="@dimen/album_4_cell_size"
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:layout_width="@dimen/album_4_cell_size"
android:layout_height="@dimen/album_4_cell_size"
android:layout_gravity="right|end|top"
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:layout_width="@dimen/album_4_cell_size"
android:layout_height="@dimen/album_4_cell_size"
android:layout_gravity="left|start|bottom"
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:layout_width="@dimen/album_4_cell_size"
android:layout_height="@dimen/album_4_cell_size"

View File

@ -6,34 +6,34 @@
android:layout_width="@dimen/album_total_width"
android:layout_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:layout_width="@dimen/album_5_cell_size_big"
android:layout_height="@dimen/album_5_cell_size_big"
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:layout_width="@dimen/album_5_cell_size_big"
android:layout_height="@dimen/album_5_cell_size_big"
android:layout_gravity="right|end|top"
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:layout_width="@dimen/album_5_cell_size_small"
android:layout_height="@dimen/album_5_cell_size_small"
android:layout_gravity="left|start|bottom"
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:layout_width="@dimen/album_5_cell_size_small"
android:layout_height="@dimen/album_5_cell_size_small"
android:layout_gravity="center_horizontal|bottom"
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:layout_width="@dimen/album_5_cell_size_small"
android:layout_height="@dimen/album_5_cell_size_small"

View File

@ -7,27 +7,27 @@
android:layout_width="@dimen/album_total_width"
android:layout_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:layout_width="@dimen/album_5_cell_size_big"
android:layout_height="@dimen/album_5_cell_size_big"
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:layout_width="@dimen/album_5_cell_size_big"
android:layout_height="@dimen/album_5_cell_size_big"
android:layout_gravity="right|end|top"
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:layout_width="@dimen/album_5_cell_size_small"
android:layout_height="@dimen/album_5_cell_size_small"
android:layout_gravity="left|start|bottom"
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:layout_width="@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_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:layout_height="match_parent"
android:layout_width="match_parent"
@ -51,7 +51,7 @@
android:layout_height="match_parent"
android:layout_width="match_parent"
android:gravity="center"
android:textSize="28dp"
android:textSize="@dimen/text_size"
android:textColor="@color/core_white"
android:background="@color/transparent_black_40"
tools:text="+2" />

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -32,6 +32,13 @@
android:scaleType="centerInside"
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>
<LinearLayout

View File

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

View File

@ -54,7 +54,7 @@
<string name="ConversationItem_click_to_approve_unencrypted">Send failed, tap for unsecured fallback</string>
<string name="ConversationItem_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_read_more">&#160; Read More</string>
<string name="ConversationItem_read_more">Read More</string>
<string name="ConversationItem_download_more">&#160; Download More</string>
<string name="ConversationItem_pending">&#160; Pending</string>
<!-- ConversationActivity -->

View File

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

View File

@ -58,7 +58,7 @@
<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_copied_text">Copied %s</string>
<string name="ConversationItem_read_more">&#160; Read More</string>
<string name="ConversationItem_read_more">Read More</string>
<string name="ConversationItem_download_more">&#160; Download More</string>
<string name="ConversationItem_pending">&#160; Pending</string>
<!-- ConversationActivity -->

View File

@ -54,7 +54,7 @@
<string name="ConversationItem_click_to_approve_unencrypted">Sendado malsukcesis, tuŝeti por nesekura retropaŝo</string>
<string name="ConversationItem_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_read_more">&#160; Legi pli</string>
<string name="ConversationItem_read_more">Legi pli</string>
<string name="ConversationItem_download_more">  Elŝuti pli</string>
<string name="ConversationItem_pending">  Pritraktota</string>
<!-- ConversationActivity -->

View File

@ -56,7 +56,7 @@
<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_copied_text">Copied %s</string>
<string name="ConversationItem_read_more">&#160; Read More</string>
<string name="ConversationItem_read_more">Read More</string>
<string name="ConversationItem_download_more">&#160; Download More</string>
<string name="ConversationItem_pending">&#160; Pending</string>
<!-- ConversationActivity -->

View File

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

View File

@ -52,7 +52,7 @@
<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_copied_text">Copied %s</string>
<string name="ConversationItem_read_more">&#160; Read More</string>
<string name="ConversationItem_read_more">Read More</string>
<string name="ConversationItem_download_more">&#160; Download More</string>
<string name="ConversationItem_pending">&#160; Pending</string>
<!-- ConversationActivity -->

View File

@ -54,7 +54,7 @@
<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_copied_text">Copied %s</string>
<string name="ConversationItem_read_more">&#160; Read More</string>
<string name="ConversationItem_read_more">Read More</string>
<string name="ConversationItem_download_more">&#160; Download More</string>
<string name="ConversationItem_pending">&#160; Pending</string>
<!-- ConversationActivity -->

View File

@ -52,7 +52,7 @@
<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_copied_text">복사됨 %s</string>
<string name="ConversationItem_read_more">&#160; Read More</string>
<string name="ConversationItem_read_more">Read More</string>
<string name="ConversationItem_download_more">&#160; Download More</string>
<string name="ConversationItem_pending">&#160; Pending</string>
<!-- ConversationActivity -->

View File

@ -52,7 +52,7 @@
<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_copied_text">Copied %s</string>
<string name="ConversationItem_read_more">&#160; Read More</string>
<string name="ConversationItem_read_more">Read More</string>
<string name="ConversationItem_download_more">&#160; Download More</string>
<string name="ConversationItem_pending">&#160; Pending</string>
<!-- ConversationActivity -->

View File

@ -56,7 +56,7 @@
<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_copied_text">Copied %s</string>
<string name="ConversationItem_read_more">&#160; Read More</string>
<string name="ConversationItem_read_more">Read More</string>
<string name="ConversationItem_download_more">&#160; Download More</string>
<string name="ConversationItem_pending">&#160; Pending</string>
<!-- ConversationActivity -->

View File

@ -54,7 +54,7 @@
<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_copied_text">ကူးပြီးပါပြီ %s </string>
<string name="ConversationItem_read_more">&#160; Read More</string>
<string name="ConversationItem_read_more">Read More</string>
<string name="ConversationItem_download_more">&#160; Download More</string>
<string name="ConversationItem_pending">&#160; Pending</string>
<!-- ConversationActivity -->

View File

@ -54,7 +54,7 @@
<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_copied_text">Copied %s</string>
<string name="ConversationItem_read_more">&#160; Read More</string>
<string name="ConversationItem_read_more">Read More</string>
<string name="ConversationItem_download_more">&#160; Download More</string>
<string name="ConversationItem_pending">&#160; Pending</string>
<!-- ConversationActivity -->

View File

@ -52,7 +52,7 @@
<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_copied_text">Copied %s</string>
<string name="ConversationItem_read_more">&#160; Read More</string>
<string name="ConversationItem_read_more">Read More</string>
<string name="ConversationItem_download_more">&#160; Download More</string>
<string name="ConversationItem_pending">&#160; Pending</string>
<!-- ConversationActivity -->

View File

@ -54,7 +54,7 @@
<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_copied_text">Copied %s</string>
<string name="ConversationItem_read_more">&#160; Read More</string>
<string name="ConversationItem_read_more">Read More</string>
<string name="ConversationItem_download_more">&#160; Download More</string>
<string name="ConversationItem_pending">&#160; Pending</string>
<!-- ConversationActivity -->

View File

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

View File

@ -54,7 +54,7 @@
<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_copied_text">Copied %s</string>
<string name="ConversationItem_read_more">&#160; Read More</string>
<string name="ConversationItem_read_more">Read More</string>
<string name="ConversationItem_download_more">&#160; Download More</string>
<string name="ConversationItem_pending">&#160; Pending</string>
<!-- ConversationActivity -->

View File

@ -54,7 +54,7 @@
<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_copied_text">Copied %s</string>
<string name="ConversationItem_read_more">&#160; Read More</string>
<string name="ConversationItem_read_more">Read More</string>
<string name="ConversationItem_download_more">&#160; Download More</string>
<string name="ConversationItem_pending">&#160; Pending</string>
<!-- ConversationActivity -->

View File

@ -54,7 +54,7 @@
<string name="ConversationItem_click_to_approve_unencrypted">Falha no envio, toque para fallback inseguro</string>
<string name="ConversationItem_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_read_more">&#160; Ler Mais</string>
<string name="ConversationItem_read_more">Ler Mais</string>
<string name="ConversationItem_download_more">&#160; Fazer Download de Mais</string>
<string name="ConversationItem_pending">&#160; Pendendo</string>
<!-- ConversationActivity -->

View File

@ -58,7 +58,7 @@
<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_copied_text">Copied %s</string>
<string name="ConversationItem_read_more">&#160; Read More</string>
<string name="ConversationItem_read_more">Read More</string>
<string name="ConversationItem_download_more">&#160; Download More</string>
<string name="ConversationItem_pending">&#160; Pending</string>
<!-- ConversationActivity -->

View File

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

View File

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

View File

@ -54,7 +54,7 @@
<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_copied_text">Copied %s</string>
<string name="ConversationItem_read_more">&#160; Read More</string>
<string name="ConversationItem_read_more">Read More</string>
<string name="ConversationItem_download_more">&#160; Download More</string>
<string name="ConversationItem_pending">&#160; Pending</string>
<!-- ConversationActivity -->

View File

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

View File

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

View File

@ -54,7 +54,7 @@
<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_copied_text">Copied %s</string>
<string name="ConversationItem_read_more">&#160; Read More</string>
<string name="ConversationItem_read_more">Read More</string>
<string name="ConversationItem_download_more">&#160; Download More</string>
<string name="ConversationItem_pending">&#160; Pending</string>
<!-- ConversationActivity -->

View File

@ -52,7 +52,7 @@
<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_copied_text">Đã sao chép %s</string>
<string name="ConversationItem_read_more">&#160; Read More</string>
<string name="ConversationItem_read_more">Read More</string>
<string name="ConversationItem_download_more">&#160; Tải thêm</string>
<string name="ConversationItem_pending">&#160; Đang chờ</string>
<!-- ConversationActivity -->

View File

@ -54,7 +54,7 @@
<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_copied_text">Copied %s</string>
<string name="ConversationItem_read_more">&#160; Read More</string>
<string name="ConversationItem_read_more">Read More</string>
<string name="ConversationItem_download_more">&#160; Download More</string>
<string name="ConversationItem_pending">&#160; Pending</string>
<!-- ConversationActivity -->

View File

@ -64,7 +64,7 @@
<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_copied_text">Copied %s</string>
<string name="ConversationItem_read_more">&#160; Read More</string>
<string name="ConversationItem_read_more">Read More</string>
<string name="ConversationItem_download_more">&#160; Download More</string>
<string name="ConversationItem_pending">&#160; Pending</string>
@ -872,4 +872,6 @@
<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_attachment_prep_failed">Failed to prepare attachment for sending.</string>
</resources>

View File

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

View File

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

View File

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

View File

@ -779,4 +779,12 @@ object TextSecurePreferences {
fun setLastOpenDate(context: Context) {
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)
}
}