diff --git a/.drone.jsonnet b/.drone.jsonnet
index dc81115ce9..b459742392 100644
--- a/.drone.jsonnet
+++ b/.drone.jsonnet
@@ -38,7 +38,9 @@ local ci_dep_mirror(want_mirror) = (if want_mirror then ' -DLOCAL_MIRROR=https:/
pull: 'always',
environment: { ANDROID_HOME: '/usr/lib/android-sdk' },
commands: [
- 'apt-get install -y ninja-build',
+ 'apt-get update',
+ 'apt-get install -y ninja-build openjdk-17-jdk',
+ 'update-java-alternatives -s java-1.17.0-openjdk-amd64',
'./gradlew testPlayDebugUnitTestCoverageReport'
],
}
@@ -78,7 +80,9 @@ local ci_dep_mirror(want_mirror) = (if want_mirror then ' -DLOCAL_MIRROR=https:/
pull: 'always',
environment: { SSH_KEY: { from_secret: 'SSH_KEY' }, ANDROID_HOME: '/usr/lib/android-sdk' },
commands: [
- 'apt-get install -y ninja-build',
+ 'apt-get update',
+ 'apt-get install -y ninja-build openjdk-17-jdk',
+ 'update-java-alternatives -s java-1.17.0-openjdk-amd64',
'./gradlew assemblePlayDebug',
'./scripts/drone-static-upload.sh'
],
diff --git a/app/build.gradle b/app/build.gradle
index df003aa855..88609da7af 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -13,8 +13,8 @@ configurations.forEach {
it.exclude module: "commons-logging"
}
-def canonicalVersionCode = 380
-def canonicalVersionName = "1.19.2"
+def canonicalVersionCode = 382
+def canonicalVersionName = "1.20.0"
def postFixSize = 10
def abiPostFix = ['armeabi-v7a' : 1,
@@ -88,7 +88,6 @@ android {
buildConfigField "String", "CONTENT_PROXY_HOST", "\"contentproxy.signal.org\""
buildConfigField "int", "CONTENT_PROXY_PORT", "443"
buildConfigField "String", "USER_AGENT", "\"OWA\""
- buildConfigField "String[]", "LANGUAGES", "new String[]{\"" + autoResConfig().collect { s -> s.replace('-r', '_') }.join('", "') + '"}'
buildConfigField "int", "CANONICAL_VERSION_CODE", "$canonicalVersionCode"
resourceConfigurations += []
@@ -221,11 +220,13 @@ android {
}
dependencies {
+ implementation project(':content-descriptions')
+
+ ksp("androidx.hilt:hilt-compiler:$jetpackHiltVersion")
+ ksp("com.google.dagger:hilt-compiler:$daggerHiltVersion")
+ ksp("com.github.bumptech.glide:ksp:$glideVersion")
implementation("com.google.dagger:hilt-android:$daggerHiltVersion")
- ksp("com.google.dagger:hilt-compiler:$daggerHiltVersion")
- ksp("androidx.hilt:hilt-compiler:$jetpackHiltVersion")
-
implementation "androidx.appcompat:appcompat:$appcompatVersion"
implementation 'androidx.recyclerview:recyclerview:1.2.1'
implementation "com.google.android.material:material:$materialVersion"
@@ -241,6 +242,7 @@ dependencies {
implementation "androidx.lifecycle:lifecycle-runtime-ktx:$lifecycleVersion"
implementation "androidx.lifecycle:lifecycle-livedata-ktx:$lifecycleVersion"
implementation "androidx.lifecycle:lifecycle-process:$lifecycleVersion"
+ implementation "androidx.lifecycle:lifecycle-viewmodel-compose:$lifecycleVersion"
implementation "androidx.lifecycle:lifecycle-extensions:2.2.0"
implementation "androidx.paging:paging-runtime-ktx:$pagingVersion"
implementation 'androidx.activity:activity-ktx:1.5.1'
@@ -248,12 +250,15 @@ dependencies {
implementation 'androidx.fragment:fragment-ktx:1.5.3'
implementation "androidx.core:core-ktx:$coreVersion"
implementation "androidx.work:work-runtime-ktx:2.7.1"
+
playImplementation ("com.google.firebase:firebase-messaging:24.0.0") {
exclude group: 'com.google.firebase', module: 'firebase-core'
exclude group: 'com.google.firebase', module: 'firebase-analytics'
exclude group: 'com.google.firebase', module: 'firebase-measurement-connector'
}
+
if (project.hasProperty('huawei')) huaweiImplementation 'com.huawei.hms:push:6.7.0.300'
+
implementation 'androidx.media3:media3-exoplayer:1.4.0'
implementation 'androidx.media3:media3-ui:1.4.0'
implementation 'org.conscrypt:conscrypt-android:2.5.2'
@@ -267,7 +272,6 @@ dependencies {
implementation 'com.github.chrisbanes:PhotoView:2.1.3'
implementation "com.github.bumptech.glide:glide:$glideVersion"
implementation "com.github.bumptech.glide:compose:1.0.0-beta01"
- ksp "com.github.bumptech.glide:ksp:$glideVersion"
implementation 'com.makeramen:roundedimageview:2.1.0'
implementation 'com.pnikosis:materialish-progress:1.5'
implementation 'org.greenrobot:eventbus:3.0.0'
@@ -305,7 +309,6 @@ dependencies {
implementation "nl.komponents.kovenant:kovenant:$kovenantVersion"
implementation "nl.komponents.kovenant:kovenant-android:$kovenantVersion"
implementation "com.jakewharton.rxbinding3:rxbinding:3.1.0"
- implementation "com.github.tbruyelle:rxpermissions:0.10.2"
implementation "com.github.ybq:Android-SpinKit:1.4.0"
implementation "com.opencsv:opencsv:4.6"
testImplementation "junit:junit:$junitVersion"
@@ -361,7 +364,7 @@ dependencies {
debugImplementation "androidx.compose.ui:ui-test-manifest:$composeVersion"
implementation "com.google.accompanist:accompanist-themeadapter-appcompat:0.33.1-alpha"
- implementation "com.google.accompanist:accompanist-permissions:0.33.1-alpha"
+ implementation "com.google.accompanist:accompanist-permissions:0.36.0"
implementation "com.google.accompanist:accompanist-drawablepainter:0.33.1-alpha"
implementation "androidx.camera:camera-camera2:1.3.2"
diff --git a/app/src/androidTest/java/network/loki/messenger/HomeActivityTests.kt b/app/src/androidTest/java/network/loki/messenger/HomeActivityTests.kt
index 2f3ba9fb10..43b347ba42 100644
--- a/app/src/androidTest/java/network/loki/messenger/HomeActivityTests.kt
+++ b/app/src/androidTest/java/network/loki/messenger/HomeActivityTests.kt
@@ -2,8 +2,6 @@ package network.loki.messenger
import android.Manifest
import android.app.Instrumentation
-import android.content.ClipboardManager
-import android.content.Context
import android.view.View
import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.Espresso.pressBack
@@ -16,8 +14,6 @@ import androidx.test.espresso.matcher.ViewMatchers.isDescendantOfA
import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
import androidx.test.espresso.matcher.ViewMatchers.isRoot
import androidx.test.espresso.matcher.ViewMatchers.withId
-import androidx.test.espresso.matcher.ViewMatchers.withSubstring
-import androidx.test.espresso.matcher.ViewMatchers.withText
import androidx.test.ext.junit.rules.ActivityScenarioRule
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.LargeTest
@@ -25,6 +21,7 @@ import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.uiautomator.By
import androidx.test.uiautomator.UiDevice
import com.adevinta.android.barista.interaction.PermissionGranter
+import com.bumptech.glide.Glide
import network.loki.messenger.util.InputBarButtonDrawableMatcher.Companion.inputButtonWithDrawable
import org.hamcrest.Matcher
import org.hamcrest.Matchers.allOf
@@ -36,11 +33,9 @@ import org.junit.Test
import org.junit.runner.RunWith
import org.session.libsession.messaging.sending_receiving.link_preview.LinkPreview
import org.session.libsession.utilities.TextSecurePreferences
-import org.session.libsignal.utilities.guava.Optional
import org.thoughtcrime.securesms.conversation.v2.ConversationActivityV2
import org.thoughtcrime.securesms.conversation.v2.input_bar.InputBar
import org.thoughtcrime.securesms.home.HomeActivity
-import com.bumptech.glide.Glide
/**
* Currently not used as part of our CI/Deployment processes !!!!
@@ -62,7 +57,6 @@ class HomeActivityTests {
@Before
fun setUp() {
InstrumentationRegistry.getInstrumentation().addMonitor(activityMonitor)
-
}
@After
@@ -96,10 +90,10 @@ class HomeActivityTests {
device.pressKeyCode(67)
// Continue with display name
- objectFromDesc(R.string.continue_2).click()
+ objectFromDesc(R.string.theContinue).click()
// Continue with default push notification setting
- objectFromDesc(R.string.continue_2).click()
+ objectFromDesc(R.string.theContinue).click()
// PN select
if (hasViewedSeed) {
@@ -110,7 +104,6 @@ class HomeActivityTests {
PermissionGranter.allowPermissionsIfNeeded(Manifest.permission.POST_NOTIFICATIONS)
}
-
/* private fun goToMyChat() {
onView(withId(R.id.newConversationButton)).perform(ViewActions.click())
onView(withId(R.id.createPrivateChatButton)).perform(ViewActions.click())
@@ -131,7 +124,7 @@ class HomeActivityTests {
@Test
fun testLaunches_dismiss_seedView() {
setupLoggedInState()
- objectFromDesc(R.string.continue_2).click()
+ objectFromDesc(R.string.theContinue).click()
objectFromDesc(R.string.copy).click()
pressBack()
onView(withId(R.id.seedReminderView)).check(matches(not(isDisplayed())))
@@ -182,6 +175,7 @@ class HomeActivityTests {
onView(withText(dialogPromptText)).check(matches(isDisplayed()))
}*/
+
/**
* Perform action of waiting for a specific time.
*/
@@ -198,5 +192,4 @@ class HomeActivityTests {
}
}
}
-
}
\ No newline at end of file
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 98f9aa4b5f..eac4ef3300 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -37,7 +37,7 @@
-
+
@@ -79,7 +79,7 @@
android:networkSecurityConfig="@xml/network_security_configuration"
android:supportsRtl="true"
android:theme="@style/Theme.Session.DayNight"
- tools:replace="android:allowBackup">
+ tools:replace="android:allowBackup,android:label" >
@@ -130,12 +130,16 @@
+ android:label="@string/sessionSettings" />
+
@@ -147,11 +151,11 @@
android:name="org.thoughtcrime.securesms.preferences.BlockedContactsActivity"
android:screenOrientation="portrait"
android:theme="@style/Theme.Session.DayNight.FlatActionBar"
- android:label="@string/blocked_contacts_title"
+ android:label="@string/conversationsBlockedContacts"
/>
@@ -264,18 +268,10 @@
-
{
- // Don't generate a new profile key here; we do that when the user changes their profile picture
- Log.d("Loki-Avatar", "Uploading Avatar Started");
- String encodedProfileKey = TextSecurePreferences.getProfileKey(ApplicationContext.this);
- try {
- // Read the file into a byte array
- InputStream inputStream = AvatarHelper.getInputStreamFor(ApplicationContext.this, Address.fromSerialized(userPublicKey));
- ByteArrayOutputStream baos = new ByteArrayOutputStream();
- int count;
- byte[] buffer = new byte[1024];
- while ((count = inputStream.read(buffer, 0, buffer.length)) != -1) {
- baos.write(buffer, 0, count);
- }
- baos.flush();
- byte[] profilePicture = baos.toByteArray();
- // Re-upload it
- ProfilePictureUtilities.INSTANCE.upload(profilePicture, encodedProfileKey, ApplicationContext.this).success(unit -> {
- // Update the last profile picture upload date
- TextSecurePreferences.setLastProfilePictureUpload(ApplicationContext.this, new Date().getTime());
- Log.d("Loki-Avatar", "Uploading Avatar Finished");
- return Unit.INSTANCE;
- });
- } catch (Exception e) {
- Log.e("Loki-Avatar", "Uploading avatar failed.");
- }
- });
+ ProfilePictureUtilities.INSTANCE.resubmitProfilePictureIfNeeded(this);
}
private void loadEmojiSearchIndexIfNeeded() {
@@ -486,7 +459,7 @@ public class ApplicationContext extends Application implements DefaultLifecycleO
// Method to clear the local data - returns true on success otherwise false
/**
- * Clear all local profile data and message history then restart the app after a brief delay.
+ * Clear all local profile data and message history.
* @return true on success, false otherwise.
*/
@SuppressLint("ApplySharedPref")
@@ -498,6 +471,16 @@ public class ApplicationContext extends Application implements DefaultLifecycleO
return false;
}
configFactory.keyPairChanged();
+ return true;
+ }
+
+ /**
+ * Clear all local profile data and message history then restart the app after a brief delay.
+ * @return true on success, false otherwise.
+ */
+ @SuppressLint("ApplySharedPref")
+ public boolean clearAllDataAndRestart() {
+ clearAllData();
Util.runOnMain(() -> new Handler().postDelayed(ApplicationContext.this::restartApplication, 200));
return true;
}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/BaseActionBarActivity.java b/app/src/main/java/org/thoughtcrime/securesms/BaseActionBarActivity.java
index a99fe83430..c3321504ea 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/BaseActionBarActivity.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/BaseActionBarActivity.java
@@ -17,8 +17,6 @@ import androidx.appcompat.app.ActionBar;
import androidx.appcompat.app.AppCompatActivity;
import org.session.libsession.utilities.TextSecurePreferences;
-import org.session.libsession.utilities.dynamiclanguage.DynamicLanguageActivityHelper;
-import org.session.libsession.utilities.dynamiclanguage.DynamicLanguageContextWrapper;
import org.thoughtcrime.securesms.conversation.v2.WindowUtil;
import org.thoughtcrime.securesms.util.ActivityUtilitiesKt;
import org.thoughtcrime.securesms.util.ThemeState;
@@ -97,7 +95,6 @@ public abstract class BaseActionBarActivity extends AppCompatActivity {
protected void onResume() {
super.onResume();
initializeScreenshotSecurity(true);
- DynamicLanguageActivityHelper.recreateIfNotInCorrectLanguage(this, TextSecurePreferences.getLanguage(this));
String name = getResources().getString(R.string.app_name);
Bitmap icon = BitmapFactory.decodeResource(getResources(), R.drawable.ic_launcher_foreground);
int color = getResources().getColor(R.color.app_icon_background);
@@ -137,9 +134,4 @@ public abstract class BaseActionBarActivity extends AppCompatActivity {
}
}
}
-
- @Override
- protected void attachBaseContext(Context newBase) {
- super.attachBaseContext(DynamicLanguageContextWrapper.updateContext(newBase, TextSecurePreferences.getLanguage(newBase)));
- }
}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/BaseActivity.java b/app/src/main/java/org/thoughtcrime/securesms/BaseActivity.java
index d44978b05b..8722c0e092 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/BaseActivity.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/BaseActivity.java
@@ -1,31 +1,20 @@
package org.thoughtcrime.securesms;
import android.app.ActivityManager;
-import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import androidx.fragment.app.FragmentActivity;
-import org.session.libsession.utilities.TextSecurePreferences;
-import org.session.libsession.utilities.dynamiclanguage.DynamicLanguageActivityHelper;
-import org.session.libsession.utilities.dynamiclanguage.DynamicLanguageContextWrapper;
-
import network.loki.messenger.R;
public abstract class BaseActivity extends FragmentActivity {
@Override
protected void onResume() {
super.onResume();
- DynamicLanguageActivityHelper.recreateIfNotInCorrectLanguage(this, TextSecurePreferences.getLanguage(this));
String name = getResources().getString(R.string.app_name);
Bitmap icon = BitmapFactory.decodeResource(getResources(), R.drawable.ic_launcher_foreground);
int color = getResources().getColor(R.color.app_icon_background);
setTaskDescription(new ActivityManager.TaskDescription(name, icon, color));
}
-
- @Override
- protected void attachBaseContext(Context newBase) {
- super.attachBaseContext(DynamicLanguageContextWrapper.updateContext(newBase, TextSecurePreferences.getLanguage(newBase)));
- }
}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/DeleteMediaDialog.kt b/app/src/main/java/org/thoughtcrime/securesms/DeleteMediaDialog.kt
index af38c31ff3..3d38857b50 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/DeleteMediaDialog.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/DeleteMediaDialog.kt
@@ -8,20 +8,9 @@ class DeleteMediaDialog {
@JvmStatic
fun show(context: Context, recordCount: Int, doDelete: Runnable) = context.showSessionDialog {
iconAttribute(R.attr.dialog_alert_icon)
- title(
- context.resources.getQuantityString(
- R.plurals.MediaOverviewActivity_Media_delete_confirm_title,
- recordCount,
- recordCount
- )
- )
- text(
- context.resources.getQuantityString(R.plurals.MediaOverviewActivity_Media_delete_confirm_message,
- recordCount,
- recordCount
- )
- )
- button(R.string.delete) { doDelete.run() }
+ title(context.resources.getQuantityString(R.plurals.deleteMessage, recordCount, recordCount))
+ text(context.resources.getString(R.string.deleteMessageDescriptionEveryone))
+ dangerButton(R.string.delete) { doDelete.run() }
cancelButton()
}
}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/DeleteMediaPreviewDialog.kt b/app/src/main/java/org/thoughtcrime/securesms/DeleteMediaPreviewDialog.kt
index 0390a3007d..b8aad6c22a 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/DeleteMediaPreviewDialog.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/DeleteMediaPreviewDialog.kt
@@ -9,9 +9,9 @@ class DeleteMediaPreviewDialog {
fun show(context: Context, doDelete: Runnable) {
context.showSessionDialog {
iconAttribute(R.attr.dialog_alert_icon)
- title(R.string.MediaPreviewActivity_media_delete_confirmation_title)
- text(R.string.MediaPreviewActivity_media_delete_confirmation_message)
- button(R.string.delete) { doDelete.run() }
+ title(context.resources.getQuantityString(R.plurals.deleteMessage, 1, 1))
+ text(R.string.deleteMessageDescriptionEveryone)
+ dangerButton(R.string.delete) { doDelete.run() }
cancelButton()
}
}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/MediaPreviewActivity.java b/app/src/main/java/org/thoughtcrime/securesms/MediaPreviewActivity.java
index c71f5d041c..f761cdd4e7 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/MediaPreviewActivity.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/MediaPreviewActivity.java
@@ -16,6 +16,8 @@
*/
package org.thoughtcrime.securesms;
+import static org.session.libsession.utilities.StringSubstitutionConstants.APP_NAME_KEY;
+
import android.annotation.SuppressLint;
import android.content.Context;
import android.content.Intent;
@@ -23,8 +25,8 @@ import android.database.Cursor;
import android.database.CursorIndexOutOfBoundsException;
import android.net.Uri;
import android.os.AsyncTask;
-import android.os.Build;
import android.os.Build.VERSION;
+import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
@@ -42,7 +44,6 @@ import android.view.WindowInsetsController;
import android.widget.FrameLayout;
import android.widget.TextView;
import android.widget.Toast;
-
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.ActionBar;
@@ -54,10 +55,14 @@ import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import androidx.viewpager.widget.PagerAdapter;
import androidx.viewpager.widget.ViewPager;
-
import com.bumptech.glide.Glide;
import com.bumptech.glide.RequestManager;
-
+import com.squareup.phrase.Phrase;
+import java.io.IOException;
+import java.util.Locale;
+import java.util.WeakHashMap;
+import kotlin.Unit;
+import network.loki.messenger.R;
import org.session.libsession.messaging.messages.control.DataExtractionNotification;
import org.session.libsession.messaging.sending_receiving.MessageSender;
import org.session.libsession.messaging.sending_receiving.attachments.DatabaseAttachment;
@@ -78,15 +83,8 @@ import org.thoughtcrime.securesms.mms.Slide;
import org.thoughtcrime.securesms.permissions.Permissions;
import org.thoughtcrime.securesms.util.AttachmentUtil;
import org.thoughtcrime.securesms.util.DateUtils;
-import org.thoughtcrime.securesms.util.SaveAttachmentTask;
import org.thoughtcrime.securesms.util.SaveAttachmentTask.Attachment;
-
-import java.io.IOException;
-import java.util.Locale;
-import java.util.WeakHashMap;
-
-import kotlin.Unit;
-import network.loki.messenger.R;
+import org.thoughtcrime.securesms.util.SaveAttachmentTask;
/**
* Activity for displaying media attachments in-app
@@ -242,12 +240,12 @@ public class MediaPreviewActivity extends PassphraseRequiredActionBarActivity im
CharSequence relativeTimeSpan;
if (mediaItem.date > 0) {
- relativeTimeSpan = DateUtils.getDisplayFormattedTimeSpanString(this, Locale.getDefault(), mediaItem.date);
+ relativeTimeSpan = DateUtils.INSTANCE.getDisplayFormattedTimeSpanString(this, Locale.getDefault(), mediaItem.date);
} else {
- relativeTimeSpan = getString(R.string.MediaPreviewActivity_draft);
+ relativeTimeSpan = getString(R.string.draft);
}
- if (mediaItem.outgoing) getSupportActionBar().setTitle(getString(R.string.MediaPreviewActivity_you));
+ if (mediaItem.outgoing) getSupportActionBar().setTitle(getString(R.string.you));
else if (mediaItem.recipient != null) getSupportActionBar().setTitle(mediaItem.recipient.toShortString());
else getSupportActionBar().setTitle("");
@@ -258,7 +256,6 @@ public class MediaPreviewActivity extends PassphraseRequiredActionBarActivity im
@Override
public void onResume() {
super.onResume();
-
initializeMedia();
}
@@ -291,7 +288,7 @@ public class MediaPreviewActivity extends PassphraseRequiredActionBarActivity im
captionContainer = findViewById(R.id.media_preview_caption_container);
playbackControlsContainer = findViewById(R.id.media_preview_playback_controls_container);
- setSupportActionBar(findViewById(R.id.toolbar));
+ setSupportActionBar(findViewById(R.id.search_toolbar));
ActionBar actionBar = getSupportActionBar();
actionBar.setDisplayHomeAsUpEnabled(true);
actionBar.setHomeButtonEnabled(true);
@@ -361,7 +358,7 @@ public class MediaPreviewActivity extends PassphraseRequiredActionBarActivity im
private void initializeMedia() {
if (!isContentTypeSupported(initialMediaType)) {
Log.w(TAG, "Unsupported media type sent to MediaPreviewActivity, finishing.");
- Toast.makeText(getApplicationContext(), R.string.MediaPreviewActivity_unssuported_media_type, Toast.LENGTH_LONG).show();
+ Toast.makeText(getApplicationContext(), R.string.attachmentsErrorNotSupported, Toast.LENGTH_LONG).show();
finish();
}
@@ -411,12 +408,14 @@ public class MediaPreviewActivity extends PassphraseRequiredActionBarActivity im
MediaItem mediaItem = getCurrentMediaItem();
if (mediaItem == null) return;
- SaveAttachmentTask.showWarningDialog(this, 1, () -> {
+ SaveAttachmentTask.showOneTimeWarningDialogOrSave(this, 1, () -> {
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(this, R.string.MediaPreviewActivity_unable_to_write_to_external_storage_without_permission, Toast.LENGTH_LONG).show())
+ .withPermanentDenialDialog(getPermanentlyDeniedStorageText())
+ .onAnyDenied(() -> {
+ Toast.makeText(this, getPermanentlyDeniedStorageText(), Toast.LENGTH_LONG).show();
+ })
.onAllGranted(() -> {
SaveAttachmentTask saveTask = new SaveAttachmentTask(MediaPreviewActivity.this);
long saveDate = (mediaItem.date > 0) ? mediaItem.date : SnodeAPI.getNowWithOffset();
@@ -432,6 +431,12 @@ public class MediaPreviewActivity extends PassphraseRequiredActionBarActivity im
});
}
+ private String getPermanentlyDeniedStorageText(){
+ return Phrase.from(getApplicationContext(), R.string.permissionsStorageDeniedLegacy)
+ .put(APP_NAME_KEY, getString(R.string.app_name))
+ .format().toString();
+ }
+
private void sendMediaSavedNotificationIfNeeded() {
if (conversationRecipient.isGroupRecipient()) return;
DataExtractionNotification message = new DataExtractionNotification(new DataExtractionNotification.Kind.MediaSaved(SnodeAPI.getNowWithOffset()));
@@ -482,6 +487,7 @@ public class MediaPreviewActivity extends PassphraseRequiredActionBarActivity im
super.onOptionsItemSelected(item);
switch (item.getItemId()) {
+ // TODO / WARNING: R.id values are NON-CONSTANT in Gradle 8.0+ - what would be the best way to address this?! -AL 2024/08/26
case R.id.media_preview__overview: showOverview(); return true;
case R.id.media_preview__forward: forward(); return true;
case R.id.save: saveToDisk(); return true;
@@ -532,15 +538,11 @@ public class MediaPreviewActivity extends PassphraseRequiredActionBarActivity im
throw new RuntimeException("restartItem = " + restartItem + ", data.second = " + data.second + " leftIsRecent = " + leftIsRecent, e);
}
- if (item == 0) {
- viewPagerListener.onPageSelected(0);
- }
+ if (item == 0) { viewPagerListener.onPageSelected(0); }
}
@Override
- public void onLoaderReset(@NonNull Loader> loader) {
-
- }
+ public void onLoaderReset(@NonNull Loader> loader) { /* Do nothing */ }
private class ViewPagerListener implements ViewPager.OnPageChangeListener {
@@ -575,13 +577,11 @@ public class MediaPreviewActivity extends PassphraseRequiredActionBarActivity im
@Override
public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
-
+ /* Do nothing */
}
@Override
- public void onPageScrollStateChanged(int state) {
-
- }
+ public void onPageScrollStateChanged(int state) { /* Do nothing */ }
}
private static class SingleItemPagerAdapter extends MediaItemAdapter {
@@ -646,9 +646,7 @@ public class MediaPreviewActivity extends PassphraseRequiredActionBarActivity im
}
@Override
- public void pause(int position) {
-
- }
+ public void pause(int position) { /* Do nothing */ }
@Override
public @Nullable View getPlaybackControls(int position) {
diff --git a/app/src/main/java/org/thoughtcrime/securesms/MuteDialog.kt b/app/src/main/java/org/thoughtcrime/securesms/MuteDialog.kt
index 071da43311..d5e551d02a 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/MuteDialog.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/MuteDialog.kt
@@ -4,24 +4,49 @@ import android.content.Context
import androidx.annotation.StringRes
import androidx.appcompat.app.AlertDialog
import network.loki.messenger.R
+import org.session.libsession.LocalisedTimeUtil
+import org.session.libsession.utilities.StringSubstitutionConstants.TIME_LARGE_KEY
+import org.thoughtcrime.securesms.ui.getSubbedString
import java.util.concurrent.TimeUnit
+import kotlin.time.Duration.Companion.milliseconds
+import kotlin.time.Duration.Companion.seconds
fun showMuteDialog(
context: Context,
onMuteDuration: (Long) -> Unit
): AlertDialog = context.showSessionDialog {
- title(R.string.MuteDialog_mute_notifications)
- items(Option.values().map { it.stringRes }.map(context::getString).toTypedArray()) {
- onMuteDuration(Option.values()[it].getTime())
+ title(R.string.notificationsMute)
+
+ items(Option.entries.mapIndexed { index, entry ->
+
+ if (entry.stringRes == R.string.notificationsMute) {
+ context.getString(R.string.notificationsMute)
+ } else {
+ val largeTimeUnitString = LocalisedTimeUtil.getDurationWithSingleLargestTimeUnit(
+ context,
+ Option.entries[index].duration.milliseconds
+ )
+ context.getSubbedString(entry.stringRes, TIME_LARGE_KEY to largeTimeUnitString)
+ }
+ }.toTypedArray()) {
+ // Note: We add the current timestamp to the mute duration to get the un-mute timestamp
+ // that gets stored in the database via ConversationMenuHelper.mute().
+ // Also: This is a kludge, but we ADD one second to the mute duration because otherwise by
+ // the time the view for how long the conversation is muted for gets set then it's actually
+ // less than the entire duration - so 1 hour becomes 59 minutes, 1 day becomes 23 hours etc.
+ // As we really want to see the actual set time (1 hour / 1 day etc.) then we'll bump it by
+ // 1 second which is neither here nor there in the grand scheme of things.
+ val muteTime = Option.entries[it].duration
+ val muteTimeFromNow = if (muteTime == Long.MAX_VALUE) muteTime
+ else muteTime + System.currentTimeMillis() + 1.seconds.inWholeMilliseconds
+ onMuteDuration(muteTimeFromNow)
}
}
-private enum class Option(@StringRes val stringRes: Int, val getTime: () -> Long) {
- ONE_HOUR(R.string.arrays__mute_for_one_hour, duration = TimeUnit.HOURS.toMillis(1)),
- TWO_HOURS(R.string.arrays__mute_for_two_hours, duration = TimeUnit.HOURS.toMillis(2)),
- ONE_DAY(R.string.arrays__mute_for_one_day, duration = TimeUnit.DAYS.toMillis(1)),
- SEVEN_DAYS(R.string.arrays__mute_for_seven_days, duration = TimeUnit.DAYS.toMillis(7)),
- FOREVER(R.string.arrays__mute_forever, getTime = { Long.MAX_VALUE });
-
- constructor(@StringRes stringRes: Int, duration: Long): this(stringRes, { System.currentTimeMillis() + duration })
-}
+private enum class Option(@StringRes val stringRes: Int, val duration: Long) {
+ ONE_HOUR(R.string.notificationsMuteFor, duration = TimeUnit.HOURS.toMillis(1)),
+ TWO_HOURS(R.string.notificationsMuteFor, duration = TimeUnit.HOURS.toMillis(2)),
+ ONE_DAY(R.string.notificationsMuteFor, duration = TimeUnit.DAYS.toMillis(1)),
+ SEVEN_DAYS(R.string.notificationsMuteFor, duration = TimeUnit.DAYS.toMillis(7)),
+ FOREVER(R.string.notificationsMute, duration = Long.MAX_VALUE );
+}
\ No newline at end of file
diff --git a/app/src/main/java/org/thoughtcrime/securesms/PassphrasePromptActivity.java b/app/src/main/java/org/thoughtcrime/securesms/PassphrasePromptActivity.java
index afc993df8a..16b5856766 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/PassphrasePromptActivity.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/PassphrasePromptActivity.java
@@ -16,6 +16,8 @@
*/
package org.thoughtcrime.securesms;
+import static org.session.libsession.utilities.StringSubstitutionConstants.APP_NAME_KEY;
+
import android.animation.Animator;
import android.app.KeyguardManager;
import android.content.ComponentName;
@@ -25,20 +27,18 @@ import android.content.ServiceConnection;
import android.graphics.PorterDuff;
import android.os.Bundle;
import android.os.IBinder;
-import android.text.SpannableString;
-import android.text.Spanned;
-import android.text.style.RelativeSizeSpan;
-import android.text.style.TypefaceSpan;
import android.view.View;
import android.view.animation.Animation;
import android.view.animation.BounceInterpolator;
import android.view.animation.TranslateAnimation;
import android.widget.Button;
import android.widget.ImageView;
-
+import android.widget.TextView;
import androidx.core.hardware.fingerprint.FingerprintManagerCompat;
import androidx.core.os.CancellationSignal;
-
+import com.squareup.phrase.Phrase;
+import java.security.Signature;
+import network.loki.messenger.R;
import org.session.libsession.utilities.TextSecurePreferences;
import org.session.libsignal.utilities.Log;
import org.thoughtcrime.securesms.components.AnimatingToggle;
@@ -46,11 +46,6 @@ import org.thoughtcrime.securesms.crypto.BiometricSecretProvider;
import org.thoughtcrime.securesms.service.KeyCachingService;
import org.thoughtcrime.securesms.util.AnimationCompleteListener;
-import java.security.InvalidKeyException;
-import java.security.Signature;
-
-import network.loki.messenger.R;
-
//TODO Rename to ScreenLockActivity and refactor to Kotlin.
public class PassphrasePromptActivity extends BaseActionBarActivity {
@@ -158,6 +153,16 @@ public class PassphrasePromptActivity extends BaseActionBarActivity {
}
private void initializeResources() {
+
+ TextView statusTitle = findViewById(R.id.app_lock_status_title);
+ if (statusTitle != null) {
+ Context c = getApplicationContext();
+ String lockedTxt = Phrase.from(c, R.string.lockAppLocked)
+ .put(APP_NAME_KEY, c.getString(R.string.app_name))
+ .format().toString();
+ statusTitle.setText(lockedTxt);
+ }
+
visibilityToggle = findViewById(R.id.button_toggle);
fingerprintPrompt = findViewById(R.id.fingerprint_auth_container);
lockScreenButton = findViewById(R.id.lock_screen_auth_container);
@@ -165,10 +170,6 @@ public class PassphrasePromptActivity extends BaseActionBarActivity {
fingerprintCancellationSignal = new CancellationSignal();
fingerprintListener = new FingerprintListener();
- SpannableString hint = new SpannableString(" " + getString(R.string.PassphrasePromptActivity_enter_passphrase));
- hint.setSpan(new RelativeSizeSpan(0.9f), 0, hint.length(), Spanned.SPAN_INCLUSIVE_INCLUSIVE);
- hint.setSpan(new TypefaceSpan("sans-serif"), 0, hint.length(), Spanned.SPAN_INCLUSIVE_INCLUSIVE);
-
fingerprintPrompt.setImageResource(R.drawable.ic_fingerprint_white_48dp);
fingerprintPrompt.getBackground().setColorFilter(getResources().getColor(R.color.signal_primary), PorterDuff.Mode.SRC_IN);
diff --git a/app/src/main/java/org/thoughtcrime/securesms/SessionDialogBuilder.kt b/app/src/main/java/org/thoughtcrime/securesms/SessionDialogBuilder.kt
index 71e04230f2..69d58411f3 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/SessionDialogBuilder.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/SessionDialogBuilder.kt
@@ -1,5 +1,7 @@
package org.thoughtcrime.securesms
+import android.content.ClipData
+import android.content.ClipboardManager
import android.content.Context
import android.content.Intent
import android.net.Uri
@@ -7,12 +9,14 @@ import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup.LayoutParams.MATCH_PARENT
import android.view.ViewGroup.LayoutParams.WRAP_CONTENT
+import android.widget.ArrayAdapter
import android.widget.Button
import android.widget.LinearLayout
import android.widget.LinearLayout.VERTICAL
import android.widget.Space
import android.widget.TextView
import androidx.annotation.AttrRes
+import androidx.annotation.ColorRes
import androidx.annotation.LayoutRes
import androidx.annotation.StringRes
import androidx.annotation.StyleRes
@@ -30,6 +34,7 @@ annotation class DialogDsl
@DialogDsl
class SessionDialogBuilder(val context: Context) {
+ private val dp8 = toPx(8, context.resources)
private val dp20 = toPx(20, context.resources)
private val dp40 = toPx(40, context.resources)
private val dp60 = toPx(60, context.resources)
@@ -37,13 +42,15 @@ class SessionDialogBuilder(val context: Context) {
private val dialogBuilder: AlertDialog.Builder = AlertDialog.Builder(context)
private var dialog: AlertDialog? = null
- private fun dismiss() = dialog?.dismiss()
+ fun dismiss() = dialog?.dismiss()
private val topView = LinearLayout(context)
.apply { setPadding(0, dp20, 0, 0) }
.apply { orientation = VERTICAL }
.also(dialogBuilder::setCustomTitle)
+
private val contentView = LinearLayout(context).apply { orientation = VERTICAL }
+
private val buttonLayout = LinearLayout(context)
private val root = LinearLayout(context).apply { orientation = VERTICAL }
@@ -53,24 +60,29 @@ class SessionDialogBuilder(val context: Context) {
addView(buttonLayout)
}
- fun title(@StringRes id: Int) = title(context.getString(id))
-
- fun title(text: CharSequence?) = title(text?.toString())
+ // Main title entry point
fun title(text: String?) {
- text(text, R.style.TextAppearance_AppCompat_Title) { setPadding(dp20, 0, dp20, 0) }
+ text(text, R.style.TextAppearance_Session_Dialog_Title) { setPadding(dp20, 0, dp20, 0) }
}
- fun text(@StringRes id: Int, style: Int = 0) = text(context.getString(id), style)
- fun text(text: CharSequence?, @StyleRes style: Int = 0) {
+ // Convenience assessor for title that takes a string resource
+ fun title(@StringRes id: Int) = title(context.getString(id))
+
+ // Convenience accessor for title that takes a CharSequence
+ fun title(text: CharSequence?) = title(text?.toString())
+
+ fun text(@StringRes id: Int, style: Int? = null) = text(context.getString(id), style)
+
+ fun text(text: CharSequence?, @StyleRes style: Int? = null) {
text(text, style) {
layoutParams = LinearLayout.LayoutParams(MATCH_PARENT, WRAP_CONTENT)
.apply { updateMargins(dp40, 0, dp40, 0) }
}
}
- private fun text(text: CharSequence?, @StyleRes style: Int, modify: TextView.() -> Unit) {
+ private fun text(text: CharSequence?, @StyleRes style: Int? = null, modify: TextView.() -> Unit) {
text ?: return
- TextView(context, null, 0, style)
+ TextView(context, null, 0, style ?: R.style.TextAppearance_Session_Dialog_Message)
.apply {
setText(text)
textAlignment = View.TEXT_ALIGNMENT_CENTER
@@ -78,7 +90,7 @@ class SessionDialogBuilder(val context: Context) {
}.let(topView::addView)
Space(context).apply {
- layoutParams = LinearLayout.LayoutParams(0, dp20)
+ layoutParams = LinearLayout.LayoutParams(0, dp8)
}.let(topView::addView)
}
@@ -95,17 +107,31 @@ class SessionDialogBuilder(val context: Context) {
fun singleChoiceItems(
options: Collection,
currentSelected: Int = 0,
+ dismissOnRadioSelect: Boolean = true,
onSelect: (Int) -> Unit
- ) = singleChoiceItems(options.toTypedArray(), currentSelected, onSelect)
+ ) = singleChoiceItems(
+ options.toTypedArray(),
+ currentSelected,
+ dismissOnRadioSelect,
+ onSelect
+ )
fun singleChoiceItems(
options: Array,
currentSelected: Int = 0,
+ dismissOnRadioSelect: Boolean = true,
onSelect: (Int) -> Unit
- ): AlertDialog.Builder = dialogBuilder.setSingleChoiceItems(
- options,
- currentSelected
- ) { dialog, it -> onSelect(it); dialog.dismiss() }
+ ): AlertDialog.Builder{
+ val adapter = ArrayAdapter(context, R.layout.view_dialog_single_choice_item, options)
+
+ return dialogBuilder.setSingleChoiceItems(
+ adapter,
+ currentSelected
+ ) { dialog, it ->
+ onSelect(it)
+ if(dismissOnRadioSelect) dialog.dismiss()
+ }
+ }
fun items(
options: Array,
@@ -125,16 +151,21 @@ class SessionDialogBuilder(val context: Context) {
) { listener() }
fun okButton(listener: (() -> Unit) = {}) = button(android.R.string.ok) { listener() }
- fun cancelButton(listener: (() -> Unit) = {}) = button(android.R.string.cancel, R.string.AccessibilityId_cancel_button) { listener() }
+
+ fun cancelButton(listener: (() -> Unit) = {}) = button(android.R.string.cancel, R.string.AccessibilityId_cancel) { listener() }
fun button(
@StringRes text: Int,
@StringRes contentDescriptionRes: Int = text,
@StyleRes style: Int = R.style.Widget_Session_Button_Dialog_UnimportantText,
+ @ColorRes textColor: Int? = null,
dismiss: Boolean = true,
listener: (() -> Unit) = {}
) = Button(context, null, 0, style).apply {
setText(text)
+ textColor?.let{
+ setTextColor(it)
+ }
contentDescription = resources.getString(contentDescriptionRes)
layoutParams = LinearLayout.LayoutParams(WRAP_CONTENT, dp60, 1f)
setOnClickListener {
@@ -149,22 +180,18 @@ class SessionDialogBuilder(val context: Context) {
fun Context.showSessionDialog(build: SessionDialogBuilder.() -> Unit): AlertDialog =
SessionDialogBuilder(this).apply { build() }.show()
-fun Context.showOpenUrlDialog(build: SessionDialogBuilder.() -> Unit): AlertDialog =
- SessionDialogBuilder(this).apply {
- title(R.string.urlOpen)
- text(R.string.urlOpenBrowser)
- build()
- }.show()
-fun Context.showOpenUrlDialog(url: String): AlertDialog =
- showOpenUrlDialog {
- okButton { openUrl(url) }
- cancelButton()
- }
+public fun Context.copyURLToClipboard(url: String) {
+ val clipboard = getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
+ val clip = ClipData.newPlainText(url, url)
+ clipboard.setPrimaryClip(clip)
+}
+// Method to actually open a given URL via an Intent that will use the default browser
fun Context.openUrl(url: String) = Intent(Intent.ACTION_VIEW, Uri.parse(url)).let(::startActivity)
fun Fragment.showSessionDialog(build: SessionDialogBuilder.() -> Unit): AlertDialog =
SessionDialogBuilder(requireContext()).apply { build() }.show()
+
fun Fragment.createSessionDialog(build: SessionDialogBuilder.() -> Unit): AlertDialog =
SessionDialogBuilder(requireContext()).apply { build() }.create()
diff --git a/app/src/main/java/org/thoughtcrime/securesms/ShareActivity.java b/app/src/main/java/org/thoughtcrime/securesms/ShareActivity.java
index f03840c1ab..3b939bf647 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/ShareActivity.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/ShareActivity.java
@@ -17,6 +17,8 @@
package org.thoughtcrime.securesms;
+import static org.session.libsession.utilities.StringSubstitutionConstants.APP_NAME_KEY;
+
import android.annotation.SuppressLint;
import android.content.Context;
import android.content.Intent;
@@ -29,12 +31,21 @@ import android.provider.OpenableColumns;
import android.view.MenuItem;
import android.view.View;
import android.widget.ImageView;
+import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.ActionBar;
import androidx.appcompat.widget.Toolbar;
+import com.squareup.phrase.Phrase;
+
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+
+import network.loki.messenger.R;
+
import org.session.libsession.utilities.Address;
import org.session.libsession.utilities.DistributionTypes;
import org.session.libsession.utilities.ViewUtil;
@@ -49,274 +60,267 @@ import org.thoughtcrime.securesms.mms.PartAuthority;
import org.thoughtcrime.securesms.providers.BlobProvider;
import org.thoughtcrime.securesms.util.MediaUtil;
-import java.io.FileInputStream;
-import java.io.IOException;
-import java.io.InputStream;
-
-import network.loki.messenger.R;
-
/**
* An activity to quickly share content with contacts
*
* @author Jake McGinty
*/
public class ShareActivity extends PassphraseRequiredActionBarActivity
- implements ContactSelectionListFragment.OnContactSelectedListener
-{
- private static final String TAG = ShareActivity.class.getSimpleName();
+ implements ContactSelectionListFragment.OnContactSelectedListener {
+ private static final String TAG = ShareActivity.class.getSimpleName();
- public static final String EXTRA_THREAD_ID = "thread_id";
- public static final String EXTRA_ADDRESS_MARSHALLED = "address_marshalled";
- public static final String EXTRA_DISTRIBUTION_TYPE = "distribution_type";
+ public static final String EXTRA_THREAD_ID = "thread_id";
+ public static final String EXTRA_ADDRESS_MARSHALLED = "address_marshalled";
+ public static final String EXTRA_DISTRIBUTION_TYPE = "distribution_type";
+ private ContactSelectionListFragment contactsFragment;
+ private SearchToolbar searchToolbar;
+ private ImageView searchAction;
+ private View progressWheel;
+ private Uri resolvedExtra;
+ private CharSequence resolvedPlaintext;
+ private String mimeType;
+ private boolean isPassingAlongMedia;
- private ContactSelectionListFragment contactsFragment;
- private SearchToolbar searchToolbar;
- private ImageView searchAction;
- private View progressWheel;
- private Uri resolvedExtra;
- private CharSequence resolvedPlaintext;
- private String mimeType;
- private boolean isPassingAlongMedia;
-
- @Override
- protected void onCreate(Bundle icicle, boolean ready) {
- if (!getIntent().hasExtra(ContactSelectionListFragment.DISPLAY_MODE)) {
- getIntent().putExtra(ContactSelectionListFragment.DISPLAY_MODE, DisplayMode.FLAG_ALL);
- }
-
- getIntent().putExtra(ContactSelectionListFragment.REFRESHABLE, false);
-
- setContentView(R.layout.share_activity);
-
- initializeToolbar();
- initializeResources();
- initializeSearch();
- initializeMedia();
- }
-
- @Override
- protected void onNewIntent(Intent intent) {
- Log.i(TAG, "onNewIntent()");
- super.onNewIntent(intent);
- setIntent(intent);
- initializeMedia();
- }
-
- @Override
- public void onPause() {
- super.onPause();
- if (!isPassingAlongMedia && resolvedExtra != null) {
- BlobProvider.getInstance().delete(this, resolvedExtra);
-
- if (!isFinishing()) {
- finish();
- }
- }
- }
-
- @Override
- public boolean onOptionsItemSelected(MenuItem item) {
- switch (item.getItemId()) {
- case android.R.id.home:
- onBackPressed();
- return true;
- }
- return super.onOptionsItemSelected(item);
- }
-
- @Override
- public void onBackPressed() {
- if (searchToolbar.isVisible()) searchToolbar.collapse();
- else super.onBackPressed();
- }
-
- private void initializeToolbar() {
- Toolbar toolbar = findViewById(R.id.toolbar);
- setSupportActionBar(toolbar);
- ActionBar actionBar = getSupportActionBar();
- actionBar.setDisplayHomeAsUpEnabled(true);
- actionBar.setHomeButtonEnabled(true);
- }
-
- private void initializeResources() {
- progressWheel = findViewById(R.id.progress_wheel);
- searchToolbar = findViewById(R.id.search_toolbar);
- searchAction = findViewById(R.id.search_action);
- contactsFragment = (ContactSelectionListFragment) getSupportFragmentManager().findFragmentById(R.id.contact_selection_list_fragment);
- contactsFragment.setOnContactSelectedListener(this);
- }
-
- private void initializeSearch() {
- searchAction.setOnClickListener(v -> searchToolbar.display(searchAction.getX() + (searchAction.getWidth() / 2),
- searchAction.getY() + (searchAction.getHeight() / 2)));
-
- searchToolbar.setListener(new SearchToolbar.SearchListener() {
- @Override
- public void onSearchTextChange(String text) {
- if (contactsFragment != null) {
- contactsFragment.setQueryFilter(text);
+ @Override
+ protected void onCreate(Bundle icicle, boolean ready) {
+ if (!getIntent().hasExtra(ContactSelectionListFragment.DISPLAY_MODE)) {
+ getIntent().putExtra(ContactSelectionListFragment.DISPLAY_MODE, DisplayMode.FLAG_ALL);
}
- }
- @Override
- public void onSearchClosed() {
- if (contactsFragment != null) {
- contactsFragment.resetQueryFilter();
- }
- }
- });
- }
+ getIntent().putExtra(ContactSelectionListFragment.REFRESHABLE, false);
- private void initializeMedia() {
- final Context context = this;
- isPassingAlongMedia = false;
+ setContentView(R.layout.share_activity);
- Uri streamExtra = getIntent().getParcelableExtra(Intent.EXTRA_STREAM);
- CharSequence charSequenceExtra = getIntent().getCharSequenceExtra(Intent.EXTRA_TEXT);
- mimeType = getMimeType(streamExtra);
-
- if (streamExtra != null && PartAuthority.isLocalUri(streamExtra)) {
- isPassingAlongMedia = true;
- resolvedExtra = streamExtra;
- handleResolvedMedia(getIntent(), false);
- } else if (charSequenceExtra != null && mimeType != null && mimeType.startsWith("text/")) {
- resolvedPlaintext = charSequenceExtra;
- handleResolvedMedia(getIntent(), false);
- } else {
- contactsFragment.getView().setVisibility(View.GONE);
- progressWheel.setVisibility(View.VISIBLE);
- new ResolveMediaTask(context).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, streamExtra);
- }
- }
-
- private void handleResolvedMedia(Intent intent, boolean animate) {
- long threadId = intent.getLongExtra(EXTRA_THREAD_ID, -1);
- int distributionType = intent.getIntExtra(EXTRA_DISTRIBUTION_TYPE, -1);
- Address address = null;
-
- if (intent.hasExtra(EXTRA_ADDRESS_MARSHALLED)) {
- Parcel parcel = Parcel.obtain();
- byte[] marshalled = intent.getByteArrayExtra(EXTRA_ADDRESS_MARSHALLED);
- parcel.unmarshall(marshalled, 0, marshalled.length);
- parcel.setDataPosition(0);
- address = parcel.readParcelable(getClassLoader());
- parcel.recycle();
- }
-
- boolean hasResolvedDestination = threadId != -1 && address != null && distributionType != -1;
-
- if (!hasResolvedDestination && animate) {
- ViewUtil.fadeIn(contactsFragment.getView(), 300);
- ViewUtil.fadeOut(progressWheel, 300);
- } else if (!hasResolvedDestination) {
- contactsFragment.getView().setVisibility(View.VISIBLE);
- progressWheel.setVisibility(View.GONE);
- } else {
- createConversation(threadId, address, distributionType);
- }
- }
-
- private void createConversation(long threadId, Address address, int distributionType) {
- final Intent intent = getBaseShareIntent(ConversationActivityV2.class);
- intent.putExtra(ConversationActivityV2.ADDRESS, address);
- intent.putExtra(ConversationActivityV2.THREAD_ID, threadId);
-
- isPassingAlongMedia = true;
- startActivity(intent);
- }
-
- private Intent getBaseShareIntent(final @NonNull Class> target) {
- final Intent intent = new Intent(this, target);
-
- if (resolvedExtra != null) {
- intent.setDataAndType(resolvedExtra, mimeType);
- } else if (resolvedPlaintext != null) {
- intent.putExtra(Intent.EXTRA_TEXT, resolvedPlaintext);
- intent.setType("text/plain");
- }
-
- return intent;
- }
-
- private String getMimeType(@Nullable Uri uri) {
- if (uri != null) {
- final String mimeType = MediaUtil.getMimeType(getApplicationContext(), uri);
- if (mimeType != null) return mimeType;
- }
- return MediaUtil.getCorrectedMimeType(getIntent().getType());
- }
-
- @Override
- public void onContactSelected(String number) {
- Recipient recipient = Recipient.from(this, Address.fromExternal(this, number), true);
- long existingThread = DatabaseComponent.get(this).threadDatabase().getThreadIdIfExistsFor(recipient);
- createConversation(existingThread, recipient.getAddress(), DistributionTypes.DEFAULT);
- }
-
- @Override
- public void onContactDeselected(String number) {
- }
-
- @SuppressLint("StaticFieldLeak")
- private class ResolveMediaTask extends AsyncTask {
- private final Context context;
-
- ResolveMediaTask(Context context) {
- this.context = context;
+ initializeToolbar();
+ initializeResources();
+ initializeSearch();
+ initializeMedia();
}
@Override
- protected Uri doInBackground(Uri... uris) {
- try {
- if (uris.length != 1 || uris[0] == null) {
- return null;
- }
+ protected void onNewIntent(Intent intent) {
+ Log.i(TAG, "onNewIntent()");
+ super.onNewIntent(intent);
+ setIntent(intent);
+ initializeMedia();
+ }
- InputStream inputStream;
+ @Override
+ public void onPause() {
+ super.onPause();
+ if (!isPassingAlongMedia && resolvedExtra != null) {
+ BlobProvider.getInstance().delete(this, resolvedExtra);
- if ("file".equals(uris[0].getScheme())) {
- inputStream = new FileInputStream(uris[0].getPath());
- } else {
- inputStream = context.getContentResolver().openInputStream(uris[0]);
- }
-
- if (inputStream == null) {
- return null;
- }
-
- Cursor cursor = getContentResolver().query(uris[0], new String[] {OpenableColumns.DISPLAY_NAME, OpenableColumns.SIZE}, null, null, null);
- String fileName = null;
- Long fileSize = null;
-
- try {
- if (cursor != null && cursor.moveToFirst()) {
- try {
- fileName = cursor.getString(cursor.getColumnIndexOrThrow(OpenableColumns.DISPLAY_NAME));
- fileSize = cursor.getLong(cursor.getColumnIndexOrThrow(OpenableColumns.SIZE));
- } catch (IllegalArgumentException e) {
- Log.w(TAG, e);
+ if (!isFinishing()) {
+ finish();
}
- }
- } finally {
- if (cursor != null) cursor.close();
}
-
- return BlobProvider.getInstance()
- .forData(inputStream, fileSize == null ? 0 : fileSize)
- .withMimeType(mimeType)
- .withFileName(fileName)
- .createForMultipleSessionsOnDisk(context, e -> Log.w(TAG, "Failed to write to disk.", e));
- } catch (IOException ioe) {
- Log.w(TAG, ioe);
- return null;
- }
}
@Override
- protected void onPostExecute(Uri uri) {
- resolvedExtra = uri;
- handleResolvedMedia(getIntent(), true);
+ public boolean onOptionsItemSelected(MenuItem item) {
+ switch (item.getItemId()) {
+ case android.R.id.home:
+ onBackPressed();
+ return true;
+ }
+ return super.onOptionsItemSelected(item);
+ }
+
+ @Override
+ public void onBackPressed() {
+ if (searchToolbar.isVisible()) searchToolbar.collapse();
+ else super.onBackPressed();
+ }
+
+ private void initializeToolbar() {
+ TextView tootlbarTitle = findViewById(R.id.title);
+ tootlbarTitle.setText(
+ Phrase.from(getApplicationContext(), R.string.shareToSession)
+ .put(APP_NAME_KEY, getString(R.string.app_name))
+ .format().toString()
+ );
+ }
+
+ private void initializeResources() {
+ progressWheel = findViewById(R.id.progress_wheel);
+ searchToolbar = findViewById(R.id.search_toolbar);
+ searchAction = findViewById(R.id.search_action);
+ contactsFragment = (ContactSelectionListFragment) getSupportFragmentManager().findFragmentById(R.id.contact_selection_list_fragment);
+ contactsFragment.setOnContactSelectedListener(this);
+ }
+
+ private void initializeSearch() {
+ searchAction.setOnClickListener(v -> searchToolbar.display(searchAction.getX() + (searchAction.getWidth() / 2),
+ searchAction.getY() + (searchAction.getHeight() / 2)));
+
+ searchToolbar.setListener(new SearchToolbar.SearchListener() {
+ @Override
+ public void onSearchTextChange(String text) {
+ if (contactsFragment != null) {
+ contactsFragment.setQueryFilter(text);
+ }
+ }
+
+ @Override
+ public void onSearchClosed() {
+ if (contactsFragment != null) {
+ contactsFragment.resetQueryFilter();
+ }
+ }
+ });
+ }
+
+ private void initializeMedia() {
+ final Context context = this;
+ isPassingAlongMedia = false;
+
+ Uri streamExtra = getIntent().getParcelableExtra(Intent.EXTRA_STREAM);
+ CharSequence charSequenceExtra = getIntent().getCharSequenceExtra(Intent.EXTRA_TEXT);
+ mimeType = getMimeType(streamExtra);
+
+ if (streamExtra != null && PartAuthority.isLocalUri(streamExtra)) {
+ isPassingAlongMedia = true;
+ resolvedExtra = streamExtra;
+ handleResolvedMedia(getIntent(), false);
+ } else if (charSequenceExtra != null && mimeType != null && mimeType.startsWith("text/")) {
+ resolvedPlaintext = charSequenceExtra;
+ handleResolvedMedia(getIntent(), false);
+ } else {
+ contactsFragment.getView().setVisibility(View.GONE);
+ progressWheel.setVisibility(View.VISIBLE);
+ new ResolveMediaTask(context).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, streamExtra);
+ }
+ }
+
+ private void handleResolvedMedia(Intent intent, boolean animate) {
+ long threadId = intent.getLongExtra(EXTRA_THREAD_ID, -1);
+ int distributionType = intent.getIntExtra(EXTRA_DISTRIBUTION_TYPE, -1);
+ Address address = null;
+
+ if (intent.hasExtra(EXTRA_ADDRESS_MARSHALLED)) {
+ Parcel parcel = Parcel.obtain();
+ byte[] marshalled = intent.getByteArrayExtra(EXTRA_ADDRESS_MARSHALLED);
+ parcel.unmarshall(marshalled, 0, marshalled.length);
+ parcel.setDataPosition(0);
+ address = parcel.readParcelable(getClassLoader());
+ parcel.recycle();
+ }
+
+ boolean hasResolvedDestination = threadId != -1 && address != null && distributionType != -1;
+
+ if (!hasResolvedDestination && animate) {
+ ViewUtil.fadeIn(contactsFragment.getView(), 300);
+ ViewUtil.fadeOut(progressWheel, 300);
+ } else if (!hasResolvedDestination) {
+ contactsFragment.getView().setVisibility(View.VISIBLE);
+ progressWheel.setVisibility(View.GONE);
+ } else {
+ createConversation(threadId, address, distributionType);
+ }
+ }
+
+ private void createConversation(long threadId, Address address, int distributionType) {
+ final Intent intent = getBaseShareIntent(ConversationActivityV2.class);
+ intent.putExtra(ConversationActivityV2.ADDRESS, address);
+ intent.putExtra(ConversationActivityV2.THREAD_ID, threadId);
+
+ isPassingAlongMedia = true;
+ startActivity(intent);
+ }
+
+ private Intent getBaseShareIntent(final @NonNull Class> target) {
+ final Intent intent = new Intent(this, target);
+
+ if (resolvedExtra != null) {
+ intent.setDataAndType(resolvedExtra, mimeType);
+ } else if (resolvedPlaintext != null) {
+ intent.putExtra(Intent.EXTRA_TEXT, resolvedPlaintext);
+ intent.setType("text/plain");
+ }
+
+ return intent;
+ }
+
+ private String getMimeType(@Nullable Uri uri) {
+ if (uri != null) {
+ final String mimeType = MediaUtil.getMimeType(getApplicationContext(), uri);
+ if (mimeType != null) return mimeType;
+ }
+ return MediaUtil.getCorrectedMimeType(getIntent().getType());
+ }
+
+ @Override
+ public void onContactSelected(String number) {
+ Recipient recipient = Recipient.from(this, Address.fromExternal(this, number), true);
+ long existingThread = DatabaseComponent.get(this).threadDatabase().getThreadIdIfExistsFor(recipient);
+ createConversation(existingThread, recipient.getAddress(), DistributionTypes.DEFAULT);
+ }
+
+ @Override
+ public void onContactDeselected(String number) {
+ }
+
+ @SuppressLint("StaticFieldLeak")
+ private class ResolveMediaTask extends AsyncTask {
+ private final Context context;
+
+ ResolveMediaTask(Context context) {
+ this.context = context;
+ }
+
+ @Override
+ protected Uri doInBackground(Uri... uris) {
+ try {
+ if (uris.length != 1 || uris[0] == null) {
+ return null;
+ }
+
+ InputStream inputStream;
+
+ if ("file".equals(uris[0].getScheme())) {
+ inputStream = new FileInputStream(uris[0].getPath());
+ } else {
+ inputStream = context.getContentResolver().openInputStream(uris[0]);
+ }
+
+ if (inputStream == null) {
+ return null;
+ }
+
+ Cursor cursor = getContentResolver().query(uris[0], new String[]{OpenableColumns.DISPLAY_NAME, OpenableColumns.SIZE}, null, null, null);
+ String fileName = null;
+ Long fileSize = null;
+
+ try {
+ if (cursor != null && cursor.moveToFirst()) {
+ try {
+ fileName = cursor.getString(cursor.getColumnIndexOrThrow(OpenableColumns.DISPLAY_NAME));
+ fileSize = cursor.getLong(cursor.getColumnIndexOrThrow(OpenableColumns.SIZE));
+ } catch (IllegalArgumentException e) {
+ Log.w(TAG, e);
+ }
+ }
+ } finally {
+ if (cursor != null) cursor.close();
+ }
+
+ return BlobProvider.getInstance()
+ .forData(inputStream, fileSize == null ? 0 : fileSize)
+ .withMimeType(mimeType)
+ .withFileName(fileName)
+ .createForMultipleSessionsOnDisk(context, e -> Log.w(TAG, "Failed to write to disk.", e));
+ } catch (IOException ioe) {
+ Log.w(TAG, ioe);
+ return null;
+ }
+ }
+
+ @Override
+ protected void onPostExecute(Uri uri) {
+ resolvedExtra = uri;
+ handleResolvedMedia(getIntent(), true);
+ }
}
- }
}
\ No newline at end of file
diff --git a/app/src/main/java/org/thoughtcrime/securesms/ShortcutLauncherActivity.java b/app/src/main/java/org/thoughtcrime/securesms/ShortcutLauncherActivity.java
index 37fdf2367d..2090e64925 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/ShortcutLauncherActivity.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/ShortcutLauncherActivity.java
@@ -37,7 +37,7 @@ public class ShortcutLauncherActivity extends AppCompatActivity {
String serializedAddress = getIntent().getStringExtra(KEY_SERIALIZED_ADDRESS);
if (serializedAddress == null) {
- Toast.makeText(this, R.string.ShortcutLauncherActivity_invalid_shortcut, Toast.LENGTH_SHORT).show();
+ Toast.makeText(this, R.string.invalidShortcut, Toast.LENGTH_SHORT).show();
startActivity(new Intent(this, HomeActivity.class));
finish();
return;
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 fc88656469..ac70a6024a 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/audio/AudioSlidePlayer.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/audio/AudioSlidePlayer.java
@@ -8,10 +8,9 @@ import android.hardware.SensorManager;
import android.media.AudioManager;
import android.os.Handler;
import android.os.Message;
-import android.os.PowerManager;
import android.os.PowerManager.WakeLock;
+import android.os.PowerManager;
import android.util.Pair;
-
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.OptIn;
@@ -23,7 +22,8 @@ import androidx.media3.common.PlaybackParameters;
import androidx.media3.common.Player;
import androidx.media3.common.util.UnstableApi;
import androidx.media3.exoplayer.ExoPlayer;
-
+import java.io.IOException;
+import java.lang.ref.WeakReference;
import org.jetbrains.annotations.NotNull;
import org.session.libsession.utilities.ServiceUtil;
import org.session.libsession.utilities.Util;
@@ -32,9 +32,6 @@ import org.session.libsignal.utilities.guava.Optional;
import org.thoughtcrime.securesms.attachments.AttachmentServer;
import org.thoughtcrime.securesms.mms.AudioSlide;
-import java.io.IOException;
-import java.lang.ref.WeakReference;
-
public class AudioSlidePlayer implements SensorEventListener {
private static final String TAG = AudioSlidePlayer.class.getSimpleName();
@@ -170,7 +167,6 @@ public class AudioSlidePlayer implements SensorEventListener {
}
}
-
@Override
public void onPlayerError(PlaybackException error) {
Log.w(TAG, "MediaPlayer Error: " + error);
@@ -209,9 +205,7 @@ public class AudioSlidePlayer implements SensorEventListener {
this.mediaPlayer.release();
}
- if (this.audioAttachmentServer != null) {
- this.audioAttachmentServer.stop();
- }
+ if (this.audioAttachmentServer != null) { this.audioAttachmentServer.stop(); }
sensorManager.unregisterListener(AudioSlidePlayer.this);
@@ -220,9 +214,7 @@ public class AudioSlidePlayer implements SensorEventListener {
}
public synchronized static void stopAll() {
- if (playing.isPresent()) {
- playing.get().stop();
- }
+ if (playing.isPresent()) { playing.get().stop(); }
}
public synchronized boolean isReady() {
@@ -364,9 +356,8 @@ public class AudioSlidePlayer implements SensorEventListener {
}
@Override
- public void onAccuracyChanged(Sensor sensor, int accuracy) {
+ public void onAccuracyChanged(Sensor sensor, int accuracy) { /* Do nothing */ }
- }
public interface Listener {
void onPlayerStart(@NonNull AudioSlidePlayer player);
diff --git a/app/src/main/java/org/thoughtcrime/securesms/avatar/AvatarSelection.kt b/app/src/main/java/org/thoughtcrime/securesms/avatar/AvatarSelection.kt
index ddf7ac5c77..bf19c3cc34 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/avatar/AvatarSelection.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/avatar/AvatarSelection.kt
@@ -32,7 +32,7 @@ class AvatarSelection(
private val bgColor by lazy { activity.getColorFromAttr(android.R.attr.colorPrimary) }
private val txtColor by lazy { activity.getColorFromAttr(android.R.attr.textColorPrimary) }
private val imageScrim by lazy { ContextCompat.getColor(activity, R.color.avatar_background) }
- private val activityTitle by lazy { activity.getString(R.string.CropImageActivity_profile_avatar) }
+ private val activityTitle by lazy { activity.getString(R.string.image) }
/**
* Returns result on [.REQUEST_CODE_CROP_IMAGE]
@@ -74,8 +74,9 @@ class AvatarSelection(
*/
fun startAvatarSelection(
includeClear: Boolean,
- attemptToIncludeCamera: Boolean
- ): File? {
+ attemptToIncludeCamera: Boolean,
+ createTempFile: ()->File?
+ ) {
var captureFile: File? = null
val hasCameraPermission = ContextCompat
.checkSelfPermission(
@@ -83,18 +84,11 @@ class AvatarSelection(
Manifest.permission.CAMERA
) == PackageManager.PERMISSION_GRANTED
if (attemptToIncludeCamera && hasCameraPermission) {
- try {
- captureFile = File.createTempFile("avatar-capture", ".jpg", getImageDir(activity))
- } catch (e: IOException) {
- Log.e("Cannot reserve a temporary avatar capture file.", e)
- } catch (e: NoExternalStorageException) {
- Log.e("Cannot reserve a temporary avatar capture file.", e)
- }
+ captureFile = createTempFile()
}
val chooserIntent = createAvatarSelectionIntent(activity, captureFile, includeClear)
onPickImage.launch(chooserIntent)
- return captureFile
}
private fun createAvatarSelectionIntent(
@@ -120,7 +114,7 @@ class AvatarSelection(
val chooserIntent = Intent.createChooser(
galleryIntent,
- context.getString(R.string.CreateProfileActivity_profile_photo)
+ context.getString(R.string.image)
)
if (!extraIntents.isEmpty()) {
diff --git a/app/src/main/java/org/thoughtcrime/securesms/calls/WebRtcCallActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/calls/WebRtcCallActivity.kt
index 2bded3cccb..9fdc6b1063 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/calls/WebRtcCallActivity.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/calls/WebRtcCallActivity.kt
@@ -13,11 +13,13 @@ import android.view.MenuItem
import android.view.View
import android.view.ViewOutlineProvider
import android.view.WindowManager
+import android.widget.TextView
import androidx.activity.viewModels
import androidx.core.content.ContextCompat
import androidx.core.view.isVisible
import androidx.lifecycle.lifecycleScope
import androidx.localbroadcastmanager.content.LocalBroadcastManager
+import com.squareup.phrase.Phrase
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
@@ -27,6 +29,7 @@ import network.loki.messenger.R
import network.loki.messenger.databinding.ActivityWebrtcBinding
import org.apache.commons.lang3.time.DurationFormatUtils
import org.session.libsession.messaging.contacts.Contact
+import org.session.libsession.utilities.StringSubstitutionConstants.APP_NAME_KEY
import org.session.libsession.utilities.TextSecurePreferences
import org.session.libsession.utilities.truncateIdForDisplay
import org.session.libsignal.utilities.Log
@@ -86,9 +89,9 @@ class WebRtcCallActivity : PassphraseRequiredActionBarActivity() {
return super.onOptionsItemSelected(item)
}
- override fun onNewIntent(intent: Intent?) {
+ override fun onNewIntent(intent: Intent) {
super.onNewIntent(intent)
- if (intent?.action == ACTION_ANSWER) {
+ if (intent.action == ACTION_ANSWER) {
val answerIntent = WebRtcCallService.acceptCallIntent(this)
answerIntent.flags = Intent.FLAG_ACTIVITY_REORDER_TO_FRONT
ContextCompat.startForegroundService(this, answerIntent)
@@ -202,6 +205,10 @@ class WebRtcCallActivity : PassphraseRequiredActionBarActivity() {
update()
}
}
+
+ // Substitute "Session" into the "{app_name} Call" text
+ val sessionCallTV = findViewById(R.id.sessionCallText)
+ sessionCallTV?.text = Phrase.from(this, R.string.callsSessionCall).put(APP_NAME_KEY, getString(R.string.app_name)).format()
}
/**
diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/AttachmentTypeSelector.java b/app/src/main/java/org/thoughtcrime/securesms/components/AttachmentTypeSelector.java
deleted file mode 100644
index 1c6a4097f5..0000000000
--- a/app/src/main/java/org/thoughtcrime/securesms/components/AttachmentTypeSelector.java
+++ /dev/null
@@ -1,299 +0,0 @@
-package org.thoughtcrime.securesms.components;
-
-import android.Manifest;
-import android.animation.Animator;
-import android.app.Activity;
-import android.content.Context;
-import android.graphics.drawable.BitmapDrawable;
-import android.net.Uri;
-import android.util.Pair;
-import android.view.Gravity;
-import android.view.LayoutInflater;
-import android.view.View;
-import android.view.ViewAnimationUtils;
-import android.view.ViewTreeObserver;
-import android.view.animation.Animation;
-import android.view.animation.AnimationSet;
-import android.view.animation.OvershootInterpolator;
-import android.view.animation.ScaleAnimation;
-import android.view.animation.TranslateAnimation;
-import android.widget.ImageView;
-import android.widget.LinearLayout;
-import android.widget.PopupWindow;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import androidx.loader.app.LoaderManager;
-
-import org.session.libsession.utilities.ViewUtil;
-import org.thoughtcrime.securesms.permissions.Permissions;
-
-import network.loki.messenger.R;
-
-public class AttachmentTypeSelector extends PopupWindow {
-
- public static final int ADD_GALLERY = 1;
- public static final int ADD_DOCUMENT = 2;
- public static final int ADD_SOUND = 3;
- public static final int ADD_CONTACT_INFO = 4;
- public static final int TAKE_PHOTO = 5;
- public static final int ADD_LOCATION = 6;
- public static final int ADD_GIF = 7;
-
- private static final int ANIMATION_DURATION = 300;
-
- @SuppressWarnings("unused")
- private static final String TAG = AttachmentTypeSelector.class.getSimpleName();
-
- private final @NonNull Context context;
- public int keyboardHeight;
- private final @NonNull LoaderManager loaderManager;
- private final @NonNull RecentPhotoViewRail recentRail;
- private final @NonNull ImageView imageButton;
- private final @NonNull ImageView audioButton;
- private final @NonNull ImageView documentButton;
- private final @NonNull ImageView contactButton;
- private final @NonNull ImageView cameraButton;
- private final @NonNull ImageView locationButton;
- private final @NonNull ImageView gifButton;
- private final @NonNull ImageView closeButton;
-
- private @Nullable View currentAnchor;
- private @Nullable AttachmentClickedListener listener;
-
- public AttachmentTypeSelector(@NonNull Context context, @NonNull LoaderManager loaderManager, @Nullable AttachmentClickedListener listener, int keyboardHeight) {
- super(context);
-
- this.context = context;
- this.keyboardHeight = keyboardHeight;
-
- LayoutInflater inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
- LinearLayout layout = (LinearLayout) inflater.inflate(R.layout.attachment_type_selector, null, true);
-
- this.listener = listener;
- this.loaderManager = loaderManager;
- this.recentRail = ViewUtil.findById(layout, R.id.recent_photos);
- this.imageButton = ViewUtil.findById(layout, R.id.gallery_button);
- this.audioButton = ViewUtil.findById(layout, R.id.audio_button);
- this.documentButton = ViewUtil.findById(layout, R.id.document_button);
- this.contactButton = ViewUtil.findById(layout, R.id.contact_button);
- this.cameraButton = ViewUtil.findById(layout, R.id.camera_button);
- this.locationButton = ViewUtil.findById(layout, R.id.location_button);
- this.gifButton = ViewUtil.findById(layout, R.id.giphy_button);
- this.closeButton = ViewUtil.findById(layout, R.id.close_button);
-
- this.imageButton.setOnClickListener(new PropagatingClickListener(ADD_GALLERY));
- this.audioButton.setOnClickListener(new PropagatingClickListener(ADD_SOUND));
- this.documentButton.setOnClickListener(new PropagatingClickListener(ADD_DOCUMENT));
- this.contactButton.setOnClickListener(new PropagatingClickListener(ADD_CONTACT_INFO));
- this.cameraButton.setOnClickListener(new PropagatingClickListener(TAKE_PHOTO));
- this.locationButton.setOnClickListener(new PropagatingClickListener(ADD_LOCATION));
- this.gifButton.setOnClickListener(new PropagatingClickListener(ADD_GIF));
- this.closeButton.setOnClickListener(new CloseClickListener());
- this.recentRail.setListener(new RecentPhotoSelectedListener());
-
- setContentView(layout);
- setWidth(LinearLayout.LayoutParams.MATCH_PARENT);
- setHeight(LinearLayout.LayoutParams.WRAP_CONTENT);
- setBackgroundDrawable(new BitmapDrawable());
- setAnimationStyle(0);
- setInputMethodMode(PopupWindow.INPUT_METHOD_NOT_NEEDED);
- setFocusable(true);
- setTouchable(true);
-
- updateHeight();
-
- loaderManager.initLoader(1, null, recentRail);
- }
-
- public void show(@NonNull Activity activity, final @NonNull View anchor) {
- updateHeight();
-
- if (Permissions.hasAll(activity, Manifest.permission.READ_EXTERNAL_STORAGE)) {
- recentRail.setVisibility(View.VISIBLE);
- loaderManager.restartLoader(1, null, recentRail);
- } else {
- recentRail.setVisibility(View.GONE);
- }
-
- this.currentAnchor = anchor;
-
- showAtLocation(anchor, Gravity.BOTTOM, 0, 0);
-
- getContentView().getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
- @Override
- public void onGlobalLayout() {
- getContentView().getViewTreeObserver().removeGlobalOnLayoutListener(this);
-
- animateWindowInCircular(anchor, getContentView());
- }
- });
-
- animateButtonIn(imageButton, ANIMATION_DURATION / 2);
- animateButtonIn(cameraButton, ANIMATION_DURATION / 2);
-
- animateButtonIn(audioButton, ANIMATION_DURATION / 3);
- animateButtonIn(locationButton, ANIMATION_DURATION / 3);
- animateButtonIn(documentButton, ANIMATION_DURATION / 4);
- animateButtonIn(gifButton, ANIMATION_DURATION / 4);
- animateButtonIn(contactButton, 0);
- animateButtonIn(closeButton, 0);
- }
-
- private void updateHeight() {
- int thresholdInDP = 120;
- float scale = context.getResources().getDisplayMetrics().density;
- int thresholdInPX = (int)(thresholdInDP * scale);
- View contentView = ViewUtil.findById(getContentView(), R.id.contentView);
- LinearLayout.LayoutParams contentViewLayoutParams = (LinearLayout.LayoutParams)contentView.getLayoutParams();
- contentViewLayoutParams.height = keyboardHeight > thresholdInPX ? keyboardHeight : LinearLayout.LayoutParams.WRAP_CONTENT;
- contentView.setLayoutParams(contentViewLayoutParams);
- }
-
- @Override
- public void dismiss() {
- animateWindowOutCircular(currentAnchor, getContentView());
- }
-
- public void setListener(@Nullable AttachmentClickedListener listener) {
- this.listener = listener;
- }
-
- private void animateButtonIn(View button, int delay) {
- AnimationSet animation = new AnimationSet(true);
- Animation scale = new ScaleAnimation(0.0f, 1.0f, 0.0f, 1.0f,
- Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF, 0.0f);
-
- animation.addAnimation(scale);
- animation.setInterpolator(new OvershootInterpolator(1));
- animation.setDuration(ANIMATION_DURATION);
- animation.setStartOffset(delay);
- button.startAnimation(animation);
- }
-
- private void animateWindowInCircular(@Nullable View anchor, @NonNull View contentView) {
- Pair coordinates = getClickOrigin(anchor, contentView);
- Animator animator = ViewAnimationUtils.createCircularReveal(contentView,
- coordinates.first,
- coordinates.second,
- 0,
- Math.max(contentView.getWidth(), contentView.getHeight()));
- animator.setDuration(ANIMATION_DURATION);
- animator.start();
- }
-
- private void animateWindowInTranslate(@NonNull View contentView) {
- Animation animation = new TranslateAnimation(0, 0, contentView.getHeight(), 0);
- animation.setDuration(ANIMATION_DURATION);
-
- getContentView().startAnimation(animation);
- }
-
- private void animateWindowOutCircular(@Nullable View anchor, @NonNull View contentView) {
- Pair coordinates = getClickOrigin(anchor, contentView);
- Animator animator = ViewAnimationUtils.createCircularReveal(getContentView(),
- coordinates.first,
- coordinates.second,
- Math.max(getContentView().getWidth(), getContentView().getHeight()),
- 0);
-
- animator.setDuration(ANIMATION_DURATION);
- animator.addListener(new Animator.AnimatorListener() {
- @Override
- public void onAnimationStart(Animator animation) {
- }
-
- @Override
- public void onAnimationEnd(Animator animation) {
- AttachmentTypeSelector.super.dismiss();
- }
-
- @Override
- public void onAnimationCancel(Animator animation) {
- }
-
- @Override
- public void onAnimationRepeat(Animator animation) {
- }
- });
-
- animator.start();
- }
-
- private void animateWindowOutTranslate(@NonNull View contentView) {
- Animation animation = new TranslateAnimation(0, 0, 0, contentView.getTop() + contentView.getHeight());
- animation.setDuration(ANIMATION_DURATION);
- animation.setAnimationListener(new Animation.AnimationListener() {
- @Override
- public void onAnimationStart(Animation animation) {
- }
-
- @Override
- public void onAnimationEnd(Animation animation) {
- AttachmentTypeSelector.super.dismiss();
- }
-
- @Override
- public void onAnimationRepeat(Animation animation) {
- }
- });
-
- getContentView().startAnimation(animation);
- }
-
- private Pair getClickOrigin(@Nullable View anchor, @NonNull View contentView) {
- if (anchor == null) return new Pair<>(0, 0);
-
- final int[] anchorCoordinates = new int[2];
- anchor.getLocationOnScreen(anchorCoordinates);
- anchorCoordinates[0] += anchor.getWidth() / 2;
- anchorCoordinates[1] += anchor.getHeight() / 2;
-
- final int[] contentCoordinates = new int[2];
- contentView.getLocationOnScreen(contentCoordinates);
-
- int x = anchorCoordinates[0] - contentCoordinates[0];
- int y = anchorCoordinates[1] - contentCoordinates[1];
-
- return new Pair<>(x, y);
- }
-
- private class RecentPhotoSelectedListener implements RecentPhotoViewRail.OnItemClickedListener {
- @Override
- public void onItemClicked(Uri uri, String mimeType, String bucketId, long dateTaken, int width, int height, long size) {
- animateWindowOutTranslate(getContentView());
-
- if (listener != null) listener.onQuickAttachment(uri, mimeType, bucketId, dateTaken, width, height, size);
- }
- }
-
- private class PropagatingClickListener implements View.OnClickListener {
-
- private final int type;
-
- private PropagatingClickListener(int type) {
- this.type = type;
- }
-
- @Override
- public void onClick(View v) {
- animateWindowOutTranslate(getContentView());
-
- if (listener != null) listener.onClick(type);
- }
-
- }
-
- private class CloseClickListener implements View.OnClickListener {
- @Override
- public void onClick(View v) {
- dismiss();
- }
- }
-
- public interface AttachmentClickedListener {
- void onClick(int type);
- void onQuickAttachment(Uri uri, String mimeType, String bucketId, long dateTaken, int width, int height, long size);
- }
-
-}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/CustomDefaultPreference.java b/app/src/main/java/org/thoughtcrime/securesms/components/CustomDefaultPreference.java
deleted file mode 100644
index 178803a9f0..0000000000
--- a/app/src/main/java/org/thoughtcrime/securesms/components/CustomDefaultPreference.java
+++ /dev/null
@@ -1,259 +0,0 @@
-package org.thoughtcrime.securesms.components;
-
-import android.app.Dialog;
-import android.content.Context;
-import android.content.res.TypedArray;
-import android.os.Bundle;
-import androidx.annotation.NonNull;
-import androidx.appcompat.app.AlertDialog;
-import androidx.preference.DialogPreference;
-import androidx.preference.PreferenceDialogFragmentCompat;
-import android.text.Editable;
-import android.text.TextUtils;
-import android.text.TextWatcher;
-import android.util.AttributeSet;
-import org.session.libsignal.utilities.Log;
-import android.view.View;
-import android.widget.AdapterView;
-import android.widget.Button;
-import android.widget.EditText;
-import android.widget.Spinner;
-import android.widget.TextView;
-
-import network.loki.messenger.R;
-import org.thoughtcrime.securesms.components.CustomDefaultPreference.CustomDefaultPreferenceDialogFragmentCompat.CustomPreferenceValidator;
-import org.session.libsession.utilities.TextSecurePreferences;
-
-import java.net.URI;
-import java.net.URISyntaxException;
-
-
-public class CustomDefaultPreference extends DialogPreference {
-
- private static final String TAG = CustomDefaultPreference.class.getSimpleName();
-
- private final int inputType;
- private final String customPreference;
- private final String customToggle;
-
- private CustomPreferenceValidator validator;
- private String defaultValue;
-
- public CustomDefaultPreference(Context context, AttributeSet attrs) {
- super(context, attrs);
-
- int[] attributeNames = new int[]{android.R.attr.inputType, R.attr.custom_pref_toggle};
- TypedArray attributes = context.obtainStyledAttributes(attrs, attributeNames);
-
- this.inputType = attributes.getInt(0, 0);
- this.customPreference = getKey();
- this.customToggle = attributes.getString(1);
- this.validator = new CustomDefaultPreferenceDialogFragmentCompat.NullValidator();
-
- attributes.recycle();
-
- setPersistent(false);
- setDialogLayoutResource(R.layout.custom_default_preference_dialog);
- }
-
- public CustomDefaultPreference setValidator(CustomPreferenceValidator validator) {
- this.validator = validator;
- return this;
- }
-
- public CustomDefaultPreference setDefaultValue(String defaultValue) {
- this.defaultValue = defaultValue;
- this.setSummary(getSummary());
- return this;
- }
-
- @Override
- public String getSummary() {
- if (isCustom()) {
- return getContext().getString(R.string.CustomDefaultPreference_using_custom,
- getPrettyPrintValue(getCustomValue()));
- } else {
- return getContext().getString(R.string.CustomDefaultPreference_using_default,
- getPrettyPrintValue(getDefaultValue()));
- }
- }
-
- private String getPrettyPrintValue(String value) {
- if (TextUtils.isEmpty(value)) return getContext().getString(R.string.CustomDefaultPreference_none);
- else return value;
- }
-
- private boolean isCustom() {
- return TextSecurePreferences.getBooleanPreference(getContext(), customToggle, false);
- }
-
- private void setCustom(boolean custom) {
- TextSecurePreferences.setBooleanPreference(getContext(), customToggle, custom);
- }
-
- private String getCustomValue() {
- return TextSecurePreferences.getStringPreference(getContext(), customPreference, "");
- }
-
- private void setCustomValue(String value) {
- TextSecurePreferences.setStringPreference(getContext(), customPreference, value);
- }
-
- private String getDefaultValue() {
- return defaultValue;
- }
-
-
- public static class CustomDefaultPreferenceDialogFragmentCompat extends PreferenceDialogFragmentCompat {
-
- private static final String INPUT_TYPE = "input_type";
-
- private Spinner spinner;
- private EditText customText;
- private TextView defaultLabel;
-
- public static CustomDefaultPreferenceDialogFragmentCompat newInstance(String key) {
- CustomDefaultPreferenceDialogFragmentCompat fragment = new CustomDefaultPreferenceDialogFragmentCompat();
- Bundle b = new Bundle(1);
- b.putString(PreferenceDialogFragmentCompat.ARG_KEY, key);
- fragment.setArguments(b);
- return fragment;
- }
-
- @Override
- protected void onBindDialogView(@NonNull View view) {
- Log.i(TAG, "onBindDialogView");
- super.onBindDialogView(view);
-
- CustomDefaultPreference preference = (CustomDefaultPreference)getPreference();
-
- this.spinner = (Spinner) view.findViewById(R.id.default_or_custom);
- this.defaultLabel = (TextView) view.findViewById(R.id.default_label);
- this.customText = (EditText) view.findViewById(R.id.custom_edit);
-
- this.customText.setInputType(preference.inputType);
- this.customText.addTextChangedListener(new TextValidator());
- this.customText.setText(preference.getCustomValue());
- this.spinner.setOnItemSelectedListener(new SelectionLister());
- this.defaultLabel.setText(preference.getPrettyPrintValue(preference.defaultValue));
- }
-
-
- @Override
- public @NonNull Dialog onCreateDialog(Bundle instanceState) {
- Dialog dialog = super.onCreateDialog(instanceState);
-
- CustomDefaultPreference preference = (CustomDefaultPreference)getPreference();
-
- if (preference.isCustom()) spinner.setSelection(1, true);
- else spinner.setSelection(0, true);
-
- return dialog;
- }
-
- @Override
- public void onDialogClosed(boolean positiveResult) {
- CustomDefaultPreference preference = (CustomDefaultPreference)getPreference();
-
- if (positiveResult) {
- if (spinner != null) preference.setCustom(spinner.getSelectedItemPosition() == 1);
- if (customText != null) preference.setCustomValue(customText.getText().toString());
-
- preference.setSummary(preference.getSummary());
- }
- }
-
- interface CustomPreferenceValidator {
- public boolean isValid(String value);
- }
-
- private static class NullValidator implements CustomPreferenceValidator {
- @Override
- public boolean isValid(String value) {
- return true;
- }
- }
-
- private class TextValidator implements TextWatcher {
-
- @Override
- public void beforeTextChanged(CharSequence s, int start, int count, int after) {}
-
- @Override
- public void onTextChanged(CharSequence s, int start, int before, int count) {}
-
- @Override
- public void afterTextChanged(Editable s) {
- CustomDefaultPreference preference = (CustomDefaultPreference)getPreference();
-
- if (spinner.getSelectedItemPosition() == 1) {
- Button positiveButton = ((AlertDialog)getDialog()).getButton(AlertDialog.BUTTON_POSITIVE);
- positiveButton.setEnabled(preference.validator.isValid(s.toString()));
- }
- }
- }
-
- public static class UriValidator implements CustomPreferenceValidator {
- @Override
- public boolean isValid(String value) {
- if (TextUtils.isEmpty(value)) return true;
-
- try {
- new URI(value);
- return true;
- } catch (URISyntaxException mue) {
- return false;
- }
- }
- }
-
- public static class HostnameValidator implements CustomPreferenceValidator {
- @Override
- public boolean isValid(String value) {
- if (TextUtils.isEmpty(value)) return true;
-
- try {
- URI uri = new URI(null, value, null, null);
- return true;
- } catch (URISyntaxException mue) {
- return false;
- }
- }
- }
-
- public static class PortValidator implements CustomPreferenceValidator {
- @Override
- public boolean isValid(String value) {
- try {
- Integer.parseInt(value);
- return true;
- } catch (NumberFormatException e) {
- return false;
- }
- }
- }
-
- private class SelectionLister implements AdapterView.OnItemSelectedListener {
-
- @Override
- public void onItemSelected(AdapterView> parent, View view, int position, long id) {
- CustomDefaultPreference preference = (CustomDefaultPreference)getPreference();
- Button positiveButton = ((AlertDialog)getDialog()).getButton(AlertDialog.BUTTON_POSITIVE);
-
- defaultLabel.setVisibility(position == 0 ? View.VISIBLE : View.GONE);
- customText.setVisibility(position == 0 ? View.GONE : View.VISIBLE);
- positiveButton.setEnabled(position == 0 || preference.validator.isValid(customText.getText().toString()));
- }
-
- @Override
- public void onNothingSelected(AdapterView> parent) {
- defaultLabel.setVisibility(View.VISIBLE);
- customText.setVisibility(View.GONE);
- }
- }
-
- }
-
-
-
-}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/DocumentView.java b/app/src/main/java/org/thoughtcrime/securesms/components/DocumentView.java
index d1d70fdac0..f51b4a7d93 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/components/DocumentView.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/components/DocumentView.java
@@ -105,7 +105,7 @@ public class DocumentView extends FrameLayout {
this.documentSlide = documentSlide;
- this.fileName.setText(documentSlide.getFileName().or(getContext().getString(R.string.DocumentView_unknown_file)));
+ this.fileName.setText(documentSlide.getFileName().or(getContext().getString(R.string.attachmentsErrorNotSupported)));
this.fileSize.setText(Util.getPrettyFileSize(documentSlide.getFileSize()));
this.document.setText(getFileType(documentSlide.getFileName()));
this.setOnClickListener(new OpenClickedListener(documentSlide));
diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/FromTextView.java b/app/src/main/java/org/thoughtcrime/securesms/components/FromTextView.java
index d98c56ede5..ae9f5e6e70 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/components/FromTextView.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/components/FromTextView.java
@@ -54,7 +54,7 @@ public class FromTextView extends EmojiTextView {
if (recipient.isLocalNumber()) {
- builder.append(getContext().getString(R.string.note_to_self));
+ builder.append(getContext().getString(R.string.noteToSelf));
} else if (recipient.getName() == null && !TextUtils.isEmpty(recipient.getProfileName())) {
SpannableString profileName = new SpannableString(" (~" + recipient.getProfileName() + ") ");
profileName.setSpan(new CenterAlignedRelativeSizeSpan(0.75f), 0, profileName.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
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 07d0883bd2..30e609a047 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/components/SearchToolbar.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/components/SearchToolbar.java
@@ -2,13 +2,11 @@ package org.thoughtcrime.securesms.components;
import android.animation.Animator;
import android.content.Context;
-import android.os.Build;
import android.util.AttributeSet;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewAnimationUtils;
import android.widget.EditText;
-import android.widget.LinearLayout;
import androidx.annotation.MainThread;
import androidx.annotation.Nullable;
@@ -19,7 +17,7 @@ import org.thoughtcrime.securesms.util.AnimationCompleteListener;
import network.loki.messenger.R;
-public class SearchToolbar extends LinearLayout {
+public class SearchToolbar extends Toolbar {
private float x, y;
private MenuItem searchItem;
@@ -41,23 +39,17 @@ public class SearchToolbar extends LinearLayout {
}
private void initialize() {
- inflate(getContext(), R.layout.search_toolbar, this);
- setOrientation(VERTICAL);
+ setNavigationIcon(getContext().getResources().getDrawable(R.drawable.ic_baseline_clear_24));
+ inflateMenu(R.menu.conversation_list_search);
- Toolbar toolbar = findViewById(R.id.toolbar);
-
- toolbar.setNavigationIcon(
- getContext().getResources().getDrawable(R.drawable.ic_baseline_clear_24));
- toolbar.inflateMenu(R.menu.conversation_list_search);
-
- this.searchItem = toolbar.getMenu().findItem(R.id.action_filter_search);
+ this.searchItem = getMenu().findItem(R.id.action_filter_search);
SearchView searchView = (SearchView) searchItem.getActionView();
EditText searchText = searchView.findViewById(androidx.appcompat.R.id.search_src_text);
searchView.setSubmitButtonEnabled(false);
- if (searchText != null) searchText.setHint(R.string.SearchToolbar_search);
- else searchView.setQueryHint(getResources().getString(R.string.SearchToolbar_search));
+ if (searchText != null) searchText.setHint(R.string.search);
+ else searchView.setQueryHint(getResources().getString(R.string.search));
searchView.setOnQueryTextListener(new SearchView.OnQueryTextListener() {
@Override
@@ -83,7 +75,7 @@ public class SearchToolbar extends LinearLayout {
}
});
- toolbar.setNavigationOnClickListener(v -> hide());
+ setNavigationOnClickListener(v -> hide());
}
@MainThread
diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/SwitchPreferenceCompat.java b/app/src/main/java/org/thoughtcrime/securesms/components/SwitchPreferenceCompat.java
deleted file mode 100644
index 6e7993a575..0000000000
--- a/app/src/main/java/org/thoughtcrime/securesms/components/SwitchPreferenceCompat.java
+++ /dev/null
@@ -1,50 +0,0 @@
-package org.thoughtcrime.securesms.components;
-
-import android.content.Context;
-import android.util.AttributeSet;
-
-import androidx.preference.CheckBoxPreference;
-import androidx.preference.Preference;
-
-import network.loki.messenger.R;
-
-public class SwitchPreferenceCompat extends CheckBoxPreference {
-
- private Preference.OnPreferenceClickListener listener;
-
- public SwitchPreferenceCompat(Context context, AttributeSet attrs, int defStyleAttr) {
- super(context, attrs, defStyleAttr);
- setLayoutRes();
- }
-
- public SwitchPreferenceCompat(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
- super(context, attrs, defStyleAttr, defStyleRes);
- setLayoutRes();
- }
-
- public SwitchPreferenceCompat(Context context, AttributeSet attrs) {
- super(context, attrs);
- setLayoutRes();
- }
-
- public SwitchPreferenceCompat(Context context) {
- super(context);
- setLayoutRes();
- }
-
- private void setLayoutRes() {
- setWidgetLayoutResource(R.layout.switch_compat_preference);
- }
-
- @Override
- public void setOnPreferenceClickListener(Preference.OnPreferenceClickListener listener) {
- this.listener = listener;
- }
-
- @Override
- protected void onClick() {
- if (listener == null || !listener.onPreferenceClick(this)) {
- super.onClick();
- }
- }
-}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/SwitchPreferenceCompat.kt b/app/src/main/java/org/thoughtcrime/securesms/components/SwitchPreferenceCompat.kt
new file mode 100644
index 0000000000..9161dd828d
--- /dev/null
+++ b/app/src/main/java/org/thoughtcrime/securesms/components/SwitchPreferenceCompat.kt
@@ -0,0 +1,59 @@
+package org.thoughtcrime.securesms.components
+
+import android.content.Context
+import android.util.AttributeSet
+import androidx.preference.CheckBoxPreference
+import com.squareup.phrase.Phrase
+import network.loki.messenger.R
+import org.session.libsession.utilities.StringSubstitutionConstants.APP_NAME_KEY
+import org.thoughtcrime.securesms.ui.getSubbedCharSequence
+import org.thoughtcrime.securesms.ui.getSubbedString
+
+class SwitchPreferenceCompat : CheckBoxPreference {
+ private var listener: OnPreferenceClickListener? = null
+
+ constructor(context: Context?, attrs: AttributeSet?, defStyleAttr: Int) : super(context!!, attrs, defStyleAttr) {
+ setLayoutRes()
+ }
+
+ constructor(context: Context?, attrs: AttributeSet?, defStyleAttr: Int, defStyleRes: Int) : super(context!!, attrs, defStyleAttr, defStyleRes) {
+ setLayoutRes()
+ }
+
+ constructor(context: Context?, attrs: AttributeSet?) : super(context!!, attrs) {
+ setLayoutRes()
+ }
+
+ constructor(context: Context?) : super(context!!) {
+ setLayoutRes()
+ }
+
+ private fun setLayoutRes() {
+ widgetLayoutResource = R.layout.switch_compat_preference
+
+ if (this.hasKey()) {
+ val key = this.key
+
+ // Substitute app name into lockscreen preference summary
+ if (key.equals(LOCK_SCREEN_KEY, ignoreCase = true)) {
+ val c = context
+ val substitutedSummaryCS = c.getSubbedCharSequence(R.string.lockAppDescription, APP_NAME_KEY to c.getString(R.string.app_name))
+ this.summary = substitutedSummaryCS
+ }
+ }
+ }
+
+ override fun setOnPreferenceClickListener(listener: OnPreferenceClickListener?) {
+ this.listener = listener
+ }
+
+ override fun onClick() {
+ if (listener == null || !listener!!.onPreferenceClick(this)) {
+ super.onClick()
+ }
+ }
+
+ companion object {
+ private const val LOCK_SCREEN_KEY = "pref_android_screen_lock"
+ }
+}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/TransferControlView.java b/app/src/main/java/org/thoughtcrime/securesms/components/TransferControlView.java
deleted file mode 100644
index 36a607c819..0000000000
--- a/app/src/main/java/org/thoughtcrime/securesms/components/TransferControlView.java
+++ /dev/null
@@ -1,227 +0,0 @@
-package org.thoughtcrime.securesms.components;
-
-import android.animation.LayoutTransition;
-import android.content.Context;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import androidx.core.content.ContextCompat;
-import android.util.AttributeSet;
-import android.view.View;
-import android.widget.FrameLayout;
-import android.widget.TextView;
-
-import com.annimon.stream.Stream;
-import com.pnikosis.materialishprogress.ProgressWheel;
-
-import org.greenrobot.eventbus.EventBus;
-import org.greenrobot.eventbus.Subscribe;
-import org.greenrobot.eventbus.ThreadMode;
-import org.session.libsession.messaging.sending_receiving.attachments.Attachment;
-import org.session.libsession.messaging.sending_receiving.attachments.AttachmentTransferProgress;
-import org.thoughtcrime.securesms.database.AttachmentDatabase;
-import org.thoughtcrime.securesms.events.PartProgressEvent;
-import org.thoughtcrime.securesms.mms.Slide;
-
-import org.session.libsession.utilities.ViewUtil;
-
-import java.util.Collections;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-
-import network.loki.messenger.R;
-
-public class TransferControlView extends FrameLayout {
-
- @Nullable private List slides;
- @Nullable private View current;
-
- private final ProgressWheel progressWheel;
- private final View downloadDetails;
- private final TextView downloadDetailsText;
-
- private final Map downloadProgress;
-
- public TransferControlView(Context context) {
- this(context, null);
- }
-
- public TransferControlView(Context context, AttributeSet attrs) {
- this(context, attrs, 0);
- }
-
- public TransferControlView(Context context, AttributeSet attrs, int defStyleAttr) {
- super(context, attrs, defStyleAttr);
- inflate(context, R.layout.transfer_controls_view, this);
-
- setLongClickable(false);
- ViewUtil.setBackground(this, ContextCompat.getDrawable(context, R.drawable.transfer_controls_background));
- setVisibility(GONE);
- setLayoutTransition(new LayoutTransition());
-
- this.downloadProgress = new HashMap<>();
- this.progressWheel = ViewUtil.findById(this, R.id.progress_wheel);
- this.downloadDetails = ViewUtil.findById(this, R.id.download_details);
- this.downloadDetailsText = ViewUtil.findById(this, R.id.download_details_text);
- }
-
- @Override
- public void setFocusable(boolean focusable) {
- super.setFocusable(focusable);
- downloadDetails.setFocusable(focusable);
- }
-
- @Override
- public void setClickable(boolean clickable) {
- super.setClickable(clickable);
- downloadDetails.setClickable(clickable);
- }
-
- @Override
- protected void onAttachedToWindow() {
- super.onAttachedToWindow();
- if (!EventBus.getDefault().isRegistered(this)) EventBus.getDefault().register(this);
- }
-
- @Override
- protected void onDetachedFromWindow() {
- super.onDetachedFromWindow();
- EventBus.getDefault().unregister(this);
- }
-
- public void setSlide(final @NonNull Slide slides) {
- setSlides(Collections.singletonList(slides));
- }
-
- public void setSlides(final @NonNull List slides) {
- if (slides.isEmpty()) {
- throw new IllegalArgumentException("Must provide at least one slide.");
- }
-
- this.slides = slides;
-
- if (!isUpdateToExistingSet(slides)) {
- downloadProgress.clear();
- Stream.of(slides).forEach(s -> downloadProgress.put(s.asAttachment(), 0f));
- }
-
- for (Slide slide : slides) {
- if (slide.asAttachment().getTransferState() == AttachmentTransferProgress.TRANSFER_PROGRESS_DONE) {
- downloadProgress.put(slide.asAttachment(), 1f);
- }
- }
-
- switch (getTransferState(slides)) {
- case AttachmentTransferProgress.TRANSFER_PROGRESS_STARTED:
- showProgressSpinner(calculateProgress(downloadProgress));
- break;
- case AttachmentTransferProgress.TRANSFER_PROGRESS_PENDING:
- case AttachmentTransferProgress.TRANSFER_PROGRESS_FAILED:
- downloadDetailsText.setText(getDownloadText(this.slides));
- display(downloadDetails);
- break;
- default:
- display(null);
- break;
- }
- }
-
- public void showProgressSpinner() {
- showProgressSpinner(calculateProgress(downloadProgress));
- }
-
- public void showProgressSpinner(float progress) {
- if (progress == 0) {
- progressWheel.spin();
- } else {
- progressWheel.setInstantProgress(progress);
- }
-
- display(progressWheel);
- }
-
- public void setDownloadClickListener(final @Nullable OnClickListener listener) {
- downloadDetails.setOnClickListener(listener);
- }
-
- public void clear() {
- clearAnimation();
- setVisibility(GONE);
- if (current != null) {
- current.clearAnimation();
- current.setVisibility(GONE);
- }
- current = null;
- slides = null;
- }
-
- public void setShowDownloadText(boolean showDownloadText) {
- downloadDetailsText.setVisibility(showDownloadText ? VISIBLE : GONE);
- forceLayout();
- }
-
- private boolean isUpdateToExistingSet(@NonNull List slides) {
- if (slides.size() != downloadProgress.size()) {
- return false;
- }
-
- for (Slide slide : slides) {
- if (!downloadProgress.containsKey(slide.asAttachment())) {
- return false;
- }
- }
-
- return true;
- }
-
- private int getTransferState(@NonNull List slides) {
- int transferState = AttachmentTransferProgress.TRANSFER_PROGRESS_DONE;
- for (Slide slide : slides) {
- if (slide.getTransferState() == AttachmentTransferProgress.TRANSFER_PROGRESS_PENDING && transferState == AttachmentTransferProgress.TRANSFER_PROGRESS_DONE) {
- transferState = slide.getTransferState();
- } else {
- transferState = Math.max(transferState, slide.getTransferState());
- }
- }
- return transferState;
- }
-
- private String getDownloadText(@NonNull List slides) {
- if (slides.size() == 1) {
- return slides.get(0).getContentDescription();
- } else {
- int downloadCount = Stream.of(slides).reduce(0, (count, slide) -> slide.getTransferState() != AttachmentTransferProgress.TRANSFER_PROGRESS_DONE ? count + 1 : count);
- return getContext().getResources().getQuantityString(R.plurals.TransferControlView_n_items, downloadCount, downloadCount);
- }
- }
-
- private void display(@Nullable final View view) {
- if (current != null) {
- current.setVisibility(GONE);
- }
-
- if (view != null) {
- view.setVisibility(VISIBLE);
- } else {
- setVisibility(GONE);
- }
-
- current = view;
- }
-
- private float calculateProgress(@NonNull Map downloadProgress) {
- float totalProgress = 0;
- for (float progress : downloadProgress.values()) {
- totalProgress += progress / downloadProgress.size();
- }
- return totalProgress;
- }
-
- @Subscribe(sticky = true, threadMode = ThreadMode.MAIN)
- public void onEventAsync(final PartProgressEvent event) {
- if (downloadProgress.containsKey(event.attachment)) {
- downloadProgress.put(event.attachment, ((float) event.progress) / event.total);
- progressWheel.setInstantProgress(calculateProgress(downloadProgress));
- }
- }
-}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/TransferControlView.kt b/app/src/main/java/org/thoughtcrime/securesms/components/TransferControlView.kt
new file mode 100644
index 0000000000..03604079a5
--- /dev/null
+++ b/app/src/main/java/org/thoughtcrime/securesms/components/TransferControlView.kt
@@ -0,0 +1,182 @@
+package org.thoughtcrime.securesms.components
+
+import android.animation.LayoutTransition
+import android.content.Context
+import android.util.AttributeSet
+import android.view.View
+import android.widget.FrameLayout
+import android.widget.TextView
+import androidx.core.content.ContextCompat
+import com.annimon.stream.Stream
+import com.pnikosis.materialishprogress.ProgressWheel
+import kotlin.math.max
+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.Attachment
+import org.session.libsession.messaging.sending_receiving.attachments.AttachmentTransferProgress
+import org.session.libsession.utilities.StringSubstitutionConstants.COUNT_KEY
+import org.session.libsession.utilities.ViewUtil
+import org.thoughtcrime.securesms.events.PartProgressEvent
+import org.thoughtcrime.securesms.mms.Slide
+import org.thoughtcrime.securesms.ui.getSubbedString
+
+class TransferControlView @JvmOverloads constructor(context: Context?, attrs: AttributeSet? = null, defStyleAttr: Int = 0) : FrameLayout(context!!, attrs, defStyleAttr) {
+ private var slides: List? = null
+ private var current: View? = null
+
+ private val progressWheel: ProgressWheel
+ private val downloadDetails: View
+ private val downloadDetailsText: TextView
+ private val downloadProgress: MutableMap
+
+ init {
+ inflate(context, R.layout.transfer_controls_view, this)
+
+ isLongClickable = false
+ ViewUtil.setBackground(this, ContextCompat.getDrawable(context!!, R.drawable.transfer_controls_background))
+ visibility = GONE
+ layoutTransition = LayoutTransition()
+
+ this.downloadProgress = HashMap()
+ this.progressWheel = ViewUtil.findById(this, R.id.progress_wheel)
+ this.downloadDetails = ViewUtil.findById(this, R.id.download_details)
+ this.downloadDetailsText = ViewUtil.findById(this, R.id.download_details_text)
+ }
+
+ override fun setFocusable(focusable: Boolean) {
+ super.setFocusable(focusable)
+ downloadDetails.isFocusable = focusable
+ }
+
+ override fun setClickable(clickable: Boolean) {
+ super.setClickable(clickable)
+ downloadDetails.isClickable = clickable
+ }
+
+ override fun onAttachedToWindow() {
+ super.onAttachedToWindow()
+ if (!EventBus.getDefault().isRegistered(this)) EventBus.getDefault().register(this)
+ }
+
+ override fun onDetachedFromWindow() {
+ super.onDetachedFromWindow()
+ EventBus.getDefault().unregister(this)
+ }
+
+ private fun setSlides(slides: List) {
+ require(slides.isNotEmpty()) { "Must provide at least one slide." }
+
+ this.slides = slides
+
+ if (!isUpdateToExistingSet(slides)) {
+ downloadProgress.clear()
+ Stream.of(slides).forEach { s: Slide -> downloadProgress[s.asAttachment()] = 0f }
+ }
+
+ for (slide in slides) {
+ if (slide.asAttachment().transferState == AttachmentTransferProgress.TRANSFER_PROGRESS_DONE) {
+ downloadProgress[slide.asAttachment()] = 1f
+ }
+ }
+
+ when (getTransferState(slides)) {
+ AttachmentTransferProgress.TRANSFER_PROGRESS_STARTED -> showProgressSpinner(calculateProgress(downloadProgress))
+ AttachmentTransferProgress.TRANSFER_PROGRESS_PENDING, AttachmentTransferProgress.TRANSFER_PROGRESS_FAILED -> {
+ downloadDetailsText.text = getDownloadText(this.slides!!)
+ display(downloadDetails)
+ }
+
+ else -> display(null)
+ }
+ }
+
+ @JvmOverloads
+ fun showProgressSpinner(progress: Float = calculateProgress(downloadProgress)) {
+ if (progress == 0f) {
+ progressWheel.spin()
+ } else {
+ progressWheel.setInstantProgress(progress)
+ }
+ display(progressWheel)
+ }
+
+ fun clear() {
+ clearAnimation()
+ visibility = GONE
+ if (current != null) {
+ current!!.clearAnimation()
+ current!!.visibility = GONE
+ }
+ current = null
+ slides = null
+ }
+
+ private fun isUpdateToExistingSet(slides: List): Boolean {
+ if (slides.size != downloadProgress.size) {
+ return false
+ }
+
+ for (slide in slides) {
+ if (!downloadProgress.containsKey(slide.asAttachment())) {
+ return false
+ }
+ }
+
+ return true
+ }
+
+ private fun getTransferState(slides: List): Int {
+ var transferState = AttachmentTransferProgress.TRANSFER_PROGRESS_DONE
+ for (slide in slides) {
+ transferState = if (slide.transferState == AttachmentTransferProgress.TRANSFER_PROGRESS_PENDING && transferState == AttachmentTransferProgress.TRANSFER_PROGRESS_DONE) {
+ slide.transferState
+ } else {
+ max(transferState.toDouble(), slide.transferState.toDouble()).toInt()
+ }
+ }
+ return transferState
+ }
+
+ private fun getDownloadText(slides: List): String {
+ if (slides.size == 1) {
+ return slides[0].contentDescription
+ } else {
+ val downloadCount = Stream.of(slides).reduce(0) { count: Int, slide: Slide ->
+ if (slide.transferState != AttachmentTransferProgress.TRANSFER_PROGRESS_DONE) count + 1 else count
+ }
+ return context.getSubbedString(R.string.andMore, COUNT_KEY to downloadCount.toString())
+ }
+ }
+
+ private fun display(view: View?) {
+ if (current != null) {
+ current!!.visibility = GONE
+ }
+
+ if (view != null) {
+ view.visibility = VISIBLE
+ } else {
+ visibility = GONE
+ }
+
+ current = view
+ }
+
+ private fun calculateProgress(downloadProgress: Map): Float {
+ var totalProgress = 0f
+ for (progress in downloadProgress.values) {
+ totalProgress += progress / downloadProgress.size
+ }
+ return totalProgress
+ }
+
+ @Subscribe(sticky = true, threadMode = ThreadMode.MAIN)
+ fun onEventAsync(event: PartProgressEvent) {
+ if (downloadProgress.containsKey(event.attachment)) {
+ downloadProgress[event.attachment] = event.progress.toFloat() / event.total
+ progressWheel.setInstantProgress(calculateProgress(downloadProgress))
+ }
+ }
+}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/emoji/EmojiPages.java b/app/src/main/java/org/thoughtcrime/securesms/components/emoji/EmojiPages.java
index 78d085fb71..dddcb56a8e 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/components/emoji/EmojiPages.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/components/emoji/EmojiPages.java
@@ -1,13 +1,9 @@
package org.thoughtcrime.securesms.components.emoji;
import android.net.Uri;
-
import network.loki.messenger.R;
-import org.session.libsignal.utilities.Pair;
import org.thoughtcrime.securesms.emoji.EmojiCategory;
-
import java.util.Arrays;
-import java.util.LinkedList;
import java.util.List;
class EmojiPages {
@@ -58,24 +54,6 @@ class EmojiPages {
new Emoji("\ud83c\udfc1"), new Emoji("\ud83d\udea9"), new Emoji("\ud83c\udf8c"), new Emoji("\ud83c\udff4"), new Emoji("\ud83c\udff3\ufe0f"), new Emoji("\ud83c\udff3\ufe0f\u200d\ud83c\udf08"), new Emoji("\ud83c\udde6\ud83c\udde8"), new Emoji("\ud83c\udde6\ud83c\udde9"), new Emoji("\ud83c\udde6\ud83c\uddea"), new Emoji("\ud83c\udde6\ud83c\uddeb"), new Emoji("\ud83c\udde6\ud83c\uddec"), new Emoji("\ud83c\udde6\ud83c\uddee"), new Emoji("\ud83c\udde6\ud83c\uddf1"), new Emoji("\ud83c\udde6\ud83c\uddf2"), new Emoji("\ud83c\udde6\ud83c\uddf4"), new Emoji("\ud83c\udde6\ud83c\uddf6"), new Emoji("\ud83c\udde6\ud83c\uddf7"), new Emoji("\ud83c\udde6\ud83c\uddf8"), new Emoji("\ud83c\udde6\ud83c\uddf9"), new Emoji("\ud83c\udde6\ud83c\uddfa"), new Emoji("\ud83c\udde6\ud83c\uddfc"), new Emoji("\ud83c\udde6\ud83c\uddfd"), new Emoji("\ud83c\udde6\ud83c\uddff"), new Emoji("\ud83c\udde7\ud83c\udde6"), new Emoji("\ud83c\udde7\ud83c\udde7"), new Emoji("\ud83c\udde7\ud83c\udde9"), new Emoji("\ud83c\udde7\ud83c\uddea"), new Emoji("\ud83c\udde7\ud83c\uddeb"), new Emoji("\ud83c\udde7\ud83c\uddec"), new Emoji("\ud83c\udde7\ud83c\udded"), new Emoji("\ud83c\udde7\ud83c\uddee"), new Emoji("\ud83c\udde7\ud83c\uddef"), new Emoji("\ud83c\udde7\ud83c\uddf1"), new Emoji("\ud83c\udde7\ud83c\uddf2"), new Emoji("\ud83c\udde7\ud83c\uddf3"), new Emoji("\ud83c\udde7\ud83c\uddf4"), new Emoji("\ud83c\udde7\ud83c\uddf6"), new Emoji("\ud83c\udde7\ud83c\uddf7"), new Emoji("\ud83c\udde7\ud83c\uddf8"), new Emoji("\ud83c\udde7\ud83c\uddf9"), new Emoji("\ud83c\udde7\ud83c\uddfb"), new Emoji("\ud83c\udde7\ud83c\uddfc"), new Emoji("\ud83c\udde7\ud83c\uddfe"), new Emoji("\ud83c\udde7\ud83c\uddff"), new Emoji("\ud83c\udde8\ud83c\udde6"), new Emoji("\ud83c\udde8\ud83c\udde8"), new Emoji("\ud83c\udde8\ud83c\udde9"), new Emoji("\ud83c\udde8\ud83c\uddeb"), new Emoji("\ud83c\udde8\ud83c\uddec"), new Emoji("\ud83c\udde8\ud83c\udded"), new Emoji("\ud83c\udde8\ud83c\uddee"), new Emoji("\ud83c\udde8\ud83c\uddf0"), new Emoji("\ud83c\udde8\ud83c\uddf1"), new Emoji("\ud83c\udde8\ud83c\uddf2"), new Emoji("\ud83c\udde8\ud83c\uddf3"), new Emoji("\ud83c\udde8\ud83c\uddf4"), new Emoji("\ud83c\udde8\ud83c\uddf5"), new Emoji("\ud83c\udde8\ud83c\uddf7"), new Emoji("\ud83c\udde8\ud83c\uddfa"), new Emoji("\ud83c\udde8\ud83c\uddfb"), new Emoji("\ud83c\udde8\ud83c\uddfc"), new Emoji("\ud83c\udde8\ud83c\uddfd"), new Emoji("\ud83c\udde8\ud83c\uddfe"), new Emoji("\ud83c\udde8\ud83c\uddff"), new Emoji("\ud83c\udde9\ud83c\uddea"), new Emoji("\ud83c\udde9\ud83c\uddec"), new Emoji("\ud83c\udde9\ud83c\uddef"), new Emoji("\ud83c\udde9\ud83c\uddf0"), new Emoji("\ud83c\udde9\ud83c\uddf2"), new Emoji("\ud83c\udde9\ud83c\uddf4"), new Emoji("\ud83c\udde9\ud83c\uddff"), new Emoji("\ud83c\uddea\ud83c\udde6"), new Emoji("\ud83c\uddea\ud83c\udde8"), new Emoji("\ud83c\uddea\ud83c\uddea"), new Emoji("\ud83c\uddea\ud83c\uddec"), new Emoji("\ud83c\uddea\ud83c\udded"), new Emoji("\ud83c\uddea\ud83c\uddf7"), new Emoji("\ud83c\uddea\ud83c\uddf8"), new Emoji("\ud83c\uddea\ud83c\uddf9"), new Emoji("\ud83c\uddea\ud83c\uddfa"), new Emoji("\ud83c\uddeb\ud83c\uddee"), new Emoji("\ud83c\uddeb\ud83c\uddef"), new Emoji("\ud83c\uddeb\ud83c\uddf0"), new Emoji("\ud83c\uddeb\ud83c\uddf2"), new Emoji("\ud83c\uddeb\ud83c\uddf4"), new Emoji("\ud83c\uddeb\ud83c\uddf7"), new Emoji("\ud83c\uddec\ud83c\udde6"), new Emoji("\ud83c\uddec\ud83c\udde7"), new Emoji("\ud83c\uddec\ud83c\udde9"), new Emoji("\ud83c\uddec\ud83c\uddea"), new Emoji("\ud83c\uddec\ud83c\uddeb"), new Emoji("\ud83c\uddec\ud83c\uddec"), new Emoji("\ud83c\uddec\ud83c\udded"), new Emoji("\ud83c\uddec\ud83c\uddee"), new Emoji("\ud83c\uddec\ud83c\uddf1"), new Emoji("\ud83c\uddec\ud83c\uddf2"), new Emoji("\ud83c\uddec\ud83c\uddf3"), new Emoji("\ud83c\uddec\ud83c\uddf5"), new Emoji("\ud83c\uddec\ud83c\uddf6"), new Emoji("\ud83c\uddec\ud83c\uddf7"), new Emoji("\ud83c\uddec\ud83c\uddf8"), new Emoji("\ud83c\uddec\ud83c\uddf9"), new Emoji("\ud83c\uddec\ud83c\uddfa"), new Emoji("\ud83c\uddec\ud83c\uddfc"), new Emoji("\ud83c\uddec\ud83c\uddfe"), new Emoji("\ud83c\udded\ud83c\uddf0"), new Emoji("\ud83c\udded\ud83c\uddf2"), new Emoji("\ud83c\udded\ud83c\uddf3"), new Emoji("\ud83c\udded\ud83c\uddf7"), new Emoji("\ud83c\udded\ud83c\uddf9"), new Emoji("\ud83c\udded\ud83c\uddfa"), new Emoji("\ud83c\uddee\ud83c\udde8"), new Emoji("\ud83c\uddee\ud83c\udde9"), new Emoji("\ud83c\uddee\ud83c\uddea"), new Emoji("\ud83c\uddee\ud83c\uddf1"), new Emoji("\ud83c\uddee\ud83c\uddf2"), new Emoji("\ud83c\uddee\ud83c\uddf3"), new Emoji("\ud83c\uddee\ud83c\uddf4"), new Emoji("\ud83c\uddee\ud83c\uddf6"), new Emoji("\ud83c\uddee\ud83c\uddf7"), new Emoji("\ud83c\uddee\ud83c\uddf8"), new Emoji("\ud83c\uddee\ud83c\uddf9"), new Emoji("\ud83c\uddef\ud83c\uddea"), new Emoji("\ud83c\uddef\ud83c\uddf2"), new Emoji("\ud83c\uddef\ud83c\uddf4"), new Emoji("\ud83c\uddef\ud83c\uddf5"), new Emoji("\ud83c\uddf0\ud83c\uddea"), new Emoji("\ud83c\uddf0\ud83c\uddec"), new Emoji("\ud83c\uddf0\ud83c\udded"), new Emoji("\ud83c\uddf0\ud83c\uddee"), new Emoji("\ud83c\uddf0\ud83c\uddf2"), new Emoji("\ud83c\uddf0\ud83c\uddf3"), new Emoji("\ud83c\uddf0\ud83c\uddf5"), new Emoji("\ud83c\uddf0\ud83c\uddf7"), new Emoji("\ud83c\uddf0\ud83c\uddfc"), new Emoji("\ud83c\uddf0\ud83c\uddfe"), new Emoji("\ud83c\uddf0\ud83c\uddff"), new Emoji("\ud83c\uddf1\ud83c\udde6"), new Emoji("\ud83c\uddf1\ud83c\udde7"), new Emoji("\ud83c\uddf1\ud83c\udde8"), new Emoji("\ud83c\uddf1\ud83c\uddee"), new Emoji("\ud83c\uddf1\ud83c\uddf0"), new Emoji("\ud83c\uddf1\ud83c\uddf7"), new Emoji("\ud83c\uddf1\ud83c\uddf8"), new Emoji("\ud83c\uddf1\ud83c\uddf9"), new Emoji("\ud83c\uddf1\ud83c\uddfa"), new Emoji("\ud83c\uddf1\ud83c\uddfb"), new Emoji("\ud83c\uddf1\ud83c\uddfe"), new Emoji("\ud83c\uddf2\ud83c\udde6"), new Emoji("\ud83c\uddf2\ud83c\udde8"), new Emoji("\ud83c\uddf2\ud83c\udde9"), new Emoji("\ud83c\uddf2\ud83c\uddea"), new Emoji("\ud83c\uddf2\ud83c\uddeb"), new Emoji("\ud83c\uddf2\ud83c\uddec"), new Emoji("\ud83c\uddf2\ud83c\udded"), new Emoji("\ud83c\uddf2\ud83c\uddf0"), new Emoji("\ud83c\uddf2\ud83c\uddf1"), new Emoji("\ud83c\uddf2\ud83c\uddf2"), new Emoji("\ud83c\uddf2\ud83c\uddf3"), new Emoji("\ud83c\uddf2\ud83c\uddf4"), new Emoji("\ud83c\uddf2\ud83c\uddf5"), new Emoji("\ud83c\uddf2\ud83c\uddf6"), new Emoji("\ud83c\uddf2\ud83c\uddf7"), new Emoji("\ud83c\uddf2\ud83c\uddf8"), new Emoji("\ud83c\uddf2\ud83c\uddf9"), new Emoji("\ud83c\uddf2\ud83c\uddfa"), new Emoji("\ud83c\uddf2\ud83c\uddfb"), new Emoji("\ud83c\uddf2\ud83c\uddfc"), new Emoji("\ud83c\uddf2\ud83c\uddfd"), new Emoji("\ud83c\uddf2\ud83c\uddfe"), new Emoji("\ud83c\uddf2\ud83c\uddff"), new Emoji("\ud83c\uddf3\ud83c\udde6"), new Emoji("\ud83c\uddf3\ud83c\udde8"), new Emoji("\ud83c\uddf3\ud83c\uddea"), new Emoji("\ud83c\uddf3\ud83c\uddeb"), new Emoji("\ud83c\uddf3\ud83c\uddec"), new Emoji("\ud83c\uddf3\ud83c\uddee"), new Emoji("\ud83c\uddf3\ud83c\uddf1"), new Emoji("\ud83c\uddf3\ud83c\uddf4"), new Emoji("\ud83c\uddf3\ud83c\uddf5"), new Emoji("\ud83c\uddf3\ud83c\uddf7"), new Emoji("\ud83c\uddf3\ud83c\uddfa"), new Emoji("\ud83c\uddf3\ud83c\uddff"), new Emoji("\ud83c\uddf4\ud83c\uddf2"), new Emoji("\ud83c\uddf5\ud83c\udde6"), new Emoji("\ud83c\uddf5\ud83c\uddea"), new Emoji("\ud83c\uddf5\ud83c\uddeb"), new Emoji("\ud83c\uddf5\ud83c\uddec"), new Emoji("\ud83c\uddf5\ud83c\udded"), new Emoji("\ud83c\uddf5\ud83c\uddf0"), new Emoji("\ud83c\uddf5\ud83c\uddf1"), new Emoji("\ud83c\uddf5\ud83c\uddf2"), new Emoji("\ud83c\uddf5\ud83c\uddf3"), new Emoji("\ud83c\uddf5\ud83c\uddf7"), new Emoji("\ud83c\uddf5\ud83c\uddf8"), new Emoji("\ud83c\uddf5\ud83c\uddf9"), new Emoji("\ud83c\uddf5\ud83c\uddfc"), new Emoji("\ud83c\uddf5\ud83c\uddfe"), new Emoji("\ud83c\uddf6\ud83c\udde6"), new Emoji("\ud83c\uddf7\ud83c\uddea"), new Emoji("\ud83c\uddf7\ud83c\uddf4"), new Emoji("\ud83c\uddf7\ud83c\uddf8"), new Emoji("\ud83c\uddf7\ud83c\uddfa"), new Emoji("\ud83c\uddf7\ud83c\uddfc"), new Emoji("\ud83c\uddf8\ud83c\udde6"), new Emoji("\ud83c\uddf8\ud83c\udde7"), new Emoji("\ud83c\uddf8\ud83c\udde8"), new Emoji("\ud83c\uddf8\ud83c\udde9"), new Emoji("\ud83c\uddf8\ud83c\uddea"), new Emoji("\ud83c\uddf8\ud83c\uddec"), new Emoji("\ud83c\uddf8\ud83c\udded"), new Emoji("\ud83c\uddf8\ud83c\uddee"), new Emoji("\ud83c\uddf8\ud83c\uddef"), new Emoji("\ud83c\uddf8\ud83c\uddf0"), new Emoji("\ud83c\uddf8\ud83c\uddf1"), new Emoji("\ud83c\uddf8\ud83c\uddf2"), new Emoji("\ud83c\uddf8\ud83c\uddf3"), new Emoji("\ud83c\uddf8\ud83c\uddf4"), new Emoji("\ud83c\uddf8\ud83c\uddf7"), new Emoji("\ud83c\uddf8\ud83c\uddf8"), new Emoji("\ud83c\uddf8\ud83c\uddf9"), new Emoji("\ud83c\uddf8\ud83c\uddfb"), new Emoji("\ud83c\uddf8\ud83c\uddfd"), new Emoji("\ud83c\uddf8\ud83c\uddfe"), new Emoji("\ud83c\uddf8\ud83c\uddff"), new Emoji("\ud83c\uddf9\ud83c\udde6"), new Emoji("\ud83c\uddf9\ud83c\udde8"), new Emoji("\ud83c\uddf9\ud83c\udde9"), new Emoji("\ud83c\uddf9\ud83c\uddeb"), new Emoji("\ud83c\uddf9\ud83c\uddec"), new Emoji("\ud83c\uddf9\ud83c\udded"), new Emoji("\ud83c\uddf9\ud83c\uddef"), new Emoji("\ud83c\uddf9\ud83c\uddf0"), new Emoji("\ud83c\uddf9\ud83c\uddf1"), new Emoji("\ud83c\uddf9\ud83c\uddf2"), new Emoji("\ud83c\uddf9\ud83c\uddf3"), new Emoji("\ud83c\uddf9\ud83c\uddf4"), new Emoji("\ud83c\uddf9\ud83c\uddf7"), new Emoji("\ud83c\uddf9\ud83c\uddf9"), new Emoji("\ud83c\uddf9\ud83c\uddfb"), new Emoji("\ud83c\uddf9\ud83c\uddfc"), new Emoji("\ud83c\uddf9\ud83c\uddff"), new Emoji("\ud83c\uddfa\ud83c\udde6"), new Emoji("\ud83c\uddfa\ud83c\uddec"), new Emoji("\ud83c\uddfa\ud83c\uddf2"), new Emoji("\ud83c\uddfa\ud83c\uddf8"), new Emoji("\ud83c\uddfa\ud83c\uddfe"), new Emoji("\ud83c\uddfa\ud83c\uddff"), new Emoji("\ud83c\uddfb\ud83c\udde6"), new Emoji("\ud83c\uddfb\ud83c\udde8"), new Emoji("\ud83c\uddfb\ud83c\uddea"), new Emoji("\ud83c\uddfb\ud83c\uddec"), new Emoji("\ud83c\uddfb\ud83c\uddee"), new Emoji("\ud83c\uddfb\ud83c\uddf3"), new Emoji("\ud83c\uddfb\ud83c\uddfa"), new Emoji("\ud83c\uddfc\ud83c\uddeb"), new Emoji("\ud83c\uddfc\ud83c\uddf8"), new Emoji("\ud83c\uddfd\ud83c\uddf0"), new Emoji("\ud83c\uddfe\ud83c\uddea"), new Emoji("\ud83c\uddfe\ud83c\uddf9"), new Emoji("\ud83c\uddff\ud83c\udde6"), new Emoji("\ud83c\uddff\ud83c\uddf2"), new Emoji("\ud83c\uddff\ud83c\uddfc"), new Emoji("\ud83c\udff4\udb40\udc67\udb40\udc62\udb40\udc65\udb40\udc6e\udb40\udc67\udb40\udc7f"), new Emoji("\ud83c\udff4\udb40\udc67\udb40\udc62\udb40\udc73\udb40\udc63\udb40\udc74\udb40\udc7f"), new Emoji("\ud83c\udff4\udb40\udc67\udb40\udc62\udb40\udc77\udb40\udc6c\udb40\udc73\udb40\udc7f")
), Uri.parse("emoji/Flags.png"));
- private static final EmojiPageModel PAGE_EMOTICONS = new StaticEmojiPageModel(EmojiCategory.EMOTICONS, new String[] {
- ":-)", ";-)", "(-:", ":->", ":-D", "\\o/",
- ":-P", "B-)", ":-$", ":-*", "O:-)", "=-O",
- "O_O", "O_o", "o_O", ":O", ":-!", ":-x",
- ":-|", ":-\\", ":-(", ":'(", ":-[", ">:-(",
- "^.^", "^_^", "\\(\u02c6\u02da\u02c6)/",
- "\u30fd(\u00b0\u25c7\u00b0 )\u30ce", "\u00af\\(\u00b0_o)/\u00af",
- "\u00af\\_(\u30c4)_/\u00af", "(\u00ac_\u00ac)",
- "(>_<)", "(\u2565\ufe4f\u2565)", "(\u261e\uff9f\u30ee\uff9f)\u261e",
- "\u261c(\uff9f\u30ee\uff9f\u261c)", "\u261c(\u2312\u25bd\u2312)\u261e",
- "(\u256f\u00b0\u25a1\u00b0)\u256f\ufe35", "\u253b\u2501\u253b",
- "\u252c\u2500\u252c", "\u30ce(\u00b0\u2013\u00b0\u30ce)",
- "(^._.^)\uff89", "\u0e05^\u2022\ufecc\u2022^\u0e05",
- "\u0295\u2022\u1d25\u2022\u0294", "(\u2022_\u2022)",
- " \u25a0-\u25a0\u00ac <(\u2022_\u2022) ", "(\u25a0_\u25a0\u00ac)",
- "\u01aa(\u0693\u05f2)\u200e\u01aa\u200b\u200b"
- }, null);
-
static final List DISPLAY_PAGES = Arrays.asList(PAGE_PEOPLE,
PAGE_NATURE,
PAGE_FOODS,
@@ -83,226 +61,7 @@ class EmojiPages {
PAGE_PLACES,
PAGE_OBJECTS,
PAGE_SYMBOLS,
- PAGE_FLAGS,
- PAGE_EMOTICONS);
+ PAGE_FLAGS);
- static final List DATA_PAGES = Arrays.asList(PAGE_PEOPLE_0,
- PAGE_PEOPLE_1,
- PAGE_PEOPLE_2,
- PAGE_PEOPLE_3,
- PAGE_NATURE,
- PAGE_FOODS,
- PAGE_ACTIVITY,
- PAGE_PLACES,
- PAGE_OBJECTS,
- PAGE_SYMBOLS,
- PAGE_FLAGS,
- PAGE_EMOTICONS);
- static final List> OBSOLETE = new LinkedList>() {{
- add(new Pair<>("\ud83d\udc6e", "\ud83d\udc6e\u200d\u2642\ufe0f"));
- add(new Pair<>("\ud83d\udc6e\ud83c\udffb", "\ud83d\udc6e\ud83c\udffb\u200d\u2642\ufe0f"));
- add(new Pair<>("\ud83d\udc6e\ud83c\udffc", "\ud83d\udc6e\ud83c\udffc\u200d\u2642\ufe0f"));
- add(new Pair<>("\ud83d\udc6e\ud83c\udffd", "\ud83d\udc6e\ud83c\udffd\u200d\u2642\ufe0f"));
- add(new Pair<>("\ud83d\udc6e\ud83c\udffe", "\ud83d\udc6e\ud83c\udffe\u200d\u2642\ufe0f"));
- add(new Pair<>("\ud83d\udc6e\ud83c\udfff", "\ud83d\udc6e\ud83c\udfff\u200d\u2642\ufe0f"));
- add(new Pair<>("\ud83d\udd75\ufe0f", "\ud83d\udd75\ufe0f\u200d\u2642\ufe0f"));
- add(new Pair<>("\ud83d\udd75\ud83c\udffb", "\ud83d\udd75\ud83c\udffb\u200d\u2642\ufe0f"));
- add(new Pair<>("\ud83d\udd75\ud83c\udffc", "\ud83d\udd75\ud83c\udffc\u200d\u2642\ufe0f"));
- add(new Pair<>("\ud83d\udd75\ud83c\udffd", "\ud83d\udd75\ud83c\udffd\u200d\u2642\ufe0f"));
- add(new Pair<>("\ud83d\udd75\ud83c\udffe", "\ud83d\udd75\ud83c\udffe\u200d\u2642\ufe0f"));
- add(new Pair<>("\ud83d\udd75\ud83c\udfff", "\ud83d\udd75\ud83c\udfff\u200d\u2642\ufe0f"));
- add(new Pair<>("\ud83d\udc82", "\ud83d\udc82\u200d\u2642\ufe0f"));
- add(new Pair<>("\ud83d\udc82\ud83c\udffb", "\ud83d\udc82\ud83c\udffb\u200d\u2642\ufe0f"));
- add(new Pair<>("\ud83d\udc82\ud83c\udffc", "\ud83d\udc82\ud83c\udffc\u200d\u2642\ufe0f"));
- add(new Pair<>("\ud83d\udc82\ud83c\udffd", "\ud83d\udc82\ud83c\udffd\u200d\u2642\ufe0f"));
- add(new Pair<>("\ud83d\udc82\ud83c\udffe", "\ud83d\udc82\ud83c\udffe\u200d\u2642\ufe0f"));
- add(new Pair<>("\ud83d\udc82\ud83c\udfff", "\ud83d\udc82\ud83c\udfff\u200d\u2642\ufe0f"));
- add(new Pair<>("\ud83d\udc77", "\ud83d\udc77\u200d\u2642\ufe0f"));
- add(new Pair<>("\ud83d\udc77\ud83c\udffb", "\ud83d\udc77\ud83c\udffb\u200d\u2642\ufe0f"));
- add(new Pair<>("\ud83d\udc77\ud83c\udffc", "\ud83d\udc77\ud83c\udffc\u200d\u2642\ufe0f"));
- add(new Pair<>("\ud83d\udc77\ud83c\udffd", "\ud83d\udc77\ud83c\udffd\u200d\u2642\ufe0f"));
- add(new Pair<>("\ud83d\udc77\ud83c\udffe", "\ud83d\udc77\ud83c\udffe\u200d\u2642\ufe0f"));
- add(new Pair<>("\ud83d\udc77\ud83c\udfff", "\ud83d\udc77\ud83c\udfff\u200d\u2642\ufe0f"));
- add(new Pair<>("\ud83d\udc73", "\ud83d\udc73\u200d\u2642\ufe0f"));
- add(new Pair<>("\ud83d\udc73\ud83c\udffb", "\ud83d\udc73\ud83c\udffb\u200d\u2642\ufe0f"));
- add(new Pair<>("\ud83d\udc73\ud83c\udffc", "\ud83d\udc73\ud83c\udffc\u200d\u2642\ufe0f"));
- add(new Pair<>("\ud83d\udc73\ud83c\udffd", "\ud83d\udc73\ud83c\udffd\u200d\u2642\ufe0f"));
- add(new Pair<>("\ud83d\udc73\ud83c\udffe", "\ud83d\udc73\ud83c\udffe\u200d\u2642\ufe0f"));
- add(new Pair<>("\ud83d\udc73\ud83c\udfff", "\ud83d\udc73\ud83c\udfff\u200d\u2642\ufe0f"));
- add(new Pair<>("\ud83d\udc71", "\ud83d\udc71\u200d\u2642\ufe0f"));
- add(new Pair<>("\ud83d\udc71\ud83c\udffb", "\ud83d\udc71\ud83c\udffb\u200d\u2642\ufe0f"));
- add(new Pair<>("\ud83d\udc71\ud83c\udffc", "\ud83d\udc71\ud83c\udffc\u200d\u2642\ufe0f"));
- add(new Pair<>("\ud83d\udc71\ud83c\udffd", "\ud83d\udc71\ud83c\udffd\u200d\u2642\ufe0f"));
- add(new Pair<>("\ud83d\udc71\ud83c\udffe", "\ud83d\udc71\ud83c\udffe\u200d\u2642\ufe0f"));
- add(new Pair<>("\ud83d\udc71\ud83c\udfff", "\ud83d\udc71\ud83c\udfff\u200d\u2642\ufe0f"));
- add(new Pair<>("\ud83e\uddd9", "\ud83e\uddd9\u200d\u2640\ufe0f"));
- add(new Pair<>("\ud83e\uddd9\ud83c\udffb", "\ud83e\uddd9\ud83c\udffb\u200d\u2640\ufe0f"));
- add(new Pair<>("\ud83e\uddd9\ud83c\udffc", "\ud83e\uddd9\ud83c\udffc\u200d\u2640\ufe0f"));
- add(new Pair<>("\ud83e\uddd9\ud83c\udffd", "\ud83e\uddd9\ud83c\udffd\u200d\u2640\ufe0f"));
- add(new Pair<>("\ud83e\uddd9\ud83c\udffe", "\ud83e\uddd9\ud83c\udffe\u200d\u2640\ufe0f"));
- add(new Pair<>("\ud83e\uddd9\ud83c\udfff", "\ud83e\uddd9\ud83c\udfff\u200d\u2640\ufe0f"));
- add(new Pair<>("\ud83e\uddda", "\ud83e\uddda\u200d\u2640\ufe0f"));
- add(new Pair<>("\ud83e\uddda\ud83c\udffb", "\ud83e\uddda\ud83c\udffb\u200d\u2640\ufe0f"));
- add(new Pair<>("\ud83e\uddda\ud83c\udffc", "\ud83e\uddda\ud83c\udffc\u200d\u2640\ufe0f"));
- add(new Pair<>("\ud83e\uddda\ud83c\udffd", "\ud83e\uddda\ud83c\udffd\u200d\u2640\ufe0f"));
- add(new Pair<>("\ud83e\uddda\ud83c\udffe", "\ud83e\uddda\ud83c\udffe\u200d\u2640\ufe0f"));
- add(new Pair<>("\ud83e\uddda\ud83c\udfff", "\ud83e\uddda\ud83c\udfff\u200d\u2640\ufe0f"));
- add(new Pair<>("\ud83e\udddb", "\ud83e\udddb\u200d\u2640\ufe0f"));
- add(new Pair<>("\ud83e\udddb\ud83c\udffb", "\ud83e\udddb\ud83c\udffb\u200d\u2640\ufe0f"));
- add(new Pair<>("\ud83e\udddb\ud83c\udffc", "\ud83e\udddb\ud83c\udffc\u200d\u2640\ufe0f"));
- add(new Pair<>("\ud83e\udddb\ud83c\udffd", "\ud83e\udddb\ud83c\udffd\u200d\u2640\ufe0f"));
- add(new Pair<>("\ud83e\udddb\ud83c\udffe", "\ud83e\udddb\ud83c\udffe\u200d\u2640\ufe0f"));
- add(new Pair<>("\ud83e\udddb\ud83c\udfff", "\ud83e\udddb\ud83c\udfff\u200d\u2640\ufe0f"));
- add(new Pair<>("\ud83e\udddc", "\ud83e\udddc\u200d\u2642\ufe0f"));
- add(new Pair<>("\ud83e\udddc\ud83c\udffb", "\ud83e\udddc\ud83c\udffb\u200d\u2642\ufe0f"));
- add(new Pair<>("\ud83e\udddc\ud83c\udffc", "\ud83e\udddc\ud83c\udffc\u200d\u2642\ufe0f"));
- add(new Pair<>("\ud83e\udddc\ud83c\udffd", "\ud83e\udddc\ud83c\udffd\u200d\u2642\ufe0f"));
- add(new Pair<>("\ud83e\udddc\ud83c\udffe", "\ud83e\udddc\ud83c\udffe\u200d\u2642\ufe0f"));
- add(new Pair<>("\ud83e\udddc\ud83c\udfff", "\ud83e\udddc\ud83c\udfff\u200d\u2642\ufe0f"));
- add(new Pair<>("\ud83e\udddd", "\ud83e\udddd\u200d\u2642\ufe0f"));
- add(new Pair<>("\ud83e\udddd\ud83c\udffb", "\ud83e\udddd\ud83c\udffb\u200d\u2642\ufe0f"));
- add(new Pair<>("\ud83e\udddd\ud83c\udffc", "\ud83e\udddd\ud83c\udffc\u200d\u2642\ufe0f"));
- add(new Pair<>("\ud83e\udddd\ud83c\udffd", "\ud83e\udddd\ud83c\udffd\u200d\u2642\ufe0f"));
- add(new Pair<>("\ud83e\udddd\ud83c\udffe", "\ud83e\udddd\ud83c\udffe\u200d\u2642\ufe0f"));
- add(new Pair<>("\ud83e\udddd\ud83c\udfff", "\ud83e\udddd\ud83c\udfff\u200d\u2642\ufe0f"));
- add(new Pair<>("\ud83e\uddde", "\ud83e\uddde\u200d\u2642\ufe0f"));
- add(new Pair<>("\ud83e\udddf", "\ud83e\udddf\u200d\u2642\ufe0f"));
- add(new Pair<>("\ud83d\ude4d", "\ud83d\ude4d\u200d\u2640\ufe0f"));
- add(new Pair<>("\ud83d\ude4d\ud83c\udffb", "\ud83d\ude4d\ud83c\udffb\u200d\u2640\ufe0f"));
- add(new Pair<>("\ud83d\ude4d\ud83c\udffc", "\ud83d\ude4d\ud83c\udffc\u200d\u2640\ufe0f"));
- add(new Pair<>("\ud83d\ude4d\ud83c\udffd", "\ud83d\ude4d\ud83c\udffd\u200d\u2640\ufe0f"));
- add(new Pair<>("\ud83d\ude4d\ud83c\udffe", "\ud83d\ude4d\ud83c\udffe\u200d\u2640\ufe0f"));
- add(new Pair<>("\ud83d\ude4d\ud83c\udfff", "\ud83d\ude4d\ud83c\udfff\u200d\u2640\ufe0f"));
- add(new Pair<>("\ud83d\ude4e", "\ud83d\ude4e\u200d\u2640\ufe0f"));
- add(new Pair<>("\ud83d\ude4e\ud83c\udffb", "\ud83d\ude4e\ud83c\udffb\u200d\u2640\ufe0f"));
- add(new Pair<>("\ud83d\ude4e\ud83c\udffc", "\ud83d\ude4e\ud83c\udffc\u200d\u2640\ufe0f"));
- add(new Pair<>("\ud83d\ude4e\ud83c\udffd", "\ud83d\ude4e\ud83c\udffd\u200d\u2640\ufe0f"));
- add(new Pair<>("\ud83d\ude4e\ud83c\udffe", "\ud83d\ude4e\ud83c\udffe\u200d\u2640\ufe0f"));
- add(new Pair<>("\ud83d\ude4e\ud83c\udfff", "\ud83d\ude4e\ud83c\udfff\u200d\u2640\ufe0f"));
- add(new Pair<>("\ud83d\ude45", "\ud83d\ude45\u200d\u2640\ufe0f"));
- add(new Pair<>("\ud83d\ude45\ud83c\udffb", "\ud83d\ude45\ud83c\udffb\u200d\u2640\ufe0f"));
- add(new Pair<>("\ud83d\ude45\ud83c\udffc", "\ud83d\ude45\ud83c\udffc\u200d\u2640\ufe0f"));
- add(new Pair<>("\ud83d\ude45\ud83c\udffd", "\ud83d\ude45\ud83c\udffd\u200d\u2640\ufe0f"));
- add(new Pair<>("\ud83d\ude45\ud83c\udffe", "\ud83d\ude45\ud83c\udffe\u200d\u2640\ufe0f"));
- add(new Pair<>("\ud83d\ude45\ud83c\udfff", "\ud83d\ude45\ud83c\udfff\u200d\u2640\ufe0f"));
- add(new Pair<>("\ud83d\ude46", "\ud83d\ude46\u200d\u2640\ufe0f"));
- add(new Pair<>("\ud83d\ude46\ud83c\udffb", "\ud83d\ude46\ud83c\udffb\u200d\u2640\ufe0f"));
- add(new Pair<>("\ud83d\ude46\ud83c\udffc", "\ud83d\ude46\ud83c\udffc\u200d\u2640\ufe0f"));
- add(new Pair<>("\ud83d\ude46\ud83c\udffd", "\ud83d\ude46\ud83c\udffd\u200d\u2640\ufe0f"));
- add(new Pair<>("\ud83d\ude46\ud83c\udffe", "\ud83d\ude46\ud83c\udffe\u200d\u2640\ufe0f"));
- add(new Pair<>("\ud83d\ude46\ud83c\udfff", "\ud83d\ude46\ud83c\udfff\u200d\u2640\ufe0f"));
- add(new Pair<>("\ud83d\udc81", "\ud83d\udc81\u200d\u2640\ufe0f"));
- add(new Pair<>("\ud83d\udc81\ud83c\udffb", "\ud83d\udc81\ud83c\udffb\u200d\u2640\ufe0f"));
- add(new Pair<>("\ud83d\udc81\ud83c\udffc", "\ud83d\udc81\ud83c\udffc\u200d\u2640\ufe0f"));
- add(new Pair<>("\ud83d\udc81\ud83c\udffd", "\ud83d\udc81\ud83c\udffd\u200d\u2640\ufe0f"));
- add(new Pair<>("\ud83d\udc81\ud83c\udffe", "\ud83d\udc81\ud83c\udffe\u200d\u2640\ufe0f"));
- add(new Pair<>("\ud83d\udc81\ud83c\udfff", "\ud83d\udc81\ud83c\udfff\u200d\u2640\ufe0f"));
- add(new Pair<>("\ud83d\ude4b", "\ud83d\ude4b\u200d\u2640\ufe0f"));
- add(new Pair<>("\ud83d\ude4b\ud83c\udffb", "\ud83d\ude4b\ud83c\udffb\u200d\u2640\ufe0f"));
- add(new Pair<>("\ud83d\ude4b\ud83c\udffc", "\ud83d\ude4b\ud83c\udffc\u200d\u2640\ufe0f"));
- add(new Pair<>("\ud83d\ude4b\ud83c\udffd", "\ud83d\ude4b\ud83c\udffd\u200d\u2640\ufe0f"));
- add(new Pair<>("\ud83d\ude4b\ud83c\udffe", "\ud83d\ude4b\ud83c\udffe\u200d\u2640\ufe0f"));
- add(new Pair<>("\ud83d\ude4b\ud83c\udfff", "\ud83d\ude4b\ud83c\udfff\u200d\u2640\ufe0f"));
- add(new Pair<>("\ud83d\ude47", "\ud83d\ude47\u200d\u2642\ufe0f"));
- add(new Pair<>("\ud83d\ude47\ud83c\udffb", "\ud83d\ude47\ud83c\udffb\u200d\u2642\ufe0f"));
- add(new Pair<>("\ud83d\ude47\ud83c\udffc", "\ud83d\ude47\ud83c\udffc\u200d\u2642\ufe0f"));
- add(new Pair<>("\ud83d\ude47\ud83c\udffd", "\ud83d\ude47\ud83c\udffd\u200d\u2642\ufe0f"));
- add(new Pair<>("\ud83d\ude47\ud83c\udffe", "\ud83d\ude47\ud83c\udffe\u200d\u2642\ufe0f"));
- add(new Pair<>("\ud83d\ude47\ud83c\udfff", "\ud83d\ude47\ud83c\udfff\u200d\u2642\ufe0f"));
- add(new Pair<>("\ud83d\udc86", "\ud83d\udc86\u200d\u2640\ufe0f"));
- add(new Pair<>("\ud83d\udc86\ud83c\udffb", "\ud83d\udc86\ud83c\udffb\u200d\u2640\ufe0f"));
- add(new Pair<>("\ud83d\udc86\ud83c\udffc", "\ud83d\udc86\ud83c\udffc\u200d\u2640\ufe0f"));
- add(new Pair<>("\ud83d\udc86\ud83c\udffd", "\ud83d\udc86\ud83c\udffd\u200d\u2640\ufe0f"));
- add(new Pair<>("\ud83d\udc86\ud83c\udffe", "\ud83d\udc86\ud83c\udffe\u200d\u2640\ufe0f"));
- add(new Pair<>("\ud83d\udc86\ud83c\udfff", "\ud83d\udc86\ud83c\udfff\u200d\u2640\ufe0f"));
- add(new Pair<>("\ud83d\udc87", "\ud83d\udc87\u200d\u2640\ufe0f"));
- add(new Pair<>("\ud83d\udc87\ud83c\udffb", "\ud83d\udc87\ud83c\udffb\u200d\u2640\ufe0f"));
- add(new Pair<>("\ud83d\udc87\ud83c\udffc", "\ud83d\udc87\ud83c\udffc\u200d\u2640\ufe0f"));
- add(new Pair<>("\ud83d\udc87\ud83c\udffd", "\ud83d\udc87\ud83c\udffd\u200d\u2640\ufe0f"));
- add(new Pair<>("\ud83d\udc87\ud83c\udffe", "\ud83d\udc87\ud83c\udffe\u200d\u2640\ufe0f"));
- add(new Pair<>("\ud83d\udc87\ud83c\udfff", "\ud83d\udc87\ud83c\udfff\u200d\u2640\ufe0f"));
- add(new Pair<>("\ud83d\udeb6", "\ud83d\udeb6\u200d\u2642\ufe0f"));
- add(new Pair<>("\ud83d\udeb6\ud83c\udffb", "\ud83d\udeb6\ud83c\udffb\u200d\u2642\ufe0f"));
- add(new Pair<>("\ud83d\udeb6\ud83c\udffc", "\ud83d\udeb6\ud83c\udffc\u200d\u2642\ufe0f"));
- add(new Pair<>("\ud83d\udeb6\ud83c\udffd", "\ud83d\udeb6\ud83c\udffd\u200d\u2642\ufe0f"));
- add(new Pair<>("\ud83d\udeb6\ud83c\udffe", "\ud83d\udeb6\ud83c\udffe\u200d\u2642\ufe0f"));
- add(new Pair<>("\ud83d\udeb6\ud83c\udfff", "\ud83d\udeb6\ud83c\udfff\u200d\u2642\ufe0f"));
- add(new Pair<>("\ud83c\udfc3", "\ud83c\udfc3\u200d\u2642\ufe0f"));
- add(new Pair<>("\ud83c\udfc3\ud83c\udffb", "\ud83c\udfc3\ud83c\udffb\u200d\u2642\ufe0f"));
- add(new Pair<>("\ud83c\udfc3\ud83c\udffc", "\ud83c\udfc3\ud83c\udffc\u200d\u2642\ufe0f"));
- add(new Pair<>("\ud83c\udfc3\ud83c\udffd", "\ud83c\udfc3\ud83c\udffd\u200d\u2642\ufe0f"));
- add(new Pair<>("\ud83c\udfc3\ud83c\udffe", "\ud83c\udfc3\ud83c\udffe\u200d\u2642\ufe0f"));
- add(new Pair<>("\ud83c\udfc3\ud83c\udfff", "\ud83c\udfc3\ud83c\udfff\u200d\u2642\ufe0f"));
- add(new Pair<>("\ud83d\udc6f", "\ud83d\udc6f\u200d\u2640\ufe0f"));
- add(new Pair<>("\ud83e\uddd6", "\ud83e\uddd6\u200d\u2642\ufe0f"));
- add(new Pair<>("\ud83e\uddd6\ud83c\udffb", "\ud83e\uddd6\ud83c\udffb\u200d\u2642\ufe0f"));
- add(new Pair<>("\ud83e\uddd6\ud83c\udffc", "\ud83e\uddd6\ud83c\udffc\u200d\u2642\ufe0f"));
- add(new Pair<>("\ud83e\uddd6\ud83c\udffd", "\ud83e\uddd6\ud83c\udffd\u200d\u2642\ufe0f"));
- add(new Pair<>("\ud83e\uddd6\ud83c\udffe", "\ud83e\uddd6\ud83c\udffe\u200d\u2642\ufe0f"));
- add(new Pair<>("\ud83e\uddd6\ud83c\udfff", "\ud83e\uddd6\ud83c\udfff\u200d\u2642\ufe0f"));
- add(new Pair<>("\ud83e\uddd7", "\ud83e\uddd7\u200d\u2640\ufe0f"));
- add(new Pair<>("\ud83e\uddd7\ud83c\udffb", "\ud83e\uddd7\ud83c\udffb\u200d\u2640\ufe0f"));
- add(new Pair<>("\ud83e\uddd7\ud83c\udffc", "\ud83e\uddd7\ud83c\udffc\u200d\u2640\ufe0f"));
- add(new Pair<>("\ud83e\uddd7\ud83c\udffd", "\ud83e\uddd7\ud83c\udffd\u200d\u2640\ufe0f"));
- add(new Pair<>("\ud83e\uddd7\ud83c\udffe", "\ud83e\uddd7\ud83c\udffe\u200d\u2640\ufe0f"));
- add(new Pair<>("\ud83e\uddd7\ud83c\udfff", "\ud83e\uddd7\ud83c\udfff\u200d\u2640\ufe0f"));
- add(new Pair<>("\ud83e\uddd8", "\ud83e\uddd8\u200d\u2640\ufe0f"));
- add(new Pair<>("\ud83e\uddd8\ud83c\udffb", "\ud83e\uddd8\ud83c\udffb\u200d\u2640\ufe0f"));
- add(new Pair<>("\ud83e\uddd8\ud83c\udffc", "\ud83e\uddd8\ud83c\udffc\u200d\u2640\ufe0f"));
- add(new Pair<>("\ud83e\uddd8\ud83c\udffd", "\ud83e\uddd8\ud83c\udffd\u200d\u2640\ufe0f"));
- add(new Pair<>("\ud83e\uddd8\ud83c\udffe", "\ud83e\uddd8\ud83c\udffe\u200d\u2640\ufe0f"));
- add(new Pair<>("\ud83e\uddd8\ud83c\udfff", "\ud83e\uddd8\ud83c\udfff\u200d\u2640\ufe0f"));
- add(new Pair<>("\ud83c\udfcc\ufe0f", "\ud83c\udfcc\ufe0f\u200d\u2642\ufe0f"));
- add(new Pair<>("\ud83c\udfcc\ud83c\udffb", "\ud83c\udfcc\ud83c\udffb\u200d\u2642\ufe0f"));
- add(new Pair<>("\ud83c\udfcc\ud83c\udffc", "\ud83c\udfcc\ud83c\udffc\u200d\u2642\ufe0f"));
- add(new Pair<>("\ud83c\udfcc\ud83c\udffd", "\ud83c\udfcc\ud83c\udffd\u200d\u2642\ufe0f"));
- add(new Pair<>("\ud83c\udfcc\ud83c\udffe", "\ud83c\udfcc\ud83c\udffe\u200d\u2642\ufe0f"));
- add(new Pair<>("\ud83c\udfcc\ud83c\udfff", "\ud83c\udfcc\ud83c\udfff\u200d\u2642\ufe0f"));
- add(new Pair<>("\ud83c\udfc4", "\ud83c\udfc4\u200d\u2642\ufe0f"));
- add(new Pair<>("\ud83c\udfc4\ud83c\udffb", "\ud83c\udfc4\ud83c\udffb\u200d\u2642\ufe0f"));
- add(new Pair<>("\ud83c\udfc4\ud83c\udffc", "\ud83c\udfc4\ud83c\udffc\u200d\u2642\ufe0f"));
- add(new Pair<>("\ud83c\udfc4\ud83c\udffd", "\ud83c\udfc4\ud83c\udffd\u200d\u2642\ufe0f"));
- add(new Pair<>("\ud83c\udfc4\ud83c\udffe", "\ud83c\udfc4\ud83c\udffe\u200d\u2642\ufe0f"));
- add(new Pair<>("\ud83c\udfc4\ud83c\udfff", "\ud83c\udfc4\ud83c\udfff\u200d\u2642\ufe0f"));
- add(new Pair<>("\ud83d\udea3", "\ud83d\udea3\u200d\u2642\ufe0f"));
- add(new Pair<>("\ud83d\udea3\ud83c\udffb", "\ud83d\udea3\ud83c\udffb\u200d\u2642\ufe0f"));
- add(new Pair<>("\ud83d\udea3\ud83c\udffc", "\ud83d\udea3\ud83c\udffc\u200d\u2642\ufe0f"));
- add(new Pair<>("\ud83d\udea3\ud83c\udffd", "\ud83d\udea3\ud83c\udffd\u200d\u2642\ufe0f"));
- add(new Pair<>("\ud83d\udea3\ud83c\udffe", "\ud83d\udea3\ud83c\udffe\u200d\u2642\ufe0f"));
- add(new Pair<>("\ud83d\udea3\ud83c\udfff", "\ud83d\udea3\ud83c\udfff\u200d\u2642\ufe0f"));
- add(new Pair<>("\ud83c\udfca", "\ud83c\udfca\u200d\u2642\ufe0f"));
- add(new Pair<>("\ud83c\udfca\ud83c\udffb", "\ud83c\udfca\ud83c\udffb\u200d\u2642\ufe0f"));
- add(new Pair<>("\ud83c\udfca\ud83c\udffc", "\ud83c\udfca\ud83c\udffc\u200d\u2642\ufe0f"));
- add(new Pair<>("\ud83c\udfca\ud83c\udffd", "\ud83c\udfca\ud83c\udffd\u200d\u2642\ufe0f"));
- add(new Pair<>("\ud83c\udfca\ud83c\udffe", "\ud83c\udfca\ud83c\udffe\u200d\u2642\ufe0f"));
- add(new Pair<>("\ud83c\udfca\ud83c\udfff", "\ud83c\udfca\ud83c\udfff\u200d\u2642\ufe0f"));
- add(new Pair<>("\u26f9\ufe0f", "\u26f9\ufe0f\u200d\u2642\ufe0f"));
- add(new Pair<>("\u26f9\ud83c\udffb", "\u26f9\ud83c\udffb\u200d\u2642\ufe0f"));
- add(new Pair<>("\u26f9\ud83c\udffc", "\u26f9\ud83c\udffc\u200d\u2642\ufe0f"));
- add(new Pair<>("\u26f9\ud83c\udffd", "\u26f9\ud83c\udffd\u200d\u2642\ufe0f"));
- add(new Pair<>("\u26f9\ud83c\udffe", "\u26f9\ud83c\udffe\u200d\u2642\ufe0f"));
- add(new Pair<>("\u26f9\ud83c\udfff", "\u26f9\ud83c\udfff\u200d\u2642\ufe0f"));
- add(new Pair<>("\ud83c\udfcb\ufe0f", "\ud83c\udfcb\ufe0f\u200d\u2642\ufe0f"));
- add(new Pair<>("\ud83c\udfcb\ud83c\udffb", "\ud83c\udfcb\ud83c\udffb\u200d\u2642\ufe0f"));
- add(new Pair<>("\ud83c\udfcb\ud83c\udffc", "\ud83c\udfcb\ud83c\udffc\u200d\u2642\ufe0f"));
- add(new Pair<>("\ud83c\udfcb\ud83c\udffd", "\ud83c\udfcb\ud83c\udffd\u200d\u2642\ufe0f"));
- add(new Pair<>("\ud83c\udfcb\ud83c\udffe", "\ud83c\udfcb\ud83c\udffe\u200d\u2642\ufe0f"));
- add(new Pair<>("\ud83c\udfcb\ud83c\udfff", "\ud83c\udfcb\ud83c\udfff\u200d\u2642\ufe0f"));
- add(new Pair<>("\ud83d\udeb4", "\ud83d\udeb4\u200d\u2642\ufe0f"));
- add(new Pair<>("\ud83d\udeb4\ud83c\udffb", "\ud83d\udeb4\ud83c\udffb\u200d\u2642\ufe0f"));
- add(new Pair<>("\ud83d\udeb4\ud83c\udffc", "\ud83d\udeb4\ud83c\udffc\u200d\u2642\ufe0f"));
- add(new Pair<>("\ud83d\udeb4\ud83c\udffd", "\ud83d\udeb4\ud83c\udffd\u200d\u2642\ufe0f"));
- add(new Pair<>("\ud83d\udeb4\ud83c\udffe", "\ud83d\udeb4\ud83c\udffe\u200d\u2642\ufe0f"));
- add(new Pair<>("\ud83d\udeb4\ud83c\udfff", "\ud83d\udeb4\ud83c\udfff\u200d\u2642\ufe0f"));
- add(new Pair<>("\ud83d\udeb5", "\ud83d\udeb5\u200d\u2642\ufe0f"));
- add(new Pair<>("\ud83d\udeb5\ud83c\udffb", "\ud83d\udeb5\ud83c\udffb\u200d\u2642\ufe0f"));
- add(new Pair<>("\ud83d\udeb5\ud83c\udffc", "\ud83d\udeb5\ud83c\udffc\u200d\u2642\ufe0f"));
- add(new Pair<>("\ud83d\udeb5\ud83c\udffd", "\ud83d\udeb5\ud83c\udffd\u200d\u2642\ufe0f"));
- add(new Pair<>("\ud83d\udeb5\ud83c\udffe", "\ud83d\udeb5\ud83c\udffe\u200d\u2642\ufe0f"));
- add(new Pair<>("\ud83d\udeb5\ud83c\udfff", "\ud83d\udeb5\ud83c\udfff\u200d\u2642\ufe0f"));
- add(new Pair<>("\ud83d\udc8f", "\ud83d\udc69\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83d\udc68"));
- add(new Pair<>("\ud83d\udc91", "\ud83d\udc69\u200d\u2764\ufe0f\u200d\ud83d\udc68"));
- add(new Pair<>("\ud83d\udc6a", "\ud83d\udc68\u200d\ud83d\udc69\u200d\ud83d\udc66"));
- }};
}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/contacts/ContactAccessor.java b/app/src/main/java/org/thoughtcrime/securesms/contacts/ContactAccessor.java
index d560247fb9..01317bc9b9 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/contacts/ContactAccessor.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/contacts/ContactAccessor.java
@@ -73,7 +73,7 @@ public class ContactAccessor {
}
}
-// if (context.getString(R.string.note_to_self).toLowerCase().contains(constraint.toLowerCase()) &&
+// if (context.getString(R.string.noteToSelf).toLowerCase().contains(constraint.toLowerCase()) &&
// !numberList.contains(TextSecurePreferences.getLocalNumber(context)))
// {
// numberList.add(TextSecurePreferences.getLocalNumber(context));
diff --git a/app/src/main/java/org/thoughtcrime/securesms/contacts/ContactSelectionListLoader.kt b/app/src/main/java/org/thoughtcrime/securesms/contacts/ContactSelectionListLoader.kt
index 90e0ce50f2..0db2ec8962 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/contacts/ContactSelectionListLoader.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/contacts/ContactSelectionListLoader.kt
@@ -36,7 +36,7 @@ class ContactSelectionListLoader(context: Context, val mode: Int, val filter: St
list.addAll(getClosedGroups(contacts))
}
if (isFlagSet(DisplayMode.FLAG_OPEN_GROUPS)) {
- list.addAll(getOpenGroups(contacts))
+ list.addAll(getCommunities(contacts))
}
if (isFlagSet(DisplayMode.FLAG_CONTACTS)) {
list.addAll(getContacts(contacts))
@@ -45,19 +45,19 @@ class ContactSelectionListLoader(context: Context, val mode: Int, val filter: St
}
private fun getContacts(contacts: List): List {
- return getItems(contacts, context.getString(R.string.fragment_contact_selection_contacts_title)) {
+ return getItems(contacts, context.getString(R.string.contactContacts)) {
!it.isGroupRecipient
}
}
private fun getClosedGroups(contacts: List): List {
- return getItems(contacts, context.getString(R.string.fragment_contact_selection_closed_groups_title)) {
+ return getItems(contacts, context.getString(R.string.conversationsGroups)) {
it.address.isClosedGroup
}
}
- private fun getOpenGroups(contacts: List): List {
- return getItems(contacts, context.getString(R.string.fragment_contact_selection_open_groups_title)) {
+ private fun getCommunities(contacts: List): List {
+ return getItems(contacts, context.getString(R.string.conversationsCommunities)) {
it.address.isCommunity
}
}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/contacts/ContactUtil.java b/app/src/main/java/org/thoughtcrime/securesms/contacts/ContactUtil.java
index 5284fb0015..046c20002d 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/contacts/ContactUtil.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/contacts/ContactUtil.java
@@ -18,10 +18,10 @@ public final class ContactUtil {
String contactName = ContactUtil.getDisplayName(contact);
if (!TextUtils.isEmpty(contactName)) {
- return context.getString(R.string.MessageNotifier_contact_message, EmojiStrings.BUST_IN_SILHOUETTE, contactName);
+ return EmojiStrings.BUST_IN_SILHOUETTE + " " + contactName;
}
- return SpanUtil.italic(context.getString(R.string.MessageNotifier_unknown_contact_message));
+ return SpanUtil.italic(context.getString(R.string.unknown));
}
private static @NonNull String getDisplayName(@Nullable Contact contact) {
diff --git a/app/src/main/java/org/thoughtcrime/securesms/contacts/ContactsCursorLoader.java b/app/src/main/java/org/thoughtcrime/securesms/contacts/ContactsCursorLoader.java
index e1ea0c5e2e..83084d2673 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/contacts/ContactsCursorLoader.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/contacts/ContactsCursorLoader.java
@@ -159,7 +159,7 @@ public class ContactsCursorLoader extends CursorLoader {
private Cursor getGroupsHeaderCursor() {
MatrixCursor groupHeader = new MatrixCursor(CONTACT_PROJECTION, 1);
- groupHeader.addRow(new Object[]{ getContext().getString(R.string.ContactsCursorLoader_groups),
+ groupHeader.addRow(new Object[]{ getContext().getString(R.string.conversationsGroups),
"",
ContactsContract.CommonDataKinds.Phone.TYPE_MOBILE,
"",
@@ -221,16 +221,6 @@ public class ContactsCursorLoader extends CursorLoader {
return groupContacts;
}
- private Cursor getNewNumberCursor() {
- MatrixCursor newNumberCursor = new MatrixCursor(CONTACT_PROJECTION, 1);
- newNumberCursor.addRow(new Object[] { getContext().getString(R.string.contact_selection_list__unknown_contact),
- filter,
- ContactsContract.CommonDataKinds.Phone.TYPE_CUSTOM,
- "\u21e2",
- NEW_TYPE });
- return newNumberCursor;
- }
-
private static boolean isCursorListEmpty(List list) {
int sum = 0;
for (Cursor cursor : list) {
diff --git a/app/src/main/java/org/thoughtcrime/securesms/contacts/SelectContactsActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/contacts/SelectContactsActivity.kt
index 1160ed92ab..a3fd6ac1dd 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/contacts/SelectContactsActivity.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/contacts/SelectContactsActivity.kt
@@ -35,7 +35,7 @@ class SelectContactsActivity : PassphraseRequiredActionBarActivity(), LoaderMana
super.onCreate(savedInstanceState, isReady)
binding = ActivitySelectContactsBinding.inflate(layoutInflater)
setContentView(binding.root)
- supportActionBar!!.title = resources.getString(R.string.activity_select_contacts_title)
+ supportActionBar!!.title = resources.getString(R.string.membersInvite)
usersToExclude = intent.getStringArrayExtra(usersToExcludeKey)?.toSet() ?: setOf()
val emptyStateText = intent.getStringExtra(emptyStateTextKey)
diff --git a/app/src/main/java/org/thoughtcrime/securesms/contacts/UserView.kt b/app/src/main/java/org/thoughtcrime/securesms/contacts/UserView.kt
index e0ca2a4242..77f579e14d 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/contacts/UserView.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/contacts/UserView.kt
@@ -14,7 +14,6 @@ import com.bumptech.glide.RequestManager
class UserView : LinearLayout {
private lateinit var binding: ViewUserBinding
- var openGroupThreadID: Long = -1 // FIXME: This is a bit ugly
enum class ActionIndicator {
None,
@@ -47,11 +46,13 @@ class UserView : LinearLayout {
// region Updating
fun bind(user: Recipient, glide: RequestManager, actionIndicator: ActionIndicator, isSelected: Boolean = false) {
val isLocalUser = user.isLocalNumber
+
fun getUserDisplayName(publicKey: String): String {
- if (isLocalUser) return context.getString(R.string.MessageRecord_you)
+ if (isLocalUser) return context.getString(R.string.you)
val contact = DatabaseComponent.get(context).sessionContactDatabase().getContactWithAccountID(publicKey)
return contact?.displayName(Contact.ContactContext.REGULAR) ?: publicKey
}
+
val address = user.address.serialize()
binding.profilePictureView.update(user)
binding.actionIndicatorImageView.setImageResource(R.drawable.ic_baseline_edit_24)
@@ -84,8 +85,6 @@ class UserView : LinearLayout {
}
}
- fun unbind() {
- binding.profilePictureView.recycle()
- }
+ fun unbind() { binding.profilePictureView.recycle() }
// endregion
}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationActionBarView.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationActionBarView.kt
index 8f2da7a733..2a88aac463 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationActionBarView.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationActionBarView.kt
@@ -15,16 +15,16 @@ import dagger.hilt.android.AndroidEntryPoint
import network.loki.messenger.R
import network.loki.messenger.databinding.ViewConversationActionBarBinding
import network.loki.messenger.databinding.ViewConversationSettingBinding
-import network.loki.messenger.libsession_util.util.ExpiryMode
+import network.loki.messenger.libsession_util.util.ExpiryMode.AfterRead
import org.session.libsession.messaging.messages.ExpirationConfiguration
import org.session.libsession.messaging.open_groups.OpenGroup
import org.session.libsession.utilities.ExpirationUtil
+import org.session.libsession.utilities.StringSubstitutionConstants.TIME_KEY
import org.session.libsession.utilities.modifyLayoutParams
import org.session.libsession.utilities.recipients.Recipient
import org.thoughtcrime.securesms.database.GroupDatabase
import org.thoughtcrime.securesms.database.LokiAPIDatabase
-import org.thoughtcrime.securesms.util.DateUtils
-import java.util.Locale
+import org.thoughtcrime.securesms.ui.getSubbedString
import javax.inject.Inject
@AndroidEntryPoint
@@ -82,7 +82,7 @@ class ConversationActionBarView @JvmOverloads constructor(
fun update(recipient: Recipient, openGroup: OpenGroup? = null, config: ExpirationConfiguration? = null) {
binding.profilePictureView.update(recipient)
- binding.conversationTitleView.text = recipient.takeUnless { it.isLocalNumber }?.toShortString() ?: context.getString(R.string.note_to_self)
+ binding.conversationTitleView.text = recipient.takeUnless { it.isLocalNumber }?.toShortString() ?: context.getString(R.string.noteToSelf)
updateSubtitle(recipient, openGroup, config)
binding.conversationTitleContainer.modifyLayoutParams {
@@ -92,37 +92,53 @@ class ConversationActionBarView @JvmOverloads constructor(
fun updateSubtitle(recipient: Recipient, openGroup: OpenGroup? = null, config: ExpirationConfiguration? = null) {
val settings = mutableListOf()
+
+ // Specify the disappearing messages subtitle if we should
if (config?.isEnabled == true) {
- val prefix = when (config.expiryMode) {
- is ExpiryMode.AfterRead -> R.string.expiration_type_disappear_after_read
- else -> R.string.expiration_type_disappear_after_send
- }.let(context::getString)
+ // Get the type of disappearing message and the abbreviated duration..
+ val dmTypeString = when (config.expiryMode) {
+ is AfterRead -> R.string.disappearingMessagesDisappearAfterReadState
+ else -> R.string.disappearingMessagesDisappearAfterSendState
+ }
+ val durationAbbreviated = ExpirationUtil.getExpirationAbbreviatedDisplayValue(config.expiryMode.expirySeconds)
+
+ // ..then substitute into the string..
+ val subtitleTxt = context.getSubbedString(dmTypeString,
+ TIME_KEY to durationAbbreviated
+ )
+
+ // .. and apply to the subtitle.
settings += ConversationSetting(
- "$prefix - ${ExpirationUtil.getExpirationAbbreviatedDisplayValue(context, config.expiryMode.expirySeconds)}",
+ subtitleTxt,
ConversationSettingType.EXPIRATION,
R.drawable.ic_timer,
- resources.getString(R.string.AccessibilityId_disappearing_messages_type_and_time)
+ resources.getString(R.string.AccessibilityId_disappearingMessagesDisappear)
)
}
+
if (recipient.isMuted) {
settings += ConversationSetting(
recipient.mutedUntil.takeUnless { it == Long.MAX_VALUE }
- ?.let { context.getString(R.string.ConversationActivity_muted_until_date, DateUtils.getFormattedDateTime(it, "EEE, MMM d, yyyy HH:mm", Locale.getDefault())) }
- ?: context.getString(R.string.ConversationActivity_muted_forever),
+ ?.let {
+ context.getString(R.string.notificationsMuted)
+ }
+ ?: context.getString(R.string.notificationsMuted),
ConversationSettingType.NOTIFICATION,
R.drawable.ic_outline_notifications_off_24
)
}
+
if (recipient.isGroupRecipient) {
val title = if (recipient.isCommunityRecipient) {
val userCount = openGroup?.let { lokiApiDb.getUserCount(it.room, it.server) } ?: 0
- context.getString(R.string.ConversationActivity_active_member_count, userCount)
+ resources.getQuantityString(R.plurals.membersActive, userCount, userCount)
} else {
val userCount = groupDb.getGroupMemberAddresses(recipient.address.toGroupString(), true).size
- context.getString(R.string.ConversationActivity_member_count, userCount)
+ resources.getQuantityString(R.plurals.members, userCount, userCount)
}
settings += ConversationSetting(title, ConversationSettingType.MEMBER_COUNT)
}
+
settingsAdapter.submitList(settings)
binding.settingsTabLayout.isVisible = settings.size > 1
}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/disappearingmessages/DisappearingMessages.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/disappearingmessages/DisappearingMessages.kt
index 38da11ae24..e086c95924 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/conversation/disappearingmessages/DisappearingMessages.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/disappearingmessages/DisappearingMessages.kt
@@ -12,10 +12,14 @@ import org.session.libsession.snode.SnodeAPI
import org.session.libsession.utilities.Address
import org.session.libsession.utilities.ExpirationUtil
import org.session.libsession.utilities.SSKEnvironment.MessageExpirationManagerProtocol
+import org.session.libsession.utilities.StringSubstitutionConstants.DISAPPEARING_MESSAGES_TYPE_KEY
+import org.session.libsession.utilities.StringSubstitutionConstants.TIME_KEY
import org.session.libsession.utilities.TextSecurePreferences
import org.session.libsession.utilities.getExpirationTypeDisplayValue
import org.thoughtcrime.securesms.database.model.MessageRecord
import org.thoughtcrime.securesms.showSessionDialog
+import org.thoughtcrime.securesms.ui.getSubbedCharSequence
+import org.thoughtcrime.securesms.ui.getSubbedString
import org.thoughtcrime.securesms.util.ConfigurationMessageUtilities
import javax.inject.Inject
import kotlin.time.Duration.Companion.milliseconds
@@ -43,22 +47,18 @@ class DisappearingMessages @Inject constructor(
}
fun showFollowSettingDialog(context: Context, message: MessageRecord) = context.showSessionDialog {
- title(R.string.dialog_disappearing_messages_follow_setting_title)
+ title(R.string.disappearingMessagesFollowSetting)
text(if (message.expiresIn == 0L) {
- context.getString(R.string.dialog_disappearing_messages_follow_setting_off_body)
+ context.getText(R.string.disappearingMessagesFollowSettingOff)
} else {
- context.getString(
- R.string.dialog_disappearing_messages_follow_setting_on_body,
- ExpirationUtil.getExpirationDisplayValue(
- context,
- message.expiresIn.milliseconds
- ),
- context.getExpirationTypeDisplayValue(message.isNotDisappearAfterRead)
- )
+ context.getSubbedCharSequence(R.string.disappearingMessagesFollowSettingOn,
+ TIME_KEY to ExpirationUtil.getExpirationDisplayValue(context, message.expiresIn.milliseconds),
+ DISAPPEARING_MESSAGES_TYPE_KEY to context.getExpirationTypeDisplayValue(message.isNotDisappearAfterRead))
})
+
dangerButton(
- text = if (message.expiresIn == 0L) R.string.dialog_disappearing_messages_follow_setting_confirm else R.string.dialog_disappearing_messages_follow_setting_set,
- contentDescription = if (message.expiresIn == 0L) R.string.AccessibilityId_confirm else R.string.AccessibilityId_set_button
+ text = if (message.expiresIn == 0L) R.string.confirm else R.string.set,
+ contentDescription = if (message.expiresIn == 0L) R.string.AccessibilityId_confirm else R.string.AccessibilityId_setButton
) {
set(message.threadId, message.recipient.address, message.expiryMode, message.recipient.isClosedGroupRecipient)
}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/disappearingmessages/DisappearingMessagesActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/disappearingmessages/DisappearingMessagesActivity.kt
index f66512d79f..2716a3d883 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/conversation/disappearingmessages/DisappearingMessagesActivity.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/disappearingmessages/DisappearingMessagesActivity.kt
@@ -52,7 +52,7 @@ class DisappearingMessagesActivity: PassphraseRequiredActionBarActivity() {
viewModel.event.collect {
when (it) {
Event.SUCCESS -> finish()
- Event.FAIL -> showToast(getString(R.string.DisappearingMessagesActivity_settings_not_updated))
+ Event.FAIL -> showToast(getString(R.string.communityErrorDescription))
}
}
}
@@ -72,9 +72,9 @@ class DisappearingMessagesActivity: PassphraseRequiredActionBarActivity() {
}
private fun setUpToolbar() {
- setSupportActionBar(binding.toolbar)
+ setSupportActionBar(binding.searchToolbar)
supportActionBar?.apply {
- title = getString(R.string.activity_disappearing_messages_title)
+ title = getString(R.string.disappearingMessages)
setDisplayHomeAsUpEnabled(true)
setHomeButtonEnabled(true)
}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/disappearingmessages/DisappearingMessagesViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/disappearingmessages/DisappearingMessagesViewModel.kt
index 32e20b73d9..915ff66971 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/conversation/disappearingmessages/DisappearingMessagesViewModel.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/disappearingmessages/DisappearingMessagesViewModel.kt
@@ -58,7 +58,7 @@ class DisappearingMessagesViewModel(
init {
viewModelScope.launch {
- val expiryMode = storage.getExpirationConfiguration(threadId)?.expiryMode?.maybeConvertToLegacy(isNewConfigEnabled) ?: ExpiryMode.NONE
+ val expiryMode = storage.getExpirationConfiguration(threadId)?.expiryMode ?: ExpiryMode.NONE
val recipient = threadDb.getRecipientForThreadId(threadId)
val groupRecord = recipient?.takeIf { it.isClosedGroupRecipient }
?.run { groupDb.getGroup(address.toGroupString()).orNull() }
@@ -80,7 +80,7 @@ class DisappearingMessagesViewModel(
override fun onSetClick() = viewModelScope.launch {
val state = _state.value
- val mode = state.expiryMode?.coerceLegacyToAfterSend()
+ val mode = state.expiryMode
val address = state.address
if (address == null || mode == null) {
_event.send(Event.FAIL)
@@ -92,8 +92,6 @@ class DisappearingMessagesViewModel(
_event.send(Event.SUCCESS)
}
- private fun ExpiryMode.coerceLegacyToAfterSend() = takeUnless { it is ExpiryMode.Legacy } ?: ExpiryMode.AfterSend(expirySeconds)
-
@dagger.assisted.AssistedFactory
interface AssistedFactory {
fun create(threadId: Long): Factory
@@ -125,5 +123,3 @@ class DisappearingMessagesViewModel(
) as T
}
}
-
-private fun ExpiryMode.maybeConvertToLegacy(isNewConfigEnabled: Boolean): ExpiryMode = takeIf { isNewConfigEnabled } ?: ExpiryMode.Legacy(expirySeconds)
diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/disappearingmessages/State.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/disappearingmessages/State.kt
index ced4cc0035..eb4114ab54 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/conversation/disappearingmessages/State.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/disappearingmessages/State.kt
@@ -24,22 +24,21 @@ data class State(
val showDebugOptions: Boolean = false
) {
val subtitle get() = when {
- isGroup || isNoteToSelf -> GetString(R.string.activity_disappearing_messages_subtitle_sent)
- else -> GetString(R.string.activity_disappearing_messages_subtitle)
+ isGroup || isNoteToSelf -> GetString(R.string.disappearingMessagesDisappearAfterSendDescription)
+ else -> GetString(R.string.disappearingMessagesDescription1)
}
val typeOptionsHidden get() = isNoteToSelf || (isGroup && isNewConfigEnabled)
val nextType get() = when {
expiryType == ExpiryType.AFTER_READ -> ExpiryType.AFTER_READ
- isNewConfigEnabled -> ExpiryType.AFTER_SEND
- else -> ExpiryType.LEGACY
+ else -> ExpiryType.AFTER_SEND
}
val duration get() = expiryMode?.duration
val expiryType get() = expiryMode?.type
- val isTimeOptionsEnabled = isNoteToSelf || isSelfAdmin && (isNewConfigEnabled || expiryType == ExpiryType.LEGACY)
+ val isTimeOptionsEnabled = isNoteToSelf || isSelfAdmin && isNewConfigEnabled
}
@@ -51,25 +50,20 @@ enum class ExpiryType(
) {
NONE(
{ ExpiryMode.NONE },
- R.string.expiration_off,
- contentDescription = R.string.AccessibilityId_disable_disappearing_messages,
- ),
- LEGACY(
- ExpiryMode::Legacy,
- R.string.expiration_type_disappear_legacy,
- contentDescription = R.string.expiration_type_disappear_legacy_description
+ R.string.off,
+ contentDescription = R.string.AccessibilityId_disappearingMessagesOff,
),
AFTER_READ(
ExpiryMode::AfterRead,
- R.string.expiration_type_disappear_after_read,
- R.string.expiration_type_disappear_after_read_description,
- R.string.AccessibilityId_disappear_after_read_option
+ R.string.disappearingMessagesDisappearAfterRead,
+ R.string.disappearingMessagesDisappearAfterReadDescription,
+ R.string.AccessibilityId_disappearingMessagesDisappearAfterRead
),
AFTER_SEND(
ExpiryMode::AfterSend,
- R.string.expiration_type_disappear_after_send,
- R.string.expiration_type_disappear_after_send_description,
- R.string.AccessibilityId_disappear_after_send_option
+ R.string.disappearingMessagesDisappearAfterSend,
+ R.string.disappearingMessagesDisappearAfterSendDescription,
+ R.string.AccessibilityId_disappearingMessagesDisappearAfterSent
);
fun mode(seconds: Long) = if (seconds != 0L) createMode(seconds) else ExpiryMode.NONE
@@ -83,7 +77,6 @@ enum class ExpiryType(
}
val ExpiryMode.type: ExpiryType get() = when(this) {
- is ExpiryMode.Legacy -> ExpiryType.LEGACY
is ExpiryMode.AfterSend -> ExpiryType.AFTER_SEND
is ExpiryMode.AfterRead -> ExpiryType.AFTER_READ
else -> ExpiryType.NONE
diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/disappearingmessages/ui/Adapter.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/disappearingmessages/ui/Adapter.kt
index d78d33a2f9..d4b3b0602a 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/conversation/disappearingmessages/ui/Adapter.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/disappearingmessages/ui/Adapter.kt
@@ -13,8 +13,8 @@ import kotlin.time.Duration.Companion.seconds
fun State.toUiState() = UiState(
cards = listOfNotNull(
- typeOptions()?.let { ExpiryOptionsCardData(GetString(R.string.activity_disappearing_messages_delete_type), it) },
- timeOptions()?.let { ExpiryOptionsCardData(GetString(R.string.activity_disappearing_messages_timer), it) }
+ typeOptions()?.let { ExpiryOptionsCardData(GetString(R.string.disappearingMessagesDeleteType), it) },
+ timeOptions()?.let { ExpiryOptionsCardData(GetString(R.string.disappearingMessagesTimer), it) }
),
showGroupFooter = isGroup && isNewConfigEnabled,
showSetButton = isSelfAdmin
@@ -23,7 +23,6 @@ fun State.toUiState() = UiState(
private fun State.typeOptions(): List? = if (typeOptionsHidden) null else {
buildList {
add(offTypeOption())
- if (!isNewConfigEnabled) add(legacyTypeOption())
if (!isGroup) add(afterReadTypeOption())
add(afterSendTypeOption())
}
@@ -48,7 +47,6 @@ private fun State.timeOptions(): List? {
}
private fun State.offTypeOption() = typeOption(ExpiryType.NONE)
-private fun State.legacyTypeOption() = typeOption(ExpiryType.LEGACY)
private fun State.afterReadTypeOption() = newTypeOption(ExpiryType.AFTER_READ)
private fun State.afterSendTypeOption() = newTypeOption(ExpiryType.AFTER_SEND)
private fun State.newTypeOption(type: ExpiryType) = typeOption(type, isNewConfigEnabled && isSelfAdmin)
@@ -66,11 +64,14 @@ private fun State.typeOption(
)
private fun debugTimes(isDebug: Boolean) = if (isDebug) listOf(10.seconds, 30.seconds, 1.minutes) else emptyList()
+
private fun debugModes(isDebug: Boolean, type: ExpiryType) =
debugTimes(isDebug).map { type.mode(it.inWholeSeconds) }
+
private fun State.debugOptions(): List =
debugModes(showDebugOptions, nextType).map { timeOption(it, subtitle = GetString("for testing purposes")) }
+// Standard list of available disappearing message times
private val afterSendTimes = listOf(12.hours, 1.days, 7.days, 14.days)
private val afterReadTimes = buildList {
diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/disappearingmessages/ui/DisappearingMessages.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/disappearingmessages/ui/DisappearingMessages.kt
index ae17e6a09b..b066a96cc7 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/conversation/disappearingmessages/ui/DisappearingMessages.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/disappearingmessages/ui/DisappearingMessages.kt
@@ -1,6 +1,5 @@
package org.thoughtcrime.securesms.conversation.disappearingmessages.ui
-import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
@@ -16,21 +15,21 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
-import androidx.compose.ui.unit.dp
import network.loki.messenger.R
import network.loki.messenger.libsession_util.util.ExpiryMode
import org.thoughtcrime.securesms.ui.Callbacks
-import org.thoughtcrime.securesms.ui.theme.LocalDimensions
import org.thoughtcrime.securesms.ui.NoOpCallbacks
import org.thoughtcrime.securesms.ui.OptionsCard
import org.thoughtcrime.securesms.ui.RadioOption
-import org.thoughtcrime.securesms.ui.theme.LocalColors
+import org.thoughtcrime.securesms.ui.components.PrimaryOutlineButton
import org.thoughtcrime.securesms.ui.components.SlimOutlineButton
import org.thoughtcrime.securesms.ui.contentDescription
import org.thoughtcrime.securesms.ui.fadingEdges
+import org.thoughtcrime.securesms.ui.theme.LocalColors
+import org.thoughtcrime.securesms.ui.theme.LocalDimensions
import org.thoughtcrime.securesms.ui.theme.LocalType
-typealias ExpiryCallbacks = Callbacks
+typealias ExpiryCallbacks = Callbacks
typealias ExpiryRadioOption = RadioOption
@Composable
@@ -59,7 +58,9 @@ fun DisappearingMessages(
}
if (state.showGroupFooter) Text(
- text = stringResource(R.string.activity_disappearing_messages_group_footer),
+ text = stringResource(R.string.disappearingMessagesDescription) +
+ "\n" +
+ stringResource(R.string.disappearingMessagesOnlyAdmins),
style = LocalType.current.extraSmall,
fontWeight = FontWeight(400),
color = LocalColors.current.textSecondary,
@@ -71,13 +72,15 @@ fun DisappearingMessages(
}
}
- if (state.showSetButton) SlimOutlineButton(
- stringResource(R.string.disappearing_messages_set_button_title),
- modifier = Modifier
- .contentDescription(R.string.AccessibilityId_set_button)
- .align(Alignment.CenterHorizontally)
- .padding(bottom = LocalDimensions.current.spacing),
- onClick = callbacks::onSetClick
- )
+ if (state.showSetButton){
+ PrimaryOutlineButton(
+ stringResource(R.string.set),
+ modifier = Modifier
+ .contentDescription(R.string.AccessibilityId_setButton)
+ .align(Alignment.CenterHorizontally)
+ .padding(bottom = LocalDimensions.current.spacing),
+ onClick = callbacks::onSetClick
+ )
+ }
}
}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/disappearingmessages/ui/DisappearingMessagesPreview.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/disappearingmessages/ui/DisappearingMessagesPreview.kt
index d043cc314f..48d6539d8a 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/conversation/disappearingmessages/ui/DisappearingMessagesPreview.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/disappearingmessages/ui/DisappearingMessagesPreview.kt
@@ -27,21 +27,18 @@ fun PreviewStates(
}
class StatePreviewParameterProvider : PreviewParameterProvider {
- override val values = newConfigValues.filter { it.expiryType != ExpiryType.LEGACY } + newConfigValues.map { it.copy(isNewConfigEnabled = false) }
+ override val values = newConfigValues + newConfigValues.map { it.copy(isNewConfigEnabled = false) }
private val newConfigValues get() = sequenceOf(
// new 1-1
State(expiryMode = ExpiryMode.NONE),
- State(expiryMode = ExpiryMode.Legacy(43200)),
State(expiryMode = ExpiryMode.AfterRead(300)),
State(expiryMode = ExpiryMode.AfterSend(43200)),
// new group non-admin
State(isGroup = true, isSelfAdmin = false),
- State(isGroup = true, isSelfAdmin = false, expiryMode = ExpiryMode.Legacy(43200)),
State(isGroup = true, isSelfAdmin = false, expiryMode = ExpiryMode.AfterSend(43200)),
// new group admin
State(isGroup = true),
- State(isGroup = true, expiryMode = ExpiryMode.Legacy(43200)),
State(isGroup = true, expiryMode = ExpiryMode.AfterSend(43200)),
// new note-to-self
State(isNoteToSelf = true),
diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/start/home/StartConversation.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/start/home/StartConversation.kt
index 15ca216777..f00fbf44a9 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/conversation/start/home/StartConversation.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/start/home/StartConversation.kt
@@ -15,6 +15,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.input.nestedscroll.nestedScroll
+import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.rememberNestedScrollInteropConnection
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
@@ -41,12 +42,14 @@ internal fun StartConversationScreen(
accountId: String,
delegate: StartConversationDelegate
) {
+ val context = LocalContext.current
+
Column(modifier = Modifier.background(
LocalColors.current.backgroundSecondary,
shape = MaterialTheme.shapes.small
)) {
BasicAppBar(
- title = stringResource(R.string.dialog_start_conversation_title),
+ title = stringResource(R.string.conversationsStart),
backgroundColor = Color.Transparent, // transparent to show the rounded shape of the container
actions = { AppBarCloseIcon(onClose = delegate::onDialogClosePressed) }
)
@@ -57,30 +60,31 @@ internal fun StartConversationScreen(
Column(
modifier = Modifier.verticalScroll(rememberScrollState())
) {
+ val newMessageTitleTxt:String = context.resources.getQuantityString(R.plurals.messageNew, 1, 1)
ItemButton(
- textId = R.string.messageNew,
+ text = newMessageTitleTxt,
icon = R.drawable.ic_message,
- modifier = Modifier.contentDescription(R.string.AccessibilityId_new_direct_message),
+ modifier = Modifier.contentDescription(R.string.AccessibilityId_messageNew),
onClick = delegate::onNewMessageSelected)
Divider(startIndent = LocalDimensions.current.dividerIndent)
ItemButton(
- textId = R.string.activity_create_group_title,
+ textId = R.string.groupCreate,
icon = R.drawable.ic_group,
- modifier = Modifier.contentDescription(R.string.AccessibilityId_create_group),
+ modifier = Modifier.contentDescription(R.string.AccessibilityId_groupCreate),
onClick = delegate::onCreateGroupSelected
)
Divider(startIndent = LocalDimensions.current.dividerIndent)
ItemButton(
- textId = R.string.dialog_join_community_title,
+ textId = R.string.communityJoin,
icon = R.drawable.ic_globe,
- modifier = Modifier.contentDescription(R.string.AccessibilityId_join_community),
+ modifier = Modifier.contentDescription(R.string.AccessibilityId_communityJoin),
onClick = delegate::onJoinCommunitySelected
)
Divider(startIndent = LocalDimensions.current.dividerIndent)
ItemButton(
- textId = R.string.activity_settings_invite_button_title,
+ textId = R.string.sessionInviteAFriend,
icon = R.drawable.ic_invite_friend,
- Modifier.contentDescription(R.string.AccessibilityId_invite_friend_button),
+ Modifier.contentDescription(R.string.AccessibilityId_sessionInviteAFriendButton),
onClick = delegate::onInviteFriend
)
Column(
@@ -99,7 +103,7 @@ internal fun StartConversationScreen(
Spacer(modifier = Modifier.height(LocalDimensions.current.smallSpacing))
QrImage(
string = accountId,
- Modifier.contentDescription(R.string.AccessibilityId_qr_code),
+ Modifier.contentDescription(R.string.AccessibilityId_qrCode),
icon = R.drawable.session
)
}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/start/invitefriend/InviteFriend.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/start/invitefriend/InviteFriend.kt
index 3453fb5722..bc298c5bd3 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/conversation/start/invitefriend/InviteFriend.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/start/invitefriend/InviteFriend.kt
@@ -14,10 +14,13 @@ import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
+import com.squareup.phrase.Phrase
import network.loki.messenger.R
+import org.session.libsession.utilities.StringSubstitutionConstants.APP_NAME_KEY
import org.thoughtcrime.securesms.ui.components.AppBarCloseIcon
import org.thoughtcrime.securesms.ui.components.BackAppBar
import org.thoughtcrime.securesms.ui.components.SlimOutlineButton
@@ -43,7 +46,7 @@ internal fun InviteFriend(
shape = MaterialTheme.shapes.small
)) {
BackAppBar(
- title = stringResource(R.string.invite_a_friend),
+ title = stringResource(R.string.sessionInviteAFriend),
backgroundColor = Color.Transparent, // transparent to show the rounded shape of the container
onBack = onBack,
actions = { AppBarCloseIcon(onClose = onClose) }
@@ -55,7 +58,7 @@ internal fun InviteFriend(
Text(
accountId,
modifier = Modifier
- .contentDescription(R.string.AccessibilityId_account_id)
+ .contentDescription(R.string.AccessibilityId_shareAccountId)
.fillMaxWidth()
.border()
.padding(LocalDimensions.current.spacing),
@@ -66,7 +69,10 @@ internal fun InviteFriend(
Spacer(modifier = Modifier.height(LocalDimensions.current.xsSpacing))
Text(
- stringResource(R.string.invite_your_friend_to_chat_with_you_on_session_by_sharing_your_account_id_with_them),
+ stringResource(R.string.shareAccountIdDescription).let { txt ->
+ val c = LocalContext.current
+ Phrase.from(txt).put(APP_NAME_KEY, c.getString(R.string.app_name)).format().toString()
+ },
textAlign = TextAlign.Center,
style = LocalType.current.small,
color = LocalColors.current.textSecondary,
diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/start/newmessage/NewMessage.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/start/newmessage/NewMessage.kt
index df54f9cae8..0a40c6ee39 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/conversation/start/newmessage/NewMessage.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/start/newmessage/NewMessage.kt
@@ -32,6 +32,7 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.input.nestedscroll.nestedScroll
+import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalView
import androidx.compose.ui.platform.rememberNestedScrollInteropConnection
@@ -61,7 +62,7 @@ import org.thoughtcrime.securesms.ui.theme.SessionColorsParameterProvider
import org.thoughtcrime.securesms.ui.theme.ThemeColors
import kotlin.math.max
-private val TITLES = listOf(R.string.enter_account_id, R.string.qrScan)
+private val TITLES = listOf(R.string.accountIdEnter, R.string.qrScan)
@OptIn(ExperimentalFoundationApi::class, ExperimentalMaterial3Api::class)
@Composable
@@ -79,8 +80,12 @@ internal fun NewMessage(
LocalColors.current.backgroundSecondary,
shape = MaterialTheme.shapes.small
)) {
+ // `messageNew` is now a plurals string so get the singular version
+ val context = LocalContext.current
+ val newMessageTitleTxt:String = context.resources.getQuantityString(R.plurals.messageNew, 1, 1)
+
BackAppBar(
- title = stringResource(R.string.messageNew),
+ title = newMessageTitleTxt,
backgroundColor = Color.Transparent, // transparent to show the rounded shape of the container
onBack = onBack,
actions = { AppBarCloseIcon(onClose = onClose) }
@@ -88,7 +93,7 @@ internal fun NewMessage(
SessionTabRow(pagerState, TITLES)
HorizontalPager(pagerState) {
when (TITLES[it]) {
- R.string.enter_account_id -> EnterAccountId(state, callbacks, onHelp)
+ R.string.accountIdEnter -> EnterAccountId(state, callbacks, onHelp)
R.string.qrScan -> QRScannerScreen(qrErrors, onScan = callbacks::onScanQrCode)
}
}
@@ -116,7 +121,7 @@ private fun EnterAccountId(
.verticalScroll(rememberScrollState())
// There is a known issue with the ime padding on android versions below 30
- /// So on these older versions we need to resort to some manual padding based on the visible height
+ // So on these older versions we need to resort to some manual padding based on the visible height
// when the keyboard is up
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) {
val keyboardHeight by keyboardHeight()
@@ -149,9 +154,9 @@ private fun EnterAccountId(
Spacer(modifier = Modifier.height(LocalDimensions.current.xxxsSpacing))
BorderlessButtonWithIcon(
- text = stringResource(R.string.messageNewDescription),
+ text = stringResource(R.string.messageNewDescriptionMobile),
modifier = Modifier
- .contentDescription(R.string.AccessibilityId_help_desk_link)
+ .contentDescription(R.string.AccessibilityId_messageNewDescriptionMobile)
.padding(horizontal = LocalDimensions.current.mediumSpacing)
.fillMaxWidth(),
style = LocalType.current.small,
diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/start/newmessage/NewMessageViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/start/newmessage/NewMessageViewModel.kt
index 6ed8a08233..be7630b536 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/conversation/start/newmessage/NewMessageViewModel.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/start/newmessage/NewMessageViewModel.kt
@@ -4,6 +4,8 @@ import android.app.Application
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel
+import java.util.concurrent.TimeoutException
+import javax.inject.Inject
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.channels.BufferOverflow
@@ -19,8 +21,6 @@ import org.session.libsession.snode.SnodeAPI
import org.session.libsignal.utilities.PublicKeyValidation
import org.session.libsignal.utilities.timeout
import org.thoughtcrime.securesms.ui.GetString
-import java.util.concurrent.TimeoutException
-import javax.inject.Inject
@HiltViewModel
internal class NewMessageViewModel @Inject constructor(
@@ -41,7 +41,6 @@ internal class NewMessageViewModel @Inject constructor(
override fun onChange(value: String) {
loadOnsJob?.cancel()
loadOnsJob = null
-
_state.update { it.copy(newMessageIdOrOns = value, isTextErrorColor = false, loading = false) }
}
@@ -59,7 +58,7 @@ internal class NewMessageViewModel @Inject constructor(
if (PublicKeyValidation.isValid(value, isPrefixRequired = false) && PublicKeyValidation.hasValidPrefix(value)) {
onPublicKey(value)
} else {
- _qrErrors.tryEmit(application.getString(R.string.this_qr_code_does_not_contain_an_account_id))
+ _qrErrors.tryEmit(application.getString(R.string.qrNotAccountId))
}
}
@@ -98,8 +97,7 @@ internal class NewMessageViewModel @Inject constructor(
private fun Exception.toMessage() = when (this) {
is SnodeAPI.Error.Generic -> application.getString(R.string.onsErrorNotRecognized)
- is TimeoutException -> application.getString(R.string.onsErrorUnableToSearch)
- else -> application.getString(R.string.fragment_enter_public_key_error_message)
+ else -> application.getString(R.string.onsErrorUnableToSearch)
}
}
@@ -112,4 +110,4 @@ internal data class State(
val isNextButtonEnabled: Boolean get() = newMessageIdOrOns.isNotBlank()
}
-internal data class Success(val publicKey: String)
+internal data class Success(val publicKey: String)
\ No newline at end of file
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
index d3192abc05..a1eca65eb4 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationActivityV2.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationActivityV2.kt
@@ -7,6 +7,7 @@ import android.content.ClipData
import android.content.ClipboardManager
import android.content.Context
import android.content.Intent
+import android.content.pm.PackageManager
import android.content.res.Resources
import android.database.Cursor
import android.graphics.Rect
@@ -18,10 +19,7 @@ import android.os.Bundle
import android.os.Handler
import android.os.Looper
import android.provider.MediaStore
-import android.text.SpannableStringBuilder
-import android.text.SpannedString
import android.text.TextUtils
-import android.text.style.StyleSpan
import android.util.Pair
import android.util.TypedValue
import android.view.ActionMode
@@ -35,8 +33,12 @@ import android.widget.Toast
import androidx.activity.result.ActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.activity.viewModels
-import androidx.core.text.set
-import androidx.core.text.toSpannable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.platform.ViewCompositionStrategy
+import androidx.core.content.ContextCompat
+import androidx.core.view.drawToBitmap
import androidx.core.view.isGone
import androidx.core.view.isVisible
import androidx.fragment.app.DialogFragment
@@ -51,6 +53,8 @@ import androidx.loader.content.Loader
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.annimon.stream.Stream
+import com.bumptech.glide.Glide
+import com.squareup.phrase.Phrase
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.channels.BufferOverflow
@@ -64,7 +68,6 @@ import network.loki.messenger.databinding.ActivityConversationV2Binding
import network.loki.messenger.libsession_util.util.ExpiryMode
import nl.komponents.kovenant.ui.successUi
import org.session.libsession.messaging.MessagingModuleConfiguration
-import org.session.libsession.messaging.contacts.Contact
import org.session.libsession.messaging.messages.ExpirationConfiguration
import org.session.libsession.messaging.messages.applyExpiryMode
import org.session.libsession.messaging.messages.control.DataExtractionNotification
@@ -84,6 +87,10 @@ import org.session.libsession.utilities.Address
import org.session.libsession.utilities.Address.Companion.fromSerialized
import org.session.libsession.utilities.GroupUtil
import org.session.libsession.utilities.MediaTypes
+import org.session.libsession.utilities.StringSubstitutionConstants.APP_NAME_KEY
+import org.session.libsession.utilities.StringSubstitutionConstants.CONVERSATION_NAME_KEY
+import org.session.libsession.utilities.StringSubstitutionConstants.GROUP_NAME_KEY
+import org.session.libsession.utilities.StringSubstitutionConstants.NAME_KEY
import org.session.libsession.utilities.Stub
import org.session.libsession.utilities.TextSecurePreferences
import org.session.libsession.utilities.concurrent.SimpleTask
@@ -109,6 +116,8 @@ import org.thoughtcrime.securesms.conversation.v2.MessageDetailActivity.Companio
import org.thoughtcrime.securesms.conversation.v2.MessageDetailActivity.Companion.ON_DELETE
import org.thoughtcrime.securesms.conversation.v2.MessageDetailActivity.Companion.ON_REPLY
import org.thoughtcrime.securesms.conversation.v2.MessageDetailActivity.Companion.ON_RESEND
+import org.thoughtcrime.securesms.conversation.v2.MessageDetailActivity.Companion.ON_COPY
+import org.thoughtcrime.securesms.conversation.v2.MessageDetailActivity.Companion.ON_SAVE
import org.thoughtcrime.securesms.conversation.v2.dialogs.BlockedDialog
import org.thoughtcrime.securesms.conversation.v2.dialogs.LinkPreviewDialog
import org.thoughtcrime.securesms.conversation.v2.input_bar.InputBarButton
@@ -155,7 +164,6 @@ import org.thoughtcrime.securesms.mediasend.Media
import org.thoughtcrime.securesms.mediasend.MediaSendActivity
import org.thoughtcrime.securesms.mms.AudioSlide
import org.thoughtcrime.securesms.mms.GifSlide
-import com.bumptech.glide.Glide
import org.thoughtcrime.securesms.mms.ImageSlide
import org.thoughtcrime.securesms.mms.MediaConstraints
import org.thoughtcrime.securesms.mms.Slide
@@ -165,19 +173,21 @@ import org.thoughtcrime.securesms.permissions.Permissions
import org.thoughtcrime.securesms.reactions.ReactionsDialogFragment
import org.thoughtcrime.securesms.reactions.any.ReactWithAnyEmojiDialogFragment
import org.thoughtcrime.securesms.showSessionDialog
+import org.thoughtcrime.securesms.ui.OpenURLAlertDialog
+import org.thoughtcrime.securesms.ui.theme.SessionMaterialTheme
import org.thoughtcrime.securesms.util.ActivityDispatcher
import org.thoughtcrime.securesms.util.ConfigurationMessageUtilities
import org.thoughtcrime.securesms.util.DateUtils
import org.thoughtcrime.securesms.util.MediaUtil
import org.thoughtcrime.securesms.util.NetworkUtils
import org.thoughtcrime.securesms.util.SaveAttachmentTask
-import org.thoughtcrime.securesms.util.drawToBitmap
import org.thoughtcrime.securesms.util.isScrolledToBottom
import org.thoughtcrime.securesms.util.isScrolledToWithin30dpOfBottom
import org.thoughtcrime.securesms.util.push
import org.thoughtcrime.securesms.util.show
import org.thoughtcrime.securesms.util.toPx
import java.lang.ref.WeakReference
+import java.util.LinkedList
import java.util.Locale
import java.util.concurrent.ExecutionException
import java.util.concurrent.atomic.AtomicBoolean
@@ -188,6 +198,8 @@ import kotlin.math.abs
import kotlin.math.min
import kotlin.math.roundToInt
import kotlin.math.sqrt
+import kotlin.time.Duration.Companion.minutes
+
private const val TAG = "ConversationActivityV2"
@@ -231,6 +243,8 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
.get(LinkPreviewViewModel::class.java)
}
+ private var openLinkDialogUrl: String? by mutableStateOf(null)
+
private val threadId: Long by lazy {
var threadId = intent.getLongExtra(THREAD_ID, -1L)
if (threadId == -1L) {
@@ -279,8 +293,17 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
var searchViewItem: MenuItem? = null
private val bufferedLastSeenChannel = Channel(capacity = 512, onBufferOverflow = BufferOverflow.DROP_OLDEST)
+
private var emojiPickerVisible = false
+ // Queue of timestamps used to rate-limit emoji reactions
+ private val emojiRateLimiterQueue = LinkedList()
+
+ // Constants used to enforce the given maximum emoji reactions allowed per minute (emoji reactions
+ // that occur above this limit will result in a "Slow down" toast rather than adding the reaction).
+ private val EMOJI_REACTIONS_ALLOWED_PER_MINUTE = 20
+ private val ONE_MINUTE_IN_MILLISECONDS = 1.minutes.inWholeMilliseconds
+
private val isScrolledToBottom: Boolean
get() = binding.conversationRecyclerView.isScrolledToBottom
@@ -385,12 +408,33 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
}
// endregion
+ fun showOpenUrlDialog(url: String){
+ openLinkDialogUrl = url
+ }
+
// region Lifecycle
override fun onCreate(savedInstanceState: Bundle?, isReady: Boolean) {
super.onCreate(savedInstanceState, isReady)
binding = ActivityConversationV2Binding.inflate(layoutInflater)
setContentView(binding.root)
+ // set the compose dialog content
+ binding.dialogOpenUrl.apply {
+ setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed)
+ setContent {
+ SessionMaterialTheme {
+ if(!openLinkDialogUrl.isNullOrEmpty()){
+ OpenURLAlertDialog(
+ url = openLinkDialogUrl!!,
+ onDismissRequest = {
+ openLinkDialogUrl = null
+ }
+ )
+ }
+ }
+ }
+ }
+
// messageIdToScroll
messageToScrollTimestamp.set(intent.getLongExtra(SCROLL_MESSAGE_ID, -1))
messageToScrollAuthor.set(intent.getParcelableExtra(SCROLL_MESSAGE_AUTHOR))
@@ -666,7 +710,6 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
// called from onCreate
private fun setUpInputBar() {
- binding.inputBar.isGone = viewModel.hidesInputBar()
binding.inputBar.delegate = this
binding.inputBarRecordingView.delegate = this
// GIF button
@@ -704,7 +747,7 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
}
override fun onFailure(e: ExecutionException?) {
- Toast.makeText(this@ConversationActivityV2, R.string.activity_conversation_attachment_prep_failed, Toast.LENGTH_LONG).show()
+ Toast.makeText(this@ConversationActivityV2, R.string.attachmentsErrorLoad, Toast.LENGTH_LONG).show()
}
})
return
@@ -755,9 +798,7 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
// called from onCreate
private fun setUpBlockedBanner() {
val recipient = viewModel.recipient?.takeUnless { it.isGroupRecipient } ?: return
- val accountID = recipient.address.toString()
- val name = sessionContactDb.getContactWithAccountID(accountID)?.displayName(Contact.ContactContext.REGULAR) ?: accountID
- binding.blockedBannerTextView.text = resources.getString(R.string.activity_conversation_blocked_banner_text, name)
+ binding.blockedBannerTextView.text = applicationContext.getString(R.string.blockBlockedDescription)
binding.blockedBanner.isVisible = recipient.isBlocked
binding.blockedBanner.setOnClickListener { viewModel.unblock() }
}
@@ -770,8 +811,11 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
binding.outdatedBanner.isVisible = shouldShowLegacy
if (shouldShowLegacy) {
- binding.outdatedBannerTextView.text =
- resources.getString(R.string.activity_conversation_outdated_client_banner_text, legacyRecipient!!.name)
+
+ val txt = Phrase.from(applicationContext, R.string.disappearingMessagesLegacy)
+ .put(NAME_KEY, legacyRecipient!!.name)
+ .format()
+ binding?.outdatedBannerTextView?.text = txt
}
}
@@ -809,6 +853,8 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
// Conversation should be deleted now, just go back
finish()
}
+
+ binding.inputBar.isGone = uiState.hideInputBar
}
}
}
@@ -903,11 +949,20 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
block(deleteThread = true)
}
binding.declineMessageRequestButton.setOnClickListener {
- viewModel.declineMessageRequest()
- lifecycleScope.launch(Dispatchers.IO) {
- ConfigurationMessageUtilities.forceSyncConfigurationNowIfNeeded(this@ConversationActivityV2)
+ fun doDecline() {
+ viewModel.declineMessageRequest()
+ lifecycleScope.launch(Dispatchers.IO) {
+ ConfigurationMessageUtilities.forceSyncConfigurationNowIfNeeded(this@ConversationActivityV2)
+ }
+ finish()
+ }
+
+ showSessionDialog {
+ title(R.string.delete)
+ text(resources.getString(R.string.messageRequestsDelete))
+ dangerButton(R.string.delete) { doDecline() }
+ button(R.string.cancel)
}
- finish()
}
}
@@ -1056,34 +1111,48 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
updateUnreadCountIndicator()
}
+ // Update placeholder / control messages in a conversation
private fun updatePlaceholder() {
val recipient = viewModel.recipient ?: return Log.w("Loki", "recipient was null in placeholder update")
val blindedRecipient = viewModel.blindedRecipient
val openGroup = viewModel.openGroup
- val (textResource, insertParam) = when {
- recipient.isLocalNumber -> R.string.activity_conversation_empty_state_note_to_self to null
- openGroup != null && !openGroup.canWrite -> R.string.activity_conversation_empty_state_read_only to recipient.toShortString()
- blindedRecipient?.blocksCommunityMessageRequests == true -> R.string.activity_conversation_empty_state_blocks_community_requests to recipient.toShortString()
- else -> R.string.activity_conversation_empty_state_default to recipient.toShortString()
+ // Get the correct placeholder text for this type of empty conversation
+ val isNoteToSelf = recipient.isLocalNumber
+ val txtCS: CharSequence = when {
+ recipient.isLocalNumber -> getString(R.string.noteToSelfEmpty)
+
+ // If this is a community which we cannot write to
+ openGroup != null && !openGroup.canWrite -> {
+ Phrase.from(applicationContext, R.string.conversationsEmpty)
+ .put(CONVERSATION_NAME_KEY, openGroup.name)
+ .format()
+ }
+
+ // If we're trying to message someone who has blocked community message requests
+ blindedRecipient?.blocksCommunityMessageRequests == true -> {
+ Phrase.from(applicationContext, R.string.messageRequestsTurnedOff)
+ .put(NAME_KEY, recipient.toShortString())
+ .format()
+ }
+
+ recipient.isGroupRecipient -> {
+ // If this is a group or community that we CAN send messages to
+ Phrase.from(applicationContext, R.string.groupNoMessages)
+ .put(GROUP_NAME_KEY, recipient.toShortString())
+ .format()
+ }
+
+ else -> {
+ Log.w(TAG, "Something else happened in updatePlaceholder - we're not sure what.")
+ ""
+ }
}
+
val showPlaceholder = adapter.itemCount == 0
binding.placeholderText.isVisible = showPlaceholder
if (showPlaceholder) {
- if (insertParam != null) {
- val span = getText(textResource) as SpannedString
- val annotations = span.getSpans(0, span.length, StyleSpan::class.java)
- val boldSpan = annotations.first()
- val spannedParam = insertParam.toSpannable()
- spannedParam[0 until spannedParam.length] = StyleSpan(boldSpan.style)
- val originalStart = span.getSpanStart(boldSpan)
- val originalEnd = span.getSpanEnd(boldSpan)
- val newString = SpannableStringBuilder(span)
- .replace(originalStart, originalEnd, spannedParam)
- binding.placeholderText.text = newString
- } else {
- binding.placeholderText.setText(textResource)
- }
+ binding.placeholderText.text = txtCS
}
}
@@ -1117,11 +1186,21 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
}
override fun block(deleteThread: Boolean) {
+ val recipient = viewModel.recipient ?: return Log.w("Loki", "Recipient was null for block action")
showSessionDialog {
- title(R.string.RecipientPreferenceActivity_block_this_contact_question)
- text(R.string.RecipientPreferenceActivity_you_will_no_longer_receive_messages_and_calls_from_this_contact)
- dangerButton(R.string.RecipientPreferenceActivity_block, R.string.AccessibilityId_block_confirm) {
+ title(R.string.block)
+ text(
+ Phrase.from(context, R.string.blockDescription)
+ .put(NAME_KEY, recipient.name)
+ .format()
+ )
+ dangerButton(R.string.block, R.string.AccessibilityId_blockConfirm) {
viewModel.block()
+
+ // Block confirmation toast added as per SS-64
+ val txt = Phrase.from(context, R.string.blockBlockedUser).put(NAME_KEY, recipient.name).format().toString()
+ Toast.makeText(context, txt, Toast.LENGTH_LONG).show()
+
if (deleteThread) {
viewModel.deleteThread()
finish()
@@ -1135,7 +1214,7 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
val clip = ClipData.newPlainText("Account ID", accountId)
val manager = getSystemService(PassphraseRequiredActionBarActivity.CLIPBOARD_SERVICE) as ClipboardManager
manager.setPrimaryClip(clip)
- Toast.makeText(this, R.string.copied_to_clipboard, Toast.LENGTH_SHORT).show()
+ Toast.makeText(this, R.string.copied, Toast.LENGTH_SHORT).show()
}
override fun copyOpenGroupUrl(thread: Recipient) {
@@ -1147,7 +1226,7 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
val clip = ClipData.newPlainText("Community URL", openGroup.joinURL)
val manager = getSystemService(PassphraseRequiredActionBarActivity.CLIPBOARD_SERVICE) as ClipboardManager
manager.setPrimaryClip(clip)
- Toast.makeText(this, R.string.copied_to_clipboard, Toast.LENGTH_SHORT).show()
+ Toast.makeText(this, R.string.copied, Toast.LENGTH_SHORT).show()
}
override fun showDisappearingMessages(thread: Recipient) {
@@ -1160,13 +1239,20 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
}
override fun unblock() {
+ val recipient = viewModel.recipient ?: return Log.w("Loki", "Recipient was null for unblock action")
+
+ if (!recipient.isContactRecipient) {
+ return Log.w("Loki", "Cannot unblock a user who is not a contact recipient - aborting unblock attempt.")
+ }
+
showSessionDialog {
- title(R.string.ConversationActivity_unblock_this_contact_question)
- text(R.string.ConversationActivity_you_will_once_again_be_able_to_receive_messages_and_calls_from_this_contact)
- dangerButton(
- R.string.ConversationActivity_unblock,
- R.string.AccessibilityId_block_confirm
- ) { viewModel.unblock() }
+ title(R.string.blockUnblock)
+ text(
+ Phrase.from(context, R.string.blockUnblockName)
+ .put(NAME_KEY, recipient.name)
+ .format()
+ )
+ dangerButton(R.string.blockUnblock, R.string.AccessibilityId_unblockConfirm) { viewModel.unblock() }
cancelButton()
}
}
@@ -1177,10 +1263,9 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
if (actionMode != null) {
onDeselect(message, position, actionMode)
} else {
- // NOTE:
- // We have to use onContentClick (rather than a click listener directly on
+ // 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.
+ // onClickListeners directly to message content views!
view.onContentClick(event)
}
}
@@ -1279,7 +1364,45 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
}
}
+ // Method to add an emoji to a queue and remove it a short while later - this is used as a
+ // rate-limiting mechanism and is called from the `sendEmojiReaction` method, below.
+
+ fun canPerformEmojiReaction(timestamp: Long): Boolean {
+ // If the emoji reaction queue is full..
+ if (emojiRateLimiterQueue.size >= EMOJI_REACTIONS_ALLOWED_PER_MINUTE) {
+ // ..grab the timestamp of the oldest emoji reaction.
+ val headTimestamp = emojiRateLimiterQueue.peekFirst()
+ if (headTimestamp == null) {
+ Log.w(TAG, "Could not get emoji react head timestamp - should never happen, but we'll allow the emoji reaction.")
+ return true
+ }
+
+ // With the queue full, if the earliest emoji reaction occurred less than 1 minute ago
+ // then we reject it..
+ if (System.currentTimeMillis() - headTimestamp <= ONE_MINUTE_IN_MILLISECONDS) {
+ return false
+ } else {
+ // ..otherwise if the earliest emoji reaction was more than a minute ago we'll
+ // remove that early reaction to move the timestamp at index 1 into index 0, add
+ // our new timestamp and return true to accept the emoji reaction.
+ emojiRateLimiterQueue.removeFirst()
+ emojiRateLimiterQueue.addLast(timestamp)
+ return true
+ }
+ } else {
+ // If the queue isn't already full then we add the new timestamp to the back of the queue and allow the emoji reaction
+ emojiRateLimiterQueue.addLast(timestamp)
+ return true
+ }
+ }
+
private fun sendEmojiReaction(emoji: String, originalMessage: MessageRecord) {
+ // Only allow the emoji reaction if we aren't currently rate limited
+ if (!canPerformEmojiReaction(System.currentTimeMillis())) {
+ Toast.makeText(this, getString(R.string.emojiReactsCoolDown), Toast.LENGTH_SHORT).show()
+ return
+ }
+
// Create the message
val recipient = viewModel.recipient ?: return Log.w(TAG, "Could not locate recipient when sending emoji reaction")
val reactionMessage = VisibleMessage()
@@ -1325,6 +1448,8 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
}
}
+ // Method to remove a emoji reaction from a message.
+ // Note: We do not count emoji removal towards the emojiRateLimiterQueue.
private fun sendEmojiRemoval(emoji: String, originalMessage: MessageRecord) {
val recipient = viewModel.recipient ?: return
val message = VisibleMessage()
@@ -1529,13 +1654,13 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
}
}
- override fun onReactionLongClicked(messageId: MessageId) {
+ override fun onReactionLongClicked(messageId: MessageId, emoji: String?) {
if (viewModel.recipient?.isGroupRecipient == true) {
val isUserModerator = viewModel.openGroup?.let { openGroup ->
val userPublicKey = textSecurePreferences.getLocalNumber() ?: return@let false
OpenGroupManager.isUserModerator(this, openGroup.id, userPublicKey, viewModel.blindedPublicKey)
} ?: false
- val fragment = ReactionsDialogFragment.create(messageId, isUserModerator)
+ val fragment = ReactionsDialogFragment.create(messageId, isUserModerator, emoji)
fragment.show(supportFragmentManager, null)
}
}
@@ -1591,9 +1716,9 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
val isNoteToSelf = (recipient.isContactRecipient && recipient.address.toString() == userPublicKey)
if (seed in text && !isNoteToSelf && !hasPermissionToSendSeed) {
showSessionDialog {
- title(R.string.dialog_send_seed_title)
- text(R.string.dialog_send_seed_explanation)
- button(R.string.dialog_send_seed_send_button_title) { sendTextOnlyMessage(true) }
+ title(R.string.warning)
+ text(R.string.recoveryPasswordWarningSendDescription)
+ button(R.string.send) { sendTextOnlyMessage(true) }
cancelButton()
}
@@ -1660,10 +1785,21 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
attachmentManager.clear()
// Reset attachments button if needed
if (isShowingAttachmentOptions) { toggleAttachmentOptions() }
- // Put the message in the database
- message.id = mmsDb.insertMessageOutbox(outgoingTextMessage, viewModel.threadId, false, null, runThreadUpdate = true)
- // Send it
- MessageSender.send(message, recipient.address, attachments, quote, linkPreview)
+
+ // do the heavy work in the bg
+ lifecycleScope.launch(Dispatchers.IO) {
+ // Put the message in the database
+ message.id = mmsDb.insertMessageOutbox(
+ outgoingTextMessage,
+ viewModel.threadId,
+ false,
+ null,
+ runThreadUpdate = true
+ )
+ // Send it
+ MessageSender.send(message, recipient.address, attachments, quote, linkPreview)
+ }
+
// Send a typing stopped message
ApplicationContext.getInstance(this).typingStatusSender.onTypingStopped(viewModel.threadId)
return Pair(recipient.address, sentTimestamp)
@@ -1673,9 +1809,9 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
val hasSeenGIFMetaDataWarning: Boolean = textSecurePreferences.hasSeenGIFMetaDataWarning()
if (!hasSeenGIFMetaDataWarning) {
showSessionDialog {
- title(R.string.giphy_permission_title)
- text(R.string.giphy_permission_message)
- button(R.string.continue_2) {
+ title(R.string.giphyWarning)
+ text(Phrase.from(context, R.string.giphyWarningDescription).put(APP_NAME_KEY, getString(R.string.app_name)).format())
+ button(R.string.theContinue) {
textSecurePreferences.setHasSeenGIFMetaDataWarning()
selectGif()
}
@@ -1718,11 +1854,27 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
val mediaPreppedListener = object : ListenableFuture.Listener {
override fun onSuccess(result: Boolean?) {
+ if (result == null) {
+ Log.w(TAG, "Media prepper returned a null result - bailing.")
+ return
+ }
+
+ // If the attachment was too large or MediaConstraints.isSatisfied failed for some
+ // other reason then we reset the attachment manager & shown buttons then bail..
+ if (!result) {
+ attachmentManager.clear()
+ if (isShowingAttachmentOptions) { toggleAttachmentOptions() }
+ return
+ }
+
+ // ..otherwise we can attempt to send the attachment(s).
+ // Note: The only multi-attachment message type is when sending images - all others
+ // attempt send the attachment immediately upon file selection.
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()
+ Toast.makeText(this@ConversationActivityV2, R.string.attachmentsErrorLoad, Toast.LENGTH_LONG).show()
}
}
when (requestCode) {
@@ -1805,8 +1957,9 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
} 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))
+ .withPermanentDenialDialog(Phrase.from(applicationContext, R.string.permissionsMicrophoneAccessRequired)
+ .put(APP_NAME_KEY, getString(R.string.app_name))
+ .format().toString())
.execute()
}
}
@@ -1876,7 +2029,7 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
}
override fun onFailure(e: ExecutionException) {
- Toast.makeText(this@ConversationActivityV2, R.string.ConversationActivity_unable_to_record_audio, Toast.LENGTH_LONG).show()
+ Toast.makeText(this@ConversationActivityV2, R.string.audioUnableToRecord, Toast.LENGTH_LONG).show()
}
})
}
@@ -1927,10 +2080,9 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
}
private fun showDeleteLocallyUI(messages: Set) {
- val messageCount = 1
showSessionDialog {
- title(resources.getQuantityString(R.plurals.ConversationFragment_delete_selected_messages, messageCount, messageCount))
- text(resources.getQuantityString(R.plurals.ConversationFragment_this_will_permanently_delete_all_n_selected_messages, messageCount, messageCount))
+ title(resources.getQuantityString(R.plurals.deleteMessage, messages.count(), messages.count()))
+ text(resources.getString(R.string.deleteMessagesDescriptionDevice))
button(R.string.delete) { messages.forEach(viewModel::deleteLocally); endActionMode() }
cancelButton(::endActionMode)
}
@@ -1950,13 +2102,10 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
// If the recipient is a community OR a Note-to-Self then we delete the message for everyone
if (recipient.isCommunityRecipient || recipient.isLocalNumber) {
- val messageCount = 1 // Only used for plurals string
showSessionDialog {
- title(resources.getQuantityString(R.plurals.ConversationFragment_delete_selected_messages, messageCount, messageCount))
- text(resources.getQuantityString(R.plurals.ConversationFragment_this_will_permanently_delete_all_n_selected_messages, messageCount, messageCount))
- button(R.string.delete) {
- messages.forEach(viewModel::deleteForEveryone); endActionMode()
- }
+ title(resources.getQuantityString(R.plurals.deleteMessage, messages.count(), messages.count()))
+ text(resources.getString(R.string.deleteMessageDescriptionEveryone))
+ dangerButton(R.string.delete) { messages.forEach(viewModel::deleteForEveryone); endActionMode() }
cancelButton { endActionMode() }
}
// Otherwise if this is a 1-on-1 conversation we may decided to delete just for ourselves or delete for everyone
@@ -1981,13 +2130,10 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
}
else // Finally, if this is a closed group and you are deleting someone else's message(s) then we can only delete locally.
{
- val messageCount = 1
showSessionDialog {
- title(resources.getQuantityString(R.plurals.ConversationFragment_delete_selected_messages, messageCount, messageCount))
- text(resources.getQuantityString(R.plurals.ConversationFragment_this_will_permanently_delete_all_n_selected_messages, messageCount, messageCount))
- button(R.string.delete) {
- messages.forEach(viewModel::deleteLocally); endActionMode()
- }
+ title(resources.getQuantityString(R.plurals.deleteMessage, messages.count(), messages.count()))
+ text(resources.getString(R.string.deleteMessageDescriptionDevice))
+ dangerButton(R.string.delete) { messages.forEach(viewModel::deleteLocally); endActionMode() }
cancelButton(::endActionMode)
}
}
@@ -1995,18 +2141,18 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
override fun banUser(messages: Set) {
showSessionDialog {
- title(R.string.ConversationFragment_ban_selected_user)
- text("This will ban the selected user from this room. It won't ban them from other rooms.")
- button(R.string.ban) { viewModel.banUser(messages.first().individualRecipient); endActionMode() }
+ title(R.string.banUser)
+ text(R.string.communityBanDescription)
+ dangerButton(R.string.theContinue) { viewModel.banUser(messages.first().individualRecipient); endActionMode() }
cancelButton(::endActionMode)
}
}
override fun banAndDeleteAll(messages: Set) {
showSessionDialog {
- title(R.string.ConversationFragment_ban_selected_user)
- text("This will ban the selected user from this room and delete all messages sent by them. It won't ban them from other rooms or delete the messages they sent there.")
- button(R.string.ban) { viewModel.banAndDeleteAll(messages.first()); endActionMode() }
+ title(R.string.banDeleteAll)
+ text(R.string.communityBanDeleteDescription)
+ dangerButton(R.string.theContinue) { viewModel.banAndDeleteAll(messages.first()); endActionMode() }
cancelButton(::endActionMode)
}
}
@@ -2042,7 +2188,7 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
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()
+ Toast.makeText(this, R.string.copied, Toast.LENGTH_SHORT).show()
endActionMode()
}
@@ -2051,7 +2197,7 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
val clip = ClipData.newPlainText("Account ID", accountID)
val manager = getSystemService(CLIPBOARD_SERVICE) as ClipboardManager
manager.setPrimaryClip(clip)
- Toast.makeText(this, R.string.copied_to_clipboard, Toast.LENGTH_SHORT).show()
+ Toast.makeText(this, R.string.copied, Toast.LENGTH_SHORT).show()
endActionMode()
}
@@ -2079,52 +2225,113 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
ON_REPLY -> reply(set)
ON_RESEND -> resendMessage(set)
ON_DELETE -> deleteMessages(set)
+ ON_COPY -> copyMessages(set)
+ ON_SAVE -> {
+ if(message is MmsMessageRecord) saveAttachmentsIfPossible(setOf(message))
+ }
}
}
override fun showMessageDetail(messages: Set) {
Intent(this, MessageDetailActivity::class.java)
.apply { putExtra(MESSAGE_TIMESTAMP, messages.first().timestamp) }
- .let { handleMessageDetail.launch(it) }
+ .let {
+ handleMessageDetail.launch(it)
+ overridePendingTransition(R.anim.slide_from_right, R.anim.slide_to_left)
+ }
endActionMode()
}
- override fun saveAttachment(messages: Set) {
+ private fun saveAttachments(message: MmsMessageRecord) {
+ 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
+ }
+ // Implied else that there were no attachment(s)
+ Toast.makeText(this, resources.getString(R.string.attachmentsSaveError), Toast.LENGTH_LONG).show()
+ }
+
+ private fun hasPermission(permission: String): Boolean {
+ val result = ContextCompat.checkSelfPermission(this, permission)
+ return result == PackageManager.PERMISSION_GRANTED
+ }
+
+ override fun saveAttachmentsIfPossible(messages: Set) {
val message = messages.first() as MmsMessageRecord
- // Do not allow the user to download a file attachment before it has finished downloading
+ // Note: The save option is only added to the menu in ConversationReactionOverlay.getMenuActionItems
+ // if the attachment has finished downloading, so we don't really have to check for message.isMediaPending
+ // here - but we'll do it anyway and bail should that be the case as a defensive programming strategy.
if (message.isMediaPending) {
- Toast.makeText(this, resources.getString(R.string.conversation_activity__wait_until_attachment_has_finished_downloading), Toast.LENGTH_LONG).show()
+ Log.w(TAG, "Somehow we were asked to download an attachment before it had finished downloading - aborting download.")
return
}
- SaveAttachmentTask.showWarningDialog(this) {
+ // Before saving an attachment, regardless of Android API version or permissions, we always want to ensure
+ // that we've warned the user just _once_ that any attachments they save can be accessed by other apps.
+ val haveWarned = TextSecurePreferences.getHaveWarnedUserAboutSavingAttachments(this)
+ if (haveWarned) {
+ // On Android versions below 29 we require the WRITE_EXTERNAL_STORAGE permission to save attachments.
+ if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.P) {
+ // Save the attachment(s) then bail if we already have permission to do so
+ if (hasPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE)) {
+ saveAttachments(message)
+ return
+ } else {
+ /* If we don't have the permission then do nothing - which means we continue on to the SaveAttachmentTask part below where we ask for permissions */
+ }
+ } else {
+ // On more modern versions of Android on API 30+ WRITE_EXTERNAL_STORAGE is no longer used and we can just
+ // save files to the public directories like "Downloads", "Pictures" etc.
+ saveAttachments(message)
+ return
+ }
+ }
+
+ // ..otherwise we must ask for it first (only on Android APIs up to 28).
+ SaveAttachmentTask.showOneTimeWarningDialogOrSave(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))
+ .maxSdkVersion(Build.VERSION_CODES.P) // P is 28
+ .withPermanentDenialDialog(Phrase.from(applicationContext, R.string.permissionsStorageDeniedLegacy)
+ .put(APP_NAME_KEY, getString(R.string.app_name))
+ .format().toString())
.onAnyDenied {
endActionMode()
- Toast.makeText(this@ConversationActivityV2, R.string.MediaPreviewActivity_unable_to_write_to_external_storage_without_permission, Toast.LENGTH_LONG).show()
+
+ // If permissions were denied inform the user that we can't proceed without them and offer to take the user to Settings
+ showSessionDialog {
+ title(R.string.permissionsRequired)
+
+ val txt = Phrase.from(applicationContext, R.string.permissionsStorageDeniedLegacy)
+ .put(APP_NAME_KEY, getString(R.string.app_name))
+ .format().toString()
+ text(txt)
+
+ // Take the user directly to the settings app for Session to grant the permission if they
+ // initially denied it but then have a change of heart when they realise they can't
+ // proceed without it.
+ dangerButton(R.string.theContinue) {
+ val intent = Intent(android.provider.Settings.ACTION_APPLICATION_DETAILS_SETTINGS)
+ intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
+ val uri = Uri.fromParts("package", packageName, null)
+ intent.setData(uri)
+ startActivity(intent)
+ }
+
+ button(R.string.cancel)
+ }
}
.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()
+ saveAttachments(message)
}
.execute()
}
@@ -2179,6 +2386,7 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
searchViewModel.onMissingResult() }
}
}
+
binding.searchBottomBar.setData(result.position, result.getResults().size)
})
}
@@ -2188,6 +2396,7 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
binding.searchBottomBar.visibility = View.VISIBLE
binding.searchBottomBar.setData(0, 0)
binding.inputBar.visibility = View.INVISIBLE
+
}
fun onSearchClosed() {
@@ -2241,7 +2450,7 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
ConversationReactionOverlay.Action.REPLY -> reply(selectedItems)
ConversationReactionOverlay.Action.RESYNC -> resyncMessage(selectedItems)
ConversationReactionOverlay.Action.RESEND -> resendMessage(selectedItems)
- ConversationReactionOverlay.Action.DOWNLOAD -> saveAttachment(selectedItems)
+ ConversationReactionOverlay.Action.DOWNLOAD -> saveAttachmentsIfPossible(selectedItems)
ConversationReactionOverlay.Action.COPY_MESSAGE -> copyMessages(selectedItems)
ConversationReactionOverlay.Action.VIEW_INFO -> showMessageDetail(selectedItems)
ConversationReactionOverlay.Action.SELECT -> selectMessages(selectedItems)
@@ -2262,7 +2471,7 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
// Note: The adapter itemCount is zero based - so calling this with the itemCount in
// a non-zero based manner scrolls us to the bottom of the last message (including
// to the bottom of long messages as required by Jira SES-789 / GitHub 1364).
- recyclerView.scrollToPosition(adapter.itemCount)
+ recyclerView.smoothScrollToPosition(adapter.itemCount)
}
}
}
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
index 1c57dc8d5f..880dacb070 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationAdapter.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationAdapter.kt
@@ -1,7 +1,6 @@
package org.thoughtcrime.securesms.conversation.v2
import android.content.Context
-import android.content.Intent
import android.database.Cursor
import android.util.SparseArray
import android.util.SparseBooleanArray
@@ -12,12 +11,12 @@ import androidx.core.util.getOrDefault
import androidx.core.util.set
import androidx.lifecycle.LifecycleCoroutineScope
import androidx.recyclerview.widget.RecyclerView.ViewHolder
+import com.bumptech.glide.RequestManager
import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.channels.BufferOverflow
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
-import network.loki.messenger.R
import org.session.libsession.messaging.contacts.Contact
import org.session.libsession.messaging.sending_receiving.attachments.DatabaseAttachment
import org.thoughtcrime.securesms.conversation.v2.messages.ControlMessageView
@@ -26,9 +25,6 @@ import org.thoughtcrime.securesms.conversation.v2.messages.VisibleMessageViewDel
import org.thoughtcrime.securesms.database.CursorRecyclerViewAdapter
import org.thoughtcrime.securesms.database.model.MessageRecord
import org.thoughtcrime.securesms.dependencies.DatabaseComponent
-import com.bumptech.glide.RequestManager
-import org.thoughtcrime.securesms.preferences.PrivacySettingsActivity
-import org.thoughtcrime.securesms.showSessionDialog
import java.util.concurrent.atomic.AtomicLong
import kotlin.math.min
@@ -118,7 +114,11 @@ class ConversationAdapter(
val senderId = message.individualRecipient.address.serialize()
val senderIdHash = senderId.hashCode()
updateQueue.trySend(senderId)
- if (contactCache[senderIdHash] == null && !contactLoadedCache.getOrDefault(senderIdHash, false)) {
+ if (contactCache[senderIdHash] == null && !contactLoadedCache.getOrDefault(
+ senderIdHash,
+ false
+ )
+ ) {
getSenderInfo(senderId)?.let { contact ->
contactCache[senderIdHash] = contact
}
@@ -126,46 +126,41 @@ class ConversationAdapter(
val contact = contactCache[senderIdHash]
visibleMessageView.bind(
- message,
- messageBefore,
- getMessageAfter(position, cursor),
- glide,
- searchQuery,
- contact,
- senderId,
- lastSeen.get(),
- visibleMessageViewDelegate,
- onAttachmentNeedsDownload,
- lastSentMessageId
+ message,
+ messageBefore,
+ getMessageAfter(position, cursor),
+ glide,
+ searchQuery,
+ contact,
+ senderId,
+ lastSeen.get(),
+ visibleMessageViewDelegate,
+ onAttachmentNeedsDownload,
+ lastSentMessageId
)
if (!message.isDeleted) {
- visibleMessageView.onPress = { event -> onItemPress(message, viewHolder.adapterPosition, visibleMessageView, event) }
- visibleMessageView.onSwipeToReply = { onItemSwipeToReply(message, viewHolder.adapterPosition) }
- visibleMessageView.onLongPress = { onItemLongPress(message, viewHolder.adapterPosition, visibleMessageView) }
+ visibleMessageView.onPress = { event ->
+ onItemPress(
+ message,
+ viewHolder.adapterPosition,
+ visibleMessageView,
+ event
+ )
+ }
+ visibleMessageView.onSwipeToReply =
+ { onItemSwipeToReply(message, viewHolder.adapterPosition) }
+ visibleMessageView.onLongPress =
+ { onItemLongPress(message, viewHolder.adapterPosition, visibleMessageView) }
} else {
visibleMessageView.onPress = null
visibleMessageView.onSwipeToReply = null
visibleMessageView.onLongPress = null
}
}
+
is ControlMessageViewHolder -> {
viewHolder.view.bind(message, messageBefore)
- if (message.isCallLog && message.isFirstMissedCall) {
- viewHolder.view.setOnClickListener {
- context.showSessionDialog {
- title(R.string.CallNotificationBuilder_first_call_title)
- text(R.string.CallNotificationBuilder_first_call_message)
- button(R.string.activity_settings_title) {
- Intent(context, PrivacySettingsActivity::class.java)
- .let(context::startActivity)
- }
- cancelButton()
- }
- }
- } else {
- viewHolder.view.setOnClickListener(null)
- }
}
}
}
@@ -190,7 +185,7 @@ class ConversationAdapter(
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 (isReversed && !cursor.moveToPosition(position + 1)) { return null }
+ if (isReversed && !cursor.moveToPosition(position + 1)) { return null }
if (!isReversed && !cursor.moveToPosition(position - 1)) { return null }
return messageDB.readerFor(cursor).current
diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationReactionOverlay.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationReactionOverlay.kt
index 9f2046334b..d445d002cb 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationReactionOverlay.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationReactionOverlay.kt
@@ -22,6 +22,7 @@ import androidx.constraintlayout.widget.ConstraintLayout
import androidx.core.content.ContextCompat
import androidx.core.view.doOnLayout
import androidx.vectordrawable.graphics.drawable.AnimatorInflaterCompat
+import com.squareup.phrase.Phrase
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
@@ -30,7 +31,9 @@ import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import network.loki.messenger.R
+import org.session.libsession.LocalisedTimeUtil.toShortTwoPartString
import org.session.libsession.snode.SnodeAPI
+import org.session.libsession.utilities.StringSubstitutionConstants.TIME_LARGE_KEY
import org.session.libsession.utilities.TextSecurePreferences.Companion.getLocalNumber
import org.session.libsession.utilities.ThemeUtil
import org.thoughtcrime.securesms.components.emoji.EmojiImageView
@@ -48,12 +51,7 @@ import org.thoughtcrime.securesms.util.AnimationCompleteListener
import org.thoughtcrime.securesms.util.DateUtils
import java.util.Locale
import javax.inject.Inject
-import kotlin.time.Duration
-import kotlin.time.Duration.Companion.days
-import kotlin.time.Duration.Companion.hours
import kotlin.time.Duration.Companion.milliseconds
-import kotlin.time.Duration.Companion.minutes
-import kotlin.time.Duration.Companion.seconds
@AndroidEntryPoint
class ConversationReactionOverlay : FrameLayout {
@@ -215,7 +213,7 @@ class ConversationReactionOverlay : FrameLayout {
endY = backgroundView.height + menuPadding + reactionBarTopPadding
}
} else {
- endY = overlayHeight - contextMenu.getMaxHeight() - menuPadding - conversationItemSnapshot.height
+ endY = overlayHeight - contextMenu.getMaxHeight() - 2*menuPadding - conversationItemSnapshot.height
reactionBarBackgroundY = endY - reactionBarHeight - menuPadding
}
endApparentTop = endY
@@ -529,46 +527,54 @@ class ConversationReactionOverlay : FrameLayout {
?: return emptyList()
val userPublicKey = getLocalNumber(context)!!
// Select message
- items += ActionItem(R.attr.menu_select_icon, R.string.conversation_context__menu_select, { handleActionItemClicked(Action.SELECT) }, R.string.AccessibilityId_select)
+ items += ActionItem(R.attr.menu_select_icon, R.string.select, { handleActionItemClicked(Action.SELECT) }, R.string.AccessibilityId_select)
// Reply
val canWrite = openGroup == null || openGroup.canWrite
if (canWrite && !message.isPending && !message.isFailed && !message.isOpenGroupInvitation) {
- items += ActionItem(R.attr.menu_reply_icon, R.string.conversation_context__menu_reply, { handleActionItemClicked(Action.REPLY) }, R.string.AccessibilityId_reply_message)
+ items += ActionItem(R.attr.menu_reply_icon, R.string.reply, { handleActionItemClicked(Action.REPLY) }, R.string.AccessibilityId_reply)
}
// Copy message text
if (!containsControlMessage && hasText) {
items += ActionItem(R.attr.menu_copy_icon, R.string.copy, { handleActionItemClicked(Action.COPY_MESSAGE) })
}
// Copy Account ID
- if (recipient.isGroupRecipient && !recipient.isCommunityRecipient && message.recipient.address.toString() != userPublicKey) {
- items += ActionItem(R.attr.menu_copy_icon, R.string.activity_conversation_menu_copy_account_id, { handleActionItemClicked(Action.COPY_ACCOUNT_ID) })
+ if (!recipient.isCommunityRecipient && message.isIncoming) {
+ items += ActionItem(R.attr.menu_copy_icon, R.string.accountIDCopy, { handleActionItemClicked(Action.COPY_ACCOUNT_ID) })
}
// Delete message
if (userCanDeleteSelectedItems(context, message, openGroup, userPublicKey, blindedPublicKey)) {
items += ActionItem(R.attr.menu_trash_icon, R.string.delete, { handleActionItemClicked(Action.DELETE) },
- R.string.AccessibilityId_delete_message, message.subtitle, ThemeUtil.getThemedColor(context, R.attr.danger))
+ R.string.AccessibilityId_deleteMessage, message.subtitle, ThemeUtil.getThemedColor(context, R.attr.danger))
}
// Ban user
if (userCanBanSelectedUsers(context, message, openGroup, userPublicKey, blindedPublicKey)) {
- items += ActionItem(R.attr.menu_block_icon, R.string.conversation_context__menu_ban_user, { handleActionItemClicked(Action.BAN_USER) })
+ items += ActionItem(R.attr.menu_block_icon, R.string.banUser, { handleActionItemClicked(Action.BAN_USER) })
}
// Ban and delete all
if (userCanBanSelectedUsers(context, message, openGroup, userPublicKey, blindedPublicKey)) {
- items += ActionItem(R.attr.menu_trash_icon, R.string.conversation_context__menu_ban_and_delete_all, { handleActionItemClicked(Action.BAN_AND_DELETE_ALL) })
+ items += ActionItem(R.attr.menu_trash_icon, R.string.banDeleteAll, { handleActionItemClicked(Action.BAN_AND_DELETE_ALL) })
}
// Message detail
- items += ActionItem(R.attr.menu_info_icon, R.string.conversation_context__menu_message_details, { handleActionItemClicked(Action.VIEW_INFO) })
+ items += ActionItem(R.attr.menu_info_icon, R.string.messageInfo, { handleActionItemClicked(Action.VIEW_INFO) })
// Resend
if (message.isFailed) {
- items += ActionItem(R.attr.menu_reply_icon, R.string.conversation_context__menu_resend_message, { handleActionItemClicked(Action.RESEND) })
+ items += ActionItem(R.attr.menu_reply_icon, R.string.resend, { handleActionItemClicked(Action.RESEND) })
}
// Resync
if (message.isSyncFailed) {
- items += ActionItem(R.attr.menu_reply_icon, R.string.conversation_context__menu_resync_message, { handleActionItemClicked(Action.RESYNC) })
+ items += ActionItem(R.attr.menu_reply_icon, R.string.resync, { handleActionItemClicked(Action.RESYNC) })
}
- // Save media
- if (message.isMms && (message as MediaMmsMessageRecord).containsMediaSlide()) {
- items += ActionItem(R.attr.menu_save_icon, R.string.conversation_context_image__save_attachment, { handleActionItemClicked(Action.DOWNLOAD) }, R.string.AccessibilityId_save_attachment)
+ // Save media..
+ if (message.isMms) {
+ // ..but only provide the save option if the there is a media attachment which has finished downloading.
+ val mmsMessage = message as MediaMmsMessageRecord
+ if (mmsMessage.containsMediaSlide() && !mmsMessage.isMediaPending) {
+ items += ActionItem(R.attr.menu_save_icon,
+ R.string.save,
+ { handleActionItemClicked(Action.DOWNLOAD) },
+ R.string.AccessibilityId_saveAttachment
+ )
+ }
}
backgroundView.visibility = VISIBLE
foregroundView.visibility = VISIBLE
@@ -704,10 +710,6 @@ class ConversationReactionOverlay : FrameLayout {
}
}
-private fun Duration.to2partString(): String? =
- toComponents { days, hours, minutes, seconds, nanoseconds -> listOf(days.days, hours.hours, minutes.minutes, seconds.seconds) }
- .filter { it.inWholeSeconds > 0L }.take(2).takeIf { it.isNotEmpty() }?.joinToString(" ")
-
private val MessageRecord.subtitle: ((Context) -> CharSequence?)?
get() = if (expiresIn <= 0) {
null
@@ -715,6 +717,10 @@ private val MessageRecord.subtitle: ((Context) -> CharSequence?)?
(expiresIn - (SnodeAPI.nowWithOffset - (expireStarted.takeIf { it > 0 } ?: timestamp)))
.coerceAtLeast(0L)
.milliseconds
- .to2partString()
- ?.let { context.getString(R.string.auto_deletes_in, it) }
+ .toShortTwoPartString()
+ .let {
+ Phrase.from(context, R.string.disappearingMessagesCountdownBigMobile)
+ .put(TIME_LARGE_KEY, it)
+ .format().toString()
+ }
}
\ No newline at end of file
diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationViewModel.kt
index b0a541a9e8..514dc24ea6 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationViewModel.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationViewModel.kt
@@ -9,8 +9,12 @@ import dagger.assisted.Assisted
import dagger.assisted.AssistedInject
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.filterNotNull
+import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import org.session.libsession.database.MessageDataProvider
@@ -29,6 +33,7 @@ import org.thoughtcrime.securesms.audio.AudioSlidePlayer
import org.thoughtcrime.securesms.database.Storage
import org.thoughtcrime.securesms.database.model.MessageRecord
import org.thoughtcrime.securesms.database.model.MmsMessageRecord
+import org.thoughtcrime.securesms.groups.OpenGroupManager
import org.thoughtcrime.securesms.repository.ConversationRepository
import java.util.UUID
@@ -65,6 +70,8 @@ class ConversationViewModel(
}
}
+ private var communityWriteAccessJob: Job? = null
+
private var _openGroup: RetrieveOnce = RetrieveOnce {
storage.getOpenGroup(threadId)
}
@@ -105,6 +112,27 @@ class ConversationViewModel(
}
}
}
+
+ // listen to community write access updates from this point
+ communityWriteAccessJob?.cancel()
+ communityWriteAccessJob = viewModelScope.launch {
+ OpenGroupManager.getCommunitiesWriteAccessFlow()
+ .map {
+ if(openGroup?.groupId != null)
+ it[openGroup?.groupId]
+ else null
+ }
+ .filterNotNull()
+ .collect{
+ // update our community object
+ _openGroup.updateTo(openGroup?.copy(canWrite = it))
+ // when we get an update on the write access of a community
+ // we need to update the input text accordingly
+ _uiState.update { state ->
+ state.copy(hideInputBar = shouldHideInputBar())
+ }
+ }
+ }
}
override fun onCleared() {
@@ -267,7 +295,7 @@ class ConversationViewModel(
* - We are dealing with a contact from a community (blinded recipient) that does not allow
* requests form community members
*/
- fun hidesInputBar(): Boolean = openGroup?.canWrite == false ||
+ fun shouldHideInputBar(): Boolean = openGroup?.canWrite == false ||
blindedRecipient?.blocksCommunityMessageRequests == true
fun legacyBannerRecipient(context: Context): Recipient? = recipient?.run {
@@ -311,7 +339,8 @@ data class UiMessage(val id: Long, val message: String)
data class ConversationUiState(
val uiMessages: List = emptyList(),
val isMessageRequestAccepted: Boolean? = null,
- val conversationExists: Boolean
+ val conversationExists: Boolean,
+ val hideInputBar: Boolean = false
)
data class RetrieveOnce(val retrieval: () -> T?) {
diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/DeleteOptionsBottomSheet.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/DeleteOptionsBottomSheet.kt
index ca5b1cec11..58c5536248 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/DeleteOptionsBottomSheet.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/DeleteOptionsBottomSheet.kt
@@ -58,7 +58,7 @@ class DeleteOptionsBottomSheet : BottomSheetDialogFragment(), View.OnClickListen
}
if (!recipient.isGroupRecipient && !contact.isNullOrEmpty()) {
binding.deleteForEveryoneTextView.text =
- resources.getString(R.string.delete_message_for_me_and_recipient, contact)
+ resources.getString(R.string.clearMessagesForEveryone, contact)
}
binding.deleteForEveryoneTextView.isVisible = !recipient.isClosedGroupRecipient
binding.deleteForMeTextView.setOnClickListener(this)
diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/MessageDetailActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/MessageDetailActivity.kt
index 9514552d28..bd491bbe70 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/MessageDetailActivity.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/MessageDetailActivity.kt
@@ -2,6 +2,7 @@ package org.thoughtcrime.securesms.conversation.v2
import android.annotation.SuppressLint
import android.content.Intent
+import android.net.Uri
import android.os.Bundle
import android.view.LayoutInflater
import android.view.MotionEvent.ACTION_UP
@@ -15,10 +16,13 @@ import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ExperimentalLayoutApi
import androidx.compose.foundation.layout.FlowRow
import androidx.compose.foundation.layout.IntrinsicSize
+import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.widthIn
import androidx.compose.foundation.pager.HorizontalPager
@@ -28,6 +32,7 @@ import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.Icon
+import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
@@ -35,6 +40,7 @@ import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.res.painterResource
@@ -42,6 +48,7 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
+import androidx.compose.ui.unit.dp
import androidx.compose.ui.viewinterop.AndroidView
import androidx.lifecycle.lifecycleScope
import com.bumptech.glide.integration.compose.ExperimentalGlideComposeApi
@@ -54,27 +61,26 @@ import org.session.libsession.messaging.sending_receiving.attachments.DatabaseAt
import org.thoughtcrime.securesms.MediaPreviewActivity.getPreviewIntent
import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity
import org.thoughtcrime.securesms.database.Storage
+import org.thoughtcrime.securesms.database.model.MediaMmsMessageRecord
import org.thoughtcrime.securesms.ui.Avatar
import org.thoughtcrime.securesms.ui.CarouselNextButton
import org.thoughtcrime.securesms.ui.CarouselPrevButton
import org.thoughtcrime.securesms.ui.Cell
-import org.thoughtcrime.securesms.ui.CellNoMargin
-import org.thoughtcrime.securesms.ui.CellWithPaddingAndMargin
import org.thoughtcrime.securesms.ui.Divider
import org.thoughtcrime.securesms.ui.GetString
import org.thoughtcrime.securesms.ui.HorizontalPagerIndicator
import org.thoughtcrime.securesms.ui.LargeItemButton
+import org.thoughtcrime.securesms.ui.TitledText
+import org.thoughtcrime.securesms.ui.setComposeContent
+import org.thoughtcrime.securesms.ui.theme.LocalColors
import org.thoughtcrime.securesms.ui.theme.LocalDimensions
+import org.thoughtcrime.securesms.ui.theme.LocalType
import org.thoughtcrime.securesms.ui.theme.PreviewTheme
import org.thoughtcrime.securesms.ui.theme.SessionColorsParameterProvider
-import org.thoughtcrime.securesms.ui.TitledText
import org.thoughtcrime.securesms.ui.theme.ThemeColors
-import org.thoughtcrime.securesms.ui.theme.LocalColors
import org.thoughtcrime.securesms.ui.theme.blackAlpha40
-import org.thoughtcrime.securesms.ui.theme.dangerButtonColors
-import org.thoughtcrime.securesms.ui.setComposeContent
-import org.thoughtcrime.securesms.ui.theme.LocalType
import org.thoughtcrime.securesms.ui.theme.bold
+import org.thoughtcrime.securesms.ui.theme.dangerButtonColors
import org.thoughtcrime.securesms.ui.theme.monospace
import javax.inject.Inject
@@ -93,12 +99,14 @@ class MessageDetailActivity : PassphraseRequiredActionBarActivity() {
const val ON_REPLY = 1
const val ON_RESEND = 2
const val ON_DELETE = 3
+ const val ON_COPY = 4
+ const val ON_SAVE = 5
}
override fun onCreate(savedInstanceState: Bundle?, ready: Boolean) {
super.onCreate(savedInstanceState, ready)
- title = resources.getString(R.string.conversation_context__menu_message_details)
+ title = resources.getString(R.string.messageInfo)
viewModel.timestamp = intent.getLongExtra(MESSAGE_TIMESTAMP, -1L)
@@ -119,11 +127,18 @@ class MessageDetailActivity : PassphraseRequiredActionBarActivity() {
@Composable
private fun MessageDetailsScreen() {
val state by viewModel.stateFlow.collectAsState()
+
+ // can only save if the there is a media attachment which has finished downloading.
+ val canSave = state.mmsRecord?.containsMediaSlide() == true
+ && state.mmsRecord?.isMediaPending == false
+
MessageDetails(
state = state,
onReply = if (state.canReply) { { setResultAndFinish(ON_REPLY) } } else null,
onResend = state.error?.let { { setResultAndFinish(ON_RESEND) } },
+ onSave = if(canSave) { { setResultAndFinish(ON_SAVE) } } else null,
onDelete = { setResultAndFinish(ON_DELETE) },
+ onCopy = { setResultAndFinish(ON_COPY) },
onClickImage = { viewModel.onClickImage(it) },
onAttachmentNeedsDownload = viewModel::onAttachmentNeedsDownload,
)
@@ -144,7 +159,9 @@ fun MessageDetails(
state: MessageDetailsState,
onReply: (() -> Unit)? = null,
onResend: (() -> Unit)? = null,
+ onSave: (() -> Unit)? = null,
onDelete: () -> Unit = {},
+ onCopy: () -> Unit = {},
onClickImage: (Int) -> Unit = {},
onAttachmentNeedsDownload: (DatabaseAttachment) -> Unit = { _ -> }
) {
@@ -178,9 +195,11 @@ fun MessageDetails(
state.nonImageAttachmentFileDetails?.let { FileDetails(it) }
CellMetadata(state)
CellButtons(
- onReply,
- onResend,
- onDelete,
+ onReply = onReply,
+ onResend = onResend,
+ onSave = onSave,
+ onDelete = onDelete,
+ onCopy = onCopy
)
}
}
@@ -191,15 +210,26 @@ fun CellMetadata(
) {
state.apply {
if (listOfNotNull(sent, received, error, senderInfo).isEmpty()) return
- CellWithPaddingAndMargin {
- Column(verticalArrangement = Arrangement.spacedBy(LocalDimensions.current.smallSpacing)) {
+ Cell(modifier = Modifier.padding(horizontal = LocalDimensions.current.spacing)) {
+ Column(
+ modifier = Modifier.padding(LocalDimensions.current.spacing),
+ verticalArrangement = Arrangement.spacedBy(LocalDimensions.current.smallSpacing)
+ ) {
TitledText(sent)
TitledText(received)
TitledErrorText(error)
senderInfo?.let {
TitledView(state.fromTitle) {
Row {
- sender?.let { Avatar(it) }
+ sender?.let {
+ Avatar(
+ recipient = it,
+ modifier = Modifier
+ .align(Alignment.CenterVertically)
+ .size(46.dp)
+ )
+ Spacer(modifier = Modifier.width(LocalDimensions.current.smallSpacing))
+ }
TitledMonospaceText(it)
}
}
@@ -213,9 +243,11 @@ fun CellMetadata(
fun CellButtons(
onReply: (() -> Unit)? = null,
onResend: (() -> Unit)? = null,
- onDelete: () -> Unit = {},
+ onSave: (() -> Unit)? = null,
+ onDelete: () -> Unit,
+ onCopy: () -> Unit
) {
- Cell {
+ Cell(modifier = Modifier.padding(horizontal = LocalDimensions.current.spacing)) {
Column {
onReply?.let {
LargeItemButton(
@@ -225,6 +257,23 @@ fun CellButtons(
)
Divider()
}
+
+ LargeItemButton(
+ R.string.copy,
+ R.drawable.ic_copy,
+ onClick = onCopy
+ )
+ Divider()
+
+ onSave?.let {
+ LargeItemButton(
+ R.string.save,
+ R.drawable.ic_baseline_save_24,
+ onClick = it
+ )
+ Divider()
+ }
+
onResend?.let {
LargeItemButton(
R.string.resend,
@@ -233,9 +282,10 @@ fun CellButtons(
)
Divider()
}
+
LargeItemButton(
R.string.delete,
- R.drawable.ic_message_details__trash,
+ R.drawable.ic_delete,
colors = dangerButtonColors(),
onClick = onDelete
)
@@ -254,8 +304,11 @@ fun Carousel(attachments: List, onClick: (Int) -> Unit) {
Row {
CarouselPrevButton(pagerState)
Box(modifier = Modifier.weight(1f)) {
- CellCarousel(pagerState, attachments, onClick)
- HorizontalPagerIndicator(pagerState)
+ CarouselPager(pagerState, attachments, onClick)
+ HorizontalPagerIndicator(
+ pagerState = pagerState,
+ modifier = Modifier.padding(bottom = LocalDimensions.current.xxsSpacing)
+ )
ExpandButton(
modifier = Modifier
.align(Alignment.BottomEnd)
@@ -273,12 +326,15 @@ fun Carousel(attachments: List, onClick: (Int) -> Unit) {
ExperimentalGlideComposeApi::class
)
@Composable
-private fun CellCarousel(
+private fun CarouselPager(
pagerState: PagerState,
attachments: List,
onClick: (Int) -> Unit
) {
- CellNoMargin {
+ Cell(
+ modifier = Modifier
+ .clip(MaterialTheme.shapes.small)
+ ) {
HorizontalPager(state = pagerState) { i ->
GlideImage(
contentScale = ContentScale.Crop,
@@ -302,12 +358,27 @@ fun ExpandButton(modifier: Modifier = Modifier, onClick: () -> Unit) {
) {
Icon(
painter = painterResource(id = R.drawable.ic_expand),
- contentDescription = stringResource(id = R.string.expand),
+ contentDescription = stringResource(id = R.string.AccessibilityId_expand),
modifier = Modifier.clickable { onClick() },
)
}
}
+@Preview
+@Composable
+fun PreviewMessageDetailsButtons(
+ @PreviewParameter(SessionColorsParameterProvider::class) colors: ThemeColors
+) {
+ PreviewTheme(colors) {
+ CellButtons(
+ onReply = {},
+ onResend = {},
+ onSave = {},
+ onDelete = {},
+ onCopy = {}
+ )
+ }
+}
@Preview
@Composable
@@ -317,15 +388,42 @@ fun PreviewMessageDetails(
PreviewTheme(colors) {
MessageDetails(
state = MessageDetailsState(
- nonImageAttachmentFileDetails = listOf(
- TitledText(R.string.message_details_header__file_id, "Screen Shot 2023-07-06 at 11.35.50 am.png"),
- TitledText(R.string.message_details_header__file_type, "image/png"),
- TitledText(R.string.message_details_header__file_size, "195.6kB"),
- TitledText(R.string.message_details_header__resolution, "342x312"),
+ imageAttachments = listOf(
+ Attachment(
+ fileDetails = listOf(
+ TitledText(R.string.attachmentsFileId, "Screen Shot 2023-07-06 at 11.35.50 am.png")
+ ),
+ fileName = "Screen Shot 2023-07-06 at 11.35.50 am.png",
+ uri = Uri.parse(""),
+ hasImage = true
+ ),
+ Attachment(
+ fileDetails = listOf(
+ TitledText(R.string.attachmentsFileId, "Screen Shot 2023-07-06 at 11.35.50 am.png")
+ ),
+ fileName = "Screen Shot 2023-07-06 at 11.35.50 am.png",
+ uri = Uri.parse(""),
+ hasImage = true
+ ),
+ Attachment(
+ fileDetails = listOf(
+ TitledText(R.string.attachmentsFileId, "Screen Shot 2023-07-06 at 11.35.50 am.png")
+ ),
+ fileName = "Screen Shot 2023-07-06 at 11.35.50 am.png",
+ uri = Uri.parse(""),
+ hasImage = true
+ )
+
),
- sent = TitledText(R.string.message_details_header__sent, "6:12 AM Tue, 09/08/2022"),
- received = TitledText(R.string.message_details_header__received, "6:12 AM Tue, 09/08/2022"),
- error = TitledText(R.string.message_details_header__error, "Message failed to send"),
+ nonImageAttachmentFileDetails = listOf(
+ TitledText(R.string.attachmentsFileId, "Screen Shot 2023-07-06 at 11.35.50 am.png"),
+ TitledText(R.string.attachmentsFileType, "image/png"),
+ TitledText(R.string.attachmentsFileSize, "195.6kB"),
+ TitledText(R.string.attachmentsResolution, "342x312"),
+ ),
+ sent = TitledText(R.string.sent, "6:12 AM Tue, 09/08/2022"),
+ received = TitledText(R.string.received, "6:12 AM Tue, 09/08/2022"),
+ error = TitledText(R.string.error, "Message failed to send"),
senderInfo = TitledText("Connor", "d4f1g54sdf5g1d5f4g65ds4564df65f4g65d54"),
)
)
@@ -337,7 +435,7 @@ fun PreviewMessageDetails(
fun FileDetails(fileDetails: List) {
if (fileDetails.isEmpty()) return
- Cell {
+ Cell(modifier = Modifier.padding(horizontal = LocalDimensions.current.spacing)) {
FlowRow(
modifier = Modifier.padding(horizontal = LocalDimensions.current.xsSpacing, vertical = LocalDimensions.current.spacing),
verticalArrangement = Arrangement.spacedBy(LocalDimensions.current.smallSpacing)
diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/MessageDetailsViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/MessageDetailsViewModel.kt
index fc54b652ae..fcaca71c6a 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/MessageDetailsViewModel.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/MessageDetailsViewModel.kt
@@ -78,9 +78,9 @@ class MessageDetailsViewModel @Inject constructor(
MessageDetailsState(
attachments = slides.map(::Attachment),
record = record,
- sent = dateSent.let(::Date).toString().let { TitledText(R.string.message_details_header__sent, it) },
- received = dateReceived.let(::Date).toString().let { TitledText(R.string.message_details_header__received, it) },
- error = lokiMessageDatabase.getErrorMessage(id)?.let { TitledText(R.string.message_details_header__error, it) },
+ sent = dateSent.let(::Date).toString().let { TitledText(R.string.sent, it) },
+ received = dateReceived.let(::Date).toString().let { TitledText(R.string.received, it) },
+ error = lokiMessageDatabase.getErrorMessage(id)?.let { TitledText(R.string.theError, it) },
senderInfo = individualRecipient.run { name?.let { TitledText(it, address.serialize()) } },
sender = individualRecipient,
thread = threadDb.getRecipientForThreadId(threadId)!!,
@@ -90,14 +90,14 @@ class MessageDetailsViewModel @Inject constructor(
private val Slide.details: List
get() = listOfNotNull(
- fileName.orNull()?.let { TitledText(R.string.message_details_header__file_id, it) },
- TitledText(R.string.message_details_header__file_type, asAttachment().contentType),
- TitledText(R.string.message_details_header__file_size, Util.getPrettyFileSize(fileSize)),
+ fileName.orNull()?.let { TitledText(R.string.attachmentsFileId, it) },
+ TitledText(R.string.attachmentsFileType, asAttachment().contentType),
+ TitledText(R.string.attachmentsFileSize, Util.getPrettyFileSize(fileSize)),
takeIf { it is ImageSlide }
?.let(Slide::asAttachment)
?.run { "${width}x$height" }
- ?.let { TitledText(R.string.message_details_header__resolution, it) },
- attachmentDb.duration(this)?.let { TitledText(R.string.message_details_header__duration, it) },
+ ?.let { TitledText(R.string.attachmentsResolution, it) },
+ attachmentDb.duration(this)?.let { TitledText(R.string.attachmentsDuration, it) },
)
private fun AttachmentDatabase.duration(slide: Slide): String? =
@@ -157,7 +157,7 @@ data class MessageDetailsState(
val sender: Recipient? = null,
val thread: Recipient? = null,
) {
- val fromTitle = GetString(R.string.message_details_header__from)
+ val fromTitle = GetString(R.string.from)
val canReply = record?.isOpenGroupInvitation != true
}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ModalUrlBottomSheet.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ModalUrlBottomSheet.kt
index 54deea1c8d..b31c298f26 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ModalUrlBottomSheet.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ModalUrlBottomSheet.kt
@@ -15,9 +15,12 @@ import android.view.View
import android.view.ViewGroup
import android.widget.Toast
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
+import com.squareup.phrase.Phrase
import network.loki.messenger.R
import network.loki.messenger.databinding.FragmentModalUrlBottomSheetBinding
-import org.thoughtcrime.securesms.util.UiModeUtilities
+import org.session.libsession.utilities.StringSubstitutionConstants.URL_KEY
+import org.session.libsignal.utilities.Log
+import org.thoughtcrime.securesms.ui.getSubbedString
class ModalUrlBottomSheet(private val url: String): BottomSheetDialogFragment(), View.OnClickListener {
private lateinit var binding: FragmentModalUrlBottomSheetBinding
@@ -29,7 +32,8 @@ class ModalUrlBottomSheet(private val url: String): BottomSheetDialogFragment(),
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
- val explanation = resources.getString(R.string.dialog_open_url_explanation, url)
+ if (context == null) { return Log.w("MUBS", "Context is null") }
+ val explanation = requireContext().getSubbedString(R.string.urlOpenDescription, URL_KEY to url)
val spannable = SpannableStringBuilder(explanation)
val startIndex = explanation.indexOf(url)
spannable.setSpan(StyleSpan(Typeface.BOLD), startIndex, startIndex + url.count(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
@@ -44,7 +48,7 @@ class ModalUrlBottomSheet(private val url: String): BottomSheetDialogFragment(),
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()
+ Toast.makeText(context, R.string.communityEnterUrlErrorInvalid, Toast.LENGTH_SHORT).show()
}
dismiss()
}
@@ -53,7 +57,7 @@ class ModalUrlBottomSheet(private val url: String): BottomSheetDialogFragment(),
val clip = ClipData.newPlainText("URL", url)
val manager = requireContext().getSystemService(CLIPBOARD_SERVICE) as ClipboardManager
manager.setPrimaryClip(clip)
- Toast.makeText(requireContext(), R.string.copied_to_clipboard, Toast.LENGTH_SHORT).show()
+ Toast.makeText(requireContext(), R.string.copied, Toast.LENGTH_SHORT).show()
dismiss()
}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/Util.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/Util.kt
index da8852d1d6..f2b19f539b 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/Util.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/Util.kt
@@ -17,22 +17,12 @@
package org.thoughtcrime.securesms.conversation.v2
import android.content.Context
-import android.graphics.Typeface
import android.net.Uri
-import android.text.Spannable
-import android.text.SpannableString
import android.text.TextUtils
-import android.text.style.StyleSpan
import android.view.View
import com.annimon.stream.Stream
-import com.google.android.mms.pdu_alt.CharacterSets
-import com.google.android.mms.pdu_alt.EncodedStringValue
-import org.session.libsignal.utilities.Log
-import org.thoughtcrime.securesms.components.ComposeText
-import java.io.ByteArrayOutputStream
-import java.io.IOException
-import java.io.UnsupportedEncodingException
import java.util.Collections
+import org.session.libsignal.utilities.Log
object Util {
private val TAG: String = Log.tag(Util::class.java)
@@ -92,22 +82,6 @@ object Util {
return sb.toString()
}
- fun isEmpty(value: Array?): Boolean {
- return value == null || value.size == 0
- }
-
- fun isEmpty(value: ComposeText?): Boolean {
- return value == null || value.text == null || TextUtils.isEmpty(value.textTrimmed)
- }
-
- fun isEmpty(collection: Collection<*>?): Boolean {
- return collection == null || collection.isEmpty()
- }
-
- fun isEmpty(charSequence: CharSequence?): Boolean {
- return charSequence == null || charSequence.length == 0
- }
-
fun wait(lock: Any, timeout: Long) {
try {
(lock as Object).wait(timeout)
@@ -123,8 +97,7 @@ object Util {
return results
}
- val elements =
- source.split(delimiter.toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()
+ val elements = source.split(delimiter.toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()
Collections.addAll(results, *elements)
return results
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
index dba6bf5b7b..0b61dec9d4 100644
--- 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
@@ -11,10 +11,12 @@ import android.widget.RelativeLayout
import android.widget.TextView
import androidx.core.view.children
import androidx.core.view.isVisible
+import com.squareup.phrase.Phrase
import network.loki.messenger.R
import network.loki.messenger.databinding.AlbumThumbnailViewBinding
import org.session.libsession.messaging.sending_receiving.attachments.AttachmentTransferProgress
import org.session.libsession.messaging.sending_receiving.attachments.DatabaseAttachment
+import org.session.libsession.utilities.StringSubstitutionConstants.COUNT_KEY
import org.session.libsession.utilities.recipients.Recipient
import org.thoughtcrime.securesms.MediaPreviewActivity
import org.thoughtcrime.securesms.components.CornerMask
@@ -97,7 +99,10 @@ class AlbumThumbnailView : RelativeLayout {
binding.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)
+ val txt = Phrase.from(context, R.string.andMore)
+ .put(COUNT_KEY, slides.size - MAX_ALBUM_DISPLAY_SIZE)
+ .format()
+ overflowText.text = txt
}
this.slideSize = slides.size
}
@@ -110,10 +115,9 @@ class AlbumThumbnailView : RelativeLayout {
// endregion
-
fun layoutRes(slideCount: Int) = when (slideCount) {
- 1 -> R.layout.album_thumbnail_1 // single
- 2 -> R.layout.album_thumbnail_2// two sidebyside
+ 1 -> R.layout.album_thumbnail_1 // single
+ 2 -> R.layout.album_thumbnail_2 // two side-by-side
else -> R.layout.album_thumbnail_3 // three stacked with additional text
}
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
index 46feefb608..9113f8ed46 100644
--- 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
@@ -11,9 +11,11 @@ import androidx.fragment.app.DialogFragment
import network.loki.messenger.R
import org.session.libsession.messaging.MessagingModuleConfiguration
import org.session.libsession.messaging.contacts.Contact
+import org.session.libsession.utilities.StringSubstitutionConstants.NAME_KEY
import org.session.libsession.utilities.recipients.Recipient
import org.thoughtcrime.securesms.createSessionDialog
import org.thoughtcrime.securesms.dependencies.DatabaseComponent
+import org.thoughtcrime.securesms.ui.getSubbedCharSequence
/** Shown upon sending a message to a user that's blocked. */
class BlockedDialog(private val recipient: Recipient, private val context: Context) : DialogFragment() {
@@ -24,14 +26,14 @@ class BlockedDialog(private val recipient: Recipient, private val context: Conte
val contact = contactDB.getContactWithAccountID(accountID)
val name = contact?.displayName(Contact.ContactContext.REGULAR) ?: accountID
- val explanation = resources.getString(R.string.dialog_blocked_explanation, name)
- val spannable = SpannableStringBuilder(explanation)
- val startIndex = explanation.indexOf(name)
+ val explanationCS = context.getSubbedCharSequence(R.string.blockUnblockName, NAME_KEY to name)
+ val spannable = SpannableStringBuilder(explanationCS)
+ val startIndex = explanationCS.indexOf(name)
spannable.setSpan(StyleSpan(Typeface.BOLD), startIndex, startIndex + name.count(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
- title(resources.getString(R.string.dialog_blocked_title, name))
+ title(resources.getString(R.string.blockUnblock))
text(spannable)
- button(R.string.ConversationActivity_unblock) { unblock() }
+ dangerButton(R.string.blockUnblock, R.string.AccessibilityId_unblockConfirm) { unblock() }
cancelButton { dismiss() }
}
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
index 1af1d669cf..d3e1de9912 100644
--- 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
@@ -7,12 +7,14 @@ import android.text.Spannable
import android.text.SpannableStringBuilder
import android.text.style.StyleSpan
import androidx.fragment.app.DialogFragment
+import com.squareup.phrase.Phrase
import dagger.hilt.android.AndroidEntryPoint
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.session.libsession.utilities.StringSubstitutionConstants.CONVERSATION_NAME_KEY
import org.thoughtcrime.securesms.createSessionDialog
import org.thoughtcrime.securesms.database.SessionContactDatabase
import org.thoughtcrime.securesms.dependencies.DatabaseComponent
@@ -29,15 +31,19 @@ class DownloadDialog(private val recipient: Recipient) : DialogFragment() {
val accountID = recipient.address.toString()
val contact = contactDB.getContactWithAccountID(accountID)
val name = contact?.displayName(Contact.ContactContext.REGULAR) ?: accountID
- title(resources.getString(R.string.dialog_download_title, name))
- val explanation = resources.getString(R.string.dialog_download_explanation, name)
+ title(getString(R.string.attachmentsAutoDownloadModalTitle))
+
+ val explanation = Phrase.from(context, R.string.attachmentsAutoDownloadModalDescription)
+ .put(CONVERSATION_NAME_KEY, recipient.name)
+ .format()
val spannable = SpannableStringBuilder(explanation)
+
val startIndex = explanation.indexOf(name)
spannable.setSpan(StyleSpan(Typeface.BOLD), startIndex, startIndex + name.count(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
text(spannable)
- button(R.string.dialog_download_button_title, R.string.AccessibilityId_download_media) { trust() }
+ button(R.string.download, R.string.AccessibilityId_download) { trust() }
cancelButton { dismiss() }
}
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
index a886e89192..21405b26c5 100644
--- 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
@@ -1,5 +1,6 @@
package org.thoughtcrime.securesms.conversation.v2.dialogs
+import org.thoughtcrime.securesms.createSessionDialog
import android.app.Dialog
import android.graphics.Typeface
import android.os.Bundle
@@ -8,11 +9,13 @@ import android.text.SpannableStringBuilder
import android.text.style.StyleSpan
import android.widget.Toast
import androidx.fragment.app.DialogFragment
+import com.squareup.phrase.Phrase
import network.loki.messenger.R
import org.session.libsession.messaging.MessagingModuleConfiguration
import org.session.libsession.utilities.OpenGroupUrlParser
+import org.session.libsession.utilities.StringSubstitutionConstants.COMMUNITY_NAME_KEY
+import org.session.libsignal.utilities.Log
import org.session.libsignal.utilities.ThreadUtils
-import org.thoughtcrime.securesms.createSessionDialog
import org.thoughtcrime.securesms.groups.OpenGroupManager
import org.thoughtcrime.securesms.util.ConfigurationMessageUtilities
@@ -20,14 +23,18 @@ import org.thoughtcrime.securesms.util.ConfigurationMessageUtilities
class JoinOpenGroupDialog(private val name: String, private val url: String) : DialogFragment() {
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog = createSessionDialog {
- title(resources.getString(R.string.dialog_join_open_group_title, name))
- val explanation = resources.getString(R.string.dialog_join_open_group_explanation, name)
+ title(resources.getString(R.string.communityJoin))
+ val explanation = Phrase.from(context, R.string.communityJoinDescription).put(COMMUNITY_NAME_KEY, name).format()
val spannable = SpannableStringBuilder(explanation)
- val startIndex = explanation.indexOf(name)
+ var startIndex = explanation.indexOf(name)
+ if (startIndex < 0) {
+ Log.w("JoinOpenGroupDialog", "Could not find $name in explanation dialog: $explanation")
+ startIndex = 0 // Limit the startIndex to zero if not found (will be -1) to prevent a crash
+ }
spannable.setSpan(StyleSpan(Typeface.BOLD), startIndex, startIndex + name.count(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
text(spannable)
cancelButton { dismiss() }
- button(R.string.open_group_invitation_view__join_accessibility_description) { join() }
+ button(R.string.join) { join() }
}
private fun join() {
@@ -39,7 +46,7 @@ class JoinOpenGroupDialog(private val name: String, private val url: String) : D
MessagingModuleConfiguration.shared.storage.onOpenGroupAdded(openGroup.server, openGroup.room)
ConfigurationMessageUtilities.forceSyncConfigurationNowIfNeeded(activity)
} catch (e: Exception) {
- Toast.makeText(activity, R.string.activity_join_public_chat_error, Toast.LENGTH_SHORT).show()
+ Toast.makeText(activity, R.string.communityErrorDescription, Toast.LENGTH_SHORT).show()
}
}
dismiss()
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
index 996dd41f94..d9e6e22a4a 100644
--- 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
@@ -4,18 +4,22 @@ import android.app.Dialog
import android.os.Bundle
import androidx.fragment.app.DialogFragment
import network.loki.messenger.R
+import org.session.libsession.utilities.NonTranslatableStringConstants.APP_NAME
+import org.session.libsession.utilities.StringSubstitutionConstants.APP_NAME_KEY
import org.session.libsession.utilities.TextSecurePreferences
import org.thoughtcrime.securesms.createSessionDialog
+import org.thoughtcrime.securesms.ui.getSubbedCharSequence
/** 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) : DialogFragment() {
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog = createSessionDialog {
- title(R.string.dialog_link_preview_title)
- text(R.string.dialog_link_preview_explanation)
- button(R.string.dialog_link_preview_enable_button_title) { enable() }
- cancelButton { dismiss() }
+ title(R.string.linkPreviewsEnable)
+ val txt = context.getSubbedCharSequence(R.string.linkPreviewsFirstDescription, APP_NAME_KEY to APP_NAME)
+ text(txt)
+ dangerButton(R.string.enable) { enable() }
+ cancelButton { dismiss() }
}
private fun enable() {
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
index c8aacdeb6e..cd911b2ace 100644
--- 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
@@ -79,9 +79,9 @@ class InputBar @JvmOverloads constructor(
var voiceMessageDurationMS = 0L
var voiceRecorderState = VoiceRecorderState.Idle
- private val attachmentsButton = InputBarButton(context, R.drawable.ic_plus_24).apply { contentDescription = context.getString(R.string.AccessibilityId_attachments_button)}
- val microphoneButton = InputBarButton(context, R.drawable.ic_microphone).apply { contentDescription = context.getString(R.string.AccessibilityId_microphone_button)}
- private val sendButton = InputBarButton(context, R.drawable.ic_arrow_up, true).apply { contentDescription = context.getString(R.string.AccessibilityId_send_message_button)}
+ private val attachmentsButton = InputBarButton(context, R.drawable.ic_plus_24).apply { contentDescription = context.getString(R.string.AccessibilityId_attachmentsButton)}
+ val microphoneButton = InputBarButton(context, R.drawable.ic_microphone).apply { contentDescription = context.getString(R.string.AccessibilityId_voiceMessageNew)}
+ private val sendButton = InputBarButton(context, R.drawable.ic_arrow_up, true).apply { contentDescription = context.getString(R.string.AccessibilityId_send)}
init {
// Attachments button
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
index 24b48ecdf7..f245dcadf4 100644
--- 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
@@ -19,7 +19,6 @@ import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
import network.loki.messenger.R
import network.loki.messenger.databinding.ViewInputBarRecordingBinding
-import org.thoughtcrime.securesms.util.DateUtils
import org.thoughtcrime.securesms.util.animateSizeChange
import org.thoughtcrime.securesms.util.disableClipping
import org.thoughtcrime.securesms.util.toPx
@@ -106,8 +105,7 @@ class InputBarRecordingView : RelativeLayout {
timerJob = scope.launch {
while (isActive) {
val duration = (Date().time - startTimestamp) / 1000L
- binding.recordingViewDurationTextView.text = DateUtils.formatElapsedTime(duration)
-
+ binding.recordingViewDurationTextView.text = android.text.format.DateUtils.formatElapsedTime(duration)
delay(500)
}
}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/mention/MentionViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/mention/MentionViewModel.kt
index d4068a3e6c..e3e5df0458 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/mention/MentionViewModel.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/mention/MentionViewModel.kt
@@ -202,13 +202,17 @@ class MentionViewModel(
val sb = StringBuilder()
var offset = 0
for ((span, range) in spansWithRanges) {
- // Add content before the mention span
- sb.append(editable, offset, range.first)
+ // Add content before the mention span. There's a possibility of overlapping spans so we need to
+ // safe guard the start offset here to not go over our span's start.
+ val thisMentionStart = range.first
+ val lastMentionEnd = offset.coerceAtMost(thisMentionStart)
+ sb.append(editable, lastMentionEnd, thisMentionStart)
// Replace the mention span with "@public key"
sb.append('@').append(span.member.publicKey).append(' ')
- offset = range.last + 1
+ // Safe guard offset to not go over the end of the editable.
+ offset = (range.last + 1).coerceAtMost(editable.length)
}
// Add the remaining content
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
index c7862ca22e..21d5de52cf 100644
--- 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
@@ -40,6 +40,8 @@ class ConversationActionModeCallback(private val adapter: ConversationAdapter, p
val edKeyPair = MessagingModuleConfiguration.shared.getUserED25519KeyPair()!!
val blindedPublicKey = openGroup?.publicKey?.let { SodiumUtilities.blindedKeyPair(it, edKeyPair)?.publicKey?.asBytes }
?.let { AccountId(IdPrefix.BLINDED, it) }?.hexString
+
+ // Embedded function
fun userCanDeleteSelectedItems(): Boolean {
val allSentByCurrentUser = selectedItems.all { it.isOutgoing }
val allReceivedByCurrentUser = selectedItems.all { !it.isOutgoing }
@@ -47,6 +49,8 @@ class ConversationActionModeCallback(private val adapter: ConversationAdapter, p
if (allSentByCurrentUser) { return true }
return OpenGroupManager.isUserModerator(context, openGroup.groupId, userPublicKey, blindedPublicKey)
}
+
+ // Embedded function
fun userCanBanSelectedUsers(): Boolean {
if (openGroup == null) { return false }
val anySentByCurrentUser = selectedItems.any { it.isOutgoing }
@@ -55,6 +59,9 @@ class ConversationActionModeCallback(private val adapter: ConversationAdapter, p
if (selectedUsers.size > 1) { return false }
return OpenGroupManager.isUserModerator(context, openGroup.groupId, userPublicKey, blindedPublicKey)
}
+
+
+
// Delete message
menu.findItem(R.id.menu_context_delete_message).isVisible = userCanDeleteSelectedItems()
// Ban user
@@ -95,7 +102,7 @@ class ConversationActionModeCallback(private val adapter: ConversationAdapter, p
R.id.menu_context_resync -> delegate?.resyncMessage(selectedItems)
R.id.menu_context_resend -> delegate?.resendMessage(selectedItems)
R.id.menu_message_details -> delegate?.showMessageDetail(selectedItems)
- R.id.menu_context_save_attachment -> delegate?.saveAttachment(selectedItems)
+ R.id.menu_context_save_attachment -> delegate?.saveAttachmentsIfPossible(selectedItems)
R.id.menu_context_reply -> delegate?.reply(selectedItems)
}
return true
@@ -119,7 +126,7 @@ interface ConversationActionModeCallbackDelegate {
fun resyncMessage(messages: Set)
fun resendMessage(messages: Set)
fun showMessageDetail(messages: Set)
- fun saveAttachment(messages: Set)
+ fun saveAttachmentsIfPossible(messages: Set)
fun reply(messages: Set)
fun destroyActionMode()
}
\ 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
index 8d018d6813..0997db1871 100644
--- 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
@@ -1,5 +1,6 @@
package org.thoughtcrime.securesms.conversation.v2.menus
+import android.Manifest
import android.annotation.SuppressLint
import android.content.Context
import android.content.Intent
@@ -16,15 +17,18 @@ 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 com.squareup.phrase.Phrase
import network.loki.messenger.R
import org.session.libsession.messaging.sending_receiving.MessageSender
import org.session.libsession.messaging.sending_receiving.leave
import org.session.libsession.utilities.GroupUtil.doubleDecodeGroupID
+import org.session.libsession.utilities.StringSubstitutionConstants.APP_NAME_KEY
+import org.session.libsession.utilities.StringSubstitutionConstants.GROUP_NAME_KEY
import org.session.libsession.utilities.TextSecurePreferences
import org.session.libsession.utilities.recipients.Recipient
+import org.session.libsignal.utilities.Log
import org.session.libsignal.utilities.guava.Optional
import org.session.libsignal.utilities.toHexString
-import org.thoughtcrime.securesms.media.MediaOverviewActivity
import org.thoughtcrime.securesms.ShortcutLauncherActivity
import org.thoughtcrime.securesms.calls.WebRtcCallActivity
import org.thoughtcrime.securesms.contacts.SelectContactsActivity
@@ -33,10 +37,14 @@ import org.thoughtcrime.securesms.conversation.v2.utilities.NotificationUtils
import org.thoughtcrime.securesms.dependencies.DatabaseComponent
import org.thoughtcrime.securesms.groups.EditClosedGroupActivity
import org.thoughtcrime.securesms.groups.EditClosedGroupActivity.Companion.groupIDKey
+import org.thoughtcrime.securesms.media.MediaOverviewActivity
+import org.thoughtcrime.securesms.permissions.Permissions
import org.thoughtcrime.securesms.preferences.PrivacySettingsActivity
import org.thoughtcrime.securesms.service.WebRtcCallService
import org.thoughtcrime.securesms.showMuteDialog
import org.thoughtcrime.securesms.showSessionDialog
+import org.thoughtcrime.securesms.ui.findActivity
+import org.thoughtcrime.securesms.ui.getSubbedString
import org.thoughtcrime.securesms.util.BitmapUtil
import java.io.IOException
@@ -50,11 +58,11 @@ object ConversationMenuHelper {
) {
// Prepare
menu.clear()
- val isOpenGroup = thread.isCommunityRecipient
+ val isCommunity = thread.isCommunityRecipient
// Base menu (options that should always be present)
inflater.inflate(R.menu.menu_conversation, menu)
// Expiring messages
- if (!isOpenGroup && (thread.hasApprovedMe() || thread.isClosedGroupRecipient || thread.isLocalNumber)) {
+ if (!isCommunity && (thread.hasApprovedMe() || thread.isClosedGroupRecipient || thread.isLocalNumber)) {
inflater.inflate(R.menu.menu_conversation_expiration, menu)
}
// One-on-one chat menu allows copying the account id
@@ -74,7 +82,7 @@ object ConversationMenuHelper {
inflater.inflate(R.menu.menu_conversation_closed_group, menu)
}
// Open group menu
- if (isOpenGroup) {
+ if (isCommunity) {
inflater.inflate(R.menu.menu_conversation_open_group, menu)
}
// Muting
@@ -160,17 +168,32 @@ object ConversationMenuHelper {
private fun call(context: Context, thread: Recipient) {
+ // if the user has not enabled voice/video calls
if (!TextSecurePreferences.isCallNotificationsEnabled(context)) {
context.showSessionDialog {
- title(R.string.ConversationActivity_call_title)
- text(R.string.ConversationActivity_call_prompt)
- button(R.string.activity_settings_title, R.string.AccessibilityId_settings) {
+ title(R.string.callsPermissionsRequired)
+ text(R.string.callsPermissionsRequiredDescription)
+ button(R.string.sessionSettings, R.string.AccessibilityId_sessionSettings) {
Intent(context, PrivacySettingsActivity::class.java).let(context::startActivity)
}
cancelButton()
}
return
}
+ // or if the user has not granted audio/microphone permissions
+ else if (!Permissions.hasAll(context, Manifest.permission.RECORD_AUDIO)) {
+ Log.d("Loki", "Attempted to make a call without audio permissions")
+
+ Permissions.with(context.findActivity())
+ .request(Manifest.permission.RECORD_AUDIO)
+ .withPermanentDenialDialog(
+ context.getSubbedString(R.string.permissionsMicrophoneAccessRequired,
+ APP_NAME_KEY to context.getString(R.string.app_name))
+ )
+ .execute()
+
+ return
+ }
WebRtcCallService.createCall(context, thread)
.let(context::startService)
@@ -178,7 +201,6 @@ object ConversationMenuHelper {
Intent(context, WebRtcCallActivity::class.java)
.apply { flags = Intent.FLAG_ACTIVITY_NEW_TASK }
.let(context::startActivity)
-
}
@SuppressLint("StaticFieldLeak")
@@ -215,7 +237,7 @@ object ConversationMenuHelper {
.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()
+ Toast.makeText(context, context.resources.getString(R.string.conversationsAddedToHome), Toast.LENGTH_LONG).show()
}
}
}.execute()
@@ -272,17 +294,26 @@ object ConversationMenuHelper {
val accountID = TextSecurePreferences.getLocalNumber(context)
val isCurrentUserAdmin = admins.any { it.toString() == accountID }
val message = if (isCurrentUserAdmin) {
- "Because you are the creator of this group it will be deleted for everyone. This cannot be undone."
+ Phrase.from(context, R.string.groupDeleteDescription)
+ .put(GROUP_NAME_KEY, group.title)
+ .format()
} else {
- context.resources.getString(R.string.ConversationActivity_are_you_sure_you_want_to_leave_this_group)
+ Phrase.from(context, R.string.groupLeaveDescription)
+ .put(GROUP_NAME_KEY, group.title)
+ .format()
}
- fun onLeaveFailed() = Toast.makeText(context, R.string.ConversationActivity_error_leaving_group, Toast.LENGTH_LONG).show()
+ fun onLeaveFailed() {
+ val txt = Phrase.from(context, R.string.groupLeaveErrorFailed)
+ .put(GROUP_NAME_KEY, group.title)
+ .format().toString()
+ Toast.makeText(context, txt, Toast.LENGTH_LONG).show()
+ }
context.showSessionDialog {
- title(R.string.ConversationActivity_leave_group)
+ title(R.string.groupLeave)
text(message)
- button(R.string.yes) {
+ dangerButton(R.string.leave) {
try {
val groupPublicKey = doubleDecodeGroupID(thread.address.toString()).toHexString()
val isClosedGroup = DatabaseComponent.get(context).lokiAPIDatabase().isClosedGroup(groupPublicKey)
@@ -293,7 +324,7 @@ object ConversationMenuHelper {
onLeaveFailed()
}
}
- button(R.string.no)
+ button(R.string.cancel)
}
}
@@ -309,7 +340,7 @@ object ConversationMenuHelper {
}
private fun mute(context: Context, thread: Recipient) {
- showMuteDialog(ContextThemeWrapper(context, context.theme)) { until ->
+ showMuteDialog(ContextThemeWrapper(context, context.theme)) { until: Long ->
DatabaseComponent.get(context).recipientDatabase().setMuted(thread, until)
}
}
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
index 1177b4afc9..1a7040b031 100644
--- 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
@@ -1,25 +1,40 @@
package org.thoughtcrime.securesms.conversation.v2.messages
+import android.Manifest
import android.content.Context
+import android.content.Intent
import android.util.AttributeSet
+import android.util.Log
import android.view.LayoutInflater
import android.widget.LinearLayout
import androidx.core.content.res.ResourcesCompat
import androidx.core.view.isGone
import androidx.core.view.isVisible
import androidx.recyclerview.widget.RecyclerView
+import com.squareup.phrase.Phrase
import dagger.hilt.android.AndroidEntryPoint
import network.loki.messenger.R
import network.loki.messenger.databinding.ViewControlMessageBinding
import network.loki.messenger.libsession_util.util.ExpiryMode
import org.session.libsession.messaging.MessagingModuleConfiguration
import org.session.libsession.messaging.messages.ExpirationConfiguration
+import org.session.libsession.utilities.StringSubstitutionConstants.APP_NAME_KEY
+import org.session.libsession.utilities.StringSubstitutionConstants.NAME_KEY
+import org.session.libsession.utilities.TextSecurePreferences
+import org.session.libsession.utilities.getColorFromAttr
import org.thoughtcrime.securesms.conversation.disappearingmessages.DisappearingMessages
import org.thoughtcrime.securesms.conversation.disappearingmessages.expiryMode
import org.thoughtcrime.securesms.database.model.MessageRecord
import org.thoughtcrime.securesms.dependencies.DatabaseComponent
+import org.thoughtcrime.securesms.permissions.Permissions
+import org.thoughtcrime.securesms.preferences.PrivacySettingsActivity
+import org.thoughtcrime.securesms.showSessionDialog
+import org.thoughtcrime.securesms.ui.findActivity
+import org.thoughtcrime.securesms.ui.getSubbedCharSequence
+import org.thoughtcrime.securesms.ui.getSubbedString
import javax.inject.Inject
+
@AndroidEntryPoint
class ControlMessageView : LinearLayout {
@@ -27,6 +42,12 @@ class ControlMessageView : LinearLayout {
private val binding = ViewControlMessageBinding.inflate(LayoutInflater.from(context), this, true)
+ private val infoDrawable by lazy {
+ val d = ResourcesCompat.getDrawable(resources, R.drawable.ic_info_outline_white_24dp, context.theme)
+ d?.setTint(context.getColorFromAttr(R.attr.message_received_text_color))
+ d
+ }
+
constructor(context: Context) : super(context)
constructor(context: Context, attrs: AttributeSet) : super(context, attrs)
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr)
@@ -75,24 +96,104 @@ class ControlMessageView : LinearLayout {
}
}
message.isMessageRequestResponse -> {
- binding.textView.text = context.getString(R.string.message_requests_accepted)
- binding.root.contentDescription=context.getString(R.string.AccessibilityId_message_request_config_message)
+ val msgRecipient = message.recipient.address.serialize()
+ val me = TextSecurePreferences.getLocalNumber(context)
+ binding.textView.text = if(me == msgRecipient) { // you accepted the user's request
+ val threadRecipient = DatabaseComponent.get(context).threadDatabase().getRecipientForThreadId(message.threadId)
+ context.getSubbedCharSequence(
+ R.string.messageRequestYouHaveAccepted,
+ NAME_KEY to (threadRecipient?.name ?: "")
+ )
+ } else { // they accepted your request
+ context.getString(R.string.messageRequestsAccepted)
+ }
+
+ binding.root.contentDescription = context.getString(R.string.AccessibilityId_message_request_config_message)
}
message.isCallLog -> {
val drawable = when {
message.isIncomingCall -> R.drawable.ic_incoming_call
message.isOutgoingCall -> R.drawable.ic_outgoing_call
- message.isFirstMissedCall -> R.drawable.ic_info_outline_light
else -> R.drawable.ic_missed_call
}
binding.textView.isVisible = false
- binding.callTextView.setCompoundDrawablesRelativeWithIntrinsicBounds(ResourcesCompat.getDrawable(resources, drawable, context.theme), null, null, null)
+ binding.callTextView.setCompoundDrawablesRelativeWithIntrinsicBounds(
+ ResourcesCompat.getDrawable(resources, drawable, context.theme),
+ null, null, null)
binding.callTextView.text = messageBody
if (message.expireStarted > 0 && message.expiresIn > 0) {
binding.expirationTimerView.isVisible = true
binding.expirationTimerView.setExpirationTime(message.expireStarted, message.expiresIn)
}
+
+ // remove clicks by default
+ setOnClickListener(null)
+ hideInfo()
+
+ // handle click behaviour depending on criteria
+ if (message.isMissedCall || message.isFirstMissedCall) {
+ when {
+ // when the call toggle is disabled in the privacy screen,
+ // show a dedicated privacy dialog
+ !TextSecurePreferences.isCallNotificationsEnabled(context) -> {
+ showInfo()
+ setOnClickListener {
+ context.showSessionDialog {
+ val titleTxt = context.getSubbedString(
+ R.string.callsMissedCallFrom,
+ NAME_KEY to message.individualRecipient.name!!
+ )
+ title(titleTxt)
+
+ val bodyTxt = context.getSubbedCharSequence(
+ R.string.callsYouMissedCallPermissions,
+ NAME_KEY to message.individualRecipient.name!!
+ )
+ text(bodyTxt)
+
+ button(R.string.sessionSettings) {
+ Intent(context, PrivacySettingsActivity::class.java)
+ .let(context::startActivity)
+ }
+ cancelButton()
+ }
+ }
+ }
+
+ // if we're currently missing the audio/microphone permission,
+ // show a dedicated permission dialog
+ !Permissions.hasAll(context, Manifest.permission.RECORD_AUDIO) -> {
+ showInfo()
+ setOnClickListener {
+ context.showSessionDialog {
+ val titleTxt = context.getSubbedString(
+ R.string.callsMissedCallFrom,
+ NAME_KEY to message.individualRecipient.name!!
+ )
+ title(titleTxt)
+
+ val bodyTxt = context.getSubbedCharSequence(
+ R.string.callsMicrophonePermissionsRequired,
+ NAME_KEY to message.individualRecipient.name!!
+ )
+ text(bodyTxt)
+
+ button(R.string.theContinue) {
+ Permissions.with(context.findActivity())
+ .request(Manifest.permission.RECORD_AUDIO)
+ .withPermanentDenialDialog(
+ context.getSubbedString(R.string.permissionsMicrophoneAccessRequired,
+ APP_NAME_KEY to context.getString(R.string.app_name))
+ )
+ .execute()
+ }
+ cancelButton()
+ }
+ }
+ }
+ }
+ }
}
}
@@ -100,6 +201,24 @@ class ControlMessageView : LinearLayout {
binding.callView.isVisible = message.isCallLog
}
+ fun showInfo(){
+ binding.callTextView.setCompoundDrawablesRelativeWithIntrinsicBounds(
+ binding.callTextView.compoundDrawablesRelative.first(),
+ null,
+ infoDrawable,
+ null
+ )
+ }
+
+ fun hideInfo(){
+ binding.callTextView.setCompoundDrawablesRelativeWithIntrinsicBounds(
+ binding.callTextView.compoundDrawablesRelative.first(),
+ null,
+ null,
+ null
+ )
+ }
+
fun recycle() {
}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/DeletedMessageView.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/DeletedMessageView.kt
index 9c725ee048..5b64df059e 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/DeletedMessageView.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/DeletedMessageView.kt
@@ -21,7 +21,7 @@ class DeletedMessageView : LinearLayout {
// region Updating
fun bind(message: MessageRecord, @ColorInt textColor: Int) {
assert(message.isDeleted)
- binding.deleteTitleTextView.text = context.getString(R.string.deleted_message)
+ binding.deleteTitleTextView.text = context.resources.getQuantityString(R.plurals.deleteMessageDeleted, 1, 1)
binding.deleteTitleTextView.setTextColor(textColor)
binding.deletedMessageViewIconImageView.imageTintList = ColorStateList.valueOf(textColor)
}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/EmojiReactionsView.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/EmojiReactionsView.kt
index 49e4b1044f..27714fbc05 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/EmojiReactionsView.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/EmojiReactionsView.kt
@@ -10,9 +10,12 @@ import android.widget.LinearLayout
import android.widget.TextView
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.core.content.ContextCompat
+import androidx.core.view.setPadding
import com.google.android.flexbox.JustifyContent
+import com.squareup.phrase.Phrase
import network.loki.messenger.R
import network.loki.messenger.databinding.ViewEmojiReactionsBinding
+import org.session.libsession.utilities.StringSubstitutionConstants.COUNT_KEY
import org.session.libsession.utilities.TextSecurePreferences.Companion.getLocalNumber
import org.session.libsession.utilities.ThemeUtil
import org.thoughtcrime.securesms.components.emoji.EmojiImageView
@@ -43,6 +46,8 @@ class EmojiReactionsView : ConstraintLayout, OnTouchListener {
private var onDownTimestamp: Long = 0
private var extended = false
+ private val overflowItemSize = ViewUtil.dpToPx(24)
+
constructor(context: Context) : super(context) { init(null) }
constructor(context: Context, attrs: AttributeSet) : super(context, attrs) { init(attrs) }
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr) { init(attrs) }
@@ -81,7 +86,9 @@ class EmojiReactionsView : ConstraintLayout, OnTouchListener {
if (v.tag == null) return false
val reaction = v.tag as Reaction
val action = event.action
- if (action == MotionEvent.ACTION_DOWN) onDown(MessageId(reaction.messageId, reaction.isMms)) else if (action == MotionEvent.ACTION_CANCEL) removeLongPressCallback() else if (action == MotionEvent.ACTION_UP) onUp(reaction)
+ if (action == MotionEvent.ACTION_DOWN) onDown(MessageId(reaction.messageId, reaction.isMms), reaction.emoji)
+ else if (action == MotionEvent.ACTION_CANCEL) removeLongPressCallback()
+ else if (action == MotionEvent.ACTION_UP) onUp(reaction)
return true
}
@@ -91,18 +98,15 @@ class EmojiReactionsView : ConstraintLayout, OnTouchListener {
binding.layoutEmojiContainer.removeAllViews()
val overflowContainer = LinearLayout(context)
overflowContainer.orientation = LinearLayout.HORIZONTAL
- val innerPadding = ViewUtil.dpToPx(4)
- overflowContainer.setPaddingRelative(innerPadding, innerPadding, innerPadding, innerPadding)
val pixelSize = ViewUtil.dpToPx(1)
- for (reaction in reactions) {
+ reactions.forEachIndexed { index, reaction ->
if (binding.layoutEmojiContainer.childCount + 1 >= DEFAULT_THRESHOLD && threshold != Int.MAX_VALUE && reactions.size > threshold) {
if (overflowContainer.parent == null) {
binding.layoutEmojiContainer.addView(overflowContainer)
val overflowParams = overflowContainer.layoutParams as MarginLayoutParams
- overflowParams.height = ViewUtil.dpToPx(26)
+ overflowParams.height = MarginLayoutParams.WRAP_CONTENT
overflowParams.setMargins(pixelSize, pixelSize, pixelSize, pixelSize)
overflowContainer.layoutParams = overflowParams
- overflowContainer.background = ContextCompat.getDrawable(context, R.drawable.reaction_pill_background)
}
val pill = buildPill(context, this, reaction, true)
pill.setOnClickListener { v: View? ->
@@ -111,6 +115,7 @@ class EmojiReactionsView : ConstraintLayout, OnTouchListener {
}
pill.findViewById(R.id.reactions_pill_count).visibility = GONE
pill.findViewById(R.id.reactions_pill_spacer).visibility = GONE
+ pill.z = reaction.count - index.toFloat() // make sure the overflow is stacked properly
overflowContainer.addView(pill)
} else {
val pill = buildPill(context, this, reaction, false)
@@ -179,9 +184,10 @@ class EmojiReactionsView : ConstraintLayout, OnTouchListener {
val countView = root.findViewById(R.id.reactions_pill_count)
val spacer = root.findViewById(R.id.reactions_pill_spacer)
if (isCompact) {
- root.setPaddingRelative(1, 1, 1, 1)
+ root.setPadding(0)
val layoutParams = root.layoutParams
- layoutParams.height = ViewGroup.LayoutParams.WRAP_CONTENT
+ layoutParams.height = overflowItemSize
+ layoutParams.width = overflowItemSize
root.layoutParams = layoutParams
}
if (reaction.emoji != null) {
@@ -195,15 +201,14 @@ class EmojiReactionsView : ConstraintLayout, OnTouchListener {
} else {
emojiView.visibility = GONE
spacer.visibility = GONE
- countView.text = context.getString(R.string.ReactionsConversationView_plus, reaction.count)
+ countView.text = Phrase.from(context, R.string.andMore).put(COUNT_KEY, reaction.count.toInt()).format()
}
if (reaction.userWasSender && !isCompact) {
root.background = ContextCompat.getDrawable(context, R.drawable.reaction_pill_background_selected)
countView.setTextColor(ThemeUtil.getThemedColor(context, R.attr.reactionsPillSelectedTextColor))
} else {
- if (!isCompact) {
- root.background = ContextCompat.getDrawable(context, R.drawable.reaction_pill_background)
- }
+ root.background = if(isCompact) ContextCompat.getDrawable(context, R.drawable.reaction_pill_background_bordered)
+ else ContextCompat.getDrawable(context, R.drawable.reaction_pill_background)
}
return root
}
@@ -215,12 +220,12 @@ class EmojiReactionsView : ConstraintLayout, OnTouchListener {
}
}
- private fun onDown(messageId: MessageId) {
+ private fun onDown(messageId: MessageId, emoji: String?) {
removeLongPressCallback()
val newLongPressCallback = Runnable {
performHapticFeedback(HapticFeedbackConstants.LONG_PRESS)
if (delegate != null) {
- delegate!!.onReactionLongClicked(messageId)
+ delegate!!.onReactionLongClicked(messageId, emoji)
}
}
longPressCallback = newLongPressCallback
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
index 8cf80dc090..d064d02872 100644
--- 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
@@ -6,16 +6,16 @@ import android.graphics.Rect
import android.util.AttributeSet
import android.view.MotionEvent
import android.widget.LinearLayout
-import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.isVisible
+import com.bumptech.glide.RequestManager
import network.loki.messenger.R
import network.loki.messenger.databinding.ViewLinkPreviewBinding
import org.session.libsession.utilities.getColorFromAttr
+import org.session.libsignal.utilities.Log
import org.thoughtcrime.securesms.components.CornerMask
-import org.thoughtcrime.securesms.conversation.v2.ModalUrlBottomSheet
+import org.thoughtcrime.securesms.conversation.v2.ConversationActivityV2
import org.thoughtcrime.securesms.conversation.v2.utilities.MessageBubbleUtilities
import org.thoughtcrime.securesms.database.model.MmsMessageRecord
-import com.bumptech.glide.RequestManager
import org.thoughtcrime.securesms.mms.ImageSlide
class LinkPreviewView : LinearLayout {
@@ -84,10 +84,11 @@ class LinkPreviewView : LinearLayout {
}
}
- fun openURL() {
- val url = this.url ?: return
- val activity = context as AppCompatActivity
- ModalUrlBottomSheet(url).show(activity.supportFragmentManager, "Open URL Dialog")
+ // Method to show the open or copy URL dialog
+ private fun openURL() {
+ val url = this.url ?: return Log.w("LinkPreviewView", "Cannot open a null URL")
+ val activity = context as? ConversationActivityV2
+ activity?.showOpenUrlDialog(url)
}
// endregion
}
\ 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
index 40cf4bc1e0..dc6b05b444 100644
--- 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
@@ -75,13 +75,13 @@ class QuoteView @JvmOverloads constructor(context: Context, attrs: AttributeSet?
val quoteIsLocalUser = localNumber != null && authorPublicKey == localNumber
val authorDisplayName =
- if (quoteIsLocalUser) context.getString(R.string.QuoteView_you)
+ if (quoteIsLocalUser) context.getString(R.string.you)
else author?.displayName(Contact.contextForRecipient(thread)) ?: "${authorPublicKey.take(4)}...${authorPublicKey.takeLast(4)}"
binding.quoteViewAuthorTextView.text = authorDisplayName
binding.quoteViewAuthorTextView.setTextColor(getTextColor(isOutgoingMessage))
// Body
binding.quoteViewBodyTextView.text = if (isOpenGroupInvitation)
- resources.getString(R.string.open_group_invitation_view__open_group_invitation)
+ resources.getString(R.string.communityInvitation)
else MentionUtilities.highlightMentions(
text = (body ?: "").toSpannable(),
isOutgoingMessage = isOutgoingMessage,
@@ -106,7 +106,16 @@ class QuoteView @JvmOverloads constructor(context: Context, attrs: AttributeSet?
attachments.audioSlide != null -> {
binding.quoteViewAttachmentPreviewImageView.setImageResource(R.drawable.ic_microphone)
binding.quoteViewAttachmentPreviewImageView.isVisible = true
- binding.quoteViewBodyTextView.text = resources.getString(R.string.Slide_audio)
+ // A missing file name is the legacy way to determine if an audio attachment is
+ // a voice note vs. other arbitrary audio attachments.
+ val attachment = attachments.asAttachments().firstOrNull()
+ val isVoiceNote = attachment?.isVoiceNote == true ||
+ attachment != null && attachment.fileName.isNullOrEmpty()
+ binding.quoteViewBodyTextView.text = if (isVoiceNote) {
+ resources.getString(R.string.messageVoice)
+ } else {
+ resources.getString(R.string.audio)
+ }
}
attachments.documentSlide != null -> {
binding.quoteViewAttachmentPreviewImageView.setImageResource(R.drawable.ic_document_large_light)
@@ -120,7 +129,7 @@ class QuoteView @JvmOverloads constructor(context: Context, attrs: AttributeSet?
.root.setRoundedCorners(toPx(4, resources))
binding.quoteViewAttachmentThumbnailImageView.root.setImageResource(glide, slide, false)
binding.quoteViewAttachmentThumbnailImageView.root.isVisible = true
- binding.quoteViewBodyTextView.text = if (MediaUtil.isVideo(slide.asAttachment())) resources.getString(R.string.Slide_video) else resources.getString(R.string.Slide_image)
+ binding.quoteViewBodyTextView.text = if (MediaUtil.isVideo(slide.asAttachment())) resources.getString(R.string.video) else resources.getString(R.string.image)
}
}
}
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
index 47034cf8ed..7d1dc625f6 100644
--- 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
@@ -5,12 +5,13 @@ import android.util.AttributeSet
import android.widget.LinearLayout
import androidx.annotation.ColorInt
import androidx.core.content.ContextCompat
+import com.squareup.phrase.Phrase
import network.loki.messenger.R
import network.loki.messenger.databinding.ViewUntrustedAttachmentBinding
+import org.session.libsession.utilities.StringSubstitutionConstants.FILE_TYPE_KEY
import org.session.libsession.utilities.recipients.Recipient
import org.thoughtcrime.securesms.conversation.v2.dialogs.DownloadDialog
import org.thoughtcrime.securesms.util.ActivityDispatcher
-import java.util.Locale
class UntrustedAttachmentView: LinearLayout {
private val binding: ViewUntrustedAttachmentBinding by lazy { ViewUntrustedAttachmentBinding.bind(this) }
@@ -30,13 +31,17 @@ class UntrustedAttachmentView: LinearLayout {
// 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.AUDIO -> R.drawable.ic_microphone to R.string.audio
+ AttachmentType.DOCUMENT -> R.drawable.ic_document_large_light to R.string.files
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))
+
+ val text = Phrase.from(context, R.string.attachmentsTapToDownload)
+ .put(FILE_TYPE_KEY, context.getString(stringRes))
+ .format()
+ binding.untrustedAttachmentTitle.text = text
binding.untrustedAttachmentIcon.setImageDrawable(iconDrawable)
binding.untrustedAttachmentTitle.text = text
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
index dcce528234..d62cc532c4 100644
--- 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
@@ -12,34 +12,29 @@ import android.util.AttributeSet
import android.view.MotionEvent
import android.view.View
import androidx.annotation.ColorInt
-import androidx.appcompat.app.AppCompatActivity
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.core.graphics.ColorUtils
import androidx.core.text.getSpans
import androidx.core.text.toSpannable
import androidx.core.view.children
import androidx.core.view.isVisible
+import com.bumptech.glide.Glide
+import com.bumptech.glide.RequestManager
import network.loki.messenger.R
import network.loki.messenger.databinding.ViewVisibleMessageContentBinding
-import okhttp3.HttpUrl
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
-import org.session.libsession.messaging.MessagingModuleConfiguration
-import org.session.libsession.messaging.sending_receiving.attachments.AttachmentTransferProgress
import org.session.libsession.messaging.sending_receiving.attachments.DatabaseAttachment
import org.session.libsession.utilities.ThemeUtil
import org.session.libsession.utilities.getColorFromAttr
import org.session.libsession.utilities.modifyLayoutParams
import org.session.libsession.utilities.recipients.Recipient
import org.thoughtcrime.securesms.conversation.v2.ConversationActivityV2
-import org.thoughtcrime.securesms.conversation.v2.ModalUrlBottomSheet
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.database.model.SmsMessageRecord
-import com.bumptech.glide.Glide
-import com.bumptech.glide.RequestManager
import org.thoughtcrime.securesms.util.GlowViewUtilities
import org.thoughtcrime.securesms.util.SearchUtil
import org.thoughtcrime.securesms.util.getAccentColor
@@ -117,7 +112,7 @@ class VisibleMessageContentView : ConstraintLayout {
binding.quoteView.root.isVisible = true
val quote = message.quote!!
val quoteText = if (quote.isOriginalMissing) {
- context.getString(R.string.QuoteView_original_missing)
+ context.getString(R.string.messageErrorOriginal)
} else {
quote.text
}
@@ -292,8 +287,8 @@ class VisibleMessageContentView : ConstraintLayout {
body.getSpans(0, body.length).toList().forEach { urlSpan ->
val updatedUrl = urlSpan.url.let { it.toHttpUrlOrNull().toString() }
val replacementSpan = ModalURLSpan(updatedUrl) { url ->
- val activity = context as AppCompatActivity
- ModalUrlBottomSheet(url).show(activity.supportFragmentManager, "Open URL Dialog")
+ val activity = context as? ConversationActivityV2
+ activity?.showOpenUrlDialog(url)
}
val start = body.getSpanStart(urlSpan)
val end = body.getSpanEnd(urlSpan)
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
index 9f7f620ab5..1734d75b08 100644
--- 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
@@ -27,6 +27,13 @@ import androidx.core.os.bundleOf
import androidx.core.view.isVisible
import androidx.core.view.marginBottom
import dagger.hilt.android.AndroidEntryPoint
+import java.util.Date
+import java.util.Locale
+import javax.inject.Inject
+import kotlin.math.abs
+import kotlin.math.min
+import kotlin.math.roundToInt
+import kotlin.math.sqrt
import network.loki.messenger.R
import network.loki.messenger.databinding.ViewEmojiReactionsBinding
import network.loki.messenger.databinding.ViewVisibleMessageBinding
@@ -54,17 +61,12 @@ import org.thoughtcrime.securesms.groups.OpenGroupManager
import org.thoughtcrime.securesms.home.UserDetailsBottomSheet
import com.bumptech.glide.Glide
import com.bumptech.glide.RequestManager
+import org.session.libsignal.utilities.Log
+import org.thoughtcrime.securesms.database.model.MmsMessageRecord
import org.thoughtcrime.securesms.util.DateUtils
import org.thoughtcrime.securesms.util.disableClipping
import org.thoughtcrime.securesms.util.toDp
import org.thoughtcrime.securesms.util.toPx
-import java.util.Date
-import java.util.Locale
-import javax.inject.Inject
-import kotlin.math.abs
-import kotlin.math.min
-import kotlin.math.roundToInt
-import kotlin.math.sqrt
private const val TAG = "VisibleMessageView"
@@ -269,8 +271,7 @@ class VisibleMessageView : FrameLayout {
// Method to display or hide the status of a message.
// Note: Although most commonly used to display the delivery status of a message, we also use the
// message status area to display the disappearing messages state - so in this latter case we'll
- // be displaying the "Sent" and the animating clock icon for outgoing messages or "Read" and the
- // animated clock icon for incoming messages.
+ // be displaying either "Sent" or "Read" and the animating clock icon.
private fun showStatusMessage(message: MessageRecord) {
// We'll start by hiding everything and then only make visible what we need
binding.messageStatusTextView.isVisible = false
@@ -384,37 +385,48 @@ class VisibleMessageView : FrameLayout {
message.isFailed ->
MessageStatusInfo(R.drawable.ic_delivery_status_failed,
getThemedColor(context, R.attr.danger),
- R.string.delivery_status_failed
+ R.string.messageStatusFailedToSend
)
message.isSyncFailed ->
MessageStatusInfo(
R.drawable.ic_delivery_status_failed,
context.getColor(R.color.accent_orange),
- R.string.delivery_status_sync_failed
- )
- message.isPending ->
- MessageStatusInfo(
- R.drawable.ic_delivery_status_sending,
- context.getColorFromAttr(R.attr.message_status_color),
- R.string.delivery_status_sending
+ R.string.messageStatusFailedToSync
)
+ message.isPending -> {
+ // Non-mms messages (or quote messages, which happen to be mms for some reason) display 'Sending'..
+ if (!message.isMms || (message as? MmsMessageRecord)?.quote != null) {
+ MessageStatusInfo(
+ R.drawable.ic_delivery_status_sending,
+ context.getColorFromAttr(R.attr.message_status_color),
+ R.string.sending
+ )
+ } else {
+ // ..and Mms messages display 'Uploading'.
+ MessageStatusInfo(
+ R.drawable.ic_delivery_status_sending,
+ context.getColorFromAttr(R.attr.message_status_color),
+ R.string.uploading
+ )
+ }
+ }
message.isSyncing || message.isResyncing ->
MessageStatusInfo(
R.drawable.ic_delivery_status_sending,
context.getColorFromAttr(R.attr.message_status_color),
- R.string.delivery_status_sending // We COULD tell the user that we're `syncing` (R.string.delivery_status_syncing) but it will likely make more sense to them if we say "Sending"
+ R.string.messageStatusSyncing
)
message.isRead || message.isIncoming ->
MessageStatusInfo(
R.drawable.ic_delivery_status_read,
context.getColorFromAttr(R.attr.message_status_color),
- R.string.delivery_status_read
+ R.string.read
)
message.isSent ->
MessageStatusInfo(
R.drawable.ic_delivery_status_sent,
context.getColorFromAttr(R.attr.message_status_color),
- R.string.delivery_status_sent
+ R.string.disappearingMessagesSent
)
else -> {
// The message isn't one we care about for message statuses we display to the user (i.e.,
diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/VisibleMessageViewDelegate.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/VisibleMessageViewDelegate.kt
index 6788dd3f38..69797b8848 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/VisibleMessageViewDelegate.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/VisibleMessageViewDelegate.kt
@@ -10,6 +10,6 @@ interface VisibleMessageViewDelegate {
fun onReactionClicked(emoji: String, messageId: MessageId, userWasSender: Boolean)
- fun onReactionLongClicked(messageId: MessageId)
+ fun onReactionLongClicked(messageId: MessageId, emoji: String?)
}
\ No newline at end of file
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
index afed74b1cc..a379a23445 100644
--- 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
@@ -5,9 +5,11 @@ import android.util.AttributeSet
import android.view.LayoutInflater
import android.view.View
import android.widget.LinearLayout
+import com.squareup.phrase.Phrase
import network.loki.messenger.R
import network.loki.messenger.databinding.ViewSearchBottomBarBinding
-
+import org.session.libsession.utilities.StringSubstitutionConstants.COUNT_KEY
+import org.session.libsession.utilities.StringSubstitutionConstants.TOTAL_COUNT_KEY
class SearchBottomBar : LinearLayout {
private lateinit var binding: ViewSearchBottomBarBinding
@@ -35,7 +37,7 @@ class SearchBottomBar : LinearLayout {
}
}
if (count > 0) {
- searchPosition.text = resources.getString(R.string.ConversationActivity_search_position, position + 1, count)
+ searchPosition.text = resources.getQuantityString(R.plurals.searchMatches, count, position + 1, count)
} else {
searchPosition.text = ""
}
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
index 48bb731c68..82156b32e7 100644
--- 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
@@ -94,6 +94,8 @@ class SearchViewModel @Inject constructor(
}
}
+ public fun getActiveQuery() = activeQuery
+
class SearchResult(private val results: CursorList, val position: Int) : Closeable {
fun getResults(): List {
diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/utilities/AttachmentManager.java b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/utilities/AttachmentManager.java
index ee98f623f2..ccbba13b3a 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/utilities/AttachmentManager.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/utilities/AttachmentManager.java
@@ -16,6 +16,8 @@
*/
package org.thoughtcrime.securesms.conversation.v2.utilities;
+import static org.session.libsession.utilities.StringSubstitutionConstants.APP_NAME_KEY;
+
import android.Manifest;
import android.annotation.SuppressLint;
import android.app.Activity;
@@ -30,10 +32,14 @@ import android.provider.OpenableColumns;
import android.text.TextUtils;
import android.util.Pair;
import android.widget.Toast;
-
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
-
+import com.squareup.phrase.Phrase;
+import java.io.IOException;
+import java.util.Iterator;
+import java.util.LinkedList;
+import java.util.List;
+import network.loki.messenger.R;
import org.session.libsession.utilities.recipients.Recipient;
import org.session.libsignal.utilities.ListenableFuture;
import org.session.libsignal.utilities.Log;
@@ -55,17 +61,13 @@ import org.thoughtcrime.securesms.permissions.Permissions;
import org.thoughtcrime.securesms.providers.BlobProvider;
import org.thoughtcrime.securesms.util.MediaUtil;
-import java.io.IOException;
-import java.util.Iterator;
-import java.util.LinkedList;
-import java.util.List;
-
-import network.loki.messenger.R;
-
public class AttachmentManager {
private final static String TAG = AttachmentManager.class.getSimpleName();
+ // Max attachment size is 10MB, above which we display a warning toast rather than sending the msg
+ private final long MAX_ATTACHMENTS_FILE_SIZE_BYTES = 10 * 1024 * 1024;
+
private final @NonNull Context context;
private final @NonNull AttachmentListener attachmentListener;
@@ -242,33 +244,58 @@ public class AttachmentManager {
public static void selectDocument(Activity activity, int requestCode) {
Permissions.PermissionsBuilder builder = Permissions.with(activity);
+ Context c = activity.getApplicationContext();
// The READ_EXTERNAL_STORAGE permission is deprecated (and will AUTO-FAIL if requested!) on
// Android 13 and above (API 33 - 'Tiramisu') we must ask for READ_MEDIA_VIDEO/IMAGES/AUDIO instead.
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
builder = builder.request(Manifest.permission.READ_MEDIA_VIDEO)
.request(Manifest.permission.READ_MEDIA_IMAGES)
- .request(Manifest.permission.READ_MEDIA_AUDIO);
+ .request(Manifest.permission.READ_MEDIA_AUDIO)
+ .withRationaleDialog(
+ Phrase.from(c, R.string.permissionsMusicAudio)
+ .put(APP_NAME_KEY, c.getString(R.string.app_name)).format().toString()
+ )
+ .withPermanentDenialDialog(
+ Phrase.from(c, R.string.permissionMusicAudioDenied)
+ .put(APP_NAME_KEY, c.getString(R.string.app_name))
+ .format().toString()
+ );
} else {
- builder = builder.request(Manifest.permission.READ_EXTERNAL_STORAGE);
+ builder = builder.request(Manifest.permission.READ_EXTERNAL_STORAGE)
+ .withPermanentDenialDialog(
+ Phrase.from(c, R.string.permissionsStorageDeniedLegacy)
+ .put(APP_NAME_KEY, c.getString(R.string.app_name))
+ .format().toString()
+ );
}
- builder.withPermanentDenialDialog(activity.getString(R.string.AttachmentManager_signal_requires_the_external_storage_permission_in_order_to_attach_photos_videos_or_audio))
- .withRationaleDialog(activity.getString(R.string.ConversationActivity_to_send_photos_and_video_allow_signal_access_to_storage), R.drawable.ic_baseline_photo_library_24)
- .onAllGranted(() -> selectMediaType(activity, "*/*", null, requestCode)) // Note: We can use startActivityForResult w/ the ACTION_OPEN_DOCUMENT or ACTION_OPEN_DOCUMENT_TREE intent if we need to modernise this.
+
+ builder.onAllGranted(() -> selectMediaType(activity, "*/*", null, requestCode)) // Note: We can use startActivityForResult w/ the ACTION_OPEN_DOCUMENT or ACTION_OPEN_DOCUMENT_TREE intent if we need to modernise this.
.execute();
}
public static void selectGallery(Activity activity, int requestCode, @NonNull Recipient recipient, @NonNull String body) {
+
+ Context c = activity.getApplicationContext();
+
Permissions.PermissionsBuilder builder = Permissions.with(activity);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
builder = builder.request(Manifest.permission.READ_MEDIA_VIDEO)
- .request(Manifest.permission.READ_MEDIA_IMAGES);
+ .request(Manifest.permission.READ_MEDIA_IMAGES)
+ .withPermanentDenialDialog(
+ Phrase.from(c, R.string.permissionsStorageDenied)
+ .put(APP_NAME_KEY, c.getString(R.string.app_name))
+ .format().toString()
+ );
} else {
- builder = builder.request(Manifest.permission.READ_EXTERNAL_STORAGE);
+ builder = builder.request(Manifest.permission.READ_EXTERNAL_STORAGE)
+ .withPermanentDenialDialog(
+ Phrase.from(c, R.string.permissionsStorageDeniedLegacy)
+ .put(APP_NAME_KEY, c.getString(R.string.app_name))
+ .format().toString()
+ );
}
- builder.withPermanentDenialDialog(activity.getString(R.string.AttachmentManager_signal_requires_the_external_storage_permission_in_order_to_attach_photos_videos_or_audio))
- .withRationaleDialog(activity.getString(R.string.ConversationActivity_to_send_photos_and_video_allow_signal_access_to_storage), R.drawable.ic_baseline_photo_library_24)
- .onAllGranted(() -> activity.startActivityForResult(MediaSendActivity.buildGalleryIntent(activity, recipient, body), requestCode))
+ builder.onAllGranted(() -> activity.startActivityForResult(MediaSendActivity.buildGalleryIntent(activity, recipient, body), requestCode))
.execute();
}
@@ -291,10 +318,14 @@ public class AttachmentManager {
}
public void capturePhoto(Activity activity, int requestCode, Recipient recipient) {
+
+ String cameraPermissionDeniedTxt = Phrase.from(context, R.string.permissionsCameraDenied)
+ .put(APP_NAME_KEY, context.getString(R.string.app_name))
+ .format().toString();
+
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))
- .withRationaleDialog(activity.getString(R.string.ConversationActivity_to_capture_photos_and_video_allow_signal_access_to_the_camera),R.drawable.ic_baseline_photo_camera_24)
+ .withPermanentDenialDialog(cameraPermissionDeniedTxt)
.onAllGranted(() -> {
Intent captureIntent = MediaSendActivity.buildCameraIntent(activity, recipient);
if (captureIntent.resolveActivity(activity.getPackageManager()) != null) {
@@ -326,7 +357,7 @@ public class AttachmentManager {
activity.startActivityForResult(intent, requestCode);
} catch (ActivityNotFoundException anfe) {
Log.w(TAG, "couldn't complete ACTION_GET_CONTENT intent, no activity found. falling back.");
- Toast.makeText(activity, R.string.AttachmentManager_cant_open_media_selection, Toast.LENGTH_LONG).show();
+ Toast.makeText(activity, R.string.attachmentsErrorNoApp, Toast.LENGTH_LONG).show();
}
}
@@ -334,9 +365,21 @@ public class AttachmentManager {
final @Nullable Slide slide,
final @NonNull MediaConstraints constraints)
{
- return slide == null ||
- constraints.isSatisfied(context, slide.asAttachment()) ||
- constraints.canResize(slide.asAttachment());
+ // Null attachment? Not satisfied.
+ if (slide == null) return false;
+
+ // Attachments are excessively large? Not satisfied.
+ // Note: This file size test must come BEFORE the `constraints.isSatisfied` check below because
+ // it is a more specific type of check.
+ if (slide.asAttachment().getSize() > MAX_ATTACHMENTS_FILE_SIZE_BYTES) {
+ Toast.makeText(context, R.string.attachmentsErrorSize, Toast.LENGTH_SHORT).show();
+ return false;
+ }
+
+ // Otherwise we return whether our constraints are satisfied OR if we can resize the attachment
+ // (in the case of one or more images) - either one will be acceptable, but if both aren't then
+ // we fail the constraint test.
+ return constraints.isSatisfied(context, slide.asAttachment()) || constraints.canResize(slide.asAttachment());
}
public interface AttachmentListener {
diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/utilities/MentionUtilities.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/utilities/MentionUtilities.kt
index 4d3e48bc5b..39301cd69f 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/utilities/MentionUtilities.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/utilities/MentionUtilities.kt
@@ -54,13 +54,14 @@ object MentionUtilities {
val userPublicKey = TextSecurePreferences.getLocalNumber(context)!!
val openGroup by lazy { DatabaseComponent.get(context).storage().getOpenGroup(threadID) }
- // format the mention text
+ // Format the mention text
if (matcher.find(startIndex)) {
while (true) {
val publicKey = text.subSequence(matcher.start() + 1, matcher.end()).toString() // +1 to get rid of the @
+
val isYou = isYou(publicKey, userPublicKey, openGroup)
val userDisplayName: String? = if (isYou) {
- context.getString(R.string.MessageRecord_you)
+ context.getString(R.string.you)
} else {
val contact = DatabaseComponent.get(context).sessionContactDatabase().getContactWithAccountID(publicKey)
@Suppress("NAME_SHADOWING") val context = if (openGroup != null) Contact.ContactContext.OPEN_GROUP else Contact.ContactContext.REGULAR
diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/utilities/NotificationUtils.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/utilities/NotificationUtils.kt
index c0ce83f631..f012f925ed 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/utilities/NotificationUtils.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/utilities/NotificationUtils.kt
@@ -8,7 +8,7 @@ import org.thoughtcrime.securesms.showSessionDialog
object NotificationUtils {
fun showNotifyDialog(context: Context, thread: Recipient, notifyTypeHandler: (Int)->Unit) {
context.showSessionDialog {
- title(R.string.RecipientPreferenceActivity_notification_settings)
+ title(R.string.sessionNotifications)
singleChoiceItems(
context.resources.getStringArray(R.array.notify_types),
thread.notifyType
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
index 7a47b92756..23d81e2513 100644
--- 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
@@ -1,9 +1,13 @@
package org.thoughtcrime.securesms.conversation.v2.utilities
import android.graphics.Rect
+import android.graphics.Typeface
import android.text.Layout
+import android.text.SpannableString
+import android.text.Spanned
import android.text.StaticLayout
import android.text.TextPaint
+import android.text.style.StyleSpan
import android.view.MotionEvent
import android.widget.TextView
import androidx.core.text.getSpans
diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/utilities/ThumbnailView.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/utilities/ThumbnailView.kt
index 83932b2ce4..b7103b9c23 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/utilities/ThumbnailView.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/utilities/ThumbnailView.kt
@@ -9,6 +9,7 @@ import android.util.AttributeSet
import android.util.TypedValue
import android.view.View
import android.view.ViewOutlineProvider
+import android.view.ViewTreeObserver
import android.widget.FrameLayout
import androidx.core.view.isVisible
import com.bumptech.glide.load.engine.DiskCacheStrategy
@@ -27,7 +28,10 @@ import org.thoughtcrime.securesms.components.GlideDrawableListeningTarget
import org.thoughtcrime.securesms.mms.DecryptableStreamUriLoader.DecryptableUri
import com.bumptech.glide.RequestBuilder
import com.bumptech.glide.RequestManager
+import org.session.libsignal.utilities.Log
import org.thoughtcrime.securesms.mms.Slide
+import org.thoughtcrime.securesms.ui.afterMeasured
+import java.lang.Float.min
open class ThumbnailView @JvmOverloads constructor(
context: Context,
@@ -114,8 +118,23 @@ open class ThumbnailView @JvmOverloads constructor(
isPreview: Boolean, naturalWidth: Int,
naturalHeight: Int
): ListenableFuture {
- binding.playOverlay.isVisible = (slide.thumbnailUri != null && slide.hasPlayOverlay() &&
- (slide.transferState == AttachmentTransferProgress.TRANSFER_PROGRESS_DONE || isPreview))
+ val showPlayOverlay = (slide.thumbnailUri != null && slide.hasPlayOverlay() &&
+ (slide.transferState == AttachmentTransferProgress.TRANSFER_PROGRESS_DONE || isPreview))
+ if(showPlayOverlay) {
+ binding.playOverlay.isVisible = true
+ // The views are poorly constructed at the moment and there is no good way to know
+ // if this is used in the main conversation or in the tiny quote window of a reply...
+ // But when the view is too small the 'play' icon does not scale,
+ // so we can do it based on measured sizes here
+ binding.playOverlay.afterMeasured {
+ // max size if 60% of the width
+ val ratio = min((binding.root.width * 0.6f) / binding.playOverlay.width, 1f)
+ binding.playOverlay.scaleX = ratio
+ binding.playOverlay.scaleY = ratio
+ }
+ } else {
+ binding.playOverlay.isVisible = false
+ }
if (equals(this.slide, slide)) {
// don't re-load slide
diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/DraftDatabase.java b/app/src/main/java/org/thoughtcrime/securesms/database/DraftDatabase.java
index 822e40129e..be083256db 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/database/DraftDatabase.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/database/DraftDatabase.java
@@ -3,14 +3,8 @@ package org.thoughtcrime.securesms.database;
import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
-import android.net.Uri;
-import androidx.annotation.Nullable;
-
import net.zetetic.database.sqlcipher.SQLiteDatabase;
-
-import network.loki.messenger.R;
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper;
-
import java.util.LinkedList;
import java.util.List;
import java.util.Set;
@@ -24,10 +18,10 @@ public class DraftDatabase extends Database {
public static final String DRAFT_VALUE = "value";
public static final String CREATE_TABLE = "CREATE TABLE " + TABLE_NAME + " (" + ID + " INTEGER PRIMARY KEY, " +
- THREAD_ID + " INTEGER, " + DRAFT_TYPE + " TEXT, " + DRAFT_VALUE + " TEXT);";
+ THREAD_ID + " INTEGER, " + DRAFT_TYPE + " TEXT, " + DRAFT_VALUE + " TEXT);";
public static final String[] CREATE_INDEXS = {
- "CREATE INDEX IF NOT EXISTS draft_thread_index ON " + TABLE_NAME + " (" + THREAD_ID + ");",
+ "CREATE INDEX IF NOT EXISTS draft_thread_index ON " + TABLE_NAME + " (" + THREAD_ID + ");",
};
public DraftDatabase(Context context, SQLCipherOpenHelper databaseHelper) {
@@ -59,8 +53,8 @@ public class DraftDatabase extends Database {
for (long threadId : threadIds) {
where.append(" OR ")
- .append(THREAD_ID)
- .append(" = ?");
+ .append(THREAD_ID)
+ .append(" = ?");
arguments.add(String.valueOf(threadId));
}
@@ -95,12 +89,10 @@ public class DraftDatabase extends Database {
}
}
+ // Class to save drafts of text (only) messages if the user is in the middle of writing a message
+ // and then the app loses focus or is closed.
public static class Draft {
- public static final String TEXT = "text";
- public static final String IMAGE = "image";
- public static final String VIDEO = "video";
- public static final String AUDIO = "audio";
- public static final String QUOTE = "quote";
+ public static final String TEXT = "text";
private final String type;
private final String value;
@@ -117,48 +109,10 @@ public class DraftDatabase extends Database {
public String getValue() {
return value;
}
-
- String getSnippet(Context context) {
- switch (type) {
- case TEXT: return value;
- case IMAGE: return context.getString(R.string.DraftDatabase_Draft_image_snippet);
- case VIDEO: return context.getString(R.string.DraftDatabase_Draft_video_snippet);
- case AUDIO: return context.getString(R.string.DraftDatabase_Draft_audio_snippet);
- case QUOTE: return context.getString(R.string.DraftDatabase_Draft_quote_snippet);
- default: return null;
- }
- }
}
public static class Drafts extends LinkedList {
- private Draft getDraftOfType(String type) {
- for (Draft draft : this) {
- if (type.equals(draft.getType())) {
- return draft;
- }
- }
- return null;
- }
-
- public String getSnippet(Context context) {
- Draft textDraft = getDraftOfType(Draft.TEXT);
- if (textDraft != null) {
- return textDraft.getSnippet(context);
- } else if (size() > 0) {
- return get(0).getSnippet(context);
- } else {
- return "";
- }
- }
-
- public @Nullable Uri getUriSnippet() {
- Draft imageDraft = getDraftOfType(Draft.IMAGE);
-
- if (imageDraft != null && imageDraft.getValue() != null) {
- return Uri.parse(imageDraft.getValue());
- }
-
- return null;
- }
+ // We don't do anything with drafts of a given type anymore (image, audio etc.) - we store TEXT
+ // drafts, and any files or audio get sent to the recipient when added as a message.
}
-}
+}
\ No newline at end of file
diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/MmsSmsColumns.java b/app/src/main/java/org/thoughtcrime/securesms/database/MmsSmsColumns.java
index e6bc04e364..de5094fbd6 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/database/MmsSmsColumns.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/database/MmsSmsColumns.java
@@ -234,7 +234,8 @@ public interface MmsSmsColumns {
public static boolean isCallLog(long type) {
long baseType = type & BASE_TYPE_MASK;
- return baseType == INCOMING_CALL_TYPE || baseType == OUTGOING_CALL_TYPE || baseType == MISSED_CALL_TYPE || baseType == FIRST_MISSED_CALL_TYPE;
+ return baseType == INCOMING_CALL_TYPE || baseType == OUTGOING_CALL_TYPE ||
+ baseType == MISSED_CALL_TYPE || baseType == FIRST_MISSED_CALL_TYPE;
}
public static boolean isExpirationTimerUpdate(long type) {
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 08aad8b6df..8fdbe2accc 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/database/Storage.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/database/Storage.kt
@@ -2,6 +2,7 @@ package org.thoughtcrime.securesms.database
import android.content.Context
import android.net.Uri
+import network.loki.messenger.R
import java.security.MessageDigest
import network.loki.messenger.libsession_util.ConfigBase
import network.loki.messenger.libsession_util.ConfigBase.Companion.PRIORITY_HIDDEN
@@ -633,7 +634,11 @@ open class Storage(
// Notify the user
val threadID = getOrCreateThreadIdFor(Address.fromSerialized(groupId))
threadDb.setDate(threadID, formationTimestamp)
- insertOutgoingInfoMessage(context, groupId, SignalServiceGroup.Type.CREATION, title, members.map { it.serialize() }, admins.map { it.serialize() }, threadID, formationTimestamp)
+
+ // Note: Commenting out this line prevents the timestamp of room creation being added to a new closed group,
+ // which in turn allows us to show the `groupNoMessages` control message text.
+ //insertOutgoingInfoMessage(context, groupId, SignalServiceGroup.Type.CREATION, title, members.map { it.serialize() }, admins.map { it.serialize() }, threadID, formationTimestamp)
+
// Don't create config group here, it's from a config update
// Start polling
ClosedGroupPollerV2.shared.startPolling(group.accountId)
@@ -1444,7 +1449,10 @@ open class Storage(
SSKEnvironment.shared.messageExpirationManager.maybeStartExpiration(sentTimestamp, senderPublicKey, expiryMode)
}
- override fun insertMessageRequestResponse(response: MessageRequestResponse) {
+ /**
+ * This will create a control message used to indicate that a contact has accepted our message request
+ */
+ override fun insertMessageRequestResponseFromContact(response: MessageRequestResponse) {
val userPublicKey = getUserPublicKey()
val senderPublicKey = response.sender!!
val recipientPublicKey = response.recipient!!
@@ -1538,6 +1546,34 @@ open class Storage(
}
}
+ /**
+ * This will create a control message used to indicate that you have accepted a message request
+ */
+ override fun insertMessageRequestResponseFromYou(threadId: Long){
+ val userPublicKey = getUserPublicKey() ?: return
+
+ val mmsDb = DatabaseComponent.get(context).mmsDatabase()
+ val message = IncomingMediaMessage(
+ fromSerialized(userPublicKey),
+ SnodeAPI.nowWithOffset,
+ -1,
+ 0,
+ 0,
+ false,
+ false,
+ true,
+ false,
+ Optional.absent(),
+ Optional.absent(),
+ Optional.absent(),
+ Optional.absent(),
+ Optional.absent(),
+ Optional.absent(),
+ Optional.absent()
+ )
+ mmsDb.insertSecureDecryptedMessageInbox(message, threadId, runThreadUpdate = false)
+ }
+
override fun getRecipientApproved(address: Address): Boolean {
return DatabaseComponent.get(context).recipientDatabase().getApproved(address)
}
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 f5c6da5fb9..f48686aded 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/database/ThreadDatabase.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/database/ThreadDatabase.java
@@ -808,8 +808,8 @@ public class ThreadDatabase extends Database {
private @NonNull String getFormattedBodyFor(@NonNull MessageRecord messageRecord) {
if (messageRecord.isMms()) {
MmsMessageRecord record = (MmsMessageRecord) messageRecord;
- if (record.getSharedContacts().size() > 0) {
- Contact contact = ((MmsMessageRecord) messageRecord).getSharedContacts().get(0);
+ if (!record.getSharedContacts().isEmpty()) {
+ Contact contact = ((MmsMessageRecord)messageRecord).getSharedContacts().get(0);
return ContactUtil.getStringSummary(context, contact).toString();
}
String attachmentString = record.getSlideDeck().getBody();
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 9ee3a6957c..b6ebd6db84 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
@@ -1,18 +1,20 @@
package org.thoughtcrime.securesms.database.helpers;
+import static org.session.libsession.utilities.StringSubstitutionConstants.APP_NAME_KEY;
+
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.content.Context;
import android.database.Cursor;
-
import androidx.annotation.NonNull;
import androidx.core.app.NotificationCompat;
-
+import com.squareup.phrase.Phrase;
+import java.io.File;
import net.zetetic.database.sqlcipher.SQLiteConnection;
import net.zetetic.database.sqlcipher.SQLiteDatabase;
import net.zetetic.database.sqlcipher.SQLiteDatabaseHook;
import net.zetetic.database.sqlcipher.SQLiteOpenHelper;
-
+import network.loki.messenger.R;
import org.session.libsession.utilities.TextSecurePreferences;
import org.session.libsignal.utilities.Log;
import org.thoughtcrime.securesms.crypto.DatabaseSecret;
@@ -39,13 +41,8 @@ import org.thoughtcrime.securesms.database.SessionContactDatabase;
import org.thoughtcrime.securesms.database.SessionJobDatabase;
import org.thoughtcrime.securesms.database.SmsDatabase;
import org.thoughtcrime.securesms.database.ThreadDatabase;
-import org.thoughtcrime.securesms.notifications.NotificationChannels;
import org.thoughtcrime.securesms.util.ConfigurationMessageUtilities;
-import java.io.File;
-
-import network.loki.messenger.R;
-
public class SQLCipherOpenHelper extends SQLiteOpenHelper {
@SuppressWarnings("unused")
@@ -250,18 +247,22 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper {
// Notify the user of the issue so they know they can downgrade until the issue is fixed
NotificationManager notificationManager = context.getSystemService(NotificationManager.class);
- String channelId = context.getString(R.string.NotificationChannel_failures);
+ String channelId = context.getString(R.string.failures);
NotificationChannel channel = new NotificationChannel(channelId, channelId, NotificationManager.IMPORTANCE_HIGH);
channel.enableVibration(true);
notificationManager.createNotificationChannel(channel);
+ CharSequence errorTxt = Phrase.from(context, R.string.databaseErrorGeneric)
+ .put(APP_NAME_KEY, R.string.app_name)
+ .format();
+
NotificationCompat.Builder builder = new NotificationCompat.Builder(context, channelId)
.setSmallIcon(R.drawable.ic_notification)
.setColor(context.getResources().getColor(R.color.textsecure_primary))
.setCategory(NotificationCompat.CATEGORY_ERROR)
- .setContentTitle(context.getString(R.string.ErrorNotifier_migration))
- .setContentText(context.getString(R.string.ErrorNotifier_migration_downgrade))
+ .setContentTitle(context.getString(R.string.errorDatabase))
+ .setContentText(errorTxt)
.setAutoCancel(true);
notificationManager.notify(5874, builder.build());
diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/loaders/BucketedThreadMediaLoader.java b/app/src/main/java/org/thoughtcrime/securesms/database/loaders/BucketedThreadMediaLoader.java
deleted file mode 100644
index 05c13cc55d..0000000000
--- a/app/src/main/java/org/thoughtcrime/securesms/database/loaders/BucketedThreadMediaLoader.java
+++ /dev/null
@@ -1,232 +0,0 @@
-package org.thoughtcrime.securesms.database.loaders;
-
-
-import android.content.Context;
-import android.database.ContentObserver;
-import android.database.Cursor;
-
-import androidx.annotation.NonNull;
-import androidx.loader.content.AsyncTaskLoader;
-
-import com.annimon.stream.Stream;
-
-import org.session.libsession.utilities.Address;
-import org.session.libsession.utilities.recipients.Recipient;
-import org.thoughtcrime.securesms.database.MediaDatabase;
-import org.thoughtcrime.securesms.dependencies.DatabaseComponent;
-
-import java.text.SimpleDateFormat;
-import java.util.ArrayList;
-import java.util.Calendar;
-import java.util.Collections;
-import java.util.Date;
-import java.util.HashMap;
-import java.util.LinkedList;
-import java.util.List;
-import java.util.Locale;
-import java.util.Map;
-
-import network.loki.messenger.R;
-
-public class BucketedThreadMediaLoader extends AsyncTaskLoader {
-
- @SuppressWarnings("unused")
- private static final String TAG = BucketedThreadMediaLoader.class.getSimpleName();
-
- private final Address address;
- private final ContentObserver observer;
-
- public BucketedThreadMediaLoader(@NonNull Context context, @NonNull Address address) {
- super(context);
- this.address = address;
- this.observer = new ForceLoadContentObserver();
-
- onContentChanged();
- }
-
- @Override
- protected void onStartLoading() {
- if (takeContentChanged()) {
- forceLoad();
- }
- }
-
- @Override
- protected void onStopLoading() {
- cancelLoad();
- }
-
- @Override
- protected void onAbandon() {
- DatabaseComponent.get(getContext()).mediaDatabase().unsubscribeToMediaChanges(observer);
- }
-
- @Override
- public BucketedThreadMedia loadInBackground() {
- BucketedThreadMedia result = new BucketedThreadMedia(getContext());
- long threadId = DatabaseComponent.get(getContext()).threadDatabase().getOrCreateThreadIdFor(Recipient.from(getContext(), address, true));
-
- MediaDatabase mediaDatabase = DatabaseComponent.get(getContext()).mediaDatabase();
-
- mediaDatabase.subscribeToMediaChanges(observer);
- try (Cursor cursor = mediaDatabase.getGalleryMediaForThread(threadId)) {
- while (cursor != null && cursor.moveToNext()) {
- result.add(MediaDatabase.MediaRecord.from(getContext(), cursor));
- }
- }
-
- return result;
- }
-
- public static class BucketedThreadMedia {
-
- private final TimeBucket TODAY;
- private final TimeBucket YESTERDAY;
- private final TimeBucket THIS_WEEK;
- private final TimeBucket THIS_MONTH;
- private final MonthBuckets OLDER;
-
- private final TimeBucket[] TIME_SECTIONS;
-
- public BucketedThreadMedia(@NonNull Context context) {
- this.TODAY = new TimeBucket(context.getString(R.string.BucketedThreadMedia_Today), TimeBucket.addToCalendar(Calendar.DAY_OF_YEAR, -1), TimeBucket.addToCalendar(Calendar.DAY_OF_YEAR, 1000));
- this.YESTERDAY = new TimeBucket(context.getString(R.string.BucketedThreadMedia_Yesterday), TimeBucket.addToCalendar(Calendar.DAY_OF_YEAR, -2), TimeBucket.addToCalendar(Calendar.DAY_OF_YEAR, -1));
- this.THIS_WEEK = new TimeBucket(context.getString(R.string.BucketedThreadMedia_This_week), TimeBucket.addToCalendar(Calendar.DAY_OF_YEAR, -7), TimeBucket.addToCalendar(Calendar.DAY_OF_YEAR, -2));
- this.THIS_MONTH = new TimeBucket(context.getString(R.string.BucketedThreadMedia_This_month), TimeBucket.addToCalendar(Calendar.DAY_OF_YEAR, -30), TimeBucket.addToCalendar(Calendar.DAY_OF_YEAR, -7));
- this.TIME_SECTIONS = new TimeBucket[]{TODAY, YESTERDAY, THIS_WEEK, THIS_MONTH};
- this.OLDER = new MonthBuckets();
- }
-
-
- public void add(MediaDatabase.MediaRecord mediaRecord) {
- for (TimeBucket timeSection : TIME_SECTIONS) {
- if (timeSection.inRange(mediaRecord.getDate())) {
- timeSection.add(mediaRecord);
- return;
- }
- }
-
- OLDER.add(mediaRecord);
- }
-
- public int getSectionCount() {
- return (int)Stream.of(TIME_SECTIONS)
- .filter(timeBucket -> !timeBucket.isEmpty())
- .count() +
- OLDER.getSectionCount();
- }
-
- public int getSectionItemCount(int section) {
- List activeTimeBuckets = Stream.of(TIME_SECTIONS).filter(timeBucket -> !timeBucket.isEmpty()).toList();
-
- if (section < activeTimeBuckets.size()) return activeTimeBuckets.get(section).getItemCount();
- else return OLDER.getSectionItemCount(section - activeTimeBuckets.size());
- }
-
- public MediaDatabase.MediaRecord get(int section, int item) {
- List activeTimeBuckets = Stream.of(TIME_SECTIONS).filter(timeBucket -> !timeBucket.isEmpty()).toList();
-
- if (section < activeTimeBuckets.size()) return activeTimeBuckets.get(section).getItem(item);
- else return OLDER.getItem(section - activeTimeBuckets.size(), item);
- }
-
- public String getName(int section, Locale locale) {
- List activeTimeBuckets = Stream.of(TIME_SECTIONS).filter(timeBucket -> !timeBucket.isEmpty()).toList();
-
- if (section < activeTimeBuckets.size()) return activeTimeBuckets.get(section).getName();
- else return OLDER.getName(section - activeTimeBuckets.size(), locale);
- }
-
- private static class TimeBucket {
-
- private final List records = new LinkedList<>();
-
- private final long startTime;
- private final long endtime;
- private final String name;
-
- TimeBucket(String name, long startTime, long endtime) {
- this.name = name;
- this.startTime = startTime;
- this.endtime = endtime;
- }
-
- void add(MediaDatabase.MediaRecord record) {
- this.records.add(record);
- }
-
- boolean inRange(long timestamp) {
- return timestamp > startTime && timestamp <= endtime;
- }
-
- boolean isEmpty() {
- return records.isEmpty();
- }
-
- int getItemCount() {
- return records.size();
- }
-
- MediaDatabase.MediaRecord getItem(int position) {
- return records.get(position);
- }
-
- String getName() {
- return name;
- }
-
- static long addToCalendar(int field, int amount) {
- Calendar calendar = Calendar.getInstance();
- calendar.add(field, amount);
- return calendar.getTimeInMillis();
- }
- }
-
- private static class MonthBuckets {
-
- private final Map> months = new HashMap<>();
-
- void add(MediaDatabase.MediaRecord record) {
- Calendar calendar = Calendar.getInstance();
- calendar.setTimeInMillis(record.getDate());
-
- int year = calendar.get(Calendar.YEAR) - 1900;
- int month = calendar.get(Calendar.MONTH);
- Date date = new Date(year, month, 1);
-
- if (months.containsKey(date)) {
- months.get(date).add(record);
- } else {
- List list = new LinkedList<>();
- list.add(record);
- months.put(date, list);
- }
- }
-
- int getSectionCount() {
- return months.size();
- }
-
- int getSectionItemCount(int section) {
- return months.get(getSection(section)).size();
- }
-
- MediaDatabase.MediaRecord getItem(int section, int position) {
- return months.get(getSection(section)).get(position);
- }
-
- Date getSection(int section) {
- ArrayList keys = new ArrayList<>(months.keySet());
- Collections.sort(keys, Collections.reverseOrder());
-
- return keys.get(section);
- }
-
- String getName(int section, Locale locale) {
- Date sectionDate = getSection(section);
-
- return new SimpleDateFormat("MMMM, yyyy", locale).format(sectionDate);
- }
- }
- }
-}
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 639ea0db09..6ae671c065 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,15 +17,11 @@
package org.thoughtcrime.securesms.database.model;
import android.content.Context;
-import android.text.SpannableString;
import androidx.annotation.NonNull;
import org.session.libsession.utilities.recipients.Recipient;
-import org.session.libsignal.utilities.Log;
-import org.thoughtcrime.securesms.database.MmsDatabase;
import org.thoughtcrime.securesms.database.MmsSmsColumns;
-import org.thoughtcrime.securesms.database.MmsSmsDatabase;
import org.thoughtcrime.securesms.database.SmsDatabase;
/**
@@ -68,7 +64,7 @@ public abstract class DisplayRecord {
public @NonNull String getBody() {
return body == null ? "" : body;
}
- public abstract SpannableString getDisplayBody(@NonNull Context context);
+ public abstract CharSequence getDisplayBody(@NonNull Context context);
public Recipient getRecipient() { return recipient; }
public long getDateSent() { return dateSent; }
public long getDateReceived() { return dateReceived; }
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 1b566169d7..0383d17bda 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,7 +17,6 @@
package org.thoughtcrime.securesms.database.model;
import android.content.Context;
-import android.text.SpannableString;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
@@ -27,14 +26,11 @@ import org.session.libsession.utilities.Contact;
import org.session.libsession.utilities.IdentityKeyMismatch;
import org.session.libsession.utilities.NetworkFailure;
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
* media (ie: they've been downloaded).
@@ -76,15 +72,7 @@ public class MediaMmsMessageRecord extends MmsMessageRecord {
}
@Override
- public SpannableString getDisplayBody(@NonNull Context context) {
- if (MmsDatabase.Types.isFailedDecryptType(type)) {
- return emphasisAdded(context.getString(R.string.MmsMessageRecord_bad_encrypted_mms_message));
- } else if (MmsDatabase.Types.isDuplicateMessageType(type)) {
- 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));
- }
-
+ public CharSequence getDisplayBody(@NonNull Context context) {
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 a61b78b4b6..5f6257ee92 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
@@ -115,7 +115,7 @@ public abstract class MessageRecord extends DisplayRecord {
}
@Override
- public SpannableString getDisplayBody(@NonNull Context context) {
+ public CharSequence getDisplayBody(@NonNull Context context) {
if (isGroupUpdateMessage()) {
UpdateMessageData updateMessageData = UpdateMessageData.Companion.fromJSON(getBody());
return new SpannableString(UpdateMessageBuilder.INSTANCE.buildGroupUpdateMessage(context, updateMessageData, getIndividualRecipient().getAddress().serialize(), isOutgoing()));
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 83ee921a2a..70e80d720e 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
@@ -18,14 +18,13 @@
package org.thoughtcrime.securesms.database.model;
import android.content.Context;
-import android.text.SpannableString;
+
import androidx.annotation.NonNull;
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;
/**
* The message record model which represents standard SMS messages.
@@ -56,16 +55,8 @@ public class SmsMessageRecord extends MessageRecord {
}
@Override
- public SpannableString getDisplayBody(@NonNull Context context) {
- if (SmsDatabase.Types.isFailedDecryptType(type)) {
- return emphasisAdded(context.getString(R.string.MessageDisplayHelper_bad_encrypted_message));
- } 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 {
- return super.getDisplayBody(context);
- }
+ public CharSequence getDisplayBody(@NonNull Context context) {
+ return super.getDisplayBody(context);
}
@Override
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 0c023a8f29..d91f4c428c 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
@@ -17,21 +17,31 @@
*/
package org.thoughtcrime.securesms.database.model;
+import static org.session.libsession.utilities.StringSubstitutionConstants.APP_NAME_KEY;
+import static org.session.libsession.utilities.StringSubstitutionConstants.AUTHOR_KEY;
+import static org.session.libsession.utilities.StringSubstitutionConstants.DISAPPEARING_MESSAGES_TYPE_KEY;
+import static org.session.libsession.utilities.StringSubstitutionConstants.MESSAGE_SNIPPET_KEY;
+import static org.session.libsession.utilities.StringSubstitutionConstants.NAME_KEY;
+import static org.session.libsession.utilities.StringSubstitutionConstants.TIME_KEY;
+
import android.content.Context;
import android.net.Uri;
import android.text.Spannable;
import android.text.SpannableString;
import android.text.TextUtils;
import android.text.style.StyleSpan;
-
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
-
+import com.squareup.phrase.Phrase;
import org.session.libsession.utilities.ExpirationUtil;
+import org.session.libsession.utilities.TextSecurePreferences;
import org.session.libsession.utilities.recipients.Recipient;
+import org.session.libsignal.utilities.Log;
import org.thoughtcrime.securesms.database.MmsSmsColumns;
import org.thoughtcrime.securesms.database.SmsDatabase;
+import org.thoughtcrime.securesms.ui.UtilKt;
+import kotlin.Pair;
import network.loki.messenger.R;
/**
@@ -42,146 +52,170 @@ import network.loki.messenger.R;
*/
public class ThreadRecord extends DisplayRecord {
- private @Nullable final Uri snippetUri;
- public @Nullable final MessageRecord lastMessage;
- private final long count;
- private final int unreadCount;
- private final int unreadMentionCount;
- private final int distributionType;
- private final boolean archived;
- private final long expiresIn;
- private final long lastSeen;
- private final boolean pinned;
- private final int initialRecipientHash;
+ private @Nullable final Uri snippetUri;
+ public @Nullable final MessageRecord lastMessage;
+ private final long count;
+ private final int unreadCount;
+ private final int unreadMentionCount;
+ private final int distributionType;
+ private final boolean archived;
+ private final long expiresIn;
+ private final long lastSeen;
+ private final boolean pinned;
+ private final int initialRecipientHash;
+ private final long dateSent;
- public ThreadRecord(@NonNull String body, @Nullable Uri snippetUri,
- @Nullable MessageRecord lastMessage, @NonNull Recipient recipient, long date, long count, int unreadCount,
- int unreadMentionCount, long threadId, int deliveryReceiptCount, int status,
- long snippetType, int distributionType, boolean archived, long expiresIn,
- long lastSeen, int readReceiptCount, boolean pinned)
- {
- super(body, recipient, date, date, threadId, status, deliveryReceiptCount, snippetType, readReceiptCount);
- this.snippetUri = snippetUri;
- this.lastMessage = lastMessage;
- this.count = count;
- this.unreadCount = unreadCount;
- this.unreadMentionCount = unreadMentionCount;
- this.distributionType = distributionType;
- this.archived = archived;
- this.expiresIn = expiresIn;
- this.lastSeen = lastSeen;
- this.pinned = pinned;
- this.initialRecipientHash = recipient.hashCode();
- }
-
- public @Nullable Uri getSnippetUri() {
- return snippetUri;
- }
-
- @Override
- public SpannableString getDisplayBody(@NonNull Context context) {
- if (isGroupUpdateMessage()) {
- return emphasisAdded(context.getString(R.string.ThreadRecord_group_updated));
- } else if (isOpenGroupInvitation()) {
- return emphasisAdded(context.getString(R.string.ThreadRecord_open_group_invitation));
- } 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 (SmsDatabase.Types.isEndSessionType(type)) {
- return emphasisAdded(context.getString(R.string.ThreadRecord_secure_session_reset));
- } 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 (MmsSmsColumns.Types.isDraftMessageType(type)) {
- String draftText = context.getString(R.string.ThreadRecord_draft);
- return emphasisAdded(draftText + " " + getBody(), 0, draftText.length());
- } else if (SmsDatabase.Types.isOutgoingCall(type)) {
- return emphasisAdded(context.getString(network.loki.messenger.R.string.ThreadRecord_called));
- } else if (SmsDatabase.Types.isIncomingCall(type)) {
- return emphasisAdded(context.getString(network.loki.messenger.R.string.ThreadRecord_called_you));
- } else if (SmsDatabase.Types.isMissedCall(type)) {
- return emphasisAdded(context.getString(network.loki.messenger.R.string.ThreadRecord_missed_call));
- } else if (SmsDatabase.Types.isJoinedType(type)) {
- return emphasisAdded(context.getString(R.string.ThreadRecord_s_is_on_signal, getRecipient().toShortString()));
- } else if (SmsDatabase.Types.isExpirationTimerUpdate(type)) {
- int seconds = (int) (getExpiresIn() / 1000);
- if (seconds <= 0) {
- return emphasisAdded(context.getString(R.string.ThreadRecord_disappearing_messages_disabled));
- }
- String time = ExpirationUtil.getExpirationDisplayValue(context, seconds);
- return emphasisAdded(context.getString(R.string.ThreadRecord_disappearing_message_time_updated_to_s, time));
- } else if (MmsSmsColumns.Types.isMediaSavedExtraction(type)) {
- return emphasisAdded(context.getString(R.string.ThreadRecord_media_saved_by_s, getRecipient().toShortString()));
- } else if (MmsSmsColumns.Types.isScreenshotExtraction(type)) {
- return emphasisAdded(context.getString(R.string.ThreadRecord_s_took_a_screenshot, getRecipient().toShortString()));
- } else if (SmsDatabase.Types.isIdentityUpdate(type)) {
- if (getRecipient().isGroupRecipient()) return emphasisAdded(context.getString(R.string.ThreadRecord_safety_number_changed));
- else return emphasisAdded(context.getString(R.string.ThreadRecord_your_safety_number_with_s_has_changed, getRecipient().toShortString()));
- } else if (SmsDatabase.Types.isIdentityVerified(type)) {
- return emphasisAdded(context.getString(R.string.ThreadRecord_you_marked_verified));
- } else if (SmsDatabase.Types.isIdentityDefault(type)) {
- return emphasisAdded(context.getString(R.string.ThreadRecord_you_marked_unverified));
- } else if (MmsSmsColumns.Types.isMessageRequestResponse(type)) {
- return emphasisAdded(context.getString(R.string.message_requests_accepted));
- } else if (getCount() == 0) {
- return new SpannableString(context.getString(R.string.ThreadRecord_empty_message));
- } else {
- if (TextUtils.isEmpty(getBody())) {
- return new SpannableString(emphasisAdded(context.getString(R.string.ThreadRecord_media_message)));
- } else {
- return new SpannableString(getBody());
- }
+ public ThreadRecord(@NonNull String body, @Nullable Uri snippetUri,
+ @Nullable MessageRecord lastMessage, @NonNull Recipient recipient, long date, long count, int unreadCount,
+ int unreadMentionCount, long threadId, int deliveryReceiptCount, int status,
+ long snippetType, int distributionType, boolean archived, long expiresIn,
+ long lastSeen, int readReceiptCount, boolean pinned)
+ {
+ super(body, recipient, date, date, threadId, status, deliveryReceiptCount, snippetType, readReceiptCount);
+ this.snippetUri = snippetUri;
+ this.lastMessage = lastMessage;
+ this.count = count;
+ this.unreadCount = unreadCount;
+ this.unreadMentionCount = unreadMentionCount;
+ this.distributionType = distributionType;
+ this.archived = archived;
+ this.expiresIn = expiresIn;
+ this.lastSeen = lastSeen;
+ this.pinned = pinned;
+ this.initialRecipientHash = recipient.hashCode();
+ this.dateSent = date;
}
- }
- private SpannableString emphasisAdded(String sequence) {
- return emphasisAdded(sequence, 0, sequence.length());
- }
+ public @Nullable Uri getSnippetUri() {
+ return snippetUri;
+ }
- private SpannableString emphasisAdded(String sequence, int start, int end) {
- SpannableString spannable = new SpannableString(sequence);
- spannable.setSpan(new StyleSpan(android.graphics.Typeface.ITALIC),
- start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
- return spannable;
- }
+ private String getName() {
+ String name = getRecipient().getName();
+ if (name == null) {
+ Log.w("ThreadRecord", "Got a null name - using: Unknown");
+ name = "Unknown";
+ }
+ return name;
+ }
- public long getCount() {
- return count;
- }
- public int getUnreadCount() {
- return unreadCount;
- }
+ @Override
+ public CharSequence getDisplayBody(@NonNull Context context) {
+ if (isGroupUpdateMessage()) {
+ return context.getString(R.string.groupUpdated);
+ } else if (isOpenGroupInvitation()) {
+ return context.getString(R.string.communityInvitation);
+ } else if (MmsSmsColumns.Types.isLegacyType(type)) {
+ return Phrase.from(context, R.string.messageErrorOld)
+ .put(APP_NAME_KEY, context.getString(R.string.app_name))
+ .format().toString();
+ } else if (MmsSmsColumns.Types.isDraftMessageType(type)) {
+ String draftText = context.getString(R.string.draft);
+ return draftText + " " + getBody();
+ } else if (SmsDatabase.Types.isOutgoingCall(type)) {
+ return Phrase.from(context, R.string.callsYouCalled)
+ .put(NAME_KEY, getName())
+ .format().toString();
+ } else if (SmsDatabase.Types.isIncomingCall(type)) {
+ return Phrase.from(context, R.string.callsCalledYou)
+ .put(NAME_KEY, getName())
+ .format().toString();
+ } else if (SmsDatabase.Types.isMissedCall(type)) {
+ return Phrase.from(context, R.string.callsMissedCallFrom)
+ .put(NAME_KEY, getName())
+ .format().toString();
+ } else if (SmsDatabase.Types.isExpirationTimerUpdate(type)) {
+ // Use the same message as we would for displaying on the conversation screen.
+ // lastMessage shouldn't be null here, but we'll check just in case.
+ if (lastMessage != null) {
+ return lastMessage.getDisplayBody(context).toString();
+ } else {
+ return "";
+ }
+ } else if (MmsSmsColumns.Types.isMediaSavedExtraction(type)) {
+ return Phrase.from(context, R.string.attachmentsMediaSaved)
+ .put(NAME_KEY, getName())
+ .format().toString();
- public int getUnreadMentionCount() {
- return unreadMentionCount;
- }
+ } else if (MmsSmsColumns.Types.isScreenshotExtraction(type)) {
+ return Phrase.from(context, R.string.screenshotTaken)
+ .put(NAME_KEY, getName())
+ .format().toString();
- public long getDate() {
- return getDateReceived();
- }
+ } else if (MmsSmsColumns.Types.isMessageRequestResponse(type)) {
+ if (lastMessage.getRecipient().getAddress().serialize().equals(
+ TextSecurePreferences.getLocalNumber(context))) {
+ return UtilKt.getSubbedCharSequence(
+ context,
+ R.string.messageRequestYouHaveAccepted,
+ new Pair<>(NAME_KEY, getName())
+ );
+ }
- public boolean isArchived() {
- return archived;
- }
+ return context.getString(R.string.messageRequestsAccepted);
+ } else if (getCount() == 0) {
+ return new SpannableString(context.getString(R.string.messageEmpty));
+ } else {
+ // This block hits when we receive a media message from an unaccepted contact - however,
+ // unaccepted contacts aren't allowed to send us media - so we'll return an empty string
+ // if it's JUST an image, or the body text that accompanied the image should any exist.
+ // We could return null here - but then we have to find all the usages of this
+ // `getDisplayBody` method and make sure it doesn't fall over if it has a null result.
+ if (TextUtils.isEmpty(getBody())) {
+ return new SpannableString("");
+ // Old behaviour was: return new SpannableString(emphasisAdded(context.getString(R.string.mediaMessage)));
+ } else {
+ return getNonControlMessageDisplayBody(context);
+ }
+ }
+ }
- public int getDistributionType() {
- return distributionType;
- }
+ /**
+ * Logic to get the body for non control messages
+ */
+ public CharSequence getNonControlMessageDisplayBody(@NonNull Context context) {
+ Recipient recipient = getRecipient();
+ // The logic will differ depending on the type.
+ // 1-1, note to self and control messages (we shouldn't have any in here, but leaving the
+ // logic to be safe) do not need author details
+ if (recipient.isLocalNumber() || recipient.is1on1() ||
+ (lastMessage != null && lastMessage.isControlMessage())
+ ) {
+ return getBody();
+ } else { // for groups (new, legacy, communities) show either 'You' or the contact's name
+ String prefix = "";
+ if (lastMessage != null && lastMessage.isOutgoing()) {
+ prefix = context.getString(R.string.you);
+ }
+ else if(lastMessage != null){
+ prefix = lastMessage.getIndividualRecipient().toShortString();
+ }
- public long getExpiresIn() {
- return expiresIn;
- }
+ return Phrase.from(context.getString(R.string.messageSnippetGroup))
+ .put(AUTHOR_KEY, prefix)
+ .put(MESSAGE_SNIPPET_KEY, getBody())
+ .format().toString();
+ }
+ }
- public long getLastSeen() {
- return lastSeen;
- }
+ public long getCount() { return count; }
- public boolean isPinned() {
- return pinned;
- }
+ public int getUnreadCount() { return unreadCount; }
- public int getInitialRecipientHash() {
- return initialRecipientHash;
- }
+ public int getUnreadMentionCount() { return unreadMentionCount; }
+
+ public long getDate() { return getDateReceived(); }
+
+ public boolean isArchived() { return archived; }
+
+ public int getDistributionType() { return distributionType; }
+
+ public long getExpiresIn() { return expiresIn; }
+
+ public long getLastSeen() { return lastSeen; }
+
+ public boolean isPinned() { return pinned; }
+
+ public int getInitialRecipientHash() { return initialRecipientHash; }
}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/debugmenu/DebugActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/debugmenu/DebugActivity.kt
new file mode 100644
index 0000000000..828b3c3a1e
--- /dev/null
+++ b/app/src/main/java/org/thoughtcrime/securesms/debugmenu/DebugActivity.kt
@@ -0,0 +1,21 @@
+package org.thoughtcrime.securesms.debugmenu
+
+import android.os.Bundle
+import androidx.appcompat.app.AppCompatActivity
+import dagger.hilt.android.AndroidEntryPoint
+import org.thoughtcrime.securesms.ui.setComposeContent
+
+
+@AndroidEntryPoint
+class DebugActivity : AppCompatActivity() {
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+
+ setComposeContent {
+ DebugMenuScreen(
+ onClose = { finish() }
+ )
+ }
+ }
+}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/debugmenu/DebugMenu.kt b/app/src/main/java/org/thoughtcrime/securesms/debugmenu/DebugMenu.kt
new file mode 100644
index 0000000000..f277d1f40b
--- /dev/null
+++ b/app/src/main/java/org/thoughtcrime/securesms/debugmenu/DebugMenu.kt
@@ -0,0 +1,184 @@
+package org.thoughtcrime.securesms.debugmenu
+
+import androidx.compose.foundation.background
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.ColumnScope
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.verticalScroll
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.Scaffold
+import androidx.compose.material3.SnackbarHost
+import androidx.compose.material3.SnackbarHostState
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.LocalClipboardManager
+import androidx.compose.ui.text.AnnotatedString
+import androidx.compose.ui.tooling.preview.Preview
+import network.loki.messenger.BuildConfig
+import network.loki.messenger.R
+import org.thoughtcrime.securesms.debugmenu.DebugMenuViewModel.Commands.ChangeEnvironment
+import org.thoughtcrime.securesms.debugmenu.DebugMenuViewModel.Commands.HideEnvironmentWarningDialog
+import org.thoughtcrime.securesms.debugmenu.DebugMenuViewModel.Commands.ShowEnvironmentWarningDialog
+import org.thoughtcrime.securesms.ui.AlertDialog
+import org.thoughtcrime.securesms.ui.Cell
+import org.thoughtcrime.securesms.ui.DialogButtonModel
+import org.thoughtcrime.securesms.ui.GetString
+import org.thoughtcrime.securesms.ui.LoadingDialog
+import org.thoughtcrime.securesms.ui.components.BackAppBar
+import org.thoughtcrime.securesms.ui.components.DropDown
+import org.thoughtcrime.securesms.ui.theme.LocalColors
+import org.thoughtcrime.securesms.ui.theme.LocalDimensions
+import org.thoughtcrime.securesms.ui.theme.LocalType
+import org.thoughtcrime.securesms.ui.theme.PreviewTheme
+import org.thoughtcrime.securesms.ui.theme.bold
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+fun DebugMenu(
+ uiState: DebugMenuViewModel.UIState,
+ sendCommand: (DebugMenuViewModel.Commands) -> Unit,
+ modifier: Modifier = Modifier,
+ onClose: () -> Unit
+) {
+ val snackbarHostState = remember { SnackbarHostState() }
+
+ Scaffold(
+ modifier = modifier.fillMaxSize(),
+ snackbarHost = {
+ SnackbarHost(hostState = snackbarHostState)
+ }
+ ) { contentPadding ->
+ // display a snackbar when required
+ LaunchedEffect(uiState.snackMessage) {
+ if (!uiState.snackMessage.isNullOrEmpty()) {
+ snackbarHostState.showSnackbar(uiState.snackMessage)
+ }
+ }
+
+ // Alert dialogs
+ if (uiState.showEnvironmentWarningDialog) {
+ AlertDialog(
+ onDismissRequest = { sendCommand(HideEnvironmentWarningDialog) },
+ title = "Are you sure you want to switch environments?",
+ text = "Changing this setting will result in all conversations and Snode data being cleared...",
+ showCloseButton = false, // don't display the 'x' button
+ buttons = listOf(
+ DialogButtonModel(
+ text = GetString(R.string.cancel),
+ contentDescription = GetString(R.string.cancel),
+ onClick = { sendCommand(HideEnvironmentWarningDialog) }
+ ),
+ DialogButtonModel(
+ text = GetString(R.string.ok),
+ contentDescription = GetString(R.string.ok),
+ onClick = { sendCommand(ChangeEnvironment) }
+ )
+ )
+ )
+ }
+
+ if (uiState.showEnvironmentLoadingDialog) {
+ LoadingDialog(title = "Changing Environment...")
+ }
+
+ Column(
+ modifier = Modifier
+ .padding(contentPadding)
+ .fillMaxSize()
+ .background(color = LocalColors.current.background)
+ ) {
+ // App bar
+ BackAppBar(title = "Debug Menu", onBack = onClose)
+
+ Column(
+ modifier = Modifier
+ .padding(horizontal = LocalDimensions.current.spacing)
+ .verticalScroll(rememberScrollState())
+ ) {
+ // Info pane
+ val clipboardManager = LocalClipboardManager.current
+ val appVersion = "${BuildConfig.VERSION_NAME} (${BuildConfig.VERSION_CODE} - ${
+ BuildConfig.GIT_HASH.take(
+ 6
+ )
+ })"
+
+ DebugCell(
+ modifier = Modifier.clickable {
+ // clicking the cell copies the version number to the clipboard
+ clipboardManager.setText(AnnotatedString(appVersion))
+ },
+ title = "App Info"
+ ) {
+ Text(
+ text = "Version: $appVersion",
+ style = LocalType.current.base
+ )
+ }
+
+ // Environment
+ DebugCell("Environment") {
+ DropDown(
+ modifier = Modifier.fillMaxWidth(0.6f),
+ selectedText = uiState.currentEnvironment,
+ values = uiState.environments,
+ onValueSelected = {
+ sendCommand(ShowEnvironmentWarningDialog(it))
+ }
+ )
+ }
+ }
+ }
+ }
+}
+
+@Composable
+fun ColumnScope.DebugCell(
+ title: String,
+ modifier: Modifier = Modifier,
+ content: @Composable () -> Unit
+) {
+ Spacer(modifier = Modifier.height(LocalDimensions.current.smallSpacing))
+
+ Cell {
+ Column(
+ modifier = modifier.padding(LocalDimensions.current.spacing)
+ ) {
+ Text(
+ text = title,
+ style = LocalType.current.large.bold()
+ )
+
+ Spacer(modifier = Modifier.height(LocalDimensions.current.xsSpacing))
+
+ content()
+ }
+ }
+}
+
+@Preview
+@Composable
+fun PreviewDebugMenu() {
+ PreviewTheme {
+ DebugMenu(
+ uiState = DebugMenuViewModel.UIState(
+ currentEnvironment = "Development",
+ environments = listOf("Development", "Production"),
+ snackMessage = null,
+ showEnvironmentWarningDialog = false,
+ showEnvironmentLoadingDialog = false
+ ),
+ sendCommand = {},
+ onClose = {}
+ )
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/org/thoughtcrime/securesms/debugmenu/DebugMenuScreen.kt b/app/src/main/java/org/thoughtcrime/securesms/debugmenu/DebugMenuScreen.kt
new file mode 100644
index 0000000000..6c0f22805a
--- /dev/null
+++ b/app/src/main/java/org/thoughtcrime/securesms/debugmenu/DebugMenuScreen.kt
@@ -0,0 +1,23 @@
+package org.thoughtcrime.securesms.debugmenu
+
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.collectAsState
+import androidx.compose.runtime.getValue
+import androidx.compose.ui.Modifier
+import androidx.lifecycle.viewmodel.compose.viewModel
+
+@Composable
+fun DebugMenuScreen(
+ modifier: Modifier = Modifier,
+ debugMenuViewModel: DebugMenuViewModel = viewModel(),
+ onClose: () -> Unit
+) {
+ val uiState by debugMenuViewModel.uiState.collectAsState()
+
+ DebugMenu(
+ modifier = modifier,
+ uiState = uiState,
+ sendCommand = debugMenuViewModel::onCommand,
+ onClose = onClose
+ )
+}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/debugmenu/DebugMenuViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/debugmenu/DebugMenuViewModel.kt
new file mode 100644
index 0000000000..750b3e20c7
--- /dev/null
+++ b/app/src/main/java/org/thoughtcrime/securesms/debugmenu/DebugMenuViewModel.kt
@@ -0,0 +1,115 @@
+package org.thoughtcrime.securesms.debugmenu
+
+import android.app.Application
+import android.widget.Toast
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.viewModelScope
+import dagger.hilt.android.lifecycle.HiltViewModel
+import kotlinx.coroutines.Dispatchers.Main
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
+import network.loki.messenger.R
+import org.session.libsession.messaging.open_groups.OpenGroupApi
+import org.session.libsession.snode.SnodeAPI
+import org.session.libsession.utilities.TextSecurePreferences
+import org.session.libsignal.utilities.Log
+import org.thoughtcrime.securesms.ApplicationContext
+import org.session.libsession.utilities.Environment
+import org.thoughtcrime.securesms.dependencies.DatabaseComponent
+import org.thoughtcrime.securesms.util.ConfigurationMessageUtilities
+import javax.inject.Inject
+
+@HiltViewModel
+class DebugMenuViewModel @Inject constructor(
+ private val application: Application,
+ private val textSecurePreferences: TextSecurePreferences
+) : ViewModel() {
+ private val TAG = "DebugMenu"
+
+ private val _uiState = MutableStateFlow(
+ UIState(
+ currentEnvironment = textSecurePreferences.getEnvironment().label,
+ environments = Environment.entries.map { it.label },
+ snackMessage = null,
+ showEnvironmentWarningDialog = false,
+ showEnvironmentLoadingDialog = false
+ )
+ )
+ val uiState: StateFlow
+ get() = _uiState
+
+ private var temporaryEnv: Environment? = null
+
+ fun onCommand(command: Commands) {
+ when (command) {
+ is Commands.ChangeEnvironment -> changeEnvironment()
+
+ is Commands.HideEnvironmentWarningDialog -> _uiState.value =
+ _uiState.value.copy(showEnvironmentWarningDialog = false)
+
+ is Commands.ShowEnvironmentWarningDialog ->
+ showEnvironmentWarningDialog(command.environment)
+ }
+ }
+
+ private fun showEnvironmentWarningDialog(environment: String) {
+ if(environment == _uiState.value.currentEnvironment) return
+ val env = Environment.entries.firstOrNull { it.label == environment } ?: return
+
+ temporaryEnv = env
+
+ _uiState.value = _uiState.value.copy(showEnvironmentWarningDialog = true)
+ }
+
+ private fun changeEnvironment() {
+ val env = temporaryEnv ?: return
+
+ // show a loading state
+ _uiState.value = _uiState.value.copy(
+ showEnvironmentWarningDialog = false,
+ showEnvironmentLoadingDialog = true
+ )
+
+ // clear remote and local data, then restart the app
+ viewModelScope.launch {
+ try {
+ ConfigurationMessageUtilities.forceSyncConfigurationNowIfNeeded(application).get()
+ } catch (e: Exception) {
+ // we can ignore fails here as we might be switching environments before the user gets a public key
+ }
+ ApplicationContext.getInstance(application).clearAllData().let { success ->
+ if(success){
+ // save the environment
+ textSecurePreferences.setEnvironment(env)
+ delay(500)
+ ApplicationContext.getInstance(application).restartApplication()
+ } else {
+ _uiState.value = _uiState.value.copy(
+ showEnvironmentWarningDialog = false,
+ showEnvironmentLoadingDialog = false
+ )
+ Log.e(TAG, "Failed to force sync when deleting data")
+ _uiState.value = _uiState.value.copy(snackMessage = "Sorry, something went wrong...")
+ return@launch
+ }
+ }
+ }
+ }
+
+ data class UIState(
+ val currentEnvironment: String,
+ val environments: List,
+ val snackMessage: String?,
+ val showEnvironmentWarningDialog: Boolean,
+ val showEnvironmentLoadingDialog: Boolean
+ )
+
+ sealed class Commands {
+ object ChangeEnvironment : Commands()
+ data class ShowEnvironmentWarningDialog(val environment: String) : Commands()
+ object HideEnvironmentWarningDialog : Commands()
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/org/thoughtcrime/securesms/emoji/EmojiCategory.kt b/app/src/main/java/org/thoughtcrime/securesms/emoji/EmojiCategory.kt
index 714996e6c8..c9a59b2107 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/emoji/EmojiCategory.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/emoji/EmojiCategory.kt
@@ -15,8 +15,7 @@ enum class EmojiCategory(val priority: Int, val key: String, @AttrRes val icon:
PLACES(4, "Places", R.attr.emoji_category_places),
OBJECTS(5, "Objects", R.attr.emoji_category_objects),
SYMBOLS(6, "Symbols", R.attr.emoji_category_symbol),
- FLAGS(7, "Flags", R.attr.emoji_category_flags),
- EMOTICONS(8, "Emoticons", R.attr.emoji_category_emoticons);
+ FLAGS(7, "Flags", R.attr.emoji_category_flags);
@StringRes
fun getCategoryLabel(): Int {
@@ -31,15 +30,14 @@ enum class EmojiCategory(val priority: Int, val key: String, @AttrRes val icon:
@StringRes
fun getCategoryLabel(@AttrRes iconAttr: Int): Int {
return when (iconAttr) {
- R.attr.emoji_category_people -> R.string.ReactWithAnyEmojiBottomSheetDialogFragment__smileys_and_people
- R.attr.emoji_category_nature -> R.string.ReactWithAnyEmojiBottomSheetDialogFragment__nature
- R.attr.emoji_category_foods -> R.string.ReactWithAnyEmojiBottomSheetDialogFragment__food
- R.attr.emoji_category_activity -> R.string.ReactWithAnyEmojiBottomSheetDialogFragment__activities
- R.attr.emoji_category_places -> R.string.ReactWithAnyEmojiBottomSheetDialogFragment__places
- R.attr.emoji_category_objects -> R.string.ReactWithAnyEmojiBottomSheetDialogFragment__objects
- R.attr.emoji_category_symbol -> R.string.ReactWithAnyEmojiBottomSheetDialogFragment__symbols
- R.attr.emoji_category_flags -> R.string.ReactWithAnyEmojiBottomSheetDialogFragment__flags
- R.attr.emoji_category_emoticons -> R.string.ReactWithAnyEmojiBottomSheetDialogFragment__emoticons
+ R.attr.emoji_category_people -> R.string.emojiCategorySmileys
+ R.attr.emoji_category_nature -> R.string.emojiCategoryAnimals
+ R.attr.emoji_category_foods -> R.string.emojiCategoryFood
+ R.attr.emoji_category_activity -> R.string.emojiCategoryActivities
+ R.attr.emoji_category_places -> R.string.emojiCategoryTravel
+ R.attr.emoji_category_objects -> R.string.emojiCategoryObjects
+ R.attr.emoji_category_symbol -> R.string.emojiCategorySymbols
+ R.attr.emoji_category_flags -> R.string.emojiCategoryFlags
else -> throw AssertionError()
}
}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/emoji/EmojiSource.kt b/app/src/main/java/org/thoughtcrime/securesms/emoji/EmojiSource.kt
index 0b221eb3d1..75b1496cc4 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/emoji/EmojiSource.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/emoji/EmojiSource.kt
@@ -110,10 +110,12 @@ class EmojiSource(
val parsedData: ParsedEmojiData = EmojiJsonParser.parse(it, ::getAssetsUri).getOrThrow()
return EmojiSource(
ScreenDensity.xhdpiRelativeDensityScaleFactor("xhdpi"),
+
parsedData.copy(
- displayPages = parsedData.displayPages + PAGE_EMOTICONS,
- dataPages = parsedData.dataPages + PAGE_EMOTICONS
+ displayPages = parsedData.displayPages,
+ dataPages = parsedData.dataPages
)
+
) { uri: Uri -> EmojiPage.Asset(uri) }
}
}
@@ -137,25 +139,3 @@ data class ObsoleteEmoji(val obsolete: String, val replaceWith: String)
data class EmojiMetrics(val rawHeight: Int, val rawWidth: Int, val perRow: Int)
private fun getAssetsUri(name: String, format: String): Uri = Uri.parse("file:///android_asset/emoji/$name.$format")
-
-private val PAGE_EMOTICONS: EmojiPageModel = StaticEmojiPageModel(
- EmojiCategory.EMOTICONS,
- arrayOf(
- ":-)", ";-)", "(-:", ":->", ":-D", "\\o/",
- ":-P", "B-)", ":-$", ":-*", "O:-)", "=-O",
- "O_O", "O_o", "o_O", ":O", ":-!", ":-x",
- ":-|", ":-\\", ":-(", ":'(", ":-[", ">:-(",
- "^.^", "^_^", "\\(\u02c6\u02da\u02c6)/",
- "\u30fd(\u00b0\u25c7\u00b0 )\u30ce", "\u00af\\(\u00b0_o)/\u00af",
- "\u00af\\_(\u30c4)_/\u00af", "(\u00ac_\u00ac)",
- "(>_<)", "(\u2565\ufe4f\u2565)", "(\u261e\uff9f\u30ee\uff9f)\u261e",
- "\u261c(\uff9f\u30ee\uff9f\u261c)", "\u261c(\u2312\u25bd\u2312)\u261e",
- "(\u256f\u00b0\u25a1\u00b0)\u256f\ufe35", "\u253b\u2501\u253b",
- "\u252c\u2500\u252c", "\u30ce(\u00b0\u2013\u00b0\u30ce)",
- "(^._.^)\uff89", "\u0e05^\u2022\ufecc\u2022^\u0e05",
- "\u0295\u2022\u1d25\u2022\u0294", "(\u2022_\u2022)",
- " \u25a0-\u25a0\u00ac <(\u2022_\u2022) ", "(\u25a0_\u25a0\u00ac)",
- "\u01aa(\u0693\u05f2)\u200e\u01aa\u200b\u200b"
- ),
- null
-)
diff --git a/app/src/main/java/org/thoughtcrime/securesms/giph/ui/GiphyActivity.java b/app/src/main/java/org/thoughtcrime/securesms/giph/ui/GiphyActivity.java
index dcfdb66112..a9edadfcb1 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/giph/ui/GiphyActivity.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/giph/ui/GiphyActivity.java
@@ -19,6 +19,7 @@ import androidx.viewpager.widget.ViewPager;
import com.google.android.material.tabs.TabLayout;
import org.session.libsession.utilities.MediaTypes;
+import org.session.libsession.utilities.NonTranslatableStringConstants;
import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity;
import org.session.libsignal.utilities.Log;
import org.thoughtcrime.securesms.providers.BlobProvider;
@@ -120,7 +121,7 @@ public class GiphyActivity extends PassphraseRequiredActionBarActivity
protected void onPostExecute(@Nullable Uri uri) {
if (uri == null) {
- Toast.makeText(GiphyActivity.this, R.string.GiphyActivity_error_while_retrieving_full_resolution_gif, Toast.LENGTH_LONG).show();
+ Toast.makeText(GiphyActivity.this, R.string.errorUnknown, Toast.LENGTH_LONG).show();
} else if (viewHolder == finishingImage) {
Intent intent = new Intent();
intent.setData(uri);
@@ -165,8 +166,8 @@ public class GiphyActivity extends PassphraseRequiredActionBarActivity
@Override
public CharSequence getPageTitle(int position) {
- if (position == 0) return context.getString(R.string.GiphyFragmentPagerAdapter_gifs);
- else return context.getString(R.string.GiphyFragmentPagerAdapter_stickers);
+ if (position == 0) return NonTranslatableStringConstants.GIF;
+ else return context.getString(R.string.stickers);
}
}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/groups/CreateGroupFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/groups/CreateGroupFragment.kt
index acb3af8db4..0f562c80b7 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/groups/CreateGroupFragment.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/groups/CreateGroupFragment.kt
@@ -76,17 +76,20 @@ class CreateGroupFragment : Fragment() {
if (isLoading) return@setOnClickListener
val name = binding.nameEditText.text.trim()
if (name.isEmpty()) {
- return@setOnClickListener Toast.makeText(context, R.string.activity_create_closed_group_group_name_missing_error, Toast.LENGTH_LONG).show()
+ return@setOnClickListener Toast.makeText(context, R.string.groupNameEnterPlease, Toast.LENGTH_LONG).show()
}
+
+ // Limit the group name length if it exceeds the limit
if (name.length > resources.getInteger(R.integer.max_group_and_community_name_length_chars)) {
- return@setOnClickListener Toast.makeText(context, R.string.activity_create_closed_group_group_name_too_long_error, Toast.LENGTH_LONG).show()
+ return@setOnClickListener Toast.makeText(context, R.string.groupNameEnterShorter, Toast.LENGTH_LONG).show()
}
+
val selectedMembers = adapter.selectedMembers
if (selectedMembers.isEmpty()) {
- return@setOnClickListener Toast.makeText(context, R.string.activity_create_closed_group_not_enough_group_members_error, Toast.LENGTH_LONG).show()
+ return@setOnClickListener Toast.makeText(context, R.string.groupCreateErrorNoMembers, Toast.LENGTH_LONG).show()
}
if (selectedMembers.count() >= groupSizeLimit) { // Minus one because we're going to include self later
- return@setOnClickListener Toast.makeText(context, R.string.activity_create_closed_group_too_many_group_members_error, Toast.LENGTH_LONG).show()
+ return@setOnClickListener Toast.makeText(context, R.string.groupAddMemberMaximum, Toast.LENGTH_LONG).show()
}
val userPublicKey = TextSecurePreferences.getLocalNumber(requireContext())!!
isLoading = true
diff --git a/app/src/main/java/org/thoughtcrime/securesms/groups/EditClosedGroupActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/groups/EditClosedGroupActivity.kt
index 2d9192ac7a..11dde4b93e 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/groups/EditClosedGroupActivity.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/groups/EditClosedGroupActivity.kt
@@ -3,6 +3,8 @@ package org.thoughtcrime.securesms.groups
import android.content.Context
import android.content.Intent
import android.os.Bundle
+import android.text.SpannableString
+import android.text.style.StyleSpan
import android.view.Menu
import android.view.MenuItem
import android.view.View
@@ -16,7 +18,10 @@ import androidx.loader.app.LoaderManager
import androidx.loader.content.Loader
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
+import com.squareup.phrase.Phrase
import dagger.hilt.android.AndroidEntryPoint
+import java.io.IOException
+import javax.inject.Inject
import network.loki.messenger.R
import nl.komponents.kovenant.Promise
import nl.komponents.kovenant.task
@@ -26,6 +31,7 @@ import org.session.libsession.messaging.sending_receiving.MessageSender
import org.session.libsession.messaging.sending_receiving.groupSizeLimit
import org.session.libsession.utilities.Address
import org.session.libsession.utilities.GroupUtil
+import org.session.libsession.utilities.StringSubstitutionConstants.COUNT_KEY
import org.session.libsession.utilities.TextSecurePreferences
import org.session.libsession.utilities.ThemeUtil
import org.session.libsession.utilities.recipients.Recipient
@@ -40,8 +46,6 @@ import org.thoughtcrime.securesms.groups.ClosedGroupManager.updateLegacyGroup
import com.bumptech.glide.Glide
import org.thoughtcrime.securesms.util.fadeIn
import org.thoughtcrime.securesms.util.fadeOut
-import java.io.IOException
-import javax.inject.Inject
@AndroidEntryPoint
class EditClosedGroupActivity : PassphraseRequiredActionBarActivity() {
@@ -107,17 +111,17 @@ class EditClosedGroupActivity : PassphraseRequiredActionBarActivity() {
groupID = intent.getStringExtra(groupIDKey)!!
val groupInfo = DatabaseComponent.get(this).groupDatabase().getGroup(groupID).get()
originalName = groupInfo.title
- isSelfAdmin = groupInfo.admins.any{ it.serialize() == TextSecurePreferences.getLocalNumber(this) }
+ isSelfAdmin = groupInfo.admins.any { it.serialize() == TextSecurePreferences.getLocalNumber(this) }
name = originalName
mainContentContainer = findViewById(R.id.mainContentContainer)
- cntGroupNameEdit = findViewById(R.id.cntGroupNameEdit)
- cntGroupNameDisplay = findViewById(R.id.cntGroupNameDisplay)
- edtGroupName = findViewById(R.id.edtGroupName)
- emptyStateContainer = findViewById(R.id.emptyStateContainer)
- lblGroupNameDisplay = findViewById(R.id.lblGroupNameDisplay)
- loaderContainer = findViewById(R.id.loaderContainer)
+ cntGroupNameEdit = findViewById(R.id.cntGroupNameEdit)
+ cntGroupNameDisplay = findViewById(R.id.cntGroupNameDisplay)
+ edtGroupName = findViewById(R.id.edtGroupName)
+ emptyStateContainer = findViewById(R.id.emptyStateContainer)
+ lblGroupNameDisplay = findViewById(R.id.lblGroupNameDisplay)
+ loaderContainer = findViewById(R.id.loaderContainer)
findViewById(R.id.addMembersClosedGroupButton).setOnClickListener {
onAddMembersClick()
@@ -129,7 +133,19 @@ class EditClosedGroupActivity : PassphraseRequiredActionBarActivity() {
}
lblGroupNameDisplay.text = originalName
- cntGroupNameDisplay.setOnClickListener { isEditingName = true }
+
+ // Only allow admins to click on the name of closed groups to edit them..
+ if (isSelfAdmin) {
+ cntGroupNameDisplay.setOnClickListener { isEditingName = true }
+ }
+ else // ..and also hide the edit `drawableEnd` for non-admins.
+ {
+ // Note: compoundDrawables returns 4 drawables (drawablesStart/Top/End/Bottom) -
+ // so the `drawableEnd` component is at index 2, which we replace with null.
+ val cd = lblGroupNameDisplay.compoundDrawables
+ lblGroupNameDisplay.setCompoundDrawables(cd[0], cd[1], null, cd[3])
+ }
+
findViewById(R.id.btnCancelGroupNameEdit).setOnClickListener { isEditingName = false }
findViewById(R.id.btnSaveGroupNameEdit).setOnClickListener { saveName() }
edtGroupName.setImeActionLabel(getString(R.string.save), EditorInfo.IME_ACTION_DONE)
@@ -245,10 +261,10 @@ class EditClosedGroupActivity : PassphraseRequiredActionBarActivity() {
private fun saveName() {
val name = edtGroupName.text.toString().trim()
if (name.isEmpty()) {
- return Toast.makeText(this, R.string.activity_edit_closed_group_group_name_missing_error, Toast.LENGTH_SHORT).show()
+ return Toast.makeText(this, R.string.groupNameEnterPlease, Toast.LENGTH_SHORT).show()
}
if (name.length >= 64) {
- return Toast.makeText(this, R.string.activity_edit_closed_group_group_name_too_long_error, Toast.LENGTH_SHORT).show()
+ return Toast.makeText(this, R.string.groupNameEnterShorter, Toast.LENGTH_SHORT).show()
}
this.name = name
lblGroupNameDisplay.text = name
@@ -283,20 +299,22 @@ class EditClosedGroupActivity : PassphraseRequiredActionBarActivity() {
}
if (members.isEmpty()) {
- return Toast.makeText(this, R.string.activity_edit_closed_group_not_enough_group_members_error, Toast.LENGTH_LONG).show()
+ return Toast.makeText(this, R.string.groupCreateErrorNoMembers, Toast.LENGTH_LONG).show()
}
val maxGroupMembers = if (isClosedGroup) groupSizeLimit else legacyGroupSizeLimit
if (members.size >= maxGroupMembers) {
- return Toast.makeText(this, R.string.activity_create_closed_group_too_many_group_members_error, Toast.LENGTH_LONG).show()
+ return Toast.makeText(this, R.string.groupAddMemberMaximum, Toast.LENGTH_LONG).show()
}
val userPublicKey = TextSecurePreferences.getLocalNumber(this)!!
val userAsRecipient = Recipient.from(this, Address.fromSerialized(userPublicKey), false)
+ // There's presently no way in the UI to get into the state whereby you could remove yourself from the group when removing any other members
+ // (you can't unselect yourself - the only way to leave is to "Leave Group" from the menu) - but it's possible that this was not always
+ // the case - so we can leave this in as defensive code in-case something goes screwy.
if (!members.contains(userAsRecipient) && !members.map { it.address.toString() }.containsAll(originalMembers.minus(userPublicKey))) {
- val message = "Can't leave while adding or removing other members."
- return Toast.makeText(this@EditClosedGroupActivity, message, Toast.LENGTH_LONG).show()
+ return Log.w("EditClosedGroup", "Can't leave group while adding or removing other members.")
}
if (isClosedGroup) {
diff --git a/app/src/main/java/org/thoughtcrime/securesms/groups/JoinCommunityFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/groups/JoinCommunityFragment.kt
index 964e1e1770..bcf12b3920 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/groups/JoinCommunityFragment.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/groups/JoinCommunityFragment.kt
@@ -12,6 +12,7 @@ import android.widget.Toast
import androidx.fragment.app.Fragment
import androidx.lifecycle.lifecycleScope
import com.google.android.material.tabs.TabLayoutMediator
+import com.squareup.phrase.Phrase
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
@@ -22,10 +23,12 @@ import org.session.libsession.messaging.MessagingModuleConfiguration
import org.session.libsession.utilities.Address
import org.session.libsession.utilities.GroupUtil
import org.session.libsession.utilities.OpenGroupUrlParser
+import org.session.libsession.utilities.StringSubstitutionConstants.GROUP_NAME_KEY
import org.session.libsession.utilities.recipients.Recipient
import org.session.libsignal.utilities.Log
import org.thoughtcrime.securesms.conversation.start.StartConversationDelegate
import org.thoughtcrime.securesms.conversation.v2.ConversationActivityV2
+import org.thoughtcrime.securesms.ui.getSubbedString
import org.thoughtcrime.securesms.util.ConfigurationMessageUtilities
@AndroidEntryPoint
@@ -35,6 +38,8 @@ class JoinCommunityFragment : Fragment() {
lateinit var delegate: StartConversationDelegate
+ var lastUrl: String? = null
+
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
@@ -47,6 +52,7 @@ class JoinCommunityFragment : Fragment() {
super.onViewCreated(view, savedInstanceState)
binding.backButton.setOnClickListener { delegate.onDialogBackPressed() }
binding.closeButton.setOnClickListener { delegate.onDialogClosePressed() }
+
fun showLoader() {
binding.loader.visibility = View.VISIBLE
binding.loader.animate().setDuration(150).alpha(1.0f).start()
@@ -61,41 +67,76 @@ class JoinCommunityFragment : Fragment() {
}
})
}
- fun joinCommunityIfPossible(url: String) {
- val openGroup = try {
- OpenGroupUrlParser.parseUrl(url)
- } catch (e: OpenGroupUrlParser.Error) {
- when (e) {
- is OpenGroupUrlParser.Error.MalformedURL -> return Toast.makeText(activity, R.string.activity_join_public_chat_error, Toast.LENGTH_SHORT).show()
- is OpenGroupUrlParser.Error.InvalidPublicKey -> return Toast.makeText(activity, R.string.invalid_public_key, Toast.LENGTH_SHORT).show()
- is OpenGroupUrlParser.Error.NoPublicKey -> return Toast.makeText(activity, R.string.invalid_public_key, Toast.LENGTH_SHORT).show()
- is OpenGroupUrlParser.Error.NoRoom -> return Toast.makeText(activity, R.string.activity_join_public_chat_error, Toast.LENGTH_SHORT).show()
- }
- }
- showLoader()
- lifecycleScope.launch(Dispatchers.IO) {
- try {
- val sanitizedServer = openGroup.server.removeSuffix("/")
- val openGroupID = "$sanitizedServer.${openGroup.room}"
- OpenGroupManager.add(sanitizedServer, openGroup.room, openGroup.serverPublicKey, requireContext())
- val storage = MessagingModuleConfiguration.shared.storage
- storage.onOpenGroupAdded(sanitizedServer, openGroup.room)
- val threadID = GroupManager.getOpenGroupThreadID(openGroupID, requireContext())
- val groupID = GroupUtil.getEncodedOpenGroupID(openGroupID.toByteArray())
- ConfigurationMessageUtilities.forceSyncConfigurationNowIfNeeded(requireContext())
- withContext(Dispatchers.Main) {
- val recipient = Recipient.from(requireContext(), Address.fromSerialized(groupID), false)
- openConversationActivity(requireContext(), threadID, recipient)
- delegate.onDialogClosePressed()
+ fun joinCommunityIfPossible(url: String) {
+ // Currently this won't try again on a failed URL but once we rework the whole
+ // fragment into Compose with a ViewModel this won't be an issue anymore as the error
+ // and state will come from Flows.
+ if(lastUrl == url) return
+ lastUrl = url
+
+ lifecycleScope.launch(Dispatchers.Main) {
+ val openGroup = try {
+ OpenGroupUrlParser.parseUrl(url)
+ } catch (e: OpenGroupUrlParser.Error) {
+ when (e) {
+ is OpenGroupUrlParser.Error.MalformedURL, OpenGroupUrlParser.Error.NoRoom -> {
+ return@launch Toast.makeText(
+ activity,
+ context?.resources?.getString(R.string.communityJoinError),
+ Toast.LENGTH_SHORT
+ ).show()
+ }
+
+ is OpenGroupUrlParser.Error.InvalidPublicKey, OpenGroupUrlParser.Error.NoPublicKey -> {
+ return@launch Toast.makeText(
+ activity,
+ R.string.communityEnterUrlErrorInvalidDescription,
+ Toast.LENGTH_SHORT
+ ).show()
+ }
}
- } catch (e: Exception) {
- Log.e("Loki", "Couldn't join open group.", e)
- withContext(Dispatchers.Main) {
- hideLoader()
- Toast.makeText(activity, R.string.activity_join_public_chat_error, Toast.LENGTH_SHORT).show()
+ }
+
+ showLoader()
+
+ withContext(Dispatchers.IO) {
+ try {
+ val sanitizedServer = openGroup.server.removeSuffix("/")
+ val openGroupID = "$sanitizedServer.${openGroup.room}"
+ OpenGroupManager.add(
+ sanitizedServer,
+ openGroup.room,
+ openGroup.serverPublicKey,
+ requireContext()
+ )
+ val storage = MessagingModuleConfiguration.shared.storage
+ storage.onOpenGroupAdded(sanitizedServer, openGroup.room)
+ val threadID =
+ GroupManager.getOpenGroupThreadID(openGroupID, requireContext())
+ val groupID = GroupUtil.getEncodedOpenGroupID(openGroupID.toByteArray())
+
+ ConfigurationMessageUtilities.forceSyncConfigurationNowIfNeeded(
+ requireContext()
+ )
+ withContext(Dispatchers.Main) {
+ val recipient = Recipient.from(
+ requireContext(),
+ Address.fromSerialized(groupID),
+ false
+ )
+ openConversationActivity(requireContext(), threadID, recipient)
+ delegate.onDialogClosePressed()
+ }
+ } catch (e: Exception) {
+ Log.e("Loki", "Couldn't join community.", e)
+ withContext(Dispatchers.Main) {
+ hideLoader()
+ val txt = context?.getSubbedString(R.string.groupErrorJoin,
+ GROUP_NAME_KEY to url)
+ Toast.makeText(activity, txt, Toast.LENGTH_SHORT).show()
+ }
}
- return@launch
}
}
}
@@ -107,8 +148,8 @@ class JoinCommunityFragment : Fragment() {
)
val mediator = TabLayoutMediator(binding.tabLayout, binding.viewPager) { tab, pos ->
tab.text = when (pos) {
- 0 -> getString(R.string.activity_join_public_chat_enter_community_url_tab_title)
- 1 -> getString(R.string.activity_join_public_chat_scan_qr_code_tab_title)
+ 0 -> getString(R.string.communityUrl)
+ 1 -> getString(R.string.qrScan)
else -> throw IllegalStateException()
}
}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/groups/OpenGroupManager.kt b/app/src/main/java/org/thoughtcrime/securesms/groups/OpenGroupManager.kt
index 8bb7a39d4a..4b6f73bd2a 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/groups/OpenGroupManager.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/groups/OpenGroupManager.kt
@@ -1,18 +1,23 @@
package org.thoughtcrime.securesms.groups
import android.content.Context
+import android.widget.Toast
import androidx.annotation.WorkerThread
-import okhttp3.HttpUrl
+import com.squareup.phrase.Phrase
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.asStateFlow
+import java.util.concurrent.Executors
+import network.loki.messenger.R
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
import org.session.libsession.messaging.MessagingModuleConfiguration
-import org.session.libsession.messaging.open_groups.GroupMemberRole
import org.session.libsession.messaging.open_groups.OpenGroup
import org.session.libsession.messaging.open_groups.OpenGroupApi
import org.session.libsession.messaging.sending_receiving.pollers.OpenGroupPoller
+import org.session.libsession.utilities.StringSubstitutionConstants.COMMUNITY_NAME_KEY
import org.session.libsignal.utilities.Log
import org.thoughtcrime.securesms.dependencies.DatabaseComponent
import org.thoughtcrime.securesms.util.ConfigurationMessageUtilities
-import java.util.concurrent.Executors
object OpenGroupManager {
private val executorService = Executors.newScheduledThreadPool(4)
@@ -37,6 +42,9 @@ object OpenGroupManager {
return true
}
+ // flow holding information on write access for our current communities
+ private val _communityWriteAccess: MutableStateFlow