mirror of
https://github.com/oxen-io/session-android.git
synced 2024-11-30 13:35:18 +00:00
Merge remote-tracking branch 'upstream/dev' into ses-637-voice-message-keeps-playing
This commit is contained in:
commit
2c90717235
@ -31,8 +31,8 @@ configurations.all {
|
|||||||
exclude module: "commons-logging"
|
exclude module: "commons-logging"
|
||||||
}
|
}
|
||||||
|
|
||||||
def canonicalVersionCode = 371
|
def canonicalVersionCode = 373
|
||||||
def canonicalVersionName = "1.18.2"
|
def canonicalVersionName = "1.18.4"
|
||||||
|
|
||||||
def postFixSize = 10
|
def postFixSize = 10
|
||||||
def abiPostFix = ['armeabi-v7a' : 1,
|
def abiPostFix = ['armeabi-v7a' : 1,
|
||||||
|
@ -57,6 +57,7 @@ import org.signal.aesgcmprovider.AesGcmProvider;
|
|||||||
import org.thoughtcrime.securesms.components.TypingStatusSender;
|
import org.thoughtcrime.securesms.components.TypingStatusSender;
|
||||||
import org.thoughtcrime.securesms.crypto.KeyPairUtilities;
|
import org.thoughtcrime.securesms.crypto.KeyPairUtilities;
|
||||||
import org.thoughtcrime.securesms.database.EmojiSearchDatabase;
|
import org.thoughtcrime.securesms.database.EmojiSearchDatabase;
|
||||||
|
import org.thoughtcrime.securesms.database.LastSentTimestampCache;
|
||||||
import org.thoughtcrime.securesms.database.LokiAPIDatabase;
|
import org.thoughtcrime.securesms.database.LokiAPIDatabase;
|
||||||
import org.thoughtcrime.securesms.database.Storage;
|
import org.thoughtcrime.securesms.database.Storage;
|
||||||
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper;
|
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper;
|
||||||
@ -149,6 +150,7 @@ public class ApplicationContext extends Application implements DefaultLifecycleO
|
|||||||
@Inject TextSecurePreferences textSecurePreferences;
|
@Inject TextSecurePreferences textSecurePreferences;
|
||||||
@Inject PushRegistry pushRegistry;
|
@Inject PushRegistry pushRegistry;
|
||||||
@Inject ConfigFactory configFactory;
|
@Inject ConfigFactory configFactory;
|
||||||
|
@Inject LastSentTimestampCache lastSentTimestampCache;
|
||||||
CallMessageProcessor callMessageProcessor;
|
CallMessageProcessor callMessageProcessor;
|
||||||
MessagingModuleConfiguration messagingModuleConfiguration;
|
MessagingModuleConfiguration messagingModuleConfiguration;
|
||||||
|
|
||||||
@ -218,7 +220,8 @@ public class ApplicationContext extends Application implements DefaultLifecycleO
|
|||||||
device,
|
device,
|
||||||
messageDataProvider,
|
messageDataProvider,
|
||||||
()-> KeyPairUtilities.INSTANCE.getUserED25519KeyPair(this),
|
()-> KeyPairUtilities.INSTANCE.getUserED25519KeyPair(this),
|
||||||
configFactory
|
configFactory,
|
||||||
|
lastSentTimestampCache
|
||||||
);
|
);
|
||||||
callMessageProcessor = new CallMessageProcessor(this, textSecurePreferences, ProcessLifecycleOwner.get().getLifecycle(), storage);
|
callMessageProcessor = new CallMessageProcessor(this, textSecurePreferences, ProcessLifecycleOwner.get().getLifecycle(), storage);
|
||||||
Log.i(TAG, "onCreate()");
|
Log.i(TAG, "onCreate()");
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -50,7 +50,7 @@ public class AttachmentServer implements Runnable {
|
|||||||
throws IOException
|
throws IOException
|
||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
this.context = context;
|
this.context = context.getApplicationContext();
|
||||||
this.attachment = attachment;
|
this.attachment = attachment;
|
||||||
this.socket = new ServerSocket(0, 0, InetAddress.getByAddress(new byte[]{127, 0, 0, 1}));
|
this.socket = new ServerSocket(0, 0, InetAddress.getByAddress(new byte[]{127, 0, 0, 1}));
|
||||||
this.port = socket.getLocalPort();
|
this.port = socket.getLocalPort();
|
||||||
|
@ -5,6 +5,8 @@ import android.text.TextUtils
|
|||||||
import com.google.protobuf.ByteString
|
import com.google.protobuf.ByteString
|
||||||
import org.greenrobot.eventbus.EventBus
|
import org.greenrobot.eventbus.EventBus
|
||||||
import org.session.libsession.database.MessageDataProvider
|
import org.session.libsession.database.MessageDataProvider
|
||||||
|
import org.session.libsession.messaging.MessagingModuleConfiguration
|
||||||
|
import org.session.libsession.messaging.messages.control.UnsendRequest
|
||||||
import org.session.libsession.messaging.sending_receiving.attachments.Attachment
|
import org.session.libsession.messaging.sending_receiving.attachments.Attachment
|
||||||
import org.session.libsession.messaging.sending_receiving.attachments.AttachmentId
|
import org.session.libsession.messaging.sending_receiving.attachments.AttachmentId
|
||||||
import org.session.libsession.messaging.sending_receiving.attachments.AttachmentState
|
import org.session.libsession.messaging.sending_receiving.attachments.AttachmentState
|
||||||
@ -184,10 +186,15 @@ class DatabaseAttachmentProvider(context: Context, helper: SQLCipherOpenHelper)
|
|||||||
override fun deleteMessage(messageID: Long, isSms: Boolean) {
|
override fun deleteMessage(messageID: Long, isSms: Boolean) {
|
||||||
val messagingDatabase: MessagingDatabase = if (isSms) DatabaseComponent.get(context).smsDatabase()
|
val messagingDatabase: MessagingDatabase = if (isSms) DatabaseComponent.get(context).smsDatabase()
|
||||||
else DatabaseComponent.get(context).mmsDatabase()
|
else DatabaseComponent.get(context).mmsDatabase()
|
||||||
|
val (threadId, timestamp) = runCatching { messagingDatabase.getMessageRecord(messageID).run { threadId to timestamp } }.getOrNull() ?: (null to null)
|
||||||
|
|
||||||
messagingDatabase.deleteMessage(messageID)
|
messagingDatabase.deleteMessage(messageID)
|
||||||
DatabaseComponent.get(context).lokiMessageDatabase().deleteMessage(messageID, isSms)
|
DatabaseComponent.get(context).lokiMessageDatabase().deleteMessage(messageID, isSms)
|
||||||
DatabaseComponent.get(context).lokiMessageDatabase().deleteMessageServerHash(messageID, mms = !isSms)
|
DatabaseComponent.get(context).lokiMessageDatabase().deleteMessageServerHash(messageID, mms = !isSms)
|
||||||
|
|
||||||
|
threadId ?: return
|
||||||
|
timestamp ?: return
|
||||||
|
MessagingModuleConfiguration.shared.lastSentTimestampCache.delete(threadId, timestamp)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun deleteMessages(messageIDs: List<Long>, threadId: Long, isSms: Boolean) {
|
override fun deleteMessages(messageIDs: List<Long>, threadId: Long, isSms: Boolean) {
|
||||||
@ -195,12 +202,17 @@ class DatabaseAttachmentProvider(context: Context, helper: SQLCipherOpenHelper)
|
|||||||
val messagingDatabase: MessagingDatabase = if (isSms) DatabaseComponent.get(context).smsDatabase()
|
val messagingDatabase: MessagingDatabase = if (isSms) DatabaseComponent.get(context).smsDatabase()
|
||||||
else DatabaseComponent.get(context).mmsDatabase()
|
else DatabaseComponent.get(context).mmsDatabase()
|
||||||
|
|
||||||
|
val messages = messageIDs.mapNotNull { runCatching { messagingDatabase.getMessageRecord(it) }.getOrNull() }
|
||||||
|
|
||||||
// Perform local delete
|
// Perform local delete
|
||||||
messagingDatabase.deleteMessages(messageIDs.toLongArray(), threadId)
|
messagingDatabase.deleteMessages(messageIDs.toLongArray(), threadId)
|
||||||
|
|
||||||
// Perform online delete
|
// Perform online delete
|
||||||
DatabaseComponent.get(context).lokiMessageDatabase().deleteMessages(messageIDs)
|
DatabaseComponent.get(context).lokiMessageDatabase().deleteMessages(messageIDs)
|
||||||
DatabaseComponent.get(context).lokiMessageDatabase().deleteMessageServerHashes(messageIDs, mms = !isSms)
|
DatabaseComponent.get(context).lokiMessageDatabase().deleteMessageServerHashes(messageIDs, mms = !isSms)
|
||||||
|
|
||||||
|
val threadId = messages.firstOrNull()?.threadId
|
||||||
|
threadId?.let{ MessagingModuleConfiguration.shared.lastSentTimestampCache.delete(it, messages.map { it.timestamp }) }
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun updateMessageAsDeleted(timestamp: Long, author: String): Long? {
|
override fun updateMessageAsDeleted(timestamp: Long, author: String): Long? {
|
||||||
|
@ -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();
|
||||||
|
@ -122,7 +122,7 @@ class ProfilePictureView @JvmOverloads constructor(
|
|||||||
|
|
||||||
glide.clear(imageView)
|
glide.clear(imageView)
|
||||||
|
|
||||||
val placeholder = PlaceholderAvatarPhoto(context, publicKey, displayName ?: "${publicKey.take(4)}...${publicKey.takeLast(4)}")
|
val placeholder = PlaceholderAvatarPhoto(publicKey, displayName ?: "${publicKey.take(4)}...${publicKey.takeLast(4)}")
|
||||||
|
|
||||||
if (signalProfilePicture != null && avatar != "0" && avatar != "") {
|
if (signalProfilePicture != null && avatar != "0" && avatar != "") {
|
||||||
glide.load(signalProfilePicture)
|
glide.load(signalProfilePicture)
|
||||||
|
@ -287,8 +287,10 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
|
|||||||
if (hexEncodedSeed == null) {
|
if (hexEncodedSeed == null) {
|
||||||
hexEncodedSeed = IdentityKeyUtil.getIdentityKeyPair(this).hexEncodedPrivateKey // Legacy account
|
hexEncodedSeed = IdentityKeyUtil.getIdentityKeyPair(this).hexEncodedPrivateKey // Legacy account
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val appContext = applicationContext
|
||||||
val loadFileContents: (String) -> String = { fileName ->
|
val loadFileContents: (String) -> String = { fileName ->
|
||||||
MnemonicUtilities.loadFileContents(this, fileName)
|
MnemonicUtilities.loadFileContents(appContext, fileName)
|
||||||
}
|
}
|
||||||
MnemonicCodec(loadFileContents).encode(hexEncodedSeed!!, MnemonicCodec.Language.Configuration.english)
|
MnemonicCodec(loadFileContents).encode(hexEncodedSeed!!, MnemonicCodec.Language.Configuration.english)
|
||||||
}
|
}
|
||||||
@ -325,7 +327,6 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
onAttachmentNeedsDownload = { attachmentId, mmsId ->
|
onAttachmentNeedsDownload = { attachmentId, mmsId ->
|
||||||
// Start download (on IO thread)
|
|
||||||
lifecycleScope.launch(Dispatchers.IO) {
|
lifecycleScope.launch(Dispatchers.IO) {
|
||||||
JobQueue.shared.add(AttachmentDownloadJob(attachmentId, mmsId))
|
JobQueue.shared.add(AttachmentDownloadJob(attachmentId, mmsId))
|
||||||
}
|
}
|
||||||
@ -335,8 +336,8 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
|
|||||||
)
|
)
|
||||||
adapter.visibleMessageViewDelegate = this
|
adapter.visibleMessageViewDelegate = this
|
||||||
|
|
||||||
// Register an AdapterDataObserver to scroll us to the bottom of the RecyclerView if we're
|
// Register an AdapterDataObserver to scroll us to the bottom of the RecyclerView for if
|
||||||
// already near the the bottom and the data changes.
|
// we're already near the the bottom and the data changes.
|
||||||
adapter.registerAdapterDataObserver(ConversationAdapterDataObserver(binding?.conversationRecyclerView!!, adapter))
|
adapter.registerAdapterDataObserver(ConversationAdapterDataObserver(binding?.conversationRecyclerView!!, adapter))
|
||||||
|
|
||||||
adapter
|
adapter
|
||||||
@ -374,7 +375,6 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
|
|||||||
const val PICK_GIF = 10
|
const val PICK_GIF = 10
|
||||||
const val PICK_FROM_LIBRARY = 12
|
const val PICK_FROM_LIBRARY = 12
|
||||||
const val INVITE_CONTACTS = 124
|
const val INVITE_CONTACTS = 124
|
||||||
|
|
||||||
}
|
}
|
||||||
// endregion
|
// endregion
|
||||||
|
|
||||||
@ -833,6 +833,7 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
|
|||||||
|
|
||||||
override fun onDestroy() {
|
override fun onDestroy() {
|
||||||
viewModel.saveDraft(binding?.inputBar?.text?.trim() ?: "")
|
viewModel.saveDraft(binding?.inputBar?.text?.trim() ?: "")
|
||||||
|
cancelVoiceMessage()
|
||||||
tearDownRecipientObserver()
|
tearDownRecipientObserver()
|
||||||
super.onDestroy()
|
super.onDestroy()
|
||||||
binding = null
|
binding = null
|
||||||
@ -1021,7 +1022,7 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun showVoiceMessageUI() {
|
override fun showVoiceMessageUI() {
|
||||||
binding?.inputBarRecordingView?.show()
|
binding?.inputBarRecordingView?.show(lifecycleScope)
|
||||||
binding?.inputBar?.alpha = 0.0f
|
binding?.inputBar?.alpha = 0.0f
|
||||||
val animation = ValueAnimator.ofObject(FloatEvaluator(), 1.0f, 0.0f)
|
val animation = ValueAnimator.ofObject(FloatEvaluator(), 1.0f, 0.0f)
|
||||||
animation.duration = 250L
|
animation.duration = 250L
|
||||||
|
@ -209,20 +209,6 @@ class ConversationAdapter(
|
|||||||
return messageDB.readerFor(cursor).current
|
return messageDB.readerFor(cursor).current
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getLastSentMessageId(cursor: Cursor): Long {
|
|
||||||
// If we don't move to first (or at least step backwards) we can step off the end of the
|
|
||||||
// cursor and any query will return an "Index = -1" error.
|
|
||||||
val cursorHasContent = cursor.moveToFirst()
|
|
||||||
if (cursorHasContent) {
|
|
||||||
val thisThreadId = cursor.getLong(4) // Column index 4 is "thread_id"
|
|
||||||
if (thisThreadId != -1L) {
|
|
||||||
val thisUsersSessionId = TextSecurePreferences.getLocalNumber(context)
|
|
||||||
return messageDB.getLastSentMessageFromSender(thisThreadId, thisUsersSessionId)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return -1L
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun changeCursor(cursor: Cursor?) {
|
override fun changeCursor(cursor: Cursor?) {
|
||||||
super.changeCursor(cursor)
|
super.changeCursor(cursor)
|
||||||
|
|
||||||
@ -243,11 +229,6 @@ class ConversationAdapter(
|
|||||||
toDeselect.iterator().forEach { (pos, record) ->
|
toDeselect.iterator().forEach { (pos, record) ->
|
||||||
onDeselect(record, pos)
|
onDeselect(record, pos)
|
||||||
}
|
}
|
||||||
|
|
||||||
// This value gets updated here ONLY when the cursor changes, and the value is then passed
|
|
||||||
// through to `VisibleMessageView.bind` each time we bind via `onBindItemViewHolder`, above.
|
|
||||||
// If there are no messages then lastSentMessageId is assigned the value -1L.
|
|
||||||
if (cursor != null) { lastSentMessageId = getLastSentMessageId(cursor) }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun findLastSeenItemPosition(lastSeenTimestamp: Long): Int? {
|
fun findLastSeenItemPosition(lastSeenTimestamp: Long): Int? {
|
||||||
|
@ -2,6 +2,7 @@ package org.thoughtcrime.securesms.conversation.v2
|
|||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.database.Cursor
|
import android.database.Cursor
|
||||||
|
import org.session.libsession.messaging.MessagingModuleConfiguration
|
||||||
import org.thoughtcrime.securesms.dependencies.DatabaseComponent
|
import org.thoughtcrime.securesms.dependencies.DatabaseComponent
|
||||||
import org.thoughtcrime.securesms.util.AbstractCursorLoader
|
import org.thoughtcrime.securesms.util.AbstractCursorLoader
|
||||||
|
|
||||||
@ -12,6 +13,7 @@ class ConversationLoader(
|
|||||||
) : AbstractCursorLoader(context) {
|
) : AbstractCursorLoader(context) {
|
||||||
|
|
||||||
override fun getCursor(): Cursor {
|
override fun getCursor(): Cursor {
|
||||||
|
MessagingModuleConfiguration.shared.lastSentTimestampCache.refresh(threadID)
|
||||||
return DatabaseComponent.get(context).mmsSmsDatabase().getConversation(threadID, reverse)
|
return DatabaseComponent.get(context).mmsSmsDatabase().getConversation(threadID, reverse)
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -4,8 +4,6 @@ import android.animation.FloatEvaluator
|
|||||||
import android.animation.IntEvaluator
|
import android.animation.IntEvaluator
|
||||||
import android.animation.ValueAnimator
|
import android.animation.ValueAnimator
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.os.Handler
|
|
||||||
import android.os.Looper
|
|
||||||
import android.util.AttributeSet
|
import android.util.AttributeSet
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.widget.ImageView
|
import android.widget.ImageView
|
||||||
@ -14,6 +12,11 @@ import android.widget.RelativeLayout
|
|||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
import androidx.core.content.res.ResourcesCompat
|
import androidx.core.content.res.ResourcesCompat
|
||||||
import androidx.core.view.isVisible
|
import androidx.core.view.isVisible
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Job
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
|
import kotlinx.coroutines.isActive
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
import network.loki.messenger.R
|
import network.loki.messenger.R
|
||||||
import network.loki.messenger.databinding.ViewInputBarRecordingBinding
|
import network.loki.messenger.databinding.ViewInputBarRecordingBinding
|
||||||
import org.thoughtcrime.securesms.util.DateUtils
|
import org.thoughtcrime.securesms.util.DateUtils
|
||||||
@ -25,10 +28,10 @@ import java.util.Date
|
|||||||
class InputBarRecordingView : RelativeLayout {
|
class InputBarRecordingView : RelativeLayout {
|
||||||
private lateinit var binding: ViewInputBarRecordingBinding
|
private lateinit var binding: ViewInputBarRecordingBinding
|
||||||
private var startTimestamp = 0L
|
private var startTimestamp = 0L
|
||||||
private val snHandler = Handler(Looper.getMainLooper())
|
|
||||||
private var dotViewAnimation: ValueAnimator? = null
|
private var dotViewAnimation: ValueAnimator? = null
|
||||||
private var pulseAnimation: ValueAnimator? = null
|
private var pulseAnimation: ValueAnimator? = null
|
||||||
var delegate: InputBarRecordingViewDelegate? = null
|
var delegate: InputBarRecordingViewDelegate? = null
|
||||||
|
private var timerJob: Job? = null
|
||||||
|
|
||||||
val lockView: LinearLayout
|
val lockView: LinearLayout
|
||||||
get() = binding.lockView
|
get() = binding.lockView
|
||||||
@ -50,9 +53,10 @@ class InputBarRecordingView : RelativeLayout {
|
|||||||
binding = ViewInputBarRecordingBinding.inflate(LayoutInflater.from(context), this, true)
|
binding = ViewInputBarRecordingBinding.inflate(LayoutInflater.from(context), this, true)
|
||||||
binding.inputBarMiddleContentContainer.disableClipping()
|
binding.inputBarMiddleContentContainer.disableClipping()
|
||||||
binding.inputBarCancelButton.setOnClickListener { hide() }
|
binding.inputBarCancelButton.setOnClickListener { hide() }
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun show() {
|
fun show(scope: CoroutineScope) {
|
||||||
startTimestamp = Date().time
|
startTimestamp = Date().time
|
||||||
binding.recordButtonOverlayImageView.setImageDrawable(ResourcesCompat.getDrawable(resources, R.drawable.ic_microphone, context.theme))
|
binding.recordButtonOverlayImageView.setImageDrawable(ResourcesCompat.getDrawable(resources, R.drawable.ic_microphone, context.theme))
|
||||||
binding.inputBarCancelButton.alpha = 0.0f
|
binding.inputBarCancelButton.alpha = 0.0f
|
||||||
@ -69,7 +73,7 @@ class InputBarRecordingView : RelativeLayout {
|
|||||||
animateDotView()
|
animateDotView()
|
||||||
pulse()
|
pulse()
|
||||||
animateLockViewUp()
|
animateLockViewUp()
|
||||||
updateTimer()
|
startTimer(scope)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun hide() {
|
fun hide() {
|
||||||
@ -86,6 +90,24 @@ class InputBarRecordingView : RelativeLayout {
|
|||||||
}
|
}
|
||||||
animation.start()
|
animation.start()
|
||||||
delegate?.handleVoiceMessageUIHidden()
|
delegate?.handleVoiceMessageUIHidden()
|
||||||
|
stopTimer()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun startTimer(scope: CoroutineScope) {
|
||||||
|
timerJob?.cancel()
|
||||||
|
timerJob = scope.launch {
|
||||||
|
while (isActive) {
|
||||||
|
val duration = (Date().time - startTimestamp) / 1000L
|
||||||
|
binding.recordingViewDurationTextView.text = DateUtils.formatElapsedTime(duration)
|
||||||
|
|
||||||
|
delay(500)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun stopTimer() {
|
||||||
|
timerJob?.cancel()
|
||||||
|
timerJob = null
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun animateDotView() {
|
private fun animateDotView() {
|
||||||
@ -129,12 +151,6 @@ class InputBarRecordingView : RelativeLayout {
|
|||||||
animation.start()
|
animation.start()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun updateTimer() {
|
|
||||||
val duration = (Date().time - startTimestamp) / 1000L
|
|
||||||
binding.recordingViewDurationTextView.text = DateUtils.formatElapsedTime(duration)
|
|
||||||
snHandler.postDelayed({ updateTimer() }, 500)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun lock() {
|
fun lock() {
|
||||||
val fadeOutAnimation = ValueAnimator.ofObject(FloatEvaluator(), 1.0f, 0.0f)
|
val fadeOutAnimation = ValueAnimator.ofObject(FloatEvaluator(), 1.0f, 0.0f)
|
||||||
fadeOutAnimation.duration = 250L
|
fadeOutAnimation.duration = 250L
|
||||||
|
@ -22,7 +22,6 @@ import androidx.appcompat.app.AppCompatActivity
|
|||||||
import androidx.constraintlayout.widget.ConstraintLayout
|
import androidx.constraintlayout.widget.ConstraintLayout
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.core.os.bundleOf
|
import androidx.core.os.bundleOf
|
||||||
import androidx.core.view.isGone
|
|
||||||
import androidx.core.view.isVisible
|
import androidx.core.view.isVisible
|
||||||
import androidx.core.view.marginBottom
|
import androidx.core.view.marginBottom
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
@ -39,6 +38,7 @@ import org.session.libsession.utilities.modifyLayoutParams
|
|||||||
import org.session.libsignal.utilities.IdPrefix
|
import org.session.libsignal.utilities.IdPrefix
|
||||||
import org.session.libsignal.utilities.Log
|
import org.session.libsignal.utilities.Log
|
||||||
import org.thoughtcrime.securesms.conversation.v2.ConversationActivityV2
|
import org.thoughtcrime.securesms.conversation.v2.ConversationActivityV2
|
||||||
|
import org.thoughtcrime.securesms.database.LastSentTimestampCache
|
||||||
import org.thoughtcrime.securesms.database.LokiAPIDatabase
|
import org.thoughtcrime.securesms.database.LokiAPIDatabase
|
||||||
import org.thoughtcrime.securesms.database.LokiThreadDatabase
|
import org.thoughtcrime.securesms.database.LokiThreadDatabase
|
||||||
import org.thoughtcrime.securesms.database.MmsDatabase
|
import org.thoughtcrime.securesms.database.MmsDatabase
|
||||||
@ -73,6 +73,7 @@ class VisibleMessageView : LinearLayout {
|
|||||||
@Inject lateinit var mmsSmsDb: MmsSmsDatabase
|
@Inject lateinit var mmsSmsDb: MmsSmsDatabase
|
||||||
@Inject lateinit var smsDb: SmsDatabase
|
@Inject lateinit var smsDb: SmsDatabase
|
||||||
@Inject lateinit var mmsDb: MmsDatabase
|
@Inject lateinit var mmsDb: MmsDatabase
|
||||||
|
@Inject lateinit var lastSentTimestampCache: LastSentTimestampCache
|
||||||
|
|
||||||
private val binding by lazy { ViewVisibleMessageBinding.bind(this) }
|
private val binding by lazy { ViewVisibleMessageBinding.bind(this) }
|
||||||
private val swipeToReplyIcon = ContextCompat.getDrawable(context, R.drawable.ic_baseline_reply_24)!!.mutate()
|
private val swipeToReplyIcon = ContextCompat.getDrawable(context, R.drawable.ic_baseline_reply_24)!!.mutate()
|
||||||
@ -303,10 +304,6 @@ class VisibleMessageView : LinearLayout {
|
|||||||
|
|
||||||
// --- If we got here then we know the message is outgoing ---
|
// --- If we got here then we know the message is outgoing ---
|
||||||
|
|
||||||
val thisUsersSessionId = TextSecurePreferences.getLocalNumber(context)
|
|
||||||
val lastSentMessageId = mmsSmsDb.getLastSentMessageFromSender(message.threadId, thisUsersSessionId)
|
|
||||||
val isLastSentMessage = lastSentMessageId == message.id
|
|
||||||
|
|
||||||
// ----- Case ii.) Message is outgoing but NOT scheduled to disappear -----
|
// ----- Case ii.) Message is outgoing but NOT scheduled to disappear -----
|
||||||
if (!scheduledToDisappear) {
|
if (!scheduledToDisappear) {
|
||||||
// If this isn't a disappearing message then we never show the timer
|
// If this isn't a disappearing message then we never show the timer
|
||||||
@ -319,9 +316,11 @@ class VisibleMessageView : LinearLayout {
|
|||||||
} else {
|
} else {
|
||||||
// ..but if the message HAS been successfully sent or read then only display the delivery status
|
// ..but if the message HAS been successfully sent or read then only display the delivery status
|
||||||
// text and image if this is the last sent message.
|
// text and image if this is the last sent message.
|
||||||
binding.messageStatusTextView.isVisible = isLastSentMessage
|
val lastSentTimestamp = lastSentTimestampCache.getTimestamp(message.threadId)
|
||||||
binding.messageStatusImageView.isVisible = isLastSentMessage
|
val isLastSent = lastSentTimestamp == message.timestamp
|
||||||
if (isLastSentMessage) { binding.messageStatusImageView.bringToFront() }
|
binding.messageStatusTextView.isVisible = isLastSent
|
||||||
|
binding.messageStatusImageView.isVisible = isLastSent
|
||||||
|
if (isLastSent) { binding.messageStatusImageView.bringToFront() }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else // ----- Case iii.) Message is outgoing AND scheduled to disappear -----
|
else // ----- Case iii.) Message is outgoing AND scheduled to disappear -----
|
||||||
|
@ -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()
|
||||||
|
@ -1,10 +1,9 @@
|
|||||||
package org.thoughtcrime.securesms.crypto
|
package org.thoughtcrime.securesms.crypto
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import com.goterl.lazysodium.LazySodiumAndroid
|
|
||||||
import com.goterl.lazysodium.SodiumAndroid
|
|
||||||
import com.goterl.lazysodium.utils.Key
|
import com.goterl.lazysodium.utils.Key
|
||||||
import com.goterl.lazysodium.utils.KeyPair
|
import com.goterl.lazysodium.utils.KeyPair
|
||||||
|
import org.session.libsession.messaging.utilities.SodiumUtilities.sodium
|
||||||
import org.session.libsignal.crypto.ecc.DjbECPrivateKey
|
import org.session.libsignal.crypto.ecc.DjbECPrivateKey
|
||||||
import org.session.libsignal.crypto.ecc.DjbECPublicKey
|
import org.session.libsignal.crypto.ecc.DjbECPublicKey
|
||||||
import org.session.libsignal.crypto.ecc.ECKeyPair
|
import org.session.libsignal.crypto.ecc.ECKeyPair
|
||||||
@ -13,8 +12,6 @@ import org.session.libsignal.utilities.Hex
|
|||||||
|
|
||||||
object KeyPairUtilities {
|
object KeyPairUtilities {
|
||||||
|
|
||||||
private val sodium by lazy { LazySodiumAndroid(SodiumAndroid()) }
|
|
||||||
|
|
||||||
fun generate(): KeyPairGenerationResult {
|
fun generate(): KeyPairGenerationResult {
|
||||||
val seed = sodium.randomBytesBuf(16)
|
val seed = sodium.randomBytesBuf(16)
|
||||||
try {
|
try {
|
||||||
|
@ -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()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -0,0 +1,38 @@
|
|||||||
|
package org.thoughtcrime.securesms.database
|
||||||
|
|
||||||
|
import org.session.libsession.messaging.LastSentTimestampCache
|
||||||
|
import javax.inject.Inject
|
||||||
|
import javax.inject.Singleton
|
||||||
|
|
||||||
|
@Singleton
|
||||||
|
class LastSentTimestampCache @Inject constructor(
|
||||||
|
val mmsSmsDatabase: MmsSmsDatabase
|
||||||
|
): LastSentTimestampCache {
|
||||||
|
|
||||||
|
private val map = mutableMapOf<Long, Long>()
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
|
override fun getTimestamp(threadId: Long): Long? = map[threadId]
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
|
override fun submitTimestamp(threadId: Long, timestamp: Long) {
|
||||||
|
if (map[threadId]?.let { timestamp <= it } == true) return
|
||||||
|
|
||||||
|
map[threadId] = timestamp
|
||||||
|
}
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
|
override fun delete(threadId: Long, timestamps: List<Long>) {
|
||||||
|
if (map[threadId]?.let { it !in timestamps } == true) return
|
||||||
|
map.remove(threadId)
|
||||||
|
refresh(threadId)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
|
override fun refresh(threadId: Long) {
|
||||||
|
if (map[threadId]?.let { it > 0 } == true) return
|
||||||
|
val lastOutgoingTimestamp = mmsSmsDatabase.getLastOutgoingTimestamp(threadId)
|
||||||
|
if (lastOutgoingTimestamp <= 0) return
|
||||||
|
map[threadId] = lastOutgoingTimestamp
|
||||||
|
}
|
||||||
|
}
|
@ -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 = (
|
||||||
|
@ -9,7 +9,11 @@ public interface MmsSmsColumns {
|
|||||||
public static final String THREAD_ID = "thread_id";
|
public static final String THREAD_ID = "thread_id";
|
||||||
public static final String READ = "read";
|
public static final String READ = "read";
|
||||||
public static final String BODY = "body";
|
public static final String BODY = "body";
|
||||||
|
|
||||||
|
// This is the address of the message recipient, which may be a single user, a group, or a community!
|
||||||
|
// It is NOT the address of the sender of any given message!
|
||||||
public static final String ADDRESS = "address";
|
public static final String ADDRESS = "address";
|
||||||
|
|
||||||
public static final String ADDRESS_DEVICE_ID = "address_device_id";
|
public static final String ADDRESS_DEVICE_ID = "address_device_id";
|
||||||
public static final String DELIVERY_RECEIPT_COUNT = "delivery_receipt_count";
|
public static final String DELIVERY_RECEIPT_COUNT = "delivery_receipt_count";
|
||||||
public static final String READ_RECEIPT_COUNT = "read_receipt_count";
|
public static final String READ_RECEIPT_COUNT = "read_receipt_count";
|
||||||
|
@ -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);
|
||||||
@ -295,15 +299,7 @@ public class MmsSmsDatabase extends Database {
|
|||||||
return identifiedMessages;
|
return identifiedMessages;
|
||||||
}
|
}
|
||||||
|
|
||||||
public long getLastSentMessageFromSender(long threadId, String serializedAuthor) {
|
public long getLastOutgoingTimestamp(long threadId) {
|
||||||
|
|
||||||
// Early exit
|
|
||||||
boolean isOwnNumber = Util.isOwnNumber(context, serializedAuthor);
|
|
||||||
if (!isOwnNumber) {
|
|
||||||
Log.i(TAG, "Asked to find last sent message but sender isn't us - returning null.");
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
String order = MmsSmsColumns.NORMALIZED_DATE_SENT + " DESC";
|
String order = MmsSmsColumns.NORMALIZED_DATE_SENT + " DESC";
|
||||||
String selection = MmsSmsColumns.THREAD_ID + " = " + threadId;
|
String selection = MmsSmsColumns.THREAD_ID + " = " + threadId;
|
||||||
|
|
||||||
@ -311,8 +307,13 @@ public class MmsSmsDatabase extends Database {
|
|||||||
try (Cursor cursor = queryTables(PROJECTION, selection, order, null)) {
|
try (Cursor cursor = queryTables(PROJECTION, selection, order, null)) {
|
||||||
try (MmsSmsDatabase.Reader reader = readerFor(cursor)) {
|
try (MmsSmsDatabase.Reader reader = readerFor(cursor)) {
|
||||||
MessageRecord messageRecord;
|
MessageRecord messageRecord;
|
||||||
|
long attempts = 0;
|
||||||
|
long maxAttempts = 20;
|
||||||
while ((messageRecord = reader.getNext()) != null) {
|
while ((messageRecord = reader.getNext()) != null) {
|
||||||
if (messageRecord.isOutgoing()) { return messageRecord.id; }
|
// Note: We rely on the message order to get us the most recent outgoing message - so we
|
||||||
|
// take the first outgoing message we find as the last outgoing message.
|
||||||
|
if (messageRecord.isOutgoing()) return messageRecord.getTimestamp();
|
||||||
|
if (attempts++ > maxAttempts) break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -638,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
|
||||||
@ -661,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() {
|
||||||
@ -678,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;
|
||||||
|
@ -881,6 +881,10 @@ public class ThreadDatabase extends Database {
|
|||||||
this.cursor = cursor;
|
this.cursor = cursor;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public int getCount() {
|
||||||
|
return cursor == null ? 0 : cursor.getCount();
|
||||||
|
}
|
||||||
|
|
||||||
public ThreadRecord getNext() {
|
public ThreadRecord getNext() {
|
||||||
if (cursor == null || !cursor.moveToNext())
|
if (cursor == null || !cursor.moveToNext())
|
||||||
return null;
|
return null;
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
package org.thoughtcrime.securesms.glide
|
package org.thoughtcrime.securesms.glide
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
import android.graphics.drawable.BitmapDrawable
|
import android.graphics.drawable.BitmapDrawable
|
||||||
import com.bumptech.glide.load.Options
|
import com.bumptech.glide.load.Options
|
||||||
import com.bumptech.glide.load.model.ModelLoader
|
import com.bumptech.glide.load.model.ModelLoader
|
||||||
@ -8,7 +9,7 @@ import com.bumptech.glide.load.model.ModelLoaderFactory
|
|||||||
import com.bumptech.glide.load.model.MultiModelLoaderFactory
|
import com.bumptech.glide.load.model.MultiModelLoaderFactory
|
||||||
import org.session.libsession.avatars.PlaceholderAvatarPhoto
|
import org.session.libsession.avatars.PlaceholderAvatarPhoto
|
||||||
|
|
||||||
class PlaceholderAvatarLoader(): ModelLoader<PlaceholderAvatarPhoto, BitmapDrawable> {
|
class PlaceholderAvatarLoader(private val appContext: Context): ModelLoader<PlaceholderAvatarPhoto, BitmapDrawable> {
|
||||||
|
|
||||||
override fun buildLoadData(
|
override fun buildLoadData(
|
||||||
model: PlaceholderAvatarPhoto,
|
model: PlaceholderAvatarPhoto,
|
||||||
@ -16,14 +17,14 @@ class PlaceholderAvatarLoader(): ModelLoader<PlaceholderAvatarPhoto, BitmapDrawa
|
|||||||
height: Int,
|
height: Int,
|
||||||
options: Options
|
options: Options
|
||||||
): LoadData<BitmapDrawable> {
|
): LoadData<BitmapDrawable> {
|
||||||
return LoadData(model, PlaceholderAvatarFetcher(model.context, model))
|
return LoadData(model, PlaceholderAvatarFetcher(appContext, model))
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun handles(model: PlaceholderAvatarPhoto): Boolean = true
|
override fun handles(model: PlaceholderAvatarPhoto): Boolean = true
|
||||||
|
|
||||||
class Factory() : ModelLoaderFactory<PlaceholderAvatarPhoto, BitmapDrawable> {
|
class Factory(private val appContext: Context) : ModelLoaderFactory<PlaceholderAvatarPhoto, BitmapDrawable> {
|
||||||
override fun build(multiFactory: MultiModelLoaderFactory): ModelLoader<PlaceholderAvatarPhoto, BitmapDrawable> {
|
override fun build(multiFactory: MultiModelLoaderFactory): ModelLoader<PlaceholderAvatarPhoto, BitmapDrawable> {
|
||||||
return PlaceholderAvatarLoader()
|
return PlaceholderAvatarLoader(appContext)
|
||||||
}
|
}
|
||||||
override fun teardown() {}
|
override fun teardown() {}
|
||||||
}
|
}
|
||||||
|
@ -2,12 +2,9 @@ package org.thoughtcrime.securesms.home
|
|||||||
|
|
||||||
import android.Manifest
|
import android.Manifest
|
||||||
import android.app.NotificationManager
|
import android.app.NotificationManager
|
||||||
import android.content.BroadcastReceiver
|
|
||||||
import android.content.ClipData
|
import android.content.ClipData
|
||||||
import android.content.ClipboardManager
|
import android.content.ClipboardManager
|
||||||
import android.content.Context
|
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.content.IntentFilter
|
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.text.SpannableString
|
import android.text.SpannableString
|
||||||
@ -18,19 +15,18 @@ import androidx.core.view.isVisible
|
|||||||
import androidx.lifecycle.Lifecycle
|
import androidx.lifecycle.Lifecycle
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import androidx.lifecycle.repeatOnLifecycle
|
import androidx.lifecycle.repeatOnLifecycle
|
||||||
import androidx.localbroadcastmanager.content.LocalBroadcastManager
|
|
||||||
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
|
||||||
|
import kotlinx.coroutines.flow.collectLatest
|
||||||
import kotlinx.coroutines.flow.filter
|
import kotlinx.coroutines.flow.filter
|
||||||
|
import kotlinx.coroutines.flow.filterNotNull
|
||||||
import kotlinx.coroutines.flow.onEach
|
import kotlinx.coroutines.flow.onEach
|
||||||
import kotlinx.coroutines.launch
|
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
|
||||||
@ -76,14 +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 org.thoughtcrime.securesms.util.themeState
|
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import java.util.Locale
|
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
@AndroidEntryPoint
|
@AndroidEntryPoint
|
||||||
@ -99,7 +92,6 @@ class HomeActivity : PassphraseRequiredActionBarActivity(),
|
|||||||
|
|
||||||
private lateinit var binding: ActivityHomeBinding
|
private lateinit var binding: ActivityHomeBinding
|
||||||
private lateinit var glide: GlideRequests
|
private lateinit var glide: GlideRequests
|
||||||
private var broadcastReceiver: BroadcastReceiver? = null
|
|
||||||
|
|
||||||
@Inject lateinit var threadDb: ThreadDatabase
|
@Inject lateinit var threadDb: ThreadDatabase
|
||||||
@Inject lateinit var mmsSmsDatabase: MmsSmsDatabase
|
@Inject lateinit var mmsSmsDatabase: MmsSmsDatabase
|
||||||
@ -117,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 ->
|
||||||
@ -189,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)
|
||||||
@ -205,18 +196,10 @@ class HomeActivity : PassphraseRequiredActionBarActivity(),
|
|||||||
// Set up empty state view
|
// Set up empty state view
|
||||||
binding.createNewPrivateChatButton.setOnClickListener { showNewConversation() }
|
binding.createNewPrivateChatButton.setOnClickListener { showNewConversation() }
|
||||||
IP2Country.configureIfNeeded(this@HomeActivity)
|
IP2Country.configureIfNeeded(this@HomeActivity)
|
||||||
startObservingUpdates()
|
|
||||||
|
|
||||||
// Set up new conversation button
|
// Set up new conversation button
|
||||||
binding.newConversationButton.setOnClickListener { showNewConversation() }
|
binding.newConversationButton.setOnClickListener { showNewConversation() }
|
||||||
// Observe blocked contacts changed events
|
// Observe blocked contacts changed events
|
||||||
val broadcastReceiver = object : BroadcastReceiver() {
|
|
||||||
override fun onReceive(context: Context, intent: Intent) {
|
|
||||||
binding.recyclerView.adapter!!.notifyDataSetChanged()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
this.broadcastReceiver = broadcastReceiver
|
|
||||||
LocalBroadcastManager.getInstance(this).registerReceiver(broadcastReceiver, IntentFilter("blockedContactsChanged"))
|
|
||||||
|
|
||||||
// subscribe to outdated config updates, this should be removed after long enough time for device migration
|
// subscribe to outdated config updates, this should be removed after long enough time for device migration
|
||||||
lifecycleScope.launch {
|
lifecycleScope.launch {
|
||||||
@ -227,6 +210,26 @@ class HomeActivity : PassphraseRequiredActionBarActivity(),
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Subscribe to threads and update the UI
|
||||||
|
lifecycleScope.launch {
|
||||||
|
repeatOnLifecycle(Lifecycle.State.STARTED) {
|
||||||
|
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 { data ->
|
||||||
|
val manager = binding.recyclerView.layoutManager as LinearLayoutManager
|
||||||
|
val firstPos = manager.findFirstCompletelyVisibleItemPosition()
|
||||||
|
val offsetTop = if(firstPos >= 0) {
|
||||||
|
manager.findViewByPosition(firstPos)?.let { view ->
|
||||||
|
manager.getDecoratedTop(view) - manager.getTopDecorationHeight(view)
|
||||||
|
} ?: 0
|
||||||
|
} else 0
|
||||||
|
homeAdapter.data = data
|
||||||
|
if(firstPos >= 0) { manager.scrollToPositionWithOffset(firstPos, offsetTop) }
|
||||||
|
updateEmptyState()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
lifecycleScope.launchWhenStarted {
|
lifecycleScope.launchWhenStarted {
|
||||||
launch(Dispatchers.IO) {
|
launch(Dispatchers.IO) {
|
||||||
// Double check that the long poller is up
|
// Double check that the long poller is up
|
||||||
@ -332,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()
|
||||||
@ -385,52 +360,20 @@ class HomeActivity : PassphraseRequiredActionBarActivity(),
|
|||||||
ConfigurationMessageUtilities.syncConfigurationIfNeeded(this@HomeActivity)
|
ConfigurationMessageUtilities.syncConfigurationIfNeeded(this@HomeActivity)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// If the theme hasn't changed then start observing updates again (if it does change then we
|
|
||||||
// will recreate the activity resulting in it responding to changes multiple times)
|
|
||||||
if (currentThemeState == textSecurePreferences.themeState() && !homeViewModel.getObservable(this).hasActiveObservers()) {
|
|
||||||
startObservingUpdates()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onPause() {
|
override fun onPause() {
|
||||||
super.onPause()
|
super.onPause()
|
||||||
ApplicationContext.getInstance(this).messageNotifier.setHomeScreenVisible(false)
|
ApplicationContext.getInstance(this).messageNotifier.setHomeScreenVisible(false)
|
||||||
|
|
||||||
homeViewModel.getObservable(this).removeObservers(this)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDestroy() {
|
override fun onDestroy() {
|
||||||
val broadcastReceiver = this.broadcastReceiver
|
|
||||||
if (broadcastReceiver != null) {
|
|
||||||
LocalBroadcastManager.getInstance(this).unregisterReceiver(broadcastReceiver)
|
|
||||||
}
|
|
||||||
super.onDestroy()
|
super.onDestroy()
|
||||||
EventBus.getDefault().unregister(this)
|
EventBus.getDefault().unregister(this)
|
||||||
}
|
}
|
||||||
// endregion
|
// endregion
|
||||||
|
|
||||||
// region Updating
|
// region Updating
|
||||||
private fun startObservingUpdates() {
|
|
||||||
homeViewModel.getObservable(this).observe(this) { newData ->
|
|
||||||
val manager = binding.recyclerView.layoutManager as LinearLayoutManager
|
|
||||||
val firstPos = manager.findFirstCompletelyVisibleItemPosition()
|
|
||||||
val offsetTop = if(firstPos >= 0) {
|
|
||||||
manager.findViewByPosition(firstPos)?.let { view ->
|
|
||||||
manager.getDecoratedTop(view) - manager.getTopDecorationHeight(view)
|
|
||||||
} ?: 0
|
|
||||||
} else 0
|
|
||||||
homeAdapter.data = newData
|
|
||||||
if(firstPos >= 0) { manager.scrollToPositionWithOffset(firstPos, offsetTop) }
|
|
||||||
setupMessageRequestsBanner()
|
|
||||||
updateEmptyState()
|
|
||||||
}
|
|
||||||
|
|
||||||
ApplicationContext.getInstance(this@HomeActivity).typingStatusRepository.typingThreads.observe(this) { threadIds ->
|
|
||||||
homeAdapter.typingThreadIDs = (threadIds ?: setOf())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun updateEmptyState() {
|
private fun updateEmptyState() {
|
||||||
val threadCount = (binding.recyclerView.adapter)!!.itemCount
|
val threadCount = (binding.recyclerView.adapter)!!.itemCount
|
||||||
binding.emptyStateContainer.isVisible = threadCount == 0 && binding.recyclerView.isVisible
|
binding.emptyStateContainer.isVisible = threadCount == 0 && binding.recyclerView.isVisible
|
||||||
@ -441,7 +384,7 @@ class HomeActivity : PassphraseRequiredActionBarActivity(),
|
|||||||
if (event.recipient.isLocalNumber) {
|
if (event.recipient.isLocalNumber) {
|
||||||
updateProfileButton()
|
updateProfileButton()
|
||||||
} else {
|
} else {
|
||||||
homeViewModel.tryUpdateChannel()
|
homeViewModel.tryReload()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -612,7 +555,7 @@ class HomeActivity : PassphraseRequiredActionBarActivity(),
|
|||||||
private fun setConversationPinned(threadId: Long, pinned: Boolean) {
|
private fun setConversationPinned(threadId: Long, pinned: Boolean) {
|
||||||
lifecycleScope.launch(Dispatchers.IO) {
|
lifecycleScope.launch(Dispatchers.IO) {
|
||||||
storage.setPinned(threadId, pinned)
|
storage.setPinned(threadId, pinned)
|
||||||
homeViewModel.tryUpdateChannel()
|
homeViewModel.tryReload()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -687,8 +630,7 @@ 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.tryUpdateChannel()
|
|
||||||
}
|
}
|
||||||
button(R.string.no)
|
button(R.string.no)
|
||||||
}
|
}
|
||||||
|
@ -9,14 +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 org.thoughtcrime.securesms.database.model.ThreadRecord
|
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 {
|
||||||
@ -24,23 +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) {
|
||||||
|
if (field == value) return
|
||||||
|
val hadHeader = hasHeaderView()
|
||||||
|
field = value
|
||||||
|
if (value != null) {
|
||||||
|
if (hadHeader) notifyItemChanged(0) else notifyItemInserted(0)
|
||||||
|
} else if (hadHeader) notifyItemRemoved(0)
|
||||||
|
}
|
||||||
|
|
||||||
private var _data: List<ThreadRecord> = emptyList()
|
var data: HomeViewModel.Data = HomeViewModel.Data()
|
||||||
var data: List<ThreadRecord>
|
|
||||||
get() = _data.toList()
|
|
||||||
set(newData) {
|
set(newData) {
|
||||||
val previousData = _data.toList()
|
if (field === newData) return
|
||||||
val diff = HomeDiffUtil(previousData, newData, context, configFactory)
|
|
||||||
|
messageRequests = newData.messageRequests
|
||||||
|
|
||||||
|
val diff = HomeDiffUtil(field, newData, context, configFactory)
|
||||||
val diffResult = DiffUtil.calculateDiff(diff)
|
val diffResult = DiffUtil.calculateDiff(diff)
|
||||||
_data = 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)
|
||||||
@ -61,23 +74,19 @@ class HomeAdapter(
|
|||||||
override fun getItemId(position: Int): Long {
|
override fun getItemId(position: Int): Long {
|
||||||
if (hasHeaderView() && position == 0) return NO_ID
|
if (hasHeaderView() && position == 0) return NO_ID
|
||||||
val offsetPosition = if (hasHeaderView()) position-1 else position
|
val offsetPosition = if (hasHeaderView()) position-1 else position
|
||||||
return _data[offsetPosition].threadId
|
return data.threads[offsetPosition].threadId
|
||||||
}
|
}
|
||||||
|
|
||||||
lateinit var glide: GlideRequests
|
lateinit var glide: GlideRequests
|
||||||
var typingThreadIDs = setOf<Long>()
|
|
||||||
set(value) {
|
|
||||||
if (field == value) { return }
|
|
||||||
|
|
||||||
field = value
|
|
||||||
// TODO: replace this with a diffed update or a partial change set with payloads
|
|
||||||
notifyDataSetChanged()
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
||||||
@ -93,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[offset]
|
val thread = data.threads[offset]
|
||||||
val isTyping = 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)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -113,10 +130,9 @@ class HomeAdapter(
|
|||||||
if (hasHeaderView() && position == 0) HEADER
|
if (hasHeaderView() && position == 0) HEADER
|
||||||
else ITEM
|
else ITEM
|
||||||
|
|
||||||
override fun getItemCount(): Int = data.size + if (hasHeaderView()) 1 else 0
|
override fun getItemCount(): Int = data.threads.size + if (hasHeaderView()) 1 else 0
|
||||||
|
|
||||||
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,27 +2,26 @@ package org.thoughtcrime.securesms.home
|
|||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import androidx.recyclerview.widget.DiffUtil
|
import androidx.recyclerview.widget.DiffUtil
|
||||||
import org.thoughtcrime.securesms.database.model.ThreadRecord
|
|
||||||
import org.thoughtcrime.securesms.dependencies.ConfigFactory
|
import org.thoughtcrime.securesms.dependencies.ConfigFactory
|
||||||
import org.thoughtcrime.securesms.util.getConversationUnread
|
import org.thoughtcrime.securesms.util.getConversationUnread
|
||||||
|
|
||||||
class HomeDiffUtil(
|
class HomeDiffUtil(
|
||||||
private val old: List<ThreadRecord>,
|
private val old: HomeViewModel.Data,
|
||||||
private val new: List<ThreadRecord>,
|
private val new: HomeViewModel.Data,
|
||||||
private val context: Context,
|
private val context: Context,
|
||||||
private val configFactory: ConfigFactory
|
private val configFactory: ConfigFactory
|
||||||
): DiffUtil.Callback() {
|
): DiffUtil.Callback() {
|
||||||
|
|
||||||
override fun getOldListSize(): Int = old.size
|
override fun getOldListSize(): Int = old.threads.size
|
||||||
|
|
||||||
override fun getNewListSize(): Int = new.size
|
override fun getNewListSize(): Int = new.threads.size
|
||||||
|
|
||||||
override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean =
|
override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean =
|
||||||
old[oldItemPosition].threadId == new[newItemPosition].threadId
|
old.threads[oldItemPosition].threadId == new.threads[newItemPosition].threadId
|
||||||
|
|
||||||
override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
|
override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
|
||||||
val oldItem = old[oldItemPosition]
|
val oldItem = old.threads[oldItemPosition]
|
||||||
val newItem = new[newItemPosition]
|
val newItem = new.threads[newItemPosition]
|
||||||
|
|
||||||
// return early to save getDisplayBody or expensive calls
|
// return early to save getDisplayBody or expensive calls
|
||||||
var isSameItem = true
|
var isSameItem = true
|
||||||
@ -47,7 +46,8 @@ class HomeDiffUtil(
|
|||||||
oldItem.isSent == newItem.isSent &&
|
oldItem.isSent == newItem.isSent &&
|
||||||
oldItem.isPending == newItem.isPending &&
|
oldItem.isPending == newItem.isPending &&
|
||||||
oldItem.lastSeen == newItem.lastSeen &&
|
oldItem.lastSeen == newItem.lastSeen &&
|
||||||
configFactory.convoVolatile?.getConversationUnread(newItem) != true
|
configFactory.convoVolatile?.getConversationUnread(newItem) != true &&
|
||||||
|
old.typingThreadIDs.contains(oldItem.threadId) == new.typingThreadIDs.contains(newItem.threadId)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,71 +1,131 @@
|
|||||||
package org.thoughtcrime.securesms.home
|
package org.thoughtcrime.securesms.home
|
||||||
|
|
||||||
|
import android.content.ContentResolver
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import androidx.lifecycle.LiveData
|
import android.util.Log
|
||||||
import androidx.lifecycle.MutableLiveData
|
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
|
import androidx.lifecycle.asFlow
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import app.cash.copper.flow.observeQuery
|
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
import kotlinx.coroutines.*
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.channels.Channel
|
import kotlinx.coroutines.FlowPreview
|
||||||
import kotlinx.coroutines.flow.collect
|
import kotlinx.coroutines.channels.BufferOverflow
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||||
|
import kotlinx.coroutines.flow.SharingStarted
|
||||||
|
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.onEach
|
||||||
|
import kotlinx.coroutines.flow.onStart
|
||||||
|
import kotlinx.coroutines.flow.stateIn
|
||||||
|
import org.session.libsession.utilities.TextSecurePreferences
|
||||||
|
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 java.lang.ref.WeakReference
|
import org.thoughtcrime.securesms.util.DateUtils
|
||||||
|
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
|
||||||
|
|
||||||
@HiltViewModel
|
@HiltViewModel
|
||||||
class HomeViewModel @Inject constructor(private val threadDb: ThreadDatabase): ViewModel() {
|
class HomeViewModel @Inject constructor(
|
||||||
|
private val threadDb: ThreadDatabase,
|
||||||
private val executor = viewModelScope + SupervisorJob()
|
private val contentResolver: ContentResolver,
|
||||||
private var lastContext: WeakReference<Context>? = null
|
private val prefs: TextSecurePreferences,
|
||||||
private var updateJobs: MutableList<Job> = mutableListOf()
|
@ApplicationContextQualifier private val context: Context,
|
||||||
|
) : ViewModel() {
|
||||||
private val _conversations = MutableLiveData<List<ThreadRecord>>()
|
// SharedFlow that emits whenever the user asks us to reload the conversation
|
||||||
val conversations: LiveData<List<ThreadRecord>> = _conversations
|
private val manualReloadTrigger = MutableSharedFlow<Unit>(
|
||||||
|
extraBufferCapacity = 1,
|
||||||
private val listUpdateChannel = Channel<Unit>(capacity = Channel.CONFLATED)
|
onBufferOverflow = BufferOverflow.DROP_OLDEST
|
||||||
|
|
||||||
fun tryUpdateChannel() = listUpdateChannel.trySend(Unit)
|
|
||||||
|
|
||||||
fun getObservable(context: Context): LiveData<List<ThreadRecord>> {
|
|
||||||
// If the context has changed (eg. the activity gets recreated) then
|
|
||||||
// we need to cancel the old executors and recreate them to prevent
|
|
||||||
// the app from triggering extra updates when data changes
|
|
||||||
if (context != lastContext?.get()) {
|
|
||||||
lastContext = WeakReference(context)
|
|
||||||
updateJobs.forEach { it.cancel() }
|
|
||||||
updateJobs.clear()
|
|
||||||
|
|
||||||
updateJobs.add(
|
|
||||||
executor.launch(Dispatchers.IO) {
|
|
||||||
context.contentResolver
|
|
||||||
.observeQuery(DatabaseContentProviders.ConversationList.CONTENT_URI)
|
|
||||||
.onEach { listUpdateChannel.trySend(Unit) }
|
|
||||||
.collect()
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
updateJobs.add(
|
|
||||||
executor.launch(Dispatchers.IO) {
|
/**
|
||||||
for (update in listUpdateChannel) {
|
* A [StateFlow] that emits the list of threads and the typing status of each thread.
|
||||||
|
*
|
||||||
|
* This flow will emit whenever the user asks us to reload the conversation list or
|
||||||
|
* whenever the conversation list changes.
|
||||||
|
*/
|
||||||
|
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
|
||||||
|
.typingThreads
|
||||||
|
.asFlow()
|
||||||
|
.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>> = reloadTriggersAndContentChanges()
|
||||||
|
.mapLatest { _ ->
|
||||||
threadDb.approvedConversationList.use { openCursor ->
|
threadDb.approvedConversationList.use { openCursor ->
|
||||||
val reader = threadDb.readerFor(openCursor)
|
threadDb.readerFor(openCursor).run { generateSequence { next }.toList() }
|
||||||
val threads = mutableListOf<ThreadRecord>()
|
|
||||||
while (true) {
|
|
||||||
threads += reader.next ?: break
|
|
||||||
}
|
}
|
||||||
withContext(Dispatchers.Main) {
|
|
||||||
_conversations.value = threads
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
return conversations
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@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> = 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
|
||||||
|
}
|
||||||
}
|
}
|
@ -6,7 +6,6 @@ import android.content.Intent
|
|||||||
import android.content.IntentFilter
|
import android.content.IntentFilter
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.os.Handler
|
|
||||||
import android.util.AttributeSet
|
import android.util.AttributeSet
|
||||||
import android.util.TypedValue
|
import android.util.TypedValue
|
||||||
import android.view.Gravity
|
import android.view.Gravity
|
||||||
@ -17,11 +16,17 @@ import android.widget.TextView
|
|||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.annotation.ColorRes
|
import androidx.annotation.ColorRes
|
||||||
import androidx.localbroadcastmanager.content.LocalBroadcastManager
|
import androidx.localbroadcastmanager.content.LocalBroadcastManager
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.GlobalScope
|
||||||
|
import kotlinx.coroutines.Job
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
|
import kotlinx.coroutines.isActive
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
import network.loki.messenger.R
|
import network.loki.messenger.R
|
||||||
import network.loki.messenger.databinding.ActivityPathBinding
|
import network.loki.messenger.databinding.ActivityPathBinding
|
||||||
import org.session.libsession.snode.OnionRequestAPI
|
import org.session.libsession.snode.OnionRequestAPI
|
||||||
import org.session.libsession.utilities.getColorFromAttr
|
import org.session.libsession.utilities.getColorFromAttr
|
||||||
import org.session.libsignal.utilities.Log
|
|
||||||
import org.session.libsignal.utilities.Snode
|
import org.session.libsignal.utilities.Snode
|
||||||
import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity
|
import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity
|
||||||
import org.thoughtcrime.securesms.util.GlowViewUtilities
|
import org.thoughtcrime.securesms.util.GlowViewUtilities
|
||||||
@ -184,6 +189,7 @@ class PathActivity : PassphraseRequiredActionBarActivity() {
|
|||||||
private lateinit var location: Location
|
private lateinit var location: Location
|
||||||
private var dotAnimationStartDelay: Long = 0
|
private var dotAnimationStartDelay: Long = 0
|
||||||
private var dotAnimationRepeatInterval: Long = 0
|
private var dotAnimationRepeatInterval: Long = 0
|
||||||
|
private var job: Job? = null
|
||||||
|
|
||||||
private val dotView by lazy {
|
private val dotView by lazy {
|
||||||
val result = PathDotView(context)
|
val result = PathDotView(context)
|
||||||
@ -240,19 +246,38 @@ class PathActivity : PassphraseRequiredActionBarActivity() {
|
|||||||
dotViewLayoutParams.addRule(CENTER_IN_PARENT)
|
dotViewLayoutParams.addRule(CENTER_IN_PARENT)
|
||||||
dotView.layoutParams = dotViewLayoutParams
|
dotView.layoutParams = dotViewLayoutParams
|
||||||
addView(dotView)
|
addView(dotView)
|
||||||
Handler().postDelayed({
|
|
||||||
performAnimation()
|
|
||||||
}, dotAnimationStartDelay)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun performAnimation() {
|
override fun onAttachedToWindow() {
|
||||||
|
super.onAttachedToWindow()
|
||||||
|
|
||||||
|
startAnimation()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDetachedFromWindow() {
|
||||||
|
super.onDetachedFromWindow()
|
||||||
|
|
||||||
|
stopAnimation()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun startAnimation() {
|
||||||
|
job?.cancel()
|
||||||
|
job = GlobalScope.launch {
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
while (isActive) {
|
||||||
|
delay(dotAnimationStartDelay)
|
||||||
expand()
|
expand()
|
||||||
Handler().postDelayed({
|
delay(EXPAND_ANIM_DELAY_MILLS)
|
||||||
collapse()
|
collapse()
|
||||||
Handler().postDelayed({
|
delay(dotAnimationRepeatInterval)
|
||||||
performAnimation()
|
}
|
||||||
}, dotAnimationRepeatInterval)
|
}
|
||||||
}, 1000)
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun stopAnimation() {
|
||||||
|
job?.cancel()
|
||||||
|
job = null
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun expand() {
|
private fun expand() {
|
||||||
@ -270,6 +295,10 @@ class PathActivity : PassphraseRequiredActionBarActivity() {
|
|||||||
val endColor = context.resources.getColorWithID(endColorID, context.theme)
|
val endColor = context.resources.getColorWithID(endColorID, context.theme)
|
||||||
GlowViewUtilities.animateShadowColorChange(dotView, startColor, endColor)
|
GlowViewUtilities.animateShadowColorChange(dotView, startColor, endColor)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val EXPAND_ANIM_DELAY_MILLS = 1000L
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// endregion
|
// endregion
|
||||||
}
|
}
|
@ -73,7 +73,7 @@ public class SignalGlideModule extends AppGlideModule {
|
|||||||
registry.append(DecryptableUri.class, InputStream.class, new DecryptableStreamUriLoader.Factory(context));
|
registry.append(DecryptableUri.class, InputStream.class, new DecryptableStreamUriLoader.Factory(context));
|
||||||
registry.append(AttachmentModel.class, InputStream.class, new AttachmentStreamUriLoader.Factory());
|
registry.append(AttachmentModel.class, InputStream.class, new AttachmentStreamUriLoader.Factory());
|
||||||
registry.append(ChunkedImageUrl.class, InputStream.class, new ChunkedImageUrlLoader.Factory());
|
registry.append(ChunkedImageUrl.class, InputStream.class, new ChunkedImageUrlLoader.Factory());
|
||||||
registry.append(PlaceholderAvatarPhoto.class, BitmapDrawable.class, new PlaceholderAvatarLoader.Factory());
|
registry.append(PlaceholderAvatarPhoto.class, BitmapDrawable.class, new PlaceholderAvatarLoader.Factory(context));
|
||||||
registry.replace(GlideUrl.class, InputStream.class, new OkHttpUrlLoader.Factory());
|
registry.replace(GlideUrl.class, InputStream.class, new OkHttpUrlLoader.Factory());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -17,6 +17,7 @@ import org.session.libsession.messaging.jobs.MessageReceiveParameters
|
|||||||
import org.session.libsession.messaging.sending_receiving.notifications.PushNotificationMetadata
|
import org.session.libsession.messaging.sending_receiving.notifications.PushNotificationMetadata
|
||||||
import org.session.libsession.messaging.utilities.MessageWrapper
|
import org.session.libsession.messaging.utilities.MessageWrapper
|
||||||
import org.session.libsession.messaging.utilities.SodiumUtilities
|
import org.session.libsession.messaging.utilities.SodiumUtilities
|
||||||
|
import org.session.libsession.messaging.utilities.SodiumUtilities.sodium
|
||||||
import org.session.libsession.utilities.bencode.Bencode
|
import org.session.libsession.utilities.bencode.Bencode
|
||||||
import org.session.libsession.utilities.bencode.BencodeList
|
import org.session.libsession.utilities.bencode.BencodeList
|
||||||
import org.session.libsession.utilities.bencode.BencodeString
|
import org.session.libsession.utilities.bencode.BencodeString
|
||||||
@ -28,7 +29,6 @@ import javax.inject.Inject
|
|||||||
private const val TAG = "PushHandler"
|
private const val TAG = "PushHandler"
|
||||||
|
|
||||||
class PushReceiver @Inject constructor(@ApplicationContext val context: Context) {
|
class PushReceiver @Inject constructor(@ApplicationContext val context: Context) {
|
||||||
private val sodium = LazySodiumAndroid(SodiumAndroid())
|
|
||||||
private val json = Json { ignoreUnknownKeys = true }
|
private val json = Json { ignoreUnknownKeys = true }
|
||||||
|
|
||||||
fun onPush(dataMap: Map<String, String>?) {
|
fun onPush(dataMap: Map<String, String>?) {
|
||||||
|
@ -18,6 +18,8 @@ import org.session.libsession.messaging.sending_receiving.notifications.Subscrip
|
|||||||
import org.session.libsession.messaging.sending_receiving.notifications.SubscriptionResponse
|
import org.session.libsession.messaging.sending_receiving.notifications.SubscriptionResponse
|
||||||
import org.session.libsession.messaging.sending_receiving.notifications.UnsubscribeResponse
|
import org.session.libsession.messaging.sending_receiving.notifications.UnsubscribeResponse
|
||||||
import org.session.libsession.messaging.sending_receiving.notifications.UnsubscriptionRequest
|
import org.session.libsession.messaging.sending_receiving.notifications.UnsubscriptionRequest
|
||||||
|
import org.session.libsession.messaging.utilities.SodiumUtilities
|
||||||
|
import org.session.libsession.messaging.utilities.SodiumUtilities.sodium
|
||||||
import org.session.libsession.snode.OnionRequestAPI
|
import org.session.libsession.snode.OnionRequestAPI
|
||||||
import org.session.libsession.snode.SnodeAPI
|
import org.session.libsession.snode.SnodeAPI
|
||||||
import org.session.libsession.snode.Version
|
import org.session.libsession.snode.Version
|
||||||
@ -34,8 +36,6 @@ private const val maxRetryCount = 4
|
|||||||
|
|
||||||
@Singleton
|
@Singleton
|
||||||
class PushRegistryV2 @Inject constructor(private val pushReceiver: PushReceiver) {
|
class PushRegistryV2 @Inject constructor(private val pushReceiver: PushReceiver) {
|
||||||
private val sodium = LazySodiumAndroid(SodiumAndroid())
|
|
||||||
|
|
||||||
fun register(
|
fun register(
|
||||||
device: Device,
|
device: Device,
|
||||||
token: String,
|
token: String,
|
||||||
|
@ -0,0 +1,31 @@
|
|||||||
|
package org.thoughtcrime.securesms.util
|
||||||
|
|
||||||
|
import android.content.ContentResolver
|
||||||
|
import android.database.ContentObserver
|
||||||
|
import android.net.Uri
|
||||||
|
import android.os.Handler
|
||||||
|
import android.os.Looper
|
||||||
|
import androidx.annotation.CheckResult
|
||||||
|
import kotlinx.coroutines.channels.awaitClose
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.callbackFlow
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Observe changes to a content Uri. This function will emit the Uri whenever the content or
|
||||||
|
* its descendants change, according to the parameter [notifyForDescendants].
|
||||||
|
*/
|
||||||
|
@CheckResult
|
||||||
|
fun ContentResolver.observeChanges(uri: Uri, notifyForDescendants: Boolean = false): Flow<Uri> {
|
||||||
|
return callbackFlow {
|
||||||
|
val observer = object : ContentObserver(Handler(Looper.getMainLooper())) {
|
||||||
|
override fun onChange(selfChange: Boolean) {
|
||||||
|
trySend(uri)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
registerContentObserver(uri, notifyForDescendants, observer)
|
||||||
|
awaitClose {
|
||||||
|
unregisterContentObserver(observer)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -55,7 +55,7 @@ class IP2Country private constructor(private val context: Context) {
|
|||||||
|
|
||||||
public fun configureIfNeeded(context: Context) {
|
public fun configureIfNeeded(context: Context) {
|
||||||
if (isInitialized) { return; }
|
if (isInitialized) { return; }
|
||||||
shared = IP2Country(context)
|
shared = IP2Country(context.applicationContext)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
#include "config_base.h"
|
#include "config_base.h"
|
||||||
#include "util.h"
|
#include "util.h"
|
||||||
|
#include "jni_utils.h"
|
||||||
|
|
||||||
extern "C" {
|
extern "C" {
|
||||||
JNIEXPORT jboolean JNICALL
|
JNIEXPORT jboolean JNICALL
|
||||||
@ -85,10 +86,11 @@ Java_network_loki_messenger_libsession_1util_ConfigBase_confirmPushed(JNIEnv *en
|
|||||||
JNIEXPORT jobject JNICALL
|
JNIEXPORT jobject JNICALL
|
||||||
Java_network_loki_messenger_libsession_1util_ConfigBase_merge___3Lkotlin_Pair_2(JNIEnv *env, jobject thiz,
|
Java_network_loki_messenger_libsession_1util_ConfigBase_merge___3Lkotlin_Pair_2(JNIEnv *env, jobject thiz,
|
||||||
jobjectArray to_merge) {
|
jobjectArray to_merge) {
|
||||||
|
return jni_utils::run_catching_cxx_exception_or_throws<jobject>(env, [=] {
|
||||||
std::lock_guard lock{util::util_mutex_};
|
std::lock_guard lock{util::util_mutex_};
|
||||||
auto conf = ptrToConfigBase(env, thiz);
|
auto conf = ptrToConfigBase(env, thiz);
|
||||||
size_t number = env->GetArrayLength(to_merge);
|
size_t number = env->GetArrayLength(to_merge);
|
||||||
std::vector<std::pair<std::string,session::ustring>> configs = {};
|
std::vector<std::pair<std::string, session::ustring>> configs = {};
|
||||||
for (int i = 0; i < number; i++) {
|
for (int i = 0; i < number; i++) {
|
||||||
auto jElement = (jobject) env->GetObjectArrayElement(to_merge, i);
|
auto jElement = (jobject) env->GetObjectArrayElement(to_merge, i);
|
||||||
auto pair = extractHashAndData(env, jElement);
|
auto pair = extractHashAndData(env, jElement);
|
||||||
@ -97,17 +99,21 @@ Java_network_loki_messenger_libsession_1util_ConfigBase_merge___3Lkotlin_Pair_2(
|
|||||||
auto returned = conf->merge(configs);
|
auto returned = conf->merge(configs);
|
||||||
auto string_stack = util::build_string_stack(env, returned);
|
auto string_stack = util::build_string_stack(env, returned);
|
||||||
return string_stack;
|
return string_stack;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
JNIEXPORT jobject JNICALL
|
JNIEXPORT jobject JNICALL
|
||||||
Java_network_loki_messenger_libsession_1util_ConfigBase_merge__Lkotlin_Pair_2(JNIEnv *env, jobject thiz,
|
Java_network_loki_messenger_libsession_1util_ConfigBase_merge__Lkotlin_Pair_2(JNIEnv *env, jobject thiz,
|
||||||
jobject to_merge) {
|
jobject to_merge) {
|
||||||
|
return jni_utils::run_catching_cxx_exception_or_throws<jobject>(env, [=] {
|
||||||
std::lock_guard lock{util::util_mutex_};
|
std::lock_guard lock{util::util_mutex_};
|
||||||
auto conf = ptrToConfigBase(env, thiz);
|
auto conf = ptrToConfigBase(env, thiz);
|
||||||
std::vector<std::pair<std::string, session::ustring>> configs = {extractHashAndData(env, to_merge)};
|
std::vector<std::pair<std::string, session::ustring>> configs = {
|
||||||
|
extractHashAndData(env, to_merge)};
|
||||||
auto returned = conf->merge(configs);
|
auto returned = conf->merge(configs);
|
||||||
auto string_stack = util::build_string_stack(env, returned);
|
auto string_stack = util::build_string_stack(env, returned);
|
||||||
return string_stack;
|
return string_stack;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
#pragma clang diagnostic pop
|
#pragma clang diagnostic pop
|
||||||
|
@ -1,10 +1,14 @@
|
|||||||
#include "contacts.h"
|
#include "contacts.h"
|
||||||
#include "util.h"
|
#include "util.h"
|
||||||
|
#include "jni_utils.h"
|
||||||
|
|
||||||
extern "C"
|
extern "C"
|
||||||
JNIEXPORT jobject JNICALL
|
JNIEXPORT jobject JNICALL
|
||||||
Java_network_loki_messenger_libsession_1util_Contacts_get(JNIEnv *env, jobject thiz,
|
Java_network_loki_messenger_libsession_1util_Contacts_get(JNIEnv *env, jobject thiz,
|
||||||
jstring session_id) {
|
jstring session_id) {
|
||||||
|
// If an exception is thrown, return nullptr
|
||||||
|
return jni_utils::run_catching_cxx_exception_or<jobject>(
|
||||||
|
[=]() -> jobject {
|
||||||
std::lock_guard lock{util::util_mutex_};
|
std::lock_guard lock{util::util_mutex_};
|
||||||
auto contacts = ptrToContacts(env, thiz);
|
auto contacts = ptrToContacts(env, thiz);
|
||||||
auto session_id_chars = env->GetStringUTFChars(session_id, nullptr);
|
auto session_id_chars = env->GetStringUTFChars(session_id, nullptr);
|
||||||
@ -13,34 +17,42 @@ Java_network_loki_messenger_libsession_1util_Contacts_get(JNIEnv *env, jobject t
|
|||||||
if (!contact) return nullptr;
|
if (!contact) return nullptr;
|
||||||
jobject j_contact = serialize_contact(env, contact.value());
|
jobject j_contact = serialize_contact(env, contact.value());
|
||||||
return j_contact;
|
return j_contact;
|
||||||
|
},
|
||||||
|
[](const char *) -> jobject { return nullptr; }
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
extern "C"
|
extern "C"
|
||||||
JNIEXPORT jobject JNICALL
|
JNIEXPORT jobject JNICALL
|
||||||
Java_network_loki_messenger_libsession_1util_Contacts_getOrConstruct(JNIEnv *env, jobject thiz,
|
Java_network_loki_messenger_libsession_1util_Contacts_getOrConstruct(JNIEnv *env, jobject thiz,
|
||||||
jstring session_id) {
|
jstring session_id) {
|
||||||
|
return jni_utils::run_catching_cxx_exception_or_throws<jobject>(env, [=] {
|
||||||
std::lock_guard lock{util::util_mutex_};
|
std::lock_guard lock{util::util_mutex_};
|
||||||
auto contacts = ptrToContacts(env, thiz);
|
auto contacts = ptrToContacts(env, thiz);
|
||||||
auto session_id_chars = env->GetStringUTFChars(session_id, nullptr);
|
auto session_id_chars = env->GetStringUTFChars(session_id, nullptr);
|
||||||
auto contact = contacts->get_or_construct(session_id_chars);
|
auto contact = contacts->get_or_construct(session_id_chars);
|
||||||
env->ReleaseStringUTFChars(session_id, session_id_chars);
|
env->ReleaseStringUTFChars(session_id, session_id_chars);
|
||||||
return serialize_contact(env, contact);
|
return serialize_contact(env, contact);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
extern "C"
|
extern "C"
|
||||||
JNIEXPORT void JNICALL
|
JNIEXPORT void JNICALL
|
||||||
Java_network_loki_messenger_libsession_1util_Contacts_set(JNIEnv *env, jobject thiz,
|
Java_network_loki_messenger_libsession_1util_Contacts_set(JNIEnv *env, jobject thiz,
|
||||||
jobject contact) {
|
jobject contact) {
|
||||||
|
jni_utils::run_catching_cxx_exception_or_throws<void>(env, [=] {
|
||||||
std::lock_guard lock{util::util_mutex_};
|
std::lock_guard lock{util::util_mutex_};
|
||||||
auto contacts = ptrToContacts(env, thiz);
|
auto contacts = ptrToContacts(env, thiz);
|
||||||
auto contact_info = deserialize_contact(env, contact, contacts);
|
auto contact_info = deserialize_contact(env, contact, contacts);
|
||||||
contacts->set(contact_info);
|
contacts->set(contact_info);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
extern "C"
|
extern "C"
|
||||||
JNIEXPORT jboolean JNICALL
|
JNIEXPORT jboolean JNICALL
|
||||||
Java_network_loki_messenger_libsession_1util_Contacts_erase(JNIEnv *env, jobject thiz,
|
Java_network_loki_messenger_libsession_1util_Contacts_erase(JNIEnv *env, jobject thiz,
|
||||||
jstring session_id) {
|
jstring session_id) {
|
||||||
|
return jni_utils::run_catching_cxx_exception_or_throws<jboolean>(env, [=] {
|
||||||
std::lock_guard lock{util::util_mutex_};
|
std::lock_guard lock{util::util_mutex_};
|
||||||
auto contacts = ptrToContacts(env, thiz);
|
auto contacts = ptrToContacts(env, thiz);
|
||||||
auto session_id_chars = env->GetStringUTFChars(session_id, nullptr);
|
auto session_id_chars = env->GetStringUTFChars(session_id, nullptr);
|
||||||
@ -48,6 +60,7 @@ Java_network_loki_messenger_libsession_1util_Contacts_erase(JNIEnv *env, jobject
|
|||||||
bool result = contacts->erase(session_id_chars);
|
bool result = contacts->erase(session_id_chars);
|
||||||
env->ReleaseStringUTFChars(session_id, session_id_chars);
|
env->ReleaseStringUTFChars(session_id, session_id_chars);
|
||||||
return result;
|
return result;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
extern "C"
|
extern "C"
|
||||||
#pragma clang diagnostic push
|
#pragma clang diagnostic push
|
||||||
@ -56,45 +69,53 @@ JNIEXPORT jobject JNICALL
|
|||||||
Java_network_loki_messenger_libsession_1util_Contacts_00024Companion_newInstance___3B(JNIEnv *env,
|
Java_network_loki_messenger_libsession_1util_Contacts_00024Companion_newInstance___3B(JNIEnv *env,
|
||||||
jobject thiz,
|
jobject thiz,
|
||||||
jbyteArray ed25519_secret_key) {
|
jbyteArray ed25519_secret_key) {
|
||||||
|
return jni_utils::run_catching_cxx_exception_or_throws<jobject>(env, [=] {
|
||||||
std::lock_guard lock{util::util_mutex_};
|
std::lock_guard lock{util::util_mutex_};
|
||||||
auto secret_key = util::ustring_from_bytes(env, ed25519_secret_key);
|
auto secret_key = util::ustring_from_bytes(env, ed25519_secret_key);
|
||||||
auto* contacts = new session::config::Contacts(secret_key, std::nullopt);
|
auto *contacts = new session::config::Contacts(secret_key, std::nullopt);
|
||||||
|
|
||||||
jclass contactsClass = env->FindClass("network/loki/messenger/libsession_util/Contacts");
|
jclass contactsClass = env->FindClass("network/loki/messenger/libsession_util/Contacts");
|
||||||
jmethodID constructor = env->GetMethodID(contactsClass, "<init>", "(J)V");
|
jmethodID constructor = env->GetMethodID(contactsClass, "<init>", "(J)V");
|
||||||
jobject newConfig = env->NewObject(contactsClass, constructor, reinterpret_cast<jlong>(contacts));
|
jobject newConfig = env->NewObject(contactsClass, constructor,
|
||||||
|
reinterpret_cast<jlong>(contacts));
|
||||||
|
|
||||||
return newConfig;
|
return newConfig;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
extern "C"
|
extern "C"
|
||||||
JNIEXPORT jobject JNICALL
|
JNIEXPORT jobject JNICALL
|
||||||
Java_network_loki_messenger_libsession_1util_Contacts_00024Companion_newInstance___3B_3B(
|
Java_network_loki_messenger_libsession_1util_Contacts_00024Companion_newInstance___3B_3B(
|
||||||
JNIEnv *env, jobject thiz, jbyteArray ed25519_secret_key, jbyteArray initial_dump) {
|
JNIEnv *env, jobject thiz, jbyteArray ed25519_secret_key, jbyteArray initial_dump) {
|
||||||
|
return jni_utils::run_catching_cxx_exception_or_throws<jobject>(env, [=] {
|
||||||
std::lock_guard lock{util::util_mutex_};
|
std::lock_guard lock{util::util_mutex_};
|
||||||
auto secret_key = util::ustring_from_bytes(env, ed25519_secret_key);
|
auto secret_key = util::ustring_from_bytes(env, ed25519_secret_key);
|
||||||
auto initial = util::ustring_from_bytes(env, initial_dump);
|
auto initial = util::ustring_from_bytes(env, initial_dump);
|
||||||
|
|
||||||
auto* contacts = new session::config::Contacts(secret_key, initial);
|
auto *contacts = new session::config::Contacts(secret_key, initial);
|
||||||
|
|
||||||
jclass contactsClass = env->FindClass("network/loki/messenger/libsession_util/Contacts");
|
jclass contactsClass = env->FindClass("network/loki/messenger/libsession_util/Contacts");
|
||||||
jmethodID constructor = env->GetMethodID(contactsClass, "<init>", "(J)V");
|
jmethodID constructor = env->GetMethodID(contactsClass, "<init>", "(J)V");
|
||||||
jobject newConfig = env->NewObject(contactsClass, constructor, reinterpret_cast<jlong>(contacts));
|
jobject newConfig = env->NewObject(contactsClass, constructor,
|
||||||
|
reinterpret_cast<jlong>(contacts));
|
||||||
|
|
||||||
return newConfig;
|
return newConfig;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
#pragma clang diagnostic pop
|
#pragma clang diagnostic pop
|
||||||
extern "C"
|
extern "C"
|
||||||
JNIEXPORT jobject JNICALL
|
JNIEXPORT jobject JNICALL
|
||||||
Java_network_loki_messenger_libsession_1util_Contacts_all(JNIEnv *env, jobject thiz) {
|
Java_network_loki_messenger_libsession_1util_Contacts_all(JNIEnv *env, jobject thiz) {
|
||||||
|
return jni_utils::run_catching_cxx_exception_or_throws<jobject>(env, [=] {
|
||||||
std::lock_guard lock{util::util_mutex_};
|
std::lock_guard lock{util::util_mutex_};
|
||||||
auto contacts = ptrToContacts(env, thiz);
|
auto contacts = ptrToContacts(env, thiz);
|
||||||
jclass stack = env->FindClass("java/util/Stack");
|
jclass stack = env->FindClass("java/util/Stack");
|
||||||
jmethodID init = env->GetMethodID(stack, "<init>", "()V");
|
jmethodID init = env->GetMethodID(stack, "<init>", "()V");
|
||||||
jobject our_stack = env->NewObject(stack, init);
|
jobject our_stack = env->NewObject(stack, init);
|
||||||
jmethodID push = env->GetMethodID(stack, "push", "(Ljava/lang/Object;)Ljava/lang/Object;");
|
jmethodID push = env->GetMethodID(stack, "push", "(Ljava/lang/Object;)Ljava/lang/Object;");
|
||||||
for (const auto& contact : *contacts) {
|
for (const auto &contact: *contacts) {
|
||||||
auto contact_obj = serialize_contact(env, contact);
|
auto contact_obj = serialize_contact(env, contact);
|
||||||
env->CallObjectMethod(our_stack, push, contact_obj);
|
env->CallObjectMethod(our_stack, push, contact_obj);
|
||||||
}
|
}
|
||||||
return our_stack;
|
return our_stack;
|
||||||
|
});
|
||||||
}
|
}
|
54
libsession-util/src/main/cpp/jni_utils.h
Normal file
54
libsession-util/src/main/cpp/jni_utils.h
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
#ifndef SESSION_ANDROID_JNI_UTILS_H
|
||||||
|
#define SESSION_ANDROID_JNI_UTILS_H
|
||||||
|
|
||||||
|
#include <jni.h>
|
||||||
|
#include <exception>
|
||||||
|
|
||||||
|
namespace jni_utils {
|
||||||
|
/**
|
||||||
|
* Run a C++ function and catch any exceptions, throwing a Java exception if one is caught,
|
||||||
|
* and returning a default-constructed value of the specified type.
|
||||||
|
*
|
||||||
|
* @tparam RetT The return type of the function
|
||||||
|
* @tparam Func The function type
|
||||||
|
* @param f The function to run
|
||||||
|
* @param fallbackRun The function to run if an exception is caught. The optional exception message reference will be passed to this function.
|
||||||
|
* @return The return value of the function, or the return value of the fallback function if an exception was caught
|
||||||
|
*/
|
||||||
|
template<class RetT, class Func, class FallbackRun>
|
||||||
|
RetT run_catching_cxx_exception_or(Func f, FallbackRun fallbackRun) {
|
||||||
|
try {
|
||||||
|
return f();
|
||||||
|
} catch (const std::exception &e) {
|
||||||
|
return fallbackRun(e.what());
|
||||||
|
} catch (...) {
|
||||||
|
return fallbackRun(nullptr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Run a C++ function and catch any exceptions, throwing a Java exception if one is caught.
|
||||||
|
*
|
||||||
|
* @tparam RetT The return type of the function
|
||||||
|
* @tparam Func The function type
|
||||||
|
* @param env The JNI environment
|
||||||
|
* @param f The function to run
|
||||||
|
* @return The return value of the function, or a default-constructed value of the specified type if an exception was caught
|
||||||
|
*/
|
||||||
|
template<class RetT, class Func>
|
||||||
|
RetT run_catching_cxx_exception_or_throws(JNIEnv *env, Func f) {
|
||||||
|
return run_catching_cxx_exception_or<RetT>(f, [env](const char *msg) {
|
||||||
|
jclass exceptionClass = env->FindClass("java/lang/RuntimeException");
|
||||||
|
if (msg) {
|
||||||
|
auto formatted_message = std::string("libsession: C++ exception: ") + msg;
|
||||||
|
env->ThrowNew(exceptionClass, formatted_message.c_str());
|
||||||
|
} else {
|
||||||
|
env->ThrowNew(exceptionClass, "libsession: Unknown C++ exception");
|
||||||
|
}
|
||||||
|
|
||||||
|
return RetT();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif //SESSION_ANDROID_JNI_UTILS_H
|
@ -1,13 +1,10 @@
|
|||||||
package org.session.libsession.avatars
|
package org.session.libsession.avatars
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import com.bumptech.glide.load.Key
|
import com.bumptech.glide.load.Key
|
||||||
import java.security.MessageDigest
|
import java.security.MessageDigest
|
||||||
|
|
||||||
class PlaceholderAvatarPhoto(val context: Context,
|
class PlaceholderAvatarPhoto(val hashString: String,
|
||||||
val hashString: String,
|
|
||||||
val displayName: String): Key {
|
val displayName: String): Key {
|
||||||
|
|
||||||
override fun updateDiskCacheKey(messageDigest: MessageDigest) {
|
override fun updateDiskCacheKey(messageDigest: MessageDigest) {
|
||||||
messageDigest.update(hashString.encodeToByteArray())
|
messageDigest.update(hashString.encodeToByteArray())
|
||||||
messageDigest.update(displayName.encodeToByteArray())
|
messageDigest.update(displayName.encodeToByteArray())
|
||||||
|
@ -0,0 +1,9 @@
|
|||||||
|
package org.session.libsession.messaging
|
||||||
|
|
||||||
|
interface LastSentTimestampCache {
|
||||||
|
fun getTimestamp(threadId: Long): Long?
|
||||||
|
fun submitTimestamp(threadId: Long, timestamp: Long)
|
||||||
|
fun delete(threadId: Long, timestamps: List<Long>)
|
||||||
|
fun delete(threadId: Long, timestamp: Long) = delete(threadId, listOf(timestamp))
|
||||||
|
fun refresh(threadId: Long)
|
||||||
|
}
|
@ -13,7 +13,8 @@ class MessagingModuleConfiguration(
|
|||||||
val device: Device,
|
val device: Device,
|
||||||
val messageDataProvider: MessageDataProvider,
|
val messageDataProvider: MessageDataProvider,
|
||||||
val getUserED25519KeyPair: () -> KeyPair?,
|
val getUserED25519KeyPair: () -> KeyPair?,
|
||||||
val configFactory: ConfigFactoryProtocol
|
val configFactory: ConfigFactoryProtocol,
|
||||||
|
val lastSentTimestampCache: LastSentTimestampCache
|
||||||
) {
|
) {
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
@ -21,6 +21,7 @@ import org.session.libsession.messaging.MessagingModuleConfiguration
|
|||||||
import org.session.libsession.messaging.sending_receiving.pollers.OpenGroupPoller.Companion.maxInactivityPeriod
|
import org.session.libsession.messaging.sending_receiving.pollers.OpenGroupPoller.Companion.maxInactivityPeriod
|
||||||
import org.session.libsession.messaging.utilities.SessionId
|
import org.session.libsession.messaging.utilities.SessionId
|
||||||
import org.session.libsession.messaging.utilities.SodiumUtilities
|
import org.session.libsession.messaging.utilities.SodiumUtilities
|
||||||
|
import org.session.libsession.messaging.utilities.SodiumUtilities.sodium
|
||||||
import org.session.libsession.snode.OnionRequestAPI
|
import org.session.libsession.snode.OnionRequestAPI
|
||||||
import org.session.libsession.snode.OnionResponse
|
import org.session.libsession.snode.OnionResponse
|
||||||
import org.session.libsession.snode.SnodeAPI
|
import org.session.libsession.snode.SnodeAPI
|
||||||
@ -48,7 +49,6 @@ object OpenGroupApi {
|
|||||||
val defaultRooms = MutableSharedFlow<List<DefaultGroup>>(replay = 1)
|
val defaultRooms = MutableSharedFlow<List<DefaultGroup>>(replay = 1)
|
||||||
private val hasPerformedInitialPoll = mutableMapOf<String, Boolean>()
|
private val hasPerformedInitialPoll = mutableMapOf<String, Boolean>()
|
||||||
private var hasUpdatedLastOpenDate = false
|
private var hasUpdatedLastOpenDate = false
|
||||||
private val sodium by lazy { LazySodiumAndroid(SodiumAndroid()) }
|
|
||||||
private val timeSinceLastOpen by lazy {
|
private val timeSinceLastOpen by lazy {
|
||||||
val context = MessagingModuleConfiguration.shared.context
|
val context = MessagingModuleConfiguration.shared.context
|
||||||
val lastOpenDate = TextSecurePreferences.getLastOpenTimeDate(context)
|
val lastOpenDate = TextSecurePreferences.getLastOpenTimeDate(context)
|
||||||
|
@ -8,6 +8,7 @@ import org.session.libsession.messaging.MessagingModuleConfiguration
|
|||||||
import org.session.libsession.messaging.sending_receiving.MessageReceiver.Error
|
import org.session.libsession.messaging.sending_receiving.MessageReceiver.Error
|
||||||
import org.session.libsession.messaging.utilities.SessionId
|
import org.session.libsession.messaging.utilities.SessionId
|
||||||
import org.session.libsession.messaging.utilities.SodiumUtilities
|
import org.session.libsession.messaging.utilities.SodiumUtilities
|
||||||
|
import org.session.libsession.messaging.utilities.SodiumUtilities.sodium
|
||||||
import org.session.libsignal.crypto.ecc.ECKeyPair
|
import org.session.libsignal.crypto.ecc.ECKeyPair
|
||||||
import org.session.libsignal.utilities.Hex
|
import org.session.libsignal.utilities.Hex
|
||||||
import org.session.libsignal.utilities.IdPrefix
|
import org.session.libsignal.utilities.IdPrefix
|
||||||
@ -17,8 +18,6 @@ import org.session.libsignal.utilities.removingIdPrefixIfNeeded
|
|||||||
|
|
||||||
object MessageDecrypter {
|
object MessageDecrypter {
|
||||||
|
|
||||||
private val sodium by lazy { LazySodiumAndroid(SodiumAndroid()) }
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Decrypts `ciphertext` using the Session protocol and `x25519KeyPair`.
|
* Decrypts `ciphertext` using the Session protocol and `x25519KeyPair`.
|
||||||
*
|
*
|
||||||
|
@ -7,6 +7,7 @@ import com.goterl.lazysodium.interfaces.Sign
|
|||||||
import org.session.libsession.messaging.MessagingModuleConfiguration
|
import org.session.libsession.messaging.MessagingModuleConfiguration
|
||||||
import org.session.libsession.messaging.sending_receiving.MessageSender.Error
|
import org.session.libsession.messaging.sending_receiving.MessageSender.Error
|
||||||
import org.session.libsession.messaging.utilities.SodiumUtilities
|
import org.session.libsession.messaging.utilities.SodiumUtilities
|
||||||
|
import org.session.libsession.messaging.utilities.SodiumUtilities.sodium
|
||||||
import org.session.libsignal.utilities.Hex
|
import org.session.libsignal.utilities.Hex
|
||||||
import org.session.libsignal.utilities.IdPrefix
|
import org.session.libsignal.utilities.IdPrefix
|
||||||
import org.session.libsignal.utilities.Log
|
import org.session.libsignal.utilities.Log
|
||||||
@ -14,8 +15,6 @@ import org.session.libsignal.utilities.removingIdPrefixIfNeeded
|
|||||||
|
|
||||||
object MessageEncrypter {
|
object MessageEncrypter {
|
||||||
|
|
||||||
private val sodium by lazy { LazySodiumAndroid(SodiumAndroid()) }
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Encrypts `plaintext` using the Session protocol for `hexEncodedX25519PublicKey`.
|
* Encrypts `plaintext` using the Session protocol for `hexEncodedX25519PublicKey`.
|
||||||
*
|
*
|
||||||
|
@ -73,6 +73,7 @@ object MessageSender {
|
|||||||
|
|
||||||
// Convenience
|
// Convenience
|
||||||
fun send(message: Message, destination: Destination, isSyncMessage: Boolean): Promise<Unit, Exception> {
|
fun send(message: Message, destination: Destination, isSyncMessage: Boolean): Promise<Unit, Exception> {
|
||||||
|
if (message is VisibleMessage) MessagingModuleConfiguration.shared.lastSentTimestampCache.submitTimestamp(message.threadID!!, message.sentTimestamp!!)
|
||||||
return if (destination is Destination.LegacyOpenGroup || destination is Destination.OpenGroup || destination is Destination.OpenGroupInbox) {
|
return if (destination is Destination.LegacyOpenGroup || destination is Destination.OpenGroup || destination is Destination.OpenGroupInbox) {
|
||||||
sendToOpenGroupDestination(destination, message)
|
sendToOpenGroupDestination(destination, message)
|
||||||
} else {
|
} else {
|
||||||
@ -372,6 +373,7 @@ object MessageSender {
|
|||||||
|
|
||||||
// Result Handling
|
// Result Handling
|
||||||
private fun handleSuccessfulMessageSend(message: Message, destination: Destination, isSyncMessage: Boolean = false, openGroupSentTimestamp: Long = -1) {
|
private fun handleSuccessfulMessageSend(message: Message, destination: Destination, isSyncMessage: Boolean = false, openGroupSentTimestamp: Long = -1) {
|
||||||
|
if (message is VisibleMessage) MessagingModuleConfiguration.shared.lastSentTimestampCache.submitTimestamp(message.threadID!!, openGroupSentTimestamp)
|
||||||
val storage = MessagingModuleConfiguration.shared.storage
|
val storage = MessagingModuleConfiguration.shared.storage
|
||||||
val userPublicKey = storage.getUserPublicKey()!!
|
val userPublicKey = storage.getUserPublicKey()!!
|
||||||
val timestamp = message.sentTimestamp!!
|
val timestamp = message.sentTimestamp!!
|
||||||
|
@ -290,6 +290,7 @@ fun MessageReceiver.handleVisibleMessage(
|
|||||||
): Long? {
|
): Long? {
|
||||||
val storage = MessagingModuleConfiguration.shared.storage
|
val storage = MessagingModuleConfiguration.shared.storage
|
||||||
val context = MessagingModuleConfiguration.shared.context
|
val context = MessagingModuleConfiguration.shared.context
|
||||||
|
message.takeIf { it.isSenderSelf }?.sentTimestamp?.let { MessagingModuleConfiguration.shared.lastSentTimestampCache.submitTimestamp(threadId, it) }
|
||||||
val userPublicKey = storage.getUserPublicKey()
|
val userPublicKey = storage.getUserPublicKey()
|
||||||
val messageSender: String? = message.sender
|
val messageSender: String? = message.sender
|
||||||
|
|
||||||
@ -410,12 +411,7 @@ fun MessageReceiver.handleVisibleMessage(
|
|||||||
message.hasMention = listOf(userPublicKey, userBlindedKey)
|
message.hasMention = listOf(userPublicKey, userBlindedKey)
|
||||||
.filterNotNull()
|
.filterNotNull()
|
||||||
.any { key ->
|
.any { key ->
|
||||||
return@any (
|
messageText?.contains("@$key") == true || key == (quoteModel?.author?.serialize() ?: "")
|
||||||
messageText != null &&
|
|
||||||
messageText.contains("@$key")
|
|
||||||
) || (
|
|
||||||
(quoteModel?.author?.serialize() ?: "") == key
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Persist the message
|
// Persist the message
|
||||||
|
@ -14,7 +14,7 @@ import org.whispersystems.curve25519.Curve25519
|
|||||||
import kotlin.experimental.xor
|
import kotlin.experimental.xor
|
||||||
|
|
||||||
object SodiumUtilities {
|
object SodiumUtilities {
|
||||||
private val sodium by lazy { LazySodiumAndroid(SodiumAndroid()) }
|
val sodium by lazy { LazySodiumAndroid(SodiumAndroid()) }
|
||||||
private val curve by lazy { Curve25519.getInstance(Curve25519.BEST) }
|
private val curve by lazy { Curve25519.getInstance(Curve25519.BEST) }
|
||||||
|
|
||||||
private const val SCALAR_LENGTH: Int = 32 // crypto_core_ed25519_scalarbytes
|
private const val SCALAR_LENGTH: Int = 32 // crypto_core_ed25519_scalarbytes
|
||||||
|
@ -3,8 +3,6 @@
|
|||||||
package org.session.libsession.snode
|
package org.session.libsession.snode
|
||||||
|
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import com.goterl.lazysodium.LazySodiumAndroid
|
|
||||||
import com.goterl.lazysodium.SodiumAndroid
|
|
||||||
import com.goterl.lazysodium.exceptions.SodiumException
|
import com.goterl.lazysodium.exceptions.SodiumException
|
||||||
import com.goterl.lazysodium.interfaces.GenericHash
|
import com.goterl.lazysodium.interfaces.GenericHash
|
||||||
import com.goterl.lazysodium.interfaces.PwHash
|
import com.goterl.lazysodium.interfaces.PwHash
|
||||||
@ -19,6 +17,7 @@ import nl.komponents.kovenant.functional.map
|
|||||||
import nl.komponents.kovenant.task
|
import nl.komponents.kovenant.task
|
||||||
import org.session.libsession.messaging.MessagingModuleConfiguration
|
import org.session.libsession.messaging.MessagingModuleConfiguration
|
||||||
import org.session.libsession.messaging.utilities.MessageWrapper
|
import org.session.libsession.messaging.utilities.MessageWrapper
|
||||||
|
import org.session.libsession.messaging.utilities.SodiumUtilities.sodium
|
||||||
import org.session.libsignal.crypto.getRandomElement
|
import org.session.libsignal.crypto.getRandomElement
|
||||||
import org.session.libsignal.database.LokiAPIDatabaseProtocol
|
import org.session.libsignal.database.LokiAPIDatabaseProtocol
|
||||||
import org.session.libsignal.protos.SignalServiceProtos
|
import org.session.libsignal.protos.SignalServiceProtos
|
||||||
@ -41,7 +40,6 @@ import kotlin.collections.set
|
|||||||
import kotlin.properties.Delegates.observable
|
import kotlin.properties.Delegates.observable
|
||||||
|
|
||||||
object SnodeAPI {
|
object SnodeAPI {
|
||||||
private val sodium by lazy { LazySodiumAndroid(SodiumAndroid()) }
|
|
||||||
internal val database: LokiAPIDatabaseProtocol
|
internal val database: LokiAPIDatabaseProtocol
|
||||||
get() = SnodeModule.shared.storage
|
get() = SnodeModule.shared.storage
|
||||||
private val broadcaster: Broadcaster
|
private val broadcaster: Broadcaster
|
||||||
|
@ -70,7 +70,7 @@ public class Recipient implements RecipientModifiedListener {
|
|||||||
private final @NonNull Address address;
|
private final @NonNull Address address;
|
||||||
private final @NonNull List<Recipient> participants = new LinkedList<>();
|
private final @NonNull List<Recipient> participants = new LinkedList<>();
|
||||||
|
|
||||||
private Context context;
|
private final Context context;
|
||||||
private @Nullable String name;
|
private @Nullable String name;
|
||||||
private @Nullable String customLabel;
|
private @Nullable String customLabel;
|
||||||
private boolean resolving;
|
private boolean resolving;
|
||||||
@ -132,7 +132,7 @@ public class Recipient implements RecipientModifiedListener {
|
|||||||
@NonNull Optional<RecipientDetails> details,
|
@NonNull Optional<RecipientDetails> details,
|
||||||
@NonNull ListenableFutureTask<RecipientDetails> future)
|
@NonNull ListenableFutureTask<RecipientDetails> future)
|
||||||
{
|
{
|
||||||
this.context = context;
|
this.context = context.getApplicationContext();
|
||||||
this.address = address;
|
this.address = address;
|
||||||
this.color = null;
|
this.color = null;
|
||||||
this.resolving = true;
|
this.resolving = true;
|
||||||
@ -259,7 +259,7 @@ public class Recipient implements RecipientModifiedListener {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Recipient(@NonNull Context context, @NonNull Address address, @NonNull RecipientDetails details) {
|
Recipient(@NonNull Context context, @NonNull Address address, @NonNull RecipientDetails details) {
|
||||||
this.context = context;
|
this.context = context.getApplicationContext();
|
||||||
this.address = address;
|
this.address = address;
|
||||||
this.contactUri = details.contactUri;
|
this.contactUri = details.contactUri;
|
||||||
this.name = details.name;
|
this.name = details.name;
|
||||||
|
@ -1,23 +1,60 @@
|
|||||||
package org.session.libsignal.utilities
|
package org.session.libsignal.utilities
|
||||||
|
|
||||||
import android.os.Process
|
import android.os.Process
|
||||||
import java.util.concurrent.*
|
import java.util.concurrent.ExecutorService
|
||||||
|
import java.util.concurrent.LinkedBlockingQueue
|
||||||
|
import java.util.concurrent.SynchronousQueue
|
||||||
|
import java.util.concurrent.ThreadPoolExecutor
|
||||||
|
import java.util.concurrent.TimeUnit
|
||||||
|
|
||||||
object ThreadUtils {
|
object ThreadUtils {
|
||||||
|
|
||||||
|
const val TAG = "ThreadUtils"
|
||||||
|
|
||||||
const val PRIORITY_IMPORTANT_BACKGROUND_THREAD = Process.THREAD_PRIORITY_DEFAULT + Process.THREAD_PRIORITY_LESS_FAVORABLE
|
const val PRIORITY_IMPORTANT_BACKGROUND_THREAD = Process.THREAD_PRIORITY_DEFAULT + Process.THREAD_PRIORITY_LESS_FAVORABLE
|
||||||
|
|
||||||
val executorPool: ExecutorService = Executors.newCachedThreadPool()
|
// Paraphrased from: https://www.baeldung.com/kotlin/create-thread-pool
|
||||||
|
// "A cached thread pool such as one created via:
|
||||||
|
// `val executorPool: ExecutorService = Executors.newCachedThreadPool()`
|
||||||
|
// will utilize resources according to the requirements of submitted tasks. It will try to reuse
|
||||||
|
// existing threads for submitted tasks but will create as many threads as it needs if new tasks
|
||||||
|
// keep pouring in (with a memory usage of at least 1MB per created thread). These threads will
|
||||||
|
// live for up to 60 seconds of idle time before terminating by default. As such, it presents a
|
||||||
|
// very sharp tool that doesn't include any backpressure mechanism - and a sudden peak in load
|
||||||
|
// can bring the system down with an OutOfMemory error. We can achieve a similar effect but with
|
||||||
|
// better control by creating a ThreadPoolExecutor manually."
|
||||||
|
|
||||||
|
private val corePoolSize = Runtime.getRuntime().availableProcessors() // Default thread pool size is our CPU core count
|
||||||
|
private val maxPoolSize = corePoolSize * 4 // Allow a maximum pool size of up to 4 threads per core
|
||||||
|
private val keepAliveTimeSecs = 100L // How long to keep idle threads in the pool before they are terminated
|
||||||
|
private val workQueue = SynchronousQueue<Runnable>()
|
||||||
|
val executorPool: ExecutorService = ThreadPoolExecutor(corePoolSize, maxPoolSize, keepAliveTimeSecs, TimeUnit.SECONDS, workQueue)
|
||||||
|
|
||||||
|
// Note: To see how many threads are running in our app at any given time we can use:
|
||||||
|
// val threadCount = getAllStackTraces().size
|
||||||
|
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
fun queue(target: Runnable) {
|
fun queue(target: Runnable) {
|
||||||
executorPool.execute(target)
|
executorPool.execute {
|
||||||
|
try {
|
||||||
|
target.run()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG, e)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun queue(target: () -> Unit) {
|
fun queue(target: () -> Unit) {
|
||||||
executorPool.execute(target)
|
executorPool.execute {
|
||||||
|
try {
|
||||||
|
target()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG, e)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Thread executor used by the audio recorder only
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
fun newDynamicSingleThreadedExecutor(): ExecutorService {
|
fun newDynamicSingleThreadedExecutor(): ExecutorService {
|
||||||
val executor = ThreadPoolExecutor(1, 1, 60, TimeUnit.SECONDS, LinkedBlockingQueue())
|
val executor = ThreadPoolExecutor(1, 1, 60, TimeUnit.SECONDS, LinkedBlockingQueue())
|
||||||
|
Loading…
Reference in New Issue
Block a user