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