Merge branch 'dev' into poller-fix

This commit is contained in:
Ryan ZHAO 2024-05-29 15:35:13 +10:00
commit 0d1ec5ed0b
6 changed files with 146 additions and 120 deletions

View File

@ -40,18 +40,16 @@ object ResendMessageUtilities {
message.recipient = messageRecord.recipient.address.serialize()
}
message.threadID = messageRecord.threadId
if (messageRecord.isMms) {
val mmsMessageRecord = messageRecord as MmsMessageRecord
if (mmsMessageRecord.linkPreviews.isNotEmpty()) {
message.linkPreview = LinkPreview.from(mmsMessageRecord.linkPreviews[0])
}
if (mmsMessageRecord.quote != null) {
message.quote = Quote.from(mmsMessageRecord.quote!!.quoteModel)
if (userBlindedKey != null && messageRecord.quote!!.author.serialize() == TextSecurePreferences.getLocalNumber(context)) {
message.quote!!.publicKey = userBlindedKey
if (messageRecord.isMms && messageRecord is MmsMessageRecord) {
messageRecord.linkPreviews.firstOrNull()?.let { message.linkPreview = LinkPreview.from(it) }
messageRecord.quote?.quoteModel?.let {
message.quote = Quote.from(it)?.apply {
if (userBlindedKey != null && publicKey == TextSecurePreferences.getLocalNumber(context)) {
publicKey = userBlindedKey
}
}
}
message.addSignalAttachments(mmsMessageRecord.slideDeck.asAttachments())
message.addSignalAttachments(messageRecord.slideDeck.asAttachments())
}
val sentTimestamp = message.sentTimestamp
val sender = MessagingModuleConfiguration.shared.storage.getUserPublicKey()

View File

@ -1147,13 +1147,9 @@ class MmsDatabase(context: Context, databaseHelper: SQLCipherOpenHelper) : Messa
}
}
fun readerFor(cursor: Cursor?): Reader {
return Reader(cursor)
}
fun readerFor(cursor: Cursor?, getQuote: Boolean = true) = Reader(cursor, getQuote)
fun readerFor(message: OutgoingMediaMessage?, threadId: Long): OutgoingMessageReader {
return OutgoingMessageReader(message, threadId)
}
fun readerFor(message: OutgoingMediaMessage?, threadId: Long) = OutgoingMessageReader(message, threadId)
fun setQuoteMissing(messageId: Long): Int {
val contentValues = ContentValues()
@ -1217,7 +1213,7 @@ class MmsDatabase(context: Context, databaseHelper: SQLCipherOpenHelper) : Messa
}
inner class Reader(private val cursor: Cursor?) : Closeable {
inner class Reader(private val cursor: Cursor?, private val getQuote: Boolean = true) : Closeable {
val next: MessageRecord?
get() = if (cursor == null || !cursor.moveToNext()) null else current
val current: MessageRecord
@ -1226,7 +1222,7 @@ class MmsDatabase(context: Context, databaseHelper: SQLCipherOpenHelper) : Messa
return if (mmsType == PduHeaders.MESSAGE_TYPE_NOTIFICATION_IND.toLong()) {
getNotificationMmsMessageRecord(cursor)
} else {
getMediaMmsMessageRecord(cursor)
getMediaMmsMessageRecord(cursor, getQuote)
}
}
@ -1253,20 +1249,10 @@ class MmsDatabase(context: Context, databaseHelper: SQLCipherOpenHelper) : Messa
DELIVERY_RECEIPT_COUNT
)
)
var readReceiptCount = cursor.getInt(cursor.getColumnIndexOrThrow(READ_RECEIPT_COUNT))
val subscriptionId = cursor.getInt(cursor.getColumnIndexOrThrow(SUBSCRIPTION_ID))
val readReceiptCount = if (isReadReceiptsEnabled(context)) cursor.getInt(cursor.getColumnIndexOrThrow(READ_RECEIPT_COUNT)) else 0
val hasMention = (cursor.getInt(cursor.getColumnIndexOrThrow(HAS_MENTION)) == 1)
if (!isReadReceiptsEnabled(context)) {
readReceiptCount = 0
}
var contentLocationBytes: ByteArray? = null
var transactionIdBytes: ByteArray? = null
if (!contentLocation.isNullOrEmpty()) contentLocationBytes = toIsoBytes(
contentLocation
)
if (!transactionId.isNullOrEmpty()) transactionIdBytes = toIsoBytes(
transactionId
)
val contentLocationBytes: ByteArray? = contentLocation?.takeUnless { it.isEmpty() }?.let(::toIsoBytes)
val transactionIdBytes: ByteArray? = transactionId?.takeUnless { it.isEmpty() }?.let(::toIsoBytes)
val slideDeck = SlideDeck(context, MmsNotificationAttachment(status, messageSize))
return NotificationMmsMessageRecord(
id, recipient, recipient,
@ -1277,7 +1263,7 @@ class MmsDatabase(context: Context, databaseHelper: SQLCipherOpenHelper) : Messa
)
}
private fun getMediaMmsMessageRecord(cursor: Cursor): MediaMmsMessageRecord {
private fun getMediaMmsMessageRecord(cursor: Cursor, getQuote: Boolean): MediaMmsMessageRecord {
val id = cursor.getLong(cursor.getColumnIndexOrThrow(ID))
val dateSent = cursor.getLong(cursor.getColumnIndexOrThrow(NORMALIZED_DATE_SENT))
val dateReceived = cursor.getLong(
@ -1328,7 +1314,7 @@ class MmsDatabase(context: Context, databaseHelper: SQLCipherOpenHelper) : Messa
.filterNot { o: DatabaseAttachment? -> o in contactAttachments }
.filterNot { o: DatabaseAttachment? -> o in previewAttachments }
)
val quote = getQuote(cursor)
val quote = if (getQuote) getQuote(cursor) else null
val reactions = get(context).reactionDatabase().getReactions(cursor)
return MediaMmsMessageRecord(
id, recipient, recipient,
@ -1381,7 +1367,7 @@ class MmsDatabase(context: Context, databaseHelper: SQLCipherOpenHelper) : Messa
val quoteId = cursor.getLong(cursor.getColumnIndexOrThrow(QUOTE_ID))
val quoteAuthor = cursor.getString(cursor.getColumnIndexOrThrow(QUOTE_AUTHOR))
if (quoteId == 0L || quoteAuthor.isNullOrBlank()) return null
val retrievedQuote = get(context).mmsSmsDatabase().getMessageFor(quoteId, quoteAuthor)
val retrievedQuote = get(context).mmsSmsDatabase().getMessageFor(quoteId, quoteAuthor, false)
val quoteText = retrievedQuote?.body
val quoteMissing = retrievedQuote == null
val quoteDeck = (

View File

@ -97,9 +97,13 @@ public class MmsSmsDatabase extends Database {
}
public @Nullable MessageRecord getMessageFor(long timestamp, String serializedAuthor) {
return getMessageFor(timestamp, serializedAuthor, true);
}
public @Nullable MessageRecord getMessageFor(long timestamp, String serializedAuthor, boolean getQuote) {
try (Cursor cursor = queryTables(PROJECTION, MmsSmsColumns.NORMALIZED_DATE_SENT + " = " + timestamp, null, null)) {
MmsSmsDatabase.Reader reader = readerFor(cursor);
MmsSmsDatabase.Reader reader = readerFor(cursor, getQuote);
MessageRecord messageRecord;
boolean isOwnNumber = Util.isOwnNumber(context, serializedAuthor);
@ -635,7 +639,11 @@ public class MmsSmsDatabase extends Database {
}
public Reader readerFor(@NonNull Cursor cursor) {
return new Reader(cursor);
return readerFor(cursor, true);
}
public Reader readerFor(@NonNull Cursor cursor, boolean getQuote) {
return new Reader(cursor, getQuote);
}
@NotNull
@ -658,11 +666,13 @@ public class MmsSmsDatabase extends Database {
public class Reader implements Closeable {
private final Cursor cursor;
private final boolean getQuote;
private SmsDatabase.Reader smsReader;
private MmsDatabase.Reader mmsReader;
public Reader(Cursor cursor) {
public Reader(Cursor cursor, boolean getQuote) {
this.cursor = cursor;
this.getQuote = getQuote;
}
private SmsDatabase.Reader getSmsReader() {
@ -675,7 +685,7 @@ public class MmsSmsDatabase extends Database {
private MmsDatabase.Reader getMmsReader() {
if (mmsReader == null) {
mmsReader = DatabaseComponent.get(context).mmsDatabase().readerFor(cursor);
mmsReader = DatabaseComponent.get(context).mmsDatabase().readerFor(cursor, getQuote);
}
return mmsReader;

View File

@ -16,7 +16,6 @@ import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.collect
@ -28,7 +27,6 @@ import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import network.loki.messenger.R
import network.loki.messenger.databinding.ActivityHomeBinding
import network.loki.messenger.databinding.ViewMessageRequestBannerBinding
import network.loki.messenger.libsession_util.ConfigBase
import org.greenrobot.eventbus.EventBus
import org.greenrobot.eventbus.Subscribe
@ -74,13 +72,11 @@ import org.thoughtcrime.securesms.preferences.SettingsActivity
import org.thoughtcrime.securesms.showMuteDialog
import org.thoughtcrime.securesms.showSessionDialog
import org.thoughtcrime.securesms.util.ConfigurationMessageUtilities
import org.thoughtcrime.securesms.util.DateUtils
import org.thoughtcrime.securesms.util.IP2Country
import org.thoughtcrime.securesms.util.disableClipping
import org.thoughtcrime.securesms.util.push
import org.thoughtcrime.securesms.util.show
import java.io.IOException
import java.util.Locale
import javax.inject.Inject
@AndroidEntryPoint
@ -113,7 +109,7 @@ class HomeActivity : PassphraseRequiredActionBarActivity(),
get() = textSecurePreferences.getLocalNumber()!!
private val homeAdapter: HomeAdapter by lazy {
HomeAdapter(context = this, configFactory = configFactory, listener = this)
HomeAdapter(context = this, configFactory = configFactory, listener = this, ::showMessageRequests, ::hideMessageRequests)
}
private val globalSearchAdapter = GlobalSearchAdapter { model ->
@ -185,7 +181,6 @@ class HomeActivity : PassphraseRequiredActionBarActivity(),
binding.seedReminderView.isVisible = false
}
}
setupMessageRequestsBanner()
// Set up recycler view
binding.globalSearchInputLayout.listener = this
homeAdapter.setHasStableIds(true)
@ -218,9 +213,9 @@ class HomeActivity : PassphraseRequiredActionBarActivity(),
// Subscribe to threads and update the UI
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
homeViewModel.threads
homeViewModel.data
.filterNotNull() // We don't actually want the null value here as it indicates a loading state (maybe we need a loading state?)
.collectLatest { threads ->
.collectLatest { data ->
val manager = binding.recyclerView.layoutManager as LinearLayoutManager
val firstPos = manager.findFirstCompletelyVisibleItemPosition()
val offsetTop = if(firstPos >= 0) {
@ -228,9 +223,8 @@ class HomeActivity : PassphraseRequiredActionBarActivity(),
manager.getDecoratedTop(view) - manager.getTopDecorationHeight(view)
} ?: 0
} else 0
homeAdapter.data = threads
homeAdapter.data = data
if(firstPos >= 0) { manager.scrollToPositionWithOffset(firstPos, offsetTop) }
setupMessageRequestsBanner()
updateEmptyState()
}
}
@ -341,34 +335,6 @@ class HomeActivity : PassphraseRequiredActionBarActivity(),
binding.newConversationButton.isVisible = !isShown
}
private fun setupMessageRequestsBanner() {
val messageRequestCount = threadDb.unapprovedConversationCount
// Set up message requests
if (messageRequestCount > 0 && !textSecurePreferences.hasHiddenMessageRequests()) {
with(ViewMessageRequestBannerBinding.inflate(layoutInflater)) {
unreadCountTextView.text = messageRequestCount.toString()
timestampTextView.text = DateUtils.getDisplayFormattedTimeSpanString(
this@HomeActivity,
Locale.getDefault(),
threadDb.latestUnapprovedConversationTimestamp
)
root.setOnClickListener { showMessageRequests() }
root.setOnLongClickListener { hideMessageRequests(); true }
root.layoutParams = RecyclerView.LayoutParams(RecyclerView.LayoutParams.MATCH_PARENT, RecyclerView.LayoutParams.WRAP_CONTENT)
val hadHeader = homeAdapter.hasHeaderView()
homeAdapter.header = root
if (hadHeader) homeAdapter.notifyItemChanged(0)
else homeAdapter.notifyItemInserted(0)
}
} else {
val hadHeader = homeAdapter.hasHeaderView()
homeAdapter.header = null
if (hadHeader) {
homeAdapter.notifyItemRemoved(0)
}
}
}
private fun updateLegacyConfigView() {
binding.configOutdatedView.isVisible = ConfigBase.isNewConfigEnabled(textSecurePreferences.hasForcedNewConfig(), SnodeAPI.nowWithOffset)
&& textSecurePreferences.getHasLegacyConfig()
@ -664,7 +630,6 @@ class HomeActivity : PassphraseRequiredActionBarActivity(),
text("Hide message requests?")
button(R.string.yes) {
textSecurePreferences.setHasHiddenMessageRequests()
setupMessageRequestsBanner()
homeViewModel.tryReload()
}
button(R.string.no)

View File

@ -9,13 +9,18 @@ import androidx.recyclerview.widget.ListUpdateCallback
import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.RecyclerView.NO_ID
import network.loki.messenger.R
import network.loki.messenger.databinding.ViewMessageRequestBannerBinding
import org.thoughtcrime.securesms.dependencies.ConfigFactory
import org.thoughtcrime.securesms.mms.GlideRequests
import org.thoughtcrime.securesms.util.DateUtils
import java.util.Locale
class HomeAdapter(
private val context: Context,
private val configFactory: ConfigFactory,
private val listener: ConversationClickListener
private val listener: ConversationClickListener,
private val showMessageRequests: () -> Unit,
private val hideMessageRequests: () -> Unit,
) : RecyclerView.Adapter<RecyclerView.ViewHolder>(), ListUpdateCallback {
companion object {
@ -23,13 +28,21 @@ class HomeAdapter(
private const val ITEM = 1
}
var header: View? = null
var messageRequests: HomeViewModel.MessageRequests? = null
set(value) {
if (field == value) return
val hadHeader = hasHeaderView()
field = value
if (value != null) {
if (hadHeader) notifyItemChanged(0) else notifyItemInserted(0)
} else if (hadHeader) notifyItemRemoved(0)
}
var data: HomeViewModel.Data = HomeViewModel.Data(emptyList(), emptySet())
var data: HomeViewModel.Data = HomeViewModel.Data()
set(newData) {
if (field === newData) {
return
}
if (field === newData) return
messageRequests = newData.messageRequests
val diff = HomeDiffUtil(field, newData, context, configFactory)
val diffResult = DiffUtil.calculateDiff(diff)
@ -37,10 +50,10 @@ class HomeAdapter(
diffResult.dispatchUpdatesTo(this as ListUpdateCallback)
}
fun hasHeaderView(): Boolean = header != null
fun hasHeaderView(): Boolean = messageRequests != null
private val headerCount: Int
get() = if (header == null) 0 else 1
get() = if (messageRequests == null) 0 else 1
override fun onInserted(position: Int, count: Int) {
notifyItemRangeInserted(position + headerCount, count)
@ -69,7 +82,11 @@ class HomeAdapter(
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder =
when (viewType) {
HEADER -> {
HeaderFooterViewHolder(header!!)
ViewMessageRequestBannerBinding.inflate(LayoutInflater.from(parent.context)).apply {
root.setOnClickListener { showMessageRequests() }
root.setOnLongClickListener { hideMessageRequests(); true }
root.layoutParams = RecyclerView.LayoutParams(RecyclerView.LayoutParams.MATCH_PARENT, RecyclerView.LayoutParams.WRAP_CONTENT)
}.let(::HeaderFooterViewHolder)
}
ITEM -> {
val conversationView = LayoutInflater.from(parent.context).inflate(R.layout.view_conversation, parent, false) as ConversationView
@ -85,19 +102,27 @@ class HomeAdapter(
}
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
if (holder is ConversationViewHolder) {
val offset = if (hasHeaderView()) position - 1 else position
val thread = data.threads[offset]
val isTyping = data.typingThreadIDs.contains(thread.threadId)
holder.view.bind(thread, isTyping, glide)
when (holder) {
is HeaderFooterViewHolder -> {
holder.binding.run {
messageRequests?.let {
unreadCountTextView.text = it.count
timestampTextView.text = it.timestamp
}
}
}
is ConversationViewHolder -> {
val offset = if (hasHeaderView()) position - 1 else position
val thread = data.threads[offset]
val isTyping = data.typingThreadIDs.contains(thread.threadId)
holder.view.bind(thread, isTyping, glide)
}
}
}
override fun onViewRecycled(holder: RecyclerView.ViewHolder) {
if (holder is ConversationViewHolder) {
holder.view.recycle()
} else {
super.onViewRecycled(holder)
}
}
@ -109,6 +134,5 @@ class HomeAdapter(
class ConversationViewHolder(val view: ConversationView) : RecyclerView.ViewHolder(view)
class HeaderFooterViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView)
class HeaderFooterViewHolder(val binding: ViewMessageRequestBannerBinding) : RecyclerView.ViewHolder(binding.root)
}

View File

@ -2,11 +2,13 @@ package org.thoughtcrime.securesms.home
import android.content.ContentResolver
import android.content.Context
import android.util.Log
import androidx.lifecycle.ViewModel
import androidx.lifecycle.asFlow
import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.channels.BufferOverflow
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow
@ -15,24 +17,31 @@ import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.debounce
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.mapLatest
import kotlinx.coroutines.flow.merge
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.onStart
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.withContext
import org.session.libsession.utilities.TextSecurePreferences
import org.thoughtcrime.securesms.ApplicationContext
import org.thoughtcrime.securesms.database.DatabaseContentProviders
import org.thoughtcrime.securesms.database.ThreadDatabase
import org.thoughtcrime.securesms.database.model.ThreadRecord
import org.thoughtcrime.securesms.util.DateUtils
import org.thoughtcrime.securesms.util.observeChanges
import java.util.Locale
import javax.inject.Inject
import dagger.hilt.android.qualifiers.ApplicationContext as ApplicationContextQualifier
@HiltViewModel
class HomeViewModel @Inject constructor(
private val threadDb: ThreadDatabase,
private val contentResolver: ContentResolver,
@ApplicationContextQualifier private val context: Context,
private val threadDb: ThreadDatabase,
private val contentResolver: ContentResolver,
private val prefs: TextSecurePreferences,
@ApplicationContextQualifier private val context: Context,
) : ViewModel() {
// SharedFlow that emits whenever the user asks us to reload the conversation
private val manualReloadTrigger = MutableSharedFlow<Unit>(
@ -46,8 +55,19 @@ class HomeViewModel @Inject constructor(
* This flow will emit whenever the user asks us to reload the conversation list or
* whenever the conversation list changes.
*/
val threads: StateFlow<Data?> = combine(observeConversationList(), observeTypingStatus(), ::Data)
.stateIn(viewModelScope, SharingStarted.Eagerly, null)
val data: StateFlow<Data?> = combine(
observeConversationList(),
observeTypingStatus(),
messageRequests(),
::Data
)
.stateIn(viewModelScope, SharingStarted.Eagerly, null)
private fun hasHiddenMessageRequests() = TextSecurePreferences.events
.filter { it == TextSecurePreferences.HAS_HIDDEN_MESSAGE_REQUESTS }
.flowOn(Dispatchers.IO)
.map { prefs.hasHiddenMessageRequests() }
.onStart { emit(prefs.hasHiddenMessageRequests()) }
private fun observeTypingStatus(): Flow<Set<Long>> =
ApplicationContext.getInstance(context).typingStatusRepository
@ -56,32 +76,55 @@ class HomeViewModel @Inject constructor(
.onStart { emit(emptySet()) }
.distinctUntilChanged()
private fun messageRequests() = combine(
unapprovedConversationCount(),
hasHiddenMessageRequests(),
latestUnapprovedConversationTimestamp(),
::createMessageRequests
)
private fun unapprovedConversationCount() = reloadTriggersAndContentChanges()
.map { threadDb.unapprovedConversationCount }
private fun latestUnapprovedConversationTimestamp() = reloadTriggersAndContentChanges()
.map { threadDb.latestUnapprovedConversationTimestamp }
@Suppress("OPT_IN_USAGE")
private fun observeConversationList(): Flow<List<ThreadRecord>> = merge(
manualReloadTrigger,
contentResolver.observeChanges(DatabaseContentProviders.ConversationList.CONTENT_URI))
.debounce(CHANGE_NOTIFICATION_DEBOUNCE_MILLS)
.onStart { emit(Unit) }
.mapLatest { _ ->
withContext(Dispatchers.IO) {
threadDb.approvedConversationList.use { openCursor ->
val reader = threadDb.readerFor(openCursor)
buildList(reader.count) {
while (true) {
add(reader.next ?: break)
}
}
}
}
private fun observeConversationList(): Flow<List<ThreadRecord>> = reloadTriggersAndContentChanges()
.mapLatest { _ ->
threadDb.approvedConversationList.use { openCursor ->
threadDb.readerFor(openCursor).run { generateSequence { next }.toList() }
}
}
@OptIn(FlowPreview::class)
private fun reloadTriggersAndContentChanges() = merge(
manualReloadTrigger,
contentResolver.observeChanges(DatabaseContentProviders.ConversationList.CONTENT_URI)
)
.flowOn(Dispatchers.IO)
.debounce(CHANGE_NOTIFICATION_DEBOUNCE_MILLS)
.onStart { emit(Unit) }
fun tryReload() = manualReloadTrigger.tryEmit(Unit)
data class Data(
val threads: List<ThreadRecord>,
val typingThreadIDs: Set<Long>
val threads: List<ThreadRecord> = emptyList(),
val typingThreadIDs: Set<Long> = emptySet(),
val messageRequests: MessageRequests? = null
)
fun createMessageRequests(
count: Int,
hidden: Boolean,
timestamp: Long
) = if (count > 0 && !hidden) MessageRequests(
count.toString(),
DateUtils.getDisplayFormattedTimeSpanString(context, Locale.getDefault(), timestamp)
) else null
data class MessageRequests(val count: String, val timestamp: String)
companion object {
private const val CHANGE_NOTIFICATION_DEBOUNCE_MILLS = 100L
}