Merge remote-tracking branch 'upstream/dev' into SES-2009-blinded-conversation

This commit is contained in:
fanchao 2024-05-29 15:22:57 +10:00
commit 9441fdec21
9 changed files with 207 additions and 186 deletions

View File

@ -21,6 +21,7 @@ import android.annotation.TargetApi;
import android.content.Context; import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.database.Cursor; import android.database.Cursor;
import android.database.CursorIndexOutOfBoundsException;
import android.net.Uri; import android.net.Uri;
import android.os.AsyncTask; import android.os.AsyncTask;
import android.os.Build; import android.os.Build;
@ -145,6 +146,7 @@ public class MediaPreviewActivity extends PassphraseRequiredActionBarActivity im
View.SYSTEM_UI_FLAG_HIDE_NAVIGATION); View.SYSTEM_UI_FLAG_HIDE_NAVIGATION);
} }
}; };
private MediaItemAdapter adapter;
public static Intent getPreviewIntent(Context context, MediaPreviewArgs args) { public static Intent getPreviewIntent(Context context, MediaPreviewArgs args) {
return getPreviewIntent(context, args.getSlide(), args.getMmsRecord(), args.getThread()); return getPreviewIntent(context, args.getSlide(), args.getMmsRecord(), args.getThread());
@ -217,13 +219,6 @@ public class MediaPreviewActivity extends PassphraseRequiredActionBarActivity im
Permissions.onRequestPermissionsResult(this, requestCode, permissions, grantResults); Permissions.onRequestPermissionsResult(this, requestCode, permissions, grantResults);
} }
@TargetApi(VERSION_CODES.JELLY_BEAN)
private void setFullscreenIfPossible() {
if (VERSION.SDK_INT >= VERSION_CODES.JELLY_BEAN) {
getWindow().getDecorView().setSystemUiVisibility(View.SYSTEM_UI_FLAG_FULLSCREEN);
}
}
@Override @Override
public void onModified(Recipient recipient) { public void onModified(Recipient recipient) {
Util.runOnMain(this::updateActionBar); Util.runOnMain(this::updateActionBar);
@ -285,9 +280,6 @@ public class MediaPreviewActivity extends PassphraseRequiredActionBarActivity im
mediaPager = findViewById(R.id.media_pager); mediaPager = findViewById(R.id.media_pager);
mediaPager.setOffscreenPageLimit(1); mediaPager.setOffscreenPageLimit(1);
viewPagerListener = new ViewPagerListener();
mediaPager.addOnPageChangeListener(viewPagerListener);
albumRail = findViewById(R.id.media_preview_album_rail); albumRail = findViewById(R.id.media_preview_album_rail);
albumRailAdapter = new MediaRailAdapter(GlideApp.with(this), this, false); albumRailAdapter = new MediaRailAdapter(GlideApp.with(this), this, false);
@ -378,7 +370,8 @@ public class MediaPreviewActivity extends PassphraseRequiredActionBarActivity im
if (conversationRecipient != null) { if (conversationRecipient != null) {
getSupportLoaderManager().restartLoader(0, null, this); getSupportLoaderManager().restartLoader(0, null, this);
} else { } else {
mediaPager.setAdapter(new SingleItemPagerAdapter(this, GlideApp.with(this), getWindow(), initialMediaUri, initialMediaType, initialMediaSize)); adapter = new SingleItemPagerAdapter(this, GlideApp.with(this), getWindow(), initialMediaUri, initialMediaType, initialMediaSize);
mediaPager.setAdapter(adapter);
if (initialCaption != null) { if (initialCaption != null) {
detailsContainer.setVisibility(View.VISIBLE); detailsContainer.setVisibility(View.VISIBLE);
@ -506,13 +499,8 @@ public class MediaPreviewActivity extends PassphraseRequiredActionBarActivity im
} }
private @Nullable MediaItem getCurrentMediaItem() { private @Nullable MediaItem getCurrentMediaItem() {
MediaItemAdapter adapter = (MediaItemAdapter)mediaPager.getAdapter(); if (adapter == null) return null;
if (adapter != null) {
return adapter.getMediaItemFor(mediaPager.getCurrentItem()); return adapter.getMediaItemFor(mediaPager.getCurrentItem());
} else {
return null;
}
} }
public static boolean isContentTypeSupported(final String contentType) { public static boolean isContentTypeSupported(final String contentType) {
@ -526,24 +514,29 @@ public class MediaPreviewActivity extends PassphraseRequiredActionBarActivity im
@Override @Override
public void onLoadFinished(@NonNull Loader<Pair<Cursor, Integer>> loader, @Nullable Pair<Cursor, Integer> data) { public void onLoadFinished(@NonNull Loader<Pair<Cursor, Integer>> loader, @Nullable Pair<Cursor, Integer> data) {
if (data != null) { if (data == null) return;
CursorPagerAdapter adapter = new CursorPagerAdapter(this, GlideApp.with(this), getWindow(), data.first, data.second, leftIsRecent);
mediaPager.removeOnPageChangeListener(viewPagerListener);
adapter = new CursorPagerAdapter(this, GlideApp.with(this), getWindow(), data.first, data.second, leftIsRecent);
mediaPager.setAdapter(adapter); mediaPager.setAdapter(adapter);
adapter.setActive(true);
viewModel.setCursor(this, data.first, leftIsRecent); viewModel.setCursor(this, data.first, leftIsRecent);
if (restartItem >= 0 || data.second >= 0) { int item = restartItem >= 0 && restartItem < adapter.getCount() ? restartItem : Math.max(Math.min(data.second, adapter.getCount() - 1), 0);
int item = restartItem >= 0 ? restartItem : data.second;
viewPagerListener = new ViewPagerListener();
mediaPager.addOnPageChangeListener(viewPagerListener);
try {
mediaPager.setCurrentItem(item); mediaPager.setCurrentItem(item);
} catch (CursorIndexOutOfBoundsException e) {
throw new RuntimeException("restartItem = " + restartItem + ", data.second = " + data.second + " leftIsRecent = " + leftIsRecent, e);
}
if (item == 0) { if (item == 0) {
viewPagerListener.onPageSelected(0); viewPagerListener.onPageSelected(0);
} }
} else {
Log.w(TAG, "one of restartItem "+restartItem+" and data.second "+data.second+" would cause OOB exception");
}
}
} }
@Override @Override
@ -560,27 +553,27 @@ public class MediaPreviewActivity extends PassphraseRequiredActionBarActivity im
if (currentPage != -1 && currentPage != position) onPageUnselected(currentPage); if (currentPage != -1 && currentPage != position) onPageUnselected(currentPage);
currentPage = position; currentPage = position;
MediaItemAdapter adapter = (MediaItemAdapter)mediaPager.getAdapter(); if (adapter == null) return;
if (adapter != null) {
MediaItem item = adapter.getMediaItemFor(position); MediaItem item = adapter.getMediaItemFor(position);
if (item.recipient != null) item.recipient.addListener(MediaPreviewActivity.this); if (item.recipient != null) item.recipient.addListener(MediaPreviewActivity.this);
viewModel.setActiveAlbumRailItem(MediaPreviewActivity.this, position); viewModel.setActiveAlbumRailItem(MediaPreviewActivity.this, position);
updateActionBar(); updateActionBar();
} }
}
public void onPageUnselected(int position) { public void onPageUnselected(int position) {
MediaItemAdapter adapter = (MediaItemAdapter)mediaPager.getAdapter(); if (adapter == null) return;
if (adapter != null) { try {
MediaItem item = adapter.getMediaItemFor(position); MediaItem item = adapter.getMediaItemFor(position);
if (item.recipient != null) item.recipient.removeListener(MediaPreviewActivity.this); if (item.recipient != null) item.recipient.removeListener(MediaPreviewActivity.this);
} catch (CursorIndexOutOfBoundsException e) {
throw new RuntimeException("position = " + position + " leftIsRecent = " + leftIsRecent, e);
}
adapter.pause(position); adapter.pause(position);
} }
}
@Override @Override
public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) { public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
@ -593,7 +586,7 @@ public class MediaPreviewActivity extends PassphraseRequiredActionBarActivity im
} }
} }
private static class SingleItemPagerAdapter extends PagerAdapter implements MediaItemAdapter { private static class SingleItemPagerAdapter extends MediaItemAdapter {
private final GlideRequests glideRequests; private final GlideRequests glideRequests;
private final Window window; private final Window window;
@ -665,7 +658,7 @@ public class MediaPreviewActivity extends PassphraseRequiredActionBarActivity im
} }
} }
private static class CursorPagerAdapter extends PagerAdapter implements MediaItemAdapter { private static class CursorPagerAdapter extends MediaItemAdapter {
private final WeakHashMap<Integer, MediaView> mediaViews = new WeakHashMap<>(); private final WeakHashMap<Integer, MediaView> mediaViews = new WeakHashMap<>();
@ -675,7 +668,6 @@ public class MediaPreviewActivity extends PassphraseRequiredActionBarActivity im
private final Cursor cursor; private final Cursor cursor;
private final boolean leftIsRecent; private final boolean leftIsRecent;
private boolean active;
private int autoPlayPosition; private int autoPlayPosition;
CursorPagerAdapter(@NonNull Context context, @NonNull GlideRequests glideRequests, CursorPagerAdapter(@NonNull Context context, @NonNull GlideRequests glideRequests,
@ -690,15 +682,9 @@ public class MediaPreviewActivity extends PassphraseRequiredActionBarActivity im
this.leftIsRecent = leftIsRecent; this.leftIsRecent = leftIsRecent;
} }
public void setActive(boolean active) {
this.active = active;
notifyDataSetChanged();
}
@Override @Override
public int getCount() { public int getCount() {
if (!active) return 0; return cursor.getCount();
else return cursor.getCount();
} }
@Override @Override
@ -771,8 +757,8 @@ public class MediaPreviewActivity extends PassphraseRequiredActionBarActivity im
} }
private int getCursorPosition(int position) { private int getCursorPosition(int position) {
if (leftIsRecent) return position; int unclamped = leftIsRecent ? position : cursor.getCount() - 1 - position;
else return cursor.getCount() - 1 - position; return Math.max(Math.min(unclamped, cursor.getCount() - 1), 0);
} }
} }
@ -800,9 +786,9 @@ public class MediaPreviewActivity extends PassphraseRequiredActionBarActivity im
} }
} }
interface MediaItemAdapter { abstract static class MediaItemAdapter extends PagerAdapter {
MediaItem getMediaItemFor(int position); abstract MediaItem getMediaItemFor(int position);
void pause(int position); abstract void pause(int position);
@Nullable View getPlaybackControls(int position); @Nullable abstract View getPlaybackControls(int position);
} }
} }

View File

@ -45,7 +45,8 @@ public class AudioRecorder {
Log.i(TAG, "Running startRecording() + " + Thread.currentThread().getId()); Log.i(TAG, "Running startRecording() + " + Thread.currentThread().getId());
try { try {
if (audioCodec != null) { if (audioCodec != null) {
throw new AssertionError("We can only record once at a time."); Log.e(TAG, "Trying to start recording while another recording is in progress, exiting...");
return;
} }
ParcelFileDescriptor fds[] = ParcelFileDescriptor.createPipe(); ParcelFileDescriptor fds[] = ParcelFileDescriptor.createPipe();

View File

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

View File

@ -5,9 +5,9 @@ import android.content.Context
import org.session.libsession.utilities.Debouncer import org.session.libsession.utilities.Debouncer
import org.thoughtcrime.securesms.ApplicationContext import org.thoughtcrime.securesms.ApplicationContext
class ConversationNotificationDebouncer(private val context: Context) { class ConversationNotificationDebouncer(private val context: ApplicationContext) {
private val threadIDs = mutableSetOf<Long>() private val threadIDs = mutableSetOf<Long>()
private val handler = (context.applicationContext as ApplicationContext).conversationListNotificationHandler private val handler = context.conversationListNotificationHandler
private val debouncer = Debouncer(handler, 100) private val debouncer = Debouncer(handler, 100)
companion object { companion object {
@ -17,20 +17,28 @@ class ConversationNotificationDebouncer(private val context: Context) {
@Synchronized @Synchronized
fun get(context: Context): ConversationNotificationDebouncer { fun get(context: Context): ConversationNotificationDebouncer {
if (::shared.isInitialized) { return shared } if (::shared.isInitialized) { return shared }
shared = ConversationNotificationDebouncer(context) shared = ConversationNotificationDebouncer(context.applicationContext as ApplicationContext)
return shared return shared
} }
} }
fun notify(threadID: Long) { fun notify(threadID: Long) {
synchronized(threadIDs) {
threadIDs.add(threadID) threadIDs.add(threadID)
}
debouncer.publish { publish() } debouncer.publish { publish() }
} }
private fun publish() { private fun publish() {
for (threadID in threadIDs.toList()) { val toNotify = synchronized(threadIDs) {
val copy = threadIDs.toList()
threadIDs.clear()
copy
}
for (threadID in toNotify) {
context.contentResolver.notifyChange(DatabaseContentProviders.Conversation.getUriForThread(threadID), null) context.contentResolver.notifyChange(DatabaseContentProviders.Conversation.getUriForThread(threadID), null)
} }
threadIDs.clear()
} }
} }

View File

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

View File

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

View File

@ -16,7 +16,6 @@ import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle import androidx.lifecycle.repeatOnLifecycle
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.collect
@ -28,7 +27,6 @@ import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import network.loki.messenger.R import network.loki.messenger.R
import network.loki.messenger.databinding.ActivityHomeBinding import network.loki.messenger.databinding.ActivityHomeBinding
import network.loki.messenger.databinding.ViewMessageRequestBannerBinding
import network.loki.messenger.libsession_util.ConfigBase import network.loki.messenger.libsession_util.ConfigBase
import org.greenrobot.eventbus.EventBus import org.greenrobot.eventbus.EventBus
import org.greenrobot.eventbus.Subscribe import org.greenrobot.eventbus.Subscribe
@ -74,13 +72,11 @@ import org.thoughtcrime.securesms.preferences.SettingsActivity
import org.thoughtcrime.securesms.showMuteDialog import org.thoughtcrime.securesms.showMuteDialog
import org.thoughtcrime.securesms.showSessionDialog import org.thoughtcrime.securesms.showSessionDialog
import org.thoughtcrime.securesms.util.ConfigurationMessageUtilities import org.thoughtcrime.securesms.util.ConfigurationMessageUtilities
import org.thoughtcrime.securesms.util.DateUtils
import org.thoughtcrime.securesms.util.IP2Country import org.thoughtcrime.securesms.util.IP2Country
import org.thoughtcrime.securesms.util.disableClipping import org.thoughtcrime.securesms.util.disableClipping
import org.thoughtcrime.securesms.util.push import org.thoughtcrime.securesms.util.push
import org.thoughtcrime.securesms.util.show import org.thoughtcrime.securesms.util.show
import java.io.IOException import java.io.IOException
import java.util.Locale
import javax.inject.Inject import javax.inject.Inject
@AndroidEntryPoint @AndroidEntryPoint
@ -113,7 +109,7 @@ class HomeActivity : PassphraseRequiredActionBarActivity(),
get() = textSecurePreferences.getLocalNumber()!! get() = textSecurePreferences.getLocalNumber()!!
private val homeAdapter: HomeAdapter by lazy { 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 -> private val globalSearchAdapter = GlobalSearchAdapter { model ->
@ -185,7 +181,6 @@ class HomeActivity : PassphraseRequiredActionBarActivity(),
binding.seedReminderView.isVisible = false binding.seedReminderView.isVisible = false
} }
} }
setupMessageRequestsBanner()
// Set up recycler view // Set up recycler view
binding.globalSearchInputLayout.listener = this binding.globalSearchInputLayout.listener = this
homeAdapter.setHasStableIds(true) homeAdapter.setHasStableIds(true)
@ -218,9 +213,9 @@ class HomeActivity : PassphraseRequiredActionBarActivity(),
// Subscribe to threads and update the UI // Subscribe to threads and update the UI
lifecycleScope.launch { lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) { 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?) .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 manager = binding.recyclerView.layoutManager as LinearLayoutManager
val firstPos = manager.findFirstCompletelyVisibleItemPosition() val firstPos = manager.findFirstCompletelyVisibleItemPosition()
val offsetTop = if(firstPos >= 0) { val offsetTop = if(firstPos >= 0) {
@ -228,9 +223,8 @@ class HomeActivity : PassphraseRequiredActionBarActivity(),
manager.getDecoratedTop(view) - manager.getTopDecorationHeight(view) manager.getDecoratedTop(view) - manager.getTopDecorationHeight(view)
} ?: 0 } ?: 0
} else 0 } else 0
homeAdapter.data = threads homeAdapter.data = data
if(firstPos >= 0) { manager.scrollToPositionWithOffset(firstPos, offsetTop) } if(firstPos >= 0) { manager.scrollToPositionWithOffset(firstPos, offsetTop) }
setupMessageRequestsBanner()
updateEmptyState() updateEmptyState()
} }
} }
@ -341,34 +335,6 @@ class HomeActivity : PassphraseRequiredActionBarActivity(),
binding.newConversationButton.isVisible = !isShown 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() { private fun updateLegacyConfigView() {
binding.configOutdatedView.isVisible = ConfigBase.isNewConfigEnabled(textSecurePreferences.hasForcedNewConfig(), SnodeAPI.nowWithOffset) binding.configOutdatedView.isVisible = ConfigBase.isNewConfigEnabled(textSecurePreferences.hasForcedNewConfig(), SnodeAPI.nowWithOffset)
&& textSecurePreferences.getHasLegacyConfig() && textSecurePreferences.getHasLegacyConfig()
@ -664,7 +630,6 @@ class HomeActivity : PassphraseRequiredActionBarActivity(),
text("Hide message requests?") text("Hide message requests?")
button(R.string.yes) { button(R.string.yes) {
textSecurePreferences.setHasHiddenMessageRequests() textSecurePreferences.setHasHiddenMessageRequests()
setupMessageRequestsBanner()
homeViewModel.tryReload() homeViewModel.tryReload()
} }
button(R.string.no) 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
import androidx.recyclerview.widget.RecyclerView.NO_ID import androidx.recyclerview.widget.RecyclerView.NO_ID
import network.loki.messenger.R import network.loki.messenger.R
import network.loki.messenger.databinding.ViewMessageRequestBannerBinding
import org.thoughtcrime.securesms.dependencies.ConfigFactory import org.thoughtcrime.securesms.dependencies.ConfigFactory
import org.thoughtcrime.securesms.mms.GlideRequests import org.thoughtcrime.securesms.mms.GlideRequests
import org.thoughtcrime.securesms.util.DateUtils
import java.util.Locale
class HomeAdapter( class HomeAdapter(
private val context: Context, private val context: Context,
private val configFactory: ConfigFactory, 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 { ) : RecyclerView.Adapter<RecyclerView.ViewHolder>(), ListUpdateCallback {
companion object { companion object {
@ -23,24 +28,32 @@ class HomeAdapter(
private const val ITEM = 1 private const val ITEM = 1
} }
var header: View? = null var messageRequests: HomeViewModel.MessageRequests? = null
set(value) {
var data: HomeViewModel.Data = HomeViewModel.Data(emptyList(), emptySet()) if (field == value) return
set(newData) { val hadHeader = hasHeaderView()
if (field === newData) { field = value
return if (value != null) {
if (hadHeader) notifyItemChanged(0) else notifyItemInserted(0)
} else if (hadHeader) notifyItemRemoved(0)
} }
var data: HomeViewModel.Data = HomeViewModel.Data()
set(newData) {
if (field === newData) return
messageRequests = newData.messageRequests
val diff = HomeDiffUtil(field, newData, context, configFactory) val diff = HomeDiffUtil(field, newData, context, configFactory)
val diffResult = DiffUtil.calculateDiff(diff) val diffResult = DiffUtil.calculateDiff(diff)
field = newData field = newData
diffResult.dispatchUpdatesTo(this as ListUpdateCallback) diffResult.dispatchUpdatesTo(this as ListUpdateCallback)
} }
fun hasHeaderView(): Boolean = header != null fun hasHeaderView(): Boolean = messageRequests != null
private val headerCount: Int 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) { override fun onInserted(position: Int, count: Int) {
notifyItemRangeInserted(position + headerCount, count) notifyItemRangeInserted(position + headerCount, count)
@ -69,7 +82,11 @@ class HomeAdapter(
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder = override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder =
when (viewType) { when (viewType) {
HEADER -> { 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 -> { ITEM -> {
val conversationView = LayoutInflater.from(parent.context).inflate(R.layout.view_conversation, parent, false) as ConversationView 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) { override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
if (holder is ConversationViewHolder) { 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 offset = if (hasHeaderView()) position - 1 else position
val thread = data.threads[offset] val thread = data.threads[offset]
val isTyping = data.typingThreadIDs.contains(thread.threadId) val isTyping = data.typingThreadIDs.contains(thread.threadId)
holder.view.bind(thread, isTyping, glide) holder.view.bind(thread, isTyping, glide)
} }
} }
}
override fun onViewRecycled(holder: RecyclerView.ViewHolder) { override fun onViewRecycled(holder: RecyclerView.ViewHolder) {
if (holder is ConversationViewHolder) { if (holder is ConversationViewHolder) {
holder.view.recycle() holder.view.recycle()
} else {
super.onViewRecycled(holder)
} }
} }
@ -109,6 +134,5 @@ class HomeAdapter(
class ConversationViewHolder(val view: ConversationView) : RecyclerView.ViewHolder(view) 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.ContentResolver
import android.content.Context import android.content.Context
import android.util.Log
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.asFlow import androidx.lifecycle.asFlow
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.channels.BufferOverflow import kotlinx.coroutines.channels.BufferOverflow
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableSharedFlow
@ -15,16 +17,22 @@ import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.debounce import kotlinx.coroutines.flow.debounce
import kotlinx.coroutines.flow.distinctUntilChanged 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.mapLatest
import kotlinx.coroutines.flow.merge import kotlinx.coroutines.flow.merge
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.onStart import kotlinx.coroutines.flow.onStart
import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.withContext import org.session.libsession.utilities.TextSecurePreferences
import org.thoughtcrime.securesms.ApplicationContext import org.thoughtcrime.securesms.ApplicationContext
import org.thoughtcrime.securesms.database.DatabaseContentProviders import org.thoughtcrime.securesms.database.DatabaseContentProviders
import org.thoughtcrime.securesms.database.ThreadDatabase import org.thoughtcrime.securesms.database.ThreadDatabase
import org.thoughtcrime.securesms.database.model.ThreadRecord import org.thoughtcrime.securesms.database.model.ThreadRecord
import org.thoughtcrime.securesms.util.DateUtils
import org.thoughtcrime.securesms.util.observeChanges import org.thoughtcrime.securesms.util.observeChanges
import java.util.Locale
import javax.inject.Inject import javax.inject.Inject
import dagger.hilt.android.qualifiers.ApplicationContext as ApplicationContextQualifier import dagger.hilt.android.qualifiers.ApplicationContext as ApplicationContextQualifier
@ -32,6 +40,7 @@ import dagger.hilt.android.qualifiers.ApplicationContext as ApplicationContextQu
class HomeViewModel @Inject constructor( class HomeViewModel @Inject constructor(
private val threadDb: ThreadDatabase, private val threadDb: ThreadDatabase,
private val contentResolver: ContentResolver, private val contentResolver: ContentResolver,
private val prefs: TextSecurePreferences,
@ApplicationContextQualifier private val context: Context, @ApplicationContextQualifier private val context: Context,
) : ViewModel() { ) : ViewModel() {
// SharedFlow that emits whenever the user asks us to reload the conversation // SharedFlow that emits whenever the user asks us to reload the conversation
@ -46,9 +55,20 @@ class HomeViewModel @Inject constructor(
* This flow will emit whenever the user asks us to reload the conversation list or * This flow will emit whenever the user asks us to reload the conversation list or
* whenever the conversation list changes. * whenever the conversation list changes.
*/ */
val threads: StateFlow<Data?> = combine(observeConversationList(), observeTypingStatus(), ::Data) val data: StateFlow<Data?> = combine(
observeConversationList(),
observeTypingStatus(),
messageRequests(),
::Data
)
.stateIn(viewModelScope, SharingStarted.Eagerly, null) .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>> = private fun observeTypingStatus(): Flow<Set<Long>> =
ApplicationContext.getInstance(context).typingStatusRepository ApplicationContext.getInstance(context).typingStatusRepository
.typingThreads .typingThreads
@ -56,32 +76,55 @@ class HomeViewModel @Inject constructor(
.onStart { emit(emptySet()) } .onStart { emit(emptySet()) }
.distinctUntilChanged() .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") @Suppress("OPT_IN_USAGE")
private fun observeConversationList(): Flow<List<ThreadRecord>> = merge( 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, manualReloadTrigger,
contentResolver.observeChanges(DatabaseContentProviders.ConversationList.CONTENT_URI)) contentResolver.observeChanges(DatabaseContentProviders.ConversationList.CONTENT_URI)
)
.flowOn(Dispatchers.IO)
.debounce(CHANGE_NOTIFICATION_DEBOUNCE_MILLS) .debounce(CHANGE_NOTIFICATION_DEBOUNCE_MILLS)
.onStart { emit(Unit) } .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)
}
}
}
}
}
fun tryReload() = manualReloadTrigger.tryEmit(Unit) fun tryReload() = manualReloadTrigger.tryEmit(Unit)
data class Data( data class Data(
val threads: List<ThreadRecord>, val threads: List<ThreadRecord> = emptyList(),
val typingThreadIDs: Set<Long> 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 { companion object {
private const val CHANGE_NOTIFICATION_DEBOUNCE_MILLS = 100L private const val CHANGE_NOTIFICATION_DEBOUNCE_MILLS = 100L
} }