diff --git a/app/build.gradle b/app/build.gradle index b36544fb9d..e10d3108a0 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -143,8 +143,8 @@ dependencies { testImplementation 'org.robolectric:shadows-multidex:4.2' } -def canonicalVersionCode = 182 -def canonicalVersionName = "1.10.13" +def canonicalVersionCode = 200 +def canonicalVersionName = "1.11.4" def postFixSize = 10 def abiPostFix = ['armeabi-v7a' : 1, @@ -194,8 +194,8 @@ android { versionCode canonicalVersionCode * postFixSize versionName canonicalVersionName - minSdkVersion 21 - targetSdkVersion 30 + minSdkVersion androidMinimumSdkVersion + targetSdkVersion androidCompileSdkVersion multiDexEnabled = true diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 34321f8552..6eae3a6927 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -1,5 +1,6 @@ - @@ -7,7 +8,7 @@ - - - - - - - + + + + + + + - + + @@ -90,88 +92,83 @@ android:name="firebase_messaging_auto_init_enabled" android:value="false" /> - + android:theme="@style/Theme.Session.DayNight.FlatActionBar" /> + android:theme="@style/Theme.Session.DayNight.FlatActionBar" /> + android:theme="@style/Theme.Session.DayNight.FlatActionBar" /> + android:theme="@style/Theme.Session.DayNight.FlatActionBar" /> - + android:label="@string/activity_settings_title" /> + android:theme="@style/Theme.Session.DayNight.FlatActionBar" /> - - - + + + - - + + - - @@ -195,7 +190,6 @@ - @@ -204,14 +198,12 @@ + android:targetActivity="org.thoughtcrime.securesms.home.HomeActivity"> - - @@ -221,40 +213,22 @@ + android:parentActivityName="org.thoughtcrime.securesms.home.HomeActivity" + android:theme="@style/Theme.Session.DayNight.FlatActionBar"> + android:value="org.thoughtcrime.securesms.home.HomeActivity" /> + android:theme="@style/Theme.TextSecure.DayNight" /> - - + android:theme="@style/Theme.TextSecure.DayNight" /> + android:theme="@style/Theme.Session.DayNight.NoActionBar" /> + android:theme="@style/Theme.Session.ForceDark" /> @@ -436,15 +410,13 @@ - - - - diff --git a/app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java b/app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java index 0eacf377a5..6ea6795f12 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java +++ b/app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java @@ -27,10 +27,10 @@ import androidx.lifecycle.DefaultLifecycleObserver; import androidx.lifecycle.LifecycleOwner; import androidx.lifecycle.ProcessLifecycleOwner; import androidx.multidex.MultiDexApplication; + import org.conscrypt.Conscrypt; import org.session.libsession.avatars.AvatarHelper; import org.session.libsession.messaging.MessagingModuleConfiguration; -import org.session.libsession.messaging.contacts.Contact; import org.session.libsession.messaging.sending_receiving.notifications.MessageNotifier; import org.session.libsession.messaging.sending_receiving.pollers.ClosedGroupPollerV2; import org.session.libsession.messaging.sending_receiving.pollers.Poller; @@ -42,11 +42,11 @@ import org.session.libsession.utilities.TextSecurePreferences; import org.session.libsession.utilities.Util; import org.session.libsession.utilities.dynamiclanguage.DynamicLanguageContextWrapper; import org.session.libsession.utilities.dynamiclanguage.LocaleParser; -import org.session.libsession.utilities.recipients.Recipient; import org.session.libsignal.utilities.Log; import org.session.libsignal.utilities.ThreadUtils; import org.signal.aesgcmprovider.AesGcmProvider; import org.thoughtcrime.securesms.components.TypingStatusSender; +import org.thoughtcrime.securesms.crypto.KeyPairUtilities; import org.thoughtcrime.securesms.database.DatabaseFactory; import org.thoughtcrime.securesms.dependencies.InjectableType; import org.thoughtcrime.securesms.dependencies.SignalCommunicationModule; @@ -58,17 +58,14 @@ import org.thoughtcrime.securesms.jobs.JobManagerFactories; import org.thoughtcrime.securesms.logging.AndroidLogger; import org.thoughtcrime.securesms.logging.PersistentLogger; import org.thoughtcrime.securesms.logging.UncaughtExceptionLogger; -import org.thoughtcrime.securesms.loki.activities.HomeActivity; -import org.thoughtcrime.securesms.loki.api.BackgroundPollWorker; -import org.thoughtcrime.securesms.loki.api.LokiPushNotificationManager; -import org.thoughtcrime.securesms.loki.api.OpenGroupManager; -import org.thoughtcrime.securesms.loki.database.LokiAPIDatabase; -import org.thoughtcrime.securesms.loki.database.LokiUserDatabase; -import org.thoughtcrime.securesms.loki.database.SessionContactDatabase; -import org.thoughtcrime.securesms.loki.utilities.Broadcaster; -import org.thoughtcrime.securesms.loki.utilities.ContactUtilities; -import org.thoughtcrime.securesms.loki.utilities.FcmUtils; -import org.thoughtcrime.securesms.loki.utilities.UiModeUtilities; +import org.thoughtcrime.securesms.home.HomeActivity; +import org.thoughtcrime.securesms.notifications.BackgroundPollWorker; +import org.thoughtcrime.securesms.notifications.LokiPushNotificationManager; +import org.thoughtcrime.securesms.groups.OpenGroupManager; +import org.thoughtcrime.securesms.database.LokiAPIDatabase; +import org.thoughtcrime.securesms.util.Broadcaster; +import org.thoughtcrime.securesms.notifications.FcmUtils; +import org.thoughtcrime.securesms.util.UiModeUtilities; import org.thoughtcrime.securesms.notifications.DefaultMessageNotifier; import org.thoughtcrime.securesms.notifications.NotificationChannels; import org.thoughtcrime.securesms.notifications.OptimizedMessageNotifier; @@ -84,12 +81,14 @@ import org.webrtc.PeerConnectionFactory; import org.webrtc.PeerConnectionFactory.InitializationOptions; import org.webrtc.voiceengine.WebRtcAudioManager; import org.webrtc.voiceengine.WebRtcAudioUtils; + import java.io.ByteArrayOutputStream; import java.io.InputStream; import java.security.Security; import java.util.Date; import java.util.HashSet; import java.util.Set; + import dagger.ObjectGraph; import kotlin.Unit; import kotlinx.coroutines.Job; @@ -154,8 +153,10 @@ public class ApplicationContext extends MultiDexApplication implements Dependenc conversationListNotificationHandler = new Handler(Looper.getMainLooper()); LokiAPIDatabase apiDB = DatabaseFactory.getLokiAPIDatabase(this); MessagingModuleConfiguration.Companion.configure(this, - DatabaseFactory.getStorage(this), - DatabaseFactory.getAttachmentProvider(this)); + DatabaseFactory.getStorage(this), + DatabaseFactory.getAttachmentProvider(this), + ()-> KeyPairUtilities.INSTANCE.getUserED25519KeyPair(this) + ); SnodeModule.Companion.configure(apiDB, broadcaster); String userPublicKey = TextSecurePreferences.getLocalNumber(this); if (userPublicKey != null) { @@ -181,27 +182,6 @@ public class ApplicationContext extends MultiDexApplication implements Dependenc Log.i(TAG, "App is now visible."); KeyCachingService.onAppForegrounded(this); - boolean hasPerformedContactMigration = TextSecurePreferences.INSTANCE.hasPerformedContactMigration(this); - if (!hasPerformedContactMigration) { - TextSecurePreferences.INSTANCE.setPerformedContactMigration(this); - Set allContacts = ContactUtilities.getAllContacts(this); - SessionContactDatabase contactDB = DatabaseFactory.getSessionContactDatabase(this); - LokiUserDatabase userDB = DatabaseFactory.getLokiUserDatabase(this); - for (Recipient recipient : allContacts) { - if (recipient.isGroupRecipient()) { continue; } - String sessionID = recipient.getAddress().serialize(); - Contact contact = contactDB.getContactWithSessionID(sessionID); - if (contact == null) { - contact = new Contact(sessionID); - String name = userDB.getDisplayName(sessionID); - contact.setName(name); - contact.setProfilePictureURL(recipient.getProfileAvatar()); - contact.setProfilePictureEncryptionKey(recipient.getProfileKey()); - contact.setTrusted(true); - } - contactDB.setContact(contact); - } - } if (poller != null) { poller.setCaughtUp(false); } @@ -487,7 +467,6 @@ public class ApplicationContext extends MultiDexApplication implements Dependenc boolean isUsingFCM = TextSecurePreferences.isUsingFCM(this); TextSecurePreferences.clearAll(this); if (isMigratingToV2KeyPair) { - TextSecurePreferences.setIsMigratingKeyPair(this, true); TextSecurePreferences.setIsUsingFCM(this, isUsingFCM); TextSecurePreferences.setProfileName(this, displayName); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/MediaGalleryAdapter.java b/app/src/main/java/org/thoughtcrime/securesms/MediaGalleryAdapter.java index 534127240f..aad4c17008 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/MediaGalleryAdapter.java +++ b/app/src/main/java/org/thoughtcrime/securesms/MediaGalleryAdapter.java @@ -26,7 +26,7 @@ import android.widget.TextView; import com.codewaves.stickyheadergrid.StickyHeaderGridAdapter; -import org.thoughtcrime.securesms.components.ThumbnailView; +import org.thoughtcrime.securesms.conversation.v2.utilities.ThumbnailView; import org.thoughtcrime.securesms.database.MediaDatabase.MediaRecord; import org.thoughtcrime.securesms.database.loaders.BucketedThreadMediaLoader.BucketedThreadMedia; import org.thoughtcrime.securesms.mms.GlideRequests; diff --git a/app/src/main/java/org/thoughtcrime/securesms/MediaPreviewActivity.java b/app/src/main/java/org/thoughtcrime/securesms/MediaPreviewActivity.java index 3f8f03fa51..e28b419880 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/MediaPreviewActivity.java +++ b/app/src/main/java/org/thoughtcrime/securesms/MediaPreviewActivity.java @@ -64,10 +64,12 @@ import org.session.libsignal.utilities.Log; import org.thoughtcrime.securesms.components.MediaView; import org.thoughtcrime.securesms.database.MediaDatabase.MediaRecord; import org.thoughtcrime.securesms.database.loaders.PagingMediaLoader; +import org.thoughtcrime.securesms.database.model.MmsMessageRecord; import org.thoughtcrime.securesms.mediapreview.MediaPreviewViewModel; import org.thoughtcrime.securesms.mediapreview.MediaRailAdapter; import org.thoughtcrime.securesms.mms.GlideApp; import org.thoughtcrime.securesms.mms.GlideRequests; +import org.thoughtcrime.securesms.mms.Slide; import org.thoughtcrime.securesms.permissions.Permissions; import org.thoughtcrime.securesms.util.AttachmentUtil; import org.thoughtcrime.securesms.util.DateUtils; @@ -116,6 +118,22 @@ public class MediaPreviewActivity extends PassphraseRequiredActionBarActivity im private int restartItem = -1; + public static Intent getPreviewIntent(Context context, Slide slide, MmsMessageRecord mms, Recipient threadRecipient) { + Intent previewIntent = null; + if (MediaPreviewActivity.isContentTypeSupported(slide.getContentType()) && slide.getUri() != null) { + previewIntent = new Intent(context, MediaPreviewActivity.class); + previewIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) + .setDataAndType(slide.getUri(), slide.getContentType()) + .putExtra(ADDRESS_EXTRA, threadRecipient.getAddress()) + .putExtra(OUTGOING_EXTRA, mms.isOutgoing()) + .putExtra(DATE_EXTRA, mms.getTimestamp()) + .putExtra(SIZE_EXTRA, slide.asAttachment().getSize()) + .putExtra(CAPTION_EXTRA, slide.getCaption().orNull()) + .putExtra(LEFT_IS_RECENT_EXTRA, false); + } + return previewIntent; + } + @SuppressWarnings("ConstantConditions") @Override @@ -171,7 +189,7 @@ public class MediaPreviewActivity extends PassphraseRequiredActionBarActivity im CharSequence relativeTimeSpan; if (mediaItem.date > 0) { - relativeTimeSpan = DateUtils.getExtendedRelativeTimeSpanString(this, Locale.getDefault(), mediaItem.date); + relativeTimeSpan = DateUtils.getDisplayFormattedTimeSpanString(this, Locale.getDefault(), mediaItem.date); } else { relativeTimeSpan = getString(R.string.MediaPreviewActivity_draft); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/MessageDetailsActivity.java b/app/src/main/java/org/thoughtcrime/securesms/MessageDetailsActivity.java deleted file mode 100644 index 314653743c..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/MessageDetailsActivity.java +++ /dev/null @@ -1,482 +0,0 @@ -/* - * Copyright (C) 2015 Open Whisper Systems - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ -package org.thoughtcrime.securesms; - -import android.annotation.SuppressLint; -import android.content.ClipData; -import android.content.ClipboardManager; -import android.content.Context; -import android.database.Cursor; -import android.graphics.drawable.ColorDrawable; -import android.os.AsyncTask; -import android.os.Bundle; -import android.view.LayoutInflater; -import android.view.MenuItem; -import android.view.View; -import android.view.ViewGroup; -import android.widget.ListView; -import android.widget.TextView; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.loader.app.LoaderManager.LoaderCallbacks; -import androidx.loader.content.Loader; -import org.session.libsession.messaging.messages.visible.LinkPreview; -import org.session.libsession.messaging.messages.visible.OpenGroupInvitation; -import org.session.libsession.messaging.messages.visible.Quote; -import org.session.libsession.messaging.messages.visible.VisibleMessage; -import org.session.libsession.messaging.open_groups.OpenGroupV2; -import org.session.libsession.messaging.sending_receiving.MessageSender; -import org.session.libsession.messaging.utilities.UpdateMessageData; -import org.thoughtcrime.securesms.MessageDetailsRecipientAdapter.RecipientDeliveryStatus; -import org.session.libsession.utilities.MaterialColor; -import org.thoughtcrime.securesms.conversation.ConversationItem; -import org.thoughtcrime.securesms.database.DatabaseFactory; -import org.thoughtcrime.securesms.database.GroupReceiptDatabase; -import org.thoughtcrime.securesms.database.GroupReceiptDatabase.GroupReceiptInfo; -import org.thoughtcrime.securesms.database.MmsDatabase; -import org.thoughtcrime.securesms.database.MmsSmsDatabase; -import org.thoughtcrime.securesms.database.SmsDatabase; -import org.thoughtcrime.securesms.database.loaders.MessageDetailsLoader; -import org.thoughtcrime.securesms.database.model.MessageRecord; -import org.session.libsignal.utilities.Log; -import org.thoughtcrime.securesms.database.model.MmsMessageRecord; -import org.thoughtcrime.securesms.loki.database.LokiMessageDatabase; -import org.thoughtcrime.securesms.mms.GlideApp; -import org.thoughtcrime.securesms.mms.GlideRequests; -import org.session.libsession.utilities.recipients.Recipient; -import org.session.libsession.utilities.recipients.RecipientModifiedListener; -import org.thoughtcrime.securesms.util.DateUtils; -import org.session.libsession.utilities.ExpirationUtil; -import org.session.libsession.utilities.Util; -import org.session.libsignal.utilities.guava.Optional; - -import java.lang.ref.WeakReference; -import java.sql.Date; -import java.text.SimpleDateFormat; -import java.util.HashSet; -import java.util.LinkedList; -import java.util.List; -import java.util.Locale; - -import network.loki.messenger.R; - -/** - * @author Jake McGinty - */ -public class MessageDetailsActivity extends PassphraseRequiredActionBarActivity implements LoaderCallbacks, RecipientModifiedListener { - private final static String TAG = MessageDetailsActivity.class.getSimpleName(); - - public final static String MESSAGE_ID_EXTRA = "message_id"; - public final static String THREAD_ID_EXTRA = "thread_id"; - public final static String IS_PUSH_GROUP_EXTRA = "is_push_group"; - public final static String TYPE_EXTRA = "type"; - public final static String ADDRESS_EXTRA = "address"; - - private GlideRequests glideRequests; - private long threadId; - private boolean isPushGroup; - private ConversationItem conversationItem; - private ViewGroup itemParent; - private View metadataContainer; - private View expiresContainer; - private TextView errorText; - private View resendButton; - private TextView sentDate; - private TextView receivedDate; - private TextView expiresInText; - private View receivedContainer; - private TextView transport; - private TextView toFrom; - private View separator; - private ListView recipientsList; - private LayoutInflater inflater; - - private boolean running; - - @Override - public void onCreate(Bundle bundle, boolean ready) { - super.onCreate(bundle, ready); - setContentView(R.layout.message_details_activity); - running = true; - - initializeResources(); - initializeActionBar(); - getSupportLoaderManager().initLoader(0, null, this); - } - - @Override - protected void onResume() { - super.onResume(); - - assert getSupportActionBar() != null; - getSupportActionBar().setTitle("Message Details"); - - ApplicationContext.getInstance(this).messageNotifier.setVisibleThread(threadId); - } - - @Override - protected void onPause() { - super.onPause(); - ApplicationContext.getInstance(this).messageNotifier.setVisibleThread(-1L); - } - - @Override - protected void onDestroy() { - super.onDestroy(); - running = false; - } - - private void initializeActionBar() { - assert getSupportActionBar() != null; - - Recipient recipient = Recipient.from(this, getIntent().getParcelableExtra(ADDRESS_EXTRA), true); - recipient.addListener(this); - } - - private void setActionBarColor(MaterialColor color) { - assert getSupportActionBar() != null; - getSupportActionBar().setBackgroundDrawable(new ColorDrawable(color.toActionBarColor(this))); - } - - @Override - public void onModified(final Recipient recipient) { - Util.runOnMain(() -> setActionBarColor(recipient.getColor())); - } - - private void initializeResources() { - inflater = LayoutInflater.from(this); - View header = inflater.inflate(R.layout.message_details_header, recipientsList, false); - - threadId = getIntent().getLongExtra(THREAD_ID_EXTRA, -1); - isPushGroup = getIntent().getBooleanExtra(IS_PUSH_GROUP_EXTRA, false); - glideRequests = GlideApp.with(this); - itemParent = header.findViewById(R.id.item_container); - recipientsList = findViewById(R.id.recipients_list); - metadataContainer = header.findViewById(R.id.metadata_container); - errorText = header.findViewById(R.id.error_text); - resendButton = header.findViewById(R.id.resend_button); - sentDate = header.findViewById(R.id.sent_time); - receivedContainer = header.findViewById(R.id.received_container); - receivedDate = header.findViewById(R.id.received_time); - transport = header.findViewById(R.id.transport); - toFrom = header.findViewById(R.id.tofrom); - separator = header.findViewById(R.id.separator); - expiresContainer = header.findViewById(R.id.expires_container); - expiresInText = header.findViewById(R.id.expires_in); - recipientsList.setHeaderDividersEnabled(false); - recipientsList.addHeaderView(header, null, false); - } - - private void updateTransport(MessageRecord messageRecord) { - final String transportText; - if (messageRecord.isOutgoing() && messageRecord.isFailed()) { - transportText = "-"; - } else if (messageRecord.isPending()) { - transportText = getString(R.string.ConversationFragment_pending); - } else if (messageRecord.isPush()) { - transportText = getString(R.string.ConversationFragment_push); - } else if (messageRecord.isMms()) { - transportText = getString(R.string.ConversationFragment_mms); - } else { - transportText = getString(R.string.ConversationFragment_sms); - } - - transport.setText(transportText); - } - - private void updateTime(MessageRecord messageRecord) { - sentDate.setOnLongClickListener(null); - receivedDate.setOnLongClickListener(null); - - if (messageRecord.isPending() || messageRecord.isFailed()) { - sentDate.setText("-"); - receivedContainer.setVisibility(View.GONE); - } else { - Locale dateLocale = Locale.getDefault(); - SimpleDateFormat dateFormatter = DateUtils.getDetailedDateFormatter(this, dateLocale); - sentDate.setText(dateFormatter.format(new Date(messageRecord.getDateSent()))); - sentDate.setOnLongClickListener(v -> { - copyToClipboard(String.valueOf(messageRecord.getDateSent())); - return true; - }); - - if (messageRecord.getDateReceived() != messageRecord.getDateSent() && !messageRecord.isOutgoing()) { - receivedDate.setText(dateFormatter.format(new Date(messageRecord.getDateReceived()))); - receivedDate.setOnLongClickListener(v -> { - copyToClipboard(String.valueOf(messageRecord.getDateReceived())); - return true; - }); - receivedContainer.setVisibility(View.VISIBLE); - } else { - receivedContainer.setVisibility(View.GONE); - } - } - } - - private void updateExpirationTime(final MessageRecord messageRecord) { - if (messageRecord.getExpiresIn() <= 0 || messageRecord.getExpireStarted() <= 0) { - expiresContainer.setVisibility(View.GONE); - return; - } - - expiresContainer.setVisibility(View.VISIBLE); - Util.runOnMain(new Runnable() { - @Override - public void run() { - long elapsed = System.currentTimeMillis() - messageRecord.getExpireStarted(); - long remaining = messageRecord.getExpiresIn() - elapsed; - - String duration = ExpirationUtil.getExpirationDisplayValue(MessageDetailsActivity.this, Math.max((int)(remaining / 1000), 1)); - expiresInText.setText(duration); - - if (running) { - Util.runOnMainDelayed(this, 500); - } - } - }); - } - - private void updateRecipients(MessageRecord messageRecord, Recipient recipient, List recipients) { - final int toFromRes; - if (messageRecord.isMms() && !messageRecord.isPush() && !messageRecord.isOutgoing()) { - toFromRes = R.string.message_details_header__with; - } else if (messageRecord.isOutgoing()) { - toFromRes = R.string.message_details_header__to; - } else { - toFromRes = R.string.message_details_header__from; - } - toFrom.setText(toFromRes); - long threadID = messageRecord.getThreadId(); - OpenGroupV2 openGroup = DatabaseFactory.getLokiThreadDatabase(this).getOpenGroupChat(threadID); - if (openGroup != null && messageRecord.isOutgoing()) { - toFrom.setVisibility(View.GONE); - separator.setVisibility(View.GONE); - } - conversationItem.bind(messageRecord, Optional.absent(), Optional.absent(), glideRequests, Locale.getDefault(), new HashSet<>(), recipient, null, false); - recipientsList.setAdapter(new MessageDetailsRecipientAdapter(this, glideRequests, messageRecord, recipients, isPushGroup)); - } - - private void inflateMessageViewIfAbsent(MessageRecord messageRecord) { - if (conversationItem == null) { - if (messageRecord.isGroupAction()) { - conversationItem = (ConversationItem) inflater.inflate(R.layout.conversation_item_update, itemParent, false); - } else if (messageRecord.isOutgoing()) { - conversationItem = (ConversationItem) inflater.inflate(R.layout.conversation_item_sent, itemParent, false); - } else { - conversationItem = (ConversationItem) inflater.inflate(R.layout.conversation_item_received, itemParent, false); - } - itemParent.addView(conversationItem); - } - } - - private @Nullable MessageRecord getMessageRecord(Context context, Cursor cursor, String type) { - switch (type) { - case MmsSmsDatabase.SMS_TRANSPORT: - SmsDatabase smsDatabase = DatabaseFactory.getSmsDatabase(context); - SmsDatabase.Reader reader = smsDatabase.readerFor(cursor); - return reader.getNext(); - case MmsSmsDatabase.MMS_TRANSPORT: - MmsDatabase mmsDatabase = DatabaseFactory.getMmsDatabase(context); - MmsDatabase.Reader mmsReader = mmsDatabase.readerFor(cursor); - return mmsReader.getNext(); - default: - throw new AssertionError("no valid message type specified"); - } - } - - private void copyToClipboard(@NonNull String text) { - ((ClipboardManager) getSystemService(Context.CLIPBOARD_SERVICE)).setPrimaryClip(ClipData.newPlainText("text", text)); - } - - @Override - public @NonNull Loader onCreateLoader(int id, Bundle args) { - return new MessageDetailsLoader(this, getIntent().getStringExtra(TYPE_EXTRA), - getIntent().getLongExtra(MESSAGE_ID_EXTRA, -1)); - } - - @Override - public void onLoadFinished(@NonNull Loader loader, Cursor cursor) { - MessageRecord messageRecord = getMessageRecord(this, cursor, getIntent().getStringExtra(TYPE_EXTRA)); - - if (messageRecord == null) { - finish(); - } else { - new MessageRecipientAsyncTask(this, messageRecord).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); - } - } - - @Override - public void onLoaderReset(@NonNull Loader loader) { - recipientsList.setAdapter(null); - } - - @Override - public boolean onOptionsItemSelected(MenuItem item) { - super.onOptionsItemSelected(item); - - switch (item.getItemId()) { - case android.R.id.home: finish(); return true; - } - - return false; - } - - @SuppressLint("StaticFieldLeak") - private class MessageRecipientAsyncTask extends AsyncTask> { - - private final WeakReference weakContext; - private final MessageRecord messageRecord; - - MessageRecipientAsyncTask(@NonNull Context context, @NonNull MessageRecord messageRecord) { - this.weakContext = new WeakReference<>(context); - this.messageRecord = messageRecord; - } - - protected Context getContext() { - return weakContext.get(); - } - - @Override - public List doInBackground(Void... voids) { - Context context = getContext(); - - if (context == null) { - Log.w(TAG, "associated context is destroyed, finishing early"); - return null; - } - - List recipients = new LinkedList<>(); - - if (!messageRecord.getRecipient().isGroupRecipient()) { - recipients.add(new RecipientDeliveryStatus(messageRecord.getRecipient(), getStatusFor(messageRecord.getDeliveryReceiptCount(), messageRecord.getReadReceiptCount(), messageRecord.isPending()), messageRecord.isUnidentified(), -1)); - } else { - List receiptInfoList = DatabaseFactory.getGroupReceiptDatabase(context).getGroupReceiptInfo(messageRecord.getId()); - - if (receiptInfoList.isEmpty()) { - List group = DatabaseFactory.getGroupDatabase(context).getGroupMembers(messageRecord.getRecipient().getAddress().toGroupString(), false); - - for (Recipient recipient : group) { - recipients.add(new RecipientDeliveryStatus(recipient, RecipientDeliveryStatus.Status.UNKNOWN, false, -1)); - } - } else { - for (GroupReceiptInfo info : receiptInfoList) { - recipients.add(new RecipientDeliveryStatus(Recipient.from(context, info.getAddress(), true), - getStatusFor(info.getStatus(), messageRecord.isPending(), messageRecord.isFailed()), - info.isUnidentified(), - info.getTimestamp())); - } - } - } - - return recipients; - } - - @Override - public void onPostExecute(List recipients) { - if (getContext() == null) { - Log.w(TAG, "AsyncTask finished with a destroyed context, leaving early."); - return; - } - - inflateMessageViewIfAbsent(messageRecord); - updateRecipients(messageRecord, messageRecord.getRecipient(), recipients); - - boolean isGroupNetworkFailure = messageRecord.isFailed() && !messageRecord.getNetworkFailures().isEmpty(); - boolean isIndividualNetworkFailure = messageRecord.isFailed() && !isPushGroup && messageRecord.getIdentityKeyMismatches().isEmpty(); - - LokiMessageDatabase lokiMessageDatabase = DatabaseFactory.getLokiMessageDatabase(getContext()); - String errorMessage = lokiMessageDatabase.getErrorMessage(messageRecord.id); - if (errorMessage != null) { - errorText.setText(errorMessage); - } - - if (isGroupNetworkFailure || isIndividualNetworkFailure) { - errorText.setVisibility(View.VISIBLE); - resendButton.setVisibility(View.VISIBLE); - resendButton.setOnClickListener(this::onResendClicked); - metadataContainer.setVisibility(View.GONE); - } else if (messageRecord.isFailed()) { - errorText.setVisibility(View.VISIBLE); - resendButton.setVisibility(View.GONE); - resendButton.setOnClickListener(null); - metadataContainer.setVisibility(View.GONE); - } else { - updateTransport(messageRecord); - updateTime(messageRecord); - updateExpirationTime(messageRecord); - errorText.setVisibility(View.GONE); - resendButton.setVisibility(View.GONE); - resendButton.setOnClickListener(null); - metadataContainer.setVisibility(View.VISIBLE); - } - } - - private RecipientDeliveryStatus.Status getStatusFor(int deliveryReceiptCount, int readReceiptCount, boolean pending) { - if (readReceiptCount > 0) return RecipientDeliveryStatus.Status.READ; - else if (deliveryReceiptCount > 0) return RecipientDeliveryStatus.Status.DELIVERED; - else if (!pending) return RecipientDeliveryStatus.Status.SENT; - else return RecipientDeliveryStatus.Status.PENDING; - } - - private RecipientDeliveryStatus.Status getStatusFor(int groupStatus, boolean pending, boolean failed) { - if (groupStatus == GroupReceiptDatabase.STATUS_READ) return RecipientDeliveryStatus.Status.READ; - else if (groupStatus == GroupReceiptDatabase.STATUS_DELIVERED) return RecipientDeliveryStatus.Status.DELIVERED; - else if (groupStatus == GroupReceiptDatabase.STATUS_UNDELIVERED && failed) return RecipientDeliveryStatus.Status.UNKNOWN; - else if (groupStatus == GroupReceiptDatabase.STATUS_UNDELIVERED && !pending) return RecipientDeliveryStatus.Status.SENT; - else if (groupStatus == GroupReceiptDatabase.STATUS_UNDELIVERED) return RecipientDeliveryStatus.Status.PENDING; - else if (groupStatus == GroupReceiptDatabase.STATUS_UNKNOWN) return RecipientDeliveryStatus.Status.UNKNOWN; - throw new AssertionError(); - } - - private void onResendClicked(View v) { - Recipient recipient = messageRecord.getRecipient(); - VisibleMessage message = new VisibleMessage(); - message.setId(messageRecord.getId()); - if (messageRecord.isOpenGroupInvitation()) { - OpenGroupInvitation openGroupInvitation = new OpenGroupInvitation(); - UpdateMessageData updateMessageData = UpdateMessageData.Companion.fromJSON(messageRecord.getBody()); - if (updateMessageData.getKind() instanceof UpdateMessageData.Kind.OpenGroupInvitation) { - UpdateMessageData.Kind.OpenGroupInvitation data = (UpdateMessageData.Kind.OpenGroupInvitation)updateMessageData.getKind(); - openGroupInvitation.setName(data.getGroupName()); - openGroupInvitation.setUrl(data.getGroupUrl()); - } - message.setOpenGroupInvitation(openGroupInvitation); - } else { - message.setText(messageRecord.getBody()); - } - message.setSentTimestamp(messageRecord.getTimestamp()); - if (recipient.isGroupRecipient()) { - message.setGroupPublicKey(recipient.getAddress().toGroupString()); - } else { - message.setRecipient(messageRecord.getRecipient().getAddress().serialize()); - } - message.setThreadID(messageRecord.getThreadId()); - if (messageRecord.isMms()) { - MmsMessageRecord mmsMessageRecord = (MmsMessageRecord) messageRecord; - if (!mmsMessageRecord.getLinkPreviews().isEmpty()) { - message.setLinkPreview(LinkPreview.Companion.from(mmsMessageRecord.getLinkPreviews().get(0))); - } - if (mmsMessageRecord.getQuote() != null) { - message.setQuote(Quote.Companion.from(mmsMessageRecord.getQuote().getQuoteModel())); - } - message.addSignalAttachments(mmsMessageRecord.getSlideDeck().asAttachments()); - } - MessageSender.send(message, recipient.getAddress()); - resendButton.setVisibility(View.GONE); - } - } -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/MessageDetailsRecipientAdapter.java b/app/src/main/java/org/thoughtcrime/securesms/MessageDetailsRecipientAdapter.java index e150c1656b..ca6cf8f6c8 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/MessageDetailsRecipientAdapter.java +++ b/app/src/main/java/org/thoughtcrime/securesms/MessageDetailsRecipientAdapter.java @@ -10,7 +10,7 @@ import androidx.annotation.NonNull; import org.thoughtcrime.securesms.database.model.MessageRecord; -import org.thoughtcrime.securesms.loki.views.UserView; +import org.thoughtcrime.securesms.contacts.UserView; import org.thoughtcrime.securesms.mms.GlideRequests; import org.session.libsession.utilities.recipients.Recipient; import org.session.libsession.utilities.Conversions; diff --git a/app/src/main/java/org/thoughtcrime/securesms/PassphrasePromptActivity.java b/app/src/main/java/org/thoughtcrime/securesms/PassphrasePromptActivity.java index df61907714..7cbb0533f0 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/PassphrasePromptActivity.java +++ b/app/src/main/java/org/thoughtcrime/securesms/PassphrasePromptActivity.java @@ -39,7 +39,7 @@ import android.widget.ImageView; import androidx.core.hardware.fingerprint.FingerprintManagerCompat; import androidx.core.os.CancellationSignal; -import org.thoughtcrime.securesms.animation.AnimationCompleteListener; +import org.thoughtcrime.securesms.util.AnimationCompleteListener; import org.thoughtcrime.securesms.components.AnimatingToggle; import org.session.libsignal.utilities.Log; import org.thoughtcrime.securesms.service.KeyCachingService; diff --git a/app/src/main/java/org/thoughtcrime/securesms/PassphraseRequiredActionBarActivity.java b/app/src/main/java/org/thoughtcrime/securesms/PassphraseRequiredActionBarActivity.java index d4e836ae4d..a791d77a57 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/PassphraseRequiredActionBarActivity.java +++ b/app/src/main/java/org/thoughtcrime/securesms/PassphraseRequiredActionBarActivity.java @@ -12,8 +12,8 @@ import androidx.annotation.Nullable; import androidx.fragment.app.Fragment; import org.session.libsignal.utilities.Log; -import org.thoughtcrime.securesms.loki.activities.HomeActivity; -import org.thoughtcrime.securesms.loki.activities.LandingActivity; +import org.thoughtcrime.securesms.home.HomeActivity; +import org.thoughtcrime.securesms.onboarding.LandingActivity; import org.thoughtcrime.securesms.service.KeyCachingService; import org.session.libsession.utilities.TextSecurePreferences; diff --git a/app/src/main/java/org/thoughtcrime/securesms/ShareActivity.java b/app/src/main/java/org/thoughtcrime/securesms/ShareActivity.java index a4a17b7389..d92ff18d0a 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/ShareActivity.java +++ b/app/src/main/java/org/thoughtcrime/securesms/ShareActivity.java @@ -37,13 +37,13 @@ import androidx.appcompat.widget.Toolbar; import org.session.libsession.utilities.DistributionTypes; import org.thoughtcrime.securesms.components.SearchToolbar; -import org.thoughtcrime.securesms.conversation.ConversationActivity; + import org.session.libsession.utilities.Address; +import org.thoughtcrime.securesms.conversation.v2.ConversationActivityV2; import org.thoughtcrime.securesms.database.DatabaseFactory; import org.session.libsignal.utilities.Log; -import org.thoughtcrime.securesms.loki.fragments.ContactSelectionListFragment; -import org.thoughtcrime.securesms.loki.fragments.ContactSelectionListLoader.DisplayMode; -import org.thoughtcrime.securesms.mediasend.Media; +import org.thoughtcrime.securesms.contacts.ContactSelectionListFragment; +import org.thoughtcrime.securesms.contacts.ContactSelectionListLoader.DisplayMode; import org.thoughtcrime.securesms.mms.PartAuthority; import org.thoughtcrime.securesms.providers.BlobProvider; import org.session.libsession.utilities.recipients.Recipient; @@ -53,7 +53,6 @@ import org.session.libsession.utilities.ViewUtil; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; -import java.util.ArrayList; import network.loki.messenger.R; @@ -215,10 +214,9 @@ public class ShareActivity extends PassphraseRequiredActionBarActivity } private void createConversation(long threadId, Address address, int distributionType) { - final Intent intent = getBaseShareIntent(ConversationActivity.class); - intent.putExtra(ConversationActivity.ADDRESS_EXTRA, address); - intent.putExtra(ConversationActivity.THREAD_ID_EXTRA, threadId); - intent.putExtra(ConversationActivity.DISTRIBUTION_TYPE_EXTRA, distributionType); + final Intent intent = getBaseShareIntent(ConversationActivityV2.class); + intent.putExtra(ConversationActivityV2.ADDRESS, address); + intent.putExtra(ConversationActivityV2.THREAD_ID, threadId); isPassingAlongMedia = true; startActivity(intent); @@ -226,11 +224,6 @@ public class ShareActivity extends PassphraseRequiredActionBarActivity private Intent getBaseShareIntent(final @NonNull Class target) { final Intent intent = new Intent(this, target); - final String textExtra = getIntent().getStringExtra(Intent.EXTRA_TEXT); - final ArrayList mediaExtra = getIntent().getParcelableArrayListExtra(ConversationActivity.MEDIA_EXTRA); - - intent.putExtra(ConversationActivity.TEXT_EXTRA, textExtra); - intent.putExtra(ConversationActivity.MEDIA_EXTRA, mediaExtra); if (resolvedExtra != null) intent.setDataAndType(resolvedExtra, mimeType); diff --git a/app/src/main/java/org/thoughtcrime/securesms/ShortcutLauncherActivity.java b/app/src/main/java/org/thoughtcrime/securesms/ShortcutLauncherActivity.java index d593cdca96..37fdf2367d 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/ShortcutLauncherActivity.java +++ b/app/src/main/java/org/thoughtcrime/securesms/ShortcutLauncherActivity.java @@ -11,7 +11,7 @@ import androidx.appcompat.app.AppCompatActivity; import android.widget.Toast; import org.session.libsession.utilities.Address; -import org.thoughtcrime.securesms.loki.activities.HomeActivity; +import org.thoughtcrime.securesms.home.HomeActivity; import org.session.libsession.utilities.recipients.Recipient; import org.thoughtcrime.securesms.util.CommunicationActions; diff --git a/app/src/main/java/org/thoughtcrime/securesms/attachments/DatabaseAttachmentProvider.kt b/app/src/main/java/org/thoughtcrime/securesms/attachments/DatabaseAttachmentProvider.kt index d017e770f4..5a13830bd1 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/attachments/DatabaseAttachmentProvider.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/attachments/DatabaseAttachmentProvider.kt @@ -9,12 +9,13 @@ import org.session.libsession.messaging.sending_receiving.attachments.* import org.session.libsession.utilities.Address import org.session.libsession.utilities.UploadResult import org.session.libsession.utilities.Util -import org.session.libsignal.utilities.guava.Optional +import org.session.libsession.utilities.recipients.Recipient import org.session.libsignal.messages.SignalServiceAttachment import org.session.libsignal.messages.SignalServiceAttachmentPointer import org.session.libsignal.messages.SignalServiceAttachmentStream import org.session.libsignal.utilities.Base64 import org.session.libsignal.utilities.Log +import org.session.libsignal.utilities.guava.Optional import org.thoughtcrime.securesms.database.AttachmentDatabase import org.thoughtcrime.securesms.database.Database import org.thoughtcrime.securesms.database.DatabaseFactory @@ -60,9 +61,9 @@ class DatabaseAttachmentProvider(context: Context, helper: SQLCipherOpenHelper) return databaseAttachment.toSignalAttachmentPointer() } - override fun setAttachmentState(attachmentState: AttachmentState, attachmentId: Long, messageID: Long) { + override fun setAttachmentState(attachmentState: AttachmentState, attachmentId: AttachmentId, messageID: Long) { val attachmentDatabase = DatabaseFactory.getAttachmentDatabase(context) - attachmentDatabase.setTransferState(messageID, AttachmentId(attachmentId, 0), attachmentState.value) + attachmentDatabase.setTransferState(messageID, attachmentId, attachmentState.value) } override fun getMessageForQuote(timestamp: Long, author: Address): Pair? { @@ -92,11 +93,39 @@ class DatabaseAttachmentProvider(context: Context, helper: SQLCipherOpenHelper) return message.linkPreviews.firstOrNull()?.attachmentId?.rowId } + override fun getIndividualRecipientForMms(mmsId: Long): Recipient? { + val mmsDb = DatabaseFactory.getMmsDatabase(context) + val message = mmsDb.getMessage(mmsId).use { + mmsDb.readerFor(it).next + } + return message?.individualRecipient + } + override fun insertAttachment(messageId: Long, attachmentId: AttachmentId, stream: InputStream) { val attachmentDatabase = DatabaseFactory.getAttachmentDatabase(context) attachmentDatabase.insertAttachmentsForPlaceholder(messageId, attachmentId, stream) } + override fun updateAudioAttachmentDuration( + attachmentId: AttachmentId, + durationMs: Long, + threadId: Long + ) { + val attachmentDb = DatabaseFactory.getAttachmentDatabase(context) + attachmentDb.setAttachmentAudioExtras(DatabaseAttachmentAudioExtras( + attachmentId = attachmentId, + visualSamples = byteArrayOf(), + durationMs = durationMs + ), threadId) + } + + override fun isMmsOutgoing(mmsMessageId: Long): Boolean { + val mmsDb = DatabaseFactory.getMmsDatabase(context) + return mmsDb.getMessage(mmsMessageId).use { cursor -> + mmsDb.readerFor(cursor).next + }.isOutgoing + } + override fun isOutgoingMessage(timestamp: Long): Boolean { val smsDatabase = DatabaseFactory.getSmsDatabase(context) val mmsDatabase = DatabaseFactory.getMmsDatabase(context) diff --git a/app/src/main/java/org/thoughtcrime/securesms/audio/AudioSlidePlayer.java b/app/src/main/java/org/thoughtcrime/securesms/audio/AudioSlidePlayer.java index a7622306ba..61a92105aa 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/audio/AudioSlidePlayer.java +++ b/app/src/main/java/org/thoughtcrime/securesms/audio/AudioSlidePlayer.java @@ -23,6 +23,7 @@ import com.google.android.exoplayer2.DefaultRenderersFactory; import com.google.android.exoplayer2.ExoPlaybackException; import com.google.android.exoplayer2.ExoPlayerFactory; import com.google.android.exoplayer2.LoadControl; +import com.google.android.exoplayer2.PlaybackParameters; import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.SimpleExoPlayer; import com.google.android.exoplayer2.audio.AudioAttributes; @@ -103,9 +104,9 @@ public class AudioSlidePlayer implements SensorEventListener { } private void play(final double progress, boolean earpiece) throws IOException { - if (this.mediaPlayer != null) return; + if (this.mediaPlayer != null) { stop(); } - LoadControl loadControl = new DefaultLoadControl.Builder().setBufferDurationsMs(Integer.MAX_VALUE, Integer.MAX_VALUE, Integer.MAX_VALUE, Integer.MAX_VALUE).createDefaultLoadControl(); + LoadControl loadControl = new DefaultLoadControl.Builder().setBufferDurationsMs(Integer.MAX_VALUE, Integer.MAX_VALUE, Integer.MAX_VALUE, Integer.MAX_VALUE).createDefaultLoadControl(); this.mediaPlayer = ExoPlayerFactory.newSimpleInstance(context, new DefaultRenderersFactory(context), new DefaultTrackSelector(), loadControl); this.audioAttachmentServer = new AttachmentServer(context, slide.asAttachment()); this.startTime = System.currentTimeMillis(); @@ -184,8 +185,6 @@ public class AudioSlidePlayer implements SensorEventListener { public void onPlayerError(ExoPlaybackException error) { Log.w(TAG, "MediaPlayer Error: " + error); - Toast.makeText(context, R.string.AudioSlidePlayer_error_playing_audio, Toast.LENGTH_SHORT).show(); - synchronized (AudioSlidePlayer.this) { mediaPlayer = null; @@ -267,8 +266,17 @@ public class AudioSlidePlayer implements SensorEventListener { return slide; } + public Long getDuration() { + if (mediaPlayer == null) { return 0L; } + return mediaPlayer.getDuration(); + } - private Pair getProgress() { + public Double getProgress() { + if (mediaPlayer == null) { return 0.0; } + return (double) mediaPlayer.getCurrentPosition() / (double) mediaPlayer.getDuration(); + } + + private Pair getProgressTuple() { if (mediaPlayer == null || mediaPlayer.getCurrentPosition() <= 0 || mediaPlayer.getDuration() <= 0) { return new Pair<>(0D, 0); } else { @@ -277,6 +285,16 @@ public class AudioSlidePlayer implements SensorEventListener { } } + public float getPlaybackSpeed() { + if (mediaPlayer == null) { return 1.0f; } + return mediaPlayer.getPlaybackParameters().speed; + } + + public void setPlaybackSpeed(float speed) { + if (mediaPlayer == null) { return; } + mediaPlayer.setPlaybackParameters(new PlaybackParameters(speed)); + } + private void notifyOnStart() { Util.runOnMain(() -> getListener().onPlayerStart(AudioSlidePlayer.this)); } @@ -383,7 +401,7 @@ public class AudioSlidePlayer implements SensorEventListener { return; } - Pair progress = player.getProgress(); + Pair progress = player.getProgressTuple(); player.notifyOnProgress(progress.first, progress.second); sendEmptyMessageDelayed(0, 50); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/loki/activities/BackupRestoreActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/backup/BackupRestoreActivity.kt similarity index 93% rename from app/src/main/java/org/thoughtcrime/securesms/loki/activities/BackupRestoreActivity.kt rename to app/src/main/java/org/thoughtcrime/securesms/backup/BackupRestoreActivity.kt index a1bc90237b..33b0a8a3fc 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/loki/activities/BackupRestoreActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/backup/BackupRestoreActivity.kt @@ -1,4 +1,4 @@ -package org.thoughtcrime.securesms.loki.activities +package org.thoughtcrime.securesms.backup; import android.app.Activity import android.app.Application @@ -9,17 +9,13 @@ import android.os.Bundle import android.provider.OpenableColumns import android.text.Spannable import android.text.SpannableStringBuilder -import android.text.method.LinkMovementMethod import android.text.style.ClickableSpan import android.text.style.StyleSpan import android.view.View -import android.view.inputmethod.InputMethodManager import android.widget.Toast import androidx.activity.result.ActivityResult import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.viewModels -import androidx.core.widget.addTextChangedListener -import androidx.databinding.DataBindingUtil import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.MutableLiveData import androidx.lifecycle.viewModelScope @@ -28,18 +24,17 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import network.loki.messenger.R -import org.thoughtcrime.securesms.ApplicationContext import org.thoughtcrime.securesms.BaseActionBarActivity -import org.thoughtcrime.securesms.backup.FullBackupImporter import org.thoughtcrime.securesms.backup.FullBackupImporter.DatabaseDowngradeException import org.thoughtcrime.securesms.crypto.AttachmentSecretProvider import org.thoughtcrime.securesms.database.DatabaseFactory import org.session.libsignal.utilities.Log -import org.thoughtcrime.securesms.loki.utilities.setUpActionBarSessionLogo -import org.thoughtcrime.securesms.loki.utilities.show +import org.thoughtcrime.securesms.util.setUpActionBarSessionLogo +import org.thoughtcrime.securesms.util.show import org.thoughtcrime.securesms.notifications.NotificationChannels import org.thoughtcrime.securesms.util.BackupUtil import org.session.libsession.utilities.TextSecurePreferences +import org.thoughtcrime.securesms.home.HomeActivity class BackupRestoreActivity : BaseActionBarActivity() { @@ -188,7 +183,6 @@ class BackupRestoreViewModel(application: Application): AndroidViewModel(applica TextSecurePreferences.setRestorationTime(context, System.currentTimeMillis()) TextSecurePreferences.setHasViewedSeed(context, true) TextSecurePreferences.setHasSeenWelcomeScreen(context, true) - val application = ApplicationContext.getInstance(context) BackupRestoreResult.SUCCESS } catch (e: DatabaseDowngradeException) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/backup/FullBackupExporter.kt b/app/src/main/java/org/thoughtcrime/securesms/backup/FullBackupExporter.kt index 25c902800e..3ea5d8e400 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/backup/FullBackupExporter.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/backup/FullBackupExporter.kt @@ -21,8 +21,7 @@ import org.thoughtcrime.securesms.crypto.ClassicDecryptingPartInputStream import org.thoughtcrime.securesms.crypto.ModernDecryptingPartInputStream import org.thoughtcrime.securesms.database.* import org.session.libsignal.utilities.Log -import org.thoughtcrime.securesms.loki.database.LokiAPIDatabase -import org.thoughtcrime.securesms.loki.database.LokiBackupFilesDatabase +import org.thoughtcrime.securesms.database.LokiBackupFilesDatabase import org.thoughtcrime.securesms.util.BackupUtil import org.session.libsession.utilities.Util import org.session.libsignal.crypto.kdf.HKDFv3 diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/AlbumThumbnailView.java b/app/src/main/java/org/thoughtcrime/securesms/components/AlbumThumbnailView.java index 9f472fb069..fde2cd6b52 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/AlbumThumbnailView.java +++ b/app/src/main/java/org/thoughtcrime/securesms/components/AlbumThumbnailView.java @@ -1,25 +1,27 @@ package org.thoughtcrime.securesms.components; import android.content.Context; -import androidx.annotation.ColorInt; -import androidx.annotation.IdRes; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; import android.util.AttributeSet; import android.view.ViewGroup; import android.widget.FrameLayout; import android.widget.TextView; -import network.loki.messenger.R; -import org.thoughtcrime.securesms.mms.GlideRequests; +import androidx.annotation.ColorInt; +import androidx.annotation.IdRes; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import org.session.libsession.utilities.Stub; +import org.thoughtcrime.securesms.conversation.v2.utilities.KThumbnailView; +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 org.session.libsession.utilities.Stub; import java.util.List; +import network.loki.messenger.R; + public class AlbumThumbnailView extends FrameLayout { private @Nullable SlideClickListener thumbnailClickListener; @@ -51,8 +53,8 @@ public class AlbumThumbnailView extends FrameLayout { private void initialize() { inflate(getContext(), R.layout.album_thumbnail_view, this); - albumCellContainer = findViewById(R.id.album_cell_container); - transferControls = new Stub<>(findViewById(R.id.album_transfer_controls_stub)); + albumCellContainer = findViewById(R.id.albumCellContainer); + transferControls = new Stub<>(findViewById(R.id.albumTransferControlsStub)); } public void setSlides(@NonNull GlideRequests glideRequests, @NonNull List slides, boolean showControls) { @@ -147,10 +149,5 @@ public class AlbumThumbnailView extends FrameLayout { } private void setSlide(@NonNull GlideRequests glideRequests, @NonNull Slide slide, @IdRes int id) { - ThumbnailView cell = findViewById(id); - cell.setImageResource(glideRequests, slide, false, false); - cell.setLoadIndicatorVisibile(slide.isInProgress()); - cell.setThumbnailClickListener(defaultThumbnailClickListener); - cell.setOnLongClickListener(defaultLongClickListener); } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/AvatarImageView.java b/app/src/main/java/org/thoughtcrime/securesms/components/AvatarImageView.java index c0757fea24..21797a9eef 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/AvatarImageView.java +++ b/app/src/main/java/org/thoughtcrime/securesms/components/AvatarImageView.java @@ -19,7 +19,7 @@ import com.bumptech.glide.load.engine.DiskCacheStrategy; -import org.thoughtcrime.securesms.loki.utilities.AvatarPlaceholderGenerator; +import org.thoughtcrime.securesms.util.AvatarPlaceholderGenerator; import org.thoughtcrime.securesms.mms.GlideApp; import org.thoughtcrime.securesms.mms.GlideRequests; diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/ComposeText.java b/app/src/main/java/org/thoughtcrime/securesms/components/ComposeText.java index 012086dc94..42825360c0 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/ComposeText.java +++ b/app/src/main/java/org/thoughtcrime/securesms/components/ComposeText.java @@ -28,166 +28,166 @@ import org.session.libsession.utilities.TextSecurePreferences; public class ComposeText extends EmojiEditText { - private CharSequence hint; - private SpannableString subHint; + private CharSequence hint; + private SpannableString subHint; - @Nullable private InputPanel.MediaListener mediaListener; - @Nullable private CursorPositionChangedListener cursorPositionChangedListener; + @Nullable private InputPanel.MediaListener mediaListener; + @Nullable private CursorPositionChangedListener cursorPositionChangedListener; - public ComposeText(Context context) { - super(context); - initialize(); - } - - public ComposeText(Context context, AttributeSet attrs) { - super(context, attrs); - initialize(); - } - - public ComposeText(Context context, AttributeSet attrs, int defStyleAttr) { - super(context, attrs, defStyleAttr); - initialize(); - } - - public String getTextTrimmed(){ - return getText().toString().trim(); - } - - @Override - protected void onLayout(boolean changed, int left, int top, int right, int bottom) { - super.onLayout(changed, left, top, right, bottom); - - if (!TextUtils.isEmpty(hint)) { - if (!TextUtils.isEmpty(subHint)) { - setHint(new SpannableStringBuilder().append(ellipsizeToWidth(hint)) - .append("\n") - .append(ellipsizeToWidth(subHint))); - } else { - setHint(ellipsizeToWidth(hint)); - } - } - } - - @Override - protected void onSelectionChanged(int selStart, int selEnd) { - super.onSelectionChanged(selStart, selEnd); - - if (cursorPositionChangedListener != null) { - cursorPositionChangedListener.onCursorPositionChanged(selStart, selEnd); - } - } - - private CharSequence ellipsizeToWidth(CharSequence text) { - return TextUtils.ellipsize(text, - getPaint(), - getWidth() - getPaddingLeft() - getPaddingRight(), - TruncateAt.END); - } - - public void setHint(@NonNull String hint, @Nullable CharSequence subHint) { - this.hint = hint; - - if (subHint != null) { - this.subHint = new SpannableString(subHint); - this.subHint.setSpan(new RelativeSizeSpan(0.5f), 0, subHint.length(), Spannable.SPAN_INCLUSIVE_INCLUSIVE); - } else { - this.subHint = null; + public ComposeText(Context context) { + super(context); + initialize(); } - if (this.subHint != null) { - super.setHint(new SpannableStringBuilder().append(ellipsizeToWidth(this.hint)) - .append("\n") - .append(ellipsizeToWidth(this.subHint))); - } else { - super.setHint(ellipsizeToWidth(this.hint)); - } - } - - public void setCursorPositionChangedListener(@Nullable CursorPositionChangedListener listener) { - this.cursorPositionChangedListener = listener; - } - - public void setTransport() { - final boolean useSystemEmoji = TextSecurePreferences.isSystemEmojiPreferred(getContext()); - final boolean isIncognito = TextSecurePreferences.isIncognitoKeyboardEnabled(getContext()); - - int imeOptions = (getImeOptions() & ~EditorInfo.IME_MASK_ACTION) | EditorInfo.IME_ACTION_SEND; - int inputType = getInputType(); - - setImeActionLabel(null, 0); - - if (useSystemEmoji) { - inputType = (inputType & ~InputType.TYPE_MASK_VARIATION) | InputType.TYPE_TEXT_VARIATION_SHORT_MESSAGE; + public ComposeText(Context context, AttributeSet attrs) { + super(context, attrs); + initialize(); } - setInputType(inputType); - if (isIncognito) { - setImeOptions(imeOptions | 16777216); - } else { - setImeOptions(imeOptions); - } - } - - @Override - public InputConnection onCreateInputConnection(EditorInfo editorInfo) { - InputConnection inputConnection = super.onCreateInputConnection(editorInfo); - - if(TextSecurePreferences.isEnterSendsEnabled(getContext())) { - editorInfo.imeOptions &= ~EditorInfo.IME_FLAG_NO_ENTER_ACTION; + public ComposeText(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + initialize(); } - if (Build.VERSION.SDK_INT < 21) return inputConnection; - if (mediaListener == null) return inputConnection; - if (inputConnection == null) return null; - - EditorInfoCompat.setContentMimeTypes(editorInfo, new String[] {"image/jpeg", "image/png", "image/gif"}); - return InputConnectionCompat.createWrapper(inputConnection, editorInfo, new CommitContentListener(mediaListener)); - } - - public void setMediaListener(@Nullable InputPanel.MediaListener mediaListener) { - this.mediaListener = mediaListener; - } - - private void initialize() { - if (TextSecurePreferences.isIncognitoKeyboardEnabled(getContext())) { - setImeOptions(getImeOptions() | 16777216); - } - } - - @RequiresApi(api = Build.VERSION_CODES.HONEYCOMB_MR2) - private static class CommitContentListener implements InputConnectionCompat.OnCommitContentListener { - - private static final String TAG = CommitContentListener.class.getSimpleName(); - - private final InputPanel.MediaListener mediaListener; - - private CommitContentListener(@NonNull InputPanel.MediaListener mediaListener) { - this.mediaListener = mediaListener; + public String getTextTrimmed(){ + return getText().toString().trim(); } @Override - public boolean onCommitContent(InputContentInfoCompat inputContentInfo, int flags, Bundle opts) { - if (BuildCompat.isAtLeastNMR1() && (flags & InputConnectionCompat.INPUT_CONTENT_GRANT_READ_URI_PERMISSION) != 0) { - try { - inputContentInfo.requestPermission(); - } catch (Exception e) { - Log.w(TAG, e); - return false; + protected void onLayout(boolean changed, int left, int top, int right, int bottom) { + super.onLayout(changed, left, top, right, bottom); + + if (!TextUtils.isEmpty(hint)) { + if (!TextUtils.isEmpty(subHint)) { + setHint(new SpannableStringBuilder().append(ellipsizeToWidth(hint)) + .append("\n") + .append(ellipsizeToWidth(subHint))); + } else { + setHint(ellipsizeToWidth(hint)); + } } - } - - if (inputContentInfo.getDescription().getMimeTypeCount() > 0) { - mediaListener.onMediaSelected(inputContentInfo.getContentUri(), - inputContentInfo.getDescription().getMimeType(0)); - - return true; - } - - return false; } - } - public interface CursorPositionChangedListener { - void onCursorPositionChanged(int start, int end); - } + @Override + protected void onSelectionChanged(int selStart, int selEnd) { + super.onSelectionChanged(selStart, selEnd); + + if (cursorPositionChangedListener != null) { + cursorPositionChangedListener.onCursorPositionChanged(selStart, selEnd); + } + } + + private CharSequence ellipsizeToWidth(CharSequence text) { + return TextUtils.ellipsize(text, + getPaint(), + getWidth() - getPaddingLeft() - getPaddingRight(), + TruncateAt.END); + } + + public void setHint(@NonNull String hint, @Nullable CharSequence subHint) { + this.hint = hint; + + if (subHint != null) { + this.subHint = new SpannableString(subHint); + this.subHint.setSpan(new RelativeSizeSpan(0.5f), 0, subHint.length(), Spannable.SPAN_INCLUSIVE_INCLUSIVE); + } else { + this.subHint = null; + } + + if (this.subHint != null) { + super.setHint(new SpannableStringBuilder().append(ellipsizeToWidth(this.hint)) + .append("\n") + .append(ellipsizeToWidth(this.subHint))); + } else { + super.setHint(ellipsizeToWidth(this.hint)); + } + } + + public void setCursorPositionChangedListener(@Nullable CursorPositionChangedListener listener) { + this.cursorPositionChangedListener = listener; + } + + public void setTransport() { + final boolean useSystemEmoji = TextSecurePreferences.isSystemEmojiPreferred(getContext()); + final boolean isIncognito = TextSecurePreferences.isIncognitoKeyboardEnabled(getContext()); + + int imeOptions = (getImeOptions() & ~EditorInfo.IME_MASK_ACTION) | EditorInfo.IME_ACTION_SEND; + int inputType = getInputType(); + + setImeActionLabel(null, 0); + + if (useSystemEmoji) { + inputType = (inputType & ~InputType.TYPE_MASK_VARIATION) | InputType.TYPE_TEXT_VARIATION_SHORT_MESSAGE; + } + + setInputType(inputType); + if (isIncognito) { + setImeOptions(imeOptions | 16777216); + } else { + setImeOptions(imeOptions); + } + } + + @Override + public InputConnection onCreateInputConnection(EditorInfo editorInfo) { + InputConnection inputConnection = super.onCreateInputConnection(editorInfo); + + if(TextSecurePreferences.isEnterSendsEnabled(getContext())) { + editorInfo.imeOptions &= ~EditorInfo.IME_FLAG_NO_ENTER_ACTION; + } + + if (Build.VERSION.SDK_INT < 21) return inputConnection; + if (mediaListener == null) return inputConnection; + if (inputConnection == null) return null; + + EditorInfoCompat.setContentMimeTypes(editorInfo, new String[] {"image/jpeg", "image/png", "image/gif"}); + return InputConnectionCompat.createWrapper(inputConnection, editorInfo, new CommitContentListener(mediaListener)); + } + + public void setMediaListener(@Nullable InputPanel.MediaListener mediaListener) { + this.mediaListener = mediaListener; + } + + private void initialize() { + if (TextSecurePreferences.isIncognitoKeyboardEnabled(getContext())) { + setImeOptions(getImeOptions() | 16777216); + } + } + + @RequiresApi(api = Build.VERSION_CODES.HONEYCOMB_MR2) + private static class CommitContentListener implements InputConnectionCompat.OnCommitContentListener { + + private static final String TAG = CommitContentListener.class.getSimpleName(); + + private final InputPanel.MediaListener mediaListener; + + private CommitContentListener(@NonNull InputPanel.MediaListener mediaListener) { + this.mediaListener = mediaListener; + } + + @Override + public boolean onCommitContent(InputContentInfoCompat inputContentInfo, int flags, Bundle opts) { + if (BuildCompat.isAtLeastNMR1() && (flags & InputConnectionCompat.INPUT_CONTENT_GRANT_READ_URI_PERMISSION) != 0) { + try { + inputContentInfo.requestPermission(); + } catch (Exception e) { + Log.w(TAG, e); + return false; + } + } + + if (inputContentInfo.getDescription().getMimeTypeCount() > 0) { + mediaListener.onMediaSelected(inputContentInfo.getContentUri(), + inputContentInfo.getDescription().getMimeType(0)); + + return true; + } + + return false; + } + } + + public interface CursorPositionChangedListener { + void onCursorPositionChanged(int start, int end); + } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/ConversationItemFooter.java b/app/src/main/java/org/thoughtcrime/securesms/components/ConversationItemFooter.java index 2715d9b773..b93ae4ab65 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/ConversationItemFooter.java +++ b/app/src/main/java/org/thoughtcrime/securesms/components/ConversationItemFooter.java @@ -4,15 +4,17 @@ import android.annotation.SuppressLint; import android.content.Context; import android.content.res.TypedArray; import android.os.AsyncTask; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; import android.util.AttributeSet; import android.view.View; import android.widget.ImageView; import android.widget.LinearLayout; import android.widget.TextView; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + import org.thoughtcrime.securesms.ApplicationContext; +import org.thoughtcrime.securesms.conversation.v2.components.ExpirationTimerView; import org.thoughtcrime.securesms.database.DatabaseFactory; import org.thoughtcrime.securesms.database.model.MessageRecord; import org.thoughtcrime.securesms.service.ExpiringMessageManager; @@ -88,8 +90,6 @@ public class ConversationItemFooter extends LinearLayout { if (messageRecord.isFailed()) { dateView.setText(R.string.ConversationItem_error_not_delivered); - } else if (messageRecord.isPendingInsecureSmsFallback()) { - dateView.setText(R.string.ConversationItem_click_to_approve_unencrypted); } else { dateView.setText(DateUtils.getExtendedRelativeTimeSpanString(getContext(), locale, messageRecord.getTimestamp())); } @@ -131,14 +131,14 @@ public class ConversationItemFooter extends LinearLayout { } private void presentInsecureIndicator(@NonNull MessageRecord messageRecord) { - insecureIndicatorView.setVisibility(messageRecord.isSecure() ? View.GONE : View.VISIBLE); + insecureIndicatorView.setVisibility(View.GONE); } private void presentDeliveryStatus(@NonNull MessageRecord messageRecord) { - if (!messageRecord.isFailed() && !messageRecord.isPendingInsecureSmsFallback()) { + if (!messageRecord.isFailed()) { if (!messageRecord.isOutgoing()) deliveryStatusView.setNone(); else if (messageRecord.isPending()) deliveryStatusView.setPending(); - else if (messageRecord.isRemoteRead()) deliveryStatusView.setRead(); + else if (messageRecord.isRead()) deliveryStatusView.setRead(); else if (messageRecord.isDelivered()) deliveryStatusView.setDelivered(); else deliveryStatusView.setSent(); } else { diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/ConversationItemThumbnail.java b/app/src/main/java/org/thoughtcrime/securesms/components/ConversationItemThumbnail.java index 379b5c77a7..af9e766416 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/ConversationItemThumbnail.java +++ b/app/src/main/java/org/thoughtcrime/securesms/components/ConversationItemThumbnail.java @@ -12,6 +12,7 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.UiThread; +import org.thoughtcrime.securesms.conversation.v2.utilities.ThumbnailView; import org.thoughtcrime.securesms.mms.GlideRequests; import org.thoughtcrime.securesms.mms.Slide; @@ -27,7 +28,7 @@ import network.loki.messenger.R; public class ConversationItemThumbnail extends FrameLayout { - private ThumbnailView thumbnail; + private ThumbnailView thumbnail; private AlbumThumbnailView album; private ImageView shade; private ConversationItemFooter footer; @@ -64,15 +65,10 @@ public class ConversationItemThumbnail extends FrameLayout { if (attrs != null) { TypedArray typedArray = getContext().getTheme().obtainStyledAttributes(attrs, R.styleable.ConversationItemThumbnail, 0, 0); - thumbnail.setBounds(typedArray.getDimensionPixelSize(R.styleable.ConversationItemThumbnail_conversationThumbnail_minWidth, 0), - typedArray.getDimensionPixelSize(R.styleable.ConversationItemThumbnail_conversationThumbnail_maxWidth, 0), - typedArray.getDimensionPixelSize(R.styleable.ConversationItemThumbnail_conversationThumbnail_minHeight, 0), - typedArray.getDimensionPixelSize(R.styleable.ConversationItemThumbnail_conversationThumbnail_maxHeight, 0)); typedArray.recycle(); } } - @SuppressWarnings("SuspiciousNameCombination") @Override protected void dispatchDraw(Canvas canvas) { super.dispatchDraw(canvas); diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/ConversationSearchBottomBar.java b/app/src/main/java/org/thoughtcrime/securesms/components/ConversationSearchBottomBar.java deleted file mode 100644 index 603c4869a1..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/components/ConversationSearchBottomBar.java +++ /dev/null @@ -1,88 +0,0 @@ -package org.thoughtcrime.securesms.components; - -import android.content.Context; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.constraintlayout.widget.ConstraintLayout; -import android.util.AttributeSet; -import android.view.View; -import android.widget.TextView; - -import network.loki.messenger.R; - -/** - * Bottom navigation bar shown in the {@link org.thoughtcrime.securesms.conversation.ConversationActivity} - * when the user is searching within a conversation. Shows details about the results and allows the - * user to move between them. - */ -public class ConversationSearchBottomBar extends ConstraintLayout { - - private View searchDown; - private View searchUp; - private TextView searchPositionText; - private View progressWheel; - - private EventListener eventListener; - - - public ConversationSearchBottomBar(Context context) { - super(context); - } - - public ConversationSearchBottomBar(Context context, AttributeSet attrs) { - super(context, attrs); - } - - @Override - protected void onFinishInflate() { - super.onFinishInflate(); - - this.searchUp = findViewById(R.id.conversation_search_up); - this.searchDown = findViewById(R.id.conversation_search_down); - this.searchPositionText = findViewById(R.id.conversation_search_position); - this.progressWheel = findViewById(R.id.conversation_search_progress_wheel); - } - - public void setData(int position, int count) { - progressWheel.setVisibility(GONE); - - searchUp.setOnClickListener(v -> { - if (eventListener != null) { - eventListener.onSearchMoveUpPressed(); - } - }); - - searchDown.setOnClickListener(v -> { - if (eventListener != null) { - eventListener.onSearchMoveDownPressed(); - } - }); - - if (count > 0) { - searchPositionText.setText(getResources().getString(R.string.ConversationActivity_search_position, position + 1, count)); - } else { - searchPositionText.setText(R.string.ConversationActivity_no_results); - } - - setViewEnabled(searchUp, position < (count - 1)); - setViewEnabled(searchDown, position > 0); - } - - public void showLoading() { - progressWheel.setVisibility(VISIBLE); - } - - private void setViewEnabled(@NonNull View view, boolean enabled) { - view.setEnabled(enabled); - view.setAlpha(enabled ? 1f : 0.25f); - } - - public void setEventListener(@Nullable EventListener eventListener) { - this.eventListener = eventListener; - } - - public interface EventListener { - void onSearchMoveUpPressed(); - void onSearchMoveDownPressed(); - } -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/ConversationTypingView.java b/app/src/main/java/org/thoughtcrime/securesms/components/ConversationTypingView.java deleted file mode 100644 index 4246a79f37..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/components/ConversationTypingView.java +++ /dev/null @@ -1,60 +0,0 @@ -package org.thoughtcrime.securesms.components; - -import android.content.Context; -import android.graphics.PorterDuff; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import android.util.AttributeSet; -import android.view.View; -import android.widget.LinearLayout; - -import org.thoughtcrime.securesms.mms.GlideRequests; -import org.session.libsession.utilities.recipients.Recipient; -import org.session.libsession.utilities.ThemeUtil; - - -import java.util.List; - -import network.loki.messenger.R; - -public class ConversationTypingView extends LinearLayout { - - private AvatarImageView avatar; - private View bubble; - private TypingIndicatorView indicator; - - public ConversationTypingView(Context context, @Nullable AttributeSet attrs) { - super(context, attrs); - } - - @Override - protected void onFinishInflate() { - super.onFinishInflate(); - - avatar = findViewById(R.id.typing_avatar); - bubble = findViewById(R.id.typing_bubble); - indicator = findViewById(R.id.typing_indicator); - } - - public void setTypists(@NonNull GlideRequests glideRequests, @NonNull List typists, boolean isGroupThread) { - if (typists.isEmpty()) { - indicator.stopAnimation(); - return; - } - - Recipient typist = typists.get(0); - - bubble.getBackground().setColorFilter( - ThemeUtil.getThemedColor(getContext(), R.attr.message_received_background_color), - PorterDuff.Mode.MULTIPLY); - - if (isGroupThread) { - avatar.setAvatar(glideRequests, typist, false); - avatar.setVisibility(VISIBLE); - } else { - avatar.setVisibility(GONE); - } - - indicator.startAnimation(); - } -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/InputPanel.java b/app/src/main/java/org/thoughtcrime/securesms/components/InputPanel.java index 9b05c269a4..e81757026c 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/InputPanel.java +++ b/app/src/main/java/org/thoughtcrime/securesms/components/InputPanel.java @@ -4,443 +4,26 @@ import android.annotation.TargetApi; import android.content.Context; import android.net.Uri; import android.os.Build; -import androidx.annotation.DimenRes; -import androidx.annotation.MainThread; import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.core.view.ViewCompat; -import android.text.format.DateUtils; import android.util.AttributeSet; -import android.view.KeyEvent; -import android.view.View; -import android.view.animation.AlphaAnimation; -import android.view.animation.Animation; -import android.view.animation.AnimationSet; -import android.view.animation.Interpolator; -import android.view.animation.TranslateAnimation; import android.widget.LinearLayout; -import android.widget.TextView; -import android.widget.Toast; -import org.thoughtcrime.securesms.components.emoji.EmojiKeyboardProvider; -import org.thoughtcrime.securesms.components.emoji.EmojiToggle; -import org.thoughtcrime.securesms.components.emoji.MediaKeyboard; -import org.session.libsignal.utilities.Log; -import org.thoughtcrime.securesms.loki.utilities.MentionUtilities; -import org.thoughtcrime.securesms.mms.GlideRequests; -import org.thoughtcrime.securesms.mms.SlideDeck; +public class InputPanel extends LinearLayout { -import org.session.libsession.messaging.sending_receiving.link_preview.LinkPreview; -import org.session.libsession.messaging.sending_receiving.quotes.QuoteModel; -import org.session.libsession.utilities.recipients.Recipient; -import org.session.libsession.utilities.TextSecurePreferences; -import org.session.libsession.utilities.Util; -import org.session.libsession.utilities.ViewUtil; -import org.session.libsession.utilities.concurrent.AssertedSuccessListener; -import org.session.libsignal.utilities.ListenableFuture; -import org.session.libsignal.utilities.SettableFuture; -import org.session.libsignal.utilities.guava.Optional; - -import java.util.concurrent.TimeUnit; - -import network.loki.messenger.R; - -public class InputPanel extends LinearLayout - implements MicrophoneRecorderView.Listener, - KeyboardAwareLinearLayout.OnKeyboardShownListener, - EmojiKeyboardProvider.EmojiEventListener -{ - - private static final String TAG = InputPanel.class.getSimpleName(); - - private static final int FADE_TIME = 150; - - private QuoteView quoteView; - private LinkPreviewView linkPreview; - private EmojiToggle mediaKeyboard; - public ComposeText composeText; - private View quickCameraToggle; - private View quickAudioToggle; - private View buttonToggle; - private View recordingContainer; - private View recordLockCancel; - - private MicrophoneRecorderView microphoneRecorderView; - private SlideToCancel slideToCancel; - private RecordTime recordTime; - - private @Nullable Listener listener; - private boolean emojiVisible; - - public InputPanel(Context context) { - super(context); - } - - public InputPanel(Context context, AttributeSet attrs) { - super(context, attrs); - } - - @TargetApi(Build.VERSION_CODES.HONEYCOMB) - public InputPanel(Context context, AttributeSet attrs, int defStyleAttr) { - super(context, attrs, defStyleAttr); - } - - @Override - public void onFinishInflate() { - super.onFinishInflate(); - - View quoteDismiss = findViewById(R.id.quote_dismiss); - - this.quoteView = findViewById(R.id.quote_view); - this.linkPreview = findViewById(R.id.link_preview); - this.mediaKeyboard = findViewById(R.id.emoji_toggle); - this.composeText = findViewById(R.id.embedded_text_editor); - this.quickCameraToggle = findViewById(R.id.quick_camera_toggle); - this.quickAudioToggle = findViewById(R.id.quick_audio_toggle); - this.buttonToggle = findViewById(R.id.button_toggle); - this.recordingContainer = findViewById(R.id.recording_container); - this.recordLockCancel = findViewById(R.id.record_cancel); - View slideToCancelView = findViewById(R.id.slide_to_cancel); - this.slideToCancel = new SlideToCancel(slideToCancelView); - this.microphoneRecorderView = findViewById(R.id.recorder_view); - this.microphoneRecorderView.setListener(this); - this.recordTime = new RecordTime(findViewById(R.id.record_time), - findViewById(R.id.microphone), - TimeUnit.HOURS.toSeconds(1), - () -> microphoneRecorderView.cancelAction()); - - this.recordLockCancel.setOnClickListener(v -> microphoneRecorderView.cancelAction()); - - if (TextSecurePreferences.isSystemEmojiPreferred(getContext())) { - mediaKeyboard.setVisibility(View.GONE); - emojiVisible = false; - } else { - mediaKeyboard.setVisibility(View.VISIBLE); - emojiVisible = true; + public InputPanel(Context context) { + super(context); } - quoteDismiss.setOnClickListener(v -> clearQuote()); - - linkPreview.setCloseClickedListener(() -> { - if (listener != null) { - listener.onLinkPreviewCanceled(); - } - }); - } - - public void setListener(final @NonNull Listener listener) { - this.listener = listener; - - mediaKeyboard.setOnClickListener(v -> listener.onEmojiToggle()); - } - - public void setMediaListener(@NonNull MediaListener listener) { - composeText.setMediaListener(listener); - } - - public void setQuote(@NonNull GlideRequests glideRequests, long id, @NonNull Recipient author, @NonNull String body, @NonNull SlideDeck attachments, @NonNull Recipient conversationRecipient, long threadID) { - this.quoteView.setQuote(glideRequests, id, author, MentionUtilities.highlightMentions(body, threadID, getContext()), false, attachments, conversationRecipient); - this.quoteView.setVisibility(View.VISIBLE); - - if (this.linkPreview.getVisibility() == View.VISIBLE) { - int cornerRadius = readDimen(R.dimen.message_corner_collapse_radius); - this.linkPreview.setCorners(cornerRadius, cornerRadius); - } - } - - public void clearQuote() { - this.quoteView.dismiss(); - - if (this.linkPreview.getVisibility() == View.VISIBLE) { - int cornerRadius = readDimen(R.dimen.message_corner_radius); - this.linkPreview.setCorners(cornerRadius, cornerRadius); - } - } - - public Optional getQuote() { - if (quoteView.getQuoteId() > 0 && quoteView.getVisibility() == View.VISIBLE) { - return Optional.of(new QuoteModel(quoteView.getQuoteId(), quoteView.getAuthor().getAddress(), quoteView.getBody(), false, quoteView.getAttachments())); - } else { - return Optional.absent(); - } - } - - public void setLinkPreviewLoading() { - this.linkPreview.setVisibility(View.VISIBLE); - this.linkPreview.setLoading(); - } - - public void setLinkPreview(@NonNull GlideRequests glideRequests, @NonNull Optional preview) { - if (preview.isPresent()) { - this.linkPreview.setVisibility(View.VISIBLE); - this.linkPreview.setLinkPreview(glideRequests, preview.get(), true); - } else { - this.linkPreview.setVisibility(View.GONE); + public InputPanel(Context context, AttributeSet attrs) { + super(context, attrs); } - int largeCornerRadius = (int)(16 * getResources().getDisplayMetrics().density); - int cornerRadius = quoteView.getVisibility() == VISIBLE ? readDimen(R.dimen.message_corner_collapse_radius) : largeCornerRadius; - - this.linkPreview.setCorners(cornerRadius, cornerRadius); - } - - public void setMediaKeyboard(@NonNull MediaKeyboard mediaKeyboard) { - this.mediaKeyboard.attach(mediaKeyboard); - } - - @Override - public void onRecordPermissionRequired() { - if (listener != null) listener.onRecorderPermissionRequired(); - } - - @Override - public void onRecordPressed() { - if (listener != null) listener.onRecorderStarted(); - recordTime.display(); - slideToCancel.display(); - - if (emojiVisible) ViewUtil.fadeOut(mediaKeyboard, FADE_TIME, View.INVISIBLE); - ViewUtil.fadeOut(composeText, FADE_TIME, View.INVISIBLE); - ViewUtil.fadeOut(quickCameraToggle, FADE_TIME, View.INVISIBLE); - ViewUtil.fadeOut(quickAudioToggle, FADE_TIME, View.INVISIBLE); - buttonToggle.animate().alpha(0).setDuration(FADE_TIME).start(); - } - - @Override - public void onRecordReleased() { - long elapsedTime = onRecordHideEvent(); - - if (listener != null) { - Log.d(TAG, "Elapsed time: " + elapsedTime); - if (elapsedTime > 1000) { - listener.onRecorderFinished(); - } else { - Toast.makeText(getContext(), R.string.InputPanel_tap_and_hold_to_record_a_voice_message_release_to_send, Toast.LENGTH_LONG).show(); - listener.onRecorderCanceled(); - } - } - } - - @Override - public void onRecordMoved(float offsetX, float absoluteX) { - slideToCancel.moveTo(offsetX); - - int direction = ViewCompat.getLayoutDirection(this); - float position = absoluteX / recordingContainer.getWidth(); - - if (direction == ViewCompat.LAYOUT_DIRECTION_LTR && position <= 0.5 || - direction == ViewCompat.LAYOUT_DIRECTION_RTL && position >= 0.6) - { - this.microphoneRecorderView.cancelAction(); - } - } - - @Override - public void onRecordCanceled() { - onRecordHideEvent(); - if (listener != null) listener.onRecorderCanceled(); - } - - @Override - public void onRecordLocked() { - slideToCancel.hide(); - recordLockCancel.setVisibility(View.VISIBLE); - buttonToggle.animate().alpha(1).setDuration(FADE_TIME).start(); - if (listener != null) listener.onRecorderLocked(); - } - - public void onPause() { - this.microphoneRecorderView.cancelAction(); - } - - public void setEnabled(boolean enabled) { - composeText.setEnabled(enabled); - mediaKeyboard.setEnabled(enabled); - quickAudioToggle.setEnabled(enabled); - quickCameraToggle.setEnabled(enabled); - } - - public void setHint(@NonNull String hint) { - composeText.setHint(hint, null); - } - - private long onRecordHideEvent() { - recordLockCancel.setVisibility(View.GONE); - - ListenableFuture future = slideToCancel.hide(); - long elapsedTime = recordTime.hide(); - - future.addListener(new AssertedSuccessListener() { - @Override - public void onSuccess(Void result) { - if (emojiVisible) ViewUtil.fadeIn(mediaKeyboard, FADE_TIME); - ViewUtil.fadeIn(composeText, FADE_TIME); - ViewUtil.fadeIn(quickCameraToggle, FADE_TIME); - ViewUtil.fadeIn(quickAudioToggle, FADE_TIME); - buttonToggle.animate().alpha(1).setDuration(FADE_TIME).start(); - } - }); - - return elapsedTime; - } - - @Override - public void onKeyboardShown() { - mediaKeyboard.setToMedia(); - } - - @Override - public void onKeyEvent(KeyEvent keyEvent) { - composeText.dispatchKeyEvent(keyEvent); - } - - @Override - public void onEmojiSelected(String emoji) { - composeText.insertEmoji(emoji); - } - - private int readDimen(@DimenRes int dimenRes) { - return getResources().getDimensionPixelSize(dimenRes); - } - - public boolean isRecordingInLockedMode() { - return microphoneRecorderView.isRecordingLocked(); - } - - public void releaseRecordingLock() { - microphoneRecorderView.unlockAction(); - } - - public interface Listener { - void onRecorderStarted(); - void onRecorderLocked(); - void onRecorderFinished(); - void onRecorderCanceled(); - void onRecorderPermissionRequired(); - void onEmojiToggle(); - void onLinkPreviewCanceled(); - } - - private static class SlideToCancel { - - private final View slideToCancelView; - - SlideToCancel(View slideToCancelView) { - this.slideToCancelView = slideToCancelView; + @TargetApi(Build.VERSION_CODES.HONEYCOMB) + public InputPanel(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); } - public void display() { - ViewUtil.fadeIn(this.slideToCancelView, FADE_TIME); + public interface MediaListener { + void onMediaSelected(@NonNull Uri uri, String contentType); } - - public ListenableFuture hide() { - final SettableFuture future = new SettableFuture<>(); - - AnimationSet animation = new AnimationSet(true); - animation.addAnimation(new TranslateAnimation(Animation.ABSOLUTE, slideToCancelView.getTranslationX(), - Animation.ABSOLUTE, 0, - Animation.RELATIVE_TO_SELF, 0, - Animation.RELATIVE_TO_SELF, 0)); - animation.addAnimation(new AlphaAnimation(1, 0)); - - animation.setDuration(MicrophoneRecorderView.ANIMATION_DURATION); - animation.setFillBefore(true); - animation.setFillAfter(false); - - slideToCancelView.postDelayed(() -> future.set(null), MicrophoneRecorderView.ANIMATION_DURATION); - slideToCancelView.setVisibility(View.GONE); - slideToCancelView.startAnimation(animation); - - return future; - } - - void moveTo(float offset) { - Animation animation = new TranslateAnimation(Animation.ABSOLUTE, offset, - Animation.ABSOLUTE, offset, - Animation.RELATIVE_TO_SELF, 0, - Animation.RELATIVE_TO_SELF, 0); - - animation.setDuration(0); - animation.setFillAfter(true); - animation.setFillBefore(true); - - slideToCancelView.startAnimation(animation); - } - } - - private static class RecordTime implements Runnable { - - private final @NonNull TextView recordTimeView; - private final @NonNull View microphone; - private final @NonNull Runnable onLimitHit; - private final long limitSeconds; - private long startTime; - - private RecordTime(@NonNull TextView recordTimeView, @NonNull View microphone, long limitSeconds, @NonNull Runnable onLimitHit) { - this.recordTimeView = recordTimeView; - this.microphone = microphone; - this.limitSeconds = limitSeconds; - this.onLimitHit = onLimitHit; - } - - @MainThread - public void display() { - this.startTime = System.currentTimeMillis(); - this.recordTimeView.setText(DateUtils.formatElapsedTime(0)); - ViewUtil.fadeIn(this.recordTimeView, FADE_TIME); - Util.runOnMainDelayed(this, TimeUnit.SECONDS.toMillis(1)); - microphone.setVisibility(View.VISIBLE); - microphone.startAnimation(pulseAnimation()); - } - - @MainThread - public long hide() { - long elapsedTime = System.currentTimeMillis() - startTime; - this.startTime = 0; - ViewUtil.fadeOut(this.recordTimeView, FADE_TIME, View.INVISIBLE); - microphone.clearAnimation(); - ViewUtil.fadeOut(this.microphone, FADE_TIME, View.INVISIBLE); - return elapsedTime; - } - - @Override - @MainThread - public void run() { - long localStartTime = startTime; - if (localStartTime > 0) { - long elapsedTime = System.currentTimeMillis() - localStartTime; - long elapsedSeconds = TimeUnit.MILLISECONDS.toSeconds(elapsedTime); - if (elapsedSeconds >= limitSeconds) { - onLimitHit.run(); - } else { - recordTimeView.setText(DateUtils.formatElapsedTime(elapsedSeconds)); - Util.runOnMainDelayed(this, TimeUnit.SECONDS.toMillis(1)); - } - } - } - - private static Animation pulseAnimation() { - AlphaAnimation animation = new AlphaAnimation(0, 1); - - animation.setInterpolator(pulseInterpolator()); - animation.setRepeatCount(Animation.INFINITE); - animation.setDuration(1000); - - return animation; - } - - private static Interpolator pulseInterpolator() { - return input -> { - input *= 5; - if (input > 1) { - input = 4 - input; - } - return Math.max(0, Math.min(1, input)); - }; - } - } - - public interface MediaListener { - void onMediaSelected(@NonNull Uri uri, String contentType); - } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/loki/views/LabeledSeparatorView.kt b/app/src/main/java/org/thoughtcrime/securesms/components/LabeledSeparatorView.kt similarity index 95% rename from app/src/main/java/org/thoughtcrime/securesms/loki/views/LabeledSeparatorView.kt rename to app/src/main/java/org/thoughtcrime/securesms/components/LabeledSeparatorView.kt index 5a7cdc8c14..34273e565c 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/loki/views/LabeledSeparatorView.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/LabeledSeparatorView.kt @@ -1,4 +1,4 @@ -package org.thoughtcrime.securesms.loki.views +package org.thoughtcrime.securesms.components import android.content.Context import android.graphics.Canvas @@ -9,7 +9,7 @@ import android.view.LayoutInflater import android.widget.RelativeLayout import kotlinx.android.synthetic.main.view_separator.view.* import network.loki.messenger.R -import org.thoughtcrime.securesms.loki.utilities.toPx +import org.thoughtcrime.securesms.util.toPx import org.session.libsession.utilities.ThemeUtil class LabeledSeparatorView : RelativeLayout { diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/MicrophoneRecorderView.java b/app/src/main/java/org/thoughtcrime/securesms/components/MicrophoneRecorderView.java deleted file mode 100644 index 26b3d4f62f..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/components/MicrophoneRecorderView.java +++ /dev/null @@ -1,272 +0,0 @@ -package org.thoughtcrime.securesms.components; - -import android.Manifest; -import android.content.Context; -import android.graphics.PorterDuff; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.core.view.ViewCompat; -import android.util.AttributeSet; -import android.view.MotionEvent; -import android.view.View; -import android.view.animation.Animation; -import android.view.animation.AnimationSet; -import android.view.animation.AnticipateOvershootInterpolator; -import android.view.animation.DecelerateInterpolator; -import android.view.animation.LinearInterpolator; -import android.view.animation.OvershootInterpolator; -import android.view.animation.ScaleAnimation; -import android.view.animation.TranslateAnimation; -import android.widget.FrameLayout; -import android.widget.ImageView; - -import org.thoughtcrime.securesms.permissions.Permissions; - -import org.session.libsession.utilities.ViewUtil; - -import network.loki.messenger.R; - -public final class MicrophoneRecorderView extends FrameLayout implements View.OnTouchListener { - - enum State { - NOT_RUNNING, - RUNNING_HELD, - RUNNING_LOCKED - } - - public static final int ANIMATION_DURATION = 200; - - private FloatingRecordButton floatingRecordButton; - private LockDropTarget lockDropTarget; - private @Nullable Listener listener; - private @NonNull State state = State.NOT_RUNNING; - - public MicrophoneRecorderView(Context context) { - super(context); - } - - public MicrophoneRecorderView(Context context, AttributeSet attrs) { - super(context, attrs); - } - - @Override - public void onFinishInflate() { - super.onFinishInflate(); - - floatingRecordButton = new FloatingRecordButton(getContext(), findViewById(R.id.quick_audio_fab)); - lockDropTarget = new LockDropTarget (getContext(), findViewById(R.id.lock_drop_target)); - - View recordButton = ViewUtil.findById(this, R.id.quick_audio_toggle); - recordButton.setOnTouchListener(this); - } - - public void cancelAction() { - if (state != State.NOT_RUNNING) { - state = State.NOT_RUNNING; - hideUi(); - - if (listener != null) listener.onRecordCanceled(); - } - } - - public boolean isRecordingLocked() { - return state == State.RUNNING_LOCKED; - } - - private void lockAction() { - if (state == State.RUNNING_HELD) { - state = State.RUNNING_LOCKED; - hideUi(); - - if (listener != null) listener.onRecordLocked(); - } - } - - public void unlockAction() { - if (state == State.RUNNING_LOCKED) { - state = State.NOT_RUNNING; - hideUi(); - - if (listener != null) listener.onRecordReleased(); - } - } - - private void hideUi() { - floatingRecordButton.hide(); - lockDropTarget.hide(); - } - - @Override - public boolean onTouch(View v, final MotionEvent event) { - switch (event.getAction()) { - case MotionEvent.ACTION_DOWN: - if (!Permissions.hasAll(getContext(), Manifest.permission.RECORD_AUDIO)) { - if (listener != null) listener.onRecordPermissionRequired(); - } else { - state = State.RUNNING_HELD; - floatingRecordButton.display(event.getX(), event.getY()); - lockDropTarget.display(); - if (listener != null) listener.onRecordPressed(); - } - break; - case MotionEvent.ACTION_CANCEL: - case MotionEvent.ACTION_UP: - if (this.state == State.RUNNING_HELD) { - state = State.NOT_RUNNING; - hideUi(); - if (listener != null) listener.onRecordReleased(); - } - break; - case MotionEvent.ACTION_MOVE: - if (this.state == State.RUNNING_HELD) { - this.floatingRecordButton.moveTo(event.getX(), event.getY()); - if (listener != null) listener.onRecordMoved(floatingRecordButton.lastOffsetX, event.getRawX()); - - int dimensionPixelSize = getResources().getDimensionPixelSize(R.dimen.recording_voice_lock_target); - if (floatingRecordButton.lastOffsetY <= dimensionPixelSize) { - lockAction(); - } - } - break; - } - - return false; - } - - public void setListener(@Nullable Listener listener) { - this.listener = listener; - } - - public interface Listener { - void onRecordPressed(); - void onRecordReleased(); - void onRecordCanceled(); - void onRecordLocked(); - void onRecordMoved(float offsetX, float absoluteX); - void onRecordPermissionRequired(); - } - - private static class FloatingRecordButton { - - private final ImageView recordButtonFab; - - private float startPositionX; - private float startPositionY; - private float lastOffsetX; - private float lastOffsetY; - - FloatingRecordButton(Context context, ImageView recordButtonFab) { - this.recordButtonFab = recordButtonFab; - this.recordButtonFab.getBackground().setColorFilter(context.getResources() - .getColor(R.color.destructive), - PorterDuff.Mode.SRC_IN); - } - - void display(float x, float y) { - this.startPositionX = x; - this.startPositionY = y; - - recordButtonFab.setVisibility(View.VISIBLE); - - AnimationSet animation = new AnimationSet(true); - animation.addAnimation(new TranslateAnimation(Animation.ABSOLUTE, 0, - Animation.ABSOLUTE, 0, - Animation.ABSOLUTE, 0, - Animation.ABSOLUTE, 0)); - - animation.addAnimation(new ScaleAnimation(.5f, 1f, .5f, 1f, - Animation.RELATIVE_TO_SELF, .5f, - Animation.RELATIVE_TO_SELF, .5f)); - - animation.setDuration(ANIMATION_DURATION); - animation.setInterpolator(new OvershootInterpolator()); - - recordButtonFab.startAnimation(animation); - } - - void moveTo(float x, float y) { - lastOffsetX = getXOffset(x); - lastOffsetY = getYOffset(y); - - if (Math.abs(lastOffsetX) > Math.abs(lastOffsetY)) { - lastOffsetY = 0; - } else { - lastOffsetX = 0; - } - - recordButtonFab.setTranslationX(lastOffsetX); - recordButtonFab.setTranslationY(lastOffsetY); - } - - void hide() { - recordButtonFab.setTranslationX(0); - recordButtonFab.setTranslationY(0); - if (recordButtonFab.getVisibility() != VISIBLE) return; - - AnimationSet animation = new AnimationSet(false); - Animation scaleAnimation = new ScaleAnimation(1, 0.5f, 1, 0.5f, - Animation.RELATIVE_TO_SELF, 0.5f, - Animation.RELATIVE_TO_SELF, 0.5f); - - Animation translateAnimation = new TranslateAnimation(Animation.ABSOLUTE, lastOffsetX, - Animation.ABSOLUTE, 0, - Animation.ABSOLUTE, lastOffsetY, - Animation.ABSOLUTE, 0); - - scaleAnimation.setInterpolator(new AnticipateOvershootInterpolator(1.5f)); - translateAnimation.setInterpolator(new DecelerateInterpolator()); - animation.addAnimation(scaleAnimation); - animation.addAnimation(translateAnimation); - animation.setDuration(ANIMATION_DURATION); - animation.setInterpolator(new AnticipateOvershootInterpolator(1.5f)); - - recordButtonFab.setVisibility(View.GONE); - recordButtonFab.clearAnimation(); - recordButtonFab.startAnimation(animation); - } - - private float getXOffset(float x) { - return ViewCompat.getLayoutDirection(recordButtonFab) == ViewCompat.LAYOUT_DIRECTION_LTR ? - -Math.max(0, this.startPositionX - x) : Math.max(0, x - this.startPositionX); - } - - private float getYOffset(float y) { - return Math.min(0, y - this.startPositionY); - } - } - - private static class LockDropTarget { - - private final View lockDropTarget; - private final int dropTargetPosition; - - LockDropTarget(Context context, View lockDropTarget) { - this.lockDropTarget = lockDropTarget; - this.dropTargetPosition = context.getResources().getDimensionPixelSize(R.dimen.recording_voice_lock_target); - } - - void display() { - lockDropTarget.setScaleX(1); - lockDropTarget.setScaleY(1); - lockDropTarget.setAlpha(0); - lockDropTarget.setTranslationY(0); - lockDropTarget.setVisibility(VISIBLE); - lockDropTarget.animate() - .setStartDelay(ANIMATION_DURATION * 2) - .setDuration(ANIMATION_DURATION) - .setInterpolator(new DecelerateInterpolator()) - .translationY(dropTargetPosition) - .alpha(1) - .start(); - } - - void hide() { - lockDropTarget.animate() - .setStartDelay(0) - .setDuration(ANIMATION_DURATION) - .setInterpolator(new LinearInterpolator()) - .scaleX(0).scaleY(0) - .start(); - } - } -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/OutlinedThumbnailView.java b/app/src/main/java/org/thoughtcrime/securesms/components/OutlinedThumbnailView.java index 228ed97e46..71bf8a2804 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/OutlinedThumbnailView.java +++ b/app/src/main/java/org/thoughtcrime/securesms/components/OutlinedThumbnailView.java @@ -5,6 +5,7 @@ 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; @@ -28,7 +29,6 @@ public class OutlinedThumbnailView extends ThumbnailView { outliner = new Outliner(); outliner.setColor(ThemeUtil.getThemedColor(getContext(), R.attr.conversation_item_image_outline_color)); - setRadius(0); setWillNotDraw(false); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/loki/views/ProfilePictureView.kt b/app/src/main/java/org/thoughtcrime/securesms/components/ProfilePictureView.kt similarity index 93% rename from app/src/main/java/org/thoughtcrime/securesms/loki/views/ProfilePictureView.kt rename to app/src/main/java/org/thoughtcrime/securesms/components/ProfilePictureView.kt index 3482088dd9..41e3f510ee 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/loki/views/ProfilePictureView.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/ProfilePictureView.kt @@ -1,4 +1,4 @@ -package org.thoughtcrime.securesms.loki.views +package org.thoughtcrime.securesms.components import android.content.Context import android.util.AttributeSet @@ -17,7 +17,7 @@ import org.session.libsession.utilities.Address import org.session.libsession.utilities.recipients.Recipient import org.session.libsession.utilities.TextSecurePreferences import org.thoughtcrime.securesms.database.DatabaseFactory -import org.thoughtcrime.securesms.loki.utilities.AvatarPlaceholderGenerator +import org.thoughtcrime.securesms.util.AvatarPlaceholderGenerator import org.thoughtcrime.securesms.mms.GlideRequests class ProfilePictureView : RelativeLayout { @@ -31,23 +31,12 @@ class ProfilePictureView : RelativeLayout { private val profilePicturesCache = mutableMapOf() // region Lifecycle - constructor(context: Context) : super(context) { - setUpViewHierarchy() - } + 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, attrs: AttributeSet, defStyleAttr: Int, defStyleRes: Int) : super(context, attrs, defStyleAttr, defStyleRes) { initialize() } - constructor(context: Context, attrs: AttributeSet) : super(context, attrs) { - setUpViewHierarchy() - } - - constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr) { - setUpViewHierarchy() - } - - constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int, defStyleRes: Int) : super(context, attrs, defStyleAttr, defStyleRes) { - setUpViewHierarchy() - } - - private fun setUpViewHierarchy() { + private fun initialize() { val inflater = context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater val contentView = inflater.inflate(R.layout.view_profile_picture, null) addView(contentView) diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/QuoteView.java b/app/src/main/java/org/thoughtcrime/securesms/components/QuoteView.java index 85cf9332bf..5e8c9a99c5 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/QuoteView.java +++ b/app/src/main/java/org/thoughtcrime/securesms/components/QuoteView.java @@ -22,8 +22,8 @@ import com.bumptech.glide.load.engine.DiskCacheStrategy; import org.session.libsession.messaging.contacts.Contact; import org.session.libsession.messaging.sending_receiving.attachments.Attachment; import org.thoughtcrime.securesms.database.DatabaseFactory; -import org.thoughtcrime.securesms.loki.database.SessionContactDatabase; -import org.thoughtcrime.securesms.loki.utilities.UiModeUtilities; +import org.thoughtcrime.securesms.database.SessionContactDatabase; +import org.thoughtcrime.securesms.util.UiModeUtilities; import org.thoughtcrime.securesms.mms.DecryptableStreamUriLoader.DecryptableUri; import org.thoughtcrime.securesms.mms.GlideRequests; import org.thoughtcrime.securesms.mms.Slide; diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/SearchToolbar.java b/app/src/main/java/org/thoughtcrime/securesms/components/SearchToolbar.java index b3ef44585a..8a56acd658 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/SearchToolbar.java +++ b/app/src/main/java/org/thoughtcrime/securesms/components/SearchToolbar.java @@ -16,7 +16,7 @@ import androidx.annotation.Nullable; import androidx.appcompat.widget.SearchView; import androidx.appcompat.widget.Toolbar; -import org.thoughtcrime.securesms.animation.AnimationCompleteListener; +import org.thoughtcrime.securesms.util.AnimationCompleteListener; import network.loki.messenger.R; diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/StickerView.java b/app/src/main/java/org/thoughtcrime/securesms/components/StickerView.java index 3b8c30d31b..6214c58531 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/StickerView.java +++ b/app/src/main/java/org/thoughtcrime/securesms/components/StickerView.java @@ -7,6 +7,7 @@ import android.util.AttributeSet; import android.view.View; import android.widget.FrameLayout; +import org.thoughtcrime.securesms.conversation.v2.utilities.ThumbnailView; import org.thoughtcrime.securesms.mms.GlideRequests; import org.thoughtcrime.securesms.mms.Slide; diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/TypingStatusSender.java b/app/src/main/java/org/thoughtcrime/securesms/components/TypingStatusSender.java index e3317ff1cd..b1ef0f9f72 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/TypingStatusSender.java +++ b/app/src/main/java/org/thoughtcrime/securesms/components/TypingStatusSender.java @@ -8,7 +8,7 @@ import org.session.libsession.messaging.messages.control.TypingIndicator; import org.session.libsession.messaging.sending_receiving.MessageSender; import org.thoughtcrime.securesms.database.DatabaseFactory; import org.thoughtcrime.securesms.database.ThreadDatabase; -import org.thoughtcrime.securesms.loki.protocol.SessionMetaProtocol; +import org.thoughtcrime.securesms.util.SessionMetaProtocol; import org.session.libsession.utilities.recipients.Recipient; import org.session.libsession.utilities.Util; @@ -79,8 +79,7 @@ public class TypingStatusSender { ThreadDatabase threadDatabase = DatabaseFactory.getThreadDatabase(context); Recipient recipient = threadDatabase.getRecipientForThreadId(threadId); if (recipient == null) { return; } - // Loki - Check whether we want to send a typing indicator to this user - if (recipient != null && !SessionMetaProtocol.shouldSendTypingIndicator(recipient.getAddress())) { return; } + if (!SessionMetaProtocol.shouldSendTypingIndicator(recipient.getAddress())) { return; } TypingIndicator typingIndicator; if (typingStarted) { typingIndicator = new TypingIndicator(TypingIndicator.Kind.STARTED); diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/emoji/EmojiTextView.java b/app/src/main/java/org/thoughtcrime/securesms/components/emoji/EmojiTextView.java index 28c8deced0..be730f275d 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/emoji/EmojiTextView.java +++ b/app/src/main/java/org/thoughtcrime/securesms/components/emoji/EmojiTextView.java @@ -11,7 +11,6 @@ import android.text.SpannableStringBuilder; import android.text.TextUtils; import android.util.AttributeSet; import android.util.TypedValue; - import network.loki.messenger.R; import org.thoughtcrime.securesms.components.emoji.EmojiProvider.EmojiDrawable; import org.thoughtcrime.securesms.components.emoji.parsing.EmojiParser; @@ -19,9 +18,7 @@ import org.session.libsession.utilities.TextSecurePreferences; import org.session.libsession.utilities.Util; import org.session.libsignal.utilities.guava.Optional; - public class EmojiTextView extends AppCompatTextView { - private final boolean scaleEmojis; private static final char ELLIPSIS = '…'; @@ -46,14 +43,9 @@ public class EmojiTextView extends AppCompatTextView { public EmojiTextView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); - TypedArray a = context.getTheme().obtainStyledAttributes(attrs, R.styleable.EmojiTextView, 0, 0); - scaleEmojis = a.getBoolean(R.styleable.EmojiTextView_scaleEmojis, false); - maxLength = a.getInteger(R.styleable.EmojiTextView_emoji_maxLength, -1); - a.recycle(); - - a = context.obtainStyledAttributes(attrs, new int[]{android.R.attr.textSize}); - originalFontSize = a.getDimensionPixelSize(0, 0); - a.recycle(); + scaleEmojis = true; + maxLength = 1000; + originalFontSize = getResources().getDimension(R.dimen.small_font_size); } @Override public void setText(@Nullable CharSequence text, BufferType type) { @@ -182,8 +174,11 @@ public class EmojiTextView extends AppCompatTextView { @Override public void invalidateDrawable(@NonNull Drawable drawable) { - if (drawable instanceof EmojiDrawable) invalidate(); - else super.invalidateDrawable(drawable); + if (drawable instanceof EmojiDrawable) { + invalidate(); + } else { + super.invalidateDrawable(drawable); + } } @Override diff --git a/app/src/main/java/org/thoughtcrime/securesms/loki/fragments/ContactSelectionListAdapter.kt b/app/src/main/java/org/thoughtcrime/securesms/contacts/ContactSelectionListAdapter.kt similarity index 96% rename from app/src/main/java/org/thoughtcrime/securesms/loki/fragments/ContactSelectionListAdapter.kt rename to app/src/main/java/org/thoughtcrime/securesms/contacts/ContactSelectionListAdapter.kt index ba039009bc..73be89270a 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/loki/fragments/ContactSelectionListAdapter.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/contacts/ContactSelectionListAdapter.kt @@ -1,4 +1,4 @@ -package org.thoughtcrime.securesms.loki.fragments +package org.thoughtcrime.securesms.contacts import android.content.Context import androidx.recyclerview.widget.RecyclerView @@ -7,7 +7,7 @@ import android.view.View import android.view.ViewGroup import kotlinx.android.synthetic.main.contact_selection_list_divider.view.* import network.loki.messenger.R -import org.thoughtcrime.securesms.loki.views.UserView +import org.thoughtcrime.securesms.contacts.UserView import org.thoughtcrime.securesms.mms.GlideRequests import org.session.libsession.utilities.recipients.Recipient diff --git a/app/src/main/java/org/thoughtcrime/securesms/loki/fragments/ContactSelectionListFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/contacts/ContactSelectionListFragment.kt similarity index 94% rename from app/src/main/java/org/thoughtcrime/securesms/loki/fragments/ContactSelectionListFragment.kt rename to app/src/main/java/org/thoughtcrime/securesms/contacts/ContactSelectionListFragment.kt index 8a68d4e3b8..b32e5a20b8 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/loki/fragments/ContactSelectionListFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/contacts/ContactSelectionListFragment.kt @@ -1,4 +1,4 @@ -package org.thoughtcrime.securesms.loki.fragments +package org.thoughtcrime.securesms.contacts import android.os.Bundle import androidx.fragment.app.Fragment @@ -11,10 +11,11 @@ import android.view.View import android.view.ViewGroup import kotlinx.android.synthetic.main.contact_selection_list_fragment.* import network.loki.messenger.R -import org.thoughtcrime.securesms.contacts.ContactsCursorLoader import org.session.libsignal.utilities.Log import org.thoughtcrime.securesms.mms.GlideApp import org.session.libsession.utilities.recipients.Recipient +import org.thoughtcrime.securesms.contacts.ContactSelectionListItem +import org.thoughtcrime.securesms.contacts.ContactSelectionListLoader class ContactSelectionListFragment : Fragment(), LoaderManager.LoaderCallbacks>, ContactClickListener { private var cursorFilter: String? = null @@ -98,7 +99,7 @@ class ContactSelectionListFragment : Fragment(), LoaderManager.LoaderCallbacks) { + private fun update(items: List) { if (activity?.isDestroyed == true) { Log.e(ContactSelectionListFragment::class.java.name, "Received a loader callback after the fragment was detached from the activity.", diff --git a/app/src/main/java/org/thoughtcrime/securesms/loki/fragments/ContactSelectionListLoader.kt b/app/src/main/java/org/thoughtcrime/securesms/contacts/ContactSelectionListLoader.kt similarity index 96% rename from app/src/main/java/org/thoughtcrime/securesms/loki/fragments/ContactSelectionListLoader.kt rename to app/src/main/java/org/thoughtcrime/securesms/contacts/ContactSelectionListLoader.kt index 8eab5b3a8b..3a2b2cbb5c 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/loki/fragments/ContactSelectionListLoader.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/contacts/ContactSelectionListLoader.kt @@ -1,8 +1,8 @@ -package org.thoughtcrime.securesms.loki.fragments +package org.thoughtcrime.securesms.contacts import android.content.Context import network.loki.messenger.R -import org.thoughtcrime.securesms.loki.utilities.ContactUtilities +import org.thoughtcrime.securesms.util.ContactUtilities import org.session.libsession.utilities.recipients.Recipient import org.thoughtcrime.securesms.util.AsyncLoader diff --git a/app/src/main/java/org/thoughtcrime/securesms/loki/activities/SelectContactsActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/contacts/SelectContactsActivity.kt similarity index 93% rename from app/src/main/java/org/thoughtcrime/securesms/loki/activities/SelectContactsActivity.kt rename to app/src/main/java/org/thoughtcrime/securesms/contacts/SelectContactsActivity.kt index 34a511c27a..bf7a4f6ec8 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/loki/activities/SelectContactsActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/contacts/SelectContactsActivity.kt @@ -1,4 +1,4 @@ -package org.thoughtcrime.securesms.loki.activities +package org.thoughtcrime.securesms.contacts import android.app.Activity import android.content.Intent @@ -40,8 +40,8 @@ class SelectContactsActivity : PassphraseRequiredActionBarActivity(), LoaderMana setContentView(R.layout.activity_select_contacts) supportActionBar!!.title = resources.getString(R.string.activity_select_contacts_title) - usersToExclude = intent.getStringArrayExtra(Companion.usersToExcludeKey)?.toSet() ?: setOf() - val emptyStateText = intent.getStringExtra(Companion.emptyStateTextKey) + usersToExclude = intent.getStringArrayExtra(usersToExcludeKey)?.toSet() ?: setOf() + val emptyStateText = intent.getStringExtra(emptyStateTextKey) if (emptyStateText != null) { emptyStateMessageTextView.text = emptyStateText } diff --git a/app/src/main/java/org/thoughtcrime/securesms/loki/activities/SelectContactsAdapter.kt b/app/src/main/java/org/thoughtcrime/securesms/contacts/SelectContactsAdapter.kt similarity index 96% rename from app/src/main/java/org/thoughtcrime/securesms/loki/activities/SelectContactsAdapter.kt rename to app/src/main/java/org/thoughtcrime/securesms/contacts/SelectContactsAdapter.kt index 2ce6d89ebf..5e3ae1213c 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/loki/activities/SelectContactsAdapter.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/contacts/SelectContactsAdapter.kt @@ -1,10 +1,9 @@ -package org.thoughtcrime.securesms.loki.activities +package org.thoughtcrime.securesms.contacts import android.content.Context import androidx.recyclerview.widget.RecyclerView import android.view.ViewGroup import org.session.libsession.utilities.Address -import org.thoughtcrime.securesms.loki.views.UserView import org.thoughtcrime.securesms.mms.GlideRequests import org.session.libsession.utilities.recipients.Recipient diff --git a/app/src/main/java/org/thoughtcrime/securesms/loki/activities/SelectContactsLoader.kt b/app/src/main/java/org/thoughtcrime/securesms/contacts/SelectContactsLoader.kt similarity index 81% rename from app/src/main/java/org/thoughtcrime/securesms/loki/activities/SelectContactsLoader.kt rename to app/src/main/java/org/thoughtcrime/securesms/contacts/SelectContactsLoader.kt index a0dc10c34d..f71c9b560c 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/loki/activities/SelectContactsLoader.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/contacts/SelectContactsLoader.kt @@ -1,7 +1,7 @@ -package org.thoughtcrime.securesms.loki.activities +package org.thoughtcrime.securesms.contacts import android.content.Context -import org.thoughtcrime.securesms.loki.utilities.ContactUtilities +import org.thoughtcrime.securesms.util.ContactUtilities import org.thoughtcrime.securesms.util.AsyncLoader class SelectContactsLoader(context: Context, val usersToExclude: Set) : AsyncLoader>(context) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/loki/views/UserView.kt b/app/src/main/java/org/thoughtcrime/securesms/contacts/UserView.kt similarity index 96% rename from app/src/main/java/org/thoughtcrime/securesms/loki/views/UserView.kt rename to app/src/main/java/org/thoughtcrime/securesms/contacts/UserView.kt index 7dae953974..d2101973e7 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/loki/views/UserView.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/contacts/UserView.kt @@ -1,4 +1,4 @@ -package org.thoughtcrime.securesms.loki.views +package org.thoughtcrime.securesms.contacts import android.content.Context import android.util.AttributeSet @@ -10,7 +10,7 @@ import kotlinx.android.synthetic.main.view_user.view.* import network.loki.messenger.R import org.session.libsession.messaging.contacts.Contact import org.thoughtcrime.securesms.database.DatabaseFactory -import org.thoughtcrime.securesms.loki.utilities.MentionManagerUtilities +import org.thoughtcrime.securesms.conversation.v2.utilities.MentionManagerUtilities import org.thoughtcrime.securesms.mms.GlideRequests import org.session.libsession.utilities.recipients.Recipient diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationActivity.java b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationActivity.java deleted file mode 100644 index c1f46a6bcf..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationActivity.java +++ /dev/null @@ -1,2441 +0,0 @@ -/* - * Copyright (C) 2011 Whisper Systems - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ -package org.thoughtcrime.securesms.conversation; - -import android.Manifest; -import android.annotation.SuppressLint; -import android.annotation.TargetApi; -import android.content.ActivityNotFoundException; -import android.content.BroadcastReceiver; -import android.content.ClipData; -import android.content.ClipboardManager; -import android.content.Context; -import android.content.Intent; -import android.content.IntentFilter; -import android.content.pm.PackageManager; -import android.content.res.Configuration; -import android.graphics.Bitmap; -import android.graphics.BitmapFactory; -import android.graphics.PorterDuff; -import android.graphics.PorterDuffColorFilter; -import android.hardware.Camera; -import android.net.Uri; -import android.os.AsyncTask; -import android.os.Build; -import android.os.Bundle; -import android.os.Handler; -import android.os.Vibrator; -import android.provider.Browser; -import android.provider.Telephony; -import android.text.Editable; -import android.text.TextUtils; -import android.text.TextWatcher; -import android.util.Pair; -import android.util.TypedValue; -import android.view.KeyEvent; -import android.view.Menu; -import android.view.MenuInflater; -import android.view.MenuItem; -import android.view.View; -import android.view.View.OnClickListener; -import android.view.View.OnFocusChangeListener; -import android.view.View.OnKeyListener; -import android.view.WindowManager; -import android.view.inputmethod.EditorInfo; -import android.widget.Button; -import android.widget.ImageButton; -import android.widget.ImageView; -import android.widget.ProgressBar; -import android.widget.TextView; -import android.widget.Toast; -import androidx.annotation.ColorInt; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.appcompat.app.ActionBar; -import androidx.appcompat.app.AlertDialog; -import androidx.appcompat.widget.SearchView; -import androidx.appcompat.widget.Toolbar; -import androidx.core.content.pm.ShortcutInfoCompat; -import androidx.core.content.pm.ShortcutManagerCompat; -import androidx.core.graphics.drawable.IconCompat; -import androidx.core.view.MenuItemCompat; -import androidx.lifecycle.ViewModelProviders; -import androidx.loader.app.LoaderManager; -import androidx.localbroadcastmanager.content.LocalBroadcastManager; -import com.annimon.stream.Stream; -import org.greenrobot.eventbus.EventBus; -import org.greenrobot.eventbus.Subscribe; -import org.greenrobot.eventbus.ThreadMode; -import org.session.libsession.messaging.mentions.MentionsManager; -import org.session.libsession.messaging.messages.control.ExpirationTimerUpdate; -import org.session.libsession.messaging.messages.signal.OutgoingMediaMessage; -import org.session.libsession.messaging.messages.signal.OutgoingSecureMediaMessage; -import org.session.libsession.messaging.messages.signal.OutgoingTextMessage; -import org.session.libsession.messaging.messages.visible.OpenGroupInvitation; -import org.session.libsession.messaging.messages.visible.VisibleMessage; -import org.session.libsession.messaging.open_groups.OpenGroupV2; -import org.session.libsession.messaging.sending_receiving.MessageSender; -import org.session.libsession.messaging.sending_receiving.attachments.Attachment; -import org.session.libsession.messaging.sending_receiving.link_preview.LinkPreview; -import org.session.libsession.messaging.sending_receiving.quotes.QuoteModel; -import org.session.libsession.utilities.Contact; -import org.session.libsession.utilities.Address; -import org.session.libsession.utilities.DistributionTypes; -import org.session.libsession.utilities.GroupRecord; -import org.session.libsession.utilities.recipients.Recipient; -import org.session.libsession.utilities.recipients.RecipientFormattingException; -import org.session.libsession.utilities.recipients.RecipientModifiedListener; -import org.session.libsession.utilities.ExpirationUtil; -import org.session.libsession.utilities.GroupUtil; -import org.session.libsession.utilities.MediaTypes; -import org.session.libsession.utilities.SSKEnvironment; -import org.session.libsession.utilities.ServiceUtil; -import org.session.libsession.utilities.TextSecurePreferences; -import org.session.libsession.utilities.Util; -import org.session.libsession.utilities.ViewUtil; -import org.session.libsession.utilities.concurrent.AssertedSuccessListener; -import org.session.libsession.utilities.Stub; -import org.session.libsignal.exceptions.InvalidMessageException; -import org.session.libsignal.utilities.guava.Optional; -import org.session.libsession.messaging.mentions.Mention; -import org.session.libsignal.utilities.HexEncodingKt; -import org.session.libsignal.utilities.PublicKeyValidation; -import org.session.libsignal.utilities.ListenableFuture; -import org.session.libsignal.utilities.SettableFuture; -import org.session.libsignal.utilities.Log; -import org.thoughtcrime.securesms.ApplicationContext; -import org.thoughtcrime.securesms.ExpirationDialog; -import org.thoughtcrime.securesms.MediaOverviewActivity; -import org.thoughtcrime.securesms.MuteDialog; -import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity; -import org.thoughtcrime.securesms.ShortcutLauncherActivity; -import org.thoughtcrime.securesms.audio.AudioRecorder; -import org.thoughtcrime.securesms.audio.AudioSlidePlayer; -import org.thoughtcrime.securesms.components.AnimatingToggle; -import org.thoughtcrime.securesms.components.AttachmentTypeSelector; -import org.thoughtcrime.securesms.components.ComposeText; -import org.thoughtcrime.securesms.components.ConversationSearchBottomBar; -import org.thoughtcrime.securesms.components.HidingLinearLayout; -import org.thoughtcrime.securesms.components.InputAwareLayout; -import org.thoughtcrime.securesms.components.InputPanel; -import org.thoughtcrime.securesms.components.KeyboardAwareLinearLayout.OnKeyboardShownListener; -import org.thoughtcrime.securesms.components.emoji.EmojiKeyboardProvider; -import org.thoughtcrime.securesms.components.emoji.EmojiStrings; -import org.thoughtcrime.securesms.components.emoji.MediaKeyboard; -import org.thoughtcrime.securesms.contacts.ContactAccessor; -import org.thoughtcrime.securesms.contacts.ContactAccessor.ContactData; -import org.thoughtcrime.securesms.contactshare.ContactUtil; -import org.thoughtcrime.securesms.contactshare.SimpleTextWatcher; -import org.thoughtcrime.securesms.database.DatabaseFactory; -import org.thoughtcrime.securesms.database.DraftDatabase; -import org.thoughtcrime.securesms.database.DraftDatabase.Draft; -import org.thoughtcrime.securesms.database.DraftDatabase.Drafts; -import org.thoughtcrime.securesms.database.MessagingDatabase.MarkedMessageInfo; -import org.thoughtcrime.securesms.database.MmsSmsColumns.Types; -import org.thoughtcrime.securesms.database.ThreadDatabase; -import org.thoughtcrime.securesms.database.model.MessageRecord; -import org.thoughtcrime.securesms.database.model.MmsMessageRecord; -import org.thoughtcrime.securesms.giph.ui.GiphyActivity; -import org.thoughtcrime.securesms.linkpreview.LinkPreviewRepository; -import org.thoughtcrime.securesms.linkpreview.LinkPreviewUtil; -import org.thoughtcrime.securesms.linkpreview.LinkPreviewViewModel; -import org.thoughtcrime.securesms.loki.activities.EditClosedGroupActivity; -import org.thoughtcrime.securesms.loki.activities.HomeActivity; -import org.thoughtcrime.securesms.loki.activities.SelectContactsActivity; -import org.thoughtcrime.securesms.loki.api.PublicChatInfoUpdateWorker; -import org.thoughtcrime.securesms.loki.protocol.SessionMetaProtocol; -import org.thoughtcrime.securesms.loki.utilities.GeneralUtilitiesKt; -import org.thoughtcrime.securesms.loki.utilities.MentionManagerUtilities; -import org.thoughtcrime.securesms.loki.utilities.OpenGroupUtilities; -import org.thoughtcrime.securesms.loki.views.MentionCandidateSelectionView; -import org.thoughtcrime.securesms.loki.views.ProfilePictureView; -import org.thoughtcrime.securesms.mediasend.Media; -import org.thoughtcrime.securesms.mediasend.MediaSendActivity; -import org.thoughtcrime.securesms.mms.AttachmentManager; -import org.thoughtcrime.securesms.mms.AttachmentManager.MediaType; -import org.thoughtcrime.securesms.mms.AudioSlide; -import org.thoughtcrime.securesms.mms.GifSlide; -import org.thoughtcrime.securesms.mms.GlideApp; -import org.thoughtcrime.securesms.mms.GlideRequests; -import org.thoughtcrime.securesms.mms.ImageSlide; -import org.thoughtcrime.securesms.mms.MediaConstraints; -import org.thoughtcrime.securesms.mms.MmsException; -import org.thoughtcrime.securesms.mms.QuoteId; -import org.thoughtcrime.securesms.mms.Slide; -import org.thoughtcrime.securesms.mms.SlideDeck; -import org.thoughtcrime.securesms.mms.TextSlide; -import org.thoughtcrime.securesms.mms.VideoSlide; -import org.thoughtcrime.securesms.notifications.MarkReadReceiver; -import org.thoughtcrime.securesms.permissions.Permissions; -import org.thoughtcrime.securesms.providers.BlobProvider; -import org.thoughtcrime.securesms.search.model.MessageResult; -import org.thoughtcrime.securesms.service.ExpiringMessageManager; -import org.thoughtcrime.securesms.util.BitmapUtil; -import org.thoughtcrime.securesms.util.DateUtils; -import org.thoughtcrime.securesms.util.MediaUtil; -import org.thoughtcrime.securesms.util.PushCharacterCalculator; -import java.io.IOException; -import java.text.SimpleDateFormat; -import java.util.ArrayList; -import java.util.Collections; -import java.util.Date; -import java.util.List; -import java.util.Locale; -import java.util.concurrent.ExecutionException; -import java.util.concurrent.atomic.AtomicBoolean; -import java.util.concurrent.atomic.AtomicInteger; -import kotlin.Unit; -import network.loki.messenger.R; - -/** - * Activity for displaying a message thread, as well as - * composing/sending a new message into that thread. - * - * @author Moxie Marlinspike - * - */ -@SuppressLint("StaticFieldLeak") -public class ConversationActivity extends PassphraseRequiredActionBarActivity - implements ConversationFragment.ConversationFragmentListener, - AttachmentManager.AttachmentListener, - RecipientModifiedListener, - OnKeyboardShownListener, - InputPanel.Listener, - InputPanel.MediaListener, - ComposeText.CursorPositionChangedListener, - ConversationSearchBottomBar.EventListener -{ - private static final String TAG = ConversationActivity.class.getSimpleName(); - - public static final String ADDRESS_EXTRA = "address"; - public static final String THREAD_ID_EXTRA = "thread_id"; - public static final String IS_ARCHIVED_EXTRA = "is_archived"; - public static final String TEXT_EXTRA = "draft_text"; - public static final String MEDIA_EXTRA = "media_list"; - public static final String STICKER_EXTRA = "media_list"; - public static final String DISTRIBUTION_TYPE_EXTRA = "distribution_type"; - public static final String TIMING_EXTRA = "timing"; - public static final String LAST_SEEN_EXTRA = "last_seen"; - public static final String STARTING_POSITION_EXTRA = "starting_position"; - - // private static final int PICK_GALLERY = 1; - private static final int PICK_DOCUMENT = 2; - private static final int PICK_AUDIO = 3; - private static final int PICK_CONTACT = 4; - // private static final int GET_CONTACT_DETAILS = 5; -// private static final int GROUP_EDIT = 6; - private static final int TAKE_PHOTO = 7; - private static final int ADD_CONTACT = 8; - private static final int PICK_LOCATION = 9; - private static final int PICK_GIF = 10; - private static final int SMS_DEFAULT = 11; - private static final int MEDIA_SENDER = 12; - private static final int INVITE_CONTACTS = 124; - - private GlideRequests glideRequests; - protected ComposeText composeText; - private AnimatingToggle buttonToggle; - private ImageButton sendButton; - private ImageButton attachButton; - private ProfilePictureView profilePictureView; - private TextView titleTextView; - private ConversationFragment fragment; - private Button unblockButton; - private Button makeDefaultSmsButton; - private InputAwareLayout container; - private TypingStatusTextWatcher typingTextWatcher; - private MentionTextWatcher mentionTextWatcher; - private ConversationSearchBottomBar searchNav; - private MenuItem searchViewItem; - private ProgressBar messageStatusProgressBar; - private ImageView muteIndicatorImageView; - private TextView subtitleTextView; - private View homeButtonContainer; - - private AttachmentTypeSelector attachmentTypeSelector; - private AttachmentManager attachmentManager; - private AudioRecorder audioRecorder; - private Handler audioHandler; - private Runnable stopRecordingTask; - private Stub emojiDrawerStub; - protected HidingLinearLayout quickAttachmentToggle; - protected HidingLinearLayout inlineAttachmentToggle; - private InputPanel inputPanel; - - private LinkPreviewViewModel linkPreviewViewModel; - private ConversationSearchViewModel searchViewModel; - - private Recipient recipient; - private long threadId; - private int distributionType; - private boolean isDefaultSms = false; - private boolean isSecurityInitialized = false; - private int expandedKeyboardHeight = 0; - private int collapsedKeyboardHeight = Integer.MAX_VALUE; - private int keyboardHeight = 0; - - // Message status bar - private ArrayList broadcastReceivers = new ArrayList<>(); - private String messageStatus = null; - - // Mentions - private View mentionCandidateSelectionViewContainer; - private MentionCandidateSelectionView mentionCandidateSelectionView; - private int currentMentionStartIndex = -1; - private ArrayList mentions = new ArrayList<>(); - private String oldText = ""; - - private final PushCharacterCalculator characterCalculator = new PushCharacterCalculator(); - - @Override - protected void onCreate(Bundle state, boolean ready) { - Log.i(TAG, "onCreate()"); - - setContentView(R.layout.conversation_activity); - - fragment = initFragment(R.id.fragment_content, new ConversationFragment(), Locale.getDefault()); - - registerMessageStatusObserver("calculatingPoW"); - registerMessageStatusObserver("contactingNetwork"); - registerMessageStatusObserver("sendingMessage"); - registerMessageStatusObserver("messageSent"); - registerMessageStatusObserver("messageFailed"); - BroadcastReceiver broadcastReceiver = new BroadcastReceiver() { - - @Override - public void onReceive(Context context, Intent intent) { - Toast.makeText(ConversationActivity.this, "Your clock is out of sync with the service node network.", Toast.LENGTH_LONG).show(); - } - }; - broadcastReceivers.add(broadcastReceiver); - LocalBroadcastManager.getInstance(this).registerReceiver(broadcastReceiver, new IntentFilter("clockOutOfSync")); - - initializeActionBar(); - initializeViews(); - initializeResources(); - initializeLinkPreviewObserver(); - initializeSearchObserver(); - initializeSecurity(false, isDefaultSms).addListener(new AssertedSuccessListener() { - @Override - public void onSuccess(Boolean result) { - initializeDraft().addListener(new AssertedSuccessListener() { - @Override - public void onSuccess(Boolean loadedDraft) { - if (loadedDraft != null && loadedDraft) { - Log.i(TAG, "Finished loading draft"); - Util.runOnMain(() -> { - if (fragment != null && fragment.isResumed()) { - fragment.moveToLastSeen(); - } else { - Log.w(TAG, "Wanted to move to the last seen position, but the fragment was in an invalid state"); - } - }); - } - - if (TextSecurePreferences.isTypingIndicatorsEnabled(ConversationActivity.this)) { - composeText.addTextChangedListener(typingTextWatcher); - } - composeText.setSelection(composeText.length(), composeText.length()); - composeText.addTextChangedListener(mentionTextWatcher); - mentionCandidateSelectionView.setGlide(glideRequests); - mentionCandidateSelectionView.setOnMentionCandidateSelected( mentionCandidate -> { - mentions.add(mentionCandidate); - String oldText = composeText.getText().toString(); - String newText = oldText.substring(0, currentMentionStartIndex) + "@" + mentionCandidate.getDisplayName() + " "; - composeText.setText(newText); - composeText.setSelection(newText.length()); - currentMentionStartIndex = -1; - mentionCandidateSelectionView.hide(); - ConversationActivity.this.oldText = newText; - return Unit.INSTANCE; - }); - } - }); - } - }); - - MentionManagerUtilities.INSTANCE.populateUserPublicKeyCacheIfNeeded(threadId, this); - - OpenGroupV2 openGroupV2 = DatabaseFactory.getLokiThreadDatabase(this).getOpenGroupChat(threadId); - if (openGroupV2 != null) { - PublicChatInfoUpdateWorker.scheduleInstant(this, openGroupV2.getServer(), openGroupV2.getRoom()); - if (openGroupV2.getRoom().equals("session") || openGroupV2.getRoom().equals("oxen") - || openGroupV2.getRoom().equals("lokinet") || openGroupV2.getRoom().equals("crypto")) { - View openGroupGuidelinesView = findViewById(R.id.open_group_guidelines_view); - openGroupGuidelinesView.setVisibility(View.VISIBLE); - } - } - - View rootView = findViewById(R.id.rootView); - rootView.getViewTreeObserver().addOnGlobalLayoutListener(() -> { - int height = rootView.getRootView().getHeight() - rootView.getHeight(); - int thresholdInDP = 120; - float scale = getResources().getDisplayMetrics().density; - int thresholdInPX = (int)(thresholdInDP * scale); - if (expandedKeyboardHeight == 0 || height > thresholdInPX) { - expandedKeyboardHeight = height; - } - collapsedKeyboardHeight = Math.min(collapsedKeyboardHeight, height); - keyboardHeight = expandedKeyboardHeight - collapsedKeyboardHeight; - - // Use 300dp if the keyboard wasn't opened yet. - if (keyboardHeight == 0) { - keyboardHeight = (int)(300f * getResources().getDisplayMetrics().density); - } - }); - } - - private void registerMessageStatusObserver(String status) { - BroadcastReceiver broadcastReceiver = new BroadcastReceiver() { - - @Override - public void onReceive(Context context, Intent intent) { - long timestamp = intent.getLongExtra("long", 0); - handleMessageStatusChanged(status, timestamp); - } - }; - broadcastReceivers.add(broadcastReceiver); - LocalBroadcastManager.getInstance(this).registerReceiver(broadcastReceiver, new IntentFilter(status)); - } - - @Override - protected void onNewIntent(Intent intent) { - super.onNewIntent(intent); - Log.i(TAG, "onNewIntent()"); - - if (isFinishing()) { - Log.w(TAG, "Activity is finishing..."); - return; - } - - if (!org.thoughtcrime.securesms.util.Util.isEmpty(composeText) || attachmentManager.isAttachmentPresent()) { - saveDraft(); - attachmentManager.clear(glideRequests, false); - silentlySetComposeText(""); - } - - setIntent(intent); - initializeResources(); - initializeSecurity(false, isDefaultSms).addListener(new AssertedSuccessListener() { - @Override - public void onSuccess(Boolean result) { - initializeDraft(); - } - }); - - if (fragment != null) { - fragment.onNewIntent(); - } - - searchNav.setVisibility(View.GONE); - } - - @Override - protected void onResume() { - super.onResume(); - - EventBus.getDefault().register(this); - initializeEnabledCheck(); - composeText.setTransport(); - - updateTitleTextView(recipient); - updateProfilePicture(); - updateSubtitleTextView(); - updateInputUI(recipient); - - ApplicationContext.getInstance(this).messageNotifier.setVisibleThread(threadId); - markThreadAsRead(); - - inputPanel.setHint(getResources().getString(R.string.ConversationActivity_message)); - - Log.i(TAG, "onResume() Finished: " + (System.currentTimeMillis() - getIntent().getLongExtra(TIMING_EXTRA, 0))); - } - - @Override - protected void onPause() { - super.onPause(); - ApplicationContext.getInstance(this).messageNotifier.setVisibleThread(-1L); - if (isFinishing()) overridePendingTransition(R.anim.fade_scale_in, R.anim.slide_to_right); - inputPanel.onPause(); - - fragment.setLastSeen(System.currentTimeMillis()); - markLastSeen(); - AudioSlidePlayer.stopAll(); - EventBus.getDefault().unregister(this); - } - - @Override - protected void onStop() { - super.onStop(); - EventBus.getDefault().unregister(this); - } - - @Override - public void onConfigurationChanged(Configuration newConfig) { - Log.i(TAG, "onConfigurationChanged(" + newConfig.orientation + ")"); - super.onConfigurationChanged(newConfig); - composeText.setTransport(); - - if (emojiDrawerStub.resolved() && container.getCurrentInput() == emojiDrawerStub.get()) { - container.hideAttachedInput(true); - } - } - - @Override - protected void onDestroy() { - saveDraft(); - if (recipient != null) recipient.removeListener(this); - for (BroadcastReceiver broadcastReceiver : broadcastReceivers) { - LocalBroadcastManager.getInstance(this).unregisterReceiver(broadcastReceiver); - } - super.onDestroy(); - } - - @Override - public void onActivityResult(final int reqCode, int resultCode, Intent data) { - Log.i(TAG, "onActivityResult called: " + reqCode + ", " + resultCode + " , " + data); - super.onActivityResult(reqCode, resultCode, data); - - if ((data == null && reqCode != TAKE_PHOTO && reqCode != SMS_DEFAULT) || - (resultCode != RESULT_OK && reqCode != SMS_DEFAULT)) - { - updateLinkPreviewState(); - return; - } - - switch (reqCode) { - case PICK_DOCUMENT: - setMedia(data.getData(), MediaType.DOCUMENT); - break; - case PICK_AUDIO: - setMedia(data.getData(), MediaType.AUDIO); - break; - case TAKE_PHOTO: - if (attachmentManager.getCaptureUri() != null) { - setMedia(attachmentManager.getCaptureUri(), MediaType.IMAGE); - } - break; - case ADD_CONTACT: - recipient = Recipient.from(this, recipient.getAddress(), true); - recipient.addListener(this); - fragment.reloadList(); - break; - /* - case PICK_LOCATION: - SignalPlace place = new SignalPlace(PlacePicker.getPlace(data, this)); - attachmentManager.setLocation(place, getCurrentMediaConstraints()); - break; - */ - case PICK_GIF: - setMedia(data.getData(), - MediaType.GIF, - data.getIntExtra(GiphyActivity.EXTRA_WIDTH, 0), - data.getIntExtra(GiphyActivity.EXTRA_HEIGHT, 0)); - break; - case SMS_DEFAULT: - initializeSecurity(true, isDefaultSms); - break; - case MEDIA_SENDER: - long expiresIn = recipient.getExpireMessages() * 1000L; - int subscriptionId = -1; - boolean initiating = threadId == -1; - String message = data.getStringExtra(MediaSendActivity.EXTRA_MESSAGE); - SlideDeck slideDeck = new SlideDeck(); - - List mediaList = data.getParcelableArrayListExtra(MediaSendActivity.EXTRA_MEDIA); - - for (Media mediaItem : mediaList) { - if (MediaUtil.isVideoType(mediaItem.getMimeType())) { - slideDeck.addSlide(new VideoSlide(this, mediaItem.getUri(), 0, mediaItem.getCaption().orNull())); - } else if (MediaUtil.isGif(mediaItem.getMimeType())) { - slideDeck.addSlide(new GifSlide(this, mediaItem.getUri(), 0, mediaItem.getWidth(), mediaItem.getHeight(), mediaItem.getCaption().orNull())); - } else if (MediaUtil.isImageType(mediaItem.getMimeType())) { - slideDeck.addSlide(new ImageSlide(this, mediaItem.getUri(), 0, mediaItem.getWidth(), mediaItem.getHeight(), mediaItem.getCaption().orNull())); - } else { - Log.w(TAG, "Asked to send an unexpected mimeType: '" + mediaItem.getMimeType() + "'. Skipping."); - } - } - - final Context context = ConversationActivity.this.getApplicationContext(); - - sendMediaMessage(message, - slideDeck, - inputPanel.getQuote().orNull(), - Optional.absent(), - initiating).addListener(new AssertedSuccessListener() { - @Override - public void onSuccess(Void result) { - AsyncTask.THREAD_POOL_EXECUTOR.execute(() -> { - Stream.of(slideDeck.getSlides()) - .map(Slide::getUri) - .withoutNulls() - .filter(BlobProvider::isAuthority) - .forEach(uri -> BlobProvider.getInstance().delete(context, uri)); - }); - } - }); - break; - case INVITE_CONTACTS: - if (data.getExtras() == null || !data.hasExtra(SelectContactsActivity.Companion.getSelectedContactsKey())) return; - String[] selectedContacts = data.getExtras().getStringArray(SelectContactsActivity.Companion.getSelectedContactsKey()); - sendOpenGroupInvitations(selectedContacts); - break; - } - } - - @Override - public void startActivity(Intent intent) { - if (intent.getStringExtra(Browser.EXTRA_APPLICATION_ID) != null) { - intent.removeExtra(Browser.EXTRA_APPLICATION_ID); - } - - try { - super.startActivity(intent); - } catch (ActivityNotFoundException e) { - Log.w(TAG, e); - Toast.makeText(this, R.string.ConversationActivity_there_is_no_app_available_to_handle_this_link_on_your_device, Toast.LENGTH_LONG).show(); - } - } - - @Override - public boolean onPrepareOptionsMenu(Menu menu) { - MenuInflater inflater = this.getMenuInflater(); - menu.clear(); - - boolean isOpenGroupOrRSSFeed = recipient.getAddress().isOpenGroup(); - - if (!isOpenGroupOrRSSFeed) { - if (recipient.getExpireMessages() > 0) { - inflater.inflate(R.menu.conversation_expiring_on, menu); - - final MenuItem item = menu.findItem(R.id.menu_expiring_messages); - final View actionView = MenuItemCompat.getActionView(item); - final ImageView iconView = actionView.findViewById(R.id.menu_badge_icon); - final TextView badgeView = actionView.findViewById(R.id.expiration_badge); - - @ColorInt int color = GeneralUtilitiesKt.getColorWithID(getResources(), R.color.text, getTheme()); - iconView.setColorFilter(new PorterDuffColorFilter(color, PorterDuff.Mode.MULTIPLY)); - badgeView.setText(ExpirationUtil.getExpirationAbbreviatedDisplayValue(this, recipient.getExpireMessages())); - actionView.setOnClickListener(v -> onOptionsItemSelected(item)); - } else { - inflater.inflate(R.menu.conversation_expiring_off, menu); - } - } - - if (isSingleConversation()) { - if (recipient.isBlocked()) { - inflater.inflate(R.menu.conversation_unblock, menu); - } else { - inflater.inflate(R.menu.conversation_block, menu); - } - inflater.inflate(R.menu.conversation_copy_session_id, menu); - } else if (isGroupConversation() && !isOpenGroupOrRSSFeed) { -// inflater.inflate(R.menu.conversation_group_options, menu); - - if (!isPushGroupConversation()) { - inflater.inflate(R.menu.conversation_mms_group_options, menu); - if (distributionType == DistributionTypes.BROADCAST) { - menu.findItem(R.id.menu_distribution_broadcast).setChecked(true); - } else { - menu.findItem(R.id.menu_distribution_conversation).setChecked(true); - } - } else if (isActiveGroup()) { - inflater.inflate(R.menu.conversation_push_group_options, menu); - } - } else if (isOpenGroupOrRSSFeed) { - inflater.inflate(R.menu.conversation_invite_open_group, menu); - } - - inflater.inflate(R.menu.conversation, menu); - -// if (isSingleConversation()) { -// inflater.inflate(R.menu.conversation_secure, menu); -// } - - if (recipient != null && recipient.isMuted()) inflater.inflate(R.menu.conversation_muted, menu); - else inflater.inflate(R.menu.conversation_unmuted, menu); - - /* - if (isSingleConversation() && getRecipient().getContactUri() == null) { - inflater.inflate(R.menu.conversation_add_to_contacts, menu); - } - if (recipient != null && recipient.isLocalNumber()) { - if (isSecureText) menu.findItem(R.id.menu_call_secure).setVisible(false); - else menu.findItem(R.id.menu_call_insecure).setVisible(false); - MenuItem muteItem = menu.findItem(R.id.menu_mute_notifications); - if (muteItem != null) { - muteItem.setVisible(false); - } - } - */ - - searchViewItem = menu.findItem(R.id.menu_search); - - SearchView searchView = (SearchView)searchViewItem.getActionView(); - SearchView.OnQueryTextListener queryListener = new SearchView.OnQueryTextListener() { - @Override - public boolean onQueryTextSubmit(String query) { - searchViewModel.onQueryUpdated(query, threadId); - searchNav.showLoading(); - fragment.onSearchQueryUpdated(query); - return true; - } - - @Override - public boolean onQueryTextChange(String query) { - searchViewModel.onQueryUpdated(query, threadId); - searchNav.showLoading(); - fragment.onSearchQueryUpdated(query); - return true; - } - }; - - searchViewItem.setOnActionExpandListener(new MenuItem.OnActionExpandListener() { - @Override - public boolean onMenuItemActionExpand(MenuItem item) { - searchView.setOnQueryTextListener(queryListener); - searchViewModel.onSearchOpened(); - searchNav.setVisibility(View.VISIBLE); - searchNav.setData(0, 0); - inputPanel.setVisibility(View.GONE); - - for (int i = 0; i < menu.size(); i++) { - if (!menu.getItem(i).equals(searchViewItem)) { - menu.getItem(i).setVisible(false); - } - } - return true; - } - - @Override - public boolean onMenuItemActionCollapse(MenuItem item) { - searchView.setOnQueryTextListener(null); - searchViewModel.onSearchClosed(); - searchNav.setVisibility(View.GONE); - inputPanel.setVisibility(View.VISIBLE); - updateInputUI(recipient); - fragment.onSearchQueryUpdated(null); - invalidateOptionsMenu(); - return true; - } - }); - - super.onPrepareOptionsMenu(menu); - return true; - } - - @Override - public boolean onOptionsItemSelected(MenuItem item) { - super.onOptionsItemSelected(item); - switch (item.getItemId()) { -// case R.id.menu_call_secure: handleDial(getRecipient(), true); return true; -// case R.id.menu_call_insecure: handleDial(getRecipient(), false); return true; - case R.id.menu_unblock: handleUnblock(); return true; - case R.id.menu_block: handleBlock(); return true; - case R.id.menu_copy_session_id: handleCopySessionID(); return true; - case R.id.menu_view_media: handleViewMedia(); return true; - case R.id.menu_add_shortcut: handleAddShortcut(); return true; - case R.id.menu_search: handleSearch(); return true; -// case R.id.menu_add_to_contacts: handleAddToContacts(); return true; -// case R.id.menu_reset_secure_session: handleResetSecureSession(); return true; -// case R.id.menu_group_recipients: handleDisplayGroupRecipients(); return true; - case R.id.menu_distribution_broadcast: handleDistributionBroadcastEnabled(item); return true; - case R.id.menu_distribution_conversation: handleDistributionConversationEnabled(item); return true; - case R.id.menu_edit_group: handleEditPushGroup(); return true; - case R.id.menu_leave: handleLeavePushGroup(); return true; - case R.id.menu_mute_notifications: handleMuteNotifications(); return true; - case R.id.menu_unmute_notifications: handleUnmuteNotifications(); return true; -// case R.id.menu_conversation_settings: handleConversationSettings(); return true; - case R.id.menu_expiring_messages_off: - case R.id.menu_expiring_messages: handleSelectMessageExpiration(); return true; - case R.id.menu_invite_to_open_group: handleInviteToOpenGroup(); return true; - case android.R.id.home: handleReturnToConversationList(); return true; - } - - return false; - } - - @Override - public void onBackPressed() { - Log.d(TAG, "onBackPressed()"); - if (container.isInputOpen()) container.hideCurrentInput(composeText); - else super.onBackPressed(); - } - - @Override - public void onKeyboardShown() { - inputPanel.onKeyboardShown(); - } - - @Override - public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { - super.onRequestPermissionsResult(requestCode, permissions, grantResults); - Permissions.onRequestPermissionsResult(this, requestCode, permissions, grantResults); - } - - //////// Event Handlers - - private void handleReturnToConversationList() { - Intent intent = new Intent(this, HomeActivity.class); - intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); - startActivity(intent); - finish(); - } - - private void handleSelectMessageExpiration() { - if (isPushGroupConversation() && !isActiveGroup()) { - return; - } - - //noinspection CodeBlock2Expr - ExpirationDialog.show(this, recipient.getExpireMessages(), expirationTime -> { - new AsyncTask() { - @Override - protected Void doInBackground(Void... params) { - DatabaseFactory.getRecipientDatabase(ConversationActivity.this).setExpireMessages(recipient, expirationTime); - ExpirationTimerUpdate message = new ExpirationTimerUpdate(expirationTime); - message.setRecipient(recipient.getAddress().serialize()); // we need the recipient in ExpiringMessageManager.insertOutgoingExpirationTimerMessage - message.setSentTimestamp(System.currentTimeMillis()); - ExpiringMessageManager expiringMessageManager = ApplicationContext.getInstance(getApplicationContext()).getExpiringMessageManager(); - expiringMessageManager.setExpirationTimer(message); - MessageSender.send(message, recipient.getAddress()); - - return null; - } - - @Override - protected void onPostExecute(Void result) { - invalidateOptionsMenu(); - if (fragment != null) fragment.setLastSeen(0); - } - }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); - }); - } - - private void handleMuteNotifications() { - MuteDialog.show(this, until -> { - recipient.setMuted(until); - - new AsyncTask() { - @Override - protected Void doInBackground(Void... params) { - DatabaseFactory.getRecipientDatabase(ConversationActivity.this) - .setMuted(recipient, until); - - return null; - } - }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); - }); - } - - private void handleUnmuteNotifications() { - recipient.setMuted(0); - - new AsyncTask() { - @Override - protected Void doInBackground(Void... params) { - DatabaseFactory.getRecipientDatabase(ConversationActivity.this) - .setMuted(recipient, 0); - - return null; - } - }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); - } - - private void handleUnblock() { - int titleRes = R.string.ConversationActivity_unblock_this_contact_question; - int bodyRes = R.string.ConversationActivity_you_will_once_again_be_able_to_receive_messages_and_calls_from_this_contact; - - new AlertDialog.Builder(this) - .setTitle(titleRes) - .setMessage(bodyRes) - .setNegativeButton(android.R.string.cancel, null) - .setPositiveButton(R.string.ConversationActivity_unblock, (dialog, which) -> { - new AsyncTask() { - @Override - protected Void doInBackground(Void... params) { - DatabaseFactory.getRecipientDatabase(ConversationActivity.this) - .setBlocked(recipient, false); - - return null; - } - }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); - }).show(); - } - - @TargetApi(Build.VERSION_CODES.KITKAT) - private void handleMakeDefaultSms() { - Intent intent = new Intent(Telephony.Sms.Intents.ACTION_CHANGE_DEFAULT); - intent.putExtra(Telephony.Sms.Intents.EXTRA_PACKAGE_NAME, getPackageName()); - startActivityForResult(intent, SMS_DEFAULT); - } - - private void handleBlock() { - int titleRes = R.string.RecipientPreferenceActivity_block_this_contact_question; - int bodyRes = R.string.RecipientPreferenceActivity_you_will_no_longer_receive_messages_and_calls_from_this_contact; - - new AlertDialog.Builder(this) - .setTitle(titleRes) - .setMessage(bodyRes) - .setNegativeButton(android.R.string.cancel, null) - .setPositiveButton(R.string.RecipientPreferenceActivity_block, (dialog, which) -> { - new AsyncTask() { - @Override - protected Void doInBackground(Void... params) { - DatabaseFactory.getRecipientDatabase(ConversationActivity.this) - .setBlocked(recipient, true); - - Util.runOnMain(() -> finish()); - - return null; - } - }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); - }).show(); - } - - private void handleCopySessionID() { - if (recipient.isGroupRecipient()) { return; } - String sessionID = recipient.getAddress().toString(); - ClipboardManager clipboard = (ClipboardManager)getSystemService(Context.CLIPBOARD_SERVICE); - ClipData clip = ClipData.newPlainText("Session ID", sessionID); - clipboard.setPrimaryClip(clip); - Toast.makeText(this, R.string.copied_to_clipboard, Toast.LENGTH_SHORT).show(); - } - - private void handleViewMedia() { - Intent intent = new Intent(this, MediaOverviewActivity.class); - intent.putExtra(MediaOverviewActivity.ADDRESS_EXTRA, recipient.getAddress()); - startActivity(intent); - } - - private void handleAddShortcut() { - Log.i(TAG, "Creating home screen shortcut for recipient " + recipient.getAddress()); - - new AsyncTask() { - - @Override - protected IconCompat doInBackground(Void... voids) { - Context context = getApplicationContext(); - IconCompat icon = null; - - if (recipient.getContactPhoto() != null) { - try { - Bitmap bitmap = BitmapFactory.decodeStream(recipient.getContactPhoto().openInputStream(context)); - bitmap = BitmapUtil.createScaledBitmap(bitmap, 300, 300); - icon = IconCompat.createWithAdaptiveBitmap(bitmap); - } catch (IOException e) { - Log.w(TAG, "Failed to decode contact photo during shortcut creation. Falling back to generic icon.", e); - } - } - - if (icon == null) { - icon = IconCompat.createWithResource(context, recipient.isGroupRecipient() ? R.mipmap.ic_group_shortcut - : R.mipmap.ic_person_shortcut); - } - - return icon; - } - - @Override - protected void onPostExecute(IconCompat icon) { - Context context = getApplicationContext(); - String name = Optional.fromNullable(recipient.getName()) - .or(Optional.fromNullable(recipient.getProfileName())) - .or(recipient.toShortString()); - - ShortcutInfoCompat shortcutInfo = new ShortcutInfoCompat.Builder(context, recipient.getAddress().serialize() + '-' + System.currentTimeMillis()) - .setShortLabel(name) - .setIcon(icon) - .setIntent(ShortcutLauncherActivity.createIntent(context, recipient.getAddress())) - .build(); - - if (ShortcutManagerCompat.requestPinShortcut(context, shortcutInfo, null)) { - Toast.makeText(context, getString(R.string.ConversationActivity_added_to_home_screen), Toast.LENGTH_LONG).show(); - } - } - }.execute(); - } - - private void handleSearch() { - searchViewModel.onSearchOpened(); - } - - private void handleLeavePushGroup() { - if (getRecipient() == null) { - Toast.makeText(this, getString(R.string.ConversationActivity_invalid_recipient), - Toast.LENGTH_LONG).show(); - return; - } - - AlertDialog.Builder builder = new AlertDialog.Builder(this); - builder.setTitle(getString(R.string.ConversationActivity_leave_group)); - builder.setIconAttribute(R.attr.dialog_info_icon); - builder.setCancelable(true); - - GroupRecord group = DatabaseFactory.getGroupDatabase(this).getGroup(getRecipient().getAddress().toGroupString()).orNull(); - List
admins = group.getAdmins(); - String userPublicKey = TextSecurePreferences.getLocalNumber(this); - String message = getString(R.string.ConversationActivity_are_you_sure_you_want_to_leave_this_group); - for (Address admin : admins) { - if (admin.toString().equals(userPublicKey)) { - message = "Because you are the creator of this group it will be deleted for everyone. This cannot be undone."; - } - } - - builder.setMessage(message); - builder.setPositiveButton(R.string.yes, (dialog, which) -> { - Recipient groupRecipient = getRecipient(); - String groupPublicKey; - boolean isClosedGroup; - try { - groupPublicKey = HexEncodingKt.toHexString(GroupUtil.doubleDecodeGroupID(groupRecipient.getAddress().toString())); - isClosedGroup = DatabaseFactory.getLokiAPIDatabase(this).isClosedGroup(groupPublicKey); - } catch (IOException e) { - groupPublicKey = null; - isClosedGroup = false; - } - try { - if (isClosedGroup) { - MessageSender.explicitLeave(groupPublicKey, true); - initializeEnabledCheck(); - } else { - Toast.makeText(this, R.string.ConversationActivity_error_leaving_group, Toast.LENGTH_LONG).show(); - } - } catch (Exception e) { - Toast.makeText(this, R.string.ConversationActivity_error_leaving_group, Toast.LENGTH_LONG).show(); - } - }); - - builder.setNegativeButton(R.string.no, null); - builder.show(); - } - - private void handleEditPushGroup() { - Intent intent = new Intent(this, EditClosedGroupActivity.class); - String groupID = this.recipient.getAddress().toGroupString(); - intent.putExtra(EditClosedGroupActivity.Companion.getGroupIDKey(), groupID); - startActivity(intent); - } - - private void handleInviteToOpenGroup() { - Intent intent = new Intent(this, SelectContactsActivity.class); - startActivityForResult(intent, INVITE_CONTACTS); - } - - private void handleDistributionBroadcastEnabled(MenuItem item) { - distributionType = DistributionTypes.BROADCAST; - item.setChecked(true); - - if (threadId != -1) { - new AsyncTask() { - @Override - protected Void doInBackground(Void... params) { - DatabaseFactory.getThreadDatabase(ConversationActivity.this) - .setDistributionType(threadId, DistributionTypes.BROADCAST); - return null; - } - }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); - } - } - - private void handleDistributionConversationEnabled(MenuItem item) { - distributionType = DistributionTypes.CONVERSATION; - item.setChecked(true); - - if (threadId != -1) { - new AsyncTask() { - @Override - protected Void doInBackground(Void... params) { - DatabaseFactory.getThreadDatabase(ConversationActivity.this) - .setDistributionType(threadId, DistributionTypes.CONVERSATION); - return null; - } - }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); - } - } - - private void handleAddAttachment() { - if (attachmentTypeSelector == null) { - attachmentTypeSelector = new AttachmentTypeSelector( - this, - LoaderManager.getInstance(this), - new AttachmentTypeListener(), - keyboardHeight); - } - attachmentTypeSelector.show(this, attachButton); - } - - private void handleSecurityChange(boolean isSecureText, boolean isDefaultSms) { - Log.i(TAG, "handleSecurityChange(" + isSecureText + ", " + isDefaultSms + ")"); - if (isSecurityInitialized && isSecureText == true && isDefaultSms == this.isDefaultSms) { - return; - } - - this.isDefaultSms = isDefaultSms; - this.isSecurityInitialized = true; - - if (recipient == null || attachmentManager == null) { return; } - - /* Loki - We don't support SMS - if (!isSecureText && !isPushGroupConversation()) sendButton.disableTransport(Type.TEXTSECURE); - if (recipient.isPushGroupRecipient()) sendButton.disableTransport(Type.SMS); - if (!recipient.isPushGroupRecipient() && recipient.isForceSmsSelection()) { - sendButton.setDefaultTransport(Type.SMS); - } else { - if (isSecureText || isPushGroupConversation()) sendButton.setDefaultTransport(Type.TEXTSECURE); - else sendButton.setDefaultTransport(Type.SMS); - } - */ - - supportInvalidateOptionsMenu(); - updateInputUI(recipient); - } - - ///// Initializers - - private ListenableFuture initializeDraft() { - final SettableFuture result = new SettableFuture<>(); - - final String draftText = getIntent().getStringExtra(TEXT_EXTRA); - final Uri draftMedia = getIntent().getData(); - final MediaType draftMediaType = MediaType.from(getIntent().getType()); - final List mediaList = getIntent().getParcelableArrayListExtra(MEDIA_EXTRA); - - if (!Util.isEmpty(mediaList)) { - Intent sendIntent = MediaSendActivity.buildEditorIntent(this, mediaList, recipient, draftText); - startActivityForResult(sendIntent, MEDIA_SENDER); - return new SettableFuture<>(false); - } - - if (draftText != null) { - composeText.setText(""); - composeText.append(draftText); - result.set(true); - } - - if (draftMedia != null && draftMediaType != null) { - return setMedia(draftMedia, draftMediaType); - } - - if (draftText == null && draftMedia == null && draftMediaType == null) { - return initializeDraftFromDatabase(); - } else { - updateToggleButtonState(); - result.set(false); - } - - return result; - } - - private void initializeEnabledCheck() { - boolean enabled = !(isPushGroupConversation() && !isActiveGroup()); - inputPanel.setEnabled(enabled); - sendButton.setEnabled(enabled); - attachButton.setEnabled(enabled); - } - - private ListenableFuture initializeDraftFromDatabase() { - SettableFuture future = new SettableFuture<>(); - - new AsyncTask>() { - @Override - protected List doInBackground(Void... params) { - DraftDatabase draftDatabase = DatabaseFactory.getDraftDatabase(ConversationActivity.this); - List results = draftDatabase.getDrafts(threadId); - - draftDatabase.clearDrafts(threadId); - - return results; - } - - @Override - protected void onPostExecute(List drafts) { - if (drafts.isEmpty()) { - future.set(false); - updateToggleButtonState(); - return; - } - - AtomicInteger draftsRemaining = new AtomicInteger(drafts.size()); - AtomicBoolean success = new AtomicBoolean(false); - ListenableFuture.Listener listener = new AssertedSuccessListener() { - @Override - public void onSuccess(Boolean result) { - success.compareAndSet(false, result); - - if (draftsRemaining.decrementAndGet() <= 0) { - future.set(success.get()); - } - } - }; - - for (Draft draft : drafts) { - switch (draft.getType()) { - case Draft.TEXT: - composeText.setText(draft.getValue()); - listener.onSuccess(true); - break; - case Draft.IMAGE: - setMedia(Uri.parse(draft.getValue()), MediaType.IMAGE).addListener(listener); - break; - case Draft.AUDIO: - setMedia(Uri.parse(draft.getValue()), MediaType.AUDIO).addListener(listener); - break; - case Draft.VIDEO: - setMedia(Uri.parse(draft.getValue()), MediaType.VIDEO).addListener(listener); - break; - case Draft.QUOTE: - SettableFuture quoteResult = new SettableFuture<>(); - new QuoteRestorationTask(draft.getValue(), quoteResult).execute(); - quoteResult.addListener(listener); - break; - } - } - - updateToggleButtonState(); - } - }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); - - return future; - } - - private ListenableFuture initializeSecurity(final boolean currentSecureText, - final boolean currentIsDefaultSms) - { - final SettableFuture future = new SettableFuture<>(); - - handleSecurityChange(currentSecureText || isPushGroupConversation(), currentIsDefaultSms); - - new AsyncTask() { - @Override - protected boolean[] doInBackground(Recipient... params) { - // Loki - Override the flag below - boolean signalEnabled = true; // TextSecurePreferences.isPushRegistered(context); - - - return new boolean[] { signalEnabled, false}; - } - - @Override - protected void onPostExecute(boolean[] result) { - if (result[0] != currentSecureText || result[1] != currentIsDefaultSms) { - Log.i(TAG, "onPostExecute() handleSecurityChange: " + result[0] + " , " + result[1]); - handleSecurityChange(result[0], result[1]); - } - future.set(true); - } - }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, recipient); - - return future; - } - - private void initializeViews() { - profilePictureView = findViewById(R.id.profilePictureView); - titleTextView = findViewById(R.id.titleTextView); - buttonToggle = ViewUtil.findById(this, R.id.button_toggle); - sendButton = ViewUtil.findById(this, R.id.send_button); - attachButton = ViewUtil.findById(this, R.id.attach_button); - composeText = ViewUtil.findById(this, R.id.embedded_text_editor); - emojiDrawerStub = ViewUtil.findStubById(this, R.id.emoji_drawer_stub); - unblockButton = ViewUtil.findById(this, R.id.unblock_button); - makeDefaultSmsButton = ViewUtil.findById(this, R.id.make_default_sms_button); - container = ViewUtil.findById(this, R.id.layout_container); - quickAttachmentToggle = ViewUtil.findById(this, R.id.quick_attachment_toggle); - inlineAttachmentToggle = ViewUtil.findById(this, R.id.inline_attachment_container); - inputPanel = ViewUtil.findById(this, R.id.bottom_panel); - searchNav = ViewUtil.findById(this, R.id.conversation_search_nav); - mentionCandidateSelectionViewContainer = ViewUtil.findById(this, R.id.mentionCandidateSelectionViewContainer); - mentionCandidateSelectionView = ViewUtil.findById(this, R.id.userSelectionView); - messageStatusProgressBar = ViewUtil.findById(this, R.id.messageStatusProgressBar); - muteIndicatorImageView = ViewUtil.findById(this, R.id.muteIndicatorImageView); - subtitleTextView = ViewUtil.findById(this, R.id.subtitleTextView); - homeButtonContainer = ViewUtil.findById(this, R.id.homeButtonContainer); - - ImageButton quickCameraToggle = ViewUtil.findById(this, R.id.quick_camera_toggle); - ImageButton inlineAttachmentButton = ViewUtil.findById(this, R.id.inline_attachment_button); - - container.addOnKeyboardShownListener(this); - inputPanel.setListener(this); - inputPanel.setMediaListener(this); - - attachmentTypeSelector = null; - attachmentManager = new AttachmentManager(this, this); - audioRecorder = new AudioRecorder(this); - typingTextWatcher = new TypingStatusTextWatcher(); - mentionTextWatcher = new MentionTextWatcher(); - - SendButtonListener sendButtonListener = new SendButtonListener(); - ComposeKeyPressedListener composeKeyPressedListener = new ComposeKeyPressedListener(); - - composeText.setOnEditorActionListener(sendButtonListener); - composeText.setCursorPositionChangedListener(this); - attachButton.setOnClickListener(new AttachButtonListener()); - attachButton.setOnLongClickListener(new AttachButtonLongClickListener()); - sendButton.setOnClickListener(sendButtonListener); - sendButton.setEnabled(true); - - unblockButton.setOnClickListener(v -> handleUnblock()); - makeDefaultSmsButton.setOnClickListener(v -> handleMakeDefaultSms()); - - composeText.setOnKeyListener(composeKeyPressedListener); - composeText.addTextChangedListener(composeKeyPressedListener); - composeText.setOnEditorActionListener(sendButtonListener); - composeText.setOnClickListener(composeKeyPressedListener); - composeText.setOnFocusChangeListener(composeKeyPressedListener); - - if (getPackageManager().hasSystemFeature(PackageManager.FEATURE_CAMERA) && Camera.getNumberOfCameras() > 0) { - quickCameraToggle.setVisibility(View.VISIBLE); - quickCameraToggle.setOnClickListener(new QuickCameraToggleListener()); - } else { - quickCameraToggle.setVisibility(View.GONE); - } - - searchNav.setEventListener(this); - - inlineAttachmentButton.setOnClickListener(v -> handleAddAttachment()); - - homeButtonContainer.setOnClickListener(v -> onSupportNavigateUp()); - } - - protected void initializeActionBar() { - Toolbar toolbar = findViewById(R.id.toolbar); - setSupportActionBar(toolbar); - - ActionBar supportActionBar = getSupportActionBar(); - if (supportActionBar == null) throw new AssertionError(); - -// supportActionBar.setDisplayHomeAsUpEnabled(true); - supportActionBar.setDisplayShowTitleEnabled(false); - } - - private void initializeResources() { - if (recipient != null) recipient.removeListener(this); - - Address address = getIntent().getParcelableExtra(ADDRESS_EXTRA); - if (address == null) { finish(); return; } - recipient = Recipient.from(this, address, true); - threadId = getIntent().getLongExtra(THREAD_ID_EXTRA, -1); - distributionType = getIntent().getIntExtra(DISTRIBUTION_TYPE_EXTRA, DistributionTypes.DEFAULT); - glideRequests = GlideApp.with(this); - - recipient.addListener(this); - } - - private void initializeLinkPreviewObserver() { - linkPreviewViewModel = ViewModelProviders.of(this, new LinkPreviewViewModel.Factory(new LinkPreviewRepository(this))).get(LinkPreviewViewModel.class); - - if (!TextSecurePreferences.isLinkPreviewsEnabled(this)) { - linkPreviewViewModel.onUserCancel(); - return; - } - - linkPreviewViewModel.getLinkPreviewState().observe(this, previewState -> { - if (previewState == null) return; - - if (previewState.isLoading()) { - Log.d(TAG, "Loading link preview."); - inputPanel.setLinkPreviewLoading(); - } else { - Log.d(TAG, "Setting link preview: " + previewState.getLinkPreview().isPresent()); - inputPanel.setLinkPreview(glideRequests, previewState.getLinkPreview()); - } - - updateToggleButtonState(); - }); - } - - private void initializeSearchObserver() { - searchViewModel = ViewModelProviders.of(this).get(ConversationSearchViewModel.class); - - searchViewModel.getSearchResults().observe(this, result -> { - if (result == null) return; - - if (!result.getResults().isEmpty()) { - MessageResult messageResult = result.getResults().get(result.getPosition()); - fragment.jumpToMessage(messageResult.messageRecipient.getAddress(), messageResult.receivedTimestampMs, searchViewModel::onMissingResult); - } - - searchNav.setData(result.getPosition(), result.getResults().size()); - }); - } - - @Override - public void onSearchMoveUpPressed() { - searchViewModel.onMoveUp(); - } - - @Override - public void onSearchMoveDownPressed() { - searchViewModel.onMoveDown(); - } - - @Override - public void onModified(final Recipient recipient) { - Log.i(TAG, "onModified(" + recipient.getAddress().serialize() + ")"); - Util.runOnMain(() -> { - Log.i(TAG, "onModifiedRun(): " + recipient.getRegistered()); - updateTitleTextView(recipient); - updateProfilePicture(); - updateSubtitleTextView(); - updateInputUI(recipient); - initializeSecurity(true, isDefaultSms); - - if (searchViewItem == null || !searchViewItem.isActionViewExpanded()) { - invalidateOptionsMenu(); - } - }); - } - - @Subscribe(threadMode = ThreadMode.MAIN) - public void onOpenGroupInfoUpdated(OpenGroupUtilities.GroupInfoUpdatedEvent event) { - OpenGroupV2 openGroup = DatabaseFactory.getLokiThreadDatabase(this).getOpenGroupChat(threadId); - if (openGroup != null && - openGroup.getRoom().equals(event.getRoom()) && - openGroup.getServer().equals(event.getUrl())) { - this.updateSubtitleTextView(); - } - } - - //////// Helper Methods - - private void addAttachment(int type) { - linkPreviewViewModel.onUserCancel(); - - Log.i(TAG, "Selected: " + type); - switch (type) { - case AttachmentTypeSelector.ADD_GALLERY: - AttachmentManager.selectGallery(this, MEDIA_SENDER, recipient, composeText.getTextTrimmed()); break; - case AttachmentTypeSelector.ADD_DOCUMENT: - AttachmentManager.selectDocument(this, PICK_DOCUMENT); break; - case AttachmentTypeSelector.ADD_SOUND: - AttachmentManager.selectAudio(this, PICK_AUDIO); break; - case AttachmentTypeSelector.ADD_CONTACT_INFO: - AttachmentManager.selectContactInfo(this, PICK_CONTACT); break; - case AttachmentTypeSelector.ADD_LOCATION: - AttachmentManager.selectLocation(this, PICK_LOCATION); break; - case AttachmentTypeSelector.TAKE_PHOTO: - attachmentManager.capturePhoto(this, TAKE_PHOTO); break; - case AttachmentTypeSelector.ADD_GIF: - boolean hasSeenGIFMetaDataWarning = TextSecurePreferences.hasSeenGIFMetaDataWarning(this); - if (!hasSeenGIFMetaDataWarning) { - AlertDialog.Builder builder = new AlertDialog.Builder(this); - builder.setTitle("Search GIFs?"); - builder.setMessage("You will not have full metadata protection when sending GIFs."); - builder.setPositiveButton("OK", (dialog, which) -> { - AttachmentManager.selectGif(this, PICK_GIF); - dialog.dismiss(); - }); - builder.setNegativeButton("Cancel", (dialog, which) -> dialog.dismiss()); - builder.create().show(); - TextSecurePreferences.setHasSeenGIFMetaDataWarning(this); - } else { - AttachmentManager.selectGif(this, PICK_GIF); - } - break; - } - } - - private ListenableFuture setMedia(@Nullable Uri uri, @NonNull MediaType mediaType) { - return setMedia(uri, mediaType, 0, 0); - } - - private ListenableFuture setMedia(@Nullable Uri uri, @NonNull MediaType mediaType, int width, int height) { - if (uri == null) { - return new SettableFuture<>(false); - } - - if (MediaType.VCARD.equals(mediaType)) { - return new SettableFuture<>(false); - } else if (MediaType.IMAGE.equals(mediaType) || MediaType.GIF.equals(mediaType) || MediaType.VIDEO.equals(mediaType)) { - Media media = new Media(uri, MediaUtil.getMimeType(this, uri), 0, width, height, 0, Optional.absent(), Optional.absent()); - startActivityForResult(MediaSendActivity.buildEditorIntent(ConversationActivity.this, Collections.singletonList(media), recipient, composeText.getTextTrimmed()), MEDIA_SENDER); - return new SettableFuture<>(false); - } else { - return attachmentManager.setMedia(glideRequests, uri, mediaType, MediaConstraints.getPushMediaConstraints(), width, height); - } - } - - private void addAttachmentContactInfo(Uri contactUri) { - ContactAccessor contactDataList = ContactAccessor.getInstance(); - ContactData contactData = contactDataList.getContactData(this, contactUri); - - if (contactData.numbers.size() == 1) composeText.append(contactData.numbers.get(0).number); - else if (contactData.numbers.size() > 1) selectContactInfo(contactData); - } - - private void selectContactInfo(ContactData contactData) { - final CharSequence[] numbers = new CharSequence[contactData.numbers.size()]; - final CharSequence[] numberItems = new CharSequence[contactData.numbers.size()]; - - for (int i = 0; i < contactData.numbers.size(); i++) { - numbers[i] = contactData.numbers.get(i).number; - numberItems[i] = contactData.numbers.get(i).type + ": " + contactData.numbers.get(i).number; - } - - AlertDialog.Builder builder = new AlertDialog.Builder(this); - builder.setIconAttribute(R.attr.conversation_attach_contact_info); - builder.setTitle(R.string.ConversationActivity_select_contact_info); - - builder.setItems(numberItems, (dialog, which) -> composeText.append(numbers[which])); - builder.show(); - } - - private Drafts getDraftsForCurrentState() { - Drafts drafts = new Drafts(); - - if (!org.thoughtcrime.securesms.util.Util.isEmpty(composeText)) { - drafts.add(new Draft(Draft.TEXT, composeText.getTextTrimmed())); - } - - for (Slide slide : attachmentManager.buildSlideDeck().getSlides()) { - if (slide.hasAudio() && slide.getUri() != null) drafts.add(new Draft(Draft.AUDIO, slide.getUri().toString())); - else if (slide.hasVideo() && slide.getUri() != null) drafts.add(new Draft(Draft.VIDEO, slide.getUri().toString())); - else if (slide.hasImage() && slide.getUri() != null) drafts.add(new Draft(Draft.IMAGE, slide.getUri().toString())); - } - - Optional quote = inputPanel.getQuote(); - - if (quote.isPresent()) { - drafts.add(new Draft(Draft.QUOTE, new QuoteId(quote.get().getId(), quote.get().getAuthor()).serialize())); - } - - return drafts; - } - - protected ListenableFuture saveDraft() { - final SettableFuture future = new SettableFuture<>(); - - if (this.recipient == null) { - future.set(threadId); - return future; - } - - final Drafts drafts = getDraftsForCurrentState(); - final long thisThreadId = this.threadId; - final int thisDistributionType = this.distributionType; - - new AsyncTask() { - @Override - protected Long doInBackground(Long... params) { - ThreadDatabase threadDatabase = DatabaseFactory.getThreadDatabase(ConversationActivity.this); - DraftDatabase draftDatabase = DatabaseFactory.getDraftDatabase(ConversationActivity.this); - long threadId = params[0]; - - if (drafts.size() > 0) { - if (threadId == -1) threadId = threadDatabase.getOrCreateThreadIdFor(getRecipient(), thisDistributionType); - - draftDatabase.insertDrafts(threadId, drafts); - threadDatabase.updateSnippet(threadId, drafts.getSnippet(ConversationActivity.this), - drafts.getUriSnippet(), - System.currentTimeMillis(), Types.BASE_DRAFT_TYPE, true); - } else if (threadId > 0) { - threadDatabase.update(threadId, false); - } - - return threadId; - } - - @Override - protected void onPostExecute(Long result) { - future.set(result); - } - - }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, thisThreadId); - - return future; - } - - private void updateInputUI(Recipient recipient) { - if (recipient.isGroupRecipient() && !isActiveGroup()) { - unblockButton.setVisibility(View.GONE); - inputPanel.setVisibility(View.GONE); - makeDefaultSmsButton.setVisibility(View.GONE); - } else if (recipient.isBlocked()) { - unblockButton.setVisibility(View.VISIBLE); - inputPanel.setVisibility(View.GONE); - makeDefaultSmsButton.setVisibility(View.GONE); - } else { - inputPanel.setVisibility(View.VISIBLE); - unblockButton.setVisibility(View.GONE); - makeDefaultSmsButton.setVisibility(View.GONE); - } - } - - private void initializeMediaKeyboardProviders(@NonNull MediaKeyboard mediaKeyboard) { - boolean isSystemEmojiPreferred = TextSecurePreferences.isSystemEmojiPreferred(this); - if (!isSystemEmojiPreferred) { - mediaKeyboard.setProviders(0, new EmojiKeyboardProvider(this, inputPanel)); - } - } - - - private boolean isSingleConversation() { - return getRecipient() != null && !getRecipient().isGroupRecipient(); - } - - private boolean isActiveGroup() { - if (!isGroupConversation()) return false; - - Optional record = DatabaseFactory.getGroupDatabase(this).getGroup(getRecipient().getAddress().toGroupString()); - return record.isPresent() && record.get().isActive(); - } - - @SuppressWarnings("SimplifiableIfStatement") - private boolean isSelfConversation() { - if (!TextSecurePreferences.isPushRegistered(this)) return false; - if (recipient.isGroupRecipient()) return false; - - return Util.isOwnNumber(this, recipient.getAddress().serialize()); - } - - private boolean isGroupConversation() { - return getRecipient() != null && getRecipient().isGroupRecipient(); - } - - private boolean isPushGroupConversation() { - return getRecipient() != null && getRecipient().isPushGroupRecipient(); - } - - protected Recipient getRecipient() { - return this.recipient; - } - - protected long getThreadId() { - return this.threadId; - } - - private String getMessage() throws InvalidMessageException { - String result = composeText.getTextTrimmed(); - if (result.length() < 1 && !attachmentManager.isAttachmentPresent()) throw new InvalidMessageException(); - for (Mention mention : mentions) { - try { - int startIndex = result.indexOf("@" + mention.getDisplayName()); - int endIndex = startIndex + mention.getDisplayName().length() + 1; // + 1 to include the @ - result = result.substring(0, startIndex) + "@" + mention.getPublicKey() + result.substring(endIndex); - } catch (Exception exception) { - Log.d("Loki", "Couldn't process mention due to error: " + exception.toString() + "."); - } - } - return result; - } - - private Pair> getSplitMessage(String rawText, int maxPrimaryMessageSize) { - String bodyText = rawText; - Optional textSlide = Optional.absent(); - - if (bodyText.length() > maxPrimaryMessageSize) { - bodyText = rawText.substring(0, maxPrimaryMessageSize); - - byte[] textData = rawText.getBytes(); - String timestamp = new SimpleDateFormat("yyyy-MM-dd-HHmmss", Locale.US).format(new Date()); - String filename = String.format("signal-%s.txt", timestamp); - Uri textUri = BlobProvider.getInstance() - .forData(textData) - .withMimeType(MediaTypes.LONG_TEXT) - .withFileName(filename) - .createForSingleSessionInMemory(); - - textSlide = Optional.of(new TextSlide(this, textUri, filename, textData.length)); - } - - return new Pair<>(bodyText, textSlide); - } - - private void markThreadAsRead() { - Recipient recipient = this.recipient; - new AsyncTask() { - @Override - protected Void doInBackground(Long... params) { - Context context = ConversationActivity.this; - List messageIds = DatabaseFactory.getThreadDatabase(context).setRead(params[0], false); - - if (!SessionMetaProtocol.shouldSendReadReceipt(recipient.getAddress())) { - for (MarkedMessageInfo messageInfo : messageIds) { - MarkReadReceiver.scheduleDeletion(context, messageInfo.getExpirationInfo()); - } - } else { - MarkReadReceiver.process(context, messageIds); - } - ApplicationContext.getInstance(context).messageNotifier.updateNotification(context); - return null; - } - }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, threadId); - } - - private void markLastSeen() { - new AsyncTask() { - @Override - protected Void doInBackground(Long... params) { - DatabaseFactory.getThreadDatabase(ConversationActivity.this).setLastSeen(params[0]); - return null; - } - }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, threadId); - } - - protected void sendComplete(long threadId) { - boolean refreshFragment = (threadId != this.threadId); - this.threadId = threadId; - - if (fragment == null || !fragment.isVisible() || isFinishing()) { - return; - } - - fragment.setLastSeen(0); - - if (refreshFragment) { - fragment.reload(recipient, threadId); - ApplicationContext.getInstance(this).messageNotifier.setVisibleThread(threadId); - } - - fragment.scrollToBottom(); - attachmentManager.cleanup(); - - updateLinkPreviewState(); - } - - private void sendMessage() { - if (inputPanel.isRecordingInLockedMode()) { - inputPanel.releaseRecordingLock(); - return; - } - - try { - Recipient recipient = getRecipient(); - - if (recipient == null) { - throw new RecipientFormattingException("Badly formatted"); - } - - String message = getMessage(); - boolean initiating = threadId == -1; - boolean needsSplit = message.length() > characterCalculator.calculateCharacters(message).maxPrimaryMessageSize; - boolean isMediaMessage = attachmentManager.isAttachmentPresent() || -// recipient.isGroupRecipient() || - inputPanel.getQuote().isPresent() || - linkPreviewViewModel.hasLinkPreview() || - LinkPreviewUtil.isValidMediaUrl(message) || // Loki - Send GIFs as media messages - needsSplit; - - if (isMediaMessage) { - sendMediaMessage(initiating); - } else { - sendTextMessage(initiating); - } - } catch (RecipientFormattingException ex) { - Log.w(TAG, ex); - } catch (InvalidMessageException ex) { - Log.w(TAG, ex); - } - - if (messageStatus == null && !isGroupConversation() && !(TextSecurePreferences.getLocalNumber(this).equals(recipient.getAddress().serialize()))) { - messageStatus = "calculatingPoW"; - updateSubtitleTextView(); - updateMessageStatusProgressBar(); - } - } - - private void sendMediaMessage(boolean initiating) - throws InvalidMessageException - { - Log.i(TAG, "Sending media message..."); - sendMediaMessage(getMessage(), attachmentManager.buildSlideDeck(), inputPanel.getQuote().orNull(), linkPreviewViewModel.getActiveLinkPreview(), initiating); - } - - private ListenableFuture sendMediaMessage(String body, - SlideDeck slideDeck, - QuoteModel quote, - Optional linkPreview, - final boolean initiating) - { - - Pair> splitMessage = getSplitMessage(body, characterCalculator.calculateCharacters(body).maxPrimaryMessageSize); - body = splitMessage.first; - - if (splitMessage.second.isPresent()) { - slideDeck.addSlide(splitMessage.second.get()); - } - - List attachments = slideDeck.asAttachments(); - - VisibleMessage message = new VisibleMessage(); - message.setSentTimestamp(System.currentTimeMillis()); - message.setText(body); - OutgoingMediaMessage outgoingMessageCandidate = OutgoingMediaMessage.from(message, recipient, attachments, quote, linkPreview.orNull()); - - final SettableFuture future = new SettableFuture<>(); - final Context context = getApplicationContext(); - - final OutgoingMediaMessage outgoingMessage; - - outgoingMessage = new OutgoingSecureMediaMessage(outgoingMessageCandidate); - ApplicationContext.getInstance(context).getTypingStatusSender().onTypingStopped(threadId); - - inputPanel.clearQuote(); - attachmentManager.clear(glideRequests, false); - silentlySetComposeText(""); - - final long id = fragment.stageOutgoingMessage(outgoingMessage); - - if (initiating) { - DatabaseFactory.getRecipientDatabase(context).setProfileSharing(recipient, true); - } - - try { - long allocatedThreadId = getAllocatedThreadId(context); - message.setId(DatabaseFactory.getMmsDatabase(context).insertMessageOutbox(outgoingMessage, allocatedThreadId, false, ()->fragment.releaseOutgoingMessage(id))); - MessageSender.send(message, recipient.getAddress(), attachments, quote, linkPreview.orNull()); - sendComplete(allocatedThreadId); - } catch (MmsException e) { - Log.w(TAG, e); - sendComplete(threadId); - } - future.set(null); - - return future; - } - - private void sendTextMessage(final boolean initiating) - throws InvalidMessageException - { - final Context context = getApplicationContext(); - final String messageBody = getMessage(); - - VisibleMessage message = new VisibleMessage(); - message.setSentTimestamp(System.currentTimeMillis()); - message.setText(messageBody); - OutgoingTextMessage outgoingTextMessage = OutgoingTextMessage.from(message, recipient); - ApplicationContext.getInstance(context).getTypingStatusSender().onTypingStopped(threadId); - - silentlySetComposeText(""); - final long id = fragment.stageOutgoingMessage(outgoingTextMessage); - - if (initiating) { - DatabaseFactory.getRecipientDatabase(context).setProfileSharing(recipient, true); - } - - long allocatedThreadId = getAllocatedThreadId(context); - message.setId(DatabaseFactory.getSmsDatabase(context).insertMessageOutbox(allocatedThreadId, outgoingTextMessage, false, message.getSentTimestamp(), ()->fragment.releaseOutgoingMessage(id))); - MessageSender.send(message, recipient.getAddress()); - - sendComplete(allocatedThreadId); - } - - private void sendOpenGroupInvitations(String[] contactIDs) { - final Context context = getApplicationContext(); - OpenGroupV2 openGroup = DatabaseFactory.getLokiThreadDatabase(context).getOpenGroupChat(threadId); - for (String contactID : contactIDs) { - Recipient recipient = Recipient.from(context, Address.fromSerialized(contactID), true); - VisibleMessage message = new VisibleMessage(); - message.setSentTimestamp(System.currentTimeMillis()); - OpenGroupInvitation openGroupInvitationMessage = new OpenGroupInvitation(); - openGroupInvitationMessage.setName(openGroup.getName()); - openGroupInvitationMessage.setUrl(openGroup.getJoinURL()); - message.setOpenGroupInvitation(openGroupInvitationMessage); - OutgoingTextMessage outgoingTextMessage = OutgoingTextMessage.fromOpenGroupInvitation(openGroupInvitationMessage, recipient, message.getSentTimestamp()); - DatabaseFactory.getSmsDatabase(context).insertMessageOutbox(-1, outgoingTextMessage, message.getSentTimestamp()); - MessageSender.send(message, recipient.getAddress()); - } - } - - private void updateToggleButtonState() { - if (inputPanel.isRecordingInLockedMode()) { - buttonToggle.display(sendButton); - quickAttachmentToggle.show(); - inlineAttachmentToggle.hide(); - return; - } - - if (composeText.getText().length() == 0 && !attachmentManager.isAttachmentPresent()) { - buttonToggle.display(attachButton); - quickAttachmentToggle.show(); - inlineAttachmentToggle.hide(); - } else { - buttonToggle.display(sendButton); - quickAttachmentToggle.hide(); - - if (!attachmentManager.isAttachmentPresent() && !linkPreviewViewModel.hasLinkPreview()) { - inlineAttachmentToggle.show(); - } else { - inlineAttachmentToggle.hide(); - } - } - } - - private void updateLinkPreviewState() { - if (TextSecurePreferences.isLinkPreviewsEnabled(this) && !attachmentManager.isAttachmentPresent()) { - linkPreviewViewModel.onEnabled(); - linkPreviewViewModel.onTextChanged(this, composeText.getTextTrimmed(), composeText.getSelectionStart(), composeText.getSelectionEnd()); - } else { - linkPreviewViewModel.onUserCancel(); - } - } - - @Override - public void onRecorderPermissionRequired() { - Permissions.with(this) - .request(Manifest.permission.RECORD_AUDIO) - .withRationaleDialog(getString(R.string.ConversationActivity_to_send_audio_messages_allow_signal_access_to_your_microphone), R.drawable.ic_baseline_mic_48) - .withPermanentDenialDialog(getString(R.string.ConversationActivity_signal_requires_the_microphone_permission_in_order_to_send_audio_messages)) - .execute(); - } - - @Override - public void onRecorderStarted() { - Vibrator vibrator = ServiceUtil.getVibrator(this); - vibrator.vibrate(20); - - getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); - - audioRecorder.startRecording(); - - audioHandler = new Handler(); - stopRecordingTask = () -> inputPanel.onRecordReleased(); - audioHandler.postDelayed(stopRecordingTask, 60000); - } - - @Override - public void onRecorderLocked() { - updateToggleButtonState(); - } - - @Override - public void onRecorderFinished() { - if (audioHandler != null && stopRecordingTask != null) { - audioHandler.removeCallbacks(stopRecordingTask); - } - updateToggleButtonState(); - Vibrator vibrator = ServiceUtil.getVibrator(this); - vibrator.vibrate(20); - - getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); - - ListenableFuture> future = audioRecorder.stopRecording(); - future.addListener(new ListenableFuture.Listener>() { - @Override - public void onSuccess(final @NonNull Pair result) { - int subscriptionId = -1; - long expiresIn = recipient.getExpireMessages() * 1000L; - boolean initiating = threadId == -1; - AudioSlide audioSlide = new AudioSlide(ConversationActivity.this, result.first, result.second, MediaTypes.AUDIO_AAC, true); - SlideDeck slideDeck = new SlideDeck(); - slideDeck.addSlide(audioSlide); - - sendMediaMessage("", slideDeck, inputPanel.getQuote().orNull(), Optional.absent(), initiating).addListener(new AssertedSuccessListener() { - @Override - public void onSuccess(Void nothing) { - new AsyncTask() { - @Override - protected Void doInBackground(Void... params) { - BlobProvider.getInstance().delete(ConversationActivity.this, result.first); - return null; - } - }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); - } - }); - } - - @Override - public void onFailure(ExecutionException e) { - Toast.makeText(ConversationActivity.this, R.string.ConversationActivity_unable_to_record_audio, Toast.LENGTH_LONG).show(); - } - }); - } - - @Override - public void onRecorderCanceled() { - if (audioHandler != null && stopRecordingTask != null) { - audioHandler.removeCallbacks(stopRecordingTask); - } - updateToggleButtonState(); - Vibrator vibrator = ServiceUtil.getVibrator(this); - vibrator.vibrate(50); - - getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); - - ListenableFuture> future = audioRecorder.stopRecording(); - future.addListener(new ListenableFuture.Listener>() { - @Override - public void onSuccess(final Pair result) { - new AsyncTask() { - @Override - protected Void doInBackground(Void... params) { - BlobProvider.getInstance().delete(ConversationActivity.this, result.first); - return null; - } - }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); - } - - @Override - public void onFailure(ExecutionException e) {} - }); - } - - @Override - public void onEmojiToggle() { - if (!emojiDrawerStub.resolved()) { - initializeMediaKeyboardProviders(emojiDrawerStub.get()); - - inputPanel.setMediaKeyboard(emojiDrawerStub.get()); - } - - if (container.getCurrentInput() == emojiDrawerStub.get()) { - container.showSoftkey(composeText); - } else { - container.show(composeText, emojiDrawerStub.get()); - } - } - - @Override - public void onLinkPreviewCanceled() { - linkPreviewViewModel.onUserCancel(); - } - - @Override - public void onMediaSelected(@NonNull Uri uri, String contentType) { - if (!TextUtils.isEmpty(contentType) && contentType.trim().equals("image/gif")) { - setMedia(uri, MediaType.GIF); - } else if (MediaUtil.isImageType(contentType)) { - setMedia(uri, MediaType.IMAGE); - } else if (MediaUtil.isVideoType(contentType)) { - setMedia(uri, MediaType.VIDEO); - } else if (MediaUtil.isAudioType(contentType)) { - setMedia(uri, MediaType.AUDIO); - } - } - - @Override - public void onCursorPositionChanged(int start, int end) { - linkPreviewViewModel.onTextChanged(this, composeText.getTextTrimmed(), start, end); - } - - private void silentlySetComposeText(String text) { - typingTextWatcher.setEnabled(false); - composeText.setText(text); - if (text.isEmpty()) { resetMentions(); } - typingTextWatcher.setEnabled(true); - } - - // Listeners - - private class AttachmentTypeListener implements AttachmentTypeSelector.AttachmentClickedListener { - @Override - public void onClick(int type) { - addAttachment(type); - } - - @Override - public void onQuickAttachment(Uri uri, String mimeType, String bucketId, long dateTaken, int width, int height, long size) { - linkPreviewViewModel.onUserCancel(); - Media media = new Media(uri, mimeType, dateTaken, width, height, size, Optional.of(Media.ALL_MEDIA_BUCKET_ID), Optional.absent()); - startActivityForResult(MediaSendActivity.buildEditorIntent(ConversationActivity.this, Collections.singletonList(media), recipient, composeText.getTextTrimmed()), MEDIA_SENDER); - } - } - - private class QuickCameraToggleListener implements OnClickListener { - @Override - public void onClick(View v) { - Permissions.with(ConversationActivity.this) - .request(Manifest.permission.CAMERA) - .withRationaleDialog(getString(R.string.ConversationActivity_to_capture_photos_and_video_allow_signal_access_to_the_camera), R.drawable.ic_baseline_photo_camera_48) - .withPermanentDenialDialog(getString(R.string.ConversationActivity_signal_needs_the_camera_permission_to_take_photos_or_video)) - .onAllGranted(() -> { - composeText.clearFocus(); - startActivityForResult(MediaSendActivity.buildCameraIntent(ConversationActivity.this, recipient), MEDIA_SENDER); - overridePendingTransition(R.anim.camera_slide_from_bottom, R.anim.stationary); - }) - .onAnyDenied(() -> Toast.makeText(ConversationActivity.this, R.string.ConversationActivity_signal_needs_camera_permissions_to_take_photos_or_video, Toast.LENGTH_LONG).show()) - .execute(); - } - } - - private class SendButtonListener implements OnClickListener, TextView.OnEditorActionListener { - @Override - public void onClick(View v) { - sendMessage(); - } - - @Override - public boolean onEditorAction(TextView v, int actionId, KeyEvent event) { - if (actionId == EditorInfo.IME_ACTION_SEND) { - sendButton.performClick(); - return true; - } - return false; - } - } - - private class AttachButtonListener implements OnClickListener { - @Override - public void onClick(View v) { - handleAddAttachment(); - } - } - - private class AttachButtonLongClickListener implements View.OnLongClickListener { - @Override - public boolean onLongClick(View v) { - return sendButton.performLongClick(); - } - } - - private class ComposeKeyPressedListener implements OnKeyListener, OnClickListener, TextWatcher, OnFocusChangeListener { - - int beforeLength; - - @Override - public boolean onKey(View v, int keyCode, KeyEvent event) { - if (event.getAction() == KeyEvent.ACTION_DOWN) { - if (keyCode == KeyEvent.KEYCODE_ENTER) { - if (TextSecurePreferences.isEnterSendsEnabled(ConversationActivity.this)) { - sendButton.dispatchKeyEvent(new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_ENTER)); - sendButton.dispatchKeyEvent(new KeyEvent(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_ENTER)); - return true; - } - } - } - return false; - } - - @Override - public void onClick(View v) { - container.showSoftkey(composeText); - } - - @Override - public void beforeTextChanged(CharSequence s, int start, int count,int after) { - beforeLength = composeText.getTextTrimmed().length(); - } - - @Override - public void afterTextChanged(Editable s) { - if (composeText.getTextTrimmed().length() == 0 || beforeLength == 0) { - composeText.postDelayed(ConversationActivity.this::updateToggleButtonState, 50); - } - } - - @Override - public void onTextChanged(CharSequence s, int start, int before,int count) {} - - @Override - public void onFocusChange(View v, boolean hasFocus) {} - } - - private class TypingStatusTextWatcher extends SimpleTextWatcher { - private boolean enabled = true; - - @Override - public void onTextChanged(String text) { - if (enabled && threadId > 0) { - ApplicationContext.getInstance(ConversationActivity.this).getTypingStatusSender().onTypingStarted(threadId); - } - } - - public void setEnabled(boolean enabled) { - this.enabled = enabled; - } - } - - private class MentionTextWatcher extends SimpleTextWatcher { - - @Override - public void onTextChanged(String text) { - boolean isBackspace = text.length() < oldText.length(); - if (isBackspace) { - currentMentionStartIndex = -1; - mentionCandidateSelectionView.hide(); - mentionCandidateSelectionViewContainer.setVisibility(View.GONE); - ArrayList mentionsToRemove = new ArrayList<>(); - for (Mention mention : mentions) { - if (!text.contains(mention.getDisplayName())) { - mentionsToRemove.add(mention); - } - } - mentions.removeAll(mentionsToRemove); - } - if (text.length() > 0) { - if (currentMentionStartIndex > text.length()) { - resetMentions(); // Should never occur - } - int lastCharacterIndex = text.length() - 1; - char lastCharacter = text.charAt(lastCharacterIndex); - char secondToLastCharacter = ' '; - if (lastCharacterIndex > 0) { - secondToLastCharacter = text.charAt(lastCharacterIndex - 1); - } - if (lastCharacter == '@' && Character.isWhitespace(secondToLastCharacter)) { - List mentionCandidates = MentionsManager.INSTANCE.getMentionCandidates("", threadId, recipient.isOpenGroupRecipient()); - currentMentionStartIndex = lastCharacterIndex; - mentionCandidateSelectionViewContainer.setVisibility(View.VISIBLE); - mentionCandidateSelectionView.show(mentionCandidates, threadId); - } else if (Character.isWhitespace(lastCharacter)) { - currentMentionStartIndex = -1; - mentionCandidateSelectionView.hide(); - mentionCandidateSelectionViewContainer.setVisibility(View.GONE); - } else { - if (currentMentionStartIndex != -1) { - String query = text.substring(currentMentionStartIndex + 1); // + 1 to get rid of the @ - List mentionCandidates = MentionsManager.INSTANCE.getMentionCandidates(query, threadId, recipient.isOpenGroupRecipient()); - mentionCandidateSelectionViewContainer.setVisibility(View.VISIBLE); - mentionCandidateSelectionView.show(mentionCandidates, threadId); - } - } - } - ConversationActivity.this.oldText = text; - } - } - - private void resetMentions() { - oldText = ""; - currentMentionStartIndex = -1; - mentions.clear(); - } - - @Override - public void setThreadId(long threadId) { - this.threadId = threadId; - } - - @Override - public void handleReplyMessage(MessageRecord messageRecord) { - if (recipient.isGroupRecipient() && !isActiveGroup()) { return; } - - Recipient author; - - if (messageRecord.isOutgoing()) { - author = Recipient.from(this, Address.fromSerialized(TextSecurePreferences.getLocalNumber(this)), true); - } else { - author = messageRecord.getIndividualRecipient(); - } - - if (messageRecord.isMms() && !((MmsMessageRecord) messageRecord).getSharedContacts().isEmpty()) { - Contact contact = ((MmsMessageRecord) messageRecord).getSharedContacts().get(0); - String displayName = ContactUtil.getDisplayName(contact); - String body = getString(R.string.ConversationActivity_quoted_contact_message, EmojiStrings.BUST_IN_SILHOUETTE, displayName); - SlideDeck slideDeck = new SlideDeck(); - - if (contact.getAvatarAttachment() != null) { - slideDeck.addSlide(MediaUtil.getSlideForAttachment(this, contact.getAvatarAttachment())); - } - - inputPanel.setQuote(GlideApp.with(this), - messageRecord.getDateSent(), - author, - body, - slideDeck, - recipient, - threadId); - - } else if (messageRecord.isMms() && !((MmsMessageRecord) messageRecord).getLinkPreviews().isEmpty()) { - LinkPreview linkPreview = ((MmsMessageRecord) messageRecord).getLinkPreviews().get(0); - SlideDeck slideDeck = new SlideDeck(); - - if (linkPreview.getThumbnail().isPresent()) { - slideDeck.addSlide(MediaUtil.getSlideForAttachment(this, linkPreview.getThumbnail().get())); - } - - inputPanel.setQuote(GlideApp.with(this), - messageRecord.getDateSent(), - author, - messageRecord.getBody(), - slideDeck, - recipient, - threadId); - } else { - inputPanel.setQuote(GlideApp.with(this), - messageRecord.getDateSent(), - author, - messageRecord.getBody(), - messageRecord.isMms() ? ((MmsMessageRecord) messageRecord).getSlideDeck() : new SlideDeck(), - recipient, - threadId); - } - } - - @Override - public void onMessageActionToolbarOpened() { - searchViewItem.collapseActionView(); - } - - @Override - public void onForwardClicked() { - inputPanel.clearQuote(); - } - - @Override - public void onAttachmentChanged() { - handleSecurityChange(true, isDefaultSms); - updateToggleButtonState(); - updateLinkPreviewState(); - } - - private class QuoteRestorationTask extends AsyncTask { - - private final String serialized; - private final SettableFuture future; - - QuoteRestorationTask(@NonNull String serialized, @NonNull SettableFuture future) { - this.serialized = serialized; - this.future = future; - } - - @Override - protected MessageRecord doInBackground(Void... voids) { - QuoteId quoteId = QuoteId.deserialize(serialized); - - if (quoteId != null) { - return DatabaseFactory.getMmsSmsDatabase(getApplicationContext()).getMessageFor(quoteId.getId(), quoteId.getAuthor()); - } - - return null; - } - - @Override - protected void onPostExecute(MessageRecord messageRecord) { - if (messageRecord != null) { - handleReplyMessage(messageRecord); - future.set(true); - } else { - Log.e(TAG, "Failed to restore a quote from a draft. No matching message record."); - future.set(false); - } - } - } - - // region Loki - private long getAllocatedThreadId(Context context) { - long allocatedThreadId; - if (threadId == -1) { - allocatedThreadId = DatabaseFactory.getThreadDatabase(context).getOrCreateThreadIdFor(recipient); - } else { - allocatedThreadId = threadId; - } - return allocatedThreadId; - } - - private void updateTitleTextView(Recipient recipient) { - String userPublicKey = TextSecurePreferences.getLocalNumber(this); - if (recipient == null) { - titleTextView.setText(R.string.ConversationActivity_compose); - } else if (recipient.getAddress().toString().toLowerCase().equals(userPublicKey)) { - titleTextView.setText(R.string.note_to_self); - } else { - String displayName = recipient.getName(); // Uses the Contact API internally - boolean hasName = (displayName != null); - titleTextView.setText(hasName ? displayName : recipient.getAddress().toString()); - } - } - - private void updateProfilePicture() { - try { - profilePictureView.glide = GlideApp.with(this); - profilePictureView.update(recipient, threadId); - } catch (Exception exception) { - // Do nothing - } - } - - private void updateSubtitleTextView() { - muteIndicatorImageView.setVisibility(View.GONE); - subtitleTextView.setVisibility(View.VISIBLE); - if (recipient.isMuted()) { - muteIndicatorImageView.setVisibility(View.VISIBLE); - subtitleTextView.setText(getString(R.string.ConversationActivity_muted_until_date,DateUtils.getFormattedDateTime(recipient.mutedUntil, "EEE, MMM d, yyyy HH:mm", Locale.getDefault()))); - } else if (recipient.isGroupRecipient() && recipient.getName() != null && !recipient.getName().equals("Session Updates") && !recipient.getName().equals("Loki News")) { - OpenGroupV2 openGroup = DatabaseFactory.getLokiThreadDatabase(this).getOpenGroupChat(threadId); - if (openGroup != null) { - Integer userCount = DatabaseFactory.getLokiAPIDatabase(this).getUserCount(openGroup.getRoom(),openGroup.getServer()); - if (userCount == null) { userCount = 0; } - subtitleTextView.setText(getString(R.string.ConversationActivity_member_count,userCount)); - } else if (PublicKeyValidation.isValid(recipient.getAddress().toString())) { - subtitleTextView.setText(recipient.getAddress().toString()); - } else { - subtitleTextView.setVisibility(View.GONE); - } - } else { - subtitleTextView.setVisibility(View.GONE); - } - titleTextView.setTextSize(TypedValue.COMPLEX_UNIT_PX, getResources().getDimension((subtitleTextView.getVisibility() == View.GONE) ? R.dimen.very_large_font_size : R.dimen.large_font_size)); - } - - private void setMessageStatusProgressAnimatedIfPossible(int progress) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { - messageStatusProgressBar.setProgress(progress, true); - } else { - messageStatusProgressBar.setProgress(progress); - } - } - - private void updateMessageStatusProgressBar() { - if (messageStatus != null) { - messageStatusProgressBar.setAlpha(1.0f); - switch (messageStatus) { - case "calculatingPoW": setMessageStatusProgressAnimatedIfPossible(25); break; - case "contactingNetwork": setMessageStatusProgressAnimatedIfPossible(50); break; - case "sendingMessage": setMessageStatusProgressAnimatedIfPossible(75); break; - case "messageSent": - setMessageStatusProgressAnimatedIfPossible(100); - new Handler().postDelayed(() -> messageStatusProgressBar.animate().alpha(0).setDuration(250).start(), 250); - new Handler().postDelayed(() -> messageStatusProgressBar.setProgress(0), 500); - break; - case "messageFailed": - messageStatusProgressBar.animate().alpha(0).setDuration(250).start(); - new Handler().postDelayed(() -> messageStatusProgressBar.setProgress(0), 250); - break; - } - } - } - - private void handleMessageStatusChanged(String newMessageStatus, long timestamp) { - if (timestamp == 0 || (TextSecurePreferences.getLocalNumber(this).equals(recipient.getAddress().serialize())) ) { return; } - updateForNewMessageStatusIfNeeded(newMessageStatus, timestamp); - if (newMessageStatus.equals("messageFailed") || newMessageStatus.equals("messageSent")) { - new Handler().postDelayed(() -> clearMessageStatusIfNeeded(timestamp), 1000); - } - } - - private int precedence(String messageStatus) { - if (messageStatus != null) { - switch (messageStatus) { - case "calculatingPoW": return 0; - case "contactingNetwork": return 1; - case "sendingMessage": return 2; - case "messageSent": return 3; - case "messageFailed": return 4; - default: return -1; - } - } else { - return -1; - } - } - - private void updateForNewMessageStatusIfNeeded(String newMessageStatus, long timestamp) { - if (!DatabaseFactory.getSmsDatabase(this).isOutgoingMessage(timestamp) && !DatabaseFactory.getMmsDatabase(this).isOutgoingMessage(timestamp)) { return; } - if (precedence(newMessageStatus) > precedence(messageStatus)) { - messageStatus = newMessageStatus; - updateSubtitleTextView(); - updateMessageStatusProgressBar(); - } - } - - private void clearMessageStatusIfNeeded(long timestamp) { - if (!DatabaseFactory.getSmsDatabase(this).isOutgoingMessage(timestamp) && !DatabaseFactory.getMmsDatabase(this).isOutgoingMessage(timestamp)) { return; } - messageStatus = null; - updateSubtitleTextView(); - updateMessageStatusProgressBar(); - } - // endregion -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationAdapter.java b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationAdapter.java deleted file mode 100644 index 8c7522a3f0..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationAdapter.java +++ /dev/null @@ -1,532 +0,0 @@ -/* - * Copyright (C) 2011 Whisper Systems - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ -package org.thoughtcrime.securesms.conversation; - -import android.content.Context; -import android.database.Cursor; -import androidx.annotation.LayoutRes; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.annotation.VisibleForTesting; -import androidx.recyclerview.widget.RecyclerView; - -import android.util.SparseArray; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.TextView; - -import com.annimon.stream.Stream; - -import org.thoughtcrime.securesms.BindableConversationItem; -import org.thoughtcrime.securesms.conversation.ConversationAdapter.HeaderViewHolder; -import org.thoughtcrime.securesms.database.DatabaseFactory; -import org.thoughtcrime.securesms.database.FastCursorRecyclerViewAdapter; -import org.thoughtcrime.securesms.database.MmsSmsColumns; -import org.thoughtcrime.securesms.database.MmsSmsDatabase; -import org.thoughtcrime.securesms.database.model.MessageRecord; -import org.thoughtcrime.securesms.database.model.MmsMessageRecord; -import org.session.libsignal.utilities.Log; -import org.thoughtcrime.securesms.mms.GlideRequests; -import org.thoughtcrime.securesms.mms.SlideDeck; -import org.session.libsession.utilities.recipients.Recipient; -import org.thoughtcrime.securesms.util.DateUtils; -import org.thoughtcrime.securesms.util.LRUCache; -import org.thoughtcrime.securesms.util.StickyHeaderDecoration; -import org.session.libsignal.utilities.guava.Optional; - -import org.session.libsession.messaging.sending_receiving.attachments.DatabaseAttachment; -import org.session.libsession.utilities.Conversions; -import org.session.libsession.utilities.ViewUtil; -import org.session.libsession.utilities.Util; - -import java.lang.ref.SoftReference; -import java.security.MessageDigest; -import java.security.NoSuchAlgorithmException; -import java.util.Calendar; -import java.util.Collections; -import java.util.Date; -import java.util.HashSet; -import java.util.List; -import java.util.Locale; -import java.util.Map; -import java.util.Set; - -import network.loki.messenger.R; - -/** - * A cursor adapter for a conversation thread. Ultimately - * used by ComposeMessageActivity to display a conversation - * thread in a ListActivity. - * - * @author Moxie Marlinspike - * - */ -public class ConversationAdapter - extends FastCursorRecyclerViewAdapter - implements StickyHeaderDecoration.StickyHeaderAdapter -{ - - private static final int MAX_CACHE_SIZE = 1000; - private static final String TAG = ConversationAdapter.class.getSimpleName(); - private final Map> messageRecordCache = - Collections.synchronizedMap(new LRUCache>(MAX_CACHE_SIZE)); - private final SparseArray positionToCacheRef = new SparseArray<>(); - - private static final int MESSAGE_TYPE_OUTGOING = 0; - private static final int MESSAGE_TYPE_INCOMING = 1; - private static final int MESSAGE_TYPE_UPDATE = 2; - private static final int MESSAGE_TYPE_AUDIO_OUTGOING = 3; - private static final int MESSAGE_TYPE_AUDIO_INCOMING = 4; - private static final int MESSAGE_TYPE_THUMBNAIL_OUTGOING = 5; - private static final int MESSAGE_TYPE_THUMBNAIL_INCOMING = 6; - private static final int MESSAGE_TYPE_DOCUMENT_OUTGOING = 7; - private static final int MESSAGE_TYPE_DOCUMENT_INCOMING = 8; - private static final int MESSAGE_TYPE_INVITATION_OUTGOING = 9; - private static final int MESSAGE_TYPE_INVITATION_INCOMING = 10; - - private final Set batchSelected = Collections.synchronizedSet(new HashSet()); - - private final @Nullable ItemClickListener clickListener; - private final @NonNull - GlideRequests glideRequests; - private final @NonNull Locale locale; - private final @NonNull Recipient recipient; - private final @NonNull MmsSmsDatabase db; - private final @NonNull LayoutInflater inflater; - private final @NonNull Calendar calendar; - private final @NonNull MessageDigest digest; - - private MessageRecord recordToPulseHighlight; - private String searchQuery; - - protected static class ViewHolder extends RecyclerView.ViewHolder { - public ViewHolder(final @NonNull V itemView) { - super(itemView); - } - - @SuppressWarnings("unchecked") - public V getView() { - return (V)itemView; - } - } - - - static class HeaderViewHolder extends RecyclerView.ViewHolder { - TextView textView; - - HeaderViewHolder(View itemView) { - super(itemView); - textView = ViewUtil.findById(itemView, R.id.text); - } - - HeaderViewHolder(TextView textView) { - super(textView); - this.textView = textView; - } - - public void setText(CharSequence text) { - textView.setText(text); - } - } - - - interface ItemClickListener extends BindableConversationItem.EventListener { - void onItemClick(MessageRecord item); - void onItemLongClick(MessageRecord item); - } - - @SuppressWarnings("ConstantConditions") - @VisibleForTesting - ConversationAdapter(Context context, Cursor cursor) { - super(context, cursor); - try { - this.glideRequests = null; - this.locale = null; - this.clickListener = null; - this.recipient = null; - this.inflater = null; - this.db = null; - this.calendar = null; - this.digest = MessageDigest.getInstance("SHA1"); - } catch (NoSuchAlgorithmException nsae) { - throw new AssertionError("SHA1 isn't supported!"); - } - } - - public ConversationAdapter(@NonNull Context context, - @NonNull GlideRequests glideRequests, - @NonNull Locale locale, - @Nullable ItemClickListener clickListener, - @Nullable Cursor cursor, - @NonNull Recipient recipient) - { - super(context, cursor); - - try { - this.glideRequests = glideRequests; - this.locale = locale; - this.clickListener = clickListener; - this.recipient = recipient; - this.inflater = LayoutInflater.from(context); - this.db = DatabaseFactory.getMmsSmsDatabase(context); - this.calendar = Calendar.getInstance(); - this.digest = MessageDigest.getInstance("SHA1"); - - setHasStableIds(true); - } catch (NoSuchAlgorithmException nsae) { - throw new AssertionError("SHA1 isn't supported!"); - } - } - - @Override - public void changeCursor(Cursor cursor) { - messageRecordCache.clear(); - positionToCacheRef.clear(); - super.cleanFastRecords(); - super.changeCursor(cursor); - } - - @Override - protected void onBindItemViewHolder(ViewHolder viewHolder, @NonNull MessageRecord messageRecord) { - int adapterPosition = viewHolder.getAdapterPosition(); - - String prevCachedId = positionToCacheRef.get(adapterPosition + 1,null); - String nextCachedId = positionToCacheRef.get(adapterPosition - 1, null); - - MessageRecord previousRecord = null; - if (adapterPosition < getItemCount() - 1 && !isFooterPosition(adapterPosition + 1)) { - if (prevCachedId != null && messageRecordCache.containsKey(prevCachedId)) { - SoftReference prevSoftRecord = messageRecordCache.get(prevCachedId); - MessageRecord prevCachedRecord = prevSoftRecord.get(); - if (prevCachedRecord != null) { - previousRecord = prevCachedRecord; - } else { - previousRecord = getRecordForPositionOrThrow(adapterPosition + 1); - } - } else { - previousRecord = getRecordForPositionOrThrow(adapterPosition + 1); - } - } - - MessageRecord nextRecord = null; - if (adapterPosition > 0 && !isHeaderPosition(adapterPosition - 1)) { - if (nextCachedId != null && messageRecordCache.containsKey(nextCachedId)) { - SoftReference nextSoftRecord = messageRecordCache.get(nextCachedId); - MessageRecord nextCachedRecord = nextSoftRecord.get(); - if (nextCachedRecord != null) { - nextRecord = nextCachedRecord; - } else { - nextRecord = getRecordForPositionOrThrow(adapterPosition - 1); - } - } else { - nextRecord = getRecordForPositionOrThrow(adapterPosition - 1); - } - } - - viewHolder.getView().bind(messageRecord, - Optional.fromNullable(previousRecord), - Optional.fromNullable(nextRecord), - glideRequests, - locale, - batchSelected, - recipient, - searchQuery, - messageRecord == recordToPulseHighlight); - - if (messageRecord == recordToPulseHighlight) { - recordToPulseHighlight = null; - } - } - - @Override - public ViewHolder onCreateItemViewHolder(ViewGroup parent, int viewType) { - long start = System.currentTimeMillis(); - final V itemView = ViewUtil.inflate(inflater, parent, getLayoutForViewType(viewType)); - itemView.setOnClickListener(view -> { - if (clickListener != null) { - clickListener.onItemClick(itemView.getMessageRecord()); - } - }); - itemView.setOnLongClickListener(view -> { - if (clickListener != null) { - clickListener.onItemLongClick(itemView.getMessageRecord()); - } - return true; - }); - itemView.setEventListener(clickListener); - Log.d(TAG, "Inflate time: " + (System.currentTimeMillis() - start)); - return new ViewHolder(itemView); - } - - @Override - public void onItemViewRecycled(ViewHolder holder) { - holder.getView().unbind(); - } - - private @LayoutRes int getLayoutForViewType(int viewType) { - switch (viewType) { - case MESSAGE_TYPE_AUDIO_OUTGOING: - case MESSAGE_TYPE_THUMBNAIL_OUTGOING: - case MESSAGE_TYPE_DOCUMENT_OUTGOING: - case MESSAGE_TYPE_INVITATION_OUTGOING: - case MESSAGE_TYPE_OUTGOING: return R.layout.conversation_item_sent; - case MESSAGE_TYPE_AUDIO_INCOMING: - case MESSAGE_TYPE_THUMBNAIL_INCOMING: - case MESSAGE_TYPE_DOCUMENT_INCOMING: - case MESSAGE_TYPE_INVITATION_INCOMING: - case MESSAGE_TYPE_INCOMING: return R.layout.conversation_item_received; - case MESSAGE_TYPE_UPDATE: return R.layout.conversation_item_update; - default: throw new IllegalArgumentException("unsupported item view type given to ConversationAdapter"); - } - } - - @Override - public int getItemViewType(@NonNull MessageRecord messageRecord) { - if (messageRecord.isUpdate()) { - return MESSAGE_TYPE_UPDATE; - } else if (messageRecord.isOpenGroupInvitation()) { - if (messageRecord.isOutgoing()) return MESSAGE_TYPE_INVITATION_OUTGOING; - else return MESSAGE_TYPE_INVITATION_INCOMING; - } else if (hasAudio(messageRecord)) { - if (messageRecord.isOutgoing()) return MESSAGE_TYPE_AUDIO_OUTGOING; - else return MESSAGE_TYPE_AUDIO_INCOMING; - } else if (hasDocument(messageRecord)) { - if (messageRecord.isOutgoing()) return MESSAGE_TYPE_DOCUMENT_OUTGOING; - else return MESSAGE_TYPE_DOCUMENT_INCOMING; - } else if (hasThumbnail(messageRecord)) { - if (messageRecord.isOutgoing()) return MESSAGE_TYPE_THUMBNAIL_OUTGOING; - else return MESSAGE_TYPE_THUMBNAIL_INCOMING; - } else if (messageRecord.isOutgoing()) { - return MESSAGE_TYPE_OUTGOING; - } else { - return MESSAGE_TYPE_INCOMING; - } - } - - @Override - protected boolean isRecordForId(@NonNull MessageRecord record, long id) { - return record.getId() == id; - } - - @Override - public long getItemId(@NonNull Cursor cursor) { - List attachments = DatabaseFactory.getAttachmentDatabase(getContext()).getAttachment(cursor); - List messageAttachments = Stream.of(attachments).filterNot(DatabaseAttachment::isQuote).toList(); - - if (messageAttachments.size() > 0 && messageAttachments.get(0).getFastPreflightId() != null) { - return Long.valueOf(messageAttachments.get(0).getFastPreflightId()); - } - - final String unique = cursor.getString(cursor.getColumnIndexOrThrow(MmsSmsColumns.UNIQUE_ROW_ID)); - final byte[] bytes = digest.digest(unique.getBytes()); - return Conversions.byteArrayToLong(bytes); - } - - @Override - protected long getItemId(@NonNull MessageRecord record) { - if (record.isOutgoing() && record.isMms()) { - MmsMessageRecord mmsRecord = (MmsMessageRecord) record; - SlideDeck slideDeck = mmsRecord.getSlideDeck(); - - if (slideDeck.getThumbnailSlide() != null && slideDeck.getThumbnailSlide().getFastPreflightId() != null) { - return Long.valueOf(slideDeck.getThumbnailSlide().getFastPreflightId()); - } - } - - return record.getId(); - } - - @Override - protected MessageRecord getRecordFromCursor(@NonNull Cursor cursor) { - long messageId = cursor.getLong(cursor.getColumnIndexOrThrow(MmsSmsColumns.ID)); - String type = cursor.getString(cursor.getColumnIndexOrThrow(MmsSmsDatabase.TRANSPORT)); - - final SoftReference reference = messageRecordCache.get(type + messageId); - if (reference != null) { - final MessageRecord record = reference.get(); - if (record != null) return record; - } - - final MessageRecord messageRecord = db.readerFor(cursor).getCurrent(); - messageRecordCache.put(type + messageId, new SoftReference<>(messageRecord)); - - return messageRecord; - } - - public void close() { - getCursor().close(); - } - - public int findLastSeenPosition(long lastSeen) { - if (lastSeen <= 0) return -1; - if (!isActiveCursor()) return -1; - - int count = getItemCount() - (hasFooterView() ? 1 : 0); - - for (int i=(hasHeaderView() ? 1 : 0);i getSelectedItems() { - return Collections.unmodifiableSet(new HashSet<>(batchSelected)); - } - - public void pulseHighlightItem(int position) { - if (position < getItemCount()) { - recordToPulseHighlight = getRecordForPositionOrThrow(position); - notifyItemChanged(position); - } - } - - public void onSearchQueryUpdated(@Nullable String query) { - this.searchQuery = query; - notifyDataSetChanged(); - } - - private boolean hasAudio(MessageRecord messageRecord) { - return messageRecord.isMms() && ((MmsMessageRecord)messageRecord).getSlideDeck().getAudioSlide() != null; - } - - private boolean hasDocument(MessageRecord messageRecord) { - return messageRecord.isMms() && ((MmsMessageRecord)messageRecord).getSlideDeck().getDocumentSlide() != null; - } - - private boolean hasThumbnail(MessageRecord messageRecord) { - return messageRecord.isMms() && ((MmsMessageRecord)messageRecord).getSlideDeck().getThumbnailSlide() != null; - } - - @Override - public long getHeaderId(int position) { - if (!isActiveCursor()) return -1; - if (isHeaderPosition(position)) return -1; - if (isFooterPosition(position)) return -1; - if (position >= getItemCount()) return -1; - if (position < 0) return -1; - - MessageRecord record = getRecordForPositionOrThrow(position); - if (record.getRecipient().getAddress().isOpenGroup()) { - calendar.setTime(new Date(record.getDateReceived())); - } else { - calendar.setTime(new Date(record.getDateSent())); - } - return Util.hashCode(calendar.get(Calendar.YEAR), calendar.get(Calendar.DAY_OF_YEAR)); - } - - public long getReceivedTimestamp(int position) { - if (!isActiveCursor()) return 0; - if (isHeaderPosition(position)) return 0; - if (isFooterPosition(position)) return 0; - if (position >= getItemCount()) return 0; - if (position < 0) return 0; - - MessageRecord messageRecord = getRecordForPositionOrThrow(position); - - if (messageRecord.isOutgoing()) return 0; - else return messageRecord.getDateReceived(); - } - - @Override - public HeaderViewHolder onCreateHeaderViewHolder(ViewGroup parent) { - return new HeaderViewHolder(LayoutInflater.from(getContext()).inflate(R.layout.conversation_item_header, parent, false)); - } - - public HeaderViewHolder onCreateLastSeenViewHolder(ViewGroup parent) { - return new HeaderViewHolder(LayoutInflater.from(getContext()).inflate(R.layout.conversation_item_last_seen, parent, false)); - } - - @Override - public void onBindHeaderViewHolder(HeaderViewHolder viewHolder, int position) { - MessageRecord messageRecord = getRecordForPositionOrThrow(position); - long timestamp = messageRecord.getDateReceived(); - if (recipient.getAddress().isOpenGroup()) { timestamp = messageRecord.getTimestamp(); } - viewHolder.setText(DateUtils.getRelativeDate(getContext(), locale, timestamp)); - } - - public void onBindLastSeenViewHolder(HeaderViewHolder viewHolder, int position) { - viewHolder.setText(getContext().getResources().getQuantityString(R.plurals.ConversationAdapter_n_unread_messages, (position + 1), (position + 1))); - } - - static class LastSeenHeader extends StickyHeaderDecoration { - - private final ConversationAdapter adapter; - private final long lastSeenTimestamp; - - LastSeenHeader(ConversationAdapter adapter, long lastSeenTimestamp) { - super(adapter, false, false); - this.adapter = adapter; - this.lastSeenTimestamp = lastSeenTimestamp; - } - - @Override - protected boolean hasHeader(RecyclerView parent, StickyHeaderAdapter stickyAdapter, int position) { - if (!adapter.isActiveCursor()) { - return false; - } - - if (lastSeenTimestamp <= 0) { - return false; - } - - long currentRecordTimestamp = adapter.getReceivedTimestamp(position); - long previousRecordTimestamp = adapter.getReceivedTimestamp(position + 1); - - return currentRecordTimestamp > lastSeenTimestamp && previousRecordTimestamp < lastSeenTimestamp; - } - - @Override - protected int getHeaderTop(RecyclerView parent, View child, View header, int adapterPos, int layoutPos) { - return parent.getLayoutManager().getDecoratedTop(child); - } - - @Override - protected HeaderViewHolder getHeader(RecyclerView parent, StickyHeaderAdapter stickyAdapter, int position) { - HeaderViewHolder viewHolder = adapter.onCreateLastSeenViewHolder(parent); - adapter.onBindLastSeenViewHolder(viewHolder, position); - - int widthSpec = View.MeasureSpec.makeMeasureSpec(parent.getWidth(), View.MeasureSpec.EXACTLY); - int heightSpec = View.MeasureSpec.makeMeasureSpec(parent.getHeight(), View.MeasureSpec.UNSPECIFIED); - - int childWidth = ViewGroup.getChildMeasureSpec(widthSpec, parent.getPaddingLeft() + parent.getPaddingRight(), viewHolder.itemView.getLayoutParams().width); - int childHeight = ViewGroup.getChildMeasureSpec(heightSpec, parent.getPaddingTop() + parent.getPaddingBottom(), viewHolder.itemView.getLayoutParams().height); - - viewHolder.itemView.measure(childWidth, childHeight); - viewHolder.itemView.layout(0, 0, viewHolder.itemView.getMeasuredWidth(), viewHolder.itemView.getMeasuredHeight()); - - return viewHolder; - } - } - -} - diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationFragment.java b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationFragment.java deleted file mode 100644 index 2e4dee6a38..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationFragment.java +++ /dev/null @@ -1,1224 +0,0 @@ -/* - * Copyright (C) 2015 Open Whisper Systems - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ -package org.thoughtcrime.securesms.conversation; - -import android.annotation.SuppressLint; -import android.app.Activity; -import android.content.ClipData; -import android.content.Context; -import android.content.DialogInterface; -import android.content.Intent; -import android.database.Cursor; -import android.net.Uri; -import android.os.AsyncTask; -import android.os.Build; -import android.os.Bundle; -import android.text.ClipboardManager; -import android.text.TextUtils; -import android.view.LayoutInflater; -import android.view.Menu; -import android.view.MenuInflater; -import android.view.MenuItem; -import android.view.View; -import android.view.ViewGroup; -import android.view.ViewParent; -import android.view.Window; -import android.view.animation.Animation; -import android.view.animation.AnimationUtils; -import android.widget.TextView; -import android.widget.Toast; -import android.widget.ViewSwitcher; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.appcompat.app.AlertDialog; -import androidx.appcompat.app.AppCompatActivity; -import androidx.appcompat.view.ActionMode; -import androidx.fragment.app.Fragment; -import androidx.loader.app.LoaderManager; -import androidx.loader.content.Loader; -import androidx.recyclerview.widget.LinearLayoutManager; -import androidx.recyclerview.widget.RecyclerView; -import androidx.recyclerview.widget.RecyclerView.OnScrollListener; -import com.annimon.stream.Stream; -import org.session.libsession.messaging.MessagingModuleConfiguration; -import org.session.libsession.messaging.messages.control.DataExtractionNotification; -import org.session.libsession.messaging.messages.signal.OutgoingMediaMessage; -import org.session.libsession.messaging.messages.signal.OutgoingTextMessage; -import org.session.libsession.messaging.messages.visible.Quote; -import org.session.libsession.messaging.messages.visible.VisibleMessage; -import org.session.libsession.messaging.open_groups.OpenGroupAPIV2; -import org.session.libsession.messaging.open_groups.OpenGroupV2; -import org.session.libsession.messaging.sending_receiving.MessageSender; -import org.session.libsession.messaging.sending_receiving.attachments.Attachment; -import org.session.libsession.messaging.sending_receiving.link_preview.LinkPreview; -import org.session.libsession.utilities.Address; -import org.session.libsession.utilities.recipients.Recipient; -import org.session.libsession.utilities.TextSecurePreferences; -import org.session.libsession.utilities.Util; -import org.session.libsession.utilities.ViewUtil; -import org.session.libsession.utilities.concurrent.SimpleTask; -import org.session.libsession.utilities.task.ProgressDialogAsyncTask; -import org.session.libsignal.utilities.guava.Optional; -import org.session.libsignal.utilities.Log; -import org.thoughtcrime.securesms.ApplicationContext; -import org.thoughtcrime.securesms.MessageDetailsActivity; -import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity; -import org.thoughtcrime.securesms.ShareActivity; -import org.thoughtcrime.securesms.components.ConversationTypingView; -import org.thoughtcrime.securesms.components.recyclerview.SmoothScrollingLinearLayoutManager; -import org.thoughtcrime.securesms.conversation.ConversationAdapter.HeaderViewHolder; -import org.thoughtcrime.securesms.conversation.ConversationAdapter.ItemClickListener; -import org.thoughtcrime.securesms.database.DatabaseFactory; -import org.thoughtcrime.securesms.database.MmsSmsDatabase; -import org.thoughtcrime.securesms.database.loaders.ConversationLoader; -import org.thoughtcrime.securesms.database.model.MediaMmsMessageRecord; -import org.thoughtcrime.securesms.database.model.MessageRecord; -import org.thoughtcrime.securesms.database.model.MmsMessageRecord; -import org.thoughtcrime.securesms.longmessage.LongMessageActivity; -import org.thoughtcrime.securesms.mediasend.Media; -import org.thoughtcrime.securesms.mms.GlideApp; -import org.thoughtcrime.securesms.mms.PartAuthority; -import org.thoughtcrime.securesms.mms.Slide; -import org.thoughtcrime.securesms.permissions.Permissions; -import org.thoughtcrime.securesms.util.CommunicationActions; -import org.thoughtcrime.securesms.util.SaveAttachmentTask; -import org.thoughtcrime.securesms.util.StickyHeaderDecoration; - -import java.io.IOException; -import java.io.InputStream; -import java.util.ArrayList; -import java.util.Collections; -import java.util.Comparator; -import java.util.HashSet; -import java.util.LinkedList; -import java.util.List; -import java.util.Locale; -import java.util.Set; - -import kotlin.Unit; -import network.loki.messenger.R; - -@SuppressLint("StaticFieldLeak") -public class ConversationFragment extends Fragment - implements LoaderManager.LoaderCallbacks -{ - private static final String TAG = ConversationFragment.class.getSimpleName(); - private static final String KEY_LIMIT = "limit"; - - private static final int PARTIAL_CONVERSATION_LIMIT = 500; - private static final int SCROLL_ANIMATION_THRESHOLD = 50; - private static final int CODE_ADD_EDIT_CONTACT = 77; - - private final ActionModeCallback actionModeCallback = new ActionModeCallback(); - private final ItemClickListener selectionClickListener = new ConversationFragmentItemClickListener(); - - private ConversationFragmentListener listener; - - private Recipient recipient; - private long threadId; - private long lastSeen; - private int startingPosition; - private int previousOffset; - private int activeOffset; - private boolean firstLoad; - private long loaderStartTime; - private ActionMode actionMode; - private Locale locale; - private RecyclerView list; - private RecyclerView.ItemDecoration lastSeenDecoration; - private ViewSwitcher topLoadMoreView; - private ViewSwitcher bottomLoadMoreView; - private ConversationTypingView typingView; - private View composeDivider; - private View scrollToBottomButton; - private TextView scrollDateHeader; - - @Override - public void onCreate(Bundle icicle) { - super.onCreate(icicle); - this.locale = (Locale) getArguments().getSerializable(PassphraseRequiredActionBarActivity.LOCALE_EXTRA); - } - - @Override - public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle bundle) { - final View view = inflater.inflate(R.layout.conversation_fragment, container, false); - list = ViewUtil.findById(view, android.R.id.list); - composeDivider = ViewUtil.findById(view, R.id.compose_divider); - scrollToBottomButton = ViewUtil.findById(view, R.id.scroll_to_bottom_button); - scrollDateHeader = ViewUtil.findById(view, R.id.scroll_date_header); - - scrollToBottomButton.setOnClickListener(v -> scrollToBottom()); - - final LinearLayoutManager layoutManager = new SmoothScrollingLinearLayoutManager(getActivity(), true); - list.setHasFixedSize(false); - list.setLayoutManager(layoutManager); - list.setItemAnimator(null); - - topLoadMoreView = (ViewSwitcher) inflater.inflate(R.layout.load_more_header, container, false); - bottomLoadMoreView = (ViewSwitcher) inflater.inflate(R.layout.load_more_header, container, false); - initializeLoadMoreView(topLoadMoreView); - initializeLoadMoreView(bottomLoadMoreView); - - typingView = (ConversationTypingView) inflater.inflate(R.layout.conversation_typing_view, container, false); - - return view; - } - - @Override - public void onActivityCreated(Bundle bundle) { - super.onActivityCreated(bundle); - - initializeResources(); - initializeListAdapter(); - } - - @Override - public void onAttach(Activity activity) { - super.onAttach(activity); - this.listener = (ConversationFragmentListener)activity; - } - - @Override - public void onStart() { - super.onStart(); - initializeTypingObserver(); - } - - @Override - public void onResume() { - super.onResume(); - - if (list.getAdapter() != null) { - list.getAdapter().notifyDataSetChanged(); - } - } - - @Override - public void onStop() { - super.onStop(); - ApplicationContext.getInstance(requireContext()).getTypingStatusRepository().getTypists(threadId).removeObservers(this); - } - - public void onNewIntent() { - if (actionMode != null) { - actionMode.finish(); - } - - initializeResources(); - initializeListAdapter(); - - if (threadId == -1) { - getLoaderManager().restartLoader(0, Bundle.EMPTY, this); - } - } - - public void reloadList() { - getLoaderManager().restartLoader(0, Bundle.EMPTY, this); - } - - public void moveToLastSeen() { - if (lastSeen <= 0) { - Log.i(TAG, "No need to move to last seen."); - return; - } - - if (list == null || getListAdapter() == null) { - Log.w(TAG, "Tried to move to last seen position, but we hadn't initialized the view yet."); - return; - } - - int position = getListAdapter().findLastSeenPosition(lastSeen); - scrollToLastSeenPosition(position); - } - - private void initializeResources() { - this.recipient = Recipient.from(getActivity(), getActivity().getIntent().getParcelableExtra(ConversationActivity.ADDRESS_EXTRA), true); - this.threadId = this.getActivity().getIntent().getLongExtra(ConversationActivity.THREAD_ID_EXTRA, -1); - this.lastSeen = this.getActivity().getIntent().getLongExtra(ConversationActivity.LAST_SEEN_EXTRA, -1); - this.startingPosition = this.getActivity().getIntent().getIntExtra(ConversationActivity.STARTING_POSITION_EXTRA, -1); - this.firstLoad = true; - - OnScrollListener scrollListener = new ConversationScrollListener(getActivity()); - list.addOnScrollListener(scrollListener); - } - - private void initializeListAdapter() { - if (this.recipient != null && this.threadId != -1) { - ConversationAdapter adapter = new ConversationAdapter(getActivity(), GlideApp.with(this), locale, selectionClickListener, null, this.recipient); - list.setAdapter(adapter); - list.addItemDecoration(new StickyHeaderDecoration(adapter, false, false)); - - setLastSeen(lastSeen); - getLoaderManager().restartLoader(0, Bundle.EMPTY, this); - } - } - - private void initializeLoadMoreView(ViewSwitcher loadMoreView) { - loadMoreView.setOnClickListener(v -> { - Bundle args = new Bundle(); - args.putInt(KEY_LIMIT, 0); - getLoaderManager().restartLoader(0, args, ConversationFragment.this); - loadMoreView.showNext(); - loadMoreView.setOnClickListener(null); - }); - } - - private void initializeTypingObserver() { - if (!TextSecurePreferences.isTypingIndicatorsEnabled(requireContext())) { - return; - } - - ApplicationContext.getInstance(requireContext()).getTypingStatusRepository().getTypists(threadId).observe(this, typingState -> { - List recipients; - boolean replacedByIncomingMessage; - - if (typingState != null) { - recipients = typingState.getTypists(); - replacedByIncomingMessage = typingState.isReplacedByIncomingMessage(); - } else { - recipients = Collections.emptyList(); - replacedByIncomingMessage = false; - } - - typingView.setTypists(GlideApp.with(ConversationFragment.this), recipients, recipient.isGroupRecipient()); - - ConversationAdapter adapter = getListAdapter(); - - if (adapter.getHeaderView() != null && adapter.getHeaderView() != typingView) { - Log.i(TAG, "Skipping typing indicator -- the header slot is occupied."); - return; - } - - if (recipients.size() > 0) { - if (adapter.getHeaderView() == null && isAtBottom()) { - list.setVerticalScrollBarEnabled(false); - list.post(() -> getListLayoutManager().smoothScrollToPosition(requireContext(), 0, 250)); - list.postDelayed(() -> list.setVerticalScrollBarEnabled(true), 300); - adapter.setHeaderView(typingView); - adapter.notifyItemInserted(0); - } else { - if (adapter.getHeaderView() == null) { - adapter.setHeaderView(typingView); - adapter.notifyItemInserted(0); - } else { - adapter.setHeaderView(typingView); - adapter.notifyItemChanged(0); - } - } - } else { - if (getListLayoutManager().findFirstCompletelyVisibleItemPosition() == 0 && getListLayoutManager().getItemCount() > 1 && !replacedByIncomingMessage) { - getListLayoutManager().smoothScrollToPosition(requireContext(), 1, 250); - list.setVerticalScrollBarEnabled(false); - list.postDelayed(() -> { - adapter.setHeaderView(null); - adapter.notifyItemRemoved(0); - list.post(() -> list.setVerticalScrollBarEnabled(true)); - }, 200); - } else if (!replacedByIncomingMessage) { - adapter.setHeaderView(null); - adapter.notifyItemRemoved(0); - } else { - adapter.setHeaderView(null); - } - } - }); - } - - private void setCorrectMenuVisibility(Menu menu) { - Set messageRecords = getListAdapter().getSelectedItems(); - boolean actionMessage = false; - boolean hasText = false; - boolean sharedContact = false; - - if (actionMode != null && messageRecords.size() == 0) { - actionMode.finish(); - return; - } - - for (MessageRecord messageRecord : messageRecords) { - if (messageRecord.isGroupAction() || messageRecord.isCallLog() || - messageRecord.isJoined() || messageRecord.isExpirationTimerUpdate() || - messageRecord.isEndSession() || messageRecord.isIdentityUpdate() || - messageRecord.isIdentityVerified() || messageRecord.isIdentityDefault() || messageRecord.isLokiSessionRestoreSent() || messageRecord.isLokiSessionRestoreDone()) - { - actionMessage = true; - } - - if (messageRecord.getBody().length() > 0) { - hasText = true; - } - - if (messageRecord.isMms() && !((MmsMessageRecord) messageRecord).getSharedContacts().isEmpty()) { - sharedContact = true; - } - } - - if (messageRecords.size() > 1) { - menu.findItem(R.id.menu_context_details).setVisible(false); - menu.findItem(R.id.menu_context_reply).setVisible(false); - menu.findItem(R.id.menu_context_save_attachment).setVisible(false); - menu.findItem(R.id.menu_context_resend).setVisible(false); - } else { - MessageRecord messageRecord = messageRecords.iterator().next(); - - menu.findItem(R.id.menu_context_details).setVisible(true); - menu.findItem(R.id.menu_context_resend).setVisible(messageRecord.isFailed()); - menu.findItem(R.id.menu_context_save_attachment).setVisible(!actionMessage && - messageRecord.isMms() && - !messageRecord.isMmsNotification() && - ((MediaMmsMessageRecord)messageRecord).containsMediaSlide()); - - menu.findItem(R.id.menu_context_reply).setVisible(!actionMessage && - !messageRecord.isPending() && - !messageRecord.isFailed() && - messageRecord.isSecure()); - } - - menu.findItem(R.id.menu_context_copy).setVisible(!actionMessage && hasText); - - boolean isGroupChat = recipient.isGroupRecipient(); - - if (isGroupChat) { - OpenGroupV2 openGroupChat = DatabaseFactory.getLokiThreadDatabase(getContext()).getOpenGroupChat(threadId); - boolean isPublicChat = (openGroupChat != null); - int selectedMessageCount = messageRecords.size(); - boolean areAllSentByUser = true; - Set uniqueUserSet = new HashSet<>(); - for (MessageRecord message : messageRecords) { - if (!message.isOutgoing()) { areAllSentByUser = false; } - uniqueUserSet.add(message.getRecipient().getAddress().toString()); - } - menu.findItem(R.id.menu_context_copy_public_key).setVisible(selectedMessageCount == 1 && !areAllSentByUser); - menu.findItem(R.id.menu_context_reply).setVisible(selectedMessageCount == 1); - String userHexEncodedPublicKey = TextSecurePreferences.getLocalNumber(requireContext()); - boolean userCanModerate = - (isPublicChat && (OpenGroupAPIV2.isUserModerator(userHexEncodedPublicKey, openGroupChat.getRoom(), openGroupChat.getServer()))); - boolean isDeleteOptionVisible = !isPublicChat || (areAllSentByUser || userCanModerate); - // allow banning if moderating a public chat and only one user's messages are selected - boolean isBanOptionVisible = isPublicChat && userCanModerate && !areAllSentByUser && uniqueUserSet.size() == 1; - menu.findItem(R.id.menu_context_delete_message).setVisible(isDeleteOptionVisible); - menu.findItem(R.id.menu_context_ban_user).setVisible(isBanOptionVisible); - } else { - menu.findItem(R.id.menu_context_copy_public_key).setVisible(false); - menu.findItem(R.id.menu_context_delete_message).setVisible(true); - menu.findItem(R.id.menu_context_ban_user).setVisible(false); - } - } - - private ConversationAdapter getListAdapter() { - return (ConversationAdapter) list.getAdapter(); - } - - private SmoothScrollingLinearLayoutManager getListLayoutManager() { - return (SmoothScrollingLinearLayoutManager) list.getLayoutManager(); - } - - private MessageRecord getSelectedMessageRecord() { - Set messageRecords = getListAdapter().getSelectedItems(); - return messageRecords.iterator().next(); - } - - public void reload(Recipient recipient, long threadId) { - this.recipient = recipient; - - if (this.threadId != threadId) { - this.threadId = threadId; - initializeListAdapter(); - } - } - - public void scrollToBottom() { - if (getListLayoutManager().findFirstVisibleItemPosition() < SCROLL_ANIMATION_THRESHOLD) { - list.smoothScrollToPosition(0); - } else { - list.scrollToPosition(0); - } - } - - public void setLastSeen(long lastSeen) { - this.lastSeen = lastSeen; - if (lastSeenDecoration != null) { - list.removeItemDecoration(lastSeenDecoration); - } - - lastSeenDecoration = new ConversationAdapter.LastSeenHeader(getListAdapter(), lastSeen); - list.addItemDecoration(lastSeenDecoration); - } - - private void handleCopyMessage(final Set messageRecords) { - List messageList = new LinkedList<>(messageRecords); - Collections.sort(messageList, new Comparator() { - @Override - public int compare(MessageRecord lhs, MessageRecord rhs) { - if (lhs.getDateReceived() < rhs.getDateReceived()) return -1; - else if (lhs.getDateReceived() == rhs.getDateReceived()) return 0; - else return 1; - } - }); - - StringBuilder bodyBuilder = new StringBuilder(); - ClipboardManager clipboard = (ClipboardManager) getActivity().getSystemService(Context.CLIPBOARD_SERVICE); - - for (MessageRecord messageRecord : messageList) { - String body = messageRecord.getDisplayBody(requireContext()).toString(); - if (!TextUtils.isEmpty(body)) { - bodyBuilder.append(body).append('\n'); - } - } - if (bodyBuilder.length() > 0 && bodyBuilder.charAt(bodyBuilder.length() - 1) == '\n') { - bodyBuilder.deleteCharAt(bodyBuilder.length() - 1); - } - - String result = bodyBuilder.toString(); - - if (!TextUtils.isEmpty(result)) - clipboard.setText(result); - } - - private void handleCopyPublicKey(MessageRecord messageRecord) { - String sessionID = messageRecord.getRecipient().getAddress().toString(); - android.content.ClipboardManager clipboard = (android.content.ClipboardManager)requireActivity().getSystemService(Context.CLIPBOARD_SERVICE); - ClipData clip = ClipData.newPlainText("Session ID", sessionID); - clipboard.setPrimaryClip(clip); - Toast.makeText(getContext(), R.string.copied_to_clipboard, Toast.LENGTH_SHORT).show(); - } - - private void handleDeleteMessages(final Set messageRecords) { - int messagesCount = messageRecords.size(); - AlertDialog.Builder builder = new AlertDialog.Builder(getActivity()); - - builder.setIconAttribute(R.attr.dialog_alert_icon); - builder.setTitle(getActivity().getResources().getQuantityString(R.plurals.ConversationFragment_delete_selected_messages, messagesCount, messagesCount)); - builder.setMessage(getActivity().getResources().getQuantityString(R.plurals.ConversationFragment_this_will_permanently_delete_all_n_selected_messages, messagesCount, messagesCount)); - builder.setCancelable(true); - - OpenGroupV2 openGroupChat = DatabaseFactory.getLokiThreadDatabase(getContext()).getOpenGroupChat(threadId); - - builder.setPositiveButton(R.string.delete, new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialog, int which) { - new ProgressDialogAsyncTask(getActivity(), - R.string.ConversationFragment_deleting, - R.string.ConversationFragment_deleting_messages) - { - @Override - protected Void doInBackground(MessageRecord... messageRecords) { - if (openGroupChat != null) { - ArrayList serverIDs = new ArrayList<>(); - ArrayList ignoredMessages = new ArrayList<>(); - ArrayList failedMessages = new ArrayList<>(); - boolean isSentByUser = true; - for (MessageRecord messageRecord : messageRecords) { - isSentByUser = isSentByUser && messageRecord.isOutgoing(); - Long serverID = DatabaseFactory.getLokiMessageDatabase(getContext()).getServerID(messageRecord.id, !messageRecord.isMms()); - if (serverID != null) { - serverIDs.add(serverID); - } else { - ignoredMessages.add(messageRecord.getId()); - } - } - if (openGroupChat != null) { - for (Long serverId : serverIDs) { - OpenGroupAPIV2 - .deleteMessage(serverId, openGroupChat.getRoom(), openGroupChat.getServer()) - .success(l -> { - for (MessageRecord messageRecord : messageRecords) { - Long serverID = DatabaseFactory.getLokiMessageDatabase(getContext()).getServerID(messageRecord.id, !messageRecord.isMms()); - if (serverID != null && serverID.equals(serverId)) { - MessagingModuleConfiguration.shared.getMessageDataProvider().deleteMessage(messageRecord.id, !messageRecord.isMms()); - break; - } - } - return null; - }).fail(e->{ - Log.e("Loki", "Couldn't delete message due to error",e); - return null; - }); - } - } - } else { - for (MessageRecord messageRecord : messageRecords) { - if (messageRecord.isMms()) { - DatabaseFactory.getMmsDatabase(getActivity()).delete(messageRecord.getId()); - } else { - DatabaseFactory.getSmsDatabase(getActivity()).deleteMessage(messageRecord.getId()); - } - } - } - return null; - } - }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, messageRecords.toArray(new MessageRecord[messageRecords.size()])); - } - }); - - builder.setNegativeButton(android.R.string.cancel, null); - builder.show(); - } - - private void handleBanUser(Set messageRecords) { - AlertDialog.Builder builder = new AlertDialog.Builder(getActivity()); - - String userPublicKey = null; - for (MessageRecord record: messageRecords) { - String currentPublicKey = record.getRecipient().getAddress().toString(); - if (userPublicKey == null) { - userPublicKey = currentPublicKey; - } - } - final String finalPublicKey = userPublicKey; - - builder.setIconAttribute(R.attr.dialog_alert_icon); - builder.setTitle(R.string.ConversationFragment_ban_selected_user); - builder.setCancelable(true); - - final OpenGroupV2 openGroupChat = DatabaseFactory.getLokiThreadDatabase(getContext()).getOpenGroupChat(threadId); - - builder.setPositiveButton(R.string.ban, (dialog, which) -> { - ConversationAdapter chatAdapter = getListAdapter(); - chatAdapter.clearSelection(); - chatAdapter.notifyDataSetChanged(); - new ProgressDialogAsyncTask(getActivity(), - R.string.ConversationFragment_banning, - R.string.ConversationFragment_banning_user) { - @Override - protected Void doInBackground(String... userPublicKeyParam) { - String userPublicKey = userPublicKeyParam[0]; - if (openGroupChat != null) { - OpenGroupAPIV2 - .ban(userPublicKey, openGroupChat.getRoom(), openGroupChat.getServer()) - .success(l -> { - Log.d("Loki", "User banned"); - return Unit.INSTANCE; - }).fail(e -> { - Log.e("Loki", "Failed to ban user",e); - return null; - }); - } else { - Log.d("Loki", "Tried to ban user from a non-public chat"); - } - return null; - } - }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, finalPublicKey); - }); - - builder.setNegativeButton(android.R.string.cancel, null); - builder.show(); - } - - private void handleDisplayDetails(MessageRecord message) { - Intent intent = new Intent(getActivity(), MessageDetailsActivity.class); - intent.putExtra(MessageDetailsActivity.MESSAGE_ID_EXTRA, message.getId()); - intent.putExtra(MessageDetailsActivity.THREAD_ID_EXTRA, threadId); - intent.putExtra(MessageDetailsActivity.TYPE_EXTRA, message.isMms() ? MmsSmsDatabase.MMS_TRANSPORT : MmsSmsDatabase.SMS_TRANSPORT); - intent.putExtra(MessageDetailsActivity.ADDRESS_EXTRA, recipient.getAddress()); - intent.putExtra(MessageDetailsActivity.IS_PUSH_GROUP_EXTRA, recipient.isGroupRecipient() && message.isPush()); - startActivity(intent); - } - - private void handleForwardMessage(MessageRecord message) { - listener.onForwardClicked(); - - SimpleTask.run(getLifecycle(), () -> { - Intent composeIntent = new Intent(getActivity(), ShareActivity.class); - composeIntent.putExtra(Intent.EXTRA_TEXT, message.getDisplayBody(requireContext()).toString()); - - if (message.isMms()) { - MmsMessageRecord mediaMessage = (MmsMessageRecord) message; - boolean isAlbum = mediaMessage.containsMediaSlide() && - mediaMessage.getSlideDeck().getSlides().size() > 1 && - mediaMessage.getSlideDeck().getAudioSlide() == null && - mediaMessage.getSlideDeck().getDocumentSlide() == null; - - if (isAlbum) { - ArrayList mediaList = new ArrayList<>(mediaMessage.getSlideDeck().getSlides().size()); - List attachments = Stream.of(mediaMessage.getSlideDeck().getSlides()) - .filter(s -> s.hasImage() || s.hasVideo()) - .map(Slide::asAttachment) - .toList(); - - for (Attachment attachment : attachments) { - Uri uri = attachment.getDataUri() != null ? attachment.getDataUri() : attachment.getThumbnailUri(); - - if (uri != null) { - mediaList.add(new Media(uri, - attachment.getContentType(), - System.currentTimeMillis(), - attachment.getWidth(), - attachment.getHeight(), - attachment.getSize(), - Optional.absent(), - Optional.fromNullable(attachment.getCaption()))); - } - }; - - if (!mediaList.isEmpty()) { - composeIntent.putExtra(ConversationActivity.MEDIA_EXTRA, mediaList); - } - } else if (mediaMessage.containsMediaSlide()) { - Slide slide = mediaMessage.getSlideDeck().getSlides().get(0); - composeIntent.putExtra(Intent.EXTRA_STREAM, slide.getUri()); - composeIntent.setType(slide.getContentType()); - } - - if (mediaMessage.getSlideDeck().getTextSlide() != null && mediaMessage.getSlideDeck().getTextSlide().getUri() != null) { - try (InputStream stream = PartAuthority.getAttachmentStream(requireContext(), mediaMessage.getSlideDeck().getTextSlide().getUri())) { - String fullBody = Util.readFullyAsString(stream); - composeIntent.putExtra(Intent.EXTRA_TEXT, fullBody); - } catch (IOException e) { - Log.w(TAG, "Failed to read long message text when forwarding."); - } - } - } - - return composeIntent; - }, this::startActivity); - } - - private void handleResendMessage(final MessageRecord message) { - new AsyncTask() { - @Override - protected Void doInBackground(MessageRecord... messageRecords) { - MessageRecord messageRecord = messageRecords[0]; - Recipient recipient = messageRecord.getRecipient(); - VisibleMessage message = new VisibleMessage(); - message.setId(messageRecord.getId()); - message.setText(messageRecord.getBody()); - message.setSentTimestamp(messageRecord.getTimestamp()); - if (recipient.isGroupRecipient()) { - message.setGroupPublicKey(recipient.getAddress().toGroupString()); - } else { - message.setRecipient(messageRecord.getRecipient().getAddress().serialize()); - } - message.setThreadID(messageRecord.getThreadId()); - if (messageRecord.isMms()) { - MmsMessageRecord mmsMessageRecord = (MmsMessageRecord) messageRecord; - if (!mmsMessageRecord.getLinkPreviews().isEmpty()) { - message.setLinkPreview(org.session.libsession.messaging.messages.visible.LinkPreview.Companion.from(mmsMessageRecord.getLinkPreviews().get(0))); - } - if (mmsMessageRecord.getQuote() != null) { - message.setQuote(Quote.Companion.from(mmsMessageRecord.getQuote().getQuoteModel())); - } - message.addSignalAttachments(mmsMessageRecord.getSlideDeck().asAttachments()); - } - MessageSender.send(message, recipient.getAddress()); - return null; - } - }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, message); - } - - private void handleReplyMessage(final MessageRecord message) { - listener.handleReplyMessage(message); - } - - private void handleSaveAttachment(final MediaMmsMessageRecord message) { - SaveAttachmentTask.showWarningDialog(getActivity(), (dialog, which) -> { - Permissions.with(this) - .request(android.Manifest.permission.WRITE_EXTERNAL_STORAGE) - .maxSdkVersion(Build.VERSION_CODES.P) - .withPermanentDenialDialog(getString(R.string.MediaPreviewActivity_signal_needs_the_storage_permission_in_order_to_write_to_external_storage_but_it_has_been_permanently_denied)) - .onAnyDenied(() -> Toast.makeText(getContext(), R.string.MediaPreviewActivity_unable_to_write_to_external_storage_without_permission, Toast.LENGTH_LONG).show()) - .onAllGranted(() -> { - List attachments = - Stream.of(message.getSlideDeck().getSlides()) - .filter(s -> s.getUri() != null && (s.hasImage() || s.hasVideo() || s.hasAudio() || s.hasDocument())) - .map(s -> new SaveAttachmentTask.Attachment(s.getUri(), s.getContentType(), message.getDateReceived(), s.getFileName().orNull())) - .toList(); - if (!Util.isEmpty(attachments)) { - SaveAttachmentTask saveTask = new SaveAttachmentTask(getActivity()); - saveTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, attachments.toArray(new SaveAttachmentTask.Attachment[0])); - if (!message.isOutgoing()) { - sendMediaSavedNotificationIfNeeded(); - } - return; - } - - Log.w(TAG, "No slide with attachable media found, failing nicely."); - Toast.makeText(getActivity(), - getResources().getQuantityString(R.plurals.ConversationFragment_error_while_saving_attachments_to_sd_card, 1), - Toast.LENGTH_LONG).show(); - }) - .execute(); - }); - } - - private void sendMediaSavedNotificationIfNeeded() { - if (recipient.isGroupRecipient()) return; - DataExtractionNotification message = new DataExtractionNotification(new DataExtractionNotification.Kind.MediaSaved(System.currentTimeMillis())); - MessageSender.send(message, recipient.getAddress()); - } - - @Override - public @NonNull Loader onCreateLoader(int id, Bundle args) { - Log.i(TAG, "onCreateLoader"); - loaderStartTime = System.currentTimeMillis(); - - int limit = args.getInt(KEY_LIMIT, PARTIAL_CONVERSATION_LIMIT); - int offset = 0; - if (limit != 0 && startingPosition >= limit) { - offset = Math.max(startingPosition - (limit / 2) + 1, 0); - startingPosition -= offset - 1; - } - - return new ConversationLoader(getActivity(), threadId, offset, limit, lastSeen); - } - - @Override - public void onLoadFinished(@NonNull Loader cursorLoader, Cursor cursor) { - long loadTime = System.currentTimeMillis() - loaderStartTime; - int count = cursor.getCount(); - Log.i(TAG, "onLoadFinished - took " + loadTime + " ms to load a cursor of size " + count); - ConversationLoader loader = (ConversationLoader)cursorLoader; - - ConversationAdapter adapter = getListAdapter(); - if (adapter == null) { - return; - } - - if (cursor.getCount() >= PARTIAL_CONVERSATION_LIMIT && loader.hasLimit()) { - adapter.setFooterView(topLoadMoreView); - } else { - adapter.setFooterView(null); - } - - if (lastSeen == -1) { - setLastSeen(loader.getLastSeen()); - } - - if (!loader.hasSent() && !recipient.isSystemContact() && !recipient.isGroupRecipient() && recipient.getRegistered() == Recipient.RegisteredState.REGISTERED) { -// adapter.setHeaderView(unknownSenderView); - } else { - clearHeaderIfNotTyping(adapter); - } - - if (loader.hasOffset()) { - adapter.setHeaderView(bottomLoadMoreView); - } - - if (firstLoad || loader.hasOffset()) { - previousOffset = loader.getOffset(); - } - - activeOffset = loader.getOffset(); - - adapter.changeCursor(cursor); - - int lastSeenPosition = adapter.findLastSeenPosition(lastSeen); - - if (adapter.getHeaderView() == typingView) { - lastSeenPosition = Math.max(lastSeenPosition - 1, 0); - } - - if (firstLoad) { - if (startingPosition >= 0) { - scrollToStartingPosition(startingPosition); - } else { - scrollToLastSeenPosition(lastSeenPosition); - } - firstLoad = false; - } else if (previousOffset > 0) { - int scrollPosition = previousOffset + getListLayoutManager().findFirstVisibleItemPosition(); - scrollPosition = Math.min(scrollPosition, count - 1); - - View firstView = list.getLayoutManager().getChildAt(scrollPosition); - int pixelOffset = (firstView == null) ? 0 : (firstView.getBottom() - list.getPaddingBottom()); - - getListLayoutManager().scrollToPositionWithOffset(scrollPosition, pixelOffset); - previousOffset = 0; - } - - if (lastSeenPosition <= 0) { - setLastSeen(0); - } - } - - private void clearHeaderIfNotTyping(ConversationAdapter adapter) { - if (adapter.getHeaderView() != typingView) { - adapter.setHeaderView(null); - } - } - - @Override - public void onLoaderReset(@NonNull Loader arg0) { - if (list.getAdapter() != null) { - getListAdapter().changeCursor(null); - } - } - - public long stageOutgoingMessage(OutgoingMediaMessage message) { - MessageRecord messageRecord = DatabaseFactory.getMmsDatabase(getContext()).readerFor(message, threadId).getCurrent(); - - if (getListAdapter() != null) { - clearHeaderIfNotTyping(getListAdapter()); - setLastSeen(0); - getListAdapter().addFastRecord(messageRecord); - } - - return messageRecord.getId(); - } - - public long stageOutgoingMessage(OutgoingTextMessage message) { - MessageRecord messageRecord = DatabaseFactory.getSmsDatabase(getContext()).readerFor(message, threadId).getCurrent(); - - if (getListAdapter() != null) { - clearHeaderIfNotTyping(getListAdapter()); - setLastSeen(0); - getListAdapter().addFastRecord(messageRecord); - } - - return messageRecord.getId(); - } - - public void releaseOutgoingMessage(long id) { - if (getListAdapter() != null) { - getListAdapter().releaseFastRecord(id); - } - } - - private void scrollToStartingPosition(final int startingPosition) { - list.post(() -> { - list.getLayoutManager().scrollToPosition(startingPosition); - getListAdapter().pulseHighlightItem(startingPosition); - }); - } - - private void scrollToLastSeenPosition(final int lastSeenPosition) { - if (lastSeenPosition > 0) { - list.post(() -> getListLayoutManager().scrollToPositionWithOffset(lastSeenPosition, list.getHeight())); - } - } - - private boolean isAtBottom() { - if (list.getChildCount() == 0) return true; - - int firstVisiblePosition = getListLayoutManager().findFirstVisibleItemPosition(); - - if (getListAdapter().getHeaderView() == typingView) { - RecyclerView.ViewHolder item1 = list.findViewHolderForAdapterPosition(1); - return firstVisiblePosition <= 1 && item1 != null && item1.itemView.getBottom() <= list.getHeight(); - } - - return firstVisiblePosition == 0 && list.getChildAt(0).getBottom() <= list.getHeight(); - } - - public void onSearchQueryUpdated(@Nullable String query) { - if (getListAdapter() != null) { - getListAdapter().onSearchQueryUpdated(query); - } - } - - public void jumpToMessage(@NonNull Address author, long timestamp, @Nullable Runnable onMessageNotFound) { - SimpleTask.run(getLifecycle(), () -> { - return DatabaseFactory.getMmsSmsDatabase(getContext()) - .getMessagePositionInConversation(threadId, timestamp, author); - }, p -> moveToMessagePosition(p, onMessageNotFound)); - } - - private void moveToMessagePosition(int position, @Nullable Runnable onMessageNotFound) { - Log.d(TAG, "Moving to message position: " + position + " activeOffset: " + activeOffset + " cursorCount: " + getListAdapter().getCursorCount()); - - if (position >= activeOffset && position >= 0 && position < getListAdapter().getCursorCount()) { - int offset = activeOffset > 0 ? activeOffset - 1 : 0; - list.scrollToPosition(position - offset); - getListAdapter().pulseHighlightItem(position - offset); - } else if (position < 0) { - Log.w(TAG, "Tried to navigate to message, but it wasn't found."); - if (onMessageNotFound != null) { - onMessageNotFound.run(); - } - } else { - Log.i(TAG, "Message was outside of the loaded range. Need to restart the loader."); - - firstLoad = true; - startingPosition = position; - getLoaderManager().restartLoader(0, Bundle.EMPTY, ConversationFragment.this); - } - } - - public interface ConversationFragmentListener { - void setThreadId(long threadId); - void handleReplyMessage(MessageRecord messageRecord); - void onMessageActionToolbarOpened(); - void onForwardClicked(); - } - - private class ConversationScrollListener extends OnScrollListener { - - private final Animation scrollButtonInAnimation; - private final Animation scrollButtonOutAnimation; - private final ConversationDateHeader conversationDateHeader; - - private boolean wasAtBottom = true; - private boolean wasAtZoomScrollHeight = false; - private long lastPositionId = -1; - - ConversationScrollListener(@NonNull Context context) { - this.scrollButtonInAnimation = AnimationUtils.loadAnimation(context, R.anim.fade_scale_in); - this.scrollButtonOutAnimation = AnimationUtils.loadAnimation(context, R.anim.fade_scale_out); - this.conversationDateHeader = new ConversationDateHeader(context, scrollDateHeader); - - this.scrollButtonInAnimation.setDuration(100); - this.scrollButtonOutAnimation.setDuration(50); - } - - @Override - public void onScrolled(@NonNull final RecyclerView rv, final int dx, final int dy) { - boolean currentlyAtBottom = isAtBottom(); - boolean currentlyAtZoomScrollHeight = isAtZoomScrollHeight(); - int positionId = getHeaderPositionId(); - - if (currentlyAtBottom && !wasAtBottom) { - ViewUtil.fadeOut(composeDivider, 50, View.INVISIBLE); - ViewUtil.animateOut(scrollToBottomButton, scrollButtonOutAnimation, View.INVISIBLE); - } else if (!currentlyAtBottom && wasAtBottom) { - ViewUtil.fadeIn(composeDivider, 500); - } - - if (currentlyAtZoomScrollHeight && !wasAtZoomScrollHeight) { - ViewUtil.animateIn(scrollToBottomButton, scrollButtonInAnimation); - } - - if (positionId != lastPositionId) { - bindScrollHeader(conversationDateHeader, positionId); - } - - wasAtBottom = currentlyAtBottom; - wasAtZoomScrollHeight = currentlyAtZoomScrollHeight; - lastPositionId = positionId; - } - - @Override - public void onScrollStateChanged(@NonNull RecyclerView recyclerView, int newState) { - if (newState == RecyclerView.SCROLL_STATE_DRAGGING) { - conversationDateHeader.show(); - } else if (newState == RecyclerView.SCROLL_STATE_IDLE) { - conversationDateHeader.hide(); - } - } - - private boolean isAtZoomScrollHeight() { - return getListLayoutManager().findFirstCompletelyVisibleItemPosition() > 4; - } - - private int getHeaderPositionId() { - return getListLayoutManager().findLastVisibleItemPosition(); - } - - private void bindScrollHeader(HeaderViewHolder headerViewHolder, int positionId) { - if (((ConversationAdapter)list.getAdapter()).getHeaderId(positionId) != -1) { - ((ConversationAdapter) list.getAdapter()).onBindHeaderViewHolder(headerViewHolder, positionId); - } - } - } - - private class ConversationFragmentItemClickListener implements ItemClickListener { - - @Override - public void onItemClick(MessageRecord messageRecord) { - if (messageRecord.isUpdate()) return; - if (actionMode != null) { - ((ConversationAdapter) list.getAdapter()).toggleSelection(messageRecord); - list.getAdapter().notifyDataSetChanged(); - - if (getListAdapter().getSelectedItems().size() == 0) { - actionMode.finish(); - } else { - setCorrectMenuVisibility(actionMode.getMenu()); - actionMode.setTitle(String.valueOf(getListAdapter().getSelectedItems().size())); - } - } - } - - @Override - public void onItemLongClick(MessageRecord messageRecord) { - if (messageRecord.isUpdate()) return; - if (actionMode == null) { - ((ConversationAdapter) list.getAdapter()).toggleSelection(messageRecord); - list.getAdapter().notifyDataSetChanged(); - - actionMode = ((AppCompatActivity)getActivity()).startSupportActionMode(actionModeCallback); - - View titleTextView = (getActivity().findViewById(R.id.action_bar_title)); - if (titleTextView != null) { - titleTextView.setBackgroundColor(getResources().getColor(R.color.transparent)); - ViewParent titleTextViewContainerView = titleTextView.getParent(); - if (titleTextViewContainerView != null) { - ((View)titleTextViewContainerView).setBackgroundColor(getResources().getColor(R.color.transparent)); - } - } - } - } - - @Override - public void onQuoteClicked(MmsMessageRecord messageRecord) { - if (messageRecord.getQuote() == null) { - Log.w(TAG, "Received a 'quote clicked' event, but there's no quote..."); - return; - } - - if (messageRecord.getQuote().isOriginalMissing()) { - Log.i(TAG, "Clicked on a quote whose original message we never had."); - Toast.makeText(getContext(), R.string.ConversationFragment_quoted_message_not_found, Toast.LENGTH_SHORT).show(); - return; - } - - SimpleTask.run(getLifecycle(), () -> { - return DatabaseFactory.getMmsSmsDatabase(getContext()) - .getQuotedMessagePosition(threadId, - messageRecord.getQuote().getId(), - messageRecord.getQuote().getAuthor()); - }, p -> moveToMessagePosition(p, () -> { - Toast.makeText(getContext(), R.string.ConversationFragment_quoted_message_no_longer_available, Toast.LENGTH_SHORT).show(); - })); - } - - @Override - public void onLinkPreviewClicked(@NonNull LinkPreview linkPreview) { - if (getContext() != null && getActivity() != null) { - CommunicationActions.openBrowserLink(getActivity(), linkPreview.getUrl()); - } - } - - @Override - public void onMoreTextClicked(@NonNull Address conversationAddress, long messageId, boolean isMms) { - if (getContext() != null && getActivity() != null) { - startActivity(LongMessageActivity.getIntent(getContext(), conversationAddress, messageId, isMms)); - } - } - } - - private class ActionModeCallback implements ActionMode.Callback { - - private int statusBarColor; - - @Override - public boolean onCreateActionMode(ActionMode mode, Menu menu) { - MenuInflater inflater = mode.getMenuInflater(); - inflater.inflate(R.menu.conversation_context, menu); - - mode.setTitle("1"); - - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { - Window window = getActivity().getWindow(); - statusBarColor = window.getStatusBarColor(); - } - - setCorrectMenuVisibility(menu); - listener.onMessageActionToolbarOpened(); - return true; - } - - @Override - public boolean onPrepareActionMode(ActionMode actionMode, Menu menu) { - return false; - } - - @Override - public void onDestroyActionMode(ActionMode mode) { - ((ConversationAdapter)list.getAdapter()).clearSelection(); - list.getAdapter().notifyDataSetChanged(); - - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { - getActivity().getWindow().setStatusBarColor(statusBarColor); - } - - actionMode = null; - } - - @Override - public boolean onActionItemClicked(ActionMode mode, MenuItem item) { - switch(item.getItemId()) { - case R.id.menu_context_copy: - handleCopyMessage(getListAdapter().getSelectedItems()); - actionMode.finish(); - return true; - case R.id.menu_context_copy_public_key: - handleCopyPublicKey((MessageRecord) getListAdapter().getSelectedItems().toArray()[0]); - actionMode.finish(); - return true; - case R.id.menu_context_delete_message: - handleDeleteMessages(getListAdapter().getSelectedItems()); - actionMode.finish(); - return true; - case R.id.menu_context_ban_user: - handleBanUser(getListAdapter().getSelectedItems()); - return true; - case R.id.menu_context_details: - handleDisplayDetails(getSelectedMessageRecord()); - actionMode.finish(); - return true; -// case R.id.menu_context_forward: -// handleForwardMessage(getSelectedMessageRecord()); -// actionMode.finish(); -// return true; - case R.id.menu_context_resend: - handleResendMessage(getSelectedMessageRecord()); - actionMode.finish(); - return true; - case R.id.menu_context_save_attachment: - handleSaveAttachment((MediaMmsMessageRecord)getSelectedMessageRecord()); - actionMode.finish(); - return true; - case R.id.menu_context_reply: - handleReplyMessage(getSelectedMessageRecord()); - actionMode.finish(); - return true; - } - - return false; - } - } - - private static class ConversationDateHeader extends HeaderViewHolder { - - private final Animation animateIn; - private final Animation animateOut; - - private boolean pendingHide = false; - - private ConversationDateHeader(Context context, TextView textView) { - super(textView); - this.animateIn = AnimationUtils.loadAnimation(context, R.anim.slide_from_top); - this.animateOut = AnimationUtils.loadAnimation(context, R.anim.slide_to_top); - - this.animateIn.setDuration(100); - this.animateOut.setDuration(100); - } - - public void show() { - if (pendingHide) { - pendingHide = false; - } else { - ViewUtil.animateIn(textView, animateIn); - } - } - - public void hide() { - pendingHide = true; - - textView.postDelayed(new Runnable() { - @Override - public void run() { - if (pendingHide) { - pendingHide = false; - ViewUtil.animateOut(textView, animateOut, View.GONE); - } - } - }, 400); - } - } -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationItem.java b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationItem.java deleted file mode 100644 index 344dfc93ee..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationItem.java +++ /dev/null @@ -1,1208 +0,0 @@ -/* - * Copyright (C) 2011 Whisper Systems - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ -package org.thoughtcrime.securesms.conversation; - -import android.annotation.SuppressLint; -import android.content.ActivityNotFoundException; -import android.content.Context; -import android.content.Intent; -import android.content.res.TypedArray; -import android.graphics.Color; -import android.graphics.PorterDuff; -import android.graphics.Typeface; -import android.net.Uri; -import android.text.Spannable; -import android.text.SpannableString; -import android.text.SpannableStringBuilder; -import android.text.Spanned; -import android.text.TextPaint; -import android.text.TextUtils; -import android.text.style.BackgroundColorSpan; -import android.text.style.CharacterStyle; -import android.text.style.ClickableSpan; -import android.text.style.ForegroundColorSpan; -import android.text.style.URLSpan; -import android.text.util.Linkify; -import android.util.AttributeSet; -import android.util.TypedValue; -import android.view.View; -import android.view.ViewGroup; -import android.widget.ImageView; -import android.widget.LinearLayout; -import android.widget.TextView; -import android.widget.Toast; - -import androidx.annotation.DimenRes; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; - -import com.annimon.stream.Stream; - -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.open_groups.OpenGroupAPIV2; -import org.session.libsession.messaging.open_groups.OpenGroupV2; -import org.session.libsession.messaging.sending_receiving.attachments.AttachmentTransferProgress; -import org.session.libsession.messaging.sending_receiving.attachments.DatabaseAttachment; -import org.session.libsession.messaging.sending_receiving.link_preview.LinkPreview; -import org.session.libsession.messaging.utilities.UpdateMessageData; -import org.session.libsession.utilities.Stub; -import org.session.libsession.utilities.TextSecurePreferences; -import org.session.libsession.utilities.ThemeUtil; -import org.session.libsession.utilities.Util; -import org.session.libsession.utilities.ViewUtil; -import org.session.libsession.utilities.recipients.Recipient; -import org.session.libsession.utilities.recipients.RecipientModifiedListener; -import org.session.libsignal.utilities.Log; -import org.session.libsignal.utilities.guava.Optional; -import org.thoughtcrime.securesms.BindableConversationItem; -import org.thoughtcrime.securesms.MediaPreviewActivity; -import org.thoughtcrime.securesms.MessageDetailsActivity; -import org.thoughtcrime.securesms.components.ConversationItemAlertView; -import org.thoughtcrime.securesms.components.ConversationItemFooter; -import org.thoughtcrime.securesms.components.ConversationItemThumbnail; -import org.thoughtcrime.securesms.components.DocumentView; -import org.thoughtcrime.securesms.components.LinkPreviewView; -import org.thoughtcrime.securesms.components.QuoteView; -import org.thoughtcrime.securesms.components.StickerView; -import org.thoughtcrime.securesms.components.emoji.EmojiTextView; -import org.thoughtcrime.securesms.database.DatabaseFactory; -import org.thoughtcrime.securesms.database.MmsSmsDatabase; -import org.thoughtcrime.securesms.database.model.MediaMmsMessageRecord; -import org.thoughtcrime.securesms.database.model.MessageRecord; -import org.thoughtcrime.securesms.database.model.MmsMessageRecord; -import org.thoughtcrime.securesms.database.model.Quote; -import org.thoughtcrime.securesms.linkpreview.LinkPreviewUtil; -import org.thoughtcrime.securesms.loki.utilities.MentionUtilities; -import org.thoughtcrime.securesms.loki.views.MessageAudioView; -import org.thoughtcrime.securesms.loki.views.OpenGroupInvitationView; -import org.thoughtcrime.securesms.loki.views.ProfilePictureView; -import org.thoughtcrime.securesms.mms.GlideRequests; -import org.thoughtcrime.securesms.mms.ImageSlide; -import org.thoughtcrime.securesms.mms.PartAuthority; -import org.thoughtcrime.securesms.mms.Slide; -import org.thoughtcrime.securesms.mms.SlideClickListener; -import org.thoughtcrime.securesms.mms.SlidesClickedListener; -import org.thoughtcrime.securesms.mms.TextSlide; -import org.thoughtcrime.securesms.util.DateUtils; -import org.thoughtcrime.securesms.util.LongClickCopySpan; -import org.thoughtcrime.securesms.util.LongClickMovementMethod; -import org.thoughtcrime.securesms.util.SearchUtil; - -import java.util.Collections; -import java.util.HashSet; -import java.util.List; -import java.util.Locale; -import java.util.Set; - -import network.loki.messenger.R; - -/** - * A view that displays an individual conversation item within a conversation - * thread. Used by ComposeMessageActivity's ListActivity via a ConversationAdapter. - * - * @author Moxie Marlinspike - * - */ - -public class ConversationItem extends LinearLayout - implements RecipientModifiedListener, BindableConversationItem -{ - private static final String TAG = ConversationItem.class.getSimpleName(); - - private static final int MAX_MEASURE_CALLS = 3; - private static final int MAX_BODY_DISPLAY_LENGTH = 1000; - - private MessageRecord messageRecord; - private Locale locale; - private boolean groupThread; - private Recipient recipient; - private GlideRequests glideRequests; - - protected ViewGroup bodyBubble; - private QuoteView quoteView; - private EmojiTextView bodyText; - private ConversationItemFooter footer; - private ConversationItemFooter stickerFooter; - private TextView groupSender; - private TextView groupSenderProfileName; - private View groupSenderHolder; - private ProfilePictureView profilePictureView; - private ImageView moderatorIconImageView; - private ViewGroup contactPhotoHolder; - private ConversationItemAlertView alertView; - private ViewGroup container; - - private @NonNull Set batchSelected = new HashSet<>(); - private Recipient conversationRecipient; - private Stub mediaThumbnailStub; - private Stub audioViewStub; - private Stub documentViewStub; - private Stub linkPreviewStub; - private Stub stickerStub; - private Stub openGroupInvitationViewStub; - private @Nullable EventListener eventListener; - - private int defaultBubbleColor; - private int measureCalls; - - private final PassthroughClickListener passthroughClickListener = new PassthroughClickListener(); - private final AttachmentDownloadClickListener downloadClickListener = new AttachmentDownloadClickListener(); - private final SlideClickPassthroughListener singleDownloadClickListener = new SlideClickPassthroughListener(downloadClickListener); - private final LinkPreviewClickListener linkPreviewClickListener = new LinkPreviewClickListener(); - - private final Context context; - - public ConversationItem(Context context) { - this(context, null); - } - - public ConversationItem(Context context, AttributeSet attrs) { - super(context, attrs); - this.context = context; - } - - @Override - public void setOnClickListener(OnClickListener l) { - super.setOnClickListener(new ClickListener(l)); - } - - @Override - protected void onFinishInflate() { - super.onFinishInflate(); - - initializeAttributes(); - - this.bodyText = findViewById(R.id.conversation_item_body); - this.footer = findViewById(R.id.conversation_item_footer); - this.stickerFooter = findViewById(R.id.conversation_item_sticker_footer); - this.groupSender = findViewById(R.id.group_message_sender); - this.groupSenderProfileName = findViewById(R.id.group_message_sender_profile); - this.alertView = findViewById(R.id.indicators_parent); - this.profilePictureView = findViewById(R.id.profilePictureView); - this.moderatorIconImageView = findViewById(R.id.moderator_icon_image_view); - this.contactPhotoHolder = findViewById(R.id.contact_photo_container); - this.bodyBubble = findViewById(R.id.body_bubble); - this.mediaThumbnailStub = new Stub<>(findViewById(R.id.image_view_stub)); - this.audioViewStub = new Stub<>(findViewById(R.id.audio_view_stub)); - this.documentViewStub = new Stub<>(findViewById(R.id.document_view_stub)); - this.linkPreviewStub = new Stub<>(findViewById(R.id.link_preview_stub)); - this.stickerStub = new Stub<>(findViewById(R.id.sticker_view_stub)); - this.openGroupInvitationViewStub = new Stub<>(findViewById(R.id.open_group_invitation_stub)); - this.groupSenderHolder = findViewById(R.id.group_sender_holder); - this.quoteView = findViewById(R.id.quote_view); - this.container = findViewById(R.id.container); - - setOnClickListener(new ClickListener(null)); - - bodyText.setOnLongClickListener(passthroughClickListener); - bodyText.setOnClickListener(passthroughClickListener); - - bodyText.setMovementMethod(LongClickMovementMethod.getInstance(getContext())); - } - - @Override - public void bind(@NonNull MessageRecord messageRecord, - @NonNull Optional previousMessageRecord, - @NonNull Optional nextMessageRecord, - @NonNull GlideRequests glideRequests, - @NonNull Locale locale, - @NonNull Set batchSelected, - @NonNull Recipient conversationRecipient, - @Nullable String searchQuery, - boolean pulseHighlight) - { - this.messageRecord = messageRecord; - this.locale = locale; - this.glideRequests = glideRequests; - this.batchSelected = batchSelected; - this.conversationRecipient = conversationRecipient; - this.groupThread = conversationRecipient.isGroupRecipient(); - this.recipient = messageRecord.getIndividualRecipient(); - - this.recipient.addListener(this); - this.conversationRecipient.addListener(this); - - setGutterSizes(messageRecord, groupThread); - setMessageShape(messageRecord, previousMessageRecord, nextMessageRecord, groupThread); - setMediaAttributes(messageRecord, previousMessageRecord, nextMessageRecord, conversationRecipient, groupThread); - setInteractionState(messageRecord, pulseHighlight); - setBodyText(messageRecord, searchQuery, groupThread); - setBubbleState(messageRecord); - setStatusIcons(messageRecord); - setContactPhoto(recipient); - setGroupMessageStatus(messageRecord, recipient); - setGroupAuthorColor(messageRecord); - setAuthor(messageRecord, previousMessageRecord, nextMessageRecord, groupThread); - setQuote(messageRecord, previousMessageRecord, nextMessageRecord, groupThread); - setMessageSpacing(context, messageRecord, previousMessageRecord, nextMessageRecord, groupThread); - setFooter(messageRecord, nextMessageRecord, locale, groupThread); - adjustMarginsIfNeeded(messageRecord); - } - - @Override - public void setEventListener(@Nullable EventListener eventListener) { - this.eventListener = eventListener; - } - - @Override - protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { - super.onMeasure(widthMeasureSpec, heightMeasureSpec); - - if (isInEditMode()) { - return; - } - - boolean needsMeasure = false; - - if (hasQuote(messageRecord)) { - int quoteWidth = quoteView.getMeasuredWidth(); - int availableWidth = getAvailableMessageBubbleWidth(quoteView); - - if (quoteWidth != availableWidth) { - quoteView.getLayoutParams().width = availableWidth; - needsMeasure = true; - } - } - - if (hasThumbnail(messageRecord) && messageRecord.getDisplayBody(context).length() > 0) { - ViewUtil.updateLayoutParams(bodyText, getAvailableMessageBubbleWidth(bodyText), ViewGroup.LayoutParams.WRAP_CONTENT); - } - - ConversationItemFooter activeFooter = getActiveFooter(messageRecord); - int availableWidth = getAvailableMessageBubbleWidth(footer); - - if (activeFooter.getVisibility() != GONE && activeFooter.getMeasuredWidth() != availableWidth) { - activeFooter.getLayoutParams().width = availableWidth; - needsMeasure = true; - } - - if (needsMeasure) { - if (measureCalls < MAX_MEASURE_CALLS) { - measureCalls++; - measure(widthMeasureSpec, heightMeasureSpec); - } else { - Log.w(TAG, "Hit measure() cap of " + MAX_MEASURE_CALLS); - } - } else { - measureCalls = 0; - } - } - - private int getAvailableMessageBubbleWidth(@NonNull View forView) { - int availableWidth; - if (hasAudio(messageRecord)) { - availableWidth = audioViewStub.get().getMeasuredWidth() + ViewUtil.getLeftMargin(audioViewStub.get()) + ViewUtil.getRightMargin(audioViewStub.get()); - } else if (hasThumbnail(messageRecord) || hasBigImageLinkPreview(messageRecord)) { - availableWidth = mediaThumbnailStub.get().getMeasuredWidth(); - } else { - availableWidth = bodyBubble.getMeasuredWidth() - bodyBubble.getPaddingLeft() - bodyBubble.getPaddingRight(); - } - - availableWidth -= ViewUtil.getLeftMargin(forView) + ViewUtil.getRightMargin(forView); - - return availableWidth; - } - - private void initializeAttributes() { - final int[] attributes = new int[] {R.attr.conversation_item_bubble_background}; - final TypedArray attrs = context.obtainStyledAttributes(attributes); - - defaultBubbleColor = attrs.getColor(0, Color.WHITE); - attrs.recycle(); - } - - @Override - public void unbind() { - if (recipient != null) { - recipient.removeListener(this); - } - if (profilePictureView != null) { - profilePictureView.recycle(); - } - } - - public MessageRecord getMessageRecord() { - return messageRecord; - } - - /// MessageRecord Attribute Parsers - - private void setBubbleState(MessageRecord messageRecord) { - int bubbleColor = ThemeUtil.getThemedColor(getContext(), messageRecord.isOutgoing() ? - R.attr.message_sent_background_color : - R.attr.message_received_background_color); - bodyBubble.getBackground().setColorFilter(bubbleColor, PorterDuff.Mode.MULTIPLY); - - if (audioViewStub.resolved()) { - setAudioViewTint(messageRecord, this.conversationRecipient); - } - } - - private void setAudioViewTint(MessageRecord messageRecord, Recipient recipient) { -// audioViewStub.get().setTint(Color.WHITE, getResources().getColor(R.color.action_bar_background)); - } - - private void setInteractionState(MessageRecord messageRecord, boolean pulseHighlight) { - if (batchSelected.contains(messageRecord)) { - setBackgroundResource(R.drawable.conversation_item_background); - setSelected(true); - } else if (pulseHighlight) { - setBackgroundResource(R.drawable.conversation_item_background_animated); - setSelected(true); - postDelayed(() -> setSelected(false), 500); - } else { - setSelected(false); - } - - if (mediaThumbnailStub.resolved()) { - mediaThumbnailStub.get().setFocusable(!shouldInterceptClicks(messageRecord) && batchSelected.isEmpty()); - mediaThumbnailStub.get().setClickable(!shouldInterceptClicks(messageRecord) && batchSelected.isEmpty()); - mediaThumbnailStub.get().setLongClickable(batchSelected.isEmpty()); - } - - if (audioViewStub.resolved()) { - audioViewStub.get().setFocusable(!shouldInterceptClicks(messageRecord) && batchSelected.isEmpty()); - audioViewStub.get().setClickable(batchSelected.isEmpty()); - audioViewStub.get().setEnabled(batchSelected.isEmpty()); - } - - if (documentViewStub.resolved()) { - documentViewStub.get().setFocusable(!shouldInterceptClicks(messageRecord) && batchSelected.isEmpty()); - documentViewStub.get().setClickable(batchSelected.isEmpty()); - } - } - - private boolean isCaptionlessMms(MessageRecord messageRecord) { - return TextUtils.isEmpty(messageRecord.getDisplayBody(getContext())) && messageRecord.isMms() && ((MmsMessageRecord) messageRecord).getSlideDeck().getTextSlide() == null; - } - - private boolean hasAudio(MessageRecord messageRecord) { - return messageRecord.isMms() && ((MmsMessageRecord)messageRecord).getSlideDeck().getAudioSlide() != null; - } - - private boolean hasThumbnail(MessageRecord messageRecord) { - return messageRecord.isMms() && ((MmsMessageRecord)messageRecord).getSlideDeck().getThumbnailSlide() != null; - } - - private boolean hasOnlyThumbnail(MessageRecord messageRecord) { - return hasThumbnail(messageRecord) && - !hasAudio(messageRecord) && - !hasDocument(messageRecord); - } - - private boolean hasOnlyDocument(MessageRecord messageRecord) { - return messageRecord.getBody().length() == 0 && - !hasThumbnail(messageRecord) && - !hasAudio(messageRecord) && - hasDocument(messageRecord) && - !hasQuote(messageRecord); - } - - private boolean hasOnlyText(MessageRecord messageRecord) { - return messageRecord.getBody().length() != 0 && - !hasThumbnail(messageRecord) && - !hasAudio(messageRecord) && - !hasDocument(messageRecord) && - !hasQuote(messageRecord); - } - - private boolean hasDocument(MessageRecord messageRecord) { - return messageRecord.isMms() && ((MmsMessageRecord)messageRecord).getSlideDeck().getDocumentSlide() != null; - } - - private boolean hasExtraText(MessageRecord messageRecord) { - boolean hasTextSlide = messageRecord.isMms() && ((MmsMessageRecord)messageRecord).getSlideDeck().getTextSlide() != null; - boolean hasOverflowText = messageRecord.getBody().length() > MAX_BODY_DISPLAY_LENGTH; - - return hasTextSlide || hasOverflowText; - } - - private boolean hasQuote(MessageRecord messageRecord) { - return messageRecord.isMms() && ((MmsMessageRecord)messageRecord).getQuote() != null; - } - - private boolean hasLinkPreview(MessageRecord messageRecord) { - return messageRecord.isMms() && !((MmsMessageRecord)messageRecord).getLinkPreviews().isEmpty(); - } - - private boolean hasBigImageLinkPreview(MessageRecord messageRecord) { - if (!hasLinkPreview(messageRecord)) return false; - - LinkPreview linkPreview = ((MmsMessageRecord) messageRecord).getLinkPreviews().get(0); - int minWidth = getResources().getDimensionPixelSize(R.dimen.media_bubble_min_width); - - return linkPreview.getThumbnail().isPresent() && - linkPreview.getThumbnail().get().getWidth() >= minWidth; - } - - private void setBodyText(MessageRecord messageRecord, @Nullable String searchQuery, boolean isGroupThread) { - bodyText.setClickable(false); - bodyText.setFocusable(false); - bodyText.setTextSize(TypedValue.COMPLEX_UNIT_SP, TextSecurePreferences.getMessageBodyTextSize(context)); - if (isCaptionlessMms(messageRecord)) { - bodyText.setVisibility(View.GONE); - } else { - Spannable text = MentionUtilities.highlightMentions(linkifyMessageBody(messageRecord.getDisplayBody(context), batchSelected.isEmpty()), messageRecord.isOutgoing(), messageRecord.getThreadId(), context); - text = SearchUtil.getHighlightedSpan(locale, () -> new BackgroundColorSpan(Color.WHITE), text, searchQuery); - text = SearchUtil.getHighlightedSpan(locale, () -> new ForegroundColorSpan(Color.BLACK), text, searchQuery); - - if (hasExtraText(messageRecord)) { - bodyText.setOverflowText(getLongMessageSpan(messageRecord)); - } else { - bodyText.setOverflowText(null); - } - - if (!messageRecord.isOpenGroupInvitation()) - bodyText.setText(text); - - bodyText.setVisibility(View.VISIBLE); - } - } - - private void adjustMarginsIfNeeded(MessageRecord messageRecord) { - LinearLayout.LayoutParams bodyTextLayoutParams = (LinearLayout.LayoutParams)bodyText.getLayoutParams(); - bodyTextLayoutParams.topMargin = 0; - if (hasOnlyThumbnail(messageRecord) || hasLinkPreview(messageRecord)) { - int topPadding = 0; - if (groupSenderHolder.getVisibility() == VISIBLE) { - topPadding = (int)getResources().getDimension(R.dimen.medium_spacing); - } - int bottomPadding = 0; - if (messageRecord.getBody().length() > 0) { - bodyTextLayoutParams.topMargin = (int)getResources().getDimension(R.dimen.medium_spacing); - bottomPadding = (int)getResources().getDimension(R.dimen.medium_spacing); - } - bodyBubble.setPadding(0, topPadding, 0, bottomPadding); - } else { - bodyBubble.setPadding(0, (int)getResources().getDimension(R.dimen.medium_spacing), 0, (int)getResources().getDimension(R.dimen.medium_spacing)); - } - bodyText.setLayoutParams(bodyTextLayoutParams); - LinearLayout.LayoutParams senderHolderLayoutParams = (LinearLayout.LayoutParams)groupSenderHolder.getLayoutParams(); - if (groupSenderHolder.getVisibility() == VISIBLE && hasOnlyText(messageRecord)) { - senderHolderLayoutParams.bottomMargin = (int)(getResources().getDisplayMetrics().density * 4); - } else { - senderHolderLayoutParams.bottomMargin = (int)getResources().getDimension(R.dimen.medium_spacing); - } - groupSenderHolder.setLayoutParams(senderHolderLayoutParams); - if (documentViewStub.resolved()) { - LinearLayout.LayoutParams documentViewLayoutParams = (LinearLayout.LayoutParams)documentViewStub.get().getLayoutParams(); - int bottomMargin = 0; - if (hasOnlyDocument(messageRecord)) { - if (footer.getVisibility() == VISIBLE) { - bottomMargin = (int)(4 * getResources().getDisplayMetrics().density); - } else { - bottomMargin = (int)(-4 * getResources().getDisplayMetrics().density); - } - } else { - bottomMargin = (int)(4 * getResources().getDisplayMetrics().density); - } - documentViewLayoutParams.bottomMargin = bottomMargin; - documentViewStub.get().setLayoutParams(documentViewLayoutParams); - } - } - - private void setMediaAttributes(@NonNull MessageRecord messageRecord, - @NonNull Optional previousRecord, - @NonNull Optional nextRecord, - @NonNull Recipient conversationRecipient, - boolean isGroupThread) - { - boolean showControls = !messageRecord.isFailed(); - - if (hasLinkPreview(messageRecord)) { - linkPreviewStub.get().setVisibility(View.VISIBLE); - if (audioViewStub.resolved()) audioViewStub.get().setVisibility(View.GONE); - if (mediaThumbnailStub.resolved()) mediaThumbnailStub.get().setVisibility(View.GONE); - if (documentViewStub.resolved()) documentViewStub.get().setVisibility(View.GONE); - if (stickerStub.resolved()) stickerStub.get().setVisibility(View.GONE); - if (openGroupInvitationViewStub.resolved()) openGroupInvitationViewStub.get().setVisibility(View.GONE); - - //noinspection ConstantConditions - LinkPreview linkPreview = ((MmsMessageRecord) messageRecord).getLinkPreviews().get(0); - - if (hasBigImageLinkPreview(messageRecord)) { - mediaThumbnailStub.get().setVisibility(VISIBLE); - mediaThumbnailStub.get().setImageResource(glideRequests, Collections.singletonList(new ImageSlide(context, linkPreview.getThumbnail().get())), showControls, false); - mediaThumbnailStub.get().setThumbnailClickListener(new LinkPreviewThumbnailClickListener()); - mediaThumbnailStub.get().setDownloadClickListener(downloadClickListener); - mediaThumbnailStub.get().setOnLongClickListener(passthroughClickListener); - - linkPreviewStub.get().setLinkPreview(glideRequests, linkPreview, false, false); - - setThumbnailCorners(messageRecord, previousRecord, nextRecord, isGroupThread); - setLinkPreviewCorners(messageRecord, previousRecord, nextRecord, isGroupThread, true); - - ViewUtil.updateLayoutParams(bodyText, ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT); - ViewUtil.updateLayoutParams(groupSenderHolder, ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT); - } else { - linkPreviewStub.get().setLinkPreview(glideRequests, linkPreview, true, false); - linkPreviewStub.get().setDownloadClickedListener(downloadClickListener); - setLinkPreviewCorners(messageRecord, previousRecord, nextRecord, isGroupThread, false); - ViewUtil.updateLayoutParams(bodyText, ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT); - ViewUtil.updateLayoutParams(groupSenderHolder, ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT); - } - - linkPreviewStub.get().setOnClickListener(linkPreviewClickListener); - linkPreviewStub.get().setOnLongClickListener(passthroughClickListener); - - footer.setVisibility(VISIBLE); - } else if (hasAudio(messageRecord)) { - audioViewStub.get().setVisibility(View.VISIBLE); - if (mediaThumbnailStub.resolved()) mediaThumbnailStub.get().setVisibility(View.GONE); - if (documentViewStub.resolved()) documentViewStub.get().setVisibility(View.GONE); - if (linkPreviewStub.resolved()) linkPreviewStub.get().setVisibility(GONE); - if (stickerStub.resolved()) stickerStub.get().setVisibility(View.GONE); - if (openGroupInvitationViewStub.resolved()) openGroupInvitationViewStub.get().setVisibility(View.GONE); - - //noinspection ConstantConditions - audioViewStub.get().setAudio(((MediaMmsMessageRecord) messageRecord).getSlideDeck().getAudioSlide(), showControls); - audioViewStub.get().setDownloadClickListener(singleDownloadClickListener); - audioViewStub.get().setOnLongClickListener(passthroughClickListener); - - ViewUtil.updateLayoutParams(bodyText, ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT); - ViewUtil.updateLayoutParams(groupSenderHolder, ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT); - - footer.setVisibility(VISIBLE); - } else if (hasDocument(messageRecord)) { - documentViewStub.get().setVisibility(View.VISIBLE); - if (mediaThumbnailStub.resolved()) mediaThumbnailStub.get().setVisibility(View.GONE); - if (audioViewStub.resolved()) audioViewStub.get().setVisibility(View.GONE); - if (linkPreviewStub.resolved()) linkPreviewStub.get().setVisibility(GONE); - if (stickerStub.resolved()) stickerStub.get().setVisibility(View.GONE); - if (openGroupInvitationViewStub.resolved()) openGroupInvitationViewStub.get().setVisibility(View.GONE); - - //noinspection ConstantConditions - documentViewStub.get().setDocument(((MediaMmsMessageRecord) messageRecord).getSlideDeck().getDocumentSlide(), showControls); - documentViewStub.get().setDocumentClickListener(new ThumbnailClickListener()); - documentViewStub.get().setDownloadClickListener(singleDownloadClickListener); - documentViewStub.get().setOnLongClickListener(passthroughClickListener); - - ViewUtil.updateLayoutParams(bodyText, ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT); - ViewUtil.updateLayoutParams(groupSenderHolder, ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT); - - footer.setVisibility(VISIBLE); - } else if (hasThumbnail(messageRecord)) { - mediaThumbnailStub.get().setVisibility(View.VISIBLE); - if (audioViewStub.resolved()) audioViewStub.get().setVisibility(View.GONE); - if (documentViewStub.resolved()) documentViewStub.get().setVisibility(View.GONE); - if (linkPreviewStub.resolved()) linkPreviewStub.get().setVisibility(GONE); - if (stickerStub.resolved()) stickerStub.get().setVisibility(View.GONE); - if (openGroupInvitationViewStub.resolved()) openGroupInvitationViewStub.get().setVisibility(View.GONE); - - //noinspection ConstantConditions - List thumbnailSlides = ((MmsMessageRecord) messageRecord).getSlideDeck().getThumbnailSlides(); - mediaThumbnailStub.get().setImageResource(glideRequests, - thumbnailSlides, - showControls, - false); - mediaThumbnailStub.get().setThumbnailClickListener(new ThumbnailClickListener()); - mediaThumbnailStub.get().setDownloadClickListener(downloadClickListener); - mediaThumbnailStub.get().setOnLongClickListener(passthroughClickListener); - mediaThumbnailStub.get().setOnClickListener(passthroughClickListener); - mediaThumbnailStub.get().showShade(TextUtils.isEmpty(messageRecord.getDisplayBody(getContext())) && !hasExtraText(messageRecord)); - mediaThumbnailStub.get().setConversationColor(messageRecord.isOutgoing() ? defaultBubbleColor - : messageRecord.getRecipient().getColor().toConversationColor(context)); - mediaThumbnailStub.get().setBorderless(false); - - setThumbnailCorners(messageRecord, previousRecord, nextRecord, isGroupThread); - - ViewUtil.updateLayoutParams(bodyText, ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT); - ViewUtil.updateLayoutParams(groupSenderHolder, ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT); - - footer.setVisibility(VISIBLE); - } else if (messageRecord.isOpenGroupInvitation()) { - openGroupInvitationViewStub.get().setVisibility(View.VISIBLE); - if (mediaThumbnailStub.resolved()) mediaThumbnailStub.get().setVisibility(View.GONE); - if (audioViewStub.resolved()) audioViewStub.get().setVisibility(View.GONE); - if (documentViewStub.resolved()) documentViewStub.get().setVisibility(View.GONE); - if (linkPreviewStub.resolved()) linkPreviewStub.get().setVisibility(GONE); - if (stickerStub.resolved()) stickerStub.get().setVisibility(View.GONE); - - UpdateMessageData updateMessageData = UpdateMessageData.Companion.fromJSON(messageRecord.getBody()); - String name = null, url = null; - if (updateMessageData.getKind() instanceof UpdateMessageData.Kind.OpenGroupInvitation) { - UpdateMessageData.Kind.OpenGroupInvitation data = (UpdateMessageData.Kind.OpenGroupInvitation)updateMessageData.getKind(); - name = data.getGroupName(); - url = data.getGroupUrl(); - } - - openGroupInvitationViewStub.get().setOpenGroup(name, url, messageRecord.isOutgoing()); - openGroupInvitationViewStub.get().setOnLongClickListener(passthroughClickListener); - - bodyText.setVisibility(View.GONE); - - ViewUtil.updateLayoutParams(bodyText, ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT); - ViewUtil.updateLayoutParams(groupSenderHolder, ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT); - } else { - if (mediaThumbnailStub.resolved()) mediaThumbnailStub.get().setVisibility(View.GONE); - if (audioViewStub.resolved()) audioViewStub.get().setVisibility(View.GONE); - if (documentViewStub.resolved()) documentViewStub.get().setVisibility(View.GONE); - if (linkPreviewStub.resolved()) linkPreviewStub.get().setVisibility(GONE); - if (stickerStub.resolved()) stickerStub.get().setVisibility(View.GONE); - - ViewUtil.updateLayoutParams(bodyText, ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT); - ViewUtil.updateLayoutParams(groupSenderHolder, ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT); - - footer.setVisibility(VISIBLE); - } - } - - private void setThumbnailCorners(@NonNull MessageRecord current, - @NonNull Optional previous, - @NonNull Optional next, - boolean isGroupThread) - { - int defaultRadius = readDimen(R.dimen.message_corner_radius); - int collapseRadius = readDimen(R.dimen.message_corner_collapse_radius); - - int topLeft = defaultRadius; - int topRight = defaultRadius; - int bottomLeft = defaultRadius; - int bottomRight = defaultRadius; - - if (isSingularMessage(current, previous, next, isGroupThread)) { - topLeft = defaultRadius; - topRight = defaultRadius; - bottomLeft = defaultRadius; - bottomRight = defaultRadius; - } else if (isStartOfMessageCluster(current, previous, isGroupThread)) { - if (current.isOutgoing()) { - bottomRight = collapseRadius; - } else { - bottomLeft = collapseRadius; - } - } else if (isEndOfMessageCluster(current, next, isGroupThread)) { - if (current.isOutgoing()) { - topRight = collapseRadius; - } else { - topLeft = collapseRadius; - } - } else { - if (current.isOutgoing()) { - topRight = collapseRadius; - bottomRight = collapseRadius; - } else { - topLeft = collapseRadius; - bottomLeft = collapseRadius; - } - } - - if (!TextUtils.isEmpty(current.getDisplayBody(getContext()))) { - bottomLeft = 0; - bottomRight = 0; - } - - if (isStartOfMessageCluster(current, previous, isGroupThread) && !current.isOutgoing() && isGroupThread) { - topLeft = 0; - topRight = 0; - } - - if (hasQuote(messageRecord)) { - topLeft = 0; - topRight = 0; - } - - if (hasLinkPreview(messageRecord) || hasExtraText(messageRecord)) { - bottomLeft = 0; - bottomRight = 0; - } - - mediaThumbnailStub.get().setCorners(topLeft, topRight, bottomRight, bottomLeft); - } - private void setLinkPreviewCorners(@NonNull MessageRecord current, @NonNull Optional previous, @NonNull Optional next, boolean isGroupThread, boolean bigImage) { - int defaultRadius = readDimen(R.dimen.message_corner_radius); - int collapseRadius = readDimen(R.dimen.message_corner_collapse_radius); - - if (bigImage) { - linkPreviewStub.get().setCorners(0, 0); - } else if (isStartOfMessageCluster(current, previous, isGroupThread) && !current.isOutgoing() && isGroupThread) { - linkPreviewStub.get().setCorners(0, 0); - } else if (isSingularMessage(current, previous, next, isGroupThread) || isStartOfMessageCluster(current, previous, isGroupThread)) { - linkPreviewStub.get().setCorners(defaultRadius, defaultRadius); - } else if (current.isOutgoing()) { - linkPreviewStub.get().setCorners(defaultRadius, collapseRadius); - } else { - linkPreviewStub.get().setCorners(collapseRadius, defaultRadius); - } - } - - private void setContactPhoto(@NonNull Recipient recipient) { - if (messageRecord == null) { return; } // TODO: Figure out how this happens - LinearLayout.LayoutParams layoutParams = (LinearLayout.LayoutParams)bodyBubble.getLayoutParams(); - int groupThreadMargin = (int)((12 * getResources().getDisplayMetrics().density) + getResources().getDimension(R.dimen.small_profile_picture_size)); - int defaultMargin = 0; - long threadID = messageRecord.getThreadId(); - Recipient r = DatabaseFactory.getThreadDatabase(context).getRecipientForThreadId(threadID); - String threadName = r != null ? r.getName() : ""; - boolean isRSSFeed = threadName != null && (threadName.equals("Loki News") || threadName.equals("Session Updates")); - layoutParams.setMarginStart((groupThread && !isRSSFeed) ? groupThreadMargin : defaultMargin); - bodyBubble.setLayoutParams(layoutParams); - if (profilePictureView == null) { return; } - String publicKey = recipient.getAddress().toString(); - profilePictureView.setPublicKey(publicKey); - String displayName = recipient.getName(); - profilePictureView.setDisplayName(displayName); - profilePictureView.setAdditionalPublicKey(null); - profilePictureView.setGlide(glideRequests); - profilePictureView.update(); - } - - private SpannableString linkifyMessageBody(SpannableString messageBody, boolean shouldLinkifyAllLinks) { - int linkPattern = Linkify.WEB_URLS | Linkify.EMAIL_ADDRESSES | Linkify.PHONE_NUMBERS; - boolean hasLinks = Linkify.addLinks(messageBody, shouldLinkifyAllLinks ? linkPattern : 0); - - if (hasLinks) { - Stream.of(messageBody.getSpans(0, messageBody.length(), URLSpan.class)) - .filterNot(url -> LinkPreviewUtil.isLegalUrl(url.getURL())) - .forEach(messageBody::removeSpan); - - URLSpan[] urlSpans = messageBody.getSpans(0, messageBody.length(), URLSpan.class); - - for (URLSpan urlSpan : urlSpans) { - int start = messageBody.getSpanStart(urlSpan); - int end = messageBody.getSpanEnd(urlSpan); - messageBody.setSpan(new LongClickCopySpan(urlSpan.getURL()), start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); - } - } - return messageBody; - } - - private void setStatusIcons(MessageRecord messageRecord) { - bodyText.setCompoundDrawablesWithIntrinsicBounds(0, 0, messageRecord.isKeyExchange() ? R.drawable.ic_menu_login : 0, 0); - - if (messageRecord.isFailed()) { - alertView.setFailed(); - } else if (messageRecord.isPendingInsecureSmsFallback()) { - alertView.setPendingApproval(); - } else { - alertView.setNone(); - } - } - - private void setQuote(@NonNull MessageRecord current, @NonNull Optional previous, @NonNull Optional next, boolean isGroupThread) { - if (current.isMms() && !current.isMmsNotification() && ((MediaMmsMessageRecord)current).getQuote() != null) { - Quote quote = ((MediaMmsMessageRecord)current).getQuote(); - //noinspection ConstantConditions - String quoteBody = MentionUtilities.highlightMentions(quote.getText(), current.getThreadId(), context); - quoteView.setQuote(glideRequests, quote.getId(), Recipient.from(context, quote.getAuthor(), true), quoteBody, quote.isOriginalMissing(), quote.getAttachment(), conversationRecipient); - quoteView.setVisibility(View.VISIBLE); - quoteView.getLayoutParams().width = ViewGroup.LayoutParams.WRAP_CONTENT; - - quoteView.setOnClickListener(view -> { - if (eventListener != null && batchSelected.isEmpty()) { - eventListener.onQuoteClicked((MmsMessageRecord) current); - } else { - passthroughClickListener.onClick(view); - } - }); - - quoteView.setOnLongClickListener(passthroughClickListener); - - if (isStartOfMessageCluster(current, previous, isGroupThread)) { - if (current.isOutgoing()) { - quoteView.setTopCornerSizes(true, true); - } else if (isGroupThread) { - quoteView.setTopCornerSizes(false, false); - } else { - quoteView.setTopCornerSizes(true, true); - } - } else if (!isSingularMessage(current, previous, next, isGroupThread)) { - if (current.isOutgoing()) { - quoteView.setTopCornerSizes(true, false); - } else { - quoteView.setTopCornerSizes(false, true); - } - } - - if (mediaThumbnailStub.resolved()) { - ViewUtil.setTopMargin(mediaThumbnailStub.get(), readDimen(R.dimen.message_bubble_top_padding)); - } - } else { - quoteView.dismiss(); - - if (mediaThumbnailStub.resolved()) { - ViewUtil.setTopMargin(mediaThumbnailStub.get(), 0); - } - } - } - - private void setGutterSizes(@NonNull MessageRecord current, boolean isGroupThread) { - if (isGroupThread && current.isOutgoing()) { - ViewUtil.setLeftMargin(container, readDimen(R.dimen.conversation_group_left_gutter)); - } else if (current.isOutgoing()) { - ViewUtil.setLeftMargin(container, readDimen(R.dimen.conversation_individual_left_gutter)); - } - } - - private void setFooter(@NonNull MessageRecord current, @NonNull Optional next, @NonNull Locale locale, boolean isGroupThread) { - ViewUtil.updateLayoutParams(footer, LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT); - - footer.setVisibility(GONE); - stickerFooter.setVisibility(GONE); - if (mediaThumbnailStub.resolved()) mediaThumbnailStub.get().getFooter().setVisibility(GONE); - - boolean differentTimestamps = next.isPresent() && !DateUtils.isSameExtendedRelativeTimestamp(context, locale, next.get().getTimestamp(), current.getTimestamp()); - - if (current.getExpiresIn() > 0 || !current.isSecure() || current.isPending() || current.isPendingInsecureSmsFallback() || - current.isFailed() || differentTimestamps || isEndOfMessageCluster(current, next, isGroupThread)) - { - ConversationItemFooter activeFooter = getActiveFooter(current); - activeFooter.setVisibility(VISIBLE); - activeFooter.setMessageRecord(current, locale); - } - } - - private ConversationItemFooter getActiveFooter(@NonNull MessageRecord messageRecord) { - if (hasOnlyThumbnail(messageRecord) && TextUtils.isEmpty(messageRecord.getDisplayBody(getContext()))) { - return mediaThumbnailStub.get().getFooter(); - } else { - return footer; - } - } - - private int readDimen(@DimenRes int dimenId) { - return context.getResources().getDimensionPixelOffset(dimenId); - } - - private boolean shouldInterceptClicks(MessageRecord messageRecord) { - return batchSelected.isEmpty() && - ((messageRecord.isFailed() && !messageRecord.isMmsNotification()) || - messageRecord.isPendingInsecureSmsFallback() || - messageRecord.isBundleKeyExchange()); - } - - @SuppressLint("SetTextI18n") - private void setGroupMessageStatus(MessageRecord messageRecord, Recipient recipient) { - if (groupThread && !messageRecord.isOutgoing()) { - String sessionID = recipient.getAddress().serialize(); - Contact contact = DatabaseFactory.getSessionContactDatabase(context).getContactWithSessionID(sessionID); - String displayName; - if (contact != null) { - Contact.ContactContext context = (this.conversationRecipient.isOpenGroupRecipient()) ? Contact.ContactContext.OPEN_GROUP : Contact.ContactContext.REGULAR; - displayName = contact.displayName(context); - } else { - displayName = sessionID; - } - - this.groupSender.setText(displayName); - - if (recipient.getName() == null && !TextUtils.isEmpty(recipient.getProfileName())) { - this.groupSenderProfileName.setText("~" + recipient.getProfileName()); - this.groupSenderProfileName.setVisibility(View.VISIBLE); - } else { - this.groupSenderProfileName.setText(null); - this.groupSenderProfileName.setVisibility(View.GONE); - } - } - } - - private void setGroupAuthorColor(@NonNull MessageRecord messageRecord) { - groupSender.setTextColor(ThemeUtil.getThemedColor(context, R.attr.conversation_item_received_text_primary_color)); - groupSenderProfileName.setTextColor(ThemeUtil.getThemedColor(context, R.attr.conversation_item_received_text_primary_color)); - } - - private void setAuthor(@NonNull MessageRecord current, @NonNull Optional previous, @NonNull Optional next, boolean isGroupThread) { - Recipient recipient = DatabaseFactory.getThreadDatabase(context).getRecipientForThreadId(current.getThreadId()); - String threadName = null; - if (recipient != null) { - threadName = recipient.getName(); - } - boolean isRSSFeed = threadName != null && (threadName.equals("Loki News") || threadName.equals("Session Updates")); - if (isGroupThread && !isRSSFeed && !current.isOutgoing()) { - contactPhotoHolder.setVisibility(VISIBLE); - - if (!previous.isPresent() || previous.get().isUpdate() || !current.getRecipient().getAddress().equals(previous.get().getRecipient().getAddress()) || - !DateUtils.isSameDay(previous.get().getTimestamp(), current.getTimestamp())) - { - groupSenderHolder.setVisibility(VISIBLE); - } else { - groupSenderHolder.setVisibility(GONE); - } - - if (!previous.isPresent() || previous.get().isUpdate() || !current.getRecipient().getAddress().equals(previous.get().getRecipient().getAddress())) { - profilePictureView.setVisibility(VISIBLE); - int visibility = View.GONE; - - OpenGroupV2 openGroupV2 = DatabaseFactory.getLokiThreadDatabase(context).getOpenGroupChat(messageRecord.getThreadId()); - if (openGroupV2 != null) { - boolean isModerator = OpenGroupAPIV2.isUserModerator(current.getRecipient().getAddress().toString(), openGroupV2.getRoom(), openGroupV2.getServer()); - visibility = isModerator ? View.VISIBLE : View.GONE; - } - - moderatorIconImageView.setVisibility(visibility); - } else { - profilePictureView.setVisibility(GONE); - moderatorIconImageView.setVisibility(GONE); - - } - } else { - groupSenderHolder.setVisibility(GONE); - - if (contactPhotoHolder != null) { - contactPhotoHolder.setVisibility(GONE); - moderatorIconImageView.setVisibility(GONE); - } - } - } - - private void setMessageShape(@NonNull MessageRecord current, @NonNull Optional previous, @NonNull Optional next, boolean isGroupThread) { - int background; - if (isSingularMessage(current, previous, next, isGroupThread)) { - background = current.isOutgoing() ? R.drawable.message_bubble_background_sent_alone : R.drawable.message_bubble_background_received_alone; - } else if (isStartOfMessageCluster(current, previous, isGroupThread)) { - background = current.isOutgoing() ? R.drawable.message_bubble_background_sent_start : R.drawable.message_bubble_background_received_start; - } else if (isEndOfMessageCluster(current, next, isGroupThread)) { - background = current.isOutgoing() ? R.drawable.message_bubble_background_sent_end : R.drawable.message_bubble_background_received_end; - } else { - background = current.isOutgoing() ? R.drawable.message_bubble_background_sent_middle : R.drawable.message_bubble_background_received_middle; - } - - bodyBubble.setBackgroundResource(background); - } - - private boolean isStartOfMessageCluster(@NonNull MessageRecord current, @NonNull Optional previous, boolean isGroupThread) { - if (isGroupThread) { - return !previous.isPresent() || previous.get().isUpdate() || !DateUtils.isSameDay(current.getTimestamp(), previous.get().getTimestamp()) || - !current.getRecipient().getAddress().equals(previous.get().getRecipient().getAddress()); - } else { - return !previous.isPresent() || previous.get().isUpdate() || !DateUtils.isSameDay(current.getTimestamp(), previous.get().getTimestamp()) || - current.isOutgoing() != previous.get().isOutgoing(); - } - } - - private boolean isEndOfMessageCluster(@NonNull MessageRecord current, @NonNull Optional next, boolean isGroupThread) { - if (isGroupThread) { - return !next.isPresent() || next.get().isUpdate() || !DateUtils.isSameDay(current.getTimestamp(), next.get().getTimestamp()) || - !current.getRecipient().getAddress().equals(next.get().getRecipient().getAddress()); - } else { - return !next.isPresent() || next.get().isUpdate() || !DateUtils.isSameDay(current.getTimestamp(), next.get().getTimestamp()) || - current.isOutgoing() != next.get().isOutgoing(); - } - } - - private boolean isSingularMessage(@NonNull MessageRecord current, @NonNull Optional previous, @NonNull Optional next, boolean isGroupThread) { - return isStartOfMessageCluster(current, previous, isGroupThread) && isEndOfMessageCluster(current, next, isGroupThread); - } - - private void setMessageSpacing(@NonNull Context context, @NonNull MessageRecord current, @NonNull Optional previous, @NonNull Optional next, boolean isGroupThread) { - int spacingTop = readDimen(context, R.dimen.conversation_vertical_message_spacing_collapse); - int spacingBottom = spacingTop; - - if (isStartOfMessageCluster(current, previous, isGroupThread)) { - spacingTop = readDimen(context, R.dimen.conversation_vertical_message_spacing_default); - } - - if (isEndOfMessageCluster(current, next, isGroupThread)) { - spacingBottom = readDimen(context, R.dimen.conversation_vertical_message_spacing_default); - } - - ViewUtil.setPaddingTop(this, spacingTop); - ViewUtil.setPaddingBottom(this, spacingBottom); - } - - private int readDimen(@NonNull Context context, @DimenRes int dimenId) { - return context.getResources().getDimensionPixelOffset(dimenId); - } - - /// Event handlers - - private Spannable getLongMessageSpan(@NonNull MessageRecord messageRecord) { - String message; - Runnable action; - - if (messageRecord.isMms()) { - TextSlide slide = ((MmsMessageRecord) messageRecord).getSlideDeck().getTextSlide(); - - if (slide != null && slide.asAttachment().getTransferState() == AttachmentTransferProgress.TRANSFER_PROGRESS_DONE) { - message = getResources().getString(R.string.ConversationItem_read_more); - action = () -> eventListener.onMoreTextClicked(conversationRecipient.getAddress(), messageRecord.getId(), messageRecord.isMms()); - } else if (slide != null && slide.asAttachment().getTransferState() == AttachmentTransferProgress.TRANSFER_PROGRESS_STARTED) { - message = getResources().getString(R.string.ConversationItem_pending); - action = () -> {}; - } else if (slide != null) { - message = getResources().getString(R.string.ConversationItem_download_more); - action = () -> singleDownloadClickListener.onClick(bodyText, slide); - } else { - message = getResources().getString(R.string.ConversationItem_read_more); - action = () -> eventListener.onMoreTextClicked(conversationRecipient.getAddress(), messageRecord.getId(), messageRecord.isMms()); - } - } else { - message = getResources().getString(R.string.ConversationItem_read_more); - action = () -> eventListener.onMoreTextClicked(conversationRecipient.getAddress(), messageRecord.getId(), messageRecord.isMms()); - } - - SpannableStringBuilder span = new SpannableStringBuilder(message); - CharacterStyle style = new ClickableSpan() { - @Override - public void onClick(@NonNull View widget) { - if (eventListener != null && batchSelected.isEmpty()) { - action.run(); - } - } - - @Override - public void updateDrawState(@NonNull TextPaint ds) { - ds.setTypeface(Typeface.DEFAULT_BOLD); - } - }; - span.setSpan(style, 0, span.length(), Spanned.SPAN_INCLUSIVE_EXCLUSIVE); - return span; - } - - @Override - public void onModified(final Recipient modified) { - Util.runOnMain(() -> { - setBubbleState(messageRecord); - setContactPhoto(recipient); - setGroupMessageStatus(messageRecord, recipient); - setAudioViewTint(messageRecord, conversationRecipient); - }); - } - - private class LinkPreviewClickListener implements View.OnClickListener { - @Override - public void onClick(View view) { - if (eventListener != null && batchSelected.isEmpty() && messageRecord.isMms() && !((MmsMessageRecord) messageRecord).getLinkPreviews().isEmpty()) { - eventListener.onLinkPreviewClicked(((MmsMessageRecord) messageRecord).getLinkPreviews().get(0)); - } else { - passthroughClickListener.onClick(view); - } - } - } - - private class LinkPreviewThumbnailClickListener implements SlideClickListener { - public void onClick(final View v, final Slide slide) { - if (eventListener != null && batchSelected.isEmpty() && messageRecord.isMms() && !((MmsMessageRecord) messageRecord).getLinkPreviews().isEmpty()) { - eventListener.onLinkPreviewClicked(((MmsMessageRecord) messageRecord).getLinkPreviews().get(0)); - } else { - performClick(); - } - } - } - - private class AttachmentDownloadClickListener implements SlidesClickedListener { - @Override - public void onClick(View v, final List slides) { - Log.i(TAG, "onClick() for attachment download"); - Log.i(TAG, "Scheduling push attachment downloads for " + slides.size() + " items"); - - for (Slide slide : slides) { - JobQueue.getShared().add( - new AttachmentDownloadJob(messageRecord.getId(), - ((DatabaseAttachment)slide.asAttachment()).getAttachmentId().getRowId()) - ); - } - } - } - - private class SlideClickPassthroughListener implements SlideClickListener { - - private final SlidesClickedListener original; - - private SlideClickPassthroughListener(@NonNull SlidesClickedListener original) { - this.original = original; - } - - @Override - public void onClick(View v, Slide slide) { - original.onClick(v, Collections.singletonList(slide)); - } - } - - private class StickerClickListener implements SlideClickListener { - @Override - public void onClick(View v, Slide slide) { - if (shouldInterceptClicks(messageRecord) || !batchSelected.isEmpty()) { - performClick(); - } - } - } - - private class ThumbnailClickListener implements SlideClickListener { - public void onClick(final View v, final Slide slide) { - if (shouldInterceptClicks(messageRecord) || !batchSelected.isEmpty()) { - performClick(); - } else if (MediaPreviewActivity.isContentTypeSupported(slide.getContentType()) && slide.getUri() != null) { - Intent intent = new Intent(context, MediaPreviewActivity.class); - intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); - intent.setDataAndType(slide.getUri(), slide.getContentType()); - intent.putExtra(MediaPreviewActivity.ADDRESS_EXTRA, conversationRecipient.getAddress()); - intent.putExtra(MediaPreviewActivity.OUTGOING_EXTRA, messageRecord.isOutgoing()); - intent.putExtra(MediaPreviewActivity.DATE_EXTRA, messageRecord.getTimestamp()); - intent.putExtra(MediaPreviewActivity.SIZE_EXTRA, slide.asAttachment().getSize()); - intent.putExtra(MediaPreviewActivity.CAPTION_EXTRA, slide.getCaption().orNull()); - intent.putExtra(MediaPreviewActivity.LEFT_IS_RECENT_EXTRA, false); - - context.startActivity(intent); - } else if (slide.getUri() != null) { - Log.i(TAG, "Clicked: " + slide.getUri() + " , " + slide.getContentType()); - Uri publicUri = PartAuthority.getAttachmentPublicUri(slide.getUri()); - Log.i(TAG, "Public URI: " + publicUri); - Intent intent = new Intent(Intent.ACTION_VIEW); - intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); - intent.setDataAndType(PartAuthority.getAttachmentPublicUri(slide.getUri()), slide.getContentType()); - try { - context.startActivity(intent); - } catch (ActivityNotFoundException anfe) { - Log.w(TAG, "No activity existed to view the media."); - Toast.makeText(context, R.string.ConversationItem_unable_to_open_media, Toast.LENGTH_LONG).show(); - } - } - } - } - - private class PassthroughClickListener implements View.OnLongClickListener, View.OnClickListener { - - @Override - public boolean onLongClick(View v) { - if (bodyText.hasSelection()) { - return false; - } - performLongClick(); - return true; - } - - @Override - public void onClick(View v) { - performClick(); - } - } - - private class ClickListener implements View.OnClickListener { - private OnClickListener parent; - - ClickListener(@Nullable OnClickListener parent) { - this.parent = parent; - } - - public void onClick(View v) { - if (!shouldInterceptClicks(messageRecord) && parent != null) { - parent.onClick(v); - } else if (messageRecord.isFailed()) { - Intent intent = new Intent(context, MessageDetailsActivity.class); - intent.putExtra(MessageDetailsActivity.MESSAGE_ID_EXTRA, messageRecord.getId()); - intent.putExtra(MessageDetailsActivity.THREAD_ID_EXTRA, messageRecord.getThreadId()); - intent.putExtra(MessageDetailsActivity.TYPE_EXTRA, messageRecord.isMms() ? MmsSmsDatabase.MMS_TRANSPORT : MmsSmsDatabase.SMS_TRANSPORT); - intent.putExtra(MessageDetailsActivity.IS_PUSH_GROUP_EXTRA, groupThread && messageRecord.isPush()); - intent.putExtra(MessageDetailsActivity.ADDRESS_EXTRA, conversationRecipient.getAddress()); - context.startActivity(intent); - } - } - } -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationPopupActivity.java b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationPopupActivity.java deleted file mode 100644 index f288ac5ba5..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationPopupActivity.java +++ /dev/null @@ -1,119 +0,0 @@ -package org.thoughtcrime.securesms.conversation; - -import android.content.Intent; -import android.os.Build.VERSION; -import android.os.Build.VERSION_CODES; -import android.os.Bundle; -import androidx.core.app.ActivityOptionsCompat; -import android.view.Display; -import android.view.Gravity; -import android.view.Menu; -import android.view.MenuInflater; -import android.view.MenuItem; -import android.view.WindowManager; - -import org.session.libsignal.utilities.Log; -import org.session.libsignal.utilities.ListenableFuture; - -import java.util.concurrent.ExecutionException; - -import network.loki.messenger.R; - -public class ConversationPopupActivity extends ConversationActivity { - - private static final String TAG = ConversationPopupActivity.class.getSimpleName(); - - @Override - protected void onPreCreate() { - super.onPreCreate(); - overridePendingTransition(R.anim.slide_from_top, R.anim.slide_to_top); - } - - @Override - protected void onCreate(Bundle bundle, boolean ready) { - getWindow().setFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND, - WindowManager.LayoutParams.FLAG_DIM_BEHIND); - - WindowManager.LayoutParams params = getWindow().getAttributes(); - params.alpha = 1.0f; - params.dimAmount = 0.1f; - params.gravity = Gravity.TOP; - getWindow().setAttributes(params); - - Display display = getWindowManager().getDefaultDisplay(); - int width = display.getWidth(); - int height = display.getHeight(); - - if (height > width) getWindow().setLayout((int) (width * .85), (int) (height * .5)); - else getWindow().setLayout((int) (width * .7), (int) (height * .75)); - - super.onCreate(bundle, ready); - } - - @Override - protected void onResume() { - super.onResume(); - composeText.requestFocus(); - quickAttachmentToggle.disable(); - } - - @Override - protected void onPause() { - super.onPause(); - if (isFinishing()) overridePendingTransition(R.anim.slide_from_top, R.anim.slide_to_top); - } - - @Override - public boolean onPrepareOptionsMenu(Menu menu) { - MenuInflater inflater = this.getMenuInflater(); - menu.clear(); - - inflater.inflate(R.menu.conversation_popup, menu); - return true; - } - - @Override - public boolean onOptionsItemSelected(MenuItem item) { - switch (item.getItemId()) { - case R.id.menu_expand: - saveDraft().addListener(new ListenableFuture.Listener() { - @Override - public void onSuccess(Long result) { - ActivityOptionsCompat transition = ActivityOptionsCompat.makeScaleUpAnimation(getWindow().getDecorView(), 0, 0, getWindow().getAttributes().width, getWindow().getAttributes().height); - Intent intent = new Intent(ConversationPopupActivity.this, ConversationActivity.class); - intent.putExtra(ConversationActivity.ADDRESS_EXTRA, getRecipient().getAddress()); - intent.putExtra(ConversationActivity.THREAD_ID_EXTRA, result); - - if (VERSION.SDK_INT >= VERSION_CODES.JELLY_BEAN) { - startActivity(intent, transition.toBundle()); - } else { - startActivity(intent); - overridePendingTransition(R.anim.fade_scale_in, R.anim.slide_to_right); - } - - finish(); - } - - @Override - public void onFailure(ExecutionException e) { - Log.w(TAG, e); - } - }); - return true; - } - - return false; - } - - @Override - protected void initializeActionBar() { - super.initializeActionBar(); - getSupportActionBar().setDisplayHomeAsUpEnabled(false); - } - - @Override - protected void sendComplete(long threadId) { - super.sendComplete(threadId); - finish(); - } -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationSearchViewModel.java b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationSearchViewModel.java deleted file mode 100644 index d24539982d..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationSearchViewModel.java +++ /dev/null @@ -1,146 +0,0 @@ -package org.thoughtcrime.securesms.conversation; - -import android.app.Application; -import androidx.lifecycle.AndroidViewModel; -import androidx.lifecycle.LiveData; -import android.content.Context; -import androidx.annotation.NonNull; - -import org.thoughtcrime.securesms.contacts.ContactAccessor; -import org.thoughtcrime.securesms.database.CursorList; -import org.thoughtcrime.securesms.database.DatabaseFactory; -import org.thoughtcrime.securesms.search.SearchRepository; -import org.thoughtcrime.securesms.search.model.MessageResult; -import org.thoughtcrime.securesms.util.CloseableLiveData; -import org.session.libsession.utilities.Debouncer; -import org.session.libsession.utilities.Util; -import org.session.libsession.utilities.concurrent.SignalExecutors; - -import java.io.Closeable; -import java.util.List; - -public class ConversationSearchViewModel extends AndroidViewModel { - - private final SearchRepository searchRepository; - private final CloseableLiveData result; - private final Debouncer debouncer; - - private boolean firstSearch; - private boolean searchOpen; - private String activeQuery; - private long activeThreadId; - - public ConversationSearchViewModel(@NonNull Application application) { - super(application); - Context context = application.getApplicationContext(); - result = new CloseableLiveData<>(); - debouncer = new Debouncer(500); - searchRepository = new SearchRepository(context, - DatabaseFactory.getSearchDatabase(context), - DatabaseFactory.getThreadDatabase(context), - ContactAccessor.getInstance(), - SignalExecutors.SERIAL); - } - - LiveData getSearchResults() { - return result; - } - - void onQueryUpdated(@NonNull String query, long threadId) { - if (firstSearch && query.length() < 2) { - result.postValue(new SearchResult(CursorList.emptyList(), 0)); - return; - } - - if (query.equals(activeQuery)) { - return; - } - - updateQuery(query, threadId); - } - - void onMissingResult() { - if (activeQuery != null) { - updateQuery(activeQuery, activeThreadId); - } - } - - void onMoveUp() { - debouncer.clear(); - - CursorList messages = (CursorList) result.getValue().getResults(); - int position = Math.min(result.getValue().getPosition() + 1, messages.size() - 1); - - result.setValue(new SearchResult(messages, position), false); - } - - void onMoveDown() { - debouncer.clear(); - - CursorList messages = (CursorList) result.getValue().getResults(); - int position = Math.max(result.getValue().getPosition() - 1, 0); - - result.setValue(new SearchResult(messages, position), false); - } - - - void onSearchOpened() { - searchOpen = true; - firstSearch = true; - } - - void onSearchClosed() { - searchOpen = false; - debouncer.clear(); - result.close(); - } - - @Override - protected void onCleared() { - super.onCleared(); - result.close(); - } - - private void updateQuery(@NonNull String query, long threadId) { - activeQuery = query; - activeThreadId = threadId; - - debouncer.publish(() -> { - firstSearch = false; - - searchRepository.query(query, threadId, messages -> { - Util.runOnMain(() -> { - if (searchOpen && query.equals(activeQuery)) { - result.setValue(new SearchResult(messages, 0)); - } else { - messages.close(); - } - }); - }); - }); - } - - static class SearchResult implements Closeable { - - private final CursorList results; - private final int position; - - SearchResult(CursorList results, int position) { - this.results = results; - this.position = position; - } - - public List getResults() { - return results; - } - - public int getPosition() { - return position; - } - - @Override - public void close() { - results.close(); - } - } -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationUpdateItem.java b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationUpdateItem.java deleted file mode 100644 index 3800f29dac..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationUpdateItem.java +++ /dev/null @@ -1,289 +0,0 @@ -package org.thoughtcrime.securesms.conversation; - -import android.content.Context; -import android.graphics.Color; -import android.graphics.PorterDuff; -import android.graphics.PorterDuffColorFilter; -import android.util.AttributeSet; -import android.view.View; -import android.widget.ImageView; -import android.widget.LinearLayout; -import android.widget.TextView; - -import androidx.annotation.ColorInt; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; - -import org.session.libsession.messaging.sending_receiving.data_extraction.DataExtractionNotificationInfoMessage; -import org.thoughtcrime.securesms.BindableConversationItem; -import org.thoughtcrime.securesms.database.model.MessageRecord; -import org.thoughtcrime.securesms.loki.utilities.GeneralUtilitiesKt; -import org.thoughtcrime.securesms.mms.GlideRequests; -import org.thoughtcrime.securesms.util.DateUtils; -import org.session.libsignal.utilities.guava.Optional; - -import org.session.libsession.utilities.recipients.Recipient; -import org.session.libsession.utilities.recipients.RecipientModifiedListener; -import org.session.libsession.utilities.ExpirationUtil; -import org.session.libsession.utilities.Util; - -import java.util.Locale; -import java.util.Set; - -import network.loki.messenger.R; - -//TODO Remove this class. -public class ConversationUpdateItem extends LinearLayout - implements RecipientModifiedListener, BindableConversationItem -{ - private static final String TAG = ConversationUpdateItem.class.getSimpleName(); - - private Set batchSelected; - - private ImageView icon; - private TextView title; - private TextView body; - private TextView date; - private Recipient sender; - private MessageRecord messageRecord; - private Locale locale; - - public ConversationUpdateItem(Context context) { - super(context); - } - - public ConversationUpdateItem(Context context, AttributeSet attrs) { - super(context, attrs); - } - - @Override - public void onFinishInflate() { - super.onFinishInflate(); - - this.icon = findViewById(R.id.conversation_update_icon); - this.title = findViewById(R.id.conversation_update_title); - this.body = findViewById(R.id.conversation_update_body); - this.date = findViewById(R.id.conversation_update_date); - - this.setOnClickListener(new InternalClickListener(null)); - } - - @Override - public void bind(@NonNull MessageRecord messageRecord, - @NonNull Optional previousMessageRecord, - @NonNull Optional nextMessageRecord, - @NonNull GlideRequests glideRequests, - @NonNull Locale locale, - @NonNull Set batchSelected, - @NonNull Recipient conversationRecipient, - @Nullable String searchQuery, - boolean pulseUpdate) - { - this.batchSelected = batchSelected; - - bind(messageRecord, locale); - } - - @Override - public void setEventListener(@Nullable EventListener listener) { - // No events to report yet - } - - @Override - public MessageRecord getMessageRecord() { - return messageRecord; - } - - private void bind(@NonNull MessageRecord messageRecord, @NonNull Locale locale) { - this.messageRecord = messageRecord; - this.sender = messageRecord.getIndividualRecipient(); - this.locale = locale; - - this.sender.addListener(this); - - if (messageRecord.isGroupAction()) setGroupRecord(messageRecord); - else if (messageRecord.isCallLog()) setCallRecord(messageRecord); - else if (messageRecord.isJoined()) setJoinedRecord(messageRecord); - else if (messageRecord.isExpirationTimerUpdate()) setTimerRecord(messageRecord); - else if (messageRecord.isScreenshotExtraction()) setDataExtractionRecord(messageRecord, DataExtractionNotificationInfoMessage.Kind.SCREENSHOT); - else if (messageRecord.isMediaSavedExtraction()) setDataExtractionRecord(messageRecord, DataExtractionNotificationInfoMessage.Kind.MEDIA_SAVED); - else if (messageRecord.isEndSession()) setEndSessionRecord(messageRecord); - else if (messageRecord.isIdentityUpdate()) setIdentityRecord(messageRecord); - else if (messageRecord.isIdentityVerified() || - messageRecord.isIdentityDefault()) setIdentityVerifyUpdate(messageRecord); - else if (messageRecord.isLokiSessionRestoreSent()) setTextMessageRecord(messageRecord); - else if (messageRecord.isLokiSessionRestoreDone()) setTextMessageRecord(messageRecord); - else throw new AssertionError("Neither group nor log nor joined."); - - if (batchSelected.contains(messageRecord)) setSelected(true); - else setSelected(false); - } - - private void setCallRecord(MessageRecord messageRecord) { - if (messageRecord.isIncomingCall()) icon.setImageResource(R.drawable.ic_call_received_grey600_24dp); - else if (messageRecord.isOutgoingCall()) icon.setImageResource(R.drawable.ic_call_made_grey600_24dp); - else icon.setImageResource(R.drawable.ic_call_missed_grey600_24dp); - - body.setText(messageRecord.getDisplayBody(getContext())); - date.setText(DateUtils.getExtendedRelativeTimeSpanString(getContext(), locale, messageRecord.getDateReceived())); - - title.setVisibility(GONE); - body.setVisibility(VISIBLE); - date.setVisibility(View.VISIBLE); - } - - private void setTimerRecord(final MessageRecord messageRecord) { - @ColorInt int color = GeneralUtilitiesKt.getColorWithID(getResources(), R.color.text, getContext().getTheme()); - if (messageRecord.getExpiresIn() > 0) { - icon.setImageResource(R.drawable.ic_timer); - icon.setColorFilter(new PorterDuffColorFilter(color, PorterDuff.Mode.MULTIPLY)); - } else { - icon.setImageResource(R.drawable.ic_timer_disabled); - icon.setColorFilter(new PorterDuffColorFilter(color, PorterDuff.Mode.MULTIPLY)); - } - - title.setText(ExpirationUtil.getExpirationDisplayValue(getContext(), (int)(messageRecord.getExpiresIn() / 1000))); - body.setText(messageRecord.getDisplayBody(getContext())); - - title.setVisibility(VISIBLE); - body.setVisibility(VISIBLE); - date.setVisibility(GONE); - } - - private void setDataExtractionRecord(final MessageRecord messageRecord, DataExtractionNotificationInfoMessage.Kind kind) { - @ColorInt int color = GeneralUtilitiesKt.getColorWithID(getResources(), R.color.text, getContext().getTheme()); - if (kind == DataExtractionNotificationInfoMessage.Kind.SCREENSHOT) { - icon.setImageResource(R.drawable.quick_camera_dark); - } else if (kind == DataExtractionNotificationInfoMessage.Kind.MEDIA_SAVED) { - icon.setImageResource(R.drawable.ic_file_download_white_36dp); - } - icon.setColorFilter(new PorterDuffColorFilter(color, PorterDuff.Mode.MULTIPLY)); - - body.setText(messageRecord.getDisplayBody(getContext())); - - title.setVisibility(VISIBLE); - body.setVisibility(VISIBLE); - date.setVisibility(GONE); - } - - private void setIdentityRecord(final MessageRecord messageRecord) { - icon.setImageResource(R.drawable.ic_security_white_24dp); - icon.setColorFilter(new PorterDuffColorFilter(Color.parseColor("#757575"), PorterDuff.Mode.MULTIPLY)); - body.setText(messageRecord.getDisplayBody(getContext())); - - title.setVisibility(GONE); - body.setVisibility(VISIBLE); - date.setVisibility(GONE); - } - - private void setIdentityVerifyUpdate(final MessageRecord messageRecord) { - if (messageRecord.isIdentityVerified()) icon.setImageResource(R.drawable.ic_check_white_24dp); - else icon.setImageResource(R.drawable.ic_info_outline_white_24dp); - - icon.setColorFilter(new PorterDuffColorFilter(Color.parseColor("#757575"), PorterDuff.Mode.MULTIPLY)); - body.setText(messageRecord.getDisplayBody(getContext())); - - title.setVisibility(GONE); - body.setVisibility(VISIBLE); - date.setVisibility(GONE); - } - - private void setGroupRecord(MessageRecord messageRecord) { - icon.setImageResource(R.drawable.ic_group_grey600_24dp); - icon.clearColorFilter(); - body.setText(messageRecord.getDisplayBody(getContext())); - - title.setVisibility(GONE); - body.setVisibility(VISIBLE); - date.setVisibility(GONE); - } - - private void setJoinedRecord(MessageRecord messageRecord) { - icon.setImageResource(R.drawable.ic_favorite_grey600_24dp); - icon.clearColorFilter(); - body.setText(messageRecord.getDisplayBody(getContext())); - - title.setVisibility(GONE); - body.setVisibility(VISIBLE); - date.setVisibility(GONE); - } - - private void setEndSessionRecord(MessageRecord messageRecord) { - icon.setImageResource(R.drawable.ic_refresh_white_24dp); - icon.setColorFilter(new PorterDuffColorFilter(Color.parseColor("#757575"), PorterDuff.Mode.MULTIPLY)); - body.setText(messageRecord.getDisplayBody(getContext())); - - title.setVisibility(GONE); - body.setVisibility(VISIBLE); - date.setVisibility(GONE); - } - - private void setTextMessageRecord(MessageRecord messageRecord) { - body.setText(messageRecord.getDisplayBody(getContext())); - - icon.setVisibility(GONE); - title.setVisibility(GONE); - body.setVisibility(VISIBLE); - date.setVisibility(GONE); - } - - @Override - public void onModified(Recipient recipient) { - Util.runOnMain(() -> bind(messageRecord, locale)); - } - - @Override - public void setOnClickListener(View.OnClickListener l) { - super.setOnClickListener(new InternalClickListener(l)); - } - - @Override - public void unbind() { - if (sender != null) { - sender.removeListener(this); - } - } - - private class InternalClickListener implements View.OnClickListener { - - @Nullable private final View.OnClickListener parent; - - InternalClickListener(@Nullable View.OnClickListener parent) { - this.parent = parent; - } - - @Override - public void onClick(View v) { - if ((!messageRecord.isIdentityUpdate() && - !messageRecord.isIdentityDefault() && - !messageRecord.isIdentityVerified()) || - !batchSelected.isEmpty()) - { - if (parent != null) parent.onClick(v); - return; - } - - final Recipient sender = ConversationUpdateItem.this.sender; - -// IdentityUtil.getRemoteIdentityKey(getContext(), sender).addListener(new ListenableFuture.Listener>() { -// @Override -// public void onSuccess(Optional result) { -// if (result.isPresent()) { -// Intent intent = new Intent(getContext(), VerifyIdentityActivity.class); -// intent.putExtra(VerifyIdentityActivity.ADDRESS_EXTRA, sender.getAddress()); -// intent.putExtra(VerifyIdentityActivity.IDENTITY_EXTRA, new IdentityKeyParcelable(result.get().getIdentityKey())); -// intent.putExtra(VerifyIdentityActivity.VERIFIED_EXTRA, result.get().getVerifiedStatus() == IdentityDatabase.VerifiedStatus.VERIFIED); -// -// getContext().startActivity(intent); -// } -// } -// -// @Override -// public void onFailure(ExecutionException e) { -// Log.w(TAG, e); -// } -// }); - } - } - -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationActivityV2.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationActivityV2.kt new file mode 100644 index 0000000000..835c35bcb6 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationActivityV2.kt @@ -0,0 +1,1342 @@ +package org.thoughtcrime.securesms.conversation.v2 + +import android.Manifest +import android.animation.FloatEvaluator +import android.animation.ValueAnimator +import android.content.ClipData +import android.content.ClipboardManager +import android.content.Context +import android.content.Intent +import android.content.res.Resources +import android.database.Cursor +import android.graphics.Rect +import android.graphics.Typeface +import android.net.Uri +import android.os.* +import android.text.TextUtils +import android.util.Log +import android.util.Pair +import android.util.TypedValue +import android.view.* +import android.widget.LinearLayout +import android.widget.RelativeLayout +import android.widget.Toast +import androidx.annotation.DimenRes +import androidx.appcompat.app.AlertDialog +import androidx.core.view.children +import androidx.core.view.get +import androidx.core.view.isVisible +import androidx.lifecycle.Observer +import androidx.lifecycle.ViewModelProvider +import androidx.lifecycle.ViewModelProviders +import androidx.loader.app.LoaderManager +import androidx.loader.content.Loader +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView +import com.annimon.stream.Stream +import kotlinx.android.synthetic.main.activity_conversation_v2.* +import kotlinx.android.synthetic.main.activity_conversation_v2.view.* +import kotlinx.android.synthetic.main.activity_conversation_v2_action_bar.* +import kotlinx.android.synthetic.main.activity_home.* +import kotlinx.android.synthetic.main.view_conversation.view.* +import kotlinx.android.synthetic.main.view_input_bar.view.* +import kotlinx.android.synthetic.main.view_input_bar_recording.* +import kotlinx.android.synthetic.main.view_input_bar_recording.view.* +import kotlinx.android.synthetic.main.view_visible_message.view.* +import network.loki.messenger.R +import nl.komponents.kovenant.ui.failUi +import nl.komponents.kovenant.ui.successUi +import org.session.libsession.messaging.MessagingModuleConfiguration +import org.session.libsession.messaging.contacts.Contact +import org.session.libsession.messaging.mentions.Mention +import org.session.libsession.messaging.mentions.MentionsManager +import org.session.libsession.messaging.messages.control.DataExtractionNotification +import org.session.libsession.messaging.messages.signal.OutgoingMediaMessage +import org.session.libsession.messaging.messages.signal.OutgoingTextMessage +import org.session.libsession.messaging.messages.visible.LinkPreview.Companion.from +import org.session.libsession.messaging.messages.visible.OpenGroupInvitation +import org.session.libsession.messaging.messages.visible.Quote.Companion.from +import org.session.libsession.messaging.messages.visible.VisibleMessage +import org.session.libsession.messaging.open_groups.OpenGroupAPIV2 +import org.session.libsession.messaging.sending_receiving.MessageSender +import org.session.libsession.messaging.sending_receiving.attachments.Attachment +import org.session.libsession.messaging.sending_receiving.link_preview.LinkPreview +import org.session.libsession.messaging.sending_receiving.quotes.QuoteModel +import org.session.libsession.messaging.utilities.UpdateMessageData +import org.session.libsession.messaging.utilities.UpdateMessageData.Companion.fromJSON +import org.session.libsession.utilities.Address +import org.session.libsession.utilities.Address.Companion.fromSerialized +import org.session.libsession.utilities.MediaTypes +import org.session.libsession.utilities.TextSecurePreferences +import org.session.libsession.utilities.concurrent.SimpleTask +import org.session.libsession.utilities.recipients.Recipient +import org.session.libsession.utilities.recipients.RecipientModifiedListener +import org.session.libsignal.utilities.ListenableFuture +import org.session.libsignal.utilities.guava.Optional +import org.thoughtcrime.securesms.ApplicationContext +import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity +import org.thoughtcrime.securesms.audio.AudioRecorder +import org.thoughtcrime.securesms.contactshare.SimpleTextWatcher + +import org.thoughtcrime.securesms.conversation.v2.dialogs.* +import org.thoughtcrime.securesms.conversation.v2.input_bar.InputBarButton +import org.thoughtcrime.securesms.conversation.v2.input_bar.InputBarDelegate +import org.thoughtcrime.securesms.conversation.v2.input_bar.InputBarRecordingViewDelegate +import org.thoughtcrime.securesms.conversation.v2.input_bar.mentions.MentionCandidatesView +import org.thoughtcrime.securesms.conversation.v2.menus.ConversationActionModeCallback +import org.thoughtcrime.securesms.conversation.v2.menus.ConversationActionModeCallbackDelegate +import org.thoughtcrime.securesms.conversation.v2.menus.ConversationMenuHelper +import org.thoughtcrime.securesms.conversation.v2.messages.* +import org.thoughtcrime.securesms.conversation.v2.search.SearchBottomBar +import org.thoughtcrime.securesms.conversation.v2.search.SearchViewModel +import org.thoughtcrime.securesms.conversation.v2.utilities.AttachmentManager +import org.thoughtcrime.securesms.conversation.v2.utilities.BaseDialog +import org.thoughtcrime.securesms.database.DatabaseFactory +import org.thoughtcrime.securesms.database.DraftDatabase +import org.thoughtcrime.securesms.database.DraftDatabase.Drafts +import org.thoughtcrime.securesms.database.model.MessageRecord +import org.thoughtcrime.securesms.database.model.MmsMessageRecord +import org.thoughtcrime.securesms.giph.ui.GiphyActivity +import org.thoughtcrime.securesms.linkpreview.LinkPreviewRepository +import org.thoughtcrime.securesms.linkpreview.LinkPreviewUtil +import org.thoughtcrime.securesms.linkpreview.LinkPreviewViewModel +import org.thoughtcrime.securesms.linkpreview.LinkPreviewViewModel.LinkPreviewState +import org.thoughtcrime.securesms.contacts.SelectContactsActivity +import org.thoughtcrime.securesms.contacts.SelectContactsActivity.Companion.selectedContactsKey +import org.thoughtcrime.securesms.conversation.v2.utilities.MentionUtilities +import org.thoughtcrime.securesms.util.toPx +import org.thoughtcrime.securesms.mediasend.Media +import org.thoughtcrime.securesms.mediasend.MediaSendActivity +import org.thoughtcrime.securesms.mms.* +import org.thoughtcrime.securesms.notifications.MarkReadReceiver +import org.thoughtcrime.securesms.permissions.Permissions +import org.thoughtcrime.securesms.util.* +import java.util.* +import java.util.concurrent.ExecutionException +import kotlin.math.* + +// Some things that seemingly belong to the input bar (e.g. the voice message recording UI) are actually +// part of the conversation activity layout. This is just because it makes the layout a lot simpler. The +// price we pay is a bit of back and forth between the input bar and the conversation activity. + +class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDelegate, + InputBarRecordingViewDelegate, AttachmentManager.AttachmentListener, ActivityDispatcher, + ConversationActionModeCallbackDelegate, VisibleMessageContentViewDelegate, RecipientModifiedListener, + SearchBottomBar.EventListener, VoiceMessageViewDelegate { + private val screenWidth = Resources.getSystem().displayMetrics.widthPixels + private var linkPreviewViewModel: LinkPreviewViewModel? = null + private var threadID: Long = -1 + private var actionMode: ActionMode? = null + private var unreadCount = 0 + // Attachments + private val audioRecorder = AudioRecorder(this) + private val stopAudioHandler = Handler(Looper.getMainLooper()) + private val stopVoiceMessageRecordingTask = Runnable { sendVoiceMessage() } + private val attachmentManager by lazy { AttachmentManager(this, this) } + private var isLockViewExpanded = false + private var isShowingAttachmentOptions = false + // Mentions + private val mentions = mutableListOf() + private var mentionCandidatesView: MentionCandidatesView? = null + private var previousText: CharSequence = "" + private var currentMentionStartIndex = -1 + private var isShowingMentionCandidatesView = false + // Search + var searchViewModel: SearchViewModel? = null + var searchViewItem: MenuItem? = null + + private val isScrolledToBottom: Boolean + get() { + val position = layoutManager.findFirstCompletelyVisibleItemPosition() + return position == 0 + } + + private val layoutManager: LinearLayoutManager + get() { return conversationRecyclerView.layoutManager as LinearLayoutManager } + + private val adapter by lazy { + val cursor = DatabaseFactory.getMmsSmsDatabase(this).getConversation(threadID) + val adapter = ConversationAdapter( + this, + cursor, + onItemPress = { message, position, view, event -> + handlePress(message, position, view, event) + }, + onItemSwipeToReply = { message, position -> + handleSwipeToReply(message, position) + }, + onItemLongPress = { message, position -> + handleLongPress(message, position) + }, + glide + ) + adapter.visibleMessageContentViewDelegate = this + adapter + } + + private val thread by lazy { + DatabaseFactory.getThreadDatabase(this).getRecipientForThreadId(threadID)!! + } + + private val glide by lazy { GlideApp.with(this) } + private val lockViewHitMargin by lazy { toPx(40, resources) } + private val gifButton by lazy { InputBarButton(this, R.drawable.ic_gif_white_24dp, hasOpaqueBackground = true, isGIFButton = true) } + private val documentButton by lazy { InputBarButton(this, R.drawable.ic_document_small_dark, hasOpaqueBackground = true) } + private val libraryButton by lazy { InputBarButton(this, R.drawable.ic_baseline_photo_library_24, hasOpaqueBackground = true) } + private val cameraButton by lazy { InputBarButton(this, R.drawable.ic_baseline_photo_camera_24, hasOpaqueBackground = true) } + + // region Settings + companion object { + // Extras + const val THREAD_ID = "thread_id" + const val ADDRESS = "address" + // Request codes + const val PICK_DOCUMENT = 2 + const val TAKE_PHOTO = 7 + const val PICK_GIF = 10 + const val PICK_FROM_LIBRARY = 12 + const val INVITE_CONTACTS = 124 + } + // endregion + + // region Lifecycle + override fun onCreate(savedInstanceState: Bundle?, isReady: Boolean) { + super.onCreate(savedInstanceState, isReady) + setContentView(R.layout.activity_conversation_v2) + var threadID = intent.getLongExtra(THREAD_ID, -1L) + if (threadID == -1L) { + val address = intent.getParcelableExtra
(ADDRESS) ?: return finish() + val recipient = Recipient.from(this, address, false) + threadID = DatabaseFactory.getThreadDatabase(this).getOrCreateThreadIdFor(recipient) + } + this.threadID = threadID + val thread = DatabaseFactory.getThreadDatabase(this).getRecipientForThreadId(threadID) + if (thread == null) { + Toast.makeText(this, "This thread has been deleted.", Toast.LENGTH_LONG).show() + return finish() + } + setUpRecyclerView() + setUpToolBar() + setUpInputBar() + restoreDraftIfNeeded() + addOpenGroupGuidelinesIfNeeded() + scrollToBottomButton.setOnClickListener { conversationRecyclerView.smoothScrollToPosition(0) } + unreadCount = DatabaseFactory.getMmsSmsDatabase(this).getUnreadCount(threadID) + updateUnreadCountIndicator() + setUpTypingObserver() + setUpRecipientObserver() + updateSubtitle() + getLatestOpenGroupInfoIfNeeded() + setUpBlockedBanner() + setUpLinkPreviewObserver() + searchBottomBar.setEventListener(this) + setUpSearchResultObserver() + scrollToFirstUnreadMessageIfNeeded() + markAllAsRead() + showOrHideInputIfNeeded() + if (this.thread.isOpenGroupRecipient) { + val openGroup = DatabaseFactory.getLokiThreadDatabase(this).getOpenGroupChat(threadID) + if (openGroup == null) { + Toast.makeText(this, "This thread has been deleted.", Toast.LENGTH_LONG).show() + return finish() + } + } + } + + override fun onResume() { + super.onResume() + ApplicationContext.getInstance(this).messageNotifier.setVisibleThread(threadID) + } + + override fun onPause() { + super.onPause() + ApplicationContext.getInstance(this).messageNotifier.setVisibleThread(-1) + } + + override fun getSystemService(name: String): Any? { + if (name == ActivityDispatcher.SERVICE) { + return this + } + return super.getSystemService(name) + } + + override fun dispatchIntent(body: (Context) -> Intent?) { + val intent = body(this) ?: return + push(intent, false) + } + + override fun showDialog(baseDialog: BaseDialog, tag: String?) { + baseDialog.show(supportFragmentManager, tag) + } + + private fun setUpRecyclerView() { + conversationRecyclerView.adapter = adapter + val layoutManager = LinearLayoutManager(this, LinearLayoutManager.VERTICAL, true) + conversationRecyclerView.layoutManager = layoutManager + // Workaround for the fact that CursorRecyclerViewAdapter doesn't auto-update automatically (even though it says it will) + LoaderManager.getInstance(this).restartLoader(0, null, object : LoaderManager.LoaderCallbacks { + + override fun onCreateLoader(id: Int, bundle: Bundle?): Loader { + return ConversationLoader(threadID, this@ConversationActivityV2) + } + + override fun onLoadFinished(loader: Loader, cursor: Cursor?) { + adapter.changeCursor(cursor) + } + + override fun onLoaderReset(cursor: Loader) { + adapter.changeCursor(null) + } + }) + conversationRecyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener() { + + override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) { + handleRecyclerViewScrolled() + } + }) + } + + private fun setUpToolBar() { + val actionBar = supportActionBar!! + actionBar.setCustomView(R.layout.activity_conversation_v2_action_bar) + actionBar.setDisplayShowCustomEnabled(true) + conversationTitleView.text = thread.toShortString() + @DimenRes val sizeID: Int + if (thread.isClosedGroupRecipient) { + sizeID = R.dimen.medium_profile_picture_size + } else { + sizeID = R.dimen.small_profile_picture_size + } + val size = resources.getDimension(sizeID).roundToInt() + profilePictureView.layoutParams = LinearLayout.LayoutParams(size, size) + profilePictureView.glide = glide + profilePictureView.update(thread, threadID) + } + + private fun setUpInputBar() { + inputBar.delegate = this + inputBarRecordingView.delegate = this + // GIF button + gifButtonContainer.addView(gifButton) + gifButton.layoutParams = RelativeLayout.LayoutParams(RelativeLayout.LayoutParams.MATCH_PARENT, RelativeLayout.LayoutParams.MATCH_PARENT) + gifButton.onUp = { showGIFPicker() } + gifButton.snIsEnabled = false + // Document button + documentButtonContainer.addView(documentButton) + documentButton.layoutParams = RelativeLayout.LayoutParams(RelativeLayout.LayoutParams.MATCH_PARENT, RelativeLayout.LayoutParams.MATCH_PARENT) + documentButton.onUp = { showDocumentPicker() } + documentButton.snIsEnabled = false + // Library button + libraryButtonContainer.addView(libraryButton) + libraryButton.layoutParams = RelativeLayout.LayoutParams(RelativeLayout.LayoutParams.MATCH_PARENT, RelativeLayout.LayoutParams.MATCH_PARENT) + libraryButton.onUp = { pickFromLibrary() } + libraryButton.snIsEnabled = false + // Camera button + cameraButtonContainer.addView(cameraButton) + cameraButton.layoutParams = RelativeLayout.LayoutParams(RelativeLayout.LayoutParams.MATCH_PARENT, RelativeLayout.LayoutParams.MATCH_PARENT) + cameraButton.onUp = { showCamera() } + cameraButton.snIsEnabled = false + } + + private fun restoreDraftIfNeeded() { + val mediaURI = intent.data + val mediaType = AttachmentManager.MediaType.from(intent.type) + if (mediaURI != null && mediaType != null) { + if (AttachmentManager.MediaType.IMAGE == mediaType || AttachmentManager.MediaType.GIF == mediaType || AttachmentManager.MediaType.VIDEO == mediaType) { + val media = Media(mediaURI, MediaUtil.getMimeType(this, mediaURI)!!, 0, 0, 0, 0, Optional.absent(), Optional.absent()) + startActivityForResult(MediaSendActivity.buildEditorIntent(this, listOf( media ), thread, ""), ConversationActivityV2.PICK_FROM_LIBRARY) + return + } else { + prepMediaForSending(mediaURI, mediaType).addListener(object : ListenableFuture.Listener { + + override fun onSuccess(result: Boolean?) { + sendAttachments(attachmentManager.buildSlideDeck().asAttachments(), null) + } + + override fun onFailure(e: ExecutionException?) { + Toast.makeText(this@ConversationActivityV2, R.string.activity_conversation_attachment_prep_failed, Toast.LENGTH_LONG).show() + } + }) + return + } + } + val draftDB = DatabaseFactory.getDraftDatabase(this) + val drafts = draftDB.getDrafts(threadID) + draftDB.clearDrafts(threadID) + val text = drafts.find { it.type == DraftDatabase.Draft.TEXT }?.value ?: return + inputBar.text = text + } + + private fun addOpenGroupGuidelinesIfNeeded() { + val openGroup = DatabaseFactory.getLokiThreadDatabase(this).getOpenGroupChat(threadID) ?: return + val isOxenHostedOpenGroup = openGroup.room == "session" || openGroup.room == "oxen" + || openGroup.room == "lokinet" || openGroup.room == "crypto" + if (!isOxenHostedOpenGroup) { return } + openGroupGuidelinesView.visibility = View.VISIBLE + val recyclerViewLayoutParams = conversationRecyclerView.layoutParams as RelativeLayout.LayoutParams + recyclerViewLayoutParams.topMargin = toPx(57, resources) // The height of the open group guidelines view is hardcoded to this + conversationRecyclerView.layoutParams = recyclerViewLayoutParams + } + + private fun setUpTypingObserver() { + ApplicationContext.getInstance(this).typingStatusRepository.getTypists(threadID).observe(this) { state -> + val recipients = if (state != null) state.typists else listOf() + // FIXME: Also checking isScrolledToBottom is a quick fix for an issue where the + // typing indicator overlays the recycler view when scrolled up + typingIndicatorViewContainer.isVisible = recipients.isNotEmpty() && isScrolledToBottom + typingIndicatorViewContainer.setTypists(recipients) + inputBarHeightChanged(inputBar.height) + } + if (TextSecurePreferences.isTypingIndicatorsEnabled(this)) { + inputBar.inputBarEditText.addTextChangedListener(object : SimpleTextWatcher() { + + override fun onTextChanged(text: String?) { + ApplicationContext.getInstance(this@ConversationActivityV2).typingStatusSender.onTypingStarted(threadID) + } + }) + } + } + + private fun setUpRecipientObserver() { + thread.addListener(this) + } + + private fun getLatestOpenGroupInfoIfNeeded() { + val openGroup = DatabaseFactory.getLokiThreadDatabase(this).getOpenGroupChat(threadID) ?: return + OpenGroupAPIV2.getMemberCount(openGroup.room, openGroup.server).successUi { updateSubtitle() } + } + + private fun setUpBlockedBanner() { + if (thread.isGroupRecipient) { return } + val contactDB = DatabaseFactory.getSessionContactDatabase(this) + val sessionID = thread.address.toString() + val contact = contactDB.getContactWithSessionID(sessionID) + val name = contact?.displayName(Contact.ContactContext.REGULAR) ?: sessionID + blockedBannerTextView.text = resources.getString(R.string.activity_conversation_blocked_banner_text, name) + blockedBanner.isVisible = thread.isBlocked + blockedBanner.setOnClickListener { unblock() } + } + + private fun setUpLinkPreviewObserver() { + val linkPreviewViewModel = ViewModelProviders.of(this, LinkPreviewViewModel.Factory(LinkPreviewRepository(this)))[LinkPreviewViewModel::class.java] + this.linkPreviewViewModel = linkPreviewViewModel + if (!TextSecurePreferences.isLinkPreviewsEnabled(this)) { + linkPreviewViewModel.onUserCancel(); return + } + linkPreviewViewModel.linkPreviewState.observe(this, { previewState: LinkPreviewState? -> + if (previewState == null) return@observe + if (previewState.isLoading) { + inputBar.draftLinkPreview() + } else if (previewState.linkPreview.isPresent) { + inputBar.updateLinkPreviewDraft(glide, previewState.linkPreview.get()) + } else { + inputBar.cancelLinkPreviewDraft() + } + }) + } + + private fun scrollToFirstUnreadMessageIfNeeded() { + val lastSeenTimestamp = DatabaseFactory.getThreadDatabase(this).getLastSeenAndHasSent(threadID).first() + val lastSeenItemPosition = adapter.findLastSeenItemPosition(lastSeenTimestamp) ?: return + if (lastSeenItemPosition <= 3) { return } + conversationRecyclerView.scrollToPosition(lastSeenItemPosition) + } + + override fun onPrepareOptionsMenu(menu: Menu): Boolean { + ConversationMenuHelper.onPrepareOptionsMenu(menu, menuInflater, thread, threadID, this) { onOptionsItemSelected(it) } + super.onPrepareOptionsMenu(menu) + return true + } + + override fun onDestroy() { + saveDraft() + super.onDestroy() + } + // endregion + + // region Animation & Updating + override fun onModified(recipient: Recipient) { + runOnUiThread { + if (thread.isContactRecipient) { + blockedBanner.isVisible = thread.isBlocked + } + updateSubtitle() + showOrHideInputIfNeeded() + } + } + + private fun showOrHideInputIfNeeded() { + if (thread.isClosedGroupRecipient) { + val group = DatabaseFactory.getGroupDatabase(this).getGroup(thread.address.toGroupString()).orNull() + val isActive = (group?.isActive == true) + inputBar.showInput = isActive + } else { + inputBar.showInput = true + } + } + + private fun markAllAsRead() { + val messages = DatabaseFactory.getThreadDatabase(this).setRead(threadID, true) + if (thread.isGroupRecipient) { + for (message in messages) { + MarkReadReceiver.scheduleDeletion(this, message.expirationInfo) + } + } else { + MarkReadReceiver.process(this, messages) + } + ApplicationContext.getInstance(this).messageNotifier.updateNotification(this) + } + + override fun inputBarHeightChanged(newValue: Int) { + @Suppress("NAME_SHADOWING") val newValue = max(newValue, resources.getDimension(R.dimen.input_bar_height).roundToInt()) + // 36 DP is the exact height of the typing indicator view. It's also exactly 18 * 2, and 18 is the large message + // corner radius. This makes 36 DP look "correct" in the context of other messages on the screen. + val typingIndicatorHeight = if (typingIndicatorViewContainer.isVisible) toPx(36, resources) else 0 + // Recycler view + val recyclerViewLayoutParams = conversationRecyclerView.layoutParams as RelativeLayout.LayoutParams + recyclerViewLayoutParams.bottomMargin = newValue + typingIndicatorHeight + conversationRecyclerView.layoutParams = recyclerViewLayoutParams + // Additional content container + val additionalContentContainerLayoutParams = additionalContentContainer.layoutParams as RelativeLayout.LayoutParams + additionalContentContainerLayoutParams.bottomMargin = newValue + additionalContentContainer.layoutParams = additionalContentContainerLayoutParams + // Attachment options + val attachmentButtonHeight = inputBar.attachmentsButtonContainer.height + val bottomMargin = (newValue - inputBar.additionalContentHeight - attachmentButtonHeight) / 2 + val margin = toPx(8, resources) + val attachmentOptionsContainerLayoutParams = attachmentOptionsContainer.layoutParams as RelativeLayout.LayoutParams + attachmentOptionsContainerLayoutParams.bottomMargin = bottomMargin + attachmentButtonHeight + margin + attachmentOptionsContainer.layoutParams = attachmentOptionsContainerLayoutParams + // Scroll to bottom button + val scrollToBottomButtonLayoutParams = scrollToBottomButton.layoutParams as RelativeLayout.LayoutParams + scrollToBottomButtonLayoutParams.bottomMargin = newValue + additionalContentContainer.height + toPx(12, resources) + scrollToBottomButton.layoutParams = scrollToBottomButtonLayoutParams + } + + override fun inputBarEditTextContentChanged(newContent: CharSequence) { + if (TextSecurePreferences.isLinkPreviewsEnabled(this)) { + linkPreviewViewModel?.onTextChanged(this, inputBar.text, 0, 0) + } + showOrHideMentionCandidatesIfNeeded(newContent) + if (LinkPreviewUtil.findWhitelistedUrls(newContent.toString()).isNotEmpty() + && !TextSecurePreferences.isLinkPreviewsEnabled(this) && !TextSecurePreferences.hasSeenLinkPreviewSuggestionDialog(this)) { + LinkPreviewDialog { + setUpLinkPreviewObserver() + linkPreviewViewModel?.onEnabled() + linkPreviewViewModel?.onTextChanged(this, inputBar.text, 0, 0) + }.show(supportFragmentManager, "Link Preview Dialog") + TextSecurePreferences.setHasSeenLinkPreviewSuggestionDialog(this) + } + } + + private fun showOrHideMentionCandidatesIfNeeded(text: CharSequence) { + if (text.length < previousText.length) { + currentMentionStartIndex = -1 + hideMentionCandidates() + val mentionsToRemove = mentions.filter { !text.contains(it.displayName) } + mentions.removeAll(mentionsToRemove) + } + if (text.isNotEmpty()) { + val lastCharIndex = text.lastIndex + val lastChar = text[lastCharIndex] + // Check if there is whitespace before the '@' or the '@' is the first character + val isCharacterBeforeLastWhiteSpaceOrStartOfLine: Boolean + if (text.length == 1) { + isCharacterBeforeLastWhiteSpaceOrStartOfLine = true // Start of line + } else { + val charBeforeLast = text[lastCharIndex - 1] + isCharacterBeforeLastWhiteSpaceOrStartOfLine = Character.isWhitespace(charBeforeLast) + } + if (lastChar == '@' && isCharacterBeforeLastWhiteSpaceOrStartOfLine) { + currentMentionStartIndex = lastCharIndex + showOrUpdateMentionCandidatesIfNeeded() + } else if (Character.isWhitespace(lastChar) || lastChar == '@') { // the lastCharacter == "@" is to check for @@ + currentMentionStartIndex = -1 + hideMentionCandidates() + } else if (currentMentionStartIndex != -1) { + val query = text.substring(currentMentionStartIndex + 1) // + 1 to get rid of the "@" + showOrUpdateMentionCandidatesIfNeeded(query) + } + } + previousText = text + } + + private fun showOrUpdateMentionCandidatesIfNeeded(query: String = "") { + if (!isShowingMentionCandidatesView) { + additionalContentContainer.removeAllViews() + val view = MentionCandidatesView(this) + view.glide = glide + view.onCandidateSelected = { handleMentionSelected(it) } + additionalContentContainer.addView(view) + val candidates = MentionsManager.getMentionCandidates(query, threadID, thread.isOpenGroupRecipient) + this.mentionCandidatesView = view + view.show(candidates, threadID) + view.alpha = 0.0f + val animation = ValueAnimator.ofObject(FloatEvaluator(), view.alpha, 1.0f) + animation.duration = 250L + animation.addUpdateListener { animator -> + view.alpha = animator.animatedValue as Float + } + animation.start() + } else { + val candidates = MentionsManager.getMentionCandidates(query, threadID, thread.isOpenGroupRecipient) + this.mentionCandidatesView!!.setMentionCandidates(candidates) + } + isShowingMentionCandidatesView = true + } + + private fun hideMentionCandidates() { + if (isShowingMentionCandidatesView) { + val mentionCandidatesView = mentionCandidatesView ?: return + val animation = ValueAnimator.ofObject(FloatEvaluator(), mentionCandidatesView.alpha, 0.0f) + animation.duration = 250L + animation.addUpdateListener { animator -> + mentionCandidatesView.alpha = animator.animatedValue as Float + if (animator.animatedFraction == 1.0f) { additionalContentContainer.removeAllViews() } + } + animation.start() + } + isShowingMentionCandidatesView = false + } + + override fun toggleAttachmentOptions() { + val targetAlpha = if (isShowingAttachmentOptions) 0.0f else 1.0f + val allButtonContainers = listOf( cameraButtonContainer, libraryButtonContainer, documentButtonContainer, gifButtonContainer) + val isReversed = isShowingAttachmentOptions // Run the animation in reverse + val count = allButtonContainers.size + allButtonContainers.indices.forEach { index -> + val view = allButtonContainers[index] + val animation = ValueAnimator.ofObject(FloatEvaluator(), view.alpha, targetAlpha) + animation.duration = 250L + animation.startDelay = if (isReversed) 50L * (count - index.toLong()) else 50L * index.toLong() + animation.addUpdateListener { animator -> + view.alpha = animator.animatedValue as Float + } + animation.start() + } + isShowingAttachmentOptions = !isShowingAttachmentOptions + val allButtons = listOf( cameraButton, libraryButton, documentButton, gifButton ) + allButtons.forEach { it.snIsEnabled = isShowingAttachmentOptions } + } + + override fun showVoiceMessageUI() { + inputBarRecordingView.show() + inputBar.alpha = 0.0f + val animation = ValueAnimator.ofObject(FloatEvaluator(), 1.0f, 0.0f) + animation.duration = 250L + animation.addUpdateListener { animator -> + inputBar.alpha = animator.animatedValue as Float + } + animation.start() + } + + private fun expandVoiceMessageLockView() { + val animation = ValueAnimator.ofObject(FloatEvaluator(), lockView.scaleX, 1.10f) + animation.duration = 250L + animation.addUpdateListener { animator -> + lockView.scaleX = animator.animatedValue as Float + lockView.scaleY = animator.animatedValue as Float + } + animation.start() + } + + private fun collapseVoiceMessageLockView() { + val animation = ValueAnimator.ofObject(FloatEvaluator(), lockView.scaleX, 1.0f) + animation.duration = 250L + animation.addUpdateListener { animator -> + lockView.scaleX = animator.animatedValue as Float + lockView.scaleY = animator.animatedValue as Float + } + animation.start() + } + + private fun hideVoiceMessageUI() { + val chevronImageView = inputBarRecordingView.inputBarChevronImageView + val slideToCancelTextView = inputBarRecordingView.inputBarSlideToCancelTextView + listOf( chevronImageView, slideToCancelTextView ).forEach { view -> + val animation = ValueAnimator.ofObject(FloatEvaluator(), view.translationX, 0.0f) + animation.duration = 250L + animation.addUpdateListener { animator -> + view.translationX = animator.animatedValue as Float + } + animation.start() + } + inputBarRecordingView.hide() + } + + override fun handleVoiceMessageUIHidden() { + inputBar.alpha = 1.0f + val animation = ValueAnimator.ofObject(FloatEvaluator(), 0.0f, 1.0f) + animation.duration = 250L + animation.addUpdateListener { animator -> + inputBar.alpha = animator.animatedValue as Float + } + animation.start() + } + + private fun handleRecyclerViewScrolled() { + val alpha = if (!isScrolledToBottom) 1.0f else 0.0f + // FIXME: Checking isScrolledToBottom is a quick fix for an issue where the + // typing indicator overlays the recycler view when scrolled up + val wasTypingIndicatorVisibleBefore = typingIndicatorViewContainer.isVisible + typingIndicatorViewContainer.isVisible = wasTypingIndicatorVisibleBefore && isScrolledToBottom + val isTypingIndicatorVisibleAfter = typingIndicatorViewContainer.isVisible + if (isTypingIndicatorVisibleAfter != wasTypingIndicatorVisibleBefore) { + inputBarHeightChanged(inputBar.height) + } + scrollToBottomButton.alpha = alpha + unreadCount = min(unreadCount, layoutManager.findFirstVisibleItemPosition()) + updateUnreadCountIndicator() + } + + private fun updateUnreadCountIndicator() { + val formattedUnreadCount = if (unreadCount < 100) unreadCount.toString() else "99+" + unreadCountTextView.text = formattedUnreadCount + val textSize = if (unreadCount < 100) 12.0f else 9.0f + unreadCountTextView.setTextSize(TypedValue.COMPLEX_UNIT_DIP, textSize) + unreadCountTextView.setTypeface(Typeface.DEFAULT, if (unreadCount < 100) Typeface.BOLD else Typeface.NORMAL) + unreadCountIndicator.isVisible = (unreadCount != 0) + } + + private fun updateSubtitle() { + muteIconImageView.isVisible = thread.isMuted + conversationSubtitleView.isVisible = true + if (thread.isMuted) { + conversationSubtitleView.text = getString(R.string.ConversationActivity_muted_until_date, DateUtils.getFormattedDateTime(thread.mutedUntil, "EEE, MMM d, yyyy HH:mm", Locale.getDefault())) + } else if (thread.isGroupRecipient) { + val openGroup = DatabaseFactory.getLokiThreadDatabase(this).getOpenGroupChat(threadID) + if (openGroup != null) { + val userCount = DatabaseFactory.getLokiAPIDatabase(this).getUserCount(openGroup.room, openGroup.server) ?: 0 + conversationSubtitleView.text = getString(R.string.ConversationActivity_member_count, userCount) + } else { + conversationSubtitleView.isVisible = false + } + } else { + conversationSubtitleView.isVisible = false + } + } + // endregion + + // region Interaction + override fun onOptionsItemSelected(item: MenuItem): Boolean { + if (item.itemId == android.R.id.home) { + return false + } + return ConversationMenuHelper.onOptionItemSelected(this, item, thread) + } + + // `position` is the adapter position; not the visual position + private fun handlePress(message: MessageRecord, position: Int, view: VisibleMessageView, event: MotionEvent) { + val actionMode = this.actionMode + if (actionMode != null) { + adapter.toggleSelection(message, position) + val actionModeCallback = ConversationActionModeCallback(adapter, threadID, this) + actionModeCallback.delegate = this + actionModeCallback.updateActionModeMenu(actionMode.menu) + if (adapter.selectedItems.isEmpty()) { + actionMode.finish() + this.actionMode = null + } + } else { + // NOTE: + // We have to use onContentClick (rather than a click listener directly on + // the view) so as to not interfere with all the other gestures. Do not add + // onClickListeners directly to message content views. + view.onContentClick(event) + } + } + + // `position` is the adapter position; not the visual position + private fun handleSwipeToReply(message: MessageRecord, position: Int) { + inputBar.draftQuote(thread, message, glide) + } + + // `position` is the adapter position; not the visual position + private fun handleLongPress(message: MessageRecord, position: Int) { + val actionMode = this.actionMode + val actionModeCallback = ConversationActionModeCallback(adapter, threadID, this) + actionModeCallback.delegate = this + searchViewItem?.collapseActionView() + if (actionMode == null) { // Nothing should be selected if this is the case + adapter.toggleSelection(message, position) + this.actionMode = if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.M) { + startActionMode(actionModeCallback, ActionMode.TYPE_PRIMARY) + } else { + startActionMode(actionModeCallback) + } + } else { + adapter.toggleSelection(message, position) + actionModeCallback.updateActionModeMenu(actionMode.menu) + if (adapter.selectedItems.isEmpty()) { + actionMode.finish() + this.actionMode = null + } + } + } + + override fun onMicrophoneButtonMove(event: MotionEvent) { + val rawX = event.rawX + val chevronImageView = inputBarRecordingView.inputBarChevronImageView + val slideToCancelTextView = inputBarRecordingView.inputBarSlideToCancelTextView + if (rawX < screenWidth / 2) { + val translationX = rawX - screenWidth / 2 + val sign = -1.0f + val chevronDamping = 4.0f + val labelDamping = 3.0f + val chevronX = (chevronDamping * (sqrt(abs(translationX)) / sqrt(chevronDamping))) * sign + val labelX = (labelDamping * (sqrt(abs(translationX)) / sqrt(labelDamping))) * sign + chevronImageView.translationX = chevronX + slideToCancelTextView.translationX = labelX + } else { + chevronImageView.translationX = 0.0f + slideToCancelTextView.translationX = 0.0f + } + if (isValidLockViewLocation(event.rawX.roundToInt(), event.rawY.roundToInt())) { + if (!isLockViewExpanded) { + expandVoiceMessageLockView() + isLockViewExpanded = true + } + } else { + if (isLockViewExpanded) { + collapseVoiceMessageLockView() + isLockViewExpanded = false + } + } + } + + override fun onMicrophoneButtonCancel(event: MotionEvent) { + hideVoiceMessageUI() + } + + override fun onMicrophoneButtonUp(event: MotionEvent) { + val x = event.rawX.roundToInt() + val y = event.rawY.roundToInt() + if (isValidLockViewLocation(x, y)) { + inputBarRecordingView.lock() + } else { + val recordButtonOverlay = inputBarRecordingView.recordButtonOverlay + val location = IntArray(2) { 0 } + recordButtonOverlay.getLocationOnScreen(location) + val hitRect = Rect(location[0], location[1], location[0] + recordButtonOverlay.width, location[1] + recordButtonOverlay.height) + if (hitRect.contains(x, y)) { + sendVoiceMessage() + } else { + cancelVoiceMessage() + } + } + } + + private fun isValidLockViewLocation(x: Int, y: Int): Boolean { + // We can be anywhere above the lock view and a bit to the side of it (at most `lockViewHitMargin` + // to the side) + val lockViewLocation = IntArray(2) { 0 } + lockView.getLocationOnScreen(lockViewLocation) + val hitRect = Rect(lockViewLocation[0] - lockViewHitMargin, 0, + lockViewLocation[0] + lockView.width + lockViewHitMargin, lockViewLocation[1] + lockView.height) + return hitRect.contains(x, y) + } + + private fun unblock() { + if (!thread.isContactRecipient) { return } + DatabaseFactory.getRecipientDatabase(this).setBlocked(thread, false) + } + + private fun handleMentionSelected(mention: Mention) { + if (currentMentionStartIndex == -1) { return } + mentions.add(mention) + val previousText = inputBar.text + val newText = previousText.substring(0, currentMentionStartIndex) + "@" + mention.displayName + " " + inputBar.text = newText + inputBar.inputBarEditText.setSelection(newText.length) + currentMentionStartIndex = -1 + hideMentionCandidates() + this.previousText = newText + } + + override fun scrollToMessageIfPossible(timestamp: Long) { + val lastSeenItemPosition = adapter.getItemPositionForTimestamp(timestamp) ?: return + conversationRecyclerView.scrollToPosition(lastSeenItemPosition) + } + + override fun playNextAudioIfPossible(current: Int) { + if (current > 0) { + val nextVisibleMessageView = conversationRecyclerView[current - 1] as? VisibleMessageView + nextVisibleMessageView?.let { visibleMessageView -> + visibleMessageView.messageContentView.mainContainer.children.forEach { child -> + val nextVoiceMessageView = child as? VoiceMessageView + nextVoiceMessageView?.let { voiceMessageView -> + voiceMessageView.togglePlayback() + return@forEach + } + } + } + } + } + + override fun sendMessage() { + if (thread.isContactRecipient && thread.isBlocked) { + BlockedDialog(thread).show(supportFragmentManager, "Blocked Dialog") + return + } + if (inputBar.linkPreview != null || inputBar.quote != null) { + sendAttachments(listOf(), getMessageBody(), inputBar.quote, inputBar.linkPreview) + } else { + sendTextOnlyMessage() + } + } + + private fun sendTextOnlyMessage() { + // Create the message + val message = VisibleMessage() + message.sentTimestamp = System.currentTimeMillis() + message.text = getMessageBody() + val outgoingTextMessage = OutgoingTextMessage.from(message, thread) + // Clear the input bar + inputBar.text = "" + inputBar.cancelQuoteDraft() + inputBar.cancelLinkPreviewDraft() + // Clear mentions + previousText = "" + currentMentionStartIndex = -1 + mentions.clear() + // Put the message in the database + message.id = DatabaseFactory.getSmsDatabase(this).insertMessageOutbox(threadID, outgoingTextMessage, false, message.sentTimestamp!!) { } + // Send it + MessageSender.send(message, thread.address) + // Send a typing stopped message + ApplicationContext.getInstance(this).typingStatusSender.onTypingStopped(threadID) + } + + private fun sendAttachments(attachments: List, body: String?, quotedMessage: MessageRecord? = null, linkPreview: LinkPreview? = null) { + // Create the message + val message = VisibleMessage() + message.sentTimestamp = System.currentTimeMillis() + message.text = body + val quote = quotedMessage?.let { + val quotedAttachments = (it as? MmsMessageRecord)?.slideDeck?.asAttachments() ?: listOf() + val sender = if (it.isOutgoing) fromSerialized(TextSecurePreferences.getLocalNumber(this)!!) else it.individualRecipient.address + QuoteModel(it.dateSent, sender, it.body, false, quotedAttachments) + } + val outgoingTextMessage = OutgoingMediaMessage.from(message, thread, attachments, quote, linkPreview) + // Clear the input bar + inputBar.text = "" + inputBar.cancelQuoteDraft() + inputBar.cancelLinkPreviewDraft() + // Clear mentions + previousText = "" + currentMentionStartIndex = -1 + mentions.clear() + // Reset the attachment manager + attachmentManager.clear() + // Reset attachments button if needed + if (isShowingAttachmentOptions) { toggleAttachmentOptions() } + // Put the message in the database + message.id = DatabaseFactory.getMmsDatabase(this).insertMessageOutbox(outgoingTextMessage, threadID, false) { } + // Send it + MessageSender.send(message, thread.address, attachments, quote, linkPreview) + // Send a typing stopped message + ApplicationContext.getInstance(this).typingStatusSender.onTypingStopped(threadID) + } + + private fun showGIFPicker() { + AttachmentManager.selectGif(this, ConversationActivityV2.PICK_GIF) + } + + private fun showDocumentPicker() { + AttachmentManager.selectDocument(this, ConversationActivityV2.PICK_DOCUMENT) + } + + private fun pickFromLibrary() { + AttachmentManager.selectGallery(this, ConversationActivityV2.PICK_FROM_LIBRARY, thread, inputBar.text.trim()) + } + + private fun showCamera() { + attachmentManager.capturePhoto(this, ConversationActivityV2.TAKE_PHOTO) + } + + override fun onAttachmentChanged() { + // Do nothing + } + + override fun onActivityResult(requestCode: Int, resultCode: Int, intent: Intent?) { + super.onActivityResult(requestCode, resultCode, intent) + val mediaPreppedListener = object : ListenableFuture.Listener { + + override fun onSuccess(result: Boolean?) { + sendAttachments(attachmentManager.buildSlideDeck().asAttachments(), null) + } + + override fun onFailure(e: ExecutionException?) { + Toast.makeText(this@ConversationActivityV2, R.string.activity_conversation_attachment_prep_failed, Toast.LENGTH_LONG).show() + } + } + when (requestCode) { + PICK_DOCUMENT -> { + val uri = intent?.data ?: return + prepMediaForSending(uri, AttachmentManager.MediaType.DOCUMENT).addListener(mediaPreppedListener) + } + TAKE_PHOTO -> { + if (resultCode != RESULT_OK) { return } + val uri = attachmentManager.captureUri ?: return + prepMediaForSending(uri, AttachmentManager.MediaType.IMAGE).addListener(mediaPreppedListener) + } + PICK_GIF -> { + intent ?: return + val uri = intent.data ?: return + val type = AttachmentManager.MediaType.GIF + val width = intent.getIntExtra(GiphyActivity.EXTRA_WIDTH, 0) + val height = intent.getIntExtra(GiphyActivity.EXTRA_HEIGHT, 0) + prepMediaForSending(uri, type, width, height).addListener(mediaPreppedListener) + } + PICK_FROM_LIBRARY -> { + intent ?: return + val body = intent.getStringExtra(MediaSendActivity.EXTRA_MESSAGE) + val media = intent.getParcelableArrayListExtra(MediaSendActivity.EXTRA_MEDIA) ?: return + val slideDeck = SlideDeck() + for (item in media) { + when { + MediaUtil.isVideoType(item.mimeType) -> { + slideDeck.addSlide(VideoSlide(this, item.uri, 0, item.caption.orNull())) + } + MediaUtil.isGif(item.mimeType) -> { + slideDeck.addSlide(GifSlide(this, item.uri, 0, item.width, item.height, item.caption.orNull())) + } + MediaUtil.isImageType(item.mimeType) -> { + slideDeck.addSlide(ImageSlide(this, item.uri, 0, item.width, item.height, item.caption.orNull())) + } + else -> { + Log.d("Loki", "Asked to send an unexpected media type: '" + item.mimeType + "'. Skipping.") + } + } + } + sendAttachments(slideDeck.asAttachments(), body) + } + INVITE_CONTACTS -> { + if (!thread.isOpenGroupRecipient) { return } + val extras = intent?.extras ?: return + if (!intent.hasExtra(SelectContactsActivity.selectedContactsKey)) { return } + val selectedContacts = extras.getStringArray(selectedContactsKey)!! + val openGroup = DatabaseFactory.getLokiThreadDatabase(this).getOpenGroupChat(threadID) + for (contact in selectedContacts) { + val recipient = Recipient.from(this, fromSerialized(contact), true) + val message = VisibleMessage() + message.sentTimestamp = System.currentTimeMillis() + val openGroupInvitation = OpenGroupInvitation() + openGroupInvitation.name = openGroup!!.name + openGroupInvitation.url = openGroup!!.joinURL + message.openGroupInvitation = openGroupInvitation + val outgoingTextMessage = OutgoingTextMessage.fromOpenGroupInvitation(openGroupInvitation, recipient, message.sentTimestamp) + DatabaseFactory.getSmsDatabase(this).insertMessageOutbox(-1, outgoingTextMessage, message.sentTimestamp!!) + MessageSender.send(message, recipient.address) + } + } + } + } + + private fun prepMediaForSending(uri: Uri, type: AttachmentManager.MediaType): ListenableFuture { + return prepMediaForSending(uri, type, null, null) + } + + private fun prepMediaForSending(uri: Uri, type: AttachmentManager.MediaType, width: Int?, height: Int?): ListenableFuture { + return attachmentManager.setMedia(glide, uri, type, MediaConstraints.getPushMediaConstraints(), width ?: 0, height ?: 0) + } + + override fun startRecordingVoiceMessage() { + if (Permissions.hasAll(this, Manifest.permission.RECORD_AUDIO)) { + showVoiceMessageUI() + window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) + audioRecorder.startRecording() + stopAudioHandler.postDelayed(stopVoiceMessageRecordingTask, 60000) // Limit voice messages to 1 minute each + } else { + Permissions.with(this) + .request(Manifest.permission.RECORD_AUDIO) + .withRationaleDialog(getString(R.string.ConversationActivity_to_send_audio_messages_allow_signal_access_to_your_microphone), R.drawable.ic_baseline_mic_48) + .withPermanentDenialDialog(getString(R.string.ConversationActivity_signal_requires_the_microphone_permission_in_order_to_send_audio_messages)) + .execute() + } + } + + override fun sendVoiceMessage() { + hideVoiceMessageUI() + window.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) + val future = audioRecorder.stopRecording() + stopAudioHandler.removeCallbacks(stopVoiceMessageRecordingTask) + future.addListener(object : ListenableFuture.Listener> { + + override fun onSuccess(result: Pair) { + val audioSlide = AudioSlide(this@ConversationActivityV2, result.first, result.second, MediaTypes.AUDIO_AAC, true) + val slideDeck = SlideDeck() + slideDeck.addSlide(audioSlide) + sendAttachments(slideDeck.asAttachments(), null) + } + + override fun onFailure(e: ExecutionException) { + Toast.makeText(this@ConversationActivityV2, R.string.ConversationActivity_unable_to_record_audio, Toast.LENGTH_LONG).show() + } + }) + } + + override fun cancelVoiceMessage() { + hideVoiceMessageUI() + window.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) + audioRecorder.stopRecording() + stopAudioHandler.removeCallbacks(stopVoiceMessageRecordingTask) + } + + override fun deleteMessages(messages: Set) { + val messageCount = messages.size + val messageDataProvider = MessagingModuleConfiguration.shared.messageDataProvider + val messageDB = DatabaseFactory.getLokiMessageDatabase(this@ConversationActivityV2) + val builder = AlertDialog.Builder(this) + builder.setTitle(resources.getQuantityString(R.plurals.ConversationFragment_delete_selected_messages, messageCount, messageCount)) + builder.setMessage(resources.getQuantityString(R.plurals.ConversationFragment_this_will_permanently_delete_all_n_selected_messages, messageCount, messageCount)) + builder.setCancelable(true) + val openGroup = DatabaseFactory.getLokiThreadDatabase(this).getOpenGroupChat(threadID) + builder.setPositiveButton(R.string.delete) { _, _ -> + if (openGroup != null) { + val messageServerIDs = mutableMapOf() + for (message in messages) { + val messageServerID = messageDB.getServerID(message.id, !message.isMms) ?: continue + messageServerIDs[messageServerID] = message + } + for ((messageServerID, message) in messageServerIDs) { + OpenGroupAPIV2.deleteMessage(messageServerID, openGroup.room, openGroup.server) + .success { + messageDataProvider.deleteMessage(message.id, !message.isMms) + }.failUi { error -> + Toast.makeText(this@ConversationActivityV2, "Couldn't delete message due to error: $error", Toast.LENGTH_LONG).show() + } + } + } else { + for (message in messages) { + if (message.isMms) { + DatabaseFactory.getMmsDatabase(this@ConversationActivityV2).delete(message.id) + } else { + DatabaseFactory.getSmsDatabase(this@ConversationActivityV2).deleteMessage(message.id) + } + } + } + endActionMode() + } + builder.setNegativeButton(android.R.string.cancel) { dialog, _ -> + dialog.dismiss() + endActionMode() + } + builder.show() + } + + override fun banUser(messages: Set) { + val builder = AlertDialog.Builder(this) + val sessionID = messages.first().individualRecipient.address.toString() + builder.setTitle(R.string.ConversationFragment_ban_selected_user) + builder.setMessage("This will ban the selected user from this room. It won't ban them from other rooms. The selected user won't know that they've been banned.") + builder.setCancelable(true) + val openGroup = DatabaseFactory.getLokiThreadDatabase(this).getOpenGroupChat(threadID)!! + builder.setPositiveButton(R.string.ban) { _, _ -> + OpenGroupAPIV2.ban(sessionID, openGroup.room, openGroup.server).successUi { + Toast.makeText(this@ConversationActivityV2, "Successfully banned user", Toast.LENGTH_LONG).show() + }.failUi { error -> + Toast.makeText(this@ConversationActivityV2, "Couldn't ban user due to error: $error", Toast.LENGTH_LONG).show() + } + endActionMode() + } + builder.setNegativeButton(android.R.string.cancel) { dialog, _ -> + dialog.dismiss() + endActionMode() + } + builder.show() + } + + override fun copyMessages(messages: Set) { + val sortedMessages = messages.sortedBy { it.dateSent } + val builder = StringBuilder() + for (message in sortedMessages) { + val body = MentionUtilities.highlightMentions(message.body, message.threadId, this) + if (TextUtils.isEmpty(body)) { continue } + val formattedTimestamp = DateUtils.getDisplayFormattedTimeSpanString(this, Locale.getDefault(), message.timestamp) + builder.append("$formattedTimestamp: $body").append('\n') + } + if (builder.isNotEmpty() && builder[builder.length - 1] == '\n') { + builder.deleteCharAt(builder.length - 1) + } + val result = builder.toString() + if (TextUtils.isEmpty(result)) { return } + val manager = getSystemService(CLIPBOARD_SERVICE) as ClipboardManager + manager.setPrimaryClip(ClipData.newPlainText("Message Content", result)) + Toast.makeText(this, R.string.copied_to_clipboard, Toast.LENGTH_SHORT).show() + endActionMode() + } + + override fun copySessionID(messages: Set) { + val sessionID = messages.first().individualRecipient.address.toString() + val clip = ClipData.newPlainText("Session ID", sessionID) + val manager = getSystemService(CLIPBOARD_SERVICE) as ClipboardManager + manager.setPrimaryClip(clip) + Toast.makeText(this, R.string.copied_to_clipboard, Toast.LENGTH_SHORT).show() + endActionMode() + } + + override fun resendMessage(messages: Set) { + messages.forEach { messageRecord -> + val recipient: Recipient = messageRecord.recipient + val message = VisibleMessage() + message.id = messageRecord.getId() + if (messageRecord.isOpenGroupInvitation) { + val openGroupInvitation = OpenGroupInvitation() + fromJSON(messageRecord.body)?.let { updateMessageData -> + val kind = updateMessageData.kind + if (kind is UpdateMessageData.Kind.OpenGroupInvitation) { + openGroupInvitation.name = kind.groupName + openGroupInvitation.url = kind.groupUrl + } + } + message.openGroupInvitation = openGroupInvitation + } else { + message.text = messageRecord.body + } + message.sentTimestamp = messageRecord.timestamp + if (recipient.isGroupRecipient) { + message.groupPublicKey = recipient.address.toGroupString() + } else { + message.recipient = messageRecord.recipient.address.serialize() + } + message.threadID = messageRecord.threadId + if (messageRecord.isMms) { + val mmsMessageRecord = messageRecord as MmsMessageRecord + if (mmsMessageRecord.linkPreviews.isNotEmpty()) { + message.linkPreview = from(mmsMessageRecord.linkPreviews[0]) + } + if (mmsMessageRecord.quote != null) { + message.quote = from(mmsMessageRecord.quote!!.quoteModel) + } + message.addSignalAttachments(mmsMessageRecord.slideDeck.asAttachments()) + } + val sentTimestamp = message.sentTimestamp + val sender = MessagingModuleConfiguration.shared.storage.getUserPublicKey() + if (sentTimestamp != null && sender != null) { + MessagingModuleConfiguration.shared.storage.markAsSending(sentTimestamp, sender) + } + MessageSender.send(message, recipient.address) + } + endActionMode() + } + + override fun saveAttachment(messages: Set) { + val message = messages.first() as MmsMessageRecord + SaveAttachmentTask.showWarningDialog(this, { _, _ -> + Permissions.with(this) + .request(Manifest.permission.WRITE_EXTERNAL_STORAGE) + .maxSdkVersion(Build.VERSION_CODES.P) + .withPermanentDenialDialog(getString(R.string.MediaPreviewActivity_signal_needs_the_storage_permission_in_order_to_write_to_external_storage_but_it_has_been_permanently_denied)) + .onAnyDenied { + endActionMode() + Toast.makeText(this@ConversationActivityV2, R.string.MediaPreviewActivity_unable_to_write_to_external_storage_without_permission, Toast.LENGTH_LONG).show() + } + .onAllGranted { + endActionMode() + val attachments: List = Stream.of(message.slideDeck.slides) + .filter { s: Slide -> s.uri != null && (s.hasImage() || s.hasVideo() || s.hasAudio() || s.hasDocument()) } + .map { s: Slide -> SaveAttachmentTask.Attachment(s.uri!!, s.contentType, message.dateReceived, s.fileName.orNull()) } + .toList() + if (attachments.isNotEmpty()) { + val saveTask = SaveAttachmentTask(this) + saveTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, *attachments.toTypedArray()) + if (!message.isOutgoing) { + sendMediaSavedNotification() + } + return@onAllGranted + } + Toast.makeText(this, + resources.getQuantityString(R.plurals.ConversationFragment_error_while_saving_attachments_to_sd_card, 1), + Toast.LENGTH_LONG).show() + } + .execute() + }) + } + + override fun reply(messages: Set) { + inputBar.draftQuote(thread, messages.first(), glide) + endActionMode() + } + + private fun sendMediaSavedNotification() { + if (thread.isGroupRecipient) { return } + val timestamp = System.currentTimeMillis() + val kind = DataExtractionNotification.Kind.MediaSaved(timestamp) + val message = DataExtractionNotification(kind) + MessageSender.send(message, thread.address) + } + + private fun endActionMode() { + actionMode?.finish() + actionMode = null + } + // endregion + + // region General + private fun getMessageBody(): String { + var result = inputBar.inputBarEditText.text?.trim() ?: "" + for (mention in mentions) { + try { + val startIndex = result.indexOf("@" + mention.displayName) + val endIndex = startIndex + mention.displayName.count() + 1 // + 1 to include the "@" + result = result.substring(0, startIndex) + "@" + mention.publicKey + result.substring(endIndex) + } catch (exception: Exception) { + Log.d("Loki", "Failed to process mention due to error: $exception") + } + } + return result.toString() + } + + private fun saveDraft() { + val text = inputBar?.text?.trim() ?: return + if (text.isEmpty()) { return } + val drafts = Drafts() + drafts.add(DraftDatabase.Draft(DraftDatabase.Draft.TEXT, text)) + val draftDB = DatabaseFactory.getDraftDatabase(this) + draftDB.insertDrafts(threadID, drafts) + } + // endregion + + // region Search + private fun setUpSearchResultObserver() { + val searchViewModel = ViewModelProvider(this).get(SearchViewModel::class.java) + this.searchViewModel = searchViewModel + searchViewModel.searchResults.observe(this, Observer { result: SearchViewModel.SearchResult? -> + if (result == null) return@Observer + if (result.getResults().isNotEmpty()) { + result.getResults()[result.position]?.let { + jumpToMessage(it.messageRecipient.address, it.receivedTimestampMs, Runnable { searchViewModel.onMissingResult() }) + } + } + this.searchBottomBar.setData(result.position, result.getResults().size) + }) + } + + fun onSearchQueryUpdated(query: String?) { + adapter.onSearchQueryUpdated(query) + } + + override fun onSearchMoveUpPressed() { + this.searchViewModel?.onMoveUp() + } + + override fun onSearchMoveDownPressed() { + this.searchViewModel?.onMoveDown() + } + + private fun jumpToMessage(author: Address, timestamp: Long, onMessageNotFound: Runnable?) { + SimpleTask.run(lifecycle, { + DatabaseFactory.getMmsSmsDatabase(this).getMessagePositionInConversation(threadID, timestamp, author) + }) { p: Int -> moveToMessagePosition(p, onMessageNotFound) } + } + + private fun moveToMessagePosition(position: Int, onMessageNotFound: Runnable?) { + if (position >= 0) { + conversationRecyclerView.scrollToPosition(position) + } else { + onMessageNotFound?.run() + } + } + // endregion +} \ No newline at end of file diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationAdapter.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationAdapter.kt new file mode 100644 index 0000000000..a4a5f1ba28 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationAdapter.kt @@ -0,0 +1,143 @@ +package org.thoughtcrime.securesms.conversation.v2 + +import android.content.Context +import android.database.Cursor +import android.graphics.Rect +import android.view.MotionEvent +import android.view.ViewGroup +import androidx.core.view.isVisible +import androidx.recyclerview.widget.RecyclerView.ViewHolder +import kotlinx.android.synthetic.main.view_visible_message.view.* +import org.thoughtcrime.securesms.conversation.v2.messages.ControlMessageView +import org.thoughtcrime.securesms.conversation.v2.messages.VisibleMessageContentViewDelegate +import org.thoughtcrime.securesms.conversation.v2.messages.VisibleMessageView +import org.thoughtcrime.securesms.database.CursorRecyclerViewAdapter +import org.thoughtcrime.securesms.database.DatabaseFactory +import org.thoughtcrime.securesms.database.model.MessageRecord +import org.thoughtcrime.securesms.mms.GlideRequests + +class ConversationAdapter(context: Context, cursor: Cursor, private val onItemPress: (MessageRecord, Int, VisibleMessageView, MotionEvent) -> Unit, + private val onItemSwipeToReply: (MessageRecord, Int) -> Unit, private val onItemLongPress: (MessageRecord, Int) -> Unit, + private val glide: GlideRequests) + : CursorRecyclerViewAdapter(context, cursor) { + private val messageDB = DatabaseFactory.getMmsSmsDatabase(context) + var selectedItems = mutableSetOf() + private var searchQuery: String? = null + var visibleMessageContentViewDelegate: VisibleMessageContentViewDelegate? = null + + sealed class ViewType(val rawValue: Int) { + object Visible : ViewType(0) + object Control : ViewType(1) + + companion object { + + val allValues: Map get() = mapOf( + Visible.rawValue to Visible, + Control.rawValue to Control + ) + } + } + + class VisibleMessageViewHolder(val view: VisibleMessageView) : ViewHolder(view) + class ControlMessageViewHolder(val view: ControlMessageView) : ViewHolder(view) + + override fun getItemViewType(cursor: Cursor): Int { + val message = getMessage(cursor)!! + if (message.isControlMessage) { return ViewType.Control.rawValue } + return ViewType.Visible.rawValue + } + + override fun onCreateItemViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { + @Suppress("NAME_SHADOWING") + val viewType = ViewType.allValues[viewType] + when (viewType) { + ViewType.Visible -> { + val view = VisibleMessageView(context) + return VisibleMessageViewHolder(view) + } + ViewType.Control -> { + val view = ControlMessageView(context) + return ControlMessageViewHolder(view) + } + else -> throw IllegalStateException("Unexpected view type: $viewType.") + } + } + + override fun onBindItemViewHolder(viewHolder: ViewHolder, cursor: Cursor) { + val message = getMessage(cursor)!! + when (viewHolder) { + is VisibleMessageViewHolder -> { + val view = viewHolder.view + val isSelected = selectedItems.contains(message) + view.snIsSelected = isSelected + view.messageTimestampTextView.isVisible = isSelected + val position = viewHolder.adapterPosition + view.viewHolderIndex = position + view.bind(message, getMessageBefore(position, cursor), getMessageAfter(position, cursor), glide, searchQuery) + view.onPress = { event -> onItemPress(message, viewHolder.adapterPosition, view, event) } + view.onSwipeToReply = { onItemSwipeToReply(message, viewHolder.adapterPosition) } + view.onLongPress = { onItemLongPress(message, viewHolder.adapterPosition) } + view.contentViewDelegate = visibleMessageContentViewDelegate + } + is ControlMessageViewHolder -> viewHolder.view.bind(message) + } + } + + override fun onItemViewRecycled(viewHolder: ViewHolder?) { + when (viewHolder) { + is VisibleMessageViewHolder -> viewHolder.view.recycle() + is ControlMessageViewHolder -> viewHolder.view.recycle() + } + super.onItemViewRecycled(viewHolder) + } + + private fun getMessage(cursor: Cursor): MessageRecord? { + return messageDB.readerFor(cursor).current + } + + private fun getMessageBefore(position: Int, cursor: Cursor): MessageRecord? { + // The message that's visually before the current one is actually after the current + // one for the cursor because the layout is reversed + if (!cursor.moveToPosition(position + 1)) { return null } + return messageDB.readerFor(cursor).current + } + + private fun getMessageAfter(position: Int, cursor: Cursor): MessageRecord? { + // The message that's visually after the current one is actually before the current + // one for the cursor because the layout is reversed + if (!cursor.moveToPosition(position - 1)) { return null } + return messageDB.readerFor(cursor).current + } + + fun toggleSelection(message: MessageRecord, position: Int) { + if (selectedItems.contains(message)) selectedItems.remove(message) else selectedItems.add(message) + notifyItemChanged(position) + } + + fun findLastSeenItemPosition(lastSeenTimestamp: Long): Int? { + val cursor = this.cursor + if (lastSeenTimestamp <= 0L || cursor == null || !isActiveCursor) return null + for (i in 0 until itemCount) { + cursor.moveToPosition(i) + val message = messageDB.readerFor(cursor).current + if (message.isOutgoing || message.dateReceived <= lastSeenTimestamp) { return i } + } + return null + } + + fun getItemPositionForTimestamp(timestamp: Long): Int? { + val cursor = this.cursor + if (timestamp <= 0L || cursor == null || !isActiveCursor) return null + for (i in 0 until itemCount) { + cursor.moveToPosition(i) + val message = messageDB.readerFor(cursor).current + if (message.dateSent == timestamp) { return i } + } + return null + } + + fun onSearchQueryUpdated(query: String?) { + this.searchQuery = query + notifyDataSetChanged() + } +} \ No newline at end of file diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationLoader.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationLoader.kt new file mode 100644 index 0000000000..08b5a02641 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationLoader.kt @@ -0,0 +1,13 @@ +package org.thoughtcrime.securesms.conversation.v2 + +import android.content.Context +import android.database.Cursor +import org.thoughtcrime.securesms.database.DatabaseFactory +import org.thoughtcrime.securesms.util.AbstractCursorLoader + +class ConversationLoader(private val threadID: Long, context: Context) : AbstractCursorLoader(context) { + + override fun getCursor(): Cursor { + return DatabaseFactory.getMmsSmsDatabase(context).getConversation(threadID) + } +} \ No newline at end of file diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationRecyclerView.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationRecyclerView.kt new file mode 100644 index 0000000000..475efaba38 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationRecyclerView.kt @@ -0,0 +1,55 @@ +package org.thoughtcrime.securesms.conversation.v2 + +import android.content.Context +import android.util.AttributeSet +import android.util.Log +import android.view.MotionEvent +import android.view.VelocityTracker +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView +import kotlinx.android.synthetic.main.activity_conversation_v2.* +import org.thoughtcrime.securesms.util.disableClipping +import org.thoughtcrime.securesms.util.toPx +import kotlin.math.abs +import kotlin.math.max + +class ConversationRecyclerView : RecyclerView { + private val maxLongPressVelocityY = toPx(10, resources) + private val minSwipeVelocityX = toPx(10, resources) + private var velocityTracker: VelocityTracker? = null + + 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() { + disableClipping() + } + + override fun onInterceptTouchEvent(e: MotionEvent): Boolean { + val velocityTracker = velocityTracker ?: return super.onInterceptTouchEvent(e) + velocityTracker.computeCurrentVelocity(1000) // Specifying 1000 gives pixels per second + val vx = velocityTracker.xVelocity + val vy = velocityTracker.yVelocity + // Only allow swipes to the left; allowing swipes to the right interferes with some back gestures + if (vx > 0) { return super.onInterceptTouchEvent(e) } + // Distinguish between scrolling gestures and long presses + if (abs(vy) > maxLongPressVelocityY && abs(vx) < minSwipeVelocityX) { return super.onInterceptTouchEvent(e) } + // Return false if abs(v.x) > abs(v.y) so that only swipes that are more horizontal than vertical + // get passed on to the message view + if (abs(vx) > abs(vy)) { + return false + } else { + return super.onInterceptTouchEvent(e) + } + } + + override fun dispatchTouchEvent(e: MotionEvent): Boolean { + when (e.action) { + MotionEvent.ACTION_DOWN -> velocityTracker = VelocityTracker.obtain() + MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> velocityTracker = null + } + velocityTracker?.addMovement(e) + return super.dispatchTouchEvent(e) + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/components/AlbumThumbnailView.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/components/AlbumThumbnailView.kt new file mode 100644 index 0000000000..7cd78cda2b --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/components/AlbumThumbnailView.kt @@ -0,0 +1,189 @@ +package org.thoughtcrime.securesms.conversation.v2.components + +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.view.ViewGroup +import android.widget.FrameLayout +import android.widget.TextView +import androidx.core.view.children +import androidx.core.view.isVisible +import kotlinx.android.synthetic.main.album_thumbnail_view.view.* +import network.loki.messenger.R +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.ViewUtil +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.database.model.MmsMessageRecord +import org.thoughtcrime.securesms.util.ActivityDispatcher +import org.thoughtcrime.securesms.longmessage.LongMessageActivity +import org.thoughtcrime.securesms.mms.GlideRequests +import org.thoughtcrime.securesms.mms.Slide +import org.thoughtcrime.securesms.video.exo.AttachmentDataSource +import kotlin.math.roundToInt + +class AlbumThumbnailView : FrameLayout { + + companion object { + const val MAX_ALBUM_DISPLAY_SIZE = 5 + } + + // 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 val cornerMask by lazy { CornerMask(this) } + private var slides: List = listOf() + private var slideSize: Int = 0 + + private fun initialize() { + LayoutInflater.from(context).inflate(R.layout.album_thumbnail_view, this) + } + + override fun dispatchDraw(canvas: Canvas?) { + super.dispatchDraw(canvas) + cornerMask.mask(canvas) + } + // endregion + + // region Interaction + + fun calculateHitObject(event: MotionEvent, mms: MmsMessageRecord, threadRecipient: Recipient) { + val rawXInt = event.rawX.toInt() + val rawYInt = event.rawY.toInt() + val eventRect = Rect(rawXInt, rawYInt, rawXInt, rawYInt) + // Z-check in specific order + val testRect = Rect() + // test "Read More" + albumCellBodyTextReadMore.getGlobalVisibleRect(testRect) + if (testRect.contains(eventRect)) { + // dispatch to activity view + ActivityDispatcher.get(context)?.dispatchIntent { context -> + LongMessageActivity.getIntent(context, mms.recipient.address, mms.getId(), true) + } + return + } + // test each album child + albumCellContainer.findViewById(R.id.album_thumbnail_root)?.children?.forEachIndexed { index, child -> + child.getGlobalVisibleRect(testRect) + if (testRect.contains(eventRect)) { + // hit intersects with this particular child + val slide = slides.getOrNull(index) ?: return + // only open to downloaded images + if (slide.transferState == AttachmentTransferProgress.TRANSFER_PROGRESS_FAILED) { + // restart download here + (slide.asAttachment() as? DatabaseAttachment)?.let { attachment -> + val attachmentId = attachment.attachmentId.rowId + JobQueue.shared.add(AttachmentDownloadJob(attachmentId, mms.getId())) + } + } + if (slide.isInProgress) return + + ActivityDispatcher.get(context)?.dispatchIntent { context -> + MediaPreviewActivity.getPreviewIntent(context, slide, mms, threadRecipient) + } + } + } + } + + fun bind(glideRequests: GlideRequests, message: MmsMessageRecord, + isStart: Boolean, isEnd: Boolean) { + slides = message.slideDeck.thumbnailSlides + if (slides.isEmpty()) { + // this should never be encountered because it's checked by parent + return + } + calculateRadius(isStart, isEnd, message.isOutgoing) + + // recreate cell views if different size to what we have already (for recycling) + if (slides.size != this.slideSize) { + albumCellContainer.removeAllViews() + LayoutInflater.from(context).inflate(layoutRes(slides.size), albumCellContainer) + val overflowed = slides.size > MAX_ALBUM_DISPLAY_SIZE + albumCellContainer.findViewById(R.id.album_cell_overflow_text)?.let { overflowText -> + // overflowText will be null if !overflowed + overflowText.isVisible = overflowed // more than max album size + overflowText.text = context.getString(R.string.AlbumThumbnailView_plus, slides.size - MAX_ALBUM_DISPLAY_SIZE) + } + this.slideSize = slides.size + } + // iterate binding + slides.take(5).forEachIndexed { position, slide -> + val thumbnailView = getThumbnailView(position) + thumbnailView.setImageResource(glideRequests, slide, isPreview = false, mms = message) + } + albumCellBodyParent.isVisible = message.body.isNotEmpty() + albumCellBodyText.text = message.body + post { + // post to await layout of text + albumCellBodyText.layout?.let { layout -> + val maxEllipsis = (0 until layout.lineCount).maxByOrNull { lineNum -> layout.getEllipsisCount(lineNum) } + ?: 0 + // show read more text if at least one line is ellipsized + ViewUtil.setPaddingTop(albumCellBodyTextParent, if (maxEllipsis > 0) resources.getDimension(R.dimen.small_spacing).roundToInt() else resources.getDimension(R.dimen.medium_spacing).roundToInt()) + albumCellBodyTextReadMore.isVisible = maxEllipsis > 0 + } + } + } + + // endregion + + + fun layoutRes(slideCount: Int) = when (slideCount) { + 1 -> R.layout.album_thumbnail_1 // single + 2 -> R.layout.album_thumbnail_2// two sidebyside + 3 -> R.layout.album_thumbnail_3// three stacked + 4 -> R.layout.album_thumbnail_4// four square + 5 -> R.layout.album_thumbnail_5// + else -> R.layout.album_thumbnail_many// five or more + } + + fun getThumbnailView(position: Int): KThumbnailView = when (position) { + 0 -> albumCellContainer.findViewById(R.id.albumCellContainer).findViewById(R.id.album_cell_1) + 1 -> albumCellContainer.findViewById(R.id.albumCellContainer).findViewById(R.id.album_cell_2) + 2 -> albumCellContainer.findViewById(R.id.albumCellContainer).findViewById(R.id.album_cell_3) + 3 -> albumCellContainer.findViewById(R.id.albumCellContainer).findViewById(R.id.album_cell_4) + 4 -> albumCellContainer.findViewById(R.id.albumCellContainer).findViewById(R.id.album_cell_5) + else -> throw Exception("Can't get thumbnail view for non-existent thumbnail at position: $position") + } + + fun calculateRadius(isStart: Boolean, isEnd: Boolean, outgoing: Boolean) { + val roundedDimen = context.resources.getDimension(R.dimen.message_corner_radius).toInt() + val collapsedDimen = context.resources.getDimension(R.dimen.message_corner_collapse_radius).toInt() + val (startTop, endTop, startBottom, endBottom) = when { + // single message, consistent dimen + isStart && isEnd -> intArrayOf(roundedDimen, roundedDimen, roundedDimen, roundedDimen) + // start of message cluster, collapsed BL + isStart -> intArrayOf(roundedDimen, roundedDimen, collapsedDimen, roundedDimen) + // end of message cluster, collapsed TL + isEnd -> intArrayOf(collapsedDimen, roundedDimen, roundedDimen, roundedDimen) + // else in the middle, no rounding left side + else -> intArrayOf(collapsedDimen, roundedDimen, collapsedDimen, roundedDimen) + } + // TL, TR, BR, BL (CW direction) + cornerMask.setRadii( + if (!outgoing) startTop else endTop, // TL + if (!outgoing) endTop else startTop, // TR + if (!outgoing) endBottom else startBottom, // BR + if (!outgoing) startBottom else endBottom // BL + ) + } + +} \ No newline at end of file diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/ExpirationTimerView.java b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/components/ExpirationTimerView.java similarity index 98% rename from app/src/main/java/org/thoughtcrime/securesms/components/ExpirationTimerView.java rename to app/src/main/java/org/thoughtcrime/securesms/conversation/v2/components/ExpirationTimerView.java index 65cad0a274..5a04e77ac2 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/ExpirationTimerView.java +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/components/ExpirationTimerView.java @@ -1,4 +1,4 @@ -package org.thoughtcrime.securesms.components; +package org.thoughtcrime.securesms.conversation.v2.components; import android.content.Context; import androidx.annotation.NonNull; @@ -118,5 +118,4 @@ public class ExpirationTimerView extends androidx.appcompat.widget.AppCompatImag Util.runOnMainDelayed(this, timerView.calculateAnimationDelay(timerView.startedAt, timerView.expiresIn)); } } - } diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/components/LinkPreviewDraftView.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/components/LinkPreviewDraftView.kt new file mode 100644 index 0000000000..b056c4f9a4 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/components/LinkPreviewDraftView.kt @@ -0,0 +1,50 @@ +package org.thoughtcrime.securesms.conversation.v2.components + +import android.content.Context +import android.util.AttributeSet +import android.view.LayoutInflater +import android.widget.LinearLayout +import androidx.core.view.isVisible +import kotlinx.android.synthetic.main.view_link_preview_draft.view.* +import network.loki.messenger.R +import org.session.libsession.messaging.sending_receiving.link_preview.LinkPreview +import org.thoughtcrime.securesms.util.toPx +import org.thoughtcrime.securesms.mms.GlideRequests +import org.thoughtcrime.securesms.mms.ImageSlide + +class LinkPreviewDraftView : LinearLayout { + var delegate: LinkPreviewDraftViewDelegate? = null + + 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() { + // Start out with the loader showing and the content view hidden + LayoutInflater.from(context).inflate(R.layout.view_link_preview_draft, this) + linkPreviewDraftContainer.isVisible = false + thumbnailImageView.clipToOutline = true + linkPreviewDraftCancelButton.setOnClickListener { cancel() } + } + + fun update(glide: GlideRequests, linkPreview: LinkPreview) { + // Hide the loader and show the content view + linkPreviewDraftContainer.isVisible = true + linkPreviewDraftLoader.isVisible = false + thumbnailImageView.radius = toPx(4, resources) + if (linkPreview.getThumbnail().isPresent) { + // This internally fetches the thumbnail + thumbnailImageView.setImageResource(glide, ImageSlide(context, linkPreview.getThumbnail().get()), false, false) + } + linkPreviewDraftTitleTextView.text = linkPreview.title + } + + private fun cancel() { + delegate?.cancelLinkPreviewDraft() + } +} + +interface LinkPreviewDraftViewDelegate { + + fun cancelLinkPreviewDraft() +} \ No newline at end of file diff --git a/app/src/main/java/org/thoughtcrime/securesms/loki/views/MentionCandidateSelectionView.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/components/MentionCandidateSelectionView.kt similarity index 97% rename from app/src/main/java/org/thoughtcrime/securesms/loki/views/MentionCandidateSelectionView.kt rename to app/src/main/java/org/thoughtcrime/securesms/conversation/v2/components/MentionCandidateSelectionView.kt index b7745a67a1..5f16b2317d 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/loki/views/MentionCandidateSelectionView.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/components/MentionCandidateSelectionView.kt @@ -1,4 +1,4 @@ -package org.thoughtcrime.securesms.loki.views +package org.thoughtcrime.securesms.conversation.v2.components import android.content.Context import android.util.AttributeSet @@ -8,7 +8,7 @@ import android.view.ViewGroup import android.widget.BaseAdapter import android.widget.ListView import org.thoughtcrime.securesms.database.DatabaseFactory -import org.thoughtcrime.securesms.loki.utilities.toPx +import org.thoughtcrime.securesms.util.toPx import org.thoughtcrime.securesms.mms.GlideRequests import org.session.libsession.messaging.mentions.Mention diff --git a/app/src/main/java/org/thoughtcrime/securesms/loki/views/MentionCandidateView.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/components/MentionCandidateView.kt similarity index 93% rename from app/src/main/java/org/thoughtcrime/securesms/loki/views/MentionCandidateView.kt rename to app/src/main/java/org/thoughtcrime/securesms/conversation/v2/components/MentionCandidateView.kt index d355597bf9..7c7d8b624b 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/loki/views/MentionCandidateView.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/components/MentionCandidateView.kt @@ -1,4 +1,4 @@ -package org.thoughtcrime.securesms.loki.views +package org.thoughtcrime.securesms.conversation.v2.components import android.content.Context import android.util.AttributeSet @@ -30,7 +30,7 @@ class MentionCandidateView(context: Context, attrs: AttributeSet?, defStyleAttr: } private fun update() { - btnGroupNameDisplay.text = mentionCandidate.displayName + mentionCandidateNameTextView.text = mentionCandidate.displayName profilePictureView.publicKey = mentionCandidate.publicKey profilePictureView.displayName = mentionCandidate.displayName profilePictureView.additionalPublicKey = null diff --git a/app/src/main/java/org/thoughtcrime/securesms/loki/views/OpenGroupGuidelinesView.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/components/OpenGroupGuidelinesView.kt similarity index 59% rename from app/src/main/java/org/thoughtcrime/securesms/loki/views/OpenGroupGuidelinesView.kt rename to app/src/main/java/org/thoughtcrime/securesms/conversation/v2/components/OpenGroupGuidelinesView.kt index d29460866c..d6cffd08db 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/loki/views/OpenGroupGuidelinesView.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/components/OpenGroupGuidelinesView.kt @@ -1,4 +1,4 @@ -package org.thoughtcrime.securesms.loki.views +package org.thoughtcrime.securesms.conversation.v2.components import android.content.Context import android.content.Intent @@ -7,30 +7,22 @@ import android.view.LayoutInflater import android.widget.FrameLayout import kotlinx.android.synthetic.main.view_open_group_guidelines.view.* import network.loki.messenger.R -import org.thoughtcrime.securesms.conversation.ConversationActivity -import org.thoughtcrime.securesms.loki.activities.OpenGroupGuidelinesActivity -import org.thoughtcrime.securesms.loki.utilities.push +import org.thoughtcrime.securesms.conversation.v2.ConversationActivityV2 +import org.thoughtcrime.securesms.groups.OpenGroupGuidelinesActivity +import org.thoughtcrime.securesms.util.push class OpenGroupGuidelinesView : FrameLayout { - constructor(context: Context) : super(context) { - setUpViewHierarchy() - } + 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, attrs: AttributeSet) : super(context, attrs) { - setUpViewHierarchy() - } - - constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr) { - setUpViewHierarchy() - } - - private fun setUpViewHierarchy() { + private fun initialize() { val inflater = context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater val contentView = inflater.inflate(R.layout.view_open_group_guidelines, null) addView(contentView) readButton.setOnClickListener { - val activity = context as ConversationActivity + val activity = context as ConversationActivityV2 val intent = Intent(activity, OpenGroupGuidelinesActivity::class.java) activity.push(intent) } diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/TypingIndicatorView.java b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/components/TypingIndicatorView.java similarity index 93% rename from app/src/main/java/org/thoughtcrime/securesms/components/TypingIndicatorView.java rename to app/src/main/java/org/thoughtcrime/securesms/conversation/v2/components/TypingIndicatorView.java index 477776a9dd..826cfe7b3a 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/TypingIndicatorView.java +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/components/TypingIndicatorView.java @@ -1,4 +1,4 @@ -package org.thoughtcrime.securesms.components; +package org.thoughtcrime.securesms.conversation.v2.components; import android.content.Context; import android.content.res.TypedArray; @@ -13,18 +13,14 @@ import android.widget.LinearLayout; import network.loki.messenger.R; public class TypingIndicatorView extends LinearLayout { + private boolean isActive; + private long startTime; - private static final long DURATION = 300; - private static final long PRE_DELAY = 500; - private static final long POST_DELAY = 500; 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 boolean isActive; - private long startTime; - private View dot1; private View dot2; private View dot3; @@ -40,7 +36,7 @@ public class TypingIndicatorView extends LinearLayout { } private void initialize(@Nullable AttributeSet attrs) { - inflate(getContext(), R.layout.typing_indicator_view, this); + inflate(getContext(), R.layout.view_typing_indicator, this); setWillNotDraw(false); diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/components/TypingIndicatorViewContainer.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/components/TypingIndicatorViewContainer.kt new file mode 100644 index 0000000000..0628b63b78 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/components/TypingIndicatorViewContainer.kt @@ -0,0 +1,25 @@ +package org.thoughtcrime.securesms.conversation.v2.components + +import android.content.Context +import android.util.AttributeSet +import android.view.LayoutInflater +import android.widget.LinearLayout +import kotlinx.android.synthetic.main.view_conversation_typing_container.view.* +import network.loki.messenger.R +import org.session.libsession.utilities.recipients.Recipient + +class TypingIndicatorViewContainer : LinearLayout { + + 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() { + LayoutInflater.from(context).inflate(R.layout.view_conversation_typing_container, this) + } + + fun setTypists(typists: List) { + if (typists.isEmpty()) { typingIndicator.stopAnimation(); return } + typingIndicator.startAnimation() + } +} \ No newline at end of file diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/dialogs/BlockedDialog.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/dialogs/BlockedDialog.kt new file mode 100644 index 0000000000..3013ab8901 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/dialogs/BlockedDialog.kt @@ -0,0 +1,42 @@ +package org.thoughtcrime.securesms.conversation.v2.dialogs + +import android.graphics.Typeface +import android.text.Spannable +import android.text.SpannableStringBuilder +import android.text.style.StyleSpan +import android.view.LayoutInflater +import androidx.appcompat.app.AlertDialog +import kotlinx.android.synthetic.main.dialog_blocked.view.* +import kotlinx.android.synthetic.main.dialog_blocked.view.cancelButton +import network.loki.messenger.R +import org.session.libsession.messaging.contacts.Contact +import org.session.libsession.utilities.recipients.Recipient +import org.thoughtcrime.securesms.conversation.v2.utilities.BaseDialog +import org.thoughtcrime.securesms.database.DatabaseFactory + +/** Shown upon sending a message to a user that's blocked. */ +class BlockedDialog(private val recipient: Recipient) : BaseDialog() { + + override fun setContentView(builder: AlertDialog.Builder) { + val contentView = LayoutInflater.from(requireContext()).inflate(R.layout.dialog_blocked, null) + val contactDB = DatabaseFactory.getSessionContactDatabase(requireContext()) + val sessionID = recipient.address.toString() + val contact = contactDB.getContactWithSessionID(sessionID) + val name = contact?.displayName(Contact.ContactContext.REGULAR) ?: sessionID + val title = resources.getString(R.string.dialog_blocked_title, name) + contentView.blockedTitleTextView.text = title + val explanation = resources.getString(R.string.dialog_blocked_explanation, name) + val spannable = SpannableStringBuilder(explanation) + val startIndex = explanation.indexOf(name) + spannable.setSpan(StyleSpan(Typeface.BOLD), startIndex, startIndex + name.count(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) + contentView.blockedExplanationTextView.text = spannable + contentView.cancelButton.setOnClickListener { dismiss() } + contentView.unblockButton.setOnClickListener { unblock() } + builder.setView(contentView) + } + + private fun unblock() { + DatabaseFactory.getRecipientDatabase(requireContext()).setBlocked(recipient, false) + dismiss() + } +} \ No newline at end of file diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/dialogs/DownloadDialog.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/dialogs/DownloadDialog.kt new file mode 100644 index 0000000000..db95e49ddd --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/dialogs/DownloadDialog.kt @@ -0,0 +1,49 @@ +package org.thoughtcrime.securesms.conversation.v2.dialogs + +import android.graphics.Typeface +import android.text.Spannable +import android.text.SpannableStringBuilder +import android.text.style.StyleSpan +import android.view.LayoutInflater +import androidx.appcompat.app.AlertDialog +import kotlinx.android.synthetic.main.dialog_download.view.* +import network.loki.messenger.R +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.utilities.recipients.Recipient +import org.thoughtcrime.securesms.conversation.v2.utilities.BaseDialog +import org.thoughtcrime.securesms.database.DatabaseFactory + +/** Shown when receiving media from a contact for the first time, to confirm that + * they are to be trusted and files sent by them are to be downloaded. */ +class DownloadDialog(private val recipient: Recipient) : BaseDialog() { + + override fun setContentView(builder: AlertDialog.Builder) { + val contentView = LayoutInflater.from(requireContext()).inflate(R.layout.dialog_download, null) + val contactDB = DatabaseFactory.getSessionContactDatabase(requireContext()) + val sessionID = recipient.address.toString() + val contact = contactDB.getContactWithSessionID(sessionID) + val name = contact?.displayName(Contact.ContactContext.REGULAR) ?: sessionID + val title = resources.getString(R.string.dialog_download_title, name) + contentView.downloadTitleTextView.text = title + val explanation = resources.getString(R.string.dialog_download_explanation, name) + val spannable = SpannableStringBuilder(explanation) + val startIndex = explanation.indexOf(name) + spannable.setSpan(StyleSpan(Typeface.BOLD), startIndex, startIndex + name.count(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) + contentView.downloadExplanationTextView.text = spannable + contentView.cancelButton.setOnClickListener { dismiss() } + contentView.downloadButton.setOnClickListener { trust() } + builder.setView(contentView) + } + + private fun trust() { + val contactDB = DatabaseFactory.getSessionContactDatabase(requireContext()) + val sessionID = recipient.address.toString() + val contact = contactDB.getContactWithSessionID(sessionID) ?: return + val threadID = DatabaseFactory.getThreadDatabase(requireContext()).getThreadIdIfExistsFor(recipient) + contactDB.setContactIsTrusted(contact, true, threadID) + JobQueue.shared.resumePendingJobs(AttachmentDownloadJob.KEY) + dismiss() + } +} \ No newline at end of file diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/dialogs/JoinOpenGroupDialog.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/dialogs/JoinOpenGroupDialog.kt new file mode 100644 index 0000000000..0d4c30508f --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/dialogs/JoinOpenGroupDialog.kt @@ -0,0 +1,44 @@ +package org.thoughtcrime.securesms.conversation.v2.dialogs + +import android.graphics.Typeface +import android.text.Spannable +import android.text.SpannableStringBuilder +import android.text.style.StyleSpan +import android.view.LayoutInflater +import androidx.appcompat.app.AlertDialog +import androidx.appcompat.app.AppCompatActivity +import kotlinx.android.synthetic.main.dialog_join_open_group.view.* +import network.loki.messenger.R +import org.session.libsession.utilities.OpenGroupUrlParser +import org.session.libsignal.utilities.ThreadUtils +import org.thoughtcrime.securesms.conversation.v2.utilities.BaseDialog +import org.thoughtcrime.securesms.groups.OpenGroupManager +import org.thoughtcrime.securesms.util.ConfigurationMessageUtilities + +/** Shown upon tapping an open group invitation. */ +class JoinOpenGroupDialog(private val name: String, private val url: String) : BaseDialog() { + + override fun setContentView(builder: AlertDialog.Builder) { + val contentView = LayoutInflater.from(requireContext()).inflate(R.layout.dialog_join_open_group, null) + val title = resources.getString(R.string.dialog_join_open_group_title, name) + contentView.joinOpenGroupTitleTextView.text = title + val explanation = resources.getString(R.string.dialog_join_open_group_explanation, name) + val spannable = SpannableStringBuilder(explanation) + val startIndex = explanation.indexOf(name) + spannable.setSpan(StyleSpan(Typeface.BOLD), startIndex, startIndex + name.count(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) + contentView.joinOpenGroupExplanationTextView.text = spannable + contentView.cancelButton.setOnClickListener { dismiss() } + contentView.joinButton.setOnClickListener { join() } + builder.setView(contentView) + } + + private fun join() { + val openGroup = OpenGroupUrlParser.parseUrl(url) + val activity = requireContext() as AppCompatActivity + ThreadUtils.queue { + OpenGroupManager.add(openGroup.server, openGroup.room, openGroup.serverPublicKey, activity) + ConfigurationMessageUtilities.forceSyncConfigurationNowIfNeeded(activity) + } + dismiss() + } +} \ No newline at end of file diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/dialogs/LinkPreviewDialog.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/dialogs/LinkPreviewDialog.kt new file mode 100644 index 0000000000..f9fa6c381e --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/dialogs/LinkPreviewDialog.kt @@ -0,0 +1,26 @@ +package org.thoughtcrime.securesms.conversation.v2.dialogs + +import android.view.LayoutInflater +import androidx.appcompat.app.AlertDialog +import kotlinx.android.synthetic.main.dialog_link_preview.view.* +import network.loki.messenger.R +import org.session.libsession.utilities.TextSecurePreferences +import org.thoughtcrime.securesms.conversation.v2.utilities.BaseDialog + +/** Shown the first time the user inputs a URL that could generate a link preview, to + * let them know that Session offers the ability to send and receive link previews. */ +class LinkPreviewDialog(private val onEnabled: () -> Unit) : BaseDialog() { + + override fun setContentView(builder: AlertDialog.Builder) { + val contentView = LayoutInflater.from(requireContext()).inflate(R.layout.dialog_link_preview, null) + contentView.cancelButton.setOnClickListener { dismiss() } + contentView.enableLinkPreviewsButton.setOnClickListener { enable() } + builder.setView(contentView) + } + + private fun enable() { + TextSecurePreferences.setLinkPreviewsEnabled(requireContext(), true) + dismiss() + onEnabled() + } +} \ No newline at end of file diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/dialogs/OpenURLDialog.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/dialogs/OpenURLDialog.kt new file mode 100644 index 0000000000..ea0230f578 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/dialogs/OpenURLDialog.kt @@ -0,0 +1,40 @@ +package org.thoughtcrime.securesms.conversation.v2.dialogs + +import android.content.Intent +import android.graphics.Typeface +import android.net.Uri +import android.text.Spannable +import android.text.SpannableStringBuilder +import android.text.style.StyleSpan +import android.view.LayoutInflater +import android.widget.Toast +import androidx.appcompat.app.AlertDialog +import kotlinx.android.synthetic.main.dialog_open_url.view.* +import network.loki.messenger.R +import org.thoughtcrime.securesms.conversation.v2.utilities.BaseDialog + +/** Shown upon tapping a URL. */ +class OpenURLDialog(private val url: String) : BaseDialog() { + + override fun setContentView(builder: AlertDialog.Builder) { + val contentView = LayoutInflater.from(requireContext()).inflate(R.layout.dialog_open_url, null) + val explanation = resources.getString(R.string.dialog_open_url_explanation, url) + val spannable = SpannableStringBuilder(explanation) + val startIndex = explanation.indexOf(url) + spannable.setSpan(StyleSpan(Typeface.BOLD), startIndex, startIndex + url.count(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) + contentView.openURLExplanationTextView.text = spannable + contentView.cancelButton.setOnClickListener { dismiss() } + contentView.openURLButton.setOnClickListener { open() } + builder.setView(contentView) + } + + private fun open() { + try { + val intent = Intent(Intent.ACTION_VIEW, Uri.parse(url)) + requireContext().startActivity(intent) + } catch (e: Exception) { + Toast.makeText(context, R.string.invalid_url, Toast.LENGTH_SHORT).show() + } + dismiss() + } +} \ No newline at end of file diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/input_bar/InputBar.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/input_bar/InputBar.kt new file mode 100644 index 0000000000..53ebf036ff --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/input_bar/InputBar.kt @@ -0,0 +1,196 @@ +package org.thoughtcrime.securesms.conversation.v2.input_bar + +import android.content.Context +import android.content.res.Resources +import android.text.InputType +import android.util.AttributeSet +import android.view.LayoutInflater +import android.view.MotionEvent +import android.widget.RelativeLayout +import androidx.core.view.isVisible +import kotlinx.android.synthetic.main.view_input_bar.view.* +import network.loki.messenger.R +import org.session.libsession.messaging.sending_receiving.link_preview.LinkPreview +import org.session.libsession.utilities.TextSecurePreferences +import org.session.libsession.utilities.recipients.Recipient +import org.thoughtcrime.securesms.conversation.v2.components.LinkPreviewDraftView +import org.thoughtcrime.securesms.conversation.v2.components.LinkPreviewDraftViewDelegate +import org.thoughtcrime.securesms.conversation.v2.messages.QuoteView +import org.thoughtcrime.securesms.conversation.v2.messages.QuoteViewDelegate +import org.thoughtcrime.securesms.database.model.MessageRecord +import org.thoughtcrime.securesms.database.model.MmsMessageRecord +import org.thoughtcrime.securesms.util.toDp +import org.thoughtcrime.securesms.util.toPx +import org.thoughtcrime.securesms.mms.GlideRequests +import kotlin.math.max +import kotlin.math.roundToInt + +class InputBar : RelativeLayout, InputBarEditTextDelegate, QuoteViewDelegate, LinkPreviewDraftViewDelegate { + private val screenWidth = Resources.getSystem().displayMetrics.widthPixels + private val vMargin by lazy { toDp(4, resources) } + private val minHeight by lazy { toPx(56, resources) } + private var linkPreviewDraftView: LinkPreviewDraftView? = null + var delegate: InputBarDelegate? = null + var additionalContentHeight = 0 + var quote: MessageRecord? = null + var linkPreview: LinkPreview? = null + var showInput: Boolean = true + set(value) { field = value; showOrHideInputIfNeeded() } + + var text: String + get() { return inputBarEditText.text?.toString() ?: "" } + set(value) { inputBarEditText.setText(value) } + + private val attachmentsButton by lazy { InputBarButton(context, R.drawable.ic_plus_24) } + private val microphoneButton by lazy { InputBarButton(context, R.drawable.ic_microphone) } + private val sendButton by lazy { InputBarButton(context, R.drawable.ic_arrow_up, true) } + + // 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() { + LayoutInflater.from(context).inflate(R.layout.view_input_bar, this) + // Attachments button + attachmentsButtonContainer.addView(attachmentsButton) + attachmentsButton.layoutParams = RelativeLayout.LayoutParams(RelativeLayout.LayoutParams.MATCH_PARENT, RelativeLayout.LayoutParams.MATCH_PARENT) + attachmentsButton.onPress = { toggleAttachmentOptions() } + // Microphone button + microphoneOrSendButtonContainer.addView(microphoneButton) + microphoneButton.layoutParams = RelativeLayout.LayoutParams(RelativeLayout.LayoutParams.MATCH_PARENT, RelativeLayout.LayoutParams.MATCH_PARENT) + microphoneButton.onLongPress = { startRecordingVoiceMessage() } + microphoneButton.onMove = { delegate?.onMicrophoneButtonMove(it) } + microphoneButton.onCancel = { delegate?.onMicrophoneButtonCancel(it) } + microphoneButton.onUp = { delegate?.onMicrophoneButtonUp(it) } + // Send button + microphoneOrSendButtonContainer.addView(sendButton) + sendButton.layoutParams = RelativeLayout.LayoutParams(RelativeLayout.LayoutParams.MATCH_PARENT, RelativeLayout.LayoutParams.MATCH_PARENT) + sendButton.isVisible = false + sendButton.onUp = { delegate?.sendMessage() } + // Edit text + inputBarEditText.imeOptions = inputBarEditText.imeOptions or 16777216 // Always use incognito keyboard + inputBarEditText.inputType = inputBarEditText.inputType or InputType.TYPE_TEXT_FLAG_CAP_SENTENCES + inputBarEditText.delegate = this + } + // endregion + + // region General + private fun setHeight(newHeight: Int) { + val layoutParams = inputBarLinearLayout.layoutParams as LayoutParams + layoutParams.height = newHeight + inputBarLinearLayout.layoutParams = layoutParams + delegate?.inputBarHeightChanged(newHeight) + } + // endregion + + // region Updating + override fun inputBarEditTextContentChanged(text: CharSequence) { + sendButton.isVisible = text.isNotEmpty() + microphoneButton.isVisible = text.isEmpty() + delegate?.inputBarEditTextContentChanged(text) + } + + override fun inputBarEditTextHeightChanged(newValue: Int) { + val newHeight = max(newValue + 2 * vMargin, minHeight) + inputBarAdditionalContentContainer.height + setHeight(newHeight) + } + + private fun toggleAttachmentOptions() { + delegate?.toggleAttachmentOptions() + } + + private fun startRecordingVoiceMessage() { + delegate?.startRecordingVoiceMessage() + } + + // Drafting quotes and drafting link previews is mutually exclusive, i.e. you can't draft + // a quote and a link preview at the same time. + + fun draftQuote(thread: Recipient, message: MessageRecord, glide: GlideRequests) { + quote = message + linkPreview = null + linkPreviewDraftView = null + inputBarAdditionalContentContainer.removeAllViews() + val quoteView = QuoteView(context, QuoteView.Mode.Draft) + quoteView.delegate = this + inputBarAdditionalContentContainer.addView(quoteView) + val attachments = (message as? MmsMessageRecord)?.slideDeck + // The max content width is the screen width - 2 times the horizontal input bar padding - the + // quote view content area's start and end margins. This unfortunately has to be calculated manually + // here to get the layout right. + val maxContentWidth = (screenWidth - 2 * resources.getDimension(R.dimen.medium_spacing) - toPx(16, resources) - toPx(30, resources)).roundToInt() + val sender = if (message.isOutgoing) TextSecurePreferences.getLocalNumber(context)!! else message.individualRecipient.address.serialize() + quoteView.bind(sender, message.body, attachments, + thread, true, maxContentWidth, message.isOpenGroupInvitation, message.threadId, false, glide) + // The 6 DP below is the padding the quote view applies to itself, which isn't included in the + // intrinsic height calculation. + val quoteViewIntrinsicHeight = quoteView.getIntrinsicHeight(maxContentWidth) + toPx(6, resources) + val newHeight = max(inputBarEditText.height + 2 * vMargin, minHeight) + quoteViewIntrinsicHeight + additionalContentHeight = quoteViewIntrinsicHeight + setHeight(newHeight) + } + + override fun cancelQuoteDraft() { + quote = null + inputBarAdditionalContentContainer.removeAllViews() + val newHeight = max(inputBarEditText.height + 2 * vMargin, minHeight) + additionalContentHeight = 0 + setHeight(newHeight) + } + + fun draftLinkPreview() { + quote = null + val linkPreviewDraftHeight = toPx(88, resources) + inputBarAdditionalContentContainer.removeAllViews() + val linkPreviewDraftView = LinkPreviewDraftView(context) + linkPreviewDraftView.delegate = this + this.linkPreviewDraftView = linkPreviewDraftView + inputBarAdditionalContentContainer.addView(linkPreviewDraftView) + val newHeight = max(inputBarEditText.height + 2 * vMargin, minHeight) + linkPreviewDraftHeight + additionalContentHeight = linkPreviewDraftHeight + setHeight(newHeight) + } + + fun updateLinkPreviewDraft(glide: GlideRequests, linkPreview: LinkPreview) { + this.linkPreview = linkPreview + val linkPreviewDraftView = this.linkPreviewDraftView ?: return + linkPreviewDraftView.update(glide, linkPreview) + } + + override fun cancelLinkPreviewDraft() { + if (quote != null) { return } + linkPreview = null + inputBarAdditionalContentContainer.removeAllViews() + val newHeight = max(inputBarEditText.height + 2 * vMargin, minHeight) + additionalContentHeight = 0 + setHeight(newHeight) + } + + private fun showOrHideInputIfNeeded() { + if (showInput) { + setOf( inputBarEditText, attachmentsButton ).forEach { it.isVisible = true } + microphoneButton.isVisible = text.isEmpty() + sendButton.isVisible = text.isNotEmpty() + } else { + cancelQuoteDraft() + cancelLinkPreviewDraft() + val views = setOf( inputBarEditText, attachmentsButton, microphoneButton, sendButton ) + views.forEach { it.isVisible = false } + } + } + // endregion +} + +interface InputBarDelegate { + + fun inputBarHeightChanged(newValue: Int) + fun inputBarEditTextContentChanged(newContent: CharSequence) + fun toggleAttachmentOptions() + fun showVoiceMessageUI() + fun startRecordingVoiceMessage() + fun onMicrophoneButtonMove(event: MotionEvent) + fun onMicrophoneButtonCancel(event: MotionEvent) + fun onMicrophoneButtonUp(event: MotionEvent) + fun sendMessage() +} \ No newline at end of file diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/input_bar/InputBarButton.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/input_bar/InputBarButton.kt new file mode 100644 index 0000000000..25e209424e --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/input_bar/InputBarButton.kt @@ -0,0 +1,174 @@ +package org.thoughtcrime.securesms.conversation.v2.input_bar + +import android.animation.PointFEvaluator +import android.animation.ValueAnimator +import android.content.Context +import android.content.res.ColorStateList +import android.graphics.PointF +import android.os.Build +import android.os.Handler +import android.os.Looper +import android.util.AttributeSet +import android.util.Log +import android.view.Gravity +import android.view.HapticFeedbackConstants +import android.view.MotionEvent +import android.widget.ImageView +import android.widget.RelativeLayout +import androidx.annotation.DrawableRes +import network.loki.messenger.R +import org.thoughtcrime.securesms.util.* +import org.thoughtcrime.securesms.util.GlowViewUtilities +import org.thoughtcrime.securesms.util.InputBarButtonImageViewContainer +import java.util.* +import kotlin.math.abs + +class InputBarButton : RelativeLayout { + private val gestureHandler = Handler(Looper.getMainLooper()) + private var isSendButton = false + private var hasOpaqueBackground = false + private var isGIFButton = false + @DrawableRes private var iconID = 0 + private var longPressCallback: Runnable? = null + private var onDownTimestamp = 0L + var snIsEnabled = true + var onPress: (() -> Unit)? = null + var onMove: ((MotionEvent) -> Unit)? = null + var onCancel: ((MotionEvent) -> Unit)? = null + var onUp: ((MotionEvent) -> Unit)? = null + var onLongPress: (() -> Unit)? = null + + companion object { + const val animationDuration = 250.toLong() + const val longPressDurationThreshold = 250L // ms + } + + private val expandedImageViewPosition by lazy { PointF(0.0f, 0.0f) } + private val collapsedImageViewPosition by lazy { PointF((expandedSize - collapsedSize) / 2, (expandedSize - collapsedSize) / 2) } + private val colorID by lazy { + if (hasOpaqueBackground) { + R.color.input_bar_button_background_opaque + } else if (isSendButton) { + R.color.accent + } else { + R.color.input_bar_button_background + } + } + + val expandedSize by lazy { resources.getDimension(R.dimen.input_bar_button_expanded_size) } + val collapsedSize by lazy { resources.getDimension(R.dimen.input_bar_button_collapsed_size) } + + private val imageViewContainer by lazy { + val result = InputBarButtonImageViewContainer(context) + val size = collapsedSize.toInt() + result.layoutParams = LayoutParams(size, size) + result.setBackgroundResource(R.drawable.input_bar_button_background) + result.mainColor = resources.getColorWithID(colorID, context.theme) + if (hasOpaqueBackground) { + result.strokeColor = resources.getColorWithID(R.color.input_bar_button_background_opaque_border, context.theme) + } + result + } + + private val imageView by lazy { + val result = ImageView(context) + val size = if (isGIFButton) toPx(24, resources) else toPx(16, resources) + result.layoutParams = LayoutParams(size, size) + result.scaleType = ImageView.ScaleType.CENTER_INSIDE + result.setImageResource(iconID) + val colorID = if (isSendButton) R.color.black else R.color.text + result.imageTintList = ColorStateList.valueOf(resources.getColorWithID(colorID, context.theme)) + result + } + + constructor(context: Context) : super(context) { throw IllegalAccessException("Use InputBarButton(context:iconID:) instead.") } + constructor(context: Context, attrs: AttributeSet) : super(context, attrs) { throw IllegalAccessException("Use InputBarButton(context:iconID:) instead.") } + constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr) { throw IllegalAccessException("Use InputBarButton(context:iconID:) instead.") } + + constructor(context: Context, @DrawableRes iconID: Int, isSendButton: Boolean = false, + hasOpaqueBackground: Boolean = false, isGIFButton: Boolean = false) : super(context) { + this.isSendButton = isSendButton + this.iconID = iconID + this.hasOpaqueBackground = hasOpaqueBackground + this.isGIFButton = isGIFButton + val size = resources.getDimension(R.dimen.input_bar_button_expanded_size).toInt() + val layoutParams = LayoutParams(size, size) + this.layoutParams = layoutParams + addView(imageViewContainer) + imageViewContainer.x = collapsedImageViewPosition.x + imageViewContainer.y = collapsedImageViewPosition.y + imageViewContainer.addView(imageView) + val imageViewLayoutParams = imageView.layoutParams as LayoutParams + imageViewLayoutParams.addRule(RelativeLayout.CENTER_IN_PARENT) + imageView.layoutParams = imageViewLayoutParams + gravity = Gravity.TOP or Gravity.LEFT // Intentionally not Gravity.START + isHapticFeedbackEnabled = true + } + + fun expand() { + GlowViewUtilities.animateColorChange(context, imageViewContainer, colorID, R.color.accent) + imageViewContainer.animateSizeChange(R.dimen.input_bar_button_collapsed_size, R.dimen.input_bar_button_expanded_size, animationDuration) + animateImageViewContainerPositionChange(collapsedImageViewPosition, expandedImageViewPosition) + } + + fun collapse() { + GlowViewUtilities.animateColorChange(context, imageViewContainer, R.color.accent, colorID) + imageViewContainer.animateSizeChange(R.dimen.input_bar_button_expanded_size, R.dimen.input_bar_button_collapsed_size, animationDuration) + animateImageViewContainerPositionChange(expandedImageViewPosition, collapsedImageViewPosition) + } + + private fun animateImageViewContainerPositionChange(startPosition: PointF, endPosition: PointF) { + val animation = ValueAnimator.ofObject(PointFEvaluator(), startPosition, endPosition) + animation.duration = animationDuration + animation.addUpdateListener { animator -> + val point = animator.animatedValue as PointF + imageViewContainer.x = point.x + imageViewContainer.y = point.y + } + animation.start() + } + + override fun onTouchEvent(event: MotionEvent): Boolean { + if (!snIsEnabled) { return false } + when (event.action) { + MotionEvent.ACTION_DOWN -> onDown(event) + MotionEvent.ACTION_MOVE -> onMove(event) + MotionEvent.ACTION_UP -> onUp(event) + MotionEvent.ACTION_CANCEL -> onCancel(event) + } + return true + } + + private fun onDown(event: MotionEvent) { + expand() + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + performHapticFeedback(HapticFeedbackConstants.CONTEXT_CLICK) + } else { + performHapticFeedback(HapticFeedbackConstants.LONG_PRESS) + } + longPressCallback?.let { gestureHandler.removeCallbacks(it) } + val newLongPressCallback = Runnable { onLongPress?.invoke() } + this.longPressCallback = newLongPressCallback + gestureHandler.postDelayed(newLongPressCallback, InputBarButton.longPressDurationThreshold) + onDownTimestamp = Date().time + } + + private fun onMove(event: MotionEvent) { + onMove?.invoke(event) + } + + private fun onCancel(event: MotionEvent) { + onCancel?.invoke(event) + collapse() + longPressCallback?.let { gestureHandler.removeCallbacks(it) } + } + + private fun onUp(event: MotionEvent) { + onUp?.invoke(event) + collapse() + if ((Date().time - onDownTimestamp) < InputBarButton.longPressDurationThreshold) { + longPressCallback?.let { gestureHandler.removeCallbacks(it) } + onPress?.invoke() + } + } +} \ No newline at end of file diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/input_bar/InputBarEditText.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/input_bar/InputBarEditText.kt new file mode 100644 index 0000000000..39db2f6d25 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/input_bar/InputBarEditText.kt @@ -0,0 +1,50 @@ +package org.thoughtcrime.securesms.conversation.v2.input_bar + +import android.content.Context +import android.content.res.Resources +import android.text.Layout +import android.text.StaticLayout +import android.util.AttributeSet +import android.util.Log +import android.widget.RelativeLayout +import androidx.appcompat.widget.AppCompatEditText +import org.thoughtcrime.securesms.conversation.v2.utilities.TextUtilities +import org.thoughtcrime.securesms.util.toPx +import kotlin.math.max +import kotlin.math.min +import kotlin.math.roundToInt + +class InputBarEditText : AppCompatEditText { + private val screenWidth get() = Resources.getSystem().displayMetrics.widthPixels + var delegate: InputBarEditTextDelegate? = null + + private val snMinHeight = toPx(40.0f, resources) + private val snMaxHeight = toPx(80.0f, resources) + + constructor(context: Context) : super(context) + constructor(context: Context, attrs: AttributeSet) : super(context, attrs) + constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr) + + override fun onTextChanged(text: CharSequence, start: Int, lengthBefore: Int, lengthAfter: Int) { + super.onTextChanged(text, start, lengthBefore, lengthAfter) + delegate?.inputBarEditTextContentChanged(text) + // Calculate the width manually to get it right even before layout has happened (i.e. + // when restoring a draft). The 64 DP is the horizontal margin around the input bar + // edit text. + val width = (screenWidth - 2 * toPx(64.0f, resources)).roundToInt() + if (width < 0) { return } // screenWidth initially evaluates to 0 + val height = TextUtilities.getIntrinsicHeight(text, paint, width).toFloat() + val constrainedHeight = min(max(height, snMinHeight), snMaxHeight) + if (constrainedHeight.roundToInt() == this.height) { return } + val layoutParams = this.layoutParams as? RelativeLayout.LayoutParams ?: return + layoutParams.height = constrainedHeight.roundToInt() + this.layoutParams = layoutParams + delegate?.inputBarEditTextHeightChanged(constrainedHeight.roundToInt()) + } +} + +interface InputBarEditTextDelegate { + + fun inputBarEditTextContentChanged(text: CharSequence) + fun inputBarEditTextHeightChanged(newValue: Int) +} \ No newline at end of file diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/input_bar/InputBarRecordingView.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/input_bar/InputBarRecordingView.kt new file mode 100644 index 0000000000..cc8166212b --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/input_bar/InputBarRecordingView.kt @@ -0,0 +1,147 @@ +package org.thoughtcrime.securesms.conversation.v2.input_bar + +import android.animation.FloatEvaluator +import android.animation.IntEvaluator +import android.animation.ValueAnimator +import android.content.Context +import android.os.Handler +import android.os.Looper +import android.util.AttributeSet +import android.view.LayoutInflater +import android.widget.RelativeLayout +import androidx.core.content.res.ResourcesCompat +import androidx.core.view.isVisible +import kotlinx.android.synthetic.main.view_input_bar_recording.view.* +import network.loki.messenger.R +import org.thoughtcrime.securesms.util.animateSizeChange +import org.thoughtcrime.securesms.util.disableClipping +import org.thoughtcrime.securesms.util.toPx +import org.thoughtcrime.securesms.util.DateUtils +import java.util.* + +class InputBarRecordingView : RelativeLayout { + private var startTimestamp = 0L + private val snHandler = Handler(Looper.getMainLooper()) + private var dotViewAnimation: ValueAnimator? = null + private var pulseAnimation: ValueAnimator? = null + var delegate: InputBarRecordingViewDelegate? = null + + 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() { + LayoutInflater.from(context).inflate(R.layout.view_input_bar_recording, this) + inputBarMiddleContentContainer.disableClipping() + inputBarCancelButton.setOnClickListener { hide() } + } + + fun show() { + startTimestamp = Date().time + recordButtonOverlayImageView.setImageDrawable(ResourcesCompat.getDrawable(resources, R.drawable.ic_microphone, context.theme)) + inputBarCancelButton.alpha = 0.0f + inputBarMiddleContentContainer.alpha = 1.0f + lockView.alpha = 1.0f + isVisible = true + alpha = 0.0f + val animation = ValueAnimator.ofObject(FloatEvaluator(), 0.0f, 1.0f) + animation.duration = 250L + animation.addUpdateListener { animator -> + alpha = animator.animatedValue as Float + } + animation.start() + animateDotView() + pulse() + animateLockViewUp() + updateTimer() + } + + fun hide() { + alpha = 1.0f + val animation = ValueAnimator.ofObject(FloatEvaluator(), 1.0f, 0.0f) + animation.duration = 250L + animation.addUpdateListener { animator -> + alpha = animator.animatedValue as Float + if (animator.animatedFraction == 1.0f) { + isVisible = false + dotViewAnimation?.repeatCount = 0 + pulseAnimation?.removeAllUpdateListeners() + } + } + animation.start() + delegate?.handleVoiceMessageUIHidden() + } + + private fun animateDotView() { + val animation = ValueAnimator.ofObject(FloatEvaluator(), 1.0f, 0.0f) + dotViewAnimation = animation + animation.duration = 500L + animation.addUpdateListener { animator -> + dotView.alpha = animator.animatedValue as Float + } + animation.repeatCount = ValueAnimator.INFINITE + animation.repeatMode = ValueAnimator.REVERSE + animation.start() + } + + private fun pulse() { + val collapsedSize = toPx(80.0f, resources) + val expandedSize = toPx(104.0f, resources) + pulseView.animateSizeChange(collapsedSize, expandedSize, 1000) + val animation = ValueAnimator.ofObject(FloatEvaluator(), 0.5, 0.0f) + pulseAnimation = animation + animation.duration = 1000L + animation.addUpdateListener { animator -> + pulseView.alpha = animator.animatedValue as Float + if (animator.animatedFraction == 1.0f && isVisible) { pulse() } + } + animation.start() + } + + private fun animateLockViewUp() { + val startMarginBottom = toPx(32, resources) + val endMarginBottom = toPx(72, resources) + val layoutParams = lockView.layoutParams as LayoutParams + layoutParams.bottomMargin = startMarginBottom + lockView.layoutParams = layoutParams + val animation = ValueAnimator.ofObject(IntEvaluator(), startMarginBottom, endMarginBottom) + animation.duration = 250L + animation.addUpdateListener { animator -> + layoutParams.bottomMargin = animator.animatedValue as Int + lockView.layoutParams = layoutParams + } + animation.start() + } + + private fun updateTimer() { + val duration = (Date().time - startTimestamp) / 1000L + recordingViewDurationTextView.text = DateUtils.formatElapsedTime(duration) + snHandler.postDelayed({ updateTimer() }, 500) + } + + fun lock() { + val fadeOutAnimation = ValueAnimator.ofObject(FloatEvaluator(), 1.0f, 0.0f) + fadeOutAnimation.duration = 250L + fadeOutAnimation.addUpdateListener { animator -> + inputBarMiddleContentContainer.alpha = animator.animatedValue as Float + lockView.alpha = animator.animatedValue as Float + } + fadeOutAnimation.start() + val fadeInAnimation = ValueAnimator.ofObject(FloatEvaluator(), 0.0f, 1.0f) + fadeInAnimation.duration = 250L + fadeInAnimation.addUpdateListener { animator -> + inputBarCancelButton.alpha = animator.animatedValue as Float + } + fadeInAnimation.start() + recordButtonOverlayImageView.setImageDrawable(ResourcesCompat.getDrawable(resources, R.drawable.ic_arrow_up, context.theme)) + recordButtonOverlay.setOnClickListener { delegate?.sendVoiceMessage() } + inputBarCancelButton.setOnClickListener { delegate?.cancelVoiceMessage() } + } +} + +interface InputBarRecordingViewDelegate { + + fun handleVoiceMessageUIHidden() + fun sendVoiceMessage() + fun cancelVoiceMessage() +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/input_bar/mentions/MentionCandidateView.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/input_bar/mentions/MentionCandidateView.kt new file mode 100644 index 0000000000..2159a5dac8 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/input_bar/mentions/MentionCandidateView.kt @@ -0,0 +1,47 @@ +package org.thoughtcrime.securesms.conversation.v2.input_bar.mentions + +import android.content.Context +import android.util.AttributeSet +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.LinearLayout +import android.widget.RelativeLayout +import kotlinx.android.synthetic.main.view_mention_candidate.view.* +import network.loki.messenger.R +import org.session.libsession.messaging.mentions.Mention +import org.session.libsession.messaging.open_groups.OpenGroupAPIV2 +import org.thoughtcrime.securesms.mms.GlideRequests + +class MentionCandidateView(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : RelativeLayout(context, attrs, defStyleAttr) { + var candidate = Mention("", "") + set(newValue) { field = newValue; update() } + var glide: GlideRequests? = null + var openGroupServer: String? = null + var openGroupRoom: String? = null + + constructor(context: Context, attrs: AttributeSet?) : this(context, attrs, 0) + constructor(context: Context) : this(context, null) + + companion object { + + fun inflate(layoutInflater: LayoutInflater, parent: ViewGroup): MentionCandidateView { + return layoutInflater.inflate(R.layout.view_mention_candidate_v2, parent, false) as MentionCandidateView + } + } + + private fun update() { + mentionCandidateNameTextView.text = candidate.displayName + profilePictureView.publicKey = candidate.publicKey + profilePictureView.displayName = candidate.displayName + profilePictureView.additionalPublicKey = null + profilePictureView.glide = glide!! + profilePictureView.update() + if (openGroupServer != null && openGroupRoom != null) { + val isUserModerator = OpenGroupAPIV2.isUserModerator(candidate.publicKey, openGroupRoom!!, openGroupServer!!) + moderatorIconImageView.visibility = if (isUserModerator) View.VISIBLE else View.GONE + } else { + moderatorIconImageView.visibility = View.GONE + } + } +} \ No newline at end of file diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/input_bar/mentions/MentionCandidatesView.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/input_bar/mentions/MentionCandidatesView.kt new file mode 100644 index 0000000000..4fcfba74b2 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/input_bar/mentions/MentionCandidatesView.kt @@ -0,0 +1,83 @@ +package org.thoughtcrime.securesms.conversation.v2.input_bar.mentions + +import android.content.Context +import android.util.AttributeSet +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.BaseAdapter +import android.widget.ListView +import org.thoughtcrime.securesms.database.DatabaseFactory +import org.thoughtcrime.securesms.util.toPx +import org.thoughtcrime.securesms.mms.GlideRequests +import org.session.libsession.messaging.mentions.Mention + +class MentionCandidatesView(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : ListView(context, attrs, defStyleAttr) { + private var candidates = listOf() + set(newValue) { field = newValue; snAdapter.candidates = newValue } + var glide: GlideRequests? = null + set(newValue) { field = newValue; snAdapter.glide = newValue } + var openGroupServer: String? = null + set(newValue) { field = newValue; snAdapter.openGroupServer = openGroupServer } + var openGroupRoom: String? = null + set(newValue) { field = newValue; snAdapter.openGroupRoom = openGroupRoom } + var onCandidateSelected: ((Mention) -> Unit)? = null + + private val snAdapter by lazy { Adapter(context) } + + private class Adapter(private val context: Context) : BaseAdapter() { + var candidates = listOf() + set(newValue) { field = newValue; notifyDataSetChanged() } + var glide: GlideRequests? = null + var openGroupServer: String? = null + var openGroupRoom: String? = null + + override fun getCount(): Int { return candidates.count() } + override fun getItemId(position: Int): Long { return position.toLong() } + override fun getItem(position: Int): Mention { return candidates[position] } + + override fun getView(position: Int, cellToBeReused: View?, parent: ViewGroup): View { + val cell = cellToBeReused as MentionCandidateView? ?: MentionCandidateView.inflate(LayoutInflater.from(context), parent) + val mentionCandidate = getItem(position) + cell.glide = glide + cell.candidate = mentionCandidate + cell.openGroupServer = openGroupServer + cell.openGroupRoom = openGroupRoom + return cell + } + } + + constructor(context: Context, attrs: AttributeSet?) : this(context, attrs, 0) + constructor(context: Context) : this(context, null) + + init { + clipToOutline = true + adapter = snAdapter + snAdapter.candidates = candidates + setOnItemClickListener { _, _, position, _ -> + onCandidateSelected?.invoke(candidates[position]) + } + } + + fun show(candidates: List, threadID: Long) { + val openGroup = DatabaseFactory.getLokiThreadDatabase(context).getOpenGroupChat(threadID) + if (openGroup != null) { + openGroupServer = openGroup.server + openGroupRoom = openGroup.room + } + setMentionCandidates(candidates) + } + + fun setMentionCandidates(candidates: List) { + this.candidates = candidates + val layoutParams = this.layoutParams as ViewGroup.LayoutParams + layoutParams.height = toPx(Math.min(candidates.count(), 4) * 44, resources) + this.layoutParams = layoutParams + } + + fun hide() { + val layoutParams = this.layoutParams as ViewGroup.LayoutParams + layoutParams.height = 0 + this.layoutParams = layoutParams + } +} \ No newline at end of file diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/menus/ConversationActionModeCallback.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/menus/ConversationActionModeCallback.kt new file mode 100644 index 0000000000..62f292e325 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/menus/ConversationActionModeCallback.kt @@ -0,0 +1,102 @@ +package org.thoughtcrime.securesms.conversation.v2.menus + +import android.content.Context +import android.view.ActionMode +import android.view.Menu +import android.view.MenuItem +import network.loki.messenger.R +import org.session.libsession.messaging.open_groups.OpenGroupAPIV2 +import org.session.libsession.utilities.TextSecurePreferences +import org.thoughtcrime.securesms.conversation.v2.ConversationAdapter +import org.thoughtcrime.securesms.database.DatabaseFactory +import org.thoughtcrime.securesms.database.model.MediaMmsMessageRecord +import org.thoughtcrime.securesms.database.model.MessageRecord + +class ConversationActionModeCallback(private val adapter: ConversationAdapter, private val threadID: Long, + private val context: Context) : ActionMode.Callback { + var delegate: ConversationActionModeCallbackDelegate? = null + + override fun onCreateActionMode(mode: ActionMode, menu: Menu): Boolean { + val inflater = mode.menuInflater + inflater.inflate(R.menu.menu_conversation_item_action, menu) + updateActionModeMenu(menu) + return true + } + + fun updateActionModeMenu(menu: Menu) { + // Prepare + val selectedItems = adapter.selectedItems + val containsControlMessage = selectedItems.any { it.isUpdate } + val hasText = selectedItems.any { it.body.isNotEmpty() } + if (selectedItems.isEmpty()) { return } + val firstMessage = selectedItems.iterator().next() + val openGroup = DatabaseFactory.getLokiThreadDatabase(context).getOpenGroupChat(threadID) + val thread = DatabaseFactory.getThreadDatabase(context).getRecipientForThreadId(threadID)!! + val userPublicKey = TextSecurePreferences.getLocalNumber(context)!! + fun userCanDeleteSelectedItems(): Boolean { + if (openGroup == null) { return true } + val allSentByCurrentUser = selectedItems.all { it.isOutgoing } + if (allSentByCurrentUser) { return true } + return OpenGroupAPIV2.isUserModerator(userPublicKey, openGroup.room, openGroup.server) + } + fun userCanBanSelectedUsers(): Boolean { + if (openGroup == null) { return false } + val anySentByCurrentUser = selectedItems.any { it.isOutgoing } + if (anySentByCurrentUser) { return false } // Users can't ban themselves + val selectedUsers = selectedItems.map { it.recipient.address.toString() }.toSet() + if (selectedUsers.size > 1) { return false } + return OpenGroupAPIV2.isUserModerator(userPublicKey, openGroup.room, openGroup.server) + } + // Delete message + menu.findItem(R.id.menu_context_delete_message).isVisible = userCanDeleteSelectedItems() + // Ban user + menu.findItem(R.id.menu_context_ban_user).isVisible = userCanBanSelectedUsers() + // Copy message text + menu.findItem(R.id.menu_context_copy).isVisible = !containsControlMessage && hasText + // Copy Session ID + menu.findItem(R.id.menu_context_copy_public_key).isVisible = + (thread.isGroupRecipient && selectedItems.size == 1 && firstMessage.recipient.address.toString() != userPublicKey) + // Resend + menu.findItem(R.id.menu_context_resend).isVisible = (selectedItems.size == 1 && firstMessage.isFailed) + // Save media + menu.findItem(R.id.menu_context_save_attachment).isVisible = (selectedItems.size == 1 + && firstMessage.isMms && (firstMessage as MediaMmsMessageRecord).containsMediaSlide()) + // Reply + menu.findItem(R.id.menu_context_reply).isVisible = + (selectedItems.size == 1 && !firstMessage.isPending && !firstMessage.isFailed) + } + + override fun onPrepareActionMode(mode: ActionMode?, menu: Menu): Boolean { + return false + } + + override fun onActionItemClicked(mode: ActionMode, item: MenuItem): Boolean { + val selectedItems = adapter.selectedItems + when (item.itemId) { + R.id.menu_context_delete_message -> delegate?.deleteMessages(selectedItems) + R.id.menu_context_ban_user -> delegate?.banUser(selectedItems) + R.id.menu_context_copy -> delegate?.copyMessages(selectedItems) + R.id.menu_context_copy_public_key -> delegate?.copySessionID(selectedItems) + R.id.menu_context_resend -> delegate?.resendMessage(selectedItems) + R.id.menu_context_save_attachment -> delegate?.saveAttachment(selectedItems) + R.id.menu_context_reply -> delegate?.reply(selectedItems) + } + return true + } + + override fun onDestroyActionMode(mode: ActionMode) { + adapter.selectedItems.clear() + adapter.notifyDataSetChanged() + } +} + +interface ConversationActionModeCallbackDelegate { + + fun deleteMessages(messages: Set) + fun banUser(messages: Set) + fun copyMessages(messages: Set) + fun copySessionID(messages: Set) + fun resendMessage(messages: Set) + fun saveAttachment(messages: Set) + fun reply(messages: Set) +} \ No newline at end of file diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/menus/ConversationMenuHelper.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/menus/ConversationMenuHelper.kt new file mode 100644 index 0000000000..93278cb6a6 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/menus/ConversationMenuHelper.kt @@ -0,0 +1,328 @@ +package org.thoughtcrime.securesms.conversation.v2.menus + +import android.annotation.SuppressLint +import android.content.ClipData +import android.content.ClipboardManager +import android.content.Context +import android.content.Intent +import android.graphics.BitmapFactory +import android.graphics.PorterDuff +import android.graphics.PorterDuffColorFilter +import android.os.AsyncTask +import android.view.Menu +import android.view.MenuInflater +import android.view.MenuItem +import android.view.View +import android.widget.ImageView +import android.widget.TextView +import android.widget.Toast +import androidx.annotation.ColorInt +import androidx.appcompat.app.AlertDialog +import androidx.appcompat.app.AppCompatActivity +import androidx.appcompat.widget.SearchView +import androidx.appcompat.widget.SearchView.OnQueryTextListener +import androidx.core.content.pm.ShortcutInfoCompat +import androidx.core.content.pm.ShortcutManagerCompat +import androidx.core.graphics.drawable.IconCompat +import kotlinx.android.synthetic.main.activity_conversation_v2.* +import network.loki.messenger.R +import org.session.libsession.messaging.messages.control.ExpirationTimerUpdate +import org.session.libsession.messaging.sending_receiving.MessageSender +import org.session.libsession.messaging.sending_receiving.leave +import org.session.libsession.utilities.ExpirationUtil +import org.session.libsession.utilities.GroupUtil.doubleDecodeGroupID +import org.session.libsession.utilities.TextSecurePreferences +import org.session.libsession.utilities.recipients.Recipient +import org.session.libsignal.utilities.guava.Optional +import org.session.libsignal.utilities.toHexString +import org.thoughtcrime.securesms.* +import org.thoughtcrime.securesms.conversation.v2.ConversationActivityV2 +import org.thoughtcrime.securesms.database.DatabaseFactory +import org.thoughtcrime.securesms.groups.EditClosedGroupActivity +import org.thoughtcrime.securesms.groups.EditClosedGroupActivity.Companion.groupIDKey +import org.thoughtcrime.securesms.contacts.SelectContactsActivity +import org.thoughtcrime.securesms.util.getColorWithID +import org.thoughtcrime.securesms.util.BitmapUtil +import java.io.IOException + +object ConversationMenuHelper { + + fun onPrepareOptionsMenu(menu: Menu, inflater: MenuInflater, thread: Recipient, threadId: Long, context: Context, onOptionsItemSelected: (MenuItem) -> Unit) { + // Prepare + menu.clear() + val isOpenGroup = thread.isOpenGroupRecipient + // Base menu (options that should always be present) + inflater.inflate(R.menu.menu_conversation, menu) + // Expiring messages + if (!isOpenGroup) { + if (thread.expireMessages > 0) { + inflater.inflate(R.menu.menu_conversation_expiration_on, menu) + val item = menu.findItem(R.id.menu_expiring_messages) + val actionView = item.actionView + val iconView = actionView.findViewById(R.id.menu_badge_icon) + val badgeView = actionView.findViewById(R.id.expiration_badge) + @ColorInt val color = context.resources.getColorWithID(R.color.text, context.theme) + iconView.colorFilter = PorterDuffColorFilter(color, PorterDuff.Mode.MULTIPLY) + badgeView.text = ExpirationUtil.getExpirationAbbreviatedDisplayValue(context, thread.expireMessages) + actionView.setOnClickListener { onOptionsItemSelected(item) } + } else { + inflater.inflate(R.menu.menu_conversation_expiration_off, menu) + } + } + // One-on-one chat menu (options that should only be present for one-on-one chats) + if (thread.isContactRecipient) { + if (thread.isBlocked) { + inflater.inflate(R.menu.menu_conversation_unblock, menu) + } else { + inflater.inflate(R.menu.menu_conversation_block, menu) + } + inflater.inflate(R.menu.menu_conversation_copy_session_id, menu) + } + // Closed group menu (options that should only be present in closed groups) + if (thread.isClosedGroupRecipient) { + inflater.inflate(R.menu.menu_conversation_closed_group, menu) + } + // Open group menu + if (isOpenGroup) { + inflater.inflate(R.menu.menu_conversation_open_group, menu) + } + // Muting + if (thread.isMuted) { + inflater.inflate(R.menu.menu_conversation_muted, menu) + } else { + inflater.inflate(R.menu.menu_conversation_unmuted, menu) + } + + // Search + val searchViewItem = menu.findItem(R.id.menu_search) + (context as ConversationActivityV2).searchViewItem = searchViewItem + val searchView = searchViewItem.actionView as SearchView + val searchViewModel = context.searchViewModel!! + val queryListener = object : OnQueryTextListener { + override fun onQueryTextSubmit(query: String): Boolean { + return true + } + + override fun onQueryTextChange(query: String): Boolean { + searchViewModel.onQueryUpdated(query, threadId) + context.searchBottomBar.showLoading() + context.onSearchQueryUpdated(query) + return true + } + } + searchViewItem.setOnActionExpandListener(object : MenuItem.OnActionExpandListener { + override fun onMenuItemActionExpand(item: MenuItem): Boolean { + searchView.setOnQueryTextListener(queryListener) + searchViewModel.onSearchOpened() + context.searchBottomBar.visibility = View.VISIBLE + context.searchBottomBar.setData(0, 0) + context.inputBar.visibility = View.GONE + for (i in 0 until menu.size()) { + if (menu.getItem(i) != searchViewItem) { + menu.getItem(i).isVisible = false + } + } + return true + } + + override fun onMenuItemActionCollapse(item: MenuItem): Boolean { + searchView.setOnQueryTextListener(null) + searchViewModel.onSearchClosed() + context.searchBottomBar.visibility = View.GONE + context.inputBar.visibility = View.VISIBLE + context.onSearchQueryUpdated(null) + context.invalidateOptionsMenu() + return true + } + }) + } + + fun onOptionItemSelected(context: Context, item: MenuItem, thread: Recipient): Boolean { + when (item.itemId) { + R.id.menu_view_all_media -> { showAllMedia(context, thread) } + R.id.menu_search -> { search(context) } + R.id.menu_add_shortcut -> { addShortcut(context, thread) } + R.id.menu_expiring_messages -> { showExpiringMessagesDialog(context, thread) } + R.id.menu_expiring_messages_off -> { showExpiringMessagesDialog(context, thread) } + R.id.menu_unblock -> { unblock(context, thread) } + R.id.menu_block -> { block(context, thread) } + R.id.menu_copy_session_id -> { copySessionID(context, thread) } + R.id.menu_edit_group -> { editClosedGroup(context, thread) } + R.id.menu_leave_group -> { leaveClosedGroup(context, thread) } + R.id.menu_invite_to_open_group -> { inviteContacts(context, thread) } + R.id.menu_unmute_notifications -> { unmute(context, thread) } + R.id.menu_mute_notifications -> { mute(context, thread) } + } + return true + } + + private fun showAllMedia(context: Context, thread: Recipient) { + val intent = Intent(context, MediaOverviewActivity::class.java) + intent.putExtra(MediaOverviewActivity.ADDRESS_EXTRA, thread.address) + val activity = context as AppCompatActivity + activity.startActivity(intent) + } + + private fun search(context: Context) { + val searchViewModel = (context as ConversationActivityV2).searchViewModel!! + searchViewModel.onSearchOpened() + } + + @SuppressLint("StaticFieldLeak") + private fun addShortcut(context: Context, thread: Recipient) { + object : AsyncTask() { + + override fun doInBackground(vararg params: Void?): IconCompat? { + var icon: IconCompat? = null + val contactPhoto = thread.contactPhoto + if (contactPhoto != null) { + try { + var bitmap = BitmapFactory.decodeStream(contactPhoto.openInputStream(context)) + bitmap = BitmapUtil.createScaledBitmap(bitmap, 300, 300) + icon = IconCompat.createWithAdaptiveBitmap(bitmap) + } catch (e: IOException) { + // Do nothing + } + } + if (icon == null) { + icon = IconCompat.createWithResource(context, if (thread.isGroupRecipient) R.mipmap.ic_group_shortcut else R.mipmap.ic_person_shortcut) + } + return icon + } + + override fun onPostExecute(icon: IconCompat?) { + val name = Optional.fromNullable(thread.name) + .or(Optional.fromNullable(thread.profileName)) + .or(thread.toShortString()) + val shortcutInfo = ShortcutInfoCompat.Builder(context, thread.address.serialize() + '-' + System.currentTimeMillis()) + .setShortLabel(name) + .setIcon(icon) + .setIntent(ShortcutLauncherActivity.createIntent(context, thread.address)) + .build() + if (ShortcutManagerCompat.requestPinShortcut(context, shortcutInfo, null)) { + Toast.makeText(context, context.resources.getString(R.string.ConversationActivity_added_to_home_screen), Toast.LENGTH_LONG).show() + } + } + }.execute() + } + + private fun showExpiringMessagesDialog(context: Context, thread: Recipient) { + if (thread.isClosedGroupRecipient) { + val group = DatabaseFactory.getGroupDatabase(context).getGroup(thread.address.toGroupString()).orNull() + if (group?.isActive == false) { return } + } + ExpirationDialog.show(context, thread.expireMessages) { expirationTime: Int -> + DatabaseFactory.getRecipientDatabase(context).setExpireMessages(thread, expirationTime) + val message = ExpirationTimerUpdate(expirationTime) + message.recipient = thread.address.serialize() + message.sentTimestamp = System.currentTimeMillis() + val expiringMessageManager = ApplicationContext.getInstance(context).expiringMessageManager + expiringMessageManager.setExpirationTimer(message) + MessageSender.send(message, thread.address) + val activity = context as AppCompatActivity + activity.invalidateOptionsMenu() + } + } + + private fun unblock(context: Context, thread: Recipient) { + if (!thread.isContactRecipient) { return } + val title = R.string.ConversationActivity_unblock_this_contact_question + val message = R.string.ConversationActivity_you_will_once_again_be_able_to_receive_messages_and_calls_from_this_contact + AlertDialog.Builder(context) + .setTitle(title) + .setMessage(message) + .setNegativeButton(android.R.string.cancel, null) + .setPositiveButton(R.string.ConversationActivity_unblock) { _, _ -> + DatabaseFactory.getRecipientDatabase(context) + .setBlocked(thread, false) + }.show() + } + + private fun block(context: Context, thread: Recipient) { + if (!thread.isContactRecipient) { return } + val title = R.string.RecipientPreferenceActivity_block_this_contact_question + val message = R.string.RecipientPreferenceActivity_you_will_no_longer_receive_messages_and_calls_from_this_contact + AlertDialog.Builder(context) + .setTitle(title) + .setMessage(message) + .setNegativeButton(android.R.string.cancel, null) + .setPositiveButton(R.string.RecipientPreferenceActivity_block) { _, _ -> + DatabaseFactory.getRecipientDatabase(context) + .setBlocked(thread, true) + }.show() + } + + private fun copySessionID(context: Context, thread: Recipient) { + if (!thread.isContactRecipient) { return } + val sessionID = thread.address.toString() + val clip = ClipData.newPlainText("Session ID", sessionID) + val activity = context as AppCompatActivity + val manager = activity.getSystemService(PassphraseRequiredActionBarActivity.CLIPBOARD_SERVICE) as ClipboardManager + manager.setPrimaryClip(clip) + Toast.makeText(context, R.string.copied_to_clipboard, Toast.LENGTH_SHORT).show() + } + + private fun editClosedGroup(context: Context, thread: Recipient) { + if (!thread.isClosedGroupRecipient) { return } + val intent = Intent(context, EditClosedGroupActivity::class.java) + val groupID: String = thread.address.toGroupString() + intent.putExtra(groupIDKey, groupID) + context.startActivity(intent) + } + + private fun leaveClosedGroup(context: Context, thread: Recipient) { + if (!thread.isClosedGroupRecipient) { return } + val builder = AlertDialog.Builder(context) + builder.setTitle(context.resources.getString(R.string.ConversationActivity_leave_group)) + builder.setCancelable(true) + val group = DatabaseFactory.getGroupDatabase(context).getGroup(thread.address.toGroupString()).orNull() + val admins = group.admins + val sessionID = TextSecurePreferences.getLocalNumber(context) + val isCurrentUserAdmin = admins.any { it.toString() == sessionID } + val message = if (isCurrentUserAdmin) { + "Because you are the creator of this group it will be deleted for everyone. This cannot be undone." + } else { + context.resources.getString(R.string.ConversationActivity_are_you_sure_you_want_to_leave_this_group) + } + builder.setMessage(message) + builder.setPositiveButton(R.string.yes) { _, _ -> + var groupPublicKey: String? + var isClosedGroup: Boolean + try { + groupPublicKey = doubleDecodeGroupID(thread.address.toString()).toHexString() + isClosedGroup = DatabaseFactory.getLokiAPIDatabase(context).isClosedGroup(groupPublicKey) + } catch (e: IOException) { + groupPublicKey = null + isClosedGroup = false + } + try { + if (isClosedGroup) { + MessageSender.leave(groupPublicKey!!, true) + } else { + Toast.makeText(context, R.string.ConversationActivity_error_leaving_group, Toast.LENGTH_LONG).show() + } + } catch (e: Exception) { + Toast.makeText(context, R.string.ConversationActivity_error_leaving_group, Toast.LENGTH_LONG).show() + } + } + builder.setNegativeButton(R.string.no, null) + builder.show() + } + + private fun inviteContacts(context: Context, thread: Recipient) { + if (!thread.isOpenGroupRecipient) { return } + val intent = Intent(context, SelectContactsActivity::class.java) + val activity = context as AppCompatActivity + activity.startActivityForResult(intent, ConversationActivityV2.INVITE_CONTACTS) + } + + private fun unmute(context: Context, thread: Recipient) { + DatabaseFactory.getRecipientDatabase(context).setMuted(thread, 0) + } + + private fun mute(context: Context, thread: Recipient) { + MuteDialog.show(context) { until: Long -> + DatabaseFactory.getRecipientDatabase(context).setMuted(thread, until) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/ControlMessageView.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/ControlMessageView.kt new file mode 100644 index 0000000000..78e926d041 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/ControlMessageView.kt @@ -0,0 +1,46 @@ +package org.thoughtcrime.securesms.conversation.v2.messages + +import android.content.Context +import android.content.res.Resources +import android.util.AttributeSet +import android.view.Gravity +import android.view.LayoutInflater +import android.view.View +import android.widget.LinearLayout +import androidx.core.content.res.ResourcesCompat +import androidx.recyclerview.widget.RecyclerView +import kotlinx.android.synthetic.main.view_control_message.view.* +import network.loki.messenger.R +import org.thoughtcrime.securesms.database.model.MessageRecord + +class ControlMessageView : LinearLayout { + + // 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() { + LayoutInflater.from(context).inflate(R.layout.view_control_message, this) + layoutParams = RecyclerView.LayoutParams(RecyclerView.LayoutParams.MATCH_PARENT, RecyclerView.LayoutParams.WRAP_CONTENT) + } + // endregion + + // region Updating + fun bind(message: MessageRecord) { + iconImageView.visibility = View.GONE + if (message.isExpirationTimerUpdate) { + iconImageView.setImageDrawable(ResourcesCompat.getDrawable(resources, R.drawable.ic_timer, context.theme)) + iconImageView.visibility = View.VISIBLE + } else if (message.isMediaSavedNotification) { + iconImageView.setImageDrawable(ResourcesCompat.getDrawable(resources, R.drawable.ic_file_download_white_36dp, context.theme)) + iconImageView.visibility = View.VISIBLE + } + textView.text = message.getDisplayBody(context) + } + + fun recycle() { + + } + // endregion +} \ No newline at end of file diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/DocumentView.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/DocumentView.kt new file mode 100644 index 0000000000..c9daca6c7a --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/DocumentView.kt @@ -0,0 +1,34 @@ +package org.thoughtcrime.securesms.conversation.v2.messages + +import android.content.Context +import android.content.res.ColorStateList +import android.util.AttributeSet +import android.view.LayoutInflater +import android.widget.LinearLayout +import androidx.annotation.ColorInt +import kotlinx.android.synthetic.main.view_document.view.* +import network.loki.messenger.R +import org.thoughtcrime.securesms.database.model.MessageRecord +import org.thoughtcrime.securesms.database.model.MmsMessageRecord + +class DocumentView : LinearLayout { + + // 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() { + LayoutInflater.from(context).inflate(R.layout.view_document, this) + } + // endregion + + // region Updating + fun bind(message: MmsMessageRecord, @ColorInt textColor: Int) { + val document = message.slideDeck.documentSlide!! + documentTitleTextView.text = document.fileName.or("Untitled File") + documentTitleTextView.setTextColor(textColor) + documentViewIconImageView.imageTintList = ColorStateList.valueOf(textColor) + } + // endregion +} \ No newline at end of file diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/LinkPreviewView.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/LinkPreviewView.kt new file mode 100644 index 0000000000..0457f82702 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/LinkPreviewView.kt @@ -0,0 +1,103 @@ +package org.thoughtcrime.securesms.conversation.v2.messages + +import android.content.Context +import android.graphics.Canvas +import android.graphics.Rect +import android.text.method.LinkMovementMethod +import android.util.AttributeSet +import android.view.LayoutInflater +import android.view.MotionEvent +import android.widget.LinearLayout +import android.widget.TextView +import androidx.appcompat.app.AppCompatActivity +import androidx.core.content.res.ResourcesCompat +import androidx.core.text.getSpans +import androidx.core.text.toSpannable +import androidx.core.view.isVisible +import kotlinx.android.synthetic.main.view_link_preview.view.* +import network.loki.messenger.R +import org.thoughtcrime.securesms.components.CornerMask +import org.thoughtcrime.securesms.conversation.v2.dialogs.OpenURLDialog +import org.thoughtcrime.securesms.conversation.v2.utilities.MessageBubbleUtilities +import org.thoughtcrime.securesms.conversation.v2.utilities.ModalURLSpan +import org.thoughtcrime.securesms.conversation.v2.utilities.TextUtilities.getIntersectedModalSpans +import org.thoughtcrime.securesms.database.model.MmsMessageRecord +import org.thoughtcrime.securesms.util.UiModeUtilities +import org.thoughtcrime.securesms.mms.GlideRequests +import org.thoughtcrime.securesms.mms.ImageSlide + +class LinkPreviewView : LinearLayout { + private val cornerMask by lazy { CornerMask(this) } + private var url: String? = null + lateinit var bodyTextView: TextView + + // 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() { + LayoutInflater.from(context).inflate(R.layout.view_link_preview, this) + } + // endregion + + // region Updating + fun bind(message: MmsMessageRecord, glide: GlideRequests, isStartOfMessageCluster: Boolean, isEndOfMessageCluster: Boolean, searchQuery: String?) { + val linkPreview = message.linkPreviews.first() + url = linkPreview.url + // Thumbnail + if (linkPreview.getThumbnail().isPresent) { + // This internally fetches the thumbnail + thumbnailImageView.setImageResource(glide, ImageSlide(context, linkPreview.getThumbnail().get()), isPreview = false, message) + thumbnailImageView.loadIndicator.isVisible = false + } + // Title + titleTextView.text = linkPreview.title + val textColorID = if (message.isOutgoing && UiModeUtilities.isDayUiMode(context)) { + R.color.white + } else { + if (UiModeUtilities.isDayUiMode(context)) R.color.black else R.color.white + } + titleTextView.setTextColor(ResourcesCompat.getColor(resources, textColorID, context.theme)) + // Body + bodyTextView = VisibleMessageContentView.getBodyTextView(context, message, searchQuery) + mainLinkPreviewContainer.addView(bodyTextView) + // Corner radii + val cornerRadii = MessageBubbleUtilities.calculateRadii(context, isStartOfMessageCluster, isEndOfMessageCluster, message.isOutgoing) + cornerMask.setTopLeftRadius(cornerRadii[0]) + cornerMask.setTopRightRadius(cornerRadii[1]) + cornerMask.setBottomRightRadius(cornerRadii[2]) + cornerMask.setBottomLeftRadius(cornerRadii[3]) + } + + override fun dispatchDraw(canvas: Canvas) { + super.dispatchDraw(canvas) + cornerMask.mask(canvas) + } + // endregion + + // region Interaction + fun calculateHit(event: MotionEvent) { + val rawXInt = event.rawX.toInt() + val rawYInt = event.rawY.toInt() + val hitRect = Rect(rawXInt, rawYInt, rawXInt, rawYInt) + val previewRect = Rect() + mainLinkPreviewParent.getGlobalVisibleRect(previewRect) + if (previewRect.contains(hitRect)) { + openURL() + return + } + // intersectedModalSpans should only be a list of one item + val hitSpans = bodyTextView.getIntersectedModalSpans(hitRect) + hitSpans.forEach { span -> + span.onClick(bodyTextView) + } + } + + fun openURL() { + val url = this.url ?: return + val activity = context as AppCompatActivity + OpenURLDialog(url).show(activity.supportFragmentManager, "Open URL Dialog") + } + // endregion +} \ No newline at end of file diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/OpenGroupInvitationView.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/OpenGroupInvitationView.kt new file mode 100644 index 0000000000..ebcdcac2dd --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/OpenGroupInvitationView.kt @@ -0,0 +1,47 @@ +package org.thoughtcrime.securesms.conversation.v2.messages + +import android.content.Context +import android.util.AttributeSet +import android.view.LayoutInflater +import android.widget.LinearLayout +import androidx.annotation.ColorInt +import androidx.appcompat.app.AppCompatActivity +import kotlinx.android.synthetic.main.view_open_group_invitation.view.* +import network.loki.messenger.R +import org.session.libsession.messaging.open_groups.OpenGroupV2 +import org.session.libsession.messaging.utilities.UpdateMessageData +import org.session.libsession.utilities.OpenGroupUrlParser +import org.thoughtcrime.securesms.conversation.v2.dialogs.JoinOpenGroupDialog +import org.thoughtcrime.securesms.database.model.MessageRecord + +class OpenGroupInvitationView : LinearLayout { + private var data: UpdateMessageData.Kind.OpenGroupInvitation? = null + + 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() { + LayoutInflater.from(context).inflate(R.layout.view_open_group_invitation, this) + } + + fun bind(message: MessageRecord, @ColorInt textColor: Int) { + // FIXME: This is a really weird approach... + val umd = UpdateMessageData.fromJSON(message.body)!! + val data = umd.kind as UpdateMessageData.Kind.OpenGroupInvitation + this.data = data + val iconID = if (message.isOutgoing) R.drawable.ic_globe else R.drawable.ic_plus + openGroupInvitationIconImageView.setImageResource(iconID) + openGroupTitleTextView.text = data.groupName + openGroupURLTextView.text = OpenGroupUrlParser.trimQueryParameter(data.groupUrl) + openGroupTitleTextView.setTextColor(textColor) + openGroupJoinMessageTextView.setTextColor(textColor) + openGroupURLTextView.setTextColor(textColor) + } + + fun joinOpenGroup() { + val data = data ?: return + val activity = context as AppCompatActivity + JoinOpenGroupDialog(data.groupName, data.groupUrl).show(activity.supportFragmentManager, "Join Open Group Dialog") + } +} \ No newline at end of file diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/QuoteView.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/QuoteView.kt new file mode 100644 index 0000000000..1798b07f0a --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/QuoteView.kt @@ -0,0 +1,197 @@ +package org.thoughtcrime.securesms.conversation.v2.messages + +import android.content.Context +import android.content.res.ColorStateList +import android.util.AttributeSet +import android.view.LayoutInflater +import android.widget.LinearLayout +import android.widget.RelativeLayout +import androidx.annotation.ColorInt +import androidx.core.content.res.ResourcesCompat +import androidx.core.text.toSpannable +import androidx.core.view.isVisible +import kotlinx.android.synthetic.main.view_link_preview.view.* +import kotlinx.android.synthetic.main.view_quote.view.* +import network.loki.messenger.R +import org.session.libsession.messaging.contacts.Contact +import org.session.libsession.utilities.recipients.Recipient +import org.thoughtcrime.securesms.conversation.v2.utilities.MentionUtilities +import org.thoughtcrime.securesms.conversation.v2.utilities.TextUtilities +import org.thoughtcrime.securesms.database.DatabaseFactory +import org.thoughtcrime.securesms.util.* +import org.thoughtcrime.securesms.mms.GlideRequests +import org.thoughtcrime.securesms.mms.SlideDeck +import org.thoughtcrime.securesms.util.MediaUtil +import org.thoughtcrime.securesms.util.UiModeUtilities +import kotlin.math.max +import kotlin.math.min +import kotlin.math.roundToInt + +// There's quite some calculation going on here. It's a bit complex so don't make changes +// if you don't need to. If you do then test: +// • Quoted text in both private chats and group chats +// • Quoted images and videos in both private chats and group chats +// • Quoted voice messages and documents in both private chats and group chats +// • All of the above in both dark mode and light mode + +class QuoteView : LinearLayout { + private lateinit var mode: Mode + private val vPadding by lazy { toPx(6, resources) } + var delegate: QuoteViewDelegate? = null + + enum class Mode { Regular, Draft } + + // region Lifecycle + constructor(context: Context) : super(context) { throw IllegalAccessError("Use QuoteView(context:mode:) instead.") } + constructor(context: Context, attrs: AttributeSet) : super(context, attrs) { throw IllegalAccessError("Use QuoteView(context:mode:) instead.") } + constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr) { throw IllegalAccessError("Use QuoteView(context:mode:) instead.") } + + constructor(context: Context, mode: Mode) : super(context) { + this.mode = mode + LayoutInflater.from(context).inflate(R.layout.view_quote, this) + // Add padding here (not on mainQuoteViewContainer) to get a bit of a top inset while avoiding + // the clipping issue described in getIntrinsicHeight(maxContentWidth:). + setPadding(0, toPx(6, resources), 0, 0) + when (mode) { + Mode.Draft -> quoteViewCancelButton.setOnClickListener { delegate?.cancelQuoteDraft() } + Mode.Regular -> { + quoteViewCancelButton.isVisible = false + mainQuoteViewContainer.setBackgroundColor(ResourcesCompat.getColor(resources, R.color.transparent, context.theme)) + val quoteViewMainContentContainerLayoutParams = quoteViewMainContentContainer.layoutParams as RelativeLayout.LayoutParams + // Since we're not showing the cancel button we can shorten the end margin + quoteViewMainContentContainerLayoutParams.marginEnd = resources.getDimension(R.dimen.medium_spacing).roundToInt() + quoteViewMainContentContainer.layoutParams = quoteViewMainContentContainerLayoutParams + } + } + } + // endregion + + // region General + fun getIntrinsicContentHeight(maxContentWidth: Int): Int { + // If we're showing an attachment thumbnail, just constrain to the height of that + if (quoteViewAttachmentPreviewContainer.isVisible) { return toPx(40, resources) } + var result = 0 + var authorTextViewIntrinsicHeight = 0 + if (quoteViewAuthorTextView.isVisible) { + val author = quoteViewAuthorTextView.text + authorTextViewIntrinsicHeight = TextUtilities.getIntrinsicHeight(author, quoteViewAuthorTextView.paint, maxContentWidth) + result += authorTextViewIntrinsicHeight + } + val body = quoteViewBodyTextView.text + val bodyTextViewIntrinsicHeight = TextUtilities.getIntrinsicHeight(body, quoteViewBodyTextView.paint, maxContentWidth) + result += bodyTextViewIntrinsicHeight + if (!quoteViewAuthorTextView.isVisible) { + // We want to at least be as high as the cancel button, and no higher than 56 DP (that's + // approximately the height of 3 lines. + return min(max(result, toPx(32, resources)), toPx(56, resources)) + } else { + // Because we're showing the author text view, we should have a height of at least 32 DP + // anyway, so there's no need to constrain to that. We constrain to a max height of 56 DP + // because that's approximately the height of the author text view + 2 lines of the body + // text view. + return min(result, toPx(56, resources)) + } + } + + fun getIntrinsicHeight(maxContentWidth: Int): Int { + // The way all this works is that we just calculate the total height the quote view should be + // and then center everything inside vertically. This effectively means we're applying padding. + // Applying padding the regular way results in a clipping issue though due to a bug in + // RelativeLayout. + return getIntrinsicContentHeight(maxContentWidth) + 2 * vPadding + } + // endregion + + // region Updating + fun bind(authorPublicKey: String, body: String?, attachments: SlideDeck?, thread: Recipient, + isOutgoingMessage: Boolean, maxContentWidth: Int, isOpenGroupInvitation: Boolean, threadID: Long, + isOriginalMissing: Boolean, glide: GlideRequests) { + val contactDB = DatabaseFactory.getSessionContactDatabase(context) + // Reduce the max body text view line count to 2 if this is a group thread because + // we'll be showing the author text view and we don't want the overall quote view height + // to get too big. + quoteViewBodyTextView.maxLines = if (thread.isGroupRecipient) 2 else 3 + // Author + if (thread.isGroupRecipient) { + val author = contactDB.getContactWithSessionID(authorPublicKey) + val authorDisplayName = author?.displayName(Contact.contextForRecipient(thread)) ?: authorPublicKey + quoteViewAuthorTextView.text = authorDisplayName + quoteViewAuthorTextView.setTextColor(getTextColor(isOutgoingMessage)) + } + quoteViewAuthorTextView.isVisible = thread.isGroupRecipient + // Body + quoteViewBodyTextView.text = if (isOpenGroupInvitation) resources.getString(R.string.open_group_invitation_view__open_group_invitation) else MentionUtilities.highlightMentions((body ?: "").toSpannable(), threadID, context); + quoteViewBodyTextView.setTextColor(getTextColor(isOutgoingMessage)) + // Accent line / attachment preview + val hasAttachments = (attachments != null && attachments.asAttachments().isNotEmpty()) && !isOriginalMissing + quoteViewAccentLine.isVisible = !hasAttachments + quoteViewAttachmentPreviewContainer.isVisible = hasAttachments + if (!hasAttachments) { + val accentLineLayoutParams = quoteViewAccentLine.layoutParams as RelativeLayout.LayoutParams + accentLineLayoutParams.height = getIntrinsicContentHeight(maxContentWidth) // Match the intrinsic * content * height + quoteViewAccentLine.layoutParams = accentLineLayoutParams + quoteViewAccentLine.setBackgroundColor(getLineColor(isOutgoingMessage)) + } else if (attachments != null) { + quoteViewAttachmentPreviewImageView.imageTintList = ColorStateList.valueOf(ResourcesCompat.getColor(resources, R.color.white, context.theme)) + val backgroundColorID = if (UiModeUtilities.isDayUiMode(context)) R.color.black else R.color.accent + val backgroundColor = ResourcesCompat.getColor(resources, backgroundColorID, context.theme) + quoteViewAttachmentPreviewContainer.backgroundTintList = ColorStateList.valueOf(backgroundColor) + quoteViewAttachmentPreviewImageView.isVisible = false + quoteViewAttachmentThumbnailImageView.isVisible = false + if (attachments.audioSlide != null) { + quoteViewAttachmentPreviewImageView.setImageResource(R.drawable.ic_microphone) + quoteViewAttachmentPreviewImageView.isVisible = true + quoteViewBodyTextView.text = resources.getString(R.string.Slide_audio) + } else if (attachments.documentSlide != null) { + quoteViewAttachmentPreviewImageView.setImageResource(R.drawable.ic_document_large_light) + quoteViewAttachmentPreviewImageView.isVisible = true + quoteViewBodyTextView.text = resources.getString(R.string.document) + } else if (attachments.thumbnailSlide != null) { + val slide = attachments.thumbnailSlide!! + // This internally fetches the thumbnail + quoteViewAttachmentThumbnailImageView.radius = toPx(4, resources) + quoteViewAttachmentThumbnailImageView.setImageResource(glide, slide, false, false) + quoteViewAttachmentThumbnailImageView.isVisible = true + quoteViewBodyTextView.text = if (MediaUtil.isVideo(slide.asAttachment())) resources.getString(R.string.Slide_video) else resources.getString(R.string.Slide_image) + } + } + mainQuoteViewContainer.layoutParams = LayoutParams(LayoutParams.WRAP_CONTENT, getIntrinsicHeight(maxContentWidth)) + val quoteViewMainContentContainerLayoutParams = quoteViewMainContentContainer.layoutParams as RelativeLayout.LayoutParams + // The start margin is different if we just show the accent line vs if we show an attachment thumbnail + quoteViewMainContentContainerLayoutParams.marginStart = if (!hasAttachments) toPx(16, resources) else toPx(48, resources) + quoteViewMainContentContainer.layoutParams = quoteViewMainContentContainerLayoutParams + } + // endregion + + // region Convenience + @ColorInt private fun getLineColor(isOutgoingMessage: Boolean): Int { + val isLightMode = UiModeUtilities.isDayUiMode(context) + if ((mode == Mode.Regular && isLightMode) || (mode == Mode.Draft && isLightMode)) { + return ResourcesCompat.getColor(resources, R.color.black, context.theme) + } else if (mode == Mode.Regular && !isLightMode) { + if (isOutgoingMessage) { + return ResourcesCompat.getColor(resources, R.color.black, context.theme) + } else { + return ResourcesCompat.getColor(resources, R.color.accent, context.theme) + } + } else { // Draft & dark mode + return ResourcesCompat.getColor(resources, R.color.accent, context.theme) + } + } + + @ColorInt private fun getTextColor(isOutgoingMessage: Boolean): Int { + if (mode == Mode.Draft) { return ResourcesCompat.getColor(resources, R.color.text, context.theme) } + val isLightMode = UiModeUtilities.isDayUiMode(context) + if ((isOutgoingMessage && !isLightMode) || (!isOutgoingMessage && isLightMode)) { + return ResourcesCompat.getColor(resources, R.color.black, context.theme) + } else { + return ResourcesCompat.getColor(resources, R.color.white, context.theme) + } + } + // endregion +} + +interface QuoteViewDelegate { + + fun cancelQuoteDraft() +} \ No newline at end of file diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/UntrustedAttachmentView.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/UntrustedAttachmentView.kt new file mode 100644 index 0000000000..967e080d92 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/UntrustedAttachmentView.kt @@ -0,0 +1,55 @@ +package org.thoughtcrime.securesms.conversation.v2.messages + +import android.content.Context +import android.util.AttributeSet +import android.view.LayoutInflater +import android.widget.LinearLayout +import androidx.annotation.ColorInt +import androidx.core.content.ContextCompat +import kotlinx.android.synthetic.main.view_untrusted_attachment.view.* +import network.loki.messenger.R +import org.session.libsession.utilities.recipients.Recipient +import org.thoughtcrime.securesms.conversation.v2.dialogs.DownloadDialog +import org.thoughtcrime.securesms.util.ActivityDispatcher +import java.util.* + +class UntrustedAttachmentView: LinearLayout { + + enum class AttachmentType { + AUDIO, + DOCUMENT, + MEDIA + } + + // 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() { + LayoutInflater.from(context).inflate(R.layout.view_untrusted_attachment, this) + } + // endregion + + // region Updating + fun bind(attachmentType: AttachmentType, @ColorInt textColor: Int) { + val (iconRes, stringRes) = when (attachmentType) { + AttachmentType.AUDIO -> R.drawable.ic_microphone to R.string.Slide_audio + AttachmentType.DOCUMENT -> R.drawable.ic_document_large_light to R.string.document + AttachmentType.MEDIA -> R.drawable.ic_image_white_24dp to R.string.media + } + val iconDrawable = ContextCompat.getDrawable(context,iconRes)!! + iconDrawable.mutate().setTint(textColor) + val text = context.getString(R.string.UntrustedAttachmentView_download_attachment, context.getString(stringRes).toLowerCase(Locale.ROOT)) + + untrustedAttachmentIcon.setImageDrawable(iconDrawable) + untrustedAttachmentTitle.text = text + } + // endregion + + // region Interaction + fun showTrustDialog(recipient: Recipient) { + ActivityDispatcher.get(context)?.showDialog(DownloadDialog(recipient)) + } + +} \ No newline at end of file diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/VisibleMessageContentView.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/VisibleMessageContentView.kt new file mode 100644 index 0000000000..1d90f8475a --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/VisibleMessageContentView.kt @@ -0,0 +1,252 @@ +package org.thoughtcrime.securesms.conversation.v2.messages + +import android.content.Context +import android.graphics.Color +import android.graphics.Rect +import android.graphics.drawable.Drawable +import android.text.style.BackgroundColorSpan +import android.text.style.ForegroundColorSpan +import android.text.style.URLSpan +import android.text.util.Linkify +import android.util.AttributeSet +import android.util.TypedValue +import android.view.LayoutInflater +import android.view.MotionEvent +import android.widget.LinearLayout +import android.widget.TextView +import androidx.annotation.ColorInt +import androidx.annotation.DrawableRes +import androidx.appcompat.app.AppCompatActivity +import androidx.core.content.res.ResourcesCompat +import androidx.core.graphics.BlendModeColorFilterCompat +import androidx.core.graphics.BlendModeCompat +import androidx.core.text.getSpans +import androidx.core.text.toSpannable +import kotlinx.android.synthetic.main.view_link_preview.view.* +import kotlinx.android.synthetic.main.view_visible_message_content.view.* +import network.loki.messenger.R +import org.session.libsession.utilities.ThemeUtil +import org.session.libsession.utilities.ViewUtil +import org.session.libsession.utilities.recipients.Recipient +import org.thoughtcrime.securesms.conversation.v2.components.AlbumThumbnailView +import org.thoughtcrime.securesms.components.emoji.EmojiTextView +import org.thoughtcrime.securesms.conversation.v2.ConversationActivityV2 +import org.thoughtcrime.securesms.conversation.v2.dialogs.OpenURLDialog +import org.thoughtcrime.securesms.conversation.v2.utilities.MentionUtilities +import org.thoughtcrime.securesms.conversation.v2.utilities.ModalURLSpan +import org.thoughtcrime.securesms.conversation.v2.utilities.TextUtilities.getIntersectedModalSpans +import org.thoughtcrime.securesms.database.model.MessageRecord +import org.thoughtcrime.securesms.database.model.MmsMessageRecord +import org.thoughtcrime.securesms.util.* +import org.thoughtcrime.securesms.mms.GlideRequests +import org.thoughtcrime.securesms.util.SearchUtil +import org.thoughtcrime.securesms.util.SearchUtil.StyleFactory +import org.thoughtcrime.securesms.util.UiModeUtilities +import java.util.* +import kotlin.math.roundToInt + +class VisibleMessageContentView : LinearLayout { + var onContentClick: ((event: MotionEvent) -> Unit)? = null + var onContentDoubleTap: (() -> Unit)? = null + var delegate: VisibleMessageContentViewDelegate? = null + var viewHolderIndex: 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() { + LayoutInflater.from(context).inflate(R.layout.view_visible_message_content, this) + } + // endregion + + // region Updating + fun bind(message: MessageRecord, isStartOfMessageCluster: Boolean, isEndOfMessageCluster: Boolean, + glide: GlideRequests, maxWidth: Int, thread: Recipient, searchQuery: String?, contactIsTrusted: Boolean) { + // Background + val background = getBackground(message.isOutgoing, isStartOfMessageCluster, isEndOfMessageCluster) + val colorID = if (message.isOutgoing) R.attr.message_sent_background_color else R.attr.message_received_background_color + val color = ThemeUtil.getThemedColor(context, colorID) + val filter = BlendModeColorFilterCompat.createBlendModeColorFilterCompat(color, BlendModeCompat.SRC_IN) + background.colorFilter = filter + setBackground(background) + // Body + mainContainer.removeAllViews() + onContentClick = null + onContentDoubleTap = null + if (message is MmsMessageRecord && message.linkPreviews.isNotEmpty()) { + val linkPreviewView = LinkPreviewView(context) + linkPreviewView.bind(message, glide, isStartOfMessageCluster, isEndOfMessageCluster, searchQuery) + mainContainer.addView(linkPreviewView) + onContentClick = { event -> linkPreviewView.calculateHit(event) } + // Body text view is inside the link preview for layout convenience + } else if (message is MmsMessageRecord && message.quote != null) { + val quote = message.quote!! + val quoteView = QuoteView(context, QuoteView.Mode.Regular) + // The max content width is the max message bubble size - 2 times the horizontal padding - 2 + // times the horizontal margin. This unfortunately has to be calculated manually + // here to get the layout right. + val maxContentWidth = (maxWidth - 2 * resources.getDimension(R.dimen.medium_spacing) - 2 * toPx(16, resources)).roundToInt() + val quoteText = if (quote.isOriginalMissing) { + context.getString(R.string.QuoteView_original_missing) + } else { + quote.text + } + quoteView.bind(quote.author.toString(), quoteText, quote.attachment, thread, + message.isOutgoing, maxContentWidth, message.isOpenGroupInvitation, message.threadId, + quote.isOriginalMissing, glide) + mainContainer.addView(quoteView) + val bodyTextView = VisibleMessageContentView.getBodyTextView(context, message, searchQuery) + ViewUtil.setPaddingTop(bodyTextView, 0) + mainContainer.addView(bodyTextView) + onContentClick = { event -> + val r = Rect() + quoteView.getGlobalVisibleRect(r) + if (r.contains(event.rawX.roundToInt(), event.rawY.roundToInt())) { + delegate?.scrollToMessageIfPossible(quote.id) + } + } + } else if (message is MmsMessageRecord && message.slideDeck.audioSlide != null) { + // Audio attachment + if (contactIsTrusted || message.isOutgoing) { + val voiceMessageView = VoiceMessageView(context) + voiceMessageView.index = viewHolderIndex + voiceMessageView.delegate = context as? ConversationActivityV2 + voiceMessageView.bind(message, isStartOfMessageCluster, isEndOfMessageCluster) + mainContainer.addView(voiceMessageView) + // We have to use onContentClick (rather than a click listener directly on the voice + // message view) so as to not interfere with all the other gestures. + onContentClick = { voiceMessageView.togglePlayback() } + onContentDoubleTap = { voiceMessageView.handleDoubleTap() } + } else { + val untrustedView = UntrustedAttachmentView(context) + untrustedView.bind(UntrustedAttachmentView.AttachmentType.AUDIO, VisibleMessageContentView.getTextColor(context,message)) + mainContainer.addView(untrustedView) + onContentClick = { untrustedView.showTrustDialog(message.individualRecipient) } + } + } else if (message is MmsMessageRecord && message.slideDeck.documentSlide != null) { + // Document attachment + if (contactIsTrusted || message.isOutgoing) { + val documentView = DocumentView(context) + documentView.bind(message, VisibleMessageContentView.getTextColor(context, message)) + mainContainer.addView(documentView) + } else { + val untrustedView = UntrustedAttachmentView(context) + untrustedView.bind(UntrustedAttachmentView.AttachmentType.DOCUMENT, VisibleMessageContentView.getTextColor(context,message)) + mainContainer.addView(untrustedView) + onContentClick = { untrustedView.showTrustDialog(message.individualRecipient) } + } + } else if (message is MmsMessageRecord && message.slideDeck.asAttachments().isNotEmpty()) { + // Images/Video attachment + if (contactIsTrusted || message.isOutgoing) { + val albumThumbnailView = AlbumThumbnailView(context) + mainContainer.addView(albumThumbnailView) + // 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 + albumThumbnailView.bind( + glideRequests = glide, + message = message, + isStart = isStartOfMessageCluster, + isEnd = isEndOfMessageCluster + ) + onContentClick = { event -> + albumThumbnailView.calculateHitObject(event, message, thread) + } + } else { + val untrustedView = UntrustedAttachmentView(context) + untrustedView.bind(UntrustedAttachmentView.AttachmentType.MEDIA, VisibleMessageContentView.getTextColor(context,message)) + mainContainer.addView(untrustedView) + onContentClick = { untrustedView.showTrustDialog(message.individualRecipient) } + } + } else if (message.isOpenGroupInvitation) { + val openGroupInvitationView = OpenGroupInvitationView(context) + openGroupInvitationView.bind(message, VisibleMessageContentView.getTextColor(context, message)) + mainContainer.addView(openGroupInvitationView) + onContentClick = { openGroupInvitationView.joinOpenGroup() } + } else { + val bodyTextView = VisibleMessageContentView.getBodyTextView(context, message, searchQuery) + mainContainer.addView(bodyTextView) + onContentClick = { event -> + // intersectedModalSpans should only be a list of one item + bodyTextView.getIntersectedModalSpans(event).forEach { span -> + span.onClick(bodyTextView) + } + } + } + } + + private fun getBackground(isOutgoing: Boolean, isStartOfMessageCluster: Boolean, isEndOfMessageCluster: Boolean): Drawable { + val isSingleMessage = (isStartOfMessageCluster && isEndOfMessageCluster) + @DrawableRes val backgroundID: Int + if (isSingleMessage) { + backgroundID = if (isOutgoing) R.drawable.message_bubble_background_sent_alone else R.drawable.message_bubble_background_received_alone + } else if (isStartOfMessageCluster) { + backgroundID = if (isOutgoing) R.drawable.message_bubble_background_sent_start else R.drawable.message_bubble_background_received_start + } else if (isEndOfMessageCluster) { + backgroundID = if (isOutgoing) R.drawable.message_bubble_background_sent_end else R.drawable.message_bubble_background_received_end + } else { + backgroundID = if (isOutgoing) R.drawable.message_bubble_background_sent_middle else R.drawable.message_bubble_background_received_middle + } + return ResourcesCompat.getDrawable(resources, backgroundID, context.theme)!! + } + + fun recycle() { + mainContainer.removeAllViews() + } + // endregion + + // region Convenience + companion object { + + fun getBodyTextView(context: Context, message: MessageRecord, searchQuery: String?): TextView { + val result = EmojiTextView(context) + val vPadding = context.resources.getDimension(R.dimen.small_spacing).toInt() + val hPadding = toPx(12, context.resources) + result.setPadding(hPadding, vPadding, hPadding, vPadding) + result.setTextSize(TypedValue.COMPLEX_UNIT_PX, context.resources.getDimension(R.dimen.small_font_size)) + val color = getTextColor(context, message) + result.setTextColor(color) + result.setLinkTextColor(color) + var body = message.body.toSpannable() + Linkify.addLinks(body, Linkify.WEB_URLS) + + // replace URLSpans with ModalURLSpans + body.getSpans(0, body.length).toList().forEach { urlSpan -> + val replacementSpan = ModalURLSpan(urlSpan.url) { url -> + val activity = context as AppCompatActivity + OpenURLDialog(url).show(activity.supportFragmentManager, "Open URL Dialog") + } + val start = body.getSpanStart(urlSpan) + val end = body.getSpanEnd(urlSpan) + val flags = body.getSpanFlags(urlSpan) + body.removeSpan(urlSpan) + body.setSpan(replacementSpan, start, end, flags) + } + + body = MentionUtilities.highlightMentions(body, message.isOutgoing, message.threadId, context) + body = SearchUtil.getHighlightedSpan(Locale.getDefault(), StyleFactory { BackgroundColorSpan(Color.WHITE) }, body, searchQuery) + body = SearchUtil.getHighlightedSpan(Locale.getDefault(), StyleFactory { ForegroundColorSpan(Color.BLACK) }, body, searchQuery) + + result.text = body + return result + } + + @ColorInt + fun getTextColor(context: Context, message: MessageRecord): Int { + val isDayUiMode = UiModeUtilities.isDayUiMode(context) + val colorID = if (message.isOutgoing) { + if (isDayUiMode) R.color.white else R.color.black + } else { + if (isDayUiMode) R.color.black else R.color.white + } + return context.resources.getColorWithID(colorID, context.theme) + } + } + // endregion +} + +interface VisibleMessageContentViewDelegate { + + fun scrollToMessageIfPossible(timestamp: Long) +} \ No newline at end of file diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/VisibleMessageView.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/VisibleMessageView.kt new file mode 100644 index 0000000000..0fcbf685b4 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/VisibleMessageView.kt @@ -0,0 +1,366 @@ +package org.thoughtcrime.securesms.conversation.v2.messages + +import android.content.Context +import android.content.res.Resources +import android.graphics.Canvas +import android.graphics.Rect +import android.graphics.drawable.ColorDrawable +import android.os.Build +import android.os.Handler +import android.os.Looper +import android.util.AttributeSet +import android.view.* +import android.widget.LinearLayout +import android.widget.RelativeLayout +import androidx.core.content.ContextCompat +import androidx.core.content.res.ResourcesCompat +import androidx.core.view.isVisible +import kotlinx.android.synthetic.main.view_visible_message.view.* +import network.loki.messenger.R +import org.session.libsession.messaging.contacts.Contact.ContactContext +import org.session.libsession.messaging.open_groups.OpenGroupAPIV2 +import org.session.libsession.utilities.ViewUtil +import org.session.libsignal.utilities.ThreadUtils +import org.thoughtcrime.securesms.ApplicationContext +import org.thoughtcrime.securesms.database.DatabaseFactory +import org.thoughtcrime.securesms.database.model.MessageRecord +import org.thoughtcrime.securesms.mms.GlideRequests +import org.thoughtcrime.securesms.util.* +import java.util.* +import kotlin.math.abs +import kotlin.math.min +import kotlin.math.roundToInt +import kotlin.math.sqrt + +class VisibleMessageView : LinearLayout { + private val screenWidth = Resources.getSystem().displayMetrics.widthPixels + private val swipeToReplyIcon = ContextCompat.getDrawable(context, R.drawable.ic_baseline_reply_24)!!.mutate() + private val swipeToReplyIconRect = Rect() + private var dx = 0.0f + private var previousTranslationX = 0.0f + private val gestureHandler = Handler(Looper.getMainLooper()) + private var pressCallback: Runnable? = null + private var longPressCallback: Runnable? = null + private var onDownTimestamp = 0L + private var onDoubleTap: (() -> Unit)? = null + var viewHolderIndex: Int = -1 + var snIsSelected = false + set(value) { field = value; handleIsSelectedChanged()} + var onPress: ((event: MotionEvent) -> Unit)? = null + var onSwipeToReply: (() -> Unit)? = null + var onLongPress: (() -> Unit)? = null + var contentViewDelegate: VisibleMessageContentViewDelegate? = null + + companion object { + const val swipeToReplyThreshold = 80.0f // dp + const val longPressMovementTreshold = 10.0f // dp + const val longPressDurationThreshold = 250L // ms + const val maxDoubleTapInterval = 200L + } + + // 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() { + LayoutInflater.from(context).inflate(R.layout.view_visible_message, this) + layoutParams = ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT) + isHapticFeedbackEnabled = true + setWillNotDraw(false) + expirationTimerViewContainer.disableClipping() + messageContentContainer.disableClipping() + } + // endregion + + // region Updating + fun bind(message: MessageRecord, previous: MessageRecord?, next: MessageRecord?, glide: GlideRequests, searchQuery: String?) { + val sender = message.individualRecipient + val senderSessionID = sender.address.serialize() + val threadID = message.threadId + val threadDB = DatabaseFactory.getThreadDatabase(context) + val thread = threadDB.getRecipientForThreadId(threadID) ?: return + val contactDB = DatabaseFactory.getSessionContactDatabase(context) + val contact = contactDB.getContactWithSessionID(senderSessionID) + val isGroupThread = thread.isGroupRecipient + val isStartOfMessageCluster = isStartOfMessageCluster(message, previous, isGroupThread) + val isEndOfMessageCluster = isEndOfMessageCluster(message, next, isGroupThread) + // Show profile picture and sender name if this is a group thread AND + // the message is incoming + if (isGroupThread && !message.isOutgoing) { + profilePictureContainer.visibility = if (isEndOfMessageCluster) View.VISIBLE else View.INVISIBLE + profilePictureView.publicKey = senderSessionID + profilePictureView.glide = glide + profilePictureView.update() + if (thread.isOpenGroupRecipient) { + val openGroup = DatabaseFactory.getLokiThreadDatabase(context).getOpenGroupChat(threadID) ?: return + val isModerator = OpenGroupAPIV2.isUserModerator(senderSessionID, openGroup.room, openGroup.server) + moderatorIconImageView.visibility = if (isModerator) View.VISIBLE else View.INVISIBLE + } else { + moderatorIconImageView.visibility = View.INVISIBLE + } + senderNameTextView.isVisible = isStartOfMessageCluster + val context = if (thread.isOpenGroupRecipient) ContactContext.OPEN_GROUP else ContactContext.REGULAR + senderNameTextView.text = contact?.displayName(context) ?: senderSessionID + } else { + profilePictureContainer.visibility = View.GONE + senderNameTextView.visibility = View.GONE + } + // Date break + val showDateBreak = (previous == null || !DateUtils.isSameHour(message.timestamp, previous.timestamp)) + dateBreakTextView.isVisible = showDateBreak + dateBreakTextView.text = if (showDateBreak) DateUtils.getDisplayFormattedTimeSpanString(context, Locale.getDefault(), message.timestamp) else "" + // Timestamp + messageTimestampTextView.text = DateUtils.getDisplayFormattedTimeSpanString(context, Locale.getDefault(), message.timestamp) + // Margins + val startPadding: Int + if (isGroupThread) { + startPadding = if (message.isOutgoing) resources.getDimension(R.dimen.very_large_spacing).toInt() else 0 + } else { + startPadding = if (message.isOutgoing) resources.getDimension(R.dimen.very_large_spacing).toInt() + else resources.getDimension(R.dimen.medium_spacing).toInt() + } + val endPadding = if (message.isOutgoing) resources.getDimension(R.dimen.medium_spacing).toInt() + else resources.getDimension(R.dimen.very_large_spacing).toInt() + messageContentContainer.setPaddingRelative(startPadding, 0, endPadding, 0) + // Set inter-message spacing + setMessageSpacing(isStartOfMessageCluster, isEndOfMessageCluster) + // Gravity + val gravity = if (message.isOutgoing) Gravity.END else Gravity.START + mainContainer.gravity = gravity or Gravity.BOTTOM + // Message status indicator + val (iconID, iconColor) = getMessageStatusImage(message) + if (iconID != null) { + val drawable = ContextCompat.getDrawable(context, iconID)?.mutate() + if (iconColor != null) { + drawable?.setTint(iconColor) + } + messageStatusImageView.setImageDrawable(drawable) + } + if (message.isOutgoing) { + val lastMessageID = DatabaseFactory.getMmsSmsDatabase(context).getLastMessageID(message.threadId) + messageStatusImageView.isVisible = !message.isSent || message.id == lastMessageID + } else { + messageStatusImageView.isVisible = false + } + // Expiration timer + updateExpirationTimer(message) + // Calculate max message bubble width + var maxWidth = screenWidth - startPadding - endPadding + if (profilePictureContainer.visibility != View.GONE) { maxWidth -= profilePictureContainer.width } + // Populate content view + messageContentView.viewHolderIndex = viewHolderIndex + messageContentView.bind(message, isStartOfMessageCluster, isEndOfMessageCluster, glide, maxWidth, thread, searchQuery, isGroupThread || (contact?.isTrusted ?: false)) + messageContentView.delegate = contentViewDelegate + onDoubleTap = { messageContentView.onContentDoubleTap?.invoke() } + } + + private fun setMessageSpacing(isStartOfMessageCluster: Boolean, isEndOfMessageCluster: Boolean) { + val topPadding = if (isStartOfMessageCluster) R.dimen.conversation_vertical_message_spacing_default else R.dimen.conversation_vertical_message_spacing_collapse + ViewUtil.setPaddingTop(this, resources.getDimension(topPadding).roundToInt()) + val bottomPadding = if (isEndOfMessageCluster) R.dimen.conversation_vertical_message_spacing_default else R.dimen.conversation_vertical_message_spacing_collapse + ViewUtil.setPaddingBottom(this, resources.getDimension(bottomPadding).roundToInt()) + } + + private fun isStartOfMessageCluster(current: MessageRecord, previous: MessageRecord?, isGroupThread: Boolean): Boolean { + return if (isGroupThread) { + previous == null || previous.isUpdate || !DateUtils.isSameDay(current.timestamp, previous.timestamp) + || current.recipient.address != previous.recipient.address + } else { + previous == null || previous.isUpdate || !DateUtils.isSameDay(current.timestamp, previous.timestamp) + || current.isOutgoing != previous.isOutgoing + } + } + + private fun isEndOfMessageCluster(current: MessageRecord, next: MessageRecord?, isGroupThread: Boolean): Boolean { + return if (isGroupThread) { + next == null || next.isUpdate || !DateUtils.isSameHour(current.timestamp, next.timestamp) + || current.recipient.address != next.recipient.address + } else { + next == null || next.isUpdate || !DateUtils.isSameHour(current.timestamp, next.timestamp) + || current.isOutgoing != next.isOutgoing + } + } + + private fun getMessageStatusImage(message: MessageRecord): Pair { + return when { + !message.isOutgoing -> null to null + message.isFailed -> R.drawable.ic_error to resources.getColor(R.color.destructive, context.theme) + message.isPending -> R.drawable.ic_circle_dot_dot_dot to null + message.isRead -> R.drawable.ic_filled_circle_check to null + else -> R.drawable.ic_circle_check to null + } + } + + private fun updateExpirationTimer(message: MessageRecord) { + val expirationTimerViewLayoutParams = expirationTimerView.layoutParams as RelativeLayout.LayoutParams + val ruleToAdd = if (message.isOutgoing) RelativeLayout.ALIGN_START else RelativeLayout.ALIGN_END + val ruleToRemove = if (message.isOutgoing) RelativeLayout.ALIGN_END else RelativeLayout.ALIGN_START + expirationTimerViewLayoutParams.removeRule(ruleToRemove) + expirationTimerViewLayoutParams.addRule(ruleToAdd, R.id.messageContentView) + val expirationTimerViewSize = toPx(12, resources) + val smallSpacing = resources.getDimension(R.dimen.small_spacing).roundToInt() + expirationTimerViewLayoutParams.marginStart = if (message.isOutgoing) -(smallSpacing + expirationTimerViewSize) else 0 + expirationTimerViewLayoutParams.marginEnd = if (message.isOutgoing) 0 else -(smallSpacing + expirationTimerViewSize) + expirationTimerView.layoutParams = expirationTimerViewLayoutParams + if (message.expiresIn > 0 && !message.isPending) { + expirationTimerView.setColorFilter(ResourcesCompat.getColor(resources, R.color.text, context.theme)) + expirationTimerView.isVisible = true + expirationTimerView.setPercentComplete(0.0f) + if (message.expireStarted > 0) { + expirationTimerView.setExpirationTime(message.expireStarted, message.expiresIn) + expirationTimerView.startAnimation() + if (message.expireStarted + message.expiresIn <= System.currentTimeMillis()) { + ApplicationContext.getInstance(context).expiringMessageManager.checkSchedule() + } + } else if (!message.isOutgoing && !message.isMediaPending) { + ThreadUtils.queue { + val expirationManager = ApplicationContext.getInstance(context).expiringMessageManager + val id = message.getId() + val mms = message.isMms + if (mms) DatabaseFactory.getMmsDatabase(context).markExpireStarted(id) else DatabaseFactory.getSmsDatabase(context).markExpireStarted(id) + expirationManager.scheduleDeletion(id, mms, message.expiresIn) + } + } + } else { + expirationTimerView.isVisible = false + } + } + + private fun handleIsSelectedChanged() { + background = if (snIsSelected) { + ColorDrawable(context.resources.getColorWithID(R.color.message_selected, context.theme)) + } else { + null + } + } + + override fun onDraw(canvas: Canvas) { + if (translationX < 0 && !expirationTimerView.isVisible) { + val spacing = context.resources.getDimensionPixelSize(R.dimen.small_spacing) + val threshold = VisibleMessageView.swipeToReplyThreshold + val iconSize = toPx(24, context.resources) + val bottomVOffset = paddingBottom + messageStatusImageView.height + (messageContentView.height - iconSize) / 2 + swipeToReplyIconRect.left = messageContentContainer.right - messageContentContainer.paddingEnd + spacing + swipeToReplyIconRect.top = height - bottomVOffset - iconSize + swipeToReplyIconRect.right = messageContentContainer.right - messageContentContainer.paddingEnd + iconSize + spacing + swipeToReplyIconRect.bottom = height - bottomVOffset + swipeToReplyIcon.bounds = swipeToReplyIconRect + swipeToReplyIcon.alpha = (255.0f * (min(abs(translationX), threshold) / threshold)).roundToInt() + } else { + swipeToReplyIcon.alpha = 0 + } + swipeToReplyIcon.draw(canvas) + super.onDraw(canvas) + } + + fun recycle() { + profilePictureView.recycle() + messageContentView.recycle() + } + // endregion + + // region Interaction + override fun onTouchEvent(event: MotionEvent): Boolean { + when (event.action) { + MotionEvent.ACTION_DOWN -> onDown(event) + MotionEvent.ACTION_MOVE -> onMove(event) + MotionEvent.ACTION_CANCEL -> onCancel(event) + MotionEvent.ACTION_UP -> onUp(event) + } + return true + } + + private fun onDown(event: MotionEvent) { + dx = x - event.rawX + longPressCallback?.let { gestureHandler.removeCallbacks(it) } + val newLongPressCallback = Runnable { onLongPress() } + this.longPressCallback = newLongPressCallback + gestureHandler.postDelayed(newLongPressCallback, VisibleMessageView.longPressDurationThreshold) + onDownTimestamp = Date().time + } + + private fun onMove(event: MotionEvent) { + val translationX = toDp(event.rawX + dx, context.resources) + if (abs(translationX) < VisibleMessageView.longPressMovementTreshold || snIsSelected) { + return + } else { + longPressCallback?.let { gestureHandler.removeCallbacks(it) } + } + if (translationX > 0) { return } // Only allow swipes to the left + // The idea here is to asymptotically approach a maximum drag distance + val damping = 50.0f + val sign = -1.0f + val x = (damping * (sqrt(abs(translationX)) / sqrt(damping))) * sign + this.translationX = x + this.dateBreakTextView.translationX = -x // Bit of a hack to keep the date break text view from moving + postInvalidate() // Ensure onDraw(canvas:) is called + if (abs(x) > VisibleMessageView.swipeToReplyThreshold && abs(previousTranslationX) < VisibleMessageView.swipeToReplyThreshold) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + performHapticFeedback(HapticFeedbackConstants.CONTEXT_CLICK) + } else { + performHapticFeedback(HapticFeedbackConstants.LONG_PRESS) + } + } + previousTranslationX = x + } + + private fun onCancel(event: MotionEvent) { + if (abs(translationX) > VisibleMessageView.swipeToReplyThreshold) { + onSwipeToReply?.invoke() + } + longPressCallback?.let { gestureHandler.removeCallbacks(it) } + resetPosition() + } + + private fun onUp(event: MotionEvent) { + if (abs(translationX) > VisibleMessageView.swipeToReplyThreshold) { + onSwipeToReply?.invoke() + } else if ((Date().time - onDownTimestamp) < VisibleMessageView.longPressDurationThreshold) { + longPressCallback?.let { gestureHandler.removeCallbacks(it) } + val pressCallback = this.pressCallback + if (pressCallback != null) { + // If we're here and pressCallback isn't null, it means that we tapped again within + // maxDoubleTapInterval ms and we should count this as a double tap + gestureHandler.removeCallbacks(pressCallback) + this.pressCallback = null + onDoubleTap?.invoke() + } else { + val newPressCallback = Runnable { onPress(event) } + this.pressCallback = newPressCallback + gestureHandler.postDelayed(newPressCallback, VisibleMessageView.maxDoubleTapInterval) + } + } + resetPosition() + } + + private fun resetPosition() { + animate() + .translationX(0.0f) + .setDuration(150) + .setUpdateListener { + postInvalidate() // Ensure onDraw(canvas:) is called + } + .start() + // Bit of a hack to keep the date break text view from moving + dateBreakTextView.animate() + .translationX(0.0f) + .setDuration(150) + .start() + } + + private fun onLongPress() { + performHapticFeedback(HapticFeedbackConstants.LONG_PRESS) + onLongPress?.invoke() + } + + fun onContentClick(event: MotionEvent) { + messageContentView.onContentClick?.invoke(event) + } + + private fun onPress(event: MotionEvent) { + onPress?.invoke(event) + pressCallback = null + } + // endregion +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/VoiceMessageView.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/VoiceMessageView.kt new file mode 100644 index 0000000000..6498a1a472 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/VoiceMessageView.kt @@ -0,0 +1,142 @@ +package org.thoughtcrime.securesms.conversation.v2.messages + +import android.content.Context +import android.graphics.Canvas +import android.util.AttributeSet +import android.util.Log +import android.view.LayoutInflater +import android.view.View +import android.widget.LinearLayout +import android.widget.RelativeLayout +import androidx.core.view.isVisible +import kotlinx.android.synthetic.main.view_voice_message.view.* +import network.loki.messenger.R +import org.session.libsession.messaging.sending_receiving.attachments.DatabaseAttachment +import org.thoughtcrime.securesms.audio.AudioSlidePlayer +import org.thoughtcrime.securesms.components.CornerMask +import org.thoughtcrime.securesms.conversation.v2.utilities.MessageBubbleUtilities +import org.thoughtcrime.securesms.database.DatabaseFactory +import org.thoughtcrime.securesms.database.model.MmsMessageRecord +import java.util.concurrent.TimeUnit +import kotlin.math.roundToInt +import kotlin.math.roundToLong + +class VoiceMessageView : LinearLayout, AudioSlidePlayer.Listener { + private val cornerMask by lazy { CornerMask(this) } + private var isPlaying = false + set(value) { + field = value + renderIcon() + } + private var progress = 0.0 + private var duration = 0L + private var player: AudioSlidePlayer? = null + var delegate: VoiceMessageViewDelegate? = null + var index = -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() { + LayoutInflater.from(context).inflate(R.layout.view_voice_message, this) + voiceMessageViewDurationTextView.text = String.format("%01d:%02d", + TimeUnit.MILLISECONDS.toMinutes(0), + TimeUnit.MILLISECONDS.toSeconds(0)) + } + // endregion + + // region Updating + fun bind(message: MmsMessageRecord, isStartOfMessageCluster: Boolean, isEndOfMessageCluster: Boolean) { + val audio = message.slideDeck.audioSlide!! + voiceMessageViewLoader.isVisible = audio.isInProgress + val cornerRadii = MessageBubbleUtilities.calculateRadii(context, isStartOfMessageCluster, isEndOfMessageCluster, message.isOutgoing) + cornerMask.setTopLeftRadius(cornerRadii[0]) + cornerMask.setTopRightRadius(cornerRadii[1]) + cornerMask.setBottomRightRadius(cornerRadii[2]) + cornerMask.setBottomLeftRadius(cornerRadii[3]) + + // only process audio if downloaded + if (audio.isPendingDownload || audio.isInProgress) { + this.player = null + return + } + + val player = AudioSlidePlayer.createFor(context, audio, this) + this.player = player + + (audio.asAttachment() as? DatabaseAttachment)?.let { attachment -> + DatabaseFactory.getAttachmentDatabase(context).getAttachmentAudioExtras(attachment.attachmentId)?.let { audioExtras -> + if (audioExtras.durationMs > 0) { + duration = audioExtras.durationMs + voiceMessageViewDurationTextView.visibility = View.VISIBLE + voiceMessageViewDurationTextView.text = String.format("%01d:%02d", + TimeUnit.MILLISECONDS.toMinutes(audioExtras.durationMs), + TimeUnit.MILLISECONDS.toSeconds(audioExtras.durationMs)) + } + } + } + } + + override fun onPlayerStart(player: AudioSlidePlayer) {} + + override fun onPlayerProgress(player: AudioSlidePlayer, progress: Double, unused: Long) { + if (progress == 1.0) { + togglePlayback() + handleProgressChanged(0.0) + delegate?.playNextAudioIfPossible(index) + } else { + handleProgressChanged(progress) + } + } + + private fun handleProgressChanged(progress: Double) { + this.progress = progress + voiceMessageViewDurationTextView.text = String.format("%01d:%02d", + TimeUnit.MILLISECONDS.toMinutes(duration - (progress * duration.toDouble()).roundToLong()), + TimeUnit.MILLISECONDS.toSeconds(duration - (progress * duration.toDouble()).roundToLong())) + val layoutParams = progressView.layoutParams as RelativeLayout.LayoutParams + layoutParams.width = (width.toFloat() * progress.toFloat()).roundToInt() + progressView.layoutParams = layoutParams + } + + override fun onPlayerStop(player: AudioSlidePlayer) { + Log.d("Loki", "Player stopped") + isPlaying = false + } + + override fun dispatchDraw(canvas: Canvas) { + super.dispatchDraw(canvas) + cornerMask.mask(canvas) + } + + private fun renderIcon() { + val iconID = if (isPlaying) R.drawable.exo_icon_pause else R.drawable.exo_icon_play + voiceMessagePlaybackImageView.setImageResource(iconID) + } + + // endregion + + // region Interaction + fun togglePlayback() { + val player = this.player ?: return + isPlaying = !isPlaying + if (isPlaying) { + player.play(progress) + } else { + player.stop() + } + } + + fun handleDoubleTap() { + val player = this.player ?: return + player.playbackSpeed = if (player.playbackSpeed == 1.0f) 1.5f else 1.0f + } + // endregion +} + +interface VoiceMessageViewDelegate { + + fun playNextAudioIfPossible(current: Int) +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/search/SearchBottomBar.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/search/SearchBottomBar.kt new file mode 100644 index 0000000000..da8df0045a --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/search/SearchBottomBar.kt @@ -0,0 +1,62 @@ +package org.thoughtcrime.securesms.conversation.v2.search + +import android.content.Context +import android.util.AttributeSet +import android.view.LayoutInflater +import android.view.View +import android.widget.LinearLayout +import kotlinx.android.synthetic.main.view_search_bottom_bar.view.* +import network.loki.messenger.R + + +class SearchBottomBar : LinearLayout { + private var eventListener: EventListener? = 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() } + + fun initialize() { + LayoutInflater.from(context).inflate(R.layout.view_search_bottom_bar, this) + } + + fun setData(position: Int, count: Int) { + searchProgressWheel.visibility = GONE + searchUp.setOnClickListener { v: View? -> + if (eventListener != null) { + eventListener!!.onSearchMoveUpPressed() + } + } + searchDown.setOnClickListener { v: View? -> + if (eventListener != null) { + eventListener!!.onSearchMoveDownPressed() + } + } + if (count > 0) { + searchPosition.text = resources.getString(R.string.ConversationActivity_search_position, position + 1, count) + } else { + searchPosition.text = "" + } + setViewEnabled(searchUp, position < count - 1) + setViewEnabled(searchDown, position > 0) + } + + fun showLoading() { + searchProgressWheel.visibility = VISIBLE + } + + private fun setViewEnabled(view: View, enabled: Boolean) { + view.isEnabled = enabled + view.alpha = if (enabled) 1f else 0.25f + } + + fun setEventListener(eventListener: EventListener?) { + this.eventListener = eventListener + } + + interface EventListener { + fun onSearchMoveUpPressed() + fun onSearchMoveDownPressed() + } +} \ No newline at end of file diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/search/SearchViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/search/SearchViewModel.kt new file mode 100644 index 0000000000..eb3dd50d98 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/search/SearchViewModel.kt @@ -0,0 +1,111 @@ +package org.thoughtcrime.securesms.conversation.v2.search + +import android.app.Application +import androidx.lifecycle.AndroidViewModel +import androidx.lifecycle.LiveData +import org.session.libsession.utilities.Debouncer +import org.session.libsession.utilities.Util.runOnMain +import org.session.libsession.utilities.concurrent.SignalExecutors +import org.thoughtcrime.securesms.contacts.ContactAccessor +import org.thoughtcrime.securesms.database.CursorList +import org.thoughtcrime.securesms.database.DatabaseFactory +import org.thoughtcrime.securesms.search.SearchRepository +import org.thoughtcrime.securesms.search.model.MessageResult +import org.thoughtcrime.securesms.util.CloseableLiveData +import java.io.Closeable + + +class SearchViewModel(application: Application) : AndroidViewModel(application) { + private val searchRepository: SearchRepository + private val result: CloseableLiveData + private val debouncer: Debouncer + private var firstSearch = false + private var searchOpen = false + private var activeQuery: String? = null + private var activeThreadId: Long = 0 + val searchResults: LiveData + get() = result + + fun onQueryUpdated(query: String, threadId: Long) { + if (query == activeQuery) { + return + } + updateQuery(query, threadId) + } + + fun onMissingResult() { + if (activeQuery != null) { + updateQuery(activeQuery!!, activeThreadId) + } + } + + fun onMoveUp() { + debouncer.clear() + val messages = result.value!!.getResults() as CursorList + val position = Math.min(result.value!!.position + 1, messages.size - 1) + result.setValue(SearchResult(messages, position), false) + } + + fun onMoveDown() { + debouncer.clear() + val messages = result.value!!.getResults() as CursorList + val position = Math.max(result.value!!.position - 1, 0) + result.setValue(SearchResult(messages, position), false) + } + + fun onSearchOpened() { + searchOpen = true + firstSearch = true + } + + fun onSearchClosed() { + searchOpen = false + activeQuery = null + debouncer.clear() + result.close() + } + + override fun onCleared() { + super.onCleared() + result.close() + } + + private fun updateQuery(query: String, threadId: Long) { + activeQuery = query + activeThreadId = threadId + debouncer.publish { + firstSearch = false + searchRepository.query(query, threadId) { messages: CursorList -> + runOnMain { + if (searchOpen && query == activeQuery) { + result.setValue(SearchResult(messages, 0)) + } else { + messages.close() + } + } + } + } + } + + class SearchResult(private val results: CursorList, val position: Int) : Closeable { + + fun getResults(): List { + return results + } + + override fun close() { + results.close() + } + } + + init { + val context = application.applicationContext + result = CloseableLiveData() + debouncer = Debouncer(500) + searchRepository = SearchRepository(context, + DatabaseFactory.getSearchDatabase(context), + DatabaseFactory.getThreadDatabase(context), + ContactAccessor.getInstance(), + SignalExecutors.SERIAL) + } +} \ No newline at end of file diff --git a/app/src/main/java/org/thoughtcrime/securesms/mms/AttachmentManager.java b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/utilities/AttachmentManager.java similarity index 58% rename from app/src/main/java/org/thoughtcrime/securesms/mms/AttachmentManager.java rename to app/src/main/java/org/thoughtcrime/securesms/conversation/v2/utilities/AttachmentManager.java index dcac22909c..a5298305a0 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/mms/AttachmentManager.java +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/utilities/AttachmentManager.java @@ -14,7 +14,7 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -package org.thoughtcrime.securesms.mms; +package org.thoughtcrime.securesms.conversation.v2.utilities; import android.Manifest; import android.annotation.SuppressLint; @@ -23,29 +23,31 @@ import android.content.ActivityNotFoundException; import android.content.Context; import android.content.Intent; import android.database.Cursor; -import android.graphics.PorterDuff; import android.net.Uri; import android.os.AsyncTask; -import android.provider.ContactsContract; import android.provider.MediaStore; import android.provider.OpenableColumns; import android.text.TextUtils; import android.util.Pair; -import android.view.View; import android.widget.Toast; import androidx.annotation.NonNull; import androidx.annotation.Nullable; -import org.thoughtcrime.securesms.MediaPreviewActivity; -import org.thoughtcrime.securesms.loki.views.MessageAudioView; -import org.thoughtcrime.securesms.components.DocumentView; -import org.thoughtcrime.securesms.components.RemovableEditableMediaView; -import org.thoughtcrime.securesms.components.ThumbnailView; import org.session.libsignal.utilities.NoExternalStorageException; import org.thoughtcrime.securesms.giph.ui.GiphyActivity; import org.session.libsignal.utilities.Log; import org.thoughtcrime.securesms.mediasend.MediaSendActivity; +import org.thoughtcrime.securesms.mms.AudioSlide; +import org.thoughtcrime.securesms.mms.DocumentSlide; +import org.thoughtcrime.securesms.mms.GifSlide; +import org.thoughtcrime.securesms.mms.GlideRequests; +import org.thoughtcrime.securesms.mms.ImageSlide; +import org.thoughtcrime.securesms.mms.MediaConstraints; +import org.thoughtcrime.securesms.mms.PartAuthority; +import org.thoughtcrime.securesms.mms.Slide; +import org.thoughtcrime.securesms.mms.SlideDeck; +import org.thoughtcrime.securesms.mms.VideoSlide; import org.thoughtcrime.securesms.permissions.Permissions; import org.thoughtcrime.securesms.providers.BlobProvider; import org.session.libsignal.utilities.ExternalStorageUtil; @@ -53,13 +55,8 @@ import org.thoughtcrime.securesms.util.FileProviderUtil; import org.thoughtcrime.securesms.util.MediaUtil; import org.session.libsignal.utilities.guava.Optional; -import org.session.libsession.messaging.sending_receiving.attachments.Attachment; import org.session.libsession.utilities.recipients.Recipient; -import org.session.libsession.utilities.ThemeUtil; -import org.session.libsession.utilities.ViewUtil; -import org.session.libsession.utilities.Stub; import org.session.libsignal.utilities.ListenableFuture; -import org.session.libsignal.utilities.ListenableFuture.Listener; import org.session.libsignal.utilities.SettableFuture; import java.io.File; @@ -67,26 +64,18 @@ import java.io.IOException; import java.util.Iterator; import java.util.LinkedList; import java.util.List; -import java.util.concurrent.ExecutionException; import network.loki.messenger.R; import static android.provider.MediaStore.EXTRA_OUTPUT; - public class AttachmentManager { private final static String TAG = AttachmentManager.class.getSimpleName(); private final @NonNull Context context; - private final @NonNull Stub attachmentViewStub; private final @NonNull AttachmentListener attachmentListener; - private RemovableEditableMediaView removableMediaView; - private ThumbnailView thumbnail; - private MessageAudioView audioView; - private DocumentView documentView; - private @NonNull List garbage = new LinkedList<>(); private @NonNull Optional slide = Optional.absent(); private @Nullable Uri captureUri; @@ -94,51 +83,12 @@ public class AttachmentManager { public AttachmentManager(@NonNull Activity activity, @NonNull AttachmentListener listener) { this.context = activity; this.attachmentListener = listener; - this.attachmentViewStub = ViewUtil.findStubById(activity, R.id.attachment_editor_stub); } - private void inflateStub() { - if (!attachmentViewStub.resolved()) { - View root = attachmentViewStub.get(); - - this.thumbnail = ViewUtil.findById(root, R.id.attachment_thumbnail); - this.audioView = ViewUtil.findById(root, R.id.attachment_audio); - this.documentView = ViewUtil.findById(root, R.id.attachment_document); - this.removableMediaView = ViewUtil.findById(root, R.id.removable_media_view); - - removableMediaView.setRemoveClickListener(new RemoveButtonListener()); - thumbnail.setOnClickListener(new ThumbnailClickListener()); - documentView.getBackground().setColorFilter(ThemeUtil.getThemedColor(context, R.attr.conversation_item_bubble_background), PorterDuff.Mode.MULTIPLY); - } - } - - public void clear(@NonNull GlideRequests glideRequests, boolean animate) { - if (attachmentViewStub.resolved()) { - - if (animate) { - ViewUtil.fadeOut(attachmentViewStub.get(), 200).addListener(new Listener() { - @Override - public void onSuccess(Boolean result) { - thumbnail.clear(glideRequests); - attachmentViewStub.get().setVisibility(View.GONE); - attachmentListener.onAttachmentChanged(); - } - - @Override - public void onFailure(ExecutionException e) { - } - }); - } else { - thumbnail.clear(glideRequests); - attachmentViewStub.get().setVisibility(View.GONE); - attachmentListener.onAttachmentChanged(); - } - - markGarbage(getSlideUri()); - slide = Optional.absent(); - - audioView.cleanup(); - } + public void clear() { + markGarbage(getSlideUri()); + slide = Optional.absent(); + attachmentListener.onAttachmentChanged(); } public void cleanup() { @@ -190,16 +140,12 @@ public class AttachmentManager { final int width, final int height) { - inflateStub(); - final SettableFuture result = new SettableFuture<>(); new AsyncTask() { @Override protected void onPreExecute() { - thumbnail.clear(glideRequests); - thumbnail.showProgressSpinner(); - attachmentViewStub.get().setVisibility(View.VISIBLE); + } @Override @@ -222,35 +168,12 @@ public class AttachmentManager { @Override protected void onPostExecute(@Nullable final Slide slide) { if (slide == null) { - attachmentViewStub.get().setVisibility(View.GONE); - Toast.makeText(context, - R.string.ConversationActivity_sorry_there_was_an_error_setting_your_attachment, - Toast.LENGTH_SHORT).show(); result.set(false); } else if (!areConstraintsSatisfied(context, slide, constraints)) { - attachmentViewStub.get().setVisibility(View.GONE); - Toast.makeText(context, - R.string.ConversationActivity_attachment_exceeds_size_limits, - Toast.LENGTH_SHORT).show(); result.set(false); } else { setSlide(slide); - attachmentViewStub.get().setVisibility(View.VISIBLE); - - if (slide.hasAudio()) { - audioView.setAudio((AudioSlide) slide, false); - removableMediaView.display(audioView, false); - result.set(true); - } else if (slide.hasDocument()) { - documentView.setDocument((DocumentSlide) slide, false); - removableMediaView.display(documentView, false); - result.set(true); - } else { - Attachment attachment = slide.asAttachment(); - result.deferTo(thumbnail.setImageResource(glideRequests, slide, false, true, attachment.getWidth(), attachment.getHeight())); - removableMediaView.display(thumbnail, mediaType == MediaType.IMAGE); - } - + result.set(true); attachmentListener.onAttachmentChanged(); } } @@ -317,11 +240,8 @@ public class AttachmentManager { return result; } - public boolean isAttachmentPresent() { - return attachmentViewStub.resolved() && attachmentViewStub.get().getVisibility() == View.VISIBLE; - } - - public @NonNull SlideDeck buildSlideDeck() { + public @NonNull + SlideDeck buildSlideDeck() { SlideDeck deck = new SlideDeck(); if (slide.isPresent()) deck.addSlide(slide.get()); return deck; @@ -333,43 +253,16 @@ public class AttachmentManager { public static void selectGallery(Activity activity, int requestCode, @NonNull Recipient recipient, @NonNull String body) { Permissions.with(activity) - .request(Manifest.permission.READ_EXTERNAL_STORAGE) - .withPermanentDenialDialog(activity.getString(R.string.AttachmentManager_signal_requires_the_external_storage_permission_in_order_to_attach_photos_videos_or_audio)) - .onAllGranted(() -> activity.startActivityForResult(MediaSendActivity.buildGalleryIntent(activity, recipient, body), requestCode)) - .execute(); + .request(Manifest.permission.READ_EXTERNAL_STORAGE) + .withPermanentDenialDialog(activity.getString(R.string.AttachmentManager_signal_requires_the_external_storage_permission_in_order_to_attach_photos_videos_or_audio)) + .onAllGranted(() -> activity.startActivityForResult(MediaSendActivity.buildGalleryIntent(activity, recipient, body), requestCode)) + .execute(); } public static void selectAudio(Activity activity, int requestCode) { selectMediaType(activity, "audio/*", null, requestCode); } - public static void selectContactInfo(Activity activity, int requestCode) { - Permissions.with(activity) - .request(Manifest.permission.WRITE_CONTACTS) - .withPermanentDenialDialog(activity.getString(R.string.AttachmentManager_signal_requires_contacts_permission_in_order_to_attach_contact_information)) - .onAllGranted(() -> { - Intent intent = new Intent(Intent.ACTION_PICK, ContactsContract.Contacts.CONTENT_URI); - activity.startActivityForResult(intent, requestCode); - }) - .execute(); - } - - public static void selectLocation(Activity activity, int requestCode) { - /* Loki - Enable again once we have location sharing - Permissions.with(activity) - .request(Manifest.permission.ACCESS_FINE_LOCATION, Manifest.permission.ACCESS_COARSE_LOCATION) - .withPermanentDenialDialog(activity.getString(R.string.AttachmentManager_signal_requires_location_information_in_order_to_attach_a_location)) - .onAllGranted(() -> { - try { - activity.startActivityForResult(new PlacePicker.IntentBuilder().build(activity), requestCode); - } catch (GooglePlayServicesRepairableException | GooglePlayServicesNotAvailableException e) { - Log.w(TAG, e); - } - }) - .execute(); - */ - } - public static void selectGif(Activity activity, int requestCode) { Intent intent = new Intent(activity, GiphyActivity.class); intent.putExtra(GiphyActivity.EXTRA_IS_MMS, false); @@ -386,28 +279,25 @@ public class AttachmentManager { public void capturePhoto(Activity activity, int requestCode) { Permissions.with(activity) - .request(Manifest.permission.CAMERA) - .withPermanentDenialDialog(activity.getString(R.string.AttachmentManager_signal_requires_the_camera_permission_in_order_to_take_photos_but_it_has_been_permanently_denied)) - .onAllGranted(() -> { - try { - File captureFile = File.createTempFile( - "conversation-capture", - ".jpg", - ExternalStorageUtil.getImageDir(activity)); - Uri captureUri = FileProviderUtil.getUriFor(context, captureFile); - Intent captureIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE); - captureIntent.putExtra(EXTRA_OUTPUT, captureUri); - captureIntent.setFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION); - if (captureIntent.resolveActivity(activity.getPackageManager()) != null) { - Log.d(TAG, "captureUri path is " + captureUri.getPath()); - this.captureUri = captureUri; - activity.startActivityForResult(captureIntent, requestCode); - } - } catch (IOException | NoExternalStorageException e) { - throw new RuntimeException("Error creating image capture intent.", e); - } - }) - .execute(); + .request(Manifest.permission.CAMERA) + .withPermanentDenialDialog(activity.getString(R.string.AttachmentManager_signal_requires_the_camera_permission_in_order_to_take_photos_but_it_has_been_permanently_denied)) + .onAllGranted(() -> { + try { + File captureFile = File.createTempFile("conversation-capture", ".jpg", ExternalStorageUtil.getImageDir(activity)); + Uri captureUri = FileProviderUtil.getUriFor(context, captureFile); + Intent captureIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE); + captureIntent.putExtra(EXTRA_OUTPUT, captureUri); + captureIntent.setFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION); + if (captureIntent.resolveActivity(activity.getPackageManager()) != null) { + Log.d(TAG, "captureUri path is " + captureUri.getPath()); + this.captureUri = captureUri; + activity.startActivityForResult(captureIntent, requestCode); + } + } catch (IOException | NoExternalStorageException e) { + throw new RuntimeException("Error creating image capture intent.", e); + } + }) + .execute(); } private static void selectMediaType(Activity activity, @NonNull String type, @Nullable String[] extraMimeType, int requestCode) { @@ -445,34 +335,6 @@ public class AttachmentManager { constraints.canResize(slide.asAttachment()); } - private void previewImageDraft(final @NonNull Slide slide) { - if (MediaPreviewActivity.isContentTypeSupported(slide.getContentType()) && slide.getUri() != null) { - Intent intent = new Intent(context, MediaPreviewActivity.class); - intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); - intent.putExtra(MediaPreviewActivity.SIZE_EXTRA, slide.asAttachment().getSize()); - intent.putExtra(MediaPreviewActivity.CAPTION_EXTRA, slide.getCaption().orNull()); - intent.putExtra(MediaPreviewActivity.OUTGOING_EXTRA, true); - intent.setDataAndType(slide.getUri(), slide.getContentType()); - - context.startActivity(intent); - } - } - - private class ThumbnailClickListener implements View.OnClickListener { - @Override - public void onClick(View v) { - if (slide.isPresent()) previewImageDraft(slide.get()); - } - } - - private class RemoveButtonListener implements View.OnClickListener { - @Override - public void onClick(View v) { - cleanup(); - clear(GlideApp.with(context.getApplicationContext()), true); - } - } - public interface AttachmentListener { void onAttachmentChanged(); } @@ -513,6 +375,5 @@ public class AttachmentManager { return DOCUMENT; } - } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/utilities/BaseDialog.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/utilities/BaseDialog.kt new file mode 100644 index 0000000000..e1456a7f94 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/utilities/BaseDialog.kt @@ -0,0 +1,26 @@ +package org.thoughtcrime.securesms.conversation.v2.utilities + +import android.app.Dialog +import android.graphics.Color +import android.graphics.drawable.ColorDrawable +import android.os.Bundle +import androidx.appcompat.app.AlertDialog +import androidx.fragment.app.DialogFragment +import org.thoughtcrime.securesms.util.UiModeUtilities + +open class BaseDialog : DialogFragment() { + + override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { + val builder = AlertDialog.Builder(requireContext()) + setContentView(builder) + val result = builder.create() + result.window?.setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT)) + val isLightMode = UiModeUtilities.isDayUiMode(requireContext()) + result.window?.setDimAmount(if (isLightMode) 0.1f else 0.75f) + return result + } + + open fun setContentView(builder: AlertDialog.Builder) { + // To be overridden by subclasses + } +} \ No newline at end of file diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/utilities/KThumbnailView.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/utilities/KThumbnailView.kt new file mode 100644 index 0000000000..80b233c42d --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/utilities/KThumbnailView.kt @@ -0,0 +1,196 @@ +package org.thoughtcrime.securesms.conversation.v2.utilities + +import android.content.Context +import android.graphics.Bitmap +import android.graphics.drawable.Drawable +import android.net.Uri +import android.util.AttributeSet +import android.view.View +import android.widget.FrameLayout +import androidx.core.view.isVisible +import com.bumptech.glide.load.engine.DiskCacheStrategy +import com.bumptech.glide.load.resource.bitmap.CenterCrop +import com.bumptech.glide.load.resource.bitmap.RoundedCorners +import com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions +import com.bumptech.glide.request.RequestOptions +import kotlinx.android.synthetic.main.thumbnail_view.view.* +import network.loki.messenger.R +import org.session.libsession.messaging.sending_receiving.attachments.AttachmentTransferProgress +import org.session.libsession.utilities.Util.equals +import org.session.libsignal.utilities.ListenableFuture +import org.session.libsignal.utilities.SettableFuture +import org.thoughtcrime.securesms.components.GlideBitmapListeningTarget +import org.thoughtcrime.securesms.components.GlideDrawableListeningTarget +import org.thoughtcrime.securesms.database.model.MmsMessageRecord +import org.thoughtcrime.securesms.mms.* +import org.thoughtcrime.securesms.mms.DecryptableStreamUriLoader.DecryptableUri + +open class KThumbnailView: FrameLayout { + + companion object { + private const val WIDTH = 0 + private const val HEIGHT = 1 + } + + // 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 { thumbnail_image } + private val playOverlay by lazy { play_overlay } + val loadIndicator: View by lazy { thumbnail_load_indicator } + val downloadIndicator: View by lazy { thumbnail_download_icon } + + private val dimensDelegate = ThumbnailDimensDelegate() + + private var slide: Slide? = null + private var radius: Int = 0 + + private fun initialize(attrs: AttributeSet?) { + inflate(context, R.layout.thumbnail_view, this) + if (attrs != null) { + val typedArray = context.theme.obtainStyledAttributes(attrs, R.styleable.ThumbnailView, 0, 0) + + dimensDelegate.setBounds(typedArray.getDimensionPixelSize(R.styleable.ThumbnailView_minWidth, 0), + typedArray.getDimensionPixelSize(R.styleable.ThumbnailView_minHeight, 0), + typedArray.getDimensionPixelSize(R.styleable.ThumbnailView_maxWidth, 0), + typedArray.getDimensionPixelSize(R.styleable.ThumbnailView_maxHeight, 0)) + + radius = typedArray.getDimensionPixelSize(R.styleable.ThumbnailView_thumbnail_radius, 0) + + typedArray.recycle() + } + } + + override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { + val adjustedDimens = dimensDelegate.resourceSize() + if (adjustedDimens[WIDTH] == 0 && adjustedDimens[HEIGHT] == 0) { + return super.onMeasure(widthMeasureSpec, heightMeasureSpec) + } + + val finalWidth: Int = adjustedDimens[WIDTH] + paddingLeft + paddingRight + val finalHeight: Int = adjustedDimens[HEIGHT] + paddingTop + paddingBottom + + super.onMeasure( + MeasureSpec.makeMeasureSpec(finalWidth, MeasureSpec.EXACTLY), + MeasureSpec.makeMeasureSpec(finalHeight, MeasureSpec.EXACTLY) + ) + } + + private fun getDefaultWidth() = maxOf(layoutParams?.width ?: 0, 0) + private fun getDefaultHeight() = maxOf(layoutParams?.height ?: 0, 0) + // endregion + + // region Interaction + fun setImageResource(glide: GlideRequests, slide: Slide, isPreview: Boolean, mms: MmsMessageRecord): ListenableFuture { + return setImageResource(glide, slide, isPreview, 0, 0, mms) + } + + fun setImageResource(glide: GlideRequests, slide: Slide, + isPreview: Boolean, naturalWidth: Int, + naturalHeight: Int, mms: MmsMessageRecord): ListenableFuture { + + val currentSlide = this.slide + + playOverlay.isVisible = (slide.thumbnailUri != null && slide.hasPlayOverlay() && + (slide.transferState == AttachmentTransferProgress.TRANSFER_PROGRESS_DONE || isPreview)) + + if (equals(currentSlide, slide)) { + // don't re-load slide + return SettableFuture(false) + } + + + if (currentSlide != null && currentSlide.fastPreflightId != null && currentSlide.fastPreflightId == slide.fastPreflightId) { + // not reloading slide for fast preflight + this.slide = slide + } + + this.slide = slide + + loadIndicator.isVisible = slide.isInProgress + downloadIndicator.isVisible = slide.transferState == AttachmentTransferProgress.TRANSFER_PROGRESS_FAILED + + dimensDelegate.setDimens(naturalWidth, naturalHeight) + invalidate() + + val result = SettableFuture() + + when { + slide.thumbnailUri != null -> { + buildThumbnailGlideRequest(glide, slide).into(GlideDrawableListeningTarget(image, result)) + } + slide.hasPlaceholder() -> { + buildPlaceholderGlideRequest(glide, slide).into(GlideBitmapListeningTarget(image, result)) + } + else -> { + glide.clear(image) + result.set(false) + } + } + return result + } + + fun buildThumbnailGlideRequest(glide: GlideRequests, slide: Slide): GlideRequest { + + val dimens = dimensDelegate.resourceSize() + + val request = glide.load(DecryptableUri(slide.thumbnailUri!!)) + .diskCacheStrategy(DiskCacheStrategy.RESOURCE) + .let { request -> + if (dimens[WIDTH] == 0 || dimens[HEIGHT] == 0) { + request.override(getDefaultWidth(), getDefaultHeight()) + } else { + request.override(dimens[WIDTH], dimens[HEIGHT]) + } + } + .transition(DrawableTransitionOptions.withCrossFade()) + .centerCrop() + + return if (slide.isInProgress) request else request.apply(RequestOptions.errorOf(R.drawable.ic_missing_thumbnail_picture)) + } + + fun buildPlaceholderGlideRequest(glide: GlideRequests, slide: Slide): GlideRequest { + + val dimens = dimensDelegate.resourceSize() + + return glide.asBitmap() + .load(slide.getPlaceholderRes(context.theme)) + .diskCacheStrategy(DiskCacheStrategy.NONE) + .let { request -> + if (dimens[WIDTH] == 0 || dimens[HEIGHT] == 0) { + request.override(getDefaultWidth(), getDefaultHeight()) + } else { + request.override(dimens[WIDTH], dimens[HEIGHT]) + } + } + .fitCenter() + } + + open fun clear(glideRequests: GlideRequests) { + glideRequests.clear(image) + slide = null + } + + fun setImageResource(glideRequests: GlideRequests, uri: Uri): ListenableFuture { + val future = SettableFuture() + + var request: GlideRequest = glideRequests.load(DecryptableUri(uri)) + .diskCacheStrategy(DiskCacheStrategy.NONE) + .transition(DrawableTransitionOptions.withCrossFade()) + + request = if (radius > 0) { + request.transforms(CenterCrop(), RoundedCorners(radius)) + } else { + request.transforms(CenterCrop()) + } + + request.into(GlideDrawableListeningTarget(image, future)) + + return future + } + + // endregion + +} \ No newline at end of file diff --git a/app/src/main/java/org/thoughtcrime/securesms/loki/utilities/MentionManagerUtilities.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/utilities/MentionManagerUtilities.kt similarity index 96% rename from app/src/main/java/org/thoughtcrime/securesms/loki/utilities/MentionManagerUtilities.kt rename to app/src/main/java/org/thoughtcrime/securesms/conversation/v2/utilities/MentionManagerUtilities.kt index 000b61db89..31d650a671 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/loki/utilities/MentionManagerUtilities.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/utilities/MentionManagerUtilities.kt @@ -1,4 +1,4 @@ -package org.thoughtcrime.securesms.loki.utilities +package org.thoughtcrime.securesms.conversation.v2.utilities import android.content.Context import org.session.libsession.utilities.TextSecurePreferences diff --git a/app/src/main/java/org/thoughtcrime/securesms/loki/utilities/MentionUtilities.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/utilities/MentionUtilities.kt similarity index 76% rename from app/src/main/java/org/thoughtcrime/securesms/loki/utilities/MentionUtilities.kt rename to app/src/main/java/org/thoughtcrime/securesms/conversation/v2/utilities/MentionUtilities.kt index 45fda301d8..3a0c6e7c15 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/loki/utilities/MentionUtilities.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/utilities/MentionUtilities.kt @@ -1,4 +1,4 @@ -package org.thoughtcrime.securesms.loki.utilities +package org.thoughtcrime.securesms.conversation.v2.utilities import android.content.Context import android.graphics.Typeface @@ -7,11 +7,13 @@ import android.text.SpannableString import android.text.style.ForegroundColorSpan import android.text.style.StyleSpan import android.util.Range +import androidx.core.content.res.ResourcesCompat import network.loki.messenger.R import nl.komponents.kovenant.combine.Tuple2 import org.session.libsession.messaging.contacts.Contact import org.thoughtcrime.securesms.database.DatabaseFactory import org.session.libsession.utilities.TextSecurePreferences +import org.thoughtcrime.securesms.util.UiModeUtilities import java.util.regex.Pattern object MentionUtilities { @@ -23,7 +25,7 @@ object MentionUtilities { @JvmStatic fun highlightMentions(text: CharSequence, isOutgoingMessage: Boolean, threadID: Long, context: Context): SpannableString { - var text = text + @Suppress("NAME_SHADOWING") var text = text val threadDB = DatabaseFactory.getThreadDatabase(context) val isOpenGroup = threadDB.getRecipientForThreadId(threadID)?.isOpenGroupRecipient ?: false val pattern = Pattern.compile("@[0-9a-fA-F]*") @@ -38,7 +40,7 @@ object MentionUtilities { TextSecurePreferences.getProfileName(context) } else { val contact = DatabaseFactory.getSessionContactDatabase(context).getContactWithSessionID(publicKey) - val context = if (isOpenGroup) Contact.ContactContext.OPEN_GROUP else Contact.ContactContext.REGULAR + @Suppress("NAME_SHADOWING") val context = if (isOpenGroup) Contact.ContactContext.OPEN_GROUP else Contact.ContactContext.REGULAR contact?.displayName(context) } if (userDisplayName != null) { @@ -54,10 +56,15 @@ object MentionUtilities { } } val result = SpannableString(text) + val isLightMode = UiModeUtilities.isDayUiMode(context) for (mention in mentions) { - val isLightMode = UiModeUtilities.isDayUiMode(context) - val colorID = if (isLightMode && isOutgoingMessage) R.color.black else R.color.accent - result.setSpan(ForegroundColorSpan(context.resources.getColorWithID(colorID, context.theme)), mention.first.lower, mention.first.upper, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) + val colorID = if (isOutgoingMessage) { + if (isLightMode) R.color.white else R.color.black + } else { + R.color.accent + } + val color = ResourcesCompat.getColor(context.resources, colorID, context.theme) + result.setSpan(ForegroundColorSpan(color), mention.first.lower, mention.first.upper, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) result.setSpan(StyleSpan(Typeface.BOLD), mention.first.lower, mention.first.upper, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) } return result diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/utilities/MessageBubbleUtilities.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/utilities/MessageBubbleUtilities.kt new file mode 100644 index 0000000000..c4c5d5a5d8 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/utilities/MessageBubbleUtilities.kt @@ -0,0 +1,31 @@ +package org.thoughtcrime.securesms.conversation.v2.utilities + +import android.content.Context +import network.loki.messenger.R +import kotlin.math.roundToInt + +object MessageBubbleUtilities { + + fun calculateRadii(context: Context, isStartOfMessageCluster: Boolean, isEndOfMessageCluster: Boolean, isOutgoing: Boolean): IntArray { + val roundedDimen = context.resources.getDimension(R.dimen.message_corner_radius).roundToInt() + val collapsedDimen = context.resources.getDimension(R.dimen.message_corner_collapse_radius).roundToInt() + val (tl, tr, bl, br) = when { + // Single message + isStartOfMessageCluster && isEndOfMessageCluster -> intArrayOf(roundedDimen, roundedDimen, roundedDimen, roundedDimen) + // Start of message cluster; collapsed BL + isStartOfMessageCluster -> intArrayOf(roundedDimen, roundedDimen, collapsedDimen, roundedDimen) + // End of message cluster; collapsed TL + isEndOfMessageCluster -> intArrayOf(collapsedDimen, roundedDimen, roundedDimen, roundedDimen) + // In the middle; no rounding on the left + else -> intArrayOf(collapsedDimen, roundedDimen, collapsedDimen, roundedDimen) + } + // TL, TR, BR, BL (CW direction) + // Flip if the message is outgoing + return intArrayOf( + if (!isOutgoing) tl else tr, // TL + if (!isOutgoing) tr else tl, // TR + if (!isOutgoing) br else bl, // BR + if (!isOutgoing) bl else br // BL + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/utilities/ModalURLSpan.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/utilities/ModalURLSpan.kt new file mode 100644 index 0000000000..358dd56357 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/utilities/ModalURLSpan.kt @@ -0,0 +1,10 @@ +package org.thoughtcrime.securesms.conversation.v2.utilities + +import android.text.style.URLSpan +import android.view.View + +class ModalURLSpan(url: String, private val openModalCallback: (String)->Unit): URLSpan(url) { + override fun onClick(widget: View) { + openModalCallback(url) + } +} \ No newline at end of file diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/utilities/TextUtilities.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/utilities/TextUtilities.kt new file mode 100644 index 0000000000..b7ced4abb3 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/utilities/TextUtilities.kt @@ -0,0 +1,49 @@ +package org.thoughtcrime.securesms.conversation.v2.utilities + +import android.graphics.Rect +import android.text.Layout +import android.text.StaticLayout +import android.text.TextPaint +import android.view.MotionEvent +import android.widget.TextView +import androidx.core.text.getSpans +import androidx.core.text.toSpannable + +object TextUtilities { + + fun getIntrinsicHeight(text: CharSequence, paint: TextPaint, width: Int): Int { + val builder = StaticLayout.Builder.obtain(text, 0, text.length, paint, width) + .setAlignment(Layout.Alignment.ALIGN_NORMAL) + .setLineSpacing(0.0f, 1.0f) + .setIncludePad(false) + val layout = builder.build() + return layout.height + } + + fun TextView.getIntersectedModalSpans(event: MotionEvent): List { + val xInt = event.rawX.toInt() + val yInt = event.rawY.toInt() + val hitRect = Rect(xInt, yInt, xInt, yInt) + return getIntersectedModalSpans(hitRect) + } + + fun TextView.getIntersectedModalSpans(hitRect: Rect): List { + val textLayout = layout ?: return emptyList() + val lineRect = Rect() + val bodyTextRect = Rect() + getGlobalVisibleRect(bodyTextRect) + val textSpannable = text.toSpannable() + return (0 until textLayout.lineCount).flatMap { line -> + textLayout.getLineBounds(line, lineRect) + lineRect.offset(bodyTextRect.left + totalPaddingLeft, bodyTextRect.top + totalPaddingTop) + if ((Rect(lineRect)).contains(hitRect)) { + // calculate the url span intersected with (if any) + val off = textLayout.getOffsetForHorizontal(line, hitRect.left.toFloat()) // left and right will be the same + textSpannable.getSpans(off, off).toList() + } else { + emptyList() + } + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/utilities/ThumbnailDimensDelegate.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/utilities/ThumbnailDimensDelegate.kt new file mode 100644 index 0000000000..fb50d8de62 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/utilities/ThumbnailDimensDelegate.kt @@ -0,0 +1,91 @@ +package org.thoughtcrime.securesms.conversation.v2.utilities + +class ThumbnailDimensDelegate { + + companion object { + // dimens array constants + private const val WIDTH = 0 + private const val HEIGHT = 1 + private const val DIMENS_ARRAY_SIZE = 2 + + // bounds array constants + private const val MIN_WIDTH = 0 + private const val MIN_HEIGHT = 1 + private const val MAX_WIDTH = 2 + private const val MAX_HEIGHT = 3 + private const val BOUNDS_ARRAY_SIZE = 4 + + // const zero int array + private val EMPTY_DIMENS = intArrayOf(0,0) + + } + + private val measured: IntArray = IntArray(DIMENS_ARRAY_SIZE) + private val dimens: IntArray = IntArray(DIMENS_ARRAY_SIZE) + private val bounds: IntArray = IntArray(BOUNDS_ARRAY_SIZE) + + fun resourceSize(): IntArray { + if (dimens.all { it == 0 }) { + // dimens are (0, 0), don't go any further + return EMPTY_DIMENS + } + + val naturalWidth = dimens[WIDTH].toDouble() + val naturalHeight = dimens[HEIGHT].toDouble() + val minWidth = dimens[MIN_WIDTH] + val maxWidth = dimens[MAX_WIDTH] + val minHeight = dimens[MIN_HEIGHT] + val maxHeight = dimens[MAX_HEIGHT] + + // calculate actual measured + var measuredWidth: Double = naturalWidth + var measuredHeight: Double = naturalHeight + + val widthInBounds = measuredWidth >= minWidth && measuredWidth <= maxWidth + val heightInBounds = measuredHeight >= minHeight && measuredHeight <= maxHeight + + if (!widthInBounds || !heightInBounds) { + val minWidthRatio: Double = naturalWidth / minWidth + val maxWidthRatio: Double = naturalWidth / maxWidth + val minHeightRatio: Double = naturalHeight / minHeight + val maxHeightRatio: Double = naturalHeight / maxHeight + if (maxWidthRatio > 1 || maxHeightRatio > 1) { + if (maxWidthRatio >= maxHeightRatio) { + measuredWidth /= maxWidthRatio + measuredHeight /= maxWidthRatio + } else { + measuredWidth /= maxHeightRatio + measuredHeight /= maxHeightRatio + } + measuredWidth = Math.max(measuredWidth, minWidth.toDouble()) + measuredHeight = Math.max(measuredHeight, minHeight.toDouble()) + } else if (minWidthRatio < 1 || minHeightRatio < 1) { + if (minWidthRatio <= minHeightRatio) { + measuredWidth /= minWidthRatio + measuredHeight /= minWidthRatio + } else { + measuredWidth /= minHeightRatio + measuredHeight /= minHeightRatio + } + measuredWidth = Math.min(measuredWidth, maxWidth.toDouble()) + measuredHeight = Math.min(measuredHeight, maxHeight.toDouble()) + } + } + measured[WIDTH] = measuredWidth.toInt() + measured[HEIGHT] = measuredHeight.toInt() + return measured + } + + fun setBounds(minWidth: Int, minHeight: Int, maxWidth: Int, maxHeight: Int) { + bounds[MIN_WIDTH] = minWidth + bounds[MIN_HEIGHT] = minHeight + bounds[MAX_WIDTH] = maxWidth + bounds[MAX_HEIGHT] = maxHeight + } + + fun setDimens(width: Int, height: Int) { + dimens[WIDTH] = width + dimens[HEIGHT] = height + } + +} \ No newline at end of file diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/utilities/ThumbnailProgressBar.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/utilities/ThumbnailProgressBar.kt new file mode 100644 index 0000000000..60ef8116d2 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/utilities/ThumbnailProgressBar.kt @@ -0,0 +1,64 @@ +package org.thoughtcrime.securesms.conversation.v2.utilities + +import android.animation.ValueAnimator +import android.content.Context +import android.graphics.Canvas +import android.graphics.Interpolator +import android.graphics.Paint +import android.graphics.Rect +import android.os.SystemClock +import android.util.AttributeSet +import android.util.Log +import android.view.View +import android.view.animation.AccelerateDecelerateInterpolator +import android.view.animation.Animation +import android.view.animation.AnimationSet +import android.view.animation.AnimationUtils +import androidx.core.content.res.ResourcesCompat +import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView +import network.loki.messenger.R +import kotlin.math.sin + +class ThumbnailProgressBar: View { + + 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 firstX: Double + get() = sin(SystemClock.elapsedRealtime() / 300.0) * 1.5 + + private val secondX: Double + get() = sin(SystemClock.elapsedRealtime() / 300.0 + (Math.PI/4)) * 1.5 + + private val paint = Paint(Paint.ANTI_ALIAS_FLAG).apply { + style = Paint.Style.FILL + color = ResourcesCompat.getColor(resources, R.color.accent, null) + } + + private val objectRect = Rect() + private val drawingRect = Rect() + + override fun dispatchDraw(canvas: Canvas?) { + if (canvas == null) return + + getDrawingRect(objectRect) + drawingRect.set(objectRect) + + val coercedFX = firstX + val coercedSX = secondX + + val firstMeasuredX = objectRect.left + (objectRect.width() * coercedFX) + val secondMeasuredX = objectRect.left + (objectRect.width() * coercedSX) + + drawingRect.set( + (if (firstMeasuredX < secondMeasuredX) firstMeasuredX else secondMeasuredX).toInt(), + objectRect.top, + (if (firstMeasuredX < secondMeasuredX) secondMeasuredX else firstMeasuredX).toInt(), + objectRect.bottom + ) + + canvas.drawRect(drawingRect, paint) + invalidate() + } +} \ No newline at end of file diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/ThumbnailView.java b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/utilities/ThumbnailView.java similarity index 96% rename from app/src/main/java/org/thoughtcrime/securesms/components/ThumbnailView.java rename to app/src/main/java/org/thoughtcrime/securesms/conversation/v2/utilities/ThumbnailView.java index 106fa45096..f40a57924a 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/ThumbnailView.java +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/utilities/ThumbnailView.java @@ -1,4 +1,4 @@ -package org.thoughtcrime.securesms.components; +package org.thoughtcrime.securesms.conversation.v2.utilities; import android.content.Context; import android.content.res.TypedArray; @@ -24,6 +24,9 @@ import com.bumptech.glide.request.RequestOptions; import network.loki.messenger.R; +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; @@ -54,7 +57,6 @@ public class ThumbnailView extends FrameLayout { private ImageView image; private View playOverlay; - private View captionIcon; private View loadIndicator; private OnClickListener parentClickListener; @@ -67,7 +69,7 @@ public class ThumbnailView extends FrameLayout { private SlidesClickedListener downloadClickListener = null; private Slide slide = null; - private int radius; + public int radius; public ThumbnailView(Context context) { this(context, null); @@ -84,7 +86,6 @@ public class ThumbnailView extends FrameLayout { this.image = findViewById(R.id.thumbnail_image); this.playOverlay = findViewById(R.id.play_overlay); - this.captionIcon = findViewById(R.id.thumbnail_caption_icon); this.loadIndicator = findViewById(R.id.thumbnail_load_indicator); super.setOnClickListener(new ThumbnailClickDispatcher()); @@ -94,10 +95,10 @@ public class ThumbnailView extends FrameLayout { 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, getResources().getDimensionPixelSize(R.dimen.thumbnail_default_radius)); + radius = typedArray.getDimensionPixelSize(R.styleable.ThumbnailView_thumbnail_radius, 0); typedArray.recycle(); } else { - radius = getResources().getDimensionPixelSize(R.dimen.message_corner_collapse_radius); + radius = 0; } } @@ -275,8 +276,6 @@ public class ThumbnailView extends FrameLayout { this.slide = slide; - this.captionIcon.setVisibility(slide.getCaption().isPresent() ? VISIBLE : GONE); - dimens[WIDTH] = naturalWidth; dimens[HEIGHT] = naturalHeight; invalidate(); @@ -398,6 +397,7 @@ public class ThumbnailView extends FrameLayout { } private class ThumbnailClickDispatcher implements View.OnClickListener { + @Override public void onClick(View view) { if (thumbnailClickListener != null && @@ -413,9 +413,9 @@ public class ThumbnailView extends FrameLayout { } private class DownloadClickDispatcher implements View.OnClickListener { + @Override public void onClick(View view) { - Log.i(TAG, "onClick() for download button"); if (downloadClickListener != null && slide != null) { downloadClickListener.onClick(view, Collections.singletonList(slide)); } else { diff --git a/libsession/src/main/java/org/session/libsession/utilities/IdentityKeyUtil.java b/app/src/main/java/org/thoughtcrime/securesms/crypto/IdentityKeyUtil.java similarity index 57% rename from libsession/src/main/java/org/session/libsession/utilities/IdentityKeyUtil.java rename to app/src/main/java/org/thoughtcrime/securesms/crypto/IdentityKeyUtil.java index d42f5c2ac5..a5333ef5d4 100644 --- a/libsession/src/main/java/org/session/libsession/utilities/IdentityKeyUtil.java +++ b/app/src/main/java/org/thoughtcrime/securesms/crypto/IdentityKeyUtil.java @@ -15,21 +15,22 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -package org.session.libsession.utilities; +package org.thoughtcrime.securesms.crypto; import android.content.Context; import android.content.SharedPreferences; import android.content.SharedPreferences.Editor; +import android.os.Build; + import androidx.annotation.NonNull; -import org.session.libsignal.crypto.ecc.ECPublicKey; import org.session.libsignal.crypto.IdentityKey; import org.session.libsignal.crypto.IdentityKeyPair; -import org.session.libsignal.exceptions.InvalidKeyException; import org.session.libsignal.crypto.ecc.Curve; import org.session.libsignal.crypto.ecc.ECKeyPair; import org.session.libsignal.crypto.ecc.ECPrivateKey; - +import org.session.libsignal.crypto.ecc.ECPublicKey; +import org.session.libsignal.exceptions.InvalidKeyException; import org.session.libsignal.utilities.Base64; import java.io.IOException; @@ -45,19 +46,41 @@ public class IdentityKeyUtil { @SuppressWarnings("unused") private static final String TAG = IdentityKeyUtil.class.getSimpleName(); + private static final String ENCRYPTED_SUFFIX = "_encrypted"; public static final String IDENTITY_PUBLIC_KEY_PREF = "pref_identity_public_v3"; public static final String IDENTITY_PRIVATE_KEY_PREF = "pref_identity_private_v3"; public static final String ED25519_PUBLIC_KEY = "pref_ed25519_public_key"; public static final String ED25519_SECRET_KEY = "pref_ed25519_secret_key"; public static final String LOKI_SEED = "loki_seed"; + public static final String HAS_MIGRATED_KEY = "has_migrated_keys"; + + private static SharedPreferences getSharedPreferences(Context context) { + return context.getSharedPreferences(MASTER_SECRET_UTIL_PREFERENCES_NAME, 0); + } public static boolean hasIdentityKey(Context context) { - SharedPreferences preferences = context.getSharedPreferences(MASTER_SECRET_UTIL_PREFERENCES_NAME, 0); + SharedPreferences preferences = getSharedPreferences(context); - return - preferences.contains(IDENTITY_PUBLIC_KEY_PREF) && - preferences.contains(IDENTITY_PRIVATE_KEY_PREF); + return (preferences.contains(IDENTITY_PUBLIC_KEY_PREF) && + preferences.contains(IDENTITY_PRIVATE_KEY_PREF)) + || (preferences.contains(IDENTITY_PUBLIC_KEY_PREF+ENCRYPTED_SUFFIX) && + preferences.contains(IDENTITY_PRIVATE_KEY_PREF+ENCRYPTED_SUFFIX)); + } + + public static void checkUpdate(Context context) { + SharedPreferences preferences = getSharedPreferences(context); + // check if any keys are not migrated + if (hasIdentityKey(context) && !preferences.getBoolean(HAS_MIGRATED_KEY, false)) { + // this will retrieve and force upgrade if possible + // retrieve will force upgrade if available + retrieve(context,IDENTITY_PUBLIC_KEY_PREF); + retrieve(context,IDENTITY_PRIVATE_KEY_PREF); + retrieve(context,ED25519_PUBLIC_KEY); + retrieve(context,ED25519_SECRET_KEY); + retrieve(context,LOKI_SEED); + preferences.edit().putBoolean(HAS_MIGRATED_KEY, true).apply(); + } } public static @NonNull IdentityKey getIdentityKey(@NonNull Context context) { @@ -94,14 +117,56 @@ public class IdentityKeyUtil { public static String retrieve(Context context, String key) { SharedPreferences preferences = context.getSharedPreferences(MASTER_SECRET_UTIL_PREFERENCES_NAME, 0); - return preferences.getString(key, null); + + String unencryptedSecret = preferences.getString(key, null); + String encryptedSecret = preferences.getString(key+ENCRYPTED_SUFFIX, null); + + if (unencryptedSecret != null) return getUnencryptedSecret(key, unencryptedSecret, context); + else if (encryptedSecret != null) return getEncryptedSecret(encryptedSecret); + + return null; } + private static String getUnencryptedSecret(String key, String unencryptedSecret, Context context) { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) { + return unencryptedSecret; + } else { + KeyStoreHelper.SealedData encryptedSecret = KeyStoreHelper.seal(unencryptedSecret.getBytes()); + + // save the encrypted suffix secret "key_encrypted" + save(context,key+ENCRYPTED_SUFFIX,encryptedSecret.serialize()); + // delete the regular secret "key" + delete(context,key); + + return unencryptedSecret; + } + } + + private static String getEncryptedSecret(String encryptedSecret) { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) { + throw new AssertionError("OS downgrade not supported. KeyStore sealed data exists on platform < M!"); + } else { + KeyStoreHelper.SealedData sealedData = KeyStoreHelper.SealedData.fromString(encryptedSecret); + return new String(KeyStoreHelper.unseal(sealedData)); + } + } + + public static void save(Context context, String key, String value) { SharedPreferences preferences = context.getSharedPreferences(MASTER_SECRET_UTIL_PREFERENCES_NAME, 0); Editor preferencesEditor = preferences.edit(); - preferencesEditor.putString(key, value); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + boolean isEncryptedSuffix = key.endsWith(ENCRYPTED_SUFFIX); + if (isEncryptedSuffix) { + preferencesEditor.putString(key, value); + } else { + KeyStoreHelper.SealedData encryptedSecret = KeyStoreHelper.seal(value.getBytes()); + preferencesEditor.putString(key+ENCRYPTED_SUFFIX, encryptedSecret.serialize()); + } + } else { + preferencesEditor.putString(key, value); + } if (!preferencesEditor.commit()) throw new AssertionError("failed to save identity key/value to shared preferences"); } diff --git a/libsession/src/main/java/org/session/libsession/utilities/KeyPairUtilities.kt b/app/src/main/java/org/thoughtcrime/securesms/crypto/KeyPairUtilities.kt similarity index 98% rename from libsession/src/main/java/org/session/libsession/utilities/KeyPairUtilities.kt rename to app/src/main/java/org/thoughtcrime/securesms/crypto/KeyPairUtilities.kt index dfefaa2f12..652732f081 100644 --- a/libsession/src/main/java/org/session/libsession/utilities/KeyPairUtilities.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/crypto/KeyPairUtilities.kt @@ -1,4 +1,4 @@ -package org.session.libsession.utilities +package org.thoughtcrime.securesms.crypto import android.content.Context import com.goterl.lazysodium.LazySodiumAndroid diff --git a/app/src/main/java/org/thoughtcrime/securesms/loki/utilities/MnemonicUtilities.kt b/app/src/main/java/org/thoughtcrime/securesms/crypto/MnemonicUtilities.kt similarity index 88% rename from app/src/main/java/org/thoughtcrime/securesms/loki/utilities/MnemonicUtilities.kt rename to app/src/main/java/org/thoughtcrime/securesms/crypto/MnemonicUtilities.kt index 226bb1abfd..04cdd85ae4 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/loki/utilities/MnemonicUtilities.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/crypto/MnemonicUtilities.kt @@ -1,4 +1,4 @@ -package org.thoughtcrime.securesms.loki.utilities +package org.thoughtcrime.securesms.crypto import android.content.Context diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/AttachmentDatabase.java b/app/src/main/java/org/thoughtcrime/securesms/database/AttachmentDatabase.java index 4b20ee202e..2f85f27d52 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/AttachmentDatabase.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/AttachmentDatabase.java @@ -44,8 +44,8 @@ import org.session.libsession.messaging.sending_receiving.attachments.DatabaseAt import org.session.libsession.messaging.sending_receiving.attachments.DatabaseAttachmentAudioExtras; import org.session.libsession.utilities.MediaTypes; import org.session.libsession.utilities.Util; -import org.session.libsignal.utilities.JsonUtil; import org.session.libsignal.utilities.ExternalStorageUtil; +import org.session.libsignal.utilities.JsonUtil; import org.session.libsignal.utilities.Log; import org.thoughtcrime.securesms.crypto.AttachmentSecret; import org.thoughtcrime.securesms.crypto.ClassicDecryptingPartInputStream; @@ -820,7 +820,7 @@ public class AttachmentDatabase extends Database { * @return true if the update operation was successful. */ @Synchronized - public boolean setAttachmentAudioExtras(@NonNull DatabaseAttachmentAudioExtras extras) { + public boolean setAttachmentAudioExtras(@NonNull DatabaseAttachmentAudioExtras extras, long threadId) { ContentValues values = new ContentValues(); values.put(AUDIO_VISUAL_SAMPLES, extras.getVisualSamples()); values.put(AUDIO_DURATION, extras.getDurationMs()); @@ -830,9 +830,22 @@ public class AttachmentDatabase extends Database { PART_ID_WHERE + " AND " + PART_AUDIO_ONLY_WHERE, extras.getAttachmentId().toStrings()); + if (threadId >= 0) { + notifyConversationListeners(threadId); + } + return alteredRows > 0; } + /** + * Updates audio extra columns for the "audio/*" mime type attachments only. + * @return true if the update operation was successful. + */ + @Synchronized + public boolean setAttachmentAudioExtras(@NonNull DatabaseAttachmentAudioExtras extras) { + return setAttachmentAudioExtras(extras, -1); // -1 for no update + } + @VisibleForTesting class ThumbnailFetchCallable implements Callable { diff --git a/app/src/main/java/org/thoughtcrime/securesms/loki/database/BackupFileRecord.kt b/app/src/main/java/org/thoughtcrime/securesms/database/BackupFileRecord.kt similarity index 88% rename from app/src/main/java/org/thoughtcrime/securesms/loki/database/BackupFileRecord.kt rename to app/src/main/java/org/thoughtcrime/securesms/database/BackupFileRecord.kt index 0d3fc63bcf..7a78a09892 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/loki/database/BackupFileRecord.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/BackupFileRecord.kt @@ -1,4 +1,4 @@ -package org.thoughtcrime.securesms.loki.database +package org.thoughtcrime.securesms.database import android.net.Uri import java.util.* diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/DatabaseFactory.java b/app/src/main/java/org/thoughtcrime/securesms/database/DatabaseFactory.java index 9a87fa8730..211223fd41 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/DatabaseFactory.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/DatabaseFactory.java @@ -25,13 +25,8 @@ import org.thoughtcrime.securesms.crypto.AttachmentSecretProvider; import org.thoughtcrime.securesms.crypto.DatabaseSecret; import org.thoughtcrime.securesms.crypto.DatabaseSecretProvider; import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper; -import org.thoughtcrime.securesms.loki.database.LokiAPIDatabase; -import org.thoughtcrime.securesms.loki.database.LokiBackupFilesDatabase; -import org.thoughtcrime.securesms.loki.database.LokiMessageDatabase; -import org.thoughtcrime.securesms.loki.database.LokiThreadDatabase; -import org.thoughtcrime.securesms.loki.database.LokiUserDatabase; -import org.thoughtcrime.securesms.loki.database.SessionJobDatabase; -import org.thoughtcrime.securesms.loki.database.SessionContactDatabase; +import org.thoughtcrime.securesms.database.LokiAPIDatabase; +import org.thoughtcrime.securesms.database.SessionJobDatabase; public class DatabaseFactory { diff --git a/app/src/main/java/org/thoughtcrime/securesms/loki/utilities/DatabaseUtilities.kt b/app/src/main/java/org/thoughtcrime/securesms/database/DatabaseUtilities.kt similarity index 97% rename from app/src/main/java/org/thoughtcrime/securesms/loki/utilities/DatabaseUtilities.kt rename to app/src/main/java/org/thoughtcrime/securesms/database/DatabaseUtilities.kt index 23834fb9af..b063eb1b63 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/loki/utilities/DatabaseUtilities.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/DatabaseUtilities.kt @@ -1,4 +1,4 @@ -package org.thoughtcrime.securesms.loki.utilities +package org.thoughtcrime.securesms.database import android.content.ContentValues import androidx.core.database.getStringOrNull diff --git a/app/src/main/java/org/thoughtcrime/securesms/loki/database/LokiAPIDatabase.kt b/app/src/main/java/org/thoughtcrime/securesms/database/LokiAPIDatabase.kt similarity index 97% rename from app/src/main/java/org/thoughtcrime/securesms/loki/database/LokiAPIDatabase.kt rename to app/src/main/java/org/thoughtcrime/securesms/database/LokiAPIDatabase.kt index 91f04adef8..ab0e0ba0f7 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/loki/database/LokiAPIDatabase.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/LokiAPIDatabase.kt @@ -1,23 +1,26 @@ -package org.thoughtcrime.securesms.loki.database +package org.thoughtcrime.securesms.database import android.content.ContentValues import android.content.Context -import org.session.libsession.utilities.IdentityKeyUtil import org.session.libsession.utilities.TextSecurePreferences 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.utilities.Snode import org.session.libsignal.database.LokiAPIDatabaseProtocol -import org.session.libsignal.utilities.PublicKeyValidation -import org.session.libsignal.utilities.removing05PrefixIfNeeded -import org.session.libsignal.utilities.toHexString -import org.session.libsignal.utilities.Hex -import org.session.libsignal.utilities.Log -import org.thoughtcrime.securesms.database.Database +import org.session.libsignal.utilities.* +import org.thoughtcrime.securesms.crypto.IdentityKeyUtil +import org.thoughtcrime.securesms.database.* import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper -import org.thoughtcrime.securesms.loki.utilities.* +import org.thoughtcrime.securesms.util.* import java.util.* +import kotlin.Array +import kotlin.Boolean +import kotlin.Int +import kotlin.Long +import kotlin.Pair +import kotlin.String +import kotlin.arrayOf +import kotlin.to class LokiAPIDatabase(context: Context, helper: SQLCipherOpenHelper) : Database(context, helper), LokiAPIDatabaseProtocol { diff --git a/app/src/main/java/org/thoughtcrime/securesms/loki/database/LokiBackupFilesDatabase.kt b/app/src/main/java/org/thoughtcrime/securesms/database/LokiBackupFilesDatabase.kt similarity index 97% rename from app/src/main/java/org/thoughtcrime/securesms/loki/database/LokiBackupFilesDatabase.kt rename to app/src/main/java/org/thoughtcrime/securesms/database/LokiBackupFilesDatabase.kt index fd92fe4bf1..03e964de71 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/loki/database/LokiBackupFilesDatabase.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/LokiBackupFilesDatabase.kt @@ -1,10 +1,9 @@ -package org.thoughtcrime.securesms.loki.database +package org.thoughtcrime.securesms.database import android.content.ContentValues import android.content.Context import android.database.Cursor import android.net.Uri -import org.thoughtcrime.securesms.database.Database import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper import java.lang.IllegalArgumentException import java.util.* diff --git a/app/src/main/java/org/thoughtcrime/securesms/loki/database/LokiMessageDatabase.kt b/app/src/main/java/org/thoughtcrime/securesms/database/LokiMessageDatabase.kt similarity index 88% rename from app/src/main/java/org/thoughtcrime/securesms/loki/database/LokiMessageDatabase.kt rename to app/src/main/java/org/thoughtcrime/securesms/database/LokiMessageDatabase.kt index fc1aa4351b..c36c197cbb 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/loki/database/LokiMessageDatabase.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/LokiMessageDatabase.kt @@ -1,14 +1,10 @@ -package org.thoughtcrime.securesms.loki.database +package org.thoughtcrime.securesms.database import android.content.ContentValues import android.content.Context import net.sqlcipher.database.SQLiteDatabase.CONFLICT_REPLACE -import org.thoughtcrime.securesms.database.Database -import org.thoughtcrime.securesms.database.DatabaseFactory import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper -import org.thoughtcrime.securesms.loki.utilities.* import org.session.libsignal.database.LokiMessageDatabaseProtocol -import org.session.libsignal.utilities.Log class LokiMessageDatabase(context: Context, helper: SQLCipherOpenHelper) : Database(context, helper), LokiMessageDatabaseProtocol { @@ -63,9 +59,9 @@ class LokiMessageDatabase(context: Context, helper: SQLCipherOpenHelper) : Datab val database = databaseHelper.writableDatabase val serverID = database.get(messageIDTable, - "${Companion.messageID} = ? AND ${Companion.messageType} = ?", + "${Companion.messageID} = ? AND $messageType = ?", arrayOf(messageID.toString(), (if (isSms) SMS_TYPE else MMS_TYPE).toString())) { cursor -> - cursor.getInt(Companion.serverID).toLong() + cursor.getInt(serverID).toLong() } ?: return database.beginTransaction() @@ -89,7 +85,7 @@ class LokiMessageDatabase(context: Context, helper: SQLCipherOpenHelper) : Datab return database.get(messageIDTable, "$messageID = ? AND ${Companion.serverID} = ?", arrayOf(mappedID.toString(), mappedServerID.toString())) { cursor -> - cursor.getInt(Companion.messageID).toLong() to (cursor.getInt(messageType) == SMS_TYPE) + cursor.getInt(messageID).toLong() to (cursor.getInt(messageType) == SMS_TYPE) } } @@ -137,18 +133,18 @@ class LokiMessageDatabase(context: Context, helper: SQLCipherOpenHelper) : Datab val database = databaseHelper.writableDatabase try { val messages = mutableSetOf>() - database.get(messageThreadMappingTable, "${Companion.threadID} = ?", arrayOf(threadId.toString())) { cursor -> + database.get(messageThreadMappingTable, "$threadID = ?", arrayOf(threadId.toString())) { cursor -> // for each add while (cursor.moveToNext()) { - messages.add(cursor.getLong(Companion.messageID) to cursor.getLong(Companion.serverID)) + messages.add(cursor.getLong(messageID) to cursor.getLong(serverID)) } } var deletedCount = 0L database.beginTransaction() messages.forEach { (messageId, serverId) -> - deletedCount += database.delete(messageIDTable, "${Companion.messageID} = ? AND ${Companion.serverID} = ?", arrayOf(messageId.toString(), serverId.toString())) + deletedCount += database.delete(messageIDTable, "$messageID = ? AND $serverID = ?", arrayOf(messageId.toString(), serverId.toString())) } - val mappingDeleted = database.delete(messageThreadMappingTable, "${Companion.threadID} = ?", arrayOf(threadId.toString())) + val mappingDeleted = database.delete(messageThreadMappingTable, "$threadID = ?", arrayOf(threadId.toString())) database.setTransactionSuccessful() } finally { database.endTransaction() diff --git a/app/src/main/java/org/thoughtcrime/securesms/loki/database/LokiThreadDatabase.kt b/app/src/main/java/org/thoughtcrime/securesms/database/LokiThreadDatabase.kt similarity index 93% rename from app/src/main/java/org/thoughtcrime/securesms/loki/database/LokiThreadDatabase.kt rename to app/src/main/java/org/thoughtcrime/securesms/database/LokiThreadDatabase.kt index f0f6123b2e..0b45875bd6 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/loki/database/LokiThreadDatabase.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/LokiThreadDatabase.kt @@ -1,12 +1,9 @@ -package org.thoughtcrime.securesms.loki.database +package org.thoughtcrime.securesms.database import android.content.ContentValues import android.content.Context import android.database.Cursor -import org.thoughtcrime.securesms.database.Database -import org.thoughtcrime.securesms.database.DatabaseFactory import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper -import org.thoughtcrime.securesms.loki.utilities.* import org.session.libsession.messaging.open_groups.OpenGroupV2 import org.session.libsession.utilities.Address import org.session.libsession.utilities.recipients.Recipient diff --git a/app/src/main/java/org/thoughtcrime/securesms/loki/database/LokiUserDatabase.kt b/app/src/main/java/org/thoughtcrime/securesms/database/LokiUserDatabase.kt similarity index 92% rename from app/src/main/java/org/thoughtcrime/securesms/loki/database/LokiUserDatabase.kt rename to app/src/main/java/org/thoughtcrime/securesms/database/LokiUserDatabase.kt index fba0d64b36..4a5468c5d4 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/loki/database/LokiUserDatabase.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/LokiUserDatabase.kt @@ -1,9 +1,7 @@ -package org.thoughtcrime.securesms.loki.database +package org.thoughtcrime.securesms.database import android.content.Context -import org.thoughtcrime.securesms.database.Database import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper -import org.thoughtcrime.securesms.loki.utilities.get import org.session.libsession.utilities.TextSecurePreferences class LokiUserDatabase(context: Context, helper: SQLCipherOpenHelper) : Database(context, helper) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/MmsDatabase.java b/app/src/main/java/org/thoughtcrime/securesms/database/MmsDatabase.java index 75dca96013..ef68a39cea 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/MmsDatabase.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/MmsDatabase.java @@ -34,41 +34,40 @@ import net.sqlcipher.database.SQLiteDatabase; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; -import org.session.libsession.utilities.GroupUtil; -import org.thoughtcrime.securesms.ApplicationContext; -import org.thoughtcrime.securesms.attachments.MmsNotificationAttachment; -import org.session.libsession.utilities.IdentityKeyMismatch; -import org.session.libsession.utilities.IdentityKeyMismatchList; -import org.session.libsession.utilities.NetworkFailure; -import org.session.libsession.utilities.NetworkFailureList; -import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper; -import org.thoughtcrime.securesms.database.model.MediaMmsMessageRecord; -import org.thoughtcrime.securesms.database.model.MessageRecord; -import org.thoughtcrime.securesms.database.model.NotificationMmsMessageRecord; -import org.thoughtcrime.securesms.database.model.Quote; import org.session.libsession.messaging.messages.signal.IncomingMediaMessage; -import org.thoughtcrime.securesms.mms.MmsException; import org.session.libsession.messaging.messages.signal.OutgoingExpirationUpdateMessage; import org.session.libsession.messaging.messages.signal.OutgoingGroupMediaMessage; import org.session.libsession.messaging.messages.signal.OutgoingMediaMessage; import org.session.libsession.messaging.messages.signal.OutgoingSecureMediaMessage; -import org.thoughtcrime.securesms.mms.SlideDeck; - import org.session.libsession.messaging.sending_receiving.attachments.Attachment; import org.session.libsession.messaging.sending_receiving.attachments.AttachmentId; import org.session.libsession.messaging.sending_receiving.attachments.DatabaseAttachment; -import org.session.libsession.utilities.Contact; import org.session.libsession.messaging.sending_receiving.link_preview.LinkPreview; import org.session.libsession.messaging.sending_receiving.quotes.QuoteModel; import org.session.libsession.utilities.Address; +import org.session.libsession.utilities.Contact; +import org.session.libsession.utilities.GroupUtil; +import org.session.libsession.utilities.IdentityKeyMismatch; +import org.session.libsession.utilities.IdentityKeyMismatchList; +import org.session.libsession.utilities.NetworkFailure; +import org.session.libsession.utilities.NetworkFailureList; +import org.session.libsession.utilities.TextSecurePreferences; +import org.session.libsession.utilities.Util; import org.session.libsession.utilities.recipients.Recipient; import org.session.libsession.utilities.recipients.RecipientFormattingException; import org.session.libsignal.utilities.JsonUtil; -import org.session.libsession.utilities.TextSecurePreferences; -import org.session.libsession.utilities.Util; - import org.session.libsignal.utilities.Log; +import org.session.libsignal.utilities.ThreadUtils; import org.session.libsignal.utilities.guava.Optional; +import org.thoughtcrime.securesms.attachments.MmsNotificationAttachment; +import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper; +import org.thoughtcrime.securesms.database.model.MediaMmsMessageRecord; +import org.thoughtcrime.securesms.database.model.MessageRecord; +import org.thoughtcrime.securesms.database.model.MmsMessageRecord; +import org.thoughtcrime.securesms.database.model.NotificationMmsMessageRecord; +import org.thoughtcrime.securesms.database.model.Quote; +import org.thoughtcrime.securesms.mms.MmsException; +import org.thoughtcrime.securesms.mms.SlideDeck; import java.io.Closeable; import java.io.IOException; @@ -883,14 +882,35 @@ public class MmsDatabase extends MessagingDatabase { } } + public void deleteQuotedFromMessages(MessageRecord toDeleteRecord) { + if (toDeleteRecord == null) { return; } + String query = THREAD_ID + " = ?"; + Cursor threadMmsCursor = rawQuery(query, new String[]{String.valueOf(toDeleteRecord.getThreadId())}); + Reader reader = readerFor(threadMmsCursor); + MmsMessageRecord messageRecord; + + while ((messageRecord = (MmsMessageRecord) reader.getNext()) != null) { + if (messageRecord.getQuote() != null && toDeleteRecord.getDateSent() == messageRecord.getQuote().getId()) { + setQuoteMissing(messageRecord.getId()); + } + } + reader.close(); + } + public boolean delete(long messageId) { - long threadId = getThreadIdForMessage(messageId); + long threadId = getThreadIdForMessage(messageId); AttachmentDatabase attachmentDatabase = DatabaseFactory.getAttachmentDatabase(context); - attachmentDatabase.deleteAttachmentsForMessage(messageId); + ThreadUtils.queue(() -> attachmentDatabase.deleteAttachmentsForMessage(messageId)); GroupReceiptDatabase groupReceiptDatabase = DatabaseFactory.getGroupReceiptDatabase(context); groupReceiptDatabase.deleteRowsForMessage(messageId); + MessageRecord toDelete; + try (Cursor messageCursor = getMessage(messageId)) { + toDelete = readerFor(messageCursor).getNext(); + } + + deleteQuotedFromMessages(toDelete); SQLiteDatabase database = databaseHelper.getWritableDatabase(); database.delete(TABLE_NAME, ID_WHERE, new String[] {messageId+""}); boolean threadDeleted = DatabaseFactory.getThreadDatabase(context).update(threadId, false); @@ -1068,6 +1088,14 @@ public class MmsDatabase extends MessagingDatabase { return new OutgoingMessageReader(message, threadId); } + public int setQuoteMissing(long messageId) { + ContentValues contentValues = new ContentValues(); + contentValues.put(QUOTE_MISSING, 1); + SQLiteDatabase database = databaseHelper.getWritableDatabase(); + int rows = database.update(TABLE_NAME, contentValues, ID + " = ?", new String[]{ String.valueOf(messageId) }); + return rows; + } + public static class Status { public static final int DOWNLOAD_INITIALIZED = 1; public static final int DOWNLOAD_NO_CONNECTIVITY = 2; @@ -1171,9 +1199,9 @@ public class MmsDatabase extends MessagingDatabase { return new NotificationMmsMessageRecord(id, recipient, recipient, - addressDeviceId, dateSent, dateReceived, deliveryReceiptCount, threadId, + dateSent, dateReceived, deliveryReceiptCount, threadId, contentLocationBytes, messageSize, expiry, status, - transactionIdBytes, mailbox, subscriptionId, slideDeck, + transactionIdBytes, mailbox, slideDeck, readReceiptCount); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/MmsSmsDatabase.java b/app/src/main/java/org/thoughtcrime/securesms/database/MmsSmsDatabase.java index f21b1ae622..42284a4ff0 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/MmsSmsDatabase.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/MmsSmsDatabase.java @@ -129,7 +129,17 @@ public class MmsSmsDatabase extends Database { String order = MmsSmsColumns.NORMALIZED_DATE_RECEIVED + " DESC"; String selection = MmsSmsColumns.THREAD_ID + " = " + threadId; - return queryTables(PROJECTION, selection, order, "1"); + return queryTables(PROJECTION, selection, order, "1"); + } + + public long getLastMessageID(long threadId) { + String order = MmsSmsColumns.NORMALIZED_DATE_SENT + " DESC"; + String selection = MmsSmsColumns.THREAD_ID + " = " + threadId; + + try (Cursor cursor = queryTables(PROJECTION, selection, order, "1")) { + cursor.moveToFirst(); + return cursor.getLong(cursor.getColumnIndex(MmsSmsColumns.ID)); + } } public Cursor getUnread() { diff --git a/app/src/main/java/org/thoughtcrime/securesms/loki/database/SessionContactDatabase.kt b/app/src/main/java/org/thoughtcrime/securesms/database/SessionContactDatabase.kt similarity index 82% rename from app/src/main/java/org/thoughtcrime/securesms/loki/database/SessionContactDatabase.kt rename to app/src/main/java/org/thoughtcrime/securesms/database/SessionContactDatabase.kt index 7d5e93b502..9bcf94ec1e 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/loki/database/SessionContactDatabase.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/SessionContactDatabase.kt @@ -1,13 +1,11 @@ -package org.thoughtcrime.securesms.loki.database +package org.thoughtcrime.securesms.database import android.content.ContentValues import android.content.Context import net.sqlcipher.Cursor import org.session.libsession.messaging.contacts.Contact import org.session.libsignal.utilities.Base64 -import org.thoughtcrime.securesms.database.Database import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper -import org.thoughtcrime.securesms.loki.utilities.* class SessionContactDatabase(context: Context, helper: SQLCipherOpenHelper) : Database(context, helper) { @@ -35,7 +33,7 @@ class SessionContactDatabase(context: Context, helper: SQLCipherOpenHelper) : Da fun getContactWithSessionID(sessionID: String): Contact? { val database = databaseHelper.readableDatabase - return database.get(sessionContactTable, "${SessionContactDatabase.sessionID} = ?", arrayOf( sessionID )) { cursor -> + return database.get(sessionContactTable, "${Companion.sessionID} = ?", arrayOf( sessionID )) { cursor -> contactFromCursor(cursor) } } @@ -47,6 +45,17 @@ class SessionContactDatabase(context: Context, helper: SQLCipherOpenHelper) : Da }.toSet() } + fun setContactIsTrusted(contact: Contact, isTrusted: Boolean, threadID: Long) { + val database = databaseHelper.writableDatabase + val contentValues = ContentValues(1) + contentValues.put(Companion.isTrusted, if (isTrusted) 1 else 0) + database.update(sessionContactTable, contentValues, "$sessionID = ?", arrayOf( contact.sessionID )) + if (threadID >= 0) { + notifyConversationListeners(threadID) + } + notifyConversationListListeners() + } + fun setContact(contact: Contact) { val database = databaseHelper.writableDatabase val contentValues = ContentValues(8) @@ -58,7 +67,7 @@ class SessionContactDatabase(context: Context, helper: SQLCipherOpenHelper) : Da contact.profilePictureEncryptionKey?.let { contentValues.put(profilePictureEncryptionKey, Base64.encodeBytes(it)) } - contentValues.put(threadID, threadID) + contentValues.put(threadID, contact.threadID) contentValues.put(isTrusted, if (contact.isTrusted) 1 else 0) database.insertOrUpdate(sessionContactTable, contentValues, "$sessionID = ?", arrayOf( contact.sessionID )) notifyConversationListListeners() diff --git a/app/src/main/java/org/thoughtcrime/securesms/loki/database/SessionJobDatabase.kt b/app/src/main/java/org/thoughtcrime/securesms/database/SessionJobDatabase.kt similarity index 97% rename from app/src/main/java/org/thoughtcrime/securesms/loki/database/SessionJobDatabase.kt rename to app/src/main/java/org/thoughtcrime/securesms/database/SessionJobDatabase.kt index a0ca59dd0f..49729241a5 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/loki/database/SessionJobDatabase.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/SessionJobDatabase.kt @@ -1,4 +1,4 @@ -package org.thoughtcrime.securesms.loki.database +package org.thoughtcrime.securesms.database import android.content.ContentValues import android.content.Context @@ -6,10 +6,10 @@ import net.sqlcipher.Cursor import org.session.libsession.messaging.jobs.* import org.session.libsession.messaging.utilities.Data import org.session.libsignal.utilities.Log -import org.thoughtcrime.securesms.database.Database +import org.thoughtcrime.securesms.database.* import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper import org.thoughtcrime.securesms.jobmanager.impl.JsonDataSerializer -import org.thoughtcrime.securesms.loki.utilities.* +import org.thoughtcrime.securesms.util.* class SessionJobDatabase(context: Context, helper: SQLCipherOpenHelper) : Database(context, helper) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/SmsDatabase.java b/app/src/main/java/org/thoughtcrime/securesms/database/SmsDatabase.java index dfbc39f156..f66479c41f 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/SmsDatabase.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/SmsDatabase.java @@ -20,6 +20,8 @@ package org.thoughtcrime.securesms.database; import android.content.ContentValues; import android.content.Context; import android.database.Cursor; +import android.os.Handler; +import android.os.Looper; import android.text.TextUtils; import android.util.Pair; @@ -28,23 +30,21 @@ import com.annimon.stream.Stream; import net.sqlcipher.database.SQLiteDatabase; import net.sqlcipher.database.SQLiteStatement; -import org.session.libsignal.utilities.Log; -import org.thoughtcrime.securesms.ApplicationContext; -import org.session.libsession.utilities.IdentityKeyMismatch; -import org.session.libsession.utilities.IdentityKeyMismatchList; -import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper; -import org.thoughtcrime.securesms.database.model.MessageRecord; -import org.thoughtcrime.securesms.database.model.SmsMessageRecord; import org.session.libsession.messaging.messages.signal.IncomingGroupMessage; import org.session.libsession.messaging.messages.signal.IncomingTextMessage; import org.session.libsession.messaging.messages.signal.OutgoingTextMessage; - import org.session.libsession.utilities.Address; -import org.session.libsession.utilities.recipients.Recipient; -import org.session.libsignal.utilities.JsonUtil; +import org.session.libsession.utilities.IdentityKeyMismatch; +import org.session.libsession.utilities.IdentityKeyMismatchList; import org.session.libsession.utilities.TextSecurePreferences; import org.session.libsession.utilities.Util; +import org.session.libsession.utilities.recipients.Recipient; +import org.session.libsignal.utilities.JsonUtil; +import org.session.libsignal.utilities.Log; import org.session.libsignal.utilities.guava.Optional; +import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper; +import org.thoughtcrime.securesms.database.model.MessageRecord; +import org.thoughtcrime.securesms.database.model.SmsMessageRecord; import java.io.IOException; import java.security.SecureRandom; @@ -413,7 +413,6 @@ public class SmsDatabase extends MessagingDatabase { notifyConversationListeners(threadId); - return Optional.of(new InsertResult(messageId, threadId)); } } @@ -514,7 +513,13 @@ public class SmsDatabase extends MessagingDatabase { public boolean deleteMessage(long messageId) { Log.i("MessageDatabase", "Deleting: " + messageId); SQLiteDatabase db = databaseHelper.getWritableDatabase(); - long threadId = getThreadIdForMessage(messageId); + long threadId = getThreadIdForMessage(messageId); + try { + SmsMessageRecord toDelete = getMessage(messageId); + DatabaseFactory.getMmsDatabase(context).deleteQuotedFromMessages(toDelete); + } catch (NoSuchMessageException e) { + Log.e(TAG, "Couldn't find message record for messageId "+messageId, e); + } db.delete(TABLE_NAME, ID_WHERE, new String[] {messageId+""}); boolean threadDeleted = DatabaseFactory.getThreadDatabase(context).update(threadId, false); notifyConversationListeners(threadId); @@ -641,10 +646,10 @@ public class SmsDatabase extends MessagingDatabase { public MessageRecord getCurrent() { return new SmsMessageRecord(id, message.getMessageBody(), message.getRecipient(), message.getRecipient(), - 1, System.currentTimeMillis(), System.currentTimeMillis(), + System.currentTimeMillis(), System.currentTimeMillis(), 0, message.isSecureMessage() ? MmsSmsColumns.Types.getOutgoingEncryptedMessageType() : MmsSmsColumns.Types.getOutgoingSmsMessageType(), threadId, 0, new LinkedList(), - message.getSubscriptionId(), message.getExpiresIn(), + message.getExpiresIn(), System.currentTimeMillis(), 0, false); } } @@ -696,9 +701,8 @@ public class SmsDatabase extends MessagingDatabase { return new SmsMessageRecord(messageId, body, recipient, recipient, - addressDeviceId, dateSent, dateReceived, deliveryReceiptCount, type, - threadId, status, mismatches, subscriptionId, + threadId, status, mismatches, expiresIn, expireStarted, readReceiptCount, unidentified); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/Storage.kt b/app/src/main/java/org/thoughtcrime/securesms/database/Storage.kt index 07ab9e20cb..0335bbe5ff 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/Storage.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/Storage.kt @@ -1,6 +1,5 @@ package org.thoughtcrime.securesms.database -import android.app.job.JobScheduler import android.content.Context import android.net.Uri import org.session.libsession.database.StorageProtocol @@ -27,13 +26,11 @@ import org.session.libsignal.messages.SignalServiceGroup import org.session.libsignal.utilities.KeyHelper import org.session.libsignal.utilities.guava.Optional import org.thoughtcrime.securesms.ApplicationContext +import org.thoughtcrime.securesms.crypto.IdentityKeyUtil import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper import org.thoughtcrime.securesms.jobs.RetrieveProfileAvatarJob -import org.thoughtcrime.securesms.loki.api.OpenGroupManager -import org.thoughtcrime.securesms.loki.database.LokiThreadDatabase -import org.thoughtcrime.securesms.loki.protocol.SessionMetaProtocol -import org.thoughtcrime.securesms.loki.utilities.get -import org.thoughtcrime.securesms.loki.utilities.getString +import org.thoughtcrime.securesms.groups.OpenGroupManager +import org.thoughtcrime.securesms.util.SessionMetaProtocol import org.thoughtcrime.securesms.mms.PartAuthority class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context, helper), StorageProtocol { @@ -99,7 +96,7 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context, } else -> Optional.absent() } - val pointerAttachments = attachments.mapNotNull { + val pointers = attachments.mapNotNull { it.toSignalAttachment() } val targetAddress = if (isUserSender && !message.syncTarget.isNullOrEmpty()) { @@ -115,7 +112,7 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context, val linkPreviews: Optional> = if (linkPreview.isEmpty()) Optional.absent() else Optional.of(linkPreview.mapNotNull { it!! }) val mmsDatabase = DatabaseFactory.getMmsDatabase(context) val insertResult = if (message.sender == getUserPublicKey()) { - val mediaMessage = OutgoingMediaMessage.from(message, targetRecipient, pointerAttachments, quote.orNull(), linkPreviews.orNull()?.firstOrNull()) + val mediaMessage = OutgoingMediaMessage.from(message, targetRecipient, pointers, quote.orNull(), linkPreviews.orNull()?.firstOrNull()) mmsDatabase.insertSecureDecryptedMessageOutbox(mediaMessage, message.threadID ?: -1, message.sentTimestamp!!) } else { // It seems like we have replaced SignalServiceAttachment with SessionServiceAttachment @@ -184,7 +181,7 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context, override fun resumeMessageSendJobIfNeeded(messageSendJobID: String) { val job = DatabaseFactory.getSessionJobDatabase(context).getMessageSendJob(messageSendJobID) ?: return - JobQueue.shared.add(job) + JobQueue.shared.resumePendingSendMessage(job) } override fun isJobCanceled(job: Job): Boolean { @@ -298,6 +295,19 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context, } } + override fun markAsSending(timestamp: Long, author: String) { + val database = DatabaseFactory.getMmsSmsDatabase(context) + val messageRecord = database.getMessageFor(timestamp, author) ?: return + if (messageRecord.isMms) { + val mmsDatabase = DatabaseFactory.getMmsDatabase(context) + mmsDatabase.markAsSending(messageRecord.getId()) + } else { + val smsDatabase = DatabaseFactory.getSmsDatabase(context) + smsDatabase.markAsSending(messageRecord.getId()) + messageRecord.isPending + } + } + override fun markUnidentified(timestamp: Long, author: String) { val database = DatabaseFactory.getMmsSmsDatabase(context) val messageRecord = database.getMessageFor(timestamp, author) ?: return @@ -487,9 +497,9 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context, val mmsDb = DatabaseFactory.getMmsDatabase(context) val cursor = mmsDb.getMessage(mmsId) val reader = mmsDb.readerFor(cursor) - val threadId = reader.next.threadId + val threadId = reader.next?.threadId cursor.close() - return threadId + return threadId ?: -1 } override fun getContactWithSessionID(sessionID: String): Contact? { @@ -504,6 +514,10 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context, DatabaseFactory.getSessionContactDatabase(context).setContact(contact) } + override fun getRecipientForThread(threadId: Long): Recipient? { + return DatabaseFactory.getThreadDatabase(context).getRecipientForThreadId(threadId) + } + override fun getRecipientSettings(address: Address): Recipient.RecipientSettings? { val recipientSettings = DatabaseFactory.getRecipientDatabase(context).getRecipientSettings(address) return if (recipientSettings.isPresent) { recipientSettings.get() } else null diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/ThreadDatabase.java b/app/src/main/java/org/thoughtcrime/securesms/database/ThreadDatabase.java index 2ba85ca66b..16f1f64d4a 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/ThreadDatabase.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/ThreadDatabase.java @@ -49,7 +49,7 @@ import org.thoughtcrime.securesms.database.model.MediaMmsMessageRecord; import org.thoughtcrime.securesms.database.model.MessageRecord; import org.thoughtcrime.securesms.database.model.MmsMessageRecord; import org.thoughtcrime.securesms.database.model.ThreadRecord; -import org.thoughtcrime.securesms.loki.protocol.SessionMetaProtocol; +import org.thoughtcrime.securesms.util.SessionMetaProtocol; import org.thoughtcrime.securesms.mms.Slide; import org.thoughtcrime.securesms.mms.SlideDeck; @@ -582,7 +582,7 @@ public class ThreadDatabase extends Database { } private @Nullable Uri getAttachmentUriFor(MessageRecord record) { - if (!record.isMms() || record.isMmsNotification() || record.isGroupAction()) return null; + if (!record.isMms() || record.isMmsNotification()) return null; SlideDeck slideDeck = ((MediaMmsMessageRecord)record).getSlideDeck(); Slide thumbnail = slideDeck.getThumbnailSlide(); diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/helpers/SQLCipherOpenHelper.java b/app/src/main/java/org/thoughtcrime/securesms/database/helpers/SQLCipherOpenHelper.java index 349f7a082b..8c0763d233 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/helpers/SQLCipherOpenHelper.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/helpers/SQLCipherOpenHelper.java @@ -23,14 +23,13 @@ import org.thoughtcrime.securesms.database.SearchDatabase; import org.thoughtcrime.securesms.database.SmsDatabase; import org.thoughtcrime.securesms.database.ThreadDatabase; import org.session.libsignal.utilities.Log; -import org.thoughtcrime.securesms.loki.database.LokiAPIDatabase; -import org.thoughtcrime.securesms.loki.database.LokiBackupFilesDatabase; -import org.thoughtcrime.securesms.loki.database.LokiMessageDatabase; -import org.thoughtcrime.securesms.loki.database.LokiThreadDatabase; -import org.thoughtcrime.securesms.loki.database.LokiUserDatabase; -import org.thoughtcrime.securesms.loki.database.SessionContactDatabase; -import org.thoughtcrime.securesms.loki.database.SessionJobDatabase; -import org.thoughtcrime.securesms.loki.protocol.ClosedGroupsMigration; +import org.thoughtcrime.securesms.database.LokiAPIDatabase; +import org.thoughtcrime.securesms.database.LokiBackupFilesDatabase; +import org.thoughtcrime.securesms.database.LokiMessageDatabase; +import org.thoughtcrime.securesms.database.LokiThreadDatabase; +import org.thoughtcrime.securesms.database.LokiUserDatabase; +import org.thoughtcrime.securesms.database.SessionContactDatabase; +import org.thoughtcrime.securesms.database.SessionJobDatabase; public class SQLCipherOpenHelper extends SQLiteOpenHelper { @@ -182,8 +181,6 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper { if (oldVersion < lokiV12) { db.execSQL(LokiAPIDatabase.getCreateLastMessageHashValueTable2Command()); - db.execSQL(ClosedGroupsMigration.getCreateCurrentClosedGroupRatchetTableCommand()); - db.execSQL(ClosedGroupsMigration.getCreateClosedGroupPrivateKeyTableCommand()); } if (oldVersion < lokiV13) { @@ -193,10 +190,6 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper { if (oldVersion < lokiV14_BACKUP_FILES) { db.execSQL(LokiBackupFilesDatabase.getCreateTableCommand()); } - - if (oldVersion < lokiV15) { - db.execSQL(ClosedGroupsMigration.getCreateOldClosedGroupRatchetTableCommand()); - } if (oldVersion < lokiV16) { db.execSQL(LokiAPIDatabase.getCreateOpenGroupProfilePictureTableCommand()); @@ -217,7 +210,6 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper { if (oldVersion < lokiV19) { db.execSQL(LokiAPIDatabase.getCreateClosedGroupEncryptionKeyPairsTable()); db.execSQL(LokiAPIDatabase.getCreateClosedGroupPublicKeysTable()); - ClosedGroupsMigration.INSTANCE.perform(db); db.execSQL("DROP TABLE identities"); deleteJobRecords(db, "RetrieveProfileJob"); deleteJobRecords(db, diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/model/DisplayRecord.java b/app/src/main/java/org/thoughtcrime/securesms/database/model/DisplayRecord.java index f592d47e9d..3adb9cbda5 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/model/DisplayRecord.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/model/DisplayRecord.java @@ -17,12 +17,13 @@ package org.thoughtcrime.securesms.database.model; import android.content.Context; -import androidx.annotation.NonNull; import android.text.SpannableString; +import androidx.annotation.NonNull; + +import org.session.libsession.utilities.recipients.Recipient; import org.thoughtcrime.securesms.database.MmsSmsColumns; import org.thoughtcrime.securesms.database.SmsDatabase; -import org.session.libsession.utilities.recipients.Recipient; /** * The base class for all message record models. Encapsulates basic data @@ -33,9 +34,7 @@ import org.session.libsession.utilities.recipients.Recipient; */ public abstract class DisplayRecord { - protected final long type; - private final Recipient recipient; private final long dateSent; private final long dateReceived; @@ -46,8 +45,8 @@ public abstract class DisplayRecord { private final int readReceiptCount; DisplayRecord(String body, Recipient recipient, long dateSent, - long dateReceived, long threadId, int deliveryStatus, int deliveryReceiptCount, - long type, int readReceiptCount) + long dateReceived, long threadId, int deliveryStatus, int deliveryReceiptCount, + long type, int readReceiptCount) { this.threadId = threadId; this.recipient = recipient; @@ -63,138 +62,63 @@ public abstract class DisplayRecord { public @NonNull String getBody() { return body == null ? "" : body; } + public abstract SpannableString getDisplayBody(@NonNull Context context); + public Recipient getRecipient() { return recipient; } + public long getDateSent() { return dateSent; } + public long getDateReceived() { return dateReceived; } + public long getThreadId() { return threadId; } + public int getDeliveryStatus() { return deliveryStatus; } + public int getDeliveryReceiptCount() { return deliveryReceiptCount; } + public int getReadReceiptCount() { return readReceiptCount; } + + public boolean isDelivered() { + return (deliveryStatus >= SmsDatabase.Status.STATUS_COMPLETE + && deliveryStatus < SmsDatabase.Status.STATUS_PENDING) || deliveryReceiptCount > 0; + } + + public boolean isSent() { + return !isFailed() && !isPending(); + } public boolean isFailed() { - return - MmsSmsColumns.Types.isFailedMessageType(type) || - MmsSmsColumns.Types.isPendingSecureSmsFallbackType(type) || - deliveryStatus >= SmsDatabase.Status.STATUS_FAILED; + return MmsSmsColumns.Types.isFailedMessageType(type) + || MmsSmsColumns.Types.isPendingSecureSmsFallbackType(type) + || deliveryStatus >= SmsDatabase.Status.STATUS_FAILED; } public boolean isPending() { - return MmsSmsColumns.Types.isPendingMessageType(type) && - !MmsSmsColumns.Types.isIdentityVerified(type) && - !MmsSmsColumns.Types.isIdentityDefault(type); + return MmsSmsColumns.Types.isPendingMessageType(type) + && !MmsSmsColumns.Types.isIdentityVerified(type) + && !MmsSmsColumns.Types.isIdentityDefault(type); } + public boolean isRead() { return readReceiptCount > 0; } + public boolean isOutgoing() { return MmsSmsColumns.Types.isOutgoingMessageType(type); } - - public abstract SpannableString getDisplayBody(@NonNull Context context); - - public Recipient getRecipient() { - return recipient; - } - - public long getDateSent() { - return dateSent; - } - - public long getDateReceived() { - return dateReceived; - } - - public long getThreadId() { - return threadId; - } - - public boolean isKeyExchange() { - return SmsDatabase.Types.isKeyExchangeType(type); - } - - public boolean isEndSession() { return SmsDatabase.Types.isEndSessionType(type); } - - public boolean isLokiSessionRestoreSent() { return SmsDatabase.Types.isLokiSessionRestoreSentType(type); } - - public boolean isLokiSessionRestoreDone() { return SmsDatabase.Types.isLokiSessionRestoreDoneType(type); } - - // TODO isGroupUpdate and isGroupQuit are kept for compatibility with old update messages, they can be removed later on - public boolean isGroupUpdate() { - return SmsDatabase.Types.isGroupUpdate(type); - } - - public boolean isGroupQuit() { - return SmsDatabase.Types.isGroupQuit(type); - } - public boolean isGroupUpdateMessage() { return SmsDatabase.Types.isGroupUpdateMessage(type); } - - //TODO isGroupAction can be replaced by isGroupUpdateMessage in the code when the 2 functions above are removed - public boolean isGroupAction() { - return isGroupUpdate() || isGroupQuit() || isGroupUpdateMessage(); - } - - public boolean isExpirationTimerUpdate() { - return SmsDatabase.Types.isExpirationTimerUpdate(type); - } - - // Data extraction - - public boolean isMediaSavedExtraction() { - return MmsSmsColumns.Types.isMediaSavedExtraction(type); - } - - public boolean isScreenshotExtraction() { - return MmsSmsColumns.Types.isScreenshotExtraction(type); - } - - public boolean isDataExtraction() { - return isMediaSavedExtraction() || isScreenshotExtraction(); - } - - public boolean isOpenGroupInvitation() { - return MmsSmsColumns.Types.isOpenGroupInvitation(type); - } - + public boolean isExpirationTimerUpdate() { return SmsDatabase.Types.isExpirationTimerUpdate(type); } + public boolean isMediaSavedNotification() { return MmsSmsColumns.Types.isMediaSavedExtraction(type); } + public boolean isScreenshotNotification() { return MmsSmsColumns.Types.isScreenshotExtraction(type); } + public boolean isDataExtractionNotification() { return isMediaSavedNotification() || isScreenshotNotification(); } + public boolean isOpenGroupInvitation() { return MmsSmsColumns.Types.isOpenGroupInvitation(type); } public boolean isCallLog() { return SmsDatabase.Types.isCallLog(type); } - - public boolean isJoined() { - return SmsDatabase.Types.isJoinedType(type); - } - public boolean isIncomingCall() { return SmsDatabase.Types.isIncomingCall(type); } - public boolean isOutgoingCall() { return SmsDatabase.Types.isOutgoingCall(type); } - public boolean isMissedCall() { return SmsDatabase.Types.isMissedCall(type); } - public boolean isVerificationStatusChange() { - return SmsDatabase.Types.isIdentityDefault(type) || SmsDatabase.Types.isIdentityVerified(type); - } - - public int getDeliveryStatus() { - return deliveryStatus; - } - - public int getDeliveryReceiptCount() { - return deliveryReceiptCount; - } - - public int getReadReceiptCount() { - return readReceiptCount; - } - - public boolean isDelivered() { - return (deliveryStatus >= SmsDatabase.Status.STATUS_COMPLETE && - deliveryStatus < SmsDatabase.Status.STATUS_PENDING) || deliveryReceiptCount > 0; - } - - public boolean isRemoteRead() { - return readReceiptCount > 0; - } - - public boolean isPendingInsecureSmsFallback() { - return SmsDatabase.Types.isPendingInsecureSmsFallbackType(type); + public boolean isControlMessage() { + return isGroupUpdateMessage() || isExpirationTimerUpdate() || isDataExtractionNotification(); } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/model/MediaMmsMessageRecord.java b/app/src/main/java/org/thoughtcrime/securesms/database/model/MediaMmsMessageRecord.java index 97b7329bc4..3385ba3a56 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/model/MediaMmsMessageRecord.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/model/MediaMmsMessageRecord.java @@ -17,21 +17,19 @@ package org.thoughtcrime.securesms.database.model; import android.content.Context; +import android.text.SpannableString; import androidx.annotation.NonNull; import androidx.annotation.Nullable; -import android.text.SpannableString; - -import network.loki.messenger.R; -import org.session.libsession.utilities.Contact; import org.session.libsession.messaging.sending_receiving.link_preview.LinkPreview; -import org.thoughtcrime.securesms.database.MmsDatabase; -import org.thoughtcrime.securesms.database.SmsDatabase.Status; +import org.session.libsession.utilities.Contact; import org.session.libsession.utilities.IdentityKeyMismatch; import org.session.libsession.utilities.NetworkFailure; -import org.thoughtcrime.securesms.mms.SlideDeck; import org.session.libsession.utilities.recipients.Recipient; - +import org.thoughtcrime.securesms.database.MmsDatabase; +import org.thoughtcrime.securesms.database.SmsDatabase.Status; +import org.thoughtcrime.securesms.mms.SlideDeck; import java.util.List; +import network.loki.messenger.R; /** * Represents the message record model for MMS messages that contain @@ -42,26 +40,24 @@ import java.util.List; */ public class MediaMmsMessageRecord extends MmsMessageRecord { - private final static String TAG = MediaMmsMessageRecord.class.getSimpleName(); - - private final int partCount; + private final int partCount; public MediaMmsMessageRecord(long id, Recipient conversationRecipient, - Recipient individualRecipient, int recipientDeviceId, - long dateSent, long dateReceived, int deliveryReceiptCount, - long threadId, String body, - @NonNull SlideDeck slideDeck, - int partCount, long mailbox, - List mismatches, - List failures, int subscriptionId, - long expiresIn, long expireStarted, int readReceiptCount, - @Nullable Quote quote, @NonNull List contacts, - @NonNull List linkPreviews, boolean unidentified) + Recipient individualRecipient, int recipientDeviceId, + long dateSent, long dateReceived, int deliveryReceiptCount, + long threadId, String body, + @NonNull SlideDeck slideDeck, + int partCount, long mailbox, + List mismatches, + List failures, int subscriptionId, + long expiresIn, long expireStarted, int readReceiptCount, + @Nullable Quote quote, @NonNull List contacts, + @NonNull List linkPreviews, boolean unidentified) { - super(id, body, conversationRecipient, individualRecipient, recipientDeviceId, dateSent, - dateReceived, threadId, Status.STATUS_NONE, deliveryReceiptCount, mailbox, mismatches, failures, - subscriptionId, expiresIn, expireStarted, slideDeck, readReceiptCount, quote, contacts, - linkPreviews, unidentified); + super(id, body, conversationRecipient, individualRecipient, dateSent, + dateReceived, threadId, Status.STATUS_NONE, deliveryReceiptCount, mailbox, mismatches, failures, + expiresIn, expireStarted, slideDeck, readReceiptCount, quote, contacts, + linkPreviews, unidentified); this.partCount = partCount; } @@ -82,8 +78,6 @@ public class MediaMmsMessageRecord extends MmsMessageRecord { return emphasisAdded(context.getString(R.string.SmsMessageRecord_duplicate_message)); } else if (MmsDatabase.Types.isNoRemoteSessionType(type)) { return emphasisAdded(context.getString(R.string.MmsMessageRecord_mms_message_encrypted_for_non_existing_session)); - } else if (isLegacyMessage()) { - return emphasisAdded(context.getString(R.string.MessageRecord_message_encrypted_with_a_legacy_protocol_version_that_is_no_longer_supported)); } return super.getDisplayBody(context); diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/model/MessageRecord.java b/app/src/main/java/org/thoughtcrime/securesms/database/model/MessageRecord.java index 13a9b4911a..31db5b4514 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/model/MessageRecord.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/model/MessageRecord.java @@ -17,22 +17,18 @@ package org.thoughtcrime.securesms.database.model; import android.content.Context; -import androidx.annotation.NonNull; import android.text.Spannable; import android.text.SpannableString; import android.text.style.RelativeSizeSpan; import android.text.style.StyleSpan; -import network.loki.messenger.R; +import androidx.annotation.NonNull; import org.session.libsession.messaging.sending_receiving.data_extraction.DataExtractionNotificationInfoMessage; import org.session.libsession.messaging.utilities.UpdateMessageBuilder; import org.session.libsession.messaging.utilities.UpdateMessageData; -import org.thoughtcrime.securesms.database.MmsSmsColumns; -import org.thoughtcrime.securesms.database.SmsDatabase; import org.session.libsession.utilities.IdentityKeyMismatch; import org.session.libsession.utilities.NetworkFailure; - import org.session.libsession.utilities.recipients.Recipient; import java.util.List; @@ -46,145 +42,79 @@ import java.util.List; * */ public abstract class MessageRecord extends DisplayRecord { - private final Recipient individualRecipient; - private final int recipientDeviceId; - public final long id; private final List mismatches; private final List networkFailures; - private final int subscriptionId; private final long expiresIn; private final long expireStarted; private final boolean unidentified; + public final long id; + + public abstract boolean isMms(); + public abstract boolean isMmsNotification(); MessageRecord(long id, String body, Recipient conversationRecipient, - Recipient individualRecipient, int recipientDeviceId, - long dateSent, long dateReceived, long threadId, - int deliveryStatus, int deliveryReceiptCount, long type, - List mismatches, - List networkFailures, - int subscriptionId, long expiresIn, long expireStarted, - int readReceiptCount, boolean unidentified) + Recipient individualRecipient, + long dateSent, long dateReceived, long threadId, + int deliveryStatus, int deliveryReceiptCount, long type, + List mismatches, + List networkFailures, + long expiresIn, long expireStarted, + int readReceiptCount, boolean unidentified) { super(body, conversationRecipient, dateSent, dateReceived, - threadId, deliveryStatus, deliveryReceiptCount, type, readReceiptCount); + threadId, deliveryStatus, deliveryReceiptCount, type, readReceiptCount); this.id = id; this.individualRecipient = individualRecipient; - this.recipientDeviceId = recipientDeviceId; this.mismatches = mismatches; this.networkFailures = networkFailures; - this.subscriptionId = subscriptionId; this.expiresIn = expiresIn; this.expireStarted = expireStarted; this.unidentified = unidentified; } - public abstract boolean isMms(); - public abstract boolean isMmsNotification(); - - public boolean isSecure() { - return MmsSmsColumns.Types.isSecureType(type); - } - - public boolean isLegacyMessage() { - return MmsSmsColumns.Types.isLegacyType(type); - } - - @Override - public SpannableString getDisplayBody(@NonNull Context context) { - if(isGroupUpdateMessage()) { - UpdateMessageData updateMessageData = UpdateMessageData.Companion.fromJSON(getBody()); - return new SpannableString(UpdateMessageBuilder.INSTANCE.buildGroupUpdateMessage(context, updateMessageData, getIndividualRecipient().getAddress().serialize(), isOutgoing())); - } else if (isExpirationTimerUpdate()) { - int seconds = (int) (getExpiresIn() / 1000); - return new SpannableString(UpdateMessageBuilder.INSTANCE.buildExpirationTimerMessage(context, seconds, getIndividualRecipient().getAddress().serialize(), isOutgoing())); - } else if (isDataExtraction()) { - if (isScreenshotExtraction()) return new SpannableString((UpdateMessageBuilder.INSTANCE.buildDataExtractionMessage(context, DataExtractionNotificationInfoMessage.Kind.SCREENSHOT, getIndividualRecipient().getAddress().serialize()))); - else if (isMediaSavedExtraction()) return new SpannableString((UpdateMessageBuilder.INSTANCE.buildDataExtractionMessage(context, DataExtractionNotificationInfoMessage.Kind.MEDIA_SAVED, getIndividualRecipient().getAddress().serialize()))); - } - // TODO below lines are left here for compatibility with older group update messages, it can be deleted later on - else if (isGroupUpdate() && isOutgoing()) { - return new SpannableString(context.getString(R.string.MessageRecord_you_updated_group)); - } else if (isGroupUpdate()) { - return new SpannableString(context.getString(R.string.MessageRecord_s_updated_group, getIndividualRecipient().toShortString())); - } else if (isGroupQuit() && isOutgoing()) { - return new SpannableString(context.getString(R.string.MessageRecord_left_group)); - } else if (isGroupQuit()) { - return new SpannableString(context.getString(R.string.ConversationItem_group_action_left, getIndividualRecipient().toShortString())); - } - - return new SpannableString(getBody()); - } - public long getId() { return id; } - - public boolean isPush() { - return SmsDatabase.Types.isPushType(type) && !SmsDatabase.Types.isForcedSms(type); - } - public long getTimestamp() { - if (getRecipient().getAddress().isOpenGroup()) { - return getDateReceived(); - } - if (isPush() && getDateSent() < getDateReceived()) { - return getDateSent(); - } - return getDateReceived(); + return getDateSent(); } - - public boolean isForcedSms() { - return SmsDatabase.Types.isForcedSms(type); + public Recipient getIndividualRecipient() { + return individualRecipient; } - - public boolean isIdentityVerified() { - return SmsDatabase.Types.isIdentityVerified(type); + public long getType() { + return type; } - - public boolean isIdentityDefault() { - return SmsDatabase.Types.isIdentityDefault(type); + public List getNetworkFailures() { + return networkFailures; } - - public boolean isBundleKeyExchange() { - return SmsDatabase.Types.isBundleKeyExchange(type); - } - - public boolean isIdentityUpdate() { - return SmsDatabase.Types.isIdentityUpdate(type); - } - - public boolean isCorruptedKeyExchange() { - return SmsDatabase.Types.isCorruptedKeyExchange(type); - } - - public boolean isInvalidVersionKeyExchange() { - return SmsDatabase.Types.isInvalidVersionKeyExchange(type); - } - - public boolean isUpdate() { - return isGroupAction() || isJoined() || isExpirationTimerUpdate() || isCallLog() || isDataExtraction() || - isEndSession() || isIdentityUpdate() || isIdentityVerified() || isIdentityDefault() || isLokiSessionRestoreSent() || isLokiSessionRestoreDone(); + public long getExpiresIn() { + return expiresIn; } + public long getExpireStarted() { return expireStarted; } public boolean isMediaPending() { return false; } - public Recipient getIndividualRecipient() { - return individualRecipient; + public boolean isUpdate() { + return isExpirationTimerUpdate() || isCallLog() || isDataExtractionNotification(); } - public long getType() { - return type; - } + @Override + public SpannableString getDisplayBody(@NonNull Context context) { + if (isGroupUpdateMessage()) { + UpdateMessageData updateMessageData = UpdateMessageData.Companion.fromJSON(getBody()); + return new SpannableString(UpdateMessageBuilder.INSTANCE.buildGroupUpdateMessage(context, updateMessageData, getIndividualRecipient().getAddress().serialize(), isOutgoing())); + } else if (isExpirationTimerUpdate()) { + int seconds = (int) (getExpiresIn() / 1000); + return new SpannableString(UpdateMessageBuilder.INSTANCE.buildExpirationTimerMessage(context, seconds, getIndividualRecipient().getAddress().serialize(), isOutgoing())); + } else if (isDataExtractionNotification()) { + if (isScreenshotNotification()) return new SpannableString((UpdateMessageBuilder.INSTANCE.buildDataExtractionMessage(context, DataExtractionNotificationInfoMessage.Kind.SCREENSHOT, getIndividualRecipient().getAddress().serialize()))); + else if (isMediaSavedNotification()) return new SpannableString((UpdateMessageBuilder.INSTANCE.buildDataExtractionMessage(context, DataExtractionNotificationInfoMessage.Kind.MEDIA_SAVED, getIndividualRecipient().getAddress().serialize()))); + } - public List getIdentityKeyMismatches() { - return mismatches; - } - - public List getNetworkFailures() { - return networkFailures; + return new SpannableString(getBody()); } protected SpannableString emphasisAdded(String sequence) { @@ -196,25 +126,12 @@ public abstract class MessageRecord extends DisplayRecord { } public boolean equals(Object other) { - return other != null && - other instanceof MessageRecord && - ((MessageRecord) other).getId() == getId() && - ((MessageRecord) other).isMms() == isMms(); + return other instanceof MessageRecord + && ((MessageRecord) other).getId() == getId() + && ((MessageRecord) other).isMms() == isMms(); } public int hashCode() { return (int)getId(); } - - public long getExpiresIn() { - return expiresIn; - } - - public long getExpireStarted() { - return expireStarted; - } - - public boolean isUnidentified() { - return unidentified; - } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/model/MmsMessageRecord.java b/app/src/main/java/org/thoughtcrime/securesms/database/model/MmsMessageRecord.java index 528720547c..937b74ec58 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/model/MmsMessageRecord.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/model/MmsMessageRecord.java @@ -1,43 +1,35 @@ package org.thoughtcrime.securesms.database.model; - import androidx.annotation.NonNull; import androidx.annotation.Nullable; - import org.session.libsession.utilities.Contact; import org.session.libsession.messaging.sending_receiving.link_preview.LinkPreview; import org.session.libsession.utilities.recipients.Recipient; - import org.session.libsession.utilities.IdentityKeyMismatch; import org.session.libsession.utilities.NetworkFailure; import org.thoughtcrime.securesms.mms.Slide; import org.thoughtcrime.securesms.mms.SlideDeck; - - import java.util.LinkedList; import java.util.List; public abstract class MmsMessageRecord extends MessageRecord { - private final @NonNull SlideDeck slideDeck; private final @Nullable Quote quote; private final @NonNull List contacts = new LinkedList<>(); private final @NonNull List linkPreviews = new LinkedList<>(); MmsMessageRecord(long id, String body, Recipient conversationRecipient, - Recipient individualRecipient, int recipientDeviceId, long dateSent, - long dateReceived, long threadId, int deliveryStatus, int deliveryReceiptCount, - long type, List mismatches, - List networkFailures, int subscriptionId, long expiresIn, - long expireStarted, @NonNull SlideDeck slideDeck, int readReceiptCount, - @Nullable Quote quote, @NonNull List contacts, - @NonNull List linkPreviews, boolean unidentified) + Recipient individualRecipient, long dateSent, + long dateReceived, long threadId, int deliveryStatus, int deliveryReceiptCount, + long type, List mismatches, + List networkFailures, long expiresIn, + long expireStarted, @NonNull SlideDeck slideDeck, int readReceiptCount, + @Nullable Quote quote, @NonNull List contacts, + @NonNull List linkPreviews, boolean unidentified) { - super(id, body, conversationRecipient, individualRecipient, recipientDeviceId, dateSent, dateReceived, threadId, deliveryStatus, deliveryReceiptCount, type, mismatches, networkFailures, subscriptionId, expiresIn, expireStarted, readReceiptCount, unidentified); - + super(id, body, conversationRecipient, individualRecipient, dateSent, dateReceived, threadId, deliveryStatus, deliveryReceiptCount, type, mismatches, networkFailures, expiresIn, expireStarted, readReceiptCount, unidentified); this.slideDeck = slideDeck; this.quote = quote; - this.contacts.addAll(contacts); this.linkPreviews.addAll(linkPreviews); } @@ -66,15 +58,12 @@ public abstract class MmsMessageRecord extends MessageRecord { public boolean containsMediaSlide() { return slideDeck.containsMediaSlide(); } - public @Nullable Quote getQuote() { return quote; } - public @NonNull List getSharedContacts() { return contacts; } - public @NonNull List getLinkPreviews() { return linkPreviews; } diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/model/NotificationMmsMessageRecord.java b/app/src/main/java/org/thoughtcrime/securesms/database/model/NotificationMmsMessageRecord.java index 361be34d39..4c3aa98868 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/model/NotificationMmsMessageRecord.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/model/NotificationMmsMessageRecord.java @@ -17,19 +17,17 @@ package org.thoughtcrime.securesms.database.model; import android.content.Context; -import androidx.annotation.NonNull; import android.text.SpannableString; - -import network.loki.messenger.R; -import org.thoughtcrime.securesms.database.MmsDatabase; -import org.thoughtcrime.securesms.database.SmsDatabase.Status; +import androidx.annotation.NonNull; import org.session.libsession.utilities.IdentityKeyMismatch; import org.session.libsession.utilities.NetworkFailure; -import org.thoughtcrime.securesms.mms.SlideDeck; import org.session.libsession.utilities.recipients.Recipient; - +import org.thoughtcrime.securesms.database.MmsDatabase; +import org.thoughtcrime.securesms.database.SmsDatabase.Status; +import org.thoughtcrime.securesms.mms.SlideDeck; import java.util.Collections; import java.util.LinkedList; +import network.loki.messenger.R; /** * Represents the message record model for MMS messages that are @@ -40,7 +38,6 @@ import java.util.LinkedList; */ public class NotificationMmsMessageRecord extends MmsMessageRecord { - private final byte[] contentLocation; private final long messageSize; private final long expiry; @@ -48,16 +45,16 @@ public class NotificationMmsMessageRecord extends MmsMessageRecord { private final byte[] transactionId; public NotificationMmsMessageRecord(long id, Recipient conversationRecipient, - Recipient individualRecipient, int recipientDeviceId, - long dateSent, long dateReceived, int deliveryReceiptCount, - long threadId, byte[] contentLocation, long messageSize, - long expiry, int status, byte[] transactionId, long mailbox, - int subscriptionId, SlideDeck slideDeck, int readReceiptCount) + Recipient individualRecipient, + long dateSent, long dateReceived, int deliveryReceiptCount, + long threadId, byte[] contentLocation, long messageSize, + long expiry, int status, byte[] transactionId, long mailbox, + SlideDeck slideDeck, int readReceiptCount) { - super(id, "", conversationRecipient, individualRecipient, recipientDeviceId, - dateSent, dateReceived, threadId, Status.STATUS_NONE, deliveryReceiptCount, mailbox, - new LinkedList(), new LinkedList(), subscriptionId, - 0, 0, slideDeck, readReceiptCount, null, Collections.emptyList(), Collections.emptyList(), false); + super(id, "", conversationRecipient, individualRecipient, + dateSent, dateReceived, threadId, Status.STATUS_NONE, deliveryReceiptCount, mailbox, + new LinkedList(), new LinkedList(), + 0, 0, slideDeck, readReceiptCount, null, Collections.emptyList(), Collections.emptyList(), false); this.contentLocation = contentLocation; this.messageSize = messageSize; @@ -69,19 +66,15 @@ public class NotificationMmsMessageRecord extends MmsMessageRecord { public byte[] getTransactionId() { return transactionId; } - public int getStatus() { return this.status; } - public byte[] getContentLocation() { return contentLocation; } - public long getMessageSize() { return (messageSize + 1023) / 1024; } - public long getExpiration() { return expiry * 1000; } @@ -91,11 +84,6 @@ public class NotificationMmsMessageRecord extends MmsMessageRecord { return false; } - @Override - public boolean isSecure() { - return false; - } - @Override public boolean isPending() { return false; diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/model/SmsMessageRecord.java b/app/src/main/java/org/thoughtcrime/securesms/database/model/SmsMessageRecord.java index 08328aaaef..319ff6fcaf 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/model/SmsMessageRecord.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/model/SmsMessageRecord.java @@ -19,17 +19,12 @@ package org.thoughtcrime.securesms.database.model; import android.content.Context; import android.text.SpannableString; - import androidx.annotation.NonNull; - -import org.thoughtcrime.securesms.database.MmsSmsColumns; -import org.thoughtcrime.securesms.database.SmsDatabase; import org.session.libsession.utilities.IdentityKeyMismatch; import org.session.libsession.utilities.recipients.Recipient; - +import org.thoughtcrime.securesms.database.SmsDatabase; import java.util.LinkedList; import java.util.List; - import network.loki.messenger.R; /** @@ -41,20 +36,19 @@ import network.loki.messenger.R; public class SmsMessageRecord extends MessageRecord { public SmsMessageRecord(long id, - String body, Recipient recipient, - Recipient individualRecipient, - int recipientDeviceId, - long dateSent, long dateReceived, - int deliveryReceiptCount, - long type, long threadId, - int status, List mismatches, - int subscriptionId, long expiresIn, long expireStarted, - int readReceiptCount, boolean unidentified) + String body, Recipient recipient, + Recipient individualRecipient, + long dateSent, long dateReceived, + int deliveryReceiptCount, + long type, long threadId, + int status, List mismatches, + long expiresIn, long expireStarted, + int readReceiptCount, boolean unidentified) { - super(id, body, recipient, individualRecipient, recipientDeviceId, - dateSent, dateReceived, threadId, status, deliveryReceiptCount, type, - mismatches, new LinkedList<>(), subscriptionId, - expiresIn, expireStarted, readReceiptCount, unidentified); + super(id, body, recipient, individualRecipient, + dateSent, dateReceived, threadId, status, deliveryReceiptCount, type, + mismatches, new LinkedList<>(), + expiresIn, expireStarted, readReceiptCount, unidentified); } public long getType() { @@ -63,33 +57,12 @@ public class SmsMessageRecord extends MessageRecord { @Override public SpannableString getDisplayBody(@NonNull Context context) { - Recipient recipient = getRecipient(); if (SmsDatabase.Types.isFailedDecryptType(type)) { return emphasisAdded(context.getString(R.string.MessageDisplayHelper_bad_encrypted_message)); - } else if (isCorruptedKeyExchange()) { - return emphasisAdded(context.getString(R.string.SmsMessageRecord_received_corrupted_key_exchange_message)); - } else if (isInvalidVersionKeyExchange()) { - return emphasisAdded(context.getString(R.string.SmsMessageRecord_received_key_exchange_message_for_invalid_protocol_version)); - } else if (MmsSmsColumns.Types.isLegacyType(type)) { - return emphasisAdded(context.getString(R.string.MessageRecord_message_encrypted_with_a_legacy_protocol_version_that_is_no_longer_supported)); - } else if (isBundleKeyExchange()) { - return emphasisAdded(context.getString(R.string.SmsMessageRecord_received_message_with_new_safety_number_tap_to_process)); - } else if (isKeyExchange() && isOutgoing()) { - return new SpannableString(""); - } else if (isKeyExchange() && !isOutgoing()) { - return emphasisAdded(context.getString(R.string.ConversationItem_received_key_exchange_message_tap_to_process)); } else if (SmsDatabase.Types.isDuplicateMessageType(type)) { return emphasisAdded(context.getString(R.string.SmsMessageRecord_duplicate_message)); } else if (SmsDatabase.Types.isNoRemoteSessionType(type)) { return emphasisAdded(context.getString(R.string.MessageDisplayHelper_message_encrypted_for_non_existing_session)); - } else if (isLokiSessionRestoreSent()) { - return emphasisAdded(context.getString(R.string.SmsMessageRecord_secure_session_reset)); - } else if (isLokiSessionRestoreDone()) { - return emphasisAdded(context.getString(R.string.view_reset_secure_session_done_message)); - } else if (isEndSession() && isOutgoing()) { - return emphasisAdded(context.getString(R.string.SmsMessageRecord_secure_session_reset)); - } else if (isEndSession()) { - return emphasisAdded(context.getString(R.string.SmsMessageRecord_secure_session_reset_s, getIndividualRecipient().toShortString())); } else { return super.getDisplayBody(context); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/model/ThreadRecord.java b/app/src/main/java/org/thoughtcrime/securesms/database/model/ThreadRecord.java index c5091651a2..91c2729573 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/model/ThreadRecord.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/model/ThreadRecord.java @@ -73,22 +73,14 @@ public class ThreadRecord extends DisplayRecord { @Override public SpannableString getDisplayBody(@NonNull Context context) { Recipient recipient = getRecipient(); - if (isGroupUpdate() || isGroupUpdateMessage()) { + if (isGroupUpdateMessage()) { return emphasisAdded(context.getString(R.string.ThreadRecord_group_updated)); - } else if (isGroupQuit()) { - return emphasisAdded(context.getString(R.string.ThreadRecord_left_the_group)); } else if (isOpenGroupInvitation()) { return emphasisAdded(context.getString(R.string.ThreadRecord_open_group_invitation)); - } else if (isKeyExchange()) { - return emphasisAdded(context.getString(R.string.ConversationListItem_key_exchange_message)); } else if (SmsDatabase.Types.isFailedDecryptType(type)) { return emphasisAdded(context.getString(R.string.MessageDisplayHelper_bad_encrypted_message)); } else if (SmsDatabase.Types.isNoRemoteSessionType(type)) { return emphasisAdded(context.getString(R.string.MessageDisplayHelper_message_encrypted_for_non_existing_session)); - } else if (isLokiSessionRestoreSent()) { - return emphasisAdded(context.getString(R.string.SmsMessageRecord_secure_session_reset)); - } else if (isLokiSessionRestoreDone()) { - return emphasisAdded(context.getString(R.string.view_reset_secure_session_done_message)); } else if (SmsDatabase.Types.isEndSessionType(type)) { return emphasisAdded(context.getString(R.string.ThreadRecord_secure_session_reset)); } else if (MmsSmsColumns.Types.isLegacyType(type)) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/loki/activities/CreatePrivateChatActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/dms/CreatePrivateChatActivity.kt similarity index 77% rename from app/src/main/java/org/thoughtcrime/securesms/loki/activities/CreatePrivateChatActivity.kt rename to app/src/main/java/org/thoughtcrime/securesms/dms/CreatePrivateChatActivity.kt index 54c80227a2..152dafaafd 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/loki/activities/CreatePrivateChatActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/dms/CreatePrivateChatActivity.kt @@ -1,4 +1,4 @@ -package org.thoughtcrime.securesms.loki.activities +package org.thoughtcrime.securesms.dms import android.animation.Animator import android.animation.AnimatorListenerAdapter @@ -7,35 +7,44 @@ import android.content.ClipboardManager import android.content.Context import android.content.Intent import android.os.Bundle -import androidx.fragment.app.Fragment -import androidx.fragment.app.FragmentPagerAdapter import android.text.InputType +import android.util.Log +import android.util.TypedValue import android.view.* import android.view.inputmethod.EditorInfo import android.view.inputmethod.InputMethodManager import android.widget.Toast -import kotlinx.android.synthetic.main.activity_create_private_chat.loader -import kotlinx.android.synthetic.main.activity_create_private_chat.tabLayout -import kotlinx.android.synthetic.main.activity_create_private_chat.viewPager +import androidx.core.view.isVisible +import androidx.fragment.app.Fragment +import androidx.fragment.app.FragmentPagerAdapter +import kotlinx.android.synthetic.main.activity_create_private_chat.* import kotlinx.android.synthetic.main.fragment_enter_public_key.* import network.loki.messenger.R import nl.komponents.kovenant.ui.failUi import nl.komponents.kovenant.ui.successUi import org.session.libsession.snode.SnodeAPI -import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity -import org.thoughtcrime.securesms.conversation.ConversationActivity import org.session.libsession.utilities.Address import org.session.libsession.utilities.DistributionTypes -import org.thoughtcrime.securesms.database.DatabaseFactory -import org.thoughtcrime.securesms.loki.fragments.ScanQRCodeWrapperFragment -import org.thoughtcrime.securesms.loki.fragments.ScanQRCodeWrapperFragmentDelegate -import org.session.libsession.utilities.recipients.Recipient import org.session.libsession.utilities.TextSecurePreferences +import org.session.libsession.utilities.recipients.Recipient import org.session.libsignal.utilities.PublicKeyValidation +import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity +import org.thoughtcrime.securesms.conversation.v2.ConversationActivityV2 +import org.thoughtcrime.securesms.database.DatabaseFactory +import org.thoughtcrime.securesms.util.ScanQRCodeWrapperFragment +import org.thoughtcrime.securesms.util.ScanQRCodeWrapperFragmentDelegate class CreatePrivateChatActivity : PassphraseRequiredActionBarActivity(), ScanQRCodeWrapperFragmentDelegate { private val adapter = CreatePrivateChatActivityAdapter(this) + private var isKeyboardShowing = false + set(value) { + val hasChanged = (field != value) + field = value + if (hasChanged) { + adapter.isKeyboardShowing = value + } + } // region Lifecycle override fun onCreate(savedInstanceState: Bundle?, isReady: Boolean) { @@ -47,11 +56,15 @@ class CreatePrivateChatActivity : PassphraseRequiredActionBarActivity(), ScanQRC // Set up view pager viewPager.adapter = adapter tabLayout.setupWithViewPager(viewPager) - } + rootLayout.viewTreeObserver.addOnGlobalLayoutListener(object : ViewTreeObserver.OnGlobalLayoutListener { - override fun onCreateOptionsMenu(menu: Menu?): Boolean { - menuInflater.inflate(R.menu.menu_done, menu) - return true + override fun onGlobalLayout() { + val diff = rootLayout.rootView.height - rootLayout.height + val displayMetrics = this@CreatePrivateChatActivity.resources.displayMetrics + val estimatedKeyboardHeight = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 200.0f, displayMetrics) + this@CreatePrivateChatActivity.isKeyboardShowing = (diff > estimatedKeyboardHeight) + } + }) } // endregion @@ -73,13 +86,6 @@ class CreatePrivateChatActivity : PassphraseRequiredActionBarActivity(), ScanQRC // endregion // region Interaction - override fun onOptionsItemSelected(item: MenuItem): Boolean { - when(item.itemId) { - R.id.doneButton -> adapter.enterPublicKeyFragment.createPrivateChatIfPossible() - } - return super.onOptionsItemSelected(item) - } - override fun handleQRCodeScanned(hexEncodedPublicKey: String) { createPrivateChatIfPossible(hexEncodedPublicKey) } @@ -106,13 +112,11 @@ class CreatePrivateChatActivity : PassphraseRequiredActionBarActivity(), ScanQRC private fun createPrivateChat(hexEncodedPublicKey: String) { val recipient = Recipient.from(this, Address.fromSerialized(hexEncodedPublicKey), false) - val intent = Intent(this, ConversationActivity::class.java) - intent.putExtra(ConversationActivity.ADDRESS_EXTRA, recipient.address) - intent.putExtra(ConversationActivity.TEXT_EXTRA, getIntent().getStringExtra(ConversationActivity.TEXT_EXTRA)) + val intent = Intent(this, ConversationActivityV2::class.java) + intent.putExtra(ConversationActivityV2.ADDRESS, recipient.address) intent.setDataAndType(getIntent().data, getIntent().type) val existingThread = DatabaseFactory.getThreadDatabase(this).getThreadIdIfExistsFor(recipient) - intent.putExtra(ConversationActivity.THREAD_ID_EXTRA, existingThread) - intent.putExtra(ConversationActivity.DISTRIBUTION_TYPE_EXTRA, DistributionTypes.DEFAULT) + intent.putExtra(ConversationActivityV2.THREAD_ID, existingThread) startActivity(intent) finish() } @@ -122,6 +126,8 @@ class CreatePrivateChatActivity : PassphraseRequiredActionBarActivity(), ScanQRC // region Adapter private class CreatePrivateChatActivityAdapter(val activity: CreatePrivateChatActivity) : FragmentPagerAdapter(activity.supportFragmentManager) { val enterPublicKeyFragment = EnterPublicKeyFragment() + var isKeyboardShowing = false + set(value) { field = value; enterPublicKeyFragment.isKeyboardShowing = isKeyboardShowing } override fun getCount(): Int { return 2 @@ -152,6 +158,8 @@ private class CreatePrivateChatActivityAdapter(val activity: CreatePrivateChatAc // region Enter Public Key Fragment class EnterPublicKeyFragment : Fragment() { + var isKeyboardShowing = false + set(value) { field = value; handleIsKeyboardShowingChanged() } private val hexEncodedPublicKey: String get() { @@ -182,6 +190,11 @@ class EnterPublicKeyFragment : Fragment() { createPrivateChatButton.setOnClickListener { createPrivateChatIfPossible() } } + private fun handleIsKeyboardShowingChanged() { + val optionalContentContainer = optionalContentContainer ?: return + optionalContentContainer.isVisible = !isKeyboardShowing + } + private fun copyPublicKey() { val clipboard = requireActivity().getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager val clip = ClipData.newPlainText("Session ID", hexEncodedPublicKey) @@ -197,9 +210,10 @@ class EnterPublicKeyFragment : Fragment() { startActivity(intent) } - fun createPrivateChatIfPossible() { - val hexEncodedPublicKey = publicKeyEditText.text?.trim().toString() ?: "" - (requireActivity() as CreatePrivateChatActivity).createPrivateChatIfPossible(hexEncodedPublicKey) + private fun createPrivateChatIfPossible() { + val hexEncodedPublicKey = publicKeyEditText.text?.trim().toString() + val activity = requireActivity() as CreatePrivateChatActivity + activity.createPrivateChatIfPossible(hexEncodedPublicKey) } } // endregion diff --git a/app/src/main/java/org/thoughtcrime/securesms/loki/dialogs/ClosedGroupEditingOptionsBottomSheet.kt b/app/src/main/java/org/thoughtcrime/securesms/groups/ClosedGroupEditingOptionsBottomSheet.kt similarity index 94% rename from app/src/main/java/org/thoughtcrime/securesms/loki/dialogs/ClosedGroupEditingOptionsBottomSheet.kt rename to app/src/main/java/org/thoughtcrime/securesms/groups/ClosedGroupEditingOptionsBottomSheet.kt index b2f62cc37f..f5a75041a5 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/loki/dialogs/ClosedGroupEditingOptionsBottomSheet.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/groups/ClosedGroupEditingOptionsBottomSheet.kt @@ -1,4 +1,4 @@ -package org.thoughtcrime.securesms.loki.dialogs +package org.thoughtcrime.securesms.groups import android.os.Bundle import com.google.android.material.bottomsheet.BottomSheetDialogFragment diff --git a/app/src/main/java/org/thoughtcrime/securesms/loki/activities/CreateClosedGroupActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/groups/CreateClosedGroupActivity.kt similarity index 90% rename from app/src/main/java/org/thoughtcrime/securesms/loki/activities/CreateClosedGroupActivity.kt rename to app/src/main/java/org/thoughtcrime/securesms/groups/CreateClosedGroupActivity.kt index f13dee1d46..e8ab3c6337 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/loki/activities/CreateClosedGroupActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/groups/CreateClosedGroupActivity.kt @@ -1,4 +1,4 @@ -package org.thoughtcrime.securesms.loki.activities +package org.thoughtcrime.securesms.groups import android.content.Context import android.content.Intent @@ -17,15 +17,17 @@ import nl.komponents.kovenant.ui.successUi import org.session.libsession.messaging.sending_receiving.MessageSender import org.session.libsession.messaging.sending_receiving.groupSizeLimit import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity -import org.thoughtcrime.securesms.conversation.ConversationActivity + import org.session.libsession.utilities.Address -import org.session.libsession.utilities.DistributionTypes import org.thoughtcrime.securesms.database.DatabaseFactory -import org.thoughtcrime.securesms.loki.utilities.fadeIn -import org.thoughtcrime.securesms.loki.utilities.fadeOut +import org.thoughtcrime.securesms.util.fadeIn +import org.thoughtcrime.securesms.util.fadeOut import org.thoughtcrime.securesms.mms.GlideApp import org.session.libsession.utilities.recipients.Recipient import org.session.libsession.utilities.TextSecurePreferences +import org.thoughtcrime.securesms.conversation.v2.ConversationActivityV2 +import org.thoughtcrime.securesms.contacts.SelectContactsAdapter +import org.thoughtcrime.securesms.contacts.SelectContactsLoader //TODO Refactor to avoid using kotlinx.android.synthetic class CreateClosedGroupActivity : PassphraseRequiredActionBarActivity(), LoaderManager.LoaderCallbacks> { @@ -135,10 +137,9 @@ class CreateClosedGroupActivity : PassphraseRequiredActionBarActivity(), LoaderM // region Convenience private fun openConversationActivity(context: Context, threadId: Long, recipient: Recipient) { - val intent = Intent(context, ConversationActivity::class.java) - intent.putExtra(ConversationActivity.THREAD_ID_EXTRA, threadId) - intent.putExtra(ConversationActivity.DISTRIBUTION_TYPE_EXTRA, DistributionTypes.DEFAULT) - intent.putExtra(ConversationActivity.ADDRESS_EXTRA, recipient.address) + val intent = Intent(context, ConversationActivityV2::class.java) + intent.putExtra(ConversationActivityV2.THREAD_ID, threadId) + intent.putExtra(ConversationActivityV2.ADDRESS, recipient.address) context.startActivity(intent) } // endregion \ No newline at end of file diff --git a/app/src/main/java/org/thoughtcrime/securesms/loki/viewmodel/DefaultGroupsViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/groups/DefaultGroupsViewModel.kt similarity index 87% rename from app/src/main/java/org/thoughtcrime/securesms/loki/viewmodel/DefaultGroupsViewModel.kt rename to app/src/main/java/org/thoughtcrime/securesms/groups/DefaultGroupsViewModel.kt index d4281a6cfb..a9b6662d8b 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/loki/viewmodel/DefaultGroupsViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/groups/DefaultGroupsViewModel.kt @@ -1,10 +1,11 @@ -package org.thoughtcrime.securesms.loki.viewmodel +package org.thoughtcrime.securesms.groups import androidx.lifecycle.ViewModel import androidx.lifecycle.asLiveData import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onStart import org.session.libsession.messaging.open_groups.OpenGroupAPIV2 +import org.thoughtcrime.securesms.util.State typealias DefaultGroups = List typealias GroupState = State diff --git a/app/src/main/java/org/thoughtcrime/securesms/loki/activities/EditClosedGroupActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/groups/EditClosedGroupActivity.kt similarity index 98% rename from app/src/main/java/org/thoughtcrime/securesms/loki/activities/EditClosedGroupActivity.kt rename to app/src/main/java/org/thoughtcrime/securesms/groups/EditClosedGroupActivity.kt index d6508e8975..6f53644190 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/loki/activities/EditClosedGroupActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/groups/EditClosedGroupActivity.kt @@ -1,4 +1,4 @@ -package org.thoughtcrime.securesms.loki.activities +package org.thoughtcrime.securesms.groups import android.content.Context import android.content.Intent @@ -24,15 +24,15 @@ import org.session.libsession.messaging.sending_receiving.groupSizeLimit import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity import org.session.libsession.utilities.Address import org.thoughtcrime.securesms.database.DatabaseFactory -import org.thoughtcrime.securesms.loki.dialogs.ClosedGroupEditingOptionsBottomSheet -import org.thoughtcrime.securesms.loki.utilities.fadeIn -import org.thoughtcrime.securesms.loki.utilities.fadeOut +import org.thoughtcrime.securesms.util.fadeIn +import org.thoughtcrime.securesms.util.fadeOut import org.thoughtcrime.securesms.mms.GlideApp import org.session.libsession.utilities.recipients.Recipient import org.session.libsession.utilities.GroupUtil import org.session.libsession.utilities.TextSecurePreferences import org.session.libsession.utilities.ThemeUtil import org.session.libsignal.utilities.toHexString +import org.thoughtcrime.securesms.contacts.SelectContactsActivity import java.io.IOException class EditClosedGroupActivity : PassphraseRequiredActionBarActivity() { diff --git a/app/src/main/java/org/thoughtcrime/securesms/loki/activities/EditClosedGroupLoader.kt b/app/src/main/java/org/thoughtcrime/securesms/groups/EditClosedGroupLoader.kt similarity index 94% rename from app/src/main/java/org/thoughtcrime/securesms/loki/activities/EditClosedGroupLoader.kt rename to app/src/main/java/org/thoughtcrime/securesms/groups/EditClosedGroupLoader.kt index d2bb17a732..5360f4261d 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/loki/activities/EditClosedGroupLoader.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/groups/EditClosedGroupLoader.kt @@ -1,4 +1,4 @@ -package org.thoughtcrime.securesms.loki.activities +package org.thoughtcrime.securesms.groups import android.content.Context import org.thoughtcrime.securesms.database.DatabaseFactory diff --git a/app/src/main/java/org/thoughtcrime/securesms/loki/activities/EditClosedGroupMembersAdapter.kt b/app/src/main/java/org/thoughtcrime/securesms/groups/EditClosedGroupMembersAdapter.kt similarity index 95% rename from app/src/main/java/org/thoughtcrime/securesms/loki/activities/EditClosedGroupMembersAdapter.kt rename to app/src/main/java/org/thoughtcrime/securesms/groups/EditClosedGroupMembersAdapter.kt index 4a451ba222..b2d0f6255a 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/loki/activities/EditClosedGroupMembersAdapter.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/groups/EditClosedGroupMembersAdapter.kt @@ -1,10 +1,10 @@ -package org.thoughtcrime.securesms.loki.activities +package org.thoughtcrime.securesms.groups import android.content.Context import androidx.recyclerview.widget.RecyclerView import android.view.ViewGroup import org.session.libsession.utilities.Address -import org.thoughtcrime.securesms.loki.views.UserView +import org.thoughtcrime.securesms.contacts.UserView import org.thoughtcrime.securesms.mms.GlideRequests import org.session.libsession.utilities.recipients.Recipient import org.session.libsession.utilities.TextSecurePreferences diff --git a/app/src/main/java/org/thoughtcrime/securesms/loki/activities/JoinPublicChatActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/groups/JoinPublicChatActivity.kt similarity index 90% rename from app/src/main/java/org/thoughtcrime/securesms/loki/activities/JoinPublicChatActivity.kt rename to app/src/main/java/org/thoughtcrime/securesms/groups/JoinPublicChatActivity.kt index 78b7ced770..5d6d559a89 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/loki/activities/JoinPublicChatActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/groups/JoinPublicChatActivity.kt @@ -1,4 +1,4 @@ -package org.thoughtcrime.securesms.loki.activities +package org.thoughtcrime.securesms.groups import android.animation.Animator import android.animation.AnimatorListenerAdapter @@ -25,21 +25,21 @@ import network.loki.messenger.R import okhttp3.HttpUrl import org.session.libsession.messaging.open_groups.OpenGroupAPIV2.DefaultGroup import org.session.libsession.utilities.Address -import org.session.libsession.utilities.DistributionTypes import org.session.libsession.utilities.recipients.Recipient import org.session.libsession.utilities.GroupUtil import org.session.libsignal.utilities.Log import org.session.libsignal.utilities.PublicKeyValidation import org.thoughtcrime.securesms.BaseActionBarActivity import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity -import org.thoughtcrime.securesms.conversation.ConversationActivity +import org.thoughtcrime.securesms.conversation.v2.ConversationActivityV2 +import org.thoughtcrime.securesms.groups.DefaultGroupsViewModel import org.thoughtcrime.securesms.groups.GroupManager -import org.thoughtcrime.securesms.loki.api.OpenGroupManager -import org.thoughtcrime.securesms.loki.fragments.ScanQRCodeWrapperFragment -import org.thoughtcrime.securesms.loki.fragments.ScanQRCodeWrapperFragmentDelegate -import org.thoughtcrime.securesms.loki.protocol.MultiDeviceProtocol -import org.thoughtcrime.securesms.loki.viewmodel.DefaultGroupsViewModel -import org.thoughtcrime.securesms.loki.viewmodel.State +import org.thoughtcrime.securesms.groups.OpenGroupManager +import org.thoughtcrime.securesms.util.ScanQRCodeWrapperFragment +import org.thoughtcrime.securesms.util.ScanQRCodeWrapperFragmentDelegate +import org.thoughtcrime.securesms.util.ConfigurationMessageUtilities +import org.thoughtcrime.securesms.util.State +import java.util.* class JoinPublicChatActivity : PassphraseRequiredActionBarActivity(), ScanQRCodeWrapperFragmentDelegate { private val adapter = JoinPublicChatActivityAdapter(this) @@ -106,7 +106,7 @@ class JoinPublicChatActivity : PassphraseRequiredActionBarActivity(), ScanQRCode } else { throw Exception("No longer supported.") } - MultiDeviceProtocol.forceSyncConfigurationNowIfNeeded(this@JoinPublicChatActivity) + ConfigurationMessageUtilities.forceSyncConfigurationNowIfNeeded(this@JoinPublicChatActivity) withContext(Dispatchers.Main) { val recipient = Recipient.from(this@JoinPublicChatActivity, Address.fromSerialized(groupID), false) openConversationActivity(this@JoinPublicChatActivity, threadID, recipient) @@ -126,10 +126,9 @@ class JoinPublicChatActivity : PassphraseRequiredActionBarActivity(), ScanQRCode // region Convenience private fun openConversationActivity(context: Context, threadId: Long, recipient: Recipient) { - val intent = Intent(context, ConversationActivity::class.java) - intent.putExtra(ConversationActivity.THREAD_ID_EXTRA, threadId) - intent.putExtra(ConversationActivity.DISTRIBUTION_TYPE_EXTRA, DistributionTypes.DEFAULT) - intent.putExtra(ConversationActivity.ADDRESS_EXTRA, recipient.address) + val intent = Intent(context, ConversationActivityV2::class.java) + intent.putExtra(ConversationActivityV2.THREAD_ID, threadId) + intent.putExtra(ConversationActivityV2.ADDRESS, recipient.address) context.startActivity(intent) } // endregion @@ -179,6 +178,7 @@ class EnterChatURLFragment : Fragment() { joinPublicChatButton.setOnClickListener { joinPublicChatIfPossible() } viewModel.defaultRooms.observe(viewLifecycleOwner) { state -> defaultRoomsContainer.isVisible = state is State.Success + defaultRoomsLoaderContainer.isVisible = state is State.Loading defaultRoomsLoader.isVisible = state is State.Loading when (state) { State.Loading -> { @@ -210,7 +210,6 @@ class EnterChatURLFragment : Fragment() { chip.setOnClickListener { (requireActivity() as JoinPublicChatActivity).joinPublicChatIfPossible(defaultGroup.joinURL) } - defaultRoomsGridLayout.addView(chip) } if ((groups.size and 1) != 0) { // This checks that the number of rooms is even @@ -222,7 +221,7 @@ class EnterChatURLFragment : Fragment() { private fun joinPublicChatIfPossible() { val inputMethodManager = requireContext().getSystemService(BaseActionBarActivity.INPUT_METHOD_SERVICE) as InputMethodManager inputMethodManager.hideSoftInputFromWindow(chatURLEditText.windowToken, 0) - val chatURL = chatURLEditText.text.trim().toString().toLowerCase() + val chatURL = chatURLEditText.text.trim().toString().toLowerCase(Locale.US) (requireActivity() as JoinPublicChatActivity).joinPublicChatIfPossible(chatURL) } // endregion diff --git a/app/src/main/java/org/thoughtcrime/securesms/loki/activities/OpenGroupGuidelinesActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/groups/OpenGroupGuidelinesActivity.kt similarity index 96% rename from app/src/main/java/org/thoughtcrime/securesms/loki/activities/OpenGroupGuidelinesActivity.kt rename to app/src/main/java/org/thoughtcrime/securesms/groups/OpenGroupGuidelinesActivity.kt index 316f07ea39..3b7827679e 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/loki/activities/OpenGroupGuidelinesActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/groups/OpenGroupGuidelinesActivity.kt @@ -1,4 +1,4 @@ -package org.thoughtcrime.securesms.loki.activities +package org.thoughtcrime.securesms.groups import android.os.Bundle import kotlinx.android.synthetic.main.activity_open_group_guidelines.* @@ -7,7 +7,6 @@ import org.thoughtcrime.securesms.BaseActionBarActivity class OpenGroupGuidelinesActivity : BaseActionBarActivity() { - // region Lifecycle override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_open_group_guidelines) @@ -49,5 +48,4 @@ class OpenGroupGuidelinesActivity : BaseActionBarActivity() { Trust only those with an admin crown in chat. No admin will ever DM you first. No admin will ever message you for Oxen coins. """.trimIndent() } - // endregion } \ No newline at end of file diff --git a/app/src/main/java/org/thoughtcrime/securesms/loki/api/OpenGroupManager.kt b/app/src/main/java/org/thoughtcrime/securesms/groups/OpenGroupManager.kt similarity index 97% rename from app/src/main/java/org/thoughtcrime/securesms/loki/api/OpenGroupManager.kt rename to app/src/main/java/org/thoughtcrime/securesms/groups/OpenGroupManager.kt index a1b2b208f7..f25e238b0c 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/loki/api/OpenGroupManager.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/groups/OpenGroupManager.kt @@ -1,4 +1,4 @@ -package org.thoughtcrime.securesms.loki.api +package org.thoughtcrime.securesms.groups import android.content.Context import android.graphics.Bitmap @@ -11,7 +11,6 @@ import org.session.libsession.messaging.sending_receiving.pollers.OpenGroupPolle import org.session.libsession.utilities.Util import org.session.libsignal.utilities.ThreadUtils import org.thoughtcrime.securesms.database.DatabaseFactory -import org.thoughtcrime.securesms.groups.GroupManager import org.thoughtcrime.securesms.util.BitmapUtil import java.util.concurrent.Executors @@ -116,6 +115,7 @@ object OpenGroupManager { val lokiThreadDB = DatabaseFactory.getLokiThreadDatabase(context) lokiThreadDB.removeOpenGroupChat(threadID) ThreadUtils.queue { + threadDB.deleteConversation(threadID) // Must be invoked on a background thread GroupManager.deleteGroup(groupID, context) // Must be invoked on a background thread } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/loki/utilities/OpenGroupUtilities.kt b/app/src/main/java/org/thoughtcrime/securesms/groups/OpenGroupUtilities.kt similarity index 97% rename from app/src/main/java/org/thoughtcrime/securesms/loki/utilities/OpenGroupUtilities.kt rename to app/src/main/java/org/thoughtcrime/securesms/groups/OpenGroupUtilities.kt index 2f562e87c6..6729b5ca2e 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/loki/utilities/OpenGroupUtilities.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/groups/OpenGroupUtilities.kt @@ -1,4 +1,4 @@ -package org.thoughtcrime.securesms.loki.utilities +package org.thoughtcrime.securesms.groups import android.content.Context import androidx.annotation.WorkerThread diff --git a/app/src/main/java/org/thoughtcrime/securesms/loki/dialogs/ConversationOptionsBottomSheet.kt b/app/src/main/java/org/thoughtcrime/securesms/home/ConversationOptionsBottomSheet.kt similarity index 97% rename from app/src/main/java/org/thoughtcrime/securesms/loki/dialogs/ConversationOptionsBottomSheet.kt rename to app/src/main/java/org/thoughtcrime/securesms/home/ConversationOptionsBottomSheet.kt index 9c7b51f31b..4664ea09c4 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/loki/dialogs/ConversationOptionsBottomSheet.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/home/ConversationOptionsBottomSheet.kt @@ -1,4 +1,4 @@ -package org.thoughtcrime.securesms.loki.dialogs +package org.thoughtcrime.securesms.home import android.os.Bundle import com.google.android.material.bottomsheet.BottomSheetDialogFragment diff --git a/app/src/main/java/org/thoughtcrime/securesms/loki/views/ConversationView.kt b/app/src/main/java/org/thoughtcrime/securesms/home/ConversationView.kt similarity index 57% rename from app/src/main/java/org/thoughtcrime/securesms/loki/views/ConversationView.kt rename to app/src/main/java/org/thoughtcrime/securesms/home/ConversationView.kt index 7ef90c0129..57681963c1 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/loki/views/ConversationView.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/home/ConversationView.kt @@ -1,42 +1,38 @@ -package org.thoughtcrime.securesms.loki.views +package org.thoughtcrime.securesms.home import android.content.Context +import android.content.res.Resources import android.graphics.Typeface import android.util.AttributeSet +import android.util.TypedValue import android.view.LayoutInflater import android.view.View import android.widget.LinearLayout +import androidx.core.content.ContextCompat +import androidx.core.view.isVisible +import androidx.recyclerview.widget.RecyclerView import kotlinx.android.synthetic.main.view_conversation.view.* import network.loki.messenger.R -import org.session.libsession.messaging.contacts.Contact import org.session.libsession.utilities.recipients.Recipient -import org.session.libsession.utilities.SSKEnvironment -import org.thoughtcrime.securesms.database.DatabaseFactory import org.thoughtcrime.securesms.database.model.ThreadRecord -import org.thoughtcrime.securesms.loki.utilities.MentionManagerUtilities.populateUserPublicKeyCacheIfNeeded -import org.thoughtcrime.securesms.loki.utilities.MentionUtilities.highlightMentions +import org.thoughtcrime.securesms.conversation.v2.utilities.MentionManagerUtilities.populateUserPublicKeyCacheIfNeeded +import org.thoughtcrime.securesms.conversation.v2.utilities.MentionUtilities.highlightMentions import org.thoughtcrime.securesms.mms.GlideRequests import org.thoughtcrime.securesms.util.DateUtils import java.util.* class ConversationView : LinearLayout { + private val screenWidth = Resources.getSystem().displayMetrics.widthPixels var thread: ThreadRecord? = null // region Lifecycle - constructor(context: Context) : super(context) { - setUpViewHierarchy() - } + 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, attrs: AttributeSet) : super(context, attrs) { - setUpViewHierarchy() - } - - constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr) { - setUpViewHierarchy() - } - - private fun setUpViewHierarchy() { + private fun initialize() { LayoutInflater.from(context).inflate(R.layout.view_conversation, this) + layoutParams = RecyclerView.LayoutParams(screenWidth, RecyclerView.LayoutParams.WRAP_CONTENT) } // endregion @@ -44,23 +40,30 @@ class ConversationView : LinearLayout { fun bind(thread: ThreadRecord, isTyping: Boolean, glide: GlideRequests) { this.thread = thread populateUserPublicKeyCacheIfNeeded(thread.threadId, context) // FIXME: This is a bad place to do this + val unreadCount = thread.unreadCount if (thread.recipient.isBlocked) { accentView.setBackgroundResource(R.color.destructive) accentView.visibility = View.VISIBLE } else { accentView.setBackgroundResource(R.color.accent) - accentView.visibility = if (thread.unreadCount > 0) View.VISIBLE else View.INVISIBLE + accentView.visibility = if (unreadCount > 0) View.VISIBLE else View.INVISIBLE } + val formattedUnreadCount = if (unreadCount < 100) unreadCount.toString() else "99+" + unreadCountTextView.text = formattedUnreadCount + val textSize = if (unreadCount < 100) 12.0f else 9.0f + unreadCountTextView.setTextSize(TypedValue.COMPLEX_UNIT_DIP, textSize) + unreadCountTextView.setTypeface(Typeface.DEFAULT, if (unreadCount < 100) Typeface.BOLD else Typeface.NORMAL) + unreadCountIndicator.isVisible = (unreadCount != 0) profilePictureView.glide = glide profilePictureView.update(thread.recipient, thread.threadId) val senderDisplayName = getUserDisplayName(thread.recipient) ?: thread.recipient.address.toString() - btnGroupNameDisplay.text = senderDisplayName - timestampTextView.text = DateUtils.getBriefRelativeTimeSpanString(context, Locale.getDefault(), thread.date) + conversationViewDisplayNameTextView.text = senderDisplayName + timestampTextView.text = DateUtils.getDisplayFormattedTimeSpanString(context, Locale.getDefault(), thread.date) muteIndicatorImageView.visibility = if (thread.recipient.isMuted) VISIBLE else GONE val rawSnippet = thread.getDisplayBody(context) val snippet = highlightMentions(rawSnippet, thread.threadId, context) snippetTextView.text = snippet - snippetTextView.typeface = if (thread.unreadCount > 0) Typeface.DEFAULT_BOLD else Typeface.DEFAULT + snippetTextView.typeface = if (unreadCount > 0) Typeface.DEFAULT_BOLD else Typeface.DEFAULT snippetTextView.visibility = if (isTyping) View.GONE else View.VISIBLE if (isTyping) { typingIndicatorView.startAnimation() @@ -71,9 +74,13 @@ class ConversationView : LinearLayout { statusIndicatorImageView.visibility = View.VISIBLE when { !thread.isOutgoing -> statusIndicatorImageView.visibility = View.GONE - thread.isFailed -> statusIndicatorImageView.setImageResource(R.drawable.ic_error) + thread.isFailed -> { + val drawable = ContextCompat.getDrawable(context, R.drawable.ic_error)?.mutate() + drawable?.setTint(ContextCompat.getColor(context,R.color.destructive)) + statusIndicatorImageView.setImageDrawable(drawable) + } thread.isPending -> statusIndicatorImageView.setImageResource(R.drawable.ic_circle_dot_dot_dot) - thread.isRemoteRead -> statusIndicatorImageView.setImageResource(R.drawable.ic_filled_circle_check) + thread.isRead -> statusIndicatorImageView.setImageResource(R.drawable.ic_filled_circle_check) else -> statusIndicatorImageView.setImageResource(R.drawable.ic_circle_check) } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/loki/activities/HomeActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/home/HomeActivity.kt similarity index 91% rename from app/src/main/java/org/thoughtcrime/securesms/loki/activities/HomeActivity.kt rename to app/src/main/java/org/thoughtcrime/securesms/home/HomeActivity.kt index 54cf2f8d7c..608091b05f 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/loki/activities/HomeActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/home/HomeActivity.kt @@ -1,4 +1,4 @@ -package org.thoughtcrime.securesms.loki.activities +package org.thoughtcrime.securesms.home import android.app.AlertDialog import android.content.BroadcastReceiver @@ -29,27 +29,31 @@ import org.greenrobot.eventbus.EventBus import org.greenrobot.eventbus.Subscribe import org.greenrobot.eventbus.ThreadMode import org.session.libsession.messaging.jobs.JobQueue -import org.session.libsession.messaging.mentions.MentionsManager import org.session.libsession.messaging.sending_receiving.MessageSender -import org.session.libsession.messaging.sending_receiving.pollers.OpenGroupPollerV2 import org.session.libsession.utilities.* -import org.session.libsignal.utilities.toHexString +import org.session.libsession.utilities.Util import org.session.libsignal.utilities.ThreadUtils +import org.session.libsignal.utilities.toHexString import org.thoughtcrime.securesms.ApplicationContext import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity -import org.thoughtcrime.securesms.conversation.ConversationActivity +import org.thoughtcrime.securesms.conversation.v2.ConversationActivityV2 +import org.thoughtcrime.securesms.crypto.IdentityKeyUtil import org.thoughtcrime.securesms.database.DatabaseFactory import org.thoughtcrime.securesms.database.model.ThreadRecord -import org.thoughtcrime.securesms.loki.api.OpenGroupManager -import org.thoughtcrime.securesms.loki.dialogs.* -import org.thoughtcrime.securesms.loki.protocol.MultiDeviceProtocol -import org.thoughtcrime.securesms.loki.utilities.* -import org.thoughtcrime.securesms.loki.views.ConversationView -import org.thoughtcrime.securesms.loki.views.NewConversationButtonSetViewDelegate -import org.thoughtcrime.securesms.loki.views.SeedReminderViewDelegate +import org.thoughtcrime.securesms.dms.CreatePrivateChatActivity +import org.thoughtcrime.securesms.groups.CreateClosedGroupActivity +import org.thoughtcrime.securesms.groups.JoinPublicChatActivity +import org.thoughtcrime.securesms.groups.OpenGroupManager +import org.thoughtcrime.securesms.util.ConfigurationMessageUtilities +import org.thoughtcrime.securesms.util.* +import org.thoughtcrime.securesms.onboarding.SeedReminderViewDelegate import org.thoughtcrime.securesms.mms.GlideApp import org.thoughtcrime.securesms.mms.GlideRequests +import org.thoughtcrime.securesms.onboarding.SeedActivity +import org.thoughtcrime.securesms.preferences.SettingsActivity +import org.thoughtcrime.securesms.util.IP2Country import java.io.IOException +import java.util.* class HomeActivity : PassphraseRequiredActionBarActivity(), ConversationClickListener, SeedReminderViewDelegate, NewConversationButtonSetViewDelegate { private lateinit var glide: GlideRequests @@ -155,8 +159,8 @@ class HomeActivity : PassphraseRequiredActionBarActivity(), ConversationClickLis override fun onResume() { super.onResume() - if (TextSecurePreferences.getLocalNumber(this) == null) { - return; } // This can be the case after a secondary device is auto-cleared + if (TextSecurePreferences.getLocalNumber(this) == null) { return; } // This can be the case after a secondary device is auto-cleared + IdentityKeyUtil.checkUpdate(this) profileButton.recycle() // clear cached image before update tje profilePictureView profileButton.update() val hasViewedSeed = TextSecurePreferences.getHasViewedSeed(this) @@ -165,7 +169,7 @@ class HomeActivity : PassphraseRequiredActionBarActivity(), ConversationClickLis } if (TextSecurePreferences.getConfigurationMessageSynced(this)) { lifecycleScope.launch(Dispatchers.IO) { - MultiDeviceProtocol.syncConfigurationIfNeeded(this@HomeActivity) + ConfigurationMessageUtilities.syncConfigurationIfNeeded(this@HomeActivity) } } } @@ -342,13 +346,8 @@ class HomeActivity : PassphraseRequiredActionBarActivity(), ConversationClickLis } private fun openConversation(thread: ThreadRecord) { - val intent = Intent(this, ConversationActivity::class.java) - intent.putExtra(ConversationActivity.ADDRESS_EXTRA, thread.recipient.address) - intent.putExtra(ConversationActivity.THREAD_ID_EXTRA, thread.threadId) - intent.putExtra(ConversationActivity.DISTRIBUTION_TYPE_EXTRA, thread.distributionType) - intent.putExtra(ConversationActivity.TIMING_EXTRA, System.currentTimeMillis()) - intent.putExtra(ConversationActivity.LAST_SEEN_EXTRA, thread.lastSeen) - intent.putExtra(ConversationActivity.STARTING_POSITION_EXTRA, -1) + val intent = Intent(this, ConversationActivityV2::class.java) + intent.putExtra(ConversationActivityV2.THREAD_ID, thread.threadId) push(intent) } diff --git a/app/src/main/java/org/thoughtcrime/securesms/loki/activities/HomeAdapter.kt b/app/src/main/java/org/thoughtcrime/securesms/home/HomeAdapter.kt similarity index 94% rename from app/src/main/java/org/thoughtcrime/securesms/loki/activities/HomeAdapter.kt rename to app/src/main/java/org/thoughtcrime/securesms/home/HomeAdapter.kt index 6005b53b8d..ba718d355f 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/loki/activities/HomeAdapter.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/home/HomeAdapter.kt @@ -1,4 +1,4 @@ -package org.thoughtcrime.securesms.loki.activities +package org.thoughtcrime.securesms.home import android.content.Context import android.database.Cursor @@ -7,7 +7,6 @@ import android.view.ViewGroup import org.thoughtcrime.securesms.database.CursorRecyclerViewAdapter import org.thoughtcrime.securesms.database.DatabaseFactory import org.thoughtcrime.securesms.database.model.ThreadRecord -import org.thoughtcrime.securesms.loki.views.ConversationView import org.thoughtcrime.securesms.mms.GlideRequests class HomeAdapter(context: Context, cursor: Cursor) : CursorRecyclerViewAdapter(context, cursor) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/loki/activities/HomeLoader.kt b/app/src/main/java/org/thoughtcrime/securesms/home/HomeLoader.kt similarity index 88% rename from app/src/main/java/org/thoughtcrime/securesms/loki/activities/HomeLoader.kt rename to app/src/main/java/org/thoughtcrime/securesms/home/HomeLoader.kt index a48da0c3ca..92e1abacb6 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/loki/activities/HomeLoader.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/home/HomeLoader.kt @@ -1,4 +1,4 @@ -package org.thoughtcrime.securesms.loki.activities +package org.thoughtcrime.securesms.home import android.content.Context import android.database.Cursor diff --git a/app/src/main/java/org/thoughtcrime/securesms/loki/views/NewConversationButtonSetView.kt b/app/src/main/java/org/thoughtcrime/securesms/home/NewConversationButtonSetView.kt similarity index 96% rename from app/src/main/java/org/thoughtcrime/securesms/loki/views/NewConversationButtonSetView.kt rename to app/src/main/java/org/thoughtcrime/securesms/home/NewConversationButtonSetView.kt index e4578e5fa9..1bd4df55d9 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/loki/views/NewConversationButtonSetView.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/home/NewConversationButtonSetView.kt @@ -1,26 +1,24 @@ -package org.thoughtcrime.securesms.loki.views +package org.thoughtcrime.securesms.home -import android.animation.ArgbEvaluator import android.animation.FloatEvaluator import android.animation.PointFEvaluator import android.animation.ValueAnimator import android.content.Context -import android.content.Context.VIBRATOR_SERVICE import android.content.res.ColorStateList import android.graphics.PointF import android.os.Build -import android.os.VibrationEffect -import android.os.VibrationEffect.DEFAULT_AMPLITUDE -import android.os.Vibrator import android.util.AttributeSet import android.view.Gravity +import android.view.HapticFeedbackConstants import android.view.MotionEvent import android.widget.ImageView import android.widget.RelativeLayout import androidx.annotation.ColorRes import androidx.annotation.DrawableRes import network.loki.messenger.R -import org.thoughtcrime.securesms.loki.utilities.* +import org.thoughtcrime.securesms.util.* +import org.thoughtcrime.securesms.util.GlowViewUtilities +import org.thoughtcrime.securesms.util.NewConversationButtonImageView class NewConversationButtonSetView : RelativeLayout { private var expandedButton: Button? = null @@ -162,6 +160,7 @@ class NewConversationButtonSetView : RelativeLayout { private fun setUpViewHierarchy() { disableClipping() + isHapticFeedbackEnabled = true // Set up session button addView(sessionButton) sessionButton.alpha = 0.0f @@ -206,11 +205,10 @@ class NewConversationButtonSetView : RelativeLayout { isExpanded = true expand() } - val vibrator = context.getSystemService(VIBRATOR_SERVICE) as Vibrator - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - vibrator.vibrate(VibrationEffect.createOneShot(50, DEFAULT_AMPLITUDE)) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + performHapticFeedback(HapticFeedbackConstants.CONTEXT_CLICK) } else { - vibrator.vibrate(50) + performHapticFeedback(HapticFeedbackConstants.LONG_PRESS) } } MotionEvent.ACTION_MOVE -> { diff --git a/app/src/main/java/org/thoughtcrime/securesms/loki/activities/PathActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/home/PathActivity.kt similarity index 98% rename from app/src/main/java/org/thoughtcrime/securesms/loki/activities/PathActivity.kt rename to app/src/main/java/org/thoughtcrime/securesms/home/PathActivity.kt index 2a06562bdf..b812d5c0db 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/loki/activities/PathActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/home/PathActivity.kt @@ -1,4 +1,4 @@ -package org.thoughtcrime.securesms.loki.activities +package org.thoughtcrime.securesms.home import android.content.BroadcastReceiver import android.content.Context @@ -22,9 +22,10 @@ import network.loki.messenger.R import org.session.libsession.snode.OnionRequestAPI import org.session.libsignal.utilities.Snode import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity -import org.thoughtcrime.securesms.loki.utilities.* -import org.thoughtcrime.securesms.loki.views.GlowViewUtilities -import org.thoughtcrime.securesms.loki.views.PathDotView +import org.thoughtcrime.securesms.util.* +import org.thoughtcrime.securesms.util.GlowViewUtilities +import org.thoughtcrime.securesms.util.IP2Country +import org.thoughtcrime.securesms.util.PathDotView class PathActivity : PassphraseRequiredActionBarActivity() { private val broadcastReceivers = mutableListOf() diff --git a/app/src/main/java/org/thoughtcrime/securesms/loki/views/PathStatusView.kt b/app/src/main/java/org/thoughtcrime/securesms/home/PathStatusView.kt similarity index 95% rename from app/src/main/java/org/thoughtcrime/securesms/loki/views/PathStatusView.kt rename to app/src/main/java/org/thoughtcrime/securesms/home/PathStatusView.kt index 6990849306..85d54a977f 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/loki/views/PathStatusView.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/home/PathStatusView.kt @@ -1,4 +1,4 @@ -package org.thoughtcrime.securesms.loki.views +package org.thoughtcrime.securesms.home import android.content.BroadcastReceiver import android.content.Context @@ -12,8 +12,8 @@ import android.view.View import androidx.annotation.ColorInt import network.loki.messenger.R import org.session.libsession.snode.OnionRequestAPI -import org.thoughtcrime.securesms.loki.utilities.getColorWithID -import org.thoughtcrime.securesms.loki.utilities.toPx +import org.thoughtcrime.securesms.util.getColorWithID +import org.thoughtcrime.securesms.util.toPx class PathStatusView : View { private val broadcastReceivers = mutableListOf() diff --git a/app/src/main/java/org/thoughtcrime/securesms/loki/dialogs/UserDetailsBottomSheet.kt b/app/src/main/java/org/thoughtcrime/securesms/home/UserDetailsBottomSheet.kt similarity index 97% rename from app/src/main/java/org/thoughtcrime/securesms/loki/dialogs/UserDetailsBottomSheet.kt rename to app/src/main/java/org/thoughtcrime/securesms/home/UserDetailsBottomSheet.kt index c020fc3aee..147216ec6a 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/loki/dialogs/UserDetailsBottomSheet.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/home/UserDetailsBottomSheet.kt @@ -1,4 +1,4 @@ -package org.thoughtcrime.securesms.loki.dialogs +package org.thoughtcrime.securesms.home import android.annotation.SuppressLint import android.content.ClipData @@ -17,7 +17,6 @@ import network.loki.messenger.R import org.session.libsession.messaging.contacts.Contact import org.session.libsession.utilities.Address import org.session.libsession.utilities.recipients.Recipient -import org.session.libsession.utilities.SSKEnvironment import org.thoughtcrime.securesms.database.DatabaseFactory import org.thoughtcrime.securesms.mms.GlideApp diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/JobManagerFactories.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/JobManagerFactories.java index 0a078fc017..028eb0b421 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/JobManagerFactories.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/JobManagerFactories.java @@ -14,7 +14,6 @@ import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraintObserver; import org.thoughtcrime.securesms.jobmanager.impl.NetworkOrCellServiceConstraint; import org.thoughtcrime.securesms.jobmanager.impl.SqlCipherMigrationConstraint; import org.thoughtcrime.securesms.jobmanager.impl.SqlCipherMigrationConstraintObserver; -import org.thoughtcrime.securesms.loki.api.PrepareAttachmentAudioExtrasJob; import java.util.ArrayList; import java.util.Arrays; diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/LocalBackupJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/LocalBackupJob.java index 147db87ce5..e5715db263 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/LocalBackupJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/LocalBackupJob.java @@ -6,7 +6,7 @@ import org.session.libsession.messaging.utilities.Data; import org.session.libsignal.utilities.NoExternalStorageException; import org.thoughtcrime.securesms.jobmanager.Job; import org.session.libsignal.utilities.Log; -import org.thoughtcrime.securesms.loki.database.BackupFileRecord; +import org.thoughtcrime.securesms.database.BackupFileRecord; import org.thoughtcrime.securesms.notifications.NotificationChannels; import org.thoughtcrime.securesms.service.GenericForegroundService; import org.thoughtcrime.securesms.util.BackupUtil; diff --git a/app/src/main/java/org/thoughtcrime/securesms/loki/api/PrepareAttachmentAudioExtrasJob.kt b/app/src/main/java/org/thoughtcrime/securesms/jobs/PrepareAttachmentAudioExtrasJob.kt similarity index 80% rename from app/src/main/java/org/thoughtcrime/securesms/loki/api/PrepareAttachmentAudioExtrasJob.kt rename to app/src/main/java/org/thoughtcrime/securesms/jobs/PrepareAttachmentAudioExtrasJob.kt index 6c1a96d1de..017c335712 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/loki/api/PrepareAttachmentAudioExtrasJob.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/PrepareAttachmentAudioExtrasJob.kt @@ -1,20 +1,17 @@ -package org.thoughtcrime.securesms.loki.api +package org.thoughtcrime.securesms.jobs -import android.media.MediaDataSource import android.os.Build import org.session.libsignal.utilities.Log -import androidx.annotation.RequiresApi import org.greenrobot.eventbus.EventBus import org.session.libsession.messaging.utilities.Data import org.session.libsession.messaging.sending_receiving.attachments.Attachment import org.session.libsession.messaging.sending_receiving.attachments.AttachmentId import org.session.libsession.messaging.sending_receiving.attachments.DatabaseAttachmentAudioExtras +import org.session.libsession.utilities.DecodedAudio +import org.session.libsession.utilities.InputStreamMediaDataSource import org.thoughtcrime.securesms.database.DatabaseFactory import org.thoughtcrime.securesms.jobmanager.Job -import org.thoughtcrime.securesms.jobs.BaseJob -import org.thoughtcrime.securesms.loki.utilities.DecodedAudio import org.thoughtcrime.securesms.mms.PartAuthority -import java.io.InputStream import java.lang.IllegalStateException import java.util.* import java.util.concurrent.TimeUnit @@ -133,35 +130,4 @@ class PrepareAttachmentAudioExtrasJob : BaseJob { /** Gets dispatched once the audio extras have been updated. */ data class AudioExtrasUpdatedEvent(val attachmentId: AttachmentId) - - @RequiresApi(Build.VERSION_CODES.M) - private class InputStreamMediaDataSource: MediaDataSource { - - private val data: ByteArray - - constructor(inputStream: InputStream): super() { - this.data = inputStream.readBytes() - } - - override fun readAt(position: Long, buffer: ByteArray, offset: Int, size: Int): Int { - val length: Int = data.size - if (position >= length) { - return -1 // -1 indicates EOF - } - var actualSize = size - if (position + size > length) { - actualSize -= (position + size - length).toInt() - } - System.arraycopy(data, position.toInt(), buffer, offset, actualSize) - return actualSize - } - - override fun getSize(): Long { - return data.size.toLong() - } - - override fun close() { - // We don't need to close the wrapped stream. - } - } } \ No newline at end of file diff --git a/app/src/main/java/org/thoughtcrime/securesms/linkpreview/LinkPreviewRepository.java b/app/src/main/java/org/thoughtcrime/securesms/linkpreview/LinkPreviewRepository.java index a6f4c801fb..dd2bd7735d 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/linkpreview/LinkPreviewRepository.java +++ b/app/src/main/java/org/thoughtcrime/securesms/linkpreview/LinkPreviewRepository.java @@ -95,7 +95,7 @@ public class LinkPreviewRepository implements InjectableType { private @NonNull RequestController fetchMetadata(@NonNull String url, Callback callback) { Call call = client.newCall(new Request.Builder().url(url).removeHeader("User-Agent").addHeader("User-Agent", - "WhatsApp").cacheControl(NO_CACHE).build()); + "WhatsApp").cacheControl(NO_CACHE).build()); call.enqueue(new okhttp3.Callback() { @Override @@ -186,18 +186,18 @@ public class LinkPreviewRepository implements InjectableType { byte[] bytes = baos.toByteArray(); Uri uri = BlobProvider.getInstance().forData(bytes).createForSingleSessionInMemory(); - return Optional.of(new UriAttachment(uri, - uri, - contentType, - AttachmentTransferProgress.TRANSFER_PROGRESS_STARTED, - bytes.length, - bitmap.getWidth(), - bitmap.getHeight(), - null, - null, - false, - false, - null)); + return Optional.of(new UriAttachment(uri, + uri, + contentType, + AttachmentTransferProgress.TRANSFER_PROGRESS_STARTED, + bytes.length, + bitmap.getWidth(), + bitmap.getHeight(), + null, + null, + false, + false, + null)); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/linkpreview/LinkPreviewViewModel.java b/app/src/main/java/org/thoughtcrime/securesms/linkpreview/LinkPreviewViewModel.java index dbf8c4bf66..8556c232a0 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/linkpreview/LinkPreviewViewModel.java +++ b/app/src/main/java/org/thoughtcrime/securesms/linkpreview/LinkPreviewViewModel.java @@ -74,7 +74,7 @@ public class LinkPreviewViewModel extends ViewModel { activeRequest = null; } - if (!link.isPresent() || !isCursorPositionValid(text, link.get(), cursorStart, cursorEnd)) { + if (!link.isPresent()) { activeUrl = null; linkPreviewState.setValue(LinkPreviewState.forEmpty()); return; diff --git a/app/src/main/java/org/thoughtcrime/securesms/loki/api/PublicChatInfoUpdateWorker.kt b/app/src/main/java/org/thoughtcrime/securesms/loki/api/PublicChatInfoUpdateWorker.kt deleted file mode 100644 index db0e6c5e7f..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/loki/api/PublicChatInfoUpdateWorker.kt +++ /dev/null @@ -1,71 +0,0 @@ -package org.thoughtcrime.securesms.loki.api - -import android.content.Context -import androidx.work.* -import org.session.libsignal.utilities.Log -import org.thoughtcrime.securesms.loki.utilities.OpenGroupUtilities - -/** - * Delegates the [OpenGroupUtilities.updateGroupInfo] call to the work manager. - */ -class PublicChatInfoUpdateWorker(val context: Context, params: WorkerParameters) : Worker(context, params) { - - companion object { - const val TAG = "PublicChatInfoUpdateWorker" - - private const val DATA_KEY_SERVER_URL = "server_uRL" - private const val DATA_KEY_CHANNEL = "channel" - private const val DATA_KEY_ROOM = "room" - - @JvmStatic - fun scheduleInstant(context: Context, serverUrl: String, room :String) { - val workRequest = OneTimeWorkRequestBuilder() - .setConstraints(Constraints.Builder() - .setRequiredNetworkType(NetworkType.CONNECTED) - .build() - ) - .setInputData(workDataOf( - DATA_KEY_SERVER_URL to serverUrl, - DATA_KEY_ROOM to room - )) - .build() - - WorkManager - .getInstance(context) - .enqueue(workRequest) - } - - @JvmStatic - fun scheduleInstant(context: Context, serverURL: String, channel: Long) { - val workRequest = OneTimeWorkRequestBuilder() - .setConstraints(Constraints.Builder() - .setRequiredNetworkType(NetworkType.CONNECTED) - .build() - ) - .setInputData(workDataOf( - DATA_KEY_SERVER_URL to serverURL, - DATA_KEY_CHANNEL to channel - )) - .build() - - WorkManager - .getInstance(context) - .enqueue(workRequest) - } - } - - override fun doWork(): Result { - val serverUrl = inputData.getString(DATA_KEY_SERVER_URL)!! - val room = inputData.getString(DATA_KEY_ROOM) - val openGroupId = "$serverUrl.$room" - return try { - Log.v(TAG, "Updating open group info for $openGroupId.") - OpenGroupUtilities.updateGroupInfo(context, serverUrl, room!!) - Log.v(TAG, "Open group info was successfully updated for $openGroupId.") - Result.success() - } catch (e: Exception) { - Log.e(TAG, "Failed to update open group info for $openGroupId", e) - Result.failure() - } - } -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/loki/dialogs/DeviceEditingOptionsBottomSheet.kt b/app/src/main/java/org/thoughtcrime/securesms/loki/dialogs/DeviceEditingOptionsBottomSheet.kt deleted file mode 100644 index 527f88d1e5..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/loki/dialogs/DeviceEditingOptionsBottomSheet.kt +++ /dev/null @@ -1,24 +0,0 @@ -package org.thoughtcrime.securesms.loki.dialogs - -import android.os.Bundle -import com.google.android.material.bottomsheet.BottomSheetDialogFragment -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import kotlinx.android.synthetic.main.fragment_device_list_bottom_sheet.* -import network.loki.messenger.R - -public class DeviceEditingOptionsBottomSheet : BottomSheetDialogFragment() { - var onEditTapped: (() -> Unit)? = null - var onUnlinkTapped: (() -> Unit)? = null - - override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { - return inflater.inflate(R.layout.fragment_device_list_bottom_sheet, container, false) - } - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - editDisplayNameText.setOnClickListener { onEditTapped?.invoke() } - unlinkDeviceText.setOnClickListener { onUnlinkTapped?.invoke() } - } -} \ No newline at end of file diff --git a/app/src/main/java/org/thoughtcrime/securesms/loki/protocol/ClosedGroupsMigration.kt b/app/src/main/java/org/thoughtcrime/securesms/loki/protocol/ClosedGroupsMigration.kt deleted file mode 100644 index d5a333d8d0..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/loki/protocol/ClosedGroupsMigration.kt +++ /dev/null @@ -1,73 +0,0 @@ -package org.thoughtcrime.securesms.loki.protocol - -import android.content.ContentValues -import org.thoughtcrime.securesms.loki.database.LokiAPIDatabase -import org.thoughtcrime.securesms.loki.utilities.get -import org.thoughtcrime.securesms.loki.utilities.getAll -import org.thoughtcrime.securesms.loki.utilities.getString -import org.thoughtcrime.securesms.loki.utilities.insertOrUpdate -import org.session.libsignal.utilities.Hex -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.utilities.PublicKeyValidation -import org.session.libsignal.utilities.removing05PrefixIfNeeded -import org.session.libsignal.utilities.toHexString -import java.util.* - -object ClosedGroupsMigration { - - public val closedGroupPublicKey = "closed_group_public_key" - // Ratchets - private val oldClosedGroupRatchetTable = "old_closed_group_ratchet_table" - private val currentClosedGroupRatchetTable = "closed_group_ratchet_table" - private val senderPublicKey = "sender_public_key" - private val chainKey = "chain_key" - private val keyIndex = "key_index" - private val messageKeys = "message_keys" - @JvmStatic val createOldClosedGroupRatchetTableCommand - = "CREATE TABLE $oldClosedGroupRatchetTable ($closedGroupPublicKey STRING, $senderPublicKey STRING, $chainKey STRING, " + - "$keyIndex INTEGER DEFAULT 0, $messageKeys TEXT, PRIMARY KEY ($closedGroupPublicKey, $senderPublicKey));" - // Private keys - @JvmStatic val createCurrentClosedGroupRatchetTableCommand - = "CREATE TABLE $currentClosedGroupRatchetTable ($closedGroupPublicKey STRING, $senderPublicKey STRING, $chainKey STRING, " + - "$keyIndex INTEGER DEFAULT 0, $messageKeys TEXT, PRIMARY KEY ($closedGroupPublicKey, $senderPublicKey));" - // Private keys - public val closedGroupPrivateKeyTable = "closed_group_private_key_table" - public val closedGroupPrivateKey = "closed_group_private_key" - @JvmStatic val createClosedGroupPrivateKeyTableCommand - = "CREATE TABLE $closedGroupPrivateKeyTable ($closedGroupPublicKey STRING PRIMARY KEY, $closedGroupPrivateKey STRING);" - - -fun perform(database: net.sqlcipher.database.SQLiteDatabase) { - val publicKeys = database.getAll(closedGroupPrivateKeyTable, null, null) { cursor -> - cursor.getString(closedGroupPublicKey) - }.filter { - PublicKeyValidation.isValid(it) - } - val keyPairs = mutableListOf() - for (publicKey in publicKeys) { - val query = "${closedGroupPublicKey} = ?" - val privateKey = database.get(closedGroupPrivateKeyTable, query, arrayOf( publicKey )) { cursor -> - cursor.getString(closedGroupPrivateKey) - } - val keyPair = ECKeyPair(DjbECPublicKey(Hex.fromStringCondensed(publicKey.removing05PrefixIfNeeded())), DjbECPrivateKey(Hex.fromStringCondensed(privateKey))) - keyPairs.add(keyPair) - val row = ContentValues(1) - row.put(LokiAPIDatabase.groupPublicKey, publicKey) - database.insertOrUpdate(LokiAPIDatabase.closedGroupPublicKeysTable, row, "${LokiAPIDatabase.groupPublicKey} = ?", arrayOf( publicKey )) - } - for (keyPair in keyPairs) { - // In this particular case keyPair.publicKey == groupPublicKey - val timestamp = Date().time.toString() - val index = "${keyPair.publicKey.serialize().toHexString()}-$timestamp" - val encryptionKeyPairPublicKey = keyPair.publicKey.serialize().toHexString().removing05PrefixIfNeeded() - val encryptionKeyPairPrivateKey = keyPair.privateKey.serialize().toHexString() - val row = ContentValues(3) - row.put(LokiAPIDatabase.closedGroupsEncryptionKeyPairIndex, index) - row.put(LokiAPIDatabase.encryptionKeyPairPublicKey, encryptionKeyPairPublicKey) - row.put(LokiAPIDatabase.encryptionKeyPairPrivateKey, encryptionKeyPairPrivateKey) - database.insertOrUpdate(LokiAPIDatabase.closedGroupEncryptionKeyPairsTable, row, "${LokiAPIDatabase.closedGroupsEncryptionKeyPairIndex} = ?", arrayOf( index )) - } - } -} \ No newline at end of file diff --git a/app/src/main/java/org/thoughtcrime/securesms/loki/protocol/ClosedGroupsProtocolV2.kt b/app/src/main/java/org/thoughtcrime/securesms/loki/protocol/ClosedGroupsProtocolV2.kt deleted file mode 100644 index 1ac34bc1e7..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/loki/protocol/ClosedGroupsProtocolV2.kt +++ /dev/null @@ -1,392 +0,0 @@ -package org.thoughtcrime.securesms.loki.protocol - -import android.content.Context -import android.util.Log -import com.google.protobuf.ByteString -import org.session.libsession.messaging.sending_receiving.* -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.protos.SignalServiceProtos.DataMessage -import org.session.libsignal.utilities.removing05PrefixIfNeeded -import org.session.libsignal.utilities.toHexString -import org.thoughtcrime.securesms.database.DatabaseFactory -import org.thoughtcrime.securesms.database.GroupDatabase -import org.thoughtcrime.securesms.loki.api.LokiPushNotificationManager -import org.thoughtcrime.securesms.loki.api.LokiPushNotificationManager.ClosedGroupOperation -import org.thoughtcrime.securesms.loki.database.LokiAPIDatabase - -import org.session.libsession.utilities.Address -import org.session.libsession.utilities.GroupRecord -import org.session.libsession.utilities.recipients.Recipient -import org.session.libsession.utilities.GroupUtil -import org.session.libsession.utilities.TextSecurePreferences - -import java.util.* - -object ClosedGroupsProtocolV2 { - - @JvmStatic - fun handleMessage(context: Context, closedGroupUpdate: DataMessage.ClosedGroupControlMessage, sentTimestamp: Long, groupPublicKey: String, senderPublicKey: String) { - if (!isValid(context, closedGroupUpdate, senderPublicKey, sentTimestamp)) { return } - when (closedGroupUpdate.type) { - DataMessage.ClosedGroupControlMessage.Type.NEW -> handleNewClosedGroup(context, closedGroupUpdate, senderPublicKey, sentTimestamp) - DataMessage.ClosedGroupControlMessage.Type.MEMBERS_REMOVED -> handleClosedGroupMembersRemoved(context, closedGroupUpdate, sentTimestamp, groupPublicKey, senderPublicKey) - DataMessage.ClosedGroupControlMessage.Type.MEMBERS_ADDED -> handleClosedGroupMembersAdded(context, closedGroupUpdate, sentTimestamp, groupPublicKey, senderPublicKey) - DataMessage.ClosedGroupControlMessage.Type.NAME_CHANGE -> handleClosedGroupNameChange(context, closedGroupUpdate, sentTimestamp, groupPublicKey, senderPublicKey) - DataMessage.ClosedGroupControlMessage.Type.MEMBER_LEFT -> handleClosedGroupMemberLeft(context, sentTimestamp, groupPublicKey, senderPublicKey) - DataMessage.ClosedGroupControlMessage.Type.ENCRYPTION_KEY_PAIR -> handleGroupEncryptionKeyPair(context, closedGroupUpdate, groupPublicKey, senderPublicKey) - else -> { - Log.d("Loki","Can't handle closed group update of unknown type: ${closedGroupUpdate.type}") - } - } - } - - private fun isValid(context: Context, closedGroupUpdate: DataMessage.ClosedGroupControlMessage, senderPublicKey: String, sentTimestamp: Long): Boolean { - val record = DatabaseFactory.getMmsSmsDatabase(context).getMessageFor(sentTimestamp, senderPublicKey) - if (record != null) return false - - return when (closedGroupUpdate.type) { - DataMessage.ClosedGroupControlMessage.Type.NEW -> { - (!closedGroupUpdate.publicKey.isEmpty && !closedGroupUpdate.name.isNullOrEmpty() && !(closedGroupUpdate.encryptionKeyPair.privateKey ?: ByteString.copyFrom(ByteArray(0))).isEmpty - && !(closedGroupUpdate.encryptionKeyPair.publicKey ?: ByteString.copyFrom(ByteArray(0))).isEmpty && closedGroupUpdate.membersCount > 0 && closedGroupUpdate.adminsCount > 0) - } - DataMessage.ClosedGroupControlMessage.Type.MEMBERS_ADDED, - DataMessage.ClosedGroupControlMessage.Type.MEMBERS_REMOVED -> { - closedGroupUpdate.membersCount > 0 - } - DataMessage.ClosedGroupControlMessage.Type.MEMBER_LEFT -> { - senderPublicKey.isNotEmpty() - } - DataMessage.ClosedGroupControlMessage.Type.NAME_CHANGE -> { - !closedGroupUpdate.name.isNullOrEmpty() - } - DataMessage.ClosedGroupControlMessage.Type.ENCRYPTION_KEY_PAIR -> true - else -> false - } - } - - public fun handleNewClosedGroup(context: Context, closedGroupUpdate: DataMessage.ClosedGroupControlMessage, senderPublicKey: String, sentTimestamp: Long) { - // Prepare - val userPublicKey = TextSecurePreferences.getLocalNumber(context)!! - val apiDB = DatabaseFactory.getLokiAPIDatabase(context) - // Unwrap the message - val groupPublicKey = closedGroupUpdate.publicKey.toByteArray().toHexString() - val name = closedGroupUpdate.name - val encryptionKeyPairAsProto = closedGroupUpdate.encryptionKeyPair - val members = closedGroupUpdate.membersList.map { it.toByteArray().toHexString() } - val admins = closedGroupUpdate.adminsList.map { it.toByteArray().toHexString() } - // Create the group - val groupID = GroupUtil.doubleEncodeGroupID(groupPublicKey) - val groupDB = DatabaseFactory.getGroupDatabase(context) - val prevGroup = groupDB.getGroup(groupID).orNull() - if (prevGroup != null) { - // Update the group - groupDB.updateTitle(groupID, name) - groupDB.updateMembers(groupID, members.map { Address.fromSerialized(it) }) - } else { - groupDB.create(groupID, name, LinkedList(members.map { Address.fromSerialized(it) }), - null, null, LinkedList(admins.map { Address.fromSerialized(it) }), sentTimestamp) - } - DatabaseFactory.getRecipientDatabase(context).setProfileSharing(Recipient.from(context, Address.fromSerialized(groupID), false), true) - // Add the group to the user's set of public keys to poll for - apiDB.addClosedGroupPublicKey(groupPublicKey) - // Store the encryption key pair - val encryptionKeyPair = ECKeyPair(DjbECPublicKey(encryptionKeyPairAsProto.publicKey.toByteArray().removing05PrefixIfNeeded()), DjbECPrivateKey(encryptionKeyPairAsProto.privateKey.toByteArray())) - apiDB.addClosedGroupEncryptionKeyPair(encryptionKeyPair, groupPublicKey) - // Notify the user (if we didn't make the group) - if (userPublicKey != senderPublicKey) { - DatabaseFactory.getStorage(context).insertIncomingInfoMessage(context, senderPublicKey, groupID, SignalServiceGroup.Type.UPDATE, name, members, admins, sentTimestamp) - } else if (prevGroup == null) { - // only notify if we created this group - val threadID = DatabaseFactory.getLokiThreadDatabase(context).getThreadID(groupID) - DatabaseFactory.getStorage(context).insertOutgoingInfoMessage(context, groupID, SignalServiceGroup.Type.UPDATE, name, members, admins, threadID, sentTimestamp) - } - // Notify the PN server - LokiPushNotificationManager.performOperation(context, ClosedGroupOperation.Subscribe, groupPublicKey, userPublicKey) - } - - fun handleClosedGroupMembersRemoved(context: Context, closedGroupUpdate: DataMessage.ClosedGroupControlMessage, sentTimestamp: Long, groupPublicKey: String, senderPublicKey: String) { - val apiDB = DatabaseFactory.getLokiAPIDatabase(context) - val groupDB = DatabaseFactory.getGroupDatabase(context) - val groupID = GroupUtil.doubleEncodeGroupID(groupPublicKey) - val group = groupDB.getGroup(groupID).orNull() - if (group == null || !group.isActive) { - Log.d("Loki", "Ignoring closed group info message for nonexistent or inactive group.") - return - } - val userPublicKey = TextSecurePreferences.getLocalNumber(context)!! - val name = group.title - // Check common group update logic - val members = group.members.map { it.serialize() } - val admins = group.admins.map { it.toString() } - - // Users that are part of this remove update - val updateMembers = closedGroupUpdate.membersList.map { it.toByteArray().toHexString() } - - if (!isValidGroupUpdate(group, sentTimestamp, senderPublicKey)) { - return - } - // If admin leaves the group is disbanded - val didAdminLeave = admins.any { it in updateMembers } - // newMembers to save is old members minus removed members - val newMembers = members - updateMembers - // user should be posting MEMBERS_LEFT so this should not be encountered - val senderLeft = senderPublicKey in updateMembers - if (senderLeft) { - Log.d("Loki", "Received a MEMBERS_REMOVED instead of a MEMBERS_LEFT from sender $senderPublicKey") - } - val wasCurrentUserRemoved = userPublicKey in updateMembers - - // admin should send a MEMBERS_LEFT message but handled here in case - if (didAdminLeave || wasCurrentUserRemoved) { - disableLocalGroupAndUnsubscribe(context, apiDB, groupPublicKey, groupDB, groupID, userPublicKey) - } else { - val isCurrentUserAdmin = admins.contains(userPublicKey) - groupDB.updateMembers(groupID, newMembers.map { Address.fromSerialized(it) }) - if (isCurrentUserAdmin) { - MessageSender.generateAndSendNewEncryptionKeyPair(groupPublicKey, newMembers) - } - } - val type = - if (senderLeft) SignalServiceGroup.Type.QUIT - else SignalServiceGroup.Type.UPDATE - if (userPublicKey == senderPublicKey) { - val threadID = DatabaseFactory.getLokiThreadDatabase(context).getThreadID(groupID) - DatabaseFactory.getStorage(context).insertOutgoingInfoMessage(context, groupID, type, name, members, admins, threadID, sentTimestamp) - } else { - DatabaseFactory.getStorage(context).insertIncomingInfoMessage(context, senderPublicKey, groupID, type, name, members, admins, sentTimestamp) - } - } - - fun handleClosedGroupMembersAdded(context: Context, closedGroupUpdate: DataMessage.ClosedGroupControlMessage, sentTimestamp: Long, groupPublicKey: String, senderPublicKey: String) { - val userPublicKey = TextSecurePreferences.getLocalNumber(context) - val apiDB = DatabaseFactory.getLokiAPIDatabase(context) - val groupDB = DatabaseFactory.getGroupDatabase(context) - val groupID = GroupUtil.doubleEncodeGroupID(groupPublicKey) - val group = groupDB.getGroup(groupID).orNull() - if (group == null || !group.isActive) { - Log.d("Loki", "Ignoring closed group info message for nonexistent or inactive group.") - return - } - // Check common group update logic - if (!isValidGroupUpdate(group, sentTimestamp, senderPublicKey)) { - return - } - val name = group.title - val members = group.members.map { it.serialize() } - val admins = group.admins.map { it.serialize() } - // Users that are part of this add update - val updateMembers = closedGroupUpdate.membersList.map { it.toByteArray().toHexString() } - // newMembers to save is old members plus members included in this update - val newMembers = members + updateMembers - groupDB.updateMembers(groupID, newMembers.map { Address.fromSerialized(it) }) - if (userPublicKey == senderPublicKey) { - val threadID = DatabaseFactory.getLokiThreadDatabase(context).getThreadID(groupID) - DatabaseFactory.getStorage(context).insertOutgoingInfoMessage(context, groupID, SignalServiceGroup.Type.UPDATE, name, members, admins, threadID, sentTimestamp) - } else { - DatabaseFactory.getStorage(context).insertIncomingInfoMessage(context, senderPublicKey, groupID, SignalServiceGroup.Type.UPDATE, name, members, admins, sentTimestamp) - } - if (userPublicKey in admins) { - // send current encryption key to the latest added members - val encryptionKeyPair = pendingKeyPairs[groupPublicKey]?.orNull() - ?: apiDB.getLatestClosedGroupEncryptionKeyPair(groupPublicKey) - if (encryptionKeyPair == null) { - Log.d("Loki", "Couldn't get encryption key pair for closed group.") - } else { - for (user in updateMembers) { - MessageSender.sendEncryptionKeyPair(groupPublicKey, encryptionKeyPair, setOf(user), targetUser = user, force = false) - } - } - } - } - - fun handleClosedGroupNameChange(context: Context, closedGroupUpdate: DataMessage.ClosedGroupControlMessage, sentTimestamp: Long, groupPublicKey: String, senderPublicKey: String) { - // Check that the sender is a member of the group (before the update) - val userPublicKey = TextSecurePreferences.getLocalNumber(context) - val groupDB = DatabaseFactory.getGroupDatabase(context) - val groupID = GroupUtil.doubleEncodeGroupID(groupPublicKey) - val group = groupDB.getGroup(groupID).orNull() - if (group == null || !group.isActive) { - Log.d("Loki", "Ignoring closed group info message for nonexistent or inactive group.") - return - } - // Check common group update logic - if (!isValidGroupUpdate(group, sentTimestamp, senderPublicKey)) { - return - } - val members = group.members.map { it.serialize() } - val admins = group.admins.map { it.serialize() } - val name = closedGroupUpdate.name - groupDB.updateTitle(groupID, name) - // Notify the user - if (userPublicKey == senderPublicKey) { - val threadID = DatabaseFactory.getLokiThreadDatabase(context).getThreadID(groupID) - DatabaseFactory.getStorage(context).insertOutgoingInfoMessage(context, groupID, SignalServiceGroup.Type.UPDATE, name, members, admins, threadID, sentTimestamp) - } else { - DatabaseFactory.getStorage(context).insertIncomingInfoMessage(context, senderPublicKey, groupID, SignalServiceGroup.Type.UPDATE, name, members, admins, sentTimestamp) - } - } - - private fun handleClosedGroupMemberLeft(context: Context, sentTimestamp: Long, groupPublicKey: String, senderPublicKey: String) { - // Check the user leaving isn't us, will already be handled - val userPublicKey = TextSecurePreferences.getLocalNumber(context)!! - val apiDB = DatabaseFactory.getLokiAPIDatabase(context) - val groupDB = DatabaseFactory.getGroupDatabase(context) - val groupID = GroupUtil.doubleEncodeGroupID(groupPublicKey) - val group = groupDB.getGroup(groupID).orNull() - if (group == null || !group.isActive) { - Log.d("Loki", "Ignoring closed group info message for nonexistent or inactive group.") - return - } - val name = group.title - // Check common group update logic - val members = group.members.map { it.serialize() } - val admins = group.admins.map { it.toString() } - if (!isValidGroupUpdate(group, sentTimestamp, senderPublicKey)) { - return - } - // If the admin leaves the group is disbanded - val didAdminLeave = admins.contains(senderPublicKey) - val updatedMemberList = members - senderPublicKey - val userLeft = (userPublicKey == senderPublicKey) - - // if the admin left, we left, or we are the only remaining member: remove the group - if (didAdminLeave || userLeft) { - disableLocalGroupAndUnsubscribe(context, apiDB, groupPublicKey, groupDB, groupID, userPublicKey) - } else { - val isCurrentUserAdmin = admins.contains(userPublicKey) - groupDB.updateMembers(groupID, updatedMemberList.map { Address.fromSerialized(it) }) - if (isCurrentUserAdmin) { - MessageSender.generateAndSendNewEncryptionKeyPair(groupPublicKey, updatedMemberList) - } - } - // Notify user - if (userLeft) { - val threadID = DatabaseFactory.getLokiThreadDatabase(context).getThreadID(groupID) - DatabaseFactory.getStorage(context).insertOutgoingInfoMessage(context, groupID, SignalServiceGroup.Type.QUIT, name, members, admins, threadID, sentTimestamp) - } else { - DatabaseFactory.getStorage(context).insertIncomingInfoMessage(context, senderPublicKey, groupID, SignalServiceGroup.Type.QUIT, name, members, admins, sentTimestamp) - } - } - - private fun disableLocalGroupAndUnsubscribe(context: Context, apiDB: LokiAPIDatabase, groupPublicKey: String, groupDB: GroupDatabase, groupID: String, userPublicKey: String) { - apiDB.removeClosedGroupPublicKey(groupPublicKey) - // Remove the key pairs - apiDB.removeAllClosedGroupEncryptionKeyPairs(groupPublicKey) - // Mark the group as inactive - groupDB.setActive(groupID, false) - groupDB.removeMember(groupID, Address.fromSerialized(userPublicKey)) - // Notify the PN server - LokiPushNotificationManager.performOperation(context, ClosedGroupOperation.Unsubscribe, groupPublicKey, userPublicKey) - } - - private fun isValidGroupUpdate(group: GroupRecord, - sentTimestamp: Long, - senderPublicKey: String): Boolean { - val oldMembers = group.members.map { it.serialize() } - // Check that the message isn't from before the group was created - // TODO: We should check that formationTimestamp is the sent timestamp of the closed group update that created the group - if (group.formationTimestamp > sentTimestamp) { - Log.d("Loki", "Ignoring closed group update from before thread was created.") - return false - } - // Check that the sender is a member of the group (before the update) - if (senderPublicKey !in oldMembers) { - Log.d("Loki", "Ignoring closed group info message from non-member.") - return false - } - return true - } - - private fun handleGroupEncryptionKeyPair(context: Context, closedGroupUpdate: DataMessage.ClosedGroupControlMessage, groupPublicKey: String, senderPublicKey: String) { - // Prepare - val userPublicKey = TextSecurePreferences.getLocalNumber(context) - val apiDB = DatabaseFactory.getLokiAPIDatabase(context) - val userKeyPair = apiDB.getUserX25519KeyPair() - // Unwrap the message - val groupDB = DatabaseFactory.getGroupDatabase(context) - val groupPublicKeyToUse = when { - groupPublicKey.isNotEmpty() -> groupPublicKey - !closedGroupUpdate.publicKey.isEmpty -> closedGroupUpdate.publicKey.toByteArray().toHexString() - else -> "" - } - val groupID = GroupUtil.doubleEncodeGroupID(groupPublicKeyToUse) - val group = groupDB.getGroup(groupID).orNull() - if (group == null) { - Log.d("Loki", "Ignoring closed group encryption key pair message for nonexistent group.") - return - } - if (!group.admins.map { it.toString() }.contains(senderPublicKey)) { - Log.d("Loki", "Ignoring closed group encryption key pair from non-admin.") - return - } - // Find our wrapper and decrypt it if possible - val wrapper = closedGroupUpdate.wrappersList.firstOrNull { it.publicKey.toByteArray().toHexString() == userPublicKey } ?: return - val encryptedKeyPair = wrapper.encryptedKeyPair.toByteArray() - val plaintext = MessageDecrypter.decrypt(encryptedKeyPair, userKeyPair).first - // Parse it - val proto = SignalServiceProtos.KeyPair.parseFrom(plaintext) - val keyPair = ECKeyPair(DjbECPublicKey(proto.publicKey.toByteArray().removing05PrefixIfNeeded()), DjbECPrivateKey(proto.privateKey.toByteArray())) - // Store it - apiDB.addClosedGroupEncryptionKeyPair(keyPair, groupPublicKeyToUse) - Log.d("Loki", "Received a new closed group encryption key pair") - } - - // region Deprecated - private fun handleClosedGroupUpdate(context: Context, closedGroupUpdate: DataMessage.ClosedGroupControlMessage, sentTimestamp: Long, groupPublicKey: String, senderPublicKey: String) { - // Prepare - val userPublicKey = TextSecurePreferences.getLocalNumber(context)!! - val apiDB = DatabaseFactory.getLokiAPIDatabase(context) - // Unwrap the message - val name = closedGroupUpdate.name - val members = closedGroupUpdate.membersList.map { it.toByteArray().toHexString() } - val groupDB = DatabaseFactory.getGroupDatabase(context) - val groupID = GroupUtil.doubleEncodeGroupID(groupPublicKey) - val group = groupDB.getGroup(groupID).orNull() - if (group == null || !group.isActive) { - Log.d("Loki", "Ignoring closed group info message for nonexistent or inactive group.") - return - } - val oldMembers = group.members.map { it.serialize() } - // Check common group update logic - if (!isValidGroupUpdate(group, sentTimestamp, senderPublicKey)) { - return - } - // Check that the admin wasn't removed unless the group was destroyed entirely - if (!members.contains(group.admins.first().toString()) && members.isNotEmpty()) { - Log.d("Loki", "Ignoring invalid closed group update message.") - return - } - // Remove the group from the user's set of public keys to poll for if the current user was removed - val wasCurrentUserRemoved = !members.contains(userPublicKey) - if (wasCurrentUserRemoved) { - disableLocalGroupAndUnsubscribe(context, apiDB, groupPublicKey, groupDB, groupID, userPublicKey) - } - // Generate and distribute a new encryption key pair if needed - val wasAnyUserRemoved = (members.toSet().intersect(oldMembers) != oldMembers.toSet()) - val isCurrentUserAdmin = group.admins.map { it.toString() }.contains(userPublicKey) - if (wasAnyUserRemoved && isCurrentUserAdmin) { - MessageSender.generateAndSendNewEncryptionKeyPair(groupPublicKey, members) - } - // Update the group - groupDB.updateTitle(groupID, name) - if (!wasCurrentUserRemoved) { - // The call below sets isActive to true, so if the user is leaving we have to use groupDB.remove(...) instead - groupDB.updateMembers(groupID, members.map { Address.fromSerialized(it) }) - } - // Notify the user - val wasSenderRemoved = !members.contains(senderPublicKey) - val type = if (wasSenderRemoved) SignalServiceGroup.Type.QUIT else SignalServiceGroup.Type.UPDATE - val admins = group.admins.map { it.toString() } - if (userPublicKey == senderPublicKey) { - val threadID = DatabaseFactory.getLokiThreadDatabase(context).getThreadID(groupID) - DatabaseFactory.getStorage(context).insertOutgoingInfoMessage(context, groupID, type, name, members, admins, threadID, sentTimestamp) - } else { - DatabaseFactory.getStorage(context).insertIncomingInfoMessage(context, senderPublicKey, groupID, type, name, members, admins, sentTimestamp) - } - } - // endregion -} \ No newline at end of file diff --git a/app/src/main/java/org/thoughtcrime/securesms/loki/views/MessageAudioView.kt b/app/src/main/java/org/thoughtcrime/securesms/loki/views/MessageAudioView.kt deleted file mode 100644 index 76f5dee9c4..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/loki/views/MessageAudioView.kt +++ /dev/null @@ -1,335 +0,0 @@ -package org.thoughtcrime.securesms.loki.views - -import android.content.Context -import android.content.res.ColorStateList -import android.graphics.Color -import android.graphics.PorterDuff -import android.graphics.drawable.AnimatedVectorDrawable -import android.util.AttributeSet -import android.view.View -import android.view.View.OnTouchListener -import android.view.ViewGroup -import android.widget.FrameLayout -import android.widget.ImageView -import android.widget.ProgressBar -import android.widget.TextView -import androidx.annotation.ColorInt -import androidx.core.content.ContextCompat -import kotlinx.coroutines.* -import network.loki.messenger.R -import org.greenrobot.eventbus.EventBus -import org.greenrobot.eventbus.Subscribe -import org.greenrobot.eventbus.ThreadMode -import org.session.libsession.messaging.sending_receiving.attachments.AttachmentTransferProgress -import org.thoughtcrime.securesms.ApplicationContext -import org.session.libsession.messaging.sending_receiving.attachments.DatabaseAttachment -import org.thoughtcrime.securesms.audio.AudioSlidePlayer -import org.thoughtcrime.securesms.components.AnimatingToggle -import org.thoughtcrime.securesms.database.DatabaseFactory -import org.thoughtcrime.securesms.events.PartProgressEvent -import org.session.libsignal.utilities.Log -import org.thoughtcrime.securesms.loki.api.PrepareAttachmentAudioExtrasJob -import org.thoughtcrime.securesms.loki.utilities.getColorWithID -import org.thoughtcrime.securesms.mms.AudioSlide -import org.thoughtcrime.securesms.mms.SlideClickListener -import java.io.IOException -import java.util.* -import java.util.concurrent.TimeUnit - -class MessageAudioView: FrameLayout, AudioSlidePlayer.Listener { - - companion object { - private const val TAG = "AudioViewKt" - } - - private val controlToggle: AnimatingToggle - private val container: ViewGroup - private val playButton: ImageView - private val pauseButton: ImageView - private val downloadButton: ImageView - private val downloadProgress: ProgressBar - private val seekBar: WaveformSeekBar - private val totalDuration: TextView - - private var downloadListener: SlideClickListener? = null - private var audioSlidePlayer: AudioSlidePlayer? = null - - /** Background coroutine scope that is available when the view is attached to a window. */ - private var asyncCoroutineScope: CoroutineScope? = null - - private val loadingAnimation: SeekBarLoadingAnimation - - constructor(context: Context): this(context, null) - - constructor(context: Context, attrs: AttributeSet?): this(context, attrs, 0) - - constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int): super(context, attrs, defStyleAttr) { - View.inflate(context, R.layout.message_audio_view, this) - container = findViewById(R.id.audio_widget_container) - controlToggle = findViewById(R.id.control_toggle) - playButton = findViewById(R.id.play) - pauseButton = findViewById(R.id.pause) - downloadButton = findViewById(R.id.download) - downloadProgress = findViewById(R.id.download_progress) - seekBar = findViewById(R.id.seek) - totalDuration = findViewById(R.id.total_duration) - - playButton.setOnClickListener { - try { - Log.d(TAG, "playbutton onClick") - if (audioSlidePlayer != null) { - togglePlayToPause() - - // Restart the playback if progress bar is nearly at the end. - val progress = if (seekBar.progress < 0.99f) seekBar.progress.toDouble() else 0.0 - - audioSlidePlayer!!.play(progress) - } - } catch (e: IOException) { - Log.w(TAG, e) - } - } - pauseButton.setOnClickListener { - Log.d(TAG, "pausebutton onClick") - if (audioSlidePlayer != null) { - togglePauseToPlay() - audioSlidePlayer!!.stop() - } - } - seekBar.isEnabled = false - seekBar.progressChangeListener = object : WaveformSeekBar.ProgressChangeListener { - override fun onProgressChanged(waveformSeekBar: WaveformSeekBar, progress: Float, fromUser: Boolean) { - if (fromUser && audioSlidePlayer != null) { - synchronized(audioSlidePlayer!!) { - audioSlidePlayer!!.seekTo(progress.toDouble()) - } - } - } - } - - playButton.setImageDrawable(ContextCompat.getDrawable(context, R.drawable.play_icon)) - pauseButton.setImageDrawable(ContextCompat.getDrawable(context, R.drawable.pause_icon)) - playButton.background = ContextCompat.getDrawable(context, R.drawable.ic_circle_fill_white_48dp) - pauseButton.background = ContextCompat.getDrawable(context, R.drawable.ic_circle_fill_white_48dp) - - if (attrs != null) { - val typedArray = context.theme.obtainStyledAttributes(attrs, R.styleable.MessageAudioView, 0, 0) - setTint(typedArray.getColor(R.styleable.MessageAudioView_foregroundTintColor, Color.WHITE), - typedArray.getColor(R.styleable.MessageAudioView_waveformFillColor, Color.WHITE), - typedArray.getColor(R.styleable.MessageAudioView_waveformBackgroundColor, Color.WHITE)) - container.setBackgroundColor(typedArray.getColor(R.styleable.MessageAudioView_widgetBackground, Color.TRANSPARENT)) - typedArray.recycle() - } - - loadingAnimation = SeekBarLoadingAnimation(this, seekBar) - loadingAnimation.start() - } - - override fun onAttachedToWindow() { - super.onAttachedToWindow() - if (!EventBus.getDefault().isRegistered(this)) EventBus.getDefault().register(this) - - asyncCoroutineScope = CoroutineScope(Job() + Dispatchers.IO) - } - - override fun onDetachedFromWindow() { - super.onDetachedFromWindow() - EventBus.getDefault().unregister(this) - - // Cancel all the background operations. - asyncCoroutineScope!!.cancel() - asyncCoroutineScope = null - } - - fun setAudio(audio: AudioSlide, showControls: Boolean) { - when { - showControls && audio.isPendingDownload -> { - controlToggle.displayQuick(downloadButton) - seekBar.isEnabled = false - downloadButton.setOnClickListener { v -> downloadListener?.onClick(v, audio) } - if (downloadProgress.isIndeterminate) { - downloadProgress.isIndeterminate = false - downloadProgress.progress = 0 - } - } - (showControls && audio.transferState == AttachmentTransferProgress.TRANSFER_PROGRESS_STARTED) -> { - controlToggle.displayQuick(downloadProgress) - seekBar.isEnabled = false - downloadProgress.isIndeterminate = true - } - else -> { - controlToggle.displayQuick(playButton) - seekBar.isEnabled = true - if (downloadProgress.isIndeterminate) { - downloadProgress.isIndeterminate = false - downloadProgress.progress = 100 - } - - // Post to make sure it executes only when the view is attached to a window. - post(::updateFromAttachmentAudioExtras) - } - } - audioSlidePlayer = AudioSlidePlayer.createFor(context, audio, this) - } - - fun cleanup() { - if (audioSlidePlayer != null && pauseButton.visibility == View.VISIBLE) { - audioSlidePlayer!!.stop() - } - } - - fun setDownloadClickListener(listener: SlideClickListener?) { - downloadListener = listener - } - - fun setTint(@ColorInt foregroundTint: Int, @ColorInt waveformFill: Int, @ColorInt waveformBackground: Int) { - playButton.backgroundTintList = ColorStateList.valueOf(resources.getColorWithID(R.color.white, context.theme)) - playButton.imageTintList = ColorStateList.valueOf(resources.getColorWithID(R.color.black, context.theme)) - pauseButton.backgroundTintList = ColorStateList.valueOf(resources.getColorWithID(R.color.white, context.theme)) - pauseButton.imageTintList = ColorStateList.valueOf(resources.getColorWithID(R.color.black, context.theme)) - - downloadButton.setColorFilter(foregroundTint, PorterDuff.Mode.SRC_IN) - - downloadProgress.backgroundTintList = ColorStateList.valueOf(resources.getColorWithID(R.color.white, context.theme)) - downloadProgress.progressTintList = ColorStateList.valueOf(resources.getColorWithID(R.color.black, context.theme)) - downloadProgress.indeterminateTintList = ColorStateList.valueOf(resources.getColorWithID(R.color.black, context.theme)) - - totalDuration.setTextColor(foregroundTint) - - seekBar.barProgressColor = waveformFill - seekBar.barBackgroundColor = waveformBackground - } - - override fun onPlayerStart(player: AudioSlidePlayer) { - if (pauseButton.visibility != View.VISIBLE) { - togglePlayToPause() - } - } - - override fun onPlayerStop(player: AudioSlidePlayer) { - if (playButton.visibility != View.VISIBLE) { - togglePauseToPlay() - } - } - - override fun onPlayerProgress(player: AudioSlidePlayer, progress: Double, millis: Long) { - seekBar.progress = progress.toFloat() - } - - override fun setFocusable(focusable: Boolean) { - super.setFocusable(focusable) - playButton.isFocusable = focusable - pauseButton.isFocusable = focusable - seekBar.isFocusable = focusable - seekBar.isFocusableInTouchMode = focusable - downloadButton.isFocusable = focusable - } - - override fun setClickable(clickable: Boolean) { - super.setClickable(clickable) - playButton.isClickable = clickable - pauseButton.isClickable = clickable - seekBar.isClickable = clickable - seekBar.setOnTouchListener(if (clickable) null else - OnTouchListener { _, _ -> return@OnTouchListener true }) // Suppress touch events. - downloadButton.isClickable = clickable - } - - override fun setEnabled(enabled: Boolean) { - super.setEnabled(enabled) - playButton.isEnabled = enabled - pauseButton.isEnabled = enabled - downloadButton.isEnabled = enabled - } - - private fun togglePlayToPause() { - controlToggle.displayQuick(pauseButton) - val playToPauseDrawable = ContextCompat.getDrawable(context, R.drawable.play_to_pause_animation) as AnimatedVectorDrawable - pauseButton.setImageDrawable(playToPauseDrawable) - playToPauseDrawable.start() - } - - private fun togglePauseToPlay() { - controlToggle.displayQuick(playButton) - val pauseToPlayDrawable = ContextCompat.getDrawable(context, R.drawable.pause_to_play_animation) as AnimatedVectorDrawable - playButton.setImageDrawable(pauseToPlayDrawable) - pauseToPlayDrawable.start() - } - - private fun obtainDatabaseAttachment(): DatabaseAttachment? { - audioSlidePlayer ?: return null - val attachment = audioSlidePlayer!!.audioSlide.asAttachment() - return if (attachment is DatabaseAttachment) attachment else null - } - - private fun updateFromAttachmentAudioExtras() { - val attachment = obtainDatabaseAttachment() ?: return - - val audioExtras = DatabaseFactory.getAttachmentDatabase(context) - .getAttachmentAudioExtras(attachment.attachmentId) - - // Schedule a job request if no audio extras were generated yet. - if (audioExtras == null) { - ApplicationContext.getInstance(context).jobManager - .add(PrepareAttachmentAudioExtrasJob(attachment.attachmentId)) - return - } - - loadingAnimation.stop() - seekBar.sampleData = audioExtras.visualSamples - - if (audioExtras.durationMs > 0) { - totalDuration.visibility = View.VISIBLE - totalDuration.text = String.format("%02d:%02d", - TimeUnit.MILLISECONDS.toMinutes(audioExtras.durationMs), - TimeUnit.MILLISECONDS.toSeconds(audioExtras.durationMs)) - } - } - - @Subscribe(sticky = true, threadMode = ThreadMode.MAIN) - fun onEvent(event: PartProgressEvent) { - if (audioSlidePlayer != null && event.attachment == audioSlidePlayer!!.audioSlide.asAttachment()) { - val progress = ((event.progress.toFloat() / event.total) * 100f).toInt() - downloadProgress.progress = progress - } - } - - @Subscribe(threadMode = ThreadMode.MAIN) - fun onEvent(event: PrepareAttachmentAudioExtrasJob.AudioExtrasUpdatedEvent) { - if (event.attachmentId == obtainDatabaseAttachment()?.attachmentId) { - updateFromAttachmentAudioExtras() - } - } - - private class SeekBarLoadingAnimation( - private val hostView: View, - private val seekBar: WaveformSeekBar): Runnable { - - private var active = false - - companion object { - private const val UPDATE_PERIOD = 250L // In milliseconds. - private val random = Random() - } - - fun start() { - stop() - active = true - hostView.postDelayed(this, UPDATE_PERIOD) - } - - fun stop() { - active = false - hostView.removeCallbacks(this) - } - - override fun run() { - if (!active) return - - // Generate a random samples with values up to the 50% of the maximum value. - seekBar.sampleData = ByteArray(PrepareAttachmentAudioExtrasJob.VISUAL_RMS_FRAMES) - { (random.nextInt(127) - 64).toByte() } - hostView.postDelayed(this, UPDATE_PERIOD) - } - } -} \ No newline at end of file diff --git a/app/src/main/java/org/thoughtcrime/securesms/loki/views/OpenGroupInvitationView.kt b/app/src/main/java/org/thoughtcrime/securesms/loki/views/OpenGroupInvitationView.kt deleted file mode 100644 index edfc81cf79..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/loki/views/OpenGroupInvitationView.kt +++ /dev/null @@ -1,71 +0,0 @@ -package org.thoughtcrime.securesms.loki.views - -import android.content.Context -import android.util.AttributeSet -import android.view.View -import android.widget.* -import androidx.appcompat.app.AlertDialog -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.GlobalScope -import kotlinx.coroutines.launch -import network.loki.messenger.R -import org.session.libsession.utilities.OpenGroupUrlParser -import org.session.libsignal.utilities.Log -import org.thoughtcrime.securesms.loki.api.OpenGroupManager -import org.thoughtcrime.securesms.loki.protocol.MultiDeviceProtocol -import org.thoughtcrime.securesms.loki.utilities.OpenGroupUtilities - -class OpenGroupInvitationView : FrameLayout { - private val joinButton: ImageView - private val openGroupIconContainer: RelativeLayout - private val openGroupIconImageView: ImageView - private val nameTextView: TextView - private val urlTextView: TextView - private var url: String = "" - - constructor(context: Context): this(context, null) - - constructor(context: Context, attrs: AttributeSet?): this(context, attrs, 0) - - constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int): super(context, attrs, defStyleAttr) { - View.inflate(context, R.layout.open_group_invitation_view, this) - joinButton = findViewById(R.id.join_open_group_button) - openGroupIconContainer = findViewById(R.id.open_group_icon_image_view_container) - openGroupIconImageView = findViewById(R.id.open_group_icon_image_view) - nameTextView = findViewById(R.id.name_text_view) - urlTextView = findViewById(R.id.url_text_view) - joinButton.setOnClickListener { joinOpenGroup(url) } - } - - fun setOpenGroup(name: String, url: String, isOutgoing: Boolean = false) { - nameTextView.text = name - urlTextView.text = OpenGroupUrlParser.trimQueryParameter(url) - this.url = url - joinButton.visibility = if (isOutgoing) View.GONE else View.VISIBLE - openGroupIconContainer.visibility = if (isOutgoing) View.VISIBLE else View.GONE - } - - private fun joinOpenGroup(url: String) { - val openGroup = OpenGroupUrlParser.parseUrl(url) - val builder = AlertDialog.Builder(context) - builder.setTitle(context.getString(R.string.ConversationActivity_join_open_group, nameTextView.text.toString())) - builder.setCancelable(true) - val message: String = - context.getString(R.string.ConversationActivity_join_open_group_confirmation_message, nameTextView.text.toString()) - builder.setMessage(message) - builder.setPositiveButton(R.string.yes) { dialog, _ -> - GlobalScope.launch(Dispatchers.IO) { - try { - dialog.dismiss() - OpenGroupManager.add(openGroup.server, openGroup.room, openGroup.serverPublicKey, context) - MultiDeviceProtocol.forceSyncConfigurationNowIfNeeded(context) - } catch (e: Exception) { - Log.e("Loki", "Failed to join open group.", e) - Toast.makeText(context, R.string.activity_join_public_chat_error, Toast.LENGTH_SHORT).show() - } - } - } - builder.setNegativeButton(R.string.no, null) - builder.show() - } -} \ No newline at end of file diff --git a/app/src/main/java/org/thoughtcrime/securesms/loki/views/WaveformSeekBar.kt b/app/src/main/java/org/thoughtcrime/securesms/loki/views/WaveformSeekBar.kt deleted file mode 100644 index 86028521f1..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/loki/views/WaveformSeekBar.kt +++ /dev/null @@ -1,315 +0,0 @@ -package org.thoughtcrime.securesms.loki.views - -import android.animation.ValueAnimator -import android.content.Context -import android.graphics.Canvas -import android.graphics.Color -import android.graphics.Paint -import android.graphics.RectF -import android.util.AttributeSet -import android.util.TypedValue -import android.view.MotionEvent -import android.view.View -import android.view.ViewConfiguration -import android.view.animation.DecelerateInterpolator -import androidx.core.math.MathUtils -import network.loki.messenger.R -import org.thoughtcrime.securesms.loki.utilities.byteToNormalizedFloat -import kotlin.math.abs -import kotlin.math.max -import kotlin.math.min -import kotlin.math.roundToInt - -class WaveformSeekBar : View { - - companion object { - @JvmStatic - fun dp(context: Context, dp: Float): Float { - return TypedValue.applyDimension( - TypedValue.COMPLEX_UNIT_DIP, - dp, - context.resources.displayMetrics - ) - } - } - - private val sampleDataHolder = SampleDataHolder(::invalidate) - /** An array of signed byte values representing the audio signal. */ - var sampleData: ByteArray? - get() { - return sampleDataHolder.getSamples() - } - set(value) { - sampleDataHolder.setSamples(value) - invalidate() - } - - /** Indicates whether the user is currently interacting with the view and performing a seeking gesture. */ - private var userSeeking = false - private var _progress: Float = 0f - /** In [0..1] range. */ - var progress: Float - set(value) { - // Do not let to modify the progress value from the outside - // when the user is currently interacting with the view. - if (userSeeking) return - - _progress = value - invalidate() - progressChangeListener?.onProgressChanged(this, _progress, false) - } - get() { - return _progress - } - - var barBackgroundColor: Int = Color.LTGRAY - set(value) { - field = value - invalidate() - } - - var barProgressColor: Int = Color.WHITE - set(value) { - field = value - invalidate() - } - - var barGap: Float = dp(context, 2f) - set(value) { - field = value - invalidate() - } - - var barWidth: Float = dp(context, 5f) - set(value) { - field = value - invalidate() - } - - var barMinHeight: Float = barWidth - set(value) { - field = value - invalidate() - } - - var barCornerRadius: Float = dp(context, 2.5f) - set(value) { - field = value - invalidate() - } - - var barGravity: WaveGravity = WaveGravity.CENTER - set(value) { - field = value - invalidate() - } - - var progressChangeListener: ProgressChangeListener? = null - - private val barPaint = Paint(Paint.ANTI_ALIAS_FLAG) - private val barRect = RectF() - - private var canvasWidth = 0 - private var canvasHeight = 0 - - private var touchDownX = 0f - private var touchDownProgress: Float = 0f - private var scaledTouchSlop = ViewConfiguration.get(context).scaledTouchSlop - - constructor(context: Context) : this(context, null) - - constructor(context: Context, attrs: AttributeSet?) : this(context, attrs, 0) - - constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) - : super(context, attrs, defStyleAttr) { - - val typedAttrs = context.obtainStyledAttributes(attrs, R.styleable.WaveformSeekBar) - barWidth = typedAttrs.getDimension(R.styleable.WaveformSeekBar_bar_width, barWidth) - barGap = typedAttrs.getDimension(R.styleable.WaveformSeekBar_bar_gap, barGap) - barCornerRadius = typedAttrs.getDimension( - R.styleable.WaveformSeekBar_bar_corner_radius, - barCornerRadius) - barMinHeight = - typedAttrs.getDimension(R.styleable.WaveformSeekBar_bar_min_height, barMinHeight) - barBackgroundColor = typedAttrs.getColor( - R.styleable.WaveformSeekBar_bar_background_color, - barBackgroundColor) - barProgressColor = - typedAttrs.getColor(R.styleable.WaveformSeekBar_bar_progress_color, barProgressColor) - progress = typedAttrs.getFloat(R.styleable.WaveformSeekBar_progress, progress) - barGravity = WaveGravity.fromString( - typedAttrs.getString(R.styleable.WaveformSeekBar_bar_gravity)) - - typedAttrs.recycle() - } - - override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) { - super.onSizeChanged(w, h, oldw, oldh) - canvasWidth = w - canvasHeight = h - invalidate() - } - - override fun onDraw(canvas: Canvas) { - super.onDraw(canvas) - - val totalWidth = getAvailableWidth() - val barAmount = (totalWidth / (barWidth + barGap)).toInt() - - var lastBarRight = paddingLeft.toFloat() - - (0 until barAmount).forEach { barIdx -> - // Convert a signed byte to a [0..1] float. - val barValue = byteToNormalizedFloat(sampleDataHolder.computeBarValue(barIdx, barAmount)) - - val barHeight = max(barMinHeight, getAvailableHeight() * barValue) - - val top: Float = when (barGravity) { - WaveGravity.TOP -> paddingTop.toFloat() - WaveGravity.CENTER -> paddingTop + getAvailableHeight() * 0.5f - barHeight * 0.5f - WaveGravity.BOTTOM -> canvasHeight - paddingBottom - barHeight - } - - barRect.set(lastBarRight, top, lastBarRight + barWidth, top + barHeight) - - barPaint.color = if (barRect.right <= totalWidth * progress) - barProgressColor else barBackgroundColor - - canvas.drawRoundRect(barRect, barCornerRadius, barCornerRadius, barPaint) - - lastBarRight = barRect.right + barGap - } - } - - override fun onTouchEvent(event: MotionEvent): Boolean { - if (!isEnabled) return false - - when (event.action) { - MotionEvent.ACTION_DOWN -> { - userSeeking = true - touchDownX = event.x - touchDownProgress = progress - updateProgress(event, false) - } - MotionEvent.ACTION_MOVE -> { - // Prevent any parent scrolling if the user scrolled more - // than scaledTouchSlop on horizontal axis. - if (abs(event.x - touchDownX) > scaledTouchSlop) { - parent.requestDisallowInterceptTouchEvent(true) - } - updateProgress(event, false) - } - MotionEvent.ACTION_UP -> { - userSeeking = false - updateProgress(event, true) - performClick() - } - MotionEvent.ACTION_CANCEL -> { - updateProgress(touchDownProgress, false) - userSeeking = false - } - } - return true - } - - private fun updateProgress(event: MotionEvent, notify: Boolean) { - updateProgress(event.x / getAvailableWidth(), notify) - } - - private fun updateProgress(progress: Float, notify: Boolean) { - _progress = MathUtils.clamp(progress, 0f, 1f) - invalidate() - - if (notify) { - progressChangeListener?.onProgressChanged(this, _progress, true) - } - } - - override fun performClick(): Boolean { - super.performClick() - return true - } - - private fun getAvailableWidth() = canvasWidth - paddingLeft - paddingRight - private fun getAvailableHeight() = canvasHeight - paddingTop - paddingBottom - - private class SampleDataHolder(private val invalidateDelegate: () -> Any) { - - private var sampleDataFrom: ByteArray? = null - private var sampleDataTo: ByteArray? = null - private var progress = 1f // Mix between from and to values. - - private var animation: ValueAnimator? = null - - fun computeBarValue(barIdx: Int, barAmount: Int): Byte { - /** @return The array's value at the interpolated index. */ - fun getSampleValue(sampleData: ByteArray?): Byte { - if (sampleData == null || sampleData.isEmpty()) - return Byte.MIN_VALUE - else { - val sampleIdx = (barIdx * (sampleData.size / barAmount.toFloat())).toInt() - return sampleData[sampleIdx] - } - } - - if (progress == 1f) { - return getSampleValue(sampleDataTo) - } - - val fromValue = getSampleValue(sampleDataFrom) - val toValue = getSampleValue(sampleDataTo) - val rawResultValue = fromValue * (1f - progress) + toValue * progress - return rawResultValue.roundToInt().toByte() - } - - fun setSamples(sampleData: ByteArray?) { - /** @return a mix between [sampleDataFrom] and [sampleDataTo] arrays according to the current [progress] value. */ - fun computeNewDataFromArray(): ByteArray? { - if (sampleDataTo == null) return null - if (sampleDataFrom == null) return sampleDataTo - - val sampleSize = min(sampleDataFrom!!.size, sampleDataTo!!.size) - return ByteArray(sampleSize) { i -> computeBarValue(i, sampleSize) } - } - - sampleDataFrom = computeNewDataFromArray() - sampleDataTo = sampleData - progress = 0f - - animation?.cancel() - animation = ValueAnimator.ofFloat(0f, 1f).apply { - addUpdateListener { animation -> - progress = animation.animatedValue as Float - invalidateDelegate() - } - interpolator = DecelerateInterpolator(3f) - duration = 500 - start() - } - } - - fun getSamples(): ByteArray? { - return sampleDataTo - } - } - - enum class WaveGravity { - TOP, - CENTER, - BOTTOM, - ; - - companion object { - @JvmStatic - fun fromString(gravity: String?): WaveGravity = when (gravity) { - "1" -> TOP - "2" -> CENTER - else -> BOTTOM - } - } - } - - interface ProgressChangeListener { - fun onProgressChanged(waveformSeekBar: WaveformSeekBar, progress: Float, fromUser: Boolean) - } -} \ No newline at end of file diff --git a/app/src/main/java/org/thoughtcrime/securesms/longmessage/LongMessageActivity.java b/app/src/main/java/org/thoughtcrime/securesms/longmessage/LongMessageActivity.java index d3c683dd46..04edf1194a 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/longmessage/LongMessageActivity.java +++ b/app/src/main/java/org/thoughtcrime/securesms/longmessage/LongMessageActivity.java @@ -2,36 +2,20 @@ package org.thoughtcrime.securesms.longmessage; import android.content.Context; import android.content.Intent; -import android.graphics.PorterDuff; import android.os.Bundle; -import android.text.SpannableString; import android.text.method.LinkMovementMethod; -import android.text.style.URLSpan; -import android.text.util.Linkify; -import android.util.TypedValue; import android.view.MenuItem; -import android.view.View; -import android.view.ViewGroup; import android.widget.TextView; import android.widget.Toast; import androidx.annotation.NonNull; import androidx.lifecycle.ViewModelProvider; -import com.annimon.stream.Stream; - -import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity; -import org.thoughtcrime.securesms.components.ConversationItemFooter; -import org.thoughtcrime.securesms.linkpreview.LinkPreviewUtil; - import org.session.libsession.utilities.Address; -import org.session.libsession.utilities.recipients.Recipient; -import org.session.libsession.utilities.TextSecurePreferences; -import org.session.libsession.utilities.ThemeUtil; import org.session.libsession.utilities.Util; -import org.session.libsession.utilities.Stub; - -import java.util.Locale; +import org.session.libsession.utilities.recipients.Recipient; +import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity; +import org.thoughtcrime.securesms.conversation.v2.utilities.MentionUtilities; import network.loki.messenger.R; @@ -43,8 +27,7 @@ public class LongMessageActivity extends PassphraseRequiredActionBarActivity { private static final int MAX_DISPLAY_LENGTH = 64 * 1024; - private Stub sentBubble; - private Stub receivedBubble; + private TextView textBody; private LongMessageViewModel viewModel; @@ -60,9 +43,7 @@ public class LongMessageActivity extends PassphraseRequiredActionBarActivity { protected void onCreate(Bundle savedInstanceState, boolean ready) { super.onCreate(savedInstanceState, ready); setContentView(R.layout.longmessage_activity); - - sentBubble = new Stub<>(findViewById(R.id.longmessage_sent_stub)); - receivedBubble = new Stub<>(findViewById(R.id.longmessage_received_stub)); + textBody = findViewById(R.id.longmessage_text); initViewModel(getIntent().getLongExtra(KEY_MESSAGE_ID, -1), getIntent().getBooleanExtra(KEY_IS_MMS, false)); } @@ -93,36 +74,19 @@ public class LongMessageActivity extends PassphraseRequiredActionBarActivity { return; } - if (message.get().getMessageRecord().isOutgoing()) { getSupportActionBar().setTitle(getString(R.string.LongMessageActivity_your_message)); } else { Recipient recipient = message.get().getMessageRecord().getRecipient(); - String name = Util.getFirstNonEmpty(recipient.getName(), recipient.getProfileName(), recipient.getAddress().serialize()) ; + String name = Util.getFirstNonEmpty(recipient.getName(), recipient.getProfileName(), recipient.getAddress().serialize()); getSupportActionBar().setTitle(getString(R.string.LongMessageActivity_message_from_s, name)); } - ViewGroup bubble; + String trimmedBody = getTrimmedBody(message.get().getFullBody()); + String mentionBody = MentionUtilities.highlightMentions(trimmedBody, message.get().getMessageRecord().getThreadId(), this); - if (message.get().getMessageRecord().isOutgoing()) { - bubble = sentBubble.get(); - bubble.getBackground().setColorFilter(ThemeUtil.getThemedColor(this, R.attr.message_sent_background_color), PorterDuff.Mode.MULTIPLY); - } else { - bubble = receivedBubble.get(); - bubble.getBackground().setColorFilter(ThemeUtil.getThemedColor(this, R.attr.message_received_background_color), PorterDuff.Mode.MULTIPLY); - } - - TextView text = bubble.findViewById(R.id.longmessage_text); - ConversationItemFooter footer = bubble.findViewById(R.id.longmessage_footer); - - String trimmedBody = getTrimmedBody(message.get().getFullBody()); - SpannableString styledBody = linkifyMessageBody(new SpannableString(trimmedBody)); - - bubble.setVisibility(View.VISIBLE); - text.setText(styledBody); - text.setMovementMethod(LinkMovementMethod.getInstance()); - text.setTextSize(TypedValue.COMPLEX_UNIT_SP, TextSecurePreferences.getMessageBodyTextSize(this)); - footer.setMessageRecord(message.get().getMessageRecord(), Locale.getDefault()); + textBody.setText(mentionBody); + textBody.setMovementMethod(LinkMovementMethod.getInstance()); }); } @@ -131,15 +95,4 @@ public class LongMessageActivity extends PassphraseRequiredActionBarActivity { : text.substring(0, MAX_DISPLAY_LENGTH); } - private SpannableString linkifyMessageBody(SpannableString messageBody) { - int linkPattern = Linkify.WEB_URLS | Linkify.EMAIL_ADDRESSES | Linkify.PHONE_NUMBERS; - boolean hasLinks = Linkify.addLinks(messageBody, linkPattern); - - if (hasLinks) { - Stream.of(messageBody.getSpans(0, messageBody.length(), URLSpan.class)) - .filterNot(url -> LinkPreviewUtil.isLegalUrl(url.getURL())) - .forEach(messageBody::removeSpan); - } - return messageBody; - } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/mediapreview/MediaRailAdapter.java b/app/src/main/java/org/thoughtcrime/securesms/mediapreview/MediaRailAdapter.java index 7767d0d7ea..dd27c42502 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/mediapreview/MediaRailAdapter.java +++ b/app/src/main/java/org/thoughtcrime/securesms/mediapreview/MediaRailAdapter.java @@ -10,7 +10,7 @@ import android.view.ViewGroup; import network.loki.messenger.R; -import org.thoughtcrime.securesms.components.ThumbnailView; +import org.thoughtcrime.securesms.conversation.v2.utilities.ThumbnailView; import org.thoughtcrime.securesms.mediasend.Media; import org.thoughtcrime.securesms.mms.GlideRequests; import org.thoughtcrime.securesms.util.StableIdGenerator; diff --git a/app/src/main/java/org/thoughtcrime/securesms/mediasend/MediaSendFragment.java b/app/src/main/java/org/thoughtcrime/securesms/mediasend/MediaSendFragment.java index 0df65497c8..ce1ccb2bbe 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/mediasend/MediaSendFragment.java +++ b/app/src/main/java/org/thoughtcrime/securesms/mediasend/MediaSendFragment.java @@ -82,7 +82,7 @@ public class MediaSendFragment extends Fragment implements ViewTreeObserver.OnGl private InputAwareLayout hud; private View captionAndRail; private ImageButton sendButton; - private ComposeText composeText; + private ComposeText composeText; private ViewGroup composeContainer; private EmojiEditText captionText; private EmojiToggle emojiToggle; diff --git a/app/src/main/java/org/thoughtcrime/securesms/mms/AudioSlide.java b/app/src/main/java/org/thoughtcrime/securesms/mms/AudioSlide.java index 9a1885ab90..3d45e6a6e7 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/mms/AudioSlide.java +++ b/app/src/main/java/org/thoughtcrime/securesms/mms/AudioSlide.java @@ -31,7 +31,6 @@ import org.session.libsession.utilities.MediaTypes; import org.thoughtcrime.securesms.database.AttachmentDatabase; import org.thoughtcrime.securesms.util.ResUtil; - public class AudioSlide extends Slide { public AudioSlide(Context context, Uri uri, long dataSize, boolean voiceNote) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/loki/api/BackgroundPollWorker.kt b/app/src/main/java/org/thoughtcrime/securesms/notifications/BackgroundPollWorker.kt similarity index 98% rename from app/src/main/java/org/thoughtcrime/securesms/loki/api/BackgroundPollWorker.kt rename to app/src/main/java/org/thoughtcrime/securesms/notifications/BackgroundPollWorker.kt index d070c25f2f..1674caf629 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/loki/api/BackgroundPollWorker.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/notifications/BackgroundPollWorker.kt @@ -1,4 +1,4 @@ -package org.thoughtcrime.securesms.loki.api +package org.thoughtcrime.securesms.notifications import android.content.BroadcastReceiver import android.content.Context diff --git a/app/src/main/java/org/thoughtcrime/securesms/notifications/DefaultMessageNotifier.java b/app/src/main/java/org/thoughtcrime/securesms/notifications/DefaultMessageNotifier.java index 532c953152..c5bf55d800 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/notifications/DefaultMessageNotifier.java +++ b/app/src/main/java/org/thoughtcrime/securesms/notifications/DefaultMessageNotifier.java @@ -48,7 +48,8 @@ import org.session.libsignal.utilities.Util; import org.session.libsignal.utilities.Log; import org.thoughtcrime.securesms.ApplicationContext; import org.thoughtcrime.securesms.contactshare.ContactUtil; -import org.thoughtcrime.securesms.conversation.ConversationActivity; + +import org.thoughtcrime.securesms.conversation.v2.ConversationActivityV2; import org.thoughtcrime.securesms.database.DatabaseFactory; import org.thoughtcrime.securesms.database.MessagingDatabase.MarkedMessageInfo; import org.thoughtcrime.securesms.database.MmsSmsDatabase; @@ -56,9 +57,8 @@ import org.thoughtcrime.securesms.database.ThreadDatabase; import org.thoughtcrime.securesms.database.model.MediaMmsMessageRecord; import org.thoughtcrime.securesms.database.model.MessageRecord; import org.thoughtcrime.securesms.database.model.MmsMessageRecord; -import org.thoughtcrime.securesms.loki.api.OpenGroupManager; -import org.thoughtcrime.securesms.loki.protocol.SessionMetaProtocol; -import org.thoughtcrime.securesms.loki.utilities.MentionUtilities; +import org.thoughtcrime.securesms.util.SessionMetaProtocol; +import org.thoughtcrime.securesms.conversation.v2.utilities.MentionUtilities; import org.thoughtcrime.securesms.mms.SlideDeck; import org.thoughtcrime.securesms.service.KeyCachingService; import org.thoughtcrime.securesms.util.SpanUtil; @@ -115,9 +115,9 @@ public class DefaultMessageNotifier implements MessageNotifier { if (visibleThread == threadId) { sendInThreadNotification(context, recipient); } else { - Intent intent = new Intent(context, ConversationActivity.class); - intent.putExtra(ConversationActivity.ADDRESS_EXTRA, recipient.getAddress()); - intent.putExtra(ConversationActivity.THREAD_ID_EXTRA, threadId); + Intent intent = new Intent(context, ConversationActivityV2.class); + intent.putExtra(ConversationActivityV2.ADDRESS, recipient.getAddress()); + intent.putExtra(ConversationActivityV2.THREAD_ID, threadId); intent.setData((Uri.parse("custom://" + System.currentTimeMillis()))); FailedNotificationBuilder builder = new FailedNotificationBuilder(context, TextSecurePreferences.getNotificationPrivacy(context), intent); diff --git a/app/src/main/java/org/thoughtcrime/securesms/loki/utilities/FcmUtils.kt b/app/src/main/java/org/thoughtcrime/securesms/notifications/FcmUtils.kt similarity index 92% rename from app/src/main/java/org/thoughtcrime/securesms/loki/utilities/FcmUtils.kt rename to app/src/main/java/org/thoughtcrime/securesms/notifications/FcmUtils.kt index 25b41f0d5e..87a9efc0de 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/loki/utilities/FcmUtils.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/notifications/FcmUtils.kt @@ -1,5 +1,5 @@ @file:JvmName("FcmUtils") -package org.thoughtcrime.securesms.loki.utilities +package org.thoughtcrime.securesms.notifications import com.google.android.gms.tasks.Task import com.google.firebase.iid.FirebaseInstanceId diff --git a/app/src/main/java/org/thoughtcrime/securesms/loki/api/LokiPushNotificationManager.kt b/app/src/main/java/org/thoughtcrime/securesms/notifications/LokiPushNotificationManager.kt similarity index 99% rename from app/src/main/java/org/thoughtcrime/securesms/loki/api/LokiPushNotificationManager.kt rename to app/src/main/java/org/thoughtcrime/securesms/notifications/LokiPushNotificationManager.kt index 05c6c768f4..64e67b4b68 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/loki/api/LokiPushNotificationManager.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/notifications/LokiPushNotificationManager.kt @@ -1,4 +1,4 @@ -package org.thoughtcrime.securesms.loki.api +package org.thoughtcrime.securesms.notifications import android.content.Context import nl.komponents.kovenant.functional.map diff --git a/app/src/main/java/org/thoughtcrime/securesms/notifications/MarkReadReceiver.java b/app/src/main/java/org/thoughtcrime/securesms/notifications/MarkReadReceiver.java index 3df423a6de..068cbac3e4 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/notifications/MarkReadReceiver.java +++ b/app/src/main/java/org/thoughtcrime/securesms/notifications/MarkReadReceiver.java @@ -21,7 +21,7 @@ import org.thoughtcrime.securesms.database.MessagingDatabase.ExpirationInfo; import org.thoughtcrime.securesms.database.MessagingDatabase.MarkedMessageInfo; import org.thoughtcrime.securesms.database.MessagingDatabase.SyncMessageId; import org.session.libsignal.utilities.Log; -import org.thoughtcrime.securesms.loki.protocol.SessionMetaProtocol; +import org.thoughtcrime.securesms.util.SessionMetaProtocol; import org.thoughtcrime.securesms.service.ExpiringMessageManager; import java.util.LinkedList; @@ -81,7 +81,6 @@ public class MarkReadReceiver extends BroadcastReceiver { for (Address address : addressMap.keySet()) { List timestamps = Stream.of(addressMap.get(address)).map(SyncMessageId::getTimetamp).toList(); - // Loki - Check whether we want to send a read receipt to this user if (!SessionMetaProtocol.shouldSendReadReceipt(address)) { continue; } ReadReceipt readReceipt = new ReadReceipt(timestamps); readReceipt.setSentTimestamp(System.currentTimeMillis()); diff --git a/app/src/main/java/org/thoughtcrime/securesms/notifications/MultipleRecipientNotificationBuilder.java b/app/src/main/java/org/thoughtcrime/securesms/notifications/MultipleRecipientNotificationBuilder.java index 5165e87ff9..24374ddd0f 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/notifications/MultipleRecipientNotificationBuilder.java +++ b/app/src/main/java/org/thoughtcrime/securesms/notifications/MultipleRecipientNotificationBuilder.java @@ -16,8 +16,8 @@ import org.session.libsession.utilities.TextSecurePreferences; import org.session.libsession.utilities.Util; import org.session.libsession.utilities.recipients.Recipient; import org.thoughtcrime.securesms.database.DatabaseFactory; -import org.thoughtcrime.securesms.loki.activities.HomeActivity; -import org.thoughtcrime.securesms.loki.database.SessionContactDatabase; +import org.thoughtcrime.securesms.home.HomeActivity; +import org.thoughtcrime.securesms.database.SessionContactDatabase; import java.util.LinkedList; import java.util.List; diff --git a/app/src/main/java/org/thoughtcrime/securesms/notifications/NotificationItem.java b/app/src/main/java/org/thoughtcrime/securesms/notifications/NotificationItem.java index e92f62aa44..991989e8da 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/notifications/NotificationItem.java +++ b/app/src/main/java/org/thoughtcrime/securesms/notifications/NotificationItem.java @@ -8,7 +8,8 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.core.app.TaskStackBuilder; -import org.thoughtcrime.securesms.conversation.ConversationActivity; + +import org.thoughtcrime.securesms.conversation.v2.ConversationActivityV2; import org.thoughtcrime.securesms.mms.SlideDeck; import org.session.libsession.utilities.recipients.Recipient; @@ -67,11 +68,11 @@ public class NotificationItem { } public PendingIntent getPendingIntent(Context context) { - Intent intent = new Intent(context, ConversationActivity.class); + Intent intent = new Intent(context, ConversationActivityV2.class); Recipient notifyRecipients = threadRecipient != null ? threadRecipient : conversationRecipient; - if (notifyRecipients != null) intent.putExtra(ConversationActivity.ADDRESS_EXTRA, notifyRecipients.getAddress()); + if (notifyRecipients != null) intent.putExtra(ConversationActivityV2.ADDRESS, notifyRecipients.getAddress()); - intent.putExtra("thread_id", threadId); + intent.putExtra(ConversationActivityV2.THREAD_ID, threadId); intent.setData((Uri.parse("custom://"+System.currentTimeMillis()))); return TaskStackBuilder.create(context) diff --git a/app/src/main/java/org/thoughtcrime/securesms/notifications/NotificationState.java b/app/src/main/java/org/thoughtcrime/securesms/notifications/NotificationState.java index 9330ec9e37..fe934e229f 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/notifications/NotificationState.java +++ b/app/src/main/java/org/thoughtcrime/securesms/notifications/NotificationState.java @@ -7,11 +7,10 @@ import android.net.Uri; import androidx.annotation.NonNull; import androidx.annotation.Nullable; -import org.thoughtcrime.securesms.conversation.ConversationActivity; -import org.thoughtcrime.securesms.conversation.ConversationPopupActivity; import org.session.libsignal.utilities.Log; import org.session.libsession.utilities.recipients.Recipient; import org.session.libsession.utilities.recipients.Recipient.*; +import org.thoughtcrime.securesms.conversation.v2.ConversationActivityV2; import java.util.LinkedHashSet; import java.util.LinkedList; @@ -167,9 +166,9 @@ public class NotificationState { public PendingIntent getQuickReplyIntent(Context context, Recipient recipient) { if (threads.size() != 1) throw new AssertionError("We only support replies to single thread notifications! " + threads.size()); - Intent intent = new Intent(context, ConversationPopupActivity.class); - intent.putExtra(ConversationActivity.ADDRESS_EXTRA, recipient.getAddress()); - intent.putExtra(ConversationActivity.THREAD_ID_EXTRA, (long)threads.toArray()[0]); + Intent intent = new Intent(context, ConversationActivityV2.class); + intent.putExtra(ConversationActivityV2.ADDRESS, recipient.getAddress()); + intent.putExtra(ConversationActivityV2.THREAD_ID, (long)threads.toArray()[0]); intent.setData((Uri.parse("custom://"+System.currentTimeMillis()))); return PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT); diff --git a/app/src/main/java/org/thoughtcrime/securesms/notifications/OptimizedMessageNotifier.java b/app/src/main/java/org/thoughtcrime/securesms/notifications/OptimizedMessageNotifier.java index aac737add2..4a72d7757c 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/notifications/OptimizedMessageNotifier.java +++ b/app/src/main/java/org/thoughtcrime/securesms/notifications/OptimizedMessageNotifier.java @@ -10,10 +10,9 @@ import org.session.libsession.messaging.sending_receiving.notifications.MessageN import org.session.libsession.messaging.sending_receiving.pollers.Poller; import org.session.libsession.utilities.recipients.Recipient; import org.session.libsession.utilities.Debouncer; -import org.session.libsignal.utilities.Log; import org.session.libsignal.utilities.ThreadUtils; import org.thoughtcrime.securesms.ApplicationContext; -import org.thoughtcrime.securesms.loki.api.OpenGroupManager; +import org.thoughtcrime.securesms.groups.OpenGroupManager; import java.util.concurrent.TimeUnit; diff --git a/app/src/main/java/org/thoughtcrime/securesms/notifications/PendingMessageNotificationBuilder.java b/app/src/main/java/org/thoughtcrime/securesms/notifications/PendingMessageNotificationBuilder.java index efd040a26d..1d19c2c8e3 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/notifications/PendingMessageNotificationBuilder.java +++ b/app/src/main/java/org/thoughtcrime/securesms/notifications/PendingMessageNotificationBuilder.java @@ -7,7 +7,7 @@ import android.content.Intent; import androidx.core.app.NotificationCompat; import org.session.libsession.utilities.recipients.Recipient; -import org.thoughtcrime.securesms.loki.activities.HomeActivity; +import org.thoughtcrime.securesms.home.HomeActivity; import org.session.libsession.utilities.NotificationPrivacyPreference; import org.session.libsession.utilities.TextSecurePreferences; diff --git a/app/src/main/java/org/thoughtcrime/securesms/loki/api/PushNotificationService.kt b/app/src/main/java/org/thoughtcrime/securesms/notifications/PushNotificationService.kt similarity index 95% rename from app/src/main/java/org/thoughtcrime/securesms/loki/api/PushNotificationService.kt rename to app/src/main/java/org/thoughtcrime/securesms/notifications/PushNotificationService.kt index 74f6f25c88..e035bbb7ea 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/loki/api/PushNotificationService.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/notifications/PushNotificationService.kt @@ -1,4 +1,4 @@ -package org.thoughtcrime.securesms.loki.api +package org.thoughtcrime.securesms.notifications import androidx.core.app.NotificationCompat import androidx.core.app.NotificationManagerCompat @@ -10,7 +10,6 @@ import org.session.libsession.messaging.utilities.MessageWrapper import org.session.libsession.utilities.TextSecurePreferences import org.session.libsignal.utilities.Base64 import org.session.libsignal.utilities.Log -import org.thoughtcrime.securesms.notifications.NotificationChannels class PushNotificationService : FirebaseMessagingService() { diff --git a/app/src/main/java/org/thoughtcrime/securesms/notifications/SingleRecipientNotificationBuilder.java b/app/src/main/java/org/thoughtcrime/securesms/notifications/SingleRecipientNotificationBuilder.java index 0b38783be8..80dd7d2602 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/notifications/SingleRecipientNotificationBuilder.java +++ b/app/src/main/java/org/thoughtcrime/securesms/notifications/SingleRecipientNotificationBuilder.java @@ -28,8 +28,8 @@ import org.session.libsession.avatars.GeneratedContactPhoto; import org.session.libsession.messaging.contacts.Contact; import org.session.libsignal.utilities.Log; import org.thoughtcrime.securesms.database.DatabaseFactory; -import org.thoughtcrime.securesms.loki.database.SessionContactDatabase; -import org.thoughtcrime.securesms.loki.utilities.AvatarPlaceholderGenerator; +import org.thoughtcrime.securesms.database.SessionContactDatabase; +import org.thoughtcrime.securesms.util.AvatarPlaceholderGenerator; import org.thoughtcrime.securesms.mms.DecryptableStreamUriLoader; import org.thoughtcrime.securesms.mms.GlideApp; import org.thoughtcrime.securesms.mms.Slide; diff --git a/app/src/main/java/org/thoughtcrime/securesms/loki/activities/DisplayNameActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/onboarding/DisplayNameActivity.kt similarity index 92% rename from app/src/main/java/org/thoughtcrime/securesms/loki/activities/DisplayNameActivity.kt rename to app/src/main/java/org/thoughtcrime/securesms/onboarding/DisplayNameActivity.kt index 50403dca9c..885878c5ff 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/loki/activities/DisplayNameActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/onboarding/DisplayNameActivity.kt @@ -1,4 +1,4 @@ -package org.thoughtcrime.securesms.loki.activities +package org.thoughtcrime.securesms.onboarding import android.content.Intent import android.os.Bundle @@ -11,8 +11,8 @@ import kotlinx.android.synthetic.main.activity_display_name.* import network.loki.messenger.R import org.session.libsession.utilities.SSKEnvironment.ProfileManagerProtocol import org.thoughtcrime.securesms.BaseActionBarActivity -import org.thoughtcrime.securesms.loki.utilities.push -import org.thoughtcrime.securesms.loki.utilities.setUpActionBarSessionLogo +import org.thoughtcrime.securesms.util.push +import org.thoughtcrime.securesms.util.setUpActionBarSessionLogo import org.session.libsession.utilities.TextSecurePreferences class DisplayNameActivity : BaseActionBarActivity() { diff --git a/app/src/main/java/org/thoughtcrime/securesms/loki/views/FakeChatView.kt b/app/src/main/java/org/thoughtcrime/securesms/onboarding/FakeChatView.kt similarity index 96% rename from app/src/main/java/org/thoughtcrime/securesms/loki/views/FakeChatView.kt rename to app/src/main/java/org/thoughtcrime/securesms/onboarding/FakeChatView.kt index 5f84df4913..68c4edd596 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/loki/views/FakeChatView.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/onboarding/FakeChatView.kt @@ -1,4 +1,4 @@ -package org.thoughtcrime.securesms.loki.views +package org.thoughtcrime.securesms.onboarding import android.animation.FloatEvaluator import android.animation.ValueAnimator @@ -12,7 +12,7 @@ import android.widget.LinearLayout import android.widget.ScrollView import kotlinx.android.synthetic.main.view_fake_chat.view.* import network.loki.messenger.R -import org.thoughtcrime.securesms.loki.utilities.disableClipping +import org.thoughtcrime.securesms.util.disableClipping class FakeChatView : ScrollView { diff --git a/app/src/main/java/org/thoughtcrime/securesms/loki/activities/LandingActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/onboarding/LandingActivity.kt similarity index 81% rename from app/src/main/java/org/thoughtcrime/securesms/loki/activities/LandingActivity.kt rename to app/src/main/java/org/thoughtcrime/securesms/onboarding/LandingActivity.kt index 50080956ba..3ca0632482 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/loki/activities/LandingActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/onboarding/LandingActivity.kt @@ -1,18 +1,15 @@ -package org.thoughtcrime.securesms.loki.activities +package org.thoughtcrime.securesms.onboarding import android.content.Intent import android.os.Bundle import android.view.View import network.loki.messenger.R -import org.thoughtcrime.securesms.BaseActionBarActivity -import org.session.libsession.utilities.IdentityKeyUtil -import org.thoughtcrime.securesms.loki.utilities.push -import org.thoughtcrime.securesms.loki.utilities.setUpActionBarSessionLogo -import org.thoughtcrime.securesms.loki.views.FakeChatView -import org.thoughtcrime.securesms.service.KeyCachingService -import org.thoughtcrime.securesms.util.Util - import org.session.libsession.utilities.TextSecurePreferences +import org.thoughtcrime.securesms.BaseActionBarActivity +import org.thoughtcrime.securesms.crypto.IdentityKeyUtil +import org.thoughtcrime.securesms.util.push +import org.thoughtcrime.securesms.util.setUpActionBarSessionLogo +import org.thoughtcrime.securesms.service.KeyCachingService class LandingActivity : BaseActionBarActivity() { @@ -25,7 +22,6 @@ class LandingActivity : BaseActionBarActivity() { findViewById(R.id.restoreButton).setOnClickListener { restore() } findViewById(R.id.linkButton).setOnClickListener { link() } IdentityKeyUtil.generateIdentityKeyPair(this) - TextSecurePreferences.setLastExperienceVersionCode(this, Util.getCanonicalVersionCode()) TextSecurePreferences.setPasswordDisabled(this, true) TextSecurePreferences.setReadReceiptsEnabled(this, true) TextSecurePreferences.setTypingIndicatorsEnabled(this, true) diff --git a/app/src/main/java/org/thoughtcrime/securesms/loki/activities/LinkDeviceActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/onboarding/LinkDeviceActivity.kt similarity index 93% rename from app/src/main/java/org/thoughtcrime/securesms/loki/activities/LinkDeviceActivity.kt rename to app/src/main/java/org/thoughtcrime/securesms/onboarding/LinkDeviceActivity.kt index ec2a76e760..24701e461c 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/loki/activities/LinkDeviceActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/onboarding/LinkDeviceActivity.kt @@ -1,4 +1,4 @@ -package org.thoughtcrime.securesms.loki.activities +package org.thoughtcrime.securesms.onboarding import android.content.Context import android.content.Intent @@ -14,7 +14,6 @@ import androidx.fragment.app.FragmentPagerAdapter import androidx.lifecycle.lifecycleScope import com.google.android.material.snackbar.Snackbar import kotlinx.android.synthetic.main.activity_link_device.* -import kotlinx.android.synthetic.main.conversation_activity.* import kotlinx.android.synthetic.main.fragment_recovery_phrase.* import kotlinx.coroutines.Job import kotlinx.coroutines.delay @@ -22,7 +21,6 @@ import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.filter import kotlinx.coroutines.launch import network.loki.messenger.R -import org.session.libsession.utilities.KeyPairUtilities import org.session.libsession.utilities.TextSecurePreferences import org.session.libsignal.crypto.MnemonicCodec import org.session.libsignal.utilities.Hex @@ -31,11 +29,12 @@ import org.session.libsignal.utilities.Log import org.session.libsignal.utilities.hexEncodedPublicKey import org.thoughtcrime.securesms.ApplicationContext import org.thoughtcrime.securesms.BaseActionBarActivity -import org.thoughtcrime.securesms.loki.fragments.ScanQRCodeWrapperFragment -import org.thoughtcrime.securesms.loki.fragments.ScanQRCodeWrapperFragmentDelegate -import org.thoughtcrime.securesms.loki.utilities.MnemonicUtilities -import org.thoughtcrime.securesms.loki.utilities.push -import org.thoughtcrime.securesms.loki.utilities.setUpActionBarSessionLogo +import org.thoughtcrime.securesms.crypto.KeyPairUtilities +import org.thoughtcrime.securesms.util.ScanQRCodeWrapperFragment +import org.thoughtcrime.securesms.util.ScanQRCodeWrapperFragmentDelegate +import org.thoughtcrime.securesms.crypto.MnemonicUtilities +import org.thoughtcrime.securesms.util.push +import org.thoughtcrime.securesms.util.setUpActionBarSessionLogo class LinkDeviceActivity : BaseActionBarActivity(), ScanQRCodeWrapperFragmentDelegate { private val adapter = LinkDeviceActivityAdapter(this) diff --git a/app/src/main/java/org/thoughtcrime/securesms/loki/activities/PNModeActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/onboarding/PNModeActivity.kt similarity index 94% rename from app/src/main/java/org/thoughtcrime/securesms/loki/activities/PNModeActivity.kt rename to app/src/main/java/org/thoughtcrime/securesms/onboarding/PNModeActivity.kt index 5ddc8b9682..cbfd5bcc93 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/loki/activities/PNModeActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/onboarding/PNModeActivity.kt @@ -1,4 +1,4 @@ -package org.thoughtcrime.securesms.loki.activities +package org.thoughtcrime.securesms.onboarding import android.animation.ArgbEvaluator import android.animation.ValueAnimator @@ -19,12 +19,13 @@ import network.loki.messenger.R import org.session.libsession.utilities.TextSecurePreferences import org.thoughtcrime.securesms.ApplicationContext import org.thoughtcrime.securesms.BaseActionBarActivity -import org.thoughtcrime.securesms.loki.utilities.disableClipping -import org.thoughtcrime.securesms.loki.utilities.getColorWithID -import org.thoughtcrime.securesms.loki.utilities.setUpActionBarSessionLogo -import org.thoughtcrime.securesms.loki.utilities.show -import org.thoughtcrime.securesms.loki.views.GlowViewUtilities -import org.thoughtcrime.securesms.loki.views.PNModeView +import org.thoughtcrime.securesms.home.HomeActivity +import org.thoughtcrime.securesms.util.disableClipping +import org.thoughtcrime.securesms.util.getColorWithID +import org.thoughtcrime.securesms.util.setUpActionBarSessionLogo +import org.thoughtcrime.securesms.util.show +import org.thoughtcrime.securesms.util.GlowViewUtilities +import org.thoughtcrime.securesms.util.PNModeView class PNModeActivity : BaseActionBarActivity() { private var selectedOptionView: PNModeView? = null diff --git a/app/src/main/java/org/thoughtcrime/securesms/loki/activities/RecoveryPhraseRestoreActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/onboarding/RecoveryPhraseRestoreActivity.kt similarity index 93% rename from app/src/main/java/org/thoughtcrime/securesms/loki/activities/RecoveryPhraseRestoreActivity.kt rename to app/src/main/java/org/thoughtcrime/securesms/onboarding/RecoveryPhraseRestoreActivity.kt index c3e01217d9..968b3d0ddc 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/loki/activities/RecoveryPhraseRestoreActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/onboarding/RecoveryPhraseRestoreActivity.kt @@ -1,4 +1,4 @@ -package org.thoughtcrime.securesms.loki.activities +package org.thoughtcrime.securesms.onboarding import android.content.Intent import android.graphics.Typeface @@ -14,15 +14,15 @@ import android.widget.Toast import kotlinx.android.synthetic.main.activity_recovery_phrase_restore.* import network.loki.messenger.R import org.session.libsession.utilities.TextSecurePreferences -import org.session.libsignal.utilities.KeyHelper import org.session.libsignal.crypto.MnemonicCodec -import org.session.libsignal.utilities.hexEncodedPublicKey import org.session.libsignal.utilities.Hex +import org.session.libsignal.utilities.KeyHelper +import org.session.libsignal.utilities.hexEncodedPublicKey import org.thoughtcrime.securesms.BaseActionBarActivity -import org.session.libsession.utilities.KeyPairUtilities -import org.thoughtcrime.securesms.loki.utilities.MnemonicUtilities -import org.thoughtcrime.securesms.loki.utilities.push -import org.thoughtcrime.securesms.loki.utilities.setUpActionBarSessionLogo +import org.thoughtcrime.securesms.crypto.KeyPairUtilities +import org.thoughtcrime.securesms.crypto.MnemonicUtilities +import org.thoughtcrime.securesms.util.push +import org.thoughtcrime.securesms.util.setUpActionBarSessionLogo class RecoveryPhraseRestoreActivity : BaseActionBarActivity() { diff --git a/app/src/main/java/org/thoughtcrime/securesms/loki/activities/RegisterActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/onboarding/RegisterActivity.kt similarity index 95% rename from app/src/main/java/org/thoughtcrime/securesms/loki/activities/RegisterActivity.kt rename to app/src/main/java/org/thoughtcrime/securesms/onboarding/RegisterActivity.kt index c9e5bd83cc..9dac8875d3 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/loki/activities/RegisterActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/onboarding/RegisterActivity.kt @@ -1,4 +1,4 @@ -package org.thoughtcrime.securesms.loki.activities +package org.thoughtcrime.securesms.onboarding import android.content.ClipData import android.content.ClipboardManager @@ -18,14 +18,14 @@ import android.widget.Toast import com.goterl.lazysodium.utils.KeyPair import kotlinx.android.synthetic.main.activity_register.* import network.loki.messenger.R -import org.session.libsession.utilities.KeyPairUtilities import org.session.libsession.utilities.TextSecurePreferences import org.session.libsignal.crypto.ecc.ECKeyPair import org.session.libsignal.utilities.KeyHelper import org.session.libsignal.utilities.hexEncodedPublicKey import org.thoughtcrime.securesms.BaseActionBarActivity -import org.thoughtcrime.securesms.loki.utilities.push -import org.thoughtcrime.securesms.loki.utilities.setUpActionBarSessionLogo +import org.thoughtcrime.securesms.crypto.KeyPairUtilities +import org.thoughtcrime.securesms.util.push +import org.thoughtcrime.securesms.util.setUpActionBarSessionLogo import java.util.* class RegisterActivity : BaseActionBarActivity() { diff --git a/app/src/main/java/org/thoughtcrime/securesms/loki/activities/SeedActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/onboarding/SeedActivity.kt similarity index 94% rename from app/src/main/java/org/thoughtcrime/securesms/loki/activities/SeedActivity.kt rename to app/src/main/java/org/thoughtcrime/securesms/onboarding/SeedActivity.kt index a7f10da4a8..ad8d3a29c7 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/loki/activities/SeedActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/onboarding/SeedActivity.kt @@ -1,4 +1,4 @@ -package org.thoughtcrime.securesms.loki.activities +package org.thoughtcrime.securesms.onboarding import android.content.ClipData import android.content.ClipboardManager @@ -11,13 +11,13 @@ import android.widget.LinearLayout import android.widget.Toast import kotlinx.android.synthetic.main.activity_seed.* import network.loki.messenger.R -import org.thoughtcrime.securesms.BaseActionBarActivity -import org.session.libsession.utilities.IdentityKeyUtil -import org.thoughtcrime.securesms.loki.utilities.MnemonicUtilities -import org.thoughtcrime.securesms.loki.utilities.getColorWithID import org.session.libsession.utilities.TextSecurePreferences import org.session.libsignal.crypto.MnemonicCodec import org.session.libsignal.utilities.hexEncodedPrivateKey +import org.thoughtcrime.securesms.BaseActionBarActivity +import org.thoughtcrime.securesms.crypto.IdentityKeyUtil +import org.thoughtcrime.securesms.crypto.MnemonicUtilities +import org.thoughtcrime.securesms.util.getColorWithID class SeedActivity : BaseActionBarActivity() { diff --git a/app/src/main/java/org/thoughtcrime/securesms/loki/views/SeedReminderView.kt b/app/src/main/java/org/thoughtcrime/securesms/onboarding/SeedReminderView.kt similarity index 97% rename from app/src/main/java/org/thoughtcrime/securesms/loki/views/SeedReminderView.kt rename to app/src/main/java/org/thoughtcrime/securesms/onboarding/SeedReminderView.kt index 591a458227..199ed7a5a1 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/loki/views/SeedReminderView.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/onboarding/SeedReminderView.kt @@ -1,4 +1,4 @@ -package org.thoughtcrime.securesms.loki.views +package org.thoughtcrime.securesms.onboarding import android.content.Context import android.os.Build diff --git a/app/src/main/java/org/thoughtcrime/securesms/preferences/AppProtectionPreferenceFragment.java b/app/src/main/java/org/thoughtcrime/securesms/preferences/AppProtectionPreferenceFragment.java index e516916de4..409b2bb24a 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/preferences/AppProtectionPreferenceFragment.java +++ b/app/src/main/java/org/thoughtcrime/securesms/preferences/AppProtectionPreferenceFragment.java @@ -103,7 +103,6 @@ public class AppProtectionPreferenceFragment extends CorrectedPreferenceFragment TextSecurePreferences.setScreenLockTimeout(getContext(), 0); } else { long timeoutSeconds = TimeUnit.MILLISECONDS.toSeconds(duration); -// long timeoutSeconds = Math.max(TimeUnit.MILLISECONDS.toSeconds(duration), 60); TextSecurePreferences.setScreenLockTimeout(getContext(), timeoutSeconds); } @@ -117,7 +116,6 @@ public class AppProtectionPreferenceFragment extends CorrectedPreferenceFragment private class ReadReceiptToggleListener implements Preference.OnPreferenceChangeListener { @Override public boolean onPreferenceChange(Preference preference, Object newValue) { - boolean enabled = (boolean)newValue; return true; } } @@ -138,21 +136,6 @@ public class AppProtectionPreferenceFragment extends CorrectedPreferenceFragment private class LinkPreviewToggleListener implements Preference.OnPreferenceChangeListener { @Override public boolean onPreferenceChange(Preference preference, Object newValue) { - boolean enabled = (boolean)newValue; - - if (enabled) { - AlertDialog.Builder builder = new AlertDialog.Builder(requireContext()); - builder.setTitle("Enable Link Previews?"); - builder.setMessage("You will not have full metadata protection when sending or receiving link previews."); - builder.setPositiveButton("OK", (dialog, which) -> dialog.dismiss()); - builder.setNegativeButton("Cancel", (dialog, which) -> { - TextSecurePreferences.setLinkPreviewsEnabled(requireContext(), false); - ((SwitchPreferenceCompat)AppProtectionPreferenceFragment.this.findPreference(TextSecurePreferences.LINK_PREVIEWS)).setChecked(false); - dialog.dismiss(); - }); - builder.create().show(); - } - return true; } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/loki/dialogs/ChangeUiModeDialog.kt b/app/src/main/java/org/thoughtcrime/securesms/preferences/ChangeUiModeDialog.kt similarity index 86% rename from app/src/main/java/org/thoughtcrime/securesms/loki/dialogs/ChangeUiModeDialog.kt rename to app/src/main/java/org/thoughtcrime/securesms/preferences/ChangeUiModeDialog.kt index 77d8bfe8c0..594d1b47f7 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/loki/dialogs/ChangeUiModeDialog.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/preferences/ChangeUiModeDialog.kt @@ -1,12 +1,12 @@ -package org.thoughtcrime.securesms.loki.dialogs +package org.thoughtcrime.securesms.preferences import android.app.Dialog import android.os.Bundle import androidx.appcompat.app.AlertDialog import androidx.fragment.app.DialogFragment import network.loki.messenger.R -import org.thoughtcrime.securesms.loki.utilities.UiMode -import org.thoughtcrime.securesms.loki.utilities.UiModeUtilities +import org.thoughtcrime.securesms.util.UiMode +import org.thoughtcrime.securesms.util.UiModeUtilities class ChangeUiModeDialog : DialogFragment() { diff --git a/app/src/main/java/org/thoughtcrime/securesms/loki/activities/ChatSettingsActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/preferences/ChatSettingsActivity.kt similarity index 93% rename from app/src/main/java/org/thoughtcrime/securesms/loki/activities/ChatSettingsActivity.kt rename to app/src/main/java/org/thoughtcrime/securesms/preferences/ChatSettingsActivity.kt index 67a2c4f2d5..67faa9da46 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/loki/activities/ChatSettingsActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/preferences/ChatSettingsActivity.kt @@ -1,4 +1,4 @@ -package org.thoughtcrime.securesms.loki.activities +package org.thoughtcrime.securesms.preferences import android.os.Bundle import network.loki.messenger.R diff --git a/app/src/main/java/org/thoughtcrime/securesms/loki/dialogs/ClearAllDataDialog.kt b/app/src/main/java/org/thoughtcrime/securesms/preferences/ClearAllDataDialog.kt similarity index 52% rename from app/src/main/java/org/thoughtcrime/securesms/loki/dialogs/ClearAllDataDialog.kt rename to app/src/main/java/org/thoughtcrime/securesms/preferences/ClearAllDataDialog.kt index e185c2c935..554f1958cd 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/loki/dialogs/ClearAllDataDialog.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/preferences/ClearAllDataDialog.kt @@ -1,29 +1,20 @@ -package org.thoughtcrime.securesms.loki.dialogs +package org.thoughtcrime.securesms.preferences -import android.app.Dialog -import android.content.DialogInterface -import android.graphics.Color -import android.graphics.drawable.ColorDrawable -import android.os.Bundle import android.view.LayoutInflater import androidx.appcompat.app.AlertDialog import androidx.core.view.isVisible -import androidx.fragment.app.DialogFragment import androidx.lifecycle.lifecycleScope import kotlinx.android.synthetic.main.dialog_clear_all_data.* import kotlinx.android.synthetic.main.dialog_clear_all_data.view.* import kotlinx.coroutines.* import network.loki.messenger.R -import nl.komponents.kovenant.Promise -import org.session.libsession.messaging.MessagingModuleConfiguration import org.session.libsession.snode.SnodeAPI -import org.session.libsession.utilities.KeyPairUtilities import org.session.libsignal.utilities.Log import org.thoughtcrime.securesms.ApplicationContext -import org.thoughtcrime.securesms.loki.protocol.MultiDeviceProtocol -import java.util.concurrent.Executors +import org.thoughtcrime.securesms.util.ConfigurationMessageUtilities +import org.thoughtcrime.securesms.conversation.v2.utilities.BaseDialog -class ClearAllDataDialog : DialogFragment() { +class ClearAllDataDialog : BaseDialog() { enum class Steps { INFO_PROMPT, @@ -42,8 +33,7 @@ class ClearAllDataDialog : DialogFragment() { updateUI() } - override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { - val builder = AlertDialog.Builder(requireContext()) + override fun setContentView(builder: AlertDialog.Builder) { val contentView = LayoutInflater.from(requireContext()).inflate(R.layout.dialog_clear_all_data, null) contentView.cancelButton.setOnClickListener { if (step == Steps.NETWORK_PROMPT) { @@ -63,9 +53,6 @@ class ClearAllDataDialog : DialogFragment() { } builder.setView(contentView) builder.setCancelable(false) - val result = builder.create() - result.window?.setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT)) - return result } private fun updateUI() { @@ -98,56 +85,43 @@ class ClearAllDataDialog : DialogFragment() { } private fun clearAllData(deleteNetworkMessages: Boolean) { - if (KeyPairUtilities.hasV2KeyPair(requireContext())) { - clearJob = lifecycleScope.launch(Dispatchers.IO) { - val previousStep = step + clearJob = lifecycleScope.launch(Dispatchers.IO) { + val previousStep = step + withContext(Dispatchers.Main) { + step = Steps.DELETING + } + + if (!deleteNetworkMessages) { + try { + ConfigurationMessageUtilities.forceSyncConfigurationNowIfNeeded(requireContext()).get() + } catch (e: Exception) { + Log.e("Loki", "Failed to force sync", e) + } + ApplicationContext.getInstance(context).clearAllData(false) withContext(Dispatchers.Main) { - step = Steps.DELETING + dismiss() + } + } else { + // finish + val result = try { + SnodeAPI.deleteAllMessages(requireContext()).get() + } catch (e: Exception) { + null } - if (!deleteNetworkMessages) { - try { - MultiDeviceProtocol.forceSyncConfigurationNowIfNeeded(requireContext()).get() - } catch (e: Exception) { - Log.e("Loki", "Failed to force sync", e) + if (result == null || result.values.any { !it } || result.isEmpty()) { + // didn't succeed (at least one) + withContext(Dispatchers.Main) { + step = previousStep } + } else if (result.values.all { it }) { + // don't force sync because all the messages are deleted? ApplicationContext.getInstance(context).clearAllData(false) withContext(Dispatchers.Main) { dismiss() } - } else { - // finish - val result = try { - SnodeAPI.deleteAllMessages(requireContext()).get() - } catch (e: Exception) { - null - } - - if (result == null || result.values.any { !it } || result.isEmpty()) { - // didn't succeed (at least one) - withContext(Dispatchers.Main) { - step = previousStep - } - } else if (result.values.all { it }) { - // don't force sync because all the messages are deleted? - ApplicationContext.getInstance(context).clearAllData(false) - withContext(Dispatchers.Main) { - dismiss() - } - } } } - } else { - val dialog = AlertDialog.Builder(requireContext()) - val message = "We’ve upgraded the way Session IDs are generated, so you will be unable to restore your current Session ID." - dialog.setMessage(message) - dialog.setPositiveButton("Yes") { _, _ -> - ApplicationContext.getInstance(context).clearAllData(false) - } - dialog.setNegativeButton("Cancel") { _, _ -> - // Do nothing - } - dialog.create().show() } } } \ No newline at end of file diff --git a/app/src/main/java/org/thoughtcrime/securesms/loki/activities/NotificationSettingsActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/preferences/NotificationSettingsActivity.kt similarity index 93% rename from app/src/main/java/org/thoughtcrime/securesms/loki/activities/NotificationSettingsActivity.kt rename to app/src/main/java/org/thoughtcrime/securesms/preferences/NotificationSettingsActivity.kt index 1468539a72..af039a4fdb 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/loki/activities/NotificationSettingsActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/preferences/NotificationSettingsActivity.kt @@ -1,4 +1,4 @@ -package org.thoughtcrime.securesms.loki.activities +package org.thoughtcrime.securesms.preferences import android.os.Bundle import network.loki.messenger.R diff --git a/app/src/main/java/org/thoughtcrime/securesms/loki/activities/PrivacySettingsActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/preferences/PrivacySettingsActivity.kt similarity index 93% rename from app/src/main/java/org/thoughtcrime/securesms/loki/activities/PrivacySettingsActivity.kt rename to app/src/main/java/org/thoughtcrime/securesms/preferences/PrivacySettingsActivity.kt index 966dd9b437..bf277dc17f 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/loki/activities/PrivacySettingsActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/preferences/PrivacySettingsActivity.kt @@ -1,4 +1,4 @@ -package org.thoughtcrime.securesms.loki.activities +package org.thoughtcrime.securesms.preferences import android.os.Bundle import network.loki.messenger.R diff --git a/app/src/main/java/org/thoughtcrime/securesms/loki/activities/QRCodeActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/preferences/QRCodeActivity.kt similarity index 85% rename from app/src/main/java/org/thoughtcrime/securesms/loki/activities/QRCodeActivity.kt rename to app/src/main/java/org/thoughtcrime/securesms/preferences/QRCodeActivity.kt index dd6bf3420e..07a1d60590 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/loki/activities/QRCodeActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/preferences/QRCodeActivity.kt @@ -1,4 +1,4 @@ -package org.thoughtcrime.securesms.loki.activities +package org.thoughtcrime.securesms.preferences import android.content.Intent import android.graphics.Bitmap @@ -14,18 +14,18 @@ import kotlinx.android.synthetic.main.activity_qr_code.* import kotlinx.android.synthetic.main.fragment_view_my_qr_code.* import network.loki.messenger.R import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity -import org.thoughtcrime.securesms.conversation.ConversationActivity + import org.session.libsession.utilities.Address -import org.session.libsession.utilities.DistributionTypes import org.thoughtcrime.securesms.database.DatabaseFactory -import org.thoughtcrime.securesms.loki.fragments.ScanQRCodeWrapperFragment -import org.thoughtcrime.securesms.loki.fragments.ScanQRCodeWrapperFragmentDelegate -import org.thoughtcrime.securesms.loki.utilities.QRCodeUtilities -import org.thoughtcrime.securesms.loki.utilities.toPx +import org.thoughtcrime.securesms.util.ScanQRCodeWrapperFragment +import org.thoughtcrime.securesms.util.ScanQRCodeWrapperFragmentDelegate +import org.thoughtcrime.securesms.util.QRCodeUtilities +import org.thoughtcrime.securesms.util.toPx import org.session.libsession.utilities.recipients.Recipient import org.thoughtcrime.securesms.util.FileProviderUtil import org.session.libsession.utilities.TextSecurePreferences import org.session.libsignal.utilities.PublicKeyValidation +import org.thoughtcrime.securesms.conversation.v2.ConversationActivityV2 import java.io.File import java.io.FileOutputStream @@ -53,13 +53,11 @@ class QRCodeActivity : PassphraseRequiredActionBarActivity(), ScanQRCodeWrapperF fun createPrivateChatIfPossible(hexEncodedPublicKey: String) { if (!PublicKeyValidation.isValid(hexEncodedPublicKey)) { return Toast.makeText(this, R.string.invalid_session_id, Toast.LENGTH_SHORT).show() } val recipient = Recipient.from(this, Address.fromSerialized(hexEncodedPublicKey), false) - val intent = Intent(this, ConversationActivity::class.java) - intent.putExtra(ConversationActivity.ADDRESS_EXTRA, recipient.address) - intent.putExtra(ConversationActivity.TEXT_EXTRA, getIntent().getStringExtra(ConversationActivity.TEXT_EXTRA)) + val intent = Intent(this, ConversationActivityV2::class.java) + intent.putExtra(ConversationActivityV2.ADDRESS, recipient.address) intent.setDataAndType(getIntent().data, getIntent().type) val existingThread = DatabaseFactory.getThreadDatabase(this).getThreadIdIfExistsFor(recipient) - intent.putExtra(ConversationActivity.THREAD_ID_EXTRA, existingThread) - intent.putExtra(ConversationActivity.DISTRIBUTION_TYPE_EXTRA, DistributionTypes.DEFAULT) + intent.putExtra(ConversationActivityV2.THREAD_ID, existingThread) startActivity(intent) finish() } diff --git a/app/src/main/java/org/thoughtcrime/securesms/loki/dialogs/SeedDialog.kt b/app/src/main/java/org/thoughtcrime/securesms/preferences/SeedDialog.kt similarity index 71% rename from app/src/main/java/org/thoughtcrime/securesms/loki/dialogs/SeedDialog.kt rename to app/src/main/java/org/thoughtcrime/securesms/preferences/SeedDialog.kt index 2bd9595b22..19fffc343f 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/loki/dialogs/SeedDialog.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/preferences/SeedDialog.kt @@ -1,24 +1,20 @@ -package org.thoughtcrime.securesms.loki.dialogs +package org.thoughtcrime.securesms.preferences -import android.app.Dialog import android.content.ClipData import android.content.ClipboardManager import android.content.Context -import android.graphics.Color -import android.graphics.drawable.ColorDrawable -import android.os.Bundle import android.view.LayoutInflater import android.widget.Toast import androidx.appcompat.app.AlertDialog -import androidx.fragment.app.DialogFragment import kotlinx.android.synthetic.main.dialog_seed.view.* import network.loki.messenger.R -import org.session.libsession.utilities.IdentityKeyUtil -import org.thoughtcrime.securesms.loki.utilities.MnemonicUtilities import org.session.libsignal.crypto.MnemonicCodec import org.session.libsignal.utilities.hexEncodedPrivateKey +import org.thoughtcrime.securesms.crypto.IdentityKeyUtil +import org.thoughtcrime.securesms.crypto.MnemonicUtilities +import org.thoughtcrime.securesms.conversation.v2.utilities.BaseDialog -class SeedDialog : DialogFragment() { +class SeedDialog : BaseDialog() { private val seed by lazy { var hexEncodedSeed = IdentityKeyUtil.retrieve(requireContext(), IdentityKeyUtil.LOKI_SEED) @@ -31,16 +27,12 @@ class SeedDialog : DialogFragment() { MnemonicCodec(loadFileContents).encode(hexEncodedSeed!!, MnemonicCodec.Language.Configuration.english) } - override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { - val builder = AlertDialog.Builder(requireContext()) + override fun setContentView(builder: AlertDialog.Builder) { val contentView = LayoutInflater.from(requireContext()).inflate(R.layout.dialog_seed, null) contentView.seedTextView.text = seed contentView.cancelButton.setOnClickListener { dismiss() } contentView.copyButton.setOnClickListener { copySeed() } builder.setView(contentView) - val result = builder.create() - result.window?.setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT)) - return result } private fun copySeed() { diff --git a/app/src/main/java/org/thoughtcrime/securesms/loki/activities/SettingsActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/preferences/SettingsActivity.kt similarity index 94% rename from app/src/main/java/org/thoughtcrime/securesms/loki/activities/SettingsActivity.kt rename to app/src/main/java/org/thoughtcrime/securesms/preferences/SettingsActivity.kt index 48edde409e..e8c3674736 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/loki/activities/SettingsActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/preferences/SettingsActivity.kt @@ -1,4 +1,4 @@ -package org.thoughtcrime.securesms.loki.activities +package org.thoughtcrime.securesms.preferences import android.Manifest import android.app.Activity @@ -27,18 +27,15 @@ import nl.komponents.kovenant.ui.alwaysUi import nl.komponents.kovenant.ui.successUi import org.session.libsession.avatars.AvatarHelper import org.session.libsession.utilities.Address +import org.session.libsession.utilities.ProfileKeyUtil import org.session.libsession.utilities.ProfilePictureUtilities import org.session.libsession.utilities.SSKEnvironment.ProfileManagerProtocol import org.session.libsession.utilities.TextSecurePreferences -import org.session.libsession.utilities.ProfileKeyUtil import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity import org.thoughtcrime.securesms.avatar.AvatarSelection -import org.thoughtcrime.securesms.loki.dialogs.ChangeUiModeDialog -import org.thoughtcrime.securesms.loki.dialogs.ClearAllDataDialog -import org.thoughtcrime.securesms.loki.dialogs.SeedDialog -import org.thoughtcrime.securesms.loki.protocol.MultiDeviceProtocol -import org.thoughtcrime.securesms.loki.utilities.UiModeUtilities -import org.thoughtcrime.securesms.loki.utilities.push +import org.thoughtcrime.securesms.util.ConfigurationMessageUtilities +import org.thoughtcrime.securesms.util.UiModeUtilities +import org.thoughtcrime.securesms.util.push import org.thoughtcrime.securesms.mms.GlideApp import org.thoughtcrime.securesms.mms.GlideRequests import org.thoughtcrime.securesms.permissions.Permissions @@ -90,6 +87,8 @@ class SettingsActivity : PassphraseRequiredActionBarActivity() { helpTranslateButton.setOnClickListener { helpTranslate() } seedButton.setOnClickListener { showSeed() } clearAllDataButton.setOnClickListener { clearAllData() } + val isLightMode = UiModeUtilities.isDayUiMode(this) + oxenLogoImageView.setImageResource(if (isLightMode) R.drawable.oxen_light_mode else R.drawable.oxen_dark_mode) versionTextView.text = String.format(getString(R.string.version_s), "${BuildConfig.VERSION_NAME} (${BuildConfig.VERSION_CODE})") } @@ -192,7 +191,7 @@ class SettingsActivity : PassphraseRequiredActionBarActivity() { ProfileKeyUtil.setEncodedProfileKey(this, encodedProfileKey) } if (profilePicture != null || displayName != null) { - MultiDeviceProtocol.forceSyncConfigurationNowIfNeeded(this@SettingsActivity) + ConfigurationMessageUtilities.forceSyncConfigurationNowIfNeeded(this@SettingsActivity) } } compoundPromise.alwaysUi { @@ -258,7 +257,8 @@ class SettingsActivity : PassphraseRequiredActionBarActivity() { intent.action = Intent.ACTION_SEND intent.putExtra(Intent.EXTRA_TEXT, hexEncodedPublicKey) intent.type = "text/plain" - startActivity(intent) + val chooser = Intent.createChooser(intent, getString(R.string.share)) + startActivity(chooser) } private fun showPrivacySettings() { @@ -282,7 +282,8 @@ class SettingsActivity : PassphraseRequiredActionBarActivity() { val invitation = "Hey, I've been using Session to chat with complete privacy and security. Come join me! Download it at https://getsession.org/. My Session ID is $hexEncodedPublicKey!" intent.putExtra(Intent.EXTRA_TEXT, invitation) intent.type = "text/plain" - startActivity(intent) + val chooser = Intent.createChooser(intent, getString(R.string.activity_settings_invite_button_title)) + startActivity(chooser) } private fun helpTranslate() { diff --git a/app/src/main/java/org/thoughtcrime/securesms/service/GenericForegroundService.java b/app/src/main/java/org/thoughtcrime/securesms/service/GenericForegroundService.java index 58ed5d7d75..0581883c5e 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/service/GenericForegroundService.java +++ b/app/src/main/java/org/thoughtcrime/securesms/service/GenericForegroundService.java @@ -13,7 +13,7 @@ import androidx.core.app.NotificationCompat; import androidx.core.content.ContextCompat; import org.session.libsignal.utilities.Log; -import org.thoughtcrime.securesms.loki.activities.HomeActivity; +import org.thoughtcrime.securesms.home.HomeActivity; import org.thoughtcrime.securesms.notifications.NotificationChannels; import org.session.libsignal.utilities.guava.Preconditions; diff --git a/app/src/main/java/org/thoughtcrime/securesms/service/KeyCachingService.java b/app/src/main/java/org/thoughtcrime/securesms/service/KeyCachingService.java index c4507cd766..9e79b93d60 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/service/KeyCachingService.java +++ b/app/src/main/java/org/thoughtcrime/securesms/service/KeyCachingService.java @@ -37,7 +37,7 @@ import org.thoughtcrime.securesms.ApplicationContext; import org.thoughtcrime.securesms.DatabaseUpgradeActivity; import org.thoughtcrime.securesms.DummyActivity; import org.session.libsignal.utilities.Log; -import org.thoughtcrime.securesms.loki.activities.HomeActivity; +import org.thoughtcrime.securesms.home.HomeActivity; import org.thoughtcrime.securesms.notifications.NotificationChannels; import org.session.libsession.utilities.ServiceUtil; import org.session.libsession.utilities.TextSecurePreferences; diff --git a/app/src/main/java/org/thoughtcrime/securesms/sskenvironment/ReadReceiptManager.kt b/app/src/main/java/org/thoughtcrime/securesms/sskenvironment/ReadReceiptManager.kt index 2fc8c1cbaf..62002c88b5 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/sskenvironment/ReadReceiptManager.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/sskenvironment/ReadReceiptManager.kt @@ -9,6 +9,7 @@ import org.thoughtcrime.securesms.database.DatabaseFactory import org.thoughtcrime.securesms.database.MessagingDatabase.SyncMessageId class ReadReceiptManager: SSKEnvironment.ReadReceiptManagerProtocol { + override fun processReadReceipts(context: Context, fromRecipientId: String, sentTimestamps: List, readTimestamp: Long) { if (TextSecurePreferences.isReadReceiptsEnabled(context)) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/loki/utilities/ActivityUtilities.kt b/app/src/main/java/org/thoughtcrime/securesms/util/ActivityUtilities.kt similarity index 74% rename from app/src/main/java/org/thoughtcrime/securesms/loki/utilities/ActivityUtilities.kt rename to app/src/main/java/org/thoughtcrime/securesms/util/ActivityUtilities.kt index 6fd9250e0d..d5daba5872 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/loki/utilities/ActivityUtilities.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/util/ActivityUtilities.kt @@ -1,11 +1,16 @@ -package org.thoughtcrime.securesms.loki.utilities +package org.thoughtcrime.securesms.util +import android.annotation.SuppressLint +import android.app.Activity +import android.content.Context import android.content.Intent +import android.os.Bundle import android.view.View import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.widget.Toolbar import network.loki.messenger.R import org.thoughtcrime.securesms.BaseActionBarActivity +import org.thoughtcrime.securesms.conversation.v2.utilities.BaseDialog fun BaseActionBarActivity.setUpActionBarSessionLogo(hideBackButton: Boolean = false) { val actionbar = supportActionBar!! @@ -52,4 +57,14 @@ fun AppCompatActivity.show(intent: Intent, isForResult: Boolean = false) { startActivity(intent) } overridePendingTransition(R.anim.slide_from_bottom, R.anim.fade_scale_out) +} + +interface ActivityDispatcher { + companion object { + const val SERVICE = "ActivityDispatcher_SERVICE" + @SuppressLint("WrongConstant") + fun get(context: Context) = context.getSystemService(SERVICE) as? ActivityDispatcher + } + fun dispatchIntent(body: (Context)->Intent?) + fun showDialog(baseDialog: BaseDialog, tag: String? = null) } \ No newline at end of file diff --git a/app/src/main/java/org/thoughtcrime/securesms/animation/AnimationCompleteListener.java b/app/src/main/java/org/thoughtcrime/securesms/util/AnimationCompleteListener.java similarity index 90% rename from app/src/main/java/org/thoughtcrime/securesms/animation/AnimationCompleteListener.java rename to app/src/main/java/org/thoughtcrime/securesms/util/AnimationCompleteListener.java index 3063a04c91..c5da069cce 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/animation/AnimationCompleteListener.java +++ b/app/src/main/java/org/thoughtcrime/securesms/util/AnimationCompleteListener.java @@ -1,4 +1,4 @@ -package org.thoughtcrime.securesms.animation; +package org.thoughtcrime.securesms.util; import android.animation.Animator; diff --git a/app/src/main/java/org/thoughtcrime/securesms/loki/utilities/AvatarPlaceholderGenerator.kt b/app/src/main/java/org/thoughtcrime/securesms/util/AvatarPlaceholderGenerator.kt similarity index 98% rename from app/src/main/java/org/thoughtcrime/securesms/loki/utilities/AvatarPlaceholderGenerator.kt rename to app/src/main/java/org/thoughtcrime/securesms/util/AvatarPlaceholderGenerator.kt index a8bcb4852b..b93a4a0905 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/loki/utilities/AvatarPlaceholderGenerator.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/util/AvatarPlaceholderGenerator.kt @@ -1,4 +1,4 @@ -package org.thoughtcrime.securesms.loki.utilities +package org.thoughtcrime.securesms.util import android.content.Context import android.graphics.* diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/BackupUtil.kt b/app/src/main/java/org/thoughtcrime/securesms/util/BackupUtil.kt index b597835e05..74014bc819 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/util/BackupUtil.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/util/BackupUtil.kt @@ -21,9 +21,9 @@ import org.thoughtcrime.securesms.backup.BackupPassphrase import org.thoughtcrime.securesms.backup.BackupProtos.SharedPreference import org.thoughtcrime.securesms.backup.FullBackupExporter import org.thoughtcrime.securesms.crypto.AttachmentSecretProvider -import org.session.libsession.utilities.IdentityKeyUtil +import org.thoughtcrime.securesms.crypto.IdentityKeyUtil import org.thoughtcrime.securesms.database.DatabaseFactory -import org.thoughtcrime.securesms.loki.database.BackupFileRecord +import org.thoughtcrime.securesms.database.BackupFileRecord import org.thoughtcrime.securesms.service.LocalBackupListener import java.io.IOException import java.security.MessageDigest @@ -118,7 +118,7 @@ object BackupUtil { if (timestamp == null) { return context.getString(R.string.BackupUtil_never) } - return DateUtils.getExtendedRelativeTimeSpanString(context, locale, timestamp.time) + return DateUtils.getDisplayFormattedTimeSpanString(context, locale, timestamp.time) } @JvmStatic diff --git a/app/src/main/java/org/thoughtcrime/securesms/loki/utilities/Broadcaster.kt b/app/src/main/java/org/thoughtcrime/securesms/util/Broadcaster.kt similarity index 92% rename from app/src/main/java/org/thoughtcrime/securesms/loki/utilities/Broadcaster.kt rename to app/src/main/java/org/thoughtcrime/securesms/util/Broadcaster.kt index 74429f394a..6060c00c81 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/loki/utilities/Broadcaster.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/util/Broadcaster.kt @@ -1,4 +1,4 @@ -package org.thoughtcrime.securesms.loki.utilities +package org.thoughtcrime.securesms.util import android.content.Context import android.content.Intent diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/CommunicationActions.java b/app/src/main/java/org/thoughtcrime/securesms/util/CommunicationActions.java index 418de713cb..9bb71ad5cc 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/util/CommunicationActions.java +++ b/app/src/main/java/org/thoughtcrime/securesms/util/CommunicationActions.java @@ -11,9 +11,10 @@ import androidx.core.app.TaskStackBuilder; import android.text.TextUtils; import android.widget.Toast; -import org.thoughtcrime.securesms.conversation.ConversationActivity; + import network.loki.messenger.R; +import org.thoughtcrime.securesms.conversation.v2.ConversationActivityV2; import org.thoughtcrime.securesms.database.DatabaseFactory; import org.session.libsession.utilities.recipients.Recipient; @@ -32,14 +33,9 @@ public class CommunicationActions { @Override protected void onPostExecute(Long threadId) { - Intent intent = new Intent(context, ConversationActivity.class); - intent.putExtra(ConversationActivity.ADDRESS_EXTRA, recipient.getAddress()); - intent.putExtra(ConversationActivity.THREAD_ID_EXTRA, threadId); - intent.putExtra(ConversationActivity.TIMING_EXTRA, System.currentTimeMillis()); - - if (!TextUtils.isEmpty(text)) { - intent.putExtra(ConversationActivity.TEXT_EXTRA, text); - } + Intent intent = new Intent(context, ConversationActivityV2.class); + intent.putExtra(ConversationActivityV2.ADDRESS, recipient.getAddress()); + intent.putExtra(ConversationActivityV2.THREAD_ID, threadId); if (backStack != null) { backStack.addNextIntent(intent); diff --git a/app/src/main/java/org/thoughtcrime/securesms/loki/protocol/MultiDeviceProtocol.kt b/app/src/main/java/org/thoughtcrime/securesms/util/ConfigurationMessageUtilities.kt similarity index 90% rename from app/src/main/java/org/thoughtcrime/securesms/loki/protocol/MultiDeviceProtocol.kt rename to app/src/main/java/org/thoughtcrime/securesms/util/ConfigurationMessageUtilities.kt index 29fb98a89a..0031bcc1bf 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/loki/protocol/MultiDeviceProtocol.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/util/ConfigurationMessageUtilities.kt @@ -1,17 +1,14 @@ -package org.thoughtcrime.securesms.loki.protocol +package org.thoughtcrime.securesms.util import android.content.Context -import com.google.protobuf.ByteString import nl.komponents.kovenant.Promise -import org.session.libsession.messaging.MessagingModuleConfiguration import org.session.libsession.messaging.messages.Destination import org.session.libsession.messaging.messages.control.ConfigurationMessage import org.session.libsession.messaging.sending_receiving.MessageSender import org.session.libsession.utilities.Address import org.session.libsession.utilities.TextSecurePreferences -import org.thoughtcrime.securesms.loki.utilities.ContactUtilities -object MultiDeviceProtocol { +object ConfigurationMessageUtilities { @JvmStatic fun syncConfigurationIfNeeded(context: Context) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/loki/utilities/ContactUtilities.kt b/app/src/main/java/org/thoughtcrime/securesms/util/ContactUtilities.kt similarity index 93% rename from app/src/main/java/org/thoughtcrime/securesms/loki/utilities/ContactUtilities.kt rename to app/src/main/java/org/thoughtcrime/securesms/util/ContactUtilities.kt index 39a5c68c67..4820d005f6 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/loki/utilities/ContactUtilities.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/util/ContactUtilities.kt @@ -1,4 +1,4 @@ -package org.thoughtcrime.securesms.loki.utilities +package org.thoughtcrime.securesms.util import android.content.Context import org.thoughtcrime.securesms.database.DatabaseFactory diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/DateUtils.java b/app/src/main/java/org/thoughtcrime/securesms/util/DateUtils.java index cb822f1eeb..7860e46242 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/util/DateUtils.java +++ b/app/src/main/java/org/thoughtcrime/securesms/util/DateUtils.java @@ -28,6 +28,7 @@ import org.session.libsignal.utilities.Log; import java.text.ParseException; import java.text.SimpleDateFormat; +import java.util.Calendar; import java.util.Date; import java.util.Locale; import java.util.concurrent.TimeUnit; @@ -40,8 +41,9 @@ import network.loki.messenger.R; public class DateUtils extends android.text.format.DateUtils { @SuppressWarnings("unused") - private static final String TAG = DateUtils.class.getSimpleName(); - private static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("yyyyMMdd"); + private static final String TAG = DateUtils.class.getSimpleName(); + private static final SimpleDateFormat DAY_PRECISION_DATE_FORMAT = new SimpleDateFormat("yyyyMMdd"); + private static final SimpleDateFormat HOUR_PRECISION_DATE_FORMAT = new SimpleDateFormat("yyyyMMddHH"); private static boolean isWithin(final long millis, final long span, final TimeUnit unit) { return System.currentTimeMillis() - millis <= unit.toMillis(span); @@ -60,6 +62,91 @@ public class DateUtils extends android.text.format.DateUtils { return new SimpleDateFormat(localizedPattern, locale).format(new Date(time)); } + public static String getHourFormat(Context c) { + return (DateFormat.is24HourFormat(c)) ? "HH:mm" : "hh:mm a"; + } + + public static String getDisplayFormattedTimeSpanString(final Context c, final Locale locale, final long timestamp) { + if (isWithin(timestamp, 1, TimeUnit.MINUTES)) { + return c.getString(R.string.DateUtils_just_now); + } else if (isToday(timestamp)) { + return getFormattedDateTime(timestamp, getHourFormat(c), locale); + } else if (isWithin(timestamp, 6, TimeUnit.DAYS)) { + return getFormattedDateTime(timestamp, "EEE " + getHourFormat(c), locale); + } else if (isWithin(timestamp, 365, TimeUnit.DAYS)) { + return getFormattedDateTime(timestamp, "MMM d " + getHourFormat(c), locale); + } else { + return getFormattedDateTime(timestamp, "MMM d " + getHourFormat(c) + ", yyyy", locale); + } + } + + public static SimpleDateFormat getDetailedDateFormatter(Context context, Locale locale) { + String dateFormatPattern; + + if (DateFormat.is24HourFormat(context)) { + dateFormatPattern = getLocalizedPattern("MMM d, yyyy HH:mm:ss zzz", locale); + } else { + dateFormatPattern = getLocalizedPattern("MMM d, yyyy hh:mm:ss a zzz", locale); + } + + return new SimpleDateFormat(dateFormatPattern, locale); + } + + public static String getRelativeDate(@NonNull Context context, + @NonNull Locale locale, + long timestamp) + { + if (isToday(timestamp)) { + return context.getString(R.string.DateUtils_today); + } else if (isYesterday(timestamp)) { + return context.getString(R.string.DateUtils_yesterday); + } else { + return getFormattedDateTime(timestamp, "EEE, MMM d, yyyy", locale); + } + } + + public static boolean isSameDay(long t1, long t2) { + return DAY_PRECISION_DATE_FORMAT.format(new Date(t1)).equals(DAY_PRECISION_DATE_FORMAT.format(new Date(t2))); + } + + public static boolean isSameHour(long t1, long t2) { + return HOUR_PRECISION_DATE_FORMAT.format(new Date(t1)).equals(HOUR_PRECISION_DATE_FORMAT.format(new Date(t2))); + } + + private static String getLocalizedPattern(String template, Locale locale) { + return DateFormat.getBestDateTimePattern(locale, template); + } + + /** + * e.g. 2020-09-04T19:17:51Z + * https://www.iso.org/iso-8601-date-and-time-format.html + * + * Note: SDK_INT == 0 check needed to pass unit tests due to JVM date parser differences. + * + * @return The timestamp if able to be parsed, otherwise -1. + */ + @SuppressLint("ObsoleteSdkInt") + public static long parseIso8601(@Nullable String date) { + SimpleDateFormat format; + if (Build.VERSION.SDK_INT == 0 || Build.VERSION.SDK_INT >= 24) { + format = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssX", Locale.getDefault()); + } else { + format = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ", Locale.getDefault()); + } + + if (date.isEmpty()) { + return -1; + } + + try { + return format.parse(date).getTime(); + } catch (ParseException e) { + Log.w(TAG, "Failed to parse date.", e); + return -1; + } + } + + // region Deprecated public static String getBriefRelativeTimeSpanString(final Context c, final Locale locale, final long timestamp) { if (isWithin(timestamp, 1, TimeUnit.MINUTES)) { return c.getString(R.string.DateUtils_just_now); @@ -96,86 +183,5 @@ public class DateUtils extends android.text.format.DateUtils { return getFormattedDateTime(timestamp, format.toString(), locale); } } - - public static String getDayPrecisionTimeSpanString(Context context, Locale locale, long timestamp) { - SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyyMMdd"); - - if (simpleDateFormat.format(System.currentTimeMillis()).equals(simpleDateFormat.format(timestamp))) { - return context.getString(R.string.DeviceListItem_today); - } else { - String format; - - if (isWithin(timestamp, 6, TimeUnit.DAYS)) format = "EEE "; - else if (isWithin(timestamp, 365, TimeUnit.DAYS)) format = "MMM d"; - else format = "MMM d, yyy"; - - return getFormattedDateTime(timestamp, format, locale); - } - } - - public static SimpleDateFormat getDetailedDateFormatter(Context context, Locale locale) { - String dateFormatPattern; - - if (DateFormat.is24HourFormat(context)) { - dateFormatPattern = getLocalizedPattern("MMM d, yyyy HH:mm:ss zzz", locale); - } else { - dateFormatPattern = getLocalizedPattern("MMM d, yyyy hh:mm:ss a zzz", locale); - } - - return new SimpleDateFormat(dateFormatPattern, locale); - } - - public static String getRelativeDate(@NonNull Context context, - @NonNull Locale locale, - long timestamp) - { - if (isToday(timestamp)) { - return context.getString(R.string.DateUtils_today); - } else if (isYesterday(timestamp)) { - return context.getString(R.string.DateUtils_yesterday); - } else { - return getFormattedDateTime(timestamp, "EEE, MMM d, yyyy", locale); - } - } - - public static boolean isSameDay(long t1, long t2) { - return DATE_FORMAT.format(new Date(t1)).equals(DATE_FORMAT.format(new Date(t2))); - } - - public static boolean isSameExtendedRelativeTimestamp(@NonNull Context context, @NonNull Locale locale, long t1, long t2) { - return getExtendedRelativeTimeSpanString(context, locale, t1).equals(getExtendedRelativeTimeSpanString(context, locale, t2)); - } - - private static String getLocalizedPattern(String template, Locale locale) { - return DateFormat.getBestDateTimePattern(locale, template); - } - - /** - * e.g. 2020-09-04T19:17:51Z - * https://www.iso.org/iso-8601-date-and-time-format.html - * - * Note: SDK_INT == 0 check needed to pass unit tests due to JVM date parser differences. - * - * @return The timestamp if able to be parsed, otherwise -1. - */ - @SuppressLint("ObsoleteSdkInt") - public static long parseIso8601(@Nullable String date) { - SimpleDateFormat format; - if (Build.VERSION.SDK_INT == 0 || Build.VERSION.SDK_INT >= 24) { - format = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssX", Locale.getDefault()); - } else { - format = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ", Locale.getDefault()); - } - - if (date.isEmpty()) { - return -1; - } - - try { - return format.parse(date).getTime(); - } catch (ParseException e) { - Log.w(TAG, "Failed to parse date.", e); - return -1; - } - } + // endregion } diff --git a/app/src/main/java/org/thoughtcrime/securesms/loki/utilities/GeneralUtilities.kt b/app/src/main/java/org/thoughtcrime/securesms/util/GeneralUtilities.kt similarity index 53% rename from app/src/main/java/org/thoughtcrime/securesms/loki/utilities/GeneralUtilities.kt rename to app/src/main/java/org/thoughtcrime/securesms/util/GeneralUtilities.kt index 6bb7a7a9e0..00e3e44418 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/loki/utilities/GeneralUtilities.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/util/GeneralUtilities.kt @@ -1,4 +1,4 @@ -package org.thoughtcrime.securesms.loki.utilities +package org.thoughtcrime.securesms.util import android.content.res.Resources import android.os.Build @@ -14,6 +14,19 @@ fun Resources.getColorWithID(@ColorRes id: Int, theme: Resources.Theme?): Int { } fun toPx(dp: Int, resources: Resources): Int { - val scale = resources.displayMetrics.density - return (dp * scale).roundToInt() + return toPx(dp.toFloat(), resources).roundToInt() +} + +fun toPx(dp: Float, resources: Resources): Float { + val scale = resources.displayMetrics.density + return (dp * scale) +} + +fun toDp(px: Int, resources: Resources): Int { + return toDp(px.toFloat(), resources).roundToInt() +} + +fun toDp(px: Float, resources: Resources): Float { + val scale = resources.displayMetrics.density + return (px / scale) } diff --git a/app/src/main/java/org/thoughtcrime/securesms/loki/views/GlowView.kt b/app/src/main/java/org/thoughtcrime/securesms/util/GlowView.kt similarity index 76% rename from app/src/main/java/org/thoughtcrime/securesms/loki/views/GlowView.kt rename to app/src/main/java/org/thoughtcrime/securesms/util/GlowView.kt index e48a67fbc9..16151fd6c4 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/loki/views/GlowView.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/util/GlowView.kt @@ -1,4 +1,4 @@ -package org.thoughtcrime.securesms.loki.views +package org.thoughtcrime.securesms.util import android.animation.ArgbEvaluator import android.animation.ValueAnimator @@ -6,13 +6,14 @@ import android.content.Context import android.graphics.* import android.util.AttributeSet import android.view.View -import android.view.ViewOutlineProvider import android.widget.LinearLayout +import android.widget.RelativeLayout import androidx.annotation.ColorInt import androidx.annotation.ColorRes import network.loki.messenger.R -import org.thoughtcrime.securesms.loki.utilities.getColorWithID -import org.thoughtcrime.securesms.loki.utilities.toPx +import org.thoughtcrime.securesms.util.getColorWithID +import org.thoughtcrime.securesms.util.toPx +import kotlin.math.roundToInt interface GlowView { var mainColor: Int @@ -155,4 +156,50 @@ class PathDotView : View, GlowView { super.onDraw(c) } // endregion -} \ No newline at end of file +} + +class InputBarButtonImageViewContainer : RelativeLayout, GlowView { + @ColorInt override var mainColor: Int = 0 + set(newValue) { field = newValue; fillPaint.color = newValue } + @ColorInt var strokeColor: Int = 0 + set(newValue) { field = newValue; strokePaint.color = newValue } + @ColorInt override var sessionShadowColor: Int = 0 // Unused + + private val fillPaint: Paint by lazy { + val result = Paint() + result.style = Paint.Style.FILL + result.isAntiAlias = true + result + } + + private val strokePaint: Paint by lazy { + val result = Paint() + result.style = Paint.Style.STROKE + result.isAntiAlias = true + result.strokeWidth = 1.0f + result.alpha = (255 * 0.2f).roundToInt() + result + } + + // region Lifecycle + constructor(context: Context) : super(context) { } + constructor(context: Context, attrs: AttributeSet) : super(context, attrs) { } + constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr) { } + + init { + setWillNotDraw(false) + } + // endregion + + // region Updating + override fun onDraw(c: Canvas) { + val w = width.toFloat() + val h = height.toFloat() + c.drawCircle(w / 2, h / 2, w / 2, fillPaint) + if (strokeColor != 0) { + c.drawCircle(w / 2, h / 2, w / 2, strokePaint) + } + super.onDraw(c) + } + // endregion +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/loki/utilities/IP2Country.kt b/app/src/main/java/org/thoughtcrime/securesms/util/IP2Country.kt similarity index 97% rename from app/src/main/java/org/thoughtcrime/securesms/loki/utilities/IP2Country.kt rename to app/src/main/java/org/thoughtcrime/securesms/util/IP2Country.kt index c3070a16e4..5f8678a58c 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/loki/utilities/IP2Country.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/util/IP2Country.kt @@ -1,4 +1,4 @@ -package org.thoughtcrime.securesms.loki.utilities +package org.thoughtcrime.securesms.util import android.content.BroadcastReceiver import android.content.Context @@ -51,7 +51,7 @@ class IP2Country private constructor(private val context: Context) { public lateinit var shared: IP2Country - public val isInitialized: Boolean get() = ::shared.isInitialized + public val isInitialized: Boolean get() = Companion::shared.isInitialized public fun configureIfNeeded(context: Context) { if (isInitialized) { return; } diff --git a/app/src/main/java/org/thoughtcrime/securesms/loki/utilities/PointFUtilities.kt b/app/src/main/java/org/thoughtcrime/securesms/util/PointFUtilities.kt similarity index 93% rename from app/src/main/java/org/thoughtcrime/securesms/loki/utilities/PointFUtilities.kt rename to app/src/main/java/org/thoughtcrime/securesms/util/PointFUtilities.kt index c05e3fe1cd..2cbe20aa00 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/loki/utilities/PointFUtilities.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/util/PointFUtilities.kt @@ -1,7 +1,8 @@ -package org.thoughtcrime.securesms.loki.utilities +package org.thoughtcrime.securesms.util import android.graphics.PointF import android.view.View +import org.thoughtcrime.securesms.util.hitRect fun PointF.distanceTo(other: PointF): Float { return Math.sqrt(Math.pow(this.x.toDouble() - other.x.toDouble(), 2.toDouble()) + Math.pow(this.y.toDouble() - other.y.toDouble(), 2.toDouble())).toFloat() diff --git a/app/src/main/java/org/thoughtcrime/securesms/loki/utilities/QRCodeUtilities.kt b/app/src/main/java/org/thoughtcrime/securesms/util/QRCodeUtilities.kt similarity index 96% rename from app/src/main/java/org/thoughtcrime/securesms/loki/utilities/QRCodeUtilities.kt rename to app/src/main/java/org/thoughtcrime/securesms/util/QRCodeUtilities.kt index 26d3f163b5..f7d1e3e8ad 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/loki/utilities/QRCodeUtilities.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/util/QRCodeUtilities.kt @@ -1,4 +1,4 @@ -package org.thoughtcrime.securesms.loki.utilities +package org.thoughtcrime.securesms.util import android.graphics.Bitmap import android.graphics.Color diff --git a/app/src/main/java/org/thoughtcrime/securesms/loki/fragments/ScanQRCodeFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/util/ScanQRCodeFragment.kt similarity index 97% rename from app/src/main/java/org/thoughtcrime/securesms/loki/fragments/ScanQRCodeFragment.kt rename to app/src/main/java/org/thoughtcrime/securesms/util/ScanQRCodeFragment.kt index d0c6af135e..9eecc15b75 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/loki/fragments/ScanQRCodeFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/util/ScanQRCodeFragment.kt @@ -1,4 +1,4 @@ -package org.thoughtcrime.securesms.loki.fragments +package org.thoughtcrime.securesms.util import android.content.res.Configuration import android.os.Bundle diff --git a/app/src/main/java/org/thoughtcrime/securesms/loki/fragments/ScanQRCodePlaceholderFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/util/ScanQRCodePlaceholderFragment.kt similarity index 94% rename from app/src/main/java/org/thoughtcrime/securesms/loki/fragments/ScanQRCodePlaceholderFragment.kt rename to app/src/main/java/org/thoughtcrime/securesms/util/ScanQRCodePlaceholderFragment.kt index 13e46b6fe1..5b36de7aea 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/loki/fragments/ScanQRCodePlaceholderFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/util/ScanQRCodePlaceholderFragment.kt @@ -1,4 +1,4 @@ -package org.thoughtcrime.securesms.loki.fragments +package org.thoughtcrime.securesms.util import android.os.Bundle import androidx.fragment.app.Fragment diff --git a/app/src/main/java/org/thoughtcrime/securesms/loki/fragments/ScanQRCodeWrapperFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/util/ScanQRCodeWrapperFragment.kt similarity index 95% rename from app/src/main/java/org/thoughtcrime/securesms/loki/fragments/ScanQRCodeWrapperFragment.kt rename to app/src/main/java/org/thoughtcrime/securesms/util/ScanQRCodeWrapperFragment.kt index 92eae8378a..6913e3cb8a 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/loki/fragments/ScanQRCodeWrapperFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/util/ScanQRCodeWrapperFragment.kt @@ -1,4 +1,4 @@ -package org.thoughtcrime.securesms.loki.fragments +package org.thoughtcrime.securesms.util import android.Manifest import android.content.pm.PackageManager @@ -11,6 +11,7 @@ import androidx.fragment.app.Fragment import com.tbruyelle.rxpermissions2.RxPermissions import network.loki.messenger.R import org.thoughtcrime.securesms.qr.ScanListener +import org.thoughtcrime.securesms.util.ScanQRCodeFragment class ScanQRCodeWrapperFragment : Fragment(), ScanQRCodePlaceholderFragmentDelegate, ScanListener { @@ -31,7 +32,7 @@ class ScanQRCodeWrapperFragment : Fragment(), ScanQRCodePlaceholderFragmentDeleg override fun setUserVisibleHint(isVisibleToUser: Boolean) { super.setUserVisibleHint(isVisibleToUser) - enabled = isVisibleToUser + enabled = isVisibleToUser } override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { @@ -87,5 +88,6 @@ class ScanQRCodeWrapperFragment : Fragment(), ScanQRCodePlaceholderFragmentDeleg } interface ScanQRCodeWrapperFragmentDelegate { + fun handleQRCodeScanned(string: String) } \ No newline at end of file diff --git a/app/src/main/java/org/thoughtcrime/securesms/loki/protocol/SessionMetaProtocol.kt b/app/src/main/java/org/thoughtcrime/securesms/util/SessionMetaProtocol.kt similarity index 94% rename from app/src/main/java/org/thoughtcrime/securesms/loki/protocol/SessionMetaProtocol.kt rename to app/src/main/java/org/thoughtcrime/securesms/util/SessionMetaProtocol.kt index 1f8bdca7ad..fcfa1d082b 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/loki/protocol/SessionMetaProtocol.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/util/SessionMetaProtocol.kt @@ -1,4 +1,4 @@ -package org.thoughtcrime.securesms.loki.protocol +package org.thoughtcrime.securesms.util import org.session.libsession.utilities.Address import org.session.libsession.utilities.recipients.Recipient @@ -22,7 +22,7 @@ object SessionMetaProtocol { } fun removeTimestamps(timestamps: Set) { - this.timestamps.removeAll(timestamps) + SessionMetaProtocol.timestamps.removeAll(timestamps) } @JvmStatic diff --git a/app/src/main/java/org/thoughtcrime/securesms/loki/viewmodel/State.kt b/app/src/main/java/org/thoughtcrime/securesms/util/State.kt similarity index 78% rename from app/src/main/java/org/thoughtcrime/securesms/loki/viewmodel/State.kt rename to app/src/main/java/org/thoughtcrime/securesms/util/State.kt index 94227d0e0c..2372c107b0 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/loki/viewmodel/State.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/util/State.kt @@ -1,4 +1,4 @@ -package org.thoughtcrime.securesms.loki.viewmodel +package org.thoughtcrime.securesms.util sealed class State { object Loading : State() diff --git a/app/src/main/java/org/thoughtcrime/securesms/loki/utilities/UiModeUtilities.kt b/app/src/main/java/org/thoughtcrime/securesms/util/UiModeUtilities.kt similarity index 97% rename from app/src/main/java/org/thoughtcrime/securesms/loki/utilities/UiModeUtilities.kt rename to app/src/main/java/org/thoughtcrime/securesms/util/UiModeUtilities.kt index 70368b85e2..39df0fccca 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/loki/utilities/UiModeUtilities.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/util/UiModeUtilities.kt @@ -1,4 +1,4 @@ -package org.thoughtcrime.securesms.loki.utilities +package org.thoughtcrime.securesms.util import android.content.Context import android.content.res.Configuration diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/Util.java b/app/src/main/java/org/thoughtcrime/securesms/util/Util.java index 2384fe482a..41b18f0247 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/util/Util.java +++ b/app/src/main/java/org/thoughtcrime/securesms/util/Util.java @@ -19,7 +19,6 @@ package org.thoughtcrime.securesms.util; import android.annotation.TargetApi; import android.app.ActivityManager; import android.content.Context; -import android.content.pm.PackageManager; import android.os.Build.VERSION; import android.os.Build.VERSION_CODES; import android.text.TextUtils; diff --git a/app/src/main/java/org/thoughtcrime/securesms/loki/utilities/ViewGroupUtilities.kt b/app/src/main/java/org/thoughtcrime/securesms/util/ViewGroupUtilities.kt similarity index 74% rename from app/src/main/java/org/thoughtcrime/securesms/loki/utilities/ViewGroupUtilities.kt rename to app/src/main/java/org/thoughtcrime/securesms/util/ViewGroupUtilities.kt index 3e9144206c..29aef9853d 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/loki/utilities/ViewGroupUtilities.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/util/ViewGroupUtilities.kt @@ -1,4 +1,4 @@ -package org.thoughtcrime.securesms.loki.utilities +package org.thoughtcrime.securesms.util import android.view.ViewGroup diff --git a/app/src/main/java/org/thoughtcrime/securesms/loki/utilities/ViewUtilities.kt b/app/src/main/java/org/thoughtcrime/securesms/util/ViewUtilities.kt similarity index 89% rename from app/src/main/java/org/thoughtcrime/securesms/loki/utilities/ViewUtilities.kt rename to app/src/main/java/org/thoughtcrime/securesms/util/ViewUtilities.kt index c9d135ba24..6d4d053b6d 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/loki/utilities/ViewUtilities.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/util/ViewUtilities.kt @@ -1,4 +1,4 @@ -package org.thoughtcrime.securesms.loki.utilities +package org.thoughtcrime.securesms.util import android.animation.Animator import android.animation.AnimatorListenerAdapter @@ -21,9 +21,13 @@ val View.hitRect: Rect } fun View.animateSizeChange(@DimenRes startSizeID: Int, @DimenRes endSizeID: Int, animationDuration: Long = 250) { - val layoutParams = this.layoutParams val startSize = resources.getDimension(startSizeID) val endSize = resources.getDimension(endSizeID) + animateSizeChange(startSize, endSize) +} + +fun View.animateSizeChange(startSize: Float, endSize: Float, animationDuration: Long = 250) { + val layoutParams = this.layoutParams val animation = ValueAnimator.ofObject(FloatEvaluator(), startSize, endSize) animation.duration = animationDuration animation.addUpdateListener { animator -> diff --git a/app/src/main/res/drawable-hdpi/ic_add_white_24dp.png b/app/src/main/res/drawable-hdpi/ic_add_white_24dp.png deleted file mode 100644 index b48ba111a3..0000000000 Binary files a/app/src/main/res/drawable-hdpi/ic_add_white_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-hdpi/ic_call_made_grey600_24dp.png b/app/src/main/res/drawable-hdpi/ic_call_made_grey600_24dp.png deleted file mode 100644 index 7d0807b0bf..0000000000 Binary files a/app/src/main/res/drawable-hdpi/ic_call_made_grey600_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-hdpi/ic_call_missed_grey600_24dp.png b/app/src/main/res/drawable-hdpi/ic_call_missed_grey600_24dp.png deleted file mode 100644 index 0241747c68..0000000000 Binary files a/app/src/main/res/drawable-hdpi/ic_call_missed_grey600_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-hdpi/ic_call_received_grey600_24dp.png b/app/src/main/res/drawable-hdpi/ic_call_received_grey600_24dp.png deleted file mode 100644 index 5c9a88d126..0000000000 Binary files a/app/src/main/res/drawable-hdpi/ic_call_received_grey600_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-hdpi/ic_check_white_24dp.png b/app/src/main/res/drawable-hdpi/ic_check_white_24dp.png deleted file mode 100644 index 468ea5acd0..0000000000 Binary files a/app/src/main/res/drawable-hdpi/ic_check_white_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-hdpi/ic_favorite_grey600_24dp.png b/app/src/main/res/drawable-hdpi/ic_favorite_grey600_24dp.png deleted file mode 100644 index 2bdad7d3f1..0000000000 Binary files a/app/src/main/res/drawable-hdpi/ic_favorite_grey600_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-hdpi/ic_group_grey600_24dp.png b/app/src/main/res/drawable-hdpi/ic_group_grey600_24dp.png deleted file mode 100644 index 816fe2053f..0000000000 Binary files a/app/src/main/res/drawable-hdpi/ic_group_grey600_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-hdpi/ic_menu_login.png b/app/src/main/res/drawable-hdpi/ic_menu_login.png deleted file mode 100644 index 163f0aa854..0000000000 Binary files a/app/src/main/res/drawable-hdpi/ic_menu_login.png and /dev/null differ diff --git a/app/src/main/res/drawable-hdpi/ic_refresh_white_24dp.png b/app/src/main/res/drawable-hdpi/ic_refresh_white_24dp.png deleted file mode 100644 index 38cd52d9d5..0000000000 Binary files a/app/src/main/res/drawable-hdpi/ic_refresh_white_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-hdpi/ic_security_white_24dp.png b/app/src/main/res/drawable-hdpi/ic_security_white_24dp.png deleted file mode 100644 index 262800a4d8..0000000000 Binary files a/app/src/main/res/drawable-hdpi/ic_security_white_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-hdpi/ic_timer_disabled.png b/app/src/main/res/drawable-hdpi/ic_timer_disabled.png deleted file mode 100644 index 9e6127d54f..0000000000 Binary files a/app/src/main/res/drawable-hdpi/ic_timer_disabled.png and /dev/null differ diff --git a/app/src/main/res/drawable-hdpi/quick_camera_dark.png b/app/src/main/res/drawable-hdpi/quick_camera_dark.png deleted file mode 100644 index aa3a3e5717..0000000000 Binary files a/app/src/main/res/drawable-hdpi/quick_camera_dark.png and /dev/null differ diff --git a/app/src/main/res/drawable-mdpi/ic_add_white_24dp.png b/app/src/main/res/drawable-mdpi/ic_add_white_24dp.png deleted file mode 100644 index eeba9f6696..0000000000 Binary files a/app/src/main/res/drawable-mdpi/ic_add_white_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-mdpi/ic_call_made_grey600_24dp.png b/app/src/main/res/drawable-mdpi/ic_call_made_grey600_24dp.png deleted file mode 100644 index f275722ffe..0000000000 Binary files a/app/src/main/res/drawable-mdpi/ic_call_made_grey600_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-mdpi/ic_call_missed_grey600_24dp.png b/app/src/main/res/drawable-mdpi/ic_call_missed_grey600_24dp.png deleted file mode 100644 index 609ef52617..0000000000 Binary files a/app/src/main/res/drawable-mdpi/ic_call_missed_grey600_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-mdpi/ic_call_received_grey600_24dp.png b/app/src/main/res/drawable-mdpi/ic_call_received_grey600_24dp.png deleted file mode 100644 index 685982d8e2..0000000000 Binary files a/app/src/main/res/drawable-mdpi/ic_call_received_grey600_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-mdpi/ic_check_white_24dp.png b/app/src/main/res/drawable-mdpi/ic_check_white_24dp.png deleted file mode 100644 index 6a93d69194..0000000000 Binary files a/app/src/main/res/drawable-mdpi/ic_check_white_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-mdpi/ic_favorite_grey600_24dp.png b/app/src/main/res/drawable-mdpi/ic_favorite_grey600_24dp.png deleted file mode 100644 index 63f13a33b4..0000000000 Binary files a/app/src/main/res/drawable-mdpi/ic_favorite_grey600_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-mdpi/ic_group_grey600_24dp.png b/app/src/main/res/drawable-mdpi/ic_group_grey600_24dp.png deleted file mode 100644 index 2c69b7cc14..0000000000 Binary files a/app/src/main/res/drawable-mdpi/ic_group_grey600_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-mdpi/ic_menu_login.png b/app/src/main/res/drawable-mdpi/ic_menu_login.png deleted file mode 100644 index 07dd6a510e..0000000000 Binary files a/app/src/main/res/drawable-mdpi/ic_menu_login.png and /dev/null differ diff --git a/app/src/main/res/drawable-mdpi/ic_plus_24.png b/app/src/main/res/drawable-mdpi/ic_plus_24.png new file mode 100644 index 0000000000..5a11ea9a1d Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_plus_24.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_refresh_white_24dp.png b/app/src/main/res/drawable-mdpi/ic_refresh_white_24dp.png deleted file mode 100644 index 48e37b75c4..0000000000 Binary files a/app/src/main/res/drawable-mdpi/ic_refresh_white_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-mdpi/ic_security_white_24dp.png b/app/src/main/res/drawable-mdpi/ic_security_white_24dp.png deleted file mode 100644 index 44ee7346e3..0000000000 Binary files a/app/src/main/res/drawable-mdpi/ic_security_white_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-mdpi/ic_timer_disabled.png b/app/src/main/res/drawable-mdpi/ic_timer_disabled.png deleted file mode 100644 index b2cd951a91..0000000000 Binary files a/app/src/main/res/drawable-mdpi/ic_timer_disabled.png and /dev/null differ diff --git a/app/src/main/res/drawable-mdpi/quick_camera_dark.png b/app/src/main/res/drawable-mdpi/quick_camera_dark.png deleted file mode 100644 index 4dbf6c77bc..0000000000 Binary files a/app/src/main/res/drawable-mdpi/quick_camera_dark.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/ic_add_white_24dp.png b/app/src/main/res/drawable-xhdpi/ic_add_white_24dp.png deleted file mode 100644 index 67bb598e52..0000000000 Binary files a/app/src/main/res/drawable-xhdpi/ic_add_white_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/ic_call_made_grey600_24dp.png b/app/src/main/res/drawable-xhdpi/ic_call_made_grey600_24dp.png deleted file mode 100644 index 1e609bb5e6..0000000000 Binary files a/app/src/main/res/drawable-xhdpi/ic_call_made_grey600_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/ic_call_missed_grey600_24dp.png b/app/src/main/res/drawable-xhdpi/ic_call_missed_grey600_24dp.png deleted file mode 100644 index fb5e2794f0..0000000000 Binary files a/app/src/main/res/drawable-xhdpi/ic_call_missed_grey600_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/ic_call_received_grey600_24dp.png b/app/src/main/res/drawable-xhdpi/ic_call_received_grey600_24dp.png deleted file mode 100644 index 91b5587a8a..0000000000 Binary files a/app/src/main/res/drawable-xhdpi/ic_call_received_grey600_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/ic_check_white_24dp.png b/app/src/main/res/drawable-xhdpi/ic_check_white_24dp.png deleted file mode 100644 index 9868d19a42..0000000000 Binary files a/app/src/main/res/drawable-xhdpi/ic_check_white_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/ic_favorite_grey600_24dp.png b/app/src/main/res/drawable-xhdpi/ic_favorite_grey600_24dp.png deleted file mode 100644 index c4ad1cb303..0000000000 Binary files a/app/src/main/res/drawable-xhdpi/ic_favorite_grey600_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/ic_group_grey600_24dp.png b/app/src/main/res/drawable-xhdpi/ic_group_grey600_24dp.png deleted file mode 100644 index 1865da6942..0000000000 Binary files a/app/src/main/res/drawable-xhdpi/ic_group_grey600_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/ic_menu_login.png b/app/src/main/res/drawable-xhdpi/ic_menu_login.png deleted file mode 100644 index 17c2e5c39b..0000000000 Binary files a/app/src/main/res/drawable-xhdpi/ic_menu_login.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/ic_plus_24.png b/app/src/main/res/drawable-xhdpi/ic_plus_24.png new file mode 100644 index 0000000000..1421a562e9 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_plus_24.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_refresh_white_24dp.png b/app/src/main/res/drawable-xhdpi/ic_refresh_white_24dp.png deleted file mode 100644 index 7e5c6ef194..0000000000 Binary files a/app/src/main/res/drawable-xhdpi/ic_refresh_white_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/ic_security_white_24dp.png b/app/src/main/res/drawable-xhdpi/ic_security_white_24dp.png deleted file mode 100644 index 7e306c303c..0000000000 Binary files a/app/src/main/res/drawable-xhdpi/ic_security_white_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/ic_timer_disabled.png b/app/src/main/res/drawable-xhdpi/ic_timer_disabled.png deleted file mode 100644 index 45165abecc..0000000000 Binary files a/app/src/main/res/drawable-xhdpi/ic_timer_disabled.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/quick_camera_dark.png b/app/src/main/res/drawable-xhdpi/quick_camera_dark.png deleted file mode 100644 index 753805275b..0000000000 Binary files a/app/src/main/res/drawable-xhdpi/quick_camera_dark.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_add_white_24dp.png b/app/src/main/res/drawable-xxhdpi/ic_add_white_24dp.png deleted file mode 100644 index 0fdced8fce..0000000000 Binary files a/app/src/main/res/drawable-xxhdpi/ic_add_white_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_call_made_grey600_24dp.png b/app/src/main/res/drawable-xxhdpi/ic_call_made_grey600_24dp.png deleted file mode 100644 index 66a9ff46e8..0000000000 Binary files a/app/src/main/res/drawable-xxhdpi/ic_call_made_grey600_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_call_missed_grey600_24dp.png b/app/src/main/res/drawable-xxhdpi/ic_call_missed_grey600_24dp.png deleted file mode 100644 index 84c13861cb..0000000000 Binary files a/app/src/main/res/drawable-xxhdpi/ic_call_missed_grey600_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_call_received_grey600_24dp.png b/app/src/main/res/drawable-xxhdpi/ic_call_received_grey600_24dp.png deleted file mode 100644 index f4be04c671..0000000000 Binary files a/app/src/main/res/drawable-xxhdpi/ic_call_received_grey600_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_check_white_24dp.png b/app/src/main/res/drawable-xxhdpi/ic_check_white_24dp.png deleted file mode 100644 index 2a7c32de61..0000000000 Binary files a/app/src/main/res/drawable-xxhdpi/ic_check_white_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_favorite_grey600_24dp.png b/app/src/main/res/drawable-xxhdpi/ic_favorite_grey600_24dp.png deleted file mode 100644 index 11f108dad1..0000000000 Binary files a/app/src/main/res/drawable-xxhdpi/ic_favorite_grey600_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_group_grey600_24dp.png b/app/src/main/res/drawable-xxhdpi/ic_group_grey600_24dp.png deleted file mode 100644 index 2aa030e4cf..0000000000 Binary files a/app/src/main/res/drawable-xxhdpi/ic_group_grey600_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_plus_24.png b/app/src/main/res/drawable-xxhdpi/ic_plus_24.png new file mode 100644 index 0000000000..d63c48c714 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_plus_24.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_refresh_white_24dp.png b/app/src/main/res/drawable-xxhdpi/ic_refresh_white_24dp.png deleted file mode 100644 index 72128fe690..0000000000 Binary files a/app/src/main/res/drawable-xxhdpi/ic_refresh_white_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_security_white_24dp.png b/app/src/main/res/drawable-xxhdpi/ic_security_white_24dp.png deleted file mode 100644 index 7bcb2fd013..0000000000 Binary files a/app/src/main/res/drawable-xxhdpi/ic_security_white_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_timer_disabled.png b/app/src/main/res/drawable-xxhdpi/ic_timer_disabled.png deleted file mode 100644 index fabf9ffdba..0000000000 Binary files a/app/src/main/res/drawable-xxhdpi/ic_timer_disabled.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxhdpi/quick_camera_dark.png b/app/src/main/res/drawable-xxhdpi/quick_camera_dark.png deleted file mode 100644 index c1a3549bfc..0000000000 Binary files a/app/src/main/res/drawable-xxhdpi/quick_camera_dark.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_check_white_24dp.png b/app/src/main/res/drawable-xxxhdpi/ic_check_white_24dp.png deleted file mode 100644 index d601ca823c..0000000000 Binary files a/app/src/main/res/drawable-xxxhdpi/ic_check_white_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_favorite_grey600_24dp.png b/app/src/main/res/drawable-xxxhdpi/ic_favorite_grey600_24dp.png deleted file mode 100644 index e8ee60e7e6..0000000000 Binary files a/app/src/main/res/drawable-xxxhdpi/ic_favorite_grey600_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_security_white_24dp.png b/app/src/main/res/drawable-xxxhdpi/ic_security_white_24dp.png deleted file mode 100644 index b1eddbd6c3..0000000000 Binary files a/app/src/main/res/drawable-xxxhdpi/ic_security_white_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_timer_disabled.png b/app/src/main/res/drawable-xxxhdpi/ic_timer_disabled.png deleted file mode 100644 index 6301f7bada..0000000000 Binary files a/app/src/main/res/drawable-xxxhdpi/ic_timer_disabled.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxxhdpi/oxen_dark_mode.png b/app/src/main/res/drawable-xxxhdpi/oxen_dark_mode.png new file mode 100644 index 0000000000..84c7bb1059 Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/oxen_dark_mode.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/oxen_light_mode.png b/app/src/main/res/drawable-xxxhdpi/oxen_light_mode.png new file mode 100644 index 0000000000..f2c7e60fbc Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/oxen_light_mode.png differ diff --git a/app/src/main/res/drawable/circle_tintable.xml b/app/src/main/res/drawable/circle_tintable.xml index 6c5c360635..60c36468e4 100644 --- a/app/src/main/res/drawable/circle_tintable.xml +++ b/app/src/main/res/drawable/circle_tintable.xml @@ -1,5 +1,6 @@ - + diff --git a/app/src/main/res/drawable/compose_divider_background.xml b/app/src/main/res/drawable/compose_divider_background.xml deleted file mode 100644 index 0046fe4e43..0000000000 --- a/app/src/main/res/drawable/compose_divider_background.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - diff --git a/app/src/main/res/drawable/contact_photo_background.xml b/app/src/main/res/drawable/contact_photo_background.xml deleted file mode 100644 index ff1153bf2a..0000000000 --- a/app/src/main/res/drawable/contact_photo_background.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/conversation_home_touch_highlight.xml b/app/src/main/res/drawable/conversation_home_touch_highlight.xml deleted file mode 100644 index 5fbf5ba7d0..0000000000 --- a/app/src/main/res/drawable/conversation_home_touch_highlight.xml +++ /dev/null @@ -1,15 +0,0 @@ - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/conversation_item_background.xml b/app/src/main/res/drawable/conversation_item_background.xml deleted file mode 100644 index dfae25e4f8..0000000000 --- a/app/src/main/res/drawable/conversation_item_background.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/app/src/main/res/drawable/conversation_item_background_animated.xml b/app/src/main/res/drawable/conversation_item_background_animated.xml deleted file mode 100644 index 4206e6a57f..0000000000 --- a/app/src/main/res/drawable/conversation_item_background_animated.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/app/src/main/res/drawable/default_dialog_background.xml b/app/src/main/res/drawable/default_dialog_background.xml index fff764ebd5..4cd206aca8 100644 --- a/app/src/main/res/drawable/default_dialog_background.xml +++ b/app/src/main/res/drawable/default_dialog_background.xml @@ -7,5 +7,4 @@ - \ No newline at end of file diff --git a/app/src/main/res/drawable/default_dialog_background_inset.xml b/app/src/main/res/drawable/default_dialog_background_inset.xml index b67cfb0b73..0ff315ebd3 100644 --- a/app/src/main/res/drawable/default_dialog_background_inset.xml +++ b/app/src/main/res/drawable/default_dialog_background_inset.xml @@ -2,6 +2,5 @@ + android:inset="@dimen/medium_spacing"> \ No newline at end of file diff --git a/app/src/main/res/drawable/error_round.xml b/app/src/main/res/drawable/error_round.xml deleted file mode 100644 index 56cc75291a..0000000000 --- a/app/src/main/res/drawable/error_round.xml +++ /dev/null @@ -1,16 +0,0 @@ - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_arrow_up.xml b/app/src/main/res/drawable/ic_arrow_up.xml new file mode 100644 index 0000000000..bbf9960747 --- /dev/null +++ b/app/src/main/res/drawable/ic_arrow_up.xml @@ -0,0 +1,15 @@ + + + + + diff --git a/app/src/main/res/drawable/ic_baseline_arrow_back_compact_24.xml b/app/src/main/res/drawable/ic_baseline_arrow_back_compact_24.xml deleted file mode 100644 index f632160152..0000000000 --- a/app/src/main/res/drawable/ic_baseline_arrow_back_compact_24.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/ic_baseline_key_24.xml b/app/src/main/res/drawable/ic_baseline_key_24.xml deleted file mode 100644 index 2316df207c..0000000000 --- a/app/src/main/res/drawable/ic_baseline_key_24.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/ic_baseline_refresh_24.xml b/app/src/main/res/drawable/ic_baseline_refresh_24.xml deleted file mode 100644 index f2be45bab5..0000000000 --- a/app/src/main/res/drawable/ic_baseline_refresh_24.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/ic_circle_plus.xml b/app/src/main/res/drawable/ic_circle_plus.xml deleted file mode 100644 index 5e243e5296..0000000000 --- a/app/src/main/res/drawable/ic_circle_plus.xml +++ /dev/null @@ -1,21 +0,0 @@ - - - - - diff --git a/app/src/main/res/drawable/ic_link.xml b/app/src/main/res/drawable/ic_link.xml new file mode 100644 index 0000000000..56b2c2168c --- /dev/null +++ b/app/src/main/res/drawable/ic_link.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_shield.xml b/app/src/main/res/drawable/ic_shield.xml deleted file mode 100644 index 67a9845daa..0000000000 --- a/app/src/main/res/drawable/ic_shield.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/info_round.xml b/app/src/main/res/drawable/info_round.xml deleted file mode 100644 index 668eaf3bee..0000000000 --- a/app/src/main/res/drawable/info_round.xml +++ /dev/null @@ -1,16 +0,0 @@ - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/scroll_to_bottom_button_background.xml b/app/src/main/res/drawable/input_bar_button_background.xml similarity index 52% rename from app/src/main/res/drawable/scroll_to_bottom_button_background.xml rename to app/src/main/res/drawable/input_bar_button_background.xml index 387c75be90..4de519558a 100644 --- a/app/src/main/res/drawable/scroll_to_bottom_button_background.xml +++ b/app/src/main/res/drawable/input_bar_button_background.xml @@ -1,8 +1,4 @@ - - - - \ No newline at end of file + android:shape="oval" /> \ No newline at end of file diff --git a/app/src/main/res/drawable/mention_candidate_view_background.xml b/app/src/main/res/drawable/mention_candidate_view_background.xml index af9549d111..1b30b3e72e 100644 --- a/app/src/main/res/drawable/mention_candidate_view_background.xml +++ b/app/src/main/res/drawable/mention_candidate_view_background.xml @@ -1,9 +1,9 @@ + android:color="@color/mention_candidates_view_background_ripple"> - + \ No newline at end of file diff --git a/app/src/main/res/drawable/message_bubble_background.xml b/app/src/main/res/drawable/message_bubble_background.xml deleted file mode 100644 index cda641b1da..0000000000 --- a/app/src/main/res/drawable/message_bubble_background.xml +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - - - - diff --git a/app/src/main/res/drawable/message_bubble_background_received_alone.xml b/app/src/main/res/drawable/message_bubble_background_received_alone.xml index cda641b1da..5e657c9793 100644 --- a/app/src/main/res/drawable/message_bubble_background_received_alone.xml +++ b/app/src/main/res/drawable/message_bubble_background_received_alone.xml @@ -1,10 +1,7 @@ - - + diff --git a/app/src/main/res/drawable/message_bubble_background_received_end.xml b/app/src/main/res/drawable/message_bubble_background_received_end.xml index 3e4e2c0562..09f5d7d6df 100644 --- a/app/src/main/res/drawable/message_bubble_background_received_end.xml +++ b/app/src/main/res/drawable/message_bubble_background_received_end.xml @@ -1,10 +1,7 @@ - - + - - + - - + - - + diff --git a/app/src/main/res/drawable/message_bubble_background_sent_end.xml b/app/src/main/res/drawable/message_bubble_background_sent_end.xml index 5bc4597c03..81325f1ad2 100644 --- a/app/src/main/res/drawable/message_bubble_background_sent_end.xml +++ b/app/src/main/res/drawable/message_bubble_background_sent_end.xml @@ -1,10 +1,7 @@ - - + - - + - - + - - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/profile_picture_view_small_foreground.xml b/app/src/main/res/drawable/profile_picture_view_small_foreground.xml deleted file mode 100644 index 6a46584bf8..0000000000 --- a/app/src/main/res/drawable/profile_picture_view_small_foreground.xml +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/prominent_dialog_button_background.xml b/app/src/main/res/drawable/prominent_dialog_button_background.xml deleted file mode 100644 index de3409d482..0000000000 --- a/app/src/main/res/drawable/prominent_dialog_button_background.xml +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/session_logo_white.xml b/app/src/main/res/drawable/session_logo_white.xml deleted file mode 100644 index 9927e8822f..0000000000 --- a/app/src/main/res/drawable/session_logo_white.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - diff --git a/app/src/main/res/drawable/view_lock_background.xml b/app/src/main/res/drawable/view_lock_background.xml new file mode 100644 index 0000000000..9d3510cd34 --- /dev/null +++ b/app/src/main/res/drawable/view_lock_background.xml @@ -0,0 +1,8 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/profile_picture_view_medium_foreground.xml b/app/src/main/res/drawable/view_quote_attachment_preview_background.xml similarity index 63% rename from app/src/main/res/drawable/profile_picture_view_medium_foreground.xml rename to app/src/main/res/drawable/view_quote_attachment_preview_background.xml index 808be8d0e4..2044a98c1a 100644 --- a/app/src/main/res/drawable/profile_picture_view_medium_foreground.xml +++ b/app/src/main/res/drawable/view_quote_attachment_preview_background.xml @@ -3,7 +3,7 @@ xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle"> - + - + \ No newline at end of file diff --git a/app/src/main/res/drawable/view_scroll_to_bottom_button_background.xml b/app/src/main/res/drawable/view_scroll_to_bottom_button_background.xml new file mode 100644 index 0000000000..512b3861a7 --- /dev/null +++ b/app/src/main/res/drawable/view_scroll_to_bottom_button_background.xml @@ -0,0 +1,7 @@ + + + + + diff --git a/app/src/main/res/drawable/profile_picture_view_large_foreground.xml b/app/src/main/res/drawable/view_voice_message_duration_text_view_background.xml similarity index 62% rename from app/src/main/res/drawable/profile_picture_view_large_foreground.xml rename to app/src/main/res/drawable/view_voice_message_duration_text_view_background.xml index e73b68d168..78871b9e1e 100644 --- a/app/src/main/res/drawable/profile_picture_view_large_foreground.xml +++ b/app/src/main/res/drawable/view_voice_message_duration_text_view_background.xml @@ -2,8 +2,6 @@ - - - - + + \ No newline at end of file diff --git a/app/src/main/res/layout-sw400dp/activity_display_name.xml b/app/src/main/res/layout-sw400dp/activity_display_name.xml index ecb0a35967..fe00d541a2 100644 --- a/app/src/main/res/layout-sw400dp/activity_display_name.xml +++ b/app/src/main/res/layout-sw400dp/activity_display_name.xml @@ -34,12 +34,13 @@ style="@style/SessionEditText" android:id="@+id/displayNameEditText" android:layout_width="match_parent" - android:layout_height="wrap_content" - android:paddingTop="28dp" - android:paddingBottom="28dp" + android:layout_height="64dp" android:layout_marginLeft="@dimen/very_large_spacing" android:layout_marginTop="12dp" android:layout_marginRight="@dimen/very_large_spacing" + android:paddingTop="0dp" + android:paddingBottom="0dp" + android:gravity="center_vertical" android:inputType="textCapWords" android:hint="@string/activity_display_name_edit_text_hint" /> diff --git a/app/src/main/res/layout-sw400dp/activity_landing.xml b/app/src/main/res/layout-sw400dp/activity_landing.xml index dc36e5fa57..d1cbc8f2f3 100644 --- a/app/src/main/res/layout-sw400dp/activity_landing.xml +++ b/app/src/main/res/layout-sw400dp/activity_landing.xml @@ -19,7 +19,7 @@ android:textStyle="bold" android:text="@string/activity_landing_title_2" /> - - - + - - +