mirror of
https://github.com/oxen-io/session-android.git
synced 2024-11-23 18:15:22 +00:00
Merge remote-tracking branch 'upstream/dev' into SES-2009-blinded-conversation
This commit is contained in:
commit
9441fdec21
@ -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;
|
||||||
|
return adapter.getMediaItemFor(mediaPager.getCurrentItem());
|
||||||
if (adapter != null) {
|
|
||||||
return adapter.getMediaItemFor(mediaPager.getCurrentItem());
|
|
||||||
} else {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static boolean isContentTypeSupported(final String contentType) {
|
public static boolean isContentTypeSupported(final String contentType) {
|
||||||
@ -526,23 +514,28 @@ 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.setAdapter(adapter);
|
|
||||||
adapter.setActive(true);
|
|
||||||
|
|
||||||
viewModel.setCursor(this, data.first, leftIsRecent);
|
mediaPager.removeOnPageChangeListener(viewPagerListener);
|
||||||
|
|
||||||
if (restartItem >= 0 || data.second >= 0) {
|
adapter = new CursorPagerAdapter(this, GlideApp.with(this), getWindow(), data.first, data.second, leftIsRecent);
|
||||||
int item = restartItem >= 0 ? restartItem : data.second;
|
mediaPager.setAdapter(adapter);
|
||||||
mediaPager.setCurrentItem(item);
|
|
||||||
|
|
||||||
if (item == 0) {
|
viewModel.setCursor(this, data.first, leftIsRecent);
|
||||||
viewPagerListener.onPageSelected(0);
|
|
||||||
}
|
int item = restartItem >= 0 && restartItem < adapter.getCount() ? restartItem : Math.max(Math.min(data.second, adapter.getCount() - 1), 0);
|
||||||
} else {
|
|
||||||
Log.w(TAG, "one of restartItem "+restartItem+" and data.second "+data.second+" would cause OOB exception");
|
viewPagerListener = new ViewPagerListener();
|
||||||
}
|
mediaPager.addOnPageChangeListener(viewPagerListener);
|
||||||
|
|
||||||
|
try {
|
||||||
|
mediaPager.setCurrentItem(item);
|
||||||
|
} catch (CursorIndexOutOfBoundsException e) {
|
||||||
|
throw new RuntimeException("restartItem = " + restartItem + ", data.second = " + data.second + " leftIsRecent = " + leftIsRecent, e);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (item == 0) {
|
||||||
|
viewPagerListener.onPageSelected(0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -560,26 +553,26 @@ 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) {
|
||||||
adapter.pause(position);
|
throw new RuntimeException("position = " + position + " leftIsRecent = " + leftIsRecent, e);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
adapter.pause(position);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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();
|
||||||
|
@ -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()
|
||||||
|
@ -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) {
|
||||||
threadIDs.add(threadID)
|
synchronized(threadIDs) {
|
||||||
|
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()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -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 = (
|
||||||
|
@ -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;
|
||||||
|
@ -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)
|
||||||
|
@ -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,13 +28,21 @@ class HomeAdapter(
|
|||||||
private const val ITEM = 1
|
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) {
|
set(newData) {
|
||||||
if (field === newData) {
|
if (field === newData) return
|
||||||
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)
|
||||||
@ -37,10 +50,10 @@ class HomeAdapter(
|
|||||||
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) {
|
||||||
val offset = if (hasHeaderView()) position - 1 else position
|
is HeaderFooterViewHolder -> {
|
||||||
val thread = data.threads[offset]
|
holder.binding.run {
|
||||||
val isTyping = data.typingThreadIDs.contains(thread.threadId)
|
messageRequests?.let {
|
||||||
holder.view.bind(thread, isTyping, glide)
|
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) {
|
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)
|
||||||
|
|
||||||
}
|
}
|
@ -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,24 +17,31 @@ 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
|
||||||
|
|
||||||
@HiltViewModel
|
@HiltViewModel
|
||||||
class HomeViewModel @Inject constructor(
|
class HomeViewModel @Inject constructor(
|
||||||
private val threadDb: ThreadDatabase,
|
private val threadDb: ThreadDatabase,
|
||||||
private val contentResolver: ContentResolver,
|
private val contentResolver: ContentResolver,
|
||||||
@ApplicationContextQualifier private val context: Context,
|
private val prefs: TextSecurePreferences,
|
||||||
|
@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
|
||||||
private val manualReloadTrigger = MutableSharedFlow<Unit>(
|
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
|
* 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(
|
||||||
.stateIn(viewModelScope, SharingStarted.Eagerly, null)
|
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>> =
|
private fun observeTypingStatus(): Flow<Set<Long>> =
|
||||||
ApplicationContext.getInstance(context).typingStatusRepository
|
ApplicationContext.getInstance(context).typingStatusRepository
|
||||||
@ -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()
|
||||||
manualReloadTrigger,
|
.mapLatest { _ ->
|
||||||
contentResolver.observeChanges(DatabaseContentProviders.ConversationList.CONTENT_URI))
|
threadDb.approvedConversationList.use { openCursor ->
|
||||||
.debounce(CHANGE_NOTIFICATION_DEBOUNCE_MILLS)
|
threadDb.readerFor(openCursor).run { generateSequence { next }.toList() }
|
||||||
.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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@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)
|
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
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user