mirror of
https://github.com/oxen-io/session-android.git
synced 2024-11-27 12:05:22 +00:00
Merge remote-tracking branch 'upstream/dev' into feature/read_status_updates
# Conflicts: # app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/VisibleMessageView.kt # app/src/main/res/values/strings.xml
This commit is contained in:
commit
bc20811431
@ -95,7 +95,8 @@ dependencies {
|
||||
implementation 'com.takisoft.fix:colorpicker:1.0.1'
|
||||
implementation 'com.codewaves.stickyheadergrid:stickyheadergrid:0.9.4'
|
||||
implementation 'com.github.dmytrodanylyk.circular-progress-button:library:1.1.3-S2'
|
||||
implementation 'org.signal:android-database-sqlcipher:3.5.9-S3'
|
||||
implementation 'androidx.sqlite:sqlite-ktx:2.2.0'
|
||||
implementation 'net.zetetic:sqlcipher-android:4.5.3@aar'
|
||||
implementation ('com.googlecode.ez-vcard:ez-vcard:0.9.11') {
|
||||
exclude group: 'com.fasterxml.jackson.core'
|
||||
exclude group: 'org.freemarker'
|
||||
@ -158,7 +159,7 @@ dependencies {
|
||||
testImplementation 'org.robolectric:shadows-multidex:4.4'
|
||||
}
|
||||
|
||||
def canonicalVersionCode = 321
|
||||
def canonicalVersionCode = 323
|
||||
def canonicalVersionName = "1.16.3"
|
||||
|
||||
def postFixSize = 10
|
||||
|
@ -47,6 +47,7 @@ import org.session.libsession.utilities.Util;
|
||||
import org.session.libsession.utilities.WindowDebouncer;
|
||||
import org.session.libsession.utilities.dynamiclanguage.DynamicLanguageContextWrapper;
|
||||
import org.session.libsession.utilities.dynamiclanguage.LocaleParser;
|
||||
import org.session.libsignal.utilities.HTTP;
|
||||
import org.session.libsignal.utilities.JsonUtil;
|
||||
import org.session.libsignal.utilities.Log;
|
||||
import org.session.libsignal.utilities.ThreadUtils;
|
||||
@ -57,6 +58,7 @@ import org.thoughtcrime.securesms.database.EmojiSearchDatabase;
|
||||
import org.thoughtcrime.securesms.database.JobDatabase;
|
||||
import org.thoughtcrime.securesms.database.LokiAPIDatabase;
|
||||
import org.thoughtcrime.securesms.database.Storage;
|
||||
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper;
|
||||
import org.thoughtcrime.securesms.database.model.EmojiSearchData;
|
||||
import org.thoughtcrime.securesms.dependencies.DatabaseComponent;
|
||||
import org.thoughtcrime.securesms.dependencies.DatabaseModule;
|
||||
@ -66,6 +68,7 @@ import org.thoughtcrime.securesms.groups.OpenGroupMigrator;
|
||||
import org.thoughtcrime.securesms.home.HomeActivity;
|
||||
import org.thoughtcrime.securesms.jobmanager.JobManager;
|
||||
import org.thoughtcrime.securesms.jobmanager.impl.JsonDataSerializer;
|
||||
import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint;
|
||||
import org.thoughtcrime.securesms.jobs.FastJobStorage;
|
||||
import org.thoughtcrime.securesms.jobs.JobManagerFactories;
|
||||
import org.thoughtcrime.securesms.logging.AndroidLogger;
|
||||
@ -236,6 +239,9 @@ public class ApplicationContext extends Application implements DefaultLifecycleO
|
||||
resubmitProfilePictureIfNeeded();
|
||||
loadEmojiSearchIndexIfNeeded();
|
||||
EmojiSource.refresh();
|
||||
|
||||
NetworkConstraint networkConstraint = new NetworkConstraint.Factory(this).create();
|
||||
HTTP.INSTANCE.setConnectedToNetwork(networkConstraint::isMet);
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -244,6 +250,12 @@ public class ApplicationContext extends Application implements DefaultLifecycleO
|
||||
Log.i(TAG, "App is now visible.");
|
||||
KeyCachingService.onAppForegrounded(this);
|
||||
|
||||
// If the user account hasn't been created or onboarding wasn't finished then don't start
|
||||
// the pollers
|
||||
if (TextSecurePreferences.getLocalNumber(this) == null || !TextSecurePreferences.hasSeenWelcomeScreen(this)) {
|
||||
return;
|
||||
}
|
||||
|
||||
ThreadUtils.queue(()->{
|
||||
if (poller != null) {
|
||||
poller.setCaughtUp(false);
|
||||
@ -537,7 +549,7 @@ public class ApplicationContext extends Application implements DefaultLifecycleO
|
||||
TextSecurePreferences.setProfileName(this, displayName);
|
||||
}
|
||||
getSharedPreferences(PREFERENCES_NAME, 0).edit().clear().commit();
|
||||
if (!deleteDatabase("signal.db")) {
|
||||
if (!deleteDatabase(SQLCipherOpenHelper.DATABASE_NAME)) {
|
||||
Log.d("Loki", "Failed to delete database.");
|
||||
}
|
||||
Util.runOnMain(() -> new Handler().postDelayed(ApplicationContext.this::restartApplication, 200));
|
||||
|
@ -114,7 +114,7 @@ class MediaGalleryAdapter extends StickyHeaderGridAdapter {
|
||||
Slide slide = MediaUtil.getSlideForAttachment(context, mediaRecord.getAttachment());
|
||||
|
||||
if (slide != null) {
|
||||
thumbnailView.setImageResource(glideRequests, slide, false, false);
|
||||
thumbnailView.setImageResource(glideRequests, slide, false, null);
|
||||
}
|
||||
|
||||
thumbnailView.setOnClickListener(view -> itemClickListener.onMediaClicked(mediaRecord));
|
||||
|
@ -176,6 +176,11 @@ class DatabaseAttachmentProvider(context: Context, helper: SQLCipherOpenHelper)
|
||||
return messageDB.getMessageID(serverId, threadId)
|
||||
}
|
||||
|
||||
override fun getMessageIDs(serverIds: List<Long>, threadId: Long): Pair<List<Long>, List<Long>> {
|
||||
val messageDB = DatabaseComponent.get(context).lokiMessageDatabase()
|
||||
return messageDB.getMessageIDs(serverIds, threadId)
|
||||
}
|
||||
|
||||
override fun deleteMessage(messageID: Long, isSms: Boolean) {
|
||||
val messagingDatabase: MessagingDatabase = if (isSms) DatabaseComponent.get(context).smsDatabase()
|
||||
else DatabaseComponent.get(context).mmsDatabase()
|
||||
@ -184,6 +189,15 @@ class DatabaseAttachmentProvider(context: Context, helper: SQLCipherOpenHelper)
|
||||
DatabaseComponent.get(context).lokiMessageDatabase().deleteMessageServerHash(messageID)
|
||||
}
|
||||
|
||||
override fun deleteMessages(messageIDs: List<Long>, threadId: Long, isSms: Boolean) {
|
||||
val messagingDatabase: MessagingDatabase = if (isSms) DatabaseComponent.get(context).smsDatabase()
|
||||
else DatabaseComponent.get(context).mmsDatabase()
|
||||
|
||||
messagingDatabase.deleteMessages(messageIDs.toLongArray(), threadId)
|
||||
DatabaseComponent.get(context).lokiMessageDatabase().deleteMessages(messageIDs)
|
||||
DatabaseComponent.get(context).lokiMessageDatabase().deleteMessageServerHashes(messageIDs)
|
||||
}
|
||||
|
||||
override fun updateMessageAsDeleted(timestamp: Long, author: String) {
|
||||
val database = DatabaseComponent.get(context).mmsSmsDatabase()
|
||||
val address = Address.fromSerialized(author)
|
||||
|
@ -13,6 +13,11 @@ class ScreenshotObserver(private val context: Context, handler: Handler, private
|
||||
override fun onChange(selfChange: Boolean, uri: Uri?) {
|
||||
super.onChange(selfChange, uri)
|
||||
uri ?: return
|
||||
|
||||
// There is an odd bug where we can get notified for changes to 'content://media/external'
|
||||
// directly which is a protected folder, this code is to prevent that crash
|
||||
if (uri.scheme == "content" && uri.host == "media" && uri.path == "/external") { return }
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
queryRelativeDataColumn(uri)
|
||||
} else {
|
||||
|
@ -8,7 +8,7 @@ import androidx.annotation.WorkerThread
|
||||
import com.annimon.stream.function.Consumer
|
||||
import com.annimon.stream.function.Predicate
|
||||
import com.google.protobuf.ByteString
|
||||
import net.sqlcipher.database.SQLiteDatabase
|
||||
import net.zetetic.database.sqlcipher.SQLiteDatabase
|
||||
import org.greenrobot.eventbus.EventBus
|
||||
import org.session.libsession.avatars.AvatarHelper
|
||||
import org.session.libsession.messaging.sending_receiving.attachments.AttachmentId
|
||||
|
@ -5,7 +5,7 @@ import android.content.ContentValues
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import androidx.annotation.WorkerThread
|
||||
import net.sqlcipher.database.SQLiteDatabase
|
||||
import net.zetetic.database.sqlcipher.SQLiteDatabase
|
||||
import org.greenrobot.eventbus.EventBus
|
||||
import org.session.libsession.avatars.AvatarHelper
|
||||
import org.session.libsession.messaging.sending_receiving.attachments.AttachmentId
|
||||
|
@ -1,158 +0,0 @@
|
||||
package org.thoughtcrime.securesms.components;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.res.TypedArray;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Color;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.FrameLayout;
|
||||
import android.widget.TextView;
|
||||
|
||||
import org.thoughtcrime.securesms.mms.GlideRequests;
|
||||
|
||||
import org.thoughtcrime.securesms.mms.ImageSlide;
|
||||
import org.thoughtcrime.securesms.mms.SlidesClickedListener;
|
||||
|
||||
import org.session.libsession.messaging.sending_receiving.link_preview.LinkPreview;
|
||||
|
||||
import network.loki.messenger.R;
|
||||
import okhttp3.HttpUrl;
|
||||
|
||||
public class LinkPreviewView extends FrameLayout {
|
||||
|
||||
private static final int TYPE_CONVERSATION = 0;
|
||||
private static final int TYPE_COMPOSE = 1;
|
||||
|
||||
private ViewGroup container;
|
||||
private OutlinedThumbnailView thumbnail;
|
||||
private TextView title;
|
||||
private TextView site;
|
||||
private View divider;
|
||||
private View closeButton;
|
||||
private View spinner;
|
||||
|
||||
private int type;
|
||||
private int defaultRadius;
|
||||
private CornerMask cornerMask;
|
||||
private Outliner outliner;
|
||||
private CloseClickedListener closeClickedListener;
|
||||
|
||||
public LinkPreviewView(Context context) {
|
||||
super(context);
|
||||
init(null);
|
||||
}
|
||||
|
||||
public LinkPreviewView(Context context, @Nullable AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
init(attrs);
|
||||
}
|
||||
|
||||
private void init(@Nullable AttributeSet attrs) {
|
||||
inflate(getContext(), R.layout.link_preview, this);
|
||||
|
||||
container = findViewById(R.id.linkpreview_container);
|
||||
thumbnail = findViewById(R.id.linkpreview_thumbnail);
|
||||
title = findViewById(R.id.linkpreview_title);
|
||||
site = findViewById(R.id.linkpreview_site);
|
||||
divider = findViewById(R.id.linkpreview_divider);
|
||||
spinner = findViewById(R.id.linkpreview_progress_wheel);
|
||||
closeButton = findViewById(R.id.linkpreview_close);
|
||||
defaultRadius = getResources().getDimensionPixelSize(R.dimen.thumbnail_default_radius);
|
||||
cornerMask = new CornerMask(this);
|
||||
outliner = new Outliner();
|
||||
|
||||
outliner.setColor(getResources().getColor(R.color.transparent));
|
||||
|
||||
if (attrs != null) {
|
||||
TypedArray typedArray = getContext().getTheme().obtainStyledAttributes(attrs, R.styleable.LinkPreviewView, 0, 0);
|
||||
type = typedArray.getInt(R.styleable.LinkPreviewView_linkpreview_type, 0);
|
||||
typedArray.recycle();
|
||||
}
|
||||
|
||||
if (type == TYPE_COMPOSE) {
|
||||
container.setBackgroundColor(Color.TRANSPARENT);
|
||||
container.setPadding(0, 0, 0, 0);
|
||||
divider.setVisibility(VISIBLE);
|
||||
|
||||
closeButton.setOnClickListener(v -> {
|
||||
if (closeClickedListener != null) {
|
||||
closeClickedListener.onCloseClicked();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
setWillNotDraw(false);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void dispatchDraw(Canvas canvas) {
|
||||
super.dispatchDraw(canvas);
|
||||
if (type == TYPE_COMPOSE) return;
|
||||
|
||||
cornerMask.mask(canvas);
|
||||
outliner.draw(canvas);
|
||||
}
|
||||
|
||||
public void setLoading() {
|
||||
title.setVisibility(GONE);
|
||||
site.setVisibility(GONE);
|
||||
thumbnail.setVisibility(GONE);
|
||||
spinner.setVisibility(VISIBLE);
|
||||
closeButton.setVisibility(GONE);
|
||||
}
|
||||
|
||||
public void setLinkPreview(@NonNull GlideRequests glideRequests, @NonNull LinkPreview linkPreview, boolean showThumbnail, boolean showCloseButton) {
|
||||
setLinkPreview(glideRequests, linkPreview, showThumbnail);
|
||||
if (showCloseButton) {
|
||||
closeButton.setVisibility(VISIBLE);
|
||||
} else {
|
||||
closeButton.setVisibility(GONE);
|
||||
}
|
||||
}
|
||||
|
||||
public void setLinkPreview(@NonNull GlideRequests glideRequests, @NonNull LinkPreview linkPreview, boolean showThumbnail) {
|
||||
title.setVisibility(VISIBLE);
|
||||
site.setVisibility(VISIBLE);
|
||||
thumbnail.setVisibility(VISIBLE);
|
||||
spinner.setVisibility(GONE);
|
||||
closeButton.setVisibility(VISIBLE);
|
||||
|
||||
title.setText(linkPreview.getTitle());
|
||||
|
||||
HttpUrl url = HttpUrl.parse(linkPreview.getUrl());
|
||||
if (url != null) {
|
||||
site.setText(url.topPrivateDomain());
|
||||
}
|
||||
|
||||
if (showThumbnail && linkPreview.getThumbnail().isPresent()) {
|
||||
thumbnail.setVisibility(VISIBLE);
|
||||
thumbnail.setImageResource(glideRequests, new ImageSlide(getContext(), linkPreview.getThumbnail().get()), type == TYPE_CONVERSATION, false);
|
||||
thumbnail.showDownloadText(false);
|
||||
} else {
|
||||
thumbnail.setVisibility(GONE);
|
||||
}
|
||||
}
|
||||
|
||||
public void setCorners(int topLeft, int topRight) {
|
||||
cornerMask.setRadii(topLeft, topRight, 0, 0);
|
||||
outliner.setRadii(topLeft, topRight, 0, 0);
|
||||
thumbnail.setCorners(topLeft, defaultRadius, defaultRadius, defaultRadius);
|
||||
postInvalidate();
|
||||
}
|
||||
|
||||
public void setCloseClickedListener(@Nullable CloseClickedListener closeClickedListener) {
|
||||
this.closeClickedListener = closeClickedListener;
|
||||
}
|
||||
|
||||
public void setDownloadClickedListener(SlidesClickedListener listener) {
|
||||
thumbnail.setDownloadClickListener(listener);
|
||||
}
|
||||
|
||||
public interface CloseClickedListener {
|
||||
void onCloseClicked();
|
||||
}
|
||||
}
|
@ -1,48 +0,0 @@
|
||||
package org.thoughtcrime.securesms.components;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.Canvas;
|
||||
import android.util.AttributeSet;
|
||||
|
||||
import org.session.libsession.utilities.ThemeUtil;
|
||||
import org.thoughtcrime.securesms.conversation.v2.utilities.ThumbnailView;
|
||||
|
||||
import network.loki.messenger.R;
|
||||
|
||||
public class OutlinedThumbnailView extends ThumbnailView {
|
||||
|
||||
private CornerMask cornerMask;
|
||||
private Outliner outliner;
|
||||
|
||||
public OutlinedThumbnailView(Context context) {
|
||||
super(context);
|
||||
init();
|
||||
}
|
||||
|
||||
public OutlinedThumbnailView(Context context, AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
init();
|
||||
}
|
||||
|
||||
private void init() {
|
||||
cornerMask = new CornerMask(this);
|
||||
outliner = new Outliner();
|
||||
|
||||
outliner.setColor(ThemeUtil.getThemedColor(getContext(), R.attr.conversation_item_image_outline_color));
|
||||
setWillNotDraw(false);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void dispatchDraw(Canvas canvas) {
|
||||
super.dispatchDraw(canvas);
|
||||
|
||||
cornerMask.mask(canvas);
|
||||
outliner.draw(canvas);
|
||||
}
|
||||
|
||||
public void setCorners(int topLeft, int topRight, int bottomRight, int bottomLeft) {
|
||||
cornerMask.setRadii(topLeft, topRight, bottomRight, bottomLeft);
|
||||
outliner.setRadii(topLeft, topRight, bottomRight, bottomLeft);
|
||||
postInvalidate();
|
||||
}
|
||||
}
|
@ -34,6 +34,8 @@ class ProfilePictureView @JvmOverloads constructor(
|
||||
private val profilePicturesCache = mutableMapOf<String, String?>()
|
||||
private val unknownRecipientDrawable = ResourceContactPhoto(R.drawable.ic_profile_default)
|
||||
.asDrawable(context, ContactColors.UNKNOWN_COLOR.toConversationColor(context), false)
|
||||
private val unknownOpenGroupDrawable = ResourceContactPhoto(R.drawable.ic_notification)
|
||||
.asDrawable(context, ContactColors.UNKNOWN_COLOR.toConversationColor(context), false)
|
||||
|
||||
// endregion
|
||||
|
||||
@ -43,10 +45,8 @@ class ProfilePictureView @JvmOverloads constructor(
|
||||
val contact = DatabaseComponent.get(context).sessionContactDatabase().getContactWithSessionID(publicKey)
|
||||
return contact?.displayName(Contact.ContactContext.REGULAR) ?: publicKey
|
||||
}
|
||||
fun isOpenGroupWithProfilePicture(recipient: Recipient): Boolean {
|
||||
return recipient.isOpenGroupRecipient && recipient.groupAvatarId != null
|
||||
}
|
||||
if (recipient.isGroupRecipient && !isOpenGroupWithProfilePicture(recipient)) {
|
||||
|
||||
if (recipient.isClosedGroupRecipient) {
|
||||
val members = DatabaseComponent.get(context).groupDatabase()
|
||||
.getGroupMemberAddresses(recipient.address.toGroupString(), true)
|
||||
.sorted()
|
||||
@ -107,7 +107,7 @@ class ProfilePictureView @JvmOverloads constructor(
|
||||
if (profilePicturesCache.containsKey(publicKey) && profilePicturesCache[publicKey] == recipient.profileAvatar) return
|
||||
val signalProfilePicture = recipient.contactPhoto
|
||||
val avatar = (signalProfilePicture as? ProfileContactPhoto)?.avatarObject
|
||||
val placeholder = PlaceholderAvatarPhoto(context, publicKey, displayName ?: "${publicKey.take(4)}...${publicKey.takeLast(4)}")
|
||||
|
||||
if (signalProfilePicture != null && avatar != "0" && avatar != "") {
|
||||
glide.clear(imageView)
|
||||
glide.load(signalProfilePicture)
|
||||
@ -117,7 +117,12 @@ class ProfilePictureView @JvmOverloads constructor(
|
||||
.diskCacheStrategy(DiskCacheStrategy.NONE)
|
||||
.circleCrop()
|
||||
.into(imageView)
|
||||
} else if (recipient.isOpenGroupRecipient && recipient.groupAvatarId == null) {
|
||||
glide.clear(imageView)
|
||||
imageView.setImageDrawable(unknownOpenGroupDrawable)
|
||||
} else {
|
||||
val placeholder = PlaceholderAvatarPhoto(context, publicKey, displayName ?: "${publicKey.take(4)}...${publicKey.takeLast(4)}")
|
||||
|
||||
glide.clear(imageView)
|
||||
glide.load(placeholder)
|
||||
.placeholder(unknownRecipientDrawable)
|
||||
|
@ -52,19 +52,4 @@ public class StickerView extends FrameLayout {
|
||||
public void setOnLongClickListener(@Nullable OnLongClickListener l) {
|
||||
image.setOnLongClickListener(l);
|
||||
}
|
||||
|
||||
public void setSticker(@NonNull GlideRequests glideRequests, @NonNull Slide stickerSlide) {
|
||||
boolean showControls = stickerSlide.asAttachment().getDataUri() == null;
|
||||
|
||||
image.setImageResource(glideRequests, stickerSlide, showControls, false);
|
||||
missingShade.setVisibility(showControls ? View.VISIBLE : View.GONE);
|
||||
}
|
||||
|
||||
public void setThumbnailClickListener(@NonNull SlideClickListener listener) {
|
||||
image.setThumbnailClickListener(listener);
|
||||
}
|
||||
|
||||
public void setDownloadClickListener(@NonNull SlidesClickedListener listener) {
|
||||
image.setDownloadClickListener(listener);
|
||||
}
|
||||
}
|
||||
|
@ -24,7 +24,7 @@ public class EmojiTextView extends AppCompatTextView {
|
||||
private static final char ELLIPSIS = '…';
|
||||
|
||||
private CharSequence previousText;
|
||||
private BufferType previousBufferType;
|
||||
private BufferType previousBufferType = BufferType.NORMAL;
|
||||
private float originalFontSize;
|
||||
private boolean useSystemEmoji;
|
||||
private boolean sizeChangeInProgress;
|
||||
@ -49,6 +49,15 @@ public class EmojiTextView extends AppCompatTextView {
|
||||
}
|
||||
|
||||
@Override public void setText(@Nullable CharSequence text, BufferType type) {
|
||||
// No need to do anything special if the text is null or empty
|
||||
if (text == null || text.length() == 0) {
|
||||
previousText = text;
|
||||
previousOverflowText = overflowText;
|
||||
previousBufferType = type;
|
||||
super.setText(text, type);
|
||||
return;
|
||||
}
|
||||
|
||||
EmojiParser.CandidateList candidates = EmojiProvider.getCandidates(text);
|
||||
|
||||
if (scaleEmojis && candidates != null && candidates.allEmojis) {
|
||||
@ -149,10 +158,15 @@ public class EmojiTextView extends AppCompatTextView {
|
||||
}
|
||||
|
||||
private boolean unchanged(CharSequence text, CharSequence overflowText, BufferType bufferType) {
|
||||
return Util.equals(previousText, text) &&
|
||||
Util.equals(previousOverflowText, overflowText) &&
|
||||
Util.equals(previousBufferType, bufferType) &&
|
||||
useSystemEmoji == useSystemEmoji() &&
|
||||
CharSequence finalPrevText = (previousText == null || previousText.length() == 0 ? "" : previousText);
|
||||
CharSequence finalText = (text == null || text.length() == 0 ? "" : text);
|
||||
CharSequence finalPrevOverflowText = (previousOverflowText == null || previousOverflowText.length() == 0 ? "" : previousOverflowText);
|
||||
CharSequence finalOverflowText = (overflowText == null || overflowText.length() == 0 ? "" : overflowText);
|
||||
|
||||
return Util.equals(finalPrevText, finalText) &&
|
||||
Util.equals(finalPrevOverflowText, finalOverflowText) &&
|
||||
Util.equals(previousBufferType, bufferType) &&
|
||||
useSystemEmoji == useSystemEmoji() &&
|
||||
!sizeChangeInProgress;
|
||||
}
|
||||
|
||||
|
@ -40,6 +40,8 @@ import network.loki.messenger.databinding.ViewVisibleMessageBinding
|
||||
import nl.komponents.kovenant.ui.successUi
|
||||
import org.session.libsession.messaging.MessagingModuleConfiguration
|
||||
import org.session.libsession.messaging.contacts.Contact
|
||||
import org.session.libsession.messaging.jobs.AttachmentDownloadJob
|
||||
import org.session.libsession.messaging.jobs.JobQueue
|
||||
import org.session.libsession.messaging.mentions.Mention
|
||||
import org.session.libsession.messaging.mentions.MentionsManager
|
||||
import org.session.libsession.messaging.messages.control.DataExtractionNotification
|
||||
@ -250,6 +252,12 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
|
||||
onDeselect(message, position, it)
|
||||
}
|
||||
},
|
||||
onAttachmentNeedsDownload = { attachmentId, mmsId ->
|
||||
// Start download (on IO thread)
|
||||
lifecycleScope.launch(Dispatchers.IO) {
|
||||
JobQueue.shared.add(AttachmentDownloadJob(attachmentId, mmsId))
|
||||
}
|
||||
},
|
||||
glide = glide,
|
||||
lifecycleCoroutineScope = lifecycleScope
|
||||
)
|
||||
@ -307,11 +315,24 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
|
||||
restoreDraftIfNeeded()
|
||||
setUpUiStateObserver()
|
||||
binding!!.scrollToBottomButton.setOnClickListener {
|
||||
val layoutManager = binding?.conversationRecyclerView?.layoutManager ?: return@setOnClickListener
|
||||
val layoutManager = (binding?.conversationRecyclerView?.layoutManager as? LinearLayoutManager) ?: return@setOnClickListener
|
||||
|
||||
if (layoutManager.isSmoothScrolling) {
|
||||
binding?.conversationRecyclerView?.scrollToPosition(0)
|
||||
} else {
|
||||
binding?.conversationRecyclerView?.smoothScrollToPosition(0)
|
||||
// It looks like 'smoothScrollToPosition' will actually load all intermediate items in
|
||||
// order to do the scroll, this can be very slow if there are a lot of messages so
|
||||
// instead we check the current position and if there are more than 10 items to scroll
|
||||
// we jump instantly to the 10th item and scroll from there (this should happen quick
|
||||
// enough to give a similar scroll effect without having to load everything)
|
||||
val position = layoutManager.findFirstVisibleItemPosition()
|
||||
if (position > 10) {
|
||||
binding?.conversationRecyclerView?.scrollToPosition(10)
|
||||
}
|
||||
|
||||
binding?.conversationRecyclerView?.post {
|
||||
binding?.conversationRecyclerView?.smoothScrollToPosition(0)
|
||||
}
|
||||
}
|
||||
}
|
||||
unreadCount = mmsSmsDb.getUnreadCount(viewModel.threadId)
|
||||
@ -343,7 +364,11 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
|
||||
super.onResume()
|
||||
ApplicationContext.getInstance(this).messageNotifier.setVisibleThread(viewModel.threadId)
|
||||
val recipient = viewModel.recipient ?: return
|
||||
threadDb.markAllAsRead(viewModel.threadId, recipient.isOpenGroupRecipient)
|
||||
|
||||
lifecycleScope.launch(Dispatchers.IO) {
|
||||
threadDb.markAllAsRead(viewModel.threadId, recipient.isOpenGroupRecipient)
|
||||
}
|
||||
|
||||
contentResolver.registerContentObserver(
|
||||
MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
|
||||
true,
|
||||
|
@ -39,10 +39,10 @@ class ConversationAdapter(
|
||||
private val onItemSwipeToReply: (MessageRecord, Int) -> Unit,
|
||||
private val onItemLongPress: (MessageRecord, Int, VisibleMessageView) -> Unit,
|
||||
private val onDeselect: (MessageRecord, Int) -> Unit,
|
||||
private val onAttachmentNeedsDownload: (Long, Long) -> Unit,
|
||||
private val glide: GlideRequests,
|
||||
lifecycleCoroutineScope: LifecycleCoroutineScope
|
||||
)
|
||||
: CursorRecyclerViewAdapter<ViewHolder>(context, cursor) {
|
||||
) : CursorRecyclerViewAdapter<ViewHolder>(context, cursor) {
|
||||
private val messageDB by lazy { DatabaseComponent.get(context).mmsSmsDatabase() }
|
||||
private val contactDB by lazy { DatabaseComponent.get(context).sessionContactDatabase() }
|
||||
var selectedItems = mutableSetOf<MessageRecord>()
|
||||
@ -120,7 +120,18 @@ class ConversationAdapter(
|
||||
}
|
||||
val contact = contactCache[senderIdHash]
|
||||
|
||||
visibleMessageView.bind(message, messageBefore, getMessageAfter(position, cursor), glide, searchQuery, contact, senderId, visibleMessageViewDelegate)
|
||||
visibleMessageView.bind(
|
||||
message,
|
||||
messageBefore,
|
||||
getMessageAfter(position, cursor),
|
||||
glide,
|
||||
searchQuery,
|
||||
contact,
|
||||
senderId,
|
||||
visibleMessageViewDelegate,
|
||||
onAttachmentNeedsDownload
|
||||
)
|
||||
|
||||
if (!message.isDeleted) {
|
||||
visibleMessageView.onPress = { event -> onItemPress(message, viewHolder.adapterPosition, visibleMessageView, event) }
|
||||
visibleMessageView.onSwipeToReply = { onItemSwipeToReply(message, viewHolder.adapterPosition) }
|
||||
|
@ -6,6 +6,8 @@ import androidx.lifecycle.viewModelScope
|
||||
import com.goterl.lazysodium.utils.KeyPair
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedInject
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
@ -48,11 +50,19 @@ class ConversationViewModel(
|
||||
}
|
||||
|
||||
fun saveDraft(text: String) {
|
||||
repository.saveDraft(threadId, text)
|
||||
GlobalScope.launch(Dispatchers.IO) {
|
||||
repository.saveDraft(threadId, text)
|
||||
}
|
||||
}
|
||||
|
||||
fun getDraft(): String? {
|
||||
return repository.getDraft(threadId)
|
||||
val draft: String? = repository.getDraft(threadId)
|
||||
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
repository.clearDrafts(threadId)
|
||||
}
|
||||
|
||||
return draft
|
||||
}
|
||||
|
||||
fun inviteContacts(contacts: List<Recipient>) {
|
||||
|
@ -8,53 +8,39 @@ import android.view.LayoutInflater
|
||||
import android.view.MotionEvent
|
||||
import android.view.ViewGroup
|
||||
import android.widget.FrameLayout
|
||||
import android.widget.RelativeLayout
|
||||
import android.widget.TextView
|
||||
import androidx.core.view.children
|
||||
import androidx.core.view.isVisible
|
||||
import network.loki.messenger.R
|
||||
import network.loki.messenger.databinding.AlbumThumbnailViewBinding
|
||||
import org.session.libsession.messaging.jobs.AttachmentDownloadJob
|
||||
import org.session.libsession.messaging.jobs.JobQueue
|
||||
import org.session.libsession.messaging.sending_receiving.attachments.AttachmentTransferProgress
|
||||
import org.session.libsession.messaging.sending_receiving.attachments.DatabaseAttachment
|
||||
import org.session.libsession.utilities.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.MediaPreviewActivity
|
||||
import org.thoughtcrime.securesms.components.CornerMask
|
||||
import org.thoughtcrime.securesms.conversation.v2.utilities.KThumbnailView
|
||||
import org.thoughtcrime.securesms.conversation.v2.utilities.ThumbnailView
|
||||
import org.thoughtcrime.securesms.database.model.MmsMessageRecord
|
||||
import org.thoughtcrime.securesms.mms.GlideRequests
|
||||
import org.thoughtcrime.securesms.mms.Slide
|
||||
import org.thoughtcrime.securesms.util.ActivityDispatcher
|
||||
|
||||
class AlbumThumbnailView : FrameLayout {
|
||||
|
||||
private lateinit var binding: AlbumThumbnailViewBinding
|
||||
|
||||
class AlbumThumbnailView : RelativeLayout {
|
||||
companion object {
|
||||
const val MAX_ALBUM_DISPLAY_SIZE = 3
|
||||
}
|
||||
|
||||
private val binding: AlbumThumbnailViewBinding by lazy { AlbumThumbnailViewBinding.bind(this) }
|
||||
|
||||
// region Lifecycle
|
||||
constructor(context: Context) : super(context) {
|
||||
initialize()
|
||||
}
|
||||
|
||||
constructor(context: Context, attrs: AttributeSet) : super(context, attrs) {
|
||||
initialize()
|
||||
}
|
||||
|
||||
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr) {
|
||||
initialize()
|
||||
}
|
||||
constructor(context: Context) : super(context)
|
||||
constructor(context: Context, attrs: AttributeSet) : super(context, attrs)
|
||||
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr)
|
||||
|
||||
private val cornerMask by lazy { CornerMask(this) }
|
||||
private var slides: List<Slide> = listOf()
|
||||
private var slideSize: Int = 0
|
||||
|
||||
private fun initialize() {
|
||||
binding = AlbumThumbnailViewBinding.inflate(LayoutInflater.from(context), this, true)
|
||||
}
|
||||
|
||||
override fun dispatchDraw(canvas: Canvas?) {
|
||||
super.dispatchDraw(canvas)
|
||||
cornerMask.mask(canvas)
|
||||
@ -63,26 +49,25 @@ class AlbumThumbnailView : FrameLayout {
|
||||
|
||||
// region Interaction
|
||||
|
||||
fun calculateHitObject(event: MotionEvent, mms: MmsMessageRecord, threadRecipient: Recipient) {
|
||||
fun calculateHitObject(event: MotionEvent, mms: MmsMessageRecord, threadRecipient: Recipient, onAttachmentNeedsDownload: (Long, Long) -> Unit) {
|
||||
val rawXInt = event.rawX.toInt()
|
||||
val rawYInt = event.rawY.toInt()
|
||||
val eventRect = Rect(rawXInt, rawYInt, rawXInt, rawYInt)
|
||||
val testRect = Rect()
|
||||
// test each album child
|
||||
binding.albumCellContainer.findViewById<ViewGroup>(R.id.album_thumbnail_root)?.children?.forEachIndexed { index, child ->
|
||||
binding.albumCellContainer.findViewById<ViewGroup>(R.id.album_thumbnail_root)?.children?.forEachIndexed forEach@{ index, child ->
|
||||
child.getGlobalVisibleRect(testRect)
|
||||
if (testRect.contains(eventRect)) {
|
||||
// hit intersects with this particular child
|
||||
val slide = slides.getOrNull(index) ?: return
|
||||
val slide = slides.getOrNull(index) ?: return@forEach
|
||||
// only open to downloaded images
|
||||
if (slide.transferState == AttachmentTransferProgress.TRANSFER_PROGRESS_FAILED) {
|
||||
// restart download here
|
||||
// Restart download here (on IO thread)
|
||||
(slide.asAttachment() as? DatabaseAttachment)?.let { attachment ->
|
||||
val attachmentId = attachment.attachmentId.rowId
|
||||
JobQueue.shared.add(AttachmentDownloadJob(attachmentId, mms.getId()))
|
||||
onAttachmentNeedsDownload(attachment.attachmentId.rowId, mms.getId())
|
||||
}
|
||||
}
|
||||
if (slide.isInProgress) return
|
||||
if (slide.isInProgress) return@forEach
|
||||
|
||||
ActivityDispatcher.get(context)?.dispatchIntent { context ->
|
||||
MediaPreviewActivity.getPreviewIntent(context, slide, mms, threadRecipient)
|
||||
@ -133,7 +118,7 @@ class AlbumThumbnailView : FrameLayout {
|
||||
else -> R.layout.album_thumbnail_3 // three stacked with additional text
|
||||
}
|
||||
|
||||
fun getThumbnailView(position: Int): KThumbnailView = when (position) {
|
||||
fun getThumbnailView(position: Int): ThumbnailView = when (position) {
|
||||
0 -> binding.albumCellContainer.findViewById<ViewGroup>(R.id.albumCellContainer).findViewById(R.id.album_cell_1)
|
||||
1 -> binding.albumCellContainer.findViewById<ViewGroup>(R.id.albumCellContainer).findViewById(R.id.album_cell_2)
|
||||
2 -> binding.albumCellContainer.findViewById<ViewGroup>(R.id.albumCellContainer).findViewById(R.id.album_cell_3)
|
||||
|
@ -23,7 +23,7 @@ class LinkPreviewDraftView : LinearLayout {
|
||||
// Start out with the loader showing and the content view hidden
|
||||
binding = ViewLinkPreviewDraftBinding.inflate(LayoutInflater.from(context), this, true)
|
||||
binding.linkPreviewDraftContainer.isVisible = false
|
||||
binding.thumbnailImageView.clipToOutline = true
|
||||
binding.thumbnailImageView.root.clipToOutline = true
|
||||
binding.linkPreviewDraftCancelButton.setOnClickListener { cancel() }
|
||||
}
|
||||
|
||||
@ -31,10 +31,10 @@ class LinkPreviewDraftView : LinearLayout {
|
||||
// Hide the loader and show the content view
|
||||
binding.linkPreviewDraftContainer.isVisible = true
|
||||
binding.linkPreviewDraftLoader.isVisible = false
|
||||
binding.thumbnailImageView.radius = toPx(4, resources)
|
||||
binding.thumbnailImageView.root.radius = toPx(4, resources)
|
||||
if (linkPreview.getThumbnail().isPresent) {
|
||||
// This internally fetches the thumbnail
|
||||
binding.thumbnailImageView.setImageResource(glide, ImageSlide(context, linkPreview.getThumbnail().get()), false, false)
|
||||
binding.thumbnailImageView.root.setImageResource(glide, ImageSlide(context, linkPreview.getThumbnail().get()), false, null)
|
||||
}
|
||||
binding.linkPreviewDraftTitleTextView.text = linkPreview.title
|
||||
}
|
||||
|
@ -1,118 +0,0 @@
|
||||
package org.thoughtcrime.securesms.conversation.v2.components;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.res.TypedArray;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Color;
|
||||
import android.graphics.PorterDuff;
|
||||
import androidx.annotation.Nullable;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.View;
|
||||
import android.widget.LinearLayout;
|
||||
|
||||
import network.loki.messenger.R;
|
||||
|
||||
public class TypingIndicatorView extends LinearLayout {
|
||||
private boolean isActive;
|
||||
private long startTime;
|
||||
|
||||
private static final long CYCLE_DURATION = 1500;
|
||||
private static final long DOT_DURATION = 600;
|
||||
private static final float MIN_ALPHA = 0.4f;
|
||||
private static final float MIN_SCALE = 0.75f;
|
||||
|
||||
private View dot1;
|
||||
private View dot2;
|
||||
private View dot3;
|
||||
|
||||
public TypingIndicatorView(Context context) {
|
||||
super(context);
|
||||
initialize(null);
|
||||
}
|
||||
|
||||
public TypingIndicatorView(Context context, @Nullable AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
initialize(attrs);
|
||||
}
|
||||
|
||||
private void initialize(@Nullable AttributeSet attrs) {
|
||||
inflate(getContext(), R.layout.view_typing_indicator, this);
|
||||
|
||||
setWillNotDraw(false);
|
||||
|
||||
dot1 = findViewById(R.id.typing_dot1);
|
||||
dot2 = findViewById(R.id.typing_dot2);
|
||||
dot3 = findViewById(R.id.typing_dot3);
|
||||
|
||||
if (attrs != null) {
|
||||
TypedArray typedArray = getContext().getTheme().obtainStyledAttributes(attrs, R.styleable.TypingIndicatorView, 0, 0);
|
||||
int tint = typedArray.getColor(R.styleable.TypingIndicatorView_typingIndicator_tint, Color.WHITE);
|
||||
typedArray.recycle();
|
||||
|
||||
dot1.getBackground().setColorFilter(tint, PorterDuff.Mode.MULTIPLY);
|
||||
dot2.getBackground().setColorFilter(tint, PorterDuff.Mode.MULTIPLY);
|
||||
dot3.getBackground().setColorFilter(tint, PorterDuff.Mode.MULTIPLY);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDraw(Canvas canvas) {
|
||||
if (!isActive) {
|
||||
super.onDraw(canvas);
|
||||
return;
|
||||
}
|
||||
|
||||
long timeInCycle = (System.currentTimeMillis() - startTime) % CYCLE_DURATION;
|
||||
|
||||
render(dot1, timeInCycle, 0);
|
||||
render(dot2, timeInCycle, 150);
|
||||
render(dot3, timeInCycle, 300);
|
||||
|
||||
super.onDraw(canvas);
|
||||
postInvalidate();
|
||||
}
|
||||
|
||||
private void render(View dot, long timeInCycle, long start) {
|
||||
long end = start + DOT_DURATION;
|
||||
long peak = start + (DOT_DURATION / 2);
|
||||
|
||||
if (timeInCycle < start || timeInCycle > end) {
|
||||
renderDefault(dot);
|
||||
} else if (timeInCycle < peak) {
|
||||
renderFadeIn(dot, timeInCycle, start);
|
||||
} else {
|
||||
renderFadeOut(dot, timeInCycle, peak);
|
||||
}
|
||||
}
|
||||
|
||||
private void renderDefault(View dot) {
|
||||
dot.setAlpha(MIN_ALPHA);
|
||||
dot.setScaleX(MIN_SCALE);
|
||||
dot.setScaleY(MIN_SCALE);
|
||||
}
|
||||
|
||||
private void renderFadeIn(View dot, long timeInCycle, long fadeInStart) {
|
||||
float percent = (float) (timeInCycle - fadeInStart) / 300;
|
||||
dot.setAlpha(MIN_ALPHA + (1 - MIN_ALPHA) * percent);
|
||||
dot.setScaleX(MIN_SCALE + (1 - MIN_SCALE) * percent);
|
||||
dot.setScaleY(MIN_SCALE + (1 - MIN_SCALE) * percent);
|
||||
}
|
||||
|
||||
private void renderFadeOut(View dot, long timeInCycle, long fadeOutStart) {
|
||||
float percent = (float) (timeInCycle - fadeOutStart) / 300;
|
||||
dot.setAlpha(1 - (1 - MIN_ALPHA) * percent);
|
||||
dot.setScaleX(1 - (1 - MIN_SCALE) * percent);
|
||||
dot.setScaleY(1 - (1 - MIN_SCALE) * percent);
|
||||
}
|
||||
|
||||
public void startAnimation() {
|
||||
isActive = true;
|
||||
startTime = System.currentTimeMillis();
|
||||
|
||||
postInvalidate();
|
||||
}
|
||||
|
||||
public void stopAnimation() {
|
||||
isActive = false;
|
||||
}
|
||||
}
|
@ -0,0 +1,105 @@
|
||||
package org.thoughtcrime.securesms.conversation.v2.components
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Canvas
|
||||
import android.graphics.Color
|
||||
import android.graphics.PorterDuff
|
||||
import android.util.AttributeSet
|
||||
import android.view.View
|
||||
import android.widget.LinearLayout
|
||||
import network.loki.messenger.R
|
||||
import network.loki.messenger.databinding.ViewTypingIndicatorBinding
|
||||
|
||||
class TypingIndicatorView : LinearLayout {
|
||||
companion object {
|
||||
private const val CYCLE_DURATION: Long = 1500
|
||||
private const val DOT_DURATION: Long = 600
|
||||
private const val MIN_ALPHA = 0.4f
|
||||
private const val MIN_SCALE = 0.75f
|
||||
}
|
||||
|
||||
private val binding: ViewTypingIndicatorBinding by lazy {
|
||||
val binding = ViewTypingIndicatorBinding.bind(this)
|
||||
|
||||
if (tint != -1) {
|
||||
binding.typingDot1.getBackground().setColorFilter(tint, PorterDuff.Mode.MULTIPLY)
|
||||
binding.typingDot2.getBackground().setColorFilter(tint, PorterDuff.Mode.MULTIPLY)
|
||||
binding.typingDot3.getBackground().setColorFilter(tint, PorterDuff.Mode.MULTIPLY)
|
||||
}
|
||||
|
||||
return@lazy binding
|
||||
}
|
||||
|
||||
private var isActive = false
|
||||
private var startTime: Long = 0
|
||||
private var tint: Int = -1
|
||||
|
||||
constructor(context: Context) : super(context) { initialize(null) }
|
||||
constructor(context: Context, attrs: AttributeSet) : super(context, attrs) { initialize(attrs) }
|
||||
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr) { initialize(attrs) }
|
||||
|
||||
private fun initialize(attrs: AttributeSet?) {
|
||||
setWillNotDraw(false)
|
||||
|
||||
if (attrs != null) {
|
||||
val typedArray = context.theme.obtainStyledAttributes(attrs, R.styleable.TypingIndicatorView, 0, 0)
|
||||
this.tint = typedArray.getColor(R.styleable.TypingIndicatorView_typingIndicator_tint, Color.WHITE)
|
||||
typedArray.recycle()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDraw(canvas: Canvas) {
|
||||
if (!isActive) {
|
||||
super.onDraw(canvas)
|
||||
return
|
||||
}
|
||||
val timeInCycle = (System.currentTimeMillis() - startTime) % CYCLE_DURATION
|
||||
render(binding.typingDot1, timeInCycle, 0)
|
||||
render(binding.typingDot2, timeInCycle, 150)
|
||||
render(binding.typingDot3, timeInCycle, 300)
|
||||
super.onDraw(canvas)
|
||||
postInvalidate()
|
||||
}
|
||||
|
||||
private fun render(dot: View?, timeInCycle: Long, start: Long) {
|
||||
val end = start + DOT_DURATION
|
||||
val peak = start + DOT_DURATION / 2
|
||||
if (timeInCycle < start || timeInCycle > end) {
|
||||
renderDefault(dot)
|
||||
} else if (timeInCycle < peak) {
|
||||
renderFadeIn(dot, timeInCycle, start)
|
||||
} else {
|
||||
renderFadeOut(dot, timeInCycle, peak)
|
||||
}
|
||||
}
|
||||
|
||||
private fun renderDefault(dot: View?) {
|
||||
dot!!.alpha = MIN_ALPHA
|
||||
dot.scaleX = MIN_SCALE
|
||||
dot.scaleY = MIN_SCALE
|
||||
}
|
||||
|
||||
private fun renderFadeIn(dot: View?, timeInCycle: Long, fadeInStart: Long) {
|
||||
val percent = (timeInCycle - fadeInStart).toFloat() / 300
|
||||
dot!!.alpha = MIN_ALPHA + (1 - MIN_ALPHA) * percent
|
||||
dot.scaleX = MIN_SCALE + (1 - MIN_SCALE) * percent
|
||||
dot.scaleY = MIN_SCALE + (1 - MIN_SCALE) * percent
|
||||
}
|
||||
|
||||
private fun renderFadeOut(dot: View?, timeInCycle: Long, fadeOutStart: Long) {
|
||||
val percent = (timeInCycle - fadeOutStart).toFloat() / 300
|
||||
dot!!.alpha = 1 - (1 - MIN_ALPHA) * percent
|
||||
dot.scaleX = 1 - (1 - MIN_SCALE) * percent
|
||||
dot.scaleY = 1 - (1 - MIN_SCALE) * percent
|
||||
}
|
||||
|
||||
fun startAnimation() {
|
||||
isActive = true
|
||||
startTime = System.currentTimeMillis()
|
||||
postInvalidate()
|
||||
}
|
||||
|
||||
fun stopAnimation() {
|
||||
isActive = false
|
||||
}
|
||||
}
|
@ -19,7 +19,7 @@ class TypingIndicatorViewContainer : LinearLayout {
|
||||
}
|
||||
|
||||
fun setTypists(typists: List<Recipient>) {
|
||||
if (typists.isEmpty()) { binding.typingIndicator.stopAnimation(); return }
|
||||
binding.typingIndicator.startAnimation()
|
||||
if (typists.isEmpty()) { binding.typingIndicator.root.stopAnimation(); return }
|
||||
binding.typingIndicator.root.startAnimation()
|
||||
}
|
||||
}
|
@ -1,346 +0,0 @@
|
||||
package org.thoughtcrime.securesms.conversation.v2.messages;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.res.TypedArray;
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.HapticFeedbackConstants;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.constraintlayout.widget.Group;
|
||||
import androidx.core.content.ContextCompat;
|
||||
|
||||
import com.google.android.flexbox.FlexboxLayout;
|
||||
import com.google.android.flexbox.JustifyContent;
|
||||
|
||||
import org.session.libsession.utilities.TextSecurePreferences;
|
||||
import org.session.libsession.utilities.ThemeUtil;
|
||||
import org.thoughtcrime.securesms.components.emoji.EmojiImageView;
|
||||
import org.thoughtcrime.securesms.components.emoji.EmojiUtil;
|
||||
import org.thoughtcrime.securesms.conversation.v2.ViewUtil;
|
||||
import org.thoughtcrime.securesms.database.model.MessageId;
|
||||
import org.thoughtcrime.securesms.database.model.ReactionRecord;
|
||||
import org.thoughtcrime.securesms.util.NumberUtil;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Date;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import network.loki.messenger.R;
|
||||
|
||||
public class EmojiReactionsView extends LinearLayout implements View.OnTouchListener {
|
||||
|
||||
// Normally 6dp, but we have 1dp left+right margin on the pills themselves
|
||||
private final int OUTER_MARGIN = ViewUtil.dpToPx(2);
|
||||
private static final int DEFAULT_THRESHOLD = 5;
|
||||
|
||||
private List<ReactionRecord> records;
|
||||
private long messageId;
|
||||
private ViewGroup container;
|
||||
private Group showLess;
|
||||
private VisibleMessageViewDelegate delegate;
|
||||
private Handler gestureHandler = new Handler(Looper.getMainLooper());
|
||||
private Runnable pressCallback;
|
||||
private Runnable longPressCallback;
|
||||
private long onDownTimestamp = 0;
|
||||
private static long longPressDurationThreshold = 250;
|
||||
private static long maxDoubleTapInterval = 200;
|
||||
private boolean extended = false;
|
||||
|
||||
public EmojiReactionsView(Context context) {
|
||||
super(context);
|
||||
init(null);
|
||||
}
|
||||
|
||||
public EmojiReactionsView(Context context, @Nullable AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
init(attrs);
|
||||
}
|
||||
|
||||
private void init(@Nullable AttributeSet attrs) {
|
||||
inflate(getContext(), R.layout.view_emoji_reactions, this);
|
||||
|
||||
this.container = findViewById(R.id.layout_emoji_container);
|
||||
this.showLess = findViewById(R.id.group_show_less);
|
||||
|
||||
records = new ArrayList<>();
|
||||
|
||||
if (attrs != null) {
|
||||
TypedArray typedArray = getContext().getTheme().obtainStyledAttributes(attrs, R.styleable.EmojiReactionsView, 0, 0);
|
||||
typedArray.recycle();
|
||||
}
|
||||
}
|
||||
|
||||
public void clear() {
|
||||
this.records.clear();
|
||||
container.removeAllViews();
|
||||
}
|
||||
|
||||
public void setReactions(long messageId, @NonNull List<ReactionRecord> records, boolean outgoing, VisibleMessageViewDelegate delegate) {
|
||||
this.delegate = delegate;
|
||||
if (records.equals(this.records)) {
|
||||
return;
|
||||
}
|
||||
|
||||
FlexboxLayout containerLayout = (FlexboxLayout) this.container;
|
||||
containerLayout.setJustifyContent(outgoing ? JustifyContent.FLEX_END : JustifyContent.FLEX_START);
|
||||
this.records.clear();
|
||||
this.records.addAll(records);
|
||||
if (this.messageId != messageId) {
|
||||
extended = false;
|
||||
}
|
||||
this.messageId = messageId;
|
||||
|
||||
displayReactions(extended ? Integer.MAX_VALUE : DEFAULT_THRESHOLD);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onTouch(View v, MotionEvent event) {
|
||||
if (v.getTag() == null) return false;
|
||||
|
||||
Reaction reaction = (Reaction) v.getTag();
|
||||
int action = event.getAction();
|
||||
if (action == MotionEvent.ACTION_DOWN) onDown(new MessageId(reaction.messageId, reaction.isMms));
|
||||
else if (action == MotionEvent.ACTION_CANCEL) removeLongPressCallback();
|
||||
else if (action == MotionEvent.ACTION_UP) onUp(reaction);
|
||||
return true;
|
||||
}
|
||||
|
||||
private void displayReactions(int threshold) {
|
||||
String userPublicKey = TextSecurePreferences.getLocalNumber(getContext());
|
||||
List<Reaction> reactions = buildSortedReactionsList(records, userPublicKey, threshold);
|
||||
|
||||
container.removeAllViews();
|
||||
LinearLayout overflowContainer = new LinearLayout(getContext());
|
||||
overflowContainer.setOrientation(LinearLayout.HORIZONTAL);
|
||||
int innerPadding = ViewUtil.dpToPx(4);
|
||||
overflowContainer.setPaddingRelative(innerPadding,innerPadding,innerPadding,innerPadding);
|
||||
|
||||
int pixelSize = ViewUtil.dpToPx(1);
|
||||
|
||||
for (Reaction reaction : reactions) {
|
||||
if (container.getChildCount() + 1 >= DEFAULT_THRESHOLD && threshold != Integer.MAX_VALUE && reactions.size() > threshold) {
|
||||
if (overflowContainer.getParent() == null) {
|
||||
container.addView(overflowContainer);
|
||||
MarginLayoutParams overflowParams = (MarginLayoutParams) overflowContainer.getLayoutParams();
|
||||
overflowParams.height = ViewUtil.dpToPx(26);
|
||||
overflowParams.setMargins(pixelSize, pixelSize, pixelSize, pixelSize);
|
||||
overflowContainer.setLayoutParams(overflowParams);
|
||||
overflowContainer.setBackground(ContextCompat.getDrawable(getContext(), R.drawable.reaction_pill_background));
|
||||
}
|
||||
View pill = buildPill(getContext(), this, reaction, true);
|
||||
pill.setOnClickListener(v -> {
|
||||
extended = true;
|
||||
displayReactions(Integer.MAX_VALUE);
|
||||
});
|
||||
pill.findViewById(R.id.reactions_pill_count).setVisibility(View.GONE);
|
||||
pill.findViewById(R.id.reactions_pill_spacer).setVisibility(View.GONE);
|
||||
overflowContainer.addView(pill);
|
||||
} else {
|
||||
View pill = buildPill(getContext(), this, reaction, false);
|
||||
pill.setTag(reaction);
|
||||
pill.setOnTouchListener(this);
|
||||
MarginLayoutParams params = (MarginLayoutParams) pill.getLayoutParams();
|
||||
params.setMargins(pixelSize, pixelSize, pixelSize, pixelSize);
|
||||
pill.setLayoutParams(params);
|
||||
container.addView(pill);
|
||||
}
|
||||
}
|
||||
|
||||
int overflowChildren = overflowContainer.getChildCount();
|
||||
int negativeMargin = ViewUtil.dpToPx(-8);
|
||||
for (int i = 0; i < overflowChildren; i++) {
|
||||
View child = overflowContainer.getChildAt(i);
|
||||
MarginLayoutParams childParams = (MarginLayoutParams) child.getLayoutParams();
|
||||
if ((i == 0 && overflowChildren > 1) || i + 1 < overflowChildren) {
|
||||
// if first and there is more than one child, or we are not the last child then set negative right margin
|
||||
childParams.setMargins(0,0, negativeMargin, 0);
|
||||
child.setLayoutParams(childParams);
|
||||
}
|
||||
}
|
||||
|
||||
if (threshold == Integer.MAX_VALUE) {
|
||||
showLess.setVisibility(VISIBLE);
|
||||
for (int id : showLess.getReferencedIds()) {
|
||||
findViewById(id).setOnClickListener(view -> {
|
||||
extended = false;
|
||||
displayReactions(DEFAULT_THRESHOLD);
|
||||
});
|
||||
}
|
||||
} else {
|
||||
showLess.setVisibility(GONE);
|
||||
}
|
||||
}
|
||||
|
||||
private void onReactionClicked(Reaction reaction) {
|
||||
if (reaction.messageId != 0) {
|
||||
MessageId messageId = new MessageId(reaction.messageId, reaction.isMms);
|
||||
delegate.onReactionClicked(reaction.emoji, messageId, reaction.userWasSender);
|
||||
}
|
||||
}
|
||||
|
||||
private static @NonNull List<Reaction> buildSortedReactionsList(@NonNull List<ReactionRecord> records, String userPublicKey, int threshold) {
|
||||
Map<String, Reaction> counters = new LinkedHashMap<>();
|
||||
|
||||
for (ReactionRecord record : records) {
|
||||
String baseEmoji = EmojiUtil.getCanonicalRepresentation(record.getEmoji());
|
||||
Reaction info = counters.get(baseEmoji);
|
||||
|
||||
if (info == null) {
|
||||
info = new Reaction(record.getMessageId(), record.isMms(), record.getEmoji(), record.getCount(), record.getSortId(), record.getDateReceived(), userPublicKey.equals(record.getAuthor()));
|
||||
} else {
|
||||
info.update(record.getEmoji(), record.getCount(), record.getDateReceived(), userPublicKey.equals(record.getAuthor()));
|
||||
}
|
||||
|
||||
counters.put(baseEmoji, info);
|
||||
}
|
||||
|
||||
List<Reaction> reactions = new ArrayList<>(counters.values());
|
||||
|
||||
Collections.sort(reactions, Collections.reverseOrder());
|
||||
|
||||
if (reactions.size() >= threshold + 2 && threshold != Integer.MAX_VALUE) {
|
||||
List<Reaction> shortened = new ArrayList<>(threshold + 2);
|
||||
shortened.addAll(reactions.subList(0, threshold + 2));
|
||||
return shortened;
|
||||
} else {
|
||||
return reactions;
|
||||
}
|
||||
}
|
||||
|
||||
private static View buildPill(@NonNull Context context, @NonNull ViewGroup parent, @NonNull Reaction reaction, boolean isCompact) {
|
||||
View root = LayoutInflater.from(context).inflate(R.layout.reactions_pill, parent, false);
|
||||
EmojiImageView emojiView = root.findViewById(R.id.reactions_pill_emoji);
|
||||
TextView countView = root.findViewById(R.id.reactions_pill_count);
|
||||
View spacer = root.findViewById(R.id.reactions_pill_spacer);
|
||||
|
||||
if (isCompact) {
|
||||
root.setPaddingRelative(1,1,1,1);
|
||||
ViewGroup.LayoutParams layoutParams = root.getLayoutParams();
|
||||
layoutParams.height = ViewGroup.LayoutParams.WRAP_CONTENT;
|
||||
root.setLayoutParams(layoutParams);
|
||||
}
|
||||
|
||||
if (reaction.emoji != null) {
|
||||
emojiView.setImageEmoji(reaction.emoji);
|
||||
|
||||
if (reaction.count >= 1) {
|
||||
countView.setText(NumberUtil.getFormattedNumber(reaction.count));
|
||||
} else {
|
||||
countView.setVisibility(GONE);
|
||||
spacer.setVisibility(GONE);
|
||||
}
|
||||
} else {
|
||||
emojiView.setVisibility(GONE);
|
||||
spacer.setVisibility(GONE);
|
||||
countView.setText(context.getString(R.string.ReactionsConversationView_plus, reaction.count));
|
||||
}
|
||||
|
||||
if (reaction.userWasSender && !isCompact) {
|
||||
root.setBackground(ContextCompat.getDrawable(context, R.drawable.reaction_pill_background_selected));
|
||||
countView.setTextColor(ThemeUtil.getThemedColor(context, R.attr.reactionsPillSelectedTextColor));
|
||||
} else {
|
||||
if (!isCompact) {
|
||||
root.setBackground(ContextCompat.getDrawable(context, R.drawable.reaction_pill_background));
|
||||
}
|
||||
}
|
||||
|
||||
return root;
|
||||
}
|
||||
|
||||
private void onDown(MessageId messageId) {
|
||||
removeLongPressCallback();
|
||||
Runnable newLongPressCallback = () -> {
|
||||
performHapticFeedback(HapticFeedbackConstants.LONG_PRESS);
|
||||
if (delegate != null) {
|
||||
delegate.onReactionLongClicked(messageId);
|
||||
}
|
||||
};
|
||||
this.longPressCallback = newLongPressCallback;
|
||||
gestureHandler.postDelayed(newLongPressCallback, longPressDurationThreshold);
|
||||
onDownTimestamp = new Date().getTime();
|
||||
}
|
||||
|
||||
private void removeLongPressCallback() {
|
||||
if (longPressCallback != null) {
|
||||
gestureHandler.removeCallbacks(longPressCallback);
|
||||
}
|
||||
}
|
||||
|
||||
private void onUp(Reaction reaction) {
|
||||
if ((new Date().getTime() - onDownTimestamp) < longPressDurationThreshold) {
|
||||
removeLongPressCallback();
|
||||
if (pressCallback != null) {
|
||||
gestureHandler.removeCallbacks(pressCallback);
|
||||
this.pressCallback = null;
|
||||
} else {
|
||||
Runnable newPressCallback = () -> {
|
||||
onReactionClicked(reaction);
|
||||
pressCallback = null;
|
||||
};
|
||||
this.pressCallback = newPressCallback;
|
||||
gestureHandler.postDelayed(newPressCallback, maxDoubleTapInterval);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static class Reaction implements Comparable<Reaction> {
|
||||
private final long messageId;
|
||||
private final boolean isMms;
|
||||
private String emoji;
|
||||
private long count;
|
||||
private long sortIndex;
|
||||
private long lastSeen;
|
||||
private boolean userWasSender;
|
||||
|
||||
Reaction(long messageId, boolean isMms, @Nullable String emoji, long count, long sortIndex, long lastSeen, boolean userWasSender) {
|
||||
this.messageId = messageId;
|
||||
this.isMms = isMms;
|
||||
this.emoji = emoji;
|
||||
this.count = count;
|
||||
this.sortIndex = sortIndex;
|
||||
this.lastSeen = lastSeen;
|
||||
this.userWasSender = userWasSender;
|
||||
}
|
||||
|
||||
void update(@NonNull String emoji, long count, long lastSeen, boolean userWasSender) {
|
||||
if (!this.userWasSender) {
|
||||
if (userWasSender || lastSeen > this.lastSeen) {
|
||||
this.emoji = emoji;
|
||||
}
|
||||
}
|
||||
|
||||
this.count = this.count + count;
|
||||
this.lastSeen = Math.max(this.lastSeen, lastSeen);
|
||||
this.userWasSender = this.userWasSender || userWasSender;
|
||||
}
|
||||
|
||||
@NonNull Reaction merge(@NonNull Reaction other) {
|
||||
this.count = this.count + other.count;
|
||||
this.lastSeen = Math.max(this.lastSeen, other.lastSeen);
|
||||
this.userWasSender = this.userWasSender || other.userWasSender;
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int compareTo(Reaction rhs) {
|
||||
Reaction lhs = this;
|
||||
if (lhs.count == rhs.count ) {
|
||||
return Long.compare(lhs.sortIndex, rhs.sortIndex);
|
||||
} else {
|
||||
return Long.compare(lhs.count, rhs.count);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,291 @@
|
||||
package org.thoughtcrime.securesms.conversation.v2.messages
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.util.AttributeSet
|
||||
import android.view.*
|
||||
import android.view.View.OnTouchListener
|
||||
import android.widget.LinearLayout
|
||||
import android.widget.TextView
|
||||
import androidx.constraintlayout.widget.ConstraintLayout
|
||||
import androidx.core.content.ContextCompat
|
||||
import com.google.android.flexbox.JustifyContent
|
||||
import network.loki.messenger.R
|
||||
import network.loki.messenger.databinding.ViewEmojiReactionsBinding
|
||||
import org.session.libsession.utilities.TextSecurePreferences.Companion.getLocalNumber
|
||||
import org.session.libsession.utilities.ThemeUtil
|
||||
import org.thoughtcrime.securesms.components.emoji.EmojiImageView
|
||||
import org.thoughtcrime.securesms.components.emoji.EmojiUtil
|
||||
import org.thoughtcrime.securesms.conversation.v2.ViewUtil
|
||||
import org.thoughtcrime.securesms.database.model.MessageId
|
||||
import org.thoughtcrime.securesms.database.model.ReactionRecord
|
||||
import org.thoughtcrime.securesms.util.NumberUtil.getFormattedNumber
|
||||
import java.util.*
|
||||
|
||||
class EmojiReactionsView : ConstraintLayout, OnTouchListener {
|
||||
companion object {
|
||||
private const val DEFAULT_THRESHOLD = 5
|
||||
private const val longPressDurationThreshold: Long = 250
|
||||
private const val maxDoubleTapInterval: Long = 200
|
||||
}
|
||||
|
||||
private val binding: ViewEmojiReactionsBinding by lazy { ViewEmojiReactionsBinding.bind(this) }
|
||||
|
||||
// Normally 6dp, but we have 1dp left+right margin on the pills themselves
|
||||
private val OUTER_MARGIN = ViewUtil.dpToPx(2)
|
||||
private var records: MutableList<ReactionRecord>? = null
|
||||
private var messageId: Long = 0
|
||||
private var delegate: VisibleMessageViewDelegate? = null
|
||||
private val gestureHandler = Handler(Looper.getMainLooper())
|
||||
private var pressCallback: Runnable? = null
|
||||
private var longPressCallback: Runnable? = null
|
||||
private var onDownTimestamp: Long = 0
|
||||
private var extended = false
|
||||
|
||||
constructor(context: Context) : super(context) { init(null) }
|
||||
constructor(context: Context, attrs: AttributeSet) : super(context, attrs) { init(attrs) }
|
||||
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr) { init(attrs) }
|
||||
|
||||
private fun init(attrs: AttributeSet?) {
|
||||
records = ArrayList()
|
||||
|
||||
if (attrs != null) {
|
||||
val typedArray = context.theme.obtainStyledAttributes(attrs, R.styleable.EmojiReactionsView, 0, 0)
|
||||
typedArray.recycle()
|
||||
}
|
||||
}
|
||||
|
||||
fun clear() {
|
||||
records!!.clear()
|
||||
binding.layoutEmojiContainer.removeAllViews()
|
||||
}
|
||||
|
||||
fun setReactions(messageId: Long, records: List<ReactionRecord>, outgoing: Boolean, delegate: VisibleMessageViewDelegate?) {
|
||||
this.delegate = delegate
|
||||
if (records == this.records) {
|
||||
return
|
||||
}
|
||||
|
||||
binding.layoutEmojiContainer.justifyContent = if (outgoing) JustifyContent.FLEX_END else JustifyContent.FLEX_START
|
||||
this.records!!.clear()
|
||||
this.records!!.addAll(records)
|
||||
if (this.messageId != messageId) {
|
||||
extended = false
|
||||
}
|
||||
this.messageId = messageId
|
||||
displayReactions(if (extended) Int.MAX_VALUE else DEFAULT_THRESHOLD)
|
||||
}
|
||||
|
||||
override fun onTouch(v: View, event: MotionEvent): Boolean {
|
||||
if (v.tag == null) return false
|
||||
val reaction = v.tag as Reaction
|
||||
val action = event.action
|
||||
if (action == MotionEvent.ACTION_DOWN) onDown(MessageId(reaction.messageId, reaction.isMms)) else if (action == MotionEvent.ACTION_CANCEL) removeLongPressCallback() else if (action == MotionEvent.ACTION_UP) onUp(reaction)
|
||||
return true
|
||||
}
|
||||
|
||||
private fun displayReactions(threshold: Int) {
|
||||
val userPublicKey = getLocalNumber(context)
|
||||
val reactions = buildSortedReactionsList(records!!, userPublicKey, threshold)
|
||||
binding.layoutEmojiContainer.removeAllViews()
|
||||
val overflowContainer = LinearLayout(context)
|
||||
overflowContainer.orientation = LinearLayout.HORIZONTAL
|
||||
val innerPadding = ViewUtil.dpToPx(4)
|
||||
overflowContainer.setPaddingRelative(innerPadding, innerPadding, innerPadding, innerPadding)
|
||||
val pixelSize = ViewUtil.dpToPx(1)
|
||||
for (reaction in reactions) {
|
||||
if (binding.layoutEmojiContainer.childCount + 1 >= DEFAULT_THRESHOLD && threshold != Int.MAX_VALUE && reactions.size > threshold) {
|
||||
if (overflowContainer.parent == null) {
|
||||
binding.layoutEmojiContainer.addView(overflowContainer)
|
||||
val overflowParams = overflowContainer.layoutParams as MarginLayoutParams
|
||||
overflowParams.height = ViewUtil.dpToPx(26)
|
||||
overflowParams.setMargins(pixelSize, pixelSize, pixelSize, pixelSize)
|
||||
overflowContainer.layoutParams = overflowParams
|
||||
overflowContainer.background = ContextCompat.getDrawable(context, R.drawable.reaction_pill_background)
|
||||
}
|
||||
val pill = buildPill(context, this, reaction, true)
|
||||
pill.setOnClickListener { v: View? ->
|
||||
extended = true
|
||||
displayReactions(Int.MAX_VALUE)
|
||||
}
|
||||
pill.findViewById<View>(R.id.reactions_pill_count).visibility = GONE
|
||||
pill.findViewById<View>(R.id.reactions_pill_spacer).visibility = GONE
|
||||
overflowContainer.addView(pill)
|
||||
} else {
|
||||
val pill = buildPill(context, this, reaction, false)
|
||||
pill.tag = reaction
|
||||
pill.setOnTouchListener(this)
|
||||
val params = pill.layoutParams as MarginLayoutParams
|
||||
params.setMargins(pixelSize, pixelSize, pixelSize, pixelSize)
|
||||
pill.layoutParams = params
|
||||
binding.layoutEmojiContainer.addView(pill)
|
||||
}
|
||||
}
|
||||
val overflowChildren = overflowContainer.childCount
|
||||
val negativeMargin = ViewUtil.dpToPx(-8)
|
||||
for (i in 0 until overflowChildren) {
|
||||
val child = overflowContainer.getChildAt(i)
|
||||
val childParams = child.layoutParams as MarginLayoutParams
|
||||
if (i == 0 && overflowChildren > 1 || i + 1 < overflowChildren) {
|
||||
// if first and there is more than one child, or we are not the last child then set negative right margin
|
||||
childParams.setMargins(0, 0, negativeMargin, 0)
|
||||
child.layoutParams = childParams
|
||||
}
|
||||
}
|
||||
if (threshold == Int.MAX_VALUE) {
|
||||
binding.groupShowLess.visibility = VISIBLE
|
||||
for (id in binding.groupShowLess.referencedIds) {
|
||||
findViewById<View>(id).setOnClickListener { view: View? ->
|
||||
extended = false
|
||||
displayReactions(DEFAULT_THRESHOLD)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
binding.groupShowLess.visibility = GONE
|
||||
}
|
||||
}
|
||||
|
||||
private fun buildSortedReactionsList(records: List<ReactionRecord>, userPublicKey: String?, threshold: Int): List<Reaction> {
|
||||
val counters: MutableMap<String, Reaction> = LinkedHashMap()
|
||||
|
||||
records.forEach {
|
||||
val baseEmoji = EmojiUtil.getCanonicalRepresentation(it.emoji)
|
||||
val info = counters[baseEmoji]
|
||||
|
||||
if (info == null) {
|
||||
counters[baseEmoji] = Reaction(messageId, it.isMms, it.emoji, it.count, it.sortId, it.dateReceived, userPublicKey == it.author)
|
||||
}
|
||||
else {
|
||||
info.update(it.emoji, it.count, it.dateReceived, userPublicKey == it.author)
|
||||
}
|
||||
}
|
||||
|
||||
val reactions: List<Reaction> = ArrayList(counters.values)
|
||||
Collections.sort(reactions, Collections.reverseOrder())
|
||||
|
||||
return if (reactions.size >= threshold + 2 && threshold != Int.MAX_VALUE) {
|
||||
val shortened: MutableList<Reaction> = ArrayList(threshold + 2)
|
||||
shortened.addAll(reactions.subList(0, threshold + 2))
|
||||
shortened
|
||||
} else {
|
||||
reactions
|
||||
}
|
||||
}
|
||||
|
||||
private fun buildPill(context: Context, parent: ViewGroup, reaction: Reaction, isCompact: Boolean): View {
|
||||
val root = LayoutInflater.from(context).inflate(R.layout.reactions_pill, parent, false)
|
||||
val emojiView = root.findViewById<EmojiImageView>(R.id.reactions_pill_emoji)
|
||||
val countView = root.findViewById<TextView>(R.id.reactions_pill_count)
|
||||
val spacer = root.findViewById<View>(R.id.reactions_pill_spacer)
|
||||
if (isCompact) {
|
||||
root.setPaddingRelative(1, 1, 1, 1)
|
||||
val layoutParams = root.layoutParams
|
||||
layoutParams.height = ViewGroup.LayoutParams.WRAP_CONTENT
|
||||
root.layoutParams = layoutParams
|
||||
}
|
||||
if (reaction.emoji != null) {
|
||||
emojiView.setImageEmoji(reaction.emoji)
|
||||
if (reaction.count >= 1) {
|
||||
countView.text = getFormattedNumber(reaction.count)
|
||||
} else {
|
||||
countView.visibility = GONE
|
||||
spacer.visibility = GONE
|
||||
}
|
||||
} else {
|
||||
emojiView.visibility = GONE
|
||||
spacer.visibility = GONE
|
||||
countView.text = context.getString(R.string.ReactionsConversationView_plus, reaction.count)
|
||||
}
|
||||
if (reaction.userWasSender && !isCompact) {
|
||||
root.background = ContextCompat.getDrawable(context, R.drawable.reaction_pill_background_selected)
|
||||
countView.setTextColor(ThemeUtil.getThemedColor(context, R.attr.reactionsPillSelectedTextColor))
|
||||
} else {
|
||||
if (!isCompact) {
|
||||
root.background = ContextCompat.getDrawable(context, R.drawable.reaction_pill_background)
|
||||
}
|
||||
}
|
||||
return root
|
||||
}
|
||||
|
||||
private fun onReactionClicked(reaction: Reaction) {
|
||||
if (reaction.messageId != 0L) {
|
||||
val messageId = MessageId(reaction.messageId, reaction.isMms)
|
||||
delegate!!.onReactionClicked(reaction.emoji!!, messageId, reaction.userWasSender)
|
||||
}
|
||||
}
|
||||
|
||||
private fun onDown(messageId: MessageId) {
|
||||
removeLongPressCallback()
|
||||
val newLongPressCallback = Runnable {
|
||||
performHapticFeedback(HapticFeedbackConstants.LONG_PRESS)
|
||||
if (delegate != null) {
|
||||
delegate!!.onReactionLongClicked(messageId)
|
||||
}
|
||||
}
|
||||
longPressCallback = newLongPressCallback
|
||||
gestureHandler.postDelayed(newLongPressCallback, longPressDurationThreshold)
|
||||
onDownTimestamp = Date().time
|
||||
}
|
||||
|
||||
private fun removeLongPressCallback() {
|
||||
if (longPressCallback != null) {
|
||||
gestureHandler.removeCallbacks(longPressCallback!!)
|
||||
}
|
||||
}
|
||||
|
||||
private fun onUp(reaction: Reaction) {
|
||||
if (Date().time - onDownTimestamp < longPressDurationThreshold) {
|
||||
removeLongPressCallback()
|
||||
if (pressCallback != null) {
|
||||
gestureHandler.removeCallbacks(pressCallback!!)
|
||||
pressCallback = null
|
||||
} else {
|
||||
val newPressCallback = Runnable {
|
||||
onReactionClicked(reaction)
|
||||
pressCallback = null
|
||||
}
|
||||
pressCallback = newPressCallback
|
||||
gestureHandler.postDelayed(newPressCallback, maxDoubleTapInterval)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal class Reaction(
|
||||
internal val messageId: Long,
|
||||
internal val isMms: Boolean,
|
||||
internal var emoji: String?,
|
||||
internal var count: Long,
|
||||
internal val sortIndex: Long,
|
||||
internal var lastSeen: Long,
|
||||
internal var userWasSender: Boolean
|
||||
) : Comparable<Reaction?> {
|
||||
fun update(emoji: String, count: Long, lastSeen: Long, userWasSender: Boolean) {
|
||||
if (!this.userWasSender) {
|
||||
if (userWasSender || lastSeen > this.lastSeen) {
|
||||
this.emoji = emoji
|
||||
}
|
||||
}
|
||||
this.count = this.count + count
|
||||
this.lastSeen = Math.max(this.lastSeen, lastSeen)
|
||||
this.userWasSender = this.userWasSender || userWasSender
|
||||
}
|
||||
|
||||
fun merge(other: Reaction): Reaction {
|
||||
count = count + other.count
|
||||
lastSeen = Math.max(lastSeen, other.lastSeen)
|
||||
userWasSender = userWasSender || other.userWasSender
|
||||
return this
|
||||
}
|
||||
|
||||
override fun compareTo(other: Reaction?): Int {
|
||||
if (other == null) { return -1 }
|
||||
|
||||
if (this.count == other.count) {
|
||||
return this.sortIndex.compareTo(other.sortIndex)
|
||||
}
|
||||
|
||||
return this.count.compareTo(other.count)
|
||||
}
|
||||
}
|
||||
}
|
@ -4,11 +4,9 @@ import android.content.Context
|
||||
import android.graphics.Canvas
|
||||
import android.graphics.Rect
|
||||
import android.util.AttributeSet
|
||||
import android.view.LayoutInflater
|
||||
import android.view.MotionEvent
|
||||
import android.widget.LinearLayout
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.core.content.res.ResourcesCompat
|
||||
import androidx.core.view.isVisible
|
||||
import network.loki.messenger.R
|
||||
import network.loki.messenger.databinding.ViewLinkPreviewBinding
|
||||
@ -19,21 +17,16 @@ import org.thoughtcrime.securesms.conversation.v2.utilities.MessageBubbleUtiliti
|
||||
import org.thoughtcrime.securesms.database.model.MmsMessageRecord
|
||||
import org.thoughtcrime.securesms.mms.GlideRequests
|
||||
import org.thoughtcrime.securesms.mms.ImageSlide
|
||||
import org.thoughtcrime.securesms.util.UiModeUtilities
|
||||
|
||||
class LinkPreviewView : LinearLayout {
|
||||
private lateinit var binding: ViewLinkPreviewBinding
|
||||
private val binding: ViewLinkPreviewBinding by lazy { ViewLinkPreviewBinding.bind(this) }
|
||||
private val cornerMask by lazy { CornerMask(this) }
|
||||
private var url: String? = null
|
||||
|
||||
// region Lifecycle
|
||||
constructor(context: Context) : super(context) { initialize() }
|
||||
constructor(context: Context, attrs: AttributeSet) : super(context, attrs) { initialize() }
|
||||
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr) { initialize() }
|
||||
|
||||
private fun initialize() {
|
||||
binding = ViewLinkPreviewBinding.inflate(LayoutInflater.from(context), this, true)
|
||||
}
|
||||
constructor(context: Context) : super(context)
|
||||
constructor(context: Context, attrs: AttributeSet) : super(context, attrs)
|
||||
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr)
|
||||
// endregion
|
||||
|
||||
// region Updating
|
||||
@ -48,8 +41,8 @@ class LinkPreviewView : LinearLayout {
|
||||
// Thumbnail
|
||||
if (linkPreview.getThumbnail().isPresent) {
|
||||
// This internally fetches the thumbnail
|
||||
binding.thumbnailImageView.setImageResource(glide, ImageSlide(context, linkPreview.getThumbnail().get()), isPreview = false, message)
|
||||
binding.thumbnailImageView.loadIndicator.isVisible = false
|
||||
binding.thumbnailImageView.root.setImageResource(glide, ImageSlide(context, linkPreview.getThumbnail().get()), isPreview = false, message)
|
||||
binding.thumbnailImageView.root.loadIndicator.isVisible = false
|
||||
}
|
||||
// Title
|
||||
binding.titleTextView.text = linkPreview.title
|
||||
@ -80,7 +73,7 @@ class LinkPreviewView : LinearLayout {
|
||||
val rawYInt = event.rawY.toInt()
|
||||
val hitRect = Rect(rawXInt, rawYInt, rawXInt, rawYInt)
|
||||
val previewRect = Rect()
|
||||
binding.mainLinkPreviewParent.getGlobalVisibleRect(previewRect)
|
||||
binding.mainLinkPreviewContainer.getGlobalVisibleRect(previewRect)
|
||||
if (previewRect.contains(hitRect)) {
|
||||
openURL()
|
||||
return
|
||||
|
@ -93,7 +93,7 @@ class QuoteView @JvmOverloads constructor(context: Context, attrs: AttributeSet?
|
||||
val backgroundColor = context.getAccentColor()
|
||||
binding.quoteViewAttachmentPreviewContainer.backgroundTintList = ColorStateList.valueOf(backgroundColor)
|
||||
binding.quoteViewAttachmentPreviewImageView.isVisible = false
|
||||
binding.quoteViewAttachmentThumbnailImageView.isVisible = false
|
||||
binding.quoteViewAttachmentThumbnailImageView.root.isVisible = false
|
||||
when {
|
||||
attachments.audioSlide != null -> {
|
||||
binding.quoteViewAttachmentPreviewImageView.setImageResource(R.drawable.ic_microphone)
|
||||
@ -108,9 +108,9 @@ class QuoteView @JvmOverloads constructor(context: Context, attrs: AttributeSet?
|
||||
attachments.thumbnailSlide != null -> {
|
||||
val slide = attachments.thumbnailSlide!!
|
||||
// This internally fetches the thumbnail
|
||||
binding.quoteViewAttachmentThumbnailImageView.radius = toPx(4, resources)
|
||||
binding.quoteViewAttachmentThumbnailImageView.setImageResource(glide, slide, false, false)
|
||||
binding.quoteViewAttachmentThumbnailImageView.isVisible = true
|
||||
binding.quoteViewAttachmentThumbnailImageView.root.radius = toPx(4, resources)
|
||||
binding.quoteViewAttachmentThumbnailImageView.root.setImageResource(glide, slide, false, null)
|
||||
binding.quoteViewAttachmentThumbnailImageView.root.isVisible = true
|
||||
binding.quoteViewBodyTextView.text = if (MediaUtil.isVideo(slide.asAttachment())) resources.getString(R.string.Slide_video) else resources.getString(R.string.Slide_image)
|
||||
}
|
||||
}
|
||||
|
@ -27,8 +27,6 @@ import network.loki.messenger.R
|
||||
import network.loki.messenger.databinding.ViewVisibleMessageContentBinding
|
||||
import okhttp3.HttpUrl
|
||||
import org.session.libsession.messaging.MessagingModuleConfiguration
|
||||
import org.session.libsession.messaging.jobs.AttachmentDownloadJob
|
||||
import org.session.libsession.messaging.jobs.JobQueue
|
||||
import org.session.libsession.messaging.sending_receiving.attachments.AttachmentTransferProgress
|
||||
import org.session.libsession.messaging.sending_receiving.attachments.DatabaseAttachment
|
||||
import org.session.libsession.utilities.getColorFromAttr
|
||||
@ -47,26 +45,30 @@ import org.thoughtcrime.securesms.util.getAccentColor
|
||||
import java.util.*
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
class VisibleMessageContentView : LinearLayout {
|
||||
private lateinit var binding: ViewVisibleMessageContentBinding
|
||||
class VisibleMessageContentView : ConstraintLayout {
|
||||
private val binding: ViewVisibleMessageContentBinding by lazy { ViewVisibleMessageContentBinding.bind(this) }
|
||||
var onContentClick: MutableList<((event: MotionEvent) -> Unit)> = mutableListOf()
|
||||
var onContentDoubleTap: (() -> Unit)? = null
|
||||
var delegate: VisibleMessageViewDelegate? = null
|
||||
var indexInAdapter: Int = -1
|
||||
|
||||
// region Lifecycle
|
||||
constructor(context: Context) : super(context) { initialize() }
|
||||
constructor(context: Context, attrs: AttributeSet) : super(context, attrs) { initialize() }
|
||||
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr) { initialize() }
|
||||
|
||||
private fun initialize() {
|
||||
binding = ViewVisibleMessageContentBinding.inflate(LayoutInflater.from(context), this, true)
|
||||
}
|
||||
constructor(context: Context) : super(context)
|
||||
constructor(context: Context, attrs: AttributeSet) : super(context, attrs)
|
||||
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr)
|
||||
// endregion
|
||||
|
||||
// region Updating
|
||||
fun bind(message: MessageRecord, isStartOfMessageCluster: Boolean, isEndOfMessageCluster: Boolean,
|
||||
glide: GlideRequests, thread: Recipient, searchQuery: String?, contactIsTrusted: Boolean) {
|
||||
fun bind(
|
||||
message: MessageRecord,
|
||||
isStartOfMessageCluster: Boolean,
|
||||
isEndOfMessageCluster: Boolean,
|
||||
glide: GlideRequests,
|
||||
thread: Recipient,
|
||||
searchQuery: String?,
|
||||
contactIsTrusted: Boolean,
|
||||
onAttachmentNeedsDownload: (Long, Long) -> Unit
|
||||
) {
|
||||
// Background
|
||||
val background = getBackground(message.isOutgoing)
|
||||
val color = if (message.isOutgoing) context.getAccentColor()
|
||||
@ -80,7 +82,7 @@ class VisibleMessageContentView : LinearLayout {
|
||||
|
||||
// reset visibilities / containers
|
||||
onContentClick.clear()
|
||||
binding.albumThumbnailView.clearViews()
|
||||
binding.albumThumbnailView.root.clearViews()
|
||||
onContentDoubleTap = null
|
||||
|
||||
if (message.isDeleted) {
|
||||
@ -88,28 +90,23 @@ class VisibleMessageContentView : LinearLayout {
|
||||
binding.deletedMessageView.root.bind(message, getTextColor(context, message))
|
||||
binding.bodyTextView.isVisible = false
|
||||
binding.quoteView.root.isVisible = false
|
||||
binding.linkPreviewView.isVisible = false
|
||||
binding.linkPreviewView.root.isVisible = false
|
||||
binding.untrustedView.root.isVisible = false
|
||||
binding.voiceMessageView.root.isVisible = false
|
||||
binding.documentView.root.isVisible = false
|
||||
binding.albumThumbnailView.isVisible = false
|
||||
binding.albumThumbnailView.root.isVisible = false
|
||||
binding.openGroupInvitationView.root.isVisible = false
|
||||
return
|
||||
} else {
|
||||
binding.deletedMessageView.root.isVisible = false
|
||||
}
|
||||
// clear the
|
||||
binding.bodyTextView.text = null
|
||||
|
||||
|
||||
binding.quoteView.root.isVisible = message is MmsMessageRecord && message.quote != null
|
||||
|
||||
binding.linkPreviewView.isVisible = message is MmsMessageRecord && message.linkPreviews.isNotEmpty()
|
||||
|
||||
binding.linkPreviewView.root.isVisible = message is MmsMessageRecord && message.linkPreviews.isNotEmpty()
|
||||
binding.untrustedView.root.isVisible = !contactIsTrusted && message is MmsMessageRecord && message.quote == null && message.linkPreviews.isEmpty()
|
||||
binding.voiceMessageView.root.isVisible = contactIsTrusted && message is MmsMessageRecord && message.slideDeck.audioSlide != null
|
||||
binding.documentView.root.isVisible = contactIsTrusted && message is MmsMessageRecord && message.slideDeck.documentSlide != null
|
||||
binding.albumThumbnailView.isVisible = mediaThumbnailMessage
|
||||
binding.albumThumbnailView.root.isVisible = mediaThumbnailMessage
|
||||
binding.openGroupInvitationView.root.isVisible = message.isOpenGroupInvitation
|
||||
|
||||
var hideBody = false
|
||||
@ -141,8 +138,7 @@ class VisibleMessageContentView : LinearLayout {
|
||||
val attachmentId = dbAttachment.attachmentId.rowId
|
||||
if (attach.transferState == AttachmentTransferProgress.TRANSFER_PROGRESS_PENDING
|
||||
&& MessagingModuleConfiguration.shared.storage.getAttachmentUploadJob(attachmentId) == null) {
|
||||
// start download
|
||||
JobQueue.shared.add(AttachmentDownloadJob(attachmentId, dbAttachment.mmsId))
|
||||
onAttachmentNeedsDownload(attachmentId, dbAttachment.mmsId)
|
||||
}
|
||||
}
|
||||
message.linkPreviews.forEach { preview ->
|
||||
@ -150,15 +146,15 @@ class VisibleMessageContentView : LinearLayout {
|
||||
val attachmentId = previewThumbnail.attachmentId.rowId
|
||||
if (previewThumbnail.transferState == AttachmentTransferProgress.TRANSFER_PROGRESS_PENDING
|
||||
&& MessagingModuleConfiguration.shared.storage.getAttachmentUploadJob(attachmentId) == null) {
|
||||
JobQueue.shared.add(AttachmentDownloadJob(attachmentId, previewThumbnail.mmsId))
|
||||
onAttachmentNeedsDownload(attachmentId, previewThumbnail.mmsId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
when {
|
||||
message is MmsMessageRecord && message.linkPreviews.isNotEmpty() -> {
|
||||
binding.linkPreviewView.bind(message, glide, isStartOfMessageCluster, isEndOfMessageCluster)
|
||||
onContentClick.add { event -> binding.linkPreviewView.calculateHit(event) }
|
||||
binding.linkPreviewView.root.bind(message, glide, isStartOfMessageCluster, isEndOfMessageCluster)
|
||||
onContentClick.add { event -> binding.linkPreviewView.root.calculateHit(event) }
|
||||
// Body text view is inside the link preview for layout convenience
|
||||
}
|
||||
message is MmsMessageRecord && message.slideDeck.audioSlide != null -> {
|
||||
@ -195,21 +191,21 @@ class VisibleMessageContentView : LinearLayout {
|
||||
if (contactIsTrusted || message.isOutgoing) {
|
||||
// isStart and isEnd of cluster needed for calculating the mask for full bubble image groups
|
||||
// bind after add view because views are inflated and calculated during bind
|
||||
binding.albumThumbnailView.bind(
|
||||
binding.albumThumbnailView.root.bind(
|
||||
glideRequests = glide,
|
||||
message = message,
|
||||
isStart = isStartOfMessageCluster,
|
||||
isEnd = isEndOfMessageCluster
|
||||
)
|
||||
val layoutParams = binding.albumThumbnailView.layoutParams as ConstraintLayout.LayoutParams
|
||||
val layoutParams = binding.albumThumbnailView.root.layoutParams as ConstraintLayout.LayoutParams
|
||||
layoutParams.horizontalBias = if (message.isOutgoing) 1f else 0f
|
||||
binding.albumThumbnailView.layoutParams = layoutParams
|
||||
binding.albumThumbnailView.root.layoutParams = layoutParams
|
||||
onContentClick.add { event ->
|
||||
binding.albumThumbnailView.calculateHitObject(event, message, thread)
|
||||
binding.albumThumbnailView.root.calculateHitObject(event, message, thread, onAttachmentNeedsDownload)
|
||||
}
|
||||
} else {
|
||||
hideBody = true
|
||||
binding.albumThumbnailView.clearViews()
|
||||
binding.albumThumbnailView.root.clearViews()
|
||||
binding.untrustedView.root.bind(UntrustedAttachmentView.AttachmentType.MEDIA, VisibleMessageContentView.getTextColor(context,message))
|
||||
onContentClick.add { binding.untrustedView.root.showTrustDialog(message.individualRecipient) }
|
||||
}
|
||||
@ -241,7 +237,7 @@ class VisibleMessageContentView : LinearLayout {
|
||||
}
|
||||
|
||||
private fun ViewVisibleMessageContentBinding.barrierViewsGone(): Boolean =
|
||||
listOf<View>(albumThumbnailView, linkPreviewView, voiceMessageView.root, quoteView.root).none { it.isVisible }
|
||||
listOf<View>(albumThumbnailView.root, linkPreviewView.root, voiceMessageView.root, quoteView.root).none { it.isVisible }
|
||||
|
||||
private fun getBackground(isOutgoing: Boolean): Drawable {
|
||||
val backgroundID = if (isOutgoing) R.drawable.message_bubble_background_sent_alone else R.drawable.message_bubble_background_received_alone
|
||||
@ -256,8 +252,8 @@ class VisibleMessageContentView : LinearLayout {
|
||||
binding.openGroupInvitationView.root,
|
||||
binding.documentView.root,
|
||||
binding.quoteView.root,
|
||||
binding.linkPreviewView,
|
||||
binding.albumThumbnailView,
|
||||
binding.linkPreviewView.root,
|
||||
binding.albumThumbnailView.root,
|
||||
binding.bodyTextView
|
||||
).forEach { view: View -> view.isVisible = false }
|
||||
}
|
||||
|
@ -85,7 +85,7 @@ class VisibleMessageView : LinearLayout {
|
||||
var onPress: ((event: MotionEvent) -> Unit)? = null
|
||||
var onSwipeToReply: (() -> Unit)? = null
|
||||
var onLongPress: (() -> Unit)? = null
|
||||
val messageContentView: VisibleMessageContentView by lazy { binding.messageContentView }
|
||||
val messageContentView: VisibleMessageContentView by lazy { binding.messageContentView.root }
|
||||
|
||||
companion object {
|
||||
const val swipeToReplyThreshold = 64.0f // dp
|
||||
@ -108,7 +108,7 @@ class VisibleMessageView : LinearLayout {
|
||||
isHapticFeedbackEnabled = true
|
||||
setWillNotDraw(false)
|
||||
binding.messageInnerContainer.disableClipping()
|
||||
binding.messageContentView.disableClipping()
|
||||
binding.messageContentView.root.disableClipping()
|
||||
}
|
||||
// endregion
|
||||
|
||||
@ -122,6 +122,7 @@ class VisibleMessageView : LinearLayout {
|
||||
contact: Contact?,
|
||||
senderSessionID: String,
|
||||
delegate: VisibleMessageViewDelegate?,
|
||||
onAttachmentNeedsDownload: (Long, Long) -> Unit
|
||||
) {
|
||||
val threadID = message.threadId
|
||||
val thread = threadDb.getRecipientForThreadId(threadID) ?: return
|
||||
@ -190,22 +191,23 @@ class VisibleMessageView : LinearLayout {
|
||||
binding.dateBreakTextView.text = if (showDateBreak) DateUtils.getDisplayFormattedTimeSpanString(context, Locale.getDefault(), message.timestamp) else null
|
||||
binding.dateBreakTextView.isVisible = showDateBreak
|
||||
// Message status indicator
|
||||
val (iconID, iconColor, textId) = getMessageStatusImage(message)
|
||||
if (textId != null) {
|
||||
binding.messageStatusTextView.setText(textId)
|
||||
|
||||
if (iconColor != null) {
|
||||
binding.messageStatusTextView.setTextColor(iconColor)
|
||||
}
|
||||
}
|
||||
if (iconID != null) {
|
||||
val drawable = ContextCompat.getDrawable(context, iconID)?.mutate()
|
||||
if (iconColor != null) {
|
||||
drawable?.setTint(iconColor)
|
||||
}
|
||||
binding.messageStatusImageView.setImageDrawable(drawable)
|
||||
}
|
||||
if (message.isOutgoing) {
|
||||
val (iconID, iconColor, textId) = getMessageStatusImage(message)
|
||||
if (textId != null) {
|
||||
binding.messageStatusTextView.setText(textId)
|
||||
|
||||
if (iconColor != null) {
|
||||
binding.messageStatusTextView.setTextColor(iconColor)
|
||||
}
|
||||
}
|
||||
if (iconID != null) {
|
||||
val drawable = ContextCompat.getDrawable(context, iconID)?.mutate()
|
||||
if (iconColor != null) {
|
||||
drawable?.setTint(iconColor)
|
||||
}
|
||||
binding.messageStatusImageView.setImageDrawable(drawable)
|
||||
}
|
||||
|
||||
val lastMessageID = mmsSmsDb.getLastMessageID(message.threadId)
|
||||
binding.messageStatusTextView.isVisible = (
|
||||
textId != null && (
|
||||
@ -226,32 +228,37 @@ class VisibleMessageView : LinearLayout {
|
||||
// Expiration timer
|
||||
updateExpirationTimer(message)
|
||||
// Emoji Reactions
|
||||
val emojiLayoutParams = binding.emojiReactionsView.layoutParams as ConstraintLayout.LayoutParams
|
||||
val emojiLayoutParams = binding.emojiReactionsView.root.layoutParams as ConstraintLayout.LayoutParams
|
||||
emojiLayoutParams.horizontalBias = if (message.isOutgoing) 1f else 0f
|
||||
binding.emojiReactionsView.layoutParams = emojiLayoutParams
|
||||
val capabilities = lokiThreadDb.getOpenGroupChat(threadID)?.server?.let { lokiApiDb.getServerCapabilities(it) }
|
||||
if (message.reactions.isNotEmpty() &&
|
||||
(capabilities.isNullOrEmpty() || capabilities.contains(OpenGroupApi.Capability.REACTIONS.name.lowercase()))
|
||||
) {
|
||||
binding.emojiReactionsView.setReactions(message.id, message.reactions, message.isOutgoing, delegate)
|
||||
binding.emojiReactionsView.isVisible = true
|
||||
} else {
|
||||
binding.emojiReactionsView.isVisible = false
|
||||
binding.emojiReactionsView.root.layoutParams = emojiLayoutParams
|
||||
|
||||
if (message.reactions.isNotEmpty()) {
|
||||
val capabilities = lokiThreadDb.getOpenGroupChat(threadID)?.server?.let { lokiApiDb.getServerCapabilities(it) }
|
||||
if (capabilities.isNullOrEmpty() || capabilities.contains(OpenGroupApi.Capability.REACTIONS.name.lowercase())) {
|
||||
binding.emojiReactionsView.root.setReactions(message.id, message.reactions, message.isOutgoing, delegate)
|
||||
binding.emojiReactionsView.root.isVisible = true
|
||||
} else {
|
||||
binding.emojiReactionsView.root.isVisible = false
|
||||
}
|
||||
}
|
||||
else {
|
||||
binding.emojiReactionsView.root.isVisible = false
|
||||
}
|
||||
|
||||
// Populate content view
|
||||
binding.messageContentView.indexInAdapter = indexInAdapter
|
||||
binding.messageContentView.bind(
|
||||
binding.messageContentView.root.indexInAdapter = indexInAdapter
|
||||
binding.messageContentView.root.bind(
|
||||
message,
|
||||
isStartOfMessageCluster,
|
||||
isEndOfMessageCluster,
|
||||
glide,
|
||||
thread,
|
||||
searchQuery,
|
||||
message.isOutgoing || isGroupThread || (contact?.isTrusted ?: false)
|
||||
message.isOutgoing || isGroupThread || (contact?.isTrusted ?: false),
|
||||
onAttachmentNeedsDownload
|
||||
)
|
||||
binding.messageContentView.delegate = delegate
|
||||
onDoubleTap = { binding.messageContentView.onContentDoubleTap?.invoke() }
|
||||
binding.messageContentView.root.delegate = delegate
|
||||
onDoubleTap = { binding.messageContentView.root.onContentDoubleTap?.invoke() }
|
||||
}
|
||||
|
||||
private fun isStartOfMessageCluster(current: MessageRecord, previous: MessageRecord?, isGroupThread: Boolean): Boolean {
|
||||
@ -290,7 +297,7 @@ class VisibleMessageView : LinearLayout {
|
||||
|
||||
private fun updateExpirationTimer(message: MessageRecord) {
|
||||
val container = binding.messageInnerContainer
|
||||
val content = binding.messageContentView
|
||||
val content = binding.messageContentView.root
|
||||
val expiration = binding.expirationTimerView
|
||||
val spacing = binding.messageContentSpacing
|
||||
container.removeAllViewsInLayout()
|
||||
@ -341,7 +348,7 @@ class VisibleMessageView : LinearLayout {
|
||||
override fun onDraw(canvas: Canvas) {
|
||||
val spacing = context.resources.getDimensionPixelSize(R.dimen.small_spacing)
|
||||
val iconSize = toPx(24, context.resources)
|
||||
val left = binding.messageInnerContainer.left + binding.messageContentView.right + spacing
|
||||
val left = binding.messageInnerContainer.left + binding.messageContentView.root.right + spacing
|
||||
val top = height - (binding.messageInnerContainer.height / 2) - binding.profilePictureView.root.marginBottom - (iconSize / 2)
|
||||
val right = left + iconSize
|
||||
val bottom = top + iconSize
|
||||
@ -363,7 +370,7 @@ class VisibleMessageView : LinearLayout {
|
||||
|
||||
fun recycle() {
|
||||
binding.profilePictureView.root.recycle()
|
||||
binding.messageContentView.recycle()
|
||||
binding.messageContentView.root.recycle()
|
||||
}
|
||||
// endregion
|
||||
|
||||
@ -459,7 +466,7 @@ class VisibleMessageView : LinearLayout {
|
||||
}
|
||||
|
||||
fun onContentClick(event: MotionEvent) {
|
||||
binding.messageContentView.onContentClick.iterator().forEach { clickHandler -> clickHandler.invoke(event) }
|
||||
binding.messageContentView.root.onContentClick.iterator().forEach { clickHandler -> clickHandler.invoke(event) }
|
||||
}
|
||||
|
||||
private fun onPress(event: MotionEvent) {
|
||||
@ -479,7 +486,7 @@ class VisibleMessageView : LinearLayout {
|
||||
}
|
||||
|
||||
fun playVoiceMessage() {
|
||||
binding.messageContentView.playVoiceMessage()
|
||||
binding.messageContentView.root.playVoiceMessage()
|
||||
}
|
||||
// endregion
|
||||
}
|
||||
|
@ -1,425 +0,0 @@
|
||||
package org.thoughtcrime.securesms.conversation.v2.utilities;
|
||||
|
||||
import static com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions.withCrossFade;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.res.TypedArray;
|
||||
import android.net.Uri;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.FrameLayout;
|
||||
import android.widget.ImageView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.UiThread;
|
||||
|
||||
import com.bumptech.glide.RequestBuilder;
|
||||
import com.bumptech.glide.load.engine.DiskCacheStrategy;
|
||||
import com.bumptech.glide.load.resource.bitmap.BitmapTransformation;
|
||||
import com.bumptech.glide.load.resource.bitmap.CenterCrop;
|
||||
import com.bumptech.glide.load.resource.bitmap.FitCenter;
|
||||
import com.bumptech.glide.load.resource.bitmap.RoundedCorners;
|
||||
import com.bumptech.glide.request.RequestOptions;
|
||||
|
||||
import org.session.libsession.messaging.sending_receiving.attachments.AttachmentTransferProgress;
|
||||
import org.session.libsession.utilities.Util;
|
||||
import org.session.libsession.utilities.ViewUtil;
|
||||
import org.session.libsignal.utilities.ListenableFuture;
|
||||
import org.session.libsignal.utilities.Log;
|
||||
import org.session.libsignal.utilities.SettableFuture;
|
||||
import org.session.libsignal.utilities.guava.Optional;
|
||||
import org.thoughtcrime.securesms.components.GlideBitmapListeningTarget;
|
||||
import org.thoughtcrime.securesms.components.GlideDrawableListeningTarget;
|
||||
import org.thoughtcrime.securesms.components.TransferControlView;
|
||||
import org.thoughtcrime.securesms.mms.DecryptableStreamUriLoader.DecryptableUri;
|
||||
import org.thoughtcrime.securesms.mms.GlideRequest;
|
||||
import org.thoughtcrime.securesms.mms.GlideRequests;
|
||||
import org.thoughtcrime.securesms.mms.Slide;
|
||||
import org.thoughtcrime.securesms.mms.SlideClickListener;
|
||||
import org.thoughtcrime.securesms.mms.SlidesClickedListener;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.Locale;
|
||||
|
||||
import network.loki.messenger.R;
|
||||
|
||||
public class ThumbnailView extends FrameLayout {
|
||||
|
||||
private static final String TAG = ThumbnailView.class.getSimpleName();
|
||||
private static final int WIDTH = 0;
|
||||
private static final int HEIGHT = 1;
|
||||
private static final int MIN_WIDTH = 0;
|
||||
private static final int MAX_WIDTH = 1;
|
||||
private static final int MIN_HEIGHT = 2;
|
||||
private static final int MAX_HEIGHT = 3;
|
||||
|
||||
private ImageView image;
|
||||
private View playOverlay;
|
||||
private View loadIndicator;
|
||||
private OnClickListener parentClickListener;
|
||||
|
||||
private final int[] dimens = new int[2];
|
||||
private final int[] bounds = new int[4];
|
||||
private final int[] measureDimens = new int[2];
|
||||
|
||||
private Optional<TransferControlView> transferControls = Optional.absent();
|
||||
private SlideClickListener thumbnailClickListener = null;
|
||||
private SlidesClickedListener downloadClickListener = null;
|
||||
private Slide slide = null;
|
||||
|
||||
public int radius;
|
||||
|
||||
public ThumbnailView(Context context) {
|
||||
this(context, null);
|
||||
}
|
||||
|
||||
public ThumbnailView(Context context, AttributeSet attrs) {
|
||||
this(context, attrs, 0);
|
||||
}
|
||||
|
||||
public ThumbnailView(final Context context, AttributeSet attrs, int defStyle) {
|
||||
super(context, attrs, defStyle);
|
||||
|
||||
inflate(context, R.layout.thumbnail_view, this);
|
||||
|
||||
this.image = findViewById(R.id.thumbnail_image);
|
||||
this.playOverlay = findViewById(R.id.play_overlay);
|
||||
this.loadIndicator = findViewById(R.id.thumbnail_load_indicator);
|
||||
super.setOnClickListener(new ThumbnailClickDispatcher());
|
||||
|
||||
if (attrs != null) {
|
||||
TypedArray typedArray = context.getTheme().obtainStyledAttributes(attrs, R.styleable.ThumbnailView, 0, 0);
|
||||
bounds[MIN_WIDTH] = typedArray.getDimensionPixelSize(R.styleable.ThumbnailView_minWidth, 0);
|
||||
bounds[MAX_WIDTH] = typedArray.getDimensionPixelSize(R.styleable.ThumbnailView_maxWidth, 0);
|
||||
bounds[MIN_HEIGHT] = typedArray.getDimensionPixelSize(R.styleable.ThumbnailView_minHeight, 0);
|
||||
bounds[MAX_HEIGHT] = typedArray.getDimensionPixelSize(R.styleable.ThumbnailView_maxHeight, 0);
|
||||
radius = typedArray.getDimensionPixelSize(R.styleable.ThumbnailView_thumbnail_radius, 0);
|
||||
typedArray.recycle();
|
||||
} else {
|
||||
radius = 0;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onMeasure(int originalWidthMeasureSpec, int originalHeightMeasureSpec) {
|
||||
fillTargetDimensions(measureDimens, dimens, bounds);
|
||||
if (measureDimens[WIDTH] == 0 && measureDimens[HEIGHT] == 0) {
|
||||
super.onMeasure(originalWidthMeasureSpec, originalHeightMeasureSpec);
|
||||
return;
|
||||
}
|
||||
|
||||
int finalWidth = measureDimens[WIDTH] + getPaddingLeft() + getPaddingRight();
|
||||
int finalHeight = measureDimens[HEIGHT] + getPaddingTop() + getPaddingBottom();
|
||||
|
||||
super.onMeasure(MeasureSpec.makeMeasureSpec(finalWidth, MeasureSpec.EXACTLY),
|
||||
MeasureSpec.makeMeasureSpec(finalHeight, MeasureSpec.EXACTLY));
|
||||
}
|
||||
|
||||
@SuppressWarnings("SuspiciousNameCombination")
|
||||
private void fillTargetDimensions(int[] targetDimens, int[] dimens, int[] bounds) {
|
||||
int dimensFilledCount = getNonZeroCount(dimens);
|
||||
int boundsFilledCount = getNonZeroCount(bounds);
|
||||
|
||||
if (dimensFilledCount == 0 || boundsFilledCount == 0) {
|
||||
targetDimens[WIDTH] = 0;
|
||||
targetDimens[HEIGHT] = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
double naturalWidth = dimens[WIDTH];
|
||||
double naturalHeight = dimens[HEIGHT];
|
||||
|
||||
int minWidth = bounds[MIN_WIDTH];
|
||||
int maxWidth = bounds[MAX_WIDTH];
|
||||
int minHeight = bounds[MIN_HEIGHT];
|
||||
int maxHeight = bounds[MAX_HEIGHT];
|
||||
|
||||
if (dimensFilledCount > 0 && dimensFilledCount < dimens.length) {
|
||||
throw new IllegalStateException(String.format(Locale.ENGLISH, "Width or height has been specified, but not both. Dimens: %f x %f",
|
||||
naturalWidth, naturalHeight));
|
||||
}
|
||||
if (boundsFilledCount > 0 && boundsFilledCount < bounds.length) {
|
||||
throw new IllegalStateException(String.format(Locale.ENGLISH, "One or more min/max dimensions have been specified, but not all. Bounds: [%d, %d, %d, %d]",
|
||||
minWidth, maxWidth, minHeight, maxHeight));
|
||||
}
|
||||
|
||||
double measuredWidth = naturalWidth;
|
||||
double measuredHeight = naturalHeight;
|
||||
|
||||
boolean widthInBounds = measuredWidth >= minWidth && measuredWidth <= maxWidth;
|
||||
boolean heightInBounds = measuredHeight >= minHeight && measuredHeight <= maxHeight;
|
||||
|
||||
if (!widthInBounds || !heightInBounds) {
|
||||
double minWidthRatio = naturalWidth / minWidth;
|
||||
double maxWidthRatio = naturalWidth / maxWidth;
|
||||
double minHeightRatio = naturalHeight / minHeight;
|
||||
double maxHeightRatio = naturalHeight / maxHeight;
|
||||
|
||||
if (maxWidthRatio > 1 || maxHeightRatio > 1) {
|
||||
if (maxWidthRatio >= maxHeightRatio) {
|
||||
measuredWidth /= maxWidthRatio;
|
||||
measuredHeight /= maxWidthRatio;
|
||||
} else {
|
||||
measuredWidth /= maxHeightRatio;
|
||||
measuredHeight /= maxHeightRatio;
|
||||
}
|
||||
|
||||
measuredWidth = Math.max(measuredWidth, minWidth);
|
||||
measuredHeight = Math.max(measuredHeight, minHeight);
|
||||
|
||||
} else if (minWidthRatio < 1 || minHeightRatio < 1) {
|
||||
if (minWidthRatio <= minHeightRatio) {
|
||||
measuredWidth /= minWidthRatio;
|
||||
measuredHeight /= minWidthRatio;
|
||||
} else {
|
||||
measuredWidth /= minHeightRatio;
|
||||
measuredHeight /= minHeightRatio;
|
||||
}
|
||||
|
||||
measuredWidth = Math.min(measuredWidth, maxWidth);
|
||||
measuredHeight = Math.min(measuredHeight, maxHeight);
|
||||
}
|
||||
}
|
||||
|
||||
targetDimens[WIDTH] = (int) measuredWidth;
|
||||
targetDimens[HEIGHT] = (int) measuredHeight;
|
||||
}
|
||||
|
||||
private int getNonZeroCount(int[] vals) {
|
||||
int count = 0;
|
||||
for (int val : vals) {
|
||||
if (val > 0) {
|
||||
count++;
|
||||
}
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setOnClickListener(OnClickListener l) {
|
||||
parentClickListener = l;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setFocusable(boolean focusable) {
|
||||
super.setFocusable(focusable);
|
||||
if (transferControls.isPresent()) transferControls.get().setFocusable(focusable);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setClickable(boolean clickable) {
|
||||
super.setClickable(clickable);
|
||||
if (transferControls.isPresent()) transferControls.get().setClickable(clickable);
|
||||
}
|
||||
|
||||
private TransferControlView getTransferControls() {
|
||||
if (!transferControls.isPresent()) {
|
||||
transferControls = Optional.of(ViewUtil.inflateStub(this, R.id.transfer_controls_stub));
|
||||
}
|
||||
return transferControls.get();
|
||||
}
|
||||
|
||||
public void setBounds(int minWidth, int maxWidth, int minHeight, int maxHeight) {
|
||||
bounds[MIN_WIDTH] = minWidth;
|
||||
bounds[MAX_WIDTH] = maxWidth;
|
||||
bounds[MIN_HEIGHT] = minHeight;
|
||||
bounds[MAX_HEIGHT] = maxHeight;
|
||||
|
||||
forceLayout();
|
||||
}
|
||||
|
||||
@UiThread
|
||||
public ListenableFuture<Boolean> setImageResource(@NonNull GlideRequests glideRequests, @NonNull Slide slide,
|
||||
boolean showControls, boolean isPreview)
|
||||
{
|
||||
return setImageResource(glideRequests, slide, showControls, isPreview, 0, 0);
|
||||
}
|
||||
|
||||
@UiThread
|
||||
public ListenableFuture<Boolean> setImageResource(@NonNull GlideRequests glideRequests, @NonNull Slide slide,
|
||||
boolean showControls, boolean isPreview,
|
||||
int naturalWidth, int naturalHeight)
|
||||
{
|
||||
if (showControls) {
|
||||
getTransferControls().setSlide(slide);
|
||||
getTransferControls().setDownloadClickListener(new DownloadClickDispatcher());
|
||||
} else if (transferControls.isPresent()) {
|
||||
getTransferControls().setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
if (slide.getThumbnailUri() != null && slide.hasPlayOverlay() &&
|
||||
(slide.getTransferState() == AttachmentTransferProgress.TRANSFER_PROGRESS_DONE || isPreview))
|
||||
{
|
||||
this.playOverlay.setVisibility(View.VISIBLE);
|
||||
} else {
|
||||
this.playOverlay.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
if (Util.equals(slide, this.slide)) {
|
||||
Log.i(TAG, "Not re-loading slide " + slide.asAttachment().getDataUri());
|
||||
return new SettableFuture<>(false);
|
||||
}
|
||||
|
||||
if (this.slide != null && this.slide.getFastPreflightId() != null &&
|
||||
this.slide.getFastPreflightId().equals(slide.getFastPreflightId()))
|
||||
{
|
||||
Log.i(TAG, "Not re-loading slide for fast preflight: " + slide.getFastPreflightId());
|
||||
this.slide = slide;
|
||||
return new SettableFuture<>(false);
|
||||
}
|
||||
|
||||
Log.i(TAG, "loading part with id " + slide.asAttachment().getDataUri()
|
||||
+ ", progress " + slide.getTransferState() + ", fast preflight id: " +
|
||||
slide.asAttachment().getFastPreflightId());
|
||||
|
||||
this.slide = slide;
|
||||
|
||||
dimens[WIDTH] = naturalWidth;
|
||||
dimens[HEIGHT] = naturalHeight;
|
||||
invalidate();
|
||||
|
||||
SettableFuture<Boolean> result = new SettableFuture<>();
|
||||
|
||||
if (slide.getThumbnailUri() != null) {
|
||||
buildThumbnailGlideRequest(glideRequests, slide).into(new GlideDrawableListeningTarget(image, result));
|
||||
} else if (slide.hasPlaceholder()) {
|
||||
buildPlaceholderGlideRequest(glideRequests, slide).into(new GlideBitmapListeningTarget(image, result));
|
||||
} else {
|
||||
glideRequests.load(R.drawable.ic_image_white_24dp).centerInside().into(image);
|
||||
result.set(false);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public ListenableFuture<Boolean> setImageResource(@NonNull GlideRequests glideRequests, @NonNull Uri uri) {
|
||||
SettableFuture<Boolean> future = new SettableFuture<>();
|
||||
|
||||
if (transferControls.isPresent()) getTransferControls().setVisibility(View.GONE);
|
||||
|
||||
GlideRequest request = glideRequests.load(new DecryptableUri(uri))
|
||||
.diskCacheStrategy(DiskCacheStrategy.NONE)
|
||||
.transition(withCrossFade());
|
||||
|
||||
if (radius > 0) {
|
||||
request = request.transforms(new CenterCrop(), new RoundedCorners(radius));
|
||||
} else {
|
||||
request = request.transforms(new CenterCrop());
|
||||
}
|
||||
|
||||
request.into(new GlideDrawableListeningTarget(image, future));
|
||||
|
||||
return future;
|
||||
}
|
||||
|
||||
public void setThumbnailClickListener(SlideClickListener listener) {
|
||||
this.thumbnailClickListener = listener;
|
||||
}
|
||||
|
||||
public void setDownloadClickListener(SlidesClickedListener listener) {
|
||||
this.downloadClickListener = listener;
|
||||
}
|
||||
|
||||
public void clear(GlideRequests glideRequests) {
|
||||
glideRequests.clear(image);
|
||||
|
||||
if (transferControls.isPresent()) {
|
||||
getTransferControls().clear();
|
||||
}
|
||||
|
||||
slide = null;
|
||||
}
|
||||
|
||||
public void showDownloadText(boolean showDownloadText) {
|
||||
getTransferControls().setShowDownloadText(showDownloadText);
|
||||
}
|
||||
|
||||
public void showProgressSpinner() {
|
||||
getTransferControls().showProgressSpinner();
|
||||
}
|
||||
|
||||
public void setLoadIndicatorVisibile(boolean visible) {
|
||||
this.loadIndicator.setVisibility(visible ? VISIBLE : GONE);
|
||||
}
|
||||
|
||||
protected void setRadius(int radius) {
|
||||
this.radius = radius;
|
||||
}
|
||||
|
||||
private GlideRequest buildThumbnailGlideRequest(@NonNull GlideRequests glideRequests, @NonNull Slide slide) {
|
||||
GlideRequest request = applySizing(glideRequests.load(new DecryptableUri(slide.getThumbnailUri()))
|
||||
.diskCacheStrategy(DiskCacheStrategy.NONE)
|
||||
.transition(withCrossFade()), new CenterCrop());
|
||||
|
||||
if (slide.isInProgress()) return request;
|
||||
else return request.apply(RequestOptions.errorOf(R.drawable.ic_missing_thumbnail_picture));
|
||||
}
|
||||
|
||||
private RequestBuilder buildPlaceholderGlideRequest(@NonNull GlideRequests glideRequests, @NonNull Slide slide) {
|
||||
return applySizing(glideRequests.asBitmap()
|
||||
.load(slide.getPlaceholderRes(getContext().getTheme()))
|
||||
.diskCacheStrategy(DiskCacheStrategy.NONE), new FitCenter());
|
||||
}
|
||||
|
||||
private GlideRequest applySizing(@NonNull GlideRequest request, @NonNull BitmapTransformation fitting) {
|
||||
int[] size = new int[2];
|
||||
fillTargetDimensions(size, dimens, bounds);
|
||||
if (size[WIDTH] == 0 && size[HEIGHT] == 0) {
|
||||
size[WIDTH] = getDefaultWidth();
|
||||
size[HEIGHT] = getDefaultHeight();
|
||||
}
|
||||
|
||||
request = request.override(size[WIDTH], size[HEIGHT]);
|
||||
|
||||
if (radius > 0) {
|
||||
return request.transforms(fitting, new RoundedCorners(radius));
|
||||
} else {
|
||||
return request.transforms(fitting);
|
||||
}
|
||||
}
|
||||
|
||||
private int getDefaultWidth() {
|
||||
ViewGroup.LayoutParams params = getLayoutParams();
|
||||
if (params != null) {
|
||||
return Math.max(params.width, 0);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
private int getDefaultHeight() {
|
||||
ViewGroup.LayoutParams params = getLayoutParams();
|
||||
if (params != null) {
|
||||
return Math.max(params.height, 0);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
private class ThumbnailClickDispatcher implements View.OnClickListener {
|
||||
|
||||
@Override
|
||||
public void onClick(View view) {
|
||||
if (thumbnailClickListener != null &&
|
||||
slide != null &&
|
||||
slide.asAttachment().getDataUri() != null &&
|
||||
slide.getTransferState() == AttachmentTransferProgress.TRANSFER_PROGRESS_DONE)
|
||||
{
|
||||
thumbnailClickListener.onClick(view, slide);
|
||||
} else if (parentClickListener != null) {
|
||||
parentClickListener.onClick(view);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class DownloadClickDispatcher implements View.OnClickListener {
|
||||
|
||||
@Override
|
||||
public void onClick(View view) {
|
||||
if (downloadClickListener != null && slide != null) {
|
||||
downloadClickListener.onClick(view, Collections.singletonList(slide));
|
||||
} else {
|
||||
Log.w(TAG, "Received a download button click, but unable to execute it. slide: " + String.valueOf(slide) + " downloadClickListener: " + String.valueOf(downloadClickListener));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -2,14 +2,11 @@ package org.thoughtcrime.securesms.conversation.v2.utilities
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.drawable.ColorDrawable
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.net.Uri
|
||||
import android.util.AttributeSet
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.widget.FrameLayout
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.view.isVisible
|
||||
import com.bumptech.glide.load.engine.DiskCacheStrategy
|
||||
import com.bumptech.glide.load.resource.bitmap.CenterCrop
|
||||
@ -29,31 +26,33 @@ import org.thoughtcrime.securesms.mms.DecryptableStreamUriLoader.DecryptableUri
|
||||
import org.thoughtcrime.securesms.mms.GlideRequest
|
||||
import org.thoughtcrime.securesms.mms.GlideRequests
|
||||
import org.thoughtcrime.securesms.mms.Slide
|
||||
import kotlin.Boolean
|
||||
import kotlin.Int
|
||||
import kotlin.getValue
|
||||
import kotlin.lazy
|
||||
import kotlin.let
|
||||
|
||||
open class KThumbnailView: FrameLayout {
|
||||
private lateinit var binding: ThumbnailViewBinding
|
||||
open class ThumbnailView: FrameLayout {
|
||||
companion object {
|
||||
private const val WIDTH = 0
|
||||
private const val HEIGHT = 1
|
||||
}
|
||||
|
||||
private val binding: ThumbnailViewBinding by lazy { ThumbnailViewBinding.bind(this) }
|
||||
|
||||
// region Lifecycle
|
||||
constructor(context: Context) : super(context) { initialize(null) }
|
||||
constructor(context: Context, attrs: AttributeSet) : super(context, attrs) { initialize(attrs) }
|
||||
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr) { initialize(attrs) }
|
||||
|
||||
private val image by lazy { binding.thumbnailImage }
|
||||
private val playOverlay by lazy { binding.playOverlay }
|
||||
val loadIndicator: View by lazy { binding.thumbnailLoadIndicator }
|
||||
val downloadIndicator: View by lazy { binding.thumbnailDownloadIcon }
|
||||
|
||||
private val dimensDelegate = ThumbnailDimensDelegate()
|
||||
|
||||
private var slide: Slide? = null
|
||||
private var radius: Int = 0
|
||||
var radius: Int = 0
|
||||
|
||||
private fun initialize(attrs: AttributeSet?) {
|
||||
binding = ThumbnailViewBinding.inflate(LayoutInflater.from(context), this)
|
||||
if (attrs != null) {
|
||||
val typedArray = context.theme.obtainStyledAttributes(attrs, R.styleable.ThumbnailView, 0, 0)
|
||||
|
||||
@ -66,8 +65,6 @@ open class KThumbnailView: FrameLayout {
|
||||
|
||||
typedArray.recycle()
|
||||
}
|
||||
val background = ContextCompat.getColor(context, R.color.transparent_black_6)
|
||||
binding.root.background = ColorDrawable(background)
|
||||
}
|
||||
|
||||
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
|
||||
@ -80,8 +77,8 @@ open class KThumbnailView: FrameLayout {
|
||||
val finalHeight: Int = adjustedDimens[HEIGHT] + paddingTop + paddingBottom
|
||||
|
||||
super.onMeasure(
|
||||
MeasureSpec.makeMeasureSpec(finalWidth, MeasureSpec.EXACTLY),
|
||||
MeasureSpec.makeMeasureSpec(finalHeight, MeasureSpec.EXACTLY)
|
||||
MeasureSpec.makeMeasureSpec(finalWidth, MeasureSpec.EXACTLY),
|
||||
MeasureSpec.makeMeasureSpec(finalHeight, MeasureSpec.EXACTLY)
|
||||
)
|
||||
}
|
||||
|
||||
@ -90,17 +87,17 @@ open class KThumbnailView: FrameLayout {
|
||||
// endregion
|
||||
|
||||
// region Interaction
|
||||
fun setImageResource(glide: GlideRequests, slide: Slide, isPreview: Boolean, mms: MmsMessageRecord): ListenableFuture<Boolean> {
|
||||
fun setImageResource(glide: GlideRequests, slide: Slide, isPreview: Boolean, mms: MmsMessageRecord?): ListenableFuture<Boolean> {
|
||||
return setImageResource(glide, slide, isPreview, 0, 0, mms)
|
||||
}
|
||||
|
||||
fun setImageResource(glide: GlideRequests, slide: Slide,
|
||||
isPreview: Boolean, naturalWidth: Int,
|
||||
naturalHeight: Int, mms: MmsMessageRecord): ListenableFuture<Boolean> {
|
||||
naturalHeight: Int, mms: MmsMessageRecord?): ListenableFuture<Boolean> {
|
||||
|
||||
val currentSlide = this.slide
|
||||
|
||||
playOverlay.isVisible = (slide.thumbnailUri != null && slide.hasPlayOverlay() &&
|
||||
binding.playOverlay.isVisible = (slide.thumbnailUri != null && slide.hasPlayOverlay() &&
|
||||
(slide.transferState == AttachmentTransferProgress.TRANSFER_PROGRESS_DONE || isPreview))
|
||||
|
||||
if (equals(currentSlide, slide)) {
|
||||
@ -116,8 +113,8 @@ open class KThumbnailView: FrameLayout {
|
||||
|
||||
this.slide = slide
|
||||
|
||||
loadIndicator.isVisible = slide.isInProgress
|
||||
downloadIndicator.isVisible = slide.transferState == AttachmentTransferProgress.TRANSFER_PROGRESS_FAILED
|
||||
binding.thumbnailLoadIndicator.isVisible = slide.isInProgress
|
||||
binding.thumbnailDownloadIcon.isVisible = slide.transferState == AttachmentTransferProgress.TRANSFER_PROGRESS_FAILED
|
||||
|
||||
dimensDelegate.setDimens(naturalWidth, naturalHeight)
|
||||
invalidate()
|
||||
@ -126,13 +123,13 @@ open class KThumbnailView: FrameLayout {
|
||||
|
||||
when {
|
||||
slide.thumbnailUri != null -> {
|
||||
buildThumbnailGlideRequest(glide, slide).into(GlideDrawableListeningTarget(image, result))
|
||||
buildThumbnailGlideRequest(glide, slide).into(GlideDrawableListeningTarget(binding.thumbnailImage, result))
|
||||
}
|
||||
slide.hasPlaceholder() -> {
|
||||
buildPlaceholderGlideRequest(glide, slide).into(GlideBitmapListeningTarget(image, result))
|
||||
buildPlaceholderGlideRequest(glide, slide).into(GlideBitmapListeningTarget(binding.thumbnailImage, result))
|
||||
}
|
||||
else -> {
|
||||
glide.clear(image)
|
||||
glide.clear(binding.thumbnailImage)
|
||||
result.set(false)
|
||||
}
|
||||
}
|
||||
@ -176,7 +173,7 @@ open class KThumbnailView: FrameLayout {
|
||||
}
|
||||
|
||||
open fun clear(glideRequests: GlideRequests) {
|
||||
glideRequests.clear(image)
|
||||
glideRequests.clear(binding.thumbnailImage)
|
||||
slide = null
|
||||
}
|
||||
|
||||
@ -193,11 +190,8 @@ open class KThumbnailView: FrameLayout {
|
||||
request.transforms(CenterCrop())
|
||||
}
|
||||
|
||||
request.into(GlideDrawableListeningTarget(image, future))
|
||||
request.into(GlideDrawableListeningTarget(binding.thumbnailImage, future))
|
||||
|
||||
return future
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
||||
}
|
@ -33,8 +33,9 @@ import androidx.annotation.VisibleForTesting;
|
||||
|
||||
import com.bumptech.glide.Glide;
|
||||
|
||||
import net.sqlcipher.database.SQLiteDatabase;
|
||||
import net.zetetic.database.sqlcipher.SQLiteDatabase;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.json.JSONArray;
|
||||
import org.json.JSONException;
|
||||
import org.session.libsession.messaging.sending_receiving.attachments.Attachment;
|
||||
@ -318,6 +319,28 @@ public class AttachmentDatabase extends Database {
|
||||
notifyAttachmentListeners();
|
||||
}
|
||||
|
||||
@SuppressWarnings("ResultOfMethodCallIgnored")
|
||||
void deleteAttachmentsForMessages(long[] mmsIds) {
|
||||
SQLiteDatabase database = databaseHelper.getWritableDatabase();
|
||||
Cursor cursor = null;
|
||||
String mmsIdString = StringUtils.join(mmsIds, ',');
|
||||
|
||||
try {
|
||||
cursor = database.query(TABLE_NAME, new String[] {DATA, THUMBNAIL, CONTENT_TYPE}, MMS_ID + " IN (?)",
|
||||
new String[] {mmsIdString}, null, null, null);
|
||||
|
||||
while (cursor != null && cursor.moveToNext()) {
|
||||
deleteAttachmentOnDisk(cursor.getString(0), cursor.getString(1), cursor.getString(2));
|
||||
}
|
||||
} finally {
|
||||
if (cursor != null)
|
||||
cursor.close();
|
||||
}
|
||||
|
||||
database.delete(TABLE_NAME, MMS_ID + " IN (?)", new String[] {mmsIdString});
|
||||
notifyAttachmentListeners();
|
||||
}
|
||||
|
||||
public void deleteAttachment(@NonNull AttachmentId id) {
|
||||
SQLiteDatabase database = databaseHelper.getWritableDatabase();
|
||||
|
||||
|
@ -23,7 +23,7 @@ import android.database.Cursor;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import net.sqlcipher.database.SQLiteDatabase;
|
||||
import net.zetetic.database.sqlcipher.SQLiteDatabase;
|
||||
|
||||
import org.session.libsession.utilities.WindowDebouncer;
|
||||
import org.thoughtcrime.securesms.ApplicationContext;
|
||||
|
@ -19,7 +19,7 @@ package org.thoughtcrime.securesms.database;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import net.sqlcipher.database.SQLiteDatabase;
|
||||
import net.zetetic.database.sqlcipher.SQLiteDatabase;
|
||||
|
||||
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper;
|
||||
import org.thoughtcrime.securesms.dependencies.DatabaseComponent;
|
||||
|
@ -1,9 +1,9 @@
|
||||
package org.thoughtcrime.securesms.database
|
||||
|
||||
import android.content.ContentValues
|
||||
import android.database.Cursor
|
||||
import androidx.core.database.getStringOrNull
|
||||
import net.sqlcipher.Cursor
|
||||
import net.sqlcipher.database.SQLiteDatabase
|
||||
import net.zetetic.database.sqlcipher.SQLiteDatabase
|
||||
import org.session.libsignal.utilities.Base64
|
||||
|
||||
fun <T> SQLiteDatabase.get(table: String, query: String?, arguments: Array<String>?, get: (Cursor) -> T): T? {
|
||||
|
@ -6,7 +6,7 @@ import android.database.Cursor;
|
||||
import android.net.Uri;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import net.sqlcipher.database.SQLiteDatabase;
|
||||
import net.zetetic.database.sqlcipher.SQLiteDatabase;
|
||||
|
||||
import network.loki.messenger.R;
|
||||
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper;
|
||||
|
@ -1,6 +1,5 @@
|
||||
package org.thoughtcrime.securesms.database;
|
||||
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.content.ContentValues;
|
||||
import android.content.Context;
|
||||
@ -12,7 +11,7 @@ import androidx.annotation.Nullable;
|
||||
|
||||
import com.annimon.stream.Stream;
|
||||
|
||||
import net.sqlcipher.database.SQLiteDatabase;
|
||||
import net.zetetic.database.sqlcipher.SQLiteDatabase;
|
||||
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.session.libsession.utilities.Address;
|
||||
@ -319,6 +318,19 @@ public class GroupDatabase extends Database implements LokiOpenGroupDatabaseProt
|
||||
notifyConversationListListeners();
|
||||
}
|
||||
|
||||
public boolean hasDownloadedProfilePicture(String groupId) {
|
||||
try (Cursor cursor = databaseHelper.getReadableDatabase().query(TABLE_NAME, new String[]{AVATAR}, GROUP_ID + " = ?",
|
||||
new String[] {groupId},
|
||||
null, null, null))
|
||||
{
|
||||
if (cursor != null && cursor.moveToNext()) {
|
||||
return !cursor.isNull(0);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public void updateMembers(String groupId, List<Address> members) {
|
||||
Collections.sort(members);
|
||||
|
||||
|
@ -1,14 +1,14 @@
|
||||
package org.thoughtcrime.securesms.database;
|
||||
|
||||
|
||||
import android.content.ContentValues;
|
||||
import android.content.Context;
|
||||
import android.database.Cursor;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import net.sqlcipher.database.SQLiteDatabase;
|
||||
import net.zetetic.database.sqlcipher.SQLiteDatabase;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.session.libsession.utilities.Address;
|
||||
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper;
|
||||
|
||||
@ -110,6 +110,11 @@ public class GroupReceiptDatabase extends Database {
|
||||
db.delete(TABLE_NAME, MMS_ID + " = ?", new String[] {String.valueOf(mmsId)});
|
||||
}
|
||||
|
||||
void deleteRowsForMessages(long[] mmsIds) {
|
||||
SQLiteDatabase db = databaseHelper.getWritableDatabase();
|
||||
db.delete(TABLE_NAME, MMS_ID + " IN (?)", new String[] {StringUtils.join(mmsIds, ',')});
|
||||
}
|
||||
|
||||
void deleteAllRows() {
|
||||
SQLiteDatabase db = databaseHelper.getWritableDatabase();
|
||||
db.delete(TABLE_NAME, null, null);
|
||||
|
@ -5,7 +5,7 @@ import android.content.Context;
|
||||
import android.database.Cursor;
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import net.sqlcipher.database.SQLiteDatabase;
|
||||
import net.zetetic.database.sqlcipher.SQLiteDatabase;
|
||||
|
||||
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper;
|
||||
import org.thoughtcrime.securesms.jobmanager.persistence.ConstraintSpec;
|
||||
|
@ -300,6 +300,11 @@ class LokiAPIDatabase(context: Context, helper: SQLCipherOpenHelper) : Database(
|
||||
val lastHash = database.insertOrUpdate(lastMessageHashValueTable2, row, query, arrayOf( snode.toString(), publicKey, namespace.toString() ))
|
||||
}
|
||||
|
||||
override fun clearAllLastMessageHashes() {
|
||||
val database = databaseHelper.writableDatabase
|
||||
database.delete(lastMessageHashValueTable2, null, null)
|
||||
}
|
||||
|
||||
override fun getReceivedMessageHashValues(publicKey: String, namespace: Int): Set<String>? {
|
||||
val database = databaseHelper.readableDatabase
|
||||
val query = "${Companion.publicKey} = ? AND ${Companion.receivedMessageHashNamespace} = ?"
|
||||
@ -321,6 +326,11 @@ class LokiAPIDatabase(context: Context, helper: SQLCipherOpenHelper) : Database(
|
||||
database.insertOrUpdate(receivedMessageHashValuesTable, row, query, arrayOf( publicKey, namespace.toString() ))
|
||||
}
|
||||
|
||||
override fun clearReceivedMessageHashValues() {
|
||||
val database = databaseHelper.writableDatabase
|
||||
database.delete(receivedMessageHashValuesTable, null, null)
|
||||
}
|
||||
|
||||
override fun getAuthToken(server: String): String? {
|
||||
val database = databaseHelper.readableDatabase
|
||||
return database.get(openGroupAuthTokenTable, "${Companion.server} = ?", wrap(server)) { cursor ->
|
||||
@ -339,7 +349,7 @@ class LokiAPIDatabase(context: Context, helper: SQLCipherOpenHelper) : Database(
|
||||
}
|
||||
|
||||
override fun getLastMessageServerID(room: String, server: String): Long? {
|
||||
val database = databaseHelper.writableDatabase
|
||||
val database = databaseHelper.readableDatabase
|
||||
val index = "$server.$room"
|
||||
return database.get(lastMessageServerIDTable, "$lastMessageServerIDTableIndex = ?", wrap(index)) { cursor ->
|
||||
cursor.getInt(lastMessageServerID)
|
||||
@ -510,7 +520,7 @@ class LokiAPIDatabase(context: Context, helper: SQLCipherOpenHelper) : Database(
|
||||
}
|
||||
|
||||
fun getServerCapabilities(serverName: String): List<String> {
|
||||
val database = databaseHelper.writableDatabase
|
||||
val database = databaseHelper.readableDatabase
|
||||
return database.get(serverCapabilitiesTable, "$server = ?", wrap(serverName)) { cursor ->
|
||||
cursor.getString(capabilities)
|
||||
}?.split(",") ?: emptyList()
|
||||
@ -523,7 +533,7 @@ class LokiAPIDatabase(context: Context, helper: SQLCipherOpenHelper) : Database(
|
||||
}
|
||||
|
||||
fun getLastInboxMessageId(serverName: String): Long? {
|
||||
val database = databaseHelper.writableDatabase
|
||||
val database = databaseHelper.readableDatabase
|
||||
return database.get(lastInboxMessageServerIdTable, "$server = ?", wrap(serverName)) { cursor ->
|
||||
cursor.getInt(lastInboxMessageServerId)
|
||||
}?.toLong()
|
||||
@ -540,7 +550,7 @@ class LokiAPIDatabase(context: Context, helper: SQLCipherOpenHelper) : Database(
|
||||
}
|
||||
|
||||
fun getLastOutboxMessageId(serverName: String): Long? {
|
||||
val database = databaseHelper.writableDatabase
|
||||
val database = databaseHelper.readableDatabase
|
||||
return database.get(lastOutboxMessageServerIdTable, "$server = ?", wrap(serverName)) { cursor ->
|
||||
cursor.getInt(lastOutboxMessageServerId)
|
||||
}?.toLong()
|
||||
|
@ -2,7 +2,7 @@ package org.thoughtcrime.securesms.database
|
||||
|
||||
import android.content.ContentValues
|
||||
import android.content.Context
|
||||
import net.sqlcipher.database.SQLiteDatabase.CONFLICT_REPLACE
|
||||
import net.zetetic.database.sqlcipher.SQLiteDatabase.CONFLICT_REPLACE
|
||||
import org.session.libsignal.database.LokiMessageDatabaseProtocol
|
||||
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper
|
||||
|
||||
@ -77,6 +77,25 @@ class LokiMessageDatabase(context: Context, helper: SQLCipherOpenHelper) : Datab
|
||||
database.endTransaction()
|
||||
}
|
||||
|
||||
fun deleteMessages(messageIDs: List<Long>) {
|
||||
val database = databaseHelper.writableDatabase
|
||||
database.beginTransaction()
|
||||
|
||||
database.delete(
|
||||
messageIDTable,
|
||||
"${Companion.messageID} IN (${messageIDs.map { "?" }.joinToString(",")})",
|
||||
messageIDs.map { "$it" }.toTypedArray()
|
||||
)
|
||||
database.delete(
|
||||
messageThreadMappingTable,
|
||||
"${Companion.messageID} IN (${messageIDs.map { "?" }.joinToString(",")})",
|
||||
messageIDs.map { "$it" }.toTypedArray()
|
||||
)
|
||||
|
||||
database.setTransactionSuccessful()
|
||||
database.endTransaction()
|
||||
}
|
||||
|
||||
/**
|
||||
* @return pair of sms or mms table-specific ID and whether it is in SMS table
|
||||
*/
|
||||
@ -96,6 +115,37 @@ class LokiMessageDatabase(context: Context, helper: SQLCipherOpenHelper) : Datab
|
||||
}
|
||||
}
|
||||
|
||||
fun getMessageIDs(serverIDs: List<Long>, threadID: Long): Pair<List<Long>, List<Long>> {
|
||||
val database = databaseHelper.readableDatabase
|
||||
|
||||
// Retrieve the message ids
|
||||
val messageIdCursor = database
|
||||
.rawQuery(
|
||||
"""
|
||||
SELECT ${messageThreadMappingTable}.${messageID}, ${messageIDTable}.${messageType}
|
||||
FROM ${messageThreadMappingTable}
|
||||
JOIN ${messageIDTable} ON ${messageIDTable}.message_id = ${messageThreadMappingTable}.${messageID}
|
||||
WHERE (
|
||||
${messageThreadMappingTable}.${Companion.threadID} = $threadID AND
|
||||
${messageThreadMappingTable}.${Companion.serverID} IN (${serverIDs.joinToString(",")})
|
||||
)
|
||||
"""
|
||||
)
|
||||
|
||||
val smsMessageIds: MutableList<Long> = mutableListOf()
|
||||
val mmsMessageIds: MutableList<Long> = mutableListOf()
|
||||
while (messageIdCursor.moveToNext()) {
|
||||
if (messageIdCursor.getInt(1) == SMS_TYPE) {
|
||||
smsMessageIds.add(messageIdCursor.getLong(0))
|
||||
}
|
||||
else {
|
||||
mmsMessageIds.add(messageIdCursor.getLong(0))
|
||||
}
|
||||
}
|
||||
|
||||
return Pair(smsMessageIds, mmsMessageIds)
|
||||
}
|
||||
|
||||
override fun setServerID(messageID: Long, serverID: Long, isSms: Boolean) {
|
||||
val database = databaseHelper.writableDatabase
|
||||
val contentValues = ContentValues(3)
|
||||
@ -183,6 +233,15 @@ class LokiMessageDatabase(context: Context, helper: SQLCipherOpenHelper) : Datab
|
||||
database.delete(messageHashTable, "${Companion.messageID} = ?", arrayOf(messageID.toString()))
|
||||
}
|
||||
|
||||
fun deleteMessageServerHashes(messageIDs: List<Long>) {
|
||||
val database = databaseHelper.writableDatabase
|
||||
database.delete(
|
||||
messageHashTable,
|
||||
"${Companion.messageID} IN (${messageIDs.map { "?" }.joinToString(",")})",
|
||||
messageIDs.map { "$it" }.toTypedArray()
|
||||
)
|
||||
}
|
||||
|
||||
fun migrateThreadId(legacyThreadId: Long, newThreadId: Long) {
|
||||
val database = databaseHelper.writableDatabase
|
||||
val contentValues = ContentValues(1)
|
||||
|
@ -7,7 +7,7 @@ import android.database.Cursor;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import net.sqlcipher.database.SQLiteDatabase;
|
||||
import net.zetetic.database.sqlcipher.SQLiteDatabase;
|
||||
|
||||
import org.session.libsession.messaging.sending_receiving.attachments.DatabaseAttachment;
|
||||
import org.session.libsession.utilities.Address;
|
||||
|
@ -5,7 +5,7 @@ import android.content.Context;
|
||||
import android.database.Cursor;
|
||||
import android.text.TextUtils;
|
||||
|
||||
import net.sqlcipher.database.SQLiteDatabase;
|
||||
import net.zetetic.database.sqlcipher.SQLiteDatabase;
|
||||
|
||||
import org.session.libsession.utilities.Address;
|
||||
import org.session.libsession.utilities.Document;
|
||||
@ -42,6 +42,7 @@ public abstract class MessagingDatabase extends Database implements MmsSmsColumn
|
||||
public abstract void markAsDeleted(long messageId, boolean read);
|
||||
|
||||
public abstract boolean deleteMessage(long messageId);
|
||||
public abstract boolean deleteMessages(long[] messageId, long threadId);
|
||||
|
||||
public abstract void updateThreadId(long fromId, long toId);
|
||||
|
||||
|
@ -995,6 +995,23 @@ class MmsDatabase(context: Context, databaseHelper: SQLCipherOpenHelper) : Messa
|
||||
return threadDeleted
|
||||
}
|
||||
|
||||
override fun deleteMessages(messageIds: LongArray, threadId: Long): Boolean {
|
||||
val attachmentDatabase = get(context).attachmentDatabase()
|
||||
val groupReceiptDatabase = get(context).groupReceiptDatabase()
|
||||
|
||||
queue(Runnable { attachmentDatabase.deleteAttachmentsForMessages(messageIds) })
|
||||
groupReceiptDatabase.deleteRowsForMessages(messageIds)
|
||||
|
||||
val database = databaseHelper.writableDatabase
|
||||
database!!.delete(TABLE_NAME, ID_IN, arrayOf(messageIds.joinToString(",")))
|
||||
|
||||
val threadDeleted = get(context).threadDatabase().update(threadId, false)
|
||||
notifyConversationListeners(threadId)
|
||||
notifyStickerListeners()
|
||||
notifyStickerPackListeners()
|
||||
return threadDeleted
|
||||
}
|
||||
|
||||
override fun updateThreadId(fromId: Long, toId: Long) {
|
||||
val contentValues = ContentValues(1)
|
||||
contentValues.put(THREAD_ID, toId)
|
||||
|
@ -22,8 +22,8 @@ import android.database.Cursor;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import net.sqlcipher.database.SQLiteDatabase;
|
||||
import net.sqlcipher.database.SQLiteQueryBuilder;
|
||||
import net.zetetic.database.sqlcipher.SQLiteDatabase;
|
||||
import net.zetetic.database.sqlcipher.SQLiteQueryBuilder;
|
||||
|
||||
import org.session.libsession.utilities.Address;
|
||||
import org.session.libsession.utilities.Util;
|
||||
|
@ -6,7 +6,7 @@ import android.database.Cursor;
|
||||
import androidx.annotation.NonNull;
|
||||
import org.session.libsignal.utilities.Log;
|
||||
|
||||
import net.sqlcipher.database.SQLiteDatabase;
|
||||
import net.zetetic.database.sqlcipher.SQLiteDatabase;
|
||||
|
||||
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper;
|
||||
import org.session.libsignal.utilities.Base64;
|
||||
|
@ -48,6 +48,14 @@ class ReactionDatabase(context: Context, helper: SQLCipherOpenHelper) : Database
|
||||
)
|
||||
""".trimIndent()
|
||||
|
||||
@JvmField
|
||||
val CREATE_INDEXS = arrayOf(
|
||||
"CREATE INDEX IF NOT EXISTS reaction_message_id_index ON " + ReactionDatabase.TABLE_NAME + " (" + ReactionDatabase.MESSAGE_ID + ");",
|
||||
"CREATE INDEX IF NOT EXISTS reaction_is_mms_index ON " + ReactionDatabase.TABLE_NAME + " (" + ReactionDatabase.IS_MMS + ");",
|
||||
"CREATE INDEX IF NOT EXISTS reaction_message_id_is_mms_index ON " + ReactionDatabase.TABLE_NAME + " (" + ReactionDatabase.MESSAGE_ID + ", " + ReactionDatabase.IS_MMS + ");",
|
||||
"CREATE INDEX IF NOT EXISTS reaction_sort_id_index ON " + ReactionDatabase.TABLE_NAME + " (" + ReactionDatabase.SORT_ID + ");",
|
||||
)
|
||||
|
||||
@JvmField
|
||||
val CREATE_REACTION_TRIGGERS = arrayOf(
|
||||
"""
|
||||
|
@ -11,7 +11,7 @@ import androidx.annotation.Nullable;
|
||||
|
||||
import com.annimon.stream.Stream;
|
||||
|
||||
import net.sqlcipher.database.SQLiteDatabase;
|
||||
import net.zetetic.database.sqlcipher.SQLiteDatabase;
|
||||
|
||||
import org.session.libsession.utilities.Address;
|
||||
import org.session.libsession.utilities.MaterialColor;
|
||||
|
@ -1,13 +1,13 @@
|
||||
package org.thoughtcrime.securesms.database;
|
||||
|
||||
import android.content.Context;
|
||||
import android.database.Cursor;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import com.annimon.stream.Stream;
|
||||
|
||||
import net.sqlcipher.Cursor;
|
||||
import net.sqlcipher.database.SQLiteDatabase;
|
||||
import net.zetetic.database.sqlcipher.SQLiteDatabase;
|
||||
|
||||
import org.session.libsession.utilities.Util;
|
||||
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper;
|
||||
|
@ -3,7 +3,7 @@ package org.thoughtcrime.securesms.database
|
||||
import android.content.ContentValues
|
||||
import android.content.Context
|
||||
import androidx.core.database.getStringOrNull
|
||||
import net.sqlcipher.Cursor
|
||||
import android.database.Cursor
|
||||
import org.session.libsession.messaging.contacts.Contact
|
||||
import org.session.libsignal.utilities.Base64
|
||||
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper
|
||||
@ -75,21 +75,6 @@ class SessionContactDatabase(context: Context, helper: SQLCipherOpenHelper) : Da
|
||||
}
|
||||
|
||||
fun contactFromCursor(cursor: Cursor): Contact {
|
||||
val sessionID = cursor.getString(sessionID)
|
||||
val contact = Contact(sessionID)
|
||||
contact.name = cursor.getStringOrNull(name)
|
||||
contact.nickname = cursor.getStringOrNull(nickname)
|
||||
contact.profilePictureURL = cursor.getStringOrNull(profilePictureURL)
|
||||
contact.profilePictureFileName = cursor.getStringOrNull(profilePictureFileName)
|
||||
cursor.getStringOrNull(profilePictureEncryptionKey)?.let {
|
||||
contact.profilePictureEncryptionKey = Base64.decode(it)
|
||||
}
|
||||
contact.threadID = cursor.getLong(threadID)
|
||||
contact.isTrusted = cursor.getInt(isTrusted) != 0
|
||||
return contact
|
||||
}
|
||||
|
||||
fun contactFromCursor(cursor: android.database.Cursor): Contact {
|
||||
val sessionID = cursor.getString(cursor.getColumnIndexOrThrow(sessionID))
|
||||
val contact = Contact(sessionID)
|
||||
contact.name = cursor.getStringOrNull(cursor.getColumnIndexOrThrow(name))
|
||||
|
@ -2,7 +2,7 @@ package org.thoughtcrime.securesms.database
|
||||
|
||||
import android.content.ContentValues
|
||||
import android.content.Context
|
||||
import net.sqlcipher.Cursor
|
||||
import android.database.Cursor
|
||||
import org.session.libsession.messaging.jobs.AttachmentUploadJob
|
||||
import org.session.libsession.messaging.jobs.BackgroundGroupAddJob
|
||||
import org.session.libsession.messaging.jobs.GroupAvatarDownloadJob
|
||||
|
@ -28,9 +28,10 @@ import androidx.annotation.Nullable;
|
||||
|
||||
import com.annimon.stream.Stream;
|
||||
|
||||
import net.sqlcipher.database.SQLiteDatabase;
|
||||
import net.sqlcipher.database.SQLiteStatement;
|
||||
import net.zetetic.database.sqlcipher.SQLiteDatabase;
|
||||
import net.zetetic.database.sqlcipher.SQLiteStatement;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.session.libsession.messaging.calls.CallMessageType;
|
||||
import org.session.libsession.messaging.messages.signal.IncomingGroupMessage;
|
||||
import org.session.libsession.messaging.messages.signal.IncomingTextMessage;
|
||||
@ -52,6 +53,7 @@ import org.thoughtcrime.securesms.dependencies.DatabaseComponent;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.security.SecureRandom;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
@ -596,6 +598,30 @@ public class SmsDatabase extends MessagingDatabase {
|
||||
return threadDeleted;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean deleteMessages(long[] messageIds, long threadId) {
|
||||
String[] argsArray = new String[messageIds.length];
|
||||
String[] argValues = new String[messageIds.length];
|
||||
Arrays.fill(argsArray, "?");
|
||||
|
||||
for (int i = 0; i < messageIds.length; i++) {
|
||||
argValues[i] = (messageIds[i] + "");
|
||||
}
|
||||
|
||||
String combinedMessageIdArgss = StringUtils.join(messageIds, ',');
|
||||
String combinedMessageIds = StringUtils.join(messageIds, ',');
|
||||
Log.i("MessageDatabase", "Deleting: " + combinedMessageIds);
|
||||
SQLiteDatabase db = databaseHelper.getWritableDatabase();
|
||||
db.delete(
|
||||
TABLE_NAME,
|
||||
ID + " IN (" + StringUtils.join(argsArray, ',') + ")",
|
||||
argValues
|
||||
);
|
||||
boolean threadDeleted = DatabaseComponent.get(context).threadDatabase().update(threadId, false);
|
||||
notifyConversationListeners(threadId);
|
||||
return threadDeleted;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateThreadId(long fromId, long toId) {
|
||||
ContentValues contentValues = new ContentValues(1);
|
||||
|
@ -16,6 +16,7 @@ import org.session.libsession.messaging.messages.visible.Reaction
|
||||
import org.session.libsession.messaging.messages.visible.VisibleMessage
|
||||
import org.session.libsession.messaging.open_groups.GroupMember
|
||||
import org.session.libsession.messaging.open_groups.OpenGroup
|
||||
import org.session.libsession.messaging.open_groups.OpenGroupApi
|
||||
import org.session.libsession.messaging.sending_receiving.attachments.AttachmentId
|
||||
import org.session.libsession.messaging.sending_receiving.attachments.DatabaseAttachment
|
||||
import org.session.libsession.messaging.sending_receiving.data_extraction.DataExtractionNotificationInfoMessage
|
||||
@ -320,6 +321,10 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context,
|
||||
DatabaseComponent.get(context).groupDatabase().updateProfilePicture(groupID, newValue)
|
||||
}
|
||||
|
||||
override fun hasDownloadedProfilePicture(groupID: String): Boolean {
|
||||
return DatabaseComponent.get(context).groupDatabase().hasDownloadedProfilePicture(groupID)
|
||||
}
|
||||
|
||||
override fun getReceivedMessageTimestamps(): Set<Long> {
|
||||
return SessionMetaProtocol.getTimestamps()
|
||||
}
|
||||
@ -552,8 +557,8 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context,
|
||||
return DatabaseComponent.get(context).groupDatabase().allGroups
|
||||
}
|
||||
|
||||
override fun addOpenGroup(urlAsString: String) {
|
||||
OpenGroupManager.addOpenGroup(urlAsString, context)
|
||||
override fun addOpenGroup(urlAsString: String): OpenGroupApi.RoomInfo? {
|
||||
return OpenGroupManager.addOpenGroup(urlAsString, context)
|
||||
}
|
||||
|
||||
override fun onOpenGroupAdded(server: String) {
|
||||
|
@ -32,7 +32,7 @@ import androidx.annotation.Nullable;
|
||||
|
||||
import com.annimon.stream.Stream;
|
||||
|
||||
import net.sqlcipher.database.SQLiteDatabase;
|
||||
import net.zetetic.database.sqlcipher.SQLiteDatabase;
|
||||
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.session.libsession.utilities.Address;
|
||||
|
@ -1,14 +1,18 @@
|
||||
package org.thoughtcrime.securesms.database.helpers;
|
||||
|
||||
|
||||
import android.app.NotificationChannel;
|
||||
import android.app.NotificationManager;
|
||||
import android.content.Context;
|
||||
import android.database.Cursor;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.core.app.NotificationCompat;
|
||||
|
||||
import net.sqlcipher.database.SQLiteDatabase;
|
||||
import net.sqlcipher.database.SQLiteDatabaseHook;
|
||||
import net.sqlcipher.database.SQLiteOpenHelper;
|
||||
import net.zetetic.database.sqlcipher.SQLiteConnection;
|
||||
import net.zetetic.database.sqlcipher.SQLiteDatabase;
|
||||
import net.zetetic.database.sqlcipher.SQLiteDatabaseHook;
|
||||
import net.zetetic.database.sqlcipher.SQLiteException;
|
||||
import net.zetetic.database.sqlcipher.SQLiteOpenHelper;
|
||||
|
||||
import org.session.libsession.utilities.TextSecurePreferences;
|
||||
import org.session.libsignal.utilities.Log;
|
||||
@ -35,6 +39,11 @@ import org.thoughtcrime.securesms.database.SessionContactDatabase;
|
||||
import org.thoughtcrime.securesms.database.SessionJobDatabase;
|
||||
import org.thoughtcrime.securesms.database.SmsDatabase;
|
||||
import org.thoughtcrime.securesms.database.ThreadDatabase;
|
||||
import org.thoughtcrime.securesms.notifications.NotificationChannels;
|
||||
|
||||
import java.io.File;
|
||||
|
||||
import network.loki.messenger.R;
|
||||
|
||||
public class SQLCipherOpenHelper extends SQLiteOpenHelper {
|
||||
|
||||
@ -75,40 +84,157 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper {
|
||||
private static final int lokiV36 = 57;
|
||||
private static final int lokiV37 = 58;
|
||||
private static final int lokiV38 = 59;
|
||||
private static final int lokiV39 = 60;
|
||||
|
||||
// Loki - onUpgrade(...) must be updated to use Loki version numbers if Signal makes any database changes
|
||||
private static final int DATABASE_VERSION = lokiV38;
|
||||
private static final String DATABASE_NAME = "signal.db";
|
||||
private static final int DATABASE_VERSION = lokiV39;
|
||||
private static final int MIN_DATABASE_VERSION = lokiV7;
|
||||
private static final String CIPHER3_DATABASE_NAME = "signal.db";
|
||||
public static final String DATABASE_NAME = "signal_v4.db";
|
||||
|
||||
private final Context context;
|
||||
private final DatabaseSecret databaseSecret;
|
||||
|
||||
public SQLCipherOpenHelper(@NonNull Context context, @NonNull DatabaseSecret databaseSecret) {
|
||||
super(context, DATABASE_NAME, null, DATABASE_VERSION, new SQLiteDatabaseHook() {
|
||||
super(context, DATABASE_NAME, databaseSecret.asString(), null, DATABASE_VERSION, MIN_DATABASE_VERSION, null, new SQLiteDatabaseHook() {
|
||||
@Override
|
||||
public void preKey(SQLiteDatabase db) {
|
||||
db.rawExecSQL("PRAGMA cipher_default_kdf_iter = 1;");
|
||||
db.rawExecSQL("PRAGMA cipher_default_page_size = 4096;");
|
||||
public void preKey(SQLiteConnection connection) {
|
||||
SQLCipherOpenHelper.applySQLCipherPragmas(connection, true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void postKey(SQLiteDatabase db) {
|
||||
db.rawExecSQL("PRAGMA kdf_iter = '1';");
|
||||
db.rawExecSQL("PRAGMA cipher_page_size = 4096;");
|
||||
public void postKey(SQLiteConnection connection) {
|
||||
SQLCipherOpenHelper.applySQLCipherPragmas(connection, true);
|
||||
|
||||
// if not vacuumed in a while, perform that operation
|
||||
long currentTime = System.currentTimeMillis();
|
||||
// 7 days
|
||||
if (currentTime - TextSecurePreferences.getLastVacuumTime(context) > 604_800_000) {
|
||||
db.rawExecSQL("VACUUM;");
|
||||
connection.execute("VACUUM;", null, null);
|
||||
TextSecurePreferences.setLastVacuumNow(context);
|
||||
}
|
||||
}
|
||||
});
|
||||
}, true);
|
||||
|
||||
this.context = context.getApplicationContext();
|
||||
this.databaseSecret = databaseSecret;
|
||||
}
|
||||
|
||||
private static void applySQLCipherPragmas(SQLiteConnection connection, boolean useSQLCipher4) {
|
||||
if (useSQLCipher4) {
|
||||
connection.execute("PRAGMA kdf_iter = '256000';", null, null);
|
||||
}
|
||||
else {
|
||||
connection.execute("PRAGMA cipher_compatibility = 3;", null, null);
|
||||
connection.execute("PRAGMA kdf_iter = '1';", null, null);
|
||||
}
|
||||
|
||||
connection.execute("PRAGMA cipher_page_size = 4096;", null, null);
|
||||
}
|
||||
|
||||
private static SQLiteDatabase open(String path, DatabaseSecret databaseSecret, boolean useSQLCipher4) throws SQLiteException {
|
||||
return SQLiteDatabase.openDatabase(path, databaseSecret.asString(), null, SQLiteDatabase.OPEN_READWRITE, new SQLiteDatabaseHook() {
|
||||
@Override
|
||||
public void preKey(SQLiteConnection connection) { SQLCipherOpenHelper.applySQLCipherPragmas(connection, useSQLCipher4); }
|
||||
|
||||
@Override
|
||||
public void postKey(SQLiteConnection connection) { SQLCipherOpenHelper.applySQLCipherPragmas(connection, useSQLCipher4); }
|
||||
});
|
||||
}
|
||||
|
||||
public static void migrateSqlCipher3To4IfNeeded(@NonNull Context context, @NonNull DatabaseSecret databaseSecret) throws Exception {
|
||||
String oldDbPath = context.getDatabasePath(CIPHER3_DATABASE_NAME).getPath();
|
||||
File oldDbFile = new File(oldDbPath);
|
||||
|
||||
// If the old SQLCipher3 database file doesn't exist then no need to do anything
|
||||
if (!oldDbFile.exists()) { return; }
|
||||
|
||||
try {
|
||||
// Define the location for the new database
|
||||
String newDbPath = context.getDatabasePath(DATABASE_NAME).getPath();
|
||||
File newDbFile = new File(newDbPath);
|
||||
|
||||
// If the new database file already exists then check if it's valid first, if it's in an
|
||||
// invalid state we should delete it and try to migrate again
|
||||
if (newDbFile.exists()) {
|
||||
// If the old database hasn't been modified since the new database was created, then we can
|
||||
// assume the user hasn't downgraded for some reason and made changes to the old database and
|
||||
// can remove the old database file (it won't be used anymore)
|
||||
if (oldDbFile.lastModified() <= newDbFile.lastModified()) {
|
||||
// TODO: Delete 'CIPHER3_DATABASE_NAME' once enough time has past
|
||||
// //noinspection ResultOfMethodCallIgnored
|
||||
// oldDbFile.delete();
|
||||
return;
|
||||
}
|
||||
|
||||
// If the old database does have newer changes then the new database could have stale/invalid
|
||||
// data and we should re-migrate to avoid losing any data or issues
|
||||
if (!newDbFile.delete()) {
|
||||
throw new Exception("Failed to remove invalid new database");
|
||||
}
|
||||
}
|
||||
|
||||
if (!newDbFile.createNewFile()) {
|
||||
throw new Exception("Failed to create new database");
|
||||
}
|
||||
|
||||
// Open the old database and extract it's version
|
||||
SQLiteDatabase oldDb = SQLCipherOpenHelper.open(oldDbPath, databaseSecret, false);
|
||||
int oldDbVersion = oldDb.getVersion();
|
||||
|
||||
// Export the old database to the new one (will have the default 'kdf_iter' and 'page_size' settings)
|
||||
oldDb.rawExecSQL(
|
||||
String.format("ATTACH DATABASE '%s' AS sqlcipher4 KEY '%s'", newDbPath, databaseSecret.asString())
|
||||
);
|
||||
Cursor cursor = oldDb.rawQuery("SELECT sqlcipher_export('sqlcipher4')");
|
||||
cursor.moveToLast();
|
||||
cursor.close();
|
||||
oldDb.rawExecSQL("DETACH DATABASE sqlcipher4");
|
||||
oldDb.close();
|
||||
|
||||
// Open the newly migrated database (to ensure it works) and set it's version so we don't try
|
||||
// to run any of our custom migrations
|
||||
SQLiteDatabase newDb = SQLCipherOpenHelper.open(newDbPath, databaseSecret, true);
|
||||
newDb.setVersion(oldDbVersion);
|
||||
newDb.close();
|
||||
|
||||
// TODO: Delete 'CIPHER3_DATABASE_NAME' once enough time has past
|
||||
// Remove the old database file since it will no longer be used
|
||||
// //noinspection ResultOfMethodCallIgnored
|
||||
// oldDbFile.delete();
|
||||
}
|
||||
catch (Exception e) {
|
||||
Log.e(TAG, "Migration from SQLCipher3 to SQLCipher4 failed", e);
|
||||
|
||||
// Notify the user of the issue so they know they can downgrade until the issue is fixed
|
||||
NotificationManager notificationManager = context.getSystemService(NotificationManager.class);
|
||||
String channelId = context.getString(R.string.NotificationChannel_failures);
|
||||
|
||||
if (NotificationChannels.supported()) {
|
||||
NotificationChannel channel = new NotificationChannel(channelId, channelId, NotificationManager.IMPORTANCE_HIGH);
|
||||
channel.enableVibration(true);
|
||||
notificationManager.createNotificationChannel(channel);
|
||||
}
|
||||
|
||||
NotificationCompat.Builder builder = new NotificationCompat.Builder(context, channelId)
|
||||
.setSmallIcon(R.drawable.ic_notification)
|
||||
.setColor(context.getResources().getColor(R.color.textsecure_primary))
|
||||
.setCategory(NotificationCompat.CATEGORY_ERROR)
|
||||
.setContentTitle(context.getString(R.string.ErrorNotifier_migration))
|
||||
.setContentText(context.getString(R.string.ErrorNotifier_migration_downgrade))
|
||||
.setAutoCancel(true);
|
||||
|
||||
if (!NotificationChannels.supported()) {
|
||||
builder.setPriority(NotificationCompat.PRIORITY_HIGH);
|
||||
}
|
||||
|
||||
notificationManager.notify(5874, builder.build());
|
||||
|
||||
// Throw the error (app will crash but there is nothing else we can do unfortunately)
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate(SQLiteDatabase db) {
|
||||
db.execSQL(SmsDatabase.CREATE_TABLE);
|
||||
@ -188,6 +314,7 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper {
|
||||
executeStatements(db, DraftDatabase.CREATE_INDEXS);
|
||||
executeStatements(db, GroupDatabase.CREATE_INDEXS);
|
||||
executeStatements(db, GroupReceiptDatabase.CREATE_INDEXES);
|
||||
executeStatements(db, ReactionDatabase.CREATE_INDEXS);
|
||||
|
||||
executeStatements(db, ReactionDatabase.CREATE_REACTION_TRIGGERS);
|
||||
}
|
||||
@ -195,9 +322,7 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper {
|
||||
@Override
|
||||
public void onConfigure(SQLiteDatabase db) {
|
||||
super.onConfigure(db);
|
||||
// Loki - Enable write ahead logging mode and increase the cache size.
|
||||
// This should be disabled if we ever run into serious race condition bugs.
|
||||
db.enableWriteAheadLogging();
|
||||
|
||||
db.execSQL("PRAGMA cache_size = 10000");
|
||||
}
|
||||
|
||||
@ -414,20 +539,16 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper {
|
||||
db.execSQL(EmojiSearchDatabase.CREATE_EMOJI_SEARCH_TABLE_COMMAND);
|
||||
}
|
||||
|
||||
if (oldVersion < lokiV39) {
|
||||
executeStatements(db, ReactionDatabase.CREATE_INDEXS);
|
||||
}
|
||||
|
||||
db.setTransactionSuccessful();
|
||||
} finally {
|
||||
db.endTransaction();
|
||||
}
|
||||
}
|
||||
|
||||
public SQLiteDatabase getReadableDatabase() {
|
||||
return getReadableDatabase(databaseSecret.asString());
|
||||
}
|
||||
|
||||
public SQLiteDatabase getWritableDatabase() {
|
||||
return getWritableDatabase(databaseSecret.asString());
|
||||
}
|
||||
|
||||
public void markCurrent(SQLiteDatabase db) {
|
||||
db.setVersion(DATABASE_VERSION);
|
||||
}
|
||||
|
@ -50,7 +50,6 @@ public class ThreadRecord extends DisplayRecord {
|
||||
private final long expiresIn;
|
||||
private final long lastSeen;
|
||||
private final boolean pinned;
|
||||
private final int recipientHash;
|
||||
|
||||
public ThreadRecord(@NonNull String body, @Nullable Uri snippetUri,
|
||||
@NonNull Recipient recipient, long date, long count, int unreadCount,
|
||||
@ -67,17 +66,12 @@ public class ThreadRecord extends DisplayRecord {
|
||||
this.expiresIn = expiresIn;
|
||||
this.lastSeen = lastSeen;
|
||||
this.pinned = pinned;
|
||||
this.recipientHash = recipient.hashCode();
|
||||
}
|
||||
|
||||
public @Nullable Uri getSnippetUri() {
|
||||
return snippetUri;
|
||||
}
|
||||
|
||||
public int getRecipientHash() {
|
||||
return recipientHash;
|
||||
}
|
||||
|
||||
@Override
|
||||
public SpannableString getDisplayBody(@NonNull Context context) {
|
||||
if (isGroupUpdateMessage()) {
|
||||
|
@ -6,7 +6,7 @@ import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import net.sqlcipher.database.SQLiteDatabase
|
||||
import net.zetetic.database.sqlcipher.SQLiteDatabase
|
||||
import org.session.libsession.database.MessageDataProvider
|
||||
import org.thoughtcrime.securesms.attachments.DatabaseAttachmentProvider
|
||||
import org.thoughtcrime.securesms.crypto.AttachmentSecret
|
||||
@ -22,7 +22,7 @@ object DatabaseModule {
|
||||
|
||||
@JvmStatic
|
||||
fun init(context: Context) {
|
||||
SQLiteDatabase.loadLibs(context)
|
||||
System.loadLibrary("sqlcipher")
|
||||
}
|
||||
|
||||
@Provides
|
||||
@ -33,6 +33,7 @@ object DatabaseModule {
|
||||
@Singleton
|
||||
fun provideOpenHelper(@ApplicationContext context: Context): SQLCipherOpenHelper {
|
||||
val dbSecret = DatabaseSecretProvider(context).orCreateDatabaseSecret
|
||||
SQLCipherOpenHelper.migrateSqlCipher3To4IfNeeded(context, dbSecret)
|
||||
return SQLCipherOpenHelper(context, dbSecret)
|
||||
}
|
||||
|
||||
|
@ -58,14 +58,14 @@ object OpenGroupManager {
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
fun add(server: String, room: String, publicKey: String, context: Context) {
|
||||
fun add(server: String, room: String, publicKey: String, context: Context): OpenGroupApi.RoomInfo? {
|
||||
val openGroupID = "$server.$room"
|
||||
var threadID = GroupManager.getOpenGroupThreadID(openGroupID, context)
|
||||
val storage = MessagingModuleConfiguration.shared.storage
|
||||
val threadDB = DatabaseComponent.get(context).lokiThreadDatabase()
|
||||
// Check it it's added already
|
||||
val existingOpenGroup = threadDB.getOpenGroupChat(threadID)
|
||||
if (existingOpenGroup != null) { return }
|
||||
if (existingOpenGroup != null) { return null }
|
||||
// Clear any existing data if needed
|
||||
storage.removeLastDeletionServerID(room, server)
|
||||
storage.removeLastMessageServerID(room, server)
|
||||
@ -73,18 +73,17 @@ object OpenGroupManager {
|
||||
storage.removeLastOutboxMessageId(server)
|
||||
// Store the public key
|
||||
storage.setOpenGroupPublicKey(server, publicKey)
|
||||
// Get capabilities
|
||||
val capabilities = OpenGroupApi.getCapabilities(server).get()
|
||||
// Get capabilities & room info
|
||||
val (capabilities, info) = OpenGroupApi.getCapabilitiesAndRoomInfo(room, server).get()
|
||||
storage.setServerCapabilities(server, capabilities.capabilities)
|
||||
// Get room info
|
||||
val info = OpenGroupApi.getRoomInfo(room, server).get()
|
||||
storage.setUserCount(room, server, info.activeUsers)
|
||||
// Create the group locally if not available already
|
||||
if (threadID < 0) {
|
||||
threadID = GroupManager.createOpenGroup(openGroupID, context, null, info.name).threadId
|
||||
}
|
||||
val openGroup = OpenGroup(server, room, info.name, info.infoUpdates, publicKey)
|
||||
val openGroup = OpenGroup(server = server, room = room, publicKey = publicKey, name = info.name, imageId = info.imageId, infoUpdates = info.infoUpdates)
|
||||
threadDB.setOpenGroupChat(openGroup, threadID)
|
||||
return info
|
||||
}
|
||||
|
||||
fun restartPollerForServer(server: String) {
|
||||
@ -130,12 +129,13 @@ object OpenGroupManager {
|
||||
}
|
||||
}
|
||||
|
||||
fun addOpenGroup(urlAsString: String, context: Context) {
|
||||
val url = HttpUrl.parse(urlAsString) ?: return
|
||||
fun addOpenGroup(urlAsString: String, context: Context): OpenGroupApi.RoomInfo? {
|
||||
val url = HttpUrl.parse(urlAsString) ?: return null
|
||||
val server = OpenGroup.getServer(urlAsString)
|
||||
val room = url.pathSegments().firstOrNull() ?: return
|
||||
val publicKey = url.queryParameter("public_key") ?: return
|
||||
add(server.toString().removeSuffix("/"), room, publicKey, context) // assume migrated from calling function
|
||||
val room = url.pathSegments().firstOrNull() ?: return null
|
||||
val publicKey = url.queryParameter("public_key") ?: return null
|
||||
|
||||
return add(server.toString().removeSuffix("/"), room, publicKey, context) // assume migrated from calling function
|
||||
}
|
||||
|
||||
fun updateOpenGroup(openGroup: OpenGroup, context: Context) {
|
||||
|
@ -99,11 +99,11 @@ class ConversationView : LinearLayout {
|
||||
binding.snippetTextView.typeface = if (unreadCount > 0 && !thread.isRead) Typeface.DEFAULT_BOLD else Typeface.DEFAULT
|
||||
binding.snippetTextView.visibility = if (isTyping) View.GONE else View.VISIBLE
|
||||
if (isTyping) {
|
||||
binding.typingIndicatorView.startAnimation()
|
||||
binding.typingIndicatorView.root.startAnimation()
|
||||
} else {
|
||||
binding.typingIndicatorView.stopAnimation()
|
||||
binding.typingIndicatorView.root.stopAnimation()
|
||||
}
|
||||
binding.typingIndicatorView.visibility = if (isTyping) View.VISIBLE else View.GONE
|
||||
binding.typingIndicatorView.root.visibility = if (isTyping) View.VISIBLE else View.GONE
|
||||
binding.statusIndicatorImageView.visibility = View.VISIBLE
|
||||
when {
|
||||
!thread.isOutgoing -> binding.statusIndicatorImageView.visibility = View.GONE
|
||||
|
@ -202,7 +202,7 @@ class HomeActivity : PassphraseRequiredActionBarActivity(),
|
||||
OpenGroupManager.startPolling()
|
||||
JobQueue.shared.resumePendingJobs()
|
||||
}
|
||||
// Set up typing observer
|
||||
|
||||
withContext(Dispatchers.Main) {
|
||||
updateProfileButton()
|
||||
TextSecurePreferences.events.filter { it == TextSecurePreferences.PROFILE_NAME_PREF }.collect {
|
||||
@ -365,6 +365,10 @@ class HomeActivity : PassphraseRequiredActionBarActivity(),
|
||||
setupMessageRequestsBanner()
|
||||
updateEmptyState()
|
||||
}
|
||||
|
||||
ApplicationContext.getInstance(this@HomeActivity).typingStatusRepository.typingThreads.observe(this) { threadIds ->
|
||||
homeAdapter.typingThreadIDs = (threadIds ?: setOf())
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateEmptyState() {
|
||||
|
@ -63,6 +63,8 @@ class HomeAdapter(
|
||||
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()
|
||||
|
@ -22,22 +22,28 @@ class HomeDiffUtil(
|
||||
val newItem = new[newItemPosition]
|
||||
|
||||
// return early to save getDisplayBody or expensive calls
|
||||
val sameCount = oldItem.count == newItem.count
|
||||
if (!sameCount) return false
|
||||
val sameUnreads = oldItem.unreadCount == newItem.unreadCount
|
||||
if (!sameUnreads) return false
|
||||
val samePinned = oldItem.isPinned == newItem.isPinned
|
||||
if (!samePinned) return false
|
||||
val sameRecipientHash = oldItem.recipientHash == newItem.recipientHash
|
||||
if (!sameRecipientHash) return false
|
||||
val sameSnippet = oldItem.getDisplayBody(context) == newItem.getDisplayBody(context)
|
||||
if (!sameSnippet) return false
|
||||
val sameSendStatus = oldItem.isFailed == newItem.isFailed && oldItem.isDelivered == newItem.isDelivered
|
||||
&& oldItem.isSent == newItem.isSent && oldItem.isPending == newItem.isPending
|
||||
if (!sameSendStatus) return false
|
||||
var isSameItem = true
|
||||
|
||||
// all same
|
||||
return true
|
||||
if (isSameItem) { isSameItem = (oldItem.count == newItem.count) }
|
||||
if (isSameItem) { isSameItem = (oldItem.unreadCount == newItem.unreadCount) }
|
||||
if (isSameItem) { isSameItem = (oldItem.isPinned == newItem.isPinned) }
|
||||
|
||||
// Note: For some reason the 'hashCode' value can change after initialisation so we can't cache it
|
||||
if (isSameItem) { isSameItem = (oldItem.recipient.hashCode() == newItem.recipient.hashCode()) }
|
||||
|
||||
// Note: Two instances of 'SpannableString' may not equate even though their content matches
|
||||
if (isSameItem) { isSameItem = (oldItem.getDisplayBody(context).toString() == newItem.getDisplayBody(context).toString()) }
|
||||
|
||||
if (isSameItem) {
|
||||
isSameItem = (
|
||||
oldItem.isFailed == newItem.isFailed &&
|
||||
oldItem.isDelivered == newItem.isDelivered &&
|
||||
oldItem.isSent == newItem.isSent &&
|
||||
oldItem.isPending == newItem.isPending
|
||||
)
|
||||
}
|
||||
|
||||
return isSameItem
|
||||
}
|
||||
|
||||
}
|
@ -44,7 +44,7 @@ class BackgroundPollWorker(val context: Context, params: WorkerParameters) : Wor
|
||||
}
|
||||
|
||||
override fun doWork(): Result {
|
||||
if (TextSecurePreferences.getLocalNumber(context) == null) {
|
||||
if (TextSecurePreferences.getLocalNumber(context) == null || !TextSecurePreferences.hasSeenWelcomeScreen(context)) {
|
||||
Log.v(TAG, "User not registered yet.")
|
||||
return Result.failure()
|
||||
}
|
||||
|
@ -22,8 +22,10 @@ import kotlinx.coroutines.launch
|
||||
import network.loki.messenger.R
|
||||
import network.loki.messenger.databinding.ActivityLinkDeviceBinding
|
||||
import network.loki.messenger.databinding.FragmentRecoveryPhraseBinding
|
||||
import org.session.libsession.snode.SnodeModule
|
||||
import org.session.libsession.utilities.TextSecurePreferences
|
||||
import org.session.libsignal.crypto.MnemonicCodec
|
||||
import org.session.libsignal.database.LokiAPIDatabaseProtocol
|
||||
import org.session.libsignal.utilities.Hex
|
||||
import org.session.libsignal.utilities.KeyHelper
|
||||
import org.session.libsignal.utilities.Log
|
||||
@ -39,6 +41,8 @@ import org.thoughtcrime.securesms.util.setUpActionBarSessionLogo
|
||||
|
||||
class LinkDeviceActivity : BaseActionBarActivity(), ScanQRCodeWrapperFragmentDelegate {
|
||||
private lateinit var binding: ActivityLinkDeviceBinding
|
||||
internal val database: LokiAPIDatabaseProtocol
|
||||
get() = SnodeModule.shared.storage
|
||||
private val adapter = LinkDeviceActivityAdapter(this)
|
||||
private var restoreJob: Job? = null
|
||||
|
||||
@ -99,6 +103,11 @@ class LinkDeviceActivity : BaseActionBarActivity(), ScanQRCodeWrapperFragmentDel
|
||||
if (restoreJob?.isActive == true) return
|
||||
|
||||
restoreJob = lifecycleScope.launch {
|
||||
// This is here to resolve a case where the app restarts before a user completes onboarding
|
||||
// which can result in an invalid database state
|
||||
database.clearAllLastMessageHashes()
|
||||
database.clearReceivedMessageHashValues()
|
||||
|
||||
// RestoreActivity handles seed this way
|
||||
val keyPairGenerationResult = KeyPairUtilities.generate(seed)
|
||||
val x25519KeyPair = keyPairGenerationResult.x25519KeyPair
|
||||
|
@ -13,8 +13,10 @@ import android.view.View
|
||||
import android.widget.Toast
|
||||
import network.loki.messenger.R
|
||||
import network.loki.messenger.databinding.ActivityRecoveryPhraseRestoreBinding
|
||||
import org.session.libsession.snode.SnodeModule
|
||||
import org.session.libsession.utilities.TextSecurePreferences
|
||||
import org.session.libsignal.crypto.MnemonicCodec
|
||||
import org.session.libsignal.database.LokiAPIDatabaseProtocol
|
||||
import org.session.libsignal.utilities.Hex
|
||||
import org.session.libsignal.utilities.KeyHelper
|
||||
import org.session.libsignal.utilities.hexEncodedPublicKey
|
||||
@ -26,6 +28,8 @@ import org.thoughtcrime.securesms.util.setUpActionBarSessionLogo
|
||||
|
||||
class RecoveryPhraseRestoreActivity : BaseActionBarActivity() {
|
||||
private lateinit var binding: ActivityRecoveryPhraseRestoreBinding
|
||||
internal val database: LokiAPIDatabaseProtocol
|
||||
get() = SnodeModule.shared.storage
|
||||
// region Lifecycle
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
@ -64,6 +68,11 @@ class RecoveryPhraseRestoreActivity : BaseActionBarActivity() {
|
||||
private fun restore() {
|
||||
val mnemonic = binding.mnemonicEditText.text.toString()
|
||||
try {
|
||||
// This is here to resolve a case where the app restarts before a user completes onboarding
|
||||
// which can result in an invalid database state
|
||||
database.clearAllLastMessageHashes()
|
||||
database.clearReceivedMessageHashValues()
|
||||
|
||||
val loadFileContents: (String) -> String = { fileName ->
|
||||
MnemonicUtilities.loadFileContents(this, fileName)
|
||||
}
|
||||
|
@ -18,8 +18,10 @@ import android.widget.Toast
|
||||
import com.goterl.lazysodium.utils.KeyPair
|
||||
import network.loki.messenger.R
|
||||
import network.loki.messenger.databinding.ActivityRegisterBinding
|
||||
import org.session.libsession.snode.SnodeModule
|
||||
import org.session.libsession.utilities.TextSecurePreferences
|
||||
import org.session.libsignal.crypto.ecc.ECKeyPair
|
||||
import org.session.libsignal.database.LokiAPIDatabaseProtocol
|
||||
import org.session.libsignal.utilities.KeyHelper
|
||||
import org.session.libsignal.utilities.hexEncodedPublicKey
|
||||
import org.thoughtcrime.securesms.BaseActionBarActivity
|
||||
@ -29,6 +31,8 @@ import org.thoughtcrime.securesms.util.setUpActionBarSessionLogo
|
||||
|
||||
class RegisterActivity : BaseActionBarActivity() {
|
||||
private lateinit var binding: ActivityRegisterBinding
|
||||
internal val database: LokiAPIDatabaseProtocol
|
||||
get() = SnodeModule.shared.storage
|
||||
private var seed: ByteArray? = null
|
||||
private var ed25519KeyPair: KeyPair? = null
|
||||
private var x25519KeyPair: ECKeyPair? = null
|
||||
@ -109,6 +113,11 @@ class RegisterActivity : BaseActionBarActivity() {
|
||||
|
||||
// region Interaction
|
||||
private fun register() {
|
||||
// This is here to resolve a case where the app restarts before a user completes onboarding
|
||||
// which can result in an invalid database state
|
||||
database.clearAllLastMessageHashes()
|
||||
database.clearReceivedMessageHashValues()
|
||||
|
||||
KeyPairUtilities.store(this, seed!!, ed25519KeyPair!!, x25519KeyPair!!)
|
||||
val userHexEncodedPublicKey = x25519KeyPair!!.hexEncodedPublicKey
|
||||
val registrationID = KeyHelper.generateRegistrationId(false)
|
||||
|
@ -35,6 +35,7 @@ interface ConversationRepository {
|
||||
fun maybeGetRecipientForThreadId(threadId: Long): Recipient?
|
||||
fun saveDraft(threadId: Long, text: String)
|
||||
fun getDraft(threadId: Long): String?
|
||||
fun clearDrafts(threadId: Long)
|
||||
fun inviteContacts(threadId: Long, contacts: List<Recipient>)
|
||||
fun setBlocked(recipient: Recipient, blocked: Boolean)
|
||||
fun deleteLocally(recipient: Recipient, message: MessageRecord)
|
||||
@ -98,10 +99,13 @@ class DefaultConversationRepository @Inject constructor(
|
||||
|
||||
override fun getDraft(threadId: Long): String? {
|
||||
val drafts = draftDb.getDrafts(threadId)
|
||||
draftDb.clearDrafts(threadId)
|
||||
return drafts.find { it.type == DraftDatabase.Draft.TEXT }?.value
|
||||
}
|
||||
|
||||
override fun clearDrafts(threadId: Long) {
|
||||
draftDb.clearDrafts(threadId)
|
||||
}
|
||||
|
||||
override fun inviteContacts(threadId: Long, contacts: List<Recipient>) {
|
||||
val openGroup = lokiThreadDb.getOpenGroupChat(threadId) ?: return
|
||||
for (contact in contacts) {
|
||||
|
@ -0,0 +1,425 @@
|
||||
package org.thoughtcrime.securesms.util
|
||||
|
||||
import android.content.Context
|
||||
import org.session.libsession.messaging.MessagingModuleConfiguration
|
||||
import org.session.libsession.messaging.contacts.Contact
|
||||
import org.session.libsession.messaging.messages.signal.IncomingTextMessage
|
||||
import org.session.libsession.messaging.messages.signal.OutgoingTextMessage
|
||||
import org.session.libsession.messaging.open_groups.OpenGroup
|
||||
import org.session.libsession.messaging.open_groups.OpenGroupApi
|
||||
import org.session.libsession.messaging.sending_receiving.notifications.PushNotificationAPI
|
||||
import org.session.libsession.messaging.sending_receiving.pollers.ClosedGroupPollerV2
|
||||
import org.session.libsession.utilities.Address
|
||||
import org.session.libsession.utilities.GroupUtil
|
||||
import org.session.libsession.utilities.recipients.Recipient
|
||||
import org.session.libsignal.crypto.ecc.Curve
|
||||
import org.session.libsignal.messages.SignalServiceGroup
|
||||
import org.session.libsignal.utilities.Log
|
||||
import org.session.libsignal.utilities.guava.Optional
|
||||
import org.session.libsignal.utilities.hexEncodedPublicKey
|
||||
import org.thoughtcrime.securesms.crypto.KeyPairUtilities
|
||||
import org.thoughtcrime.securesms.dependencies.DatabaseComponent
|
||||
import org.thoughtcrime.securesms.groups.GroupManager
|
||||
import java.security.SecureRandom
|
||||
import java.util.*
|
||||
import kotlin.random.asKotlinRandom
|
||||
|
||||
object MockDataGenerator {
|
||||
private var printProgress = true
|
||||
private var hasStartedGenerationThisRun = false
|
||||
|
||||
// FIXME: Update this to run in a transaction instead of individual db writes (should drastically speed it up)
|
||||
fun generateMockData(context: Context) {
|
||||
// Don't re-generate the mock data if it already exists
|
||||
val mockDataExistsRecipient = Recipient.from(context, Address.fromSerialized("MockDatabaseThread"), false)
|
||||
val storage = MessagingModuleConfiguration.shared.storage
|
||||
val threadDb = DatabaseComponent.get(context).threadDatabase()
|
||||
val lokiThreadDB = DatabaseComponent.get(context).lokiThreadDatabase()
|
||||
val contactDb = DatabaseComponent.get(context).sessionContactDatabase()
|
||||
val recipientDb = DatabaseComponent.get(context).recipientDatabase()
|
||||
val smsDb = DatabaseComponent.get(context).smsDatabase()
|
||||
|
||||
if (hasStartedGenerationThisRun || threadDb.getThreadIdIfExistsFor(mockDataExistsRecipient) != -1L) {
|
||||
hasStartedGenerationThisRun = true
|
||||
return
|
||||
}
|
||||
|
||||
/// The mock data generation is quite slow, there are 3 parts which take a decent amount of time (deleting the account afterwards will
|
||||
/// also take a long time):
|
||||
/// Generating the threads & content - ~3m per 100
|
||||
val dmThreadCount: Int = 1000
|
||||
val closedGroupThreadCount: Int = 50
|
||||
val openGroupThreadCount: Int = 20
|
||||
val messageRangePerThread: List<IntRange> = listOf(0..500)
|
||||
val dmRandomSeed: String = "1111"
|
||||
val cgRandomSeed: String = "2222"
|
||||
val ogRandomSeed: String = "3333"
|
||||
val chunkSize: Int = 1000 // Chunk up the thread writing to prevent memory issues
|
||||
val stringContent: List<String> = "abcdefghijklmnopqrstuvwxyz ABCDEFGHIJKLMNOPQRSTUVWXYZ 0123456789 ".map { it.toString() }
|
||||
val wordContent: List<String> = listOf("alias", "consequatur", "aut", "perferendis", "sit", "voluptatem", "accusantium", "doloremque", "aperiam", "eaque", "ipsa", "quae", "ab", "illo", "inventore", "veritatis", "et", "quasi", "architecto", "beatae", "vitae", "dicta", "sunt", "explicabo", "aspernatur", "aut", "odit", "aut", "fugit", "sed", "quia", "consequuntur", "magni", "dolores", "eos", "qui", "ratione", "voluptatem", "sequi", "nesciunt", "neque", "dolorem", "ipsum", "quia", "dolor", "sit", "amet", "consectetur", "adipisci", "velit", "sed", "quia", "non", "numquam", "eius", "modi", "tempora", "incidunt", "ut", "labore", "et", "dolore", "magnam", "aliquam", "quaerat", "voluptatem", "ut", "enim", "ad", "minima", "veniam", "quis", "nostrum", "exercitationem", "ullam", "corporis", "nemo", "enim", "ipsam", "voluptatem", "quia", "voluptas", "sit", "suscipit", "laboriosam", "nisi", "ut", "aliquid", "ex", "ea", "commodi", "consequatur", "quis", "autem", "vel", "eum", "iure", "reprehenderit", "qui", "in", "ea", "voluptate", "velit", "esse", "quam", "nihil", "molestiae", "et", "iusto", "odio", "dignissimos", "ducimus", "qui", "blanditiis", "praesentium", "laudantium", "totam", "rem", "voluptatum", "deleniti", "atque", "corrupti", "quos", "dolores", "et", "quas", "molestias", "excepturi", "sint", "occaecati", "cupiditate", "non", "provident", "sed", "ut", "perspiciatis", "unde", "omnis", "iste", "natus", "error", "similique", "sunt", "in", "culpa", "qui", "officia", "deserunt", "mollitia", "animi", "id", "est", "laborum", "et", "dolorum", "fuga", "et", "harum", "quidem", "rerum", "facilis", "est", "et", "expedita", "distinctio", "nam", "libero", "tempore", "cum", "soluta", "nobis", "est", "eligendi", "optio", "cumque", "nihil", "impedit", "quo", "porro", "quisquam", "est", "qui", "minus", "id", "quod", "maxime", "placeat", "facere", "possimus", "omnis", "voluptas", "assumenda", "est", "omnis", "dolor", "repellendus", "temporibus", "autem", "quibusdam", "et", "aut", "consequatur", "vel", "illum", "qui", "dolorem", "eum", "fugiat", "quo", "voluptas", "nulla", "pariatur", "at", "vero", "eos", "et", "accusamus", "officiis", "debitis", "aut", "rerum", "necessitatibus", "saepe", "eveniet", "ut", "et", "voluptates", "repudiandae", "sint", "et", "molestiae", "non", "recusandae", "itaque", "earum", "rerum", "hic", "tenetur", "a", "sapiente", "delectus", "ut", "aut", "reiciendis", "voluptatibus", "maiores", "doloribus", "asperiores", "repellat")
|
||||
val timestampNow: Long = System.currentTimeMillis()
|
||||
val userSessionId: String = MessagingModuleConfiguration.shared.storage.getUserPublicKey()!!
|
||||
val logProgress: ((String, String) -> Unit) = logProgress@{ title, event ->
|
||||
if (!printProgress) { return@logProgress }
|
||||
|
||||
Log.i("[MockDataGenerator]", "${System.currentTimeMillis()} $title - $event")
|
||||
}
|
||||
|
||||
hasStartedGenerationThisRun = true
|
||||
|
||||
// FIXME: Make sure this data doesn't go off device somehow?
|
||||
logProgress("", "Start")
|
||||
|
||||
// First create the thread used to indicate that the mock data has been generated
|
||||
threadDb.getOrCreateThreadIdFor(mockDataExistsRecipient)
|
||||
|
||||
// -- DM Thread
|
||||
val dmThreadRandomGenerator: SecureRandom = SecureRandom(dmRandomSeed.toByteArray())
|
||||
var dmThreadIndex: Int = 0
|
||||
logProgress("DM Threads", "Start Generating $dmThreadCount threads")
|
||||
|
||||
while (dmThreadIndex < dmThreadCount) {
|
||||
val remainingThreads: Int = (dmThreadCount - dmThreadIndex)
|
||||
|
||||
(0 until Math.min(chunkSize, remainingThreads)).forEach { index ->
|
||||
val threadIndex: Int = (dmThreadIndex + index)
|
||||
|
||||
logProgress("DM Thread $threadIndex", "Start")
|
||||
|
||||
val dataBytes = (0 until 16).map { dmThreadRandomGenerator.nextInt(UByte.MAX_VALUE.toInt()).toByte() }
|
||||
val randomSessionId: String = KeyPairUtilities.generate(dataBytes.toByteArray()).x25519KeyPair.hexEncodedPublicKey
|
||||
val isMessageRequest: Boolean = dmThreadRandomGenerator.nextBoolean()
|
||||
val contactNameLength: Int = (5 + dmThreadRandomGenerator.nextInt(15))
|
||||
|
||||
val numMessages: Int = (
|
||||
messageRangePerThread[threadIndex % messageRangePerThread.count()].first +
|
||||
dmThreadRandomGenerator.nextInt(messageRangePerThread[threadIndex % messageRangePerThread.count()].last())
|
||||
)
|
||||
|
||||
// Generate the thread
|
||||
val recipient = Recipient.from(context, Address.fromSerialized(randomSessionId), false)
|
||||
val contact = Contact(randomSessionId)
|
||||
val threadId = threadDb.getOrCreateThreadIdFor(recipient)
|
||||
|
||||
// Generate the contact
|
||||
val contactIsApproved: Boolean = (!isMessageRequest || dmThreadRandomGenerator.nextBoolean())
|
||||
contactDb.setContact(contact)
|
||||
contactDb.setContactIsTrusted(contact, true, threadId)
|
||||
recipientDb.setApproved(recipient, contactIsApproved)
|
||||
recipientDb.setApprovedMe(recipient, (!isMessageRequest && (dmThreadRandomGenerator.nextInt(10) < 8))) // 80% approved the current user
|
||||
|
||||
contact.name = (0 until dmThreadRandomGenerator.nextInt(contactNameLength))
|
||||
.map { stringContent.random(dmThreadRandomGenerator.asKotlinRandom()) }
|
||||
.joinToString()
|
||||
recipientDb.setProfileName(recipient, contact.name)
|
||||
contactDb.setContact(contact)
|
||||
|
||||
// Generate the message history (Note: Unapproved message requests will only include incoming messages)
|
||||
logProgress("DM Thread $threadIndex", "Generate $numMessages Messages")
|
||||
(0 until numMessages).forEach { index ->
|
||||
val isIncoming: Boolean = (
|
||||
dmThreadRandomGenerator.nextBoolean() &&
|
||||
(!isMessageRequest || contactIsApproved)
|
||||
)
|
||||
val messageWords: Int = (1 + dmThreadRandomGenerator.nextInt(19))
|
||||
|
||||
if (isIncoming) {
|
||||
smsDb.insertMessageInbox(
|
||||
IncomingTextMessage(
|
||||
recipient.address,
|
||||
1,
|
||||
(timestampNow - (index * 5000)),
|
||||
(0 until messageWords)
|
||||
.map { wordContent.random(dmThreadRandomGenerator.asKotlinRandom()) }
|
||||
.joinToString(),
|
||||
Optional.absent(),
|
||||
0,
|
||||
false,
|
||||
-1
|
||||
),
|
||||
(timestampNow - (index * 5000)),
|
||||
false,
|
||||
false
|
||||
)
|
||||
}
|
||||
else {
|
||||
smsDb.insertMessageOutbox(
|
||||
threadId,
|
||||
OutgoingTextMessage(
|
||||
recipient,
|
||||
(0 until messageWords)
|
||||
.map { wordContent.random(dmThreadRandomGenerator.asKotlinRandom()) }
|
||||
.joinToString(),
|
||||
0,
|
||||
-1,
|
||||
(timestampNow - (index * 5000))
|
||||
),
|
||||
(timestampNow - (index * 5000)),
|
||||
false
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
logProgress("DM Thread $threadIndex", "Done")
|
||||
}
|
||||
logProgress("DM Threads", "Done")
|
||||
|
||||
dmThreadIndex += chunkSize
|
||||
}
|
||||
logProgress("DM Threads", "Done")
|
||||
|
||||
// -- Closed Group
|
||||
|
||||
val cgThreadRandomGenerator: SecureRandom = SecureRandom(cgRandomSeed.toByteArray())
|
||||
var cgThreadIndex: Int = 0
|
||||
logProgress("Closed Group Threads", "Start Generating $closedGroupThreadCount threads")
|
||||
|
||||
while (cgThreadIndex < closedGroupThreadCount) {
|
||||
val remainingThreads: Int = (closedGroupThreadCount - cgThreadIndex)
|
||||
|
||||
(0 until Math.min(chunkSize, remainingThreads)).forEach { index ->
|
||||
val threadIndex: Int = (cgThreadIndex + index)
|
||||
|
||||
logProgress("Closed Group Thread $threadIndex", "Start")
|
||||
|
||||
val dataBytes = (0 until 16).map { cgThreadRandomGenerator.nextInt(UByte.MAX_VALUE.toInt()).toByte() }
|
||||
val randomGroupPublicKey: String = KeyPairUtilities.generate(dataBytes.toByteArray()).x25519KeyPair.hexEncodedPublicKey
|
||||
val groupNameLength: Int = (5 + cgThreadRandomGenerator.nextInt(15))
|
||||
val groupName: String = (0 until groupNameLength)
|
||||
.map { stringContent.random(cgThreadRandomGenerator.asKotlinRandom()) }
|
||||
.joinToString()
|
||||
val numGroupMembers: Int = cgThreadRandomGenerator.nextInt (10)
|
||||
val numMessages: Int = (
|
||||
messageRangePerThread[threadIndex % messageRangePerThread.count()].first +
|
||||
cgThreadRandomGenerator.nextInt(messageRangePerThread[threadIndex % messageRangePerThread.count()].last())
|
||||
)
|
||||
|
||||
// Generate the Contacts in the group
|
||||
val members: MutableList<String> = mutableListOf(userSessionId)
|
||||
logProgress("Closed Group Thread $threadIndex", "Generate $numGroupMembers Contacts")
|
||||
|
||||
(0 until numGroupMembers).forEach {
|
||||
val contactBytes = (0 until 16).map { cgThreadRandomGenerator.nextInt(UByte.MAX_VALUE.toInt()).toByte() }
|
||||
val randomSessionId: String = KeyPairUtilities.generate(contactBytes.toByteArray()).x25519KeyPair.hexEncodedPublicKey
|
||||
val contactNameLength: Int = (5 + cgThreadRandomGenerator.nextInt(15))
|
||||
|
||||
val recipient = Recipient.from(context, Address.fromSerialized(randomSessionId), false)
|
||||
val contact = Contact(randomSessionId)
|
||||
contactDb.setContact(contact)
|
||||
recipientDb.setApproved(recipient, true)
|
||||
recipientDb.setApprovedMe(recipient, true)
|
||||
|
||||
contact.name = (0 until cgThreadRandomGenerator.nextInt(contactNameLength))
|
||||
.map { stringContent.random(cgThreadRandomGenerator.asKotlinRandom()) }
|
||||
.joinToString()
|
||||
recipientDb.setProfileName(recipient, contact.name)
|
||||
contactDb.setContact(contact)
|
||||
members.add(randomSessionId)
|
||||
}
|
||||
|
||||
val groupId = GroupUtil.doubleEncodeGroupID(randomGroupPublicKey)
|
||||
val threadId = storage.getOrCreateThreadIdFor(Address.fromSerialized(groupId))
|
||||
val adminUserId = members.random(cgThreadRandomGenerator.asKotlinRandom())
|
||||
storage.createGroup(
|
||||
groupId,
|
||||
groupName,
|
||||
members.map { Address.fromSerialized(it) },
|
||||
null,
|
||||
null,
|
||||
listOf(Address.fromSerialized(adminUserId)),
|
||||
timestampNow
|
||||
)
|
||||
storage.setProfileSharing(Address.fromSerialized(groupId), true)
|
||||
storage.addClosedGroupPublicKey(randomGroupPublicKey)
|
||||
|
||||
// Add the group to the user's set of public keys to poll for and store the key pair
|
||||
val encryptionKeyPair = Curve.generateKeyPair()
|
||||
storage.addClosedGroupEncryptionKeyPair(encryptionKeyPair, randomGroupPublicKey)
|
||||
storage.setExpirationTimer(groupId, 0)
|
||||
|
||||
// Add the group created message
|
||||
if (userSessionId == adminUserId) {
|
||||
storage.insertOutgoingInfoMessage(context, groupId, SignalServiceGroup.Type.CREATION, groupName, members, listOf(adminUserId), threadId, (timestampNow - (numMessages * 5000)))
|
||||
}
|
||||
else {
|
||||
storage.insertIncomingInfoMessage(context, adminUserId, groupId, SignalServiceGroup.Type.CREATION, groupName, members, listOf(adminUserId), (timestampNow - (numMessages * 5000)))
|
||||
}
|
||||
|
||||
// Generate the message history (Note: Unapproved message requests will only include incoming messages)
|
||||
logProgress("Closed Group Thread $threadIndex", "Generate $numMessages Messages")
|
||||
|
||||
(0 until numGroupMembers).forEach {
|
||||
val messageWords: Int = (1 + cgThreadRandomGenerator.nextInt(19))
|
||||
val senderId: String = members.random(cgThreadRandomGenerator.asKotlinRandom())
|
||||
|
||||
if (senderId != userSessionId) {
|
||||
smsDb.insertMessageInbox(
|
||||
IncomingTextMessage(
|
||||
Address.fromSerialized(senderId),
|
||||
1,
|
||||
(timestampNow - (index * 5000)),
|
||||
(0 until messageWords)
|
||||
.map { wordContent.random(cgThreadRandomGenerator.asKotlinRandom()) }
|
||||
.joinToString(),
|
||||
Optional.absent(),
|
||||
0,
|
||||
false,
|
||||
-1
|
||||
),
|
||||
(timestampNow - (index * 5000)),
|
||||
false,
|
||||
false
|
||||
)
|
||||
}
|
||||
else {
|
||||
smsDb.insertMessageOutbox(
|
||||
threadId,
|
||||
OutgoingTextMessage(
|
||||
threadDb.getRecipientForThreadId(threadId),
|
||||
(0 until messageWords)
|
||||
.map { wordContent.random(cgThreadRandomGenerator.asKotlinRandom()) }
|
||||
.joinToString(),
|
||||
0,
|
||||
-1,
|
||||
(timestampNow - (index * 5000))
|
||||
),
|
||||
(timestampNow - (index * 5000)),
|
||||
false
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
logProgress("Closed Group Thread $threadIndex", "Done")
|
||||
}
|
||||
|
||||
cgThreadIndex += chunkSize
|
||||
}
|
||||
logProgress("Closed Group Threads", "Done")
|
||||
|
||||
// --Open Group
|
||||
|
||||
val ogThreadRandomGenerator: SecureRandom = SecureRandom(cgRandomSeed.toByteArray())
|
||||
var ogThreadIndex: Int = 0
|
||||
logProgress("Open Group Threads", "Start Generating $openGroupThreadCount threads")
|
||||
|
||||
while (ogThreadIndex < openGroupThreadCount) {
|
||||
val remainingThreads: Int = (openGroupThreadCount - ogThreadIndex)
|
||||
|
||||
(0 until Math.min(chunkSize, remainingThreads)).forEach { index ->
|
||||
val threadIndex: Int = (ogThreadIndex + index)
|
||||
|
||||
logProgress("Open Group Thread $threadIndex", "Start")
|
||||
|
||||
val dataBytes = (0 until 32).map { ogThreadRandomGenerator.nextInt(UByte.MAX_VALUE.toInt()).toByte() }
|
||||
val randomGroupPublicKey: String = KeyPairUtilities.generate(dataBytes.toByteArray()).x25519KeyPair.hexEncodedPublicKey
|
||||
val serverNameLength: Int = (5 + ogThreadRandomGenerator.nextInt(15))
|
||||
val roomNameLength: Int = (5 + ogThreadRandomGenerator.nextInt(15))
|
||||
val roomDescriptionLength: Int = (10 + ogThreadRandomGenerator.nextInt(40))
|
||||
val serverName: String = (0 until serverNameLength)
|
||||
.map { stringContent.random(ogThreadRandomGenerator.asKotlinRandom()) }
|
||||
.joinToString()
|
||||
val roomName: String = (0 until roomNameLength)
|
||||
.map { stringContent.random(ogThreadRandomGenerator.asKotlinRandom()) }
|
||||
.joinToString()
|
||||
val roomDescription: String = (0 until roomDescriptionLength)
|
||||
.map { stringContent.random(ogThreadRandomGenerator.asKotlinRandom()) }
|
||||
.joinToString()
|
||||
val numGroupMembers: Int = ogThreadRandomGenerator.nextInt(250)
|
||||
val numMessages: Int = (
|
||||
messageRangePerThread[threadIndex % messageRangePerThread.count()].first +
|
||||
ogThreadRandomGenerator.nextInt(messageRangePerThread[threadIndex % messageRangePerThread.count()].last())
|
||||
)
|
||||
|
||||
// Generate the Contacts in the group
|
||||
val members: MutableList<String> = mutableListOf(userSessionId)
|
||||
logProgress("Open Group Thread $threadIndex", "Generate $numGroupMembers Contacts")
|
||||
|
||||
(0 until numGroupMembers).forEach {
|
||||
val contactBytes = (0 until 16).map { ogThreadRandomGenerator.nextInt(UByte.MAX_VALUE.toInt()).toByte() }
|
||||
val randomSessionId: String = KeyPairUtilities.generate(contactBytes.toByteArray()).x25519KeyPair.hexEncodedPublicKey
|
||||
val contactNameLength: Int = (5 + ogThreadRandomGenerator.nextInt(15))
|
||||
|
||||
val recipient = Recipient.from(context, Address.fromSerialized(randomSessionId), false)
|
||||
val contact = Contact(randomSessionId)
|
||||
contactDb.setContact(contact)
|
||||
recipientDb.setApproved(recipient, true)
|
||||
recipientDb.setApprovedMe(recipient, true)
|
||||
|
||||
contact.name = (0 until ogThreadRandomGenerator.nextInt(contactNameLength))
|
||||
.map { stringContent.random(cgThreadRandomGenerator.asKotlinRandom()) }
|
||||
.joinToString()
|
||||
recipientDb.setProfileName(recipient, contact.name)
|
||||
contactDb.setContact(contact)
|
||||
members.add(randomSessionId)
|
||||
}
|
||||
|
||||
// Create the open group model and the thread
|
||||
val openGroupId = "$serverName.$roomName"
|
||||
val threadId = GroupManager.createOpenGroup(openGroupId, context, null, roomName).threadId
|
||||
val hasBlinding: Boolean = ogThreadRandomGenerator.nextBoolean()
|
||||
|
||||
// Generate the capabilities and other data
|
||||
storage.setOpenGroupPublicKey(serverName, randomGroupPublicKey)
|
||||
storage.setServerCapabilities(
|
||||
serverName,
|
||||
(
|
||||
listOf(OpenGroupApi.Capability.SOGS.name.lowercase()) +
|
||||
if (hasBlinding) { listOf(OpenGroupApi.Capability.BLIND.name.lowercase()) } else { emptyList() }
|
||||
)
|
||||
)
|
||||
storage.setUserCount(roomName, serverName, numGroupMembers)
|
||||
lokiThreadDB.setOpenGroupChat(OpenGroup(server = serverName, room = roomName, publicKey = randomGroupPublicKey, name = roomName, imageId = null, infoUpdates = 0), threadId)
|
||||
|
||||
// Generate the message history (Note: Unapproved message requests will only include incoming messages)
|
||||
logProgress("Open Group Thread $threadIndex", "Generate $numMessages Messages")
|
||||
|
||||
(0 until numMessages).forEach { index ->
|
||||
val messageWords: Int = (1 + ogThreadRandomGenerator.nextInt(19))
|
||||
val senderId: String = members.random(ogThreadRandomGenerator.asKotlinRandom())
|
||||
|
||||
if (senderId != userSessionId) {
|
||||
smsDb.insertMessageInbox(
|
||||
IncomingTextMessage(
|
||||
Address.fromSerialized(senderId),
|
||||
1,
|
||||
(timestampNow - (index * 5000)),
|
||||
(0 until messageWords)
|
||||
.map { wordContent.random(ogThreadRandomGenerator.asKotlinRandom()) }
|
||||
.joinToString(),
|
||||
Optional.absent(),
|
||||
0,
|
||||
false,
|
||||
-1
|
||||
),
|
||||
(timestampNow - (index * 5000)),
|
||||
false,
|
||||
false
|
||||
)
|
||||
} else {
|
||||
smsDb.insertMessageOutbox(
|
||||
threadId,
|
||||
OutgoingTextMessage(
|
||||
threadDb.getRecipientForThreadId(threadId),
|
||||
(0 until messageWords)
|
||||
.map { wordContent.random(ogThreadRandomGenerator.asKotlinRandom()) }
|
||||
.joinToString(),
|
||||
0,
|
||||
-1,
|
||||
(timestampNow - (index * 5000))
|
||||
),
|
||||
(timestampNow - (index * 5000)),
|
||||
false
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
logProgress("Open Group Thread $threadIndex", "Done")
|
||||
}
|
||||
|
||||
ogThreadIndex += chunkSize
|
||||
}
|
||||
|
||||
logProgress("Open Group Threads", "Done")
|
||||
logProgress("", "Complete")
|
||||
}
|
||||
}
|
@ -1,7 +1,9 @@
|
||||
package org.thoughtcrime.securesms.webrtc
|
||||
|
||||
import android.content.Context
|
||||
import android.content.pm.PackageManager
|
||||
import android.telephony.TelephonyManager
|
||||
import androidx.core.content.ContextCompat
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.asSharedFlow
|
||||
import kotlinx.serialization.json.Json
|
||||
@ -176,8 +178,22 @@ class CallManager(context: Context, audioManager: AudioManagerCompat, private va
|
||||
_callStateEvents.value = newState
|
||||
}
|
||||
|
||||
fun isBusy(context: Context, callId: UUID) = callId != this.callId && (currentConnectionState != CallState.Idle
|
||||
|| context.getSystemService(TelephonyManager::class.java).callState != TelephonyManager.CALL_STATE_IDLE)
|
||||
fun isBusy(context: Context, callId: UUID): Boolean {
|
||||
// Make sure we have the permission before accessing the callState
|
||||
if (ContextCompat.checkSelfPermission(context, android.Manifest.permission.READ_PHONE_STATE) == PackageManager.PERMISSION_GRANTED) {
|
||||
return (
|
||||
callId != this.callId && (
|
||||
currentConnectionState != CallState.Idle ||
|
||||
context.getSystemService(TelephonyManager::class.java).callState != TelephonyManager.CALL_STATE_IDLE
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
callId != this.callId &&
|
||||
currentConnectionState != CallState.Idle
|
||||
)
|
||||
}
|
||||
|
||||
fun isPreOffer() = currentConnectionState == CallState.RemotePreOffer
|
||||
|
||||
|
@ -12,6 +12,7 @@ import org.session.libsession.database.StorageProtocol
|
||||
import org.session.libsession.messaging.calls.CallMessageType
|
||||
import org.session.libsession.messaging.messages.control.CallMessage
|
||||
import org.session.libsession.messaging.utilities.WebRtcUtils
|
||||
import org.session.libsession.snode.SnodeAPI
|
||||
import org.session.libsession.utilities.Address
|
||||
import org.session.libsession.utilities.TextSecurePreferences
|
||||
import org.session.libsession.utilities.recipients.Recipient
|
||||
@ -29,6 +30,10 @@ import org.webrtc.IceCandidate
|
||||
|
||||
class CallMessageProcessor(private val context: Context, private val textSecurePreferences: TextSecurePreferences, lifecycle: Lifecycle, private val storage: StorageProtocol) {
|
||||
|
||||
companion object {
|
||||
private const val VERY_EXPIRED_TIME = 15 * 60 * 1000L
|
||||
}
|
||||
|
||||
init {
|
||||
lifecycle.coroutineScope.launch(IO) {
|
||||
while (isActive) {
|
||||
@ -53,6 +58,13 @@ class CallMessageProcessor(private val context: Context, private val textSecureP
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
val isVeryExpired = (nextMessage.sentTimestamp?:0) + VERY_EXPIRED_TIME < SnodeAPI.nowWithOffset
|
||||
if (isVeryExpired) {
|
||||
Log.e("Loki", "Dropping very expired call message")
|
||||
continue
|
||||
}
|
||||
|
||||
when (nextMessage.type) {
|
||||
OFFER -> incomingCall(nextMessage)
|
||||
ANSWER -> incomingAnswer(nextMessage)
|
||||
@ -78,7 +90,7 @@ class CallMessageProcessor(private val context: Context, private val textSecureP
|
||||
private fun incomingHangup(callMessage: CallMessage) {
|
||||
val callId = callMessage.callId ?: return
|
||||
val hangupIntent = WebRtcCallService.remoteHangupIntent(context, callId)
|
||||
ContextCompat.startForegroundService(context, hangupIntent)
|
||||
context.startService(hangupIntent)
|
||||
}
|
||||
|
||||
private fun incomingAnswer(callMessage: CallMessage) {
|
||||
@ -91,7 +103,7 @@ class CallMessageProcessor(private val context: Context, private val textSecureP
|
||||
sdp = sdp,
|
||||
callId = callId
|
||||
)
|
||||
ContextCompat.startForegroundService(context, answerIntent)
|
||||
context.startService(answerIntent)
|
||||
}
|
||||
|
||||
private fun handleIceCandidates(callMessage: CallMessage) {
|
||||
@ -120,7 +132,7 @@ class CallMessageProcessor(private val context: Context, private val textSecureP
|
||||
callId = callId,
|
||||
callTime = callMessage.sentTimestamp!!
|
||||
)
|
||||
ContextCompat.startForegroundService(context, incomingIntent)
|
||||
context.startService(incomingIntent)
|
||||
}
|
||||
|
||||
private fun incomingCall(callMessage: CallMessage) {
|
||||
@ -134,8 +146,7 @@ class CallMessageProcessor(private val context: Context, private val textSecureP
|
||||
callId = callId,
|
||||
callTime = callMessage.sentTimestamp!!
|
||||
)
|
||||
ContextCompat.startForegroundService(context, incomingIntent)
|
||||
|
||||
context.startService(incomingIntent)
|
||||
}
|
||||
|
||||
private fun CallMessage.iceCandidates(): List<IceCandidate> {
|
||||
|
@ -6,7 +6,7 @@
|
||||
android:layout_width="@dimen/media_bubble_default_dimens"
|
||||
android:layout_height="@dimen/media_bubble_default_dimens">
|
||||
|
||||
<org.thoughtcrime.securesms.conversation.v2.utilities.KThumbnailView
|
||||
<include layout="@layout/thumbnail_view"
|
||||
android:id="@+id/album_cell_1"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
|
@ -7,13 +7,13 @@
|
||||
android:layout_width="@dimen/album_total_width"
|
||||
android:layout_height="@dimen/album_2_total_height">
|
||||
|
||||
<org.thoughtcrime.securesms.conversation.v2.utilities.KThumbnailView
|
||||
<include layout="@layout/thumbnail_view"
|
||||
android:id="@+id/album_cell_1"
|
||||
android:layout_width="@dimen/album_2_cell_width"
|
||||
android:layout_height="@dimen/album_2_total_height"
|
||||
app:thumbnail_radius="0dp"/>
|
||||
|
||||
<org.thoughtcrime.securesms.conversation.v2.utilities.KThumbnailView
|
||||
<include layout="@layout/thumbnail_view"
|
||||
android:id="@+id/album_cell_2"
|
||||
android:layout_width="@dimen/album_2_cell_width"
|
||||
android:layout_height="@dimen/album_2_total_height"
|
||||
|
@ -6,13 +6,13 @@
|
||||
android:layout_width="@dimen/album_total_width"
|
||||
android:layout_height="@dimen/album_3_total_height">
|
||||
|
||||
<org.thoughtcrime.securesms.conversation.v2.utilities.KThumbnailView
|
||||
<include layout="@layout/thumbnail_view"
|
||||
android:id="@+id/album_cell_1"
|
||||
android:layout_width="@dimen/album_3_cell_width_big"
|
||||
android:layout_height="@dimen/album_3_total_height"
|
||||
app:thumbnail_radius="0dp"/>
|
||||
|
||||
<org.thoughtcrime.securesms.conversation.v2.utilities.KThumbnailView
|
||||
<include layout="@layout/thumbnail_view"
|
||||
android:id="@+id/album_cell_2"
|
||||
android:layout_width="@dimen/album_3_cell_size_small"
|
||||
android:layout_height="@dimen/album_3_cell_size_small"
|
||||
@ -25,7 +25,7 @@
|
||||
android:layout_height="@dimen/album_5_cell_size_small"
|
||||
android:layout_gravity="end|bottom">
|
||||
|
||||
<org.thoughtcrime.securesms.conversation.v2.utilities.KThumbnailView
|
||||
<include layout="@layout/thumbnail_view"
|
||||
android:id="@+id/album_cell_3"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_width="match_parent"
|
||||
|
@ -1,5 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
<org.thoughtcrime.securesms.conversation.v2.components.AlbumThumbnailView
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
@ -20,4 +21,4 @@
|
||||
android:layout_gravity="center"
|
||||
android:layout="@layout/transfer_controls_stub" />
|
||||
|
||||
</RelativeLayout>
|
||||
</org.thoughtcrime.securesms.conversation.v2.components.AlbumThumbnailView>
|
@ -1,104 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<merge
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:id="@+id/linkpreview_container"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:padding="@dimen/small_spacing"
|
||||
android:background="?linkpreview_background_color">
|
||||
|
||||
<org.thoughtcrime.securesms.components.OutlinedThumbnailView
|
||||
android:id="@+id/linkpreview_thumbnail"
|
||||
android:layout_width="72dp"
|
||||
android:layout_height="0dp"
|
||||
android:scaleType="centerCrop"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintBottom_toTopOf="@+id/linkpreview_divider"
|
||||
app:layout_constraintHeight_min="72dp"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="@+id/linkpreview_title"
|
||||
tools:src="@drawable/ic_contact_picture"
|
||||
tools:visibility="visible" />
|
||||
|
||||
<org.thoughtcrime.securesms.components.emoji.EmojiTextView
|
||||
android:id="@+id/linkpreview_title"
|
||||
style="@style/Signal.Text.Body"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="8dp"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:ellipsize="end"
|
||||
android:fontFamily="sans-serif-medium"
|
||||
android:maxLines="1"
|
||||
android:textSize="@dimen/medium_font_size"
|
||||
android:textColor="?linkpreview_primary_text_color"
|
||||
app:layout_constraintEnd_toStartOf="@+id/linkpreview_close"
|
||||
app:layout_constraintHorizontal_bias="0.0"
|
||||
app:layout_constraintStart_toEndOf="@+id/linkpreview_thumbnail"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
tools:text="Wall Crawler Strikes Again!" />
|
||||
|
||||
<org.thoughtcrime.securesms.components.emoji.EmojiTextView
|
||||
android:id="@+id/linkpreview_site"
|
||||
style="@style/Signal.Text.Caption"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="8dp"
|
||||
android:layout_marginTop="2dp"
|
||||
android:maxLines="1"
|
||||
android:textSize="@dimen/small_font_size"
|
||||
android:textColor="?linkpreview_secondary_text_color"
|
||||
android:alpha="0.6"
|
||||
app:layout_constraintStart_toEndOf="@+id/linkpreview_thumbnail"
|
||||
app:layout_constraintTop_toBottomOf="@+id/linkpreview_title"
|
||||
tools:text="dailybugle.com" />
|
||||
|
||||
<View
|
||||
android:id="@+id/linkpreview_divider"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="1dp"
|
||||
android:layout_marginStart="8dp"
|
||||
android:layout_marginTop="6dp"
|
||||
android:background="?linkpreview_divider_color"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintBottom_toBottomOf="@+id/linkpreview_thumbnail"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@+id/linkpreview_thumbnail"
|
||||
app:layout_constraintTop_toBottomOf="@+id/linkpreview_site"
|
||||
app:layout_constraintVertical_bias="0.0"
|
||||
tools:visibility="visible" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/linkpreview_close"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginEnd="6dp"
|
||||
android:layout_marginTop="4dp"
|
||||
android:src="@drawable/ic_close_white_18dp"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:tint="@color/gray70"
|
||||
tools:visibility="visible" />
|
||||
|
||||
<com.github.ybq.android.spinkit.SpinKitView
|
||||
style="@style/SpinKitView.DoubleBounce"
|
||||
android:id="@+id/linkpreview_progress_wheel"
|
||||
android:layout_width="72dp"
|
||||
android:layout_height="72dp"
|
||||
android:layout_gravity="center"
|
||||
android:padding="@dimen/small_spacing"
|
||||
app:SpinKit_Color="?android:textColorPrimary"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintBottom_toTopOf="@+id/linkpreview_divider"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
</merge>
|
@ -5,7 +5,7 @@
|
||||
android:layout_height="match_parent"
|
||||
android:padding="2dp">
|
||||
|
||||
<org.thoughtcrime.securesms.conversation.v2.utilities.ThumbnailView
|
||||
<include layout="@layout/thumbnail_view"
|
||||
android:id="@+id/image"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
|
@ -8,7 +8,7 @@
|
||||
android:layout_margin="2dp"
|
||||
android:animateLayoutChanges="true">
|
||||
|
||||
<org.thoughtcrime.securesms.conversation.v2.utilities.ThumbnailView
|
||||
<include layout="@layout/thumbnail_view"
|
||||
android:id="@+id/rail_item_image"
|
||||
android:layout_width="56dp"
|
||||
android:layout_height="56dp"
|
||||
|
@ -1,8 +1,11 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<merge
|
||||
<org.thoughtcrime.securesms.conversation.v2.utilities.ThumbnailView
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="@color/transparent_black_6">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/thumbnail_image"
|
||||
@ -60,4 +63,4 @@
|
||||
android:layout="@layout/transfer_controls_stub"
|
||||
android:visibility="gone" />
|
||||
|
||||
</merge>
|
||||
</org.thoughtcrime.securesms.conversation.v2.utilities.ThumbnailView>
|
||||
|
@ -34,64 +34,69 @@
|
||||
android:layout_gravity="center_vertical"
|
||||
android:orientation="vertical">
|
||||
|
||||
<LinearLayout
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:gravity="center_vertical">
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="0dp"
|
||||
<TextView
|
||||
android:id="@+id/conversationViewDisplayNameTextView"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:orientation="horizontal"
|
||||
android:gravity="center_vertical">
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toStartOf="@id/unreadCountIndicator"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constrainedWidth="true"
|
||||
app:layout_constraintHorizontal_chainStyle="packed"
|
||||
app:layout_constraintHorizontal_bias="0"
|
||||
android:drawablePadding="4dp"
|
||||
android:maxLines="1"
|
||||
android:ellipsize="end"
|
||||
android:textAlignment="viewStart"
|
||||
android:textSize="@dimen/medium_font_size"
|
||||
android:textStyle="bold"
|
||||
android:textColor="?android:textColorPrimary"
|
||||
app:drawableTint="?conversation_pinned_icon_color"
|
||||
tools:drawableRight="@drawable/ic_pin"
|
||||
tools:text="I'm a very long display name. What are you going to do about it?" />
|
||||
|
||||
<RelativeLayout
|
||||
android:id="@+id/unreadCountIndicator"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="20dp"
|
||||
android:layout_marginStart="4dp"
|
||||
app:layout_constraintStart_toEndOf="@id/conversationViewDisplayNameTextView"
|
||||
app:layout_constraintEnd_toStartOf="@id/timestampTextView"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
android:minWidth="20dp"
|
||||
android:maxWidth="40dp"
|
||||
android:paddingLeft="4dp"
|
||||
android:paddingRight="4dp"
|
||||
android:background="@drawable/rounded_rectangle"
|
||||
android:backgroundTint="?unreadIndicatorBackgroundColor">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/conversationViewDisplayNameTextView"
|
||||
android:id="@+id/unreadCountTextView"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:drawablePadding="4dp"
|
||||
android:maxLines="1"
|
||||
android:ellipsize="end"
|
||||
android:textAlignment="viewStart"
|
||||
android:textSize="@dimen/medium_font_size"
|
||||
android:layout_centerInParent="true"
|
||||
android:textColor="?unreadIndicatorTextColor"
|
||||
android:textSize="@dimen/very_small_font_size"
|
||||
android:textStyle="bold"
|
||||
android:textColor="?android:textColorPrimary"
|
||||
app:drawableTint="?conversation_pinned_icon_color"
|
||||
tools:drawableRight="@drawable/ic_pin"
|
||||
tools:text="I'm a very long display name. What are you going to do about it?" />
|
||||
tools:text="8"
|
||||
tools:textColor="?android:textColorPrimary" />
|
||||
|
||||
<RelativeLayout
|
||||
android:id="@+id/unreadCountIndicator"
|
||||
android:layout_width="wrap_content"
|
||||
android:maxWidth="40dp"
|
||||
android:paddingLeft="4dp"
|
||||
android:paddingRight="4dp"
|
||||
android:layout_height="20dp"
|
||||
android:layout_marginStart="4dp"
|
||||
android:background="@drawable/rounded_rectangle"
|
||||
android:backgroundTint="?unreadIndicatorBackgroundColor">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/unreadCountTextView"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_centerInParent="true"
|
||||
android:text="8"
|
||||
android:textColor="?unreadIndicatorTextColor"
|
||||
android:textSize="@dimen/very_small_font_size"
|
||||
android:textStyle="bold" />
|
||||
|
||||
</RelativeLayout>
|
||||
|
||||
</LinearLayout>
|
||||
</RelativeLayout>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/timestampTextView"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="@dimen/medium_spacing"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
android:paddingStart="@dimen/medium_spacing"
|
||||
android:maxLines="1"
|
||||
android:ellipsize="end"
|
||||
android:textSize="@dimen/small_font_size"
|
||||
@ -99,7 +104,7 @@
|
||||
android:alpha="0.4"
|
||||
tools:text="9:41 AM" />
|
||||
|
||||
</LinearLayout>
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
@ -131,7 +136,7 @@
|
||||
android:textSize="@dimen/medium_font_size"
|
||||
tools:text="Sorry, gotta go fight crime again" />
|
||||
|
||||
<org.thoughtcrime.securesms.conversation.v2.components.TypingIndicatorView
|
||||
<include layout="@layout/view_typing_indicator"
|
||||
android:id="@+id/typingIndicatorView"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
|
@ -17,7 +17,7 @@
|
||||
android:background="@drawable/message_bubble_background_received_alone"
|
||||
android:backgroundTint="?message_received_background_color">
|
||||
|
||||
<org.thoughtcrime.securesms.conversation.v2.components.TypingIndicatorView
|
||||
<include layout="@layout/view_typing_indicator"
|
||||
android:id="@+id/typingIndicator"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
|
@ -1,9 +1,10 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
<org.thoughtcrime.securesms.conversation.v2.messages.EmojiReactionsView
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:padding="@dimen/small_spacing"
|
||||
android:gravity="center">
|
||||
|
||||
@ -65,4 +66,4 @@
|
||||
android:visibility="gone"
|
||||
app:constraint_referenced_ids="image_view_show_less, text_view_show_less"/>
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
</org.thoughtcrime.securesms.conversation.v2.messages.EmojiReactionsView>
|
@ -1,54 +1,47 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout android:id="@+id/mainLinkPreviewContainer"
|
||||
<org.thoughtcrime.securesms.conversation.v2.messages.LinkPreviewView
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:id="@+id/mainLinkPreviewContainer"
|
||||
android:background="@color/transparent_black_6"
|
||||
android:layout_width="300dp"
|
||||
android:layout_height="wrap_content"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:orientation="vertical"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
android:orientation="horizontal"
|
||||
android:gravity="center">
|
||||
|
||||
<LinearLayout
|
||||
android:background="@color/transparent_black_6"
|
||||
android:id="@+id/mainLinkPreviewParent"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:gravity="center">
|
||||
<RelativeLayout
|
||||
android:layout_width="96dp"
|
||||
android:layout_height="96dp">
|
||||
|
||||
<RelativeLayout
|
||||
android:layout_width="96dp"
|
||||
android:layout_height="96dp">
|
||||
<ImageView
|
||||
android:layout_width="24dp"
|
||||
android:layout_height="24dp"
|
||||
android:layout_centerInParent="true"
|
||||
android:src="@drawable/ic_link"
|
||||
app:tint="?android:textColorPrimary" />
|
||||
|
||||
<ImageView
|
||||
android:layout_width="24dp"
|
||||
android:layout_height="24dp"
|
||||
android:layout_centerInParent="true"
|
||||
android:src="@drawable/ic_link"
|
||||
app:tint="?android:textColorPrimary" />
|
||||
|
||||
<org.thoughtcrime.securesms.conversation.v2.utilities.KThumbnailView
|
||||
android:background="@color/transparent_black_6"
|
||||
android:id="@+id/thumbnailImageView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:scaleType="centerCrop" />
|
||||
|
||||
</RelativeLayout>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/titleTextView"
|
||||
<include layout="@layout/thumbnail_view"
|
||||
android:background="@color/transparent_black_6"
|
||||
android:id="@+id/thumbnailImageView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:paddingHorizontal="12dp"
|
||||
android:gravity="center_vertical"
|
||||
android:textSize="@dimen/small_font_size"
|
||||
android:textStyle="bold"
|
||||
tools:text="Some Text here"
|
||||
android:minWidth="@dimen/media_bubble_min_width"
|
||||
android:maxLines="3"
|
||||
android:ellipsize="end"
|
||||
android:textColor="?android:textColorPrimary"/>
|
||||
android:scaleType="centerCrop" />
|
||||
|
||||
</LinearLayout>
|
||||
</RelativeLayout>
|
||||
|
||||
</LinearLayout>
|
||||
<TextView
|
||||
android:id="@+id/titleTextView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:paddingHorizontal="12dp"
|
||||
android:gravity="center_vertical"
|
||||
android:textSize="@dimen/small_font_size"
|
||||
android:textStyle="bold"
|
||||
tools:text="Some Text here"
|
||||
android:minWidth="@dimen/media_bubble_min_width"
|
||||
android:maxLines="3"
|
||||
android:ellipsize="end"
|
||||
android:textColor="?android:textColorPrimary"/>
|
||||
|
||||
</org.thoughtcrime.securesms.conversation.v2.messages.LinkPreviewView>
|
@ -26,7 +26,7 @@
|
||||
android:src="@drawable/ic_link"
|
||||
app:tint="?android:textColorPrimary" />
|
||||
|
||||
<org.thoughtcrime.securesms.conversation.v2.utilities.ThumbnailView
|
||||
<include layout="@layout/thumbnail_view"
|
||||
android:id="@+id/thumbnailImageView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
|
@ -44,7 +44,7 @@
|
||||
android:scaleType="centerInside"
|
||||
android:src="@drawable/ic_microphone" />
|
||||
|
||||
<org.thoughtcrime.securesms.conversation.v2.utilities.ThumbnailView
|
||||
<include layout="@layout/thumbnail_view"
|
||||
android:id="@+id/quoteViewAttachmentThumbnailImageView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
|
@ -44,7 +44,7 @@
|
||||
android:scaleType="centerInside"
|
||||
android:src="@drawable/ic_microphone" />
|
||||
|
||||
<org.thoughtcrime.securesms.conversation.v2.utilities.ThumbnailView
|
||||
<include layout="@layout/thumbnail_view"
|
||||
android:id="@+id/quoteViewAttachmentThumbnailImageView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
|
@ -1,11 +1,10 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<merge
|
||||
<org.thoughtcrime.securesms.conversation.v2.components.TypingIndicatorView
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:orientation="horizontal"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
tools:context="org.thoughtcrime.securesms.conversation.v2.components.TypingIndicatorView">
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<View
|
||||
android:id="@+id/typing_dot1"
|
||||
@ -37,4 +36,4 @@
|
||||
android:alpha="0.5"
|
||||
android:background="@drawable/circle_white" />
|
||||
|
||||
</merge>
|
||||
</org.thoughtcrime.securesms.conversation.v2.components.TypingIndicatorView>
|
@ -1,5 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<org.thoughtcrime.securesms.conversation.v2.messages.VisibleMessageView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
<org.thoughtcrime.securesms.conversation.v2.messages.VisibleMessageView
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:id="@+id/visibleMessageView"
|
||||
@ -76,7 +77,7 @@
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/senderNameTextView">
|
||||
|
||||
<org.thoughtcrime.securesms.conversation.v2.messages.VisibleMessageContentView
|
||||
<include layout="@layout/view_visible_message_content"
|
||||
android:id="@+id/messageContentView"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content" />
|
||||
@ -99,7 +100,7 @@
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<org.thoughtcrime.securesms.conversation.v2.messages.EmojiReactionsView
|
||||
<include layout="@layout/view_emoji_reactions"
|
||||
android:id="@+id/emojiReactionsView"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
|
@ -1,5 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
<org.thoughtcrime.securesms.conversation.v2.messages.VisibleMessageContentView
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
@ -75,7 +75,7 @@
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"/>
|
||||
|
||||
<org.thoughtcrime.securesms.conversation.v2.messages.LinkPreviewView
|
||||
<include layout="@layout/view_link_preview"
|
||||
app:layout_constraintTop_toBottomOf="@+id/quoteView"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
android:visibility="gone"
|
||||
@ -112,8 +112,8 @@
|
||||
android:layout_height="wrap_content"/>
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
<org.thoughtcrime.securesms.conversation.v2.components.AlbumThumbnailView
|
||||
android:visibility="visible"
|
||||
<include layout="@layout/album_thumbnail_view"
|
||||
android:visibility="gone"
|
||||
android:id="@+id/albumThumbnailView"
|
||||
android:layout_marginTop="4dp"
|
||||
app:layout_constraintTop_toBottomOf="@+id/contentParent"
|
||||
@ -123,4 +123,4 @@
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"/>
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
</org.thoughtcrime.securesms.conversation.v2.messages.VisibleMessageContentView>
|
@ -864,6 +864,8 @@
|
||||
<string name="fragment_enter_community_url_join_button_title">Join</string>
|
||||
<string name="new_conversation_dialog_back_button_content_description">Navigate Back</string>
|
||||
<string name="new_conversation_dialog_close_button_content_description">Close Dialog</string>
|
||||
<string name="ErrorNotifier_migration">Database Upgrade Failed</string>
|
||||
<string name="ErrorNotifier_migration_downgrade">Please contact support to report the error.</string>
|
||||
<string name="delivery_status_sending">Sending</string>
|
||||
<string name="delivery_status_read">Read</string>
|
||||
<string name="delivery_status_sent">Sent</string>
|
||||
|
@ -20,7 +20,9 @@ interface MessageDataProvider {
|
||||
* @return pair of sms or mms table-specific ID and whether it is in SMS table
|
||||
*/
|
||||
fun getMessageID(serverId: Long, threadId: Long): Pair<Long, Boolean>?
|
||||
fun getMessageIDs(serverIDs: List<Long>, threadID: Long): Pair<List<Long>, List<Long>>
|
||||
fun deleteMessage(messageID: Long, isSms: Boolean)
|
||||
fun deleteMessages(messageIDs: List<Long>, threadId: Long, isSms: Boolean)
|
||||
fun updateMessageAsDeleted(timestamp: Long, author: String)
|
||||
fun getServerHashForMessage(messageID: Long): String?
|
||||
fun getDatabaseAttachment(attachmentId: Long): DatabaseAttachment?
|
||||
|
@ -16,6 +16,7 @@ import org.session.libsession.messaging.messages.visible.Reaction
|
||||
import org.session.libsession.messaging.messages.visible.VisibleMessage
|
||||
import org.session.libsession.messaging.open_groups.GroupMember
|
||||
import org.session.libsession.messaging.open_groups.OpenGroup
|
||||
import org.session.libsession.messaging.open_groups.OpenGroupApi
|
||||
import org.session.libsession.messaging.sending_receiving.attachments.AttachmentId
|
||||
import org.session.libsession.messaging.sending_receiving.attachments.DatabaseAttachment
|
||||
import org.session.libsession.messaging.sending_receiving.data_extraction.DataExtractionNotificationInfoMessage
|
||||
@ -66,7 +67,7 @@ interface StorageProtocol {
|
||||
fun getAllOpenGroups(): Map<Long, OpenGroup>
|
||||
fun updateOpenGroup(openGroup: OpenGroup)
|
||||
fun getOpenGroup(threadId: Long): OpenGroup?
|
||||
fun addOpenGroup(urlAsString: String)
|
||||
fun addOpenGroup(urlAsString: String): OpenGroupApi.RoomInfo?
|
||||
fun onOpenGroupAdded(server: String)
|
||||
fun hasBackgroundGroupAddJob(groupJoinUrl: String): Boolean
|
||||
fun setOpenGroupServerMessageID(messageID: Long, serverID: Long, threadID: Long, isSms: Boolean)
|
||||
@ -80,6 +81,7 @@ interface StorageProtocol {
|
||||
// Open Group Metadata
|
||||
fun updateTitle(groupID: String, newValue: String)
|
||||
fun updateProfilePicture(groupID: String, newValue: ByteArray)
|
||||
fun hasDownloadedProfilePicture(groupID: String): Boolean
|
||||
fun setUserCount(room: String, server: String, newValue: Int)
|
||||
|
||||
// Last Message Server ID
|
||||
|
@ -77,7 +77,11 @@ object FileServerApi {
|
||||
OnionRequestAPI.sendOnionRequest(requestBuilder.build(), server, serverPublicKey).map {
|
||||
it.body ?: throw Error.ParsingFailed
|
||||
}.fail { e ->
|
||||
Log.e("Loki", "File server request failed.", e)
|
||||
when (e) {
|
||||
// No need for the stack trace for HTTP errors
|
||||
is HTTP.HTTPRequestFailedException -> Log.e("Loki", "File server request failed due to error: ${e.message}")
|
||||
else -> Log.e("Loki", "File server request failed", e)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Promise.ofFail(IllegalStateException("It's currently not allowed to send non onion routed requests."))
|
||||
|
@ -41,15 +41,10 @@ class BackgroundGroupAddJob(val joinUrl: String): Job {
|
||||
}
|
||||
// get image
|
||||
storage.setOpenGroupPublicKey(openGroup.server, openGroup.serverPublicKey)
|
||||
val (capabilities, info) = OpenGroupApi.getCapabilitiesAndRoomInfo(openGroup.room, openGroup.server, false).get()
|
||||
storage.setServerCapabilities(openGroup.server, capabilities.capabilities)
|
||||
val imageId = info.imageId
|
||||
storage.addOpenGroup(openGroup.joinUrl())
|
||||
val info = storage.addOpenGroup(openGroup.joinUrl())
|
||||
val imageId = info?.imageId
|
||||
if (imageId != null) {
|
||||
val bytes = OpenGroupApi.downloadOpenGroupProfilePicture(openGroup.server, openGroup.room, imageId).get()
|
||||
val groupId = GroupUtil.getEncodedOpenGroupID("${openGroup.server}.${openGroup.room}".toByteArray())
|
||||
storage.updateProfilePicture(groupId, bytes)
|
||||
storage.updateTimestampUpdated(groupId, System.currentTimeMillis())
|
||||
JobQueue.shared.add(GroupAvatarDownloadJob(openGroup.room, openGroup.server))
|
||||
}
|
||||
Log.d(KEY, "onOpenGroupAdded(${openGroup.server})")
|
||||
storage.onOpenGroupAdded(openGroup.server)
|
||||
|
@ -94,12 +94,23 @@ class BatchMessageReceiveJob(
|
||||
threadMap[threadID]!! += parsedParams
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Couldn't receive message.", e)
|
||||
if (e is MessageReceiver.Error && !e.isRetryable) {
|
||||
Log.e(TAG, "Message failed permanently",e)
|
||||
} else {
|
||||
Log.e(TAG, "Message failed",e)
|
||||
failures += messageParameters
|
||||
when (e) {
|
||||
is MessageReceiver.Error.DuplicateMessage, MessageReceiver.Error.SelfSend -> {
|
||||
Log.i(TAG, "Couldn't receive message, failed with error: ${e.message}")
|
||||
}
|
||||
is MessageReceiver.Error -> {
|
||||
if (!e.isRetryable) {
|
||||
Log.e(TAG, "Couldn't receive message, failed permanently", e)
|
||||
}
|
||||
else {
|
||||
Log.e(TAG, "Couldn't receive message, failed", e)
|
||||
failures += messageParameters
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
Log.e(TAG, "Couldn't receive message, failed", e)
|
||||
failures += messageParameters
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -14,10 +14,9 @@ class GroupAvatarDownloadJob(val room: String, val server: String) : Job {
|
||||
|
||||
override fun execute() {
|
||||
val storage = MessagingModuleConfiguration.shared.storage
|
||||
val imageId = storage.getOpenGroup(room, server)?.imageId ?: return
|
||||
try {
|
||||
val info = OpenGroupApi.getRoomInfo(room, server).get()
|
||||
val imageId = info.imageId ?: return
|
||||
val bytes = OpenGroupApi.downloadOpenGroupProfilePicture(server, info.token, imageId).get()
|
||||
val bytes = OpenGroupApi.downloadOpenGroupProfilePicture(server, room, imageId).get()
|
||||
val groupId = GroupUtil.getEncodedOpenGroupID("$server.$room".toByteArray())
|
||||
storage.updateProfilePicture(groupId, bytes)
|
||||
storage.updateTimestampUpdated(groupId, System.currentTimeMillis())
|
||||
|
@ -26,7 +26,7 @@ class JobQueue : JobDelegate {
|
||||
private val jobTimestampMap = ConcurrentHashMap<Long, AtomicInteger>()
|
||||
private val rxDispatcher = Executors.newSingleThreadExecutor().asCoroutineDispatcher()
|
||||
private val rxMediaDispatcher = Executors.newFixedThreadPool(4).asCoroutineDispatcher()
|
||||
private val openGroupDispatcher = Executors.newCachedThreadPool().asCoroutineDispatcher()
|
||||
private val openGroupDispatcher = Executors.newFixedThreadPool(8).asCoroutineDispatcher()
|
||||
private val txDispatcher = Executors.newSingleThreadExecutor().asCoroutineDispatcher()
|
||||
private val scope = CoroutineScope(Dispatchers.Default) + SupervisorJob()
|
||||
private val queue = Channel<Job>(UNLIMITED)
|
||||
|
@ -11,6 +11,7 @@ import org.session.libsession.messaging.messages.visible.VisibleMessage
|
||||
import org.session.libsession.messaging.sending_receiving.MessageSender
|
||||
import org.session.libsession.messaging.utilities.Data
|
||||
import org.session.libsession.snode.OnionRequestAPI
|
||||
import org.session.libsignal.utilities.HTTP
|
||||
import org.session.libsignal.utilities.Log
|
||||
|
||||
class MessageSendJob(val message: Message, val destination: Destination) : Job {
|
||||
@ -67,14 +68,25 @@ class MessageSendJob(val message: Message, val destination: Destination) : Job {
|
||||
val promise = MessageSender.send(this.message, this.destination).success {
|
||||
this.handleSuccess()
|
||||
}.fail { exception ->
|
||||
Log.e(TAG, "Couldn't send message due to error: $exception.")
|
||||
if (exception is MessageSender.Error) {
|
||||
if (!exception.isRetryable) { this.handlePermanentFailure(exception) }
|
||||
var logStacktrace = true
|
||||
|
||||
when (exception) {
|
||||
// No need for the stack trace for HTTP errors
|
||||
is HTTP.HTTPRequestFailedException -> {
|
||||
logStacktrace = false
|
||||
|
||||
if (exception.statusCode == 429) { this.handlePermanentFailure(exception) }
|
||||
else { this.handleFailure(exception) }
|
||||
}
|
||||
is MessageSender.Error -> {
|
||||
if (!exception.isRetryable) { this.handlePermanentFailure(exception) }
|
||||
else { this.handleFailure(exception) }
|
||||
}
|
||||
else -> this.handleFailure(exception)
|
||||
}
|
||||
if (exception is OnionRequestAPI.HTTPRequestFailedAtDestinationException && exception.statusCode == 429) {
|
||||
this.handlePermanentFailure(exception)
|
||||
}
|
||||
this.handleFailure(exception)
|
||||
|
||||
if (logStacktrace) { Log.e(TAG, "Couldn't send message due to error", exception) }
|
||||
else { Log.e(TAG, "Couldn't send message due to error: ${exception.message}") }
|
||||
}
|
||||
try {
|
||||
promise.get()
|
||||
|
@ -23,14 +23,27 @@ class OpenGroupDeleteJob(private val messageServerIds: LongArray, private val th
|
||||
val dataProvider = MessagingModuleConfiguration.shared.messageDataProvider
|
||||
val numberToDelete = messageServerIds.size
|
||||
Log.d(TAG, "Deleting $numberToDelete messages")
|
||||
var numberDeleted = 0
|
||||
messageServerIds.forEach { serverId ->
|
||||
val (messageId, isSms) = dataProvider.getMessageID(serverId, threadId) ?: return@forEach
|
||||
dataProvider.deleteMessage(messageId, isSms)
|
||||
numberDeleted++
|
||||
|
||||
// FIXME: This entire process should probably run in a transaction (with the attachment deletion happening only if it succeeded)
|
||||
try {
|
||||
val messageIds = dataProvider.getMessageIDs(messageServerIds.toList(), threadId)
|
||||
|
||||
// Delete the SMS messages
|
||||
if (messageIds.first.isNotEmpty()) {
|
||||
dataProvider.deleteMessages(messageIds.first, threadId, true)
|
||||
}
|
||||
|
||||
// Delete the MMS messages
|
||||
if (messageIds.second.isNotEmpty()) {
|
||||
dataProvider.deleteMessages(messageIds.second, threadId, false)
|
||||
}
|
||||
|
||||
Log.d(TAG, "Deleted ${messageIds.first.size + messageIds.second.size} messages successfully")
|
||||
delegate?.handleJobSucceeded(this)
|
||||
}
|
||||
catch (e: Exception) {
|
||||
delegate?.handleJobFailed(this, e)
|
||||
}
|
||||
Log.d(TAG, "Deleted $numberDeleted messages successfully")
|
||||
delegate?.handleJobSucceeded(this)
|
||||
}
|
||||
|
||||
override fun serialize(): Data = Data.Builder()
|
||||
|
@ -11,15 +11,17 @@ data class OpenGroup(
|
||||
val id: String,
|
||||
val name: String,
|
||||
val publicKey: String,
|
||||
val imageId: String?,
|
||||
val infoUpdates: Int,
|
||||
) {
|
||||
|
||||
constructor(server: String, room: String, name: String, infoUpdates: Int, publicKey: String) : this(
|
||||
constructor(server: String, room: String, publicKey: String, name: String, imageId: String?, infoUpdates: Int) : this(
|
||||
server = server,
|
||||
room = room,
|
||||
id = "$server.$room",
|
||||
name = name,
|
||||
publicKey = publicKey,
|
||||
imageId = imageId,
|
||||
infoUpdates = infoUpdates,
|
||||
)
|
||||
|
||||
@ -31,11 +33,12 @@ data class OpenGroup(
|
||||
if (!json.has("room")) return null
|
||||
val room = json.get("room").asText().toLowerCase(Locale.US)
|
||||
val server = json.get("server").asText().toLowerCase(Locale.US)
|
||||
val displayName = json.get("displayName").asText()
|
||||
val publicKey = json.get("publicKey").asText()
|
||||
val displayName = json.get("displayName").asText()
|
||||
val imageId = json.get("imageId")?.asText()
|
||||
val infoUpdates = json.get("infoUpdates")?.asText()?.toIntOrNull() ?: 0
|
||||
val capabilities = json.get("capabilities")?.asText()?.split(",") ?: emptyList()
|
||||
OpenGroup(server, room, displayName, infoUpdates, publicKey)
|
||||
OpenGroup(server = server, room = room, name = displayName, publicKey = publicKey, imageId = imageId, infoUpdates = infoUpdates)
|
||||
} catch (e: Exception) {
|
||||
Log.w("Loki", "Couldn't parse open group from JSON: $jsonAsString.", e);
|
||||
null
|
||||
@ -53,11 +56,12 @@ data class OpenGroup(
|
||||
}
|
||||
}
|
||||
|
||||
fun toJson(): Map<String,String> = mapOf(
|
||||
fun toJson(): Map<String,String?> = mapOf(
|
||||
"room" to room,
|
||||
"server" to server,
|
||||
"displayName" to name,
|
||||
"publicKey" to publicKey,
|
||||
"displayName" to name,
|
||||
"imageId" to imageId,
|
||||
"infoUpdates" to infoUpdates.toString(),
|
||||
)
|
||||
|
||||
|
@ -91,7 +91,7 @@ object OpenGroupApi {
|
||||
val created: Long = 0,
|
||||
val activeUsers: Int = 0,
|
||||
val activeUsersCutoff: Int = 0,
|
||||
val imageId: Long? = null,
|
||||
val imageId: String? = null,
|
||||
val pinnedMessages: List<PinnedMessage> = emptyList(),
|
||||
val admin: Boolean = false,
|
||||
val globalAdmin: Boolean = false,
|
||||
@ -148,7 +148,7 @@ object OpenGroupApi {
|
||||
)
|
||||
|
||||
enum class Capability {
|
||||
BLIND, REACTIONS
|
||||
SOGS, BLIND, REACTIONS
|
||||
}
|
||||
|
||||
@JsonNaming(PropertyNamingStrategy.SnakeCaseStrategy::class)
|
||||
@ -337,7 +337,7 @@ object OpenGroupApi {
|
||||
.plus(request.verb.rawValue.toByteArray())
|
||||
.plus("/${request.endpoint.value}".toByteArray())
|
||||
.plus(bodyHash)
|
||||
if (serverCapabilities.contains(Capability.BLIND.name.lowercase())) {
|
||||
if (serverCapabilities.isEmpty() || serverCapabilities.contains(Capability.BLIND.name.lowercase())) {
|
||||
SodiumUtilities.blindedKeyPair(publicKey, ed25519KeyPair)?.let { keyPair ->
|
||||
pubKey = SessionId(
|
||||
IdPrefix.BLINDED,
|
||||
@ -383,7 +383,11 @@ object OpenGroupApi {
|
||||
}
|
||||
return if (request.useOnionRouting) {
|
||||
OnionRequestAPI.sendOnionRequest(requestBuilder.build(), request.server, publicKey).fail { e ->
|
||||
Log.e("SOGS", "Failed onion request", e)
|
||||
when (e) {
|
||||
// No need for the stack trace for HTTP errors
|
||||
is HTTP.HTTPRequestFailedException -> Log.e("SOGS", "Failed onion request: ${e.message}")
|
||||
else -> Log.e("SOGS", "Failed onion request", e)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Promise.ofFail(IllegalStateException("It's currently not allowed to send non onion routed requests."))
|
||||
@ -395,13 +399,13 @@ object OpenGroupApi {
|
||||
fun downloadOpenGroupProfilePicture(
|
||||
server: String,
|
||||
roomID: String,
|
||||
imageId: Long
|
||||
imageId: String
|
||||
): Promise<ByteArray, Exception> {
|
||||
val request = Request(
|
||||
verb = GET,
|
||||
room = roomID,
|
||||
server = server,
|
||||
endpoint = Endpoint.RoomFileIndividual(roomID, imageId.toString())
|
||||
endpoint = Endpoint.RoomFileIndividual(roomID, imageId)
|
||||
)
|
||||
return getResponseBody(request)
|
||||
}
|
||||
@ -794,16 +798,14 @@ object OpenGroupApi {
|
||||
|
||||
private fun sequentialBatch(
|
||||
server: String,
|
||||
requests: MutableList<BatchRequestInfo<*>>,
|
||||
authRequired: Boolean = true
|
||||
requests: MutableList<BatchRequestInfo<*>>
|
||||
): Promise<List<BatchResponse<*>>, Exception> {
|
||||
val request = Request(
|
||||
verb = POST,
|
||||
room = null,
|
||||
server = server,
|
||||
endpoint = Endpoint.Sequence,
|
||||
parameters = requests.map { it.request },
|
||||
isAuthRequired = authRequired
|
||||
parameters = requests.map { it.request }
|
||||
)
|
||||
return getBatchResponseJson(request, requests)
|
||||
}
|
||||
@ -912,8 +914,7 @@ object OpenGroupApi {
|
||||
|
||||
fun getCapabilitiesAndRoomInfo(
|
||||
room: String,
|
||||
server: String,
|
||||
authRequired: Boolean = true
|
||||
server: String
|
||||
): Promise<Pair<Capabilities, RoomInfo>, Exception> {
|
||||
val requests = mutableListOf<BatchRequestInfo<*>>(
|
||||
BatchRequestInfo(
|
||||
@ -933,7 +934,7 @@ object OpenGroupApi {
|
||||
responseType = object : TypeReference<RoomInfo>(){}
|
||||
)
|
||||
)
|
||||
return sequentialBatch(server, requests, authRequired).map {
|
||||
return sequentialBatch(server, requests).map {
|
||||
val capabilities = it.firstOrNull()?.body as? Capabilities ?: throw Error.ParsingFailed
|
||||
val roomInfo = it.lastOrNull()?.body as? RoomInfo ?: throw Error.ParsingFailed
|
||||
capabilities to roomInfo
|
||||
|
@ -6,7 +6,15 @@ import org.session.libsession.messaging.MessagingModuleConfiguration
|
||||
import org.session.libsession.messaging.jobs.BackgroundGroupAddJob
|
||||
import org.session.libsession.messaging.jobs.JobQueue
|
||||
import org.session.libsession.messaging.messages.Message
|
||||
import org.session.libsession.messaging.messages.control.*
|
||||
import org.session.libsession.messaging.messages.control.CallMessage
|
||||
import org.session.libsession.messaging.messages.control.ClosedGroupControlMessage
|
||||
import org.session.libsession.messaging.messages.control.ConfigurationMessage
|
||||
import org.session.libsession.messaging.messages.control.DataExtractionNotification
|
||||
import org.session.libsession.messaging.messages.control.ExpirationTimerUpdate
|
||||
import org.session.libsession.messaging.messages.control.MessageRequestResponse
|
||||
import org.session.libsession.messaging.messages.control.ReadReceipt
|
||||
import org.session.libsession.messaging.messages.control.TypingIndicator
|
||||
import org.session.libsession.messaging.messages.control.UnsendRequest
|
||||
import org.session.libsession.messaging.messages.visible.Attachment
|
||||
import org.session.libsession.messaging.messages.visible.Reaction
|
||||
import org.session.libsession.messaging.messages.visible.VisibleMessage
|
||||
@ -21,18 +29,26 @@ import org.session.libsession.messaging.utilities.SessionId
|
||||
import org.session.libsession.messaging.utilities.SodiumUtilities
|
||||
import org.session.libsession.messaging.utilities.WebRtcUtils
|
||||
import org.session.libsession.snode.SnodeAPI
|
||||
import org.session.libsession.utilities.*
|
||||
import org.session.libsession.utilities.Address
|
||||
import org.session.libsession.utilities.GroupRecord
|
||||
import org.session.libsession.utilities.GroupUtil
|
||||
import org.session.libsession.utilities.ProfileKeyUtil
|
||||
import org.session.libsession.utilities.SSKEnvironment
|
||||
import org.session.libsession.utilities.TextSecurePreferences
|
||||
import org.session.libsession.utilities.recipients.Recipient
|
||||
import org.session.libsignal.crypto.ecc.DjbECPrivateKey
|
||||
import org.session.libsignal.crypto.ecc.DjbECPublicKey
|
||||
import org.session.libsignal.crypto.ecc.ECKeyPair
|
||||
import org.session.libsignal.messages.SignalServiceGroup
|
||||
import org.session.libsignal.protos.SignalServiceProtos
|
||||
import org.session.libsignal.utilities.*
|
||||
import org.session.libsignal.utilities.Base64
|
||||
import org.session.libsignal.utilities.IdPrefix
|
||||
import org.session.libsignal.utilities.Log
|
||||
import org.session.libsignal.utilities.guava.Optional
|
||||
import org.session.libsignal.utilities.removingIdPrefixIfNeeded
|
||||
import org.session.libsignal.utilities.toHexString
|
||||
import java.security.MessageDigest
|
||||
import java.util.*
|
||||
import java.util.LinkedList
|
||||
import kotlin.math.min
|
||||
|
||||
internal fun MessageReceiver.isBlocked(publicKey: String): Boolean {
|
||||
@ -407,7 +423,7 @@ private fun MessageReceiver.handleClosedGroupControlMessage(message: ClosedGroup
|
||||
private fun MessageReceiver.handleNewClosedGroup(message: ClosedGroupControlMessage) {
|
||||
val kind = message.kind!! as? ClosedGroupControlMessage.Kind.New ?: return
|
||||
val recipient = Recipient.from(MessagingModuleConfiguration.shared.context, Address.fromSerialized(message.sender!!), false)
|
||||
if (!recipient.isApproved) return
|
||||
if (!recipient.isApproved && !recipient.isLocalNumber) return
|
||||
val groupPublicKey = kind.publicKey.toByteArray().toHexString()
|
||||
val members = kind.members.map { it.toByteArray().toHexString() }
|
||||
val admins = kind.admins.map { it.toByteArray().toHexString() }
|
||||
|
@ -59,7 +59,7 @@ class OpenGroupPoller(private val server: String, private val executorService: S
|
||||
fun poll(isPostCapabilitiesRetry: Boolean = false): Promise<Unit, Exception> {
|
||||
val storage = MessagingModuleConfiguration.shared.storage
|
||||
val rooms = storage.getAllOpenGroups().values.filter { it.server == server }.map { it.room }
|
||||
rooms.forEach { downloadGroupAvatarIfNeeded(it) }
|
||||
|
||||
return OpenGroupApi.poll(rooms, server).successBackground { responses ->
|
||||
responses.filterNot { it.body == null }.forEach { response ->
|
||||
when (response.endpoint) {
|
||||
@ -117,15 +117,17 @@ class OpenGroupPoller(private val server: String, private val executorService: S
|
||||
) {
|
||||
val storage = MessagingModuleConfiguration.shared.storage
|
||||
val groupId = "$server.$roomToken"
|
||||
val dbGroupId = GroupUtil.getEncodedOpenGroupID(groupId.toByteArray())
|
||||
|
||||
val existingOpenGroup = storage.getOpenGroup(roomToken, server)
|
||||
val publicKey = existingOpenGroup?.publicKey ?: return
|
||||
val openGroup = OpenGroup(
|
||||
server = server,
|
||||
room = pollInfo.token,
|
||||
name = pollInfo.details?.name ?: "",
|
||||
infoUpdates = pollInfo.details?.infoUpdates ?: 0,
|
||||
name = if (pollInfo.details != null) { pollInfo.details.name } else { existingOpenGroup.name },
|
||||
infoUpdates = if (pollInfo.details != null) { pollInfo.details.infoUpdates } else { existingOpenGroup.infoUpdates },
|
||||
publicKey = publicKey,
|
||||
imageId = if (pollInfo.details != null) { pollInfo.details.imageId } else { existingOpenGroup.imageId }
|
||||
)
|
||||
// - Open Group changes
|
||||
storage.updateOpenGroup(openGroup)
|
||||
@ -155,6 +157,22 @@ class OpenGroupPoller(private val server: String, private val executorService: S
|
||||
GroupMember(groupId, it, GroupMemberRole.HIDDEN_ADMIN)
|
||||
})
|
||||
}
|
||||
|
||||
if (
|
||||
(
|
||||
pollInfo.details != null &&
|
||||
pollInfo.details.imageId != null && (
|
||||
pollInfo.details.imageId != existingOpenGroup.imageId ||
|
||||
!storage.hasDownloadedProfilePicture(dbGroupId)
|
||||
)
|
||||
) || (
|
||||
pollInfo.details == null &&
|
||||
existingOpenGroup.imageId != null &&
|
||||
!storage.hasDownloadedProfilePicture(dbGroupId)
|
||||
)
|
||||
) {
|
||||
JobQueue.shared.add(GroupAvatarDownloadJob(roomToken, server))
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleMessages(
|
||||
@ -284,16 +302,4 @@ class OpenGroupPoller(private val server: String, private val executorService: S
|
||||
JobQueue.shared.add(deleteJob)
|
||||
}
|
||||
}
|
||||
|
||||
private fun downloadGroupAvatarIfNeeded(room: String) {
|
||||
val storage = MessagingModuleConfiguration.shared.storage
|
||||
if (storage.getGroupAvatarDownloadJob(server, room) != null) return
|
||||
val groupId = GroupUtil.getEncodedOpenGroupID("$server.$room".toByteArray())
|
||||
storage.getGroup(groupId)?.let {
|
||||
if (System.currentTimeMillis() > it.updatedTimestamp + TimeUnit.DAYS.toMillis(7)) {
|
||||
JobQueue.shared.add(GroupAvatarDownloadJob(room, server))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -78,8 +78,8 @@ object OnionRequestAPI {
|
||||
// endregion
|
||||
|
||||
class HTTPRequestFailedBlindingRequiredException(statusCode: Int, json: Map<*, *>, destination: String): HTTPRequestFailedAtDestinationException(statusCode, json, destination)
|
||||
open class HTTPRequestFailedAtDestinationException(val statusCode: Int, val json: Map<*, *>, val destination: String)
|
||||
: Exception("HTTP request failed at destination ($destination) with status code $statusCode.")
|
||||
open class HTTPRequestFailedAtDestinationException(statusCode: Int, json: Map<*, *>, val destination: String)
|
||||
: HTTP.HTTPRequestFailedException(statusCode, json, "HTTP request failed at destination ($destination) with status code $statusCode.")
|
||||
class InsufficientSnodesException : Exception("Couldn't find enough snodes to build a path.")
|
||||
|
||||
private data class OnionBuildingResult(
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user