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:
jubb 2021-06-28 11:39:11 +10:00
commit 4498b6e00f
20 changed files with 503 additions and 285 deletions

View File

@ -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);

View File

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

View File

@ -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 }

View File

@ -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)
}

View File

@ -8,7 +8,6 @@ import android.view.MotionEvent
import android.widget.RelativeLayout import android.widget.RelativeLayout
import androidx.core.view.isVisible import androidx.core.view.isVisible
import kotlinx.android.synthetic.main.view_input_bar.view.* import kotlinx.android.synthetic.main.view_input_bar.view.*
import kotlinx.android.synthetic.main.view_quote.view.*
import network.loki.messenger.R import network.loki.messenger.R
import org.session.libsession.messaging.sending_receiving.link_preview.LinkPreview import org.session.libsession.messaging.sending_receiving.link_preview.LinkPreview
import org.thoughtcrime.securesms.conversation.v2.components.LinkPreviewDraftView import org.thoughtcrime.securesms.conversation.v2.components.LinkPreviewDraftView
@ -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()
} }

View File

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

View File

@ -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)

View File

@ -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

View File

@ -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() {

View File

@ -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())

View File

@ -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)
} }

View File

@ -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
} }

View File

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

View File

@ -0,0 +1,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
)
}
}

View File

@ -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());

View File

@ -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)) {

View File

@ -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"

View File

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

View File

@ -872,4 +872,6 @@
<string name="dialog_download_button_title">Download</string> <string name="dialog_download_button_title">Download</string>
<string name="activity_conversation_blocked_banner_text">%s is blocked. Unblock them?</string> <string name="activity_conversation_blocked_banner_text">%s is blocked. Unblock them?</string>
<string name="activity_conversation_attachment_prep_failed">Failed to prepare attachment for sending.</string>
</resources> </resources>

View File

@ -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>