Initial squash merge for strings

This commit is contained in:
fanchao 2024-07-26 11:34:05 +10:00
parent bc968dcdae
commit aa1db13e3a
691 changed files with 28770 additions and 67811 deletions

View File

@ -10,29 +10,26 @@ buildscript {
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion" classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion"
classpath "org.jetbrains.kotlin:kotlin-serialization:$kotlinVersion" classpath "org.jetbrains.kotlin:kotlin-serialization:$kotlinVersion"
classpath "com.google.gms:google-services:$googleServicesVersion" classpath "com.google.gms:google-services:$googleServicesVersion"
classpath "com.google.dagger:hilt-android-gradle-plugin:$daggerVersion"
} }
} }
plugins { plugins {
id 'kotlin-kapt' id 'com.google.devtools.ksp'
id 'com.google.dagger.hilt.android' id 'com.google.dagger.hilt.android'
} }
apply plugin: 'com.android.application' apply plugin: 'com.android.application'
apply plugin: 'kotlin-android' apply plugin: 'kotlin-android'
apply plugin: 'witness' apply plugin: 'witness'
apply plugin: 'kotlin-kapt'
apply plugin: 'kotlin-parcelize' apply plugin: 'kotlin-parcelize'
apply plugin: 'kotlinx-serialization' apply plugin: 'kotlinx-serialization'
apply plugin: 'dagger.hilt.android.plugin'
configurations.all { configurations.all {
exclude module: "commons-logging" exclude module: "commons-logging"
} }
def canonicalVersionCode = 374 def canonicalVersionCode = 376
def canonicalVersionName = "1.18.5" def canonicalVersionName = "1.18.6"
def postFixSize = 10 def postFixSize = 10
def abiPostFix = ['armeabi-v7a' : 1, def abiPostFix = ['armeabi-v7a' : 1,
@ -89,7 +86,7 @@ android {
compose true compose true
} }
composeOptions { composeOptions {
kotlinCompilerExtensionVersion '1.4.7' kotlinCompilerExtensionVersion '1.5.14'
} }
defaultConfig { defaultConfig {
@ -238,10 +235,14 @@ android {
} }
dependencies { dependencies {
implementation project(':content-descriptions')
implementation("com.google.dagger:hilt-android:2.46.1") ksp("com.google.dagger:hilt-compiler:$daggerHiltVersion")
kapt("com.google.dagger:hilt-android-compiler:2.44") ksp("androidx.hilt:hilt-compiler:$jetpackHiltVersion")
ksp "com.github.bumptech.glide:ksp:$glideVersion"
implementation 'androidx.compose.material3:material3-android:1.2.1'
implementation("com.google.dagger:hilt-android:$daggerHiltVersion")
implementation "androidx.appcompat:appcompat:$appcompatVersion" implementation "androidx.appcompat:appcompat:$appcompatVersion"
implementation 'androidx.recyclerview:recyclerview:1.2.1' implementation 'androidx.recyclerview:recyclerview:1.2.1'
implementation "com.google.android.material:material:$materialVersion" implementation "com.google.android.material:material:$materialVersion"
@ -263,12 +264,15 @@ dependencies {
implementation 'androidx.fragment:fragment-ktx:1.5.3' implementation 'androidx.fragment:fragment-ktx:1.5.3'
implementation "androidx.core:core-ktx:$coreVersion" implementation "androidx.core:core-ktx:$coreVersion"
implementation "androidx.work:work-runtime-ktx:2.7.1" implementation "androidx.work:work-runtime-ktx:2.7.1"
playImplementation ("com.google.firebase:firebase-messaging:18.0.0") { playImplementation ("com.google.firebase:firebase-messaging:18.0.0") {
exclude group: 'com.google.firebase', module: 'firebase-core' exclude group: 'com.google.firebase', module: 'firebase-core'
exclude group: 'com.google.firebase', module: 'firebase-analytics' exclude group: 'com.google.firebase', module: 'firebase-analytics'
exclude group: 'com.google.firebase', module: 'firebase-measurement-connector' exclude group: 'com.google.firebase', module: 'firebase-measurement-connector'
} }
if (project.hasProperty('huawei')) huaweiImplementation 'com.huawei.hms:push:6.7.0.300' if (project.hasProperty('huawei')) huaweiImplementation 'com.huawei.hms:push:6.7.0.300'
implementation 'com.google.android.exoplayer:exoplayer-core:2.9.1' implementation 'com.google.android.exoplayer:exoplayer-core:2.9.1'
implementation 'com.google.android.exoplayer:exoplayer-ui:2.9.1' implementation 'com.google.android.exoplayer:exoplayer-ui:2.9.1'
implementation 'org.conscrypt:conscrypt-android:2.5.2' implementation 'org.conscrypt:conscrypt-android:2.5.2'
@ -281,8 +285,6 @@ dependencies {
implementation 'commons-net:commons-net:3.7.2' implementation 'commons-net:commons-net:3.7.2'
implementation 'com.github.chrisbanes:PhotoView:2.1.3' implementation 'com.github.chrisbanes:PhotoView:2.1.3'
implementation "com.github.bumptech.glide:glide:$glideVersion" implementation "com.github.bumptech.glide:glide:$glideVersion"
annotationProcessor "com.github.bumptech.glide:compiler:$glideVersion"
kapt "com.github.bumptech.glide:compiler:$glideVersion"
implementation 'com.makeramen:roundedimageview:2.1.0' implementation 'com.makeramen:roundedimageview:2.1.0'
implementation 'com.pnikosis:materialish-progress:1.5' implementation 'com.pnikosis:materialish-progress:1.5'
implementation 'org.greenrobot:eventbus:3.0.0' implementation 'org.greenrobot:eventbus:3.0.0'
@ -290,8 +292,6 @@ dependencies {
implementation 'com.theartofdev.edmodo:android-image-cropper:2.8.0' implementation 'com.theartofdev.edmodo:android-image-cropper:2.8.0'
implementation 'com.melnykov:floatingactionbutton:1.3.0' implementation 'com.melnykov:floatingactionbutton:1.3.0'
implementation 'com.google.zxing:android-integration:3.1.0' implementation 'com.google.zxing:android-integration:3.1.0'
implementation "com.google.dagger:hilt-android:$daggerVersion"
kapt "com.google.dagger:hilt-compiler:$daggerVersion"
implementation 'mobi.upod:time-duration-picker:1.1.3' implementation 'mobi.upod:time-duration-picker:1.1.3'
implementation 'com.google.zxing:core:3.2.1' implementation 'com.google.zxing:core:3.2.1'
implementation ('com.davemorrissey.labs:subsampling-scale-image-view:3.6.0') { implementation ('com.davemorrissey.labs:subsampling-scale-image-view:3.6.0') {
@ -415,8 +415,3 @@ def autoResConfig() {
.collect { matcher -> matcher.group(1) } .collect { matcher -> matcher.group(1) }
.sort() .sort()
} }
// Allow references to generated code
kapt {
correctErrorTypes = true
}

View File

@ -25,6 +25,7 @@ import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.uiautomator.By import androidx.test.uiautomator.By
import androidx.test.uiautomator.UiDevice import androidx.test.uiautomator.UiDevice
import com.adevinta.android.barista.interaction.PermissionGranter import com.adevinta.android.barista.interaction.PermissionGranter
import com.squareup.phrase.Phrase
import network.loki.messenger.util.InputBarButtonDrawableMatcher.Companion.inputButtonWithDrawable import network.loki.messenger.util.InputBarButtonDrawableMatcher.Companion.inputButtonWithDrawable
import org.hamcrest.Matcher import org.hamcrest.Matcher
import org.hamcrest.Matchers.allOf import org.hamcrest.Matchers.allOf
@ -37,10 +38,11 @@ import org.junit.runner.RunWith
import org.session.libsession.messaging.sending_receiving.link_preview.LinkPreview import org.session.libsession.messaging.sending_receiving.link_preview.LinkPreview
import org.session.libsession.utilities.TextSecurePreferences import org.session.libsession.utilities.TextSecurePreferences
import org.session.libsignal.utilities.guava.Optional import org.session.libsignal.utilities.guava.Optional
import org.session.libsession.utilities.StringSubstitutionConstants.URL_KEY
import org.thoughtcrime.securesms.conversation.v2.ConversationActivityV2 import org.thoughtcrime.securesms.conversation.v2.ConversationActivityV2
import org.thoughtcrime.securesms.conversation.v2.input_bar.InputBar import org.thoughtcrime.securesms.conversation.v2.input_bar.InputBar
import org.thoughtcrime.securesms.home.HomeActivity import org.thoughtcrime.securesms.home.HomeActivity
import org.thoughtcrime.securesms.mms.GlideApp import com.bumptech.glide.Glide
@RunWith(AndroidJUnit4::class) @RunWith(AndroidJUnit4::class)
@LargeTest @LargeTest
@ -71,7 +73,7 @@ class HomeActivityTests {
onView(allOf(isDescendantOfA(withId(R.id.inputBar)),withId(R.id.inputBarEditText))).perform(ViewActions.replaceText(messageToSend)) onView(allOf(isDescendantOfA(withId(R.id.inputBar)),withId(R.id.inputBarEditText))).perform(ViewActions.replaceText(messageToSend))
if (linkPreview != null) { if (linkPreview != null) {
val activity = activityMonitor.waitForActivity() as ConversationActivityV2 val activity = activityMonitor.waitForActivity() as ConversationActivityV2
val glide = GlideApp.with(activity) val glide = Glide.with(activity)
activity.findViewById<InputBar>(R.id.inputBar).updateLinkPreviewDraft(glide, linkPreview) activity.findViewById<InputBar>(R.id.inputBar).updateLinkPreviewDraft(glide, linkPreview)
} }
onView(allOf(isDescendantOfA(withId(R.id.inputBar)),inputButtonWithDrawable(R.drawable.ic_arrow_up))).perform(ViewActions.click()) onView(allOf(isDescendantOfA(withId(R.id.inputBar)),inputButtonWithDrawable(R.drawable.ic_arrow_up))).perform(ViewActions.click())
@ -92,10 +94,10 @@ class HomeActivityTests {
device.pressKeyCode(67) device.pressKeyCode(67)
// Continue with display name // Continue with display name
objectFromDesc(R.string.continue_2).click() objectFromDesc(R.string.theContinue).click()
// Continue with default push notification setting // Continue with default push notification setting
objectFromDesc(R.string.continue_2).click() objectFromDesc(R.string.theContinue).click()
// PN select // PN select
if (hasViewedSeed) { if (hasViewedSeed) {
@ -127,7 +129,7 @@ class HomeActivityTests {
@Test @Test
fun testLaunches_dismiss_seedView() { fun testLaunches_dismiss_seedView() {
setupLoggedInState() setupLoggedInState()
objectFromDesc(R.string.continue_2).click() objectFromDesc(R.string.theContinue).click()
objectFromDesc(R.string.copy).click() objectFromDesc(R.string.copy).click()
pressBack() pressBack()
onView(withId(R.id.seedReminderView)).check(matches(not(isDisplayed()))) onView(withId(R.id.seedReminderView)).check(matches(not(isDisplayed())))
@ -172,7 +174,11 @@ class HomeActivityTests {
// then the URL dialog should be displayed with a known punycode url // then the URL dialog should be displayed with a known punycode url
val amazonPuny = "https://www.xn--mazon-wqa.com/" val amazonPuny = "https://www.xn--mazon-wqa.com/"
val dialogPromptText = InstrumentationRegistry.getInstrumentation().targetContext.getString(R.string.dialog_open_url_explanation, amazonPuny) // Substitute the URL into our string
val c = InstrumentationRegistry.getInstrumentation().targetContext
val dialogPromptText = Phrase.from(c, R.string.urlOpenDescription)
.put(URL_KEY, amazonPuny)
.format().toString()
onView(isRoot()).perform(waitFor(1000)) // no other way for this to work apparently onView(isRoot()).perform(waitFor(1000)) // no other way for this to work apparently
onView(withText(dialogPromptText)).check(matches(isDisplayed())) onView(withText(dialogPromptText)).check(matches(isDisplayed()))

View File

@ -60,7 +60,6 @@
<uses-permission android:name="android.permission.RAISED_THREAD_PRIORITY" /> <uses-permission android:name="android.permission.RAISED_THREAD_PRIORITY" />
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" /> <uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" />
<uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" tools:node="remove"/> <uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" tools:node="remove"/>
<uses-permission android:name="android.permission.USE_FULL_SCREEN_INTENT"/>
<queries> <queries>
<intent> <intent>
@ -130,12 +129,12 @@
<activity <activity
android:name="org.thoughtcrime.securesms.messagerequests.MessageRequestsActivity" android:name="org.thoughtcrime.securesms.messagerequests.MessageRequestsActivity"
android:exported="false" android:exported="false"
android:label="@string/activity_message_requests_title" android:label="@string/sessionMessageRequests"
android:screenOrientation="portrait" /> android:screenOrientation="portrait" />
<activity <activity
android:name="org.thoughtcrime.securesms.preferences.SettingsActivity" android:name="org.thoughtcrime.securesms.preferences.SettingsActivity"
android:screenOrientation="portrait" android:screenOrientation="portrait"
android:label="@string/activity_settings_title" /> android:label="@string/sessionSettings" />
<activity <activity
android:name="org.thoughtcrime.securesms.home.PathActivity" android:name="org.thoughtcrime.securesms.home.PathActivity"
android:screenOrientation="portrait" /> android:screenOrientation="portrait" />
@ -147,11 +146,11 @@
android:name="org.thoughtcrime.securesms.preferences.BlockedContactsActivity" android:name="org.thoughtcrime.securesms.preferences.BlockedContactsActivity"
android:screenOrientation="portrait" android:screenOrientation="portrait"
android:theme="@style/Theme.Session.DayNight.FlatActionBar" android:theme="@style/Theme.Session.DayNight.FlatActionBar"
android:label="@string/blocked_contacts_title" android:label="@string/conversationsBlockedContacts"
/> />
<activity <activity
android:name="org.thoughtcrime.securesms.groups.EditClosedGroupActivity" android:name="org.thoughtcrime.securesms.groups.EditClosedGroupActivity"
android:label="@string/activity_edit_closed_group_title" android:label="@string/groupEdit"
android:screenOrientation="portrait" /> android:screenOrientation="portrait" />
<activity <activity
android:name="org.thoughtcrime.securesms.recoverypassword.RecoveryPasswordActivity" android:name="org.thoughtcrime.securesms.recoverypassword.RecoveryPasswordActivity"
@ -161,7 +160,7 @@
android:screenOrientation="portrait" /> android:screenOrientation="portrait" />
<activity <activity
android:name="org.thoughtcrime.securesms.preferences.PrivacySettingsActivity" android:name="org.thoughtcrime.securesms.preferences.PrivacySettingsActivity"
android:label="@string/activity_privacy_settings_title" android:label="@string/sessionPrivacy"
android:screenOrientation="portrait" /> android:screenOrientation="portrait" />
<activity <activity
android:name="org.thoughtcrime.securesms.preferences.NotificationSettingsActivity" android:name="org.thoughtcrime.securesms.preferences.NotificationSettingsActivity"
@ -171,7 +170,7 @@
android:screenOrientation="portrait" /> android:screenOrientation="portrait" />
<activity <activity
android:name="org.thoughtcrime.securesms.preferences.HelpSettingsActivity" android:name="org.thoughtcrime.securesms.preferences.HelpSettingsActivity"
android:label="@string/activity_help_settings_title" android:label="@string/sessionHelp"
android:screenOrientation="portrait" /> android:screenOrientation="portrait" />
<activity android:name="org.thoughtcrime.securesms.preferences.appearance.AppearanceSettingsActivity" <activity android:name="org.thoughtcrime.securesms.preferences.appearance.AppearanceSettingsActivity"
android:screenOrientation="portrait"/> android:screenOrientation="portrait"/>
@ -264,7 +263,6 @@
<activity <activity
android:name="org.thoughtcrime.securesms.MediaPreviewActivity" android:name="org.thoughtcrime.securesms.MediaPreviewActivity"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize" android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
android:label="@string/AndroidManifest__media_preview"
android:screenOrientation="portrait" android:screenOrientation="portrait"
android:theme="@style/Theme.Session.DayNight.NoActionBar" android:theme="@style/Theme.Session.DayNight.NoActionBar"
android:launchMode="singleTask" android:launchMode="singleTask"

View File

@ -214,6 +214,17 @@ public class ApplicationContext extends Application implements DefaultLifecycleO
DatabaseModule.init(this); DatabaseModule.init(this);
MessagingModuleConfiguration.configure(this); MessagingModuleConfiguration.configure(this);
super.onCreate(); super.onCreate();
// we need to clear the snode and onionrequest databases once on first launch
// in order to apply a patch that adds a version number to the Snode objects.
if(!TextSecurePreferences.hasAppliedPatchSnodeVersion(this)) {
ThreadUtils.queue(() -> {
lokiAPIDatabase.clearSnodePool();
lokiAPIDatabase.clearOnionRequestPaths();
TextSecurePreferences.setHasAppliedPatchSnodeVersion(this, true);
});
}
messagingModuleConfiguration = new MessagingModuleConfiguration( messagingModuleConfiguration = new MessagingModuleConfiguration(
this, this,
storage, storage,

View File

@ -4,12 +4,8 @@ import android.app.ActivityManager;
import android.content.Context; import android.content.Context;
import android.graphics.Bitmap; import android.graphics.Bitmap;
import android.graphics.BitmapFactory; import android.graphics.BitmapFactory;
import android.os.Build;
import android.os.Build.VERSION;
import android.os.Build.VERSION_CODES;
import androidx.annotation.NonNull;
import androidx.fragment.app.FragmentActivity; import androidx.fragment.app.FragmentActivity;
import android.view.KeyEvent;
import org.session.libsession.utilities.TextSecurePreferences; import org.session.libsession.utilities.TextSecurePreferences;
import org.session.libsession.utilities.dynamiclanguage.DynamicLanguageActivityHelper; import org.session.libsession.utilities.dynamiclanguage.DynamicLanguageActivityHelper;

View File

@ -8,19 +8,8 @@ class DeleteMediaDialog {
@JvmStatic @JvmStatic
fun show(context: Context, recordCount: Int, doDelete: Runnable) = context.showSessionDialog { fun show(context: Context, recordCount: Int, doDelete: Runnable) = context.showSessionDialog {
iconAttribute(R.attr.dialog_alert_icon) iconAttribute(R.attr.dialog_alert_icon)
title( title(context.resources.getString(R.string.deleteMessages))
context.resources.getQuantityString( text(context.resources.getString(R.string.deleteMessageDescriptionEveryone))
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() } button(R.string.delete) { doDelete.run() }
cancelButton() cancelButton()
} }

View File

@ -9,8 +9,8 @@ class DeleteMediaPreviewDialog {
fun show(context: Context, doDelete: Runnable) { fun show(context: Context, doDelete: Runnable) {
context.showSessionDialog { context.showSessionDialog {
iconAttribute(R.attr.dialog_alert_icon) iconAttribute(R.attr.dialog_alert_icon)
title(R.string.MediaPreviewActivity_media_delete_confirmation_title) title(R.string.deleteMessage)
text(R.string.MediaPreviewActivity_media_delete_confirmation_message) text(R.string.deleteMessageDescriptionEveryone)
button(R.string.delete) { doDelete.run() } button(R.string.delete) { doDelete.run() }
cancelButton() cancelButton()
} }

View File

@ -60,7 +60,10 @@ public class MediaDocumentsAdapter extends CursorRecyclerViewAdapter<ViewHolder>
if (slide != null && slide.hasDocument()) { if (slide != null && slide.hasDocument()) {
viewHolder.documentView.setDocument((DocumentSlide)slide, false); viewHolder.documentView.setDocument((DocumentSlide)slide, false);
viewHolder.date.setText(DateUtils.getRelativeDate(getContext(), locale, mediaRecord.getDate()));
String relativeDate = DateUtils.INSTANCE.getRelativeDate(getContext(), locale, mediaRecord.getDate());
viewHolder.date.setText(relativeDate);
viewHolder.documentView.setVisibility(View.VISIBLE); viewHolder.documentView.setVisibility(View.VISIBLE);
viewHolder.date.setVisibility(View.VISIBLE); viewHolder.date.setVisibility(View.VISIBLE);
viewHolder.documentView.setOnClickListener(view -> { viewHolder.documentView.setOnClickListener(view -> {
@ -71,7 +74,7 @@ public class MediaDocumentsAdapter extends CursorRecyclerViewAdapter<ViewHolder>
getContext().startActivity(intent); getContext().startActivity(intent);
} catch (ActivityNotFoundException anfe) { } catch (ActivityNotFoundException anfe) {
Log.w(TAG, "No activity existed to view the media."); Log.w(TAG, "No activity existed to view the media.");
Toast.makeText(getContext(), R.string.ConversationItem_unable_to_open_media, Toast.LENGTH_LONG).show(); Toast.makeText(getContext(), R.string.attachmentsErrorOpen, Toast.LENGTH_LONG).show();
} }
}); });
} else { } else {
@ -104,7 +107,7 @@ public class MediaDocumentsAdapter extends CursorRecyclerViewAdapter<ViewHolder>
public void onBindHeaderViewHolder(HeaderViewHolder viewHolder, int position) { public void onBindHeaderViewHolder(HeaderViewHolder viewHolder, int position) {
Cursor cursor = getCursorAtPositionOrThrow(position); Cursor cursor = getCursorAtPositionOrThrow(position);
MediaDatabase.MediaRecord mediaRecord = MediaDatabase.MediaRecord.from(getContext(), cursor); MediaDatabase.MediaRecord mediaRecord = MediaDatabase.MediaRecord.from(getContext(), cursor);
viewHolder.textView.setText(DateUtils.getRelativeDate(getContext(), locale, mediaRecord.getDate())); viewHolder.textView.setText(DateUtils.INSTANCE.getRelativeDate(getContext(), locale, mediaRecord.getDate()));
} }
public static class ViewHolder extends RecyclerView.ViewHolder { public static class ViewHolder extends RecyclerView.ViewHolder {

View File

@ -29,7 +29,7 @@ import com.codewaves.stickyheadergrid.StickyHeaderGridAdapter;
import org.thoughtcrime.securesms.conversation.v2.utilities.ThumbnailView; import org.thoughtcrime.securesms.conversation.v2.utilities.ThumbnailView;
import org.thoughtcrime.securesms.database.MediaDatabase.MediaRecord; import org.thoughtcrime.securesms.database.MediaDatabase.MediaRecord;
import org.thoughtcrime.securesms.database.loaders.BucketedThreadMediaLoader.BucketedThreadMedia; import org.thoughtcrime.securesms.database.loaders.BucketedThreadMediaLoader.BucketedThreadMedia;
import org.thoughtcrime.securesms.mms.GlideRequests; import com.bumptech.glide.RequestManager;
import org.thoughtcrime.securesms.mms.Slide; import org.thoughtcrime.securesms.mms.Slide;
import org.thoughtcrime.securesms.util.MediaUtil; import org.thoughtcrime.securesms.util.MediaUtil;
@ -46,7 +46,7 @@ class MediaGalleryAdapter extends StickyHeaderGridAdapter {
private static final String TAG = MediaGalleryAdapter.class.getSimpleName(); private static final String TAG = MediaGalleryAdapter.class.getSimpleName();
private final Context context; private final Context context;
private final GlideRequests glideRequests; private final RequestManager glideRequests;
private final Locale locale; private final Locale locale;
private final ItemClickListener itemClickListener; private final ItemClickListener itemClickListener;
private final Set<MediaRecord> selected; private final Set<MediaRecord> selected;
@ -74,7 +74,7 @@ class MediaGalleryAdapter extends StickyHeaderGridAdapter {
} }
MediaGalleryAdapter(@NonNull Context context, MediaGalleryAdapter(@NonNull Context context,
@NonNull GlideRequests glideRequests, @NonNull RequestManager glideRequests,
BucketedThreadMedia media, BucketedThreadMedia media,
Locale locale, Locale locale,
ItemClickListener clickListener) ItemClickListener clickListener)

View File

@ -16,11 +16,12 @@
*/ */
package org.thoughtcrime.securesms; package org.thoughtcrime.securesms;
import static org.session.libsession.utilities.StringSubstitutionConstants.APP_NAME_KEY;
import android.annotation.SuppressLint; import android.annotation.SuppressLint;
import android.content.Context; import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.content.res.Configuration; import android.content.res.Configuration;
import android.content.res.Resources;
import android.database.Cursor; import android.database.Cursor;
import android.os.Build; import android.os.Build;
import android.os.Bundle; import android.os.Bundle;
@ -32,10 +33,8 @@ import android.view.ViewGroup;
import android.view.Window; import android.view.Window;
import android.widget.TextView; import android.widget.TextView;
import android.widget.Toast; import android.widget.Toast;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.appcompat.app.ActionBar; import androidx.appcompat.app.ActionBar;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.app.AppCompatActivity; import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.view.ActionMode; import androidx.appcompat.view.ActionMode;
import androidx.appcompat.widget.Toolbar; import androidx.appcompat.widget.Toolbar;
@ -48,36 +47,34 @@ import androidx.recyclerview.widget.DividerItemDecoration;
import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView; import androidx.recyclerview.widget.RecyclerView;
import androidx.viewpager.widget.ViewPager; import androidx.viewpager.widget.ViewPager;
import com.bumptech.glide.Glide;
import com.codewaves.stickyheadergrid.StickyHeaderGridLayoutManager; import com.codewaves.stickyheadergrid.StickyHeaderGridLayoutManager;
import com.google.android.material.tabs.TabLayout; import com.google.android.material.tabs.TabLayout;
import com.squareup.phrase.Phrase;
import org.session.libsession.messaging.messages.control.DataExtractionNotification;
import org.session.libsession.messaging.sending_receiving.MessageSender;
import org.session.libsession.snode.SnodeAPI;
import org.session.libsession.utilities.Address;
import org.thoughtcrime.securesms.database.CursorRecyclerViewAdapter;
import org.thoughtcrime.securesms.database.MediaDatabase;
import org.thoughtcrime.securesms.database.loaders.BucketedThreadMediaLoader;
import org.thoughtcrime.securesms.database.loaders.BucketedThreadMediaLoader.BucketedThreadMedia;
import org.thoughtcrime.securesms.database.loaders.ThreadMediaLoader;
import org.thoughtcrime.securesms.mms.GlideApp;
import org.thoughtcrime.securesms.permissions.Permissions;
import org.session.libsession.utilities.recipients.Recipient;
import org.thoughtcrime.securesms.util.AttachmentUtil;
import org.thoughtcrime.securesms.util.SaveAttachmentTask;
import org.thoughtcrime.securesms.util.StickyHeaderDecoration;
import org.session.libsession.utilities.Util;
import org.session.libsession.utilities.ViewUtil;
import org.session.libsession.utilities.task.ProgressDialogAsyncTask;
import java.util.Collection; import java.util.Collection;
import java.util.LinkedList; import java.util.LinkedList;
import java.util.List; import java.util.List;
import java.util.Locale; import java.util.Locale;
import kotlin.Unit; import kotlin.Unit;
import network.loki.messenger.R; 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.snode.SnodeAPI;
import org.session.libsession.utilities.Address;
import org.session.libsession.utilities.Util;
import org.session.libsession.utilities.ViewUtil;
import org.session.libsession.utilities.recipients.Recipient;
import org.session.libsession.utilities.task.ProgressDialogAsyncTask;
import org.session.libsignal.utilities.Log;
import org.thoughtcrime.securesms.database.CursorRecyclerViewAdapter;
import org.thoughtcrime.securesms.database.MediaDatabase;
import org.thoughtcrime.securesms.database.loaders.BucketedThreadMediaLoader.BucketedThreadMedia;
import org.thoughtcrime.securesms.database.loaders.BucketedThreadMediaLoader;
import org.thoughtcrime.securesms.database.loaders.ThreadMediaLoader;
import org.thoughtcrime.securesms.permissions.Permissions;
import org.thoughtcrime.securesms.util.AttachmentUtil;
import org.thoughtcrime.securesms.util.SaveAttachmentTask;
import org.thoughtcrime.securesms.util.StickyHeaderDecoration;
/** /**
* Activity for displaying media attachments in-app * Activity for displaying media attachments in-app
@ -117,17 +114,26 @@ public class MediaOverviewActivity extends PassphraseRequiredActionBarActivity {
} }
private void initializeResources() { private void initializeResources() {
Address address = getIntent().getParcelableExtra(ADDRESS_EXTRA);
this.viewPager = ViewUtil.findById(this, R.id.pager); this.viewPager = ViewUtil.findById(this, R.id.pager);
this.toolbar = ViewUtil.findById(this, R.id.toolbar); this.toolbar = ViewUtil.findById(this, R.id.search_toolbar);
this.tabLayout = ViewUtil.findById(this, R.id.tab_layout); this.tabLayout = ViewUtil.findById(this, R.id.tab_layout);
this.recipient = Recipient.from(this, address, true);
Address address = getIntent().getParcelableExtra(ADDRESS_EXTRA);
if (address == null) {
Log.w(TAG, "Got null address in initializeResources.");
} else {
this.recipient = Recipient.from(this, address, true);
}
} }
private void initializeToolbar() { private void initializeToolbar() {
setSupportActionBar(this.toolbar); setSupportActionBar(this.toolbar);
ActionBar actionBar = getSupportActionBar(); ActionBar actionBar = getSupportActionBar();
if (actionBar == null) {
Log.w(TAG, "Could not get support actionbar");
return;
}
// Implied else that the actionbar is fine to work with...
actionBar.setTitle(recipient.toShortString()); actionBar.setTitle(recipient.toShortString());
actionBar.setDisplayHomeAsUpEnabled(true); actionBar.setDisplayHomeAsUpEnabled(true);
actionBar.setHomeButtonEnabled(true); actionBar.setHomeButtonEnabled(true);
@ -176,8 +182,8 @@ public class MediaOverviewActivity extends PassphraseRequiredActionBarActivity {
@Override @Override
public CharSequence getPageTitle(int position) { public CharSequence getPageTitle(int position) {
if (position == 0) return getString(R.string.MediaOverviewActivity_Media); if (position == 0) return getString(R.string.media);
else if (position == 1) return getString(R.string.MediaOverviewActivity_Documents); else if (position == 1) return getString(R.string.files);
else throw new AssertionError(); else throw new AssertionError();
} }
} }
@ -227,7 +233,7 @@ public class MediaOverviewActivity extends PassphraseRequiredActionBarActivity {
this.gridManager = new StickyHeaderGridLayoutManager(getResources().getInteger(R.integer.media_overview_cols)); this.gridManager = new StickyHeaderGridLayoutManager(getResources().getInteger(R.integer.media_overview_cols));
this.recyclerView.setAdapter(new MediaGalleryAdapter(getContext(), this.recyclerView.setAdapter(new MediaGalleryAdapter(getContext(),
GlideApp.with(this), Glide.with(this),
new BucketedThreadMedia(getContext()), new BucketedThreadMedia(getContext()),
locale, locale,
this)); this));
@ -325,13 +331,19 @@ public class MediaOverviewActivity extends PassphraseRequiredActionBarActivity {
Permissions.with(this) Permissions.with(this)
.request(android.Manifest.permission.WRITE_EXTERNAL_STORAGE) .request(android.Manifest.permission.WRITE_EXTERNAL_STORAGE)
.maxSdkVersion(Build.VERSION_CODES.P) .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)) .withPermanentDenialDialog(Phrase.from(context, R.string.permissionsStorageSaveDenied)
.onAnyDenied(() -> Toast.makeText(getContext(), R.string.MediaPreviewActivity_unable_to_write_to_external_storage_without_permission, Toast.LENGTH_LONG).show()) .put(APP_NAME_KEY, getString(R.string.app_name))
.format().toString())
.onAnyDenied(() -> Toast.makeText(getContext(),
Phrase.from(context, R.string.permissionsStorageSaveDenied)
.put(APP_NAME_KEY, getString(R.string.app_name))
.format().toString(),
Toast.LENGTH_LONG).show())
.onAllGranted(() -> { .onAllGranted(() -> {
new ProgressDialogAsyncTask<Void, Void, List<SaveAttachmentTask.Attachment>>( new ProgressDialogAsyncTask<Void, Void, List<SaveAttachmentTask.Attachment>>(
context, context,
R.string.MediaOverviewActivity_collecting_attachments, R.string.attachmentsCollecting,
R.string.please_wait) { R.string.waitOneMoment) {
@Override @Override
protected List<SaveAttachmentTask.Attachment> doInBackground(Void... params) { protected List<SaveAttachmentTask.Attachment> doInBackground(Void... params) {
List<SaveAttachmentTask.Attachment> attachments = new LinkedList<>(); List<SaveAttachmentTask.Attachment> attachments = new LinkedList<>();
@ -382,8 +394,8 @@ public class MediaOverviewActivity extends PassphraseRequiredActionBarActivity {
recordCount, recordCount,
() -> new ProgressDialogAsyncTask<MediaDatabase.MediaRecord, Void, Void>( () -> new ProgressDialogAsyncTask<MediaDatabase.MediaRecord, Void, Void>(
requireContext(), requireContext(),
R.string.MediaOverviewActivity_Media_delete_progress_title, R.string.deleting,
R.string.MediaOverviewActivity_Media_delete_progress_message) { R.string.deleting) {
@Override @Override
protected Void doInBackground(MediaDatabase.MediaRecord... records) { protected Void doInBackground(MediaDatabase.MediaRecord... records) {
if (records == null || records.length == 0) { if (records == null || records.length == 0) {

View File

@ -16,17 +16,17 @@
*/ */
package org.thoughtcrime.securesms; package org.thoughtcrime.securesms;
import static org.session.libsession.utilities.StringSubstitutionConstants.APP_NAME_KEY;
import android.annotation.SuppressLint; import android.annotation.SuppressLint;
import android.annotation.TargetApi;
import android.content.Context; import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.database.Cursor; import android.database.Cursor;
import android.database.CursorIndexOutOfBoundsException; import android.database.CursorIndexOutOfBoundsException;
import android.net.Uri; import android.net.Uri;
import android.os.AsyncTask; import android.os.AsyncTask;
import android.os.Build;
import android.os.Build.VERSION; import android.os.Build.VERSION;
import android.os.Build.VERSION_CODES; import android.os.Build;
import android.os.Bundle; import android.os.Bundle;
import android.os.Handler; import android.os.Handler;
import android.os.Looper; import android.os.Looper;
@ -44,7 +44,6 @@ import android.view.WindowInsetsController;
import android.widget.FrameLayout; import android.widget.FrameLayout;
import android.widget.TextView; import android.widget.TextView;
import android.widget.Toast; import android.widget.Toast;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.appcompat.app.ActionBar; import androidx.appcompat.app.ActionBar;
@ -56,7 +55,12 @@ import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView; import androidx.recyclerview.widget.RecyclerView;
import androidx.viewpager.widget.PagerAdapter; import androidx.viewpager.widget.PagerAdapter;
import androidx.viewpager.widget.ViewPager; import androidx.viewpager.widget.ViewPager;
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.messages.control.DataExtractionNotification;
import org.session.libsession.messaging.sending_receiving.MessageSender; import org.session.libsession.messaging.sending_receiving.MessageSender;
import org.session.libsession.messaging.sending_receiving.attachments.DatabaseAttachment; import org.session.libsession.messaging.sending_receiving.attachments.DatabaseAttachment;
@ -72,21 +76,14 @@ import org.thoughtcrime.securesms.database.loaders.PagingMediaLoader;
import org.thoughtcrime.securesms.database.model.MmsMessageRecord; import org.thoughtcrime.securesms.database.model.MmsMessageRecord;
import org.thoughtcrime.securesms.mediapreview.MediaPreviewViewModel; import org.thoughtcrime.securesms.mediapreview.MediaPreviewViewModel;
import org.thoughtcrime.securesms.mediapreview.MediaRailAdapter; import org.thoughtcrime.securesms.mediapreview.MediaRailAdapter;
import org.thoughtcrime.securesms.mms.GlideApp; import com.bumptech.glide.Glide;
import org.thoughtcrime.securesms.mms.GlideRequests; import com.bumptech.glide.RequestManager;
import org.thoughtcrime.securesms.mms.Slide; import org.thoughtcrime.securesms.mms.Slide;
import org.thoughtcrime.securesms.permissions.Permissions; import org.thoughtcrime.securesms.permissions.Permissions;
import org.thoughtcrime.securesms.util.AttachmentUtil; import org.thoughtcrime.securesms.util.AttachmentUtil;
import org.thoughtcrime.securesms.util.DateUtils; import org.thoughtcrime.securesms.util.DateUtils;
import org.thoughtcrime.securesms.util.SaveAttachmentTask;
import org.thoughtcrime.securesms.util.SaveAttachmentTask.Attachment; import org.thoughtcrime.securesms.util.SaveAttachmentTask.Attachment;
import org.thoughtcrime.securesms.util.SaveAttachmentTask;
import java.io.IOException;
import java.util.Locale;
import java.util.WeakHashMap;
import kotlin.Unit;
import network.loki.messenger.R;
/** /**
* Activity for displaying media attachments in-app * Activity for displaying media attachments in-app
@ -242,12 +239,12 @@ public class MediaPreviewActivity extends PassphraseRequiredActionBarActivity im
CharSequence relativeTimeSpan; CharSequence relativeTimeSpan;
if (mediaItem.date > 0) { if (mediaItem.date > 0) {
relativeTimeSpan = DateUtils.getDisplayFormattedTimeSpanString(this, Locale.getDefault(), mediaItem.date); relativeTimeSpan = DateUtils.INSTANCE.getDisplayFormattedTimeSpanString(this, Locale.getDefault(), mediaItem.date);
} else { } 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 if (mediaItem.recipient != null) getSupportActionBar().setTitle(mediaItem.recipient.toShortString());
else getSupportActionBar().setTitle(""); else getSupportActionBar().setTitle("");
@ -258,7 +255,6 @@ public class MediaPreviewActivity extends PassphraseRequiredActionBarActivity im
@Override @Override
public void onResume() { public void onResume() {
super.onResume(); super.onResume();
initializeMedia(); initializeMedia();
} }
@ -281,7 +277,7 @@ public class MediaPreviewActivity extends PassphraseRequiredActionBarActivity im
mediaPager.setOffscreenPageLimit(1); mediaPager.setOffscreenPageLimit(1);
albumRail = findViewById(R.id.media_preview_album_rail); albumRail = findViewById(R.id.media_preview_album_rail);
albumRailAdapter = new MediaRailAdapter(GlideApp.with(this), this, false); albumRailAdapter = new MediaRailAdapter(Glide.with(this), this, false);
albumRail.setLayoutManager(new LinearLayoutManager(this, LinearLayoutManager.HORIZONTAL, false)); albumRail.setLayoutManager(new LinearLayoutManager(this, LinearLayoutManager.HORIZONTAL, false));
albumRail.setAdapter(albumRailAdapter); albumRail.setAdapter(albumRailAdapter);
@ -291,7 +287,7 @@ public class MediaPreviewActivity extends PassphraseRequiredActionBarActivity im
captionContainer = findViewById(R.id.media_preview_caption_container); captionContainer = findViewById(R.id.media_preview_caption_container);
playbackControlsContainer = findViewById(R.id.media_preview_playback_controls_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 actionBar = getSupportActionBar();
actionBar.setDisplayHomeAsUpEnabled(true); actionBar.setDisplayHomeAsUpEnabled(true);
actionBar.setHomeButtonEnabled(true); actionBar.setHomeButtonEnabled(true);
@ -361,7 +357,7 @@ public class MediaPreviewActivity extends PassphraseRequiredActionBarActivity im
private void initializeMedia() { private void initializeMedia() {
if (!isContentTypeSupported(initialMediaType)) { if (!isContentTypeSupported(initialMediaType)) {
Log.w(TAG, "Unsupported media type sent to MediaPreviewActivity, finishing."); 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(); finish();
} }
@ -370,7 +366,7 @@ public class MediaPreviewActivity extends PassphraseRequiredActionBarActivity im
if (conversationRecipient != null) { if (conversationRecipient != null) {
getSupportLoaderManager().restartLoader(0, null, this); getSupportLoaderManager().restartLoader(0, null, this);
} else { } else {
adapter = new SingleItemPagerAdapter(this, GlideApp.with(this), getWindow(), initialMediaUri, initialMediaType, initialMediaSize); adapter = new SingleItemPagerAdapter(this, Glide.with(this), getWindow(), initialMediaUri, initialMediaType, initialMediaSize);
mediaPager.setAdapter(adapter); mediaPager.setAdapter(adapter);
if (initialCaption != null) { if (initialCaption != null) {
@ -410,6 +406,7 @@ public class MediaPreviewActivity extends PassphraseRequiredActionBarActivity im
@SuppressWarnings("CodeBlock2Expr") @SuppressWarnings("CodeBlock2Expr")
@SuppressLint("InlinedApi") @SuppressLint("InlinedApi")
private void saveToDisk() { private void saveToDisk() {
Log.w("ACL", "Asked to save to disk!");
MediaItem mediaItem = getCurrentMediaItem(); MediaItem mediaItem = getCurrentMediaItem();
if (mediaItem == null) return; if (mediaItem == null) return;
@ -417,8 +414,15 @@ public class MediaPreviewActivity extends PassphraseRequiredActionBarActivity im
Permissions.with(this) Permissions.with(this)
.request(android.Manifest.permission.WRITE_EXTERNAL_STORAGE) .request(android.Manifest.permission.WRITE_EXTERNAL_STORAGE)
.maxSdkVersion(Build.VERSION_CODES.P) .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)) .withPermanentDenialDialog(Phrase.from(getApplicationContext(), R.string.permissionsStorageSaveDenied)
.onAnyDenied(() -> Toast.makeText(this, R.string.MediaPreviewActivity_unable_to_write_to_external_storage_without_permission, Toast.LENGTH_LONG).show()) .put(APP_NAME_KEY, getString(R.string.app_name))
.format().toString())
.onAnyDenied(() -> {
String txt = Phrase.from(getApplicationContext(), R.string.permissionsStorageSaveDenied)
.put(APP_NAME_KEY, getString(R.string.app_name))
.format().toString();
Toast.makeText(this, txt, Toast.LENGTH_LONG).show();
})
.onAllGranted(() -> { .onAllGranted(() -> {
SaveAttachmentTask saveTask = new SaveAttachmentTask(MediaPreviewActivity.this); SaveAttachmentTask saveTask = new SaveAttachmentTask(MediaPreviewActivity.this);
long saveDate = (mediaItem.date > 0) ? mediaItem.date : SnodeAPI.getNowWithOffset(); long saveDate = (mediaItem.date > 0) ? mediaItem.date : SnodeAPI.getNowWithOffset();
@ -518,7 +522,7 @@ public class MediaPreviewActivity extends PassphraseRequiredActionBarActivity im
mediaPager.removeOnPageChangeListener(viewPagerListener); mediaPager.removeOnPageChangeListener(viewPagerListener);
adapter = new CursorPagerAdapter(this, GlideApp.with(this), getWindow(), data.first, data.second, leftIsRecent); adapter = new CursorPagerAdapter(this, Glide.with(this), getWindow(), data.first, data.second, leftIsRecent);
mediaPager.setAdapter(adapter); mediaPager.setAdapter(adapter);
viewModel.setCursor(this, data.first, leftIsRecent); viewModel.setCursor(this, data.first, leftIsRecent);
@ -588,7 +592,7 @@ public class MediaPreviewActivity extends PassphraseRequiredActionBarActivity im
private static class SingleItemPagerAdapter extends MediaItemAdapter { private static class SingleItemPagerAdapter extends MediaItemAdapter {
private final GlideRequests glideRequests; private final RequestManager glideRequests;
private final Window window; private final Window window;
private final Uri uri; private final Uri uri;
private final String mediaType; private final String mediaType;
@ -596,7 +600,7 @@ public class MediaPreviewActivity extends PassphraseRequiredActionBarActivity im
private final LayoutInflater inflater; private final LayoutInflater inflater;
SingleItemPagerAdapter(@NonNull Context context, @NonNull GlideRequests glideRequests, SingleItemPagerAdapter(@NonNull Context context, @NonNull RequestManager glideRequests,
@NonNull Window window, @NonNull Uri uri, @NonNull String mediaType, @NonNull Window window, @NonNull Uri uri, @NonNull String mediaType,
long size) long size)
{ {
@ -663,14 +667,14 @@ public class MediaPreviewActivity extends PassphraseRequiredActionBarActivity im
private final WeakHashMap<Integer, MediaView> mediaViews = new WeakHashMap<>(); private final WeakHashMap<Integer, MediaView> mediaViews = new WeakHashMap<>();
private final Context context; private final Context context;
private final GlideRequests glideRequests; private final RequestManager glideRequests;
private final Window window; private final Window window;
private final Cursor cursor; private final Cursor cursor;
private final boolean leftIsRecent; private final boolean leftIsRecent;
private int autoPlayPosition; private int autoPlayPosition;
CursorPagerAdapter(@NonNull Context context, @NonNull GlideRequests glideRequests, CursorPagerAdapter(@NonNull Context context, @NonNull RequestManager glideRequests,
@NonNull Window window, @NonNull Cursor cursor, int autoPlayPosition, @NonNull Window window, @NonNull Cursor cursor, int autoPlayPosition,
boolean leftIsRecent) boolean leftIsRecent)
{ {

View File

@ -10,18 +10,18 @@ fun showMuteDialog(
context: Context, context: Context,
onMuteDuration: (Long) -> Unit onMuteDuration: (Long) -> Unit
): AlertDialog = context.showSessionDialog { ): AlertDialog = context.showSessionDialog {
title(R.string.MuteDialog_mute_notifications) title(R.string.notificationsMute)
items(Option.values().map { it.stringRes }.map(context::getString).toTypedArray()) { items(Option.values().map { it.stringRes }.map(context::getString).toTypedArray()) {
onMuteDuration(Option.values()[it].getTime()) onMuteDuration(Option.values()[it].getTime())
} }
} }
private enum class Option(@StringRes val stringRes: Int, val getTime: () -> Long) { 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)), ONE_HOUR(R.string.notificationsMute1Hour, duration = TimeUnit.HOURS.toMillis(1)),
TWO_HOURS(R.string.arrays__mute_for_two_hours, duration = TimeUnit.DAYS.toMillis(2)), TWO_HOURS(R.string.notificationsMute2Hours, duration = TimeUnit.DAYS.toMillis(2)),
ONE_DAY(R.string.arrays__mute_for_one_day, duration = TimeUnit.DAYS.toMillis(1)), ONE_DAY(R.string.notificationsMute1Day, duration = TimeUnit.DAYS.toMillis(1)),
SEVEN_DAYS(R.string.arrays__mute_for_seven_days, duration = TimeUnit.DAYS.toMillis(7)), SEVEN_DAYS(R.string.notificationsMute1Week, duration = TimeUnit.DAYS.toMillis(7)),
FOREVER(R.string.arrays__mute_forever, getTime = { Long.MAX_VALUE }); FOREVER(R.string.notificationsMute, getTime = { Long.MAX_VALUE });
constructor(@StringRes stringRes: Int, duration: Long): this(stringRes, { System.currentTimeMillis() + duration }) constructor(@StringRes stringRes: Int, duration: Long): this(stringRes, { System.currentTimeMillis() + duration })
} }

View File

@ -16,6 +16,8 @@
*/ */
package org.thoughtcrime.securesms; package org.thoughtcrime.securesms;
import static org.session.libsession.utilities.StringSubstitutionConstants.APP_NAME_KEY;
import android.animation.Animator; import android.animation.Animator;
import android.app.KeyguardManager; import android.app.KeyguardManager;
import android.content.ComponentName; import android.content.ComponentName;
@ -25,20 +27,18 @@ import android.content.ServiceConnection;
import android.graphics.PorterDuff; import android.graphics.PorterDuff;
import android.os.Bundle; import android.os.Bundle;
import android.os.IBinder; 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.View;
import android.view.animation.Animation; import android.view.animation.Animation;
import android.view.animation.BounceInterpolator; import android.view.animation.BounceInterpolator;
import android.view.animation.TranslateAnimation; import android.view.animation.TranslateAnimation;
import android.widget.Button; import android.widget.Button;
import android.widget.ImageView; import android.widget.ImageView;
import android.widget.TextView;
import androidx.core.hardware.fingerprint.FingerprintManagerCompat; import androidx.core.hardware.fingerprint.FingerprintManagerCompat;
import androidx.core.os.CancellationSignal; 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.libsession.utilities.TextSecurePreferences;
import org.session.libsignal.utilities.Log; import org.session.libsignal.utilities.Log;
import org.thoughtcrime.securesms.components.AnimatingToggle; 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.service.KeyCachingService;
import org.thoughtcrime.securesms.util.AnimationCompleteListener; 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. //TODO Rename to ScreenLockActivity and refactor to Kotlin.
public class PassphrasePromptActivity extends BaseActionBarActivity { public class PassphrasePromptActivity extends BaseActionBarActivity {
@ -158,6 +153,16 @@ public class PassphrasePromptActivity extends BaseActionBarActivity {
} }
private void initializeResources() { 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); visibilityToggle = findViewById(R.id.button_toggle);
fingerprintPrompt = findViewById(R.id.fingerprint_auth_container); fingerprintPrompt = findViewById(R.id.fingerprint_auth_container);
lockScreenButton = findViewById(R.id.lock_screen_auth_container); lockScreenButton = findViewById(R.id.lock_screen_auth_container);
@ -165,10 +170,6 @@ public class PassphrasePromptActivity extends BaseActionBarActivity {
fingerprintCancellationSignal = new CancellationSignal(); fingerprintCancellationSignal = new CancellationSignal();
fingerprintListener = new FingerprintListener(); 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.setImageResource(R.drawable.ic_fingerprint_white_48dp);
fingerprintPrompt.getBackground().setColorFilter(getResources().getColor(R.color.signal_primary), PorterDuff.Mode.SRC_IN); fingerprintPrompt.getBackground().setColorFilter(getResources().getColor(R.color.signal_primary), PorterDuff.Mode.SRC_IN);

View File

@ -34,7 +34,7 @@ public abstract class PassphraseRequiredActionBarActivity extends BaseActionBarA
private BroadcastReceiver clearKeyReceiver; private BroadcastReceiver clearKeyReceiver;
@Override @Override
protected final void onCreate(Bundle savedInstanceState) { protected void onCreate(Bundle savedInstanceState) {
Log.i(TAG, "onCreate(" + savedInstanceState + ")"); Log.i(TAG, "onCreate(" + savedInstanceState + ")");
onPreCreate(); onPreCreate();

View File

@ -132,7 +132,7 @@ public class ShareActivity extends PassphraseRequiredActionBarActivity
} }
private void initializeToolbar() { private void initializeToolbar() {
Toolbar toolbar = findViewById(R.id.toolbar); Toolbar toolbar = findViewById(R.id.search_toolbar);
setSupportActionBar(toolbar); setSupportActionBar(toolbar);
ActionBar actionBar = getSupportActionBar(); ActionBar actionBar = getSupportActionBar();
actionBar.setDisplayHomeAsUpEnabled(true); actionBar.setDisplayHomeAsUpEnabled(true);

View File

@ -37,7 +37,7 @@ public class ShortcutLauncherActivity extends AppCompatActivity {
String serializedAddress = getIntent().getStringExtra(KEY_SERIALIZED_ADDRESS); String serializedAddress = getIntent().getStringExtra(KEY_SERIALIZED_ADDRESS);
if (serializedAddress == null) { 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)); startActivity(new Intent(this, HomeActivity.class));
finish(); finish();
return; return;

View File

@ -105,7 +105,7 @@ public final class AvatarSelection {
extraIntents.add(new Intent("network.loki.securesms.action.CLEAR_PROFILE_PHOTO")); extraIntents.add(new Intent("network.loki.securesms.action.CLEAR_PROFILE_PHOTO"));
} }
Intent chooserIntent = Intent.createChooser(galleryIntent, context.getString(R.string.CreateProfileActivity_profile_photo)); Intent chooserIntent = Intent.createChooser(galleryIntent, context.getString(R.string.profileDisplayPicture));
if (!extraIntents.isEmpty()) { if (!extraIntents.isEmpty()) {
chooserIntent.putExtra(Intent.EXTRA_INITIAL_INTENTS, extraIntents.toArray(new Intent[0])); chooserIntent.putExtra(Intent.EXTRA_INITIAL_INTENTS, extraIntents.toArray(new Intent[0]));

View File

@ -22,6 +22,7 @@ import androidx.core.content.ContextCompat
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.localbroadcastmanager.content.LocalBroadcastManager import androidx.localbroadcastmanager.content.LocalBroadcastManager
import com.bumptech.glide.Glide
import com.bumptech.glide.load.engine.DiskCacheStrategy import com.bumptech.glide.load.engine.DiskCacheStrategy
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
@ -38,7 +39,6 @@ import org.session.libsession.utilities.truncateIdForDisplay
import org.session.libsignal.utilities.Log import org.session.libsignal.utilities.Log
import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity
import org.thoughtcrime.securesms.dependencies.DatabaseComponent import org.thoughtcrime.securesms.dependencies.DatabaseComponent
import org.thoughtcrime.securesms.mms.GlideApp
import org.thoughtcrime.securesms.permissions.Permissions import org.thoughtcrime.securesms.permissions.Permissions
import org.thoughtcrime.securesms.service.WebRtcCallService import org.thoughtcrime.securesms.service.WebRtcCallService
import org.thoughtcrime.securesms.util.AvatarPlaceholderGenerator import org.thoughtcrime.securesms.util.AvatarPlaceholderGenerator
@ -70,7 +70,7 @@ class WebRtcCallActivity : PassphraseRequiredActionBarActivity() {
} }
private val viewModel by viewModels<CallViewModel>() private val viewModel by viewModels<CallViewModel>()
private val glide by lazy { GlideApp.with(this) } private val glide by lazy { Glide.with(this) }
private lateinit var binding: ActivityWebrtcBinding private lateinit var binding: ActivityWebrtcBinding
private var uiJob: Job? = null private var uiJob: Job? = null
private var wantsToAnswer = false private var wantsToAnswer = false

View File

@ -1,312 +0,0 @@
package org.thoughtcrime.securesms.components;
import android.Manifest;
import android.animation.Animator;
import android.annotation.TargetApi;
import android.app.Activity;
import android.content.Context;
import android.graphics.drawable.BitmapDrawable;
import android.net.Uri;
import android.os.Build;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.loader.app.LoaderManager;
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 org.thoughtcrime.securesms.permissions.Permissions;
import org.session.libsession.utilities.ViewUtil;
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);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
animateWindowInCircular(anchor, getContentView());
} else {
animateWindowInTranslate(getContentView());
}
}
});
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
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() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
animateWindowOutCircular(currentAnchor, getContentView());
} else {
animateWindowOutTranslate(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);
}
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
private void animateWindowInCircular(@Nullable View anchor, @NonNull View contentView) {
Pair<Integer, Integer> 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);
}
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
private void animateWindowOutCircular(@Nullable View anchor, @NonNull View contentView) {
Pair<Integer, Integer> 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<Integer, Integer> 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);
}
}

View File

@ -25,8 +25,8 @@ import org.session.libsession.utilities.Address;
import org.session.libsession.utilities.ThemeUtil; import org.session.libsession.utilities.ThemeUtil;
import org.session.libsession.utilities.recipients.Recipient; import org.session.libsession.utilities.recipients.Recipient;
import org.session.libsession.utilities.recipients.RecipientExporter; import org.session.libsession.utilities.recipients.RecipientExporter;
import org.thoughtcrime.securesms.mms.GlideApp; import com.bumptech.glide.Glide;
import org.thoughtcrime.securesms.mms.GlideRequests; import com.bumptech.glide.RequestManager;
import org.thoughtcrime.securesms.util.AvatarPlaceholderGenerator; import org.thoughtcrime.securesms.util.AvatarPlaceholderGenerator;
import java.util.Objects; import java.util.Objects;
@ -117,10 +117,10 @@ public class AvatarImageView extends AppCompatImageView {
} }
private void updateAvatar(Recipient recipient) { private void updateAvatar(Recipient recipient) {
setAvatar(GlideApp.with(getContext()), recipient, false); setAvatar(Glide.with(getContext()), recipient, false);
} }
public void setAvatar(@NonNull GlideRequests requestManager, @Nullable Recipient recipient, boolean quickContactEnabled) { public void setAvatar(@NonNull RequestManager requestManager, @Nullable Recipient recipient, boolean quickContactEnabled) {
if (recipient != null) { if (recipient != null) {
if (recipient.isLocalNumber()) { if (recipient.isLocalNumber()) {
setImageDrawable(new ResourceContactPhoto(R.drawable.ic_note_to_self).asDrawable(getContext(), recipient.getColor().toAvatarColor(getContext()), inverted)); setImageDrawable(new ResourceContactPhoto(R.drawable.ic_note_to_self).asDrawable(getContext(), recipient.getColor().toAvatarColor(getContext()), inverted));
@ -156,7 +156,7 @@ public class AvatarImageView extends AppCompatImageView {
} }
} }
public void clear(@NonNull GlideRequests glideRequests) { public void clear(@NonNull RequestManager glideRequests) {
glideRequests.clear(this); glideRequests.clear(this);
} }

View File

@ -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);
}
}
}
}

View File

@ -105,7 +105,7 @@ public class DocumentView extends FrameLayout {
this.documentSlide = documentSlide; 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.fileSize.setText(Util.getPrettyFileSize(documentSlide.getFileSize()));
this.document.setText(getFileType(documentSlide.getFileName())); this.document.setText(getFileType(documentSlide.getFileName()));
this.setOnClickListener(new OpenClickedListener(documentSlide)); this.setOnClickListener(new OpenClickedListener(documentSlide));

View File

@ -54,7 +54,7 @@ public class FromTextView extends EmojiTextView {
if (recipient.isLocalNumber()) { 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())) { } else if (recipient.getName() == null && !TextUtils.isEmpty(recipient.getProfileName())) {
SpannableString profileName = new SpannableString(" (~" + recipient.getProfileName() + ") "); SpannableString profileName = new SpannableString(" (~" + recipient.getProfileName() + ") ");
profileName.setSpan(new CenterAlignedRelativeSizeSpan(0.75f), 0, profileName.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); profileName.setSpan(new CenterAlignedRelativeSizeSpan(0.75f), 0, profileName.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);

View File

@ -13,7 +13,7 @@ import android.view.Window;
import android.widget.FrameLayout; import android.widget.FrameLayout;
import network.loki.messenger.R; import network.loki.messenger.R;
import org.thoughtcrime.securesms.mms.GlideRequests; import com.bumptech.glide.RequestManager;
import org.thoughtcrime.securesms.mms.VideoSlide; import org.thoughtcrime.securesms.mms.VideoSlide;
import org.thoughtcrime.securesms.video.VideoPlayer; import org.thoughtcrime.securesms.video.VideoPlayer;
@ -54,7 +54,7 @@ public class MediaView extends FrameLayout {
this.videoView = new Stub<>(findViewById(R.id.video_player_stub)); this.videoView = new Stub<>(findViewById(R.id.video_player_stub));
} }
public void set(@NonNull GlideRequests glideRequests, public void set(@NonNull RequestManager glideRequests,
@NonNull Window window, @NonNull Window window,
@NonNull Uri source, @NonNull Uri source,
@NonNull String mediaType, @NonNull String mediaType,

View File

@ -20,8 +20,8 @@ import org.session.libsession.utilities.GroupUtil
import org.session.libsession.utilities.recipients.Recipient import org.session.libsession.utilities.recipients.Recipient
import org.session.libsignal.utilities.Log import org.session.libsignal.utilities.Log
import org.thoughtcrime.securesms.dependencies.DatabaseComponent import org.thoughtcrime.securesms.dependencies.DatabaseComponent
import org.thoughtcrime.securesms.mms.GlideApp import com.bumptech.glide.Glide
import org.thoughtcrime.securesms.mms.GlideRequests import com.bumptech.glide.RequestManager
class ProfilePictureView @JvmOverloads constructor( class ProfilePictureView @JvmOverloads constructor(
context: Context, attrs: AttributeSet? = null context: Context, attrs: AttributeSet? = null
@ -29,7 +29,7 @@ class ProfilePictureView @JvmOverloads constructor(
private val TAG = "ProfilePictureView" private val TAG = "ProfilePictureView"
private val binding = ViewProfilePictureBinding.inflate(LayoutInflater.from(context), this) private val binding = ViewProfilePictureBinding.inflate(LayoutInflater.from(context), this)
private val glide: GlideRequests = GlideApp.with(this) private val glide: RequestManager = Glide.with(this)
private val prefs = AppTextSecurePreferences(context) private val prefs = AppTextSecurePreferences(context)
private val userPublicKey = prefs.getLocalNumber() private val userPublicKey = prefs.getLocalNumber()
var publicKey: String? = null var publicKey: String? = null

View File

@ -28,7 +28,7 @@ import com.bumptech.glide.signature.MediaStoreSignature;
import network.loki.messenger.R; import network.loki.messenger.R;
import org.thoughtcrime.securesms.database.CursorRecyclerViewAdapter; import org.thoughtcrime.securesms.database.CursorRecyclerViewAdapter;
import org.thoughtcrime.securesms.database.loaders.RecentPhotosLoader; import org.thoughtcrime.securesms.database.loaders.RecentPhotosLoader;
import org.thoughtcrime.securesms.mms.GlideApp; import com.bumptech.glide.Glide;
import org.session.libsession.utilities.ViewUtil; import org.session.libsession.utilities.ViewUtil;
@ -118,7 +118,7 @@ public class RecentPhotoViewRail extends FrameLayout implements LoaderManager.Lo
Key signature = new MediaStoreSignature(mimeType, dateModified, orientation); Key signature = new MediaStoreSignature(mimeType, dateModified, orientation);
GlideApp.with(getContext().getApplicationContext()) Glide.with(getContext().getApplicationContext())
.load(uri) .load(uri)
.signature(signature) .signature(signature)
.diskCacheStrategy(DiskCacheStrategy.NONE) .diskCacheStrategy(DiskCacheStrategy.NONE)

View File

@ -44,10 +44,9 @@ public class SearchToolbar extends LinearLayout {
inflate(getContext(), R.layout.search_toolbar, this); inflate(getContext(), R.layout.search_toolbar, this);
setOrientation(VERTICAL); setOrientation(VERTICAL);
Toolbar toolbar = findViewById(R.id.toolbar); Toolbar toolbar = findViewById(R.id.search_toolbar);
toolbar.setNavigationIcon( toolbar.setNavigationIcon(getContext().getResources().getDrawable(R.drawable.ic_baseline_clear_24));
getContext().getResources().getDrawable(R.drawable.ic_baseline_clear_24));
toolbar.inflateMenu(R.menu.conversation_list_search); toolbar.inflateMenu(R.menu.conversation_list_search);
this.searchItem = toolbar.getMenu().findItem(R.id.action_filter_search); this.searchItem = toolbar.getMenu().findItem(R.id.action_filter_search);
@ -56,8 +55,8 @@ public class SearchToolbar extends LinearLayout {
searchView.setSubmitButtonEnabled(false); searchView.setSubmitButtonEnabled(false);
if (searchText != null) searchText.setHint(R.string.SearchToolbar_search); if (searchText != null) searchText.setHint(R.string.search);
else searchView.setQueryHint(getResources().getString(R.string.SearchToolbar_search)); else searchView.setQueryHint(getResources().getString(R.string.search));
searchView.setOnQueryTextListener(new SearchView.OnQueryTextListener() { searchView.setOnQueryTextListener(new SearchView.OnQueryTextListener() {
@Override @Override

View File

@ -8,7 +8,7 @@ import android.view.View;
import android.widget.FrameLayout; import android.widget.FrameLayout;
import org.thoughtcrime.securesms.conversation.v2.utilities.ThumbnailView; import org.thoughtcrime.securesms.conversation.v2.utilities.ThumbnailView;
import org.thoughtcrime.securesms.mms.GlideRequests; import com.bumptech.glide.RequestManager;
import org.thoughtcrime.securesms.mms.Slide; import org.thoughtcrime.securesms.mms.Slide;
import org.thoughtcrime.securesms.mms.SlideClickListener; import org.thoughtcrime.securesms.mms.SlideClickListener;

View File

@ -1,15 +1,18 @@
package org.thoughtcrime.securesms.components; package org.thoughtcrime.securesms.components;
import static org.session.libsession.utilities.StringSubstitutionConstants.APP_NAME_KEY;
import android.content.Context; import android.content.Context;
import android.util.AttributeSet; import android.util.AttributeSet;
import androidx.preference.CheckBoxPreference; import androidx.preference.CheckBoxPreference;
import androidx.preference.Preference; import androidx.preference.Preference;
import com.squareup.phrase.Phrase;
import network.loki.messenger.R; import network.loki.messenger.R;
public class SwitchPreferenceCompat extends CheckBoxPreference { public class SwitchPreferenceCompat extends CheckBoxPreference {
private static String LOCK_SCREEN_KEY = "pref_android_screen_lock";
private Preference.OnPreferenceClickListener listener; private Preference.OnPreferenceClickListener listener;
public SwitchPreferenceCompat(Context context, AttributeSet attrs, int defStyleAttr) { public SwitchPreferenceCompat(Context context, AttributeSet attrs, int defStyleAttr) {
@ -34,6 +37,19 @@ public class SwitchPreferenceCompat extends CheckBoxPreference {
private void setLayoutRes() { private void setLayoutRes() {
setWidgetLayoutResource(R.layout.switch_compat_preference); setWidgetLayoutResource(R.layout.switch_compat_preference);
if (this.hasKey()) {
String key = this.getKey();
// Substitute app name into lockscreen preference summary
if (key.equalsIgnoreCase(LOCK_SCREEN_KEY)) {
Context c = getContext();
CharSequence substitutedSummaryCS = Phrase.from(c, R.string.lockAppDescriptionAndroid)
.put(APP_NAME_KEY, c.getString(R.string.app_name))
.format();
this.setSummary(substitutedSummaryCS);
}
}
} }
@Override @Override

View File

@ -1,36 +1,33 @@
package org.thoughtcrime.securesms.components; package org.thoughtcrime.securesms.components;
import static org.session.libsession.utilities.StringSubstitutionConstants.COUNT_KEY;
import android.animation.LayoutTransition; import android.animation.LayoutTransition;
import android.content.Context; import android.content.Context;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.content.ContextCompat;
import android.util.AttributeSet; import android.util.AttributeSet;
import android.view.View; import android.view.View;
import android.widget.FrameLayout; import android.widget.FrameLayout;
import android.widget.TextView; import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.content.ContextCompat;
import com.annimon.stream.Stream; import com.annimon.stream.Stream;
import com.pnikosis.materialishprogress.ProgressWheel; import com.pnikosis.materialishprogress.ProgressWheel;
import com.squareup.phrase.Phrase;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import network.loki.messenger.R;
import org.greenrobot.eventbus.EventBus; import org.greenrobot.eventbus.EventBus;
import org.greenrobot.eventbus.Subscribe; import org.greenrobot.eventbus.Subscribe;
import org.greenrobot.eventbus.ThreadMode; import org.greenrobot.eventbus.ThreadMode;
import org.session.libsession.messaging.sending_receiving.attachments.Attachment; import org.session.libsession.messaging.sending_receiving.attachments.Attachment;
import org.session.libsession.messaging.sending_receiving.attachments.AttachmentTransferProgress; import org.session.libsession.messaging.sending_receiving.attachments.AttachmentTransferProgress;
import org.thoughtcrime.securesms.database.AttachmentDatabase; import org.session.libsession.utilities.ViewUtil;
import org.thoughtcrime.securesms.events.PartProgressEvent; import org.thoughtcrime.securesms.events.PartProgressEvent;
import org.thoughtcrime.securesms.mms.Slide; 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 { public class TransferControlView extends FrameLayout {
@Nullable private List<Slide> slides; @Nullable private List<Slide> slides;
@ -191,7 +188,8 @@ public class TransferControlView extends FrameLayout {
return slides.get(0).getContentDescription(); return slides.get(0).getContentDescription();
} else { } else {
int downloadCount = Stream.of(slides).reduce(0, (count, slide) -> slide.getTransferState() != AttachmentTransferProgress.TRANSFER_PROGRESS_DONE ? count + 1 : count); 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);
return Phrase.from(getContext(), R.string.andMore).put(COUNT_KEY, downloadCount).format().toString();
} }
} }

View File

@ -25,7 +25,7 @@ import network.loki.messenger.R;
import org.thoughtcrime.securesms.components.subsampling.AttachmentBitmapDecoder; import org.thoughtcrime.securesms.components.subsampling.AttachmentBitmapDecoder;
import org.thoughtcrime.securesms.components.subsampling.AttachmentRegionDecoder; import org.thoughtcrime.securesms.components.subsampling.AttachmentRegionDecoder;
import org.thoughtcrime.securesms.mms.DecryptableStreamUriLoader.DecryptableUri; import org.thoughtcrime.securesms.mms.DecryptableStreamUriLoader.DecryptableUri;
import org.thoughtcrime.securesms.mms.GlideRequests; import com.bumptech.glide.RequestManager;
import org.thoughtcrime.securesms.mms.PartAuthority; import org.thoughtcrime.securesms.mms.PartAuthority;
import org.thoughtcrime.securesms.util.BitmapDecodingException; import org.thoughtcrime.securesms.util.BitmapDecodingException;
import org.thoughtcrime.securesms.util.BitmapUtil; import org.thoughtcrime.securesms.util.BitmapUtil;
@ -62,7 +62,7 @@ public class ZoomingImageView extends FrameLayout {
} }
@SuppressLint("StaticFieldLeak") @SuppressLint("StaticFieldLeak")
public void setImageUri(@NonNull GlideRequests glideRequests, @NonNull Uri uri, @NonNull String contentType) public void setImageUri(@NonNull RequestManager glideRequests, @NonNull Uri uri, @NonNull String contentType)
{ {
final Context context = getContext(); final Context context = getContext();
final int maxTextureSize = BitmapUtil.getMaxTextureSize(); final int maxTextureSize = BitmapUtil.getMaxTextureSize();
@ -97,7 +97,7 @@ public class ZoomingImageView extends FrameLayout {
}.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
} }
private void setImageViewUri(@NonNull GlideRequests glideRequests, @NonNull Uri uri) { private void setImageViewUri(@NonNull RequestManager glideRequests, @NonNull Uri uri) {
photoView.setVisibility(View.VISIBLE); photoView.setVisibility(View.VISIBLE);
subsamplingImageView.setVisibility(View.GONE); subsamplingImageView.setVisibility(View.GONE);

View File

@ -12,7 +12,7 @@ import android.widget.ImageView;
import org.thoughtcrime.securesms.components.emoji.EmojiPageViewGridAdapter.VariationSelectorListener; import org.thoughtcrime.securesms.components.emoji.EmojiPageViewGridAdapter.VariationSelectorListener;
import org.thoughtcrime.securesms.mms.GlideRequests; import com.bumptech.glide.RequestManager;
import org.thoughtcrime.securesms.util.ResUtil; import org.thoughtcrime.securesms.util.ResUtil;
import org.session.libsession.utilities.ThemeUtil; import org.session.libsession.utilities.ThemeUtil;
@ -87,7 +87,7 @@ public class EmojiKeyboardProvider implements MediaKeyboardProvider,
} }
@Override @Override
public void loadCategoryTabIcon(@NonNull GlideRequests glideRequests, @NonNull ImageView imageView, int index) { public void loadCategoryTabIcon(@NonNull RequestManager glideRequests, @NonNull ImageView imageView, int index) {
Drawable drawable = ResUtil.getDrawable(context, models.get(index).getIconAttr()); Drawable drawable = ResUtil.getDrawable(context, models.get(index).getIconAttr());
imageView.setImageDrawable(drawable); imageView.setImageDrawable(drawable);
} }

File diff suppressed because one or more lines are too long

View File

@ -17,7 +17,7 @@ import android.widget.FrameLayout;
import org.thoughtcrime.securesms.components.InputAwareLayout.InputView; import org.thoughtcrime.securesms.components.InputAwareLayout.InputView;
import org.thoughtcrime.securesms.components.RepeatableImageKey; import org.thoughtcrime.securesms.components.RepeatableImageKey;
import org.session.libsignal.utilities.Log; import org.session.libsignal.utilities.Log;
import org.thoughtcrime.securesms.mms.GlideApp; import com.bumptech.glide.Glide;
import java.util.Arrays; import java.util.Arrays;
@ -158,7 +158,7 @@ public class MediaKeyboard extends FrameLayout implements InputView,
this.searchButton = view.findViewById(R.id.media_keyboard_search); this.searchButton = view.findViewById(R.id.media_keyboard_search);
this.addButton = view.findViewById(R.id.media_keyboard_add); this.addButton = view.findViewById(R.id.media_keyboard_add);
this.categoryTabAdapter = new MediaKeyboardBottomTabAdapter(GlideApp.with(this), this); this.categoryTabAdapter = new MediaKeyboardBottomTabAdapter(Glide.with(this), this);
categoryTabs.setLayoutManager(new LinearLayoutManager(getContext(), LinearLayoutManager.HORIZONTAL, false)); categoryTabs.setLayoutManager(new LinearLayoutManager(getContext(), LinearLayoutManager.HORIZONTAL, false));
categoryTabs.setAdapter(categoryTabAdapter); categoryTabs.setAdapter(categoryTabAdapter);

View File

@ -9,20 +9,20 @@ import android.widget.ImageView;
import org.thoughtcrime.securesms.components.emoji.MediaKeyboardProvider.TabIconProvider; import org.thoughtcrime.securesms.components.emoji.MediaKeyboardProvider.TabIconProvider;
import org.thoughtcrime.securesms.mms.GlideRequests; import com.bumptech.glide.RequestManager;
import network.loki.messenger.R; import network.loki.messenger.R;
public class MediaKeyboardBottomTabAdapter extends RecyclerView.Adapter<MediaKeyboardBottomTabAdapter.MediaKeyboardBottomTabViewHolder> { public class MediaKeyboardBottomTabAdapter extends RecyclerView.Adapter<MediaKeyboardBottomTabAdapter.MediaKeyboardBottomTabViewHolder> {
private final GlideRequests glideRequests; private final RequestManager glideRequests;
private final EventListener eventListener; private final EventListener eventListener;
private TabIconProvider tabIconProvider; private TabIconProvider tabIconProvider;
private int activePosition; private int activePosition;
private int count; private int count;
public MediaKeyboardBottomTabAdapter(@NonNull GlideRequests glideRequests, @NonNull EventListener eventListener) { public MediaKeyboardBottomTabAdapter(@NonNull RequestManager glideRequests, @NonNull EventListener eventListener) {
this.glideRequests = glideRequests; this.glideRequests = glideRequests;
this.eventListener = eventListener; this.eventListener = eventListener;
} }
@ -71,7 +71,7 @@ public class MediaKeyboardBottomTabAdapter extends RecyclerView.Adapter<MediaKey
this.indicator = itemView.findViewById(R.id.media_keyboard_bottom_tab_indicator); this.indicator = itemView.findViewById(R.id.media_keyboard_bottom_tab_indicator);
} }
void bind(@NonNull GlideRequests glideRequests, void bind(@NonNull RequestManager glideRequests,
@NonNull EventListener eventListener, @NonNull EventListener eventListener,
@NonNull TabIconProvider tabIconProvider, @NonNull TabIconProvider tabIconProvider,
int index, int index,

View File

@ -8,7 +8,7 @@ import android.widget.ImageView;
import org.thoughtcrime.securesms.mms.GlideRequests; import com.bumptech.glide.RequestManager;
public interface MediaKeyboardProvider { public interface MediaKeyboardProvider {
@LayoutRes int getProviderIconView(boolean selected); @LayoutRes int getProviderIconView(boolean selected);
@ -48,6 +48,6 @@ public interface MediaKeyboardProvider {
} }
interface TabIconProvider { interface TabIconProvider {
void loadCategoryTabIcon(@NonNull GlideRequests glideRequests, @NonNull ImageView imageView, int index); void loadCategoryTabIcon(@NonNull RequestManager glideRequests, @NonNull ImageView imageView, int index);
} }
} }

View File

@ -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.contains(TextSecurePreferences.getLocalNumber(context)))
// { // {
// numberList.add(TextSecurePreferences.getLocalNumber(context)); // numberList.add(TextSecurePreferences.getLocalNumber(context));

View File

@ -6,10 +6,10 @@ import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import network.loki.messenger.databinding.ContactSelectionListDividerBinding import network.loki.messenger.databinding.ContactSelectionListDividerBinding
import org.session.libsession.utilities.recipients.Recipient import org.session.libsession.utilities.recipients.Recipient
import org.thoughtcrime.securesms.mms.GlideRequests import com.bumptech.glide.RequestManager
class ContactSelectionListAdapter(private val context: Context, private val multiSelect: Boolean) : RecyclerView.Adapter<RecyclerView.ViewHolder>() { class ContactSelectionListAdapter(private val context: Context, private val multiSelect: Boolean) : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
lateinit var glide: GlideRequests lateinit var glide: RequestManager
val selectedContacts = mutableSetOf<Recipient>() val selectedContacts = mutableSetOf<Recipient>()
var items = listOf<ContactSelectionListItem>() var items = listOf<ContactSelectionListItem>()
set(value) { field = value; notifyDataSetChanged() } set(value) { field = value; notifyDataSetChanged() }

View File

@ -11,7 +11,7 @@ import androidx.recyclerview.widget.LinearLayoutManager
import network.loki.messenger.databinding.ContactSelectionListFragmentBinding import network.loki.messenger.databinding.ContactSelectionListFragmentBinding
import org.session.libsession.utilities.recipients.Recipient import org.session.libsession.utilities.recipients.Recipient
import org.session.libsignal.utilities.Log import org.session.libsignal.utilities.Log
import org.thoughtcrime.securesms.mms.GlideApp import com.bumptech.glide.Glide
class ContactSelectionListFragment : Fragment(), LoaderManager.LoaderCallbacks<List<ContactSelectionListItem>>, ContactClickListener { class ContactSelectionListFragment : Fragment(), LoaderManager.LoaderCallbacks<List<ContactSelectionListItem>>, ContactClickListener {
private lateinit var binding: ContactSelectionListFragmentBinding private lateinit var binding: ContactSelectionListFragmentBinding
@ -27,7 +27,7 @@ class ContactSelectionListFragment : Fragment(), LoaderManager.LoaderCallbacks<L
private val listAdapter by lazy { private val listAdapter by lazy {
val result = ContactSelectionListAdapter(requireActivity(), multiSelect) val result = ContactSelectionListAdapter(requireActivity(), multiSelect)
result.glide = GlideApp.with(this) result.glide = Glide.with(this)
result.contactClickListener = this result.contactClickListener = this
result result
} }

View File

@ -36,7 +36,7 @@ class ContactSelectionListLoader(context: Context, val mode: Int, val filter: St
list.addAll(getClosedGroups(contacts)) list.addAll(getClosedGroups(contacts))
} }
if (isFlagSet(DisplayMode.FLAG_OPEN_GROUPS)) { if (isFlagSet(DisplayMode.FLAG_OPEN_GROUPS)) {
list.addAll(getOpenGroups(contacts)) list.addAll(getCommunities(contacts))
} }
if (isFlagSet(DisplayMode.FLAG_CONTACTS)) { if (isFlagSet(DisplayMode.FLAG_CONTACTS)) {
list.addAll(getContacts(contacts)) list.addAll(getContacts(contacts))
@ -45,19 +45,19 @@ class ContactSelectionListLoader(context: Context, val mode: Int, val filter: St
} }
private fun getContacts(contacts: List<Recipient>): List<ContactSelectionListItem> { private fun getContacts(contacts: List<Recipient>): List<ContactSelectionListItem> {
return getItems(contacts, context.getString(R.string.fragment_contact_selection_contacts_title)) { return getItems(contacts, context.getString(R.string.contactsContacts)) {
!it.isGroupRecipient !it.isGroupRecipient
} }
} }
private fun getClosedGroups(contacts: List<Recipient>): List<ContactSelectionListItem> { private fun getClosedGroups(contacts: List<Recipient>): List<ContactSelectionListItem> {
return getItems(contacts, context.getString(R.string.fragment_contact_selection_closed_groups_title)) { return getItems(contacts, context.getString(R.string.conversationsGroups)) {
it.address.isClosedGroup it.address.isClosedGroup
} }
} }
private fun getOpenGroups(contacts: List<Recipient>): List<ContactSelectionListItem> { private fun getCommunities(contacts: List<Recipient>): List<ContactSelectionListItem> {
return getItems(contacts, context.getString(R.string.fragment_contact_selection_open_groups_title)) { return getItems(contacts, context.getString(R.string.conversationsCommunities)) {
it.address.isCommunity it.address.isCommunity
} }
} }

View File

@ -18,10 +18,10 @@ public final class ContactUtil {
String contactName = ContactUtil.getDisplayName(contact); String contactName = ContactUtil.getDisplayName(contact);
if (!TextUtils.isEmpty(contactName)) { 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) { private static @NonNull String getDisplayName(@Nullable Contact contact) {

View File

@ -159,7 +159,7 @@ public class ContactsCursorLoader extends CursorLoader {
private Cursor getGroupsHeaderCursor() { private Cursor getGroupsHeaderCursor() {
MatrixCursor groupHeader = new MatrixCursor(CONTACT_PROJECTION, 1); 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, ContactsContract.CommonDataKinds.Phone.TYPE_MOBILE,
"", "",
@ -221,16 +221,6 @@ public class ContactsCursorLoader extends CursorLoader {
return groupContacts; 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<Cursor> list) { private static boolean isCursorListEmpty(List<Cursor> list) {
int sum = 0; int sum = 0;
for (Cursor cursor : list) { for (Cursor cursor : list) {

View File

@ -12,7 +12,7 @@ import androidx.recyclerview.widget.LinearLayoutManager
import network.loki.messenger.R import network.loki.messenger.R
import network.loki.messenger.databinding.ActivitySelectContactsBinding import network.loki.messenger.databinding.ActivitySelectContactsBinding
import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity
import org.thoughtcrime.securesms.mms.GlideApp import com.bumptech.glide.Glide
class SelectContactsActivity : PassphraseRequiredActionBarActivity(), LoaderManager.LoaderCallbacks<List<String>> { class SelectContactsActivity : PassphraseRequiredActionBarActivity(), LoaderManager.LoaderCallbacks<List<String>> {
private lateinit var binding: ActivitySelectContactsBinding private lateinit var binding: ActivitySelectContactsBinding
@ -21,7 +21,7 @@ class SelectContactsActivity : PassphraseRequiredActionBarActivity(), LoaderMana
private lateinit var usersToExclude: Set<String> private lateinit var usersToExclude: Set<String>
private val selectContactsAdapter by lazy { private val selectContactsAdapter by lazy {
SelectContactsAdapter(this, GlideApp.with(this)) SelectContactsAdapter(this, Glide.with(this))
} }
companion object { companion object {
@ -35,7 +35,7 @@ class SelectContactsActivity : PassphraseRequiredActionBarActivity(), LoaderMana
super.onCreate(savedInstanceState, isReady) super.onCreate(savedInstanceState, isReady)
binding = ActivitySelectContactsBinding.inflate(layoutInflater) binding = ActivitySelectContactsBinding.inflate(layoutInflater)
setContentView(binding.root) setContentView(binding.root)
supportActionBar!!.title = resources.getString(R.string.activity_select_contacts_title) supportActionBar!!.title = resources.getString(R.string.contactSelect)
usersToExclude = intent.getStringArrayExtra(usersToExcludeKey)?.toSet() ?: setOf() usersToExclude = intent.getStringArrayExtra(usersToExcludeKey)?.toSet() ?: setOf()
val emptyStateText = intent.getStringExtra(emptyStateTextKey) val emptyStateText = intent.getStringExtra(emptyStateTextKey)

View File

@ -4,10 +4,10 @@ import android.content.Context
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import android.view.ViewGroup import android.view.ViewGroup
import org.session.libsession.utilities.Address import org.session.libsession.utilities.Address
import org.thoughtcrime.securesms.mms.GlideRequests import com.bumptech.glide.RequestManager
import org.session.libsession.utilities.recipients.Recipient import org.session.libsession.utilities.recipients.Recipient
class SelectContactsAdapter(private val context: Context, private val glide: GlideRequests) : RecyclerView.Adapter<SelectContactsAdapter.ViewHolder>() { class SelectContactsAdapter(private val context: Context, private val glide: RequestManager) : RecyclerView.Adapter<SelectContactsAdapter.ViewHolder>() {
val selectedMembers = mutableSetOf<String>() val selectedMembers = mutableSetOf<String>()
var members = listOf<String>() var members = listOf<String>()
set(value) { field = value; notifyDataSetChanged() } set(value) { field = value; notifyDataSetChanged() }

View File

@ -10,11 +10,11 @@ import network.loki.messenger.databinding.ViewUserBinding
import org.session.libsession.messaging.contacts.Contact import org.session.libsession.messaging.contacts.Contact
import org.session.libsession.utilities.recipients.Recipient import org.session.libsession.utilities.recipients.Recipient
import org.thoughtcrime.securesms.dependencies.DatabaseComponent import org.thoughtcrime.securesms.dependencies.DatabaseComponent
import org.thoughtcrime.securesms.mms.GlideRequests import com.bumptech.glide.RequestManager
class UserView : LinearLayout { class UserView : LinearLayout {
private lateinit var binding: ViewUserBinding private lateinit var binding: ViewUserBinding
var openGroupThreadID: Long = -1 // FIXME: This is a bit ugly var openGroupThreadID: Long = -1L // FIXME: This is a bit ugly
enum class ActionIndicator { enum class ActionIndicator {
None, None,
@ -45,13 +45,15 @@ class UserView : LinearLayout {
// endregion // endregion
// region Updating // region Updating
fun bind(user: Recipient, glide: GlideRequests, actionIndicator: ActionIndicator, isSelected: Boolean = false) { fun bind(user: Recipient, glide: RequestManager, actionIndicator: ActionIndicator, isSelected: Boolean = false) {
val isLocalUser = user.isLocalNumber val isLocalUser = user.isLocalNumber
fun getUserDisplayName(publicKey: String): String { 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) val contact = DatabaseComponent.get(context).sessionContactDatabase().getContactWithAccountID(publicKey)
return contact?.displayName(Contact.ContactContext.REGULAR) ?: publicKey return contact?.displayName(Contact.ContactContext.REGULAR) ?: publicKey
} }
val address = user.address.serialize() val address = user.address.serialize()
binding.profilePictureView.update(user) binding.profilePictureView.update(user)
binding.actionIndicatorImageView.setImageResource(R.drawable.ic_baseline_edit_24) binding.actionIndicatorImageView.setImageResource(R.drawable.ic_baseline_edit_24)
@ -84,8 +86,7 @@ class UserView : LinearLayout {
} }
} }
fun unbind() { fun unbind() { binding.profilePictureView.recycle() }
binding.profilePictureView.recycle()
}
// endregion // endregion
} }

View File

@ -11,21 +11,26 @@ import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import androidx.viewpager2.widget.ViewPager2.OnPageChangeCallback import androidx.viewpager2.widget.ViewPager2.OnPageChangeCallback
import com.google.android.material.tabs.TabLayoutMediator import com.google.android.material.tabs.TabLayoutMediator
import com.squareup.phrase.Phrase
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject
import kotlin.time.Duration.Companion.milliseconds
import network.loki.messenger.R import network.loki.messenger.R
import network.loki.messenger.databinding.ViewConversationActionBarBinding import network.loki.messenger.databinding.ViewConversationActionBarBinding
import network.loki.messenger.databinding.ViewConversationSettingBinding 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.LocalisedTimeUtil
import org.session.libsession.messaging.messages.ExpirationConfiguration import org.session.libsession.messaging.messages.ExpirationConfiguration
import org.session.libsession.messaging.open_groups.OpenGroup import org.session.libsession.messaging.open_groups.OpenGroup
import org.session.libsession.utilities.ExpirationUtil import org.session.libsession.utilities.ExpirationUtil
import org.session.libsession.utilities.StringSubstitutionConstants.COUNT_KEY
import org.session.libsession.utilities.StringSubstitutionConstants.DISAPPEARING_MESSAGES_TYPE_KEY
import org.session.libsession.utilities.StringSubstitutionConstants.TIME_KEY
import org.session.libsession.utilities.StringSubstitutionConstants.TIME_LARGE_KEY
import org.session.libsession.utilities.modifyLayoutParams import org.session.libsession.utilities.modifyLayoutParams
import org.session.libsession.utilities.recipients.Recipient import org.session.libsession.utilities.recipients.Recipient
import org.thoughtcrime.securesms.database.GroupDatabase import org.thoughtcrime.securesms.database.GroupDatabase
import org.thoughtcrime.securesms.database.LokiAPIDatabase import org.thoughtcrime.securesms.database.LokiAPIDatabase
import org.thoughtcrime.securesms.util.DateUtils
import java.util.Locale
import javax.inject.Inject
@AndroidEntryPoint @AndroidEntryPoint
class ConversationActionBarView @JvmOverloads constructor( class ConversationActionBarView @JvmOverloads constructor(
@ -82,7 +87,7 @@ class ConversationActionBarView @JvmOverloads constructor(
fun update(recipient: Recipient, openGroup: OpenGroup? = null, config: ExpirationConfiguration? = null) { fun update(recipient: Recipient, openGroup: OpenGroup? = null, config: ExpirationConfiguration? = null) {
binding.profilePictureView.update(recipient) 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) updateSubtitle(recipient, openGroup, config)
binding.conversationTitleContainer.modifyLayoutParams<MarginLayoutParams> { binding.conversationTitleContainer.modifyLayoutParams<MarginLayoutParams> {
@ -92,37 +97,58 @@ class ConversationActionBarView @JvmOverloads constructor(
fun updateSubtitle(recipient: Recipient, openGroup: OpenGroup? = null, config: ExpirationConfiguration? = null) { fun updateSubtitle(recipient: Recipient, openGroup: OpenGroup? = null, config: ExpirationConfiguration? = null) {
val settings = mutableListOf<ConversationSetting>() val settings = mutableListOf<ConversationSetting>()
// Specify the disappearing messages subtitle if we should
if (config?.isEnabled == true) { if (config?.isEnabled == true) {
val prefix = when (config.expiryMode) { // Get the type of disappearing message and the abbreviated duration..
is ExpiryMode.AfterRead -> R.string.expiration_type_disappear_after_read val dmTypeString = when (config.expiryMode) {
else -> R.string.expiration_type_disappear_after_send is AfterRead -> context.getString(R.string.read)
}.let(context::getString) else -> context.getString(R.string.send)
}
val durationAbbreviated = ExpirationUtil.getExpirationAbbreviatedDisplayValue(config.expiryMode.expirySeconds)
// ..then substitute into the string..
val subtitleTxt = Phrase.from(context, R.string.disappearingMessagesDisappear)
.put(DISAPPEARING_MESSAGES_TYPE_KEY, dmTypeString)
.put(TIME_KEY, durationAbbreviated)
.format().toString()
// .. and apply to the subtitle.
settings += ConversationSetting( settings += ConversationSetting(
"$prefix - ${ExpirationUtil.getExpirationAbbreviatedDisplayValue(context, config.expiryMode.expirySeconds)}", subtitleTxt,
ConversationSettingType.EXPIRATION, ConversationSettingType.EXPIRATION,
R.drawable.ic_timer, R.drawable.ic_timer,
resources.getString(R.string.AccessibilityId_disappearing_messages_type_and_time) resources.getString(R.string.AccessibilityId_disappearing_messages_type_and_time)
) )
} }
if (recipient.isMuted) { if (recipient.isMuted) {
settings += ConversationSetting( settings += ConversationSetting(
recipient.mutedUntil.takeUnless { it == Long.MAX_VALUE } 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())) } ?.let {
?: context.getString(R.string.ConversationActivity_muted_forever), val mutedDuration = it.milliseconds
val durationString = LocalisedTimeUtil.getDurationWithSingleLargestTimeUnit(context, mutedDuration)
Phrase.from(context, R.string.notificationsMuteFor)
.put(TIME_LARGE_KEY, durationString)
.format().toString()
}
?: context.getString(R.string.notificationsMuted),
ConversationSettingType.NOTIFICATION, ConversationSettingType.NOTIFICATION,
R.drawable.ic_outline_notifications_off_24 R.drawable.ic_outline_notifications_off_24
) )
} }
if (recipient.isGroupRecipient) { if (recipient.isGroupRecipient) {
val title = if (recipient.isCommunityRecipient) { val title = if (recipient.isCommunityRecipient) {
val userCount = openGroup?.let { lokiApiDb.getUserCount(it.room, it.server) } ?: 0 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 { } else {
val userCount = groupDb.getGroupMemberAddresses(recipient.address.toGroupString(), true).size 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) settings += ConversationSetting(title, ConversationSettingType.MEMBER_COUNT)
} }
settingsAdapter.submitList(settings) settingsAdapter.submitList(settings)
binding.settingsTabLayout.isVisible = settings.size > 1 binding.settingsTabLayout.isVisible = settings.size > 1
} }

View File

@ -1,7 +1,10 @@
package org.thoughtcrime.securesms.conversation.disappearingmessages package org.thoughtcrime.securesms.conversation.disappearingmessages
import android.content.Context import android.content.Context
import com.squareup.phrase.Phrase
import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.android.qualifiers.ApplicationContext
import javax.inject.Inject
import kotlin.time.Duration.Companion.milliseconds
import network.loki.messenger.R import network.loki.messenger.R
import network.loki.messenger.libsession_util.util.ExpiryMode import network.loki.messenger.libsession_util.util.ExpiryMode
import org.session.libsession.messaging.MessagingModuleConfiguration import org.session.libsession.messaging.MessagingModuleConfiguration
@ -12,13 +15,13 @@ import org.session.libsession.snode.SnodeAPI
import org.session.libsession.utilities.Address import org.session.libsession.utilities.Address
import org.session.libsession.utilities.ExpirationUtil import org.session.libsession.utilities.ExpirationUtil
import org.session.libsession.utilities.SSKEnvironment.MessageExpirationManagerProtocol import org.session.libsession.utilities.SSKEnvironment.MessageExpirationManagerProtocol
import org.session.libsession.utilities.StringSubstitutionConstants.DISAPPEARING_MESSAGES_TYPE_KEY
import org.session.libsession.utilities.StringSubstitutionConstants.TIME_LARGE_KEY
import org.session.libsession.utilities.TextSecurePreferences import org.session.libsession.utilities.TextSecurePreferences
import org.session.libsession.utilities.getExpirationTypeDisplayValue import org.session.libsession.utilities.getExpirationTypeDisplayValue
import org.thoughtcrime.securesms.database.model.MessageRecord import org.thoughtcrime.securesms.database.model.MessageRecord
import org.thoughtcrime.securesms.showSessionDialog import org.thoughtcrime.securesms.showSessionDialog
import org.thoughtcrime.securesms.util.ConfigurationMessageUtilities import org.thoughtcrime.securesms.util.ConfigurationMessageUtilities
import javax.inject.Inject
import kotlin.time.Duration.Companion.milliseconds
class DisappearingMessages @Inject constructor( class DisappearingMessages @Inject constructor(
@ApplicationContext private val context: Context, @ApplicationContext private val context: Context,
@ -43,21 +46,18 @@ class DisappearingMessages @Inject constructor(
} }
fun showFollowSettingDialog(context: Context, message: MessageRecord) = context.showSessionDialog { 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) { text(if (message.expiresIn == 0L) {
context.getString(R.string.dialog_disappearing_messages_follow_setting_off_body) context.getString(R.string.disappearingMessagesFollowSettingOff)
} else { } else {
context.getString( Phrase.from(context, R.string.disappearingMessagesFollowSettingOn)
R.string.dialog_disappearing_messages_follow_setting_on_body, .put(TIME_LARGE_KEY, ExpirationUtil.getExpirationDisplayValue(context, message.expiresIn.milliseconds))
ExpirationUtil.getExpirationDisplayValue( .put(DISAPPEARING_MESSAGES_TYPE_KEY, context.getExpirationTypeDisplayValue(message.isNotDisappearAfterRead))
context, .format().toString()
message.expiresIn.milliseconds
),
context.getExpirationTypeDisplayValue(message.isNotDisappearAfterRead)
)
}) })
dangerButton( dangerButton(
text = if (message.expiresIn == 0L) R.string.dialog_disappearing_messages_follow_setting_confirm else R.string.dialog_disappearing_messages_follow_setting_set, text = if (message.expiresIn == 0L) R.string.dialog_disappearing_messages_follow_setting_confirm else R.string.set,
contentDescription = if (message.expiresIn == 0L) R.string.AccessibilityId_confirm else R.string.AccessibilityId_set_button contentDescription = if (message.expiresIn == 0L) R.string.AccessibilityId_confirm else R.string.AccessibilityId_set_button
) { ) {
set(message.threadId, message.recipient.address, message.expiryMode, message.recipient.isClosedGroupRecipient) set(message.threadId, message.recipient.address, message.expiryMode, message.recipient.isClosedGroupRecipient)

View File

@ -72,7 +72,7 @@ class DisappearingMessagesActivity: PassphraseRequiredActionBarActivity() {
} }
private fun setUpToolbar() { private fun setUpToolbar() {
setSupportActionBar(binding.toolbar) setSupportActionBar(binding.searchToolbar)
supportActionBar?.apply { supportActionBar?.apply {
title = getString(R.string.activity_disappearing_messages_title) title = getString(R.string.activity_disappearing_messages_title)
setDisplayHomeAsUpEnabled(true) setDisplayHomeAsUpEnabled(true)

View File

@ -51,7 +51,7 @@ enum class ExpiryType(
) { ) {
NONE( NONE(
{ ExpiryMode.NONE }, { ExpiryMode.NONE },
R.string.expiration_off, R.string.off,
contentDescription = R.string.AccessibilityId_disable_disappearing_messages, contentDescription = R.string.AccessibilityId_disable_disappearing_messages,
), ),
LEGACY( LEGACY(
@ -61,14 +61,14 @@ enum class ExpiryType(
), ),
AFTER_READ( AFTER_READ(
ExpiryMode::AfterRead, ExpiryMode::AfterRead,
R.string.expiration_type_disappear_after_read, R.string.disappearingMessagesDisappearAfterRead,
R.string.expiration_type_disappear_after_read_description, R.string.disappearingMessagesDisappearAfterReadDescription,
R.string.AccessibilityId_disappear_after_read_option R.string.AccessibilityId_disappear_after_read_option
), ),
AFTER_SEND( AFTER_SEND(
ExpiryMode::AfterSend, ExpiryMode::AfterSend,
R.string.expiration_type_disappear_after_send, R.string.disappearingMessagesDisappearAfterSend,
R.string.expiration_type_disappear_after_send_description, R.string.disappearingMessagesDisappearAfterSendDescription,
R.string.AccessibilityId_disappear_after_send_option R.string.AccessibilityId_disappear_after_send_option
); );

View File

@ -13,7 +13,7 @@ import kotlin.time.Duration.Companion.seconds
fun State.toUiState() = UiState( fun State.toUiState() = UiState(
cards = listOfNotNull( cards = listOfNotNull(
typeOptions()?.let { ExpiryOptionsCardData(GetString(R.string.activity_disappearing_messages_delete_type), it) }, typeOptions()?.let { ExpiryOptionsCardData(GetString(R.string.disappearingMessagesDeleteType), it) },
timeOptions()?.let { ExpiryOptionsCardData(GetString(R.string.activity_disappearing_messages_timer), it) } timeOptions()?.let { ExpiryOptionsCardData(GetString(R.string.activity_disappearing_messages_timer), it) }
), ),
showGroupFooter = isGroup && isNewConfigEnabled, showGroupFooter = isGroup && isNewConfigEnabled,
@ -66,11 +66,14 @@ private fun State.typeOption(
) )
private fun debugTimes(isDebug: Boolean) = if (isDebug) listOf(10.seconds, 30.seconds, 1.minutes) else emptyList() private fun debugTimes(isDebug: Boolean) = if (isDebug) listOf(10.seconds, 30.seconds, 1.minutes) else emptyList()
private fun debugModes(isDebug: Boolean, type: ExpiryType) = private fun debugModes(isDebug: Boolean, type: ExpiryType) =
debugTimes(isDebug).map { type.mode(it.inWholeSeconds) } debugTimes(isDebug).map { type.mode(it.inWholeSeconds) }
private fun State.debugOptions(): List<ExpiryRadioOption> = private fun State.debugOptions(): List<ExpiryRadioOption> =
debugModes(showDebugOptions, nextType).map { timeOption(it, subtitle = GetString("for testing purposes")) } 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 afterSendTimes = listOf(12.hours, 1.days, 7.days, 14.days)
private val afterReadTimes = buildList { private val afterReadTimes = buildList {

View File

@ -1,6 +1,5 @@
package org.thoughtcrime.securesms.conversation.disappearingmessages.ui package org.thoughtcrime.securesms.conversation.disappearingmessages.ui
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
@ -16,21 +15,20 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import network.loki.messenger.R import network.loki.messenger.R
import network.loki.messenger.libsession_util.util.ExpiryMode import network.loki.messenger.libsession_util.util.ExpiryMode
import org.thoughtcrime.securesms.ui.Callbacks import org.thoughtcrime.securesms.ui.Callbacks
import org.thoughtcrime.securesms.ui.theme.LocalDimensions
import org.thoughtcrime.securesms.ui.NoOpCallbacks import org.thoughtcrime.securesms.ui.NoOpCallbacks
import org.thoughtcrime.securesms.ui.OptionsCard import org.thoughtcrime.securesms.ui.OptionsCard
import org.thoughtcrime.securesms.ui.RadioOption import org.thoughtcrime.securesms.ui.RadioOption
import org.thoughtcrime.securesms.ui.theme.LocalColors
import org.thoughtcrime.securesms.ui.components.SlimOutlineButton import org.thoughtcrime.securesms.ui.components.SlimOutlineButton
import org.thoughtcrime.securesms.ui.contentDescription import org.thoughtcrime.securesms.ui.contentDescription
import org.thoughtcrime.securesms.ui.fadingEdges 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 import org.thoughtcrime.securesms.ui.theme.LocalType
typealias ExpiryCallbacks = Callbacks<ExpiryMode> typealias ExpiryCallbacks = Callbacks<ExpiryMode>
typealias ExpiryRadioOption = RadioOption<ExpiryMode> typealias ExpiryRadioOption = RadioOption<ExpiryMode>
@Composable @Composable
@ -59,7 +57,9 @@ fun DisappearingMessages(
} }
if (state.showGroupFooter) Text( 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, style = LocalType.current.extraSmall,
fontWeight = FontWeight(400), fontWeight = FontWeight(400),
color = LocalColors.current.textSecondary, color = LocalColors.current.textSecondary,
@ -72,7 +72,7 @@ fun DisappearingMessages(
} }
if (state.showSetButton) SlimOutlineButton( if (state.showSetButton) SlimOutlineButton(
stringResource(R.string.disappearing_messages_set_button_title), stringResource(R.string.set),
modifier = Modifier modifier = Modifier
.contentDescription(R.string.AccessibilityId_set_button) .contentDescription(R.string.AccessibilityId_set_button)
.align(Alignment.CenterHorizontally) .align(Alignment.CenterHorizontally)

View File

@ -41,7 +41,7 @@ internal fun StartConversationScreen(
LocalColors.current.backgroundSecondary, LocalColors.current.backgroundSecondary,
shape = MaterialTheme.shapes.small shape = MaterialTheme.shapes.small
)) { )) {
AppBar(stringResource(R.string.dialog_start_conversation_title), onClose = delegate::onDialogClosePressed) AppBar(stringResource(R.string.conversationsStart), onClose = delegate::onDialogClosePressed)
Surface( Surface(
modifier = Modifier.nestedScroll(rememberNestedScrollInteropConnection()), modifier = Modifier.nestedScroll(rememberNestedScrollInteropConnection()),
color = LocalColors.current.backgroundSecondary color = LocalColors.current.backgroundSecondary
@ -56,21 +56,21 @@ internal fun StartConversationScreen(
onClick = delegate::onNewMessageSelected) onClick = delegate::onNewMessageSelected)
Divider(startIndent = LocalDimensions.current.dividerIndent) Divider(startIndent = LocalDimensions.current.dividerIndent)
ItemButton( ItemButton(
textId = R.string.activity_create_group_title, textId = R.string.groupCreate,
icon = R.drawable.ic_group, icon = R.drawable.ic_group,
modifier = Modifier.contentDescription(R.string.AccessibilityId_create_group), modifier = Modifier.contentDescription(R.string.AccessibilityId_create_group),
onClick = delegate::onCreateGroupSelected onClick = delegate::onCreateGroupSelected
) )
Divider(startIndent = LocalDimensions.current.dividerIndent) Divider(startIndent = LocalDimensions.current.dividerIndent)
ItemButton( ItemButton(
textId = R.string.dialog_join_community_title, textId = R.string.communityJoin,
icon = R.drawable.ic_globe, icon = R.drawable.ic_globe,
modifier = Modifier.contentDescription(R.string.AccessibilityId_join_community), modifier = Modifier.contentDescription(R.string.AccessibilityId_join_community),
onClick = delegate::onJoinCommunitySelected onClick = delegate::onJoinCommunitySelected
) )
Divider(startIndent = LocalDimensions.current.dividerIndent) Divider(startIndent = LocalDimensions.current.dividerIndent)
ItemButton( ItemButton(
textId = R.string.activity_settings_invite_button_title, textId = R.string.sessionInviteAFriend,
icon = R.drawable.ic_invite_friend, icon = R.drawable.ic_invite_friend,
Modifier.contentDescription(R.string.AccessibilityId_invite_friend_button), Modifier.contentDescription(R.string.AccessibilityId_invite_friend_button),
onClick = delegate::onInviteFriend onClick = delegate::onInviteFriend

View File

@ -16,15 +16,15 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import network.loki.messenger.R import network.loki.messenger.R
import org.thoughtcrime.securesms.ui.theme.LocalDimensions
import org.thoughtcrime.securesms.ui.theme.PreviewTheme
import org.thoughtcrime.securesms.ui.theme.LocalColors
import org.thoughtcrime.securesms.ui.components.AppBar import org.thoughtcrime.securesms.ui.components.AppBar
import org.thoughtcrime.securesms.ui.components.SlimOutlineButton import org.thoughtcrime.securesms.ui.components.SlimOutlineButton
import org.thoughtcrime.securesms.ui.components.SlimOutlineCopyButton import org.thoughtcrime.securesms.ui.components.SlimOutlineCopyButton
import org.thoughtcrime.securesms.ui.components.border import org.thoughtcrime.securesms.ui.components.border
import org.thoughtcrime.securesms.ui.contentDescription import org.thoughtcrime.securesms.ui.contentDescription
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.LocalType
import org.thoughtcrime.securesms.ui.theme.PreviewTheme
@Composable @Composable
internal fun InviteFriend( internal fun InviteFriend(
@ -38,7 +38,11 @@ internal fun InviteFriend(
LocalColors.current.backgroundSecondary, LocalColors.current.backgroundSecondary,
shape = MaterialTheme.shapes.small shape = MaterialTheme.shapes.small
)) { )) {
AppBar(stringResource(R.string.invite_a_friend), onBack = onBack, onClose = onClose) AppBar(
stringResource(R.string.sessionInviteAFriend),
onBack = onBack,
onClose = onClose
)
Column( Column(
modifier = Modifier.padding(horizontal = LocalDimensions.current.spacing) modifier = Modifier.padding(horizontal = LocalDimensions.current.spacing)
.padding(top = LocalDimensions.current.spacing), .padding(top = LocalDimensions.current.spacing),
@ -57,7 +61,7 @@ internal fun InviteFriend(
Spacer(modifier = Modifier.height(LocalDimensions.current.xsSpacing)) Spacer(modifier = Modifier.height(LocalDimensions.current.xsSpacing))
Text( Text(
stringResource(R.string.invite_your_friend_to_chat_with_you_on_session_by_sharing_your_account_id_with_them), stringResource(R.string.shareAccountIdDescription),
textAlign = TextAlign.Center, textAlign = TextAlign.Center,
style = LocalType.current.small, style = LocalType.current.small,
color = LocalColors.current.textSecondary, color = LocalColors.current.textSecondary,

View File

@ -41,7 +41,7 @@ import org.thoughtcrime.securesms.ui.components.SessionTabRow
import org.thoughtcrime.securesms.ui.contentDescription import org.thoughtcrime.securesms.ui.contentDescription
import org.thoughtcrime.securesms.ui.theme.LocalType import org.thoughtcrime.securesms.ui.theme.LocalType
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) @OptIn(ExperimentalFoundationApi::class)
@Composable @Composable
@ -63,7 +63,7 @@ internal fun NewMessage(
SessionTabRow(pagerState, TITLES) SessionTabRow(pagerState, TITLES)
HorizontalPager(pagerState) { HorizontalPager(pagerState) {
when (TITLES[it]) { when (TITLES[it]) {
R.string.enter_account_id -> EnterAccountId(state, callbacks, onHelp) R.string.accountIdEnter -> EnterAccountId(state, callbacks, onHelp)
R.string.qrScan -> MaybeScanQrCode(qrErrors, onScan = callbacks::onScanQrCode) R.string.qrScan -> MaybeScanQrCode(qrErrors, onScan = callbacks::onScanQrCode)
} }
} }
@ -101,7 +101,7 @@ private fun EnterAccountId(
Spacer(modifier = Modifier.height(LocalDimensions.current.xxxsSpacing)) Spacer(modifier = Modifier.height(LocalDimensions.current.xxxsSpacing))
BorderlessButtonWithIcon( BorderlessButtonWithIcon(
text = stringResource(R.string.messageNewDescription), text = stringResource(R.string.messageNewDescriptionMobile),
modifier = Modifier modifier = Modifier
.contentDescription(R.string.AccessibilityId_help_desk_link) .contentDescription(R.string.AccessibilityId_help_desk_link)
.padding(horizontal = LocalDimensions.current.mediumSpacing) .padding(horizontal = LocalDimensions.current.mediumSpacing)

View File

@ -4,6 +4,8 @@ import android.app.Application
import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import java.util.concurrent.TimeoutException
import javax.inject.Inject
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.channels.BufferOverflow 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.PublicKeyValidation
import org.session.libsignal.utilities.timeout import org.session.libsignal.utilities.timeout
import org.thoughtcrime.securesms.ui.GetString import org.thoughtcrime.securesms.ui.GetString
import java.util.concurrent.TimeoutException
import javax.inject.Inject
@HiltViewModel @HiltViewModel
internal class NewMessageViewModel @Inject constructor( internal class NewMessageViewModel @Inject constructor(
@ -59,7 +59,7 @@ internal class NewMessageViewModel @Inject constructor(
if (PublicKeyValidation.isValid(value, isPrefixRequired = false) && PublicKeyValidation.hasValidPrefix(value)) { if (PublicKeyValidation.isValid(value, isPrefixRequired = false) && PublicKeyValidation.hasValidPrefix(value)) {
onPublicKey(value) onPublicKey(value)
} else { } else {
_qrErrors.tryEmit(application.getString(R.string.this_qr_code_does_not_contain_an_account_id)) _qrErrors.tryEmit(application.getString(R.string.qrNotAccountId))
} }
} }
@ -99,7 +99,7 @@ internal class NewMessageViewModel @Inject constructor(
private fun Exception.toMessage() = when (this) { private fun Exception.toMessage() = when (this) {
is SnodeAPI.Error.Generic -> application.getString(R.string.onsErrorNotRecognized) is SnodeAPI.Error.Generic -> application.getString(R.string.onsErrorNotRecognized)
is TimeoutException -> application.getString(R.string.onsErrorUnableToSearch) is TimeoutException -> application.getString(R.string.onsErrorUnableToSearch)
else -> application.getString(R.string.fragment_enter_public_key_error_message) else -> application.getString(R.string.accountIdErrorInvalid)
} }
} }

View File

@ -18,8 +18,8 @@ import android.os.Bundle
import android.os.Handler import android.os.Handler
import android.os.Looper import android.os.Looper
import android.provider.MediaStore import android.provider.MediaStore
import android.text.SpannableStringBuilder import android.text.SpannableString
import android.text.SpannedString import android.text.Spanned
import android.text.TextUtils import android.text.TextUtils
import android.text.style.StyleSpan import android.text.style.StyleSpan
import android.util.Pair import android.util.Pair
@ -35,9 +35,9 @@ import android.widget.Toast
import androidx.activity.result.ActivityResult import androidx.activity.result.ActivityResult
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
import androidx.activity.viewModels import androidx.activity.viewModels
import androidx.core.view.drawToBitmap
import androidx.core.text.set import androidx.core.text.set
import androidx.core.text.toSpannable import androidx.core.text.toSpannable
import androidx.core.view.drawToBitmap
import androidx.core.view.isGone import androidx.core.view.isGone
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.fragment.app.DialogFragment import androidx.fragment.app.DialogFragment
@ -52,7 +52,19 @@ import androidx.loader.content.Loader
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import com.annimon.stream.Stream import com.annimon.stream.Stream
import com.squareup.phrase.Phrase
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import java.lang.ref.WeakReference
import java.util.Locale
import java.util.concurrent.ExecutionException
import java.util.concurrent.atomic.AtomicBoolean
import java.util.concurrent.atomic.AtomicLong
import java.util.concurrent.atomic.AtomicReference
import javax.inject.Inject
import kotlin.math.abs
import kotlin.math.min
import kotlin.math.roundToInt
import kotlin.math.sqrt
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.channels.BufferOverflow import kotlinx.coroutines.channels.BufferOverflow
import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.channels.Channel
@ -65,7 +77,6 @@ import network.loki.messenger.databinding.ActivityConversationV2Binding
import network.loki.messenger.libsession_util.util.ExpiryMode import network.loki.messenger.libsession_util.util.ExpiryMode
import nl.komponents.kovenant.ui.successUi import nl.komponents.kovenant.ui.successUi
import org.session.libsession.messaging.MessagingModuleConfiguration 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.ExpirationConfiguration
import org.session.libsession.messaging.messages.applyExpiryMode import org.session.libsession.messaging.messages.applyExpiryMode
import org.session.libsession.messaging.messages.control.DataExtractionNotification import org.session.libsession.messaging.messages.control.DataExtractionNotification
@ -85,6 +96,10 @@ import org.session.libsession.utilities.Address
import org.session.libsession.utilities.Address.Companion.fromSerialized import org.session.libsession.utilities.Address.Companion.fromSerialized
import org.session.libsession.utilities.GroupUtil import org.session.libsession.utilities.GroupUtil
import org.session.libsession.utilities.MediaTypes 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.Stub
import org.session.libsession.utilities.TextSecurePreferences import org.session.libsession.utilities.TextSecurePreferences
import org.session.libsession.utilities.concurrent.SimpleTask import org.session.libsession.utilities.concurrent.SimpleTask
@ -156,7 +171,7 @@ import org.thoughtcrime.securesms.mediasend.Media
import org.thoughtcrime.securesms.mediasend.MediaSendActivity import org.thoughtcrime.securesms.mediasend.MediaSendActivity
import org.thoughtcrime.securesms.mms.AudioSlide import org.thoughtcrime.securesms.mms.AudioSlide
import org.thoughtcrime.securesms.mms.GifSlide import org.thoughtcrime.securesms.mms.GifSlide
import org.thoughtcrime.securesms.mms.GlideApp import com.bumptech.glide.Glide
import org.thoughtcrime.securesms.mms.ImageSlide import org.thoughtcrime.securesms.mms.ImageSlide
import org.thoughtcrime.securesms.mms.MediaConstraints import org.thoughtcrime.securesms.mms.MediaConstraints
import org.thoughtcrime.securesms.mms.Slide import org.thoughtcrime.securesms.mms.Slide
@ -173,23 +188,13 @@ import org.thoughtcrime.securesms.util.DateUtils
import org.thoughtcrime.securesms.util.MediaUtil import org.thoughtcrime.securesms.util.MediaUtil
import org.thoughtcrime.securesms.util.NetworkUtils import org.thoughtcrime.securesms.util.NetworkUtils
import org.thoughtcrime.securesms.util.SaveAttachmentTask import org.thoughtcrime.securesms.util.SaveAttachmentTask
import org.thoughtcrime.securesms.util.drawToBitmap
import org.thoughtcrime.securesms.util.isScrolledToBottom import org.thoughtcrime.securesms.util.isScrolledToBottom
import org.thoughtcrime.securesms.util.isScrolledToWithin30dpOfBottom import org.thoughtcrime.securesms.util.isScrolledToWithin30dpOfBottom
import org.thoughtcrime.securesms.util.push import org.thoughtcrime.securesms.util.push
import org.thoughtcrime.securesms.util.show import org.thoughtcrime.securesms.util.show
import org.thoughtcrime.securesms.util.start import org.thoughtcrime.securesms.util.start
import org.thoughtcrime.securesms.util.toPx import org.thoughtcrime.securesms.util.toPx
import java.lang.ref.WeakReference
import java.util.Locale
import java.util.concurrent.ExecutionException
import java.util.concurrent.atomic.AtomicBoolean
import java.util.concurrent.atomic.AtomicLong
import java.util.concurrent.atomic.AtomicReference
import javax.inject.Inject
import kotlin.math.abs
import kotlin.math.min
import kotlin.math.roundToInt
import kotlin.math.sqrt
private const val TAG = "ConversationActivityV2" private const val TAG = "ConversationActivityV2"
@ -349,7 +354,7 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
adapter adapter
} }
private val glide by lazy { GlideApp.with(this) } private val glide by lazy { Glide.with(this) }
private val lockViewHitMargin by lazy { toPx(40, resources) } private val lockViewHitMargin by lazy { toPx(40, resources) }
private val gifButton by lazy { InputBarButton(this, R.drawable.ic_gif_white_24dp, hasOpaqueBackground = true, isGIFButton = true) } private val gifButton by lazy { InputBarButton(this, R.drawable.ic_gif_white_24dp, hasOpaqueBackground = true, isGIFButton = true) }
private val documentButton by lazy { InputBarButton(this, R.drawable.ic_document_small_dark, hasOpaqueBackground = true) } private val documentButton by lazy { InputBarButton(this, R.drawable.ic_document_small_dark, hasOpaqueBackground = true) }
@ -706,7 +711,7 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
} }
override fun onFailure(e: ExecutionException?) { 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 return
@ -757,9 +762,7 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
// called from onCreate // called from onCreate
private fun setUpBlockedBanner() { private fun setUpBlockedBanner() {
val recipient = viewModel.recipient?.takeUnless { it.isGroupRecipient } ?: return val recipient = viewModel.recipient?.takeUnless { it.isGroupRecipient } ?: return
val accountID = recipient.address.toString() binding.blockedBannerTextView.text = applicationContext.getString(R.string.blockBlockedDescription)
val name = sessionContactDb.getContactWithAccountID(accountID)?.displayName(Contact.ContactContext.REGULAR) ?: accountID
binding.blockedBannerTextView.text = resources.getString(R.string.activity_conversation_blocked_banner_text, name)
binding.blockedBanner.isVisible = recipient.isBlocked binding.blockedBanner.isVisible = recipient.isBlocked
binding.blockedBanner.setOnClickListener { viewModel.unblock() } binding.blockedBanner.setOnClickListener { viewModel.unblock() }
} }
@ -772,8 +775,11 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
binding.outdatedBanner.isVisible = shouldShowLegacy binding.outdatedBanner.isVisible = shouldShowLegacy
if (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
} }
} }
@ -1056,33 +1062,64 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
updateUnreadCountIndicator() updateUnreadCountIndicator()
} }
// Method that takes a char sequence that contains one or more elements surrounded in bold tags
// like "Hello <b>world</b>" and returns a SpannableString that will display the appropriate
// elements in bold. If there are no such <b> or </b> elements then the original string is returned
// as a SpannableString without any bold highlighting.
private fun makeBoldBetweenTags(input: CharSequence): SpannableString {
val spannable = SpannableString(input)
var startIndex = 0
while (true) {
startIndex = input.indexOf("<b>", startIndex)
if (startIndex == -1) break
val endIndex = input.indexOf("</b>", startIndex + 3)
if (endIndex == -1) break
spannable.setSpan(StyleSpan(Typeface.BOLD),startIndex + 3, endIndex, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
startIndex = endIndex + 4
}
return spannable
}
// Update placeholder / control messages in a conversation
private fun updatePlaceholder() { private fun updatePlaceholder() {
val recipient = viewModel.recipient ?: return Log.w("Loki", "recipient was null in placeholder update") val recipient = viewModel.recipient ?: return Log.w("Loki", "recipient was null in placeholder update")
val blindedRecipient = viewModel.blindedRecipient val blindedRecipient = viewModel.blindedRecipient
val openGroup = viewModel.openGroup val openGroup = viewModel.openGroup
val (textResource, insertParam) = when { // Get the correct placeholder text for this type of empty conversation
recipient.isLocalNumber -> R.string.activity_conversation_empty_state_note_to_self to null val isNoteToSelf = recipient.isLocalNumber
openGroup != null && !openGroup.canWrite -> R.string.activity_conversation_empty_state_read_only to recipient.toShortString() val txtCS: CharSequence = when {
blindedRecipient?.blocksCommunityMessageRequests == true -> R.string.activity_conversation_empty_state_blocks_community_requests to recipient.toShortString() recipient.isLocalNumber -> getString(R.string.noteToSelfEmpty)
else -> R.string.activity_conversation_empty_state_default to recipient.toShortString()
// 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()
}
else -> {
// 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()
}
} }
val showPlaceholder = adapter.itemCount == 0 val showPlaceholder = adapter.itemCount == 0
binding.placeholderText.isVisible = showPlaceholder binding.placeholderText.isVisible = showPlaceholder
if (showPlaceholder) { if (showPlaceholder) {
if (insertParam != null) { if (!isNoteToSelf) {
val span = getText(textResource) as SpannedString binding.placeholderText.text = makeBoldBetweenTags(txtCS)
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 { } else {
binding.placeholderText.setText(textResource) binding.placeholderText.text = txtCS
} }
} }
} }
@ -1117,11 +1154,21 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
} }
override fun block(deleteThread: Boolean) { override fun block(deleteThread: Boolean) {
val recipient = viewModel.recipient ?: return Log.w("Loki", "Recipient was null for block action")
showSessionDialog { showSessionDialog {
title(R.string.RecipientPreferenceActivity_block_this_contact_question) title(R.string.block)
text(R.string.RecipientPreferenceActivity_you_will_no_longer_receive_messages_and_calls_from_this_contact) text(
dangerButton(R.string.RecipientPreferenceActivity_block, R.string.AccessibilityId_block_confirm) { Phrase.from(context, R.string.blockDescription)
.put(NAME_KEY, recipient.name)
.format()
)
dangerButton(R.string.block, R.string.AccessibilityId_block_confirm) {
viewModel.block() 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) { if (deleteThread) {
viewModel.deleteThread() viewModel.deleteThread()
finish() finish()
@ -1135,7 +1182,7 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
val clip = ClipData.newPlainText("Account ID", accountId) val clip = ClipData.newPlainText("Account ID", accountId)
val manager = getSystemService(PassphraseRequiredActionBarActivity.CLIPBOARD_SERVICE) as ClipboardManager val manager = getSystemService(PassphraseRequiredActionBarActivity.CLIPBOARD_SERVICE) as ClipboardManager
manager.setPrimaryClip(clip) 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) { override fun copyOpenGroupUrl(thread: Recipient) {
@ -1147,7 +1194,7 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
val clip = ClipData.newPlainText("Community URL", openGroup.joinURL) val clip = ClipData.newPlainText("Community URL", openGroup.joinURL)
val manager = getSystemService(PassphraseRequiredActionBarActivity.CLIPBOARD_SERVICE) as ClipboardManager val manager = getSystemService(PassphraseRequiredActionBarActivity.CLIPBOARD_SERVICE) as ClipboardManager
manager.setPrimaryClip(clip) 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) { override fun showDisappearingMessages(thread: Recipient) {
@ -1160,13 +1207,31 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
} }
override fun unblock() { 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 { showSessionDialog {
title(R.string.ConversationActivity_unblock_this_contact_question) title(R.string.blockUnblock)
text(R.string.ConversationActivity_you_will_once_again_be_able_to_receive_messages_and_calls_from_this_contact) text(
Phrase.from(context, R.string.blockUnblockName)
.put(NAME_KEY, recipient.name)
.format()
)
dangerButton( dangerButton(
R.string.ConversationActivity_unblock, R.string.blockUnblock,
R.string.AccessibilityId_block_confirm R.string.AccessibilityId_unblock_confirm
) { viewModel.unblock() } ) {
viewModel.unblock()
// Unblock confirmation toast added as per SS-64
val txt = Phrase.from(context, R.string.blockUnblockedUser)
.put(NAME_KEY, recipient.name).
format().toString()
Toast.makeText(context, txt, Toast.LENGTH_LONG).show()
}
cancelButton() cancelButton()
} }
} }
@ -1666,9 +1731,9 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
val hasSeenGIFMetaDataWarning: Boolean = textSecurePreferences.hasSeenGIFMetaDataWarning() val hasSeenGIFMetaDataWarning: Boolean = textSecurePreferences.hasSeenGIFMetaDataWarning()
if (!hasSeenGIFMetaDataWarning) { if (!hasSeenGIFMetaDataWarning) {
showSessionDialog { showSessionDialog {
title(R.string.giphy_permission_title) title(R.string.giphyWarning)
text(R.string.giphy_permission_message) text(Phrase.from(context, R.string.giphyWarningDescription).put(APP_NAME_KEY, getString(R.string.app_name)).format())
button(R.string.continue_2) { button(R.string.theContinue) {
textSecurePreferences.setHasSeenGIFMetaDataWarning() textSecurePreferences.setHasSeenGIFMetaDataWarning()
selectGif() selectGif()
} }
@ -1715,7 +1780,7 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
} }
override fun onFailure(e: ExecutionException?) { 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) { when (requestCode) {
@ -1798,8 +1863,10 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
} else { } else {
Permissions.with(this) Permissions.with(this)
.request(Manifest.permission.RECORD_AUDIO) .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) .withRationaleDialog(getString(R.string.permissionsMicrophoneAccessRequired), 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.permissionsMicrophoneAccessRequiredAndroid)
.put(APP_NAME_KEY, getString(R.string.app_name))
.format().toString())
.execute() .execute()
} }
} }
@ -1869,7 +1936,7 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
} }
override fun onFailure(e: ExecutionException) { 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()
} }
}) })
} }
@ -1920,10 +1987,10 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
} }
private fun showDeleteLocallyUI(messages: Set<MessageRecord>) { private fun showDeleteLocallyUI(messages: Set<MessageRecord>) {
val messageCount = 1 val titleStringId = if (messages.count() == 1) R.string.deleteMessage else R.string.deleteMessages
showSessionDialog { showSessionDialog {
title(resources.getQuantityString(R.plurals.ConversationFragment_delete_selected_messages, messageCount, messageCount)) title(resources.getString(titleStringId))
text(resources.getQuantityString(R.plurals.ConversationFragment_this_will_permanently_delete_all_n_selected_messages, messageCount, messageCount)) text(resources.getString(R.string.deleteMessagesDescriptionDevice))
button(R.string.delete) { messages.forEach(viewModel::deleteLocally); endActionMode() } button(R.string.delete) { messages.forEach(viewModel::deleteLocally); endActionMode() }
cancelButton(::endActionMode) cancelButton(::endActionMode)
} }
@ -1943,13 +2010,10 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
// If the recipient is a community OR a Note-to-Self then we delete the message for everyone // If the recipient is a community OR a Note-to-Self then we delete the message for everyone
if (recipient.isCommunityRecipient || recipient.isLocalNumber) { if (recipient.isCommunityRecipient || recipient.isLocalNumber) {
val messageCount = 1 // Only used for plurals string
showSessionDialog { showSessionDialog {
title(resources.getQuantityString(R.plurals.ConversationFragment_delete_selected_messages, messageCount, messageCount)) title(resources.getString(R.string.deleteMessage))
text(resources.getQuantityString(R.plurals.ConversationFragment_this_will_permanently_delete_all_n_selected_messages, messageCount, messageCount)) text(resources.getString(R.string.deleteMessageDescriptionEveryone))
button(R.string.delete) { button(R.string.delete) { messages.forEach(viewModel::deleteForEveryone); endActionMode() }
messages.forEach(viewModel::deleteForEveryone); endActionMode()
}
cancelButton { endActionMode() } cancelButton { endActionMode() }
} }
// Otherwise if this is a 1-on-1 conversation we may decided to delete just for ourselves or delete for everyone // Otherwise if this is a 1-on-1 conversation we may decided to delete just for ourselves or delete for everyone
@ -1974,13 +2038,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. 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 { showSessionDialog {
title(resources.getQuantityString(R.plurals.ConversationFragment_delete_selected_messages, messageCount, messageCount)) title(resources.getString(R.string.deleteMessage))
text(resources.getQuantityString(R.plurals.ConversationFragment_this_will_permanently_delete_all_n_selected_messages, messageCount, messageCount)) text(resources.getString(R.string.deleteMessageDescriptionEveryone))
button(R.string.delete) { button(R.string.delete) { messages.forEach(viewModel::deleteLocally); endActionMode() }
messages.forEach(viewModel::deleteLocally); endActionMode()
}
cancelButton(::endActionMode) cancelButton(::endActionMode)
} }
} }
@ -1988,18 +2049,20 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
override fun banUser(messages: Set<MessageRecord>) { override fun banUser(messages: Set<MessageRecord>) {
showSessionDialog { showSessionDialog {
title(R.string.ConversationFragment_ban_selected_user) title(R.string.banUser)
// ACL TODO - We need a string for the below `text` element
text("This will ban the selected user from this room. It won't ban them from other rooms.") 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() } button(R.string.banUser) { viewModel.banUser(messages.first().individualRecipient); endActionMode() }
cancelButton(::endActionMode) cancelButton(::endActionMode)
} }
} }
override fun banAndDeleteAll(messages: Set<MessageRecord>) { override fun banAndDeleteAll(messages: Set<MessageRecord>) {
showSessionDialog { showSessionDialog {
title(R.string.ConversationFragment_ban_selected_user) title(R.string.banUser)
// ACL TODO - We need a string for the below `text` element
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.") 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() } button(R.string.banUser) { viewModel.banAndDeleteAll(messages.first()); endActionMode() }
cancelButton(::endActionMode) cancelButton(::endActionMode)
} }
} }
@ -2035,7 +2098,7 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
if (TextUtils.isEmpty(result)) { return } if (TextUtils.isEmpty(result)) { return }
val manager = getSystemService(CLIPBOARD_SERVICE) as ClipboardManager val manager = getSystemService(CLIPBOARD_SERVICE) as ClipboardManager
manager.setPrimaryClip(ClipData.newPlainText("Message Content", result)) 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() endActionMode()
} }
@ -2044,7 +2107,7 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
val clip = ClipData.newPlainText("Account ID", accountID) val clip = ClipData.newPlainText("Account ID", accountID)
val manager = getSystemService(CLIPBOARD_SERVICE) as ClipboardManager val manager = getSystemService(CLIPBOARD_SERVICE) as ClipboardManager
manager.setPrimaryClip(clip) 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() endActionMode()
} }
@ -2096,10 +2159,15 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
Permissions.with(this) Permissions.with(this)
.request(Manifest.permission.WRITE_EXTERNAL_STORAGE) .request(Manifest.permission.WRITE_EXTERNAL_STORAGE)
.maxSdkVersion(Build.VERSION_CODES.P) .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)) .withPermanentDenialDialog(Phrase.from(applicationContext, R.string.permissionsStorageSaveDenied)
.put(APP_NAME_KEY, getString(R.string.app_name))
.format().toString())
.onAnyDenied { .onAnyDenied {
endActionMode() endActionMode()
Toast.makeText(this@ConversationActivityV2, R.string.MediaPreviewActivity_unable_to_write_to_external_storage_without_permission, Toast.LENGTH_LONG).show() val txt = Phrase.from(applicationContext, R.string.permissionsStorageSaveDenied)
.put(APP_NAME_KEY, getString(R.string.app_name))
.format().toString()
Toast.makeText(this@ConversationActivityV2, txt, Toast.LENGTH_LONG).show()
} }
.onAllGranted { .onAllGranted {
endActionMode() endActionMode()
@ -2116,7 +2184,7 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
return@onAllGranted return@onAllGranted
} }
Toast.makeText(this, Toast.makeText(this,
resources.getQuantityString(R.plurals.ConversationFragment_error_while_saving_attachments_to_sd_card, 1), resources.getString(R.string.attachmentsSaveError),
Toast.LENGTH_LONG).show() Toast.LENGTH_LONG).show()
} }
.execute() .execute()
@ -2172,6 +2240,7 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
searchViewModel.onMissingResult() } searchViewModel.onMissingResult() }
} }
} }
binding.searchBottomBar.setData(result.position, result.getResults().size) binding.searchBottomBar.setData(result.position, result.getResults().size)
}) })
} }
@ -2181,6 +2250,7 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
binding.searchBottomBar.visibility = View.VISIBLE binding.searchBottomBar.visibility = View.VISIBLE
binding.searchBottomBar.setData(0, 0) binding.searchBottomBar.setData(0, 0)
binding.inputBar.visibility = View.INVISIBLE binding.inputBar.visibility = View.INVISIBLE
} }
fun onSearchClosed() { fun onSearchClosed() {

View File

@ -12,6 +12,9 @@ import androidx.core.util.getOrDefault
import androidx.core.util.set import androidx.core.util.set
import androidx.lifecycle.LifecycleCoroutineScope import androidx.lifecycle.LifecycleCoroutineScope
import androidx.recyclerview.widget.RecyclerView.ViewHolder import androidx.recyclerview.widget.RecyclerView.ViewHolder
import com.squareup.phrase.Phrase
import java.util.concurrent.atomic.AtomicLong
import kotlin.math.min
import kotlinx.coroutines.Dispatchers.IO import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.channels.BufferOverflow import kotlinx.coroutines.channels.BufferOverflow
import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.channels.Channel
@ -20,17 +23,16 @@ import kotlinx.coroutines.launch
import network.loki.messenger.R import network.loki.messenger.R
import org.session.libsession.messaging.contacts.Contact import org.session.libsession.messaging.contacts.Contact
import org.session.libsession.messaging.sending_receiving.attachments.DatabaseAttachment import org.session.libsession.messaging.sending_receiving.attachments.DatabaseAttachment
import org.session.libsession.utilities.StringSubstitutionConstants.NAME_KEY
import org.thoughtcrime.securesms.conversation.v2.messages.ControlMessageView import org.thoughtcrime.securesms.conversation.v2.messages.ControlMessageView
import org.thoughtcrime.securesms.conversation.v2.messages.VisibleMessageView import org.thoughtcrime.securesms.conversation.v2.messages.VisibleMessageView
import org.thoughtcrime.securesms.conversation.v2.messages.VisibleMessageViewDelegate import org.thoughtcrime.securesms.conversation.v2.messages.VisibleMessageViewDelegate
import org.thoughtcrime.securesms.database.CursorRecyclerViewAdapter import org.thoughtcrime.securesms.database.CursorRecyclerViewAdapter
import org.thoughtcrime.securesms.database.model.MessageRecord import org.thoughtcrime.securesms.database.model.MessageRecord
import org.thoughtcrime.securesms.dependencies.DatabaseComponent import org.thoughtcrime.securesms.dependencies.DatabaseComponent
import org.thoughtcrime.securesms.mms.GlideRequests import com.bumptech.glide.RequestManager
import org.thoughtcrime.securesms.preferences.PrivacySettingsActivity import org.thoughtcrime.securesms.preferences.PrivacySettingsActivity
import org.thoughtcrime.securesms.showSessionDialog import org.thoughtcrime.securesms.showSessionDialog
import java.util.concurrent.atomic.AtomicLong
import kotlin.math.min
class ConversationAdapter( class ConversationAdapter(
context: Context, context: Context,
@ -42,7 +44,7 @@ class ConversationAdapter(
private val onItemLongPress: (MessageRecord, Int, VisibleMessageView) -> Unit, private val onItemLongPress: (MessageRecord, Int, VisibleMessageView) -> Unit,
private val onDeselect: (MessageRecord, Int) -> Unit, private val onDeselect: (MessageRecord, Int) -> Unit,
private val onAttachmentNeedsDownload: (DatabaseAttachment) -> Unit, private val onAttachmentNeedsDownload: (DatabaseAttachment) -> Unit,
private val glide: GlideRequests, private val glide: RequestManager,
lifecycleCoroutineScope: LifecycleCoroutineScope lifecycleCoroutineScope: LifecycleCoroutineScope
) : CursorRecyclerViewAdapter<ViewHolder>(context, cursor) { ) : CursorRecyclerViewAdapter<ViewHolder>(context, cursor) {
private val messageDB by lazy { DatabaseComponent.get(context).mmsSmsDatabase() } private val messageDB by lazy { DatabaseComponent.get(context).mmsSmsDatabase() }
@ -154,9 +156,15 @@ class ConversationAdapter(
if (message.isCallLog && message.isFirstMissedCall) { if (message.isCallLog && message.isFirstMissedCall) {
viewHolder.view.setOnClickListener { viewHolder.view.setOnClickListener {
context.showSessionDialog { context.showSessionDialog {
title(R.string.CallNotificationBuilder_first_call_title) val titleTxt = Phrase.from(context, R.string.callsMissedCallFrom)
text(R.string.CallNotificationBuilder_first_call_message) .put(NAME_KEY, message.individualRecipient.name)
button(R.string.activity_settings_title) { .format().toString()
title(titleTxt)
val bodyTxt = Phrase.from(context, R.string.callsYouMissedCallPermissions)
.put(NAME_KEY, message.individualRecipient.name)
.format().toString()
text(bodyTxt)
button(R.string.sessionSettings) {
Intent(context, PrivacySettingsActivity::class.java) Intent(context, PrivacySettingsActivity::class.java)
.let(context::startActivity) .let(context::startActivity)
} }
@ -190,7 +198,7 @@ class ConversationAdapter(
private fun getMessageBefore(position: Int, cursor: Cursor): MessageRecord? { private fun getMessageBefore(position: Int, cursor: Cursor): MessageRecord? {
// The message that's visually before the current one is actually after the current // The message that's visually before the current one is actually after the current
// one for the cursor because the layout is reversed // 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 } if (!isReversed && !cursor.moveToPosition(position - 1)) { return null }
return messageDB.readerFor(cursor).current return messageDB.readerFor(cursor).current

View File

@ -22,7 +22,11 @@ import androidx.constraintlayout.widget.ConstraintLayout
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.core.view.doOnLayout import androidx.core.view.doOnLayout
import androidx.vectordrawable.graphics.drawable.AnimatorInflaterCompat import androidx.vectordrawable.graphics.drawable.AnimatorInflaterCompat
import com.squareup.phrase.Phrase
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import java.util.Locale
import javax.inject.Inject
import kotlin.time.Duration.Companion.milliseconds
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
@ -30,7 +34,9 @@ import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import network.loki.messenger.R import network.loki.messenger.R
import org.session.libsession.LocalisedTimeUtil.toShortTwoPartString
import org.session.libsession.snode.SnodeAPI 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.TextSecurePreferences.Companion.getLocalNumber
import org.session.libsession.utilities.ThemeUtil import org.session.libsession.utilities.ThemeUtil
import org.thoughtcrime.securesms.components.emoji.EmojiImageView import org.thoughtcrime.securesms.components.emoji.EmojiImageView
@ -46,14 +52,6 @@ import org.thoughtcrime.securesms.dependencies.DatabaseComponent.Companion.get
import org.thoughtcrime.securesms.repository.ConversationRepository import org.thoughtcrime.securesms.repository.ConversationRepository
import org.thoughtcrime.securesms.util.AnimationCompleteListener import org.thoughtcrime.securesms.util.AnimationCompleteListener
import org.thoughtcrime.securesms.util.DateUtils 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 @AndroidEntryPoint
class ConversationReactionOverlay : FrameLayout { class ConversationReactionOverlay : FrameLayout {
@ -529,11 +527,11 @@ class ConversationReactionOverlay : FrameLayout {
?: return emptyList() ?: return emptyList()
val userPublicKey = getLocalNumber(context)!! val userPublicKey = getLocalNumber(context)!!
// Select message // 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 // Reply
val canWrite = openGroup == null || openGroup.canWrite val canWrite = openGroup == null || openGroup.canWrite
if (canWrite && !message.isPending && !message.isFailed && !message.isOpenGroupInvitation) { 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_message)
} }
// Copy message text // Copy message text
if (!containsControlMessage && hasText) { if (!containsControlMessage && hasText) {
@ -541,7 +539,7 @@ class ConversationReactionOverlay : FrameLayout {
} }
// Copy Account ID // Copy Account ID
if (recipient.isGroupRecipient && !recipient.isCommunityRecipient && message.recipient.address.toString() != userPublicKey) { 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) }) items += ActionItem(R.attr.menu_copy_icon, R.string.accountIdCopy, { handleActionItemClicked(Action.COPY_ACCOUNT_ID) })
} }
// Delete message // Delete message
if (userCanDeleteSelectedItems(context, message, openGroup, userPublicKey, blindedPublicKey)) { if (userCanDeleteSelectedItems(context, message, openGroup, userPublicKey, blindedPublicKey)) {
@ -550,17 +548,17 @@ class ConversationReactionOverlay : FrameLayout {
} }
// Ban user // Ban user
if (userCanBanSelectedUsers(context, message, openGroup, userPublicKey, blindedPublicKey)) { 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 // Ban and delete all
if (userCanBanSelectedUsers(context, message, openGroup, userPublicKey, blindedPublicKey)) { 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 // 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 // Resend
if (message.isFailed) { 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 // Resync
if (message.isSyncFailed) { if (message.isSyncFailed) {
@ -704,10 +702,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?)? private val MessageRecord.subtitle: ((Context) -> CharSequence?)?
get() = if (expiresIn <= 0) { get() = if (expiresIn <= 0) {
null null
@ -715,6 +709,10 @@ private val MessageRecord.subtitle: ((Context) -> CharSequence?)?
(expiresIn - (SnodeAPI.nowWithOffset - (expireStarted.takeIf { it > 0 } ?: timestamp))) (expiresIn - (SnodeAPI.nowWithOffset - (expireStarted.takeIf { it > 0 } ?: timestamp)))
.coerceAtLeast(0L) .coerceAtLeast(0L)
.milliseconds .milliseconds
.to2partString() .toShortTwoPartString()
?.let { context.getString(R.string.auto_deletes_in, it) } .let {
Phrase.from(context, R.string.disappearingMessagesCountdownBigMobile)
.put(TIME_LARGE_KEY, it)
.format().toString()
}
} }

View File

@ -58,7 +58,7 @@ class DeleteOptionsBottomSheet : BottomSheetDialogFragment(), View.OnClickListen
} }
if (!recipient.isGroupRecipient && !contact.isNullOrEmpty()) { if (!recipient.isGroupRecipient && !contact.isNullOrEmpty()) {
binding.deleteForEveryoneTextView.text = 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.deleteForEveryoneTextView.isVisible = !recipient.isClosedGroupRecipient
binding.deleteForMeTextView.setOnClickListener(this) binding.deleteForMeTextView.setOnClickListener(this)

View File

@ -98,7 +98,7 @@ class MessageDetailActivity : PassphraseRequiredActionBarActivity() {
override fun onCreate(savedInstanceState: Bundle?, ready: Boolean) { override fun onCreate(savedInstanceState: Bundle?, ready: Boolean) {
super.onCreate(savedInstanceState, ready) 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) viewModel.timestamp = intent.getLongExtra(MESSAGE_TIMESTAMP, -1L)
@ -319,13 +319,13 @@ fun PreviewMessageDetails(
state = MessageDetailsState( state = MessageDetailsState(
nonImageAttachmentFileDetails = listOf( 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_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.attachmentsFileType, "image/png"),
TitledText(R.string.message_details_header__file_size, "195.6kB"), TitledText(R.string.attachmentsFileSize, "195.6kB"),
TitledText(R.string.message_details_header__resolution, "342x312"), TitledText(R.string.attachmentsResolution, "342x312"),
), ),
sent = TitledText(R.string.message_details_header__sent, "6:12 AM Tue, 09/08/2022"), sent = TitledText(R.string.sent, "6:12 AM Tue, 09/08/2022"),
received = TitledText(R.string.message_details_header__received, "6:12 AM Tue, 09/08/2022"), received = TitledText(R.string.received, "6:12 AM Tue, 09/08/2022"),
error = TitledText(R.string.message_details_header__error, "Message failed to send"), error = TitledText(R.string.error, "Message failed to send"),
senderInfo = TitledText("Connor", "d4f1g54sdf5g1d5f4g65ds4564df65f4g65d54"), senderInfo = TitledText("Connor", "d4f1g54sdf5g1d5f4g65ds4564df65f4g65d54"),
) )
) )

View File

@ -78,9 +78,9 @@ class MessageDetailsViewModel @Inject constructor(
MessageDetailsState( MessageDetailsState(
attachments = slides.map(::Attachment), attachments = slides.map(::Attachment),
record = record, record = record,
sent = dateSent.let(::Date).toString().let { TitledText(R.string.message_details_header__sent, it) }, sent = dateSent.let(::Date).toString().let { TitledText(R.string.sent, it) },
received = dateReceived.let(::Date).toString().let { TitledText(R.string.message_details_header__received, it) }, received = dateReceived.let(::Date).toString().let { TitledText(R.string.received, it) },
error = lokiMessageDatabase.getErrorMessage(id)?.let { TitledText(R.string.message_details_header__error, it) }, error = lokiMessageDatabase.getErrorMessage(id)?.let { TitledText(R.string.theError, it) },
senderInfo = individualRecipient.run { name?.let { TitledText(it, address.serialize()) } }, senderInfo = individualRecipient.run { name?.let { TitledText(it, address.serialize()) } },
sender = individualRecipient, sender = individualRecipient,
thread = threadDb.getRecipientForThreadId(threadId)!!, thread = threadDb.getRecipientForThreadId(threadId)!!,
@ -91,13 +91,13 @@ class MessageDetailsViewModel @Inject constructor(
private val Slide.details: List<TitledText> private val Slide.details: List<TitledText>
get() = listOfNotNull( get() = listOfNotNull(
fileName.orNull()?.let { TitledText(R.string.message_details_header__file_id, it) }, 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.attachmentsFileType, asAttachment().contentType),
TitledText(R.string.message_details_header__file_size, Util.getPrettyFileSize(fileSize)), TitledText(R.string.attachmentsFileSize, Util.getPrettyFileSize(fileSize)),
takeIf { it is ImageSlide } takeIf { it is ImageSlide }
?.let(Slide::asAttachment) ?.let(Slide::asAttachment)
?.run { "${width}x$height" } ?.run { "${width}x$height" }
?.let { TitledText(R.string.message_details_header__resolution, it) }, ?.let { TitledText(R.string.attachmentsResolution, it) },
attachmentDb.duration(this)?.let { TitledText(R.string.message_details_header__duration, it) }, attachmentDb.duration(this)?.let { TitledText(R.string.attachmentsDuration, it) },
) )
private fun AttachmentDatabase.duration(slide: Slide): String? = private fun AttachmentDatabase.duration(slide: Slide): String? =
@ -157,7 +157,7 @@ data class MessageDetailsState(
val sender: Recipient? = null, val sender: Recipient? = null,
val thread: 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 val canReply = record?.isOpenGroupInvitation != true
} }

View File

@ -15,9 +15,10 @@ import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.Toast import android.widget.Toast
import com.google.android.material.bottomsheet.BottomSheetDialogFragment import com.google.android.material.bottomsheet.BottomSheetDialogFragment
import com.squareup.phrase.Phrase
import network.loki.messenger.R import network.loki.messenger.R
import network.loki.messenger.databinding.FragmentModalUrlBottomSheetBinding import network.loki.messenger.databinding.FragmentModalUrlBottomSheetBinding
import org.thoughtcrime.securesms.util.UiModeUtilities import org.session.libsession.utilities.StringSubstitutionConstants.URL_KEY
class ModalUrlBottomSheet(private val url: String): BottomSheetDialogFragment(), View.OnClickListener { class ModalUrlBottomSheet(private val url: String): BottomSheetDialogFragment(), View.OnClickListener {
private lateinit var binding: FragmentModalUrlBottomSheetBinding private lateinit var binding: FragmentModalUrlBottomSheetBinding
@ -29,7 +30,9 @@ class ModalUrlBottomSheet(private val url: String): BottomSheetDialogFragment(),
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
val explanation = resources.getString(R.string.dialog_open_url_explanation, url) val explanation = Phrase.from(context, R.string.urlOpenDescription)
.put(URL_KEY, url)
.format()
val spannable = SpannableStringBuilder(explanation) val spannable = SpannableStringBuilder(explanation)
val startIndex = explanation.indexOf(url) val startIndex = explanation.indexOf(url)
spannable.setSpan(StyleSpan(Typeface.BOLD), startIndex, startIndex + url.count(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) spannable.setSpan(StyleSpan(Typeface.BOLD), startIndex, startIndex + url.count(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
@ -44,7 +47,7 @@ class ModalUrlBottomSheet(private val url: String): BottomSheetDialogFragment(),
val intent = Intent(Intent.ACTION_VIEW, Uri.parse(url)) val intent = Intent(Intent.ACTION_VIEW, Uri.parse(url))
requireContext().startActivity(intent) requireContext().startActivity(intent)
} catch (e: Exception) { } 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() dismiss()
} }
@ -53,7 +56,7 @@ class ModalUrlBottomSheet(private val url: String): BottomSheetDialogFragment(),
val clip = ClipData.newPlainText("URL", url) val clip = ClipData.newPlainText("URL", url)
val manager = requireContext().getSystemService(CLIPBOARD_SERVICE) as ClipboardManager val manager = requireContext().getSystemService(CLIPBOARD_SERVICE) as ClipboardManager
manager.setPrimaryClip(clip) 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() dismiss()
} }

View File

@ -11,16 +11,18 @@ import android.widget.RelativeLayout
import android.widget.TextView import android.widget.TextView
import androidx.core.view.children import androidx.core.view.children
import androidx.core.view.isVisible import androidx.core.view.isVisible
import com.squareup.phrase.Phrase
import network.loki.messenger.R import network.loki.messenger.R
import network.loki.messenger.databinding.AlbumThumbnailViewBinding import network.loki.messenger.databinding.AlbumThumbnailViewBinding
import org.session.libsession.messaging.sending_receiving.attachments.AttachmentTransferProgress import org.session.libsession.messaging.sending_receiving.attachments.AttachmentTransferProgress
import org.session.libsession.messaging.sending_receiving.attachments.DatabaseAttachment import org.session.libsession.messaging.sending_receiving.attachments.DatabaseAttachment
import org.session.libsession.utilities.StringSubstitutionConstants.COUNT_KEY
import org.session.libsession.utilities.recipients.Recipient import org.session.libsession.utilities.recipients.Recipient
import org.thoughtcrime.securesms.MediaPreviewActivity import org.thoughtcrime.securesms.MediaPreviewActivity
import org.thoughtcrime.securesms.components.CornerMask import org.thoughtcrime.securesms.components.CornerMask
import org.thoughtcrime.securesms.conversation.v2.utilities.ThumbnailView import org.thoughtcrime.securesms.conversation.v2.utilities.ThumbnailView
import org.thoughtcrime.securesms.database.model.MmsMessageRecord import org.thoughtcrime.securesms.database.model.MmsMessageRecord
import org.thoughtcrime.securesms.mms.GlideRequests import com.bumptech.glide.RequestManager
import org.thoughtcrime.securesms.mms.Slide import org.thoughtcrime.securesms.mms.Slide
import org.thoughtcrime.securesms.util.ActivityDispatcher import org.thoughtcrime.securesms.util.ActivityDispatcher
@ -80,7 +82,7 @@ class AlbumThumbnailView : RelativeLayout {
slideSize = -1 slideSize = -1
} }
fun bind(glideRequests: GlideRequests, message: MmsMessageRecord, fun bind(glideRequests: RequestManager, message: MmsMessageRecord,
isStart: Boolean, isEnd: Boolean) { isStart: Boolean, isEnd: Boolean) {
slides = message.slideDeck.thumbnailSlides slides = message.slideDeck.thumbnailSlides
if (slides.isEmpty()) { if (slides.isEmpty()) {
@ -97,7 +99,10 @@ class AlbumThumbnailView : RelativeLayout {
binding.albumCellContainer.findViewById<TextView>(R.id.album_cell_overflow_text)?.let { overflowText -> binding.albumCellContainer.findViewById<TextView>(R.id.album_cell_overflow_text)?.let { overflowText ->
// overflowText will be null if !overflowed // overflowText will be null if !overflowed
overflowText.isVisible = overflowed // more than max album size 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 this.slideSize = slides.size
} }
@ -110,10 +115,9 @@ class AlbumThumbnailView : RelativeLayout {
// endregion // endregion
fun layoutRes(slideCount: Int) = when (slideCount) { fun layoutRes(slideCount: Int) = when (slideCount) {
1 -> R.layout.album_thumbnail_1 // single 1 -> R.layout.album_thumbnail_1 // single
2 -> R.layout.album_thumbnail_2// two sidebyside 2 -> R.layout.album_thumbnail_2 // two side-by-side
else -> R.layout.album_thumbnail_3 // three stacked with additional text else -> R.layout.album_thumbnail_3 // three stacked with additional text
} }

View File

@ -7,7 +7,7 @@ import android.widget.LinearLayout
import androidx.core.view.isVisible import androidx.core.view.isVisible
import network.loki.messenger.databinding.ViewLinkPreviewDraftBinding import network.loki.messenger.databinding.ViewLinkPreviewDraftBinding
import org.session.libsession.messaging.sending_receiving.link_preview.LinkPreview import org.session.libsession.messaging.sending_receiving.link_preview.LinkPreview
import org.thoughtcrime.securesms.mms.GlideRequests import com.bumptech.glide.RequestManager
import org.thoughtcrime.securesms.mms.ImageSlide import org.thoughtcrime.securesms.mms.ImageSlide
import org.thoughtcrime.securesms.util.toPx import org.thoughtcrime.securesms.util.toPx
@ -27,7 +27,7 @@ class LinkPreviewDraftView : LinearLayout {
binding.linkPreviewDraftCancelButton.setOnClickListener { cancel() } binding.linkPreviewDraftCancelButton.setOnClickListener { cancel() }
} }
fun update(glide: GlideRequests, linkPreview: LinkPreview) { fun update(glide: RequestManager, linkPreview: LinkPreview) {
// Hide the loader and show the content view // Hide the loader and show the content view
binding.linkPreviewDraftContainer.isVisible = true binding.linkPreviewDraftContainer.isVisible = true
binding.linkPreviewDraftLoader.isVisible = false binding.linkPreviewDraftLoader.isVisible = false

View File

@ -9,13 +9,13 @@ import android.widget.BaseAdapter
import android.widget.ListView import android.widget.ListView
import org.session.libsession.messaging.mentions.Mention import org.session.libsession.messaging.mentions.Mention
import org.thoughtcrime.securesms.dependencies.DatabaseComponent import org.thoughtcrime.securesms.dependencies.DatabaseComponent
import org.thoughtcrime.securesms.mms.GlideRequests import com.bumptech.glide.RequestManager
import org.thoughtcrime.securesms.util.toPx import org.thoughtcrime.securesms.util.toPx
class MentionCandidateSelectionView(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : ListView(context, attrs, defStyleAttr) { class MentionCandidateSelectionView(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : ListView(context, attrs, defStyleAttr) {
private var mentionCandidates = listOf<Mention>() private var mentionCandidates = listOf<Mention>()
set(newValue) { field = newValue; mentionCandidateSelectionViewAdapter.mentionCandidates = newValue } set(newValue) { field = newValue; mentionCandidateSelectionViewAdapter.mentionCandidates = newValue }
var glide: GlideRequests? = null var glide: RequestManager? = null
set(newValue) { field = newValue; mentionCandidateSelectionViewAdapter.glide = newValue } set(newValue) { field = newValue; mentionCandidateSelectionViewAdapter.glide = newValue }
var openGroupServer: String? = null var openGroupServer: String? = null
set(newValue) { field = newValue; mentionCandidateSelectionViewAdapter.openGroupServer = openGroupServer } set(newValue) { field = newValue; mentionCandidateSelectionViewAdapter.openGroupServer = openGroupServer }
@ -28,7 +28,7 @@ class MentionCandidateSelectionView(context: Context, attrs: AttributeSet?, defS
private class Adapter(private val context: Context) : BaseAdapter() { private class Adapter(private val context: Context) : BaseAdapter() {
var mentionCandidates = listOf<Mention>() var mentionCandidates = listOf<Mention>()
set(newValue) { field = newValue; notifyDataSetChanged() } set(newValue) { field = newValue; notifyDataSetChanged() }
var glide: GlideRequests? = null var glide: RequestManager? = null
var openGroupServer: String? = null var openGroupServer: String? = null
var openGroupRoom: String? = null var openGroupRoom: String? = null

View File

@ -8,13 +8,13 @@ import android.widget.LinearLayout
import network.loki.messenger.databinding.ViewMentionCandidateBinding import network.loki.messenger.databinding.ViewMentionCandidateBinding
import org.session.libsession.messaging.mentions.Mention import org.session.libsession.messaging.mentions.Mention
import org.thoughtcrime.securesms.groups.OpenGroupManager import org.thoughtcrime.securesms.groups.OpenGroupManager
import org.thoughtcrime.securesms.mms.GlideRequests import com.bumptech.glide.RequestManager
class MentionCandidateView : LinearLayout { class MentionCandidateView : LinearLayout {
private lateinit var binding: ViewMentionCandidateBinding private lateinit var binding: ViewMentionCandidateBinding
var mentionCandidate = Mention("", "") var mentionCandidate = Mention("", "")
set(newValue) { field = newValue; update() } set(newValue) { field = newValue; update() }
var glide: GlideRequests? = null var glide: RequestManager? = null
var openGroupServer: String? = null var openGroupServer: String? = null
var openGroupRoom: String? = null var openGroupRoom: String? = null

View File

@ -8,9 +8,11 @@ import android.text.Spannable
import android.text.SpannableStringBuilder import android.text.SpannableStringBuilder
import android.text.style.StyleSpan import android.text.style.StyleSpan
import androidx.fragment.app.DialogFragment import androidx.fragment.app.DialogFragment
import com.squareup.phrase.Phrase
import network.loki.messenger.R import network.loki.messenger.R
import org.session.libsession.messaging.MessagingModuleConfiguration import org.session.libsession.messaging.MessagingModuleConfiguration
import org.session.libsession.messaging.contacts.Contact import org.session.libsession.messaging.contacts.Contact
import org.session.libsession.utilities.StringSubstitutionConstants.COMMUNITY_NAME_KEY
import org.session.libsession.utilities.recipients.Recipient import org.session.libsession.utilities.recipients.Recipient
import org.thoughtcrime.securesms.createSessionDialog import org.thoughtcrime.securesms.createSessionDialog
import org.thoughtcrime.securesms.dependencies.DatabaseComponent import org.thoughtcrime.securesms.dependencies.DatabaseComponent
@ -24,14 +26,14 @@ class BlockedDialog(private val recipient: Recipient, private val context: Conte
val contact = contactDB.getContactWithAccountID(accountID) val contact = contactDB.getContactWithAccountID(accountID)
val name = contact?.displayName(Contact.ContactContext.REGULAR) ?: accountID val name = contact?.displayName(Contact.ContactContext.REGULAR) ?: accountID
val explanation = resources.getString(R.string.dialog_blocked_explanation, name) val explanation = Phrase.from(context, R.string.communityJoinDescription).put(COMMUNITY_NAME_KEY, name).format()
val spannable = SpannableStringBuilder(explanation) val spannable = SpannableStringBuilder(explanation)
val startIndex = explanation.indexOf(name) val startIndex = explanation.indexOf(name)
spannable.setSpan(StyleSpan(Typeface.BOLD), startIndex, startIndex + name.count(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) 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) text(spannable)
button(R.string.ConversationActivity_unblock) { unblock() } button(R.string.blockUnblock) { unblock() }
cancelButton { dismiss() } cancelButton { dismiss() }
} }

View File

@ -7,12 +7,14 @@ import android.text.Spannable
import android.text.SpannableStringBuilder import android.text.SpannableStringBuilder
import android.text.style.StyleSpan import android.text.style.StyleSpan
import androidx.fragment.app.DialogFragment import androidx.fragment.app.DialogFragment
import com.squareup.phrase.Phrase
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import network.loki.messenger.R import network.loki.messenger.R
import org.session.libsession.messaging.contacts.Contact import org.session.libsession.messaging.contacts.Contact
import org.session.libsession.messaging.jobs.AttachmentDownloadJob import org.session.libsession.messaging.jobs.AttachmentDownloadJob
import org.session.libsession.messaging.jobs.JobQueue import org.session.libsession.messaging.jobs.JobQueue
import org.session.libsession.utilities.recipients.Recipient 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.createSessionDialog
import org.thoughtcrime.securesms.database.SessionContactDatabase import org.thoughtcrime.securesms.database.SessionContactDatabase
import org.thoughtcrime.securesms.dependencies.DatabaseComponent import org.thoughtcrime.securesms.dependencies.DatabaseComponent
@ -29,15 +31,19 @@ class DownloadDialog(private val recipient: Recipient) : DialogFragment() {
val accountID = recipient.address.toString() val accountID = recipient.address.toString()
val contact = contactDB.getContactWithAccountID(accountID) val contact = contactDB.getContactWithAccountID(accountID)
val name = contact?.displayName(Contact.ContactContext.REGULAR) ?: 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 spannable = SpannableStringBuilder(explanation)
val startIndex = explanation.indexOf(name) val startIndex = explanation.indexOf(name)
spannable.setSpan(StyleSpan(Typeface.BOLD), startIndex, startIndex + name.count(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) spannable.setSpan(StyleSpan(Typeface.BOLD), startIndex, startIndex + name.count(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
text(spannable) text(spannable)
button(R.string.dialog_download_button_title, R.string.AccessibilityId_download_media) { trust() } button(R.string.download, R.string.AccessibilityId_download_media) { trust() }
cancelButton { dismiss() } cancelButton { dismiss() }
} }

View File

@ -1,5 +1,6 @@
package org.thoughtcrime.securesms.conversation.v2.dialogs package org.thoughtcrime.securesms.conversation.v2.dialogs
import org.thoughtcrime.securesms.createSessionDialog
import android.app.Dialog import android.app.Dialog
import android.graphics.Typeface import android.graphics.Typeface
import android.os.Bundle import android.os.Bundle
@ -8,11 +9,13 @@ import android.text.SpannableStringBuilder
import android.text.style.StyleSpan import android.text.style.StyleSpan
import android.widget.Toast import android.widget.Toast
import androidx.fragment.app.DialogFragment import androidx.fragment.app.DialogFragment
import com.squareup.phrase.Phrase
import network.loki.messenger.R import network.loki.messenger.R
import org.session.libsession.messaging.MessagingModuleConfiguration import org.session.libsession.messaging.MessagingModuleConfiguration
import org.session.libsession.utilities.OpenGroupUrlParser 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.session.libsignal.utilities.ThreadUtils
import org.thoughtcrime.securesms.createSessionDialog
import org.thoughtcrime.securesms.groups.OpenGroupManager import org.thoughtcrime.securesms.groups.OpenGroupManager
import org.thoughtcrime.securesms.util.ConfigurationMessageUtilities 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() { class JoinOpenGroupDialog(private val name: String, private val url: String) : DialogFragment() {
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog = createSessionDialog { override fun onCreateDialog(savedInstanceState: Bundle?): Dialog = createSessionDialog {
title(resources.getString(R.string.dialog_join_open_group_title, name)) title(resources.getString(R.string.communityJoin))
val explanation = resources.getString(R.string.dialog_join_open_group_explanation, name) val explanation = Phrase.from(context, R.string.communityJoinDescription).put(COMMUNITY_NAME_KEY, name).format()
val spannable = SpannableStringBuilder(explanation) 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) spannable.setSpan(StyleSpan(Typeface.BOLD), startIndex, startIndex + name.count(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
text(spannable) text(spannable)
cancelButton { dismiss() } cancelButton { dismiss() }
button(R.string.open_group_invitation_view__join_accessibility_description) { join() } button(R.string.join) { join() }
} }
private fun 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) MessagingModuleConfiguration.shared.storage.onOpenGroupAdded(openGroup.server, openGroup.room)
ConfigurationMessageUtilities.forceSyncConfigurationNowIfNeeded(activity) ConfigurationMessageUtilities.forceSyncConfigurationNowIfNeeded(activity)
} catch (e: Exception) { } 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() dismiss()

View File

@ -3,7 +3,9 @@ package org.thoughtcrime.securesms.conversation.v2.dialogs
import android.app.Dialog import android.app.Dialog
import android.os.Bundle import android.os.Bundle
import androidx.fragment.app.DialogFragment import androidx.fragment.app.DialogFragment
import com.squareup.phrase.Phrase
import network.loki.messenger.R import network.loki.messenger.R
import org.session.libsession.utilities.StringSubstitutionConstants.APP_NAME_KEY
import org.session.libsession.utilities.TextSecurePreferences import org.session.libsession.utilities.TextSecurePreferences
import org.thoughtcrime.securesms.createSessionDialog import org.thoughtcrime.securesms.createSessionDialog
@ -12,9 +14,12 @@ import org.thoughtcrime.securesms.createSessionDialog
class LinkPreviewDialog(private val onEnabled: () -> Unit) : DialogFragment() { class LinkPreviewDialog(private val onEnabled: () -> Unit) : DialogFragment() {
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog = createSessionDialog { override fun onCreateDialog(savedInstanceState: Bundle?): Dialog = createSessionDialog {
title(R.string.dialog_link_preview_title) title(R.string.linkPreviewsEnable)
text(R.string.dialog_link_preview_explanation) val txt = Phrase.from(context, R.string.linkPreviewsFirstDescription)
button(R.string.dialog_link_preview_enable_button_title) { enable() } .put(APP_NAME_KEY, context.getString(R.string.app_name))
.format()
text(txt)
button(R.string.enable) { enable() }
cancelButton { dismiss() } cancelButton { dismiss() }
} }

View File

@ -10,9 +10,9 @@ import org.thoughtcrime.securesms.createSessionDialog
class SendSeedDialog(private val proceed: (() -> Unit)? = null) : DialogFragment() { class SendSeedDialog(private val proceed: (() -> Unit)? = null) : DialogFragment() {
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog = createSessionDialog { override fun onCreateDialog(savedInstanceState: Bundle?): Dialog = createSessionDialog {
title(R.string.dialog_send_seed_title) title(R.string.warning)
text(R.string.dialog_send_seed_explanation) text(R.string.recoveryPasswordWarningSendDescription)
button(R.string.dialog_send_seed_send_button_title) { send() } button(R.string.send) { send() }
cancelButton() cancelButton()
} }

View File

@ -27,7 +27,7 @@ import org.thoughtcrime.securesms.conversation.v2.messages.QuoteView
import org.thoughtcrime.securesms.conversation.v2.messages.QuoteViewDelegate import org.thoughtcrime.securesms.conversation.v2.messages.QuoteViewDelegate
import org.thoughtcrime.securesms.database.model.MessageRecord import org.thoughtcrime.securesms.database.model.MessageRecord
import org.thoughtcrime.securesms.database.model.MmsMessageRecord import org.thoughtcrime.securesms.database.model.MmsMessageRecord
import org.thoughtcrime.securesms.mms.GlideRequests import com.bumptech.glide.RequestManager
import org.thoughtcrime.securesms.util.addTextChangedListener import org.thoughtcrime.securesms.util.addTextChangedListener
import org.thoughtcrime.securesms.util.contains import org.thoughtcrime.securesms.util.contains
@ -188,7 +188,7 @@ class InputBar @JvmOverloads constructor(
private fun startRecordingVoiceMessage() { delegate?.startRecordingVoiceMessage() } private fun startRecordingVoiceMessage() { delegate?.startRecordingVoiceMessage() }
fun draftQuote(thread: Recipient, message: MessageRecord, glide: GlideRequests) { fun draftQuote(thread: Recipient, message: MessageRecord, glide: RequestManager) {
quoteView?.let(binding.inputBarAdditionalContentContainer::removeView) quoteView?.let(binding.inputBarAdditionalContentContainer::removeView)
quote = message quote = message
@ -238,7 +238,7 @@ class InputBar @JvmOverloads constructor(
requestLayout() requestLayout()
} }
fun updateLinkPreviewDraft(glide: GlideRequests, updatedLinkPreview: LinkPreview) { fun updateLinkPreviewDraft(glide: RequestManager, updatedLinkPreview: LinkPreview) {
// Update our `linkPreview` property with the new (provided as an argument to this function) // Update our `linkPreview` property with the new (provided as an argument to this function)
// then update the View from that. // then update the View from that.
linkPreview = updatedLinkPreview.also { linkPreviewDraftView?.update(glide, it) } linkPreview = updatedLinkPreview.also { linkPreviewDraftView?.update(glide, it) }

View File

@ -19,7 +19,6 @@ import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import network.loki.messenger.R import network.loki.messenger.R
import network.loki.messenger.databinding.ViewInputBarRecordingBinding import network.loki.messenger.databinding.ViewInputBarRecordingBinding
import org.thoughtcrime.securesms.util.DateUtils
import org.thoughtcrime.securesms.util.animateSizeChange import org.thoughtcrime.securesms.util.animateSizeChange
import org.thoughtcrime.securesms.util.disableClipping import org.thoughtcrime.securesms.util.disableClipping
import org.thoughtcrime.securesms.util.toPx import org.thoughtcrime.securesms.util.toPx
@ -106,8 +105,7 @@ class InputBarRecordingView : RelativeLayout {
timerJob = scope.launch { timerJob = scope.launch {
while (isActive) { while (isActive) {
val duration = (Date().time - startTimestamp) / 1000L val duration = (Date().time - startTimestamp) / 1000L
binding.recordingViewDurationTextView.text = DateUtils.formatElapsedTime(duration) binding.recordingViewDurationTextView.text = android.text.format.DateUtils.formatElapsedTime(duration)
delay(500) delay(500)
} }
} }

View File

@ -16,10 +16,13 @@ import androidx.appcompat.widget.SearchView.OnQueryTextListener
import androidx.core.content.pm.ShortcutInfoCompat import androidx.core.content.pm.ShortcutInfoCompat
import androidx.core.content.pm.ShortcutManagerCompat import androidx.core.content.pm.ShortcutManagerCompat
import androidx.core.graphics.drawable.IconCompat import androidx.core.graphics.drawable.IconCompat
import com.squareup.phrase.Phrase
import java.io.IOException
import network.loki.messenger.R import network.loki.messenger.R
import org.session.libsession.messaging.sending_receiving.MessageSender import org.session.libsession.messaging.sending_receiving.MessageSender
import org.session.libsession.messaging.sending_receiving.leave import org.session.libsession.messaging.sending_receiving.leave
import org.session.libsession.utilities.GroupUtil.doubleDecodeGroupID import org.session.libsession.utilities.GroupUtil.doubleDecodeGroupID
import org.session.libsession.utilities.StringSubstitutionConstants.GROUP_NAME_KEY
import org.session.libsession.utilities.TextSecurePreferences import org.session.libsession.utilities.TextSecurePreferences
import org.session.libsession.utilities.recipients.Recipient import org.session.libsession.utilities.recipients.Recipient
import org.session.libsignal.utilities.guava.Optional import org.session.libsignal.utilities.guava.Optional
@ -38,7 +41,6 @@ import org.thoughtcrime.securesms.service.WebRtcCallService
import org.thoughtcrime.securesms.showMuteDialog import org.thoughtcrime.securesms.showMuteDialog
import org.thoughtcrime.securesms.showSessionDialog import org.thoughtcrime.securesms.showSessionDialog
import org.thoughtcrime.securesms.util.BitmapUtil import org.thoughtcrime.securesms.util.BitmapUtil
import java.io.IOException
object ConversationMenuHelper { object ConversationMenuHelper {
@ -164,9 +166,9 @@ object ConversationMenuHelper {
if (!TextSecurePreferences.isCallNotificationsEnabled(context)) { if (!TextSecurePreferences.isCallNotificationsEnabled(context)) {
context.showSessionDialog { context.showSessionDialog {
title(R.string.ConversationActivity_call_title) title(R.string.callsPermissionsRequired)
text(R.string.ConversationActivity_call_prompt) text(R.string.callsPermissionsRequiredDescription)
button(R.string.activity_settings_title, R.string.AccessibilityId_settings) { button(R.string.sessionSettings, R.string.AccessibilityId_settings) {
Intent(context, PrivacySettingsActivity::class.java).let(context::startActivity) Intent(context, PrivacySettingsActivity::class.java).let(context::startActivity)
} }
cancelButton() cancelButton()
@ -217,7 +219,7 @@ object ConversationMenuHelper {
.setIntent(ShortcutLauncherActivity.createIntent(context, thread.address)) .setIntent(ShortcutLauncherActivity.createIntent(context, thread.address))
.build() .build()
if (ShortcutManagerCompat.requestPinShortcut(context, shortcutInfo, null)) { 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() }.execute()
@ -274,15 +276,24 @@ object ConversationMenuHelper {
val accountID = TextSecurePreferences.getLocalNumber(context) val accountID = TextSecurePreferences.getLocalNumber(context)
val isCurrentUserAdmin = admins.any { it.toString() == accountID } val isCurrentUserAdmin = admins.any { it.toString() == accountID }
val message = if (isCurrentUserAdmin) { 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.groupLeaveDescriptionAdmin)
.put(GROUP_NAME_KEY, group.title)
.format().toString()
} else { } 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().toString()
} }
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 { context.showSessionDialog {
title(R.string.ConversationActivity_leave_group) title(R.string.groupLeave)
text(message) text(message)
button(R.string.yes) { button(R.string.yes) {
try { try {

View File

@ -8,17 +8,19 @@ import androidx.core.content.res.ResourcesCompat
import androidx.core.view.isGone import androidx.core.view.isGone
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import com.squareup.phrase.Phrase
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject
import network.loki.messenger.R import network.loki.messenger.R
import network.loki.messenger.databinding.ViewControlMessageBinding import network.loki.messenger.databinding.ViewControlMessageBinding
import network.loki.messenger.libsession_util.util.ExpiryMode import network.loki.messenger.libsession_util.util.ExpiryMode
import org.session.libsession.messaging.MessagingModuleConfiguration import org.session.libsession.messaging.MessagingModuleConfiguration
import org.session.libsession.messaging.messages.ExpirationConfiguration import org.session.libsession.messaging.messages.ExpirationConfiguration
import org.session.libsession.utilities.StringSubstitutionConstants.NAME_KEY
import org.thoughtcrime.securesms.conversation.disappearingmessages.DisappearingMessages import org.thoughtcrime.securesms.conversation.disappearingmessages.DisappearingMessages
import org.thoughtcrime.securesms.conversation.disappearingmessages.expiryMode import org.thoughtcrime.securesms.conversation.disappearingmessages.expiryMode
import org.thoughtcrime.securesms.database.model.MessageRecord import org.thoughtcrime.securesms.database.model.MessageRecord
import org.thoughtcrime.securesms.dependencies.DatabaseComponent import org.thoughtcrime.securesms.dependencies.DatabaseComponent
import javax.inject.Inject
@AndroidEntryPoint @AndroidEntryPoint
class ControlMessageView : LinearLayout { class ControlMessageView : LinearLayout {
@ -75,8 +77,10 @@ class ControlMessageView : LinearLayout {
} }
} }
message.isMessageRequestResponse -> { message.isMessageRequestResponse -> {
binding.textView.text = context.getString(R.string.message_requests_accepted) binding.textView.text = context.getString(R.string.messageRequestsAccepted)
binding.root.contentDescription=context.getString(R.string.AccessibilityId_message_request_config_message) binding.root.contentDescription = Phrase.from(context, R.string.messageRequestYouHaveAccepted)
.put(NAME_KEY, message.individualRecipient.name)
.format()
} }
message.isCallLog -> { message.isCallLog -> {
val drawable = when { val drawable = when {

View File

@ -21,7 +21,7 @@ class DeletedMessageView : LinearLayout {
// region Updating // region Updating
fun bind(message: MessageRecord, @ColorInt textColor: Int) { fun bind(message: MessageRecord, @ColorInt textColor: Int) {
assert(message.isDeleted) assert(message.isDeleted)
binding.deleteTitleTextView.text = context.getString(R.string.deleted_message) binding.deleteTitleTextView.text = context.getString(R.string.deleteMessageDeleted)
binding.deleteTitleTextView.setTextColor(textColor) binding.deleteTitleTextView.setTextColor(textColor)
binding.deletedMessageViewIconImageView.imageTintList = ColorStateList.valueOf(textColor) binding.deletedMessageViewIconImageView.imageTintList = ColorStateList.valueOf(textColor)
} }

View File

@ -15,7 +15,7 @@ import org.thoughtcrime.securesms.components.CornerMask
import org.thoughtcrime.securesms.conversation.v2.ModalUrlBottomSheet import org.thoughtcrime.securesms.conversation.v2.ModalUrlBottomSheet
import org.thoughtcrime.securesms.conversation.v2.utilities.MessageBubbleUtilities import org.thoughtcrime.securesms.conversation.v2.utilities.MessageBubbleUtilities
import org.thoughtcrime.securesms.database.model.MmsMessageRecord import org.thoughtcrime.securesms.database.model.MmsMessageRecord
import org.thoughtcrime.securesms.mms.GlideRequests import com.bumptech.glide.RequestManager
import org.thoughtcrime.securesms.mms.ImageSlide import org.thoughtcrime.securesms.mms.ImageSlide
class LinkPreviewView : LinearLayout { class LinkPreviewView : LinearLayout {
@ -32,7 +32,7 @@ class LinkPreviewView : LinearLayout {
// region Updating // region Updating
fun bind( fun bind(
message: MmsMessageRecord, message: MmsMessageRecord,
glide: GlideRequests, glide: RequestManager,
isStartOfMessageCluster: Boolean, isStartOfMessageCluster: Boolean,
isEndOfMessageCluster: Boolean isEndOfMessageCluster: Boolean
) { ) {

View File

@ -18,7 +18,7 @@ import org.session.libsession.utilities.getColorFromAttr
import org.session.libsession.utilities.recipients.Recipient import org.session.libsession.utilities.recipients.Recipient
import org.thoughtcrime.securesms.conversation.v2.utilities.MentionUtilities import org.thoughtcrime.securesms.conversation.v2.utilities.MentionUtilities
import org.thoughtcrime.securesms.database.SessionContactDatabase import org.thoughtcrime.securesms.database.SessionContactDatabase
import org.thoughtcrime.securesms.mms.GlideRequests import com.bumptech.glide.RequestManager
import org.thoughtcrime.securesms.mms.SlideDeck import org.thoughtcrime.securesms.mms.SlideDeck
import org.thoughtcrime.securesms.util.MediaUtil import org.thoughtcrime.securesms.util.MediaUtil
import org.thoughtcrime.securesms.util.getAccentColor import org.thoughtcrime.securesms.util.getAccentColor
@ -68,20 +68,20 @@ class QuoteView @JvmOverloads constructor(context: Context, attrs: AttributeSet?
// region Updating // region Updating
fun bind(authorPublicKey: String, body: String?, attachments: SlideDeck?, thread: Recipient, fun bind(authorPublicKey: String, body: String?, attachments: SlideDeck?, thread: Recipient,
isOutgoingMessage: Boolean, isOpenGroupInvitation: Boolean, threadID: Long, isOutgoingMessage: Boolean, isOpenGroupInvitation: Boolean, threadID: Long,
isOriginalMissing: Boolean, glide: GlideRequests) { isOriginalMissing: Boolean, glide: RequestManager) {
// Author // Author
val author = contactDb.getContactWithAccountID(authorPublicKey) val author = contactDb.getContactWithAccountID(authorPublicKey)
val localNumber = TextSecurePreferences.getLocalNumber(context) val localNumber = TextSecurePreferences.getLocalNumber(context)
val quoteIsLocalUser = localNumber != null && authorPublicKey == localNumber val quoteIsLocalUser = localNumber != null && authorPublicKey == localNumber
val authorDisplayName = 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)}" else author?.displayName(Contact.contextForRecipient(thread)) ?: "${authorPublicKey.take(4)}...${authorPublicKey.takeLast(4)}"
binding.quoteViewAuthorTextView.text = authorDisplayName binding.quoteViewAuthorTextView.text = authorDisplayName
binding.quoteViewAuthorTextView.setTextColor(getTextColor(isOutgoingMessage)) binding.quoteViewAuthorTextView.setTextColor(getTextColor(isOutgoingMessage))
// Body // Body
binding.quoteViewBodyTextView.text = if (isOpenGroupInvitation) binding.quoteViewBodyTextView.text = if (isOpenGroupInvitation)
resources.getString(R.string.open_group_invitation_view__open_group_invitation) resources.getString(R.string.communityInvitation)
else MentionUtilities.highlightMentions( else MentionUtilities.highlightMentions(
text = (body ?: "").toSpannable(), text = (body ?: "").toSpannable(),
isOutgoingMessage = isOutgoingMessage, isOutgoingMessage = isOutgoingMessage,
@ -106,7 +106,7 @@ class QuoteView @JvmOverloads constructor(context: Context, attrs: AttributeSet?
attachments.audioSlide != null -> { attachments.audioSlide != null -> {
binding.quoteViewAttachmentPreviewImageView.setImageResource(R.drawable.ic_microphone) binding.quoteViewAttachmentPreviewImageView.setImageResource(R.drawable.ic_microphone)
binding.quoteViewAttachmentPreviewImageView.isVisible = true binding.quoteViewAttachmentPreviewImageView.isVisible = true
binding.quoteViewBodyTextView.text = resources.getString(R.string.Slide_audio) binding.quoteViewBodyTextView.text = resources.getString(R.string.audio)
} }
attachments.documentSlide != null -> { attachments.documentSlide != null -> {
binding.quoteViewAttachmentPreviewImageView.setImageResource(R.drawable.ic_document_large_light) binding.quoteViewAttachmentPreviewImageView.setImageResource(R.drawable.ic_document_large_light)
@ -120,7 +120,7 @@ class QuoteView @JvmOverloads constructor(context: Context, attrs: AttributeSet?
.root.setRoundedCorners(toPx(4, resources)) .root.setRoundedCorners(toPx(4, resources))
binding.quoteViewAttachmentThumbnailImageView.root.setImageResource(glide, slide, false) binding.quoteViewAttachmentThumbnailImageView.root.setImageResource(glide, slide, false)
binding.quoteViewAttachmentThumbnailImageView.root.isVisible = true 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)
} }
} }
} }

View File

@ -5,12 +5,13 @@ import android.util.AttributeSet
import android.widget.LinearLayout import android.widget.LinearLayout
import androidx.annotation.ColorInt import androidx.annotation.ColorInt
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import com.squareup.phrase.Phrase
import network.loki.messenger.R import network.loki.messenger.R
import network.loki.messenger.databinding.ViewUntrustedAttachmentBinding import network.loki.messenger.databinding.ViewUntrustedAttachmentBinding
import org.session.libsession.utilities.StringSubstitutionConstants.FILE_TYPE_KEY
import org.session.libsession.utilities.recipients.Recipient import org.session.libsession.utilities.recipients.Recipient
import org.thoughtcrime.securesms.conversation.v2.dialogs.DownloadDialog import org.thoughtcrime.securesms.conversation.v2.dialogs.DownloadDialog
import org.thoughtcrime.securesms.util.ActivityDispatcher import org.thoughtcrime.securesms.util.ActivityDispatcher
import java.util.Locale
class UntrustedAttachmentView: LinearLayout { class UntrustedAttachmentView: LinearLayout {
private val binding: ViewUntrustedAttachmentBinding by lazy { ViewUntrustedAttachmentBinding.bind(this) } private val binding: ViewUntrustedAttachmentBinding by lazy { ViewUntrustedAttachmentBinding.bind(this) }
@ -30,13 +31,17 @@ class UntrustedAttachmentView: LinearLayout {
// region Updating // region Updating
fun bind(attachmentType: AttachmentType, @ColorInt textColor: Int) { fun bind(attachmentType: AttachmentType, @ColorInt textColor: Int) {
val (iconRes, stringRes) = when (attachmentType) { val (iconRes, stringRes) = when (attachmentType) {
AttachmentType.AUDIO -> R.drawable.ic_microphone to R.string.Slide_audio AttachmentType.AUDIO -> R.drawable.ic_microphone to R.string.audio
AttachmentType.DOCUMENT -> R.drawable.ic_document_large_light to R.string.document AttachmentType.DOCUMENT -> R.drawable.ic_document_large_light to R.string.files
AttachmentType.MEDIA -> R.drawable.ic_image_white_24dp to R.string.media AttachmentType.MEDIA -> R.drawable.ic_image_white_24dp to R.string.media
} }
val iconDrawable = ContextCompat.getDrawable(context,iconRes)!! val iconDrawable = ContextCompat.getDrawable(context,iconRes)!!
iconDrawable.mutate().setTint(textColor) 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, stringRes)
.format()
binding.untrustedAttachmentTitle.text = text
binding.untrustedAttachmentIcon.setImageDrawable(iconDrawable) binding.untrustedAttachmentIcon.setImageDrawable(iconDrawable)
binding.untrustedAttachmentTitle.text = text binding.untrustedAttachmentTitle.text = text

View File

@ -38,8 +38,8 @@ import org.thoughtcrime.securesms.conversation.v2.utilities.TextUtilities.getInt
import org.thoughtcrime.securesms.database.model.MessageRecord import org.thoughtcrime.securesms.database.model.MessageRecord
import org.thoughtcrime.securesms.database.model.MmsMessageRecord import org.thoughtcrime.securesms.database.model.MmsMessageRecord
import org.thoughtcrime.securesms.database.model.SmsMessageRecord import org.thoughtcrime.securesms.database.model.SmsMessageRecord
import org.thoughtcrime.securesms.mms.GlideApp import com.bumptech.glide.Glide
import org.thoughtcrime.securesms.mms.GlideRequests import com.bumptech.glide.RequestManager
import org.thoughtcrime.securesms.util.GlowViewUtilities import org.thoughtcrime.securesms.util.GlowViewUtilities
import org.thoughtcrime.securesms.util.SearchUtil import org.thoughtcrime.securesms.util.SearchUtil
import org.thoughtcrime.securesms.util.getAccentColor import org.thoughtcrime.securesms.util.getAccentColor
@ -63,7 +63,7 @@ class VisibleMessageContentView : ConstraintLayout {
message: MessageRecord, message: MessageRecord,
isStartOfMessageCluster: Boolean = true, isStartOfMessageCluster: Boolean = true,
isEndOfMessageCluster: Boolean = true, isEndOfMessageCluster: Boolean = true,
glide: GlideRequests = GlideApp.with(this), glide: RequestManager = Glide.with(this),
thread: Recipient, thread: Recipient,
searchQuery: String? = null, searchQuery: String? = null,
contactIsTrusted: Boolean = true, contactIsTrusted: Boolean = true,
@ -117,7 +117,7 @@ class VisibleMessageContentView : ConstraintLayout {
binding.quoteView.root.isVisible = true binding.quoteView.root.isVisible = true
val quote = message.quote!! val quote = message.quote!!
val quoteText = if (quote.isOriginalMissing) { val quoteText = if (quote.isOriginalMissing) {
context.getString(R.string.QuoteView_original_missing) context.getString(R.string.messageErrorOriginal)
} else { } else {
quote.text quote.text
} }

View File

@ -16,7 +16,6 @@ import android.view.MotionEvent
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.FrameLayout import android.widget.FrameLayout
import android.widget.LinearLayout
import androidx.annotation.ColorInt import androidx.annotation.ColorInt
import androidx.annotation.DrawableRes import androidx.annotation.DrawableRes
import androidx.annotation.StringRes import androidx.annotation.StringRes
@ -27,6 +26,13 @@ import androidx.core.os.bundleOf
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.core.view.marginBottom import androidx.core.view.marginBottom
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
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.R
import network.loki.messenger.databinding.ViewEmojiReactionsBinding import network.loki.messenger.databinding.ViewEmojiReactionsBinding
import network.loki.messenger.databinding.ViewVisibleMessageBinding import network.loki.messenger.databinding.ViewVisibleMessageBinding
@ -52,19 +58,12 @@ import org.thoughtcrime.securesms.database.ThreadDatabase
import org.thoughtcrime.securesms.database.model.MessageRecord import org.thoughtcrime.securesms.database.model.MessageRecord
import org.thoughtcrime.securesms.groups.OpenGroupManager import org.thoughtcrime.securesms.groups.OpenGroupManager
import org.thoughtcrime.securesms.home.UserDetailsBottomSheet import org.thoughtcrime.securesms.home.UserDetailsBottomSheet
import org.thoughtcrime.securesms.mms.GlideApp import com.bumptech.glide.Glide
import org.thoughtcrime.securesms.mms.GlideRequests import com.bumptech.glide.RequestManager
import org.thoughtcrime.securesms.util.DateUtils import org.thoughtcrime.securesms.util.DateUtils
import org.thoughtcrime.securesms.util.disableClipping import org.thoughtcrime.securesms.util.disableClipping
import org.thoughtcrime.securesms.util.toDp import org.thoughtcrime.securesms.util.toDp
import org.thoughtcrime.securesms.util.toPx 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" private const val TAG = "VisibleMessageView"
@ -141,7 +140,7 @@ class VisibleMessageView : FrameLayout {
message: MessageRecord, message: MessageRecord,
previous: MessageRecord? = null, previous: MessageRecord? = null,
next: MessageRecord? = null, next: MessageRecord? = null,
glide: GlideRequests = GlideApp.with(this), glide: RequestManager = Glide.with(this),
searchQuery: String? = null, searchQuery: String? = null,
contact: Contact? = null, contact: Contact? = null,
senderAccountID: String, senderAccountID: String,
@ -384,37 +383,37 @@ class VisibleMessageView : FrameLayout {
message.isFailed -> message.isFailed ->
MessageStatusInfo(R.drawable.ic_delivery_status_failed, MessageStatusInfo(R.drawable.ic_delivery_status_failed,
getThemedColor(context, R.attr.danger), getThemedColor(context, R.attr.danger),
R.string.delivery_status_failed R.string.messageStatusFailedToSend
) )
message.isSyncFailed -> message.isSyncFailed ->
MessageStatusInfo( MessageStatusInfo(
R.drawable.ic_delivery_status_failed, R.drawable.ic_delivery_status_failed,
context.getColor(R.color.accent_orange), context.getColor(R.color.accent_orange),
R.string.delivery_status_sync_failed R.string.messageStatusFailedToSync
) )
message.isPending -> message.isPending ->
MessageStatusInfo( MessageStatusInfo(
R.drawable.ic_delivery_status_sending, R.drawable.ic_delivery_status_sending,
context.getColorFromAttr(R.attr.message_status_color), context.getColorFromAttr(R.attr.message_status_color),
R.string.delivery_status_sending R.string.sending
) )
message.isSyncing || message.isResyncing -> message.isSyncing || message.isResyncing ->
MessageStatusInfo( MessageStatusInfo(
R.drawable.ic_delivery_status_sending, R.drawable.ic_delivery_status_sending,
context.getColorFromAttr(R.attr.message_status_color), 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 -> message.isRead || message.isIncoming ->
MessageStatusInfo( MessageStatusInfo(
R.drawable.ic_delivery_status_read, R.drawable.ic_delivery_status_read,
context.getColorFromAttr(R.attr.message_status_color), context.getColorFromAttr(R.attr.message_status_color),
R.string.delivery_status_read R.string.read
) )
message.isSent -> message.isSent ->
MessageStatusInfo( MessageStatusInfo(
R.drawable.ic_delivery_status_sent, R.drawable.ic_delivery_status_sent,
context.getColorFromAttr(R.attr.message_status_color), context.getColorFromAttr(R.attr.message_status_color),
R.string.delivery_status_sent R.string.disappearingMessagesSent
) )
else -> { else -> {
// The message isn't one we care about for message statuses we display to the user (i.e., // The message isn't one we care about for message statuses we display to the user (i.e.,

View File

@ -5,9 +5,11 @@ import android.util.AttributeSet
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.widget.LinearLayout import android.widget.LinearLayout
import com.squareup.phrase.Phrase
import network.loki.messenger.R import network.loki.messenger.R
import network.loki.messenger.databinding.ViewSearchBottomBarBinding 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 { class SearchBottomBar : LinearLayout {
private lateinit var binding: ViewSearchBottomBarBinding private lateinit var binding: ViewSearchBottomBarBinding
@ -35,7 +37,10 @@ class SearchBottomBar : LinearLayout {
} }
} }
if (count > 0) { if (count > 0) {
searchPosition.text = resources.getString(R.string.ConversationActivity_search_position, position + 1, count) searchPosition.text = Phrase.from(context, R.string.searchMatches)
.put(COUNT_KEY, position + 1)
.put(TOTAL_COUNT_KEY, count)
.format()
} else { } else {
searchPosition.text = "" searchPosition.text = ""
} }
@ -43,6 +48,44 @@ class SearchBottomBar : LinearLayout {
setViewEnabled(searchDown, position > 0) setViewEnabled(searchDown, position > 0)
} }
/*
fun setData(position: Int, count: Int, query: String?) = with(binding) {
searchProgressWheel.visibility = GONE
searchUp.setOnClickListener { v: View? ->
if (eventListener != null) {
eventListener!!.onSearchMoveUpPressed()
}
}
searchDown.setOnClickListener { v: View? ->
if (eventListener != null) {
eventListener!!.onSearchMoveDownPressed()
}
}
// If we found search results list how many we found
if (count > 0) {
searchPosition.text = Phrase.from(context, R.string.searchMatches)
.put(COUNT_KEY, position + 1)
.put(TOTAL_COUNT_KEY, count)
.format()
} else {
// If there are no results we don't display anything if the query is
// empty, but we'll substitute "No results found for <query>" otherwise.
var txt = ""
if (query != null) {
if (query.isNotEmpty()) {
txt = Phrase.from(context, R.string.searchMatchesNoneSpecific)
.put(QUERY_KEY, query)
.format().toString()
}
}
searchPosition.text = txt
}
setViewEnabled(searchUp, position < count - 1)
setViewEnabled(searchDown, position > 0)
}
*/
fun showLoading() { fun showLoading() {
binding.searchProgressWheel.visibility = VISIBLE binding.searchProgressWheel.visibility = VISIBLE
} }

View File

@ -94,6 +94,8 @@ class SearchViewModel @Inject constructor(
} }
} }
public fun getActiveQuery() = activeQuery
class SearchResult(private val results: CursorList<MessageResult?>, val position: Int) : Closeable { class SearchResult(private val results: CursorList<MessageResult?>, val position: Int) : Closeable {
fun getResults(): List<MessageResult?> { fun getResults(): List<MessageResult?> {

View File

@ -16,6 +16,8 @@
*/ */
package org.thoughtcrime.securesms.conversation.v2.utilities; package org.thoughtcrime.securesms.conversation.v2.utilities;
import static org.session.libsession.utilities.StringSubstitutionConstants.APP_NAME_KEY;
import android.Manifest; import android.Manifest;
import android.annotation.SuppressLint; import android.annotation.SuppressLint;
import android.app.Activity; import android.app.Activity;
@ -30,10 +32,14 @@ import android.provider.OpenableColumns;
import android.text.TextUtils; import android.text.TextUtils;
import android.util.Pair; import android.util.Pair;
import android.widget.Toast; import android.widget.Toast;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable; 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.libsession.utilities.recipients.Recipient;
import org.session.libsignal.utilities.ListenableFuture; import org.session.libsignal.utilities.ListenableFuture;
import org.session.libsignal.utilities.Log; import org.session.libsignal.utilities.Log;
@ -44,7 +50,7 @@ import org.thoughtcrime.securesms.mediasend.MediaSendActivity;
import org.thoughtcrime.securesms.mms.AudioSlide; import org.thoughtcrime.securesms.mms.AudioSlide;
import org.thoughtcrime.securesms.mms.DocumentSlide; import org.thoughtcrime.securesms.mms.DocumentSlide;
import org.thoughtcrime.securesms.mms.GifSlide; import org.thoughtcrime.securesms.mms.GifSlide;
import org.thoughtcrime.securesms.mms.GlideRequests; import com.bumptech.glide.RequestManager;
import org.thoughtcrime.securesms.mms.ImageSlide; import org.thoughtcrime.securesms.mms.ImageSlide;
import org.thoughtcrime.securesms.mms.MediaConstraints; import org.thoughtcrime.securesms.mms.MediaConstraints;
import org.thoughtcrime.securesms.mms.PartAuthority; import org.thoughtcrime.securesms.mms.PartAuthority;
@ -55,13 +61,6 @@ import org.thoughtcrime.securesms.permissions.Permissions;
import org.thoughtcrime.securesms.providers.BlobProvider; import org.thoughtcrime.securesms.providers.BlobProvider;
import org.thoughtcrime.securesms.util.MediaUtil; 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 { public class AttachmentManager {
private final static String TAG = AttachmentManager.class.getSimpleName(); private final static String TAG = AttachmentManager.class.getSimpleName();
@ -126,7 +125,7 @@ public class AttachmentManager {
} }
@SuppressLint("StaticFieldLeak") @SuppressLint("StaticFieldLeak")
public ListenableFuture<Boolean> setMedia(@NonNull final GlideRequests glideRequests, public ListenableFuture<Boolean> setMedia(@NonNull final RequestManager glideRequests,
@NonNull final Uri uri, @NonNull final Uri uri,
@NonNull final MediaType mediaType, @NonNull final MediaType mediaType,
@NonNull final MediaConstraints constraints, @NonNull final MediaConstraints constraints,
@ -252,13 +251,31 @@ public class AttachmentManager {
} else { } else {
builder = builder.request(Manifest.permission.READ_EXTERNAL_STORAGE); builder = builder.request(Manifest.permission.READ_EXTERNAL_STORAGE);
} }
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) Context c = activity.getApplicationContext();
String needStoragePermissionTxt = Phrase.from(c, R.string.permissionsStorageSend)
.put(APP_NAME_KEY, c.getString(R.string.app_name))
.format().toString();
String storagePermissionDeniedTxt = Phrase.from(c, R.string.cameraGrantAccessStorageDenied)
.put(APP_NAME_KEY, c.getString(R.string.app_name))
.format().toString();
builder.withPermanentDenialDialog(needStoragePermissionTxt)
.withRationaleDialog(storagePermissionDeniedTxt, 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. .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(); .execute();
} }
public static void selectGallery(Activity activity, int requestCode, @NonNull Recipient recipient, @NonNull String body) { public static void selectGallery(Activity activity, int requestCode, @NonNull Recipient recipient, @NonNull String body) {
Context c = activity.getApplicationContext();
String needStoragePermissionTxt = Phrase.from(c, R.string.permissionsStorageSend)
.put(APP_NAME_KEY, c.getString(R.string.app_name))
.format().toString();
String cameraPermissionDeniedTxt = Phrase.from(c, R.string.cameraGrantAccessDenied)
.put(APP_NAME_KEY, c.getString(R.string.app_name))
.format().toString();
Permissions.PermissionsBuilder builder = Permissions.with(activity); Permissions.PermissionsBuilder builder = Permissions.with(activity);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
builder = builder.request(Manifest.permission.READ_MEDIA_VIDEO) builder = builder.request(Manifest.permission.READ_MEDIA_VIDEO)
@ -266,8 +283,8 @@ public class AttachmentManager {
} else { } else {
builder = builder.request(Manifest.permission.READ_EXTERNAL_STORAGE); builder = builder.request(Manifest.permission.READ_EXTERNAL_STORAGE);
} }
builder.withPermanentDenialDialog(activity.getString(R.string.AttachmentManager_signal_requires_the_external_storage_permission_in_order_to_attach_photos_videos_or_audio)) builder.withPermanentDenialDialog(cameraPermissionDeniedTxt)
.withRationaleDialog(activity.getString(R.string.ConversationActivity_to_send_photos_and_video_allow_signal_access_to_storage), R.drawable.ic_baseline_photo_library_24) .withRationaleDialog(needStoragePermissionTxt, R.drawable.ic_baseline_photo_library_24)
.onAllGranted(() -> activity.startActivityForResult(MediaSendActivity.buildGalleryIntent(activity, recipient, body), requestCode)) .onAllGranted(() -> activity.startActivityForResult(MediaSendActivity.buildGalleryIntent(activity, recipient, body), requestCode))
.execute(); .execute();
} }
@ -291,10 +308,19 @@ public class AttachmentManager {
} }
public void capturePhoto(Activity activity, int requestCode, Recipient recipient) { public void capturePhoto(Activity activity, int requestCode, Recipient recipient) {
String cameraPermissionDeniedTxt = Phrase.from(context, R.string.cameraGrantAccessDenied)
.put(APP_NAME_KEY, context.getString(R.string.app_name))
.format().toString();
String requireCameraPermissionTxt = Phrase.from(context, R.string.cameraGrantAccessDescription)
.put(APP_NAME_KEY, context.getString(R.string.app_name))
.format().toString();
Permissions.with(activity) Permissions.with(activity)
.request(Manifest.permission.CAMERA) .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)) .withPermanentDenialDialog(cameraPermissionDeniedTxt)
.withRationaleDialog(activity.getString(R.string.ConversationActivity_to_capture_photos_and_video_allow_signal_access_to_the_camera),R.drawable.ic_baseline_photo_camera_24) .withRationaleDialog(requireCameraPermissionTxt, R.drawable.ic_baseline_photo_camera_24)
.onAllGranted(() -> { .onAllGranted(() -> {
Intent captureIntent = MediaSendActivity.buildCameraIntent(activity, recipient); Intent captureIntent = MediaSendActivity.buildCameraIntent(activity, recipient);
if (captureIntent.resolveActivity(activity.getPackageManager()) != null) { if (captureIntent.resolveActivity(activity.getPackageManager()) != null) {
@ -326,7 +352,7 @@ public class AttachmentManager {
activity.startActivityForResult(intent, requestCode); activity.startActivityForResult(intent, requestCode);
} catch (ActivityNotFoundException anfe) { } catch (ActivityNotFoundException anfe) {
Log.w(TAG, "couldn't complete ACTION_GET_CONTENT intent, no activity found. falling back."); 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();
} }
} }

View File

@ -54,13 +54,14 @@ object MentionUtilities {
val userPublicKey = TextSecurePreferences.getLocalNumber(context)!! val userPublicKey = TextSecurePreferences.getLocalNumber(context)!!
val openGroup by lazy { DatabaseComponent.get(context).storage().getOpenGroup(threadID) } val openGroup by lazy { DatabaseComponent.get(context).storage().getOpenGroup(threadID) }
// format the mention text // Format the mention text
if (matcher.find(startIndex)) { if (matcher.find(startIndex)) {
while (true) { while (true) {
val publicKey = text.subSequence(matcher.start() + 1, matcher.end()).toString() // +1 to get rid of the @ val publicKey = text.subSequence(matcher.start() + 1, matcher.end()).toString() // +1 to get rid of the @
val isYou = isYou(publicKey, userPublicKey, openGroup) val isYou = isYou(publicKey, userPublicKey, openGroup)
val userDisplayName: String? = if (isYou) { val userDisplayName: String? = if (isYou) {
context.getString(R.string.MessageRecord_you) context.getString(R.string.you)
} else { } else {
val contact = DatabaseComponent.get(context).sessionContactDatabase().getContactWithAccountID(publicKey) val contact = DatabaseComponent.get(context).sessionContactDatabase().getContactWithAccountID(publicKey)
@Suppress("NAME_SHADOWING") val context = if (openGroup != null) Contact.ContactContext.OPEN_GROUP else Contact.ContactContext.REGULAR @Suppress("NAME_SHADOWING") val context = if (openGroup != null) Contact.ContactContext.OPEN_GROUP else Contact.ContactContext.REGULAR

View File

@ -8,7 +8,7 @@ import org.thoughtcrime.securesms.showSessionDialog
object NotificationUtils { object NotificationUtils {
fun showNotifyDialog(context: Context, thread: Recipient, notifyTypeHandler: (Int)->Unit) { fun showNotifyDialog(context: Context, thread: Recipient, notifyTypeHandler: (Int)->Unit) {
context.showSessionDialog { context.showSessionDialog {
title(R.string.RecipientPreferenceActivity_notification_settings) title(R.string.sessionNotifications)
singleChoiceItems( singleChoiceItems(
context.resources.getStringArray(R.array.notify_types), context.resources.getStringArray(R.array.notify_types),
thread.notifyType thread.notifyType

View File

@ -25,8 +25,8 @@ import org.session.libsignal.utilities.SettableFuture
import org.thoughtcrime.securesms.components.GlideBitmapListeningTarget import org.thoughtcrime.securesms.components.GlideBitmapListeningTarget
import org.thoughtcrime.securesms.components.GlideDrawableListeningTarget import org.thoughtcrime.securesms.components.GlideDrawableListeningTarget
import org.thoughtcrime.securesms.mms.DecryptableStreamUriLoader.DecryptableUri import org.thoughtcrime.securesms.mms.DecryptableStreamUriLoader.DecryptableUri
import org.thoughtcrime.securesms.mms.GlideRequest import com.bumptech.glide.RequestBuilder
import org.thoughtcrime.securesms.mms.GlideRequests import com.bumptech.glide.RequestManager
import org.thoughtcrime.securesms.mms.Slide import org.thoughtcrime.securesms.mms.Slide
open class ThumbnailView @JvmOverloads constructor( open class ThumbnailView @JvmOverloads constructor(
@ -104,13 +104,13 @@ open class ThumbnailView @JvmOverloads constructor(
} }
fun setImageResource( fun setImageResource(
glide: GlideRequests, glide: RequestManager,
slide: Slide, slide: Slide,
isPreview: Boolean isPreview: Boolean
): ListenableFuture<Boolean> = setImageResource(glide, slide, isPreview, 0, 0) ): ListenableFuture<Boolean> = setImageResource(glide, slide, isPreview, 0, 0)
fun setImageResource( fun setImageResource(
glide: GlideRequests, slide: Slide, glide: RequestManager, slide: Slide,
isPreview: Boolean, naturalWidth: Int, isPreview: Boolean, naturalWidth: Int,
naturalHeight: Int naturalHeight: Int
): ListenableFuture<Boolean> { ): ListenableFuture<Boolean> {
@ -152,9 +152,9 @@ open class ThumbnailView @JvmOverloads constructor(
} }
private fun buildThumbnailGlideRequest( private fun buildThumbnailGlideRequest(
glide: GlideRequests, glide: RequestManager,
slide: Slide slide: Slide
): GlideRequest<Drawable> = glide.load(DecryptableUri(slide.thumbnailUri!!)) ): RequestBuilder<Drawable> = glide.load(DecryptableUri(slide.thumbnailUri!!))
.diskCacheStrategy(DiskCacheStrategy.NONE) .diskCacheStrategy(DiskCacheStrategy.NONE)
.overrideDimensions() .overrideDimensions()
.transition(DrawableTransitionOptions.withCrossFade()) .transition(DrawableTransitionOptions.withCrossFade())
@ -162,21 +162,21 @@ open class ThumbnailView @JvmOverloads constructor(
.missingThumbnailPicture(slide.isInProgress) .missingThumbnailPicture(slide.isInProgress)
private fun buildPlaceholderGlideRequest( private fun buildPlaceholderGlideRequest(
glide: GlideRequests, glide: RequestManager,
slide: Slide slide: Slide
): GlideRequest<Bitmap> = glide.asBitmap() ): RequestBuilder<Bitmap> = glide.asBitmap()
.load(slide.getPlaceholderRes(context.theme)) .load(slide.getPlaceholderRes(context.theme))
.diskCacheStrategy(DiskCacheStrategy.NONE) .diskCacheStrategy(DiskCacheStrategy.NONE)
.overrideDimensions() .overrideDimensions()
.fitCenter() .fitCenter()
open fun clear(glideRequests: GlideRequests) { open fun clear(glideRequests: RequestManager) {
glideRequests.clear(binding.thumbnailImage) glideRequests.clear(binding.thumbnailImage)
slide = null slide = null
} }
fun setImageResource( fun setImageResource(
glideRequests: GlideRequests, glideRequests: RequestManager,
uri: Uri uri: Uri
): ListenableFuture<Boolean> = glideRequests.load(DecryptableUri(uri)) ): ListenableFuture<Boolean> = glideRequests.load(DecryptableUri(uri))
.diskCacheStrategy(DiskCacheStrategy.NONE) .diskCacheStrategy(DiskCacheStrategy.NONE)
@ -184,19 +184,19 @@ open class ThumbnailView @JvmOverloads constructor(
.transform(CenterCrop()) .transform(CenterCrop())
.intoDrawableTargetAsFuture() .intoDrawableTargetAsFuture()
private fun GlideRequest<Drawable>.intoDrawableTargetAsFuture() = private fun RequestBuilder<Drawable>.intoDrawableTargetAsFuture() =
SettableFuture<Boolean>().also { SettableFuture<Boolean>().also {
binding.run { binding.run {
GlideDrawableListeningTarget(thumbnailImage, thumbnailLoadIndicator, it) GlideDrawableListeningTarget(thumbnailImage, thumbnailLoadIndicator, it)
}.let { into(it) } }.let { into(it) }
} }
private fun <T> GlideRequest<T>.overrideDimensions() = private fun <T> RequestBuilder<T>.overrideDimensions() =
dimensDelegate.resourceSize().takeIf { 0 !in it } dimensDelegate.resourceSize().takeIf { 0 !in it }
?.let { override(it[WIDTH], it[HEIGHT]) } ?.let { override(it[WIDTH], it[HEIGHT]) }
?: override(getDefaultWidth(), getDefaultHeight()) ?: override(getDefaultWidth(), getDefaultHeight())
} }
private fun <T> GlideRequest<T>.missingThumbnailPicture( private fun <T> RequestBuilder<T>.missingThumbnailPicture(
inProgress: Boolean inProgress: Boolean
) = takeIf { inProgress } ?: apply(RequestOptions.errorOf(R.drawable.ic_missing_thumbnail_picture)) ) = takeIf { inProgress } ?: apply(RequestOptions.errorOf(R.drawable.ic_missing_thumbnail_picture))

View File

@ -3,14 +3,8 @@ package org.thoughtcrime.securesms.database;
import android.content.ContentValues; import android.content.ContentValues;
import android.content.Context; import android.content.Context;
import android.database.Cursor; import android.database.Cursor;
import android.net.Uri;
import androidx.annotation.Nullable;
import net.zetetic.database.sqlcipher.SQLiteDatabase; import net.zetetic.database.sqlcipher.SQLiteDatabase;
import network.loki.messenger.R;
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper; import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper;
import java.util.LinkedList; import java.util.LinkedList;
import java.util.List; import java.util.List;
import java.util.Set; import java.util.Set;
@ -24,10 +18,10 @@ public class DraftDatabase extends Database {
public static final String DRAFT_VALUE = "value"; public static final String DRAFT_VALUE = "value";
public static final String CREATE_TABLE = "CREATE TABLE " + TABLE_NAME + " (" + ID + " INTEGER PRIMARY KEY, " + 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 = { 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) { public DraftDatabase(Context context, SQLCipherOpenHelper databaseHelper) {
@ -59,8 +53,8 @@ public class DraftDatabase extends Database {
for (long threadId : threadIds) { for (long threadId : threadIds) {
where.append(" OR ") where.append(" OR ")
.append(THREAD_ID) .append(THREAD_ID)
.append(" = ?"); .append(" = ?");
arguments.add(String.valueOf(threadId)); 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 class Draft {
public static final String TEXT = "text"; 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";
private final String type; private final String type;
private final String value; private final String value;
@ -117,48 +109,10 @@ public class DraftDatabase extends Database {
public String getValue() { public String getValue() {
return value; 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<Draft> { public static class Drafts extends LinkedList<Draft> {
private Draft getDraftOfType(String type) { // We don't do anything with drafts of a given type anymore (image, audio etc.) - we store TEXT
for (Draft draft : this) { // drafts, and any files or audio get sent to the recipient when added as a message.
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;
}
} }
} }

View File

@ -166,6 +166,8 @@ class LokiAPIDatabase(context: Context, helper: SQLCipherOpenHelper) : Database(
const val RESET_SEQ_NO = "UPDATE $lastMessageServerIDTable SET $lastMessageServerID = 0;" const val RESET_SEQ_NO = "UPDATE $lastMessageServerIDTable SET $lastMessageServerID = 0;"
const val EMPTY_VERSION = "0.0.0"
// endregion // endregion
} }
@ -179,7 +181,8 @@ class LokiAPIDatabase(context: Context, helper: SQLCipherOpenHelper) : Database(
val port = components.getOrNull(1)?.toIntOrNull() ?: return@mapNotNull null val port = components.getOrNull(1)?.toIntOrNull() ?: return@mapNotNull null
val ed25519Key = components.getOrNull(2) ?: return@mapNotNull null val ed25519Key = components.getOrNull(2) ?: return@mapNotNull null
val x25519Key = components.getOrNull(3) ?: return@mapNotNull null val x25519Key = components.getOrNull(3) ?: return@mapNotNull null
Snode(address, port, Snode.KeySet(ed25519Key, x25519Key)) val version = components.getOrNull(4) ?: EMPTY_VERSION
Snode(address, port, Snode.KeySet(ed25519Key, x25519Key), version)
} }
}?.toSet() ?: setOf() }?.toSet() ?: setOf()
} }
@ -192,6 +195,7 @@ class LokiAPIDatabase(context: Context, helper: SQLCipherOpenHelper) : Database(
if (keySet != null) { if (keySet != null) {
string += "-${keySet.ed25519Key}-${keySet.x25519Key}" string += "-${keySet.ed25519Key}-${keySet.x25519Key}"
} }
string += "-${snode.version}"
string string
} }
val row = wrap(mapOf( Companion.dummyKey to "dummy_key", snodePool to snodePoolAsString )) val row = wrap(mapOf( Companion.dummyKey to "dummy_key", snodePool to snodePoolAsString ))
@ -207,6 +211,7 @@ class LokiAPIDatabase(context: Context, helper: SQLCipherOpenHelper) : Database(
if (keySet != null) { if (keySet != null) {
snodeAsString += "-${keySet.ed25519Key}-${keySet.x25519Key}" snodeAsString += "-${keySet.ed25519Key}-${keySet.x25519Key}"
} }
snodeAsString += "-${snode.version}"
val row = wrap(mapOf( Companion.indexPath to indexPath, Companion.snode to snodeAsString )) val row = wrap(mapOf( Companion.indexPath to indexPath, Companion.snode to snodeAsString ))
database.insertOrUpdate(onionRequestPathTable, row, "${Companion.indexPath} = ?", wrap(indexPath)) database.insertOrUpdate(onionRequestPathTable, row, "${Companion.indexPath} = ?", wrap(indexPath))
} }
@ -232,8 +237,9 @@ class LokiAPIDatabase(context: Context, helper: SQLCipherOpenHelper) : Database(
val port = components.getOrNull(1)?.toIntOrNull() val port = components.getOrNull(1)?.toIntOrNull()
val ed25519Key = components.getOrNull(2) val ed25519Key = components.getOrNull(2)
val x25519Key = components.getOrNull(3) val x25519Key = components.getOrNull(3)
val version = components.getOrNull(4) ?: EMPTY_VERSION
if (port != null && ed25519Key != null && x25519Key != null) { if (port != null && ed25519Key != null && x25519Key != null) {
Snode(address, port, Snode.KeySet(ed25519Key, x25519Key)) Snode(address, port, Snode.KeySet(ed25519Key, x25519Key), version)
} else { } else {
null null
} }
@ -251,6 +257,11 @@ class LokiAPIDatabase(context: Context, helper: SQLCipherOpenHelper) : Database(
return result return result
} }
override fun clearSnodePool() {
val database = databaseHelper.writableDatabase
database.delete(snodePoolTable, null, null)
}
override fun clearOnionRequestPaths() { override fun clearOnionRequestPaths() {
val database = databaseHelper.writableDatabase val database = databaseHelper.writableDatabase
fun delete(indexPath: String) { fun delete(indexPath: String) {
@ -271,7 +282,8 @@ class LokiAPIDatabase(context: Context, helper: SQLCipherOpenHelper) : Database(
val port = components.getOrNull(1)?.toIntOrNull() ?: return@mapNotNull null val port = components.getOrNull(1)?.toIntOrNull() ?: return@mapNotNull null
val ed25519Key = components.getOrNull(2) ?: return@mapNotNull null val ed25519Key = components.getOrNull(2) ?: return@mapNotNull null
val x25519Key = components.getOrNull(3) ?: return@mapNotNull null val x25519Key = components.getOrNull(3) ?: return@mapNotNull null
Snode(address, port, Snode.KeySet(ed25519Key, x25519Key)) val version = components.getOrNull(4) ?: EMPTY_VERSION
Snode(address, port, Snode.KeySet(ed25519Key, x25519Key), version)
} }
}?.toSet() }?.toSet()
} }

View File

@ -808,8 +808,8 @@ public class ThreadDatabase extends Database {
private @NonNull String getFormattedBodyFor(@NonNull MessageRecord messageRecord) { private @NonNull String getFormattedBodyFor(@NonNull MessageRecord messageRecord) {
if (messageRecord.isMms()) { if (messageRecord.isMms()) {
MmsMessageRecord record = (MmsMessageRecord) messageRecord; MmsMessageRecord record = (MmsMessageRecord) messageRecord;
if (record.getSharedContacts().size() > 0) { if (!record.getSharedContacts().isEmpty()) {
Contact contact = ((MmsMessageRecord) messageRecord).getSharedContacts().get(0); Contact contact = ((MmsMessageRecord)messageRecord).getSharedContacts().get(0);
return ContactUtil.getStringSummary(context, contact).toString(); return ContactUtil.getStringSummary(context, contact).toString();
} }
String attachmentString = record.getSlideDeck().getBody(); String attachmentString = record.getSlideDeck().getBody();

View File

@ -250,7 +250,7 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper {
// Notify the user of the issue so they know they can downgrade until the issue is fixed // Notify the user of the issue so they know they can downgrade until the issue is fixed
NotificationManager notificationManager = context.getSystemService(NotificationManager.class); NotificationManager notificationManager = context.getSystemService(NotificationManager.class);
String channelId = context.getString(R.string.NotificationChannel_failures); String channelId = context.getString(R.string.failures);
if (NotificationChannels.supported()) { if (NotificationChannels.supported()) {
NotificationChannel channel = new NotificationChannel(channelId, channelId, NotificationManager.IMPORTANCE_HIGH); NotificationChannel channel = new NotificationChannel(channelId, channelId, NotificationManager.IMPORTANCE_HIGH);

View File

@ -14,6 +14,8 @@ import org.session.libsession.utilities.Address;
import org.session.libsession.utilities.recipients.Recipient; import org.session.libsession.utilities.recipients.Recipient;
import org.thoughtcrime.securesms.database.MediaDatabase; import org.thoughtcrime.securesms.database.MediaDatabase;
import org.thoughtcrime.securesms.dependencies.DatabaseComponent; import org.thoughtcrime.securesms.dependencies.DatabaseComponent;
import org.thoughtcrime.securesms.util.DateUtils;
import org.thoughtcrime.securesms.util.RelativeDay;
import java.text.SimpleDateFormat; import java.text.SimpleDateFormat;
import java.util.ArrayList; import java.util.ArrayList;
@ -88,15 +90,17 @@ public class BucketedThreadMediaLoader extends AsyncTaskLoader<BucketedThreadMed
private final TimeBucket[] TIME_SECTIONS; private final TimeBucket[] TIME_SECTIONS;
public BucketedThreadMedia(@NonNull Context context) { 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)); String localisedTodayString = DateUtils.INSTANCE.getLocalisedRelativeDayString(RelativeDay.TODAY);
this.YESTERDAY = new TimeBucket(context.getString(R.string.BucketedThreadMedia_Yesterday), TimeBucket.addToCalendar(Calendar.DAY_OF_YEAR, -2), TimeBucket.addToCalendar(Calendar.DAY_OF_YEAR, -1)); String localisedYesterdayString = DateUtils.INSTANCE.getLocalisedRelativeDayString(RelativeDay.YESTERDAY);
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();
}
this.TODAY = new TimeBucket(localisedTodayString, TimeBucket.addToCalendar(Calendar.DAY_OF_YEAR, -1), TimeBucket.addToCalendar(Calendar.DAY_OF_YEAR, 1000));
this.YESTERDAY = new TimeBucket(localisedYesterdayString, TimeBucket.addToCalendar(Calendar.DAY_OF_YEAR, -2), TimeBucket.addToCalendar(Calendar.DAY_OF_YEAR, -1));
this.THIS_WEEK = new TimeBucket(context.getString(R.string.attachmentsThisWeek), TimeBucket.addToCalendar(Calendar.DAY_OF_YEAR, -7), TimeBucket.addToCalendar(Calendar.DAY_OF_YEAR, -2));
this.THIS_MONTH = new TimeBucket(context.getString(R.string.attachmentsThisMonth), 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) { public void add(MediaDatabase.MediaRecord mediaRecord) {
for (TimeBucket timeSection : TIME_SECTIONS) { for (TimeBucket timeSection : TIME_SECTIONS) {

View File

@ -77,14 +77,6 @@ public class MediaMmsMessageRecord extends MmsMessageRecord {
@Override @Override
public SpannableString getDisplayBody(@NonNull Context context) { 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));
}
return super.getDisplayBody(context); return super.getDisplayBody(context);
} }
} }

View File

@ -57,15 +57,7 @@ public class SmsMessageRecord extends MessageRecord {
@Override @Override
public SpannableString getDisplayBody(@NonNull Context context) { public SpannableString getDisplayBody(@NonNull Context context) {
if (SmsDatabase.Types.isFailedDecryptType(type)) { return super.getDisplayBody(context);
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);
}
} }
@Override @Override

View File

@ -17,21 +17,25 @@
*/ */
package org.thoughtcrime.securesms.database.model; package org.thoughtcrime.securesms.database.model;
import static org.session.libsession.utilities.StringSubstitutionConstants.APP_NAME_KEY;
import static org.session.libsession.utilities.StringSubstitutionConstants.DISAPPEARING_MESSAGES_TYPE_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.content.Context;
import android.net.Uri; import android.net.Uri;
import android.text.Spannable; import android.text.Spannable;
import android.text.SpannableString; import android.text.SpannableString;
import android.text.TextUtils; import android.text.TextUtils;
import android.text.style.StyleSpan; import android.text.style.StyleSpan;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import com.squareup.phrase.Phrase;
import org.session.libsession.utilities.ExpirationUtil; import org.session.libsession.utilities.ExpirationUtil;
import org.session.libsession.utilities.recipients.Recipient; import org.session.libsession.utilities.recipients.Recipient;
import org.session.libsignal.utilities.Log;
import org.thoughtcrime.securesms.database.MmsSmsColumns; import org.thoughtcrime.securesms.database.MmsSmsColumns;
import org.thoughtcrime.securesms.database.SmsDatabase; import org.thoughtcrime.securesms.database.SmsDatabase;
import network.loki.messenger.R; import network.loki.messenger.R;
/** /**
@ -42,146 +46,174 @@ import network.loki.messenger.R;
*/ */
public class ThreadRecord extends DisplayRecord { public class ThreadRecord extends DisplayRecord {
private @Nullable final Uri snippetUri; private @Nullable final Uri snippetUri;
public @Nullable final MessageRecord lastMessage; public @Nullable final MessageRecord lastMessage;
private final long count; private final long count;
private final int unreadCount; private final int unreadCount;
private final int unreadMentionCount; private final int unreadMentionCount;
private final int distributionType; private final int distributionType;
private final boolean archived; private final boolean archived;
private final long expiresIn; private final long expiresIn;
private final long lastSeen; private final long lastSeen;
private final boolean pinned; private final boolean pinned;
private final int initialRecipientHash; private final int initialRecipientHash;
private final long dateSent;
public ThreadRecord(@NonNull String body, @Nullable Uri snippetUri, public ThreadRecord(@NonNull String body, @Nullable Uri snippetUri,
@Nullable MessageRecord lastMessage, @NonNull Recipient recipient, long date, long count, int unreadCount, @Nullable MessageRecord lastMessage, @NonNull Recipient recipient, long date, long count, int unreadCount,
int unreadMentionCount, long threadId, int deliveryReceiptCount, int status, int unreadMentionCount, long threadId, int deliveryReceiptCount, int status,
long snippetType, int distributionType, boolean archived, long expiresIn, long snippetType, int distributionType, boolean archived, long expiresIn,
long lastSeen, int readReceiptCount, boolean pinned) long lastSeen, int readReceiptCount, boolean pinned)
{ {
super(body, recipient, date, date, threadId, status, deliveryReceiptCount, snippetType, readReceiptCount); super(body, recipient, date, date, threadId, status, deliveryReceiptCount, snippetType, readReceiptCount);
this.snippetUri = snippetUri; this.snippetUri = snippetUri;
this.lastMessage = lastMessage; this.lastMessage = lastMessage;
this.count = count; this.count = count;
this.unreadCount = unreadCount; this.unreadCount = unreadCount;
this.unreadMentionCount = unreadMentionCount; this.unreadMentionCount = unreadMentionCount;
this.distributionType = distributionType; this.distributionType = distributionType;
this.archived = archived; this.archived = archived;
this.expiresIn = expiresIn; this.expiresIn = expiresIn;
this.lastSeen = lastSeen; this.lastSeen = lastSeen;
this.pinned = pinned; this.pinned = pinned;
this.initialRecipientHash = recipient.hashCode(); this.initialRecipientHash = recipient.hashCode();
} this.dateSent = date;
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());
}
} }
}
private SpannableString emphasisAdded(String sequence) { public @Nullable Uri getSnippetUri() {
return emphasisAdded(sequence, 0, sequence.length()); return snippetUri;
} }
private SpannableString emphasisAdded(String sequence, int start, int end) { private String getName() {
SpannableString spannable = new SpannableString(sequence); String name = getRecipient().getName();
spannable.setSpan(new StyleSpan(android.graphics.Typeface.ITALIC), if (name == null) {
start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); Log.w("ThreadRecord", "Got a null name - using: Unknown");
return spannable; name = "Unknown";
} }
return name;
}
public long getCount() { private String getDisappearingMsgExpiryTypeString(Context context) {
return count; MessageRecord lm = this.lastMessage;
} if (lm == null) {
Log.w("ThreadRecord", "Could not get last message to determine disappearing msg type.");
return "Unknown";
}
long expireStarted = lm.getExpireStarted();
public int getUnreadCount() { // Note: This works because expireStarted is 0 for messages which are 'Disappear after read'
return unreadCount; // while it's a touch higher than the sent timestamp for "Disappear after send". We could then
} // use `expireStarted == 0`, but that's not how it's done in UpdateMessageBuilder so to keep
// things the same I'll assume there's a reason for this and follow suit.
// Also: `this.lastMessage.getExpiresIn()` is available.
if (expireStarted >= dateSent) {
return context.getString(R.string.disappearingMessagesSent);
}
return context.getString(R.string.disappearingMessagesRead);
}
public int getUnreadMentionCount() { @Override
return unreadMentionCount; public SpannableString getDisplayBody(@NonNull Context context) {
} if (isGroupUpdateMessage()) {
return emphasisAdded(context.getString(R.string.groupUpdated));
} else if (isOpenGroupInvitation()) {
return emphasisAdded(context.getString(R.string.communityInvitation));
} else if (MmsSmsColumns.Types.isLegacyType(type)) {
String txt = Phrase.from(context, R.string.messageErrorOld)
.put(APP_NAME_KEY, context.getString(R.string.app_name))
.format().toString();
return emphasisAdded(txt);
} else if (MmsSmsColumns.Types.isDraftMessageType(type)) {
String draftText = context.getString(R.string.draft);
return emphasisAdded(draftText + " " + getBody(), 0, draftText.length());
} else if (SmsDatabase.Types.isOutgoingCall(type)) {
String txt = Phrase.from(context, R.string.callsYouCalled)
.put(NAME_KEY, getName())
.format().toString();
return emphasisAdded(txt);
} else if (SmsDatabase.Types.isIncomingCall(type)) {
String txt = Phrase.from(context, R.string.callsCalledYou)
.put(NAME_KEY, getName())
.format().toString();
return emphasisAdded(txt);
} else if (SmsDatabase.Types.isMissedCall(type)) {
String txt = Phrase.from(context, R.string.callsMissedCallFrom)
.put(NAME_KEY, getName())
.format().toString();
return emphasisAdded(txt);
} else if (SmsDatabase.Types.isExpirationTimerUpdate(type)) {
int seconds = (int) (getExpiresIn() / 1000);
if (seconds <= 0) {
String txt = Phrase.from(context, R.string.disappearingMessagesTurnedOff)
.put(NAME_KEY, getName())
.format().toString();
return emphasisAdded(txt);
}
public long getDate() { // Implied that disappearing messages is enabled..
return getDateReceived(); String time = ExpirationUtil.getExpirationDisplayValue(context, seconds);
} String disappearAfterWhat = getDisappearingMsgExpiryTypeString(context); // Disappear after send or read?
String txt = Phrase.from(context, R.string.disappearingMessagesSet)
.put(NAME_KEY, getName())
.put(TIME_KEY, time)
.put(DISAPPEARING_MESSAGES_TYPE_KEY, disappearAfterWhat)
.format().toString();
return emphasisAdded(txt);
public boolean isArchived() { } else if (MmsSmsColumns.Types.isMediaSavedExtraction(type)) {
return archived; String txt = Phrase.from(context, R.string.attachmentsMediaSaved)
} .put(NAME_KEY, getName())
.format().toString();
return emphasisAdded(txt);
public int getDistributionType() { } else if (MmsSmsColumns.Types.isScreenshotExtraction(type)) {
return distributionType; String txt = Phrase.from(context, R.string.screenshotTaken)
} .put(NAME_KEY, getName())
.format().toString();
return emphasisAdded(txt);
public long getExpiresIn() { } else if (MmsSmsColumns.Types.isMessageRequestResponse(type)) {
return expiresIn; return emphasisAdded(context.getString(R.string.messageRequestsAccepted));
} } else if (getCount() == 0) {
return new SpannableString(context.getString(R.string.messageEmpty));
} else {
// This is shown when we receive a media message from an un-accepted contact
if (TextUtils.isEmpty(getBody())) {
return new SpannableString(emphasisAdded(context.getString(R.string.mediaMessage)));
} else {
return new SpannableString(getBody());
}
}
}
public long getLastSeen() { private SpannableString emphasisAdded(String sequence) {
return lastSeen; return emphasisAdded(sequence, 0, sequence.length());
} }
public boolean isPinned() { private SpannableString emphasisAdded(String sequence, int start, int end) {
return pinned; SpannableString spannable = new SpannableString(sequence);
} spannable.setSpan(new StyleSpan(android.graphics.Typeface.ITALIC),
start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
return spannable;
}
public int getInitialRecipientHash() { public long getCount() { return count; }
return initialRecipientHash;
} public int getUnreadCount() { return unreadCount; }
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; }
} }

View File

@ -0,0 +1,101 @@
package org.thoughtcrime.securesms.dms
import android.content.ClipData
import android.content.ClipboardManager
import android.content.Context
import android.content.Intent
import android.os.Bundle
import android.text.InputType
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.view.inputmethod.EditorInfo
import android.widget.Toast
import androidx.core.view.isVisible
import androidx.core.widget.addTextChangedListener
import androidx.fragment.app.Fragment
import network.loki.messenger.R
import network.loki.messenger.databinding.FragmentEnterPublicKeyBinding
import org.session.libsession.utilities.TextSecurePreferences
import org.thoughtcrime.securesms.util.QRCodeUtilities
import org.thoughtcrime.securesms.util.hideKeyboard
import org.thoughtcrime.securesms.util.toPx
class EnterPublicKeyFragment : Fragment() {
private lateinit var binding: FragmentEnterPublicKeyBinding
var delegate: EnterPublicKeyDelegate? = null
private val hexEncodedPublicKey: String
get() {
return TextSecurePreferences.getLocalNumber(requireContext())!!
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
binding = FragmentEnterPublicKeyBinding.inflate(inflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
with(binding) {
publicKeyEditText.imeOptions = EditorInfo.IME_ACTION_DONE or 16777216 // Always use incognito keyboard
publicKeyEditText.setRawInputType(InputType.TYPE_CLASS_TEXT)
publicKeyEditText.setOnEditorActionListener { v, actionID, _ ->
if (actionID == EditorInfo.IME_ACTION_DONE) {
v.hideKeyboard()
handlePublicKeyEntered()
true
} else {
false
}
}
publicKeyEditText.addTextChangedListener { text -> createPrivateChatButton.isVisible = !text.isNullOrBlank() }
publicKeyEditText.setOnFocusChangeListener { _, hasFocus -> optionalContentContainer.isVisible = !hasFocus }
mainContainer.setOnTouchListener { _, _ ->
binding.optionalContentContainer.isVisible = true
publicKeyEditText.clearFocus()
publicKeyEditText.hideKeyboard()
true
}
val size = toPx(228, resources)
val qrCode = QRCodeUtilities.encode(hexEncodedPublicKey, size, isInverted = false, hasTransparentBackground = false)
qrCodeImageView.setImageBitmap(qrCode)
publicKeyTextView.text = hexEncodedPublicKey
publicKeyTextView.setOnCreateContextMenuListener { contextMenu, view, _ ->
contextMenu.add(0, view.id, 0, R.string.copy).setOnMenuItemClickListener {
copyPublicKey()
true
}
}
copyButton.setOnClickListener { copyPublicKey() }
shareButton.setOnClickListener { sharePublicKey() }
createPrivateChatButton.setOnClickListener { handlePublicKeyEntered(); publicKeyEditText.hideKeyboard() }
}
}
private fun copyPublicKey() {
val clipboard = requireActivity().getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
val clip = ClipData.newPlainText("Session ID", hexEncodedPublicKey)
clipboard.setPrimaryClip(clip)
Toast.makeText(requireContext(), R.string.copied, Toast.LENGTH_SHORT).show()
}
private fun sharePublicKey() {
val intent = Intent()
intent.action = Intent.ACTION_SEND
intent.putExtra(Intent.EXTRA_TEXT, hexEncodedPublicKey)
intent.type = "text/plain"
startActivity(intent)
}
private fun handlePublicKeyEntered() {
val hexEncodedPublicKey = binding.publicKeyEditText.text?.trim()?.toString()
if (hexEncodedPublicKey.isNullOrEmpty()) return
delegate?.handlePublicKeyEntered(hexEncodedPublicKey)
}
}
fun interface EnterPublicKeyDelegate {
fun handlePublicKeyEntered(publicKey: String)
}

View File

@ -15,8 +15,7 @@ enum class EmojiCategory(val priority: Int, val key: String, @AttrRes val icon:
PLACES(4, "Places", R.attr.emoji_category_places), PLACES(4, "Places", R.attr.emoji_category_places),
OBJECTS(5, "Objects", R.attr.emoji_category_objects), OBJECTS(5, "Objects", R.attr.emoji_category_objects),
SYMBOLS(6, "Symbols", R.attr.emoji_category_symbol), SYMBOLS(6, "Symbols", R.attr.emoji_category_symbol),
FLAGS(7, "Flags", R.attr.emoji_category_flags), FLAGS(7, "Flags", R.attr.emoji_category_flags);
EMOTICONS(8, "Emoticons", R.attr.emoji_category_emoticons);
@StringRes @StringRes
fun getCategoryLabel(): Int { fun getCategoryLabel(): Int {
@ -31,15 +30,14 @@ enum class EmojiCategory(val priority: Int, val key: String, @AttrRes val icon:
@StringRes @StringRes
fun getCategoryLabel(@AttrRes iconAttr: Int): Int { fun getCategoryLabel(@AttrRes iconAttr: Int): Int {
return when (iconAttr) { return when (iconAttr) {
R.attr.emoji_category_people -> R.string.ReactWithAnyEmojiBottomSheetDialogFragment__smileys_and_people R.attr.emoji_category_people -> R.string.emojiCategorySmileys
R.attr.emoji_category_nature -> R.string.ReactWithAnyEmojiBottomSheetDialogFragment__nature R.attr.emoji_category_nature -> R.string.emojiCategoryAnimals
R.attr.emoji_category_foods -> R.string.ReactWithAnyEmojiBottomSheetDialogFragment__food R.attr.emoji_category_foods -> R.string.emojiCategoryFood
R.attr.emoji_category_activity -> R.string.ReactWithAnyEmojiBottomSheetDialogFragment__activities R.attr.emoji_category_activity -> R.string.emojiCategoryActivities
R.attr.emoji_category_places -> R.string.ReactWithAnyEmojiBottomSheetDialogFragment__places R.attr.emoji_category_places -> R.string.emojiCategoryTravel
R.attr.emoji_category_objects -> R.string.ReactWithAnyEmojiBottomSheetDialogFragment__objects R.attr.emoji_category_objects -> R.string.emojiCategoryObjects
R.attr.emoji_category_symbol -> R.string.ReactWithAnyEmojiBottomSheetDialogFragment__symbols R.attr.emoji_category_symbol -> R.string.emojiCategorySymbols
R.attr.emoji_category_flags -> R.string.ReactWithAnyEmojiBottomSheetDialogFragment__flags R.attr.emoji_category_flags -> R.string.emojiCategoryFlags
R.attr.emoji_category_emoticons -> R.string.ReactWithAnyEmojiBottomSheetDialogFragment__emoticons
else -> throw AssertionError() else -> throw AssertionError()
} }
} }

View File

@ -110,10 +110,12 @@ class EmojiSource(
val parsedData: ParsedEmojiData = EmojiJsonParser.parse(it, ::getAssetsUri).getOrThrow() val parsedData: ParsedEmojiData = EmojiJsonParser.parse(it, ::getAssetsUri).getOrThrow()
return EmojiSource( return EmojiSource(
ScreenDensity.xhdpiRelativeDensityScaleFactor("xhdpi"), ScreenDensity.xhdpiRelativeDensityScaleFactor("xhdpi"),
parsedData.copy( parsedData.copy(
displayPages = parsedData.displayPages + PAGE_EMOTICONS, displayPages = parsedData.displayPages,
dataPages = parsedData.dataPages + PAGE_EMOTICONS dataPages = parsedData.dataPages
) )
) { uri: Uri -> EmojiPage.Asset(uri) } ) { 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) 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 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
)

View File

@ -120,7 +120,7 @@ public class GiphyActivity extends PassphraseRequiredActionBarActivity
protected void onPostExecute(@Nullable Uri uri) { protected void onPostExecute(@Nullable Uri uri) {
if (uri == null) { 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) { } else if (viewHolder == finishingImage) {
Intent intent = new Intent(); Intent intent = new Intent();
intent.setData(uri); intent.setData(uri);
@ -165,8 +165,8 @@ public class GiphyActivity extends PassphraseRequiredActionBarActivity
@Override @Override
public CharSequence getPageTitle(int position) { public CharSequence getPageTitle(int position) {
if (position == 0) return context.getString(R.string.GiphyFragmentPagerAdapter_gifs); if (position == 0) return context.getString(R.string.gif);
else return context.getString(R.string.GiphyFragmentPagerAdapter_stickers); else return context.getString(R.string.stickers);
} }
} }

View File

@ -29,8 +29,8 @@ import org.session.libsession.utilities.ViewUtil;
import org.session.libsignal.utilities.Log; import org.session.libsignal.utilities.Log;
import org.thoughtcrime.securesms.giph.model.ChunkedImageUrl; import org.thoughtcrime.securesms.giph.model.ChunkedImageUrl;
import org.thoughtcrime.securesms.giph.model.GiphyImage; import org.thoughtcrime.securesms.giph.model.GiphyImage;
import org.thoughtcrime.securesms.mms.GlideApp; import com.bumptech.glide.Glide;
import org.thoughtcrime.securesms.mms.GlideRequests; import com.bumptech.glide.RequestManager;
import java.util.List; import java.util.List;
import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutionException;
@ -43,7 +43,7 @@ class GiphyAdapter extends RecyclerView.Adapter<GiphyAdapter.GiphyViewHolder> {
private static final String TAG = GiphyAdapter.class.getSimpleName(); private static final String TAG = GiphyAdapter.class.getSimpleName();
private final Context context; private final Context context;
private final GlideRequests glideRequests; private final RequestManager glideRequests;
private List<GiphyImage> images; private List<GiphyImage> images;
private OnItemClickListener listener; private OnItemClickListener listener;
@ -117,7 +117,7 @@ class GiphyAdapter extends RecyclerView.Adapter<GiphyAdapter.GiphyViewHolder> {
} }
} }
GiphyAdapter(@NonNull Context context, @NonNull GlideRequests glideRequests, @NonNull List<GiphyImage> images) { GiphyAdapter(@NonNull Context context, @NonNull RequestManager glideRequests, @NonNull List<GiphyImage> images) {
this.context = context.getApplicationContext(); this.context = context.getApplicationContext();
this.glideRequests = glideRequests; this.glideRequests = glideRequests;
this.images = images; this.images = images;
@ -150,7 +150,7 @@ class GiphyAdapter extends RecyclerView.Adapter<GiphyAdapter.GiphyViewHolder> {
holder.thumbnail.setAspectRatio(image.getGifAspectRatio()); holder.thumbnail.setAspectRatio(image.getGifAspectRatio());
holder.gifProgress.setVisibility(View.GONE); holder.gifProgress.setVisibility(View.GONE);
RequestBuilder<Drawable> thumbnailRequest = GlideApp.with(context) RequestBuilder<Drawable> thumbnailRequest = Glide.with(context)
.load(new ChunkedImageUrl(image.getStillUrl(), image.getStillSize())) .load(new ChunkedImageUrl(image.getStillUrl(), image.getStillSize()))
.diskCacheStrategy(DiskCacheStrategy.NONE); .diskCacheStrategy(DiskCacheStrategy.NONE);

Some files were not shown because too many files have changed in this diff Show More