Merge remote-tracking branch 'upstream/ui' into ui

# Conflicts:
#	app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationActivityV2.kt
This commit is contained in:
jubb 2021-07-01 11:06:34 +10:00
commit 7459765a52
34 changed files with 759 additions and 196 deletions

View File

@ -143,7 +143,7 @@ dependencies {
testImplementation 'org.robolectric:shadows-multidex:4.2' testImplementation 'org.robolectric:shadows-multidex:4.2'
} }
def canonicalVersionCode = 184 def canonicalVersionCode = 186
def canonicalVersionName = "1.11.0" def canonicalVersionName = "1.11.0"
def postFixSize = 10 def postFixSize = 10

View File

@ -39,6 +39,7 @@ import org.session.libsession.utilities.DistributionTypes;
import org.thoughtcrime.securesms.components.SearchToolbar; import org.thoughtcrime.securesms.components.SearchToolbar;
import org.thoughtcrime.securesms.conversation.ConversationActivity; import org.thoughtcrime.securesms.conversation.ConversationActivity;
import org.session.libsession.utilities.Address; import org.session.libsession.utilities.Address;
import org.thoughtcrime.securesms.conversation.v2.ConversationActivityV2;
import org.thoughtcrime.securesms.database.DatabaseFactory; import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.session.libsignal.utilities.Log; import org.session.libsignal.utilities.Log;
import org.thoughtcrime.securesms.loki.fragments.ContactSelectionListFragment; import org.thoughtcrime.securesms.loki.fragments.ContactSelectionListFragment;
@ -215,9 +216,9 @@ public class ShareActivity extends PassphraseRequiredActionBarActivity
} }
private void createConversation(long threadId, Address address, int distributionType) { private void createConversation(long threadId, Address address, int distributionType) {
final Intent intent = getBaseShareIntent(ConversationActivity.class); final Intent intent = getBaseShareIntent(ConversationActivityV2.class);
intent.putExtra(ConversationActivity.ADDRESS_EXTRA, address); intent.putExtra(ConversationActivityV2.ADDRESS, address);
intent.putExtra(ConversationActivity.THREAD_ID_EXTRA, threadId); intent.putExtra(ConversationActivityV2.THREAD_ID, threadId);
intent.putExtra(ConversationActivity.DISTRIBUTION_TYPE_EXTRA, distributionType); intent.putExtra(ConversationActivity.DISTRIBUTION_TYPE_EXTRA, distributionType);
isPassingAlongMedia = true; isPassingAlongMedia = true;

View File

@ -14,6 +14,7 @@ import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import org.thoughtcrime.securesms.ApplicationContext; import org.thoughtcrime.securesms.ApplicationContext;
import org.thoughtcrime.securesms.conversation.v2.components.ExpirationTimerView;
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.service.ExpiringMessageManager; import org.thoughtcrime.securesms.service.ExpiringMessageManager;

View File

@ -14,6 +14,7 @@ import android.view.WindowManager;
import org.session.libsignal.utilities.Log; import org.session.libsignal.utilities.Log;
import org.session.libsignal.utilities.ListenableFuture; import org.session.libsignal.utilities.ListenableFuture;
import org.thoughtcrime.securesms.conversation.v2.ConversationActivityV2;
import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutionException;
@ -80,9 +81,9 @@ public class ConversationPopupActivity extends ConversationActivity {
@Override @Override
public void onSuccess(Long result) { public void onSuccess(Long result) {
ActivityOptionsCompat transition = ActivityOptionsCompat.makeScaleUpAnimation(getWindow().getDecorView(), 0, 0, getWindow().getAttributes().width, getWindow().getAttributes().height); ActivityOptionsCompat transition = ActivityOptionsCompat.makeScaleUpAnimation(getWindow().getDecorView(), 0, 0, getWindow().getAttributes().width, getWindow().getAttributes().height);
Intent intent = new Intent(ConversationPopupActivity.this, ConversationActivity.class); Intent intent = new Intent(ConversationPopupActivity.this, ConversationActivityV2.class);
intent.putExtra(ConversationActivity.ADDRESS_EXTRA, getRecipient().getAddress()); intent.putExtra(ConversationActivityV2.ADDRESS, getRecipient().getAddress());
intent.putExtra(ConversationActivity.THREAD_ID_EXTRA, result); intent.putExtra(ConversationActivityV2.THREAD_ID, result);
if (VERSION.SDK_INT >= VERSION_CODES.JELLY_BEAN) { if (VERSION.SDK_INT >= VERSION_CODES.JELLY_BEAN) {
startActivity(intent, transition.toBundle()); startActivity(intent, transition.toBundle());

View File

@ -18,10 +18,14 @@ import android.util.Log
import android.util.Pair import android.util.Pair
import android.util.TypedValue import android.util.TypedValue
import android.view.* import android.view.*
import android.widget.LinearLayout
import android.widget.RelativeLayout import android.widget.RelativeLayout
import android.widget.Toast import android.widget.Toast
import androidx.annotation.DimenRes
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelProvider
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
@ -57,16 +61,21 @@ import org.session.libsession.messaging.sending_receiving.link_preview.LinkPrevi
import org.session.libsession.messaging.sending_receiving.quotes.QuoteModel import org.session.libsession.messaging.sending_receiving.quotes.QuoteModel
import org.session.libsession.messaging.utilities.UpdateMessageData import org.session.libsession.messaging.utilities.UpdateMessageData
import org.session.libsession.messaging.utilities.UpdateMessageData.Companion.fromJSON import org.session.libsession.messaging.utilities.UpdateMessageData.Companion.fromJSON
import org.session.libsession.utilities.Address
import org.session.libsession.utilities.Address.Companion.fromSerialized import org.session.libsession.utilities.Address.Companion.fromSerialized
import org.session.libsession.utilities.MediaTypes import org.session.libsession.utilities.MediaTypes
import org.session.libsession.utilities.TextSecurePreferences import org.session.libsession.utilities.TextSecurePreferences
import org.session.libsession.utilities.concurrent.SimpleTask
import org.session.libsession.utilities.recipients.Recipient import org.session.libsession.utilities.recipients.Recipient
import org.session.libsession.utilities.recipients.RecipientModifiedListener
import org.session.libsignal.utilities.ListenableFuture import org.session.libsignal.utilities.ListenableFuture
import org.session.libsignal.utilities.ThreadUtils import org.session.libsignal.utilities.SettableFuture
import org.session.libsignal.utilities.guava.Optional
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.audio.AudioRecorder
import org.thoughtcrime.securesms.contactshare.SimpleTextWatcher import org.thoughtcrime.securesms.contactshare.SimpleTextWatcher
import org.thoughtcrime.securesms.conversation.ConversationActivity
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
@ -75,7 +84,10 @@ 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.ConversationActionModeCallbackDelegate import org.thoughtcrime.securesms.conversation.v2.menus.ConversationActionModeCallbackDelegate
import org.thoughtcrime.securesms.conversation.v2.menus.ConversationMenuHelper import org.thoughtcrime.securesms.conversation.v2.menus.ConversationMenuHelper
import org.thoughtcrime.securesms.conversation.v2.messages.VisibleMessageContentViewDelegate
import org.thoughtcrime.securesms.conversation.v2.messages.VisibleMessageView import org.thoughtcrime.securesms.conversation.v2.messages.VisibleMessageView
import org.thoughtcrime.securesms.conversation.v2.search.SearchBottomBar
import org.thoughtcrime.securesms.conversation.v2.search.SearchViewModel
import org.thoughtcrime.securesms.conversation.v2.utilities.AttachmentManager 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
@ -111,7 +123,8 @@ import kotlin.math.*
class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDelegate, class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDelegate,
InputBarRecordingViewDelegate, AttachmentManager.AttachmentListener, ActivityDispatcher, InputBarRecordingViewDelegate, AttachmentManager.AttachmentListener, ActivityDispatcher,
ConversationActionModeCallbackDelegate { ConversationActionModeCallbackDelegate, VisibleMessageContentViewDelegate, RecipientModifiedListener,
SearchBottomBar.EventListener {
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
@ -130,6 +143,15 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
private var previousText: CharSequence = "" private var previousText: CharSequence = ""
private var currentMentionStartIndex = -1 private var currentMentionStartIndex = -1
private var isShowingMentionCandidatesView = false private var isShowingMentionCandidatesView = false
// Search
var searchViewModel: SearchViewModel? = null
var searchViewItem: MenuItem? = null
private val isScrolledToBottom: Boolean
get() {
val position = layoutManager.findFirstCompletelyVisibleItemPosition()
return position == 0
}
private val layoutManager: LinearLayoutManager private val layoutManager: LinearLayoutManager
get() { return conversationRecyclerView.layoutManager as LinearLayoutManager } get() { return conversationRecyclerView.layoutManager as LinearLayoutManager }
@ -150,6 +172,7 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
}, },
glide glide
) )
adapter.visibleMessageContentViewDelegate = this
adapter adapter
} }
@ -166,7 +189,10 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
// region Settings // region Settings
companion object { companion object {
// Extras
const val THREAD_ID = "thread_id" const val THREAD_ID = "thread_id"
const val ADDRESS = "address"
// Request codes
const val PICK_DOCUMENT = 2 const val PICK_DOCUMENT = 2
const val TAKE_PHOTO = 7 const val TAKE_PHOTO = 7
const val PICK_GIF = 10 const val PICK_GIF = 10
@ -179,7 +205,13 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
override fun onCreate(savedInstanceState: Bundle?, isReady: Boolean) { override fun onCreate(savedInstanceState: Bundle?, isReady: Boolean) {
super.onCreate(savedInstanceState, isReady) super.onCreate(savedInstanceState, isReady)
setContentView(R.layout.activity_conversation_v2) setContentView(R.layout.activity_conversation_v2)
threadID = intent.getLongExtra(THREAD_ID, -1) var threadID = intent.getLongExtra(THREAD_ID, -1L)
if (threadID == -1L) {
val address = intent.getParcelableExtra<Address>(ADDRESS) ?: return finish()
val recipient = Recipient.from(this, address, false)
threadID = DatabaseFactory.getThreadDatabase(this).getOrCreateThreadIdFor(recipient)
}
this.threadID = threadID
setUpRecyclerView() setUpRecyclerView()
setUpToolBar() setUpToolBar()
setUpInputBar() setUpInputBar()
@ -189,12 +221,16 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
unreadCount = DatabaseFactory.getMmsSmsDatabase(this).getUnreadCount(threadID) unreadCount = DatabaseFactory.getMmsSmsDatabase(this).getUnreadCount(threadID)
updateUnreadCountIndicator() updateUnreadCountIndicator()
setUpTypingObserver() setUpTypingObserver()
setUpRecipientObserver()
updateSubtitle() updateSubtitle()
getLatestOpenGroupInfoIfNeeded() getLatestOpenGroupInfoIfNeeded()
setUpBlockedBanner() setUpBlockedBanner()
setUpLinkPreviewObserver() setUpLinkPreviewObserver()
searchBottomBar.setEventListener(this)
setUpSearchResultObserver()
scrollToFirstUnreadMessageIfNeeded() scrollToFirstUnreadMessageIfNeeded()
markAllAsRead() markAllAsRead()
showOrHideInputIfNeeded()
} }
override fun onResume() { override fun onResume() {
@ -251,6 +287,14 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
actionBar.setCustomView(R.layout.activity_conversation_v2_action_bar) actionBar.setCustomView(R.layout.activity_conversation_v2_action_bar)
actionBar.setDisplayShowCustomEnabled(true) actionBar.setDisplayShowCustomEnabled(true)
conversationTitleView.text = thread.toShortString() conversationTitleView.text = thread.toShortString()
@DimenRes val sizeID: Int
if (thread.isClosedGroupRecipient) {
sizeID = R.dimen.medium_profile_picture_size
} else {
sizeID = R.dimen.small_profile_picture_size
}
val size = resources.getDimension(sizeID).roundToInt()
profilePictureView.layoutParams = LinearLayout.LayoutParams(size, size)
profilePictureView.glide = glide profilePictureView.glide = glide
profilePictureView.update(thread, threadID) profilePictureView.update(thread, threadID)
} }
@ -281,6 +325,27 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
} }
private fun restoreDraftIfNeeded() { private fun restoreDraftIfNeeded() {
val mediaURI = intent.data
val mediaType = AttachmentManager.MediaType.from(intent.type)
if (mediaURI != null && mediaType != null) {
if (AttachmentManager.MediaType.IMAGE == mediaType || AttachmentManager.MediaType.GIF == mediaType || AttachmentManager.MediaType.VIDEO == mediaType) {
val media = Media(mediaURI, MediaUtil.getMimeType(this, mediaURI)!!, 0, 0, 0, 0, Optional.absent(), Optional.absent())
startActivityForResult(MediaSendActivity.buildEditorIntent(this, listOf( media ), thread, ""), ConversationActivityV2.PICK_FROM_LIBRARY)
return
} else {
prepMediaForSending(mediaURI, mediaType).addListener(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()
}
})
return
}
}
val draftDB = DatabaseFactory.getDraftDatabase(this) val draftDB = DatabaseFactory.getDraftDatabase(this)
val drafts = draftDB.getDrafts(threadID) val drafts = draftDB.getDrafts(threadID)
draftDB.clearDrafts(threadID) draftDB.clearDrafts(threadID)
@ -302,7 +367,9 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
private fun setUpTypingObserver() { private fun setUpTypingObserver() {
ApplicationContext.getInstance(this).typingStatusRepository.getTypists(threadID).observe(this) { state -> ApplicationContext.getInstance(this).typingStatusRepository.getTypists(threadID).observe(this) { state ->
val recipients = if (state != null) state.typists else listOf() val recipients = if (state != null) state.typists else listOf()
typingIndicatorViewContainer.isVisible = recipients.isNotEmpty() // FIXME: Also checking isScrolledToBottom is a quick fix for an issue where the
// typing indicator overlays the recycler view when scrolled up
typingIndicatorViewContainer.isVisible = recipients.isNotEmpty() && isScrolledToBottom
typingIndicatorViewContainer.setTypists(recipients) typingIndicatorViewContainer.setTypists(recipients)
inputBarHeightChanged(inputBar.height) inputBarHeightChanged(inputBar.height)
} }
@ -316,6 +383,10 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
} }
} }
private fun setUpRecipientObserver() {
thread.addListener(this)
}
private fun getLatestOpenGroupInfoIfNeeded() { private fun getLatestOpenGroupInfoIfNeeded() {
val openGroup = DatabaseFactory.getLokiThreadDatabase(this).getOpenGroupChat(threadID) ?: return val openGroup = DatabaseFactory.getLokiThreadDatabase(this).getOpenGroupChat(threadID) ?: return
OpenGroupAPIV2.getMemberCount(openGroup.room, openGroup.server).successUi { updateSubtitle() } OpenGroupAPIV2.getMemberCount(openGroup.room, openGroup.server).successUi { updateSubtitle() }
@ -358,7 +429,7 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
} }
override fun onPrepareOptionsMenu(menu: Menu): Boolean { override fun onPrepareOptionsMenu(menu: Menu): Boolean {
ConversationMenuHelper.onPrepareOptionsMenu(menu, menuInflater, thread, this) { onOptionsItemSelected(it) } ConversationMenuHelper.onPrepareOptionsMenu(menu, menuInflater, thread, threadID, this) { onOptionsItemSelected(it) }
super.onPrepareOptionsMenu(menu) super.onPrepareOptionsMenu(menu)
return true return true
} }
@ -369,7 +440,28 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
} }
// endregion // endregion
// region Updating & Animation // region Animation & Updating
override fun onModified(recipient: Recipient) {
runOnUiThread {
if (thread.isContactRecipient) {
blockedBanner.isVisible = thread.isBlocked
}
updateSubtitle()
showOrHideInputIfNeeded()
}
}
private fun showOrHideInputIfNeeded() {
if (thread.isClosedGroupRecipient) {
val group = DatabaseFactory.getGroupDatabase(this).getGroup(thread.address.toGroupString()).orNull()
val isActive = (group?.isActive == true)
Log.d("Test", "isActive: $isActive")
inputBar.showInput = isActive
} else {
inputBar.showInput = true
}
}
private fun markAllAsRead() { private fun markAllAsRead() {
val messages = DatabaseFactory.getThreadDatabase(this).setRead(threadID, true) val messages = DatabaseFactory.getThreadDatabase(this).setRead(threadID, true)
if (thread.isGroupRecipient) { if (thread.isGroupRecipient) {
@ -570,8 +662,15 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
} }
private fun handleRecyclerViewScrolled() { private fun handleRecyclerViewScrolled() {
val position = layoutManager.findFirstCompletelyVisibleItemPosition() val alpha = if (!isScrolledToBottom) 1.0f else 0.0f
val alpha = if (position > 0) 1.0f else 0.0f // FIXME: Checking isScrolledToBottom is a quick fix for an issue where the
// typing indicator overlays the recycler view when scrolled up
val wasTypingIndicatorVisibleBefore = typingIndicatorViewContainer.isVisible
typingIndicatorViewContainer.isVisible = wasTypingIndicatorVisibleBefore && isScrolledToBottom
val isTypingIndicatorVisibleAfter = typingIndicatorViewContainer.isVisible
if (isTypingIndicatorVisibleAfter != wasTypingIndicatorVisibleBefore) {
inputBarHeightChanged(inputBar.height)
}
scrollToBottomButton.alpha = alpha scrollToBottomButton.alpha = alpha
unreadCount = min(unreadCount, layoutManager.findFirstVisibleItemPosition()) unreadCount = min(unreadCount, layoutManager.findFirstVisibleItemPosition())
updateUnreadCountIndicator() updateUnreadCountIndicator()
@ -644,6 +743,7 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
val actionMode = this.actionMode val actionMode = this.actionMode
val actionModeCallback = ConversationActionModeCallback(adapter, threadID, this) val actionModeCallback = ConversationActionModeCallback(adapter, threadID, this)
actionModeCallback.delegate = this actionModeCallback.delegate = this
searchViewItem?.collapseActionView()
if (actionMode == null) { // Nothing should be selected if this is the case if (actionMode == null) { // Nothing should be selected if this is the case
adapter.toggleSelection(message, position) adapter.toggleSelection(message, position)
this.actionMode = if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.M) { this.actionMode = if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.M) {
@ -740,6 +840,11 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
this.previousText = newText this.previousText = newText
} }
override fun scrollToMessageIfPossible(timestamp: Long) {
val lastSeenItemPosition = adapter.getItemPositionForTimestamp(timestamp) ?: return
conversationRecyclerView.scrollToPosition(lastSeenItemPosition)
}
override fun sendMessage() { override fun sendMessage() {
if (thread.isContactRecipient && thread.isBlocked) { if (thread.isContactRecipient && thread.isBlocked) {
BlockedDialog(thread).show(supportFragmentManager, "Blocked Dialog") BlockedDialog(thread).show(supportFragmentManager, "Blocked Dialog")
@ -908,10 +1013,18 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
} }
override fun startRecordingVoiceMessage() { override fun startRecordingVoiceMessage() {
if (Permissions.hasAll(this, Manifest.permission.RECORD_AUDIO)) {
showVoiceMessageUI() showVoiceMessageUI()
window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
audioRecorder.startRecording() audioRecorder.startRecording()
stopAudioHandler.postDelayed(stopVoiceMessageRecordingTask, 60000) // Limit voice messages to 1 minute each stopAudioHandler.postDelayed(stopVoiceMessageRecordingTask, 60000) // Limit voice messages to 1 minute each
} else {
Permissions.with(this)
.request(Manifest.permission.RECORD_AUDIO)
.withRationaleDialog(getString(R.string.ConversationActivity_to_send_audio_messages_allow_signal_access_to_your_microphone), R.drawable.ic_baseline_mic_48)
.withPermanentDenialDialog(getString(R.string.ConversationActivity_signal_requires_the_microphone_permission_in_order_to_send_audio_messages))
.execute()
}
} }
override fun sendVoiceMessage() { override fun sendVoiceMessage() {
@ -966,7 +1079,6 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
} }
} }
} else { } else {
ThreadUtils.queue {
for (message in messages) { for (message in messages) {
if (message.isMms) { if (message.isMms) {
DatabaseFactory.getMmsDatabase(this@ConversationActivityV2).delete(message.id) DatabaseFactory.getMmsDatabase(this@ConversationActivityV2).delete(message.id)
@ -975,7 +1087,6 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
} }
} }
} }
}
endActionMode() endActionMode()
} }
builder.setNegativeButton(android.R.string.cancel) { dialog, _ -> builder.setNegativeButton(android.R.string.cancel) { dialog, _ ->
@ -1157,4 +1268,46 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
draftDB.insertDrafts(threadID, drafts) draftDB.insertDrafts(threadID, drafts)
} }
// endregion // endregion
// region Search
private fun setUpSearchResultObserver() {
val searchViewModel = ViewModelProvider(this).get(SearchViewModel::class.java)
this.searchViewModel = searchViewModel
searchViewModel.searchResults.observe(this, Observer { result: SearchViewModel.SearchResult? ->
if (result == null) return@Observer
if (result.getResults().isNotEmpty()) {
result.getResults()[result.position]?.let {
jumpToMessage(it.messageRecipient.address, it.receivedTimestampMs, Runnable { searchViewModel.onMissingResult() })
}
}
this.searchBottomBar.setData(result.position, result.getResults().size)
})
}
fun onSearchQueryUpdated(query: String?) {
adapter.onSearchQueryUpdated(query)
}
override fun onSearchMoveUpPressed() {
this.searchViewModel?.onMoveUp()
}
override fun onSearchMoveDownPressed() {
this.searchViewModel?.onMoveDown()
}
private fun jumpToMessage(author: Address, timestamp: Long, onMessageNotFound: Runnable?) {
SimpleTask.run(lifecycle, {
DatabaseFactory.getMmsSmsDatabase(this).getMessagePositionInConversation(threadID, timestamp, author)
}) { p: Int -> moveToMessagePosition(p, onMessageNotFound) }
}
private fun moveToMessagePosition(position: Int, onMessageNotFound: Runnable?) {
if (position >= 0) {
conversationRecyclerView.scrollToPosition(position)
} else {
onMessageNotFound?.run()
}
}
// endregion
} }

View File

@ -9,6 +9,7 @@ import androidx.core.view.isVisible
import androidx.recyclerview.widget.RecyclerView.ViewHolder import androidx.recyclerview.widget.RecyclerView.ViewHolder
import kotlinx.android.synthetic.main.view_visible_message.view.* import kotlinx.android.synthetic.main.view_visible_message.view.*
import org.thoughtcrime.securesms.conversation.v2.messages.ControlMessageView import org.thoughtcrime.securesms.conversation.v2.messages.ControlMessageView
import org.thoughtcrime.securesms.conversation.v2.messages.VisibleMessageContentViewDelegate
import org.thoughtcrime.securesms.conversation.v2.messages.VisibleMessageView import org.thoughtcrime.securesms.conversation.v2.messages.VisibleMessageView
import org.thoughtcrime.securesms.database.CursorRecyclerViewAdapter import org.thoughtcrime.securesms.database.CursorRecyclerViewAdapter
import org.thoughtcrime.securesms.database.DatabaseFactory import org.thoughtcrime.securesms.database.DatabaseFactory
@ -21,6 +22,8 @@ class ConversationAdapter(context: Context, cursor: Cursor, private val onItemPr
: CursorRecyclerViewAdapter<ViewHolder>(context, cursor) { : CursorRecyclerViewAdapter<ViewHolder>(context, cursor) {
private val messageDB = DatabaseFactory.getMmsSmsDatabase(context) private val messageDB = DatabaseFactory.getMmsSmsDatabase(context)
var selectedItems = mutableSetOf<MessageRecord>() var selectedItems = mutableSetOf<MessageRecord>()
private var searchQuery: String? = null
var visibleMessageContentViewDelegate: VisibleMessageContentViewDelegate? = null
sealed class ViewType(val rawValue: Int) { sealed class ViewType(val rawValue: Int) {
object Visible : ViewType(0) object Visible : ViewType(0)
@ -69,10 +72,11 @@ class ConversationAdapter(context: Context, cursor: Cursor, private val onItemPr
view.snIsSelected = isSelected view.snIsSelected = isSelected
view.messageTimestampTextView.isVisible = isSelected view.messageTimestampTextView.isVisible = isSelected
val position = viewHolder.adapterPosition val position = viewHolder.adapterPosition
view.bind(message, getMessageBefore(position, cursor), getMessageAfter(position, cursor), glide) view.bind(message, getMessageBefore(position, cursor), getMessageAfter(position, cursor), glide, searchQuery)
view.onPress = { event -> onItemPress(message, viewHolder.adapterPosition, view, event) } view.onPress = { event -> onItemPress(message, viewHolder.adapterPosition, view, event) }
view.onSwipeToReply = { onItemSwipeToReply(message, viewHolder.adapterPosition) } view.onSwipeToReply = { onItemSwipeToReply(message, viewHolder.adapterPosition) }
view.onLongPress = { onItemLongPress(message, viewHolder.adapterPosition) } view.onLongPress = { onItemLongPress(message, viewHolder.adapterPosition) }
view.contentViewDelegate = visibleMessageContentViewDelegate
} }
is ControlMessageViewHolder -> viewHolder.view.bind(message) is ControlMessageViewHolder -> viewHolder.view.bind(message)
} }
@ -114,9 +118,25 @@ class ConversationAdapter(context: Context, cursor: Cursor, private val onItemPr
if (lastSeenTimestamp <= 0L || cursor == null || !isActiveCursor) return null if (lastSeenTimestamp <= 0L || cursor == null || !isActiveCursor) return null
for (i in 0 until itemCount) { for (i in 0 until itemCount) {
cursor.moveToPosition(i) cursor.moveToPosition(i)
val messageRecord = messageDB.readerFor(cursor).current val message = messageDB.readerFor(cursor).current
if (messageRecord.isOutgoing || messageRecord.dateReceived <= lastSeenTimestamp) { return i } if (message.isOutgoing || message.dateReceived <= lastSeenTimestamp) { return i }
} }
return null return null
} }
fun getItemPositionForTimestamp(timestamp: Long): Int? {
val cursor = this.cursor
if (timestamp <= 0L || cursor == null || !isActiveCursor) return null
for (i in 0 until itemCount) {
cursor.moveToPosition(i)
val message = messageDB.readerFor(cursor).current
if (message.dateSent == timestamp) { return i }
}
return null
}
fun onSearchQueryUpdated(query: String?) {
this.searchQuery = query
notifyDataSetChanged()
}
} }

View File

@ -1,4 +1,4 @@
package org.thoughtcrime.securesms.components; package org.thoughtcrime.securesms.conversation.v2.components;
import android.content.Context; import android.content.Context;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
@ -118,5 +118,4 @@ public class ExpirationTimerView extends androidx.appcompat.widget.AppCompatImag
Util.runOnMainDelayed(this, timerView.calculateAnimationDelay(timerView.startedAt, timerView.expiresIn)); Util.runOnMainDelayed(this, timerView.calculateAnimationDelay(timerView.startedAt, timerView.expiresIn));
} }
} }
} }

View File

@ -6,10 +6,12 @@ import android.text.SpannableStringBuilder
import android.text.style.StyleSpan import android.text.style.StyleSpan
import android.view.LayoutInflater import android.view.LayoutInflater
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity
import kotlinx.android.synthetic.main.dialog_join_open_group.view.* import kotlinx.android.synthetic.main.dialog_join_open_group.view.*
import network.loki.messenger.R import network.loki.messenger.R
import org.session.libsession.messaging.open_groups.OpenGroupV2 import org.session.libsession.messaging.open_groups.OpenGroupV2
import org.session.libsession.utilities.OpenGroupUrlParser import org.session.libsession.utilities.OpenGroupUrlParser
import org.session.libsignal.utilities.ThreadUtils
import org.thoughtcrime.securesms.conversation.v2.utilities.BaseDialog import org.thoughtcrime.securesms.conversation.v2.utilities.BaseDialog
import org.thoughtcrime.securesms.loki.api.OpenGroupManager import org.thoughtcrime.securesms.loki.api.OpenGroupManager
import org.thoughtcrime.securesms.loki.protocol.MultiDeviceProtocol import org.thoughtcrime.securesms.loki.protocol.MultiDeviceProtocol
@ -33,8 +35,11 @@ class JoinOpenGroupDialog(private val name: String, private val url: String) : B
private fun join() { private fun join() {
val openGroup = OpenGroupUrlParser.parseUrl(url) val openGroup = OpenGroupUrlParser.parseUrl(url)
OpenGroupManager.add(openGroup.server, openGroup.room, openGroup.serverPublicKey, requireContext()) val activity = requireContext() as AppCompatActivity
MultiDeviceProtocol.forceSyncConfigurationNowIfNeeded(requireContext()) ThreadUtils.queue {
OpenGroupManager.add(openGroup.server, openGroup.room, openGroup.serverPublicKey, activity)
MultiDeviceProtocol.forceSyncConfigurationNowIfNeeded(activity)
}
dismiss() dismiss()
} }
} }

View File

@ -32,6 +32,8 @@ class InputBar : RelativeLayout, InputBarEditTextDelegate, QuoteViewDelegate, Li
var additionalContentHeight = 0 var additionalContentHeight = 0
var quote: MessageRecord? = null var quote: MessageRecord? = null
var linkPreview: LinkPreview? = null var linkPreview: LinkPreview? = null
var showInput: Boolean = true
set(value) { field = value; showOrHideInputIfNeeded() }
var text: String var text: String
get() { return inputBarEditText.text.toString() } get() { return inputBarEditText.text.toString() }
@ -161,6 +163,19 @@ class InputBar : RelativeLayout, InputBarEditTextDelegate, QuoteViewDelegate, Li
additionalContentHeight = 0 additionalContentHeight = 0
setHeight(newHeight) setHeight(newHeight)
} }
private fun showOrHideInputIfNeeded() {
if (showInput) {
setOf( inputBarEditText, attachmentsButton ).forEach { it.isVisible = true }
microphoneButton.isVisible = text.isEmpty()
sendButton.isVisible = text.isNotEmpty()
} else {
cancelQuoteDraft()
cancelLinkPreviewDraft()
val views = setOf( inputBarEditText, attachmentsButton, microphoneButton, sendButton )
views.forEach { it.isVisible = false }
}
}
// endregion // endregion
} }

View File

@ -12,17 +12,20 @@ import android.os.AsyncTask
import android.view.Menu import android.view.Menu
import android.view.MenuInflater import android.view.MenuInflater
import android.view.MenuItem import android.view.MenuItem
import android.view.View
import android.widget.ImageView import android.widget.ImageView
import android.widget.TextView import android.widget.TextView
import android.widget.Toast import android.widget.Toast
import androidx.annotation.ColorInt import androidx.annotation.ColorInt
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.widget.SearchView
import androidx.appcompat.widget.SearchView.OnQueryTextListener
import androidx.core.content.pm.ShortcutInfoCompat import androidx.core.content.pm.ShortcutInfoCompat
import androidx.core.content.pm.ShortcutManagerCompat import androidx.core.content.pm.ShortcutManagerCompat
import androidx.core.graphics.drawable.IconCompat import androidx.core.graphics.drawable.IconCompat
import kotlinx.android.synthetic.main.activity_conversation_v2.*
import network.loki.messenger.R import network.loki.messenger.R
import org.session.libsession.avatars.ContactPhoto
import org.session.libsession.messaging.messages.control.ExpirationTimerUpdate import org.session.libsession.messaging.messages.control.ExpirationTimerUpdate
import org.session.libsession.messaging.sending_receiving.MessageSender import org.session.libsession.messaging.sending_receiving.MessageSender
import org.session.libsession.messaging.sending_receiving.leave import org.session.libsession.messaging.sending_receiving.leave
@ -30,11 +33,9 @@ import org.session.libsession.utilities.ExpirationUtil
import org.session.libsession.utilities.GroupUtil.doubleDecodeGroupID import org.session.libsession.utilities.GroupUtil.doubleDecodeGroupID
import org.session.libsession.utilities.TextSecurePreferences import org.session.libsession.utilities.TextSecurePreferences
import org.session.libsession.utilities.recipients.Recipient import org.session.libsession.utilities.recipients.Recipient
import org.session.libsignal.utilities.Log
import org.session.libsignal.utilities.guava.Optional import org.session.libsignal.utilities.guava.Optional
import org.session.libsignal.utilities.toHexString import org.session.libsignal.utilities.toHexString
import org.thoughtcrime.securesms.* import org.thoughtcrime.securesms.*
import org.thoughtcrime.securesms.conversation.ConversationActivity
import org.thoughtcrime.securesms.conversation.v2.ConversationActivityV2 import org.thoughtcrime.securesms.conversation.v2.ConversationActivityV2
import org.thoughtcrime.securesms.database.DatabaseFactory import org.thoughtcrime.securesms.database.DatabaseFactory
import org.thoughtcrime.securesms.loki.activities.EditClosedGroupActivity import org.thoughtcrime.securesms.loki.activities.EditClosedGroupActivity
@ -43,11 +44,10 @@ import org.thoughtcrime.securesms.loki.activities.SelectContactsActivity
import org.thoughtcrime.securesms.loki.utilities.getColorWithID import org.thoughtcrime.securesms.loki.utilities.getColorWithID
import org.thoughtcrime.securesms.util.BitmapUtil import org.thoughtcrime.securesms.util.BitmapUtil
import java.io.IOException import java.io.IOException
import java.lang.ref.WeakReference
object ConversationMenuHelper { object ConversationMenuHelper {
fun onPrepareOptionsMenu(menu: Menu, inflater: MenuInflater, thread: Recipient, context: Context, onOptionsItemSelected: (MenuItem) -> Unit) { fun onPrepareOptionsMenu(menu: Menu, inflater: MenuInflater, thread: Recipient, threadId: Long, context: Context, onOptionsItemSelected: (MenuItem) -> Unit) {
// Prepare // Prepare
menu.clear() menu.clear()
val isOpenGroup = thread.isOpenGroupRecipient val isOpenGroup = thread.isOpenGroupRecipient
@ -92,6 +92,49 @@ object ConversationMenuHelper {
} else { } else {
inflater.inflate(R.menu.menu_conversation_unmuted, menu) inflater.inflate(R.menu.menu_conversation_unmuted, menu)
} }
// Search
val searchViewItem = menu.findItem(R.id.menu_search)
(context as ConversationActivityV2).searchViewItem = searchViewItem
val searchView = searchViewItem.actionView as SearchView
val searchViewModel = context.searchViewModel!!
val queryListener = object : OnQueryTextListener {
override fun onQueryTextSubmit(query: String): Boolean {
return true
}
override fun onQueryTextChange(query: String): Boolean {
searchViewModel.onQueryUpdated(query, threadId)
context.searchBottomBar.showLoading()
context.onSearchQueryUpdated(query)
return true
}
}
searchViewItem.setOnActionExpandListener(object : MenuItem.OnActionExpandListener {
override fun onMenuItemActionExpand(item: MenuItem): Boolean {
searchView.setOnQueryTextListener(queryListener)
searchViewModel.onSearchOpened()
context.searchBottomBar.visibility = View.VISIBLE
context.searchBottomBar.setData(0, 0)
context.inputBar.visibility = View.GONE
for (i in 0 until menu.size()) {
if (menu.getItem(i) != searchViewItem) {
menu.getItem(i).isVisible = false
}
}
return true
}
override fun onMenuItemActionCollapse(item: MenuItem): Boolean {
searchView.setOnQueryTextListener(null)
searchViewModel.onSearchClosed()
context.searchBottomBar.visibility = View.GONE
context.inputBar.visibility = View.VISIBLE
context.onSearchQueryUpdated(null)
context.invalidateOptionsMenu()
return true
}
})
} }
fun onOptionItemSelected(context: Context, item: MenuItem, thread: Recipient): Boolean { fun onOptionItemSelected(context: Context, item: MenuItem, thread: Recipient): Boolean {
@ -121,7 +164,8 @@ object ConversationMenuHelper {
} }
private fun search(context: Context) { private fun search(context: Context) {
Toast.makeText(context, "Not yet implemented", Toast.LENGTH_LONG).show() // TODO: Implement val searchViewModel = (context as ConversationActivityV2).searchViewModel!!
searchViewModel.onSearchOpened()
} }
@SuppressLint("StaticFieldLeak") @SuppressLint("StaticFieldLeak")
@ -254,7 +298,6 @@ object ConversationMenuHelper {
try { try {
if (isClosedGroup) { if (isClosedGroup) {
MessageSender.leave(groupPublicKey!!, true) MessageSender.leave(groupPublicKey!!, true)
// TODO: Disable input?
} else { } else {
Toast.makeText(context, R.string.ConversationActivity_error_leaving_group, Toast.LENGTH_LONG).show() Toast.makeText(context, R.string.ConversationActivity_error_leaving_group, Toast.LENGTH_LONG).show()
} }
@ -274,13 +317,11 @@ object ConversationMenuHelper {
} }
private fun unmute(context: Context, thread: Recipient) { private fun unmute(context: Context, thread: Recipient) {
thread.setMuted(0)
DatabaseFactory.getRecipientDatabase(context).setMuted(thread, 0) DatabaseFactory.getRecipientDatabase(context).setMuted(thread, 0)
} }
private fun mute(context: Context, thread: Recipient) { private fun mute(context: Context, thread: Recipient) {
MuteDialog.show(context) { until: Long -> MuteDialog.show(context) { until: Long ->
thread.setMuted(until)
DatabaseFactory.getRecipientDatabase(context).setMuted(thread, until) DatabaseFactory.getRecipientDatabase(context).setMuted(thread, until)
} }
} }

View File

@ -32,6 +32,9 @@ class ControlMessageView : LinearLayout {
if (message.isExpirationTimerUpdate) { if (message.isExpirationTimerUpdate) {
iconImageView.setImageDrawable(ResourcesCompat.getDrawable(resources, R.drawable.ic_timer, context.theme)) iconImageView.setImageDrawable(ResourcesCompat.getDrawable(resources, R.drawable.ic_timer, context.theme))
iconImageView.visibility = View.VISIBLE iconImageView.visibility = View.VISIBLE
} else if (message.isMediaSavedNotification) {
iconImageView.setImageDrawable(ResourcesCompat.getDrawable(resources, R.drawable.ic_file_download_white_36dp, context.theme))
iconImageView.visibility = View.VISIBLE
} }
textView.text = message.getDisplayBody(context) textView.text = message.getDisplayBody(context)
} }

View File

@ -42,7 +42,7 @@ class LinkPreviewView : LinearLayout {
// endregion // endregion
// region Updating // region Updating
fun bind(message: MmsMessageRecord, glide: GlideRequests, isStartOfMessageCluster: Boolean, isEndOfMessageCluster: Boolean) { fun bind(message: MmsMessageRecord, glide: GlideRequests, isStartOfMessageCluster: Boolean, isEndOfMessageCluster: Boolean, searchQuery: String?) {
val linkPreview = message.linkPreviews.first() val linkPreview = message.linkPreviews.first()
url = linkPreview.url url = linkPreview.url
// Thumbnail // Thumbnail
@ -60,7 +60,7 @@ class LinkPreviewView : LinearLayout {
} }
titleTextView.setTextColor(ResourcesCompat.getColor(resources, textColorID, context.theme)) titleTextView.setTextColor(ResourcesCompat.getColor(resources, textColorID, context.theme))
// Body // Body
bodyTextView = VisibleMessageContentView.getBodyTextView(context, message) bodyTextView = VisibleMessageContentView.getBodyTextView(context, message, searchQuery)
mainLinkPreviewContainer.addView(bodyTextView) mainLinkPreviewContainer.addView(bodyTextView)
// Corner radii // Corner radii
val cornerRadii = MessageBubbleUtilities.calculateRadii(context, isStartOfMessageCluster, isEndOfMessageCluster, message.isOutgoing) val cornerRadii = MessageBubbleUtilities.calculateRadii(context, isStartOfMessageCluster, isEndOfMessageCluster, message.isOutgoing)

View File

@ -1,20 +1,20 @@
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.Color
import android.graphics.Rect import android.graphics.Rect
import android.graphics.drawable.Drawable import android.graphics.drawable.Drawable
import android.text.style.BackgroundColorSpan
import android.text.style.ForegroundColorSpan
import android.text.method.LinkMovementMethod import android.text.method.LinkMovementMethod
import android.text.style.ReplacementSpan
import android.text.style.URLSpan import android.text.style.URLSpan
import android.text.util.Linkify import android.text.util.Linkify
import android.util.AttributeSet import android.util.AttributeSet
import android.util.Log
import android.util.TypedValue import android.util.TypedValue
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.MotionEvent import android.view.MotionEvent
import android.widget.LinearLayout import android.widget.LinearLayout
import android.widget.TextView import android.widget.TextView
import android.widget.Toast
import androidx.annotation.ColorInt import androidx.annotation.ColorInt
import androidx.annotation.DrawableRes import androidx.annotation.DrawableRes
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
@ -24,6 +24,7 @@ import androidx.core.graphics.BlendModeCompat
import androidx.core.text.getSpans import androidx.core.text.getSpans
import androidx.core.text.toSpannable import androidx.core.text.toSpannable
import androidx.core.text.util.LinkifyCompat import androidx.core.text.util.LinkifyCompat
import kotlinx.android.synthetic.main.view_link_preview.view.*
import kotlinx.android.synthetic.main.view_visible_message_content.view.* import kotlinx.android.synthetic.main.view_visible_message_content.view.*
import network.loki.messenger.R import network.loki.messenger.R
import org.session.libsession.utilities.ThemeUtil import org.session.libsession.utilities.ThemeUtil
@ -38,11 +39,15 @@ import org.thoughtcrime.securesms.database.model.MessageRecord
import org.thoughtcrime.securesms.database.model.MmsMessageRecord import org.thoughtcrime.securesms.database.model.MmsMessageRecord
import org.thoughtcrime.securesms.loki.utilities.* import org.thoughtcrime.securesms.loki.utilities.*
import org.thoughtcrime.securesms.mms.GlideRequests import org.thoughtcrime.securesms.mms.GlideRequests
import org.thoughtcrime.securesms.util.SearchUtil
import org.thoughtcrime.securesms.util.SearchUtil.StyleFactory
import java.util.*
import kotlin.math.roundToInt import kotlin.math.roundToInt
class VisibleMessageContentView : LinearLayout { class VisibleMessageContentView : LinearLayout {
var onContentClick: ((event: MotionEvent) -> Unit)? = null var onContentClick: ((event: MotionEvent) -> Unit)? = null
var onContentDoubleTap: (() -> Unit)? = null var onContentDoubleTap: (() -> Unit)? = null
var delegate: VisibleMessageContentViewDelegate? = null
// region Lifecycle // region Lifecycle
constructor(context: Context) : super(context) { initialize() } constructor(context: Context) : super(context) { initialize() }
@ -56,7 +61,7 @@ class VisibleMessageContentView : LinearLayout {
// region Updating // region Updating
fun bind(message: MessageRecord, isStartOfMessageCluster: Boolean, isEndOfMessageCluster: Boolean, fun bind(message: MessageRecord, isStartOfMessageCluster: Boolean, isEndOfMessageCluster: Boolean,
glide: GlideRequests, maxWidth: Int, thread: Recipient) { glide: GlideRequests, maxWidth: Int, thread: Recipient, searchQuery: String?) {
// Background // Background
val background = getBackground(message.isOutgoing, isStartOfMessageCluster, isEndOfMessageCluster) val background = getBackground(message.isOutgoing, isStartOfMessageCluster, isEndOfMessageCluster)
val colorID = if (message.isOutgoing) R.attr.message_sent_background_color else R.attr.message_received_background_color val colorID = if (message.isOutgoing) R.attr.message_sent_background_color else R.attr.message_received_background_color
@ -70,7 +75,7 @@ class VisibleMessageContentView : LinearLayout {
onContentDoubleTap = null onContentDoubleTap = null
if (message is MmsMessageRecord && message.linkPreviews.isNotEmpty()) { if (message is MmsMessageRecord && message.linkPreviews.isNotEmpty()) {
val linkPreviewView = LinkPreviewView(context) val linkPreviewView = LinkPreviewView(context)
linkPreviewView.bind(message, glide, isStartOfMessageCluster, isEndOfMessageCluster) linkPreviewView.bind(message, glide, isStartOfMessageCluster, isEndOfMessageCluster, searchQuery)
mainContainer.addView(linkPreviewView) mainContainer.addView(linkPreviewView)
onContentClick = { event -> linkPreviewView.calculateHit(event) } onContentClick = { event -> linkPreviewView.calculateHit(event) }
// Body text view is inside the link preview for layout convenience // Body text view is inside the link preview for layout convenience
@ -84,9 +89,16 @@ class VisibleMessageContentView : LinearLayout {
quoteView.bind(quote.author.toString(), quote.text, quote.attachment, thread, quoteView.bind(quote.author.toString(), quote.text, quote.attachment, thread,
message.isOutgoing, maxContentWidth, message.isOpenGroupInvitation, message.threadId, glide) message.isOutgoing, maxContentWidth, message.isOpenGroupInvitation, message.threadId, glide)
mainContainer.addView(quoteView) mainContainer.addView(quoteView)
val bodyTextView = VisibleMessageContentView.getBodyTextView(context, message) val bodyTextView = VisibleMessageContentView.getBodyTextView(context, message, searchQuery)
ViewUtil.setPaddingTop(bodyTextView, 0) ViewUtil.setPaddingTop(bodyTextView, 0)
mainContainer.addView(bodyTextView) mainContainer.addView(bodyTextView)
onContentClick = { event ->
val r = Rect()
quoteView.getGlobalVisibleRect(r)
if (r.contains(event.rawX.roundToInt(), event.rawY.roundToInt())) {
delegate?.scrollToMessageIfPossible(quote.id)
}
}
} 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, isStartOfMessageCluster, isEndOfMessageCluster) voiceMessageView.bind(message, isStartOfMessageCluster, isEndOfMessageCluster)
@ -119,7 +131,7 @@ class VisibleMessageContentView : LinearLayout {
mainContainer.addView(openGroupInvitationView) mainContainer.addView(openGroupInvitationView)
onContentClick = { openGroupInvitationView.joinOpenGroup() } onContentClick = { openGroupInvitationView.joinOpenGroup() }
} else { } else {
val bodyTextView = VisibleMessageContentView.getBodyTextView(context, message) val bodyTextView = VisibleMessageContentView.getBodyTextView(context, message, searchQuery)
mainContainer.addView(bodyTextView) mainContainer.addView(bodyTextView)
onContentClick = { event -> onContentClick = { event ->
// intersectedModalSpans should only be a list of one item // intersectedModalSpans should only be a list of one item
@ -153,7 +165,7 @@ class VisibleMessageContentView : LinearLayout {
// region Convenience // region Convenience
companion object { companion object {
fun getBodyTextView(context: Context, message: MessageRecord): TextView { fun getBodyTextView(context: Context, message: MessageRecord, searchQuery: String?): TextView {
val result = EmojiTextView(context) val result = EmojiTextView(context)
val vPadding = context.resources.getDimension(R.dimen.small_spacing).toInt() val vPadding = context.resources.getDimension(R.dimen.small_spacing).toInt()
val hPadding = toPx(12, context.resources) val hPadding = toPx(12, context.resources)
@ -179,6 +191,9 @@ class VisibleMessageContentView : LinearLayout {
} }
body = MentionUtilities.highlightMentions(body, message.isOutgoing, message.threadId, context) body = MentionUtilities.highlightMentions(body, message.isOutgoing, message.threadId, context)
body = SearchUtil.getHighlightedSpan(Locale.getDefault(), StyleFactory { BackgroundColorSpan(Color.WHITE) }, body, searchQuery)
body = SearchUtil.getHighlightedSpan(Locale.getDefault(), StyleFactory { ForegroundColorSpan(Color.BLACK) }, body, searchQuery)
result.text = body result.text = body
return result return result
} }
@ -196,3 +211,8 @@ class VisibleMessageContentView : LinearLayout {
} }
// endregion // endregion
} }
interface VisibleMessageContentViewDelegate {
fun scrollToMessageIfPossible(timestamp: Long)
}

View File

@ -3,21 +3,27 @@ package org.thoughtcrime.securesms.conversation.v2.messages
import android.content.Context import android.content.Context
import android.content.res.Resources import android.content.res.Resources
import android.graphics.Canvas import android.graphics.Canvas
import android.graphics.ColorFilter
import android.graphics.Rect import android.graphics.Rect
import android.graphics.drawable.ColorDrawable import android.graphics.drawable.ColorDrawable
import android.os.AsyncTask
import android.os.Build import android.os.Build
import android.os.Handler import android.os.Handler
import android.os.Looper import android.os.Looper
import android.util.AttributeSet import android.util.AttributeSet
import android.view.* import android.view.*
import android.widget.LinearLayout import android.widget.LinearLayout
import android.widget.RelativeLayout
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.core.content.res.ResourcesCompat
import androidx.core.view.isVisible import androidx.core.view.isVisible
import kotlinx.android.synthetic.main.view_visible_message.view.* import kotlinx.android.synthetic.main.view_visible_message.view.*
import network.loki.messenger.R import network.loki.messenger.R
import org.session.libsession.messaging.contacts.Contact.ContactContext import org.session.libsession.messaging.contacts.Contact.ContactContext
import org.session.libsession.messaging.open_groups.OpenGroupAPIV2 import org.session.libsession.messaging.open_groups.OpenGroupAPIV2
import org.session.libsession.utilities.ViewUtil import org.session.libsession.utilities.ViewUtil
import org.session.libsignal.utilities.ThreadUtils
import org.thoughtcrime.securesms.ApplicationContext
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.getColorWithID import org.thoughtcrime.securesms.loki.utilities.getColorWithID
@ -47,6 +53,7 @@ class VisibleMessageView : LinearLayout {
var onPress: ((event: MotionEvent) -> Unit)? = null var onPress: ((event: MotionEvent) -> Unit)? = null
var onSwipeToReply: (() -> Unit)? = null var onSwipeToReply: (() -> Unit)? = null
var onLongPress: (() -> Unit)? = null var onLongPress: (() -> Unit)? = null
var contentViewDelegate: VisibleMessageContentViewDelegate? = null
companion object { companion object {
const val swipeToReplyThreshold = 80.0f // dp const val swipeToReplyThreshold = 80.0f // dp
@ -69,7 +76,7 @@ class VisibleMessageView : LinearLayout {
// endregion // endregion
// region Updating // region Updating
fun bind(message: MessageRecord, previous: MessageRecord?, next: MessageRecord?, glide: GlideRequests) { fun bind(message: MessageRecord, previous: MessageRecord?, next: MessageRecord?, glide: GlideRequests, searchQuery: String?) {
val sender = message.individualRecipient val sender = message.individualRecipient
val senderSessionID = sender.address.serialize() val senderSessionID = sender.address.serialize()
val threadID = message.threadId val threadID = message.threadId
@ -137,11 +144,14 @@ class VisibleMessageView : LinearLayout {
} else { } else {
messageStatusImageView.isVisible = false messageStatusImageView.isVisible = false
} }
// Expiration timer
updateExpirationTimer(message)
// Calculate max message bubble width // Calculate max message bubble width
var maxWidth = screenWidth - messageContentContainerLayoutParams.leftMargin - messageContentContainerLayoutParams.rightMargin var maxWidth = screenWidth - messageContentContainerLayoutParams.leftMargin - messageContentContainerLayoutParams.rightMargin
if (profilePictureContainer.visibility != View.GONE) { maxWidth -= profilePictureContainer.width } if (profilePictureContainer.visibility != View.GONE) { maxWidth -= profilePictureContainer.width }
// Populate content view // Populate content view
messageContentView.bind(message, isStartOfMessageCluster, isEndOfMessageCluster, glide, maxWidth, thread) messageContentView.bind(message, isStartOfMessageCluster, isEndOfMessageCluster, glide, maxWidth, thread, searchQuery)
messageContentView.delegate = contentViewDelegate
onDoubleTap = { messageContentView.onContentDoubleTap?.invoke() } onDoubleTap = { messageContentView.onContentDoubleTap?.invoke() }
} }
@ -182,6 +192,37 @@ class VisibleMessageView : LinearLayout {
} }
} }
private fun updateExpirationTimer(message: MessageRecord) {
val expirationTimerViewLayoutParams = expirationTimerView.layoutParams as RelativeLayout.LayoutParams
val ruleToAdd = if (message.isOutgoing) RelativeLayout.ALIGN_PARENT_START else RelativeLayout.ALIGN_PARENT_END
val ruleToRemove = if (message.isOutgoing) RelativeLayout.ALIGN_PARENT_END else RelativeLayout.ALIGN_PARENT_START
expirationTimerViewLayoutParams.removeRule(ruleToRemove)
expirationTimerViewLayoutParams.addRule(ruleToAdd)
expirationTimerView.layoutParams = expirationTimerViewLayoutParams
if (message.expiresIn > 0 && !message.isPending) {
expirationTimerView.setColorFilter(ResourcesCompat.getColor(resources, R.color.text, context.theme))
expirationTimerView.isVisible = true
expirationTimerView.setPercentComplete(0.0f)
if (message.expireStarted > 0) {
expirationTimerView.setExpirationTime(message.expireStarted, message.expiresIn)
expirationTimerView.startAnimation()
if (message.expireStarted + message.expiresIn <= System.currentTimeMillis()) {
ApplicationContext.getInstance(context).expiringMessageManager.checkSchedule()
}
} else if (!message.isOutgoing && !message.isMediaPending) {
ThreadUtils.queue {
val expirationManager = ApplicationContext.getInstance(context).expiringMessageManager
val id = message.getId()
val mms = message.isMms
if (mms) DatabaseFactory.getMmsDatabase(context).markExpireStarted(id) else DatabaseFactory.getSmsDatabase(context).markExpireStarted(id)
expirationManager.scheduleDeletion(id, mms, message.expiresIn)
}
}
} else {
expirationTimerView.isVisible = false
}
}
private fun handleIsSelectedChanged() { private fun handleIsSelectedChanged() {
background = if (snIsSelected) { background = if (snIsSelected) {
ColorDrawable(context.resources.getColorWithID(R.color.message_selected, context.theme)) ColorDrawable(context.resources.getColorWithID(R.color.message_selected, context.theme))
@ -242,6 +283,7 @@ class VisibleMessageView : LinearLayout {
} else { } else {
longPressCallback?.let { gestureHandler.removeCallbacks(it) } longPressCallback?.let { gestureHandler.removeCallbacks(it) }
} }
if (translationX > 0) { return } // Only allow swipes to the left
// The idea here is to asymptotically approach a maximum drag distance // The idea here is to asymptotically approach a maximum drag distance
val damping = 50.0f val damping = 50.0f
val sign = -1.0f val sign = -1.0f

View File

@ -0,0 +1,62 @@
package org.thoughtcrime.securesms.conversation.v2.search
import android.content.Context
import android.util.AttributeSet
import android.view.LayoutInflater
import android.view.View
import android.widget.LinearLayout
import kotlinx.android.synthetic.main.view_search_bottom_bar.view.*
import network.loki.messenger.R
class SearchBottomBar : LinearLayout {
private var eventListener: EventListener? = null
// region Lifecycle
constructor(context: Context) : super(context) { initialize() }
constructor(context: Context, attrs: AttributeSet) : super(context, attrs) { initialize() }
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr) { initialize() }
fun initialize() {
LayoutInflater.from(context).inflate(R.layout.view_search_bottom_bar, this)
}
fun setData(position: Int, count: Int) {
searchProgressWheel.visibility = GONE
searchUp.setOnClickListener { v: View? ->
if (eventListener != null) {
eventListener!!.onSearchMoveUpPressed()
}
}
searchDown.setOnClickListener { v: View? ->
if (eventListener != null) {
eventListener!!.onSearchMoveDownPressed()
}
}
if (count > 0) {
searchPosition.text = resources.getString(R.string.ConversationActivity_search_position, position + 1, count)
} else {
searchPosition.text = ""
}
setViewEnabled(searchUp, position < count - 1)
setViewEnabled(searchDown, position > 0)
}
fun showLoading() {
searchProgressWheel.visibility = VISIBLE
}
private fun setViewEnabled(view: View, enabled: Boolean) {
view.isEnabled = enabled
view.alpha = if (enabled) 1f else 0.25f
}
fun setEventListener(eventListener: EventListener?) {
this.eventListener = eventListener
}
interface EventListener {
fun onSearchMoveUpPressed()
fun onSearchMoveDownPressed()
}
}

View File

@ -0,0 +1,111 @@
package org.thoughtcrime.securesms.conversation.v2.search
import android.app.Application
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.LiveData
import org.session.libsession.utilities.Debouncer
import org.session.libsession.utilities.Util.runOnMain
import org.session.libsession.utilities.concurrent.SignalExecutors
import org.thoughtcrime.securesms.contacts.ContactAccessor
import org.thoughtcrime.securesms.database.CursorList
import org.thoughtcrime.securesms.database.DatabaseFactory
import org.thoughtcrime.securesms.search.SearchRepository
import org.thoughtcrime.securesms.search.model.MessageResult
import org.thoughtcrime.securesms.util.CloseableLiveData
import java.io.Closeable
class SearchViewModel(application: Application) : AndroidViewModel(application) {
private val searchRepository: SearchRepository
private val result: CloseableLiveData<SearchResult>
private val debouncer: Debouncer
private var firstSearch = false
private var searchOpen = false
private var activeQuery: String? = null
private var activeThreadId: Long = 0
val searchResults: LiveData<SearchResult>
get() = result
fun onQueryUpdated(query: String, threadId: Long) {
if (query == activeQuery) {
return
}
updateQuery(query, threadId)
}
fun onMissingResult() {
if (activeQuery != null) {
updateQuery(activeQuery!!, activeThreadId)
}
}
fun onMoveUp() {
debouncer.clear()
val messages = result.value!!.getResults() as CursorList<MessageResult?>
val position = Math.min(result.value!!.position + 1, messages.size - 1)
result.setValue(SearchResult(messages, position), false)
}
fun onMoveDown() {
debouncer.clear()
val messages = result.value!!.getResults() as CursorList<MessageResult?>
val position = Math.max(result.value!!.position - 1, 0)
result.setValue(SearchResult(messages, position), false)
}
fun onSearchOpened() {
searchOpen = true
firstSearch = true
}
fun onSearchClosed() {
searchOpen = false
activeQuery = null
debouncer.clear()
result.close()
}
override fun onCleared() {
super.onCleared()
result.close()
}
private fun updateQuery(query: String, threadId: Long) {
activeQuery = query
activeThreadId = threadId
debouncer.publish {
firstSearch = false
searchRepository.query(query, threadId) { messages: CursorList<MessageResult?> ->
runOnMain {
if (searchOpen && query == activeQuery) {
result.setValue(SearchResult(messages, 0))
} else {
messages.close()
}
}
}
}
}
class SearchResult(private val results: CursorList<MessageResult?>, val position: Int) : Closeable {
fun getResults(): List<MessageResult?> {
return results
}
override fun close() {
results.close()
}
}
init {
val context = application.applicationContext
result = CloseableLiveData()
debouncer = Debouncer(500)
searchRepository = SearchRepository(context,
DatabaseFactory.getSearchDatabase(context),
DatabaseFactory.getThreadDatabase(context),
ContactAccessor.getInstance(),
SignalExecutors.SERIAL)
}
}

View File

@ -57,6 +57,7 @@ import org.session.libsession.utilities.recipients.Recipient;
import org.session.libsession.utilities.recipients.RecipientFormattingException; import org.session.libsession.utilities.recipients.RecipientFormattingException;
import org.session.libsignal.utilities.JsonUtil; import org.session.libsignal.utilities.JsonUtil;
import org.session.libsignal.utilities.Log; import org.session.libsignal.utilities.Log;
import org.session.libsignal.utilities.ThreadUtils;
import org.session.libsignal.utilities.guava.Optional; import org.session.libsignal.utilities.guava.Optional;
import org.thoughtcrime.securesms.attachments.MmsNotificationAttachment; import org.thoughtcrime.securesms.attachments.MmsNotificationAttachment;
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper; import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper;
@ -883,7 +884,7 @@ public class MmsDatabase extends MessagingDatabase {
public boolean delete(long messageId) { public boolean delete(long messageId) {
long threadId = getThreadIdForMessage(messageId); long threadId = getThreadIdForMessage(messageId);
AttachmentDatabase attachmentDatabase = DatabaseFactory.getAttachmentDatabase(context); AttachmentDatabase attachmentDatabase = DatabaseFactory.getAttachmentDatabase(context);
attachmentDatabase.deleteAttachmentsForMessage(messageId); ThreadUtils.queue(() -> attachmentDatabase.deleteAttachmentsForMessage(messageId));
GroupReceiptDatabase groupReceiptDatabase = DatabaseFactory.getGroupReceiptDatabase(context); GroupReceiptDatabase groupReceiptDatabase = DatabaseFactory.getGroupReceiptDatabase(context);
groupReceiptDatabase.deleteRowsForMessage(messageId); groupReceiptDatabase.deleteRowsForMessage(messageId);

View File

@ -20,6 +20,8 @@ package org.thoughtcrime.securesms.database;
import android.content.ContentValues; import android.content.ContentValues;
import android.content.Context; import android.content.Context;
import android.database.Cursor; import android.database.Cursor;
import android.os.Handler;
import android.os.Looper;
import android.text.TextUtils; import android.text.TextUtils;
import android.util.Pair; import android.util.Pair;
@ -411,7 +413,6 @@ public class SmsDatabase extends MessagingDatabase {
notifyConversationListeners(threadId); notifyConversationListeners(threadId);
return Optional.of(new InsertResult(messageId, threadId)); return Optional.of(new InsertResult(messageId, threadId));
} }
} }

View File

@ -1,6 +1,5 @@
package org.thoughtcrime.securesms.database package org.thoughtcrime.securesms.database
import android.app.job.JobScheduler
import android.content.Context import android.content.Context
import android.net.Uri import android.net.Uri
import org.session.libsession.database.StorageProtocol import org.session.libsession.database.StorageProtocol
@ -106,7 +105,7 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context,
} }
else -> Optional.absent() else -> Optional.absent()
} }
val pointerAttachments = attachments.mapNotNull { val pointers = attachments.mapNotNull {
it.toSignalAttachment() it.toSignalAttachment()
} }
val targetAddress = if (isUserSender && !message.syncTarget.isNullOrEmpty()) { val targetAddress = if (isUserSender && !message.syncTarget.isNullOrEmpty()) {
@ -122,7 +121,7 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context,
val linkPreviews: Optional<List<LinkPreview>> = if (linkPreview.isEmpty()) Optional.absent() else Optional.of(linkPreview.mapNotNull { it!! }) val linkPreviews: Optional<List<LinkPreview>> = if (linkPreview.isEmpty()) Optional.absent() else Optional.of(linkPreview.mapNotNull { it!! })
val mmsDatabase = DatabaseFactory.getMmsDatabase(context) val mmsDatabase = DatabaseFactory.getMmsDatabase(context)
val insertResult = if (message.sender == getUserPublicKey()) { val insertResult = if (message.sender == getUserPublicKey()) {
val mediaMessage = OutgoingMediaMessage.from(message, targetRecipient, pointerAttachments, quote.orNull(), linkPreviews.orNull()?.firstOrNull()) val mediaMessage = OutgoingMediaMessage.from(message, targetRecipient, pointers, quote.orNull(), linkPreviews.orNull()?.firstOrNull())
mmsDatabase.insertSecureDecryptedMessageOutbox(mediaMessage, message.threadID ?: -1, message.sentTimestamp!!) mmsDatabase.insertSecureDecryptedMessageOutbox(mediaMessage, message.threadID ?: -1, message.sentTimestamp!!)
} else { } else {
// It seems like we have replaced SignalServiceAttachment with SessionServiceAttachment // It seems like we have replaced SignalServiceAttachment with SessionServiceAttachment

View File

@ -26,6 +26,7 @@ import org.thoughtcrime.securesms.loki.utilities.fadeOut
import org.thoughtcrime.securesms.mms.GlideApp import org.thoughtcrime.securesms.mms.GlideApp
import org.session.libsession.utilities.recipients.Recipient import org.session.libsession.utilities.recipients.Recipient
import org.session.libsession.utilities.TextSecurePreferences import org.session.libsession.utilities.TextSecurePreferences
import org.thoughtcrime.securesms.conversation.v2.ConversationActivityV2
//TODO Refactor to avoid using kotlinx.android.synthetic //TODO Refactor to avoid using kotlinx.android.synthetic
class CreateClosedGroupActivity : PassphraseRequiredActionBarActivity(), LoaderManager.LoaderCallbacks<List<String>> { class CreateClosedGroupActivity : PassphraseRequiredActionBarActivity(), LoaderManager.LoaderCallbacks<List<String>> {
@ -135,10 +136,10 @@ class CreateClosedGroupActivity : PassphraseRequiredActionBarActivity(), LoaderM
// region Convenience // region Convenience
private fun openConversationActivity(context: Context, threadId: Long, recipient: Recipient) { private fun openConversationActivity(context: Context, threadId: Long, recipient: Recipient) {
val intent = Intent(context, ConversationActivity::class.java) val intent = Intent(context, ConversationActivityV2::class.java)
intent.putExtra(ConversationActivity.THREAD_ID_EXTRA, threadId) intent.putExtra(ConversationActivityV2.THREAD_ID, threadId)
intent.putExtra(ConversationActivity.DISTRIBUTION_TYPE_EXTRA, DistributionTypes.DEFAULT) intent.putExtra(ConversationActivity.DISTRIBUTION_TYPE_EXTRA, DistributionTypes.DEFAULT)
intent.putExtra(ConversationActivity.ADDRESS_EXTRA, recipient.address) intent.putExtra(ConversationActivityV2.ADDRESS, recipient.address)
context.startActivity(intent) context.startActivity(intent)
} }
// endregion // endregion

View File

@ -30,6 +30,7 @@ import org.session.libsession.utilities.recipients.Recipient
import org.session.libsignal.utilities.PublicKeyValidation import org.session.libsignal.utilities.PublicKeyValidation
import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity
import org.thoughtcrime.securesms.conversation.ConversationActivity import org.thoughtcrime.securesms.conversation.ConversationActivity
import org.thoughtcrime.securesms.conversation.v2.ConversationActivityV2
import org.thoughtcrime.securesms.database.DatabaseFactory import org.thoughtcrime.securesms.database.DatabaseFactory
import org.thoughtcrime.securesms.loki.fragments.ScanQRCodeWrapperFragment import org.thoughtcrime.securesms.loki.fragments.ScanQRCodeWrapperFragment
import org.thoughtcrime.securesms.loki.fragments.ScanQRCodeWrapperFragmentDelegate import org.thoughtcrime.securesms.loki.fragments.ScanQRCodeWrapperFragmentDelegate
@ -111,12 +112,12 @@ class CreatePrivateChatActivity : PassphraseRequiredActionBarActivity(), ScanQRC
private fun createPrivateChat(hexEncodedPublicKey: String) { private fun createPrivateChat(hexEncodedPublicKey: String) {
val recipient = Recipient.from(this, Address.fromSerialized(hexEncodedPublicKey), false) val recipient = Recipient.from(this, Address.fromSerialized(hexEncodedPublicKey), false)
val intent = Intent(this, ConversationActivity::class.java) val intent = Intent(this, ConversationActivityV2::class.java)
intent.putExtra(ConversationActivity.ADDRESS_EXTRA, recipient.address) intent.putExtra(ConversationActivityV2.ADDRESS, recipient.address)
intent.putExtra(ConversationActivity.TEXT_EXTRA, getIntent().getStringExtra(ConversationActivity.TEXT_EXTRA)) intent.putExtra(ConversationActivity.TEXT_EXTRA, getIntent().getStringExtra(ConversationActivity.TEXT_EXTRA))
intent.setDataAndType(getIntent().data, getIntent().type) intent.setDataAndType(getIntent().data, getIntent().type)
val existingThread = DatabaseFactory.getThreadDatabase(this).getThreadIdIfExistsFor(recipient) val existingThread = DatabaseFactory.getThreadDatabase(this).getThreadIdIfExistsFor(recipient)
intent.putExtra(ConversationActivity.THREAD_ID_EXTRA, existingThread) intent.putExtra(ConversationActivityV2.THREAD_ID, existingThread)
intent.putExtra(ConversationActivity.DISTRIBUTION_TYPE_EXTRA, DistributionTypes.DEFAULT) intent.putExtra(ConversationActivity.DISTRIBUTION_TYPE_EXTRA, DistributionTypes.DEFAULT)
startActivity(intent) startActivity(intent)
finish() finish()

View File

@ -33,6 +33,7 @@ import org.session.libsignal.utilities.PublicKeyValidation
import org.thoughtcrime.securesms.BaseActionBarActivity import org.thoughtcrime.securesms.BaseActionBarActivity
import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity
import org.thoughtcrime.securesms.conversation.ConversationActivity import org.thoughtcrime.securesms.conversation.ConversationActivity
import org.thoughtcrime.securesms.conversation.v2.ConversationActivityV2
import org.thoughtcrime.securesms.groups.GroupManager import org.thoughtcrime.securesms.groups.GroupManager
import org.thoughtcrime.securesms.loki.api.OpenGroupManager import org.thoughtcrime.securesms.loki.api.OpenGroupManager
import org.thoughtcrime.securesms.loki.fragments.ScanQRCodeWrapperFragment import org.thoughtcrime.securesms.loki.fragments.ScanQRCodeWrapperFragment
@ -127,10 +128,10 @@ class JoinPublicChatActivity : PassphraseRequiredActionBarActivity(), ScanQRCode
// region Convenience // region Convenience
private fun openConversationActivity(context: Context, threadId: Long, recipient: Recipient) { private fun openConversationActivity(context: Context, threadId: Long, recipient: Recipient) {
val intent = Intent(context, ConversationActivity::class.java) val intent = Intent(context, ConversationActivityV2::class.java)
intent.putExtra(ConversationActivity.THREAD_ID_EXTRA, threadId) intent.putExtra(ConversationActivityV2.THREAD_ID, threadId)
intent.putExtra(ConversationActivity.DISTRIBUTION_TYPE_EXTRA, DistributionTypes.DEFAULT) intent.putExtra(ConversationActivity.DISTRIBUTION_TYPE_EXTRA, DistributionTypes.DEFAULT)
intent.putExtra(ConversationActivity.ADDRESS_EXTRA, recipient.address) intent.putExtra(ConversationActivityV2.ADDRESS, recipient.address)
context.startActivity(intent) context.startActivity(intent)
} }
// endregion // endregion

View File

@ -26,6 +26,7 @@ import org.session.libsession.utilities.recipients.Recipient
import org.thoughtcrime.securesms.util.FileProviderUtil import org.thoughtcrime.securesms.util.FileProviderUtil
import org.session.libsession.utilities.TextSecurePreferences import org.session.libsession.utilities.TextSecurePreferences
import org.session.libsignal.utilities.PublicKeyValidation import org.session.libsignal.utilities.PublicKeyValidation
import org.thoughtcrime.securesms.conversation.v2.ConversationActivityV2
import java.io.File import java.io.File
import java.io.FileOutputStream import java.io.FileOutputStream
@ -53,12 +54,12 @@ class QRCodeActivity : PassphraseRequiredActionBarActivity(), ScanQRCodeWrapperF
fun createPrivateChatIfPossible(hexEncodedPublicKey: String) { fun createPrivateChatIfPossible(hexEncodedPublicKey: String) {
if (!PublicKeyValidation.isValid(hexEncodedPublicKey)) { return Toast.makeText(this, R.string.invalid_session_id, Toast.LENGTH_SHORT).show() } if (!PublicKeyValidation.isValid(hexEncodedPublicKey)) { return Toast.makeText(this, R.string.invalid_session_id, Toast.LENGTH_SHORT).show() }
val recipient = Recipient.from(this, Address.fromSerialized(hexEncodedPublicKey), false) val recipient = Recipient.from(this, Address.fromSerialized(hexEncodedPublicKey), false)
val intent = Intent(this, ConversationActivity::class.java) val intent = Intent(this, ConversationActivityV2::class.java)
intent.putExtra(ConversationActivity.ADDRESS_EXTRA, recipient.address) intent.putExtra(ConversationActivityV2.ADDRESS, recipient.address)
intent.putExtra(ConversationActivity.TEXT_EXTRA, getIntent().getStringExtra(ConversationActivity.TEXT_EXTRA)) intent.putExtra(ConversationActivity.TEXT_EXTRA, getIntent().getStringExtra(ConversationActivity.TEXT_EXTRA))
intent.setDataAndType(getIntent().data, getIntent().type) intent.setDataAndType(getIntent().data, getIntent().type)
val existingThread = DatabaseFactory.getThreadDatabase(this).getThreadIdIfExistsFor(recipient) val existingThread = DatabaseFactory.getThreadDatabase(this).getThreadIdIfExistsFor(recipient)
intent.putExtra(ConversationActivity.THREAD_ID_EXTRA, existingThread) intent.putExtra(ConversationActivityV2.THREAD_ID, existingThread)
intent.putExtra(ConversationActivity.DISTRIBUTION_TYPE_EXTRA, DistributionTypes.DEFAULT) intent.putExtra(ConversationActivity.DISTRIBUTION_TYPE_EXTRA, DistributionTypes.DEFAULT)
startActivity(intent) startActivity(intent)
finish() finish()

View File

@ -31,23 +31,12 @@ class ProfilePictureView : RelativeLayout {
private val profilePicturesCache = mutableMapOf<String, String?>() private val profilePicturesCache = mutableMapOf<String, String?>()
// region Lifecycle // region Lifecycle
constructor(context: Context) : super(context) { constructor(context: Context) : super(context) { initialize() }
setUpViewHierarchy() constructor(context: Context, attrs: AttributeSet) : super(context, attrs) { initialize() }
} constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr) { initialize() }
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int, defStyleRes: Int) : super(context, attrs, defStyleAttr, defStyleRes) { initialize() }
constructor(context: Context, attrs: AttributeSet) : super(context, attrs) { private fun initialize() {
setUpViewHierarchy()
}
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr) {
setUpViewHierarchy()
}
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int, defStyleRes: Int) : super(context, attrs, defStyleAttr, defStyleRes) {
setUpViewHierarchy()
}
private fun setUpViewHierarchy() {
val inflater = context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater val inflater = context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater
val contentView = inflater.inflate(R.layout.view_profile_picture, null) val contentView = inflater.inflate(R.layout.view_profile_picture, null)
addView(contentView) addView(contentView)

View File

@ -49,6 +49,7 @@ import org.session.libsignal.utilities.Log;
import org.thoughtcrime.securesms.ApplicationContext; import org.thoughtcrime.securesms.ApplicationContext;
import org.thoughtcrime.securesms.contactshare.ContactUtil; import org.thoughtcrime.securesms.contactshare.ContactUtil;
import org.thoughtcrime.securesms.conversation.ConversationActivity; import org.thoughtcrime.securesms.conversation.ConversationActivity;
import org.thoughtcrime.securesms.conversation.v2.ConversationActivityV2;
import org.thoughtcrime.securesms.database.DatabaseFactory; import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.MessagingDatabase.MarkedMessageInfo; import org.thoughtcrime.securesms.database.MessagingDatabase.MarkedMessageInfo;
import org.thoughtcrime.securesms.database.MmsSmsDatabase; import org.thoughtcrime.securesms.database.MmsSmsDatabase;
@ -115,9 +116,9 @@ public class DefaultMessageNotifier implements MessageNotifier {
if (visibleThread == threadId) { if (visibleThread == threadId) {
sendInThreadNotification(context, recipient); sendInThreadNotification(context, recipient);
} else { } else {
Intent intent = new Intent(context, ConversationActivity.class); Intent intent = new Intent(context, ConversationActivityV2.class);
intent.putExtra(ConversationActivity.ADDRESS_EXTRA, recipient.getAddress()); intent.putExtra(ConversationActivityV2.ADDRESS, recipient.getAddress());
intent.putExtra(ConversationActivity.THREAD_ID_EXTRA, threadId); intent.putExtra(ConversationActivityV2.THREAD_ID, threadId);
intent.setData((Uri.parse("custom://" + System.currentTimeMillis()))); intent.setData((Uri.parse("custom://" + System.currentTimeMillis())));
FailedNotificationBuilder builder = new FailedNotificationBuilder(context, TextSecurePreferences.getNotificationPrivacy(context), intent); FailedNotificationBuilder builder = new FailedNotificationBuilder(context, TextSecurePreferences.getNotificationPrivacy(context), intent);

View File

@ -9,6 +9,7 @@ import androidx.annotation.Nullable;
import androidx.core.app.TaskStackBuilder; import androidx.core.app.TaskStackBuilder;
import org.thoughtcrime.securesms.conversation.ConversationActivity; import org.thoughtcrime.securesms.conversation.ConversationActivity;
import org.thoughtcrime.securesms.conversation.v2.ConversationActivityV2;
import org.thoughtcrime.securesms.mms.SlideDeck; import org.thoughtcrime.securesms.mms.SlideDeck;
import org.session.libsession.utilities.recipients.Recipient; import org.session.libsession.utilities.recipients.Recipient;
@ -67,11 +68,11 @@ public class NotificationItem {
} }
public PendingIntent getPendingIntent(Context context) { public PendingIntent getPendingIntent(Context context) {
Intent intent = new Intent(context, ConversationActivity.class); Intent intent = new Intent(context, ConversationActivityV2.class);
Recipient notifyRecipients = threadRecipient != null ? threadRecipient : conversationRecipient; Recipient notifyRecipients = threadRecipient != null ? threadRecipient : conversationRecipient;
if (notifyRecipients != null) intent.putExtra(ConversationActivity.ADDRESS_EXTRA, notifyRecipients.getAddress()); if (notifyRecipients != null) intent.putExtra(ConversationActivityV2.ADDRESS, notifyRecipients.getAddress());
intent.putExtra("thread_id", threadId); intent.putExtra(ConversationActivityV2.THREAD_ID, threadId);
intent.setData((Uri.parse("custom://"+System.currentTimeMillis()))); intent.setData((Uri.parse("custom://"+System.currentTimeMillis())));
return TaskStackBuilder.create(context) return TaskStackBuilder.create(context)

View File

@ -14,6 +14,7 @@ import android.widget.Toast;
import org.thoughtcrime.securesms.conversation.ConversationActivity; import org.thoughtcrime.securesms.conversation.ConversationActivity;
import network.loki.messenger.R; import network.loki.messenger.R;
import org.thoughtcrime.securesms.conversation.v2.ConversationActivityV2;
import org.thoughtcrime.securesms.database.DatabaseFactory; import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.session.libsession.utilities.recipients.Recipient; import org.session.libsession.utilities.recipients.Recipient;
@ -32,9 +33,9 @@ public class CommunicationActions {
@Override @Override
protected void onPostExecute(Long threadId) { protected void onPostExecute(Long threadId) {
Intent intent = new Intent(context, ConversationActivity.class); Intent intent = new Intent(context, ConversationActivityV2.class);
intent.putExtra(ConversationActivity.ADDRESS_EXTRA, recipient.getAddress()); intent.putExtra(ConversationActivityV2.ADDRESS, recipient.getAddress());
intent.putExtra(ConversationActivity.THREAD_ID_EXTRA, threadId); intent.putExtra(ConversationActivityV2.THREAD_ID, threadId);
intent.putExtra(ConversationActivity.TIMING_EXTRA, System.currentTimeMillis()); intent.putExtra(ConversationActivity.TIMING_EXTRA, System.currentTimeMillis());
if (!TextUtils.isEmpty(text)) { if (!TextUtils.isEmpty(text)) {

View File

@ -34,6 +34,13 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_alignParentBottom="true" /> android:layout_alignParentBottom="true" />
<org.thoughtcrime.securesms.conversation.v2.search.SearchBottomBar
android:id="@+id/searchBottomBar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:visibility="gone"/>
<FrameLayout <FrameLayout
android:id="@+id/additionalContentContainer" android:id="@+id/additionalContentContainer"
android:layout_width="match_parent" android:layout_width="match_parent"

View File

@ -1,5 +1,6 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" <LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize" android:layout_height="?attr/actionBarSize"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
@ -9,8 +10,8 @@
<org.thoughtcrime.securesms.loki.views.ProfilePictureView <org.thoughtcrime.securesms.loki.views.ProfilePictureView
android:id="@+id/profilePictureView" android:id="@+id/profilePictureView"
android:layout_width="@dimen/small_profile_picture_size" android:layout_width="@dimen/medium_profile_picture_size"
android:layout_height="@dimen/small_profile_picture_size" /> android:layout_height="@dimen/medium_profile_picture_size" />
<LinearLayout <LinearLayout
android:layout_width="wrap_content" android:layout_width="wrap_content"

View File

@ -26,7 +26,7 @@
android:textAllCaps="true" android:textAllCaps="true"
tools:text="30 mins"/> tools:text="30 mins"/>
<org.thoughtcrime.securesms.components.ExpirationTimerView <org.thoughtcrime.securesms.conversation.v2.components.ExpirationTimerView
android:id="@+id/footer_expiration_timer" android:id="@+id/footer_expiration_timer"
android:layout_gravity="center_vertical|end" android:layout_gravity="center_vertical|end"
android:layout_marginStart="6dp" android:layout_marginStart="6dp"

View File

@ -10,8 +10,8 @@
<ImageView <ImageView
android:id="@+id/menu_badge_icon" android:id="@+id/menu_badge_icon"
android:layout_width="20dp" android:layout_width="16dp"
android:layout_height="20dp" android:layout_height="16dp"
android:layout_gravity="center" android:layout_gravity="center"
android:src="@drawable/ic_timer" android:src="@drawable/ic_timer"
android:background="@color/transparent" android:background="@color/transparent"

View File

@ -3,6 +3,7 @@
xmlns:android="http://schemas.android.com/apk/res/android" xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:orientation="vertical" android:orientation="vertical"
android:paddingVertical="@dimen/medium_spacing" android:paddingVertical="@dimen/medium_spacing"
android:paddingHorizontal="@dimen/massive_spacing" android:paddingHorizontal="@dimen/massive_spacing"
@ -12,7 +13,8 @@
android:id="@+id/iconImageView" android:id="@+id/iconImageView"
android:layout_width="12dp" android:layout_width="12dp"
android:layout_height="12dp" android:layout_height="12dp"
android:layout_marginBottom="@dimen/small_spacing" /> android:layout_marginBottom="@dimen/small_spacing"
app:tint="@color/text" />
<TextView <TextView
android:id="@+id/textView" android:id="@+id/textView"

View File

@ -0,0 +1,70 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/searchBottomBarConstraintLayout"
android:layout_width="match_parent"
android:layout_height="@dimen/input_bar_height"
android:background="@color/input_bar_background"
android:orientation="vertical">
<View
android:layout_width="match_parent"
android:layout_height="1px"
android:background="@color/separator" />
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:gravity="center_vertical">
<ImageView
android:id="@+id/searchUp"
android:layout_width="40dp"
android:layout_height="40dp"
android:layout_marginStart="16dp"
android:padding="4dp"
android:background="?selectableItemBackgroundBorderless"
android:src="@drawable/ic_baseline_keyboard_arrow_up_24"
android:tint="@color/accent"
tools:ignore="UseAppTint" />
<ImageView
android:id="@+id/searchDown"
android:layout_width="40dp"
android:layout_height="40dp"
android:padding="4dp"
android:layout_gravity="center_vertical"
android:background="?selectableItemBackgroundBorderless"
android:src="@drawable/ic_baseline_keyboard_arrow_down_24"
android:tint="@color/accent"
tools:ignore="UseAppTint" />
</LinearLayout>
<TextView
android:id="@+id/searchPosition"
style="@style/Signal.Text.Body"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:text="37 of 73"
android:textStyle="bold"/>
<com.github.ybq.android.spinkit.SpinKitView
style="@style/SpinKitView.DoubleBounce"
android:id="@+id/searchProgressWheel"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="8dp"
android:background="@color/compose_view_background"
app:SpinKit_Color="?android:textColorPrimary"
android:visibility="gone"/>
</RelativeLayout>
</LinearLayout>

View File

@ -1,9 +1,12 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<LinearLayout <RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android" xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<LinearLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:orientation="vertical"> android:orientation="vertical">
<TextView <TextView
@ -111,4 +114,13 @@
</LinearLayout> </LinearLayout>
</LinearLayout> </LinearLayout>
<org.thoughtcrime.securesms.conversation.v2.components.ExpirationTimerView
android:id="@+id/expirationTimerView"
android:layout_width="12dp"
android:layout_height="12dp"
android:layout_marginHorizontal="@dimen/medium_spacing"
android:layout_centerVertical="true" />
</RelativeLayout>