mirror of
https://github.com/oxen-io/session-android.git
synced 2024-12-24 16:57:50 +00:00
Merge remote-tracking branch 'upstream/ui' into ui
# Conflicts: # app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationActivityV2.kt # app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/VisibleMessageContentView.kt # app/src/main/java/org/thoughtcrime/securesms/conversation/v2/utilities/AttachmentManager.java
This commit is contained in:
commit
4498b6e00f
@ -79,8 +79,7 @@ public class TypingStatusSender {
|
|||||||
ThreadDatabase threadDatabase = DatabaseFactory.getThreadDatabase(context);
|
ThreadDatabase threadDatabase = DatabaseFactory.getThreadDatabase(context);
|
||||||
Recipient recipient = threadDatabase.getRecipientForThreadId(threadId);
|
Recipient recipient = threadDatabase.getRecipientForThreadId(threadId);
|
||||||
if (recipient == null) { return; }
|
if (recipient == null) { return; }
|
||||||
// Loki - Check whether we want to send a typing indicator to this user
|
if (!SessionMetaProtocol.shouldSendTypingIndicator(recipient.getAddress())) { return; }
|
||||||
if (recipient != null && !SessionMetaProtocol.shouldSendTypingIndicator(recipient.getAddress())) { return; }
|
|
||||||
TypingIndicator typingIndicator;
|
TypingIndicator typingIndicator;
|
||||||
if (typingStarted) {
|
if (typingStarted) {
|
||||||
typingIndicator = new TypingIndicator(TypingIndicator.Kind.STARTED);
|
typingIndicator = new TypingIndicator(TypingIndicator.Kind.STARTED);
|
||||||
|
@ -102,7 +102,6 @@ import org.session.libsession.utilities.recipients.RecipientModifiedListener;
|
|||||||
import org.session.libsession.utilities.ExpirationUtil;
|
import org.session.libsession.utilities.ExpirationUtil;
|
||||||
import org.session.libsession.utilities.GroupUtil;
|
import org.session.libsession.utilities.GroupUtil;
|
||||||
import org.session.libsession.utilities.MediaTypes;
|
import org.session.libsession.utilities.MediaTypes;
|
||||||
import org.session.libsession.utilities.SSKEnvironment;
|
|
||||||
import org.session.libsession.utilities.ServiceUtil;
|
import org.session.libsession.utilities.ServiceUtil;
|
||||||
import org.session.libsession.utilities.TextSecurePreferences;
|
import org.session.libsession.utilities.TextSecurePreferences;
|
||||||
import org.session.libsession.utilities.Util;
|
import org.session.libsession.utilities.Util;
|
||||||
@ -165,8 +164,8 @@ import org.thoughtcrime.securesms.loki.views.MentionCandidateSelectionView;
|
|||||||
import org.thoughtcrime.securesms.loki.views.ProfilePictureView;
|
import org.thoughtcrime.securesms.loki.views.ProfilePictureView;
|
||||||
import org.thoughtcrime.securesms.mediasend.Media;
|
import org.thoughtcrime.securesms.mediasend.Media;
|
||||||
import org.thoughtcrime.securesms.mediasend.MediaSendActivity;
|
import org.thoughtcrime.securesms.mediasend.MediaSendActivity;
|
||||||
import org.thoughtcrime.securesms.mms.AttachmentManager;
|
import org.thoughtcrime.securesms.conversation.v2.utilities.AttachmentManager;
|
||||||
import org.thoughtcrime.securesms.mms.AttachmentManager.MediaType;
|
import org.thoughtcrime.securesms.conversation.v2.utilities.AttachmentManager.MediaType;
|
||||||
import org.thoughtcrime.securesms.mms.AudioSlide;
|
import org.thoughtcrime.securesms.mms.AudioSlide;
|
||||||
import org.thoughtcrime.securesms.mms.GifSlide;
|
import org.thoughtcrime.securesms.mms.GifSlide;
|
||||||
import org.thoughtcrime.securesms.mms.GlideApp;
|
import org.thoughtcrime.securesms.mms.GlideApp;
|
||||||
@ -422,9 +421,9 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!org.thoughtcrime.securesms.util.Util.isEmpty(composeText) || attachmentManager.isAttachmentPresent()) {
|
if (!org.thoughtcrime.securesms.util.Util.isEmpty(composeText)) {
|
||||||
saveDraft();
|
saveDraft();
|
||||||
attachmentManager.clear(glideRequests, false);
|
attachmentManager.clear();
|
||||||
silentlySetComposeText("");
|
silentlySetComposeText("");
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1424,9 +1423,9 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
|||||||
case AttachmentTypeSelector.ADD_SOUND:
|
case AttachmentTypeSelector.ADD_SOUND:
|
||||||
AttachmentManager.selectAudio(this, PICK_AUDIO); break;
|
AttachmentManager.selectAudio(this, PICK_AUDIO); break;
|
||||||
case AttachmentTypeSelector.ADD_CONTACT_INFO:
|
case AttachmentTypeSelector.ADD_CONTACT_INFO:
|
||||||
AttachmentManager.selectContactInfo(this, PICK_CONTACT); break;
|
break;
|
||||||
case AttachmentTypeSelector.ADD_LOCATION:
|
case AttachmentTypeSelector.ADD_LOCATION:
|
||||||
AttachmentManager.selectLocation(this, PICK_LOCATION); break;
|
break;
|
||||||
case AttachmentTypeSelector.TAKE_PHOTO:
|
case AttachmentTypeSelector.TAKE_PHOTO:
|
||||||
attachmentManager.capturePhoto(this, TAKE_PHOTO); break;
|
attachmentManager.capturePhoto(this, TAKE_PHOTO); break;
|
||||||
case AttachmentTypeSelector.ADD_GIF:
|
case AttachmentTypeSelector.ADD_GIF:
|
||||||
@ -1620,7 +1619,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
|||||||
|
|
||||||
private String getMessage() throws InvalidMessageException {
|
private String getMessage() throws InvalidMessageException {
|
||||||
String result = composeText.getTextTrimmed();
|
String result = composeText.getTextTrimmed();
|
||||||
if (result.length() < 1 && !attachmentManager.isAttachmentPresent()) throw new InvalidMessageException();
|
if (result.length() < 1) throw new InvalidMessageException();
|
||||||
for (Mention mention : mentions) {
|
for (Mention mention : mentions) {
|
||||||
try {
|
try {
|
||||||
int startIndex = result.indexOf("@" + mention.getDisplayName());
|
int startIndex = result.indexOf("@" + mention.getDisplayName());
|
||||||
@ -1723,7 +1722,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
|||||||
String message = getMessage();
|
String message = getMessage();
|
||||||
boolean initiating = threadId == -1;
|
boolean initiating = threadId == -1;
|
||||||
boolean needsSplit = message.length() > characterCalculator.calculateCharacters(message).maxPrimaryMessageSize;
|
boolean needsSplit = message.length() > characterCalculator.calculateCharacters(message).maxPrimaryMessageSize;
|
||||||
boolean isMediaMessage = attachmentManager.isAttachmentPresent() ||
|
boolean isMediaMessage = false ||
|
||||||
// recipient.isGroupRecipient() ||
|
// recipient.isGroupRecipient() ||
|
||||||
inputPanel.getQuote().isPresent() ||
|
inputPanel.getQuote().isPresent() ||
|
||||||
linkPreviewViewModel.hasLinkPreview() ||
|
linkPreviewViewModel.hasLinkPreview() ||
|
||||||
@ -1785,7 +1784,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
|||||||
ApplicationContext.getInstance(context).getTypingStatusSender().onTypingStopped(threadId);
|
ApplicationContext.getInstance(context).getTypingStatusSender().onTypingStopped(threadId);
|
||||||
|
|
||||||
inputPanel.clearQuote();
|
inputPanel.clearQuote();
|
||||||
attachmentManager.clear(glideRequests, false);
|
attachmentManager.clear();
|
||||||
silentlySetComposeText("");
|
silentlySetComposeText("");
|
||||||
|
|
||||||
final long id = fragment.stageOutgoingMessage(outgoingMessage);
|
final long id = fragment.stageOutgoingMessage(outgoingMessage);
|
||||||
@ -1859,7 +1858,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (composeText.getText().length() == 0 && !attachmentManager.isAttachmentPresent()) {
|
if (composeText.getText().length() == 0) {
|
||||||
buttonToggle.display(attachButton);
|
buttonToggle.display(attachButton);
|
||||||
quickAttachmentToggle.show();
|
quickAttachmentToggle.show();
|
||||||
inlineAttachmentToggle.hide();
|
inlineAttachmentToggle.hide();
|
||||||
@ -1867,7 +1866,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
|||||||
buttonToggle.display(sendButton);
|
buttonToggle.display(sendButton);
|
||||||
quickAttachmentToggle.hide();
|
quickAttachmentToggle.hide();
|
||||||
|
|
||||||
if (!attachmentManager.isAttachmentPresent() && !linkPreviewViewModel.hasLinkPreview()) {
|
if (!linkPreviewViewModel.hasLinkPreview()) {
|
||||||
inlineAttachmentToggle.show();
|
inlineAttachmentToggle.show();
|
||||||
} else {
|
} else {
|
||||||
inlineAttachmentToggle.hide();
|
inlineAttachmentToggle.hide();
|
||||||
@ -1876,7 +1875,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void updateLinkPreviewState() {
|
private void updateLinkPreviewState() {
|
||||||
if (TextSecurePreferences.isLinkPreviewsEnabled(this) && !attachmentManager.isAttachmentPresent()) {
|
if (TextSecurePreferences.isLinkPreviewsEnabled(this)) {
|
||||||
linkPreviewViewModel.onEnabled();
|
linkPreviewViewModel.onEnabled();
|
||||||
linkPreviewViewModel.onTextChanged(this, composeText.getTextTrimmed(), composeText.getSelectionStart(), composeText.getSelectionEnd());
|
linkPreviewViewModel.onTextChanged(this, composeText.getTextTrimmed(), composeText.getSelectionStart(), composeText.getSelectionEnd());
|
||||||
} else {
|
} else {
|
||||||
|
@ -5,19 +5,26 @@ import android.animation.ValueAnimator
|
|||||||
import android.app.Activity
|
import android.app.Activity
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
|
import android.content.Intent
|
||||||
import android.content.res.Resources
|
import android.content.res.Resources
|
||||||
import android.database.Cursor
|
import android.database.Cursor
|
||||||
import android.graphics.Rect
|
import android.graphics.Rect
|
||||||
import android.graphics.Typeface
|
import android.graphics.Typeface
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
import android.net.Uri
|
||||||
|
import android.os.*
|
||||||
|
import android.util.Log
|
||||||
|
import android.util.Pair
|
||||||
import android.util.TypedValue
|
import android.util.TypedValue
|
||||||
import android.view.*
|
import android.view.*
|
||||||
import android.widget.RelativeLayout
|
import android.widget.RelativeLayout
|
||||||
|
import android.widget.Toast
|
||||||
import androidx.core.view.isVisible
|
import androidx.core.view.isVisible
|
||||||
import androidx.lifecycle.ViewModelProviders
|
import androidx.lifecycle.ViewModelProviders
|
||||||
import androidx.loader.app.LoaderManager
|
import androidx.loader.app.LoaderManager
|
||||||
import androidx.loader.content.Loader
|
import androidx.loader.content.Loader
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import kotlinx.android.synthetic.main.activity_conversation_v2.*
|
import kotlinx.android.synthetic.main.activity_conversation_v2.*
|
||||||
import kotlinx.android.synthetic.main.activity_conversation_v2.view.*
|
import kotlinx.android.synthetic.main.activity_conversation_v2.view.*
|
||||||
import kotlinx.android.synthetic.main.activity_conversation_v2_action_bar.*
|
import kotlinx.android.synthetic.main.activity_conversation_v2_action_bar.*
|
||||||
@ -29,11 +36,22 @@ import kotlinx.android.synthetic.main.view_input_bar_recording.view.*
|
|||||||
import network.loki.messenger.R
|
import network.loki.messenger.R
|
||||||
import nl.komponents.kovenant.ui.successUi
|
import nl.komponents.kovenant.ui.successUi
|
||||||
import org.session.libsession.messaging.contacts.Contact
|
import org.session.libsession.messaging.contacts.Contact
|
||||||
|
import org.session.libsession.messaging.mentions.Mention
|
||||||
import org.session.libsession.messaging.mentions.MentionsManager
|
import org.session.libsession.messaging.mentions.MentionsManager
|
||||||
|
import org.session.libsession.messaging.messages.signal.OutgoingMediaMessage
|
||||||
|
import org.session.libsession.messaging.messages.signal.OutgoingTextMessage
|
||||||
|
import org.session.libsession.messaging.messages.visible.VisibleMessage
|
||||||
import org.session.libsession.messaging.open_groups.OpenGroupAPIV2
|
import org.session.libsession.messaging.open_groups.OpenGroupAPIV2
|
||||||
|
import org.session.libsession.messaging.sending_receiving.MessageSender
|
||||||
|
import org.session.libsession.messaging.sending_receiving.attachments.Attachment
|
||||||
|
import org.session.libsession.utilities.MediaTypes
|
||||||
|
import org.session.libsession.utilities.ServiceUtil
|
||||||
import org.session.libsession.utilities.TextSecurePreferences
|
import org.session.libsession.utilities.TextSecurePreferences
|
||||||
|
import org.session.libsignal.utilities.ListenableFuture
|
||||||
import org.thoughtcrime.securesms.ApplicationContext
|
import org.thoughtcrime.securesms.ApplicationContext
|
||||||
import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity
|
import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity
|
||||||
|
import org.thoughtcrime.securesms.audio.AudioRecorder
|
||||||
|
import org.thoughtcrime.securesms.contactshare.SimpleTextWatcher
|
||||||
import org.thoughtcrime.securesms.conversation.v2.dialogs.*
|
import org.thoughtcrime.securesms.conversation.v2.dialogs.*
|
||||||
import org.thoughtcrime.securesms.conversation.v2.input_bar.InputBarButton
|
import org.thoughtcrime.securesms.conversation.v2.input_bar.InputBarButton
|
||||||
import org.thoughtcrime.securesms.conversation.v2.input_bar.InputBarDelegate
|
import org.thoughtcrime.securesms.conversation.v2.input_bar.InputBarDelegate
|
||||||
@ -42,10 +60,12 @@ import org.thoughtcrime.securesms.conversation.v2.input_bar.mentions.MentionCand
|
|||||||
import org.thoughtcrime.securesms.conversation.v2.menus.ConversationActionModeCallback
|
import org.thoughtcrime.securesms.conversation.v2.menus.ConversationActionModeCallback
|
||||||
import org.thoughtcrime.securesms.conversation.v2.menus.ConversationMenuHelper
|
import org.thoughtcrime.securesms.conversation.v2.menus.ConversationMenuHelper
|
||||||
import org.thoughtcrime.securesms.conversation.v2.messages.VisibleMessageView
|
import org.thoughtcrime.securesms.conversation.v2.messages.VisibleMessageView
|
||||||
|
import org.thoughtcrime.securesms.conversation.v2.utilities.AttachmentManager
|
||||||
import org.thoughtcrime.securesms.database.DatabaseFactory
|
import org.thoughtcrime.securesms.database.DatabaseFactory
|
||||||
import org.thoughtcrime.securesms.database.DraftDatabase
|
import org.thoughtcrime.securesms.database.DraftDatabase
|
||||||
import org.thoughtcrime.securesms.database.DraftDatabase.Drafts
|
import org.thoughtcrime.securesms.database.DraftDatabase.Drafts
|
||||||
import org.thoughtcrime.securesms.database.model.MessageRecord
|
import org.thoughtcrime.securesms.database.model.MessageRecord
|
||||||
|
import org.thoughtcrime.securesms.giph.ui.GiphyActivity
|
||||||
import org.thoughtcrime.securesms.linkpreview.LinkPreviewRepository
|
import org.thoughtcrime.securesms.linkpreview.LinkPreviewRepository
|
||||||
import org.thoughtcrime.securesms.linkpreview.LinkPreviewViewModel
|
import org.thoughtcrime.securesms.linkpreview.LinkPreviewViewModel
|
||||||
import org.thoughtcrime.securesms.linkpreview.LinkPreviewViewModel.LinkPreviewState
|
import org.thoughtcrime.securesms.linkpreview.LinkPreviewViewModel.LinkPreviewState
|
||||||
@ -53,9 +73,15 @@ import org.thoughtcrime.securesms.loki.utilities.ActivityDispatcher
|
|||||||
import org.thoughtcrime.securesms.loki.utilities.push
|
import org.thoughtcrime.securesms.loki.utilities.push
|
||||||
import org.thoughtcrime.securesms.loki.utilities.show
|
import org.thoughtcrime.securesms.loki.utilities.show
|
||||||
import org.thoughtcrime.securesms.loki.utilities.toPx
|
import org.thoughtcrime.securesms.loki.utilities.toPx
|
||||||
import org.thoughtcrime.securesms.mms.GlideApp
|
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.providers.BlobProvider
|
||||||
import org.thoughtcrime.securesms.util.DateUtils
|
import org.thoughtcrime.securesms.util.DateUtils
|
||||||
|
import org.thoughtcrime.securesms.util.MediaUtil
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
import java.util.concurrent.ExecutionException
|
||||||
import kotlin.math.*
|
import kotlin.math.*
|
||||||
|
|
||||||
// Some things that seemingly belong to the input bar (e.g. the voice message recording UI) are actually
|
// Some things that seemingly belong to the input bar (e.g. the voice message recording UI) are actually
|
||||||
@ -63,16 +89,28 @@ import kotlin.math.*
|
|||||||
// price we pay is a bit of back and forth between the input bar and the conversation activity.
|
// price we pay is a bit of back and forth between the input bar and the conversation activity.
|
||||||
|
|
||||||
class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDelegate,
|
class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDelegate,
|
||||||
InputBarRecordingViewDelegate, ConversationRecyclerViewDelegate, ActivityDispatcher {
|
InputBarRecordingViewDelegate, AttachmentManager.AttachmentListener, ActivityDispatcher {
|
||||||
private val scrollButtonFullVisibilityThreshold by lazy { toPx(120.0f, resources) }
|
|
||||||
private val scrollButtonNoVisibilityThreshold by lazy { toPx(20.0f, resources) }
|
|
||||||
private val screenWidth = Resources.getSystem().displayMetrics.widthPixels
|
private val screenWidth = Resources.getSystem().displayMetrics.widthPixels
|
||||||
private var linkPreviewViewModel: LinkPreviewViewModel? = null
|
private var linkPreviewViewModel: LinkPreviewViewModel? = null
|
||||||
private var threadID: Long = -1
|
private var threadID: Long = -1
|
||||||
private var actionMode: ActionMode? = null
|
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 isLockViewExpanded = false
|
||||||
private var isShowingAttachmentOptions = false
|
private var isShowingAttachmentOptions = false
|
||||||
|
// Mentions
|
||||||
|
private val mentions = mutableListOf<Mention>()
|
||||||
private var mentionCandidatesView: MentionCandidatesView? = null
|
private var mentionCandidatesView: MentionCandidatesView? = null
|
||||||
|
private var previousText: CharSequence = ""
|
||||||
|
private var currentMentionStartIndex = -1
|
||||||
|
private var isShowingMentionCandidatesView = false
|
||||||
|
|
||||||
|
private val layoutManager: LinearLayoutManager
|
||||||
|
get() { return conversationRecyclerView.layoutManager as LinearLayoutManager }
|
||||||
|
|
||||||
// TODO: Selected message background color
|
// TODO: Selected message background color
|
||||||
// TODO: Overflow menu background + text color
|
// TODO: Overflow menu background + text color
|
||||||
@ -110,6 +148,10 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
|
|||||||
// region Settings
|
// region Settings
|
||||||
companion object {
|
companion object {
|
||||||
const val THREAD_ID = "thread_id"
|
const val THREAD_ID = "thread_id"
|
||||||
|
const val PICK_DOCUMENT = 2
|
||||||
|
const val TAKE_PHOTO = 7
|
||||||
|
const val PICK_GIF = 10
|
||||||
|
const val PICK_FROM_LIBRARY = 12
|
||||||
}
|
}
|
||||||
// endregion
|
// endregion
|
||||||
|
|
||||||
@ -124,13 +166,14 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
|
|||||||
restoreDraftIfNeeded()
|
restoreDraftIfNeeded()
|
||||||
addOpenGroupGuidelinesIfNeeded()
|
addOpenGroupGuidelinesIfNeeded()
|
||||||
scrollToBottomButton.setOnClickListener { conversationRecyclerView.smoothScrollToPosition(0) }
|
scrollToBottomButton.setOnClickListener { conversationRecyclerView.smoothScrollToPosition(0) }
|
||||||
updateUnreadCount()
|
unreadCount = DatabaseFactory.getMmsSmsDatabase(this).getUnreadCount(threadID)
|
||||||
|
updateUnreadCountIndicator()
|
||||||
setUpTypingObserver()
|
setUpTypingObserver()
|
||||||
updateSubtitle()
|
updateSubtitle()
|
||||||
getLatestOpenGroupInfoIfNeeded()
|
getLatestOpenGroupInfoIfNeeded()
|
||||||
setUpBlockedBanner()
|
setUpBlockedBanner()
|
||||||
setUpLinkPreviewObserver()
|
setUpLinkPreviewObserver()
|
||||||
scrollToFirstUnreadMessage()
|
scrollToFirstUnreadMessageIfNeeded()
|
||||||
markAllAsRead()
|
markAllAsRead()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -160,7 +203,6 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
|
|||||||
conversationRecyclerView.adapter = adapter
|
conversationRecyclerView.adapter = adapter
|
||||||
val layoutManager = LinearLayoutManager(this, LinearLayoutManager.VERTICAL, true)
|
val layoutManager = LinearLayoutManager(this, LinearLayoutManager.VERTICAL, true)
|
||||||
conversationRecyclerView.layoutManager = layoutManager
|
conversationRecyclerView.layoutManager = layoutManager
|
||||||
conversationRecyclerView.delegate = this
|
|
||||||
// Workaround for the fact that CursorRecyclerViewAdapter doesn't auto-update automatically (even though it says it will)
|
// Workaround for the fact that CursorRecyclerViewAdapter doesn't auto-update automatically (even though it says it will)
|
||||||
LoaderManager.getInstance(this).restartLoader(0, null, object : LoaderManager.LoaderCallbacks<Cursor> {
|
LoaderManager.getInstance(this).restartLoader(0, null, object : LoaderManager.LoaderCallbacks<Cursor> {
|
||||||
|
|
||||||
@ -176,6 +218,12 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
|
|||||||
adapter.changeCursor(null)
|
adapter.changeCursor(null)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
conversationRecyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener() {
|
||||||
|
|
||||||
|
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
|
||||||
|
handleRecyclerViewScrolled()
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun setUpToolBar() {
|
private fun setUpToolBar() {
|
||||||
@ -193,15 +241,19 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
|
|||||||
// GIF button
|
// GIF button
|
||||||
gifButtonContainer.addView(gifButton)
|
gifButtonContainer.addView(gifButton)
|
||||||
gifButton.layoutParams = RelativeLayout.LayoutParams(RelativeLayout.LayoutParams.MATCH_PARENT, RelativeLayout.LayoutParams.MATCH_PARENT)
|
gifButton.layoutParams = RelativeLayout.LayoutParams(RelativeLayout.LayoutParams.MATCH_PARENT, RelativeLayout.LayoutParams.MATCH_PARENT)
|
||||||
|
gifButton.onUp = { showGIFPicker() }
|
||||||
// Document button
|
// Document button
|
||||||
documentButtonContainer.addView(documentButton)
|
documentButtonContainer.addView(documentButton)
|
||||||
documentButton.layoutParams = RelativeLayout.LayoutParams(RelativeLayout.LayoutParams.MATCH_PARENT, RelativeLayout.LayoutParams.MATCH_PARENT)
|
documentButton.layoutParams = RelativeLayout.LayoutParams(RelativeLayout.LayoutParams.MATCH_PARENT, RelativeLayout.LayoutParams.MATCH_PARENT)
|
||||||
|
documentButton.onUp = { showDocumentPicker() }
|
||||||
// Library button
|
// Library button
|
||||||
libraryButtonContainer.addView(libraryButton)
|
libraryButtonContainer.addView(libraryButton)
|
||||||
libraryButton.layoutParams = RelativeLayout.LayoutParams(RelativeLayout.LayoutParams.MATCH_PARENT, RelativeLayout.LayoutParams.MATCH_PARENT)
|
libraryButton.layoutParams = RelativeLayout.LayoutParams(RelativeLayout.LayoutParams.MATCH_PARENT, RelativeLayout.LayoutParams.MATCH_PARENT)
|
||||||
|
libraryButton.onUp = { pickFromLibrary() }
|
||||||
// Camera button
|
// Camera button
|
||||||
cameraButtonContainer.addView(cameraButton)
|
cameraButtonContainer.addView(cameraButton)
|
||||||
cameraButton.layoutParams = RelativeLayout.LayoutParams(RelativeLayout.LayoutParams.MATCH_PARENT, RelativeLayout.LayoutParams.MATCH_PARENT)
|
cameraButton.layoutParams = RelativeLayout.LayoutParams(RelativeLayout.LayoutParams.MATCH_PARENT, RelativeLayout.LayoutParams.MATCH_PARENT)
|
||||||
|
cameraButton.onUp = { showCamera() }
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun restoreDraftIfNeeded() {
|
private fun restoreDraftIfNeeded() {
|
||||||
@ -230,6 +282,14 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
|
|||||||
typingIndicatorViewContainer.setTypists(recipients)
|
typingIndicatorViewContainer.setTypists(recipients)
|
||||||
inputBarHeightChanged(inputBar.height)
|
inputBarHeightChanged(inputBar.height)
|
||||||
}
|
}
|
||||||
|
if (TextSecurePreferences.isTypingIndicatorsEnabled(this)) {
|
||||||
|
inputBar.inputBarEditText.addTextChangedListener(object : SimpleTextWatcher() {
|
||||||
|
|
||||||
|
override fun onTextChanged(text: String?) {
|
||||||
|
ApplicationContext.getInstance(this@ConversationActivityV2).typingStatusSender.onTypingStarted(threadID)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getLatestOpenGroupInfoIfNeeded() {
|
private fun getLatestOpenGroupInfoIfNeeded() {
|
||||||
@ -266,9 +326,10 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun scrollToFirstUnreadMessage() {
|
private fun scrollToFirstUnreadMessageIfNeeded() {
|
||||||
val lastSeenTimestamp = DatabaseFactory.getThreadDatabase(this).getLastSeenAndHasSent(threadID).first()
|
val lastSeenTimestamp = DatabaseFactory.getThreadDatabase(this).getLastSeenAndHasSent(threadID).first()
|
||||||
val lastSeenItemPosition = adapter.findLastSeenItemPosition(lastSeenTimestamp) ?: return
|
val lastSeenItemPosition = adapter.findLastSeenItemPosition(lastSeenTimestamp) ?: return
|
||||||
|
if (lastSeenItemPosition <= 3) { return }
|
||||||
conversationRecyclerView.scrollToPosition(lastSeenItemPosition)
|
conversationRecyclerView.scrollToPosition(lastSeenItemPosition)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -286,7 +347,15 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
|
|||||||
|
|
||||||
// region Updating & Animation
|
// region Updating & Animation
|
||||||
private fun markAllAsRead() {
|
private fun markAllAsRead() {
|
||||||
DatabaseFactory.getThreadDatabase(this).setRead(threadID, true)
|
val messages = DatabaseFactory.getThreadDatabase(this).setRead(threadID, true)
|
||||||
|
if (thread.isGroupRecipient) {
|
||||||
|
for (message in messages) {
|
||||||
|
MarkReadReceiver.scheduleDeletion(this, message.expirationInfo)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
MarkReadReceiver.process(this, messages)
|
||||||
|
}
|
||||||
|
ApplicationContext.getInstance(this).messageNotifier.updateNotification(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun inputBarHeightChanged(newValue: Int) {
|
override fun inputBarHeightChanged(newValue: Int) {
|
||||||
@ -296,7 +365,7 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
|
|||||||
val typingIndicatorHeight = if (typingIndicatorViewContainer.isVisible) toPx(36, resources) else 0
|
val typingIndicatorHeight = if (typingIndicatorViewContainer.isVisible) toPx(36, resources) else 0
|
||||||
// Recycler view
|
// Recycler view
|
||||||
val recyclerViewLayoutParams = conversationRecyclerView.layoutParams as RelativeLayout.LayoutParams
|
val recyclerViewLayoutParams = conversationRecyclerView.layoutParams as RelativeLayout.LayoutParams
|
||||||
recyclerViewLayoutParams.bottomMargin = newValue + additionalContentContainer.height + typingIndicatorHeight
|
recyclerViewLayoutParams.bottomMargin = newValue + typingIndicatorHeight
|
||||||
conversationRecyclerView.layoutParams = recyclerViewLayoutParams
|
conversationRecyclerView.layoutParams = recyclerViewLayoutParams
|
||||||
// Additional content container
|
// Additional content container
|
||||||
val additionalContentContainerLayoutParams = additionalContentContainer.layoutParams as RelativeLayout.LayoutParams
|
val additionalContentContainerLayoutParams = additionalContentContainer.layoutParams as RelativeLayout.LayoutParams
|
||||||
@ -317,40 +386,77 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
|
|||||||
|
|
||||||
override fun inputBarEditTextContentChanged(newContent: CharSequence) {
|
override fun inputBarEditTextContentChanged(newContent: CharSequence) {
|
||||||
linkPreviewViewModel?.onTextChanged(this, inputBar.text, 0, 0)
|
linkPreviewViewModel?.onTextChanged(this, inputBar.text, 0, 0)
|
||||||
// TODO: Implement the full mention show/hide logic
|
showOrHideMentionCandidatesIfNeeded(newContent)
|
||||||
if (newContent.contains("@")) {
|
|
||||||
showMentionCandidates()
|
|
||||||
} else {
|
|
||||||
hideMentionCandidates()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun showMentionCandidates() {
|
private fun showOrHideMentionCandidatesIfNeeded(text: CharSequence) {
|
||||||
additionalContentContainer.removeAllViews()
|
if (text.length < previousText.length) {
|
||||||
val mentionCandidatesView = MentionCandidatesView(this)
|
currentMentionStartIndex = -1
|
||||||
mentionCandidatesView.glide = glide
|
hideMentionCandidates()
|
||||||
additionalContentContainer.addView(mentionCandidatesView)
|
val mentionsToRemove = mentions.filter { !text.contains(it.displayName) }
|
||||||
val mentionCandidates = MentionsManager.getMentionCandidates("", threadID, thread.isOpenGroupRecipient)
|
mentions.removeAll(mentionsToRemove)
|
||||||
this.mentionCandidatesView = mentionCandidatesView
|
|
||||||
mentionCandidatesView.show(mentionCandidates, threadID)
|
|
||||||
mentionCandidatesView.alpha = 0.0f
|
|
||||||
val animation = ValueAnimator.ofObject(FloatEvaluator(), mentionCandidatesView.alpha, 1.0f)
|
|
||||||
animation.duration = 250L
|
|
||||||
animation.addUpdateListener { animator ->
|
|
||||||
mentionCandidatesView.alpha = animator.animatedValue as Float
|
|
||||||
}
|
}
|
||||||
animation.start()
|
if (text.isNotEmpty()) {
|
||||||
|
val lastCharIndex = text.lastIndex
|
||||||
|
val lastChar = text[lastCharIndex]
|
||||||
|
// Check if there is whitespace before the '@' or the '@' is the first character
|
||||||
|
val isCharacterBeforeLastWhiteSpaceOrStartOfLine: Boolean
|
||||||
|
if (text.length == 1) {
|
||||||
|
isCharacterBeforeLastWhiteSpaceOrStartOfLine = true // Start of line
|
||||||
|
} else {
|
||||||
|
val charBeforeLast = text[lastCharIndex - 1]
|
||||||
|
isCharacterBeforeLastWhiteSpaceOrStartOfLine = Character.isWhitespace(charBeforeLast)
|
||||||
|
}
|
||||||
|
if (lastChar == '@' && isCharacterBeforeLastWhiteSpaceOrStartOfLine) {
|
||||||
|
currentMentionStartIndex = lastCharIndex
|
||||||
|
showOrUpdateMentionCandidatesIfNeeded()
|
||||||
|
} else if (Character.isWhitespace(lastChar) || lastChar == '@') { // the lastCharacter == "@" is to check for @@
|
||||||
|
currentMentionStartIndex = -1
|
||||||
|
hideMentionCandidates()
|
||||||
|
} else if (currentMentionStartIndex != -1) {
|
||||||
|
val query = text.substring(currentMentionStartIndex + 1) // + 1 to get rid of the "@"
|
||||||
|
showOrUpdateMentionCandidatesIfNeeded(query)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
previousText = text
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun showOrUpdateMentionCandidatesIfNeeded(query: String = "") {
|
||||||
|
if (!isShowingMentionCandidatesView) {
|
||||||
|
additionalContentContainer.removeAllViews()
|
||||||
|
val view = MentionCandidatesView(this)
|
||||||
|
view.glide = glide
|
||||||
|
view.onCandidateSelected = { handleMentionSelected(it) }
|
||||||
|
additionalContentContainer.addView(view)
|
||||||
|
val candidates = MentionsManager.getMentionCandidates(query, threadID, thread.isOpenGroupRecipient)
|
||||||
|
this.mentionCandidatesView = view
|
||||||
|
view.show(candidates, threadID)
|
||||||
|
view.alpha = 0.0f
|
||||||
|
val animation = ValueAnimator.ofObject(FloatEvaluator(), view.alpha, 1.0f)
|
||||||
|
animation.duration = 250L
|
||||||
|
animation.addUpdateListener { animator ->
|
||||||
|
view.alpha = animator.animatedValue as Float
|
||||||
|
}
|
||||||
|
animation.start()
|
||||||
|
} else {
|
||||||
|
val candidates = MentionsManager.getMentionCandidates(query, threadID, thread.isOpenGroupRecipient)
|
||||||
|
this.mentionCandidatesView!!.setMentionCandidates(candidates)
|
||||||
|
}
|
||||||
|
isShowingMentionCandidatesView = true
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun hideMentionCandidates() {
|
private fun hideMentionCandidates() {
|
||||||
val mentionCandidatesView = mentionCandidatesView ?: return
|
if (isShowingMentionCandidatesView) {
|
||||||
val animation = ValueAnimator.ofObject(FloatEvaluator(), mentionCandidatesView.alpha, 0.0f)
|
val mentionCandidatesView = mentionCandidatesView ?: return
|
||||||
animation.duration = 250L
|
val animation = ValueAnimator.ofObject(FloatEvaluator(), mentionCandidatesView.alpha, 0.0f)
|
||||||
animation.addUpdateListener { animator ->
|
animation.duration = 250L
|
||||||
mentionCandidatesView.alpha = animator.animatedValue as Float
|
animation.addUpdateListener { animator ->
|
||||||
if (animator.animatedFraction == 1.0f) { additionalContentContainer.removeAllViews() }
|
mentionCandidatesView.alpha = animator.animatedValue as Float
|
||||||
|
if (animator.animatedFraction == 1.0f) { additionalContentContainer.removeAllViews() }
|
||||||
|
}
|
||||||
|
animation.start()
|
||||||
}
|
}
|
||||||
animation.start()
|
isShowingMentionCandidatesView = false
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun toggleAttachmentOptions() {
|
override fun toggleAttachmentOptions() {
|
||||||
@ -426,16 +532,15 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
|
|||||||
animation.start()
|
animation.start()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun handleConversationRecyclerViewBottomOffsetChanged(bottomOffset: Int) {
|
private fun handleRecyclerViewScrolled() {
|
||||||
val rawAlpha = (bottomOffset.toFloat() - scrollButtonNoVisibilityThreshold) /
|
val position = layoutManager.findFirstCompletelyVisibleItemPosition()
|
||||||
(scrollButtonFullVisibilityThreshold - scrollButtonNoVisibilityThreshold)
|
val alpha = if (position > 0) 1.0f else 0.0f
|
||||||
val alpha = max(min(rawAlpha, 1.0f), 0.0f)
|
|
||||||
scrollToBottomButton.alpha = alpha
|
scrollToBottomButton.alpha = alpha
|
||||||
updateUnreadCount()
|
unreadCount = min(unreadCount, layoutManager.findFirstVisibleItemPosition())
|
||||||
|
updateUnreadCountIndicator()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun updateUnreadCount() {
|
private fun updateUnreadCountIndicator() {
|
||||||
val unreadCount = DatabaseFactory.getMmsSmsDatabase(this).getUnreadCount(threadID)
|
|
||||||
val formattedUnreadCount = if (unreadCount < 100) unreadCount.toString() else "99+"
|
val formattedUnreadCount = if (unreadCount < 100) unreadCount.toString() else "99+"
|
||||||
unreadCountTextView.text = formattedUnreadCount
|
unreadCountTextView.text = formattedUnreadCount
|
||||||
val textSize = if (unreadCount < 100) 12.0f else 9.0f
|
val textSize = if (unreadCount < 100) 12.0f else 9.0f
|
||||||
@ -550,10 +655,20 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onMicrophoneButtonUp(event: MotionEvent) {
|
override fun onMicrophoneButtonUp(event: MotionEvent) {
|
||||||
if (isValidLockViewLocation(event.rawX.roundToInt(), event.rawY.roundToInt())) {
|
val x = event.rawX.roundToInt()
|
||||||
|
val y = event.rawY.roundToInt()
|
||||||
|
if (isValidLockViewLocation(x, y)) {
|
||||||
inputBarRecordingView.lock()
|
inputBarRecordingView.lock()
|
||||||
} else {
|
} else {
|
||||||
hideVoiceMessageUI()
|
val recordButtonOverlay = inputBarRecordingView.recordButtonOverlay
|
||||||
|
val location = IntArray(2) { 0 }
|
||||||
|
recordButtonOverlay.getLocationOnScreen(location)
|
||||||
|
val hitRect = Rect(location[0], location[1], location[0] + recordButtonOverlay.width, location[1] + recordButtonOverlay.height)
|
||||||
|
if (hitRect.contains(x, y)) {
|
||||||
|
sendVoiceMessage()
|
||||||
|
} else {
|
||||||
|
cancelVoiceMessage()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -570,9 +685,197 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
|
|||||||
private fun unblock() {
|
private fun unblock() {
|
||||||
// TODO: Implement
|
// TODO: Implement
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun handleMentionSelected(mention: Mention) {
|
||||||
|
if (currentMentionStartIndex == -1) { return }
|
||||||
|
mentions.add(mention)
|
||||||
|
val previousText = inputBar.text
|
||||||
|
val newText = previousText.substring(0, currentMentionStartIndex) + "@" + mention.displayName + " "
|
||||||
|
inputBar.text = newText
|
||||||
|
inputBar.inputBarEditText.setSelection(newText.length)
|
||||||
|
currentMentionStartIndex = -1
|
||||||
|
hideMentionCandidates()
|
||||||
|
this.previousText = newText
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun sendMessage() {
|
||||||
|
// Create the message
|
||||||
|
val message = VisibleMessage()
|
||||||
|
message.sentTimestamp = System.currentTimeMillis()
|
||||||
|
message.text = getMessageBody()
|
||||||
|
val outgoingTextMessage = OutgoingTextMessage.from(message, thread)
|
||||||
|
// Clear the input bar
|
||||||
|
inputBar.text = ""
|
||||||
|
// Clear mentions
|
||||||
|
previousText = ""
|
||||||
|
currentMentionStartIndex = -1
|
||||||
|
mentions.clear()
|
||||||
|
// Put the message in the database
|
||||||
|
message.id = DatabaseFactory.getSmsDatabase(this).insertMessageOutbox(threadID, outgoingTextMessage, false, message.sentTimestamp!!) { }
|
||||||
|
// Send it
|
||||||
|
MessageSender.send(message, thread.address)
|
||||||
|
// Send a typing stopped message
|
||||||
|
ApplicationContext.getInstance(this).typingStatusSender.onTypingStopped(threadID)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun sendAttachments(attachments: List<Attachment>, body: String?) {
|
||||||
|
// TODO: Quotes & link previews
|
||||||
|
// Create the message
|
||||||
|
val message = VisibleMessage()
|
||||||
|
message.sentTimestamp = System.currentTimeMillis()
|
||||||
|
message.text = body
|
||||||
|
val outgoingTextMessage = OutgoingMediaMessage.from(message, thread, attachments, null, null)
|
||||||
|
// Clear the input bar
|
||||||
|
inputBar.text = ""
|
||||||
|
// 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, null, null)
|
||||||
|
// Send a typing stopped message
|
||||||
|
ApplicationContext.getInstance(this).typingStatusSender.onTypingStopped(threadID)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun showGIFPicker() {
|
||||||
|
AttachmentManager.selectGif(this, ConversationActivityV2.PICK_GIF)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun showDocumentPicker() {
|
||||||
|
AttachmentManager.selectDocument(this, ConversationActivityV2.PICK_DOCUMENT)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun pickFromLibrary() {
|
||||||
|
AttachmentManager.selectGallery(this, ConversationActivityV2.PICK_FROM_LIBRARY, thread, inputBar.text.trim())
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun showCamera() {
|
||||||
|
attachmentManager.capturePhoto(this, ConversationActivityV2.TAKE_PHOTO)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onAttachmentChanged() {
|
||||||
|
// Do nothing
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onActivityResult(requestCode: Int, resultCode: Int, intent: Intent?) {
|
||||||
|
super.onActivityResult(requestCode, resultCode, intent)
|
||||||
|
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 uri = intent?.data ?: return
|
||||||
|
prepMediaForSending(uri, AttachmentManager.MediaType.DOCUMENT).addListener(mediaPreppedListener)
|
||||||
|
}
|
||||||
|
TAKE_PHOTO -> {
|
||||||
|
val uri = attachmentManager.captureUri ?: return
|
||||||
|
prepMediaForSending(uri, AttachmentManager.MediaType.IMAGE).addListener(mediaPreppedListener)
|
||||||
|
}
|
||||||
|
PICK_GIF -> {
|
||||||
|
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 -> {
|
||||||
|
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) {
|
||||||
|
when {
|
||||||
|
MediaUtil.isVideoType(item.mimeType) -> {
|
||||||
|
slideDeck.addSlide(VideoSlide(this, item.uri, 0, item.caption.orNull()))
|
||||||
|
}
|
||||||
|
MediaUtil.isGif(item.mimeType) -> {
|
||||||
|
slideDeck.addSlide(GifSlide(this, item.uri, 0, item.width, item.height, item.caption.orNull()))
|
||||||
|
}
|
||||||
|
MediaUtil.isImageType(item.mimeType) -> {
|
||||||
|
slideDeck.addSlide(ImageSlide(this, item.uri, 0, item.width, item.height, item.caption.orNull()))
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
Log.d("Loki", "Asked to send an unexpected media type: '" + item.mimeType + "'. Skipping.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sendAttachments(slideDeck.asAttachments(), body)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
// endregion
|
// endregion
|
||||||
|
|
||||||
// region General
|
// region General
|
||||||
|
private fun getMessageBody(): String {
|
||||||
|
var result = inputBar.inputBarEditText.text?.trim() ?: ""
|
||||||
|
for (mention in mentions) {
|
||||||
|
try {
|
||||||
|
val startIndex = result.indexOf("@" + mention.displayName)
|
||||||
|
val endIndex = startIndex + mention.displayName.count() + 1 // + 1 to include the "@"
|
||||||
|
result = result.substring(0, startIndex) + "@" + mention.publicKey + result.substring(endIndex)
|
||||||
|
} catch (exception: Exception) {
|
||||||
|
Log.d("Loki", "Failed to process mention due to error: $exception")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result.toString()
|
||||||
|
}
|
||||||
|
|
||||||
private fun saveDraft() {
|
private fun saveDraft() {
|
||||||
val text = inputBar.text.trim()
|
val text = inputBar.text.trim()
|
||||||
if (text.isEmpty()) { return }
|
if (text.isEmpty()) { return }
|
||||||
|
@ -17,7 +17,6 @@ class ConversationRecyclerView : RecyclerView {
|
|||||||
private val maxLongPressVelocityY = toPx(10, resources)
|
private val maxLongPressVelocityY = toPx(10, resources)
|
||||||
private val minSwipeVelocityX = toPx(10, resources)
|
private val minSwipeVelocityX = toPx(10, resources)
|
||||||
private var velocityTracker: VelocityTracker? = null
|
private var velocityTracker: VelocityTracker? = null
|
||||||
var delegate: ConversationRecyclerViewDelegate? = null
|
|
||||||
|
|
||||||
constructor(context: Context) : super(context) { initialize() }
|
constructor(context: Context) : super(context) { initialize() }
|
||||||
constructor(context: Context, attrs: AttributeSet) : super(context, attrs) { initialize() }
|
constructor(context: Context, attrs: AttributeSet) : super(context, attrs) { initialize() }
|
||||||
@ -25,18 +24,6 @@ class ConversationRecyclerView : RecyclerView {
|
|||||||
|
|
||||||
private fun initialize() {
|
private fun initialize() {
|
||||||
disableClipping()
|
disableClipping()
|
||||||
addOnScrollListener(object : RecyclerView.OnScrollListener() {
|
|
||||||
private var bottomOffset = 0
|
|
||||||
|
|
||||||
override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
|
|
||||||
// Do nothing
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
|
|
||||||
bottomOffset += dy // FIXME: Not sure this is fully accurate, but it seems close enough
|
|
||||||
delegate?.handleConversationRecyclerViewBottomOffsetChanged(abs(bottomOffset))
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onInterceptTouchEvent(e: MotionEvent): Boolean {
|
override fun onInterceptTouchEvent(e: MotionEvent): Boolean {
|
||||||
@ -66,8 +53,3 @@ class ConversationRecyclerView : RecyclerView {
|
|||||||
return super.dispatchTouchEvent(e)
|
return super.dispatchTouchEvent(e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ConversationRecyclerViewDelegate {
|
|
||||||
|
|
||||||
fun handleConversationRecyclerViewBottomOffsetChanged(bottomOffset: Int)
|
|
||||||
}
|
|
@ -8,7 +8,6 @@ import android.view.MotionEvent
|
|||||||
import android.widget.RelativeLayout
|
import android.widget.RelativeLayout
|
||||||
import androidx.core.view.isVisible
|
import androidx.core.view.isVisible
|
||||||
import kotlinx.android.synthetic.main.view_input_bar.view.*
|
import kotlinx.android.synthetic.main.view_input_bar.view.*
|
||||||
import kotlinx.android.synthetic.main.view_quote.view.*
|
|
||||||
import network.loki.messenger.R
|
import network.loki.messenger.R
|
||||||
import org.session.libsession.messaging.sending_receiving.link_preview.LinkPreview
|
import org.session.libsession.messaging.sending_receiving.link_preview.LinkPreview
|
||||||
import org.thoughtcrime.securesms.conversation.v2.components.LinkPreviewDraftView
|
import org.thoughtcrime.securesms.conversation.v2.components.LinkPreviewDraftView
|
||||||
@ -53,7 +52,7 @@ class InputBar : RelativeLayout, InputBarEditTextDelegate, QuoteViewDelegate, Li
|
|||||||
// Microphone button
|
// Microphone button
|
||||||
microphoneOrSendButtonContainer.addView(microphoneButton)
|
microphoneOrSendButtonContainer.addView(microphoneButton)
|
||||||
microphoneButton.layoutParams = RelativeLayout.LayoutParams(RelativeLayout.LayoutParams.MATCH_PARENT, RelativeLayout.LayoutParams.MATCH_PARENT)
|
microphoneButton.layoutParams = RelativeLayout.LayoutParams(RelativeLayout.LayoutParams.MATCH_PARENT, RelativeLayout.LayoutParams.MATCH_PARENT)
|
||||||
microphoneButton.onLongPress = { showVoiceMessageUI() }
|
microphoneButton.onLongPress = { startRecordingVoiceMessage() }
|
||||||
microphoneButton.onMove = { delegate?.onMicrophoneButtonMove(it) }
|
microphoneButton.onMove = { delegate?.onMicrophoneButtonMove(it) }
|
||||||
microphoneButton.onCancel = { delegate?.onMicrophoneButtonCancel(it) }
|
microphoneButton.onCancel = { delegate?.onMicrophoneButtonCancel(it) }
|
||||||
microphoneButton.onUp = { delegate?.onMicrophoneButtonUp(it) }
|
microphoneButton.onUp = { delegate?.onMicrophoneButtonUp(it) }
|
||||||
@ -61,6 +60,7 @@ class InputBar : RelativeLayout, InputBarEditTextDelegate, QuoteViewDelegate, Li
|
|||||||
microphoneOrSendButtonContainer.addView(sendButton)
|
microphoneOrSendButtonContainer.addView(sendButton)
|
||||||
sendButton.layoutParams = RelativeLayout.LayoutParams(RelativeLayout.LayoutParams.MATCH_PARENT, RelativeLayout.LayoutParams.MATCH_PARENT)
|
sendButton.layoutParams = RelativeLayout.LayoutParams(RelativeLayout.LayoutParams.MATCH_PARENT, RelativeLayout.LayoutParams.MATCH_PARENT)
|
||||||
sendButton.isVisible = false
|
sendButton.isVisible = false
|
||||||
|
sendButton.onUp = { delegate?.sendMessage() }
|
||||||
// Edit text
|
// Edit text
|
||||||
inputBarEditText.imeOptions = inputBarEditText.imeOptions or 16777216 // Always use incognito keyboard
|
inputBarEditText.imeOptions = inputBarEditText.imeOptions or 16777216 // Always use incognito keyboard
|
||||||
inputBarEditText.delegate = this
|
inputBarEditText.delegate = this
|
||||||
@ -92,8 +92,8 @@ class InputBar : RelativeLayout, InputBarEditTextDelegate, QuoteViewDelegate, Li
|
|||||||
delegate?.toggleAttachmentOptions()
|
delegate?.toggleAttachmentOptions()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun showVoiceMessageUI() {
|
private fun startRecordingVoiceMessage() {
|
||||||
delegate?.showVoiceMessageUI()
|
delegate?.startRecordingVoiceMessage()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Drafting quotes and drafting link previews is mutually exclusive, i.e. you can't draft
|
// Drafting quotes and drafting link previews is mutually exclusive, i.e. you can't draft
|
||||||
@ -111,7 +111,7 @@ class InputBar : RelativeLayout, InputBarEditTextDelegate, QuoteViewDelegate, Li
|
|||||||
// here to get the layout right.
|
// here to get the layout right.
|
||||||
val maxContentWidth = (screenWidth - 2 * resources.getDimension(R.dimen.medium_spacing) - toPx(16, resources) - toPx(30, resources)).roundToInt()
|
val maxContentWidth = (screenWidth - 2 * resources.getDimension(R.dimen.medium_spacing) - toPx(16, resources) - toPx(30, resources)).roundToInt()
|
||||||
quoteView.bind(message.individualRecipient.address.toString(), message.body, attachments,
|
quoteView.bind(message.individualRecipient.address.toString(), message.body, attachments,
|
||||||
message.recipient, true, maxContentWidth, message.isOpenGroupInvitation)
|
message.recipient, true, maxContentWidth, message.isOpenGroupInvitation, message.threadId)
|
||||||
// The 6 DP below is the padding the quote view applies to itself, which isn't included in the
|
// The 6 DP below is the padding the quote view applies to itself, which isn't included in the
|
||||||
// intrinsic height calculation.
|
// intrinsic height calculation.
|
||||||
val quoteViewIntrinsicHeight = quoteView.getIntrinsicHeight(maxContentWidth) + toPx(6, resources)
|
val quoteViewIntrinsicHeight = quoteView.getIntrinsicHeight(maxContentWidth) + toPx(6, resources)
|
||||||
@ -159,7 +159,9 @@ interface InputBarDelegate {
|
|||||||
fun inputBarEditTextContentChanged(newContent: CharSequence)
|
fun inputBarEditTextContentChanged(newContent: CharSequence)
|
||||||
fun toggleAttachmentOptions()
|
fun toggleAttachmentOptions()
|
||||||
fun showVoiceMessageUI()
|
fun showVoiceMessageUI()
|
||||||
|
fun startRecordingVoiceMessage()
|
||||||
fun onMicrophoneButtonMove(event: MotionEvent)
|
fun onMicrophoneButtonMove(event: MotionEvent)
|
||||||
fun onMicrophoneButtonCancel(event: MotionEvent)
|
fun onMicrophoneButtonCancel(event: MotionEvent)
|
||||||
fun onMicrophoneButtonUp(event: MotionEvent)
|
fun onMicrophoneButtonUp(event: MotionEvent)
|
||||||
|
fun sendMessage()
|
||||||
}
|
}
|
@ -134,10 +134,14 @@ class InputBarRecordingView : RelativeLayout {
|
|||||||
}
|
}
|
||||||
fadeInAnimation.start()
|
fadeInAnimation.start()
|
||||||
recordButtonOverlayImageView.setImageDrawable(ResourcesCompat.getDrawable(resources, R.drawable.ic_arrow_up, context.theme))
|
recordButtonOverlayImageView.setImageDrawable(ResourcesCompat.getDrawable(resources, R.drawable.ic_arrow_up, context.theme))
|
||||||
|
recordButtonOverlay.setOnClickListener { delegate?.sendVoiceMessage() }
|
||||||
|
inputBarCancelButton.setOnClickListener { delegate?.cancelVoiceMessage() }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
interface InputBarRecordingViewDelegate {
|
interface InputBarRecordingViewDelegate {
|
||||||
|
|
||||||
fun handleVoiceMessageUIHidden()
|
fun handleVoiceMessageUIHidden()
|
||||||
|
fun sendVoiceMessage()
|
||||||
|
fun cancelVoiceMessage()
|
||||||
}
|
}
|
||||||
|
@ -65,6 +65,10 @@ class MentionCandidatesView(context: Context, attrs: AttributeSet?, defStyleAttr
|
|||||||
openGroupServer = openGroup.server
|
openGroupServer = openGroup.server
|
||||||
openGroupRoom = openGroup.room
|
openGroupRoom = openGroup.room
|
||||||
}
|
}
|
||||||
|
setMentionCandidates(candidates)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setMentionCandidates(candidates: List<Mention>) {
|
||||||
this.candidates = candidates
|
this.candidates = candidates
|
||||||
val layoutParams = this.layoutParams as ViewGroup.LayoutParams
|
val layoutParams = this.layoutParams as ViewGroup.LayoutParams
|
||||||
layoutParams.height = toPx(Math.min(candidates.count(), 4) * 44, resources)
|
layoutParams.height = toPx(Math.min(candidates.count(), 4) * 44, resources)
|
||||||
|
@ -1,11 +1,14 @@
|
|||||||
package org.thoughtcrime.securesms.conversation.v2.messages
|
package org.thoughtcrime.securesms.conversation.v2.messages
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import android.content.res.Resources
|
||||||
import android.util.AttributeSet
|
import android.util.AttributeSet
|
||||||
|
import android.view.Gravity
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.widget.LinearLayout
|
import android.widget.LinearLayout
|
||||||
import androidx.core.content.res.ResourcesCompat
|
import androidx.core.content.res.ResourcesCompat
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import kotlinx.android.synthetic.main.view_control_message.view.*
|
import kotlinx.android.synthetic.main.view_control_message.view.*
|
||||||
import network.loki.messenger.R
|
import network.loki.messenger.R
|
||||||
import org.thoughtcrime.securesms.database.model.MessageRecord
|
import org.thoughtcrime.securesms.database.model.MessageRecord
|
||||||
@ -19,6 +22,7 @@ class ControlMessageView : LinearLayout {
|
|||||||
|
|
||||||
private fun initialize() {
|
private fun initialize() {
|
||||||
LayoutInflater.from(context).inflate(R.layout.view_control_message, this)
|
LayoutInflater.from(context).inflate(R.layout.view_control_message, this)
|
||||||
|
layoutParams = RecyclerView.LayoutParams(RecyclerView.LayoutParams.MATCH_PARENT, RecyclerView.LayoutParams.WRAP_CONTENT)
|
||||||
}
|
}
|
||||||
// endregion
|
// endregion
|
||||||
|
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
package org.thoughtcrime.securesms.conversation.v2.messages
|
package org.thoughtcrime.securesms.conversation.v2.messages
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import android.graphics.Canvas
|
||||||
import android.graphics.drawable.Drawable
|
import android.graphics.drawable.Drawable
|
||||||
import android.util.AttributeSet
|
import android.util.AttributeSet
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
@ -9,12 +10,15 @@ import android.widget.LinearLayout
|
|||||||
import androidx.core.content.res.ResourcesCompat
|
import androidx.core.content.res.ResourcesCompat
|
||||||
import kotlinx.android.synthetic.main.view_link_preview.view.*
|
import kotlinx.android.synthetic.main.view_link_preview.view.*
|
||||||
import network.loki.messenger.R
|
import network.loki.messenger.R
|
||||||
|
import org.thoughtcrime.securesms.components.CornerMask
|
||||||
|
import org.thoughtcrime.securesms.conversation.v2.utilities.MessageBubbleUtilities
|
||||||
import org.thoughtcrime.securesms.database.model.MmsMessageRecord
|
import org.thoughtcrime.securesms.database.model.MmsMessageRecord
|
||||||
import org.thoughtcrime.securesms.loki.utilities.UiModeUtilities
|
import org.thoughtcrime.securesms.loki.utilities.UiModeUtilities
|
||||||
import org.thoughtcrime.securesms.mms.GlideRequests
|
import org.thoughtcrime.securesms.mms.GlideRequests
|
||||||
import org.thoughtcrime.securesms.mms.ImageSlide
|
import org.thoughtcrime.securesms.mms.ImageSlide
|
||||||
|
|
||||||
class LinkPreviewView : LinearLayout {
|
class LinkPreviewView : LinearLayout {
|
||||||
|
private val cornerMask by lazy { CornerMask(this) }
|
||||||
|
|
||||||
// region Lifecycle
|
// region Lifecycle
|
||||||
constructor(context: Context) : super(context) { initialize() }
|
constructor(context: Context) : super(context) { initialize() }
|
||||||
@ -27,7 +31,7 @@ class LinkPreviewView : LinearLayout {
|
|||||||
// endregion
|
// endregion
|
||||||
|
|
||||||
// region Updating
|
// region Updating
|
||||||
fun bind(message: MmsMessageRecord, glide: GlideRequests, background: Drawable) {
|
fun bind(message: MmsMessageRecord, glide: GlideRequests, isStartOfMessageCluster: Boolean, isEndOfMessageCluster: Boolean) {
|
||||||
mainLinkPreviewContainer.background = background
|
mainLinkPreviewContainer.background = background
|
||||||
mainLinkPreviewContainer.outlineProvider = ViewOutlineProvider.BACKGROUND
|
mainLinkPreviewContainer.outlineProvider = ViewOutlineProvider.BACKGROUND
|
||||||
mainLinkPreviewContainer.clipToOutline = true
|
mainLinkPreviewContainer.clipToOutline = true
|
||||||
@ -48,6 +52,17 @@ class LinkPreviewView : LinearLayout {
|
|||||||
// Body
|
// Body
|
||||||
val bodyTextView = VisibleMessageContentView.getBodyTextView(context, message)
|
val bodyTextView = VisibleMessageContentView.getBodyTextView(context, message)
|
||||||
mainLinkPreviewContainer.addView(bodyTextView)
|
mainLinkPreviewContainer.addView(bodyTextView)
|
||||||
|
// Corner radii
|
||||||
|
val cornerRadii = MessageBubbleUtilities.calculateRadii(context, isStartOfMessageCluster, isEndOfMessageCluster, message.isOutgoing)
|
||||||
|
cornerMask.setTopLeftRadius(cornerRadii[0])
|
||||||
|
cornerMask.setTopRightRadius(cornerRadii[1])
|
||||||
|
cornerMask.setBottomRightRadius(cornerRadii[2])
|
||||||
|
cornerMask.setBottomLeftRadius(cornerRadii[3])
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun dispatchDraw(canvas: Canvas) {
|
||||||
|
super.dispatchDraw(canvas)
|
||||||
|
cornerMask.mask(canvas)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun recycle() {
|
fun recycle() {
|
||||||
|
@ -10,6 +10,7 @@ import android.widget.LinearLayout
|
|||||||
import android.widget.RelativeLayout
|
import android.widget.RelativeLayout
|
||||||
import androidx.annotation.ColorInt
|
import androidx.annotation.ColorInt
|
||||||
import androidx.core.content.res.ResourcesCompat
|
import androidx.core.content.res.ResourcesCompat
|
||||||
|
import androidx.core.text.toSpannable
|
||||||
import androidx.core.view.isVisible
|
import androidx.core.view.isVisible
|
||||||
import androidx.core.view.marginStart
|
import androidx.core.view.marginStart
|
||||||
import kotlinx.android.synthetic.main.view_quote.view.*
|
import kotlinx.android.synthetic.main.view_quote.view.*
|
||||||
@ -20,10 +21,7 @@ import org.session.libsession.utilities.recipients.Recipient
|
|||||||
import org.thoughtcrime.securesms.conversation.v2.utilities.TextUtilities
|
import org.thoughtcrime.securesms.conversation.v2.utilities.TextUtilities
|
||||||
import org.thoughtcrime.securesms.database.DatabaseFactory
|
import org.thoughtcrime.securesms.database.DatabaseFactory
|
||||||
import org.thoughtcrime.securesms.database.model.MessageRecord
|
import org.thoughtcrime.securesms.database.model.MessageRecord
|
||||||
import org.thoughtcrime.securesms.loki.utilities.UiMode
|
import org.thoughtcrime.securesms.loki.utilities.*
|
||||||
import org.thoughtcrime.securesms.loki.utilities.UiModeUtilities
|
|
||||||
import org.thoughtcrime.securesms.loki.utilities.toDp
|
|
||||||
import org.thoughtcrime.securesms.loki.utilities.toPx
|
|
||||||
import org.thoughtcrime.securesms.mms.SlideDeck
|
import org.thoughtcrime.securesms.mms.SlideDeck
|
||||||
import kotlin.math.max
|
import kotlin.math.max
|
||||||
import kotlin.math.min
|
import kotlin.math.min
|
||||||
@ -106,7 +104,7 @@ class QuoteView : LinearLayout {
|
|||||||
|
|
||||||
// region Updating
|
// region Updating
|
||||||
fun bind(authorPublicKey: String, body: String?, attachments: SlideDeck?, thread: Recipient,
|
fun bind(authorPublicKey: String, body: String?, attachments: SlideDeck?, thread: Recipient,
|
||||||
isOutgoingMessage: Boolean, maxContentWidth: Int, isOpenGroupInvitation: Boolean) {
|
isOutgoingMessage: Boolean, maxContentWidth: Int, isOpenGroupInvitation: Boolean, threadID: Long) {
|
||||||
val contactDB = DatabaseFactory.getSessionContactDatabase(context)
|
val contactDB = DatabaseFactory.getSessionContactDatabase(context)
|
||||||
// Reduce the max body text view line count to 2 if this is a group thread because
|
// Reduce the max body text view line count to 2 if this is a group thread because
|
||||||
// we'll be showing the author text view and we don't want the overall quote view height
|
// we'll be showing the author text view and we don't want the overall quote view height
|
||||||
@ -121,7 +119,7 @@ class QuoteView : LinearLayout {
|
|||||||
}
|
}
|
||||||
quoteViewAuthorTextView.isVisible = thread.isGroupRecipient
|
quoteViewAuthorTextView.isVisible = thread.isGroupRecipient
|
||||||
// Body
|
// Body
|
||||||
quoteViewBodyTextView.text = if (isOpenGroupInvitation) resources.getString(R.string.open_group_invitation_view__open_group_invitation) else body
|
quoteViewBodyTextView.text = if (isOpenGroupInvitation) resources.getString(R.string.open_group_invitation_view__open_group_invitation) else MentionUtilities.highlightMentions((body ?: "").toSpannable(), threadID, context);
|
||||||
quoteViewBodyTextView.setTextColor(getTextColor(isOutgoingMessage))
|
quoteViewBodyTextView.setTextColor(getTextColor(isOutgoingMessage))
|
||||||
// Accent line / attachment preview
|
// Accent line / attachment preview
|
||||||
val hasAttachments = (attachments != null && attachments.asAttachments().isNotEmpty())
|
val hasAttachments = (attachments != null && attachments.asAttachments().isNotEmpty())
|
||||||
|
@ -2,6 +2,8 @@ package org.thoughtcrime.securesms.conversation.v2.messages
|
|||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.graphics.Rect
|
import android.graphics.Rect
|
||||||
|
import android.content.res.ColorStateList
|
||||||
|
import android.graphics.Color
|
||||||
import android.graphics.drawable.Drawable
|
import android.graphics.drawable.Drawable
|
||||||
import android.text.util.Linkify
|
import android.text.util.Linkify
|
||||||
import android.util.AttributeSet
|
import android.util.AttributeSet
|
||||||
@ -58,7 +60,7 @@ class VisibleMessageContentView : LinearLayout {
|
|||||||
onContentClick = null
|
onContentClick = null
|
||||||
if (message is MmsMessageRecord && message.linkPreviews.isNotEmpty()) {
|
if (message is MmsMessageRecord && message.linkPreviews.isNotEmpty()) {
|
||||||
val linkPreviewView = LinkPreviewView(context)
|
val linkPreviewView = LinkPreviewView(context)
|
||||||
linkPreviewView.bind(message, glide, background)
|
linkPreviewView.bind(message, glide, isStartOfMessageCluster, isEndOfMessageCluster)
|
||||||
mainContainer.addView(linkPreviewView)
|
mainContainer.addView(linkPreviewView)
|
||||||
// Body text view is inside the link preview for layout convenience
|
// Body text view is inside the link preview for layout convenience
|
||||||
} else if (message is MmsMessageRecord && message.quote != null) {
|
} else if (message is MmsMessageRecord && message.quote != null) {
|
||||||
@ -69,14 +71,14 @@ class VisibleMessageContentView : LinearLayout {
|
|||||||
// here to get the layout right.
|
// here to get the layout right.
|
||||||
val maxContentWidth = (maxWidth - 2 * resources.getDimension(R.dimen.medium_spacing) - toPx(16, resources)).roundToInt()
|
val maxContentWidth = (maxWidth - 2 * resources.getDimension(R.dimen.medium_spacing) - toPx(16, resources)).roundToInt()
|
||||||
quoteView.bind(quote.author.toString(), quote.text, quote.attachment, thread,
|
quoteView.bind(quote.author.toString(), quote.text, quote.attachment, thread,
|
||||||
message.isOutgoing, maxContentWidth, message.isOpenGroupInvitation)
|
message.isOutgoing, maxContentWidth, message.isOpenGroupInvitation, message.threadId)
|
||||||
mainContainer.addView(quoteView)
|
mainContainer.addView(quoteView)
|
||||||
val bodyTextView = VisibleMessageContentView.getBodyTextView(context, message)
|
val bodyTextView = VisibleMessageContentView.getBodyTextView(context, message)
|
||||||
ViewUtil.setPaddingTop(bodyTextView, 0)
|
ViewUtil.setPaddingTop(bodyTextView, 0)
|
||||||
mainContainer.addView(bodyTextView)
|
mainContainer.addView(bodyTextView)
|
||||||
} else if (message is MmsMessageRecord && message.slideDeck.audioSlide != null) {
|
} else if (message is MmsMessageRecord && message.slideDeck.audioSlide != null) {
|
||||||
val voiceMessageView = VoiceMessageView(context)
|
val voiceMessageView = VoiceMessageView(context)
|
||||||
voiceMessageView.bind(message, background)
|
voiceMessageView.bind(message, isStartOfMessageCluster, isEndOfMessageCluster)
|
||||||
mainContainer.addView(voiceMessageView)
|
mainContainer.addView(voiceMessageView)
|
||||||
// We have to use onContentClick (rather than a click listener directly on the voice
|
// We have to use onContentClick (rather than a click listener directly on the voice
|
||||||
// message view) so as to not interfere with all the other gestures.
|
// message view) so as to not interfere with all the other gestures.
|
||||||
@ -148,11 +150,11 @@ class VisibleMessageContentView : LinearLayout {
|
|||||||
|
|
||||||
@ColorInt
|
@ColorInt
|
||||||
fun getTextColor(context: Context, message: MessageRecord): Int {
|
fun getTextColor(context: Context, message: MessageRecord): Int {
|
||||||
val uiMode = UiModeUtilities.getUserSelectedUiMode(context)
|
val isDayUiMode = UiModeUtilities.isDayUiMode(context)
|
||||||
val colorID = if (message.isOutgoing) {
|
val colorID = if (message.isOutgoing) {
|
||||||
if (uiMode == UiMode.NIGHT) R.color.black else R.color.white
|
if (isDayUiMode) R.color.white else R.color.black
|
||||||
} else {
|
} else {
|
||||||
if (uiMode == UiMode.NIGHT) R.color.white else R.color.black
|
if (isDayUiMode) R.color.black else R.color.white
|
||||||
}
|
}
|
||||||
return context.resources.getColorWithID(colorID, context.theme)
|
return context.resources.getColorWithID(colorID, context.theme)
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
package org.thoughtcrime.securesms.conversation.v2.messages
|
package org.thoughtcrime.securesms.conversation.v2.messages
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import android.graphics.Canvas
|
||||||
import android.graphics.drawable.Drawable
|
import android.graphics.drawable.Drawable
|
||||||
import android.os.Handler
|
import android.os.Handler
|
||||||
import android.os.Looper
|
import android.os.Looper
|
||||||
@ -12,12 +13,15 @@ import android.widget.RelativeLayout
|
|||||||
import androidx.core.view.isVisible
|
import androidx.core.view.isVisible
|
||||||
import kotlinx.android.synthetic.main.view_voice_message.view.*
|
import kotlinx.android.synthetic.main.view_voice_message.view.*
|
||||||
import network.loki.messenger.R
|
import network.loki.messenger.R
|
||||||
|
import org.thoughtcrime.securesms.components.CornerMask
|
||||||
|
import org.thoughtcrime.securesms.conversation.v2.utilities.MessageBubbleUtilities
|
||||||
import org.thoughtcrime.securesms.database.model.MmsMessageRecord
|
import org.thoughtcrime.securesms.database.model.MmsMessageRecord
|
||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
import kotlin.math.roundToInt
|
import kotlin.math.roundToInt
|
||||||
|
|
||||||
class VoiceMessageView : LinearLayout {
|
class VoiceMessageView : LinearLayout {
|
||||||
private val snHandler = Handler(Looper.getMainLooper())
|
private val snHandler = Handler(Looper.getMainLooper())
|
||||||
|
private val cornerMask by lazy { CornerMask(this) }
|
||||||
private var runnable: Runnable? = null
|
private var runnable: Runnable? = null
|
||||||
private var mockIsPlaying = false
|
private var mockIsPlaying = false
|
||||||
private var mockProgress = 0L
|
private var mockProgress = 0L
|
||||||
@ -38,12 +42,14 @@ class VoiceMessageView : LinearLayout {
|
|||||||
// endregion
|
// endregion
|
||||||
|
|
||||||
// region Updating
|
// region Updating
|
||||||
fun bind(message: MmsMessageRecord, background: Drawable) {
|
fun bind(message: MmsMessageRecord, isStartOfMessageCluster: Boolean, isEndOfMessageCluster: Boolean) {
|
||||||
val audio = message.slideDeck.audioSlide!!
|
val audio = message.slideDeck.audioSlide!!
|
||||||
voiceMessageViewLoader.isVisible = audio.isPendingDownload
|
voiceMessageViewLoader.isVisible = audio.isPendingDownload
|
||||||
mainVoiceMessageViewContainer.background = background
|
val cornerRadii = MessageBubbleUtilities.calculateRadii(context, isStartOfMessageCluster, isEndOfMessageCluster, message.isOutgoing)
|
||||||
mainVoiceMessageViewContainer.outlineProvider = ViewOutlineProvider.BACKGROUND
|
cornerMask.setTopLeftRadius(cornerRadii[0])
|
||||||
mainVoiceMessageViewContainer.clipToOutline = true
|
cornerMask.setTopRightRadius(cornerRadii[1])
|
||||||
|
cornerMask.setBottomRightRadius(cornerRadii[2])
|
||||||
|
cornerMask.setBottomLeftRadius(cornerRadii[3])
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handleProgressChanged() {
|
private fun handleProgressChanged() {
|
||||||
@ -56,6 +62,11 @@ class VoiceMessageView : LinearLayout {
|
|||||||
progressView.layoutParams = layoutParams
|
progressView.layoutParams = layoutParams
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun dispatchDraw(canvas: Canvas) {
|
||||||
|
super.dispatchDraw(canvas)
|
||||||
|
cornerMask.mask(canvas)
|
||||||
|
}
|
||||||
|
|
||||||
fun recycle() {
|
fun recycle() {
|
||||||
// TODO: Implement
|
// TODO: Implement
|
||||||
}
|
}
|
||||||
|
@ -14,7 +14,7 @@
|
|||||||
* You should have received a copy of the GNU General Public License
|
* You should have received a copy of the GNU General Public License
|
||||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
package org.thoughtcrime.securesms.mms;
|
package org.thoughtcrime.securesms.conversation.v2.utilities;
|
||||||
|
|
||||||
import android.Manifest;
|
import android.Manifest;
|
||||||
import android.annotation.SuppressLint;
|
import android.annotation.SuppressLint;
|
||||||
@ -23,29 +23,31 @@ import android.content.ActivityNotFoundException;
|
|||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.database.Cursor;
|
import android.database.Cursor;
|
||||||
import android.graphics.PorterDuff;
|
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
import android.os.AsyncTask;
|
import android.os.AsyncTask;
|
||||||
import android.provider.ContactsContract;
|
|
||||||
import android.provider.MediaStore;
|
import android.provider.MediaStore;
|
||||||
import android.provider.OpenableColumns;
|
import android.provider.OpenableColumns;
|
||||||
import android.text.TextUtils;
|
import android.text.TextUtils;
|
||||||
import android.util.Pair;
|
import android.util.Pair;
|
||||||
import android.view.View;
|
|
||||||
import android.widget.Toast;
|
import android.widget.Toast;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
|
|
||||||
import org.thoughtcrime.securesms.MediaPreviewActivity;
|
|
||||||
import org.thoughtcrime.securesms.loki.views.MessageAudioView;
|
|
||||||
import org.thoughtcrime.securesms.components.DocumentView;
|
|
||||||
import org.thoughtcrime.securesms.components.RemovableEditableMediaView;
|
|
||||||
import org.thoughtcrime.securesms.conversation.v2.utilities.ThumbnailView;
|
|
||||||
import org.session.libsignal.utilities.NoExternalStorageException;
|
import org.session.libsignal.utilities.NoExternalStorageException;
|
||||||
import org.thoughtcrime.securesms.giph.ui.GiphyActivity;
|
import org.thoughtcrime.securesms.giph.ui.GiphyActivity;
|
||||||
import org.session.libsignal.utilities.Log;
|
import org.session.libsignal.utilities.Log;
|
||||||
import org.thoughtcrime.securesms.mediasend.MediaSendActivity;
|
import org.thoughtcrime.securesms.mediasend.MediaSendActivity;
|
||||||
|
import org.thoughtcrime.securesms.mms.AudioSlide;
|
||||||
|
import org.thoughtcrime.securesms.mms.DocumentSlide;
|
||||||
|
import org.thoughtcrime.securesms.mms.GifSlide;
|
||||||
|
import org.thoughtcrime.securesms.mms.GlideRequests;
|
||||||
|
import org.thoughtcrime.securesms.mms.ImageSlide;
|
||||||
|
import org.thoughtcrime.securesms.mms.MediaConstraints;
|
||||||
|
import org.thoughtcrime.securesms.mms.PartAuthority;
|
||||||
|
import org.thoughtcrime.securesms.mms.Slide;
|
||||||
|
import org.thoughtcrime.securesms.mms.SlideDeck;
|
||||||
|
import org.thoughtcrime.securesms.mms.VideoSlide;
|
||||||
import org.thoughtcrime.securesms.permissions.Permissions;
|
import org.thoughtcrime.securesms.permissions.Permissions;
|
||||||
import org.thoughtcrime.securesms.providers.BlobProvider;
|
import org.thoughtcrime.securesms.providers.BlobProvider;
|
||||||
import org.session.libsignal.utilities.ExternalStorageUtil;
|
import org.session.libsignal.utilities.ExternalStorageUtil;
|
||||||
@ -53,13 +55,8 @@ import org.thoughtcrime.securesms.util.FileProviderUtil;
|
|||||||
import org.thoughtcrime.securesms.util.MediaUtil;
|
import org.thoughtcrime.securesms.util.MediaUtil;
|
||||||
import org.session.libsignal.utilities.guava.Optional;
|
import org.session.libsignal.utilities.guava.Optional;
|
||||||
|
|
||||||
import org.session.libsession.messaging.sending_receiving.attachments.Attachment;
|
|
||||||
import org.session.libsession.utilities.recipients.Recipient;
|
import org.session.libsession.utilities.recipients.Recipient;
|
||||||
import org.session.libsession.utilities.ThemeUtil;
|
|
||||||
import org.session.libsession.utilities.ViewUtil;
|
|
||||||
import org.session.libsession.utilities.Stub;
|
|
||||||
import org.session.libsignal.utilities.ListenableFuture;
|
import org.session.libsignal.utilities.ListenableFuture;
|
||||||
import org.session.libsignal.utilities.ListenableFuture.Listener;
|
|
||||||
import org.session.libsignal.utilities.SettableFuture;
|
import org.session.libsignal.utilities.SettableFuture;
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
@ -67,26 +64,18 @@ import java.io.IOException;
|
|||||||
import java.util.Iterator;
|
import java.util.Iterator;
|
||||||
import java.util.LinkedList;
|
import java.util.LinkedList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.concurrent.ExecutionException;
|
|
||||||
|
|
||||||
import network.loki.messenger.R;
|
import network.loki.messenger.R;
|
||||||
|
|
||||||
import static android.provider.MediaStore.EXTRA_OUTPUT;
|
import static android.provider.MediaStore.EXTRA_OUTPUT;
|
||||||
|
|
||||||
|
|
||||||
public class AttachmentManager {
|
public class AttachmentManager {
|
||||||
|
|
||||||
private final static String TAG = AttachmentManager.class.getSimpleName();
|
private final static String TAG = AttachmentManager.class.getSimpleName();
|
||||||
|
|
||||||
private final @NonNull Context context;
|
private final @NonNull Context context;
|
||||||
private final @NonNull Stub<View> attachmentViewStub;
|
|
||||||
private final @NonNull AttachmentListener attachmentListener;
|
private final @NonNull AttachmentListener attachmentListener;
|
||||||
|
|
||||||
private RemovableEditableMediaView removableMediaView;
|
|
||||||
private ThumbnailView thumbnail;
|
|
||||||
private MessageAudioView audioView;
|
|
||||||
private DocumentView documentView;
|
|
||||||
|
|
||||||
private @NonNull List<Uri> garbage = new LinkedList<>();
|
private @NonNull List<Uri> garbage = new LinkedList<>();
|
||||||
private @NonNull Optional<Slide> slide = Optional.absent();
|
private @NonNull Optional<Slide> slide = Optional.absent();
|
||||||
private @Nullable Uri captureUri;
|
private @Nullable Uri captureUri;
|
||||||
@ -94,51 +83,12 @@ public class AttachmentManager {
|
|||||||
public AttachmentManager(@NonNull Activity activity, @NonNull AttachmentListener listener) {
|
public AttachmentManager(@NonNull Activity activity, @NonNull AttachmentListener listener) {
|
||||||
this.context = activity;
|
this.context = activity;
|
||||||
this.attachmentListener = listener;
|
this.attachmentListener = listener;
|
||||||
this.attachmentViewStub = ViewUtil.findStubById(activity, R.id.attachment_editor_stub);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void inflateStub() {
|
public void clear() {
|
||||||
if (!attachmentViewStub.resolved()) {
|
markGarbage(getSlideUri());
|
||||||
View root = attachmentViewStub.get();
|
slide = Optional.absent();
|
||||||
|
attachmentListener.onAttachmentChanged();
|
||||||
this.thumbnail = ViewUtil.findById(root, R.id.attachment_thumbnail);
|
|
||||||
this.audioView = ViewUtil.findById(root, R.id.attachment_audio);
|
|
||||||
this.documentView = ViewUtil.findById(root, R.id.attachment_document);
|
|
||||||
this.removableMediaView = ViewUtil.findById(root, R.id.removable_media_view);
|
|
||||||
|
|
||||||
removableMediaView.setRemoveClickListener(new RemoveButtonListener());
|
|
||||||
thumbnail.setOnClickListener(new ThumbnailClickListener());
|
|
||||||
documentView.getBackground().setColorFilter(ThemeUtil.getThemedColor(context, R.attr.conversation_item_bubble_background), PorterDuff.Mode.MULTIPLY);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void clear(@NonNull GlideRequests glideRequests, boolean animate) {
|
|
||||||
if (attachmentViewStub.resolved()) {
|
|
||||||
|
|
||||||
if (animate) {
|
|
||||||
ViewUtil.fadeOut(attachmentViewStub.get(), 200).addListener(new Listener<Boolean>() {
|
|
||||||
@Override
|
|
||||||
public void onSuccess(Boolean result) {
|
|
||||||
thumbnail.clear(glideRequests);
|
|
||||||
attachmentViewStub.get().setVisibility(View.GONE);
|
|
||||||
attachmentListener.onAttachmentChanged();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onFailure(ExecutionException e) {
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
thumbnail.clear(glideRequests);
|
|
||||||
attachmentViewStub.get().setVisibility(View.GONE);
|
|
||||||
attachmentListener.onAttachmentChanged();
|
|
||||||
}
|
|
||||||
|
|
||||||
markGarbage(getSlideUri());
|
|
||||||
slide = Optional.absent();
|
|
||||||
|
|
||||||
audioView.cleanup();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void cleanup() {
|
public void cleanup() {
|
||||||
@ -190,16 +140,12 @@ public class AttachmentManager {
|
|||||||
final int width,
|
final int width,
|
||||||
final int height)
|
final int height)
|
||||||
{
|
{
|
||||||
inflateStub();
|
|
||||||
|
|
||||||
final SettableFuture<Boolean> result = new SettableFuture<>();
|
final SettableFuture<Boolean> result = new SettableFuture<>();
|
||||||
|
|
||||||
new AsyncTask<Void, Void, Slide>() {
|
new AsyncTask<Void, Void, Slide>() {
|
||||||
@Override
|
@Override
|
||||||
protected void onPreExecute() {
|
protected void onPreExecute() {
|
||||||
thumbnail.clear(glideRequests);
|
|
||||||
thumbnail.showProgressSpinner();
|
|
||||||
attachmentViewStub.get().setVisibility(View.VISIBLE);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -222,35 +168,12 @@ public class AttachmentManager {
|
|||||||
@Override
|
@Override
|
||||||
protected void onPostExecute(@Nullable final Slide slide) {
|
protected void onPostExecute(@Nullable final Slide slide) {
|
||||||
if (slide == null) {
|
if (slide == null) {
|
||||||
attachmentViewStub.get().setVisibility(View.GONE);
|
|
||||||
Toast.makeText(context,
|
|
||||||
R.string.ConversationActivity_sorry_there_was_an_error_setting_your_attachment,
|
|
||||||
Toast.LENGTH_SHORT).show();
|
|
||||||
result.set(false);
|
result.set(false);
|
||||||
} else if (!areConstraintsSatisfied(context, slide, constraints)) {
|
} else if (!areConstraintsSatisfied(context, slide, constraints)) {
|
||||||
attachmentViewStub.get().setVisibility(View.GONE);
|
|
||||||
Toast.makeText(context,
|
|
||||||
R.string.ConversationActivity_attachment_exceeds_size_limits,
|
|
||||||
Toast.LENGTH_SHORT).show();
|
|
||||||
result.set(false);
|
result.set(false);
|
||||||
} else {
|
} else {
|
||||||
setSlide(slide);
|
setSlide(slide);
|
||||||
attachmentViewStub.get().setVisibility(View.VISIBLE);
|
result.set(true);
|
||||||
|
|
||||||
if (slide.hasAudio()) {
|
|
||||||
audioView.setAudio((AudioSlide) slide, false);
|
|
||||||
removableMediaView.display(audioView, false);
|
|
||||||
result.set(true);
|
|
||||||
} else if (slide.hasDocument()) {
|
|
||||||
documentView.setDocument((DocumentSlide) slide, false);
|
|
||||||
removableMediaView.display(documentView, false);
|
|
||||||
result.set(true);
|
|
||||||
} else {
|
|
||||||
Attachment attachment = slide.asAttachment();
|
|
||||||
result.deferTo(thumbnail.setImageResource(glideRequests, slide, false, true, attachment.getWidth(), attachment.getHeight()));
|
|
||||||
removableMediaView.display(thumbnail, mediaType == MediaType.IMAGE);
|
|
||||||
}
|
|
||||||
|
|
||||||
attachmentListener.onAttachmentChanged();
|
attachmentListener.onAttachmentChanged();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -317,11 +240,8 @@ public class AttachmentManager {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean isAttachmentPresent() {
|
public @NonNull
|
||||||
return attachmentViewStub.resolved() && attachmentViewStub.get().getVisibility() == View.VISIBLE;
|
SlideDeck buildSlideDeck() {
|
||||||
}
|
|
||||||
|
|
||||||
public @NonNull SlideDeck buildSlideDeck() {
|
|
||||||
SlideDeck deck = new SlideDeck();
|
SlideDeck deck = new SlideDeck();
|
||||||
if (slide.isPresent()) deck.addSlide(slide.get());
|
if (slide.isPresent()) deck.addSlide(slide.get());
|
||||||
return deck;
|
return deck;
|
||||||
@ -333,43 +253,16 @@ public class AttachmentManager {
|
|||||||
|
|
||||||
public static void selectGallery(Activity activity, int requestCode, @NonNull Recipient recipient, @NonNull String body) {
|
public static void selectGallery(Activity activity, int requestCode, @NonNull Recipient recipient, @NonNull String body) {
|
||||||
Permissions.with(activity)
|
Permissions.with(activity)
|
||||||
.request(Manifest.permission.READ_EXTERNAL_STORAGE)
|
.request(Manifest.permission.READ_EXTERNAL_STORAGE)
|
||||||
.withPermanentDenialDialog(activity.getString(R.string.AttachmentManager_signal_requires_the_external_storage_permission_in_order_to_attach_photos_videos_or_audio))
|
.withPermanentDenialDialog(activity.getString(R.string.AttachmentManager_signal_requires_the_external_storage_permission_in_order_to_attach_photos_videos_or_audio))
|
||||||
.onAllGranted(() -> activity.startActivityForResult(MediaSendActivity.buildGalleryIntent(activity, recipient, body), requestCode))
|
.onAllGranted(() -> activity.startActivityForResult(MediaSendActivity.buildGalleryIntent(activity, recipient, body), requestCode))
|
||||||
.execute();
|
.execute();
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void selectAudio(Activity activity, int requestCode) {
|
public static void selectAudio(Activity activity, int requestCode) {
|
||||||
selectMediaType(activity, "audio/*", null, requestCode);
|
selectMediaType(activity, "audio/*", null, requestCode);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void selectContactInfo(Activity activity, int requestCode) {
|
|
||||||
Permissions.with(activity)
|
|
||||||
.request(Manifest.permission.WRITE_CONTACTS)
|
|
||||||
.withPermanentDenialDialog(activity.getString(R.string.AttachmentManager_signal_requires_contacts_permission_in_order_to_attach_contact_information))
|
|
||||||
.onAllGranted(() -> {
|
|
||||||
Intent intent = new Intent(Intent.ACTION_PICK, ContactsContract.Contacts.CONTENT_URI);
|
|
||||||
activity.startActivityForResult(intent, requestCode);
|
|
||||||
})
|
|
||||||
.execute();
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void selectLocation(Activity activity, int requestCode) {
|
|
||||||
/* Loki - Enable again once we have location sharing
|
|
||||||
Permissions.with(activity)
|
|
||||||
.request(Manifest.permission.ACCESS_FINE_LOCATION, Manifest.permission.ACCESS_COARSE_LOCATION)
|
|
||||||
.withPermanentDenialDialog(activity.getString(R.string.AttachmentManager_signal_requires_location_information_in_order_to_attach_a_location))
|
|
||||||
.onAllGranted(() -> {
|
|
||||||
try {
|
|
||||||
activity.startActivityForResult(new PlacePicker.IntentBuilder().build(activity), requestCode);
|
|
||||||
} catch (GooglePlayServicesRepairableException | GooglePlayServicesNotAvailableException e) {
|
|
||||||
Log.w(TAG, e);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.execute();
|
|
||||||
*/
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void selectGif(Activity activity, int requestCode) {
|
public static void selectGif(Activity activity, int requestCode) {
|
||||||
Intent intent = new Intent(activity, GiphyActivity.class);
|
Intent intent = new Intent(activity, GiphyActivity.class);
|
||||||
intent.putExtra(GiphyActivity.EXTRA_IS_MMS, false);
|
intent.putExtra(GiphyActivity.EXTRA_IS_MMS, false);
|
||||||
@ -386,28 +279,25 @@ public class AttachmentManager {
|
|||||||
|
|
||||||
public void capturePhoto(Activity activity, int requestCode) {
|
public void capturePhoto(Activity activity, int requestCode) {
|
||||||
Permissions.with(activity)
|
Permissions.with(activity)
|
||||||
.request(Manifest.permission.CAMERA)
|
.request(Manifest.permission.CAMERA)
|
||||||
.withPermanentDenialDialog(activity.getString(R.string.AttachmentManager_signal_requires_the_camera_permission_in_order_to_take_photos_but_it_has_been_permanently_denied))
|
.withPermanentDenialDialog(activity.getString(R.string.AttachmentManager_signal_requires_the_camera_permission_in_order_to_take_photos_but_it_has_been_permanently_denied))
|
||||||
.onAllGranted(() -> {
|
.onAllGranted(() -> {
|
||||||
try {
|
try {
|
||||||
File captureFile = File.createTempFile(
|
File captureFile = File.createTempFile("conversation-capture", ".jpg", ExternalStorageUtil.getImageDir(activity));
|
||||||
"conversation-capture",
|
Uri captureUri = FileProviderUtil.getUriFor(context, captureFile);
|
||||||
".jpg",
|
Intent captureIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
|
||||||
ExternalStorageUtil.getImageDir(activity));
|
captureIntent.putExtra(EXTRA_OUTPUT, captureUri);
|
||||||
Uri captureUri = FileProviderUtil.getUriFor(context, captureFile);
|
captureIntent.setFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
|
||||||
Intent captureIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
|
if (captureIntent.resolveActivity(activity.getPackageManager()) != null) {
|
||||||
captureIntent.putExtra(EXTRA_OUTPUT, captureUri);
|
Log.d(TAG, "captureUri path is " + captureUri.getPath());
|
||||||
captureIntent.setFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
|
this.captureUri = captureUri;
|
||||||
if (captureIntent.resolveActivity(activity.getPackageManager()) != null) {
|
activity.startActivityForResult(captureIntent, requestCode);
|
||||||
Log.d(TAG, "captureUri path is " + captureUri.getPath());
|
}
|
||||||
this.captureUri = captureUri;
|
} catch (IOException | NoExternalStorageException e) {
|
||||||
activity.startActivityForResult(captureIntent, requestCode);
|
throw new RuntimeException("Error creating image capture intent.", e);
|
||||||
}
|
}
|
||||||
} catch (IOException | NoExternalStorageException e) {
|
})
|
||||||
throw new RuntimeException("Error creating image capture intent.", e);
|
.execute();
|
||||||
}
|
|
||||||
})
|
|
||||||
.execute();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void selectMediaType(Activity activity, @NonNull String type, @Nullable String[] extraMimeType, int requestCode) {
|
private static void selectMediaType(Activity activity, @NonNull String type, @Nullable String[] extraMimeType, int requestCode) {
|
||||||
@ -445,34 +335,6 @@ public class AttachmentManager {
|
|||||||
constraints.canResize(slide.asAttachment());
|
constraints.canResize(slide.asAttachment());
|
||||||
}
|
}
|
||||||
|
|
||||||
private void previewImageDraft(final @NonNull Slide slide) {
|
|
||||||
if (MediaPreviewActivity.isContentTypeSupported(slide.getContentType()) && slide.getUri() != null) {
|
|
||||||
Intent intent = new Intent(context, MediaPreviewActivity.class);
|
|
||||||
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
|
|
||||||
intent.putExtra(MediaPreviewActivity.SIZE_EXTRA, slide.asAttachment().getSize());
|
|
||||||
intent.putExtra(MediaPreviewActivity.CAPTION_EXTRA, slide.getCaption().orNull());
|
|
||||||
intent.putExtra(MediaPreviewActivity.OUTGOING_EXTRA, true);
|
|
||||||
intent.setDataAndType(slide.getUri(), slide.getContentType());
|
|
||||||
|
|
||||||
context.startActivity(intent);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private class ThumbnailClickListener implements View.OnClickListener {
|
|
||||||
@Override
|
|
||||||
public void onClick(View v) {
|
|
||||||
if (slide.isPresent()) previewImageDraft(slide.get());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private class RemoveButtonListener implements View.OnClickListener {
|
|
||||||
@Override
|
|
||||||
public void onClick(View v) {
|
|
||||||
cleanup();
|
|
||||||
clear(GlideApp.with(context.getApplicationContext()), true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public interface AttachmentListener {
|
public interface AttachmentListener {
|
||||||
void onAttachmentChanged();
|
void onAttachmentChanged();
|
||||||
}
|
}
|
||||||
@ -513,6 +375,5 @@ public class AttachmentManager {
|
|||||||
|
|
||||||
return DOCUMENT;
|
return DOCUMENT;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -0,0 +1,31 @@
|
|||||||
|
package org.thoughtcrime.securesms.conversation.v2.utilities
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import network.loki.messenger.R
|
||||||
|
import kotlin.math.roundToInt
|
||||||
|
|
||||||
|
object MessageBubbleUtilities {
|
||||||
|
|
||||||
|
fun calculateRadii(context: Context, isStartOfMessageCluster: Boolean, isEndOfMessageCluster: Boolean, isOutgoing: Boolean): IntArray {
|
||||||
|
val roundedDimen = context.resources.getDimension(R.dimen.message_corner_radius).roundToInt()
|
||||||
|
val collapsedDimen = context.resources.getDimension(R.dimen.message_corner_collapse_radius).roundToInt()
|
||||||
|
val (tl, tr, bl, br) = when {
|
||||||
|
// Single message
|
||||||
|
isStartOfMessageCluster && isEndOfMessageCluster -> intArrayOf(roundedDimen, roundedDimen, roundedDimen, roundedDimen)
|
||||||
|
// Start of message cluster; collapsed BL
|
||||||
|
isStartOfMessageCluster -> intArrayOf(roundedDimen, roundedDimen, collapsedDimen, roundedDimen)
|
||||||
|
// End of message cluster; collapsed TL
|
||||||
|
isEndOfMessageCluster -> intArrayOf(collapsedDimen, roundedDimen, roundedDimen, roundedDimen)
|
||||||
|
// In the middle; no rounding on the left
|
||||||
|
else -> intArrayOf(collapsedDimen, roundedDimen, collapsedDimen, roundedDimen)
|
||||||
|
}
|
||||||
|
// TL, TR, BR, BL (CW direction)
|
||||||
|
// Flip if the message is outgoing
|
||||||
|
return intArrayOf(
|
||||||
|
if (!isOutgoing) tl else tr, // TL
|
||||||
|
if (!isOutgoing) tr else tl, // TR
|
||||||
|
if (!isOutgoing) br else bl, // BR
|
||||||
|
if (!isOutgoing) bl else br // BL
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
@ -81,7 +81,6 @@ public class MarkReadReceiver extends BroadcastReceiver {
|
|||||||
|
|
||||||
for (Address address : addressMap.keySet()) {
|
for (Address address : addressMap.keySet()) {
|
||||||
List<Long> timestamps = Stream.of(addressMap.get(address)).map(SyncMessageId::getTimetamp).toList();
|
List<Long> timestamps = Stream.of(addressMap.get(address)).map(SyncMessageId::getTimetamp).toList();
|
||||||
// Loki - Check whether we want to send a read receipt to this user
|
|
||||||
if (!SessionMetaProtocol.shouldSendReadReceipt(address)) { continue; }
|
if (!SessionMetaProtocol.shouldSendReadReceipt(address)) { continue; }
|
||||||
ReadReceipt readReceipt = new ReadReceipt(timestamps);
|
ReadReceipt readReceipt = new ReadReceipt(timestamps);
|
||||||
readReceipt.setSentTimestamp(System.currentTimeMillis());
|
readReceipt.setSentTimestamp(System.currentTimeMillis());
|
||||||
|
@ -9,6 +9,7 @@ import org.thoughtcrime.securesms.database.DatabaseFactory
|
|||||||
import org.thoughtcrime.securesms.database.MessagingDatabase.SyncMessageId
|
import org.thoughtcrime.securesms.database.MessagingDatabase.SyncMessageId
|
||||||
|
|
||||||
class ReadReceiptManager: SSKEnvironment.ReadReceiptManagerProtocol {
|
class ReadReceiptManager: SSKEnvironment.ReadReceiptManagerProtocol {
|
||||||
|
|
||||||
override fun processReadReceipts(context: Context, fromRecipientId: String, sentTimestamps: List<Long>, readTimestamp: Long) {
|
override fun processReadReceipts(context: Context, fromRecipientId: String, sentTimestamps: List<Long>, readTimestamp: Long) {
|
||||||
if (TextSecurePreferences.isReadReceiptsEnabled(context)) {
|
if (TextSecurePreferences.isReadReceiptsEnabled(context)) {
|
||||||
|
|
||||||
|
@ -16,7 +16,7 @@
|
|||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/textView"
|
android:id="@+id/textView"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:textSize="@dimen/very_small_font_size"
|
android:textSize="@dimen/very_small_font_size"
|
||||||
android:textColor="@color/text"
|
android:textColor="@color/text"
|
||||||
|
@ -128,6 +128,7 @@
|
|||||||
<!-- The actual record button overlay -->
|
<!-- The actual record button overlay -->
|
||||||
|
|
||||||
<RelativeLayout
|
<RelativeLayout
|
||||||
|
android:id="@+id/recordButtonOverlay"
|
||||||
android:layout_width="80dp"
|
android:layout_width="80dp"
|
||||||
android:layout_height="80dp"
|
android:layout_height="80dp"
|
||||||
android:layout_alignParentEnd="true"
|
android:layout_alignParentEnd="true"
|
||||||
|
@ -872,4 +872,6 @@
|
|||||||
<string name="dialog_download_button_title">Download</string>
|
<string name="dialog_download_button_title">Download</string>
|
||||||
|
|
||||||
<string name="activity_conversation_blocked_banner_text">%s is blocked. Unblock them?</string>
|
<string name="activity_conversation_blocked_banner_text">%s is blocked. Unblock them?</string>
|
||||||
|
|
||||||
|
<string name="activity_conversation_attachment_prep_failed">Failed to prepare attachment for sending.</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
@ -18,7 +18,7 @@
|
|||||||
<item name="android:colorBackground">@color/default_background_start</item>
|
<item name="android:colorBackground">@color/default_background_start</item>
|
||||||
<item name="android:navigationBarColor">@color/compose_view_background</item>
|
<item name="android:navigationBarColor">@color/compose_view_background</item>
|
||||||
|
|
||||||
<item name="actionBarPopupTheme">@style/ThemeOverlay.AppCompat.Light</item>
|
<item name="actionBarPopupTheme">@style/ThemeOverlay.AppCompat.DayNight</item>
|
||||||
<item name="actionBarWidgetTheme">@null</item>
|
<item name="actionBarWidgetTheme">@null</item>
|
||||||
<item name="actionBarTheme">@style/ThemeOverlay.AppCompat.DayNight.ActionBar</item>
|
<item name="actionBarTheme">@style/ThemeOverlay.AppCompat.DayNight.ActionBar</item>
|
||||||
<item name="actionBarStyle">@style/Widget.Session.ActionBar</item>
|
<item name="actionBarStyle">@style/Widget.Session.ActionBar</item>
|
||||||
|
Loading…
x
Reference in New Issue
Block a user