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'
}
def canonicalVersionCode = 184
def canonicalVersionCode = 186
def canonicalVersionName = "1.11.0"
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.conversation.ConversationActivity;
import org.session.libsession.utilities.Address;
import org.thoughtcrime.securesms.conversation.v2.ConversationActivityV2;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.session.libsignal.utilities.Log;
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) {
final Intent intent = getBaseShareIntent(ConversationActivity.class);
intent.putExtra(ConversationActivity.ADDRESS_EXTRA, address);
intent.putExtra(ConversationActivity.THREAD_ID_EXTRA, threadId);
final Intent intent = getBaseShareIntent(ConversationActivityV2.class);
intent.putExtra(ConversationActivityV2.ADDRESS, address);
intent.putExtra(ConversationActivityV2.THREAD_ID, threadId);
intent.putExtra(ConversationActivity.DISTRIBUTION_TYPE_EXTRA, distributionType);
isPassingAlongMedia = true;

View File

@ -14,6 +14,7 @@ import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.thoughtcrime.securesms.ApplicationContext;
import org.thoughtcrime.securesms.conversation.v2.components.ExpirationTimerView;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.model.MessageRecord;
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.ListenableFuture;
import org.thoughtcrime.securesms.conversation.v2.ConversationActivityV2;
import java.util.concurrent.ExecutionException;
@ -80,9 +81,9 @@ public class ConversationPopupActivity extends ConversationActivity {
@Override
public void onSuccess(Long result) {
ActivityOptionsCompat transition = ActivityOptionsCompat.makeScaleUpAnimation(getWindow().getDecorView(), 0, 0, getWindow().getAttributes().width, getWindow().getAttributes().height);
Intent intent = new Intent(ConversationPopupActivity.this, ConversationActivity.class);
intent.putExtra(ConversationActivity.ADDRESS_EXTRA, getRecipient().getAddress());
intent.putExtra(ConversationActivity.THREAD_ID_EXTRA, result);
Intent intent = new Intent(ConversationPopupActivity.this, ConversationActivityV2.class);
intent.putExtra(ConversationActivityV2.ADDRESS, getRecipient().getAddress());
intent.putExtra(ConversationActivityV2.THREAD_ID, result);
if (VERSION.SDK_INT >= VERSION_CODES.JELLY_BEAN) {
startActivity(intent, transition.toBundle());

View File

@ -18,10 +18,14 @@ import android.util.Log
import android.util.Pair
import android.util.TypedValue
import android.view.*
import android.widget.LinearLayout
import android.widget.RelativeLayout
import android.widget.Toast
import androidx.annotation.DimenRes
import androidx.appcompat.app.AlertDialog
import androidx.core.view.isVisible
import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.ViewModelProviders
import androidx.loader.app.LoaderManager
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.utilities.UpdateMessageData
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.MediaTypes
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.RecipientModifiedListener
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.PassphraseRequiredActionBarActivity
import org.thoughtcrime.securesms.audio.AudioRecorder
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.input_bar.InputBarButton
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.ConversationActionModeCallbackDelegate
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.search.SearchBottomBar
import org.thoughtcrime.securesms.conversation.v2.search.SearchViewModel
import org.thoughtcrime.securesms.conversation.v2.utilities.AttachmentManager
import org.thoughtcrime.securesms.database.DatabaseFactory
import org.thoughtcrime.securesms.database.DraftDatabase
@ -111,7 +123,8 @@ import kotlin.math.*
class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDelegate,
InputBarRecordingViewDelegate, AttachmentManager.AttachmentListener, ActivityDispatcher,
ConversationActionModeCallbackDelegate {
ConversationActionModeCallbackDelegate, VisibleMessageContentViewDelegate, RecipientModifiedListener,
SearchBottomBar.EventListener {
private val screenWidth = Resources.getSystem().displayMetrics.widthPixels
private var linkPreviewViewModel: LinkPreviewViewModel? = null
private var threadID: Long = -1
@ -130,6 +143,15 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
private var previousText: CharSequence = ""
private var currentMentionStartIndex = -1
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
get() { return conversationRecyclerView.layoutManager as LinearLayoutManager }
@ -150,6 +172,7 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
},
glide
)
adapter.visibleMessageContentViewDelegate = this
adapter
}
@ -166,7 +189,10 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
// region Settings
companion object {
// Extras
const val THREAD_ID = "thread_id"
const val ADDRESS = "address"
// Request codes
const val PICK_DOCUMENT = 2
const val TAKE_PHOTO = 7
const val PICK_GIF = 10
@ -179,7 +205,13 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
override fun onCreate(savedInstanceState: Bundle?, isReady: Boolean) {
super.onCreate(savedInstanceState, isReady)
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()
setUpToolBar()
setUpInputBar()
@ -189,12 +221,16 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
unreadCount = DatabaseFactory.getMmsSmsDatabase(this).getUnreadCount(threadID)
updateUnreadCountIndicator()
setUpTypingObserver()
setUpRecipientObserver()
updateSubtitle()
getLatestOpenGroupInfoIfNeeded()
setUpBlockedBanner()
setUpLinkPreviewObserver()
searchBottomBar.setEventListener(this)
setUpSearchResultObserver()
scrollToFirstUnreadMessageIfNeeded()
markAllAsRead()
showOrHideInputIfNeeded()
}
override fun onResume() {
@ -251,6 +287,14 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
actionBar.setCustomView(R.layout.activity_conversation_v2_action_bar)
actionBar.setDisplayShowCustomEnabled(true)
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.update(thread, threadID)
}
@ -281,6 +325,27 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
}
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 drafts = draftDB.getDrafts(threadID)
draftDB.clearDrafts(threadID)
@ -302,7 +367,9 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
private fun setUpTypingObserver() {
ApplicationContext.getInstance(this).typingStatusRepository.getTypists(threadID).observe(this) { state ->
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)
inputBarHeightChanged(inputBar.height)
}
@ -316,6 +383,10 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
}
}
private fun setUpRecipientObserver() {
thread.addListener(this)
}
private fun getLatestOpenGroupInfoIfNeeded() {
val openGroup = DatabaseFactory.getLokiThreadDatabase(this).getOpenGroupChat(threadID) ?: return
OpenGroupAPIV2.getMemberCount(openGroup.room, openGroup.server).successUi { updateSubtitle() }
@ -358,7 +429,7 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
}
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)
return true
}
@ -369,7 +440,28 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
}
// 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() {
val messages = DatabaseFactory.getThreadDatabase(this).setRead(threadID, true)
if (thread.isGroupRecipient) {
@ -570,8 +662,15 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
}
private fun handleRecyclerViewScrolled() {
val position = layoutManager.findFirstCompletelyVisibleItemPosition()
val alpha = if (position > 0) 1.0f else 0.0f
val alpha = if (!isScrolledToBottom) 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
unreadCount = min(unreadCount, layoutManager.findFirstVisibleItemPosition())
updateUnreadCountIndicator()
@ -644,6 +743,7 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
val actionMode = this.actionMode
val actionModeCallback = ConversationActionModeCallback(adapter, threadID, this)
actionModeCallback.delegate = this
searchViewItem?.collapseActionView()
if (actionMode == null) { // Nothing should be selected if this is the case
adapter.toggleSelection(message, position)
this.actionMode = if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.M) {
@ -740,6 +840,11 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
this.previousText = newText
}
override fun scrollToMessageIfPossible(timestamp: Long) {
val lastSeenItemPosition = adapter.getItemPositionForTimestamp(timestamp) ?: return
conversationRecyclerView.scrollToPosition(lastSeenItemPosition)
}
override fun sendMessage() {
if (thread.isContactRecipient && thread.isBlocked) {
BlockedDialog(thread).show(supportFragmentManager, "Blocked Dialog")
@ -908,10 +1013,18 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
}
override fun startRecordingVoiceMessage() {
if (Permissions.hasAll(this, Manifest.permission.RECORD_AUDIO)) {
showVoiceMessageUI()
window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
audioRecorder.startRecording()
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() {
@ -966,7 +1079,6 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
}
}
} else {
ThreadUtils.queue {
for (message in messages) {
if (message.isMms) {
DatabaseFactory.getMmsDatabase(this@ConversationActivityV2).delete(message.id)
@ -975,7 +1087,6 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
}
}
}
}
endActionMode()
}
builder.setNegativeButton(android.R.string.cancel) { dialog, _ ->
@ -1157,4 +1268,46 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
draftDB.insertDrafts(threadID, drafts)
}
// 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 kotlinx.android.synthetic.main.view_visible_message.view.*
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.database.CursorRecyclerViewAdapter
import org.thoughtcrime.securesms.database.DatabaseFactory
@ -21,6 +22,8 @@ class ConversationAdapter(context: Context, cursor: Cursor, private val onItemPr
: CursorRecyclerViewAdapter<ViewHolder>(context, cursor) {
private val messageDB = DatabaseFactory.getMmsSmsDatabase(context)
var selectedItems = mutableSetOf<MessageRecord>()
private var searchQuery: String? = null
var visibleMessageContentViewDelegate: VisibleMessageContentViewDelegate? = null
sealed class ViewType(val rawValue: Int) {
object Visible : ViewType(0)
@ -69,10 +72,11 @@ class ConversationAdapter(context: Context, cursor: Cursor, private val onItemPr
view.snIsSelected = isSelected
view.messageTimestampTextView.isVisible = isSelected
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.onSwipeToReply = { onItemSwipeToReply(message, viewHolder.adapterPosition) }
view.onLongPress = { onItemLongPress(message, viewHolder.adapterPosition) }
view.contentViewDelegate = visibleMessageContentViewDelegate
}
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
for (i in 0 until itemCount) {
cursor.moveToPosition(i)
val messageRecord = messageDB.readerFor(cursor).current
if (messageRecord.isOutgoing || messageRecord.dateReceived <= lastSeenTimestamp) { return i }
val message = messageDB.readerFor(cursor).current
if (message.isOutgoing || message.dateReceived <= lastSeenTimestamp) { return i }
}
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 androidx.annotation.NonNull;
@ -118,5 +118,4 @@ public class ExpirationTimerView extends androidx.appcompat.widget.AppCompatImag
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.view.LayoutInflater
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity
import kotlinx.android.synthetic.main.dialog_join_open_group.view.*
import network.loki.messenger.R
import org.session.libsession.messaging.open_groups.OpenGroupV2
import org.session.libsession.utilities.OpenGroupUrlParser
import org.session.libsignal.utilities.ThreadUtils
import org.thoughtcrime.securesms.conversation.v2.utilities.BaseDialog
import org.thoughtcrime.securesms.loki.api.OpenGroupManager
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() {
val openGroup = OpenGroupUrlParser.parseUrl(url)
OpenGroupManager.add(openGroup.server, openGroup.room, openGroup.serverPublicKey, requireContext())
MultiDeviceProtocol.forceSyncConfigurationNowIfNeeded(requireContext())
val activity = requireContext() as AppCompatActivity
ThreadUtils.queue {
OpenGroupManager.add(openGroup.server, openGroup.room, openGroup.serverPublicKey, activity)
MultiDeviceProtocol.forceSyncConfigurationNowIfNeeded(activity)
}
dismiss()
}
}

View File

@ -32,6 +32,8 @@ class InputBar : RelativeLayout, InputBarEditTextDelegate, QuoteViewDelegate, Li
var additionalContentHeight = 0
var quote: MessageRecord? = null
var linkPreview: LinkPreview? = null
var showInput: Boolean = true
set(value) { field = value; showOrHideInputIfNeeded() }
var text: String
get() { return inputBarEditText.text.toString() }
@ -161,6 +163,19 @@ class InputBar : RelativeLayout, InputBarEditTextDelegate, QuoteViewDelegate, Li
additionalContentHeight = 0
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
}

View File

@ -12,17 +12,20 @@ import android.os.AsyncTask
import android.view.Menu
import android.view.MenuInflater
import android.view.MenuItem
import android.view.View
import android.widget.ImageView
import android.widget.TextView
import android.widget.Toast
import androidx.annotation.ColorInt
import androidx.appcompat.app.AlertDialog
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.ShortcutManagerCompat
import androidx.core.graphics.drawable.IconCompat
import kotlinx.android.synthetic.main.activity_conversation_v2.*
import network.loki.messenger.R
import org.session.libsession.avatars.ContactPhoto
import org.session.libsession.messaging.messages.control.ExpirationTimerUpdate
import org.session.libsession.messaging.sending_receiving.MessageSender
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.TextSecurePreferences
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.toHexString
import org.thoughtcrime.securesms.*
import org.thoughtcrime.securesms.conversation.ConversationActivity
import org.thoughtcrime.securesms.conversation.v2.ConversationActivityV2
import org.thoughtcrime.securesms.database.DatabaseFactory
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.util.BitmapUtil
import java.io.IOException
import java.lang.ref.WeakReference
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
menu.clear()
val isOpenGroup = thread.isOpenGroupRecipient
@ -92,6 +92,49 @@ object ConversationMenuHelper {
} else {
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 {
@ -121,7 +164,8 @@ object ConversationMenuHelper {
}
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")
@ -254,7 +298,6 @@ object ConversationMenuHelper {
try {
if (isClosedGroup) {
MessageSender.leave(groupPublicKey!!, true)
// TODO: Disable input?
} else {
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) {
thread.setMuted(0)
DatabaseFactory.getRecipientDatabase(context).setMuted(thread, 0)
}
private fun mute(context: Context, thread: Recipient) {
MuteDialog.show(context) { until: Long ->
thread.setMuted(until)
DatabaseFactory.getRecipientDatabase(context).setMuted(thread, until)
}
}

View File

@ -32,6 +32,9 @@ class ControlMessageView : LinearLayout {
if (message.isExpirationTimerUpdate) {
iconImageView.setImageDrawable(ResourcesCompat.getDrawable(resources, R.drawable.ic_timer, context.theme))
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)
}

View File

@ -42,7 +42,7 @@ class LinkPreviewView : LinearLayout {
// endregion
// 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()
url = linkPreview.url
// Thumbnail
@ -60,7 +60,7 @@ class LinkPreviewView : LinearLayout {
}
titleTextView.setTextColor(ResourcesCompat.getColor(resources, textColorID, context.theme))
// Body
bodyTextView = VisibleMessageContentView.getBodyTextView(context, message)
bodyTextView = VisibleMessageContentView.getBodyTextView(context, message, searchQuery)
mainLinkPreviewContainer.addView(bodyTextView)
// Corner radii
val cornerRadii = MessageBubbleUtilities.calculateRadii(context, isStartOfMessageCluster, isEndOfMessageCluster, message.isOutgoing)

View File

@ -1,20 +1,20 @@
package org.thoughtcrime.securesms.conversation.v2.messages
import android.content.Context
import android.graphics.Color
import android.graphics.Rect
import android.graphics.drawable.Drawable
import android.text.style.BackgroundColorSpan
import android.text.style.ForegroundColorSpan
import android.text.method.LinkMovementMethod
import android.text.style.ReplacementSpan
import android.text.style.URLSpan
import android.text.util.Linkify
import android.util.AttributeSet
import android.util.Log
import android.util.TypedValue
import android.view.LayoutInflater
import android.view.MotionEvent
import android.widget.LinearLayout
import android.widget.TextView
import android.widget.Toast
import androidx.annotation.ColorInt
import androidx.annotation.DrawableRes
import androidx.appcompat.app.AppCompatActivity
@ -24,6 +24,7 @@ import androidx.core.graphics.BlendModeCompat
import androidx.core.text.getSpans
import androidx.core.text.toSpannable
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 network.loki.messenger.R
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.loki.utilities.*
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
class VisibleMessageContentView : LinearLayout {
var onContentClick: ((event: MotionEvent) -> Unit)? = null
var onContentDoubleTap: (() -> Unit)? = null
var delegate: VisibleMessageContentViewDelegate? = null
// region Lifecycle
constructor(context: Context) : super(context) { initialize() }
@ -56,7 +61,7 @@ class VisibleMessageContentView : LinearLayout {
// region Updating
fun bind(message: MessageRecord, isStartOfMessageCluster: Boolean, isEndOfMessageCluster: Boolean,
glide: GlideRequests, maxWidth: Int, thread: Recipient) {
glide: GlideRequests, maxWidth: Int, thread: Recipient, searchQuery: String?) {
// Background
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
@ -70,7 +75,7 @@ class VisibleMessageContentView : LinearLayout {
onContentDoubleTap = null
if (message is MmsMessageRecord && message.linkPreviews.isNotEmpty()) {
val linkPreviewView = LinkPreviewView(context)
linkPreviewView.bind(message, glide, isStartOfMessageCluster, isEndOfMessageCluster)
linkPreviewView.bind(message, glide, isStartOfMessageCluster, isEndOfMessageCluster, searchQuery)
mainContainer.addView(linkPreviewView)
onContentClick = { event -> linkPreviewView.calculateHit(event) }
// 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,
message.isOutgoing, maxContentWidth, message.isOpenGroupInvitation, message.threadId, glide)
mainContainer.addView(quoteView)
val bodyTextView = VisibleMessageContentView.getBodyTextView(context, message)
val bodyTextView = VisibleMessageContentView.getBodyTextView(context, message, searchQuery)
ViewUtil.setPaddingTop(bodyTextView, 0)
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) {
val voiceMessageView = VoiceMessageView(context)
voiceMessageView.bind(message, isStartOfMessageCluster, isEndOfMessageCluster)
@ -119,7 +131,7 @@ class VisibleMessageContentView : LinearLayout {
mainContainer.addView(openGroupInvitationView)
onContentClick = { openGroupInvitationView.joinOpenGroup() }
} else {
val bodyTextView = VisibleMessageContentView.getBodyTextView(context, message)
val bodyTextView = VisibleMessageContentView.getBodyTextView(context, message, searchQuery)
mainContainer.addView(bodyTextView)
onContentClick = { event ->
// intersectedModalSpans should only be a list of one item
@ -153,7 +165,7 @@ class VisibleMessageContentView : LinearLayout {
// region Convenience
companion object {
fun getBodyTextView(context: Context, message: MessageRecord): TextView {
fun getBodyTextView(context: Context, message: MessageRecord, searchQuery: String?): TextView {
val result = EmojiTextView(context)
val vPadding = context.resources.getDimension(R.dimen.small_spacing).toInt()
val hPadding = toPx(12, context.resources)
@ -179,6 +191,9 @@ class VisibleMessageContentView : LinearLayout {
}
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
return result
}
@ -196,3 +211,8 @@ class VisibleMessageContentView : LinearLayout {
}
// 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.res.Resources
import android.graphics.Canvas
import android.graphics.ColorFilter
import android.graphics.Rect
import android.graphics.drawable.ColorDrawable
import android.os.AsyncTask
import android.os.Build
import android.os.Handler
import android.os.Looper
import android.util.AttributeSet
import android.view.*
import android.widget.LinearLayout
import android.widget.RelativeLayout
import androidx.core.content.ContextCompat
import androidx.core.content.res.ResourcesCompat
import androidx.core.view.isVisible
import kotlinx.android.synthetic.main.view_visible_message.view.*
import network.loki.messenger.R
import org.session.libsession.messaging.contacts.Contact.ContactContext
import org.session.libsession.messaging.open_groups.OpenGroupAPIV2
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.model.MessageRecord
import org.thoughtcrime.securesms.loki.utilities.getColorWithID
@ -47,6 +53,7 @@ class VisibleMessageView : LinearLayout {
var onPress: ((event: MotionEvent) -> Unit)? = null
var onSwipeToReply: (() -> Unit)? = null
var onLongPress: (() -> Unit)? = null
var contentViewDelegate: VisibleMessageContentViewDelegate? = null
companion object {
const val swipeToReplyThreshold = 80.0f // dp
@ -69,7 +76,7 @@ class VisibleMessageView : LinearLayout {
// endregion
// 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 senderSessionID = sender.address.serialize()
val threadID = message.threadId
@ -137,11 +144,14 @@ class VisibleMessageView : LinearLayout {
} else {
messageStatusImageView.isVisible = false
}
// Expiration timer
updateExpirationTimer(message)
// Calculate max message bubble width
var maxWidth = screenWidth - messageContentContainerLayoutParams.leftMargin - messageContentContainerLayoutParams.rightMargin
if (profilePictureContainer.visibility != View.GONE) { maxWidth -= profilePictureContainer.width }
// 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() }
}
@ -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() {
background = if (snIsSelected) {
ColorDrawable(context.resources.getColorWithID(R.color.message_selected, context.theme))
@ -242,6 +283,7 @@ class VisibleMessageView : LinearLayout {
} else {
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
val damping = 50.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.libsignal.utilities.JsonUtil;
import org.session.libsignal.utilities.Log;
import org.session.libsignal.utilities.ThreadUtils;
import org.session.libsignal.utilities.guava.Optional;
import org.thoughtcrime.securesms.attachments.MmsNotificationAttachment;
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper;
@ -883,7 +884,7 @@ public class MmsDatabase extends MessagingDatabase {
public boolean delete(long messageId) {
long threadId = getThreadIdForMessage(messageId);
AttachmentDatabase attachmentDatabase = DatabaseFactory.getAttachmentDatabase(context);
attachmentDatabase.deleteAttachmentsForMessage(messageId);
ThreadUtils.queue(() -> attachmentDatabase.deleteAttachmentsForMessage(messageId));
GroupReceiptDatabase groupReceiptDatabase = DatabaseFactory.getGroupReceiptDatabase(context);
groupReceiptDatabase.deleteRowsForMessage(messageId);

View File

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

View File

@ -1,6 +1,5 @@
package org.thoughtcrime.securesms.database
import android.app.job.JobScheduler
import android.content.Context
import android.net.Uri
import org.session.libsession.database.StorageProtocol
@ -106,7 +105,7 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context,
}
else -> Optional.absent()
}
val pointerAttachments = attachments.mapNotNull {
val pointers = attachments.mapNotNull {
it.toSignalAttachment()
}
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 mmsDatabase = DatabaseFactory.getMmsDatabase(context)
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!!)
} else {
// 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.session.libsession.utilities.recipients.Recipient
import org.session.libsession.utilities.TextSecurePreferences
import org.thoughtcrime.securesms.conversation.v2.ConversationActivityV2
//TODO Refactor to avoid using kotlinx.android.synthetic
class CreateClosedGroupActivity : PassphraseRequiredActionBarActivity(), LoaderManager.LoaderCallbacks<List<String>> {
@ -135,10 +136,10 @@ class CreateClosedGroupActivity : PassphraseRequiredActionBarActivity(), LoaderM
// region Convenience
private fun openConversationActivity(context: Context, threadId: Long, recipient: Recipient) {
val intent = Intent(context, ConversationActivity::class.java)
intent.putExtra(ConversationActivity.THREAD_ID_EXTRA, threadId)
val intent = Intent(context, ConversationActivityV2::class.java)
intent.putExtra(ConversationActivityV2.THREAD_ID, threadId)
intent.putExtra(ConversationActivity.DISTRIBUTION_TYPE_EXTRA, DistributionTypes.DEFAULT)
intent.putExtra(ConversationActivity.ADDRESS_EXTRA, recipient.address)
intent.putExtra(ConversationActivityV2.ADDRESS, recipient.address)
context.startActivity(intent)
}
// endregion

View File

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

View File

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

View File

@ -26,6 +26,7 @@ import org.session.libsession.utilities.recipients.Recipient
import org.thoughtcrime.securesms.util.FileProviderUtil
import org.session.libsession.utilities.TextSecurePreferences
import org.session.libsignal.utilities.PublicKeyValidation
import org.thoughtcrime.securesms.conversation.v2.ConversationActivityV2
import java.io.File
import java.io.FileOutputStream
@ -53,12 +54,12 @@ class QRCodeActivity : PassphraseRequiredActionBarActivity(), ScanQRCodeWrapperF
fun createPrivateChatIfPossible(hexEncodedPublicKey: String) {
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 intent = Intent(this, ConversationActivity::class.java)
intent.putExtra(ConversationActivity.ADDRESS_EXTRA, recipient.address)
val intent = Intent(this, ConversationActivityV2::class.java)
intent.putExtra(ConversationActivityV2.ADDRESS, recipient.address)
intent.putExtra(ConversationActivity.TEXT_EXTRA, getIntent().getStringExtra(ConversationActivity.TEXT_EXTRA))
intent.setDataAndType(getIntent().data, getIntent().type)
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)
startActivity(intent)
finish()

View File

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

View File

@ -49,6 +49,7 @@ import org.session.libsignal.utilities.Log;
import org.thoughtcrime.securesms.ApplicationContext;
import org.thoughtcrime.securesms.contactshare.ContactUtil;
import org.thoughtcrime.securesms.conversation.ConversationActivity;
import org.thoughtcrime.securesms.conversation.v2.ConversationActivityV2;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.MessagingDatabase.MarkedMessageInfo;
import org.thoughtcrime.securesms.database.MmsSmsDatabase;
@ -115,9 +116,9 @@ public class DefaultMessageNotifier implements MessageNotifier {
if (visibleThread == threadId) {
sendInThreadNotification(context, recipient);
} else {
Intent intent = new Intent(context, ConversationActivity.class);
intent.putExtra(ConversationActivity.ADDRESS_EXTRA, recipient.getAddress());
intent.putExtra(ConversationActivity.THREAD_ID_EXTRA, threadId);
Intent intent = new Intent(context, ConversationActivityV2.class);
intent.putExtra(ConversationActivityV2.ADDRESS, recipient.getAddress());
intent.putExtra(ConversationActivityV2.THREAD_ID, threadId);
intent.setData((Uri.parse("custom://" + System.currentTimeMillis())));
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 org.thoughtcrime.securesms.conversation.ConversationActivity;
import org.thoughtcrime.securesms.conversation.v2.ConversationActivityV2;
import org.thoughtcrime.securesms.mms.SlideDeck;
import org.session.libsession.utilities.recipients.Recipient;
@ -67,11 +68,11 @@ public class NotificationItem {
}
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;
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())));
return TaskStackBuilder.create(context)

View File

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

View File

@ -34,6 +34,13 @@
android:layout_height="wrap_content"
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
android:id="@+id/additionalContentContainer"
android:layout_width="match_parent"

View File

@ -1,5 +1,6 @@
<?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_height="?attr/actionBarSize"
xmlns:tools="http://schemas.android.com/tools"
@ -9,8 +10,8 @@
<org.thoughtcrime.securesms.loki.views.ProfilePictureView
android:id="@+id/profilePictureView"
android:layout_width="@dimen/small_profile_picture_size"
android:layout_height="@dimen/small_profile_picture_size" />
android:layout_width="@dimen/medium_profile_picture_size"
android:layout_height="@dimen/medium_profile_picture_size" />
<LinearLayout
android:layout_width="wrap_content"

View File

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

View File

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

View File

@ -3,6 +3,7 @@
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:orientation="vertical"
android:paddingVertical="@dimen/medium_spacing"
android:paddingHorizontal="@dimen/massive_spacing"
@ -12,7 +13,8 @@
android:id="@+id/iconImageView"
android:layout_width="12dp"
android:layout_height="12dp"
android:layout_marginBottom="@dimen/small_spacing" />
android:layout_marginBottom="@dimen/small_spacing"
app:tint="@color/text" />
<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"?>
<LinearLayout
<RelativeLayout
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_height="wrap_content"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:orientation="vertical">
<TextView
@ -111,4 +114,13 @@
</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>