mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-04-11 20:43:34 +01:00
Compare commits
128 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6755b25361 | ||
|
|
11d0a73675 | ||
|
|
44119b6437 | ||
|
|
d4a3b442f4 | ||
|
|
aba5774446 | ||
|
|
911dd9efb1 | ||
|
|
f2a490b07e | ||
|
|
5675f080f2 | ||
|
|
f0dbe230b5 | ||
|
|
8b81800052 | ||
|
|
f598c14298 | ||
|
|
58b070e6e3 | ||
|
|
71c92a1c90 | ||
|
|
b86acb9773 | ||
|
|
1b8758b657 | ||
|
|
ed4bab1b8b | ||
|
|
a71fe0fd75 | ||
|
|
3d2a634aac | ||
|
|
01047f0546 | ||
|
|
9dac5691f0 | ||
|
|
3c489ad247 | ||
|
|
7797351341 | ||
|
|
f7212b9916 | ||
|
|
93bb49dc16 | ||
|
|
e504c490c8 | ||
|
|
42e865813c | ||
|
|
fc14d1d464 | ||
|
|
2a1e5e4471 | ||
|
|
da2ee33dff | ||
|
|
f19033a7a2 | ||
|
|
6502ef64ce | ||
|
|
b3ebf778fd | ||
|
|
1dca3698d2 | ||
|
|
2bfe1198d1 | ||
|
|
4f704670b1 | ||
|
|
a1aafd7453 | ||
|
|
4932623937 | ||
|
|
b93568d9c6 | ||
|
|
b3041ab6e0 | ||
|
|
3a151b30ac | ||
|
|
97b3d36433 | ||
|
|
81e3252128 | ||
|
|
426c83c6cc | ||
|
|
b427754a81 | ||
|
|
08f023fb12 | ||
|
|
5f1454aeb8 | ||
|
|
0d254e0724 | ||
|
|
e882e6e111 | ||
|
|
4b0811f9aa | ||
|
|
817f1ee938 | ||
|
|
2d93d74b9f | ||
|
|
93f37ad70f | ||
|
|
3c6bed90db | ||
|
|
fa26fb6b8b | ||
|
|
263ddb0d1e | ||
|
|
8be659c1c8 | ||
|
|
e5c9dddb5a | ||
|
|
6da72aad6d | ||
|
|
5dd5a024c9 | ||
|
|
c0eac5564c | ||
|
|
0d0ee753df | ||
|
|
908f952893 | ||
|
|
1c80e65c5a | ||
|
|
20b13a929b | ||
|
|
4637e1b5d8 | ||
|
|
4b6cb79c75 | ||
|
|
feaf2a33a9 | ||
|
|
4c893a11fc | ||
|
|
f4dd80c929 | ||
|
|
4af078007e | ||
|
|
be297120a1 | ||
|
|
a9741cadbf | ||
|
|
79200c82da | ||
|
|
d9c9ae8dae | ||
|
|
8ee96b40d0 | ||
|
|
67f0f45b67 | ||
|
|
881ab90982 | ||
|
|
6d7e09fec1 | ||
|
|
c274ed6a96 | ||
|
|
53ffca964d | ||
|
|
3da3367291 | ||
|
|
412ee220ce | ||
|
|
a3e3667dc2 | ||
|
|
d5f63da9e4 | ||
|
|
f8d2044356 | ||
|
|
4d2dc61f5d | ||
|
|
5492685df2 | ||
|
|
ad8c6bc579 | ||
|
|
fb08f8ae17 | ||
|
|
7833d7c99a | ||
|
|
335ff61011 | ||
|
|
2029ea378f | ||
|
|
cd7bc63cec | ||
|
|
958331a8ea | ||
|
|
2ba206b9db | ||
|
|
9b90e371f9 | ||
|
|
ff1c298817 | ||
|
|
dfe804dfa0 | ||
|
|
978c6f9349 | ||
|
|
c5c176a818 | ||
|
|
9f2d57493d | ||
|
|
0972d8f1e1 | ||
|
|
cf361334c4 | ||
|
|
c72dd86fed | ||
|
|
b6c653ff77 | ||
|
|
5e3bbb0e64 | ||
|
|
64124f6f4b | ||
|
|
6f6a6826d9 | ||
|
|
57c0b8fd0f | ||
|
|
c54f016213 | ||
|
|
bece58d939 | ||
|
|
36443c59f9 | ||
|
|
02f0301f25 | ||
|
|
334cf669ed | ||
|
|
8442143818 | ||
|
|
b25b8b90e4 | ||
|
|
06aec0b7d7 | ||
|
|
835d7f5ccb | ||
|
|
ffd0b16753 | ||
|
|
b351fb43e6 | ||
|
|
7da47c9586 | ||
|
|
e4755b298f | ||
|
|
4a65487842 | ||
|
|
1466875293 | ||
|
|
fd1e552ad1 | ||
|
|
be3e89ac20 | ||
|
|
b8f1b98c74 | ||
|
|
4bdd07db16 |
3
.github/workflows/android.yml
vendored
3
.github/workflows/android.yml
vendored
@@ -24,6 +24,9 @@ jobs:
|
||||
- name: Validate Gradle Wrapper
|
||||
uses: gradle/wrapper-validation-action@v1
|
||||
|
||||
- name: Remove Android S
|
||||
run: $ANDROID_HOME/tools/bin/sdkmanager --uninstall "platforms;android-S"
|
||||
|
||||
- name: Build with Gradle
|
||||
run: ./gradlew qa
|
||||
|
||||
|
||||
3
.idea/codeStyles/Project.xml
generated
3
.idea/codeStyles/Project.xml
generated
@@ -10,6 +10,9 @@
|
||||
<option name="ALIGN_MULTILINE_TEXT_BLOCKS" value="true" />
|
||||
</JavaCodeStyleSettings>
|
||||
<JetCodeStyleSettings>
|
||||
<option name="PACKAGES_TO_USE_STAR_IMPORTS">
|
||||
<value />
|
||||
</option>
|
||||
<option name="NAME_COUNT_TO_USE_STAR_IMPORT" value="2147483647" />
|
||||
<option name="NAME_COUNT_TO_USE_STAR_IMPORT_FOR_MEMBERS" value="2147483647" />
|
||||
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
|
||||
|
||||
@@ -11,6 +11,8 @@ apply plugin: 'witness'
|
||||
apply plugin: 'org.jlleitschuh.gradle.ktlint'
|
||||
apply from: 'translations.gradle'
|
||||
apply from: 'witness-verifications.gradle'
|
||||
apply plugin: 'org.jetbrains.kotlin.android'
|
||||
apply plugin: 'app.cash.exhaustive'
|
||||
|
||||
repositories {
|
||||
maven {
|
||||
@@ -55,8 +57,8 @@ protobuf {
|
||||
}
|
||||
}
|
||||
|
||||
def canonicalVersionCode = 859
|
||||
def canonicalVersionName = "5.13.7"
|
||||
def canonicalVersionCode = 871
|
||||
def canonicalVersionName = "5.15.4"
|
||||
|
||||
def postFixSize = 100
|
||||
def abiPostFix = ['universal' : 0,
|
||||
@@ -75,6 +77,7 @@ android {
|
||||
useLibrary 'org.apache.http.legacy'
|
||||
|
||||
kotlinOptions {
|
||||
jvmTarget = "1.8"
|
||||
freeCompilerArgs = ["-Xallow-result-return-type"]
|
||||
}
|
||||
|
||||
@@ -115,6 +118,8 @@ android {
|
||||
buildConfigField "String", "SIGNAL_SERVICE_STATUS_URL", "\"uptime.signal.org\""
|
||||
buildConfigField "String", "SIGNAL_KEY_BACKUP_URL", "\"https://api.backup.signal.org\""
|
||||
buildConfigField "String", "SIGNAL_SFU_URL", "\"https://sfu.voip.signal.org\""
|
||||
buildConfigField "String[]", "SIGNAL_SFU_INTERNAL_NAMES", "new String[]{\"Test\", \"Staging\"}"
|
||||
buildConfigField "String[]", "SIGNAL_SFU_INTERNAL_URLS", "new String[]{\"https://sfu.test.voip.signal.org\", \"https://sfu.staging.voip.signal.org\"}"
|
||||
buildConfigField "String", "CONTENT_PROXY_HOST", "\"contentproxy.signal.org\""
|
||||
buildConfigField "int", "CONTENT_PROXY_PORT", "443"
|
||||
buildConfigField "String", "SIGNAL_AGENT", "\"OWA\""
|
||||
@@ -132,6 +137,10 @@ android {
|
||||
buildConfigField "String", "GIPHY_API_KEY", "\"3o6ZsYH6U6Eri53TXy\""
|
||||
buildConfigField "String", "RECAPTCHA_PROOF_URL", "\"https://signalcaptchas.org/challenge/generate.html\""
|
||||
|
||||
buildConfigField "String", "BUILD_DISTRIBUTION_TYPE", "\"unset\""
|
||||
buildConfigField "String", "BUILD_ENVIRONMENT_TYPE", "\"unset\""
|
||||
buildConfigField "String", "BUILD_VARIANT_TYPE", "\"unset\""
|
||||
|
||||
ndk {
|
||||
abiFilters 'armeabi-v7a', 'arm64-v8a', 'x86', 'x86_64'
|
||||
}
|
||||
@@ -196,28 +205,34 @@ android {
|
||||
'proguard/proguard.cfg'
|
||||
testProguardFiles 'proguard/proguard-automation.pro',
|
||||
'proguard/proguard.cfg'
|
||||
|
||||
buildConfigField "String", "BUILD_VARIANT_TYPE", "\"Debug\""
|
||||
}
|
||||
flipper {
|
||||
initWith debug
|
||||
isDefault false
|
||||
minifyEnabled false
|
||||
matchingFallbacks = ['debug']
|
||||
buildConfigField "String", "BUILD_VARIANT_TYPE", "\"Flipper\""
|
||||
}
|
||||
release {
|
||||
minifyEnabled true
|
||||
proguardFiles = buildTypes.debug.proguardFiles
|
||||
buildConfigField "String", "BUILD_VARIANT_TYPE", "\"Release\""
|
||||
}
|
||||
perf {
|
||||
initWith debug
|
||||
isDefault false
|
||||
debuggable false
|
||||
matchingFallbacks = ['debug']
|
||||
buildConfigField "String", "BUILD_VARIANT_TYPE", "\"Perf\""
|
||||
}
|
||||
mock {
|
||||
initWith debug
|
||||
isDefault false
|
||||
minifyEnabled false
|
||||
matchingFallbacks = ['debug']
|
||||
buildConfigField "String", "BUILD_VARIANT_TYPE", "\"Mock\""
|
||||
}
|
||||
}
|
||||
|
||||
@@ -228,6 +243,7 @@ android {
|
||||
ext.websiteUpdateUrl = "null"
|
||||
buildConfigField "boolean", "PLAY_STORE_DISABLED", "false"
|
||||
buildConfigField "String", "NOPLAY_UPDATE_URL", "$ext.websiteUpdateUrl"
|
||||
buildConfigField "String", "BUILD_DISTRIBUTION_TYPE", "\"play\""
|
||||
}
|
||||
|
||||
website {
|
||||
@@ -235,6 +251,7 @@ android {
|
||||
ext.websiteUpdateUrl = "https://updates.signal.org/android"
|
||||
buildConfigField "boolean", "PLAY_STORE_DISABLED", "true"
|
||||
buildConfigField "String", "NOPLAY_UPDATE_URL", "\"$ext.websiteUpdateUrl\""
|
||||
buildConfigField "String", "BUILD_DISTRIBUTION_TYPE", "\"website\""
|
||||
}
|
||||
|
||||
internal {
|
||||
@@ -242,6 +259,7 @@ android {
|
||||
ext.websiteUpdateUrl = "null"
|
||||
buildConfigField "boolean", "PLAY_STORE_DISABLED", "false"
|
||||
buildConfigField "String", "NOPLAY_UPDATE_URL", "$ext.websiteUpdateUrl"
|
||||
buildConfigField "String", "BUILD_DISTRIBUTION_TYPE", "\"internal\""
|
||||
}
|
||||
|
||||
study {
|
||||
@@ -251,6 +269,7 @@ android {
|
||||
ext.websiteUpdateUrl = "null"
|
||||
buildConfigField "boolean", "PLAY_STORE_DISABLED", "false"
|
||||
buildConfigField "String", "NOPLAY_UPDATE_URL", "$ext.websiteUpdateUrl"
|
||||
buildConfigField "String", "BUILD_DISTRIBUTION_TYPE", "\"study\""
|
||||
}
|
||||
|
||||
prod {
|
||||
@@ -259,6 +278,7 @@ android {
|
||||
isDefault true
|
||||
|
||||
buildConfigField "String", "MOBILE_COIN_ENVIRONMENT", "\"mainnet\""
|
||||
buildConfigField "String", "BUILD_ENVIRONMENT_TYPE", "\"Prod\""
|
||||
}
|
||||
|
||||
staging {
|
||||
@@ -281,6 +301,8 @@ android {
|
||||
buildConfigField "String", "ZKGROUP_SERVER_PUBLIC_PARAMS", "\"ABSY21VckQcbSXVNCGRYJcfWHiAMZmpTtTELcDmxgdFbtp/bWsSxZdMKzfCp8rvIs8ocCU3B37fT3r4Mi5qAemeGeR2X+/YmOGR5ofui7tD5mDQfstAI9i+4WpMtIe8KC3wU5w3Inq3uNWVmoGtpKndsNfwJrCg0Hd9zmObhypUnSkfYn2ooMOOnBpfdanRtrvetZUayDMSC5iSRcXKpdls=\""
|
||||
buildConfigField "String", "MOBILE_COIN_ENVIRONMENT", "\"testnet\""
|
||||
buildConfigField "String", "RECAPTCHA_PROOF_URL", "\"https://signalcaptchas.org/staging/challenge/generate.html\""
|
||||
|
||||
buildConfigField "String", "BUILD_ENVIRONMENT_TYPE", "\"Staging\""
|
||||
}
|
||||
}
|
||||
|
||||
@@ -353,7 +375,7 @@ dependencies {
|
||||
implementation "androidx.autofill:autofill:1.0.0"
|
||||
implementation "androidx.biometric:biometric:1.1.0"
|
||||
|
||||
implementation ('com.google.firebase:firebase-messaging:20.2.0') {
|
||||
implementation ('com.google.firebase:firebase-messaging:22.0.0') {
|
||||
exclude group: 'com.google.firebase', module: 'firebase-core'
|
||||
exclude group: 'com.google.firebase', module: 'firebase-analytics'
|
||||
exclude group: 'com.google.firebase', module: 'firebase-measurement-connector'
|
||||
@@ -376,10 +398,10 @@ dependencies {
|
||||
implementation project(':device-transfer')
|
||||
|
||||
implementation 'org.signal:zkgroup-android:0.7.0'
|
||||
implementation 'org.whispersystems:signal-client-android:0.5.1'
|
||||
implementation 'org.whispersystems:signal-client-android:0.8.0'
|
||||
implementation 'com.google.protobuf:protobuf-javalite:3.10.0'
|
||||
|
||||
implementation('com.mobilecoin:android-sdk:1.0.0') {
|
||||
implementation('com.mobilecoin:android-sdk:1.1.0') {
|
||||
exclude group: 'com.google.protobuf'
|
||||
}
|
||||
|
||||
|
||||
@@ -153,6 +153,14 @@
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
<data android:scheme="tsdevice"/>
|
||||
</intent-filter>
|
||||
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
<data android:scheme="sgnl"
|
||||
android:host="linkdevice"/>
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<activity android:name=".preferences.MmsPreferencesActivity"
|
||||
@@ -300,14 +308,6 @@
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize|uiMode"
|
||||
android:theme="@style/Theme.Signal.DayNight.NoActionBar" />
|
||||
|
||||
<activity android:name=".groups.ui.managegroup.ManageGroupActivity"
|
||||
android:windowSoftInputMode="stateAlwaysHidden"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
|
||||
|
||||
<activity android:name=".recipients.ui.managerecipient.ManageRecipientActivity"
|
||||
android:windowSoftInputMode="stateAlwaysHidden"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
|
||||
|
||||
<activity android:name=".recipients.ui.disappearingmessages.RecipientDisappearingMessagesActivity"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
|
||||
android:theme="@style/Signal.DayNight.NoActionBar"
|
||||
@@ -373,6 +373,13 @@
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<activity android:name=".components.settings.conversation.ConversationSettingsActivity"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
|
||||
android:theme="@style/Signal.DayNight.ConversationSettings"
|
||||
android:windowSoftInputMode="stateAlwaysHidden">
|
||||
</activity>
|
||||
|
||||
|
||||
<activity android:name=".wallpaper.ChatWallpaperActivity"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
|
||||
android:windowSoftInputMode="stateAlwaysHidden">
|
||||
@@ -710,24 +717,12 @@
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
|
||||
<receiver android:name=".notifications.AndroidAutoHeardReceiver"
|
||||
android:exported="false">
|
||||
<intent-filter>
|
||||
<action android:name="org.thoughtcrime.securesms.notifications.ANDROID_AUTO_HEARD"/>
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
|
||||
<receiver android:name=".notifications.AndroidAutoReplyReceiver"
|
||||
android:exported="false">
|
||||
<intent-filter>
|
||||
<action android:name="org.thoughtcrime.securesms.notifications.ANDROID_AUTO_REPLY"/>
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
|
||||
<receiver android:name=".service.ExpirationListener" />
|
||||
|
||||
<receiver android:name=".revealable.ViewOnceMessageManager$ViewOnceAlarm" />
|
||||
|
||||
<receiver android:name=".service.PendingRetryReceiptManager$PendingRetryReceiptAlarm" />
|
||||
|
||||
<receiver android:name=".service.TrimThreadsByDateManager$TrimThreadsByDateAlarm" />
|
||||
|
||||
<receiver android:name=".payments.backup.phrase.ClearClipboardAlarmReceiver" />
|
||||
|
||||
@@ -17,6 +17,6 @@ public final class AppCapabilities {
|
||||
* asking if the user has set a Signal PIN or not.
|
||||
*/
|
||||
public static AccountAttributes.Capabilities getCapabilities(boolean storageCapable) {
|
||||
return new AccountAttributes.Capabilities(UUID_CAPABLE, GV2_CAPABLE, storageCapable, GV1_MIGRATION);
|
||||
return new AccountAttributes.Capabilities(UUID_CAPABLE, GV2_CAPABLE, storageCapable, GV1_MIGRATION, FeatureFlags.senderKey());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -71,6 +71,7 @@ import org.thoughtcrime.securesms.service.UpdateApkRefreshListener;
|
||||
import org.thoughtcrime.securesms.storage.StorageSyncHelper;
|
||||
import org.thoughtcrime.securesms.util.AppForegroundObserver;
|
||||
import org.thoughtcrime.securesms.util.AppStartup;
|
||||
import org.thoughtcrime.securesms.util.ByteUnit;
|
||||
import org.thoughtcrime.securesms.util.DynamicTheme;
|
||||
import org.thoughtcrime.securesms.util.FeatureFlags;
|
||||
import org.thoughtcrime.securesms.util.SignalUncaughtExceptionHandler;
|
||||
@@ -146,6 +147,7 @@ public class ApplicationContext extends MultiDexApplication implements AppForegr
|
||||
.addBlocking("blob-provider", this::initializeBlobProvider)
|
||||
.addBlocking("feature-flags", FeatureFlags::init)
|
||||
.addNonBlocking(this::initializeRevealableMessageManager)
|
||||
.addNonBlocking(this::initializePendingRetryReceiptManager)
|
||||
.addNonBlocking(this::initializeGcmCheck)
|
||||
.addNonBlocking(this::initializeSignedPreKeyCheck)
|
||||
.addNonBlocking(this::initializePeriodicTasks)
|
||||
@@ -162,6 +164,7 @@ public class ApplicationContext extends MultiDexApplication implements AppForegr
|
||||
.addPostRender(() -> SignalStore.settings().setDefaultSms(Util.isDefaultSmsProvider(this)))
|
||||
.addPostRender(() -> DownloadLatestEmojiDataJob.scheduleIfNecessary(this))
|
||||
.addPostRender(EmojiSearchIndexDownloadJob::scheduleIfNecessary)
|
||||
.addPostRender(() -> DatabaseFactory.getMessageLogDatabase(this).trimOldMessages(System.currentTimeMillis(), FeatureFlags.retryRespondMaxAge()))
|
||||
.execute();
|
||||
|
||||
Log.d(TAG, "onCreate() took " + (System.currentTimeMillis() - startTime) + " ms");
|
||||
@@ -235,7 +238,7 @@ public class ApplicationContext extends MultiDexApplication implements AppForegr
|
||||
}
|
||||
|
||||
private void initializeLogging() {
|
||||
persistentLogger = new PersistentLogger(this, LogSecretProvider.getOrCreateAttachmentSecret(this), BuildConfig.VERSION_NAME);
|
||||
persistentLogger = new PersistentLogger(this, LogSecretProvider.getOrCreateAttachmentSecret(this), BuildConfig.VERSION_NAME, FeatureFlags.internalUser() ? 15 : 7, ByteUnit.KILOBYTES.toBytes(300));
|
||||
org.signal.core.util.logging.Log.initialize(FeatureFlags::internalUser, new AndroidLogger(), persistentLogger);
|
||||
|
||||
SignalProtocolLoggerProvider.setProvider(new CustomSignalProtocolLogger());
|
||||
@@ -300,6 +303,10 @@ public class ApplicationContext extends MultiDexApplication implements AppForegr
|
||||
ApplicationDependencies.getViewOnceMessageManager().scheduleIfNecessary();
|
||||
}
|
||||
|
||||
private void initializePendingRetryReceiptManager() {
|
||||
ApplicationDependencies.getPendingRetryReceiptManager().scheduleIfNecessary();
|
||||
}
|
||||
|
||||
private void initializePeriodicTasks() {
|
||||
RotateSignedPreKeyListener.schedule(this);
|
||||
DirectoryRefreshListener.schedule(this);
|
||||
|
||||
@@ -52,6 +52,10 @@ public interface BindableConversationItem extends Unbindable, GiphyMp4Playable,
|
||||
|
||||
void setEventListener(@Nullable EventListener listener);
|
||||
|
||||
default void updateTimestamps() {
|
||||
// Intentionally Blank.
|
||||
}
|
||||
|
||||
interface EventListener {
|
||||
void onQuoteClicked(MmsMessageRecord messageRecord);
|
||||
void onLinkPreviewClicked(@NonNull LinkPreview linkPreview);
|
||||
@@ -72,7 +76,8 @@ public interface BindableConversationItem extends Unbindable, GiphyMp4Playable,
|
||||
void onVoiceNotePlay(@NonNull Uri uri, long messageId, double position);
|
||||
void onVoiceNoteSeekTo(@NonNull Uri uri, double position);
|
||||
void onGroupMigrationLearnMoreClicked(@NonNull GroupMigrationMembershipChange membershipChange);
|
||||
void onDecryptionFailedLearnMoreClicked();
|
||||
void onChatSessionRefreshLearnMoreClicked();
|
||||
void onBadDecryptLearnMoreClicked(@NonNull RecipientId author);
|
||||
void onSafetyNumberLearnMoreClicked(@NonNull Recipient recipient);
|
||||
void onJoinGroupCallClicked();
|
||||
void onInviteFriendsToGroupClicked(@NonNull GroupId.V2 groupId);
|
||||
|
||||
@@ -12,11 +12,10 @@ import android.widget.TextView;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
|
||||
import org.signal.core.util.logging.Log;
|
||||
import org.thoughtcrime.securesms.crypto.DatabaseSessionLock;
|
||||
import org.thoughtcrime.securesms.crypto.ReentrantSessionLock;
|
||||
import org.thoughtcrime.securesms.crypto.storage.TextSecureIdentityKeyStore;
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
import org.thoughtcrime.securesms.database.MessageDatabase;
|
||||
import org.thoughtcrime.securesms.database.PushDatabase;
|
||||
import org.thoughtcrime.securesms.database.documents.IdentityKeyMismatch;
|
||||
import org.thoughtcrime.securesms.database.model.MessageRecord;
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
@@ -95,7 +94,7 @@ public class ConfirmIdentityDialog extends AlertDialog {
|
||||
{
|
||||
@Override
|
||||
protected Void doInBackground(Void... params) {
|
||||
try (SignalSessionLock.Lock unused = DatabaseSessionLock.INSTANCE.acquire()) {
|
||||
try (SignalSessionLock.Lock unused = ReentrantSessionLock.INSTANCE.acquire()) {
|
||||
SignalProtocolAddress mismatchAddress = new SignalProtocolAddress(Recipient.resolved(recipientId).requireServiceId(), 1);
|
||||
TextSecureIdentityKeyStore identityKeyStore = new TextSecureIdentityKeyStore(getContext());
|
||||
|
||||
|
||||
@@ -114,6 +114,8 @@ public final class ContactSelectionListFragment extends LoggingFragment
|
||||
public static final String HIDE_COUNT = "hide_count";
|
||||
public static final String CAN_SELECT_SELF = "can_select_self";
|
||||
public static final String DISPLAY_CHIPS = "display_chips";
|
||||
public static final String RV_PADDING_BOTTOM = "recycler_view_padding_bottom";
|
||||
public static final String RV_CLIP = "recycler_view_clipping";
|
||||
|
||||
private ConstraintLayout constraintLayout;
|
||||
private TextView emptyText;
|
||||
@@ -245,6 +247,15 @@ public final class ContactSelectionListFragment extends LoggingFragment
|
||||
Intent intent = requireActivity().getIntent();
|
||||
Bundle arguments = safeArguments();
|
||||
|
||||
int recyclerViewPadBottom = arguments.getInt(RV_PADDING_BOTTOM, intent.getIntExtra(RV_PADDING_BOTTOM, -1));
|
||||
boolean recyclerViewClipping = arguments.getBoolean(RV_CLIP, intent.getBooleanExtra(RV_CLIP, true));
|
||||
|
||||
if (recyclerViewPadBottom != -1) {
|
||||
ViewUtil.setPaddingBottom(recyclerView, recyclerViewPadBottom);
|
||||
}
|
||||
|
||||
recyclerView.setClipToPadding(recyclerViewClipping);
|
||||
|
||||
swipeRefresh.setEnabled(arguments.getBoolean(REFRESHABLE, intent.getBooleanExtra(REFRESHABLE, true)));
|
||||
|
||||
hideCount = arguments.getBoolean(HIDE_COUNT, intent.getBooleanExtra(HIDE_COUNT, false));
|
||||
|
||||
@@ -103,6 +103,7 @@ public final class MediaPreviewActivity extends PassphraseRequiredActivity
|
||||
public static final String HIDE_ALL_MEDIA_EXTRA = "came_from_all_media";
|
||||
public static final String SHOW_THREAD_EXTRA = "show_thread";
|
||||
public static final String SORTING_EXTRA = "sorting";
|
||||
public static final String IS_VIDEO_GIF = "is_video_gif";
|
||||
|
||||
private ViewPager mediaPager;
|
||||
private View detailsContainer;
|
||||
@@ -115,6 +116,7 @@ public final class MediaPreviewActivity extends PassphraseRequiredActivity
|
||||
private String initialMediaType;
|
||||
private long initialMediaSize;
|
||||
private String initialCaption;
|
||||
private boolean initialMediaIsVideoGif;
|
||||
private boolean leftIsRecent;
|
||||
private MediaPreviewViewModel viewModel;
|
||||
private ViewPagerListener viewPagerListener;
|
||||
@@ -139,6 +141,7 @@ public final class MediaPreviewActivity extends PassphraseRequiredActivity
|
||||
intent.putExtra(MediaPreviewActivity.SIZE_EXTRA, attachment.getSize());
|
||||
intent.putExtra(MediaPreviewActivity.CAPTION_EXTRA, attachment.getCaption());
|
||||
intent.putExtra(MediaPreviewActivity.LEFT_IS_RECENT_EXTRA, leftIsRecent);
|
||||
intent.putExtra(MediaPreviewActivity.IS_VIDEO_GIF, attachment.isVideoGif());
|
||||
intent.setDataAndType(attachment.getUri(), mediaRecord.getContentType());
|
||||
return intent;
|
||||
}
|
||||
@@ -296,12 +299,13 @@ public final class MediaPreviewActivity extends PassphraseRequiredActivity
|
||||
showThread = intent.getBooleanExtra(SHOW_THREAD_EXTRA, false);
|
||||
sorting = MediaDatabase.Sorting.values()[intent.getIntExtra(SORTING_EXTRA, 0)];
|
||||
|
||||
initialMediaUri = intent.getData();
|
||||
initialMediaType = intent.getType();
|
||||
initialMediaSize = intent.getLongExtra(SIZE_EXTRA, 0);
|
||||
initialCaption = intent.getStringExtra(CAPTION_EXTRA);
|
||||
leftIsRecent = intent.getBooleanExtra(LEFT_IS_RECENT_EXTRA, false);
|
||||
restartItem = -1;
|
||||
initialMediaUri = intent.getData();
|
||||
initialMediaType = intent.getType();
|
||||
initialMediaSize = intent.getLongExtra(SIZE_EXTRA, 0);
|
||||
initialCaption = intent.getStringExtra(CAPTION_EXTRA);
|
||||
leftIsRecent = intent.getBooleanExtra(LEFT_IS_RECENT_EXTRA, false);
|
||||
initialMediaIsVideoGif = intent.getBooleanExtra(IS_VIDEO_GIF, false);
|
||||
restartItem = -1;
|
||||
}
|
||||
|
||||
private void initializeObservers() {
|
||||
@@ -354,7 +358,7 @@ public final class MediaPreviewActivity extends PassphraseRequiredActivity
|
||||
if (isMediaInDb()) {
|
||||
LoaderManager.getInstance(this).restartLoader(0, null, this);
|
||||
} else {
|
||||
mediaPager.setAdapter(new SingleItemPagerAdapter(getSupportFragmentManager(), initialMediaUri, initialMediaType, initialMediaSize));
|
||||
mediaPager.setAdapter(new SingleItemPagerAdapter(getSupportFragmentManager(), initialMediaUri, initialMediaType, initialMediaSize, initialMediaIsVideoGif));
|
||||
|
||||
if (initialCaption != null) {
|
||||
detailsContainer.setVisibility(View.VISIBLE);
|
||||
@@ -632,21 +636,24 @@ public final class MediaPreviewActivity extends PassphraseRequiredActivity
|
||||
|
||||
private static class SingleItemPagerAdapter extends FragmentStatePagerAdapter implements MediaItemAdapter {
|
||||
|
||||
private final Uri uri;
|
||||
private final String mediaType;
|
||||
private final long size;
|
||||
private final Uri uri;
|
||||
private final String mediaType;
|
||||
private final long size;
|
||||
private final boolean isVideoGif;
|
||||
|
||||
private MediaPreviewFragment mediaPreviewFragment;
|
||||
|
||||
SingleItemPagerAdapter(@NonNull FragmentManager fragmentManager,
|
||||
@NonNull Uri uri,
|
||||
@NonNull String mediaType,
|
||||
long size)
|
||||
long size,
|
||||
boolean isVideoGif)
|
||||
{
|
||||
super(fragmentManager, BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT);
|
||||
this.uri = uri;
|
||||
this.mediaType = mediaType;
|
||||
this.size = size;
|
||||
this.uri = uri;
|
||||
this.mediaType = mediaType;
|
||||
this.size = size;
|
||||
this.isVideoGif = isVideoGif;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -657,7 +664,7 @@ public final class MediaPreviewActivity extends PassphraseRequiredActivity
|
||||
@NonNull
|
||||
@Override
|
||||
public Fragment getItem(int position) {
|
||||
mediaPreviewFragment = MediaPreviewFragment.newInstance(uri, mediaType, size, true);
|
||||
mediaPreviewFragment = MediaPreviewFragment.newInstance(uri, mediaType, size, true, isVideoGif);
|
||||
return mediaPreviewFragment;
|
||||
}
|
||||
|
||||
|
||||
@@ -7,6 +7,8 @@ import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
|
||||
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
public class MuteDialog extends AlertDialog {
|
||||
@@ -29,7 +31,7 @@ public class MuteDialog extends AlertDialog {
|
||||
}
|
||||
|
||||
public static void show(final Context context, final @NonNull MuteSelectionListener listener, @Nullable Runnable cancelListener) {
|
||||
AlertDialog.Builder builder = new AlertDialog.Builder(context);
|
||||
AlertDialog.Builder builder = new MaterialAlertDialogBuilder(context);
|
||||
builder.setTitle(R.string.MuteDialog_mute_notifications);
|
||||
builder.setItems(R.array.mute_durations, new DialogInterface.OnClickListener() {
|
||||
@Override
|
||||
|
||||
@@ -29,7 +29,6 @@ import android.graphics.BitmapFactory;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.PorterDuff;
|
||||
import android.graphics.drawable.BitmapDrawable;
|
||||
import android.graphics.drawable.ColorDrawable;
|
||||
import android.os.AsyncTask;
|
||||
import android.os.Bundle;
|
||||
import android.os.Vibrator;
|
||||
@@ -63,9 +62,8 @@ import androidx.fragment.app.FragmentTransaction;
|
||||
import org.signal.core.util.ThreadUtil;
|
||||
import org.signal.core.util.concurrent.SignalExecutors;
|
||||
import org.signal.core.util.logging.Log;
|
||||
import org.thoughtcrime.securesms.color.MaterialColor;
|
||||
import org.thoughtcrime.securesms.components.camera.CameraView;
|
||||
import org.thoughtcrime.securesms.crypto.DatabaseSessionLock;
|
||||
import org.thoughtcrime.securesms.crypto.ReentrantSessionLock;
|
||||
import org.thoughtcrime.securesms.crypto.IdentityKeyParcelable;
|
||||
import org.thoughtcrime.securesms.crypto.IdentityKeyUtil;
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
@@ -87,16 +85,13 @@ import org.thoughtcrime.securesms.util.IdentityUtil;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
import org.thoughtcrime.securesms.util.ViewUtil;
|
||||
import org.thoughtcrime.securesms.util.WindowUtil;
|
||||
import org.whispersystems.libsignal.IdentityKey;
|
||||
import org.whispersystems.libsignal.fingerprint.Fingerprint;
|
||||
import org.whispersystems.libsignal.fingerprint.FingerprintParsingException;
|
||||
import org.whispersystems.libsignal.fingerprint.FingerprintVersionMismatchException;
|
||||
import org.whispersystems.libsignal.fingerprint.NumericFingerprintGenerator;
|
||||
import org.whispersystems.signalservice.api.SignalSessionLock;
|
||||
import org.whispersystems.signalservice.api.util.UuidUtil;
|
||||
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.nio.charset.Charset;
|
||||
import java.util.Locale;
|
||||
|
||||
@@ -615,7 +610,7 @@ public class VerifyIdentityActivity extends PassphraseRequiredActivity implement
|
||||
final RecipientId recipientId = recipient.getId();
|
||||
|
||||
SignalExecutors.BOUNDED.execute(() -> {
|
||||
try (SignalSessionLock.Lock unused = DatabaseSessionLock.INSTANCE.acquire()) {
|
||||
try (SignalSessionLock.Lock unused = ReentrantSessionLock.INSTANCE.acquire()) {
|
||||
if (isChecked) {
|
||||
Log.i(TAG, "Saving identity: " + recipientId);
|
||||
DatabaseFactory.getIdentityDatabase(getActivity())
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
package org.thoughtcrime.securesms.animation.transitions
|
||||
|
||||
import android.animation.Animator
|
||||
import android.animation.ObjectAnimator
|
||||
import android.animation.PropertyValuesHolder
|
||||
import android.animation.TypeEvaluator
|
||||
import android.content.Context
|
||||
import android.transition.Transition
|
||||
import android.transition.TransitionValues
|
||||
import android.util.AttributeSet
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.view.animation.AccelerateInterpolator
|
||||
import android.view.animation.DecelerateInterpolator
|
||||
import android.view.animation.Interpolator
|
||||
import androidx.annotation.RequiresApi
|
||||
import org.thoughtcrime.securesms.components.AvatarImageView
|
||||
|
||||
private const val POSITION_ON_SCREEN = "signal.circleavatartransition.positiononscreen"
|
||||
private const val WIDTH = "signal.circleavatartransition.width"
|
||||
private const val HEIGHT = "signal.circleavatartransition.height"
|
||||
|
||||
/**
|
||||
* Custom transition for Circular avatars, because once you have multiple things animating stuff was getting broken and weird.
|
||||
*/
|
||||
@RequiresApi(21)
|
||||
class CircleAvatarTransition(context: Context, attrs: AttributeSet?) : Transition(context, attrs) {
|
||||
override fun captureStartValues(transitionValues: TransitionValues) {
|
||||
captureValues(transitionValues)
|
||||
}
|
||||
|
||||
override fun captureEndValues(transitionValues: TransitionValues) {
|
||||
captureValues(transitionValues)
|
||||
}
|
||||
|
||||
private fun captureValues(transitionValues: TransitionValues) {
|
||||
val view: View = transitionValues.view
|
||||
|
||||
if (view is AvatarImageView) {
|
||||
val topLeft = intArrayOf(0, 0)
|
||||
view.getLocationOnScreen(topLeft)
|
||||
transitionValues.values[POSITION_ON_SCREEN] = topLeft
|
||||
transitionValues.values[WIDTH] = view.measuredWidth
|
||||
transitionValues.values[HEIGHT] = view.measuredHeight
|
||||
}
|
||||
}
|
||||
|
||||
override fun createAnimator(sceneRoot: ViewGroup, startValues: TransitionValues?, endValues: TransitionValues?): Animator? {
|
||||
if (startValues == null || endValues == null) {
|
||||
return null
|
||||
}
|
||||
|
||||
val view: View = endValues.view
|
||||
if (view !is AvatarImageView || view.transitionName != "avatar") {
|
||||
return null
|
||||
}
|
||||
|
||||
val startCoords: IntArray = startValues.values[POSITION_ON_SCREEN] as? IntArray ?: intArrayOf(0, 0).apply { view.getLocationOnScreen(this) }
|
||||
val endCoords: IntArray = endValues.values[POSITION_ON_SCREEN] as? IntArray ?: intArrayOf(0, 0).apply { view.getLocationOnScreen(this) }
|
||||
|
||||
val startWidth: Int = startValues.values[WIDTH] as? Int ?: view.measuredWidth
|
||||
val endWidth: Int = endValues.values[WIDTH] as? Int ?: view.measuredWidth
|
||||
|
||||
val startHeight: Int = startValues.values[HEIGHT] as? Int ?: view.measuredHeight
|
||||
val endHeight: Int = endValues.values[HEIGHT] as? Int ?: view.measuredHeight
|
||||
|
||||
val startHeightOffset = (endHeight - startHeight) / 2f
|
||||
val startWidthOffset = (endWidth - startWidth) / 2f
|
||||
|
||||
val translateXHolder = PropertyValuesHolder.ofFloat("translationX", startCoords[0] - endCoords[0] - startWidthOffset, 0f).apply {
|
||||
setEvaluator(FloatInterpolatorEvaluator(DecelerateInterpolator()))
|
||||
}
|
||||
val translateYHolder = PropertyValuesHolder.ofFloat("translationY", startCoords[1] - endCoords[1] - startHeightOffset, 0f).apply {
|
||||
setEvaluator(FloatInterpolatorEvaluator(AccelerateInterpolator()))
|
||||
}
|
||||
|
||||
val widthRatio = startWidth.toFloat() / endWidth
|
||||
val scaleXHolder = PropertyValuesHolder.ofFloat("scaleX", widthRatio, 1f)
|
||||
|
||||
val heightRatio = startHeight.toFloat() / endHeight
|
||||
val scaleYHolder = PropertyValuesHolder.ofFloat("scaleY", heightRatio, 1f)
|
||||
|
||||
return ObjectAnimator.ofPropertyValuesHolder(view, translateXHolder, translateYHolder, scaleXHolder, scaleYHolder)
|
||||
}
|
||||
|
||||
private class FloatInterpolatorEvaluator(
|
||||
private val interpolator: Interpolator
|
||||
) : TypeEvaluator<Float> {
|
||||
|
||||
override fun evaluate(fraction: Float, startValue: Float, endValue: Float): Float {
|
||||
val interpolatedFraction = interpolator.getInterpolation(fraction)
|
||||
val delta = endValue - startValue
|
||||
|
||||
return delta * interpolatedFraction + startValue
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
package org.thoughtcrime.securesms.animation.transitions
|
||||
|
||||
import android.animation.Animator
|
||||
import android.animation.ObjectAnimator
|
||||
import android.animation.RectEvaluator
|
||||
import android.content.Context
|
||||
import android.graphics.Rect
|
||||
import android.transition.Transition
|
||||
import android.transition.TransitionValues
|
||||
import android.util.AttributeSet
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.core.animation.addListener
|
||||
import androidx.fragment.app.FragmentContainerView
|
||||
|
||||
private const val BOUNDS = "signal.wipedowntransition.bottom"
|
||||
|
||||
/**
|
||||
* WipeDownTransition will animate the bottom position of a view such that it "wipes" down the screen to a final position.
|
||||
*/
|
||||
@RequiresApi(21)
|
||||
class WipeDownTransition(context: Context, attrs: AttributeSet?) : Transition(context, attrs) {
|
||||
override fun captureStartValues(transitionValues: TransitionValues) {
|
||||
captureValues(transitionValues)
|
||||
}
|
||||
|
||||
override fun captureEndValues(transitionValues: TransitionValues) {
|
||||
captureValues(transitionValues)
|
||||
}
|
||||
|
||||
private fun captureValues(transitionValues: TransitionValues) {
|
||||
val view: View = transitionValues.view
|
||||
|
||||
if (view is ViewGroup) {
|
||||
val rect = Rect()
|
||||
view.getLocalVisibleRect(rect)
|
||||
transitionValues.values[BOUNDS] = rect
|
||||
}
|
||||
}
|
||||
|
||||
override fun createAnimator(sceneRoot: ViewGroup, startValues: TransitionValues?, endValues: TransitionValues?): Animator? {
|
||||
if (startValues == null || endValues == null) {
|
||||
return null
|
||||
}
|
||||
|
||||
val view: View = endValues.view
|
||||
if (view !is FragmentContainerView) {
|
||||
return null
|
||||
}
|
||||
|
||||
val startBottom: Rect = startValues.values[BOUNDS] as? Rect ?: Rect().apply { view.getLocalVisibleRect(this) }
|
||||
val endBottom: Rect = endValues.values[BOUNDS] as? Rect ?: Rect().apply { view.getLocalVisibleRect(this) }
|
||||
|
||||
return ObjectAnimator.ofObject(view, "clipBounds", RectEvaluator(), startBottom, endBottom).apply {
|
||||
addListener(
|
||||
onEnd = {
|
||||
view.clipBounds = null
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -30,7 +30,10 @@ import org.thoughtcrime.securesms.database.KeyValueDatabase;
|
||||
import org.thoughtcrime.securesms.database.MmsDatabase;
|
||||
import org.thoughtcrime.securesms.database.MmsSmsColumns;
|
||||
import org.thoughtcrime.securesms.database.OneTimePreKeyDatabase;
|
||||
import org.thoughtcrime.securesms.database.PendingRetryReceiptDatabase;
|
||||
import org.thoughtcrime.securesms.database.SearchDatabase;
|
||||
import org.thoughtcrime.securesms.database.SenderKeyDatabase;
|
||||
import org.thoughtcrime.securesms.database.SenderKeySharedDatabase;
|
||||
import org.thoughtcrime.securesms.database.SessionDatabase;
|
||||
import org.thoughtcrime.securesms.database.SignedPreKeyDatabase;
|
||||
import org.thoughtcrime.securesms.database.SmsDatabase;
|
||||
@@ -39,6 +42,7 @@ import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.keyvalue.KeyValueDataSet;
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
||||
import org.thoughtcrime.securesms.profiles.AvatarHelper;
|
||||
import org.thoughtcrime.securesms.service.PendingRetryReceiptManager;
|
||||
import org.thoughtcrime.securesms.util.SetUtil;
|
||||
import org.thoughtcrime.securesms.util.Stopwatch;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
@@ -77,7 +81,10 @@ public class FullBackupExporter extends FullBackupBase {
|
||||
SessionDatabase.TABLE_NAME,
|
||||
SearchDatabase.SMS_FTS_TABLE_NAME,
|
||||
SearchDatabase.MMS_FTS_TABLE_NAME,
|
||||
EmojiSearchDatabase.TABLE_NAME
|
||||
EmojiSearchDatabase.TABLE_NAME,
|
||||
SenderKeyDatabase.TABLE_NAME,
|
||||
SenderKeySharedDatabase.TABLE_NAME,
|
||||
PendingRetryReceiptDatabase.TABLE_NAME
|
||||
);
|
||||
|
||||
public static void export(@NonNull Context context,
|
||||
|
||||
@@ -6,12 +6,15 @@ import android.graphics.Bitmap;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Color;
|
||||
import android.graphics.Paint;
|
||||
import android.graphics.Rect;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.util.AttributeSet;
|
||||
|
||||
import androidx.annotation.DrawableRes;
|
||||
import androidx.annotation.IntRange;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.Px;
|
||||
import androidx.appcompat.widget.AppCompatImageView;
|
||||
import androidx.fragment.app.FragmentActivity;
|
||||
|
||||
@@ -20,21 +23,23 @@ import com.bumptech.glide.load.Transformation;
|
||||
import com.bumptech.glide.load.engine.DiskCacheStrategy;
|
||||
import com.bumptech.glide.load.resource.bitmap.CircleCrop;
|
||||
import com.bumptech.glide.load.resource.bitmap.DownsampleStrategy;
|
||||
import com.bumptech.glide.request.target.SimpleTarget;
|
||||
import com.bumptech.glide.request.transition.Transition;
|
||||
|
||||
import org.signal.core.util.logging.Log;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.components.settings.conversation.ConversationSettingsActivity;
|
||||
import org.thoughtcrime.securesms.contacts.avatars.ContactPhoto;
|
||||
import org.thoughtcrime.securesms.contacts.avatars.ProfileContactPhoto;
|
||||
import org.thoughtcrime.securesms.contacts.avatars.ResourceContactPhoto;
|
||||
import org.thoughtcrime.securesms.conversation.colors.AvatarColor;
|
||||
import org.thoughtcrime.securesms.conversation.colors.ChatColors;
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.groups.ui.managegroup.ManageGroupActivity;
|
||||
import org.thoughtcrime.securesms.mms.GlideApp;
|
||||
import org.thoughtcrime.securesms.mms.GlideRequest;
|
||||
import org.thoughtcrime.securesms.mms.GlideRequests;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.ui.bottomsheet.RecipientBottomSheetDialogFragment;
|
||||
import org.thoughtcrime.securesms.recipients.ui.managerecipient.ManageRecipientActivity;
|
||||
import org.thoughtcrime.securesms.util.AvatarUtil;
|
||||
import org.thoughtcrime.securesms.util.BlurTransformation;
|
||||
import org.thoughtcrime.securesms.util.ThemeUtil;
|
||||
@@ -74,6 +79,7 @@ public final class AvatarImageView extends AppCompatImageView {
|
||||
private Recipient.FallbackPhotoProvider fallbackPhotoProvider;
|
||||
private boolean blurred;
|
||||
private ChatColors chatColors;
|
||||
private FixedSizeTarget fixedSizeTarget;
|
||||
|
||||
private @Nullable RecipientContactPhoto recipientContactPhoto;
|
||||
private @NonNull Drawable unknownRecipientDrawable;
|
||||
@@ -93,8 +99,8 @@ public final class AvatarImageView extends AppCompatImageView {
|
||||
|
||||
if (attrs != null) {
|
||||
TypedArray typedArray = context.getTheme().obtainStyledAttributes(attrs, R.styleable.AvatarImageView, 0, 0);
|
||||
inverted = typedArray.getBoolean(R.styleable.AvatarImageView_inverted, false);
|
||||
size = typedArray.getInt(R.styleable.AvatarImageView_fallbackImageSize, SIZE_LARGE);
|
||||
inverted = typedArray.getBoolean(R.styleable.AvatarImageView_inverted, false);
|
||||
size = typedArray.getInt(R.styleable.AvatarImageView_fallbackImageSize, SIZE_LARGE);
|
||||
typedArray.recycle();
|
||||
}
|
||||
|
||||
@@ -105,6 +111,11 @@ public final class AvatarImageView extends AppCompatImageView {
|
||||
chatColors = null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setClipBounds(Rect clipBounds) {
|
||||
super.setClipBounds(clipBounds);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDraw(Canvas canvas) {
|
||||
super.onDraw(canvas);
|
||||
@@ -148,6 +159,10 @@ public final class AvatarImageView extends AppCompatImageView {
|
||||
}
|
||||
}
|
||||
|
||||
public AvatarOptions.Builder buildOptions() {
|
||||
return new AvatarOptions.Builder(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows self as the note to self icon.
|
||||
*/
|
||||
@@ -167,11 +182,22 @@ public final class AvatarImageView extends AppCompatImageView {
|
||||
}
|
||||
|
||||
public void setAvatar(@NonNull GlideRequests requestManager, @Nullable Recipient recipient, boolean quickContactEnabled, boolean useSelfProfileAvatar) {
|
||||
setAvatar(requestManager, recipient, new AvatarOptions.Builder(this)
|
||||
.withUseSelfProfileAvatar(useSelfProfileAvatar)
|
||||
.withQuickContactEnabled(quickContactEnabled)
|
||||
.build());
|
||||
}
|
||||
|
||||
private void setAvatar(@Nullable Recipient recipient, @NonNull AvatarOptions avatarOptions) {
|
||||
setAvatar(GlideApp.with(this), recipient, avatarOptions);
|
||||
}
|
||||
|
||||
private void setAvatar(@NonNull GlideRequests requestManager, @Nullable Recipient recipient, @NonNull AvatarOptions avatarOptions) {
|
||||
if (recipient != null) {
|
||||
RecipientContactPhoto photo = (recipient.isSelf() && useSelfProfileAvatar) ? new RecipientContactPhoto(recipient,
|
||||
new ProfileContactPhoto(Recipient.self(),
|
||||
Recipient.self().getProfileAvatar()))
|
||||
: new RecipientContactPhoto(recipient);
|
||||
RecipientContactPhoto photo = (recipient.isSelf() && avatarOptions.useSelfProfileAvatar) ? new RecipientContactPhoto(recipient,
|
||||
new ProfileContactPhoto(Recipient.self(),
|
||||
Recipient.self().getProfileAvatar()))
|
||||
: new RecipientContactPhoto(recipient);
|
||||
|
||||
boolean shouldBlur = recipient.shouldBlurAvatar();
|
||||
ChatColors chatColors = recipient.getChatColors();
|
||||
@@ -184,6 +210,10 @@ public final class AvatarImageView extends AppCompatImageView {
|
||||
Drawable fallbackContactPhotoDrawable = size == SIZE_SMALL ? photo.recipient.getSmallFallbackContactPhotoDrawable(getContext(), inverted, fallbackPhotoProvider)
|
||||
: photo.recipient.getFallbackContactPhotoDrawable(getContext(), inverted, fallbackPhotoProvider);
|
||||
|
||||
if (fixedSizeTarget != null) {
|
||||
requestManager.clear(fixedSizeTarget);
|
||||
}
|
||||
|
||||
if (photo.contactPhoto != null) {
|
||||
|
||||
List<Transformation<Bitmap>> transforms = new ArrayList<>();
|
||||
@@ -193,19 +223,26 @@ public final class AvatarImageView extends AppCompatImageView {
|
||||
transforms.add(new CircleCrop());
|
||||
blurred = shouldBlur;
|
||||
|
||||
requestManager.load(photo.contactPhoto)
|
||||
.fallback(fallbackContactPhotoDrawable)
|
||||
.error(fallbackContactPhotoDrawable)
|
||||
.diskCacheStrategy(DiskCacheStrategy.ALL)
|
||||
.downsample(DownsampleStrategy.CENTER_INSIDE)
|
||||
.transform(new MultiTransformation<>(transforms))
|
||||
.into(this);
|
||||
GlideRequest<Drawable> request = requestManager.load(photo.contactPhoto)
|
||||
.fallback(fallbackContactPhotoDrawable)
|
||||
.error(fallbackContactPhotoDrawable)
|
||||
.diskCacheStrategy(DiskCacheStrategy.ALL)
|
||||
.downsample(DownsampleStrategy.CENTER_INSIDE)
|
||||
.transform(new MultiTransformation<>(transforms));
|
||||
|
||||
if (avatarOptions.fixedSize > 0) {
|
||||
fixedSizeTarget = new FixedSizeTarget(avatarOptions.fixedSize);
|
||||
request.into(fixedSizeTarget);
|
||||
} else {
|
||||
request.into(this);
|
||||
}
|
||||
|
||||
} else {
|
||||
setImageDrawable(fallbackContactPhotoDrawable);
|
||||
}
|
||||
}
|
||||
|
||||
setAvatarClickHandler(recipient, quickContactEnabled);
|
||||
setAvatarClickHandler(recipient, avatarOptions.quickContactEnabled);
|
||||
} else {
|
||||
recipientContactPhoto = null;
|
||||
requestManager.clear(this);
|
||||
@@ -225,15 +262,15 @@ public final class AvatarImageView extends AppCompatImageView {
|
||||
super.setOnClickListener(v -> {
|
||||
Context context = getContext();
|
||||
if (recipient.isPushGroup()) {
|
||||
context.startActivity(ManageGroupActivity.newIntent(context, recipient.requireGroupId().requirePush()),
|
||||
ManageGroupActivity.createTransitionBundle(context, this));
|
||||
context.startActivity(ConversationSettingsActivity.forGroup(context, recipient.requireGroupId().requirePush()),
|
||||
ConversationSettingsActivity.createTransitionBundle(context, this));
|
||||
} else {
|
||||
if (context instanceof FragmentActivity) {
|
||||
RecipientBottomSheetDialogFragment.create(recipient.getId(), null)
|
||||
.show(((FragmentActivity) context).getSupportFragmentManager(), "BOTTOM");
|
||||
} else {
|
||||
context.startActivity(ManageRecipientActivity.newIntent(context, recipient.getId()),
|
||||
ManageRecipientActivity.createTransitionBundle(context, this));
|
||||
context.startActivity(ConversationSettingsActivity.forRecipient(context, recipient.getId()),
|
||||
ConversationSettingsActivity.createTransitionBundle(context, this));
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -294,4 +331,65 @@ public final class AvatarImageView extends AppCompatImageView {
|
||||
Objects.equals(other.contactPhoto, contactPhoto);
|
||||
}
|
||||
}
|
||||
|
||||
private final class FixedSizeTarget extends SimpleTarget<Drawable> {
|
||||
|
||||
FixedSizeTarget(int size) {
|
||||
super(size, size);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResourceReady(@NonNull Drawable resource, @Nullable Transition<? super Drawable> transition) {
|
||||
setImageDrawable(resource);
|
||||
}
|
||||
}
|
||||
|
||||
public static final class AvatarOptions {
|
||||
|
||||
private final boolean quickContactEnabled;
|
||||
private final boolean useSelfProfileAvatar;
|
||||
private final int fixedSize;
|
||||
|
||||
private AvatarOptions(@NonNull Builder builder) {
|
||||
this.quickContactEnabled = builder.quickContactEnabled;
|
||||
this.useSelfProfileAvatar = builder.useSelfProfileAvatar;
|
||||
this.fixedSize = builder.fixedSize;
|
||||
}
|
||||
|
||||
public static final class Builder {
|
||||
|
||||
private final AvatarImageView avatarImageView;
|
||||
|
||||
private boolean quickContactEnabled = false;
|
||||
private boolean useSelfProfileAvatar = false;
|
||||
private int fixedSize = -1;
|
||||
|
||||
private Builder(@NonNull AvatarImageView avatarImageView) {
|
||||
this.avatarImageView = avatarImageView;
|
||||
}
|
||||
|
||||
public @NonNull Builder withQuickContactEnabled(boolean quickContactEnabled) {
|
||||
this.quickContactEnabled = quickContactEnabled;
|
||||
return this;
|
||||
}
|
||||
|
||||
public @NonNull Builder withUseSelfProfileAvatar(boolean useSelfProfileAvatar) {
|
||||
this.useSelfProfileAvatar = useSelfProfileAvatar;
|
||||
return this;
|
||||
}
|
||||
|
||||
public @NonNull Builder withFixedSize(@Px @IntRange(from = 1) int fixedSize) {
|
||||
this.fixedSize = fixedSize;
|
||||
return this;
|
||||
}
|
||||
|
||||
public AvatarOptions build() {
|
||||
return new AvatarOptions(this);
|
||||
}
|
||||
|
||||
public void load(@Nullable Recipient recipient) {
|
||||
avatarImageView.setAvatar(recipient, build());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,9 @@ import android.content.Context;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.View;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.google.android.material.tabs.TabLayout;
|
||||
|
||||
import java.util.List;
|
||||
@@ -15,6 +18,8 @@ public class ControllableTabLayout extends TabLayout {
|
||||
|
||||
private List<View> touchables;
|
||||
|
||||
private NewTabListener newTabListener;
|
||||
|
||||
public ControllableTabLayout(Context context) {
|
||||
super(context);
|
||||
}
|
||||
@@ -39,4 +44,28 @@ public class ControllableTabLayout extends TabLayout {
|
||||
|
||||
super.setEnabled(enabled);
|
||||
}
|
||||
|
||||
public void setNewTabListener(@Nullable NewTabListener newTabListener) {
|
||||
this.newTabListener = newTabListener;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull Tab newTab() {
|
||||
Tab tab = super.newTab();
|
||||
|
||||
if (newTabListener != null) {
|
||||
newTabListener.onNewTab(tab);
|
||||
}
|
||||
|
||||
return tab;
|
||||
}
|
||||
|
||||
/**
|
||||
* Allows implementor to modify tabs when they are created, before they are added to the tab layout.
|
||||
* This is useful for loading custom views, to ensure that time is not spent inflating these views
|
||||
* as the user is switching between pages.
|
||||
*/
|
||||
public interface NewTabListener {
|
||||
void onNewTab(@NonNull Tab tab);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -124,7 +124,7 @@ public class ConversationItemFooter extends LinearLayout {
|
||||
revealDot.addValueCallback(
|
||||
new KeyPath("**"),
|
||||
LottieProperty.COLOR_FILTER,
|
||||
frameInfo -> new PorterDuffColorFilter(color, PorterDuff.Mode.SRC_ATOP)
|
||||
frameInfo -> new PorterDuffColorFilter(color, PorterDuff.Mode.SRC_IN)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -313,10 +313,7 @@ public class ConversationItemFooter extends LinearLayout {
|
||||
private void showAudioDurationViews() {
|
||||
audioSpace.setVisibility(View.VISIBLE);
|
||||
audioDuration.setVisibility(View.GONE);
|
||||
|
||||
if (FeatureFlags.viewedReceipts()) {
|
||||
revealDot.setVisibility(View.VISIBLE);
|
||||
}
|
||||
revealDot.setVisibility(View.VISIBLE);
|
||||
}
|
||||
|
||||
private void hideAudioDurationViews() {
|
||||
|
||||
@@ -1,27 +1,34 @@
|
||||
package org.thoughtcrime.securesms.components;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.graphics.drawable.LayerDrawable;
|
||||
import android.graphics.PorterDuff;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.View;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.constraintlayout.widget.ConstraintLayout;
|
||||
import androidx.core.content.ContextCompat;
|
||||
|
||||
import com.annimon.stream.Stream;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.mms.GlideRequests;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.whispersystems.libsignal.util.Pair;
|
||||
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
|
||||
public class ConversationTypingView extends LinearLayout {
|
||||
public class ConversationTypingView extends ConstraintLayout {
|
||||
|
||||
private AvatarImageView avatar;
|
||||
private AvatarImageView avatar1;
|
||||
private AvatarImageView avatar2;
|
||||
private AvatarImageView avatar3;
|
||||
private View bubble;
|
||||
private TypingIndicatorView indicator;
|
||||
private TextView typistCount;
|
||||
|
||||
public ConversationTypingView(Context context, @Nullable AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
@@ -31,9 +38,12 @@ public class ConversationTypingView extends LinearLayout {
|
||||
protected void onFinishInflate() {
|
||||
super.onFinishInflate();
|
||||
|
||||
avatar = findViewById(R.id.typing_avatar);
|
||||
bubble = findViewById(R.id.typing_bubble);
|
||||
indicator = findViewById(R.id.typing_indicator);
|
||||
avatar1 = findViewById(R.id.typing_avatar_1);
|
||||
avatar2 = findViewById(R.id.typing_avatar_2);
|
||||
avatar3 = findViewById(R.id.typing_avatar_3);
|
||||
typistCount = findViewById(R.id.typing_count);
|
||||
bubble = findViewById(R.id.typing_bubble);
|
||||
indicator = findViewById(R.id.typing_indicator);
|
||||
}
|
||||
|
||||
public void setTypists(@NonNull GlideRequests glideRequests, @NonNull List<Recipient> typists, boolean isGroupThread, boolean hasWallpaper) {
|
||||
@@ -42,21 +52,44 @@ public class ConversationTypingView extends LinearLayout {
|
||||
return;
|
||||
}
|
||||
|
||||
Recipient typist = typists.get(0);
|
||||
avatar1.setVisibility(GONE);
|
||||
avatar2.setVisibility(GONE);
|
||||
avatar3.setVisibility(GONE);
|
||||
typistCount.setVisibility(GONE);
|
||||
|
||||
if (isGroupThread) {
|
||||
avatar.setAvatar(glideRequests, typist, true);
|
||||
avatar.setVisibility(VISIBLE);
|
||||
} else {
|
||||
avatar.setVisibility(GONE);
|
||||
presentGroupThreadAvatars(glideRequests, typists);
|
||||
}
|
||||
|
||||
if (hasWallpaper) {
|
||||
bubble.setBackgroundColor(ContextCompat.getColor(getContext(), R.color.conversation_item_wallpaper_bubble_color));
|
||||
typistCount.getBackground().setColorFilter(ContextCompat.getColor(getContext(), R.color.conversation_item_wallpaper_bubble_color), PorterDuff.Mode.SRC_IN);
|
||||
} else {
|
||||
bubble.setBackgroundColor(ContextCompat.getColor(getContext(), R.color.signal_background_secondary));
|
||||
typistCount.getBackground().setColorFilter(ContextCompat.getColor(getContext(), R.color.signal_background_secondary), PorterDuff.Mode.SRC_IN);
|
||||
}
|
||||
|
||||
indicator.startAnimation();
|
||||
}
|
||||
|
||||
private void presentGroupThreadAvatars(@NonNull GlideRequests glideRequests, @NonNull List<Recipient> typists) {
|
||||
avatar1.setAvatar(glideRequests, typists.get(0), typists.size() == 1);
|
||||
avatar1.setVisibility(VISIBLE);
|
||||
|
||||
if (typists.size() > 1) {
|
||||
avatar2.setAvatar(glideRequests, typists.get(1), false);
|
||||
avatar2.setVisibility(VISIBLE);
|
||||
}
|
||||
|
||||
if (typists.size() == 3) {
|
||||
avatar3.setAvatar(glideRequests, typists.get(2), false);
|
||||
avatar3.setVisibility(VISIBLE);
|
||||
}
|
||||
|
||||
if (typists.size() > 3) {
|
||||
typistCount.setText(getResources().getString(R.string.ConversationTypingView__plus_d, typists.size() - 2));
|
||||
typistCount.setVisibility(VISIBLE);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
package org.thoughtcrime.securesms.components;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.PorterDuff;
|
||||
import android.graphics.PorterDuffColorFilter;
|
||||
import android.graphics.Typeface;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.text.Spannable;
|
||||
import android.text.SpannableString;
|
||||
import android.text.SpannableStringBuilder;
|
||||
@@ -9,11 +12,15 @@ import android.text.style.StyleSpan;
|
||||
import android.util.AttributeSet;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.core.content.ContextCompat;
|
||||
|
||||
import org.signal.core.util.logging.Log;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.components.emoji.EmojiTextView;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.util.ViewUtil;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
public class FromTextView extends EmojiTextView {
|
||||
|
||||
@@ -65,10 +72,17 @@ public class FromTextView extends EmojiTextView {
|
||||
|
||||
setText(builder);
|
||||
|
||||
if (recipient.isBlocked()) setCompoundDrawablesWithIntrinsicBounds(R.drawable.ic_block_grey600_18dp, 0, 0, 0);
|
||||
else if (recipient.isMuted()) setCompoundDrawablesWithIntrinsicBounds(R.drawable.ic_volume_off_grey600_18dp, 0, 0, 0);
|
||||
if (recipient.isBlocked()) setCompoundDrawablesRelativeWithIntrinsicBounds(R.drawable.ic_block_grey600_18dp, 0, 0, 0);
|
||||
else if (recipient.isMuted()) setCompoundDrawablesRelativeWithIntrinsicBounds(getMuted(), null, null, null);
|
||||
else setCompoundDrawablesWithIntrinsicBounds(0, 0, 0, 0);
|
||||
}
|
||||
|
||||
private Drawable getMuted() {
|
||||
Drawable mutedDrawable = Objects.requireNonNull(ContextCompat.getDrawable(getContext(), R.drawable.ic_bell_disabled_16));
|
||||
|
||||
mutedDrawable.setBounds(0, 0, ViewUtil.dpToPx(18), ViewUtil.dpToPx(18));
|
||||
mutedDrawable.setColorFilter(new PorterDuffColorFilter(ContextCompat.getColor(getContext(), R.color.signal_icon_tint_secondary), PorterDuff.Mode.SRC_IN));
|
||||
|
||||
return mutedDrawable;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
/**
|
||||
* Copyright (C) 2014 Open Whisper Systems
|
||||
*
|
||||
* <p>
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* <p>
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* <p>
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
@@ -23,6 +23,7 @@ import android.os.Build;
|
||||
import android.os.Build.VERSION_CODES;
|
||||
import android.preference.PreferenceManager;
|
||||
import android.util.AttributeSet;
|
||||
import android.util.DisplayMetrics;
|
||||
import android.view.Surface;
|
||||
import android.view.View;
|
||||
import android.view.WindowInsets;
|
||||
@@ -46,21 +47,25 @@ import java.util.Set;
|
||||
public class KeyboardAwareLinearLayout extends LinearLayoutCompat {
|
||||
private static final String TAG = Log.tag(KeyboardAwareLinearLayout.class);
|
||||
|
||||
private final Rect rect = new Rect();
|
||||
private final Set<OnKeyboardHiddenListener> hiddenListeners = new HashSet<>();
|
||||
private final Set<OnKeyboardShownListener> shownListeners = new HashSet<>();
|
||||
private final int minKeyboardSize;
|
||||
private final int minCustomKeyboardSize;
|
||||
private final int defaultCustomKeyboardSize;
|
||||
private final int minCustomKeyboardTopMarginPortrait;
|
||||
private final int minCustomKeyboardTopMarginLandscape;
|
||||
private final int statusBarHeight;
|
||||
private final Rect rect = new Rect();
|
||||
private final Set<OnKeyboardHiddenListener> hiddenListeners = new HashSet<>();
|
||||
private final Set<OnKeyboardShownListener> shownListeners = new HashSet<>();
|
||||
private final DisplayMetrics displayMetrics = new DisplayMetrics();
|
||||
|
||||
private final int minKeyboardSize;
|
||||
private final int minCustomKeyboardSize;
|
||||
private final int defaultCustomKeyboardSize;
|
||||
private final int minCustomKeyboardTopMarginPortrait;
|
||||
private final int minCustomKeyboardTopMarginLandscape;
|
||||
private final int minCustomKeyboardTopMarginLandscapeBubble;
|
||||
private final int statusBarHeight;
|
||||
|
||||
private int viewInset;
|
||||
|
||||
private boolean keyboardOpen = false;
|
||||
private int rotation = -1;
|
||||
private boolean isFullscreen = false;
|
||||
private boolean isBubble = false;
|
||||
|
||||
public KeyboardAwareLinearLayout(Context context) {
|
||||
this(context, null);
|
||||
@@ -72,13 +77,14 @@ public class KeyboardAwareLinearLayout extends LinearLayoutCompat {
|
||||
|
||||
public KeyboardAwareLinearLayout(Context context, AttributeSet attrs, int defStyle) {
|
||||
super(context, attrs, defStyle);
|
||||
minKeyboardSize = getResources().getDimensionPixelSize(R.dimen.min_keyboard_size);
|
||||
minCustomKeyboardSize = getResources().getDimensionPixelSize(R.dimen.min_custom_keyboard_size);
|
||||
defaultCustomKeyboardSize = getResources().getDimensionPixelSize(R.dimen.default_custom_keyboard_size);
|
||||
minCustomKeyboardTopMarginPortrait = getResources().getDimensionPixelSize(R.dimen.min_custom_keyboard_top_margin_portrait);
|
||||
minCustomKeyboardTopMarginLandscape = getResources().getDimensionPixelSize(R.dimen.min_custom_keyboard_top_margin_portrait);
|
||||
statusBarHeight = ViewUtil.getStatusBarHeight(this);
|
||||
viewInset = getViewInset();
|
||||
minKeyboardSize = getResources().getDimensionPixelSize(R.dimen.min_keyboard_size);
|
||||
minCustomKeyboardSize = getResources().getDimensionPixelSize(R.dimen.min_custom_keyboard_size);
|
||||
defaultCustomKeyboardSize = getResources().getDimensionPixelSize(R.dimen.default_custom_keyboard_size);
|
||||
minCustomKeyboardTopMarginPortrait = getResources().getDimensionPixelSize(R.dimen.min_custom_keyboard_top_margin_portrait);
|
||||
minCustomKeyboardTopMarginLandscape = getResources().getDimensionPixelSize(R.dimen.min_custom_keyboard_top_margin_portrait);
|
||||
minCustomKeyboardTopMarginLandscapeBubble = getResources().getDimensionPixelSize(R.dimen.min_custom_keyboard_top_margin_landscape_bubble);
|
||||
statusBarHeight = ViewUtil.getStatusBarHeight(this);
|
||||
viewInset = getViewInset();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -88,6 +94,10 @@ public class KeyboardAwareLinearLayout extends LinearLayoutCompat {
|
||||
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
|
||||
}
|
||||
|
||||
public void setIsBubble(boolean isBubble) {
|
||||
this.isBubble = isBubble;
|
||||
}
|
||||
|
||||
private void updateRotation() {
|
||||
int oldRotation = rotation;
|
||||
rotation = getDeviceRotation();
|
||||
@@ -149,7 +159,7 @@ public class KeyboardAwareLinearLayout extends LinearLayoutCompat {
|
||||
if (attachInfo != null) {
|
||||
Field stableInsetsField = attachInfo.getClass().getDeclaredField("mStableInsets");
|
||||
stableInsetsField.setAccessible(true);
|
||||
Rect insets = (Rect)stableInsetsField.get(attachInfo);
|
||||
Rect insets = (Rect) stableInsetsField.get(attachInfo);
|
||||
if (insets != null) {
|
||||
return insets.bottom;
|
||||
}
|
||||
@@ -197,28 +207,51 @@ public class KeyboardAwareLinearLayout extends LinearLayoutCompat {
|
||||
int rotation = getDeviceRotation();
|
||||
return rotation == Surface.ROTATION_90 || rotation == Surface.ROTATION_270;
|
||||
}
|
||||
|
||||
private int getDeviceRotation() {
|
||||
return ServiceUtil.getWindowManager(getContext()).getDefaultDisplay().getRotation();
|
||||
if (Build.VERSION.SDK_INT >= 30) {
|
||||
getContext().getDisplay().getRealMetrics(displayMetrics);
|
||||
} else {
|
||||
ServiceUtil.getWindowManager(getContext()).getDefaultDisplay().getRealMetrics(displayMetrics);
|
||||
}
|
||||
return displayMetrics.widthPixels > displayMetrics.heightPixels ? Surface.ROTATION_90 : Surface.ROTATION_0;
|
||||
}
|
||||
|
||||
private int getKeyboardLandscapeHeight() {
|
||||
if (isBubble) {
|
||||
return getRootView().getHeight() - minCustomKeyboardTopMarginLandscapeBubble;
|
||||
}
|
||||
|
||||
int keyboardHeight = PreferenceManager.getDefaultSharedPreferences(getContext())
|
||||
.getInt("keyboard_height_landscape", defaultCustomKeyboardSize);
|
||||
return Util.clamp(keyboardHeight, minCustomKeyboardSize, getRootView().getHeight() - minCustomKeyboardTopMarginLandscape);
|
||||
}
|
||||
|
||||
private int getKeyboardPortraitHeight() {
|
||||
if (isBubble) {
|
||||
int height = getRootView().getHeight();
|
||||
return height - (int)(height * 0.45);
|
||||
}
|
||||
|
||||
int keyboardHeight = PreferenceManager.getDefaultSharedPreferences(getContext())
|
||||
.getInt("keyboard_height_portrait", defaultCustomKeyboardSize);
|
||||
return Util.clamp(keyboardHeight, minCustomKeyboardSize, getRootView().getHeight() - minCustomKeyboardTopMarginPortrait);
|
||||
}
|
||||
|
||||
private void setKeyboardPortraitHeight(int height) {
|
||||
if (isBubble) {
|
||||
return;
|
||||
}
|
||||
|
||||
PreferenceManager.getDefaultSharedPreferences(getContext())
|
||||
.edit().putInt("keyboard_height_portrait", height).apply();
|
||||
}
|
||||
|
||||
private void setKeyboardLandscapeHeight(int height) {
|
||||
if (isBubble) {
|
||||
return;
|
||||
}
|
||||
|
||||
PreferenceManager.getDefaultSharedPreferences(getContext())
|
||||
.edit().putInt("keyboard_height_landscape", height).apply();
|
||||
}
|
||||
|
||||
@@ -293,6 +293,10 @@ public class QuoteView extends FrameLayout implements RecipientForeverObserver {
|
||||
footerView.setBackgroundColor(ContextCompat.getColor(getContext(), R.color.quote_view_background));
|
||||
}
|
||||
|
||||
public void setTextSize(int unit, float size) {
|
||||
bodyView.setTextSize(unit, size);
|
||||
}
|
||||
|
||||
public long getQuoteId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
@@ -3,6 +3,12 @@ package org.thoughtcrime.securesms.components;
|
||||
import android.content.Context;
|
||||
import android.content.res.TypedArray;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.PorterDuff;
|
||||
import android.graphics.PorterDuffColorFilter;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.graphics.drawable.ShapeDrawable;
|
||||
import android.graphics.drawable.shapes.RoundRectShape;
|
||||
import android.graphics.drawable.shapes.Shape;
|
||||
import android.net.Uri;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.View;
|
||||
@@ -12,6 +18,7 @@ import android.widget.ImageView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.UiThread;
|
||||
import androidx.core.content.ContextCompat;
|
||||
|
||||
import com.bumptech.glide.RequestBuilder;
|
||||
import com.bumptech.glide.load.engine.DiskCacheStrategy;
|
||||
@@ -25,6 +32,7 @@ import org.signal.core.util.logging.Log;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.blurhash.BlurHash;
|
||||
import org.thoughtcrime.securesms.database.AttachmentDatabase;
|
||||
import org.thoughtcrime.securesms.giph.mp4.GiphyMp4PlaybackPolicy;
|
||||
import org.thoughtcrime.securesms.mms.DecryptableStreamUriLoader.DecryptableUri;
|
||||
import org.thoughtcrime.securesms.mms.GlideRequest;
|
||||
import org.thoughtcrime.securesms.mms.GlideRequests;
|
||||
@@ -40,6 +48,7 @@ import org.thoughtcrime.securesms.util.views.Stub;
|
||||
import org.thoughtcrime.securesms.video.VideoPlayer;
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.Locale;
|
||||
import java.util.Objects;
|
||||
@@ -61,7 +70,6 @@ public class ThumbnailView extends FrameLayout {
|
||||
private ImageView blurhash;
|
||||
private View playOverlay;
|
||||
private View captionIcon;
|
||||
private Stub<VideoPlayer> videoPlayer;
|
||||
private OnClickListener parentClickListener;
|
||||
|
||||
private final int[] dimens = new int[2];
|
||||
@@ -93,7 +101,7 @@ public class ThumbnailView extends FrameLayout {
|
||||
this.blurhash = findViewById(R.id.thumbnail_blurhash);
|
||||
this.playOverlay = findViewById(R.id.play_overlay);
|
||||
this.captionIcon = findViewById(R.id.thumbnail_caption_icon);
|
||||
this.videoPlayer = new Stub<>(findViewById(R.id.thumbnail_player_stub));
|
||||
|
||||
super.setOnClickListener(new ThumbnailClickDispatcher());
|
||||
|
||||
if (attrs != null) {
|
||||
@@ -104,9 +112,18 @@ public class ThumbnailView extends FrameLayout {
|
||||
bounds[MAX_HEIGHT] = typedArray.getDimensionPixelSize(R.styleable.ThumbnailView_maxHeight, 0);
|
||||
radius = typedArray.getDimensionPixelSize(R.styleable.ThumbnailView_thumbnail_radius, getResources().getDimensionPixelSize(R.dimen.thumbnail_default_radius));
|
||||
fit = typedArray.getInt(R.styleable.ThumbnailView_thumbnail_fit, 0) == 1 ? new FitCenter() : new CenterCrop();
|
||||
|
||||
int transparentOverlayColor = typedArray.getColor(R.styleable.ThumbnailView_transparent_overlay_color, -1);
|
||||
if (transparentOverlayColor > 0) {
|
||||
image.setColorFilter(new PorterDuffColorFilter(transparentOverlayColor, PorterDuff.Mode.SRC_ATOP));
|
||||
} else {
|
||||
image.setColorFilter(null);
|
||||
}
|
||||
|
||||
typedArray.recycle();
|
||||
} else {
|
||||
radius = getResources().getDimensionPixelSize(R.dimen.message_corner_collapse_radius);
|
||||
image.setColorFilter(null);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -6,19 +6,25 @@ import androidx.annotation.AttrRes;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import java.util.Arrays;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
|
||||
public class CompositeEmojiPageModel implements EmojiPageModel {
|
||||
@AttrRes private final int iconAttr;
|
||||
@NonNull private final List<EmojiPageModel> models;
|
||||
@AttrRes private final int iconAttr;
|
||||
@NonNull private final List<EmojiPageModel> models;
|
||||
|
||||
public CompositeEmojiPageModel(@AttrRes int iconAttr, @NonNull List<EmojiPageModel> models) {
|
||||
this.iconAttr = iconAttr;
|
||||
this.models = models;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getKey() {
|
||||
return Util.hasItems(models) ? models.get(0).getKey() : "";
|
||||
}
|
||||
|
||||
public int getIconAttr() {
|
||||
return iconAttr;
|
||||
}
|
||||
|
||||
@@ -22,4 +22,8 @@ public class Emoji {
|
||||
public List<String> getVariations() {
|
||||
return variations;
|
||||
}
|
||||
|
||||
public boolean hasMultipleVariations() {
|
||||
return variations.size() > 1;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
package org.thoughtcrime.securesms.components.emoji
|
||||
|
||||
import android.graphics.Canvas
|
||||
import android.graphics.Rect
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.view.View
|
||||
import androidx.appcompat.widget.AppCompatTextView
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import org.thoughtcrime.securesms.components.emoji.EmojiPageViewGridAdapter.EmojiModel
|
||||
import org.thoughtcrime.securesms.util.InsetItemDecoration
|
||||
import org.thoughtcrime.securesms.util.ViewUtil
|
||||
|
||||
private val EDGE_LENGTH: Int = ViewUtil.dpToPx(7)
|
||||
private val HORIZONTAL_INSET: Int = ViewUtil.dpToPx(11)
|
||||
private val VERTICAL_INSET: Int = ViewUtil.dpToPx(8)
|
||||
|
||||
/**
|
||||
* Use super class to add insets to the emojis and use the [onDrawOver] to draw the variation
|
||||
* hint if the emoji has more than one variation.
|
||||
*/
|
||||
class EmojiItemDecoration(private val allowVariations: Boolean, private val variationsDrawable: Drawable) : InsetItemDecoration(SetInset()) {
|
||||
|
||||
override fun onDrawOver(canvas: Canvas, parent: RecyclerView, state: RecyclerView.State) {
|
||||
super.onDrawOver(canvas, parent, state)
|
||||
|
||||
val adapter: EmojiPageViewGridAdapter? = parent.adapter as? EmojiPageViewGridAdapter
|
||||
if (allowVariations && adapter != null) {
|
||||
for (i in 0 until parent.childCount) {
|
||||
val child: View = parent.getChildAt(i)
|
||||
val position: Int = parent.getChildAdapterPosition(child)
|
||||
if (position >= 0 && position <= adapter.itemCount) {
|
||||
val model = adapter.currentList[position]
|
||||
if (model is EmojiModel && model.emoji.hasMultipleVariations()) {
|
||||
variationsDrawable.setBounds(child.right, child.bottom - EDGE_LENGTH, child.right + EDGE_LENGTH, child.bottom)
|
||||
variationsDrawable.draw(canvas)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class SetInset : InsetItemDecoration.SetInset() {
|
||||
override fun setInset(outRect: Rect, view: View, parent: RecyclerView) {
|
||||
val isFirstHeader = view.javaClass == AppCompatTextView::class.java && getPosition(view, parent) == 0
|
||||
outRect.left = HORIZONTAL_INSET
|
||||
outRect.right = HORIZONTAL_INSET
|
||||
outRect.top = if (isFirstHeader) 0 else VERTICAL_INSET
|
||||
outRect.bottom = VERTICAL_INSET
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,7 @@ import androidx.annotation.Nullable;
|
||||
import java.util.List;
|
||||
|
||||
public interface EmojiPageModel {
|
||||
String getKey();
|
||||
int getIconAttr();
|
||||
List<String> getEmoji();
|
||||
List<Emoji> getDisplayEmoji();
|
||||
|
||||
@@ -1,91 +1,143 @@
|
||||
package org.thoughtcrime.securesms.components.emoji;
|
||||
|
||||
import android.content.Context;
|
||||
import android.view.LayoutInflater;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.View;
|
||||
import android.widget.FrameLayout;
|
||||
|
||||
import androidx.annotation.LayoutRes;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import androidx.recyclerview.widget.GridLayoutManager;
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
import androidx.recyclerview.widget.LinearSmoothScroller;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import com.annimon.stream.Stream;
|
||||
|
||||
import org.signal.core.util.logging.Log;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.components.emoji.EmojiKeyboardProvider.EmojiEventListener;
|
||||
import org.thoughtcrime.securesms.components.emoji.EmojiPageViewGridAdapter.EmojiHeader;
|
||||
import org.thoughtcrime.securesms.components.emoji.EmojiPageViewGridAdapter.EmojiNoResultsModel;
|
||||
import org.thoughtcrime.securesms.components.emoji.EmojiPageViewGridAdapter.VariationSelectorListener;
|
||||
import org.thoughtcrime.securesms.emoji.EmojiCategory;
|
||||
import org.thoughtcrime.securesms.util.ContextUtil;
|
||||
import org.thoughtcrime.securesms.util.DrawableUtil;
|
||||
import org.thoughtcrime.securesms.util.MappingModel;
|
||||
import org.thoughtcrime.securesms.util.MappingModelList;
|
||||
import org.thoughtcrime.securesms.util.ViewUtil;
|
||||
|
||||
public class EmojiPageView extends FrameLayout implements VariationSelectorListener {
|
||||
private static final String TAG = Log.tag(EmojiPageView.class);
|
||||
import java.util.Optional;
|
||||
|
||||
public class EmojiPageView extends RecyclerView implements VariationSelectorListener {
|
||||
|
||||
private EmojiPageModel model;
|
||||
private AdapterFactory adapterFactory;
|
||||
private RecyclerView recyclerView;
|
||||
private RecyclerView.LayoutManager layoutManager;
|
||||
private LinearLayoutManager layoutManager;
|
||||
private RecyclerView.OnItemTouchListener scrollDisabler;
|
||||
private VariationSelectorListener variationSelectorListener;
|
||||
private EmojiVariationSelectorPopup popup;
|
||||
|
||||
public EmojiPageView(@NonNull Context context, @Nullable AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
}
|
||||
|
||||
public EmojiPageView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
|
||||
super(context, attrs, defStyleAttr);
|
||||
}
|
||||
|
||||
public EmojiPageView(@NonNull Context context,
|
||||
@NonNull EmojiEventListener emojiSelectionListener,
|
||||
@NonNull VariationSelectorListener variationSelectorListener,
|
||||
boolean allowVariations)
|
||||
{
|
||||
this(context, emojiSelectionListener, variationSelectorListener, allowVariations, new GridLayoutManager(context, 8), R.layout.emoji_display_item);
|
||||
super(context);
|
||||
initialize(emojiSelectionListener, variationSelectorListener, allowVariations);
|
||||
}
|
||||
|
||||
public EmojiPageView(@NonNull Context context,
|
||||
@NonNull EmojiEventListener emojiSelectionListener,
|
||||
@NonNull VariationSelectorListener variationSelectorListener,
|
||||
boolean allowVariations,
|
||||
@NonNull RecyclerView.LayoutManager layoutManager,
|
||||
@NonNull LinearLayoutManager layoutManager,
|
||||
@LayoutRes int displayItemLayoutResId)
|
||||
{
|
||||
super(context);
|
||||
final View view = LayoutInflater.from(getContext()).inflate(R.layout.emoji_grid_layout, this, true);
|
||||
initialize(emojiSelectionListener, variationSelectorListener, allowVariations, layoutManager, displayItemLayoutResId);
|
||||
}
|
||||
|
||||
public void initialize(@NonNull EmojiEventListener emojiSelectionListener,
|
||||
@NonNull VariationSelectorListener variationSelectorListener,
|
||||
boolean allowVariations)
|
||||
{
|
||||
initialize(emojiSelectionListener, variationSelectorListener, allowVariations, new GridLayoutManager(getContext(), 8), R.layout.emoji_display_item);
|
||||
Drawable drawable = DrawableUtil.tint(ContextUtil.requireDrawable(getContext(), R.drawable.triangle_bottom_right_corner), ContextCompat.getColor(getContext(), R.color.signal_button_secondary_text_disabled));
|
||||
addItemDecoration(new EmojiItemDecoration(allowVariations, drawable));
|
||||
}
|
||||
|
||||
public void initialize(@NonNull EmojiEventListener emojiSelectionListener,
|
||||
@NonNull VariationSelectorListener variationSelectorListener,
|
||||
boolean allowVariations,
|
||||
@NonNull LinearLayoutManager layoutManager,
|
||||
@LayoutRes int displayItemLayoutResId)
|
||||
{
|
||||
this.variationSelectorListener = variationSelectorListener;
|
||||
|
||||
this.recyclerView = view.findViewById(R.id.emoji);
|
||||
this.layoutManager = layoutManager;
|
||||
this.scrollDisabler = new ScrollDisabler();
|
||||
this.popup = new EmojiVariationSelectorPopup(context, emojiSelectionListener);
|
||||
this.popup = new EmojiVariationSelectorPopup(getContext(), emojiSelectionListener);
|
||||
this.adapterFactory = () -> new EmojiPageViewGridAdapter(popup,
|
||||
emojiSelectionListener,
|
||||
this,
|
||||
allowVariations,
|
||||
displayItemLayoutResId);
|
||||
|
||||
recyclerView.setLayoutManager(layoutManager);
|
||||
recyclerView.setItemAnimator(null);
|
||||
if (this.layoutManager instanceof GridLayoutManager) {
|
||||
GridLayoutManager gridLayout = (GridLayoutManager) this.layoutManager;
|
||||
gridLayout.setSpanSizeLookup(new GridLayoutManager.SpanSizeLookup() {
|
||||
@Override
|
||||
public int getSpanSize(int position) {
|
||||
if (getAdapter() != null) {
|
||||
Optional<MappingModel<?>> model = getAdapter().getModel(position);
|
||||
if (model.isPresent() && (model.get() instanceof EmojiHeader || model.get() instanceof EmojiNoResultsModel)) {
|
||||
return gridLayout.getSpanCount();
|
||||
}
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
setLayoutManager(layoutManager);
|
||||
}
|
||||
|
||||
public void presentForEmojiKeyboard() {
|
||||
recyclerView.setPadding(recyclerView.getPaddingLeft(),
|
||||
recyclerView.getPaddingTop(),
|
||||
recyclerView.getPaddingRight(),
|
||||
recyclerView.getPaddingBottom() + ViewUtil.dpToPx(56));
|
||||
setPadding(getPaddingLeft(),
|
||||
getPaddingTop(),
|
||||
getPaddingRight(),
|
||||
getPaddingBottom() + ViewUtil.dpToPx(56));
|
||||
|
||||
recyclerView.setClipToPadding(false);
|
||||
setClipToPadding(false);
|
||||
}
|
||||
|
||||
public void onSelected() {
|
||||
if (model.isDynamic() && recyclerView.getAdapter() != null) {
|
||||
recyclerView.getAdapter().notifyDataSetChanged();
|
||||
if (getAdapter() != null && (model == null || model.isDynamic())) {
|
||||
getAdapter().notifyDataSetChanged();
|
||||
}
|
||||
}
|
||||
|
||||
public void setList(@NonNull MappingModelList list) {
|
||||
this.model = null;
|
||||
EmojiPageViewGridAdapter adapter = adapterFactory.create();
|
||||
setAdapter(adapter);
|
||||
adapter.submitList(list);
|
||||
}
|
||||
|
||||
public void setModel(@Nullable EmojiPageModel model) {
|
||||
this.model = model;
|
||||
|
||||
EmojiPageViewGridAdapter adapter = adapterFactory.create();
|
||||
recyclerView.setAdapter(adapter);
|
||||
setAdapter(adapter);
|
||||
adapter.submitList(getMappingModelList());
|
||||
}
|
||||
|
||||
@@ -93,18 +145,21 @@ public class EmojiPageView extends FrameLayout implements VariationSelectorListe
|
||||
this.model = model;
|
||||
|
||||
EmojiPageViewGridAdapter adapter = adapterFactory.create();
|
||||
recyclerView.setAdapter(adapter);
|
||||
setAdapter(adapter);
|
||||
adapter.submitList(getMappingModelList());
|
||||
}
|
||||
|
||||
private @NonNull MappingModelList getMappingModelList() {
|
||||
MappingModelList mappingModels = new MappingModelList();
|
||||
|
||||
if (model != null) {
|
||||
mappingModels.addAll(Stream.of(model.getDisplayEmoji()).map(EmojiPageViewGridAdapter.EmojiModel::new).toList());
|
||||
boolean emoticonPage = EmojiCategory.EMOTICONS.getKey().equals(model.getKey());
|
||||
return model.getDisplayEmoji()
|
||||
.stream()
|
||||
.map(e -> emoticonPage ? new EmojiPageViewGridAdapter.EmojiTextModel(model.getKey(), e)
|
||||
: new EmojiPageViewGridAdapter.EmojiModel(model.getKey(), e))
|
||||
.collect(MappingModelList.collect());
|
||||
}
|
||||
|
||||
return mappingModels;
|
||||
return new MappingModelList();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -117,8 +172,8 @@ public class EmojiPageView extends FrameLayout implements VariationSelectorListe
|
||||
@Override
|
||||
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
|
||||
if (layoutManager instanceof GridLayoutManager) {
|
||||
int idealWidth = getContext().getResources().getDimensionPixelOffset(R.dimen.emoji_drawer_item_width);
|
||||
int spanCount = Math.max(w / idealWidth, 1);
|
||||
int idealWidth = getContext().getResources().getDimensionPixelOffset(R.dimen.emoji_drawer_item_width);
|
||||
int spanCount = Math.max(w / idealWidth, 1);
|
||||
|
||||
((GridLayoutManager) layoutManager).setSpanCount(spanCount);
|
||||
}
|
||||
@@ -127,9 +182,9 @@ public class EmojiPageView extends FrameLayout implements VariationSelectorListe
|
||||
@Override
|
||||
public void onVariationSelectorStateChanged(boolean open) {
|
||||
if (open) {
|
||||
recyclerView.addOnItemTouchListener(scrollDisabler);
|
||||
addOnItemTouchListener(scrollDisabler);
|
||||
} else {
|
||||
post(() -> recyclerView.removeOnItemTouchListener(scrollDisabler));
|
||||
post(() -> removeOnItemTouchListener(scrollDisabler));
|
||||
}
|
||||
|
||||
if (variationSelectorListener != null) {
|
||||
@@ -138,7 +193,29 @@ public class EmojiPageView extends FrameLayout implements VariationSelectorListe
|
||||
}
|
||||
|
||||
public void setRecyclerNestedScrollingEnabled(boolean enabled) {
|
||||
recyclerView.setNestedScrollingEnabled(enabled);
|
||||
setNestedScrollingEnabled(enabled);
|
||||
}
|
||||
|
||||
public void smoothScrollToPositionTop(int position) {
|
||||
int currentPosition = layoutManager.findFirstCompletelyVisibleItemPosition();
|
||||
boolean shortTrip = Math.abs(currentPosition - position) < 475;
|
||||
|
||||
if (shortTrip) {
|
||||
RecyclerView.SmoothScroller smoothScroller = new LinearSmoothScroller(getContext()) {
|
||||
@Override
|
||||
protected int getVerticalSnapPreference() {
|
||||
return LinearSmoothScroller.SNAP_TO_START;
|
||||
}
|
||||
};
|
||||
smoothScroller.setTargetPosition(position);
|
||||
layoutManager.startSmoothScroll(smoothScroller);
|
||||
} else {
|
||||
layoutManager.scrollToPositionWithOffset(position, 0);
|
||||
}
|
||||
}
|
||||
|
||||
public @Nullable EmojiPageViewGridAdapter getAdapter() {
|
||||
return (EmojiPageViewGridAdapter) super.getAdapter();
|
||||
}
|
||||
|
||||
private static class ScrollDisabler implements RecyclerView.OnItemTouchListener {
|
||||
|
||||
@@ -2,26 +2,22 @@ package org.thoughtcrime.securesms.components.emoji;
|
||||
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.PopupWindow;
|
||||
import android.widget.Space;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.LayoutRes;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.components.emoji.EmojiKeyboardProvider.EmojiEventListener;
|
||||
import org.thoughtcrime.securesms.keyboard.emoji.KeyboardPageSearchView;
|
||||
import org.thoughtcrime.securesms.util.MappingAdapter;
|
||||
import org.thoughtcrime.securesms.util.MappingModel;
|
||||
import org.thoughtcrime.securesms.util.MappingViewHolder;
|
||||
|
||||
public class EmojiPageViewGridAdapter extends MappingAdapter implements PopupWindow.OnDismissListener {
|
||||
|
||||
private final VariationSelectorListener variationSelectorListener;
|
||||
private final VariationSelectorListener variationSelectorListener;
|
||||
|
||||
public EmojiPageViewGridAdapter(@NonNull EmojiVariationSelectorPopup popup,
|
||||
@NonNull EmojiEventListener emojiEventListener,
|
||||
@@ -33,7 +29,10 @@ public class EmojiPageViewGridAdapter extends MappingAdapter implements PopupWin
|
||||
|
||||
popup.setOnDismissListener(this);
|
||||
|
||||
registerFactory(EmojiHeader.class, new LayoutFactory<>(EmojiHeaderViewHolder::new, R.layout.emoji_grid_header));
|
||||
registerFactory(EmojiModel.class, new LayoutFactory<>(v -> new EmojiViewHolder(v, emojiEventListener, variationSelectorListener, popup, allowVariations), displayItemLayoutResId));
|
||||
registerFactory(EmojiTextModel.class, new LayoutFactory<>(v -> new EmojiTextViewHolder(v, emojiEventListener), R.layout.emoji_text_display_item));
|
||||
registerFactory(EmojiNoResultsModel.class, new LayoutFactory<>(MappingViewHolder.SimpleViewHolder::new, R.layout.emoji_grid_no_results));
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -41,21 +40,73 @@ public class EmojiPageViewGridAdapter extends MappingAdapter implements PopupWin
|
||||
variationSelectorListener.onVariationSelectorStateChanged(false);
|
||||
}
|
||||
|
||||
static class EmojiModel implements MappingModel<EmojiModel> {
|
||||
public static class EmojiHeader implements MappingModel<EmojiHeader>, HasKey {
|
||||
|
||||
private final Emoji emoji;
|
||||
private final String key;
|
||||
private final int title;
|
||||
|
||||
EmojiModel(@NonNull Emoji emoji) {
|
||||
public EmojiHeader(@NonNull String key, int title) {
|
||||
this.key = key;
|
||||
this.title = title;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull String getKey() {
|
||||
return key;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean areItemsTheSame(@NonNull EmojiHeader newItem) {
|
||||
return title == newItem.title;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean areContentsTheSame(@NonNull EmojiHeader newItem) {
|
||||
return areItemsTheSame(newItem);
|
||||
}
|
||||
}
|
||||
|
||||
static class EmojiHeaderViewHolder extends MappingViewHolder<EmojiHeader> {
|
||||
|
||||
private final TextView title;
|
||||
|
||||
public EmojiHeaderViewHolder(@NonNull View itemView) {
|
||||
super(itemView);
|
||||
title = findViewById(R.id.emoji_grid_header_title);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void bind(@NonNull EmojiHeader model) {
|
||||
title.setText(model.title);
|
||||
}
|
||||
}
|
||||
|
||||
public static class EmojiModel implements MappingModel<EmojiModel>, HasKey {
|
||||
|
||||
private final String key;
|
||||
private final Emoji emoji;
|
||||
|
||||
public EmojiModel(@NonNull String key, @NonNull Emoji emoji) {
|
||||
this.key = key;
|
||||
this.emoji = emoji;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean areItemsTheSame(@NonNull @NotNull EmojiModel newItem) {
|
||||
public @NonNull String getKey() {
|
||||
return key;
|
||||
}
|
||||
|
||||
public @NonNull Emoji getEmoji() {
|
||||
return emoji;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean areItemsTheSame(@NonNull EmojiModel newItem) {
|
||||
return newItem.emoji.getValue().equals(emoji.getValue());
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean areContentsTheSame(@NonNull @NotNull EmojiModel newItem) {
|
||||
public boolean areContentsTheSame(@NonNull EmojiModel newItem) {
|
||||
return areItemsTheSame(newItem);
|
||||
}
|
||||
}
|
||||
@@ -67,9 +118,8 @@ public class EmojiPageViewGridAdapter extends MappingAdapter implements PopupWin
|
||||
private final EmojiEventListener emojiEventListener;
|
||||
private final boolean allowVariations;
|
||||
|
||||
private final ImageView imageView;
|
||||
private final AsciiEmojiView textView;
|
||||
private final ImageView hintCorner;
|
||||
private final ImageView imageView;
|
||||
private final ImageView hintCorner;
|
||||
|
||||
public EmojiViewHolder(@NonNull View itemView,
|
||||
@NonNull EmojiEventListener emojiEventListener,
|
||||
@@ -85,31 +135,26 @@ public class EmojiPageViewGridAdapter extends MappingAdapter implements PopupWin
|
||||
this.allowVariations = allowVariations;
|
||||
|
||||
this.imageView = itemView.findViewById(R.id.emoji_image);
|
||||
this.textView = itemView.findViewById(R.id.emoji_text);
|
||||
this.hintCorner = itemView.findViewById(R.id.emoji_variation_hint);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void bind(@NonNull @NotNull EmojiModel model) {
|
||||
public void bind(@NonNull EmojiModel model) {
|
||||
final Drawable drawable = EmojiProvider.getEmojiDrawable(imageView.getContext(), model.emoji.getValue());
|
||||
|
||||
if (drawable != null) {
|
||||
textView.setVisibility(View.GONE);
|
||||
imageView.setVisibility(View.VISIBLE);
|
||||
|
||||
imageView.setImageDrawable(drawable);
|
||||
} else {
|
||||
textView.setVisibility(View.VISIBLE);
|
||||
imageView.setVisibility(View.GONE);
|
||||
|
||||
textView.setEmoji(model.emoji.getValue());
|
||||
}
|
||||
|
||||
itemView.setOnClickListener(v -> {
|
||||
emojiEventListener.onEmojiSelected(model.emoji.getValue());
|
||||
});
|
||||
|
||||
if (allowVariations && model.emoji.getVariations().size() > 1) {
|
||||
if (allowVariations && model.emoji.hasMultipleVariations()) {
|
||||
if (hintCorner != null) {
|
||||
hintCorner.setVisibility(View.VISIBLE);
|
||||
}
|
||||
itemView.setOnLongClickListener(v -> {
|
||||
popup.dismiss();
|
||||
popup.setVariations(model.emoji.getVariations());
|
||||
@@ -117,14 +162,84 @@ public class EmojiPageViewGridAdapter extends MappingAdapter implements PopupWin
|
||||
variationSelectorListener.onVariationSelectorStateChanged(true);
|
||||
return true;
|
||||
});
|
||||
hintCorner.setVisibility(View.VISIBLE);
|
||||
} else {
|
||||
if (hintCorner != null) {
|
||||
hintCorner.setVisibility(View.GONE);
|
||||
}
|
||||
itemView.setOnLongClickListener(null);
|
||||
hintCorner.setVisibility(View.GONE);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static class EmojiTextModel implements MappingModel<EmojiTextModel>, HasKey {
|
||||
private final String key;
|
||||
private final Emoji emoji;
|
||||
|
||||
public EmojiTextModel(@NonNull String key, @NonNull Emoji emoji) {
|
||||
this.key = key;
|
||||
this.emoji = emoji;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull String getKey() {
|
||||
return key;
|
||||
}
|
||||
|
||||
public @NonNull Emoji getEmoji() {
|
||||
return emoji;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean areItemsTheSame(@NonNull EmojiTextModel newItem) {
|
||||
return newItem.emoji.getValue().equals(emoji.getValue());
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean areContentsTheSame(@NonNull EmojiTextModel newItem) {
|
||||
return areItemsTheSame(newItem);
|
||||
}
|
||||
}
|
||||
|
||||
static class EmojiTextViewHolder extends MappingViewHolder<EmojiTextModel> {
|
||||
|
||||
private final EmojiEventListener emojiEventListener;
|
||||
private final AsciiEmojiView textView;
|
||||
|
||||
public EmojiTextViewHolder(@NonNull View itemView,
|
||||
@NonNull EmojiEventListener emojiEventListener)
|
||||
{
|
||||
super(itemView);
|
||||
|
||||
this.emojiEventListener = emojiEventListener;
|
||||
this.textView = itemView.findViewById(R.id.emoji_text);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void bind(@NonNull EmojiTextModel model) {
|
||||
textView.setEmoji(model.emoji.getValue());
|
||||
|
||||
itemView.setOnClickListener(v -> {
|
||||
emojiEventListener.onEmojiSelected(model.emoji.getValue());
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public static class EmojiNoResultsModel implements MappingModel<EmojiNoResultsModel> {
|
||||
@Override
|
||||
public boolean areItemsTheSame(@NonNull EmojiNoResultsModel newItem) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean areContentsTheSame(@NonNull EmojiNoResultsModel newItem) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
public interface HasKey {
|
||||
@NonNull String getKey();
|
||||
}
|
||||
|
||||
public interface VariationSelectorListener {
|
||||
void onVariationSelectorStateChanged(boolean open);
|
||||
}
|
||||
|
||||
@@ -28,6 +28,7 @@ import java.util.List;
|
||||
public class RecentEmojiPageModel implements EmojiPageModel {
|
||||
private static final String TAG = Log.tag(RecentEmojiPageModel.class);
|
||||
private static final int EMOJI_LRU_SIZE = 50;
|
||||
public static final String KEY = "Recents";
|
||||
|
||||
private final SharedPreferences prefs;
|
||||
private final String preferenceName;
|
||||
@@ -55,6 +56,11 @@ public class RecentEmojiPageModel implements EmojiPageModel {
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getKey() {
|
||||
return KEY;
|
||||
}
|
||||
|
||||
@Override public int getIconAttr() {
|
||||
return R.attr.emoji_category_recent;
|
||||
}
|
||||
@@ -100,13 +106,4 @@ public class RecentEmojiPageModel implements EmojiPageModel {
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private String[] toReversePrimitiveArray(@NonNull LinkedHashSet<String> emojiSet) {
|
||||
String[] emojis = new String[emojiSet.size()];
|
||||
int i = emojiSet.size() - 1;
|
||||
for (String emoji : emojiSet) {
|
||||
emojis[i--] = emoji;
|
||||
}
|
||||
return emojis;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,39 +2,39 @@ package org.thoughtcrime.securesms.components.emoji;
|
||||
|
||||
import android.net.Uri;
|
||||
|
||||
import androidx.annotation.AttrRes;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import org.thoughtcrime.securesms.emoji.EmojiCategory;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public class StaticEmojiPageModel implements EmojiPageModel {
|
||||
@AttrRes private final int iconAttr;
|
||||
@NonNull private final List<Emoji> emoji;
|
||||
@Nullable private final Uri sprite;
|
||||
private final @NonNull EmojiCategory category;
|
||||
private final @NonNull List<Emoji> emoji;
|
||||
private final @Nullable Uri sprite;
|
||||
|
||||
public StaticEmojiPageModel(@AttrRes int iconAttr, @NonNull String[] strings, @Nullable Uri sprite) {
|
||||
List<Emoji> emoji = new ArrayList<>(strings.length);
|
||||
for (String s : strings) {
|
||||
emoji.add(new Emoji(Collections.singletonList(s)));
|
||||
}
|
||||
public StaticEmojiPageModel(@NonNull EmojiCategory category, @NonNull String[] strings, @Nullable Uri sprite) {
|
||||
this(category, Arrays.stream(strings).map(s -> new Emoji(Collections.singletonList(s))).collect(Collectors.toList()), sprite);
|
||||
}
|
||||
|
||||
this.iconAttr = iconAttr;
|
||||
this.emoji = emoji;
|
||||
public StaticEmojiPageModel(@NonNull EmojiCategory category, @NonNull List<Emoji> emoji, @Nullable Uri sprite) {
|
||||
this.category = category;
|
||||
this.emoji = Collections.unmodifiableList(emoji);
|
||||
this.sprite = sprite;
|
||||
}
|
||||
|
||||
public StaticEmojiPageModel(@AttrRes int iconAttr, @NonNull List<Emoji> emoji, @Nullable Uri sprite) {
|
||||
this.iconAttr = iconAttr;
|
||||
this.emoji = Collections.unmodifiableList(emoji);
|
||||
this.sprite = sprite;
|
||||
@Override
|
||||
public String getKey() {
|
||||
return category.getKey();
|
||||
}
|
||||
|
||||
public int getIconAttr() {
|
||||
return iconAttr;
|
||||
return category.getIcon();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -8,7 +8,7 @@ import androidx.annotation.NonNull;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.crypto.DatabaseSessionLock;
|
||||
import org.thoughtcrime.securesms.crypto.ReentrantSessionLock;
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
import org.thoughtcrime.securesms.database.IdentityDatabase;
|
||||
import org.thoughtcrime.securesms.database.IdentityDatabase.IdentityRecord;
|
||||
@@ -43,7 +43,7 @@ public class UntrustedSendDialog extends AlertDialog.Builder implements DialogIn
|
||||
final IdentityDatabase identityDatabase = DatabaseFactory.getIdentityDatabase(getContext());
|
||||
|
||||
SimpleTask.run(() -> {
|
||||
try(SignalSessionLock.Lock unused = DatabaseSessionLock.INSTANCE.acquire()) {
|
||||
try(SignalSessionLock.Lock unused = ReentrantSessionLock.INSTANCE.acquire()) {
|
||||
for (IdentityRecord identityRecord : untrustedRecords) {
|
||||
identityDatabase.setApproval(identityRecord.getRecipientId(), true);
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ import androidx.annotation.NonNull;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.crypto.DatabaseSessionLock;
|
||||
import org.thoughtcrime.securesms.crypto.ReentrantSessionLock;
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
import org.thoughtcrime.securesms.database.IdentityDatabase;
|
||||
import org.thoughtcrime.securesms.database.IdentityDatabase.IdentityRecord;
|
||||
@@ -44,7 +44,7 @@ public class UnverifiedSendDialog extends AlertDialog.Builder implements DialogI
|
||||
new AsyncTask<Void, Void, Void>() {
|
||||
@Override
|
||||
protected Void doInBackground(Void... params) {
|
||||
try(SignalSessionLock.Lock unused = DatabaseSessionLock.INSTANCE.acquire()) {
|
||||
try(SignalSessionLock.Lock unused = ReentrantSessionLock.INSTANCE.acquire()) {
|
||||
for (IdentityRecord identityRecord : untrustedRecords) {
|
||||
identityDatabase.setVerified(identityRecord.getRecipientId(),
|
||||
identityRecord.getIdentityKey(),
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
package org.thoughtcrime.securesms.components.reminder;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
|
||||
/**
|
||||
* Shows a reminder to upgrade a group to GV2.
|
||||
*/
|
||||
public class GroupsV1MigrationInitiationReminder extends Reminder {
|
||||
|
||||
public GroupsV1MigrationInitiationReminder(@NonNull Context context) {
|
||||
super(null, context.getString(R.string.GroupsV1MigrationInitiationReminder_to_access_new_features_like_mentions));
|
||||
addAction(new Action(context.getString(R.string.GroupsV1MigrationInitiationReminder_upgrade_group), R.id.reminder_action_gv1_initiation_update_group));
|
||||
addAction(new Action(context.getResources().getString(R.string.GroupsV1MigrationInitiationReminder_not_now), R.id.reminder_action_gv1_initiation_not_now));
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isDismissable() {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -8,10 +8,11 @@ import androidx.navigation.fragment.NavHostFragment
|
||||
import org.thoughtcrime.securesms.PassphraseRequiredActivity
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.util.DynamicNoActionBarTheme
|
||||
import org.thoughtcrime.securesms.util.DynamicTheme
|
||||
|
||||
open class DSLSettingsActivity : PassphraseRequiredActivity() {
|
||||
|
||||
private val dynamicTheme = DynamicNoActionBarTheme()
|
||||
protected open val dynamicTheme: DynamicTheme = DynamicNoActionBarTheme()
|
||||
|
||||
protected lateinit var navController: NavController
|
||||
private set
|
||||
|
||||
@@ -16,6 +16,7 @@ import org.thoughtcrime.securesms.util.CommunicationActions
|
||||
import org.thoughtcrime.securesms.util.MappingAdapter
|
||||
import org.thoughtcrime.securesms.util.MappingViewHolder
|
||||
import org.thoughtcrime.securesms.util.ViewUtil
|
||||
import org.thoughtcrime.securesms.util.visible
|
||||
|
||||
class DSLSettingsAdapter : MappingAdapter() {
|
||||
init {
|
||||
@@ -42,13 +43,9 @@ abstract class PreferenceViewHolder<T : PreferenceModel<T>>(itemView: View) : Ma
|
||||
it.isEnabled = model.isEnabled
|
||||
}
|
||||
|
||||
if (model.iconId != -1) {
|
||||
iconView.setImageResource(model.iconId)
|
||||
iconView.visibility = View.VISIBLE
|
||||
} else {
|
||||
iconView.setImageDrawable(null)
|
||||
iconView.visibility = View.GONE
|
||||
}
|
||||
val icon = model.icon?.resolve(context)
|
||||
iconView.setImageDrawable(icon)
|
||||
iconView.visible = icon != null
|
||||
|
||||
val title = model.title?.resolve(context)
|
||||
if (title != null) {
|
||||
@@ -93,13 +90,31 @@ class RadioListPreferenceViewHolder(itemView: View) : PreferenceViewHolder<Radio
|
||||
summaryView.text = model.listItems[model.selected]
|
||||
|
||||
itemView.setOnClickListener {
|
||||
MaterialAlertDialogBuilder(context)
|
||||
.setTitle(model.title.resolve(context))
|
||||
var selection = -1
|
||||
val builder = MaterialAlertDialogBuilder(context)
|
||||
.setTitle(model.dialogTitle.resolve(context))
|
||||
.setSingleChoiceItems(model.listItems, model.selected) { dialog, which ->
|
||||
model.onSelected(which)
|
||||
dialog.dismiss()
|
||||
if (model.confirmAction) {
|
||||
selection = which
|
||||
} else {
|
||||
model.onSelected(which)
|
||||
dialog.dismiss()
|
||||
}
|
||||
}
|
||||
.show()
|
||||
|
||||
if (model.confirmAction) {
|
||||
builder
|
||||
.setPositiveButton(android.R.string.ok) { dialog, _ ->
|
||||
model.onSelected(selection)
|
||||
dialog.dismiss()
|
||||
}
|
||||
.setNegativeButton(android.R.string.cancel) { dialog, _ ->
|
||||
dialog.dismiss()
|
||||
}
|
||||
.show()
|
||||
} else {
|
||||
builder.show()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,19 +14,21 @@ import androidx.recyclerview.widget.RecyclerView
|
||||
import org.thoughtcrime.securesms.R
|
||||
|
||||
abstract class DSLSettingsFragment(
|
||||
@StringRes private val titleId: Int,
|
||||
@StringRes private val titleId: Int = -1,
|
||||
@MenuRes private val menuId: Int = -1,
|
||||
@LayoutRes layoutId: Int = R.layout.dsl_settings_fragment
|
||||
) : Fragment(layoutId) {
|
||||
|
||||
private lateinit var recyclerView: RecyclerView
|
||||
private lateinit var toolbarShadowHelper: ToolbarShadowHelper
|
||||
private lateinit var scrollAnimationHelper: OnScrollAnimationHelper
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
val toolbar: Toolbar = view.findViewById(R.id.toolbar)
|
||||
val toolbarShadow: View = view.findViewById(R.id.toolbar_shadow)
|
||||
|
||||
toolbar.setTitle(titleId)
|
||||
if (titleId != -1) {
|
||||
toolbar.setTitle(titleId)
|
||||
}
|
||||
|
||||
toolbar.setNavigationOnClickListener {
|
||||
requireActivity().onBackPressed()
|
||||
@@ -39,18 +41,17 @@ abstract class DSLSettingsFragment(
|
||||
|
||||
recyclerView = view.findViewById(R.id.recycler)
|
||||
recyclerView.edgeEffectFactory = EdgeEffectFactory()
|
||||
toolbarShadowHelper = ToolbarShadowHelper(toolbarShadow)
|
||||
scrollAnimationHelper = getOnScrollAnimationHelper(toolbarShadow)
|
||||
val adapter = DSLSettingsAdapter()
|
||||
|
||||
recyclerView.adapter = adapter
|
||||
recyclerView.addOnScrollListener(toolbarShadowHelper)
|
||||
recyclerView.addOnScrollListener(scrollAnimationHelper)
|
||||
|
||||
bindAdapter(adapter)
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
toolbarShadowHelper.onScrolled(recyclerView, 0, 0)
|
||||
protected open fun getOnScrollAnimationHelper(toolbarShadow: View): OnScrollAnimationHelper {
|
||||
return ToolbarShadowAnimationHelper(toolbarShadow)
|
||||
}
|
||||
|
||||
abstract fun bindAdapter(adapter: DSLSettingsAdapter)
|
||||
@@ -66,31 +67,71 @@ abstract class DSLSettingsFragment(
|
||||
}
|
||||
}
|
||||
|
||||
class ToolbarShadowHelper(private val toolbarShadow: View) : RecyclerView.OnScrollListener() {
|
||||
abstract class OnScrollAnimationHelper : RecyclerView.OnScrollListener() {
|
||||
private var lastAnimationState = AnimationState.NONE
|
||||
|
||||
private var lastAnimationState = ToolbarAnimationState.NONE
|
||||
protected open val duration: Long = 250L
|
||||
|
||||
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
|
||||
val newAnimationState =
|
||||
if (recyclerView.canScrollVertically(-1)) ToolbarAnimationState.SHOW else ToolbarAnimationState.HIDE
|
||||
val newAnimationState = getAnimationState(recyclerView)
|
||||
|
||||
if (newAnimationState == lastAnimationState) {
|
||||
return
|
||||
}
|
||||
|
||||
if (lastAnimationState == AnimationState.NONE) {
|
||||
setImmediateState(recyclerView)
|
||||
return
|
||||
}
|
||||
|
||||
when (newAnimationState) {
|
||||
ToolbarAnimationState.NONE -> throw AssertionError()
|
||||
ToolbarAnimationState.HIDE -> toolbarShadow.animate().alpha(0f)
|
||||
ToolbarAnimationState.SHOW -> toolbarShadow.animate().alpha(1f)
|
||||
AnimationState.NONE -> throw AssertionError()
|
||||
AnimationState.HIDE -> hide(duration)
|
||||
AnimationState.SHOW -> show(duration)
|
||||
}
|
||||
|
||||
lastAnimationState = newAnimationState
|
||||
}
|
||||
|
||||
fun setImmediateState(recyclerView: RecyclerView) {
|
||||
val newAnimationState = getAnimationState(recyclerView)
|
||||
|
||||
when (newAnimationState) {
|
||||
AnimationState.NONE -> throw AssertionError()
|
||||
AnimationState.HIDE -> hide(0L)
|
||||
AnimationState.SHOW -> show(0L)
|
||||
}
|
||||
|
||||
lastAnimationState = newAnimationState
|
||||
}
|
||||
|
||||
protected open fun getAnimationState(recyclerView: RecyclerView): AnimationState {
|
||||
return if (recyclerView.canScrollVertically(-1)) AnimationState.SHOW else AnimationState.HIDE
|
||||
}
|
||||
|
||||
protected abstract fun show(duration: Long)
|
||||
|
||||
protected abstract fun hide(duration: Long)
|
||||
|
||||
enum class AnimationState {
|
||||
NONE,
|
||||
HIDE,
|
||||
SHOW
|
||||
}
|
||||
}
|
||||
|
||||
private enum class ToolbarAnimationState {
|
||||
NONE,
|
||||
HIDE,
|
||||
SHOW
|
||||
open class ToolbarShadowAnimationHelper(private val toolbarShadow: View) : OnScrollAnimationHelper() {
|
||||
|
||||
override fun show(duration: Long) {
|
||||
toolbarShadow.animate()
|
||||
.setDuration(duration)
|
||||
.alpha(1f)
|
||||
}
|
||||
|
||||
override fun hide(duration: Long) {
|
||||
toolbarShadow.animate()
|
||||
.setDuration(duration)
|
||||
.alpha(0f)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
package org.thoughtcrime.securesms.components.settings
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.PorterDuff
|
||||
import android.graphics.PorterDuffColorFilter
|
||||
import android.graphics.drawable.Drawable
|
||||
import androidx.annotation.ColorRes
|
||||
import androidx.annotation.DrawableRes
|
||||
import androidx.core.content.ContextCompat
|
||||
import org.thoughtcrime.securesms.R
|
||||
|
||||
const val NO_TINT = -1
|
||||
|
||||
sealed class DSLSettingsIcon {
|
||||
|
||||
private data class FromResource(
|
||||
@DrawableRes private val iconId: Int,
|
||||
@ColorRes private val iconTintId: Int
|
||||
) : DSLSettingsIcon() {
|
||||
override fun resolve(context: Context) = requireNotNull(ContextCompat.getDrawable(context, iconId)).apply {
|
||||
if (iconTintId != NO_TINT) {
|
||||
colorFilter = PorterDuffColorFilter(ContextCompat.getColor(context, iconTintId), PorterDuff.Mode.SRC_IN)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private data class FromDrawable(
|
||||
private val drawable: Drawable
|
||||
) : DSLSettingsIcon() {
|
||||
override fun resolve(context: Context): Drawable = drawable
|
||||
}
|
||||
|
||||
abstract fun resolve(context: Context): Drawable
|
||||
|
||||
companion object {
|
||||
@JvmStatic
|
||||
fun from(@DrawableRes iconId: Int, @ColorRes iconTintId: Int = R.color.signal_icon_tint_primary): DSLSettingsIcon = FromResource(iconId, iconTintId)
|
||||
|
||||
@JvmStatic
|
||||
fun from(drawable: Drawable): DSLSettingsIcon = FromDrawable(drawable)
|
||||
}
|
||||
}
|
||||
@@ -9,6 +9,7 @@ import org.thoughtcrime.securesms.components.AvatarImageView
|
||||
import org.thoughtcrime.securesms.components.settings.DSLConfiguration
|
||||
import org.thoughtcrime.securesms.components.settings.DSLSettingsAdapter
|
||||
import org.thoughtcrime.securesms.components.settings.DSLSettingsFragment
|
||||
import org.thoughtcrime.securesms.components.settings.DSLSettingsIcon
|
||||
import org.thoughtcrime.securesms.components.settings.DSLSettingsText
|
||||
import org.thoughtcrime.securesms.components.settings.PreferenceModel
|
||||
import org.thoughtcrime.securesms.components.settings.PreferenceViewHolder
|
||||
@@ -44,7 +45,7 @@ class AppSettingsFragment : DSLSettingsFragment(R.string.text_secure_normal__men
|
||||
|
||||
clickPref(
|
||||
title = DSLSettingsText.from(R.string.AccountSettingsFragment__account),
|
||||
iconId = R.drawable.ic_profile_circle_24,
|
||||
icon = DSLSettingsIcon.from(R.drawable.ic_profile_circle_24),
|
||||
onClick = {
|
||||
Navigation.findNavController(requireView()).navigate(R.id.action_appSettingsFragment_to_accountSettingsFragment)
|
||||
}
|
||||
@@ -52,7 +53,7 @@ class AppSettingsFragment : DSLSettingsFragment(R.string.text_secure_normal__men
|
||||
|
||||
clickPref(
|
||||
title = DSLSettingsText.from(R.string.preferences__linked_devices),
|
||||
iconId = R.drawable.ic_linked_devices_24,
|
||||
icon = DSLSettingsIcon.from(R.drawable.ic_linked_devices_24),
|
||||
onClick = {
|
||||
Navigation.findNavController(requireView()).navigate(R.id.action_appSettingsFragment_to_deviceActivity)
|
||||
}
|
||||
@@ -72,7 +73,7 @@ class AppSettingsFragment : DSLSettingsFragment(R.string.text_secure_normal__men
|
||||
|
||||
clickPref(
|
||||
title = DSLSettingsText.from(R.string.preferences__appearance),
|
||||
iconId = R.drawable.ic_appearance_24,
|
||||
icon = DSLSettingsIcon.from(R.drawable.ic_appearance_24),
|
||||
onClick = {
|
||||
Navigation.findNavController(requireView()).navigate(R.id.action_appSettingsFragment_to_appearanceSettingsFragment)
|
||||
}
|
||||
@@ -80,7 +81,7 @@ class AppSettingsFragment : DSLSettingsFragment(R.string.text_secure_normal__men
|
||||
|
||||
clickPref(
|
||||
title = DSLSettingsText.from(R.string.preferences_chats__chats),
|
||||
iconId = R.drawable.ic_message_tinted_bitmap_24,
|
||||
icon = DSLSettingsIcon.from(R.drawable.ic_message_tinted_bitmap_24),
|
||||
onClick = {
|
||||
Navigation.findNavController(requireView()).navigate(R.id.action_appSettingsFragment_to_chatsSettingsFragment)
|
||||
}
|
||||
@@ -88,7 +89,7 @@ class AppSettingsFragment : DSLSettingsFragment(R.string.text_secure_normal__men
|
||||
|
||||
clickPref(
|
||||
title = DSLSettingsText.from(R.string.preferences__notifications),
|
||||
iconId = R.drawable.ic_bell_24,
|
||||
icon = DSLSettingsIcon.from(R.drawable.ic_bell_24),
|
||||
onClick = {
|
||||
Navigation.findNavController(requireView()).navigate(R.id.action_appSettingsFragment_to_notificationsSettingsFragment)
|
||||
}
|
||||
@@ -96,7 +97,7 @@ class AppSettingsFragment : DSLSettingsFragment(R.string.text_secure_normal__men
|
||||
|
||||
clickPref(
|
||||
title = DSLSettingsText.from(R.string.preferences__privacy),
|
||||
iconId = R.drawable.ic_lock_24,
|
||||
icon = DSLSettingsIcon.from(R.drawable.ic_lock_24),
|
||||
onClick = {
|
||||
Navigation.findNavController(requireView()).navigate(R.id.action_appSettingsFragment_to_privacySettingsFragment)
|
||||
}
|
||||
@@ -104,7 +105,7 @@ class AppSettingsFragment : DSLSettingsFragment(R.string.text_secure_normal__men
|
||||
|
||||
clickPref(
|
||||
title = DSLSettingsText.from(R.string.preferences__data_and_storage),
|
||||
iconId = R.drawable.ic_archive_24dp,
|
||||
icon = DSLSettingsIcon.from(R.drawable.ic_archive_24dp),
|
||||
onClick = {
|
||||
Navigation.findNavController(requireView()).navigate(R.id.action_appSettingsFragment_to_dataAndStorageSettingsFragment)
|
||||
}
|
||||
@@ -114,7 +115,7 @@ class AppSettingsFragment : DSLSettingsFragment(R.string.text_secure_normal__men
|
||||
|
||||
clickPref(
|
||||
title = DSLSettingsText.from(R.string.preferences__help),
|
||||
iconId = R.drawable.ic_help_24,
|
||||
icon = DSLSettingsIcon.from(R.drawable.ic_help_24),
|
||||
onClick = {
|
||||
Navigation.findNavController(requireView()).navigate(R.id.action_appSettingsFragment_to_helpSettingsFragment)
|
||||
}
|
||||
@@ -122,7 +123,7 @@ class AppSettingsFragment : DSLSettingsFragment(R.string.text_secure_normal__men
|
||||
|
||||
clickPref(
|
||||
title = DSLSettingsText.from(R.string.AppSettingsFragment__invite_your_friends),
|
||||
iconId = R.drawable.ic_invite_24,
|
||||
icon = DSLSettingsIcon.from(R.drawable.ic_invite_24),
|
||||
onClick = {
|
||||
Navigation.findNavController(requireView()).navigate(R.id.action_appSettingsFragment_to_inviteActivity)
|
||||
}
|
||||
@@ -130,7 +131,7 @@ class AppSettingsFragment : DSLSettingsFragment(R.string.text_secure_normal__men
|
||||
|
||||
externalLinkPref(
|
||||
title = DSLSettingsText.from(R.string.preferences__donate_to_signal),
|
||||
iconId = R.drawable.ic_heart_24,
|
||||
icon = DSLSettingsIcon.from(R.drawable.ic_heart_24),
|
||||
linkId = R.string.donate_url
|
||||
)
|
||||
|
||||
|
||||
@@ -8,12 +8,14 @@ import android.widget.Toast
|
||||
import androidx.lifecycle.ViewModelProviders
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import org.signal.core.util.concurrent.SignalExecutors
|
||||
import org.thoughtcrime.securesms.BuildConfig
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.components.settings.DSLConfiguration
|
||||
import org.thoughtcrime.securesms.components.settings.DSLSettingsAdapter
|
||||
import org.thoughtcrime.securesms.components.settings.DSLSettingsFragment
|
||||
import org.thoughtcrime.securesms.components.settings.DSLSettingsText
|
||||
import org.thoughtcrime.securesms.components.settings.configure
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
|
||||
import org.thoughtcrime.securesms.jobs.RefreshAttributesJob
|
||||
import org.thoughtcrime.securesms.jobs.RefreshOwnProfileJob
|
||||
@@ -212,6 +214,60 @@ class InternalSettingsFragment : DSLSettingsFragment(R.string.preferences__inter
|
||||
viewModel.setDisableAutoMigrationNotification(!state.useBuiltInEmojiSet)
|
||||
}
|
||||
)
|
||||
|
||||
dividerPref()
|
||||
|
||||
sectionHeaderPref(R.string.preferences__internal_sender_key)
|
||||
|
||||
clickPref(
|
||||
title = DSLSettingsText.from(R.string.preferences__internal_clear_all_state),
|
||||
summary = DSLSettingsText.from(R.string.preferences__internal_click_to_delete_all_sender_key_state),
|
||||
onClick = {
|
||||
clearAllSenderKeyState()
|
||||
}
|
||||
)
|
||||
|
||||
clickPref(
|
||||
title = DSLSettingsText.from(R.string.preferences__internal_clear_shared_state),
|
||||
summary = DSLSettingsText.from(R.string.preferences__internal_click_to_delete_all_sharing_state),
|
||||
onClick = {
|
||||
clearAllSenderKeySharedState()
|
||||
}
|
||||
)
|
||||
|
||||
switchPref(
|
||||
title = DSLSettingsText.from(R.string.preferences__internal_remove_two_person_minimum),
|
||||
summary = DSLSettingsText.from(R.string.preferences__internal_remove_the_requirement_that_you_need),
|
||||
isChecked = state.removeSenderKeyMinimium,
|
||||
onClick = {
|
||||
viewModel.setRemoveSenderKeyMinimum(!state.removeSenderKeyMinimium)
|
||||
}
|
||||
)
|
||||
|
||||
dividerPref()
|
||||
|
||||
sectionHeaderPref(R.string.preferences__internal_calling)
|
||||
|
||||
radioPref(
|
||||
title = DSLSettingsText.from(R.string.preferences__internal_calling_default),
|
||||
summary = DSLSettingsText.from(BuildConfig.SIGNAL_SFU_URL),
|
||||
isChecked = state.callingServer == BuildConfig.SIGNAL_SFU_URL,
|
||||
onClick = {
|
||||
viewModel.setInternalGroupCallingServer(null)
|
||||
}
|
||||
)
|
||||
|
||||
BuildConfig.SIGNAL_SFU_INTERNAL_NAMES.zip(BuildConfig.SIGNAL_SFU_INTERNAL_URLS)
|
||||
.forEach { (name, server) ->
|
||||
radioPref(
|
||||
title = DSLSettingsText.from(requireContext().getString(R.string.preferences__internal_calling_s_server, name)),
|
||||
summary = DSLSettingsText.from(server),
|
||||
isChecked = state.callingServer == server,
|
||||
onClick = {
|
||||
viewModel.setInternalGroupCallingServer(server)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -278,4 +334,15 @@ class InternalSettingsFragment : DSLSettingsFragment(R.string.preferences__inter
|
||||
ConversationUtil.clearAllShortcuts(requireContext())
|
||||
Toast.makeText(context, "Deleted all dynamic shortcuts.", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
|
||||
private fun clearAllSenderKeyState() {
|
||||
DatabaseFactory.getSenderKeyDatabase(requireContext()).deleteAll()
|
||||
DatabaseFactory.getSenderKeySharedDatabase(requireContext()).deleteAll()
|
||||
Toast.makeText(context, "Deleted all sender key state.", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
|
||||
private fun clearAllSenderKeySharedState() {
|
||||
DatabaseFactory.getSenderKeySharedDatabase(requireContext()).deleteAll()
|
||||
Toast.makeText(context, "Deleted all sender key shared state.", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,6 +11,8 @@ data class InternalSettingsState(
|
||||
val disableAutoMigrationInitiation: Boolean,
|
||||
val disableAutoMigrationNotification: Boolean,
|
||||
val forceCensorship: Boolean,
|
||||
val callingServer: String,
|
||||
val useBuiltInEmojiSet: Boolean,
|
||||
val emojiVersion: EmojiFiles.Version?
|
||||
val emojiVersion: EmojiFiles.Version?,
|
||||
val removeSenderKeyMinimium: Boolean,
|
||||
)
|
||||
|
||||
@@ -65,6 +65,16 @@ class InternalSettingsViewModel(private val repository: InternalSettingsReposito
|
||||
refresh()
|
||||
}
|
||||
|
||||
fun setRemoveSenderKeyMinimum(enabled: Boolean) {
|
||||
preferenceDataStore.putBoolean(InternalValues.REMOVE_SENDER_KEY_MINIMUM, enabled)
|
||||
refresh()
|
||||
}
|
||||
|
||||
fun setInternalGroupCallingServer(server: String?) {
|
||||
preferenceDataStore.putString(InternalValues.CALLING_SERVER, server)
|
||||
refresh()
|
||||
}
|
||||
|
||||
private fun refresh() {
|
||||
store.update { getState().copy(emojiVersion = it.emojiVersion) }
|
||||
}
|
||||
@@ -78,8 +88,10 @@ class InternalSettingsViewModel(private val repository: InternalSettingsReposito
|
||||
disableAutoMigrationInitiation = SignalStore.internalValues().disableGv1AutoMigrateInitiation(),
|
||||
disableAutoMigrationNotification = SignalStore.internalValues().disableGv1AutoMigrateNotification(),
|
||||
forceCensorship = SignalStore.internalValues().forcedCensorship(),
|
||||
callingServer = SignalStore.internalValues().groupCallingServer(),
|
||||
useBuiltInEmojiSet = SignalStore.internalValues().forceBuiltInEmoji(),
|
||||
emojiVersion = null
|
||||
emojiVersion = null,
|
||||
removeSenderKeyMinimium = SignalStore.internalValues().removeSenderKeyMinimum()
|
||||
)
|
||||
|
||||
class Factory(private val repository: InternalSettingsRepository) : ViewModelProvider.Factory {
|
||||
|
||||
@@ -234,7 +234,7 @@ class NotificationsSettingsFragment : DSLSettingsFragment(R.string.preferences__
|
||||
} else {
|
||||
val tone = RingtoneUtil.getRingtone(requireContext(), uri)
|
||||
if (tone != null) {
|
||||
tone.getTitle(requireContext())
|
||||
tone.getTitle(requireContext()) ?: getString(R.string.NotificationsSettingsFragment__unknown_ringtone)
|
||||
} else {
|
||||
getString(R.string.preferences__default)
|
||||
}
|
||||
@@ -289,7 +289,7 @@ class NotificationsSettingsFragment : DSLSettingsFragment(R.string.preferences__
|
||||
val radioListPreference: RadioListPreference
|
||||
) : PreferenceModel<LedColorPreference>(
|
||||
title = radioListPreference.title,
|
||||
iconId = radioListPreference.iconId,
|
||||
icon = radioListPreference.icon,
|
||||
summary = radioListPreference.summary
|
||||
) {
|
||||
override fun areContentsTheSame(newItem: LedColorPreference): Boolean {
|
||||
|
||||
@@ -138,24 +138,22 @@ class PrivacySettingsFragment : DSLSettingsFragment(R.string.preferences__privac
|
||||
}
|
||||
)
|
||||
|
||||
if (FeatureFlags.defaultMessageTimer()) {
|
||||
dividerPref()
|
||||
dividerPref()
|
||||
|
||||
sectionHeaderPref(R.string.PrivacySettingsFragment__disappearing_messages)
|
||||
sectionHeaderPref(R.string.PrivacySettingsFragment__disappearing_messages)
|
||||
|
||||
customPref(
|
||||
ValueClickPreference(
|
||||
value = DSLSettingsText.from(ExpirationUtil.getExpirationAbbreviatedDisplayValue(requireContext(), state.universalExpireTimer)),
|
||||
clickPreference = ClickPreference(
|
||||
title = DSLSettingsText.from(R.string.PrivacySettingsFragment__default_timer_for_new_changes),
|
||||
summary = DSLSettingsText.from(R.string.PrivacySettingsFragment__set_a_default_disappearing_message_timer_for_all_new_chats_started_by_you),
|
||||
onClick = {
|
||||
NavHostFragment.findNavController(this@PrivacySettingsFragment).navigate(R.id.action_privacySettingsFragment_to_disappearingMessagesTimerSelectFragment)
|
||||
}
|
||||
)
|
||||
customPref(
|
||||
ValueClickPreference(
|
||||
value = DSLSettingsText.from(ExpirationUtil.getExpirationAbbreviatedDisplayValue(requireContext(), state.universalExpireTimer)),
|
||||
clickPreference = ClickPreference(
|
||||
title = DSLSettingsText.from(R.string.PrivacySettingsFragment__default_timer_for_new_changes),
|
||||
summary = DSLSettingsText.from(R.string.PrivacySettingsFragment__set_a_default_disappearing_message_timer_for_all_new_chats_started_by_you),
|
||||
onClick = {
|
||||
NavHostFragment.findNavController(this@PrivacySettingsFragment).navigate(R.id.action_privacySettingsFragment_to_disappearingMessagesTimerSelectFragment)
|
||||
}
|
||||
)
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
dividerPref()
|
||||
|
||||
@@ -432,7 +430,7 @@ class PrivacySettingsFragment : DSLSettingsFragment(R.string.preferences__privac
|
||||
) : PreferenceModel<ValueClickPreference>(
|
||||
title = clickPreference.title,
|
||||
summary = clickPreference.summary,
|
||||
iconId = clickPreference.iconId,
|
||||
icon = clickPreference.icon,
|
||||
isEnabled = clickPreference.isEnabled
|
||||
) {
|
||||
override fun areContentsTheSame(newItem: ValueClickPreference): Boolean {
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
package org.thoughtcrime.securesms.components.settings.app.privacy.advanced
|
||||
|
||||
import android.content.Context
|
||||
import com.google.firebase.iid.FirebaseInstanceId
|
||||
import com.google.android.gms.tasks.Tasks
|
||||
import com.google.firebase.installations.FirebaseInstallations
|
||||
import org.signal.core.util.concurrent.SignalExecutors
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory
|
||||
@@ -14,6 +15,7 @@ import org.thoughtcrime.securesms.util.TextSecurePreferences
|
||||
import org.whispersystems.libsignal.util.guava.Optional
|
||||
import org.whispersystems.signalservice.api.push.exceptions.AuthorizationFailedException
|
||||
import java.io.IOException
|
||||
import java.util.concurrent.ExecutionException
|
||||
|
||||
private val TAG = Log.tag(AdvancedPrivacySettingsRepository::class.java)
|
||||
|
||||
@@ -29,12 +31,18 @@ class AdvancedPrivacySettingsRepository(private val context: Context) {
|
||||
Log.w(TAG, e)
|
||||
}
|
||||
if (!TextSecurePreferences.isFcmDisabled(context)) {
|
||||
FirebaseInstanceId.getInstance().deleteInstanceId()
|
||||
Tasks.await(FirebaseInstallations.getInstance().delete())
|
||||
}
|
||||
DisablePushMessagesResult.SUCCESS
|
||||
} catch (ioe: IOException) {
|
||||
Log.w(TAG, ioe)
|
||||
DisablePushMessagesResult.NETWORK_ERROR
|
||||
} catch (e: InterruptedException) {
|
||||
Log.w(TAG, "Interrupted while deleting", e)
|
||||
DisablePushMessagesResult.NETWORK_ERROR
|
||||
} catch (e: ExecutionException) {
|
||||
Log.w(TAG, "Error deleting", e.cause)
|
||||
DisablePushMessagesResult.NETWORK_ERROR
|
||||
}
|
||||
|
||||
consumer(result)
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
package org.thoughtcrime.securesms.components.settings.conversation
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import androidx.core.app.ActivityCompat
|
||||
import androidx.core.app.ActivityOptionsCompat
|
||||
import androidx.core.util.Pair
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.components.settings.DSLSettingsActivity
|
||||
import org.thoughtcrime.securesms.groups.GroupId
|
||||
import org.thoughtcrime.securesms.groups.ParcelableGroupId
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
import org.thoughtcrime.securesms.util.DynamicConversationSettingsTheme
|
||||
import org.thoughtcrime.securesms.util.DynamicTheme
|
||||
|
||||
class ConversationSettingsActivity : DSLSettingsActivity(), ConversationSettingsFragment.Callback {
|
||||
|
||||
override val dynamicTheme: DynamicTheme = DynamicConversationSettingsTheme()
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?, ready: Boolean) {
|
||||
ActivityCompat.postponeEnterTransition(this)
|
||||
super.onCreate(savedInstanceState, ready)
|
||||
}
|
||||
|
||||
override fun onContentWillRender() {
|
||||
ActivityCompat.startPostponedEnterTransition(this)
|
||||
}
|
||||
|
||||
override fun finish() {
|
||||
super.finish()
|
||||
overridePendingTransition(0, R.anim.slide_fade_to_bottom)
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
@JvmStatic
|
||||
fun createTransitionBundle(context: Context, avatar: View, windowContent: View): Bundle? {
|
||||
return if (context is Activity) {
|
||||
ActivityOptionsCompat.makeSceneTransitionAnimation(
|
||||
context,
|
||||
Pair.create(avatar, "avatar"),
|
||||
Pair.create(windowContent, "window_content")
|
||||
).toBundle()
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun createTransitionBundle(context: Context, avatar: View): Bundle? {
|
||||
return if (context is Activity) {
|
||||
ActivityOptionsCompat.makeSceneTransitionAnimation(
|
||||
context,
|
||||
avatar,
|
||||
"avatar",
|
||||
).toBundle()
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun forGroup(context: Context, groupId: GroupId): Intent {
|
||||
val startBundle = ConversationSettingsFragmentArgs.Builder(null, ParcelableGroupId.from(groupId))
|
||||
.build()
|
||||
.toBundle()
|
||||
|
||||
return getIntent(context)
|
||||
.putExtra(ARG_START_BUNDLE, startBundle)
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun forRecipient(context: Context, recipientId: RecipientId): Intent {
|
||||
val startBundle = ConversationSettingsFragmentArgs.Builder(recipientId, null)
|
||||
.build()
|
||||
.toBundle()
|
||||
|
||||
return getIntent(context)
|
||||
.putExtra(ARG_START_BUNDLE, startBundle)
|
||||
}
|
||||
|
||||
private fun getIntent(context: Context): Intent {
|
||||
return Intent(context, ConversationSettingsActivity::class.java)
|
||||
.putExtra(ARG_NAV_GRAPH, R.navigation.conversation_settings)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
package org.thoughtcrime.securesms.components.settings.conversation
|
||||
|
||||
import org.thoughtcrime.securesms.groups.GroupId
|
||||
import org.thoughtcrime.securesms.groups.ui.GroupChangeFailureReason
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
|
||||
sealed class ConversationSettingsEvent {
|
||||
class AddToAGroup(
|
||||
val recipientId: RecipientId,
|
||||
val groupMembership: List<RecipientId>
|
||||
) : ConversationSettingsEvent()
|
||||
|
||||
class AddMembersToGroup(
|
||||
val groupId: GroupId,
|
||||
val selectionWarning: Int,
|
||||
val selectionLimit: Int,
|
||||
val groupMembersWithoutSelf: List<RecipientId>
|
||||
) : ConversationSettingsEvent()
|
||||
|
||||
object ShowGroupHardLimitDialog : ConversationSettingsEvent()
|
||||
|
||||
class ShowAddMembersToGroupError(
|
||||
val failureReason: GroupChangeFailureReason
|
||||
) : ConversationSettingsEvent()
|
||||
|
||||
class ShowGroupInvitesSentDialog(
|
||||
val invitesSentTo: List<Recipient>
|
||||
) : ConversationSettingsEvent()
|
||||
|
||||
class ShowMembersAdded(
|
||||
val membersAddedCount: Int
|
||||
) : ConversationSettingsEvent()
|
||||
|
||||
class InitiateGroupMigration(
|
||||
val recipientId: RecipientId
|
||||
) : ConversationSettingsEvent()
|
||||
}
|
||||
@@ -0,0 +1,765 @@
|
||||
package org.thoughtcrime.securesms.components.settings.conversation
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.graphics.Color
|
||||
import android.graphics.PorterDuff
|
||||
import android.graphics.PorterDuffColorFilter
|
||||
import android.graphics.Rect
|
||||
import android.os.Bundle
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.TextView
|
||||
import android.widget.Toast
|
||||
import androidx.appcompat.widget.Toolbar
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.view.doOnPreDraw
|
||||
import androidx.fragment.app.viewModels
|
||||
import androidx.navigation.Navigation
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import app.cash.exhaustive.Exhaustive
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import org.thoughtcrime.securesms.AvatarPreviewActivity
|
||||
import org.thoughtcrime.securesms.BlockUnblockDialog
|
||||
import org.thoughtcrime.securesms.InviteActivity
|
||||
import org.thoughtcrime.securesms.MediaPreviewActivity
|
||||
import org.thoughtcrime.securesms.MuteDialog
|
||||
import org.thoughtcrime.securesms.PushContactSelectionActivity
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.VerifyIdentityActivity
|
||||
import org.thoughtcrime.securesms.components.AvatarImageView
|
||||
import org.thoughtcrime.securesms.components.settings.DSLConfiguration
|
||||
import org.thoughtcrime.securesms.components.settings.DSLSettingsAdapter
|
||||
import org.thoughtcrime.securesms.components.settings.DSLSettingsFragment
|
||||
import org.thoughtcrime.securesms.components.settings.DSLSettingsIcon
|
||||
import org.thoughtcrime.securesms.components.settings.DSLSettingsText
|
||||
import org.thoughtcrime.securesms.components.settings.NO_TINT
|
||||
import org.thoughtcrime.securesms.components.settings.configure
|
||||
import org.thoughtcrime.securesms.components.settings.conversation.preferences.AvatarPreference
|
||||
import org.thoughtcrime.securesms.components.settings.conversation.preferences.BioTextPreference
|
||||
import org.thoughtcrime.securesms.components.settings.conversation.preferences.ButtonStripPreference
|
||||
import org.thoughtcrime.securesms.components.settings.conversation.preferences.GroupDescriptionPreference
|
||||
import org.thoughtcrime.securesms.components.settings.conversation.preferences.InternalPreference
|
||||
import org.thoughtcrime.securesms.components.settings.conversation.preferences.LargeIconClickPreference
|
||||
import org.thoughtcrime.securesms.components.settings.conversation.preferences.LegacyGroupPreference
|
||||
import org.thoughtcrime.securesms.components.settings.conversation.preferences.RecipientPreference
|
||||
import org.thoughtcrime.securesms.components.settings.conversation.preferences.SharedMediaPreference
|
||||
import org.thoughtcrime.securesms.components.settings.conversation.preferences.Utils.formatMutedUntil
|
||||
import org.thoughtcrime.securesms.contacts.ContactsCursorLoader
|
||||
import org.thoughtcrime.securesms.conversation.ConversationIntents
|
||||
import org.thoughtcrime.securesms.groups.ParcelableGroupId
|
||||
import org.thoughtcrime.securesms.groups.ui.GroupErrors
|
||||
import org.thoughtcrime.securesms.groups.ui.GroupLimitDialog
|
||||
import org.thoughtcrime.securesms.groups.ui.LeaveGroupDialog
|
||||
import org.thoughtcrime.securesms.groups.ui.addmembers.AddMembersActivity
|
||||
import org.thoughtcrime.securesms.groups.ui.addtogroup.AddToGroupsActivity
|
||||
import org.thoughtcrime.securesms.groups.ui.invitesandrequests.ManagePendingAndRequestingMembersActivity
|
||||
import org.thoughtcrime.securesms.groups.ui.managegroup.dialogs.GroupDescriptionDialog
|
||||
import org.thoughtcrime.securesms.groups.ui.managegroup.dialogs.GroupInviteSentDialog
|
||||
import org.thoughtcrime.securesms.groups.ui.managegroup.dialogs.GroupsLearnMoreBottomSheetDialogFragment
|
||||
import org.thoughtcrime.securesms.groups.ui.migration.GroupsV1MigrationInitiationBottomSheetDialogFragment
|
||||
import org.thoughtcrime.securesms.mediaoverview.MediaOverviewActivity
|
||||
import org.thoughtcrime.securesms.profiles.edit.EditProfileActivity
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.recipients.RecipientExporter
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
import org.thoughtcrime.securesms.recipients.ui.bottomsheet.RecipientBottomSheetDialogFragment
|
||||
import org.thoughtcrime.securesms.recipients.ui.sharablegrouplink.ShareableGroupLinkDialogFragment
|
||||
import org.thoughtcrime.securesms.util.CommunicationActions
|
||||
import org.thoughtcrime.securesms.util.ContextUtil
|
||||
import org.thoughtcrime.securesms.util.ExpirationUtil
|
||||
import org.thoughtcrime.securesms.util.ThemeUtil
|
||||
import org.thoughtcrime.securesms.util.ViewUtil
|
||||
import org.thoughtcrime.securesms.util.views.SimpleProgressDialog
|
||||
import org.thoughtcrime.securesms.wallpaper.ChatWallpaperActivity
|
||||
|
||||
private const val REQUEST_CODE_VIEW_CONTACT = 1
|
||||
private const val REQUEST_CODE_ADD_CONTACT = 2
|
||||
private const val REQUEST_CODE_ADD_MEMBERS_TO_GROUP = 3
|
||||
private const val REQUEST_CODE_RETURN_FROM_MEDIA = 4
|
||||
|
||||
class ConversationSettingsFragment : DSLSettingsFragment(
|
||||
layoutId = R.layout.conversation_settings_fragment,
|
||||
menuId = R.menu.conversation_settings
|
||||
) {
|
||||
|
||||
private val alertTint by lazy { ContextCompat.getColor(requireContext(), R.color.signal_alert_primary) }
|
||||
private val blockIcon by lazy {
|
||||
ContextUtil.requireDrawable(requireContext(), R.drawable.ic_block_tinted_24).apply {
|
||||
colorFilter = PorterDuffColorFilter(alertTint, PorterDuff.Mode.SRC_IN)
|
||||
}
|
||||
}
|
||||
|
||||
private val unblockIcon by lazy {
|
||||
ContextUtil.requireDrawable(requireContext(), R.drawable.ic_block_tinted_24)
|
||||
}
|
||||
|
||||
private val leaveIcon by lazy {
|
||||
ContextUtil.requireDrawable(requireContext(), R.drawable.ic_leave_tinted_24).apply {
|
||||
colorFilter = PorterDuffColorFilter(alertTint, PorterDuff.Mode.SRC_IN)
|
||||
}
|
||||
}
|
||||
|
||||
private val viewModel by viewModels<ConversationSettingsViewModel>(
|
||||
factoryProducer = {
|
||||
val args = ConversationSettingsFragmentArgs.fromBundle(requireArguments())
|
||||
val groupId = args.groupId as? ParcelableGroupId
|
||||
|
||||
ConversationSettingsViewModel.Factory(
|
||||
recipientId = args.recipientId,
|
||||
groupId = ParcelableGroupId.get(groupId),
|
||||
repository = ConversationSettingsRepository(requireContext())
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
private lateinit var callback: Callback
|
||||
|
||||
private lateinit var toolbar: Toolbar
|
||||
private lateinit var toolbarAvatar: AvatarImageView
|
||||
private lateinit var toolbarTitle: TextView
|
||||
private lateinit var toolbarBackground: View
|
||||
|
||||
private val navController get() = Navigation.findNavController(requireView())
|
||||
|
||||
override fun onAttach(context: Context) {
|
||||
super.onAttach(context)
|
||||
|
||||
callback = context as Callback
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
toolbar = view.findViewById(R.id.toolbar)
|
||||
toolbarAvatar = view.findViewById(R.id.toolbar_avatar)
|
||||
toolbarTitle = view.findViewById(R.id.toolbar_title)
|
||||
toolbarBackground = view.findViewById(R.id.toolbar_background)
|
||||
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
}
|
||||
|
||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||
when (requestCode) {
|
||||
REQUEST_CODE_ADD_MEMBERS_TO_GROUP -> if (data != null) {
|
||||
val selected: List<RecipientId> = requireNotNull(data.getParcelableArrayListExtra(PushContactSelectionActivity.KEY_SELECTED_RECIPIENTS))
|
||||
val progress: SimpleProgressDialog.DismissibleDialog = SimpleProgressDialog.showDelayed(requireContext())
|
||||
|
||||
viewModel.onAddToGroupComplete(selected) {
|
||||
progress.dismiss()
|
||||
}
|
||||
}
|
||||
REQUEST_CODE_RETURN_FROM_MEDIA -> viewModel.refreshSharedMedia()
|
||||
REQUEST_CODE_ADD_CONTACT -> viewModel.refreshRecipient()
|
||||
REQUEST_CODE_VIEW_CONTACT -> viewModel.refreshRecipient()
|
||||
}
|
||||
}
|
||||
|
||||
override fun getOnScrollAnimationHelper(toolbarShadow: View): OnScrollAnimationHelper {
|
||||
return ConversationSettingsOnUserScrolledAnimationHelper(toolbarAvatar, toolbarTitle, toolbarBackground, toolbarShadow)
|
||||
}
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
return if (item.itemId == R.id.action_edit) {
|
||||
val args = ConversationSettingsFragmentArgs.fromBundle(requireArguments())
|
||||
val groupId = args.groupId as ParcelableGroupId
|
||||
|
||||
startActivity(EditProfileActivity.getIntentForGroupProfile(requireActivity(), requireNotNull(ParcelableGroupId.get(groupId))))
|
||||
true
|
||||
} else {
|
||||
super.onOptionsItemSelected(item)
|
||||
}
|
||||
}
|
||||
|
||||
override fun bindAdapter(adapter: DSLSettingsAdapter) {
|
||||
BioTextPreference.register(adapter)
|
||||
AvatarPreference.register(adapter)
|
||||
ButtonStripPreference.register(adapter)
|
||||
LargeIconClickPreference.register(adapter)
|
||||
SharedMediaPreference.register(adapter)
|
||||
RecipientPreference.register(adapter)
|
||||
InternalPreference.register(adapter)
|
||||
GroupDescriptionPreference.register(adapter)
|
||||
LegacyGroupPreference.register(adapter)
|
||||
|
||||
viewModel.state.observe(viewLifecycleOwner) { state ->
|
||||
|
||||
if (state.recipient != Recipient.UNKNOWN) {
|
||||
toolbarAvatar.buildOptions()
|
||||
.withQuickContactEnabled(false)
|
||||
.withUseSelfProfileAvatar(false)
|
||||
.withFixedSize(ViewUtil.dpToPx(80))
|
||||
.load(state.recipient)
|
||||
|
||||
state.withRecipientSettingsState {
|
||||
toolbarTitle.text = state.recipient.getDisplayName(requireContext())
|
||||
}
|
||||
|
||||
state.withGroupSettingsState {
|
||||
toolbarTitle.text = it.groupTitle
|
||||
toolbar.menu.findItem(R.id.action_edit).isVisible = it.canEditGroupAttributes
|
||||
}
|
||||
}
|
||||
|
||||
adapter.submitList(getConfiguration(state).toMappingModelList()) {
|
||||
if (state.isLoaded) {
|
||||
(requireView().parent as? ViewGroup)?.doOnPreDraw {
|
||||
callback.onContentWillRender()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
viewModel.events.observe(viewLifecycleOwner) { event ->
|
||||
@Exhaustive
|
||||
when (event) {
|
||||
is ConversationSettingsEvent.AddToAGroup -> handleAddToAGroup(event)
|
||||
is ConversationSettingsEvent.AddMembersToGroup -> handleAddMembersToGroup(event)
|
||||
ConversationSettingsEvent.ShowGroupHardLimitDialog -> showGroupHardLimitDialog()
|
||||
is ConversationSettingsEvent.ShowAddMembersToGroupError -> showAddMembersToGroupError(event)
|
||||
is ConversationSettingsEvent.ShowGroupInvitesSentDialog -> showGroupInvitesSentDialog(event)
|
||||
is ConversationSettingsEvent.ShowMembersAdded -> showMembersAdded(event)
|
||||
is ConversationSettingsEvent.InitiateGroupMigration -> GroupsV1MigrationInitiationBottomSheetDialogFragment.showForInitiation(parentFragmentManager, event.recipientId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun getConfiguration(state: ConversationSettingsState): DSLConfiguration {
|
||||
return configure {
|
||||
if (state.recipient == Recipient.UNKNOWN) {
|
||||
return@configure
|
||||
}
|
||||
|
||||
customPref(
|
||||
AvatarPreference.Model(
|
||||
recipient = state.recipient,
|
||||
onAvatarClick = { avatar ->
|
||||
requireActivity().apply {
|
||||
startActivity(
|
||||
AvatarPreviewActivity.intentFromRecipientId(this, state.recipient.id),
|
||||
AvatarPreviewActivity.createTransitionBundle(this, avatar)
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
state.withRecipientSettingsState {
|
||||
customPref(BioTextPreference.RecipientModel(recipient = state.recipient))
|
||||
}
|
||||
|
||||
state.withGroupSettingsState { groupState ->
|
||||
|
||||
val groupMembershipDescription = if (groupState.groupId.isV1) {
|
||||
String.format("%s · %s", groupState.membershipCountDescription, getString(R.string.ManageGroupActivity_legacy_group))
|
||||
} else if (!groupState.canEditGroupAttributes && groupState.groupDescription.isNullOrEmpty()) {
|
||||
groupState.membershipCountDescription
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
customPref(
|
||||
BioTextPreference.GroupModel(
|
||||
groupTitle = groupState.groupTitle,
|
||||
groupMembershipDescription = groupMembershipDescription
|
||||
)
|
||||
)
|
||||
|
||||
if (groupState.groupId.isV2) {
|
||||
customPref(
|
||||
GroupDescriptionPreference.Model(
|
||||
groupId = groupState.groupId,
|
||||
groupDescription = groupState.groupDescription,
|
||||
descriptionShouldLinkify = groupState.groupDescriptionShouldLinkify,
|
||||
canEditGroupAttributes = groupState.canEditGroupAttributes,
|
||||
onEditGroupDescription = {
|
||||
startActivity(EditProfileActivity.getIntentForGroupProfile(requireActivity(), groupState.groupId))
|
||||
},
|
||||
onViewGroupDescription = {
|
||||
GroupDescriptionDialog.show(childFragmentManager, groupState.groupId, null, groupState.groupDescriptionShouldLinkify)
|
||||
}
|
||||
)
|
||||
)
|
||||
} else if (groupState.legacyGroupState != LegacyGroupPreference.State.NONE) {
|
||||
customPref(
|
||||
LegacyGroupPreference.Model(
|
||||
state = groupState.legacyGroupState,
|
||||
onLearnMoreClick = { GroupsLearnMoreBottomSheetDialogFragment.show(parentFragmentManager) },
|
||||
onUpgradeClick = { viewModel.initiateGroupUpgrade() },
|
||||
onMmsWarningClick = { startActivity(Intent(requireContext(), InviteActivity::class.java)) }
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
state.withRecipientSettingsState { recipientState ->
|
||||
if (recipientState.displayInternalRecipientDetails) {
|
||||
customPref(
|
||||
InternalPreference.Model(
|
||||
recipient = state.recipient,
|
||||
onDisableProfileSharingClick = {
|
||||
viewModel.disableProfileSharing()
|
||||
}
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
customPref(
|
||||
ButtonStripPreference.Model(
|
||||
state = state.buttonStripState,
|
||||
onVideoClick = {
|
||||
CommunicationActions.startVideoCall(requireActivity(), state.recipient)
|
||||
},
|
||||
onAudioClick = {
|
||||
CommunicationActions.startVoiceCall(requireActivity(), state.recipient)
|
||||
},
|
||||
onMuteClick = {
|
||||
if (!state.buttonStripState.isMuted) {
|
||||
MuteDialog.show(requireContext(), viewModel::setMuteUntil)
|
||||
} else {
|
||||
MaterialAlertDialogBuilder(requireContext())
|
||||
.setMessage(state.recipient.muteUntil.formatMutedUntil(requireContext()))
|
||||
.setPositiveButton(R.string.ConversationSettingsFragment__unmute) { dialog, _ ->
|
||||
viewModel.unmute()
|
||||
dialog.dismiss()
|
||||
}
|
||||
.setNegativeButton(android.R.string.cancel) { dialog, _ -> dialog.dismiss() }
|
||||
.show()
|
||||
}
|
||||
},
|
||||
onSearchClick = {
|
||||
val intent = ConversationIntents.createBuilder(requireContext(), state.recipient.id, state.threadId)
|
||||
.withSearchOpen(true)
|
||||
.build()
|
||||
|
||||
startActivity(intent)
|
||||
requireActivity().finish()
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
dividerPref()
|
||||
|
||||
val summary = DSLSettingsText.from(formatDisappearingMessagesLifespan(state.disappearingMessagesLifespan))
|
||||
val icon = if (state.disappearingMessagesLifespan <= 0) {
|
||||
R.drawable.ic_update_timer_disabled_16
|
||||
} else {
|
||||
R.drawable.ic_update_timer_16
|
||||
}
|
||||
|
||||
var enabled = true
|
||||
state.withGroupSettingsState {
|
||||
enabled = it.canEditGroupAttributes
|
||||
}
|
||||
|
||||
clickPref(
|
||||
title = DSLSettingsText.from(R.string.ConversationSettingsFragment__disappearing_messages),
|
||||
summary = summary,
|
||||
icon = DSLSettingsIcon.from(icon),
|
||||
isEnabled = enabled,
|
||||
onClick = {
|
||||
val action = ConversationSettingsFragmentDirections.actionConversationSettingsFragmentToAppSettingsExpireTimer()
|
||||
.setInitialValue(state.disappearingMessagesLifespan)
|
||||
.setRecipientId(state.recipient.id)
|
||||
.setForResultMode(false)
|
||||
|
||||
navController.navigate(action)
|
||||
}
|
||||
)
|
||||
|
||||
clickPref(
|
||||
title = DSLSettingsText.from(R.string.preferences__chat_color_and_wallpaper),
|
||||
icon = DSLSettingsIcon.from(R.drawable.ic_color_24),
|
||||
onClick = {
|
||||
startActivity(ChatWallpaperActivity.createIntent(requireContext(), state.recipient.id))
|
||||
}
|
||||
)
|
||||
|
||||
if (!state.recipient.isSelf) {
|
||||
clickPref(
|
||||
title = DSLSettingsText.from(R.string.ConversationSettingsFragment__sounds_and_notifications),
|
||||
icon = DSLSettingsIcon.from(R.drawable.ic_speaker_24),
|
||||
onClick = {
|
||||
val action = ConversationSettingsFragmentDirections.actionConversationSettingsFragmentToSoundsAndNotificationsSettingsFragment(state.recipient.id)
|
||||
|
||||
navController.navigate(action)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
state.withRecipientSettingsState { recipientState ->
|
||||
when (recipientState.contactLinkState) {
|
||||
ContactLinkState.OPEN -> {
|
||||
clickPref(
|
||||
title = DSLSettingsText.from(R.string.ConversationSettingsFragment__contact_details),
|
||||
icon = DSLSettingsIcon.from(R.drawable.ic_profile_circle_24),
|
||||
onClick = {
|
||||
startActivityForResult(Intent(Intent.ACTION_VIEW, state.recipient.contactUri), REQUEST_CODE_VIEW_CONTACT)
|
||||
}
|
||||
)
|
||||
}
|
||||
ContactLinkState.ADD -> {
|
||||
clickPref(
|
||||
title = DSLSettingsText.from(R.string.ConversationSettingsFragment__add_as_a_contact),
|
||||
icon = DSLSettingsIcon.from(R.drawable.ic_plus_24),
|
||||
onClick = {
|
||||
startActivityForResult(RecipientExporter.export(state.recipient).asAddContactIntent(), REQUEST_CODE_ADD_CONTACT)
|
||||
}
|
||||
)
|
||||
}
|
||||
ContactLinkState.NONE -> {
|
||||
}
|
||||
}
|
||||
|
||||
if (recipientState.identityRecord != null) {
|
||||
clickPref(
|
||||
title = DSLSettingsText.from(R.string.ConversationSettingsFragment__view_safety_number),
|
||||
icon = DSLSettingsIcon.from(R.drawable.ic_safety_number_24),
|
||||
onClick = {
|
||||
startActivity(VerifyIdentityActivity.newIntent(requireActivity(), recipientState.identityRecord))
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (state.sharedMedia != null && state.sharedMedia.count > 0) {
|
||||
dividerPref()
|
||||
|
||||
sectionHeaderPref(R.string.recipient_preference_activity__shared_media)
|
||||
|
||||
customPref(
|
||||
SharedMediaPreference.Model(
|
||||
mediaCursor = state.sharedMedia,
|
||||
mediaIds = state.sharedMediaIds,
|
||||
onMediaRecordClick = { mediaRecord, isLtr ->
|
||||
startActivityForResult(
|
||||
MediaPreviewActivity.intentFromMediaRecord(requireContext(), mediaRecord, isLtr),
|
||||
REQUEST_CODE_RETURN_FROM_MEDIA
|
||||
)
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
clickPref(
|
||||
title = DSLSettingsText.from(R.string.ConversationSettingsFragment__see_all),
|
||||
onClick = {
|
||||
startActivity(MediaOverviewActivity.forThread(requireContext(), state.threadId))
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
state.withRecipientSettingsState { groupState ->
|
||||
if (groupState.selfHasGroups) {
|
||||
|
||||
dividerPref()
|
||||
|
||||
val groupsInCommonCount = groupState.allGroupsInCommon.size
|
||||
sectionHeaderPref(
|
||||
DSLSettingsText.from(
|
||||
if (groupsInCommonCount == 0) {
|
||||
getString(R.string.ManageRecipientActivity_no_groups_in_common)
|
||||
} else {
|
||||
resources.getQuantityString(
|
||||
R.plurals.ManageRecipientActivity_d_groups_in_common,
|
||||
groupsInCommonCount,
|
||||
groupsInCommonCount
|
||||
)
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
customPref(
|
||||
LargeIconClickPreference.Model(
|
||||
title = DSLSettingsText.from(R.string.ConversationSettingsFragment__add_to_a_group),
|
||||
icon = DSLSettingsIcon.from(R.drawable.add_to_a_group, NO_TINT),
|
||||
onClick = {
|
||||
viewModel.onAddToGroup()
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
for (group in groupState.groupsInCommon) {
|
||||
customPref(
|
||||
RecipientPreference.Model(
|
||||
recipient = group,
|
||||
onClick = {
|
||||
CommunicationActions.startConversation(requireActivity(), group, null)
|
||||
requireActivity().finish()
|
||||
}
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
if (groupState.canShowMoreGroupsInCommon) {
|
||||
customPref(
|
||||
LargeIconClickPreference.Model(
|
||||
title = DSLSettingsText.from(R.string.ConversationSettingsFragment__see_all),
|
||||
icon = DSLSettingsIcon.from(R.drawable.show_more, NO_TINT),
|
||||
onClick = {
|
||||
viewModel.revealAllMembers()
|
||||
}
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
state.withGroupSettingsState { groupState ->
|
||||
val memberCount = groupState.allMembers.size
|
||||
|
||||
if (groupState.canAddToGroup || memberCount > 0) {
|
||||
dividerPref()
|
||||
|
||||
sectionHeaderPref(DSLSettingsText.from(resources.getQuantityString(R.plurals.ContactSelectionListFragment_d_members, memberCount, memberCount)))
|
||||
}
|
||||
|
||||
if (groupState.canAddToGroup) {
|
||||
customPref(
|
||||
LargeIconClickPreference.Model(
|
||||
title = DSLSettingsText.from(R.string.ConversationSettingsFragment__add_members),
|
||||
icon = DSLSettingsIcon.from(R.drawable.add_to_a_group, NO_TINT),
|
||||
onClick = {
|
||||
viewModel.onAddToGroup()
|
||||
}
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
for (member in groupState.members) {
|
||||
customPref(
|
||||
RecipientPreference.Model(
|
||||
recipient = member.member,
|
||||
isAdmin = member.isAdmin,
|
||||
onClick = {
|
||||
RecipientBottomSheetDialogFragment.create(member.member.id, groupState.groupId).show(parentFragmentManager, "BOTTOM")
|
||||
}
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
if (groupState.canShowMoreGroupMembers) {
|
||||
customPref(
|
||||
LargeIconClickPreference.Model(
|
||||
title = DSLSettingsText.from(R.string.ConversationSettingsFragment__see_all),
|
||||
icon = DSLSettingsIcon.from(R.drawable.show_more, NO_TINT),
|
||||
onClick = {
|
||||
viewModel.revealAllMembers()
|
||||
}
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
if (state.recipient.isPushV2Group) {
|
||||
dividerPref()
|
||||
|
||||
clickPref(
|
||||
title = DSLSettingsText.from(R.string.ConversationSettingsFragment__group_link),
|
||||
summary = DSLSettingsText.from(if (groupState.groupLinkEnabled) R.string.preferences_on else R.string.preferences_off),
|
||||
icon = DSLSettingsIcon.from(R.drawable.ic_link_16),
|
||||
onClick = {
|
||||
ShareableGroupLinkDialogFragment.create(groupState.groupId.requireV2()).show(parentFragmentManager, "DIALOG")
|
||||
}
|
||||
)
|
||||
|
||||
clickPref(
|
||||
title = DSLSettingsText.from(R.string.ConversationSettingsFragment__requests_and_invites),
|
||||
icon = DSLSettingsIcon.from(R.drawable.ic_update_group_add_16),
|
||||
onClick = {
|
||||
startActivity(ManagePendingAndRequestingMembersActivity.newIntent(requireContext(), groupState.groupId.requireV2()))
|
||||
}
|
||||
)
|
||||
|
||||
if (groupState.isSelfAdmin) {
|
||||
clickPref(
|
||||
title = DSLSettingsText.from(R.string.ConversationSettingsFragment__permissions),
|
||||
icon = DSLSettingsIcon.from(R.drawable.ic_lock_24),
|
||||
onClick = {
|
||||
val action = ConversationSettingsFragmentDirections.actionConversationSettingsFragmentToPermissionsSettingsFragment(ParcelableGroupId.from(groupState.groupId))
|
||||
navController.navigate(action)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (groupState.canLeave) {
|
||||
dividerPref()
|
||||
|
||||
clickPref(
|
||||
title = DSLSettingsText.from(R.string.conversation__menu_leave_group, alertTint),
|
||||
icon = DSLSettingsIcon.from(leaveIcon),
|
||||
onClick = {
|
||||
LeaveGroupDialog.handleLeavePushGroup(requireActivity(), groupState.groupId.requirePush(), null)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (state.canModifyBlockedState) {
|
||||
state.withRecipientSettingsState {
|
||||
dividerPref()
|
||||
}
|
||||
|
||||
state.withGroupSettingsState {
|
||||
if (!it.canLeave) {
|
||||
dividerPref()
|
||||
}
|
||||
}
|
||||
|
||||
val isBlocked = state.recipient.isBlocked
|
||||
val isGroup = state.recipient.isPushGroup
|
||||
|
||||
val title = when {
|
||||
isBlocked && isGroup -> R.string.ConversationSettingsFragment__unblock_group
|
||||
isBlocked -> R.string.ConversationSettingsFragment__unblock
|
||||
isGroup -> R.string.ConversationSettingsFragment__block_group
|
||||
else -> R.string.ConversationSettingsFragment__block
|
||||
}
|
||||
|
||||
val titleTint = if (isBlocked) null else alertTint
|
||||
val blockUnblockIcon = if (isBlocked) unblockIcon else blockIcon
|
||||
|
||||
clickPref(
|
||||
title = DSLSettingsText.from(title, titleTint),
|
||||
icon = DSLSettingsIcon.from(blockUnblockIcon),
|
||||
onClick = {
|
||||
if (state.recipient.isBlocked) {
|
||||
BlockUnblockDialog.showUnblockFor(requireContext(), viewLifecycleOwner.lifecycle, state.recipient) {
|
||||
viewModel.unblock()
|
||||
}
|
||||
} else {
|
||||
BlockUnblockDialog.showBlockFor(requireContext(), viewLifecycleOwner.lifecycle, state.recipient) {
|
||||
viewModel.block()
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun formatDisappearingMessagesLifespan(disappearingMessagesLifespan: Int): String {
|
||||
return if (disappearingMessagesLifespan <= 0) {
|
||||
getString(R.string.preferences_off)
|
||||
} else {
|
||||
ExpirationUtil.getExpirationDisplayValue(requireContext(), disappearingMessagesLifespan)
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleAddToAGroup(addToAGroup: ConversationSettingsEvent.AddToAGroup) {
|
||||
startActivity(AddToGroupsActivity.newIntent(requireContext(), addToAGroup.recipientId, addToAGroup.groupMembership))
|
||||
}
|
||||
|
||||
private fun handleAddMembersToGroup(addMembersToGroup: ConversationSettingsEvent.AddMembersToGroup) {
|
||||
startActivityForResult(
|
||||
AddMembersActivity.createIntent(
|
||||
requireContext(),
|
||||
addMembersToGroup.groupId,
|
||||
ContactsCursorLoader.DisplayMode.FLAG_PUSH,
|
||||
addMembersToGroup.selectionWarning,
|
||||
addMembersToGroup.selectionLimit,
|
||||
addMembersToGroup.groupMembersWithoutSelf
|
||||
),
|
||||
REQUEST_CODE_ADD_MEMBERS_TO_GROUP
|
||||
)
|
||||
}
|
||||
|
||||
private fun showGroupHardLimitDialog() {
|
||||
GroupLimitDialog.showHardLimitMessage(requireContext())
|
||||
}
|
||||
|
||||
private fun showAddMembersToGroupError(showAddMembersToGroupError: ConversationSettingsEvent.ShowAddMembersToGroupError) {
|
||||
Toast.makeText(requireContext(), GroupErrors.getUserDisplayMessage(showAddMembersToGroupError.failureReason), Toast.LENGTH_LONG).show()
|
||||
}
|
||||
|
||||
private fun showGroupInvitesSentDialog(showGroupInvitesSentDialog: ConversationSettingsEvent.ShowGroupInvitesSentDialog) {
|
||||
GroupInviteSentDialog.showInvitesSent(requireContext(), showGroupInvitesSentDialog.invitesSentTo)
|
||||
}
|
||||
|
||||
private fun showMembersAdded(showMembersAdded: ConversationSettingsEvent.ShowMembersAdded) {
|
||||
val string = resources.getQuantityString(
|
||||
R.plurals.ManageGroupActivity_added,
|
||||
showMembersAdded.membersAddedCount,
|
||||
showMembersAdded.membersAddedCount
|
||||
)
|
||||
|
||||
Snackbar.make(requireView(), string, Snackbar.LENGTH_SHORT).setTextColor(Color.WHITE).show()
|
||||
}
|
||||
|
||||
private class ConversationSettingsOnUserScrolledAnimationHelper(
|
||||
private val toolbarAvatar: View,
|
||||
private val toolbarTitle: View,
|
||||
private val toolbarBackground: View,
|
||||
toolbarShadow: View
|
||||
) : ToolbarShadowAnimationHelper(toolbarShadow) {
|
||||
|
||||
override val duration: Long = 200L
|
||||
|
||||
private val actionBarSize = ThemeUtil.getThemedDimen(toolbarShadow.context, R.attr.actionBarSize)
|
||||
private val rect = Rect()
|
||||
|
||||
override fun getAnimationState(recyclerView: RecyclerView): AnimationState {
|
||||
val layoutManager = recyclerView.layoutManager as LinearLayoutManager
|
||||
|
||||
return if (layoutManager.findFirstVisibleItemPosition() == 0) {
|
||||
val firstChild = requireNotNull(layoutManager.getChildAt(0))
|
||||
firstChild.getLocalVisibleRect(rect)
|
||||
|
||||
if (rect.height() <= actionBarSize) {
|
||||
AnimationState.SHOW
|
||||
} else {
|
||||
AnimationState.HIDE
|
||||
}
|
||||
} else {
|
||||
AnimationState.SHOW
|
||||
}
|
||||
}
|
||||
|
||||
override fun show(duration: Long) {
|
||||
super.show(duration)
|
||||
|
||||
toolbarAvatar
|
||||
.animate()
|
||||
.setDuration(duration)
|
||||
.translationY(0f)
|
||||
.alpha(1f)
|
||||
|
||||
toolbarTitle
|
||||
.animate()
|
||||
.setDuration(duration)
|
||||
.translationY(0f)
|
||||
.alpha(1f)
|
||||
|
||||
toolbarBackground
|
||||
.animate()
|
||||
.setDuration(duration)
|
||||
.alpha(1f)
|
||||
}
|
||||
|
||||
override fun hide(duration: Long) {
|
||||
super.hide(duration)
|
||||
|
||||
toolbarAvatar
|
||||
.animate()
|
||||
.setDuration(duration)
|
||||
.translationY(ViewUtil.dpToPx(56).toFloat())
|
||||
.alpha(0f)
|
||||
|
||||
toolbarTitle
|
||||
.animate()
|
||||
.setDuration(duration)
|
||||
.translationY(ViewUtil.dpToPx(56).toFloat())
|
||||
.alpha(0f)
|
||||
|
||||
toolbarBackground
|
||||
.animate()
|
||||
.setDuration(duration)
|
||||
.alpha(0f)
|
||||
}
|
||||
}
|
||||
|
||||
interface Callback {
|
||||
fun onContentWillRender()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,208 @@
|
||||
package org.thoughtcrime.securesms.components.settings.conversation
|
||||
|
||||
import android.content.Context
|
||||
import android.database.Cursor
|
||||
import androidx.annotation.WorkerThread
|
||||
import androidx.lifecycle.LiveData
|
||||
import org.signal.core.util.concurrent.SignalExecutors
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.signal.storageservice.protos.groups.local.DecryptedGroup
|
||||
import org.signal.storageservice.protos.groups.local.DecryptedPendingMember
|
||||
import org.thoughtcrime.securesms.contacts.sync.DirectoryHelper
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory
|
||||
import org.thoughtcrime.securesms.database.GroupDatabase
|
||||
import org.thoughtcrime.securesms.database.IdentityDatabase
|
||||
import org.thoughtcrime.securesms.database.MediaDatabase
|
||||
import org.thoughtcrime.securesms.groups.GroupId
|
||||
import org.thoughtcrime.securesms.groups.GroupManager
|
||||
import org.thoughtcrime.securesms.groups.GroupProtoUtil
|
||||
import org.thoughtcrime.securesms.groups.LiveGroup
|
||||
import org.thoughtcrime.securesms.groups.ui.GroupChangeFailureReason
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
import org.thoughtcrime.securesms.recipients.RecipientUtil
|
||||
import org.thoughtcrime.securesms.util.FeatureFlags
|
||||
import org.whispersystems.libsignal.util.guava.Optional
|
||||
import java.io.IOException
|
||||
|
||||
private val TAG = Log.tag(ConversationSettingsRepository::class.java)
|
||||
|
||||
class ConversationSettingsRepository(
|
||||
private val context: Context
|
||||
) {
|
||||
|
||||
@WorkerThread
|
||||
fun getThreadMedia(threadId: Long): Optional<Cursor> {
|
||||
return if (threadId <= 0) {
|
||||
Optional.absent()
|
||||
} else {
|
||||
Optional.of(DatabaseFactory.getMediaDatabase(context).getGalleryMediaForThread(threadId, MediaDatabase.Sorting.Newest))
|
||||
}
|
||||
}
|
||||
|
||||
fun getThreadId(recipientId: RecipientId, consumer: (Long) -> Unit) {
|
||||
SignalExecutors.BOUNDED.execute {
|
||||
consumer(DatabaseFactory.getThreadDatabase(context).getThreadIdIfExistsFor(recipientId))
|
||||
}
|
||||
}
|
||||
|
||||
fun getThreadId(groupId: GroupId, consumer: (Long) -> Unit) {
|
||||
SignalExecutors.BOUNDED.execute {
|
||||
val recipientId = Recipient.externalGroupExact(context, groupId).id
|
||||
consumer(DatabaseFactory.getThreadDatabase(context).getThreadIdIfExistsFor(recipientId))
|
||||
}
|
||||
}
|
||||
|
||||
fun isInternalRecipientDetailsEnabled(): Boolean = SignalStore.internalValues().recipientDetails()
|
||||
|
||||
fun hasGroups(consumer: (Boolean) -> Unit) {
|
||||
SignalExecutors.BOUNDED.execute { consumer(DatabaseFactory.getGroupDatabase(context).activeGroupCount > 0) }
|
||||
}
|
||||
|
||||
fun getIdentity(recipientId: RecipientId, consumer: (IdentityDatabase.IdentityRecord?) -> Unit) {
|
||||
SignalExecutors.BOUNDED.execute {
|
||||
consumer(
|
||||
DatabaseFactory.getIdentityDatabase(context)
|
||||
.getIdentity(recipientId)
|
||||
.orNull()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun getGroupsInCommon(recipientId: RecipientId, consumer: (List<Recipient>) -> Unit) {
|
||||
SignalExecutors.BOUNDED.execute {
|
||||
consumer(
|
||||
DatabaseFactory
|
||||
.getGroupDatabase(context)
|
||||
.getPushGroupsContainingMember(recipientId)
|
||||
.asSequence()
|
||||
.filter { it.members.contains(Recipient.self().id) }
|
||||
.map(GroupDatabase.GroupRecord::getRecipientId)
|
||||
.map(Recipient::resolved)
|
||||
.sortedBy { gr -> gr.getDisplayName(context) }
|
||||
.toList()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun getGroupMembership(recipientId: RecipientId, consumer: (List<RecipientId>) -> Unit) {
|
||||
SignalExecutors.BOUNDED.execute {
|
||||
val groupDatabase = DatabaseFactory.getGroupDatabase(context)
|
||||
val groupRecords = groupDatabase.getPushGroupsContainingMember(recipientId)
|
||||
val groupRecipients = ArrayList<RecipientId>(groupRecords.size)
|
||||
for (groupRecord in groupRecords) {
|
||||
groupRecipients.add(groupRecord.recipientId)
|
||||
}
|
||||
consumer(groupRecipients)
|
||||
}
|
||||
}
|
||||
|
||||
fun refreshRecipient(recipientId: RecipientId) {
|
||||
SignalExecutors.UNBOUNDED.execute {
|
||||
try {
|
||||
DirectoryHelper.refreshDirectoryFor(context, Recipient.resolved(recipientId), false)
|
||||
} catch (e: IOException) {
|
||||
Log.w(TAG, "Failed to refresh user after adding to contacts.")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun setMuteUntil(recipientId: RecipientId, until: Long) {
|
||||
SignalExecutors.BOUNDED.execute {
|
||||
DatabaseFactory.getRecipientDatabase(context).setMuted(recipientId, until)
|
||||
}
|
||||
}
|
||||
|
||||
fun getGroupCapacity(groupId: GroupId, consumer: (GroupCapacityResult) -> Unit) {
|
||||
SignalExecutors.BOUNDED.execute {
|
||||
val groupRecord: GroupDatabase.GroupRecord = DatabaseFactory.getGroupDatabase(context).getGroup(groupId).get()
|
||||
consumer(
|
||||
if (groupRecord.isV2Group) {
|
||||
val decryptedGroup: DecryptedGroup = groupRecord.requireV2GroupProperties().decryptedGroup
|
||||
val pendingMembers: List<RecipientId> = decryptedGroup.pendingMembersList
|
||||
.map(DecryptedPendingMember::getUuid)
|
||||
.map(GroupProtoUtil::uuidByteStringToRecipientId)
|
||||
|
||||
val members = mutableListOf<RecipientId>()
|
||||
|
||||
members.addAll(groupRecord.members)
|
||||
members.addAll(pendingMembers)
|
||||
|
||||
GroupCapacityResult(Recipient.self().id, members, FeatureFlags.groupLimits())
|
||||
} else {
|
||||
GroupCapacityResult(Recipient.self().id, groupRecord.members, FeatureFlags.groupLimits())
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun addMembers(groupId: GroupId, selected: List<RecipientId>, consumer: (GroupAddMembersResult) -> Unit) {
|
||||
SignalExecutors.BOUNDED.execute {
|
||||
consumer(
|
||||
try {
|
||||
val groupActionResult = GroupManager.addMembers(context, groupId.requirePush(), selected)
|
||||
GroupAddMembersResult.Success(groupActionResult.addedMemberCount, Recipient.resolvedList(groupActionResult.invitedMembers))
|
||||
} catch (e: Exception) {
|
||||
GroupAddMembersResult.Failure(GroupChangeFailureReason.fromException(e))
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun setMuteUntil(groupId: GroupId, until: Long) {
|
||||
SignalExecutors.BOUNDED.execute {
|
||||
val recipientId = Recipient.externalGroupExact(context, groupId).id
|
||||
DatabaseFactory.getRecipientDatabase(context).setMuted(recipientId, until)
|
||||
}
|
||||
}
|
||||
|
||||
fun block(recipientId: RecipientId) {
|
||||
SignalExecutors.BOUNDED.execute {
|
||||
val recipient = Recipient.resolved(recipientId)
|
||||
RecipientUtil.blockNonGroup(context, recipient)
|
||||
}
|
||||
}
|
||||
|
||||
fun unblock(recipientId: RecipientId) {
|
||||
SignalExecutors.BOUNDED.execute {
|
||||
val recipient = Recipient.resolved(recipientId)
|
||||
RecipientUtil.unblock(context, recipient)
|
||||
}
|
||||
}
|
||||
|
||||
fun block(groupId: GroupId) {
|
||||
SignalExecutors.BOUNDED.execute {
|
||||
val recipient = Recipient.externalGroupExact(context, groupId)
|
||||
RecipientUtil.block(context, recipient)
|
||||
}
|
||||
}
|
||||
|
||||
fun unblock(groupId: GroupId) {
|
||||
SignalExecutors.BOUNDED.execute {
|
||||
val recipient = Recipient.externalGroupExact(context, groupId)
|
||||
RecipientUtil.unblock(context, recipient)
|
||||
}
|
||||
}
|
||||
|
||||
fun disableProfileSharing(recipientId: RecipientId) {
|
||||
SignalExecutors.BOUNDED.execute {
|
||||
DatabaseFactory.getRecipientDatabase(context).setProfileSharing(recipientId, false)
|
||||
}
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
fun isMessageRequestAccepted(recipient: Recipient): Boolean {
|
||||
return RecipientUtil.isMessageRequestAccepted(context, recipient)
|
||||
}
|
||||
|
||||
fun getMembershipCountDescription(liveGroup: LiveGroup): LiveData<String> {
|
||||
return liveGroup.getMembershipCountDescription(context.resources)
|
||||
}
|
||||
|
||||
fun getExternalPossiblyMigratedGroupRecipientId(groupId: GroupId, consumer: (RecipientId) -> Unit) {
|
||||
SignalExecutors.BOUNDED.execute {
|
||||
consumer(Recipient.externalPossiblyMigratedGroup(context, groupId).id)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
package org.thoughtcrime.securesms.components.settings.conversation
|
||||
|
||||
import android.database.Cursor
|
||||
import org.thoughtcrime.securesms.components.settings.conversation.preferences.ButtonStripPreference
|
||||
import org.thoughtcrime.securesms.components.settings.conversation.preferences.LegacyGroupPreference
|
||||
import org.thoughtcrime.securesms.database.IdentityDatabase
|
||||
import org.thoughtcrime.securesms.groups.GroupId
|
||||
import org.thoughtcrime.securesms.groups.ui.GroupMemberEntry
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
|
||||
data class ConversationSettingsState(
|
||||
val threadId: Long = -1,
|
||||
val recipient: Recipient = Recipient.UNKNOWN,
|
||||
val buttonStripState: ButtonStripPreference.State = ButtonStripPreference.State(),
|
||||
val disappearingMessagesLifespan: Int = 0,
|
||||
val canModifyBlockedState: Boolean = false,
|
||||
val sharedMedia: Cursor? = null,
|
||||
val sharedMediaIds: List<Long> = listOf(),
|
||||
private val sharedMediaLoaded: Boolean = false,
|
||||
private val specificSettingsState: SpecificSettingsState,
|
||||
) {
|
||||
|
||||
val isLoaded: Boolean = recipient != Recipient.UNKNOWN && sharedMediaLoaded && specificSettingsState.isLoaded
|
||||
|
||||
fun withRecipientSettingsState(consumer: (SpecificSettingsState.RecipientSettingsState) -> Unit) {
|
||||
if (specificSettingsState is SpecificSettingsState.RecipientSettingsState) {
|
||||
consumer(specificSettingsState)
|
||||
}
|
||||
}
|
||||
|
||||
fun withGroupSettingsState(consumer: (SpecificSettingsState.GroupSettingsState) -> Unit) {
|
||||
if (specificSettingsState is SpecificSettingsState.GroupSettingsState) {
|
||||
consumer(specificSettingsState)
|
||||
}
|
||||
}
|
||||
|
||||
fun requireRecipientSettingsState(): SpecificSettingsState.RecipientSettingsState = specificSettingsState.requireRecipientSettingsState()
|
||||
fun requireGroupSettingsState(): SpecificSettingsState.GroupSettingsState = specificSettingsState.requireGroupSettingsState()
|
||||
}
|
||||
|
||||
sealed class SpecificSettingsState {
|
||||
|
||||
abstract val isLoaded: Boolean
|
||||
|
||||
data class RecipientSettingsState(
|
||||
val identityRecord: IdentityDatabase.IdentityRecord? = null,
|
||||
val allGroupsInCommon: List<Recipient> = listOf(),
|
||||
val groupsInCommon: List<Recipient> = listOf(),
|
||||
val selfHasGroups: Boolean = false,
|
||||
val canShowMoreGroupsInCommon: Boolean = false,
|
||||
val groupsInCommonExpanded: Boolean = false,
|
||||
val contactLinkState: ContactLinkState = ContactLinkState.NONE,
|
||||
val displayInternalRecipientDetails: Boolean
|
||||
) : SpecificSettingsState() {
|
||||
|
||||
override val isLoaded: Boolean = true
|
||||
|
||||
override fun requireRecipientSettingsState() = this
|
||||
}
|
||||
|
||||
data class GroupSettingsState(
|
||||
val groupId: GroupId,
|
||||
val allMembers: List<GroupMemberEntry.FullMember> = listOf(),
|
||||
val members: List<GroupMemberEntry.FullMember> = listOf(),
|
||||
val isSelfAdmin: Boolean = false,
|
||||
val canAddToGroup: Boolean = false,
|
||||
val canEditGroupAttributes: Boolean = false,
|
||||
val canLeave: Boolean = false,
|
||||
val canShowMoreGroupMembers: Boolean = false,
|
||||
val groupMembersExpanded: Boolean = false,
|
||||
val groupTitle: String = "",
|
||||
private val groupTitleLoaded: Boolean = false,
|
||||
val groupDescription: String? = null,
|
||||
val groupDescriptionShouldLinkify: Boolean = false,
|
||||
private val groupDescriptionLoaded: Boolean = false,
|
||||
val groupLinkEnabled: Boolean = false,
|
||||
val membershipCountDescription: String = "",
|
||||
val legacyGroupState: LegacyGroupPreference.State = LegacyGroupPreference.State.NONE
|
||||
) : SpecificSettingsState() {
|
||||
|
||||
override val isLoaded: Boolean = groupTitleLoaded && groupDescriptionLoaded
|
||||
|
||||
override fun requireGroupSettingsState(): GroupSettingsState = this
|
||||
}
|
||||
|
||||
open fun requireRecipientSettingsState(): RecipientSettingsState = error("Not a recipient settings state")
|
||||
open fun requireGroupSettingsState(): GroupSettingsState = error("Not a group settings state")
|
||||
}
|
||||
|
||||
enum class ContactLinkState {
|
||||
OPEN,
|
||||
ADD,
|
||||
NONE
|
||||
}
|
||||
@@ -0,0 +1,472 @@
|
||||
package org.thoughtcrime.securesms.components.settings.conversation
|
||||
|
||||
import android.database.Cursor
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.Transformations
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import org.signal.core.util.ThreadUtil
|
||||
import org.signal.core.util.concurrent.SignalExecutors
|
||||
import org.thoughtcrime.securesms.components.settings.conversation.preferences.ButtonStripPreference
|
||||
import org.thoughtcrime.securesms.components.settings.conversation.preferences.LegacyGroupPreference
|
||||
import org.thoughtcrime.securesms.database.AttachmentDatabase
|
||||
import org.thoughtcrime.securesms.database.RecipientDatabase
|
||||
import org.thoughtcrime.securesms.groups.GroupId
|
||||
import org.thoughtcrime.securesms.groups.LiveGroup
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
import org.thoughtcrime.securesms.recipients.RecipientUtil
|
||||
import org.thoughtcrime.securesms.util.CursorUtil
|
||||
import org.thoughtcrime.securesms.util.FeatureFlags
|
||||
import org.thoughtcrime.securesms.util.SingleLiveEvent
|
||||
import org.thoughtcrime.securesms.util.livedata.LiveDataUtil
|
||||
import org.thoughtcrime.securesms.util.livedata.Store
|
||||
import org.whispersystems.libsignal.util.guava.Optional
|
||||
|
||||
sealed class ConversationSettingsViewModel(
|
||||
private val repository: ConversationSettingsRepository,
|
||||
specificSettingsState: SpecificSettingsState,
|
||||
) : ViewModel() {
|
||||
|
||||
private val openedMediaCursors = HashSet<Cursor>()
|
||||
|
||||
@Volatile
|
||||
private var cleared = false
|
||||
|
||||
protected val store = Store(
|
||||
ConversationSettingsState(
|
||||
specificSettingsState = specificSettingsState
|
||||
)
|
||||
)
|
||||
protected val internalEvents = SingleLiveEvent<ConversationSettingsEvent>()
|
||||
|
||||
private val sharedMediaUpdateTrigger = MutableLiveData(Unit)
|
||||
|
||||
val state: LiveData<ConversationSettingsState> = store.stateLiveData
|
||||
val events: LiveData<ConversationSettingsEvent> = internalEvents
|
||||
|
||||
init {
|
||||
val threadId: LiveData<Long> = Transformations.distinctUntilChanged(Transformations.map(state) { it.threadId })
|
||||
val updater: LiveData<Long> = LiveDataUtil.combineLatest(threadId, sharedMediaUpdateTrigger) { tId, _ -> tId }
|
||||
|
||||
val sharedMedia: LiveData<Optional<Cursor>> = LiveDataUtil.mapAsync(SignalExecutors.BOUNDED, updater) { tId ->
|
||||
repository.getThreadMedia(tId)
|
||||
}
|
||||
|
||||
store.update(sharedMedia) { cursor, state ->
|
||||
if (!cleared) {
|
||||
if (cursor.isPresent) {
|
||||
openedMediaCursors.add(cursor.get())
|
||||
}
|
||||
|
||||
val ids: List<Long> = cursor.transform<List<Long>> {
|
||||
val result = mutableListOf<Long>()
|
||||
while (it.moveToNext()) {
|
||||
result.add(CursorUtil.requireLong(it, AttachmentDatabase.ROW_ID))
|
||||
}
|
||||
result
|
||||
}.or(listOf())
|
||||
|
||||
state.copy(
|
||||
sharedMedia = cursor.orNull(),
|
||||
sharedMediaIds = ids,
|
||||
sharedMediaLoaded = true
|
||||
)
|
||||
} else {
|
||||
cursor.orNull().ensureClosed()
|
||||
state.copy(sharedMedia = null)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun refreshSharedMedia() {
|
||||
sharedMediaUpdateTrigger.postValue(Unit)
|
||||
}
|
||||
|
||||
open fun refreshRecipient(): Unit = error("This ViewModel does not support this interaction")
|
||||
|
||||
abstract fun setMuteUntil(muteUntil: Long)
|
||||
|
||||
abstract fun unmute()
|
||||
|
||||
abstract fun block()
|
||||
|
||||
abstract fun unblock()
|
||||
|
||||
abstract fun onAddToGroup()
|
||||
|
||||
abstract fun onAddToGroupComplete(selected: List<RecipientId>, onComplete: () -> Unit)
|
||||
|
||||
abstract fun revealAllMembers()
|
||||
|
||||
override fun onCleared() {
|
||||
cleared = true
|
||||
store.update { state ->
|
||||
openedMediaCursors.forEach { it.ensureClosed() }
|
||||
state.copy(sharedMedia = null)
|
||||
}
|
||||
}
|
||||
|
||||
private fun Cursor?.ensureClosed() {
|
||||
if (this != null && !this.isClosed) {
|
||||
this.close()
|
||||
}
|
||||
}
|
||||
|
||||
open fun disableProfileSharing(): Unit = error("This ViewModel does not support this interaction")
|
||||
|
||||
open fun initiateGroupUpgrade(): Unit = error("This ViewModel does not support this interaction")
|
||||
|
||||
private class RecipientSettingsViewModel(
|
||||
private val recipientId: RecipientId,
|
||||
private val repository: ConversationSettingsRepository
|
||||
) : ConversationSettingsViewModel(
|
||||
repository,
|
||||
SpecificSettingsState.RecipientSettingsState(
|
||||
displayInternalRecipientDetails = repository.isInternalRecipientDetailsEnabled()
|
||||
)
|
||||
) {
|
||||
|
||||
private val liveRecipient = Recipient.live(recipientId)
|
||||
|
||||
init {
|
||||
store.update(liveRecipient.liveData) { recipient, state ->
|
||||
state.copy(
|
||||
recipient = recipient,
|
||||
buttonStripState = ButtonStripPreference.State(
|
||||
isVideoAvailable = recipient.registered == RecipientDatabase.RegisteredState.REGISTERED && !recipient.isSelf,
|
||||
isAudioAvailable = !recipient.isGroup && !recipient.isSelf,
|
||||
isAudioSecure = recipient.registered == RecipientDatabase.RegisteredState.REGISTERED,
|
||||
isMuted = recipient.isMuted,
|
||||
isMuteAvailable = !recipient.isSelf,
|
||||
isSearchAvailable = true
|
||||
),
|
||||
disappearingMessagesLifespan = recipient.expireMessages,
|
||||
canModifyBlockedState = !recipient.isSelf,
|
||||
specificSettingsState = state.requireRecipientSettingsState().copy(
|
||||
contactLinkState = when {
|
||||
recipient.isSelf -> ContactLinkState.NONE
|
||||
recipient.isSystemContact -> ContactLinkState.OPEN
|
||||
else -> ContactLinkState.ADD
|
||||
}
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
repository.getThreadId(recipientId) { threadId ->
|
||||
store.update { state ->
|
||||
state.copy(threadId = threadId)
|
||||
}
|
||||
}
|
||||
|
||||
if (recipientId != Recipient.self().id) {
|
||||
repository.getGroupsInCommon(recipientId) { groupsInCommon ->
|
||||
store.update { state ->
|
||||
val recipientSettings = state.requireRecipientSettingsState()
|
||||
val expanded = recipientSettings.groupsInCommonExpanded
|
||||
state.copy(
|
||||
specificSettingsState = recipientSettings.copy(
|
||||
allGroupsInCommon = groupsInCommon,
|
||||
groupsInCommon = if (expanded) groupsInCommon else groupsInCommon.take(5),
|
||||
canShowMoreGroupsInCommon = !expanded && groupsInCommon.size > 5
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
repository.hasGroups { hasGroups ->
|
||||
store.update { state ->
|
||||
val recipientSettings = state.requireRecipientSettingsState()
|
||||
state.copy(
|
||||
specificSettingsState = recipientSettings.copy(
|
||||
selfHasGroups = hasGroups
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
repository.getIdentity(recipientId) { identityRecord ->
|
||||
store.update { state ->
|
||||
state.copy(specificSettingsState = state.requireRecipientSettingsState().copy(identityRecord = identityRecord))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onAddToGroup() {
|
||||
repository.getGroupMembership(recipientId) {
|
||||
internalEvents.postValue(ConversationSettingsEvent.AddToAGroup(recipientId, it))
|
||||
}
|
||||
}
|
||||
|
||||
override fun onAddToGroupComplete(selected: List<RecipientId>, onComplete: () -> Unit) {
|
||||
}
|
||||
|
||||
override fun revealAllMembers() {
|
||||
store.update { state ->
|
||||
state.copy(
|
||||
specificSettingsState = state.requireRecipientSettingsState().copy(
|
||||
groupsInCommon = state.requireRecipientSettingsState().allGroupsInCommon,
|
||||
groupsInCommonExpanded = true,
|
||||
canShowMoreGroupsInCommon = false
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun refreshRecipient() {
|
||||
repository.refreshRecipient(recipientId)
|
||||
}
|
||||
|
||||
override fun setMuteUntil(muteUntil: Long) {
|
||||
repository.setMuteUntil(recipientId, muteUntil)
|
||||
}
|
||||
|
||||
override fun unmute() {
|
||||
repository.setMuteUntil(recipientId, 0)
|
||||
}
|
||||
|
||||
override fun block() {
|
||||
repository.block(recipientId)
|
||||
}
|
||||
|
||||
override fun unblock() {
|
||||
repository.unblock(recipientId)
|
||||
}
|
||||
|
||||
override fun disableProfileSharing() {
|
||||
repository.disableProfileSharing(recipientId)
|
||||
}
|
||||
}
|
||||
|
||||
private class GroupSettingsViewModel(
|
||||
private val groupId: GroupId,
|
||||
private val repository: ConversationSettingsRepository
|
||||
) : ConversationSettingsViewModel(repository, SpecificSettingsState.GroupSettingsState(groupId)) {
|
||||
|
||||
private val liveGroup = LiveGroup(groupId)
|
||||
|
||||
init {
|
||||
store.update(liveGroup.groupRecipient) { recipient, state ->
|
||||
state.copy(
|
||||
recipient = recipient,
|
||||
buttonStripState = ButtonStripPreference.State(
|
||||
isVideoAvailable = recipient.isPushV2Group,
|
||||
isAudioAvailable = false,
|
||||
isAudioSecure = recipient.isPushV2Group,
|
||||
isMuted = recipient.isMuted,
|
||||
isMuteAvailable = true,
|
||||
isSearchAvailable = true
|
||||
),
|
||||
canModifyBlockedState = RecipientUtil.isBlockable(recipient),
|
||||
specificSettingsState = state.requireGroupSettingsState().copy(
|
||||
legacyGroupState = getLegacyGroupState(recipient)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
repository.getThreadId(groupId) { threadId ->
|
||||
store.update { state ->
|
||||
state.copy(threadId = threadId)
|
||||
}
|
||||
}
|
||||
|
||||
store.update(liveGroup.selfCanEditGroupAttributes()) { selfCanEditGroupAttributes, state ->
|
||||
state.copy(
|
||||
specificSettingsState = state.requireGroupSettingsState().copy(
|
||||
canEditGroupAttributes = selfCanEditGroupAttributes
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
store.update(liveGroup.isSelfAdmin) { isSelfAdmin, state ->
|
||||
state.copy(
|
||||
specificSettingsState = state.requireGroupSettingsState().copy(
|
||||
isSelfAdmin = isSelfAdmin
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
store.update(liveGroup.expireMessages) { expireMessages, state ->
|
||||
state.copy(
|
||||
disappearingMessagesLifespan = expireMessages
|
||||
)
|
||||
}
|
||||
|
||||
store.update(liveGroup.selfCanAddMembers()) { canAddMembers, state ->
|
||||
state.copy(
|
||||
specificSettingsState = state.requireGroupSettingsState().copy(
|
||||
canAddToGroup = canAddMembers
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
store.update(liveGroup.fullMembers) { fullMembers, state ->
|
||||
val groupState = state.requireGroupSettingsState()
|
||||
|
||||
state.copy(
|
||||
specificSettingsState = groupState.copy(
|
||||
allMembers = fullMembers,
|
||||
members = if (groupState.groupMembersExpanded) fullMembers else fullMembers.take(5),
|
||||
canShowMoreGroupMembers = !groupState.groupMembersExpanded && fullMembers.size > 5
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
val isMessageRequestAccepted: LiveData<Boolean> = LiveDataUtil.mapAsync(liveGroup.groupRecipient) { r -> repository.isMessageRequestAccepted(r) }
|
||||
val descriptionState: LiveData<DescriptionState> = LiveDataUtil.combineLatest(liveGroup.description, isMessageRequestAccepted, ::DescriptionState)
|
||||
|
||||
store.update(descriptionState) { d, state ->
|
||||
state.copy(
|
||||
specificSettingsState = state.requireGroupSettingsState().copy(
|
||||
groupDescription = d.description,
|
||||
groupDescriptionShouldLinkify = d.canLinkify,
|
||||
groupDescriptionLoaded = true
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
store.update(liveGroup.isActive) { isActive, state ->
|
||||
state.copy(
|
||||
specificSettingsState = state.requireGroupSettingsState().copy(
|
||||
canLeave = isActive && groupId.isPush
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
store.update(liveGroup.title) { title, state ->
|
||||
state.copy(
|
||||
specificSettingsState = state.requireGroupSettingsState().copy(
|
||||
groupTitle = title,
|
||||
groupTitleLoaded = true
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
store.update(liveGroup.groupLink) { groupLink, state ->
|
||||
state.copy(
|
||||
specificSettingsState = state.requireGroupSettingsState().copy(
|
||||
groupLinkEnabled = groupLink.isEnabled
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
store.update(repository.getMembershipCountDescription(liveGroup)) { description, state ->
|
||||
state.copy(
|
||||
specificSettingsState = state.requireGroupSettingsState().copy(
|
||||
membershipCountDescription = description
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun getLegacyGroupState(recipient: Recipient): LegacyGroupPreference.State {
|
||||
val showLegacyInfo = recipient.requireGroupId().isV1
|
||||
|
||||
return if (showLegacyInfo && recipient.participants.size > FeatureFlags.groupLimits().hardLimit) {
|
||||
LegacyGroupPreference.State.TOO_LARGE
|
||||
} else if (showLegacyInfo) {
|
||||
LegacyGroupPreference.State.UPGRADE
|
||||
} else if (groupId.isMms) {
|
||||
LegacyGroupPreference.State.MMS_WARNING
|
||||
} else {
|
||||
LegacyGroupPreference.State.NONE
|
||||
}
|
||||
}
|
||||
|
||||
override fun onAddToGroup() {
|
||||
repository.getGroupCapacity(groupId) { capacityResult ->
|
||||
if (capacityResult.getRemainingCapacity() > 0) {
|
||||
internalEvents.postValue(
|
||||
ConversationSettingsEvent.AddMembersToGroup(
|
||||
groupId,
|
||||
capacityResult.getSelectionWarning(),
|
||||
capacityResult.getSelectionLimit(),
|
||||
capacityResult.getMembersWithoutSelf()
|
||||
)
|
||||
)
|
||||
} else {
|
||||
internalEvents.postValue(ConversationSettingsEvent.ShowGroupHardLimitDialog)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onAddToGroupComplete(selected: List<RecipientId>, onComplete: () -> Unit) {
|
||||
repository.addMembers(groupId, selected) {
|
||||
ThreadUtil.runOnMain { onComplete() }
|
||||
|
||||
when (it) {
|
||||
is GroupAddMembersResult.Success -> {
|
||||
if (it.newMembersInvited.isNotEmpty()) {
|
||||
internalEvents.postValue(ConversationSettingsEvent.ShowGroupInvitesSentDialog(it.newMembersInvited))
|
||||
}
|
||||
|
||||
if (it.numberOfMembersAdded > 0) {
|
||||
internalEvents.postValue(ConversationSettingsEvent.ShowMembersAdded(it.numberOfMembersAdded))
|
||||
}
|
||||
}
|
||||
is GroupAddMembersResult.Failure -> internalEvents.postValue(ConversationSettingsEvent.ShowAddMembersToGroupError(it.reason))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun revealAllMembers() {
|
||||
store.update { state ->
|
||||
state.copy(
|
||||
specificSettingsState = state.requireGroupSettingsState().copy(
|
||||
members = state.requireGroupSettingsState().allMembers,
|
||||
groupMembersExpanded = true,
|
||||
canShowMoreGroupMembers = false
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun setMuteUntil(muteUntil: Long) {
|
||||
repository.setMuteUntil(groupId, muteUntil)
|
||||
}
|
||||
|
||||
override fun unmute() {
|
||||
repository.setMuteUntil(groupId, 0)
|
||||
}
|
||||
|
||||
override fun block() {
|
||||
repository.block(groupId)
|
||||
}
|
||||
|
||||
override fun unblock() {
|
||||
repository.unblock(groupId)
|
||||
}
|
||||
|
||||
override fun initiateGroupUpgrade() {
|
||||
repository.getExternalPossiblyMigratedGroupRecipientId(groupId) {
|
||||
internalEvents.postValue(ConversationSettingsEvent.InitiateGroupMigration(it))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class Factory(
|
||||
private val recipientId: RecipientId? = null,
|
||||
private val groupId: GroupId? = null,
|
||||
private val repository: ConversationSettingsRepository,
|
||||
) : ViewModelProvider.Factory {
|
||||
|
||||
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
|
||||
return requireNotNull(
|
||||
modelClass.cast(
|
||||
when {
|
||||
recipientId != null -> RecipientSettingsViewModel(recipientId, repository)
|
||||
groupId != null -> GroupSettingsViewModel(groupId, repository)
|
||||
else -> error("One of RecipientId or GroupId required.")
|
||||
}
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private class DescriptionState(
|
||||
val description: String?,
|
||||
val canLinkify: Boolean
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package org.thoughtcrime.securesms.components.settings.conversation
|
||||
|
||||
import org.thoughtcrime.securesms.groups.ui.GroupChangeFailureReason
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
|
||||
sealed class GroupAddMembersResult {
|
||||
class Success(
|
||||
val numberOfMembersAdded: Int,
|
||||
val newMembersInvited: List<Recipient>
|
||||
) : GroupAddMembersResult()
|
||||
|
||||
class Failure(
|
||||
val reason: GroupChangeFailureReason
|
||||
) : GroupAddMembersResult()
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
package org.thoughtcrime.securesms.components.settings.conversation
|
||||
|
||||
import org.thoughtcrime.securesms.ContactSelectionListFragment
|
||||
import org.thoughtcrime.securesms.groups.SelectionLimits
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
|
||||
class GroupCapacityResult(
|
||||
private val selfId: RecipientId,
|
||||
private val members: List<RecipientId>,
|
||||
private val selectionLimits: SelectionLimits
|
||||
) {
|
||||
fun getMembers(): List<RecipientId?> {
|
||||
return members
|
||||
}
|
||||
|
||||
fun getSelectionLimit(): Int {
|
||||
if (!selectionLimits.hasHardLimit()) {
|
||||
return ContactSelectionListFragment.NO_LIMIT
|
||||
}
|
||||
val containsSelf = members.indexOf(selfId) != -1
|
||||
return selectionLimits.hardLimit - if (containsSelf) 1 else 0
|
||||
}
|
||||
|
||||
fun getSelectionWarning(): Int {
|
||||
if (!selectionLimits.hasRecommendedLimit()) {
|
||||
return ContactSelectionListFragment.NO_LIMIT
|
||||
}
|
||||
|
||||
val containsSelf = members.indexOf(selfId) != -1
|
||||
return selectionLimits.recommendedLimit - if (containsSelf) 1 else 0
|
||||
}
|
||||
|
||||
fun getRemainingCapacity(): Int {
|
||||
return selectionLimits.hardLimit - members.size
|
||||
}
|
||||
|
||||
fun getMembersWithoutSelf(): List<RecipientId> {
|
||||
val recipientIds = ArrayList<RecipientId>(members.size)
|
||||
for (recipientId in members) {
|
||||
if (recipientId != selfId) {
|
||||
recipientIds.add(recipientId)
|
||||
}
|
||||
}
|
||||
return recipientIds
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package org.thoughtcrime.securesms.components.settings.conversation.permissions
|
||||
|
||||
import org.thoughtcrime.securesms.groups.ui.GroupChangeFailureReason
|
||||
|
||||
sealed class PermissionsSettingsEvents {
|
||||
class GroupChangeError(val reason: GroupChangeFailureReason) : PermissionsSettingsEvents()
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
package org.thoughtcrime.securesms.components.settings.conversation.permissions
|
||||
|
||||
import android.widget.Toast
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.fragment.app.viewModels
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.components.settings.DSLConfiguration
|
||||
import org.thoughtcrime.securesms.components.settings.DSLSettingsAdapter
|
||||
import org.thoughtcrime.securesms.components.settings.DSLSettingsFragment
|
||||
import org.thoughtcrime.securesms.components.settings.DSLSettingsText
|
||||
import org.thoughtcrime.securesms.components.settings.configure
|
||||
import org.thoughtcrime.securesms.groups.ParcelableGroupId
|
||||
import org.thoughtcrime.securesms.groups.ui.GroupErrors
|
||||
|
||||
class PermissionsSettingsFragment : DSLSettingsFragment(
|
||||
titleId = R.string.ConversationSettingsFragment__permissions
|
||||
) {
|
||||
|
||||
private val permissionsOptions: Array<String> by lazy {
|
||||
resources.getStringArray(R.array.PermissionsSettingsFragment__editor_labels)
|
||||
}
|
||||
|
||||
private val viewModel: PermissionsSettingsViewModel by viewModels(
|
||||
factoryProducer = {
|
||||
val args = PermissionsSettingsFragmentArgs.fromBundle(requireArguments())
|
||||
val groupId = requireNotNull(ParcelableGroupId.get(args.groupId as ParcelableGroupId))
|
||||
val repository = PermissionsSettingsRepository(requireContext())
|
||||
|
||||
PermissionsSettingsViewModel.Factory(groupId, repository)
|
||||
}
|
||||
)
|
||||
|
||||
override fun bindAdapter(adapter: DSLSettingsAdapter) {
|
||||
viewModel.state.observe(viewLifecycleOwner) { state ->
|
||||
adapter.submitList(getConfiguration(state).toMappingModelList())
|
||||
}
|
||||
|
||||
viewModel.events.observe(viewLifecycleOwner) { event ->
|
||||
when (event) {
|
||||
is PermissionsSettingsEvents.GroupChangeError -> handleGroupChangeError(event)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleGroupChangeError(groupChangeError: PermissionsSettingsEvents.GroupChangeError) {
|
||||
Toast.makeText(context, GroupErrors.getUserDisplayMessage(groupChangeError.reason), Toast.LENGTH_LONG).show()
|
||||
}
|
||||
|
||||
private fun getConfiguration(state: PermissionsSettingsState): DSLConfiguration {
|
||||
return configure {
|
||||
|
||||
radioListPref(
|
||||
title = DSLSettingsText.from(R.string.PermissionsSettingsFragment__add_members),
|
||||
isEnabled = state.selfCanEditSettings,
|
||||
listItems = permissionsOptions,
|
||||
dialogTitle = DSLSettingsText.from(R.string.PermissionsSettingsFragment__who_can_add_new_members),
|
||||
selected = getSelected(state.nonAdminCanAddMembers),
|
||||
confirmAction = true,
|
||||
onSelected = {
|
||||
viewModel.setNonAdminCanAddMembers(it == 1)
|
||||
}
|
||||
)
|
||||
|
||||
radioListPref(
|
||||
title = DSLSettingsText.from(R.string.PermissionsSettingsFragment__edit_group_info),
|
||||
isEnabled = state.selfCanEditSettings,
|
||||
listItems = permissionsOptions,
|
||||
dialogTitle = DSLSettingsText.from(R.string.PermissionsSettingsFragment__who_can_edit_this_groups_info),
|
||||
selected = getSelected(state.nonAdminCanEditGroupInfo),
|
||||
confirmAction = true,
|
||||
onSelected = {
|
||||
viewModel.setNonAdminCanEditGroupInfo(it == 1)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@StringRes
|
||||
private fun getSelected(isNonAdminAllowed: Boolean): Int {
|
||||
return if (isNonAdminAllowed) {
|
||||
1
|
||||
} else {
|
||||
0
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
package org.thoughtcrime.securesms.components.settings.conversation.permissions
|
||||
|
||||
import android.content.Context
|
||||
import org.signal.core.util.concurrent.SignalExecutors
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.thoughtcrime.securesms.groups.GroupAccessControl
|
||||
import org.thoughtcrime.securesms.groups.GroupChangeException
|
||||
import org.thoughtcrime.securesms.groups.GroupId
|
||||
import org.thoughtcrime.securesms.groups.GroupManager
|
||||
import org.thoughtcrime.securesms.groups.ui.GroupChangeErrorCallback
|
||||
import org.thoughtcrime.securesms.groups.ui.GroupChangeFailureReason
|
||||
import java.io.IOException
|
||||
|
||||
private val TAG = Log.tag(PermissionsSettingsRepository::class.java)
|
||||
|
||||
class PermissionsSettingsRepository(private val context: Context) {
|
||||
|
||||
fun applyMembershipRightsChange(groupId: GroupId, newRights: GroupAccessControl, error: GroupChangeErrorCallback) {
|
||||
SignalExecutors.UNBOUNDED.execute {
|
||||
try {
|
||||
GroupManager.applyMembershipAdditionRightsChange(context, groupId.requireV2(), newRights)
|
||||
} catch (e: GroupChangeException) {
|
||||
Log.w(TAG, e)
|
||||
error.onError(GroupChangeFailureReason.fromException(e))
|
||||
} catch (e: IOException) {
|
||||
Log.w(TAG, e)
|
||||
error.onError(GroupChangeFailureReason.fromException(e))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun applyAttributesRightsChange(groupId: GroupId, newRights: GroupAccessControl, error: GroupChangeErrorCallback) {
|
||||
SignalExecutors.UNBOUNDED.execute {
|
||||
try {
|
||||
GroupManager.applyAttributesRightsChange(context, groupId.requireV2(), newRights)
|
||||
} catch (e: GroupChangeException) {
|
||||
Log.w(TAG, e)
|
||||
error.onError(GroupChangeFailureReason.fromException(e))
|
||||
} catch (e: IOException) {
|
||||
Log.w(TAG, e)
|
||||
error.onError(GroupChangeFailureReason.fromException(e))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package org.thoughtcrime.securesms.components.settings.conversation.permissions
|
||||
|
||||
data class PermissionsSettingsState(
|
||||
val selfCanEditSettings: Boolean = false,
|
||||
val nonAdminCanAddMembers: Boolean = false,
|
||||
val nonAdminCanEditGroupInfo: Boolean = false
|
||||
)
|
||||
@@ -0,0 +1,66 @@
|
||||
package org.thoughtcrime.securesms.components.settings.conversation.permissions
|
||||
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import org.thoughtcrime.securesms.groups.GroupAccessControl
|
||||
import org.thoughtcrime.securesms.groups.GroupId
|
||||
import org.thoughtcrime.securesms.groups.LiveGroup
|
||||
import org.thoughtcrime.securesms.util.SingleLiveEvent
|
||||
import org.thoughtcrime.securesms.util.livedata.Store
|
||||
|
||||
class PermissionsSettingsViewModel(
|
||||
private val groupId: GroupId,
|
||||
private val repository: PermissionsSettingsRepository
|
||||
) : ViewModel() {
|
||||
|
||||
private val store = Store(PermissionsSettingsState())
|
||||
private val liveGroup = LiveGroup(groupId)
|
||||
private val internalEvents = SingleLiveEvent<PermissionsSettingsEvents>()
|
||||
|
||||
val state: LiveData<PermissionsSettingsState> = store.stateLiveData
|
||||
val events: LiveData<PermissionsSettingsEvents> = internalEvents
|
||||
|
||||
init {
|
||||
store.update(liveGroup.isSelfAdmin) { isSelfAdmin, state ->
|
||||
state.copy(selfCanEditSettings = isSelfAdmin)
|
||||
}
|
||||
|
||||
store.update(liveGroup.membershipAdditionAccessControl) { membershipAdditionAccessControl, state ->
|
||||
state.copy(nonAdminCanAddMembers = membershipAdditionAccessControl == GroupAccessControl.ALL_MEMBERS)
|
||||
}
|
||||
|
||||
store.update(liveGroup.attributesAccessControl) { attributesAccessControl, state ->
|
||||
state.copy(nonAdminCanEditGroupInfo = attributesAccessControl == GroupAccessControl.ALL_MEMBERS)
|
||||
}
|
||||
}
|
||||
|
||||
fun setNonAdminCanAddMembers(nonAdminCanAddMembers: Boolean) {
|
||||
repository.applyMembershipRightsChange(groupId, nonAdminCanAddMembers.asGroupAccessControl()) { reason ->
|
||||
internalEvents.postValue(PermissionsSettingsEvents.GroupChangeError(reason))
|
||||
}
|
||||
}
|
||||
|
||||
fun setNonAdminCanEditGroupInfo(nonAdminCanEditGroupInfo: Boolean) {
|
||||
repository.applyAttributesRightsChange(groupId, nonAdminCanEditGroupInfo.asGroupAccessControl()) { reason ->
|
||||
internalEvents.postValue(PermissionsSettingsEvents.GroupChangeError(reason))
|
||||
}
|
||||
}
|
||||
|
||||
private fun Boolean.asGroupAccessControl(): GroupAccessControl {
|
||||
return if (this) {
|
||||
GroupAccessControl.ALL_MEMBERS
|
||||
} else {
|
||||
GroupAccessControl.ONLY_ADMINS
|
||||
}
|
||||
}
|
||||
|
||||
class Factory(
|
||||
private val groupId: GroupId,
|
||||
private val repository: PermissionsSettingsRepository
|
||||
) : ViewModelProvider.Factory {
|
||||
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
|
||||
return requireNotNull(modelClass.cast(PermissionsSettingsViewModel(groupId, repository)))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
package org.thoughtcrime.securesms.components.settings.conversation.preferences
|
||||
|
||||
import android.view.View
|
||||
import androidx.core.view.ViewCompat
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.components.AvatarImageView
|
||||
import org.thoughtcrime.securesms.components.settings.PreferenceModel
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.util.MappingAdapter
|
||||
import org.thoughtcrime.securesms.util.MappingViewHolder
|
||||
|
||||
/**
|
||||
* Renders a large avatar (80dp) for a given Recipient.
|
||||
*/
|
||||
object AvatarPreference {
|
||||
|
||||
fun register(adapter: MappingAdapter) {
|
||||
adapter.registerFactory(Model::class.java, MappingAdapter.LayoutFactory(::ViewHolder, R.layout.conversation_settings_avatar_preference_item))
|
||||
}
|
||||
|
||||
class Model(
|
||||
val recipient: Recipient,
|
||||
val onAvatarClick: (View) -> Unit
|
||||
) : PreferenceModel<Model>() {
|
||||
override fun areItemsTheSame(newItem: Model): Boolean {
|
||||
return recipient == newItem.recipient
|
||||
}
|
||||
|
||||
override fun areContentsTheSame(newItem: Model): Boolean {
|
||||
return super.areContentsTheSame(newItem) && recipient.hasSameContent(newItem.recipient)
|
||||
}
|
||||
}
|
||||
|
||||
private class ViewHolder(itemView: View) : MappingViewHolder<Model>(itemView) {
|
||||
private val avatar: AvatarImageView = itemView.findViewById<AvatarImageView>(R.id.bio_preference_avatar).apply {
|
||||
ViewCompat.setTransitionName(this, "avatar")
|
||||
}
|
||||
|
||||
override fun bind(model: Model) {
|
||||
avatar.setAvatar(model.recipient)
|
||||
avatar.disableQuickContact()
|
||||
avatar.setOnClickListener { model.onAvatarClick(avatar) }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
package org.thoughtcrime.securesms.components.settings.conversation.preferences
|
||||
|
||||
import android.content.Context
|
||||
import android.view.View
|
||||
import android.widget.TextView
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.components.settings.PreferenceModel
|
||||
import org.thoughtcrime.securesms.phonenumbers.PhoneNumberFormatter
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.util.MappingAdapter
|
||||
import org.thoughtcrime.securesms.util.MappingViewHolder
|
||||
|
||||
/**
|
||||
* Renders name, description, about, etc. for a given group or recipient.
|
||||
*/
|
||||
object BioTextPreference {
|
||||
|
||||
fun register(adapter: MappingAdapter) {
|
||||
adapter.registerFactory(RecipientModel::class.java, MappingAdapter.LayoutFactory(::RecipientViewHolder, R.layout.conversation_settings_bio_preference_item))
|
||||
adapter.registerFactory(GroupModel::class.java, MappingAdapter.LayoutFactory(::GroupViewHolder, R.layout.conversation_settings_bio_preference_item))
|
||||
}
|
||||
|
||||
abstract class BioTextPreferenceModel<T : BioTextPreferenceModel<T>> : PreferenceModel<T>() {
|
||||
abstract fun getHeadlineText(context: Context): String
|
||||
abstract fun getSubhead1Text(): String?
|
||||
abstract fun getSubhead2Text(): String?
|
||||
}
|
||||
|
||||
class RecipientModel(
|
||||
private val recipient: Recipient,
|
||||
) : BioTextPreferenceModel<RecipientModel>() {
|
||||
|
||||
override fun getHeadlineText(context: Context): String = recipient.getDisplayNameOrUsername(context)
|
||||
|
||||
override fun getSubhead1Text(): String? = recipient.combinedAboutAndEmoji
|
||||
|
||||
override fun getSubhead2Text(): String? = recipient.e164.transform(PhoneNumberFormatter::prettyPrint).orNull()
|
||||
|
||||
override fun areContentsTheSame(newItem: RecipientModel): Boolean {
|
||||
return super.areContentsTheSame(newItem) && newItem.recipient.hasSameContent(recipient)
|
||||
}
|
||||
|
||||
override fun areItemsTheSame(newItem: RecipientModel): Boolean {
|
||||
return newItem.recipient.id == recipient.id
|
||||
}
|
||||
}
|
||||
|
||||
class GroupModel(
|
||||
val groupTitle: String,
|
||||
val groupMembershipDescription: String?
|
||||
) : BioTextPreferenceModel<GroupModel>() {
|
||||
override fun getHeadlineText(context: Context): String = groupTitle
|
||||
|
||||
override fun getSubhead1Text(): String? = groupMembershipDescription
|
||||
|
||||
override fun getSubhead2Text(): String? = null
|
||||
|
||||
override fun areContentsTheSame(newItem: GroupModel): Boolean {
|
||||
return super.areContentsTheSame(newItem) &&
|
||||
groupTitle == newItem.groupTitle &&
|
||||
groupMembershipDescription == newItem.groupMembershipDescription
|
||||
}
|
||||
|
||||
override fun areItemsTheSame(newItem: GroupModel): Boolean {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
private abstract class BioTextViewHolder<T : BioTextPreferenceModel<T>>(itemView: View) : MappingViewHolder<T>(itemView) {
|
||||
|
||||
private val headline: TextView = itemView.findViewById(R.id.bio_preference_headline)
|
||||
private val subhead1: TextView = itemView.findViewById(R.id.bio_preference_subhead_1)
|
||||
private val subhead2: TextView = itemView.findViewById(R.id.bio_preference_subhead_2)
|
||||
|
||||
override fun bind(model: T) {
|
||||
headline.text = model.getHeadlineText(context)
|
||||
|
||||
model.getSubhead1Text().let {
|
||||
subhead1.text = it
|
||||
subhead1.visibility = if (it == null) View.GONE else View.VISIBLE
|
||||
}
|
||||
|
||||
model.getSubhead2Text().let {
|
||||
subhead2.text = it
|
||||
subhead2.visibility = if (it == null) View.GONE else View.VISIBLE
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class RecipientViewHolder(itemView: View) : BioTextViewHolder<RecipientModel>(itemView)
|
||||
private class GroupViewHolder(itemView: View) : BioTextViewHolder<GroupModel>(itemView)
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
package org.thoughtcrime.securesms.components.settings.conversation.preferences
|
||||
|
||||
import android.view.View
|
||||
import android.widget.ImageView
|
||||
import android.widget.TextView
|
||||
import androidx.appcompat.content.res.AppCompatResources
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.components.settings.DSLSettingsIcon
|
||||
import org.thoughtcrime.securesms.components.settings.PreferenceModel
|
||||
import org.thoughtcrime.securesms.util.MappingAdapter
|
||||
import org.thoughtcrime.securesms.util.MappingViewHolder
|
||||
import org.thoughtcrime.securesms.util.visible
|
||||
|
||||
/**
|
||||
* Renders a configurable strip of buttons
|
||||
*/
|
||||
object ButtonStripPreference {
|
||||
|
||||
fun register(adapter: MappingAdapter) {
|
||||
adapter.registerFactory(Model::class.java, MappingAdapter.LayoutFactory(::ViewHolder, R.layout.conversation_settings_button_strip))
|
||||
}
|
||||
|
||||
class Model(
|
||||
val state: State,
|
||||
val background: DSLSettingsIcon? = null,
|
||||
val onMessageClick: () -> Unit = {},
|
||||
val onVideoClick: () -> Unit = {},
|
||||
val onAudioClick: () -> Unit = {},
|
||||
val onMuteClick: () -> Unit = {},
|
||||
val onSearchClick: () -> Unit = {}
|
||||
) : PreferenceModel<Model>() {
|
||||
override fun areContentsTheSame(newItem: Model): Boolean {
|
||||
return super.areContentsTheSame(newItem) && state == newItem.state
|
||||
}
|
||||
|
||||
override fun areItemsTheSame(newItem: Model): Boolean {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
class ViewHolder(itemView: View) : MappingViewHolder<Model>(itemView) {
|
||||
|
||||
private val message: View = itemView.findViewById(R.id.message)
|
||||
private val messageLabel: View = itemView.findViewById(R.id.message_label)
|
||||
private val videoCall: View = itemView.findViewById(R.id.start_video)
|
||||
private val videoLabel: View = itemView.findViewById(R.id.start_video_label)
|
||||
private val audioCall: ImageView = itemView.findViewById(R.id.start_audio)
|
||||
private val audioLabel: TextView = itemView.findViewById(R.id.start_audio_label)
|
||||
private val mute: ImageView = itemView.findViewById(R.id.mute)
|
||||
private val muteLabel: TextView = itemView.findViewById(R.id.mute_label)
|
||||
private val search: View = itemView.findViewById(R.id.search)
|
||||
private val searchLabel: View = itemView.findViewById(R.id.search_label)
|
||||
|
||||
override fun bind(model: Model) {
|
||||
message.visible = model.state.isMessageAvailable
|
||||
messageLabel.visible = model.state.isMessageAvailable
|
||||
videoCall.visible = model.state.isVideoAvailable
|
||||
videoLabel.visible = model.state.isVideoAvailable
|
||||
audioCall.visible = model.state.isAudioAvailable
|
||||
audioLabel.visible = model.state.isAudioAvailable
|
||||
mute.visible = model.state.isMuteAvailable
|
||||
muteLabel.visible = model.state.isMuteAvailable
|
||||
search.visible = model.state.isSearchAvailable
|
||||
searchLabel.visible = model.state.isSearchAvailable
|
||||
|
||||
if (model.state.isAudioSecure) {
|
||||
audioLabel.setText(R.string.ConversationSettingsFragment__audio)
|
||||
audioCall.setImageDrawable(AppCompatResources.getDrawable(context, R.drawable.ic_phone_right_24))
|
||||
} else {
|
||||
audioLabel.setText(R.string.ConversationSettingsFragment__call)
|
||||
audioCall.setImageDrawable(AppCompatResources.getDrawable(context, R.drawable.ic_phone_right_unlock_primary_accent_24))
|
||||
}
|
||||
|
||||
if (model.state.isMuted) {
|
||||
mute.setImageDrawable(AppCompatResources.getDrawable(context, R.drawable.ic_bell_disabled_24))
|
||||
muteLabel.setText(R.string.ConversationSettingsFragment__muted)
|
||||
} else {
|
||||
mute.setImageDrawable(AppCompatResources.getDrawable(context, R.drawable.ic_bell_24))
|
||||
muteLabel.setText(R.string.ConversationSettingsFragment__mute)
|
||||
}
|
||||
|
||||
if (model.background != null) {
|
||||
listOf(message, videoCall, audioCall, mute, search).forEach {
|
||||
it.background = model.background.resolve(context)
|
||||
}
|
||||
}
|
||||
|
||||
message.setOnClickListener { model.onMessageClick() }
|
||||
videoCall.setOnClickListener { model.onVideoClick() }
|
||||
audioCall.setOnClickListener { model.onAudioClick() }
|
||||
mute.setOnClickListener { model.onMuteClick() }
|
||||
search.setOnClickListener { model.onSearchClick() }
|
||||
}
|
||||
}
|
||||
|
||||
data class State(
|
||||
val isMessageAvailable: Boolean = false,
|
||||
val isVideoAvailable: Boolean = false,
|
||||
val isAudioAvailable: Boolean = false,
|
||||
val isMuteAvailable: Boolean = false,
|
||||
val isSearchAvailable: Boolean = false,
|
||||
val isAudioSecure: Boolean = false,
|
||||
val isMuted: Boolean = false,
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
package org.thoughtcrime.securesms.components.settings.conversation.preferences
|
||||
|
||||
import android.view.View
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.components.emoji.EmojiTextView
|
||||
import org.thoughtcrime.securesms.components.settings.PreferenceModel
|
||||
import org.thoughtcrime.securesms.groups.GroupId
|
||||
import org.thoughtcrime.securesms.groups.v2.GroupDescriptionUtil
|
||||
import org.thoughtcrime.securesms.util.LongClickMovementMethod
|
||||
import org.thoughtcrime.securesms.util.MappingAdapter
|
||||
import org.thoughtcrime.securesms.util.MappingViewHolder
|
||||
|
||||
object GroupDescriptionPreference {
|
||||
|
||||
fun register(adapter: MappingAdapter) {
|
||||
adapter.registerFactory(Model::class.java, MappingAdapter.LayoutFactory(::ViewHolder, R.layout.conversation_settings_group_description_preference))
|
||||
}
|
||||
|
||||
class Model(
|
||||
private val groupId: GroupId,
|
||||
val groupDescription: String?,
|
||||
val descriptionShouldLinkify: Boolean,
|
||||
val canEditGroupAttributes: Boolean,
|
||||
val onEditGroupDescription: () -> Unit,
|
||||
val onViewGroupDescription: () -> Unit
|
||||
) : PreferenceModel<Model>() {
|
||||
override fun areItemsTheSame(newItem: Model): Boolean {
|
||||
return groupId == newItem.groupId
|
||||
}
|
||||
|
||||
override fun areContentsTheSame(newItem: Model): Boolean {
|
||||
return super.areContentsTheSame(newItem) &&
|
||||
groupDescription == newItem.groupDescription &&
|
||||
descriptionShouldLinkify == newItem.descriptionShouldLinkify &&
|
||||
canEditGroupAttributes == newItem.canEditGroupAttributes
|
||||
}
|
||||
}
|
||||
|
||||
class ViewHolder(itemView: View) : MappingViewHolder<Model>(itemView) {
|
||||
|
||||
private val groupDescriptionTextView: EmojiTextView = findViewById(R.id.manage_group_description)
|
||||
|
||||
override fun bind(model: Model) {
|
||||
groupDescriptionTextView.movementMethod = LongClickMovementMethod.getInstance(context)
|
||||
|
||||
if (model.groupDescription.isNullOrEmpty()) {
|
||||
if (model.canEditGroupAttributes) {
|
||||
groupDescriptionTextView.setOverflowText(null)
|
||||
groupDescriptionTextView.setText(R.string.ManageGroupActivity_add_group_description)
|
||||
groupDescriptionTextView.setOnClickListener { model.onEditGroupDescription() }
|
||||
}
|
||||
} else {
|
||||
groupDescriptionTextView.setOnClickListener(null)
|
||||
GroupDescriptionUtil.setText(
|
||||
context,
|
||||
groupDescriptionTextView,
|
||||
model.groupDescription,
|
||||
model.descriptionShouldLinkify
|
||||
) {
|
||||
model.onViewGroupDescription()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
package org.thoughtcrime.securesms.components.settings.conversation.preferences
|
||||
|
||||
import android.view.View
|
||||
import android.widget.TextView
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.components.settings.PreferenceModel
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.util.Base64
|
||||
import org.thoughtcrime.securesms.util.Hex
|
||||
import org.thoughtcrime.securesms.util.MappingAdapter
|
||||
import org.thoughtcrime.securesms.util.MappingViewHolder
|
||||
import java.util.UUID
|
||||
|
||||
object InternalPreference {
|
||||
|
||||
fun register(adapter: MappingAdapter) {
|
||||
adapter.registerFactory(Model::class.java, MappingAdapter.LayoutFactory(::ViewHolder, R.layout.conversation_settings_internal_preference))
|
||||
}
|
||||
|
||||
class Model(
|
||||
private val recipient: Recipient,
|
||||
val onDisableProfileSharingClick: () -> Unit
|
||||
) : PreferenceModel<Model>() {
|
||||
|
||||
val body: String get() {
|
||||
return String.format(
|
||||
"""
|
||||
-- Profile Name --
|
||||
[${recipient.profileName.givenName}] [${recipient.profileName.familyName}]
|
||||
|
||||
-- Profile Sharing --
|
||||
${recipient.isProfileSharing}
|
||||
|
||||
-- Profile Key (Base64) --
|
||||
${recipient.profileKey?.let(Base64::encodeBytes) ?: "None"}
|
||||
|
||||
-- Profile Key (Hex) --
|
||||
${recipient.profileKey?.let(Hex::toStringCondensed) ?: "None"}
|
||||
|
||||
-- Sealed Sender Mode --
|
||||
${recipient.unidentifiedAccessMode}
|
||||
|
||||
-- UUID --
|
||||
${recipient.uuid.transform { obj: UUID -> obj.toString() }.or("None")}
|
||||
|
||||
-- RecipientId --
|
||||
${recipient.id.serialize()}
|
||||
""".trimIndent(),
|
||||
)
|
||||
}
|
||||
|
||||
override fun areItemsTheSame(newItem: Model): Boolean {
|
||||
return recipient == newItem.recipient
|
||||
}
|
||||
}
|
||||
|
||||
private class ViewHolder(itemView: View) : MappingViewHolder<Model>(itemView) {
|
||||
|
||||
private val body: TextView = itemView.findViewById(R.id.internal_preference_body)
|
||||
private val disableProfileSharing: View = itemView.findViewById(R.id.internal_disable_profile_sharing)
|
||||
|
||||
override fun bind(model: Model) {
|
||||
body.text = model.body
|
||||
disableProfileSharing.setOnClickListener { model.onDisableProfileSharingClick() }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package org.thoughtcrime.securesms.components.settings.conversation.preferences
|
||||
|
||||
import android.view.View
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.components.settings.DSLSettingsIcon
|
||||
import org.thoughtcrime.securesms.components.settings.DSLSettingsText
|
||||
import org.thoughtcrime.securesms.components.settings.PreferenceModel
|
||||
import org.thoughtcrime.securesms.components.settings.PreferenceViewHolder
|
||||
import org.thoughtcrime.securesms.util.MappingAdapter
|
||||
|
||||
/**
|
||||
* Renders a preference line item with a larger (40dp) icon
|
||||
*/
|
||||
object LargeIconClickPreference {
|
||||
|
||||
fun register(adapter: MappingAdapter) {
|
||||
adapter.registerFactory(Model::class.java, MappingAdapter.LayoutFactory(::ViewHolder, R.layout.large_icon_preference_item))
|
||||
}
|
||||
|
||||
class Model(
|
||||
override val title: DSLSettingsText?,
|
||||
override val icon: DSLSettingsIcon,
|
||||
val onClick: () -> Unit
|
||||
) : PreferenceModel<Model>()
|
||||
|
||||
private class ViewHolder(itemView: View) : PreferenceViewHolder<Model>(itemView) {
|
||||
override fun bind(model: Model) {
|
||||
super.bind(model)
|
||||
itemView.setOnClickListener { model.onClick() }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
package org.thoughtcrime.securesms.components.settings.conversation.preferences
|
||||
|
||||
import android.view.View
|
||||
import androidx.core.content.ContextCompat
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.components.settings.PreferenceModel
|
||||
import org.thoughtcrime.securesms.util.FeatureFlags
|
||||
import org.thoughtcrime.securesms.util.MappingAdapter
|
||||
import org.thoughtcrime.securesms.util.MappingViewHolder
|
||||
import org.thoughtcrime.securesms.util.views.LearnMoreTextView
|
||||
|
||||
object LegacyGroupPreference {
|
||||
|
||||
fun register(adapter: MappingAdapter) {
|
||||
adapter.registerFactory(Model::class.java, MappingAdapter.LayoutFactory(::ViewHolder, R.layout.conversation_settings_legacy_group_preference))
|
||||
}
|
||||
|
||||
class Model(
|
||||
val state: State,
|
||||
val onLearnMoreClick: () -> Unit,
|
||||
val onUpgradeClick: () -> Unit,
|
||||
val onMmsWarningClick: () -> Unit
|
||||
) : PreferenceModel<Model>() {
|
||||
override fun areItemsTheSame(newItem: Model): Boolean {
|
||||
return state == newItem.state
|
||||
}
|
||||
}
|
||||
|
||||
private class ViewHolder(itemView: View) : MappingViewHolder<Model>(itemView) {
|
||||
|
||||
private val groupInfoText: LearnMoreTextView = findViewById(R.id.manage_group_info_text)
|
||||
|
||||
override fun bind(model: Model) {
|
||||
itemView.visibility = View.VISIBLE
|
||||
|
||||
groupInfoText.setLinkColor(ContextCompat.getColor(context, R.color.signal_text_primary))
|
||||
|
||||
when (model.state) {
|
||||
State.LEARN_MORE -> {
|
||||
groupInfoText.setText(R.string.ManageGroupActivity_legacy_group_learn_more)
|
||||
groupInfoText.setOnLinkClickListener { model.onLearnMoreClick() }
|
||||
groupInfoText.setLearnMoreVisible(true)
|
||||
}
|
||||
State.UPGRADE -> {
|
||||
groupInfoText.setText(R.string.ManageGroupActivity_legacy_group_upgrade)
|
||||
groupInfoText.setOnLinkClickListener { model.onUpgradeClick() }
|
||||
groupInfoText.setLearnMoreVisible(true, R.string.ManageGroupActivity_upgrade_this_group)
|
||||
}
|
||||
State.TOO_LARGE -> {
|
||||
groupInfoText.text = context.getString(R.string.ManageGroupActivity_legacy_group_too_large, FeatureFlags.groupLimits().hardLimit - 1)
|
||||
groupInfoText.setLearnMoreVisible(false)
|
||||
}
|
||||
State.MMS_WARNING -> {
|
||||
groupInfoText.setText(R.string.ManageGroupActivity_this_is_an_insecure_mms_group)
|
||||
groupInfoText.setOnLinkClickListener { model.onMmsWarningClick() }
|
||||
groupInfoText.setLearnMoreVisible(true, R.string.ManageGroupActivity_invite_now)
|
||||
}
|
||||
State.NONE -> itemView.visibility = View.GONE
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum class State {
|
||||
LEARN_MORE,
|
||||
UPGRADE,
|
||||
TOO_LARGE,
|
||||
MMS_WARNING,
|
||||
NONE
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
package org.thoughtcrime.securesms.components.settings.conversation.preferences
|
||||
|
||||
import android.view.View
|
||||
import android.widget.TextView
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.components.AvatarImageView
|
||||
import org.thoughtcrime.securesms.components.settings.PreferenceModel
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.util.MappingAdapter
|
||||
import org.thoughtcrime.securesms.util.MappingViewHolder
|
||||
import org.thoughtcrime.securesms.util.visible
|
||||
|
||||
/**
|
||||
* Renders a Recipient as a row item with an icon, avatar, status, and admin state
|
||||
*/
|
||||
object RecipientPreference {
|
||||
|
||||
fun register(adapter: MappingAdapter) {
|
||||
adapter.registerFactory(Model::class.java, MappingAdapter.LayoutFactory(::ViewHolder, R.layout.group_recipient_list_item))
|
||||
}
|
||||
|
||||
class Model(
|
||||
val recipient: Recipient,
|
||||
val isAdmin: Boolean = false,
|
||||
val onClick: () -> Unit
|
||||
) : PreferenceModel<Model>() {
|
||||
override fun areItemsTheSame(newItem: Model): Boolean {
|
||||
return recipient.id == newItem.recipient.id
|
||||
}
|
||||
|
||||
override fun areContentsTheSame(newItem: Model): Boolean {
|
||||
return super.areContentsTheSame(newItem) &&
|
||||
recipient.hasSameContent(newItem.recipient) &&
|
||||
isAdmin == newItem.isAdmin
|
||||
}
|
||||
}
|
||||
|
||||
private class ViewHolder(itemView: View) : MappingViewHolder<Model>(itemView) {
|
||||
private val avatar: AvatarImageView = itemView.findViewById(R.id.recipient_avatar)
|
||||
private val name: TextView = itemView.findViewById(R.id.recipient_name)
|
||||
private val about: TextView = itemView.findViewById(R.id.recipient_about)
|
||||
private val admin: View = itemView.findViewById(R.id.admin)
|
||||
|
||||
override fun bind(model: Model) {
|
||||
itemView.setOnClickListener { model.onClick() }
|
||||
|
||||
avatar.setRecipient(model.recipient)
|
||||
name.text = if (model.recipient.isSelf) {
|
||||
context.getString(R.string.Recipient_you)
|
||||
} else {
|
||||
model.recipient.getDisplayName(context)
|
||||
}
|
||||
|
||||
val aboutText = model.recipient.combinedAboutAndEmoji
|
||||
if (aboutText.isNullOrEmpty()) {
|
||||
about.visibility = View.GONE
|
||||
} else {
|
||||
about.text = model.recipient.combinedAboutAndEmoji
|
||||
about.visibility = View.VISIBLE
|
||||
}
|
||||
|
||||
admin.visible = model.isAdmin
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
package org.thoughtcrime.securesms.components.settings.conversation.preferences
|
||||
|
||||
import android.database.Cursor
|
||||
import android.view.View
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.components.ThreadPhotoRailView
|
||||
import org.thoughtcrime.securesms.components.settings.PreferenceModel
|
||||
import org.thoughtcrime.securesms.database.MediaDatabase
|
||||
import org.thoughtcrime.securesms.mms.GlideApp
|
||||
import org.thoughtcrime.securesms.util.MappingAdapter
|
||||
import org.thoughtcrime.securesms.util.MappingViewHolder
|
||||
import org.thoughtcrime.securesms.util.ViewUtil
|
||||
|
||||
/**
|
||||
* Renders the shared media photo rail.
|
||||
*/
|
||||
object SharedMediaPreference {
|
||||
|
||||
fun register(adapter: MappingAdapter) {
|
||||
adapter.registerFactory(Model::class.java, MappingAdapter.LayoutFactory(::ViewHolder, R.layout.conversation_settings_shared_media))
|
||||
}
|
||||
|
||||
class Model(
|
||||
val mediaCursor: Cursor,
|
||||
val mediaIds: List<Long>,
|
||||
val onMediaRecordClick: (MediaDatabase.MediaRecord, Boolean) -> Unit
|
||||
) : PreferenceModel<Model>() {
|
||||
override fun areItemsTheSame(newItem: Model): Boolean {
|
||||
return true
|
||||
}
|
||||
|
||||
override fun areContentsTheSame(newItem: Model): Boolean {
|
||||
return super.areContentsTheSame(newItem) &&
|
||||
mediaIds == newItem.mediaIds
|
||||
}
|
||||
}
|
||||
|
||||
private class ViewHolder(itemView: View) : MappingViewHolder<Model>(itemView) {
|
||||
|
||||
private val rail: ThreadPhotoRailView = itemView.findViewById(R.id.rail_view)
|
||||
|
||||
override fun bind(model: Model) {
|
||||
rail.setCursor(GlideApp.with(rail), model.mediaCursor)
|
||||
rail.setListener {
|
||||
model.onMediaRecordClick(it, ViewUtil.isLtr(rail))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package org.thoughtcrime.securesms.components.settings.conversation.preferences
|
||||
|
||||
import android.content.Context
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.util.DateUtils
|
||||
import java.util.Locale
|
||||
|
||||
object Utils {
|
||||
|
||||
fun Long.formatMutedUntil(context: Context): String {
|
||||
return if (this == Long.MAX_VALUE) {
|
||||
context.getString(R.string.ConversationSettingsFragment__conversation_muted_forever)
|
||||
} else {
|
||||
context.getString(
|
||||
R.string.ConversationSettingsFragment__conversation_muted_until_s,
|
||||
DateUtils.getTimeString(context, Locale.getDefault(), this)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
package org.thoughtcrime.securesms.components.settings.conversation.sounds
|
||||
|
||||
import androidx.fragment.app.viewModels
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import org.thoughtcrime.securesms.MuteDialog
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.components.settings.DSLConfiguration
|
||||
import org.thoughtcrime.securesms.components.settings.DSLSettingsAdapter
|
||||
import org.thoughtcrime.securesms.components.settings.DSLSettingsFragment
|
||||
import org.thoughtcrime.securesms.components.settings.DSLSettingsIcon
|
||||
import org.thoughtcrime.securesms.components.settings.DSLSettingsText
|
||||
import org.thoughtcrime.securesms.components.settings.configure
|
||||
import org.thoughtcrime.securesms.components.settings.conversation.preferences.Utils.formatMutedUntil
|
||||
import org.thoughtcrime.securesms.database.RecipientDatabase
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.recipients.ui.notifications.CustomNotificationsDialogFragment
|
||||
|
||||
class SoundsAndNotificationsSettingsFragment : DSLSettingsFragment(
|
||||
titleId = R.string.ConversationSettingsFragment__sounds_and_notifications
|
||||
) {
|
||||
|
||||
private val mentionLabels: Array<String> by lazy {
|
||||
resources.getStringArray(R.array.SoundsAndNotificationsSettingsFragment__mention_labels)
|
||||
}
|
||||
|
||||
private val viewModel: SoundsAndNotificationsSettingsViewModel by viewModels(
|
||||
factoryProducer = {
|
||||
val recipientId = SoundsAndNotificationsSettingsFragmentArgs.fromBundle(requireArguments()).recipientId
|
||||
val repository = SoundsAndNotificationsSettingsRepository(requireContext())
|
||||
|
||||
SoundsAndNotificationsSettingsViewModel.Factory(recipientId, repository)
|
||||
}
|
||||
)
|
||||
|
||||
override fun bindAdapter(adapter: DSLSettingsAdapter) {
|
||||
viewModel.state.observe(viewLifecycleOwner) { state ->
|
||||
if (state.recipientId != Recipient.UNKNOWN.id) {
|
||||
adapter.submitList(getConfiguration(state).toMappingModelList())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun getConfiguration(state: SoundsAndNotificationsSettingsState): DSLConfiguration {
|
||||
return configure {
|
||||
|
||||
val muteSummary = if (state.muteUntil > 0) {
|
||||
state.muteUntil.formatMutedUntil(requireContext())
|
||||
} else {
|
||||
getString(R.string.SoundsAndNotificationsSettingsFragment__not_muted)
|
||||
}
|
||||
|
||||
val muteIcon = if (state.muteUntil > 0) {
|
||||
R.drawable.ic_bell_disabled_24
|
||||
} else {
|
||||
R.drawable.ic_bell_24
|
||||
}
|
||||
|
||||
clickPref(
|
||||
title = DSLSettingsText.from(R.string.SoundsAndNotificationsSettingsFragment__mute_notifications),
|
||||
icon = DSLSettingsIcon.from(muteIcon),
|
||||
summary = DSLSettingsText.from(muteSummary),
|
||||
onClick = {
|
||||
if (state.muteUntil <= 0) {
|
||||
MuteDialog.show(requireContext(), viewModel::setMuteUntil)
|
||||
} else {
|
||||
MaterialAlertDialogBuilder(requireContext())
|
||||
.setMessage(muteSummary)
|
||||
.setPositiveButton(R.string.ConversationSettingsFragment__unmute) { dialog, _ ->
|
||||
viewModel.unmute()
|
||||
dialog.dismiss()
|
||||
}
|
||||
.setNegativeButton(android.R.string.cancel) { dialog, _ -> dialog.dismiss() }
|
||||
.show()
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
if (state.hasMentionsSupport) {
|
||||
val mentionSelection = if (state.mentionSetting == RecipientDatabase.MentionSetting.ALWAYS_NOTIFY) {
|
||||
0
|
||||
} else {
|
||||
1
|
||||
}
|
||||
|
||||
radioListPref(
|
||||
title = DSLSettingsText.from(R.string.SoundsAndNotificationsSettingsFragment__mentions),
|
||||
icon = DSLSettingsIcon.from(R.drawable.ic_at_24),
|
||||
selected = mentionSelection,
|
||||
listItems = mentionLabels,
|
||||
onSelected = {
|
||||
viewModel.setMentionSetting(
|
||||
if (it == 0) {
|
||||
RecipientDatabase.MentionSetting.ALWAYS_NOTIFY
|
||||
} else {
|
||||
RecipientDatabase.MentionSetting.DO_NOT_NOTIFY
|
||||
}
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
val customSoundSummary = if (state.hasCustomNotificationSettings) {
|
||||
R.string.preferences_on
|
||||
} else {
|
||||
R.string.preferences_off
|
||||
}
|
||||
|
||||
clickPref(
|
||||
title = DSLSettingsText.from(R.string.SoundsAndNotificationsSettingsFragment__custom_notifications),
|
||||
icon = DSLSettingsIcon.from(R.drawable.ic_speaker_24),
|
||||
summary = DSLSettingsText.from(customSoundSummary),
|
||||
onClick = {
|
||||
CustomNotificationsDialogFragment.create(state.recipientId).show(parentFragmentManager, null)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
package org.thoughtcrime.securesms.components.settings.conversation.sounds
|
||||
|
||||
import android.content.Context
|
||||
import org.signal.core.util.concurrent.SignalExecutors
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory
|
||||
import org.thoughtcrime.securesms.database.RecipientDatabase
|
||||
import org.thoughtcrime.securesms.notifications.NotificationChannels
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
|
||||
class SoundsAndNotificationsSettingsRepository(private val context: Context) {
|
||||
|
||||
fun setMuteUntil(recipientId: RecipientId, muteUntil: Long) {
|
||||
SignalExecutors.BOUNDED.execute {
|
||||
DatabaseFactory.getRecipientDatabase(context).setMuted(recipientId, muteUntil)
|
||||
}
|
||||
}
|
||||
|
||||
fun setMentionSetting(recipientId: RecipientId, mentionSetting: RecipientDatabase.MentionSetting) {
|
||||
SignalExecutors.BOUNDED.execute {
|
||||
DatabaseFactory.getRecipientDatabase(context).setMentionSetting(recipientId, mentionSetting)
|
||||
}
|
||||
}
|
||||
|
||||
fun hasCustomNotificationSettings(recipientId: RecipientId, consumer: (Boolean) -> Unit) {
|
||||
SignalExecutors.BOUNDED.execute {
|
||||
val recipient = Recipient.resolved(recipientId)
|
||||
consumer(
|
||||
if (recipient.notificationChannel != null || !NotificationChannels.supported()) {
|
||||
true
|
||||
} else {
|
||||
NotificationChannels.updateWithShortcutBasedChannel(context, recipient)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package org.thoughtcrime.securesms.components.settings.conversation.sounds
|
||||
|
||||
import org.thoughtcrime.securesms.database.RecipientDatabase
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
|
||||
data class SoundsAndNotificationsSettingsState(
|
||||
val recipientId: RecipientId = Recipient.UNKNOWN.id,
|
||||
val muteUntil: Long = 0L,
|
||||
val mentionSetting: RecipientDatabase.MentionSetting = RecipientDatabase.MentionSetting.DO_NOT_NOTIFY,
|
||||
val hasCustomNotificationSettings: Boolean = false,
|
||||
val hasMentionsSupport: Boolean = false
|
||||
)
|
||||
@@ -0,0 +1,51 @@
|
||||
package org.thoughtcrime.securesms.components.settings.conversation.sounds
|
||||
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import org.thoughtcrime.securesms.database.RecipientDatabase
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
import org.thoughtcrime.securesms.util.livedata.Store
|
||||
|
||||
class SoundsAndNotificationsSettingsViewModel(
|
||||
private val recipientId: RecipientId,
|
||||
private val repository: SoundsAndNotificationsSettingsRepository
|
||||
) : ViewModel() {
|
||||
|
||||
private val store = Store(SoundsAndNotificationsSettingsState())
|
||||
|
||||
val state: LiveData<SoundsAndNotificationsSettingsState> = store.stateLiveData
|
||||
|
||||
init {
|
||||
store.update(Recipient.live(recipientId).liveData) { recipient, state ->
|
||||
state.copy(
|
||||
recipientId = recipientId,
|
||||
muteUntil = recipient.muteUntil,
|
||||
mentionSetting = recipient.mentionSetting,
|
||||
hasMentionsSupport = recipient.isPushV2Group
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun setMuteUntil(muteUntil: Long) {
|
||||
repository.setMuteUntil(recipientId, muteUntil)
|
||||
}
|
||||
|
||||
fun unmute() {
|
||||
repository.setMuteUntil(recipientId, 0L)
|
||||
}
|
||||
|
||||
fun setMentionSetting(mentionSetting: RecipientDatabase.MentionSetting) {
|
||||
repository.setMentionSetting(recipientId, mentionSetting)
|
||||
}
|
||||
|
||||
class Factory(
|
||||
private val recipientId: RecipientId,
|
||||
private val repository: SoundsAndNotificationsSettingsRepository
|
||||
) : ViewModelProvider.Factory {
|
||||
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
|
||||
return requireNotNull(modelClass.cast(SoundsAndNotificationsSettingsViewModel(recipientId, repository)))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,13 +1,10 @@
|
||||
package org.thoughtcrime.securesms.components.settings
|
||||
|
||||
import androidx.annotation.CallSuper
|
||||
import androidx.annotation.DrawableRes
|
||||
import androidx.annotation.StringRes
|
||||
import org.thoughtcrime.securesms.util.MappingModel
|
||||
import org.thoughtcrime.securesms.util.MappingModelList
|
||||
|
||||
private const val UNSET = -1
|
||||
|
||||
fun configure(init: DSLConfiguration.() -> Unit): DSLConfiguration {
|
||||
val configuration = DSLConfiguration()
|
||||
configuration.init()
|
||||
@@ -23,13 +20,24 @@ class DSLConfiguration {
|
||||
|
||||
fun radioListPref(
|
||||
title: DSLSettingsText,
|
||||
@DrawableRes iconId: Int = UNSET,
|
||||
icon: DSLSettingsIcon? = null,
|
||||
dialogTitle: DSLSettingsText = title,
|
||||
isEnabled: Boolean = true,
|
||||
listItems: Array<String>,
|
||||
selected: Int,
|
||||
confirmAction: Boolean = false,
|
||||
onSelected: (Int) -> Unit
|
||||
) {
|
||||
val preference = RadioListPreference(title, iconId, isEnabled, listItems, selected, onSelected)
|
||||
val preference = RadioListPreference(
|
||||
title = title,
|
||||
icon = icon,
|
||||
isEnabled = isEnabled,
|
||||
dialogTitle = dialogTitle,
|
||||
listItems = listItems,
|
||||
selected = selected,
|
||||
confirmAction = confirmAction,
|
||||
onSelected = onSelected
|
||||
)
|
||||
children.add(preference)
|
||||
}
|
||||
|
||||
@@ -47,12 +55,12 @@ class DSLConfiguration {
|
||||
fun switchPref(
|
||||
title: DSLSettingsText,
|
||||
summary: DSLSettingsText? = null,
|
||||
@DrawableRes iconId: Int = UNSET,
|
||||
icon: DSLSettingsIcon? = null,
|
||||
isEnabled: Boolean = true,
|
||||
isChecked: Boolean,
|
||||
onClick: () -> Unit
|
||||
) {
|
||||
val preference = SwitchPreference(title, summary, iconId, isEnabled, isChecked, onClick)
|
||||
val preference = SwitchPreference(title, summary, icon, isEnabled, isChecked, onClick)
|
||||
children.add(preference)
|
||||
}
|
||||
|
||||
@@ -70,20 +78,20 @@ class DSLConfiguration {
|
||||
fun clickPref(
|
||||
title: DSLSettingsText,
|
||||
summary: DSLSettingsText? = null,
|
||||
@DrawableRes iconId: Int = UNSET,
|
||||
icon: DSLSettingsIcon? = null,
|
||||
isEnabled: Boolean = true,
|
||||
onClick: () -> Unit
|
||||
) {
|
||||
val preference = ClickPreference(title, summary, iconId, isEnabled, onClick)
|
||||
val preference = ClickPreference(title, summary, icon, isEnabled, onClick)
|
||||
children.add(preference)
|
||||
}
|
||||
|
||||
fun externalLinkPref(
|
||||
title: DSLSettingsText,
|
||||
@DrawableRes iconId: Int = UNSET,
|
||||
icon: DSLSettingsIcon? = null,
|
||||
@StringRes linkId: Int
|
||||
) {
|
||||
val preference = ExternalLinkPreference(title, iconId, linkId)
|
||||
val preference = ExternalLinkPreference(title, icon, linkId)
|
||||
children.add(preference)
|
||||
}
|
||||
|
||||
@@ -116,8 +124,8 @@ class DSLConfiguration {
|
||||
abstract class PreferenceModel<T : PreferenceModel<T>>(
|
||||
open val title: DSLSettingsText? = null,
|
||||
open val summary: DSLSettingsText? = null,
|
||||
@DrawableRes open val iconId: Int = UNSET,
|
||||
open val isEnabled: Boolean = true
|
||||
open val icon: DSLSettingsIcon? = null,
|
||||
open val isEnabled: Boolean = true,
|
||||
) : MappingModel<T> {
|
||||
override fun areItemsTheSame(newItem: T): Boolean {
|
||||
return when {
|
||||
@@ -131,7 +139,7 @@ abstract class PreferenceModel<T : PreferenceModel<T>>(
|
||||
override fun areContentsTheSame(newItem: T): Boolean {
|
||||
return areItemsTheSame(newItem) &&
|
||||
newItem.summary == summary &&
|
||||
newItem.iconId == iconId &&
|
||||
newItem.icon == icon &&
|
||||
newItem.isEnabled == isEnabled
|
||||
}
|
||||
}
|
||||
@@ -147,12 +155,14 @@ class DividerPreference : PreferenceModel<DividerPreference>() {
|
||||
|
||||
class RadioListPreference(
|
||||
override val title: DSLSettingsText,
|
||||
@DrawableRes override val iconId: Int = UNSET,
|
||||
override val icon: DSLSettingsIcon? = null,
|
||||
override val isEnabled: Boolean,
|
||||
val dialogTitle: DSLSettingsText = title,
|
||||
val listItems: Array<String>,
|
||||
val selected: Int,
|
||||
val onSelected: (Int) -> Unit
|
||||
) : PreferenceModel<RadioListPreference>(title = title, iconId = iconId, isEnabled = isEnabled) {
|
||||
val onSelected: (Int) -> Unit,
|
||||
val confirmAction: Boolean = false
|
||||
) : PreferenceModel<RadioListPreference>() {
|
||||
|
||||
override fun areContentsTheSame(newItem: RadioListPreference): Boolean {
|
||||
return super.areContentsTheSame(newItem) && listItems.contentEquals(newItem.listItems) && selected == newItem.selected
|
||||
@@ -176,11 +186,11 @@ class MultiSelectListPreference(
|
||||
class SwitchPreference(
|
||||
override val title: DSLSettingsText,
|
||||
override val summary: DSLSettingsText? = null,
|
||||
@DrawableRes override val iconId: Int = UNSET,
|
||||
isEnabled: Boolean,
|
||||
override val icon: DSLSettingsIcon? = null,
|
||||
override val isEnabled: Boolean,
|
||||
val isChecked: Boolean,
|
||||
val onClick: () -> Unit
|
||||
) : PreferenceModel<SwitchPreference>(title = title, summary = summary, iconId = iconId, isEnabled = isEnabled) {
|
||||
) : PreferenceModel<SwitchPreference>() {
|
||||
override fun areContentsTheSame(newItem: SwitchPreference): Boolean {
|
||||
return super.areContentsTheSame(newItem) && isChecked == newItem.isChecked
|
||||
}
|
||||
@@ -201,15 +211,15 @@ class RadioPreference(
|
||||
class ClickPreference(
|
||||
override val title: DSLSettingsText,
|
||||
override val summary: DSLSettingsText? = null,
|
||||
@DrawableRes override val iconId: Int = UNSET,
|
||||
isEnabled: Boolean = true,
|
||||
override val icon: DSLSettingsIcon? = null,
|
||||
override val isEnabled: Boolean = true,
|
||||
val onClick: () -> Unit
|
||||
) : PreferenceModel<ClickPreference>(title = title, summary = summary, iconId = iconId, isEnabled = isEnabled)
|
||||
) : PreferenceModel<ClickPreference>()
|
||||
|
||||
class ExternalLinkPreference(
|
||||
override val title: DSLSettingsText,
|
||||
@DrawableRes override val iconId: Int,
|
||||
override val icon: DSLSettingsIcon?,
|
||||
@StringRes val linkId: Int
|
||||
) : PreferenceModel<ExternalLinkPreference>(title = title, iconId = iconId)
|
||||
) : PreferenceModel<ExternalLinkPreference>()
|
||||
|
||||
class SectionHeaderPreference(override val title: DSLSettingsText) : PreferenceModel<SectionHeaderPreference>(title = title)
|
||||
class SectionHeaderPreference(override val title: DSLSettingsText) : PreferenceModel<SectionHeaderPreference>()
|
||||
|
||||
@@ -27,12 +27,13 @@ import java.util.Objects;
|
||||
*/
|
||||
class VoiceNoteMediaDescriptionCompatFactory {
|
||||
|
||||
public static final String EXTRA_MESSAGE_POSITION = "voice.note.extra.MESSAGE_POSITION";
|
||||
public static final String EXTRA_THREAD_RECIPIENT_ID = "voice.note.extra.RECIPIENT_ID";
|
||||
public static final String EXTRA_AVATAR_RECIPIENT_ID = "voice.note.extra.SENDER_ID";
|
||||
public static final String EXTRA_THREAD_ID = "voice.note.extra.THREAD_ID";
|
||||
public static final String EXTRA_COLOR = "voice.note.extra.COLOR";
|
||||
public static final String EXTRA_MESSAGE_ID = "voice.note.extra.MESSAGE_ID";
|
||||
public static final String EXTRA_MESSAGE_POSITION = "voice.note.extra.MESSAGE_POSITION";
|
||||
public static final String EXTRA_THREAD_RECIPIENT_ID = "voice.note.extra.RECIPIENT_ID";
|
||||
public static final String EXTRA_AVATAR_RECIPIENT_ID = "voice.note.extra.AVATAR_ID";
|
||||
public static final String EXTRA_INDIVIDUAL_RECIPIENT_ID = "voice.note.extras.INDIVIDUAL_ID";
|
||||
public static final String EXTRA_THREAD_ID = "voice.note.extra.THREAD_ID";
|
||||
public static final String EXTRA_COLOR = "voice.note.extra.COLOR";
|
||||
public static final String EXTRA_MESSAGE_ID = "voice.note.extra.MESSAGE_ID";
|
||||
|
||||
private static final String TAG = Log.tag(VoiceNoteMediaDescriptionCompatFactory.class);
|
||||
|
||||
@@ -63,6 +64,7 @@ class VoiceNoteMediaDescriptionCompatFactory {
|
||||
Bundle extras = new Bundle();
|
||||
extras.putString(EXTRA_THREAD_RECIPIENT_ID, threadRecipient.getId().serialize());
|
||||
extras.putString(EXTRA_AVATAR_RECIPIENT_ID, avatarRecipient.getId().serialize());
|
||||
extras.putString(EXTRA_INDIVIDUAL_RECIPIENT_ID, sender.getId().serialize());
|
||||
extras.putLong(EXTRA_MESSAGE_POSITION, startingPosition);
|
||||
extras.putLong(EXTRA_THREAD_ID, messageRecord.getThreadId());
|
||||
extras.putLong(EXTRA_COLOR, threadRecipient.getChatColors().asSingleColor());
|
||||
|
||||
@@ -205,8 +205,8 @@ public class VoiceNotePlaybackService extends MediaBrowserServiceCompat {
|
||||
private void sendViewedReceiptForCurrentWindowIndex() {
|
||||
if (player.getPlaybackState() == Player.STATE_READY &&
|
||||
player.getPlayWhenReady() &&
|
||||
player.getCurrentWindowIndex() != C.INDEX_UNSET &&
|
||||
FeatureFlags.sendViewedReceipts()) {
|
||||
player.getCurrentWindowIndex() != C.INDEX_UNSET)
|
||||
{
|
||||
|
||||
final MediaDescriptionCompat descriptionCompat = queueDataAdapter.getMediaDescription(player.getCurrentWindowIndex());
|
||||
|
||||
@@ -217,7 +217,7 @@ public class VoiceNotePlaybackService extends MediaBrowserServiceCompat {
|
||||
SignalExecutors.BOUNDED.execute(() -> {
|
||||
Bundle extras = descriptionCompat.getExtras();
|
||||
long messageId = extras.getLong(VoiceNoteMediaDescriptionCompatFactory.EXTRA_MESSAGE_ID);
|
||||
RecipientId recipientId = RecipientId.from(extras.getString(VoiceNoteMediaDescriptionCompatFactory.EXTRA_THREAD_RECIPIENT_ID));
|
||||
RecipientId recipientId = RecipientId.from(extras.getString(VoiceNoteMediaDescriptionCompatFactory.EXTRA_INDIVIDUAL_RECIPIENT_ID));
|
||||
MessageDatabase messageDatabase = DatabaseFactory.getMmsDatabase(this);
|
||||
|
||||
MessageDatabase.MarkedMessageInfo markedMessageInfo = messageDatabase.setIncomingMessageViewed(messageId);
|
||||
|
||||
@@ -42,6 +42,7 @@ import org.thoughtcrime.securesms.database.RecipientDatabase;
|
||||
import org.thoughtcrime.securesms.phonenumbers.PhoneNumberFormatter;
|
||||
import org.thoughtcrime.securesms.profiles.ProfileName;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
import org.thoughtcrime.securesms.util.CursorUtil;
|
||||
import org.thoughtcrime.securesms.util.SqlUtil;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
@@ -179,41 +180,6 @@ public class ContactAccessor {
|
||||
return contactData;
|
||||
}
|
||||
|
||||
public List<String> getNumbersForThreadSearchFilter(Context context, String constraint) {
|
||||
LinkedList<String> numberList = new LinkedList<>();
|
||||
|
||||
try (Cursor cursor = DatabaseFactory.getRecipientDatabase(context).queryAllContacts(constraint)) {
|
||||
while (cursor != null && cursor.moveToNext()) {
|
||||
String phone = cursor.getString(cursor.getColumnIndexOrThrow(RecipientDatabase.PHONE));
|
||||
String email = cursor.getString(cursor.getColumnIndexOrThrow(RecipientDatabase.EMAIL));
|
||||
|
||||
numberList.add(Util.getFirstNonEmpty(phone, email));
|
||||
}
|
||||
}
|
||||
|
||||
GroupDatabase.Reader reader = null;
|
||||
GroupRecord record;
|
||||
|
||||
try {
|
||||
reader = DatabaseFactory.getGroupDatabase(context).getGroupsFilteredByTitle(constraint, true, false);
|
||||
|
||||
while ((record = reader.getNext()) != null) {
|
||||
numberList.add(record.getId().toString());
|
||||
}
|
||||
} finally {
|
||||
if (reader != null)
|
||||
reader.close();
|
||||
}
|
||||
|
||||
if (context.getString(R.string.note_to_self).toLowerCase().contains(constraint.toLowerCase()) &&
|
||||
!numberList.contains(TextSecurePreferences.getLocalNumber(context)))
|
||||
{
|
||||
numberList.add(TextSecurePreferences.getLocalNumber(context));
|
||||
}
|
||||
|
||||
return numberList;
|
||||
}
|
||||
|
||||
public CharSequence phoneTypeToString(Context mContext, int type, CharSequence label) {
|
||||
return Phone.getTypeLabel(mContext.getResources(), type, label);
|
||||
}
|
||||
|
||||
@@ -38,6 +38,10 @@ public class ContactSelectionListItem extends LinearLayout implements RecipientF
|
||||
private String number;
|
||||
private String chipName;
|
||||
private int contactType;
|
||||
private String contactName;
|
||||
private String contactNumber;
|
||||
private String contactLabel;
|
||||
private String contactAbout;
|
||||
private LiveRecipient recipient;
|
||||
private GlideRequests glideRequests;
|
||||
|
||||
@@ -74,6 +78,10 @@ public class ContactSelectionListItem extends LinearLayout implements RecipientF
|
||||
this.glideRequests = glideRequests;
|
||||
this.number = number;
|
||||
this.contactType = type;
|
||||
this.contactName = name;
|
||||
this.contactNumber = number;
|
||||
this.contactLabel = label;
|
||||
this.contactAbout = about;
|
||||
|
||||
if (type == ContactRepository.NEW_PHONE_TYPE || type == ContactRepository.NEW_USERNAME_TYPE) {
|
||||
this.recipient = null;
|
||||
@@ -88,9 +96,13 @@ public class ContactSelectionListItem extends LinearLayout implements RecipientF
|
||||
|
||||
this.nameView.setTextColor(color);
|
||||
this.numberView.setTextColor(color);
|
||||
this.contactPhotoImage.setAvatar(glideRequests, recipientSnapshot, false);
|
||||
|
||||
setText(recipientSnapshot, type, name, number, label, about);
|
||||
if (recipientSnapshot == null || recipientSnapshot.isResolving()) {
|
||||
this.contactPhotoImage.setAvatar(glideRequests, null, false);
|
||||
setText(null, type, name, number, label, about);
|
||||
} else {
|
||||
this.contactPhotoImage.setAvatar(glideRequests, recipientSnapshot, false);
|
||||
setText(recipientSnapshot, type, name, number, label, about);
|
||||
}
|
||||
|
||||
this.checkBox.setVisibility(checkboxVisible ? View.VISIBLE : View.GONE);
|
||||
}
|
||||
@@ -177,10 +189,11 @@ public class ContactSelectionListItem extends LinearLayout implements RecipientF
|
||||
|
||||
@Override
|
||||
public void onRecipientChanged(@NonNull Recipient recipient) {
|
||||
contactPhotoImage.setAvatar(glideRequests, recipient, false);
|
||||
nameView.setText(recipient);
|
||||
if (recipient.isGroup()) {
|
||||
numberView.setText(getGroupMemberCount(recipient));
|
||||
if (this.recipient != null && this.recipient.getId().equals(recipient.getId())) {
|
||||
contactPhotoImage.setAvatar(glideRequests, recipient, false);
|
||||
setText(recipient, contactType, contactName, contactNumber, contactLabel, contactAbout);
|
||||
} else {
|
||||
Log.w(TAG, "Bad change! Local recipient doesn't match. Ignoring. Local: " + (this.recipient == null ? "null" : this.recipient.getId()) + ", Changed: " + recipient.getId());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -213,7 +213,7 @@ public class ContactsCursorLoader extends AbstractContactsCursorLoader {
|
||||
|
||||
private Cursor getGroupsCursor() {
|
||||
MatrixCursor groupContacts = ContactsCursorRows.createMatrixCursor();
|
||||
try (GroupDatabase.Reader reader = DatabaseFactory.getGroupDatabase(getContext()).getGroupsFilteredByTitle(getFilter(), flagSet(mode, DisplayMode.FLAG_INACTIVE_GROUPS), hideGroupsV1(mode))) {
|
||||
try (GroupDatabase.Reader reader = DatabaseFactory.getGroupDatabase(getContext()).getGroupsFilteredByTitle(getFilter(), flagSet(mode, DisplayMode.FLAG_INACTIVE_GROUPS), hideGroupsV1(mode), !smsEnabled(mode))) {
|
||||
GroupDatabase.GroupRecord groupRecord;
|
||||
while ((groupRecord = reader.getNext()) != null) {
|
||||
groupContacts.addRow(ContactsCursorRows.forGroup(groupRecord));
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
package org.thoughtcrime.securesms.conversation;
|
||||
|
||||
import android.app.Dialog;
|
||||
import android.os.Bundle;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.fragment.app.DialogFragment;
|
||||
import androidx.fragment.app.FragmentManager;
|
||||
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
|
||||
|
||||
import org.signal.core.util.logging.Log;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.database.model.MessageRecord;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
|
||||
/**
|
||||
* A dialog fragment that shows when you click 'learn more' on a {@link MessageRecord#isBadDecryptType()}.
|
||||
*/
|
||||
public final class BadDecryptLearnMoreDialog extends DialogFragment {
|
||||
|
||||
private static final String TAG = Log.tag(BadDecryptLearnMoreDialog.class);
|
||||
private static final String FRAGMENT_TAG = "BadDecryptLearnMoreDialog";
|
||||
|
||||
private static final String KEY_DISPLAY_NAME = "display_name";
|
||||
private static final String KEY_GROUP_CHAT = "group_chat";
|
||||
|
||||
public static void show(@NonNull FragmentManager fragmentManager, @NonNull String displayName, boolean isGroupChat) {
|
||||
if (fragmentManager.findFragmentByTag(FRAGMENT_TAG) != null) {
|
||||
Log.i(TAG, "Already shown!");
|
||||
return;
|
||||
}
|
||||
|
||||
Bundle args = new Bundle();
|
||||
args.putString(KEY_DISPLAY_NAME, displayName);
|
||||
args.putBoolean(KEY_GROUP_CHAT, isGroupChat);
|
||||
|
||||
BadDecryptLearnMoreDialog fragment = new BadDecryptLearnMoreDialog();
|
||||
fragment.setArguments(args);
|
||||
fragment.show(fragmentManager, FRAGMENT_TAG);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Dialog onCreateDialog(Bundle savedInstanceState) {
|
||||
MaterialAlertDialogBuilder dialogBuilder = new MaterialAlertDialogBuilder(requireContext());
|
||||
|
||||
View view = LayoutInflater.from(requireContext()).inflate(R.layout.bad_decrypt_learn_more_dialog_fragment, null);
|
||||
TextView body = view.findViewById(R.id.bad_decrypt_dialog_body);
|
||||
|
||||
String displayName = requireArguments().getString(KEY_DISPLAY_NAME);
|
||||
boolean isGroup = requireArguments().getBoolean(KEY_GROUP_CHAT);
|
||||
|
||||
if (isGroup) {
|
||||
body.setText(getString(R.string.BadDecryptLearnMoreDialog_couldnt_be_delivered_group, displayName));
|
||||
} else {
|
||||
body.setText(getString(R.string.BadDecryptLearnMoreDialog_couldnt_be_delivered_individual, displayName));
|
||||
}
|
||||
|
||||
dialogBuilder.setView(view)
|
||||
.setPositiveButton(android.R.string.ok, null);
|
||||
|
||||
return dialogBuilder.create();
|
||||
}
|
||||
}
|
||||
@@ -3,8 +3,13 @@ package org.thoughtcrime.securesms.conversation;
|
||||
/**
|
||||
* Activity which encapsulates a conversation for a Bubble window.
|
||||
*
|
||||
* This activity is empty, and exists so that we can override some of its manifest parameters
|
||||
* without clashing with ConversationActivity.
|
||||
* This activity exists so that we can override some of its manifest parameters
|
||||
* without clashing with {@link ConversationActivity} and provide an API-level
|
||||
* independent "is in bubble?" check.
|
||||
*/
|
||||
public class BubbleConversationActivity extends ConversationActivity {
|
||||
@Override
|
||||
protected boolean isInBubble() {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,7 +44,6 @@ import android.text.Spannable;
|
||||
import android.text.SpannableString;
|
||||
import android.text.SpannableStringBuilder;
|
||||
import android.text.TextWatcher;
|
||||
import android.view.Display;
|
||||
import android.view.Gravity;
|
||||
import android.view.KeyEvent;
|
||||
import android.view.Menu;
|
||||
@@ -71,6 +70,7 @@ import androidx.appcompat.app.ActionBar;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.appcompat.widget.SearchView;
|
||||
import androidx.appcompat.widget.Toolbar;
|
||||
import androidx.core.app.ActivityCompat;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import androidx.core.content.pm.ShortcutInfoCompat;
|
||||
import androidx.core.content.pm.ShortcutManagerCompat;
|
||||
@@ -123,13 +123,13 @@ import org.thoughtcrime.securesms.components.identity.UnverifiedBannerView;
|
||||
import org.thoughtcrime.securesms.components.location.SignalPlace;
|
||||
import org.thoughtcrime.securesms.components.mention.MentionAnnotation;
|
||||
import org.thoughtcrime.securesms.components.reminder.ExpiredBuildReminder;
|
||||
import org.thoughtcrime.securesms.components.reminder.GroupsV1MigrationInitiationReminder;
|
||||
import org.thoughtcrime.securesms.components.reminder.GroupsV1MigrationSuggestionsReminder;
|
||||
import org.thoughtcrime.securesms.components.reminder.PendingGroupJoinRequestsReminder;
|
||||
import org.thoughtcrime.securesms.components.reminder.Reminder;
|
||||
import org.thoughtcrime.securesms.components.reminder.ReminderView;
|
||||
import org.thoughtcrime.securesms.components.reminder.ServiceOutageReminder;
|
||||
import org.thoughtcrime.securesms.components.reminder.UnauthorizedReminder;
|
||||
import org.thoughtcrime.securesms.components.settings.conversation.ConversationSettingsActivity;
|
||||
import org.thoughtcrime.securesms.contacts.ContactAccessor;
|
||||
import org.thoughtcrime.securesms.contacts.ContactAccessor.ContactData;
|
||||
import org.thoughtcrime.securesms.contacts.sync.DirectoryHelper;
|
||||
@@ -142,8 +142,7 @@ import org.thoughtcrime.securesms.conversation.ConversationMessage.ConversationM
|
||||
import org.thoughtcrime.securesms.conversation.ui.error.SafetyNumberChangeDialog;
|
||||
import org.thoughtcrime.securesms.conversation.ui.groupcall.GroupCallViewModel;
|
||||
import org.thoughtcrime.securesms.conversation.ui.mentions.MentionsPickerViewModel;
|
||||
import org.thoughtcrime.securesms.conversationlist.model.MessageResult;
|
||||
import org.thoughtcrime.securesms.crypto.DatabaseSessionLock;
|
||||
import org.thoughtcrime.securesms.crypto.ReentrantSessionLock;
|
||||
import org.thoughtcrime.securesms.crypto.SecurityEvent;
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
import org.thoughtcrime.securesms.database.DraftDatabase;
|
||||
@@ -174,7 +173,6 @@ import org.thoughtcrime.securesms.groups.ui.GroupChangeFailureReason;
|
||||
import org.thoughtcrime.securesms.groups.ui.GroupErrors;
|
||||
import org.thoughtcrime.securesms.groups.ui.LeaveGroupDialog;
|
||||
import org.thoughtcrime.securesms.groups.ui.invitesandrequests.ManagePendingAndRequestingMembersActivity;
|
||||
import org.thoughtcrime.securesms.groups.ui.managegroup.ManageGroupActivity;
|
||||
import org.thoughtcrime.securesms.groups.ui.migration.GroupsV1MigrationInitiationBottomSheetDialogFragment;
|
||||
import org.thoughtcrime.securesms.groups.ui.migration.GroupsV1MigrationSuggestionsDialog;
|
||||
import org.thoughtcrime.securesms.insights.InsightsLauncher;
|
||||
@@ -239,8 +237,8 @@ import org.thoughtcrime.securesms.recipients.RecipientFormattingException;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientUtil;
|
||||
import org.thoughtcrime.securesms.recipients.ui.disappearingmessages.RecipientDisappearingMessagesActivity;
|
||||
import org.thoughtcrime.securesms.recipients.ui.managerecipient.ManageRecipientActivity;
|
||||
import org.thoughtcrime.securesms.registration.RegistrationNavigationActivity;
|
||||
import org.thoughtcrime.securesms.search.MessageResult;
|
||||
import org.thoughtcrime.securesms.service.KeyCachingService;
|
||||
import org.thoughtcrime.securesms.sms.MessageSender;
|
||||
import org.thoughtcrime.securesms.sms.OutgoingEncryptedMessage;
|
||||
@@ -338,6 +336,9 @@ public class ConversationActivity extends PassphraseRequiredActivity
|
||||
private static final String TAG = Log.tag(ConversationActivity.class);
|
||||
|
||||
private static final String STATE_REACT_WITH_ANY_PAGE = "STATE_REACT_WITH_ANY_PAGE";
|
||||
private static final String STATE_IS_SEARCH_REQUESTED = "STATE_IS_SEARCH_REQUESTED";
|
||||
|
||||
private static final int REQUEST_CODE_SETTINGS = 1000;
|
||||
|
||||
private static final int PICK_GALLERY = 1;
|
||||
private static final int PICK_DOCUMENT = 2;
|
||||
@@ -400,6 +401,7 @@ public class ConversationActivity extends PassphraseRequiredActivity
|
||||
private ConversationGroupViewModel groupViewModel;
|
||||
private MentionsPickerViewModel mentionsViewModel;
|
||||
private GroupCallViewModel groupCallViewModel;
|
||||
private VoiceRecorderWakeLock voiceRecorderWakeLock;
|
||||
|
||||
private LiveRecipient recipient;
|
||||
private long threadId;
|
||||
@@ -409,6 +411,7 @@ public class ConversationActivity extends PassphraseRequiredActivity
|
||||
private boolean isDefaultSms = true;
|
||||
private boolean isMmsEnabled = true;
|
||||
private boolean isSecurityInitialized = false;
|
||||
private boolean isSearchRequested = false;
|
||||
|
||||
private volatile boolean screenInitialized = false;
|
||||
|
||||
@@ -432,10 +435,14 @@ public class ConversationActivity extends PassphraseRequiredActivity
|
||||
return;
|
||||
}
|
||||
|
||||
voiceRecorderWakeLock = new VoiceRecorderWakeLock(this);
|
||||
|
||||
new FullscreenHelper(this).showSystemUI();
|
||||
|
||||
ConversationIntents.Args args = ConversationIntents.Args.from(getIntent());
|
||||
|
||||
isSearchRequested = args.isWithSearchOpen();
|
||||
|
||||
reportShortcutLaunch(args.getRecipientId());
|
||||
setContentView(R.layout.conversation_activity);
|
||||
|
||||
@@ -499,7 +506,7 @@ public class ConversationActivity extends PassphraseRequiredActivity
|
||||
protected void onNewIntent(Intent intent) {
|
||||
super.onNewIntent(intent);
|
||||
Log.i(TAG, "onNewIntent()");
|
||||
|
||||
|
||||
if (isFinishing()) {
|
||||
Log.w(TAG, "Activity is finishing...");
|
||||
return;
|
||||
@@ -530,7 +537,10 @@ public class ConversationActivity extends PassphraseRequiredActivity
|
||||
|
||||
setIntent(intent);
|
||||
|
||||
viewModel.setArgs(ConversationIntents.Args.from(intent));
|
||||
ConversationIntents.Args args = ConversationIntents.Args.from(intent);
|
||||
isSearchRequested = args.isWithSearchOpen();
|
||||
|
||||
viewModel.setArgs(args);
|
||||
|
||||
reportShortcutLaunch(viewModel.getArgs().getRecipientId());
|
||||
initializeResources(viewModel.getArgs());
|
||||
@@ -546,6 +556,12 @@ public class ConversationActivity extends PassphraseRequiredActivity
|
||||
}
|
||||
|
||||
searchNav.setVisibility(View.GONE);
|
||||
|
||||
if (args.isWithSearchOpen()) {
|
||||
if (searchViewItem != null && searchViewItem.expandActionView()) {
|
||||
searchViewModel.onSearchOpened();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -678,7 +694,7 @@ public class ConversationActivity extends PassphraseRequiredActivity
|
||||
titleView.setTitle(glideRequests, recipientSnapshot);
|
||||
NotificationChannels.updateContactChannelName(this, recipientSnapshot);
|
||||
setBlockedUserState(recipientSnapshot, isSecureText, isDefaultSms);
|
||||
supportInvalidateOptionsMenu();
|
||||
invalidateOptionsMenu();
|
||||
break;
|
||||
case TAKE_PHOTO:
|
||||
handleImageFromDeviceCameraApp();
|
||||
@@ -776,6 +792,7 @@ public class ConversationActivity extends PassphraseRequiredActivity
|
||||
super.onSaveInstanceState(outState);
|
||||
|
||||
outState.putInt(STATE_REACT_WITH_ANY_PAGE, reactWithAnyEmojiStartPage);
|
||||
outState.putBoolean(STATE_IS_SEARCH_REQUESTED, isSearchRequested);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -783,6 +800,7 @@ public class ConversationActivity extends PassphraseRequiredActivity
|
||||
super.onRestoreInstanceState(savedInstanceState);
|
||||
|
||||
reactWithAnyEmojiStartPage = savedInstanceState.getInt(STATE_REACT_WITH_ANY_PAGE, -1);
|
||||
isSearchRequested = savedInstanceState.getBoolean(STATE_IS_SEARCH_REQUESTED, false);
|
||||
}
|
||||
|
||||
private void setVisibleThread(long threadId) {
|
||||
@@ -998,6 +1016,7 @@ public class ConversationActivity extends PassphraseRequiredActivity
|
||||
@Override
|
||||
public boolean onMenuItemActionCollapse(MenuItem item) {
|
||||
searchView.setOnQueryTextListener(null);
|
||||
isSearchRequested = false;
|
||||
searchViewModel.onSearchClosed();
|
||||
searchNav.setVisibility(View.GONE);
|
||||
inputPanel.setVisibility(View.VISIBLE);
|
||||
@@ -1008,10 +1027,23 @@ public class ConversationActivity extends PassphraseRequiredActivity
|
||||
}
|
||||
});
|
||||
|
||||
if (isSearchRequested) {
|
||||
if (searchViewItem.expandActionView()) {
|
||||
searchViewModel.onSearchOpened();
|
||||
}
|
||||
}
|
||||
|
||||
super.onCreateOptionsMenu(menu);
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void invalidateOptionsMenu() {
|
||||
if (!isSearchRequested) {
|
||||
super.invalidateOptionsMenu();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem item) {
|
||||
super.onOptionsItemSelected(item);
|
||||
@@ -1175,8 +1207,10 @@ public class ConversationActivity extends PassphraseRequiredActivity
|
||||
|
||||
if (isInMessageRequest()) return;
|
||||
|
||||
Intent intent = ManageRecipientActivity.newIntentFromConversation(this, recipient.getId());
|
||||
startActivitySceneTransition(intent, titleView.findViewById(R.id.contact_photo_image), "avatar");
|
||||
Intent intent = ConversationSettingsActivity.forRecipient(this, recipient.getId());
|
||||
Bundle bundle = ConversationSettingsActivity.createTransitionBundle(this, titleView.findViewById(R.id.contact_photo_image), toolbar);
|
||||
|
||||
ActivityCompat.startActivity(this, intent, bundle);
|
||||
}
|
||||
|
||||
private void handleUnmuteNotifications() {
|
||||
@@ -1335,9 +1369,10 @@ public class ConversationActivity extends PassphraseRequiredActivity
|
||||
}
|
||||
|
||||
private void handleManageGroup() {
|
||||
startActivityForResult(ManageGroupActivity.newIntent(ConversationActivity.this, recipient.get().requireGroupId()),
|
||||
GROUP_EDIT,
|
||||
ManageGroupActivity.createTransitionBundle(this, titleView.findViewById(R.id.contact_photo_image)));
|
||||
Intent intent = ConversationSettingsActivity.forGroup(this, recipient.get().requireGroupId());
|
||||
Bundle bundle = ConversationSettingsActivity.createTransitionBundle(this, titleView.findViewById(R.id.contact_photo_image), toolbar);
|
||||
|
||||
ActivityCompat.startActivity(this, intent, bundle);
|
||||
}
|
||||
|
||||
private void handleDistributionBroadcastEnabled(MenuItem item) {
|
||||
@@ -1472,6 +1507,7 @@ public class ConversationActivity extends PassphraseRequiredActivity
|
||||
|
||||
@Override
|
||||
public void onSendAnywayAfterSafetyNumberChange(@NonNull List<RecipientId> changedRecipients) {
|
||||
Log.d(TAG, "onSendAnywayAfterSafetyNumberChange");
|
||||
initializeIdentityRecords().addListener(new AssertedSuccessListener<Boolean>() {
|
||||
@Override
|
||||
public void onSuccess(Boolean result) {
|
||||
@@ -1482,6 +1518,7 @@ public class ConversationActivity extends PassphraseRequiredActivity
|
||||
|
||||
@Override
|
||||
public void onMessageResentAfterSafetyNumberChange() {
|
||||
Log.d(TAG, "onMessageResentAfterSafetyNumberChange");
|
||||
initializeIdentityRecords().addListener(new AssertedSuccessListener<Boolean>() {
|
||||
@Override
|
||||
public void onSuccess(Boolean result) { }
|
||||
@@ -1521,7 +1558,7 @@ public class ConversationActivity extends PassphraseRequiredActivity
|
||||
}
|
||||
|
||||
calculateCharactersRemaining();
|
||||
supportInvalidateOptionsMenu();
|
||||
invalidateOptionsMenu();
|
||||
setBlockedUserState(recipient.get(), isSecureText, isDefaultSms);
|
||||
}
|
||||
|
||||
@@ -1649,8 +1686,6 @@ public class ConversationActivity extends PassphraseRequiredActivity
|
||||
private void initializeGroupV1MigrationsBanners() {
|
||||
groupViewModel.getGroupV1MigrationSuggestions()
|
||||
.observe(this, s -> updateReminders());
|
||||
groupViewModel.getShowGroupsV1MigrationBanner()
|
||||
.observe(this, b -> updateReminders());
|
||||
}
|
||||
|
||||
private ListenableFuture<Boolean> initializeDraftFromDatabase() {
|
||||
@@ -1809,7 +1844,6 @@ public class ConversationActivity extends PassphraseRequiredActivity
|
||||
Optional<Reminder> inviteReminder = inviteReminderModel.getReminder();
|
||||
Integer actionableRequestingMembers = groupViewModel.getActionableRequestingMembers().getValue();
|
||||
List<RecipientId> gv1MigrationSuggestions = groupViewModel.getGroupV1MigrationSuggestions().getValue();
|
||||
Boolean gv1MigrationBanner = groupViewModel.getShowGroupsV1MigrationBanner().getValue();
|
||||
|
||||
if (UnauthorizedReminder.isEligible(this)) {
|
||||
reminderView.get().showReminder(new UnauthorizedReminder(this));
|
||||
@@ -1834,15 +1868,6 @@ public class ConversationActivity extends PassphraseRequiredActivity
|
||||
startActivity(ManagePendingAndRequestingMembersActivity.newIntent(this, getRecipient().getGroupId().get().requireV2()));
|
||||
}
|
||||
});
|
||||
} else if (gv1MigrationBanner == Boolean.TRUE && recipient.get().isPushV1Group()) {
|
||||
reminderView.get().showReminder(new GroupsV1MigrationInitiationReminder(this));
|
||||
reminderView.get().setOnActionClickListener(actionId -> {
|
||||
if (actionId == R.id.reminder_action_gv1_initiation_update_group) {
|
||||
GroupsV1MigrationInitiationBottomSheetDialogFragment.showForInitiation(getSupportFragmentManager(), recipient.getId());
|
||||
} else if (actionId == R.id.reminder_action_gv1_initiation_not_now) {
|
||||
groupViewModel.onMigrationInitiationReminderBannerDismissed(recipient.getId());
|
||||
}
|
||||
});
|
||||
} else if (gv1MigrationSuggestions != null && gv1MigrationSuggestions.size() > 0 && recipient.get().isPushV2Group()) {
|
||||
reminderView.get().showReminder(new GroupsV1MigrationSuggestionsReminder(this, gv1MigrationSuggestions));
|
||||
reminderView.get().setOnActionClickListener(actionId -> {
|
||||
@@ -1984,6 +2009,7 @@ public class ConversationActivity extends PassphraseRequiredActivity
|
||||
cancelJoinRequest = findViewById(R.id.conversation_cancel_request);
|
||||
joinGroupCallButton = findViewById(R.id.conversation_group_call_join);
|
||||
|
||||
container.setIsBubble(isInBubble());
|
||||
container.addOnKeyboardShownListener(this);
|
||||
inputPanel.setListener(this);
|
||||
inputPanel.setMediaListener(this);
|
||||
@@ -2090,14 +2116,8 @@ public class ConversationActivity extends PassphraseRequiredActivity
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isInBubble() {
|
||||
if (Build.VERSION.SDK_INT >= ConversationUtil.CONVERSATION_SUPPORT_VERSION) {
|
||||
Display display = getDisplay();
|
||||
|
||||
return display != null && display.getDisplayId() != Display.DEFAULT_DISPLAY;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
protected boolean isInBubble() {
|
||||
return false;
|
||||
}
|
||||
|
||||
private void initializeResources(@NonNull ConversationIntents.Args args) {
|
||||
@@ -2141,7 +2161,7 @@ public class ConversationActivity extends PassphraseRequiredActivity
|
||||
|
||||
if (!result.getResults().isEmpty()) {
|
||||
MessageResult messageResult = result.getResults().get(result.getPosition());
|
||||
fragment.jumpToMessage(messageResult.messageRecipient.getId(), messageResult.receivedTimestampMs, searchViewModel::onMissingResult);
|
||||
fragment.jumpToMessage(messageResult.getMessageRecipient().getId(), messageResult.getReceivedTimestampMs(), searchViewModel::onMissingResult);
|
||||
}
|
||||
|
||||
searchNav.setData(result.getPosition(), result.getResults().size());
|
||||
@@ -2331,7 +2351,7 @@ public class ConversationActivity extends PassphraseRequiredActivity
|
||||
messageRecord.isMms(),
|
||||
oldRecord));
|
||||
} else {
|
||||
reactionDelegate.hideAllButMask();
|
||||
reactionDelegate.hideForReactWithAny();
|
||||
|
||||
ReactWithAnyEmojiBottomSheetDialogFragment.createForMessageRecord(messageRecord, reactWithAnyEmojiStartPage)
|
||||
.show(getSupportFragmentManager(), "BOTTOM");
|
||||
@@ -2343,11 +2363,6 @@ public class ConversationActivity extends PassphraseRequiredActivity
|
||||
reactionDelegate.hideMask();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onReactWithAnyEmojiPageChanged(int page) {
|
||||
reactWithAnyEmojiStartPage = page;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onReactWithAnyEmojiSelected(@NonNull String emoji) {
|
||||
}
|
||||
@@ -3033,12 +3048,14 @@ public class ConversationActivity extends PassphraseRequiredActivity
|
||||
|
||||
@Override
|
||||
public void onRecorderLocked() {
|
||||
voiceRecorderWakeLock.acquire();
|
||||
updateToggleButtonState();
|
||||
setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRecorderFinished() {
|
||||
voiceRecorderWakeLock.release();
|
||||
updateToggleButtonState();
|
||||
Vibrator vibrator = ServiceUtil.getVibrator(this);
|
||||
vibrator.vibrate(20);
|
||||
@@ -3095,6 +3112,7 @@ public class ConversationActivity extends PassphraseRequiredActivity
|
||||
|
||||
@Override
|
||||
public void onRecorderCanceled() {
|
||||
voiceRecorderWakeLock.release();
|
||||
updateToggleButtonState();
|
||||
Vibrator vibrator = ServiceUtil.getVibrator(this);
|
||||
vibrator.vibrate(50);
|
||||
@@ -3768,7 +3786,7 @@ public class ConversationActivity extends PassphraseRequiredActivity
|
||||
new AsyncTask<Void, Void, Void>() {
|
||||
@Override
|
||||
protected Void doInBackground(Void... params) {
|
||||
try (SignalSessionLock.Lock unused = DatabaseSessionLock.INSTANCE.acquire()) {
|
||||
try (SignalSessionLock.Lock unused = ReentrantSessionLock.INSTANCE.acquire()) {
|
||||
for (IdentityRecord identityRecord : unverifiedIdentities) {
|
||||
identityDatabase.setVerified(identityRecord.getRecipientId(),
|
||||
identityRecord.getIdentityKey(),
|
||||
|
||||
@@ -48,12 +48,12 @@ import org.thoughtcrime.securesms.conversation.colors.Colorizer;
|
||||
import org.thoughtcrime.securesms.database.model.MessageRecord;
|
||||
import org.thoughtcrime.securesms.giph.mp4.GiphyMp4Playable;
|
||||
import org.thoughtcrime.securesms.giph.mp4.GiphyMp4PlaybackPolicyEnforcer;
|
||||
import org.thoughtcrime.securesms.util.Projection;
|
||||
import org.thoughtcrime.securesms.mms.GlideRequests;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
import org.thoughtcrime.securesms.util.CachedInflater;
|
||||
import org.thoughtcrime.securesms.util.DateUtils;
|
||||
import org.thoughtcrime.securesms.util.Projection;
|
||||
import org.thoughtcrime.securesms.util.StickyHeaderDecoration;
|
||||
import org.thoughtcrime.securesms.util.ThemeUtil;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
@@ -100,6 +100,8 @@ public class ConversationAdapter
|
||||
private static final int MESSAGE_TYPE_FOOTER = 6;
|
||||
private static final int MESSAGE_TYPE_PLACEHOLDER = 7;
|
||||
|
||||
private static final int PAYLOAD_TIMESTAMP = 0;
|
||||
|
||||
private static final long HEADER_ID = Long.MIN_VALUE;
|
||||
private static final long FOOTER_ID = Long.MIN_VALUE + 1;
|
||||
|
||||
@@ -247,6 +249,24 @@ public class ConversationAdapter
|
||||
}
|
||||
}
|
||||
|
||||
@Override public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position, @NonNull List<Object> payloads) {
|
||||
if (payloads.contains(PAYLOAD_TIMESTAMP)) {
|
||||
switch (getItemViewType(position)) {
|
||||
case MESSAGE_TYPE_INCOMING_TEXT:
|
||||
case MESSAGE_TYPE_INCOMING_MULTIMEDIA:
|
||||
case MESSAGE_TYPE_OUTGOING_TEXT:
|
||||
case MESSAGE_TYPE_OUTGOING_MULTIMEDIA:
|
||||
case MESSAGE_TYPE_UPDATE:
|
||||
ConversationViewHolder conversationViewHolder = (ConversationViewHolder) holder;
|
||||
conversationViewHolder.getBindable().updateTimestamps();
|
||||
default:
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
super.onBindViewHolder(holder, position, payloads);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) {
|
||||
switch (getItemViewType(position)) {
|
||||
@@ -367,7 +387,13 @@ public class ConversationAdapter
|
||||
if (pagingController != null) {
|
||||
pagingController.onDataNeededAroundIndex(correctedPosition);
|
||||
}
|
||||
return super.getItem(correctedPosition);
|
||||
|
||||
if (correctedPosition < getItemCount()) {
|
||||
return super.getItem(correctedPosition);
|
||||
} else {
|
||||
Log.d(TAG, "Could not access corrected position " + correctedPosition + " as it is out of bounds.");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -640,6 +666,10 @@ public class ConversationAdapter
|
||||
}
|
||||
}
|
||||
|
||||
public void updateTimestamps() {
|
||||
notifyItemRangeChanged(0, getItemCount(), PAYLOAD_TIMESTAMP);
|
||||
}
|
||||
|
||||
final static class ConversationViewHolder extends RecyclerView.ViewHolder implements GiphyMp4Playable, Colorizable {
|
||||
public ConversationViewHolder(final @NonNull View itemView) {
|
||||
super(itemView);
|
||||
|
||||
@@ -9,14 +9,17 @@ import com.annimon.stream.Stream;
|
||||
|
||||
import org.signal.core.util.logging.Log;
|
||||
import org.signal.paging.PagedDataSource;
|
||||
import org.thoughtcrime.securesms.attachments.DatabaseAttachment;
|
||||
import org.thoughtcrime.securesms.conversation.ConversationData.MessageRequestData;
|
||||
import org.thoughtcrime.securesms.conversation.ConversationMessage.ConversationMessageFactory;
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
import org.thoughtcrime.securesms.database.MmsSmsDatabase;
|
||||
import org.thoughtcrime.securesms.database.model.InMemoryMessageRecord;
|
||||
import org.thoughtcrime.securesms.database.model.MediaMmsMessageRecord;
|
||||
import org.thoughtcrime.securesms.database.model.Mention;
|
||||
import org.thoughtcrime.securesms.database.model.MessageRecord;
|
||||
import org.thoughtcrime.securesms.util.Stopwatch;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
@@ -24,6 +27,7 @@ import java.util.HashMap;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* Core data source for loading an individual conversation.
|
||||
@@ -58,16 +62,18 @@ class ConversationDataSource implements PagedDataSource<ConversationMessage> {
|
||||
|
||||
@Override
|
||||
public @NonNull List<ConversationMessage> load(int start, int length, @NonNull CancellationSignal cancellationSignal) {
|
||||
Stopwatch stopwatch = new Stopwatch("load(" + start + ", " + length + "), thread " + threadId);
|
||||
MmsSmsDatabase db = DatabaseFactory.getMmsSmsDatabase(context);
|
||||
List<MessageRecord> records = new ArrayList<>(length);
|
||||
MentionHelper mentionHelper = new MentionHelper();
|
||||
Stopwatch stopwatch = new Stopwatch("load(" + start + ", " + length + "), thread " + threadId);
|
||||
MmsSmsDatabase db = DatabaseFactory.getMmsSmsDatabase(context);
|
||||
List<MessageRecord> records = new ArrayList<>(length);
|
||||
MentionHelper mentionHelper = new MentionHelper();
|
||||
AttachmentHelper attachmentHelper = new AttachmentHelper();
|
||||
|
||||
try (MmsSmsDatabase.Reader reader = MmsSmsDatabase.readerFor(db.getConversation(threadId, start, length))) {
|
||||
MessageRecord record;
|
||||
while ((record = reader.getNext()) != null && !cancellationSignal.isCanceled()) {
|
||||
records.add(record);
|
||||
mentionHelper.add(record);
|
||||
attachmentHelper.add(record);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -85,6 +91,14 @@ class ConversationDataSource implements PagedDataSource<ConversationMessage> {
|
||||
|
||||
stopwatch.split("mentions");
|
||||
|
||||
attachmentHelper.fetchAttachments(context);
|
||||
|
||||
stopwatch.split("attachments");
|
||||
|
||||
records = attachmentHelper.buildUpdatedModels(context, records);
|
||||
|
||||
stopwatch.split("attachment-models");
|
||||
|
||||
List<ConversationMessage> messages = Stream.of(records)
|
||||
.map(m -> ConversationMessageFactory.createWithUnresolvedData(context, m, mentionHelper.getMentions(m.getId())))
|
||||
.toList();
|
||||
@@ -114,4 +128,37 @@ class ConversationDataSource implements PagedDataSource<ConversationMessage> {
|
||||
return messageIdToMentions.get(id);
|
||||
}
|
||||
}
|
||||
|
||||
private static class AttachmentHelper {
|
||||
|
||||
private Collection<Long> messageIds = new LinkedList<>();
|
||||
private Map<Long, List<DatabaseAttachment>> messageIdToAttachments = new HashMap<>();
|
||||
|
||||
void add(MessageRecord record) {
|
||||
if (record.isMms()) {
|
||||
messageIds.add(record.getId());
|
||||
}
|
||||
}
|
||||
|
||||
void fetchAttachments(Context context) {
|
||||
messageIdToAttachments = DatabaseFactory.getAttachmentDatabase(context).getAttachmentsForMessages(messageIds);
|
||||
}
|
||||
|
||||
@NonNull List<MessageRecord> buildUpdatedModels(@NonNull Context context, @NonNull List<MessageRecord> records) {
|
||||
return records.stream()
|
||||
.map(record -> {
|
||||
if (record instanceof MediaMmsMessageRecord) {
|
||||
List<DatabaseAttachment> attachments = messageIdToAttachments.get(record.getId());
|
||||
|
||||
if (Util.hasItems(attachments)) {
|
||||
return ((MediaMmsMessageRecord) record).withAttachments(context, attachments);
|
||||
}
|
||||
}
|
||||
|
||||
return record;
|
||||
})
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -221,6 +221,7 @@ public class ConversationFragment extends LoggingFragment {
|
||||
|
||||
private GiphyMp4ProjectionRecycler giphyMp4ProjectionRecycler;
|
||||
private Colorizer colorizer;
|
||||
private ConversationUpdateTick conversationUpdateTick;
|
||||
|
||||
public static void prepare(@NonNull Context context) {
|
||||
FrameLayout parent = new FrameLayout(context);
|
||||
@@ -332,6 +333,9 @@ public class ConversationFragment extends LoggingFragment {
|
||||
}
|
||||
});
|
||||
|
||||
conversationUpdateTick = new ConversationUpdateTick(this::updateConversationItemTimestamps);
|
||||
getViewLifecycleOwner().getLifecycle().addObserver(conversationUpdateTick);
|
||||
|
||||
return view;
|
||||
}
|
||||
|
||||
@@ -387,6 +391,13 @@ public class ConversationFragment extends LoggingFragment {
|
||||
listener.onListVerticalTranslationChanged(list.getTranslationY() - offset);
|
||||
}
|
||||
|
||||
private void updateConversationItemTimestamps() {
|
||||
ConversationAdapter conversationAdapter = getListAdapter();
|
||||
if (conversationAdapter != null) {
|
||||
getListAdapter().updateTimestamps();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onActivityCreated(Bundle bundle) {
|
||||
super.onActivityCreated(bundle);
|
||||
@@ -618,7 +629,7 @@ public class ConversationFragment extends LoggingFragment {
|
||||
|
||||
this.recipient = Recipient.live(conversationViewModel.getArgs().getRecipientId());
|
||||
this.threadId = conversationViewModel.getArgs().getThreadId();
|
||||
this.markReadHelper = new MarkReadHelper(threadId, requireContext());
|
||||
this.markReadHelper = new MarkReadHelper(threadId, requireContext(), getViewLifecycleOwner());
|
||||
|
||||
conversationViewModel.onConversationDataAvailable(recipient.getId(), threadId, startingPosition);
|
||||
messageCountsViewModel.setThreadId(threadId);
|
||||
@@ -795,7 +806,7 @@ public class ConversationFragment extends LoggingFragment {
|
||||
snapToTopDataObserver.requestScrollPosition(0);
|
||||
conversationViewModel.onConversationDataAvailable(recipient.getId(), threadId, -1);
|
||||
messageCountsViewModel.setThreadId(threadId);
|
||||
markReadHelper = new MarkReadHelper(threadId, requireContext());
|
||||
markReadHelper = new MarkReadHelper(threadId, requireContext(), getViewLifecycleOwner());
|
||||
initializeListAdapter();
|
||||
initializeTypingObserver();
|
||||
}
|
||||
@@ -1605,7 +1616,7 @@ public class ConversationFragment extends LoggingFragment {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDecryptionFailedLearnMoreClicked() {
|
||||
public void onChatSessionRefreshLearnMoreClicked() {
|
||||
new AlertDialog.Builder(requireContext())
|
||||
.setView(R.layout.decryption_failed_dialog)
|
||||
.setPositiveButton(android.R.string.ok, (d, w) -> {
|
||||
@@ -1618,6 +1629,13 @@ public class ConversationFragment extends LoggingFragment {
|
||||
.show();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBadDecryptLearnMoreClicked(@NonNull RecipientId author) {
|
||||
SimpleTask.run(getLifecycle(),
|
||||
() -> Recipient.resolved(author).getDisplayName(requireContext()),
|
||||
name -> BadDecryptLearnMoreDialog.show(getParentFragmentManager(), name, recipient.get().isGroup()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSafetyNumberLearnMoreClicked(@NonNull Recipient recipient) {
|
||||
if (recipient.isGroup()) {
|
||||
|
||||
@@ -43,15 +43,12 @@ import java.util.concurrent.TimeUnit;
|
||||
|
||||
final class ConversationGroupViewModel extends ViewModel {
|
||||
|
||||
private static final long GV1_MIGRATION_REMINDER_INTERVAL = TimeUnit.DAYS.toMillis(1);
|
||||
|
||||
private final MutableLiveData<Recipient> liveRecipient;
|
||||
private final LiveData<GroupActiveState> groupActiveState;
|
||||
private final LiveData<GroupDatabase.MemberLevel> selfMembershipLevel;
|
||||
private final LiveData<Integer> actionableRequestingMembers;
|
||||
private final LiveData<ReviewState> reviewState;
|
||||
private final LiveData<List<RecipientId>> gv1MigrationSuggestions;
|
||||
private final LiveData<Boolean> gv1MigrationReminder;
|
||||
|
||||
private boolean firstTimeInviteFriendsTriggered;
|
||||
|
||||
@@ -73,7 +70,6 @@ final class ConversationGroupViewModel extends ViewModel {
|
||||
this.selfMembershipLevel = Transformations.distinctUntilChanged(Transformations.map(groupRecord, ConversationGroupViewModel::mapToSelfMembershipLevel));
|
||||
this.actionableRequestingMembers = Transformations.distinctUntilChanged(Transformations.map(groupRecord, ConversationGroupViewModel::mapToActionableRequestingMemberCount));
|
||||
this.gv1MigrationSuggestions = Transformations.distinctUntilChanged(LiveDataUtil.mapAsync(groupRecord, ConversationGroupViewModel::mapToGroupV1MigrationSuggestions));
|
||||
this.gv1MigrationReminder = Transformations.distinctUntilChanged(LiveDataUtil.mapAsync(groupRecord, ConversationGroupViewModel::mapToGroupV1MigrationReminder));
|
||||
this.reviewState = LiveDataUtil.combineLatest(groupRecord,
|
||||
duplicates,
|
||||
(record, dups) -> dups.isEmpty()
|
||||
@@ -95,13 +91,6 @@ final class ConversationGroupViewModel extends ViewModel {
|
||||
});
|
||||
}
|
||||
|
||||
void onMigrationInitiationReminderBannerDismissed(@NonNull RecipientId recipientId) {
|
||||
SignalExecutors.BOUNDED.execute(() -> {
|
||||
DatabaseFactory.getRecipientDatabase(ApplicationDependencies.getApplication()).markGroupsV1MigrationReminderSeen(recipientId, System.currentTimeMillis());
|
||||
liveRecipient.postValue(liveRecipient.getValue());
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* The number of pending group join requests that can be actioned by this client.
|
||||
*/
|
||||
@@ -125,10 +114,6 @@ final class ConversationGroupViewModel extends ViewModel {
|
||||
return gv1MigrationSuggestions;
|
||||
}
|
||||
|
||||
@NonNull LiveData<Boolean> getShowGroupsV1MigrationBanner() {
|
||||
return gv1MigrationReminder;
|
||||
}
|
||||
|
||||
private static @Nullable GroupRecord getGroupRecordForRecipient(@Nullable Recipient recipient) {
|
||||
if (recipient != null && recipient.isGroup()) {
|
||||
Application context = ApplicationDependencies.getApplication();
|
||||
@@ -188,31 +173,6 @@ final class ConversationGroupViewModel extends ViewModel {
|
||||
.toList();
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
private static boolean mapToGroupV1MigrationReminder(@Nullable GroupRecord record) {
|
||||
if (record == null ||
|
||||
!record.isV1Group() ||
|
||||
!record.isActive() ||
|
||||
FeatureFlags.groupsV1ForcedMigration() ||
|
||||
Recipient.self().getGroupsV1MigrationCapability() != Recipient.Capability.SUPPORTED ||
|
||||
!Recipient.resolved(record.getRecipientId()).isProfileSharing())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
boolean canAutoMigrate = Stream.of(Recipient.resolvedList(record.getMembers()))
|
||||
.allMatch(GroupsV1MigrationUtil::isAutoMigratable);
|
||||
|
||||
if (canAutoMigrate) {
|
||||
return false;
|
||||
}
|
||||
|
||||
Context context = ApplicationDependencies.getApplication();
|
||||
long lastReminderTime = DatabaseFactory.getRecipientDatabase(context).getGroupsV1MigrationReminderLastSeen(record.getRecipientId());
|
||||
|
||||
return System.currentTimeMillis() - lastReminderTime > GV1_MIGRATION_REMINDER_INTERVAL;
|
||||
}
|
||||
|
||||
public static void onCancelJoinRequest(@NonNull Recipient recipient,
|
||||
@NonNull AsynchronousCallback.WorkerThread<Void, GroupChangeFailureReason> callback)
|
||||
{
|
||||
|
||||
@@ -32,6 +32,7 @@ public class ConversationIntents {
|
||||
private static final String EXTRA_DISTRIBUTION_TYPE = "distribution_type";
|
||||
private static final String EXTRA_STARTING_POSITION = "starting_position";
|
||||
private static final String EXTRA_FIRST_TIME_IN_SELF_CREATED_GROUP = "first_time_in_group";
|
||||
private static final String EXTRA_WITH_SEARCH_OPEN = "with_search_open";
|
||||
|
||||
private ConversationIntents() {
|
||||
}
|
||||
@@ -70,6 +71,7 @@ public class ConversationIntents {
|
||||
private final int distributionType;
|
||||
private final int startingPosition;
|
||||
private final boolean firstTimeInSelfCreatedGroup;
|
||||
private final boolean withSearchOpen;
|
||||
|
||||
static Args from(@NonNull Intent intent) {
|
||||
if (isBubbleIntent(intent)) {
|
||||
@@ -81,6 +83,7 @@ public class ConversationIntents {
|
||||
false,
|
||||
ThreadDatabase.DistributionTypes.DEFAULT,
|
||||
-1,
|
||||
false,
|
||||
false);
|
||||
}
|
||||
|
||||
@@ -92,7 +95,8 @@ public class ConversationIntents {
|
||||
intent.getBooleanExtra(EXTRA_BORDERLESS, false),
|
||||
intent.getIntExtra(EXTRA_DISTRIBUTION_TYPE, ThreadDatabase.DistributionTypes.DEFAULT),
|
||||
intent.getIntExtra(EXTRA_STARTING_POSITION, -1),
|
||||
intent.getBooleanExtra(EXTRA_FIRST_TIME_IN_SELF_CREATED_GROUP, false));
|
||||
intent.getBooleanExtra(EXTRA_FIRST_TIME_IN_SELF_CREATED_GROUP, false),
|
||||
intent.getBooleanExtra(EXTRA_WITH_SEARCH_OPEN, false));
|
||||
}
|
||||
|
||||
private Args(@NonNull RecipientId recipientId,
|
||||
@@ -103,7 +107,8 @@ public class ConversationIntents {
|
||||
boolean isBorderless,
|
||||
int distributionType,
|
||||
int startingPosition,
|
||||
boolean firstTimeInSelfCreatedGroup)
|
||||
boolean firstTimeInSelfCreatedGroup,
|
||||
boolean withSearchOpen)
|
||||
{
|
||||
this.recipientId = recipientId;
|
||||
this.threadId = threadId;
|
||||
@@ -114,6 +119,7 @@ public class ConversationIntents {
|
||||
this.distributionType = distributionType;
|
||||
this.startingPosition = startingPosition;
|
||||
this.firstTimeInSelfCreatedGroup = firstTimeInSelfCreatedGroup;
|
||||
this.withSearchOpen = withSearchOpen;
|
||||
}
|
||||
|
||||
public @NonNull RecipientId getRecipientId() {
|
||||
@@ -160,6 +166,10 @@ public class ConversationIntents {
|
||||
public @NonNull ChatColors getChatColors() {
|
||||
return Recipient.resolved(recipientId).getChatColors();
|
||||
}
|
||||
|
||||
public boolean isWithSearchOpen() {
|
||||
return withSearchOpen;
|
||||
}
|
||||
}
|
||||
|
||||
public final static class Builder {
|
||||
@@ -177,6 +187,7 @@ public class ConversationIntents {
|
||||
private Uri dataUri;
|
||||
private String dataType;
|
||||
private boolean firstTimeInSelfCreatedGroup;
|
||||
private boolean withSearchOpen;
|
||||
|
||||
private Builder(@NonNull Context context,
|
||||
@NonNull RecipientId recipientId,
|
||||
@@ -236,6 +247,11 @@ public class ConversationIntents {
|
||||
return this;
|
||||
}
|
||||
|
||||
public @NonNull Builder withSearchOpen(boolean withSearchOpen) {
|
||||
this.withSearchOpen = withSearchOpen;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder firstTimeInSelfCreatedGroup() {
|
||||
this.firstTimeInSelfCreatedGroup = true;
|
||||
return this;
|
||||
@@ -265,6 +281,7 @@ public class ConversationIntents {
|
||||
intent.putExtra(EXTRA_STARTING_POSITION, startingPosition);
|
||||
intent.putExtra(EXTRA_BORDERLESS, isBorderless);
|
||||
intent.putExtra(EXTRA_FIRST_TIME_IN_SELF_CREATED_GROUP, firstTimeInSelfCreatedGroup);
|
||||
intent.putExtra(EXTRA_WITH_SEARCH_OPEN, withSearchOpen);
|
||||
|
||||
if (draftText != null) {
|
||||
intent.putExtra(EXTRA_TEXT, draftText);
|
||||
|
||||
@@ -323,6 +323,11 @@ public final class ConversationItem extends RelativeLayout implements BindableCo
|
||||
setFooter(messageRecord, nextMessageRecord, locale, groupThread, hasWallpaper);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateTimestamps() {
|
||||
getActiveFooter(messageRecord).setMessageRecord(messageRecord, locale);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDetachedFromWindow() {
|
||||
ConversationSwipeAnimationHelper.update(this, 0f, 1f);
|
||||
@@ -462,6 +467,7 @@ public final class ConversationItem extends RelativeLayout implements BindableCo
|
||||
bodyText.setLinkTextColor(colorizer.getOutgoingBodyTextColor(context));
|
||||
footer.setTextColor(colorizer.getOutgoingFooterTextColor(context));
|
||||
footer.setIconColor(colorizer.getOutgoingFooterIconColor(context));
|
||||
footer.setRevealDotColor(colorizer.getOutgoingFooterIconColor(context));
|
||||
footer.setOnlyShowSendingStatus(false, messageRecord);
|
||||
} else if (messageRecord.isRemoteDelete() || (isViewOnceMessage(messageRecord) && ViewOnceUtil.isViewed((MmsMessageRecord) messageRecord))) {
|
||||
if (hasWallpaper) {
|
||||
@@ -469,6 +475,7 @@ public final class ConversationItem extends RelativeLayout implements BindableCo
|
||||
} else {
|
||||
bodyBubble.getBackground().setColorFilter(ContextCompat.getColor(context, R.color.signal_background_primary), PorterDuff.Mode.MULTIPLY);
|
||||
footer.setIconColor(ContextCompat.getColor(context, R.color.signal_icon_tint_secondary));
|
||||
footer.setRevealDotColor(ContextCompat.getColor(context, R.color.signal_icon_tint_secondary));
|
||||
}
|
||||
footer.setTextColor(ContextCompat.getColor(context, R.color.signal_text_secondary));
|
||||
footer.setOnlyShowSendingStatus(messageRecord.isRemoteDelete(), messageRecord);
|
||||
@@ -476,6 +483,7 @@ public final class ConversationItem extends RelativeLayout implements BindableCo
|
||||
bodyBubble.getBackground().setColorFilter(getDefaultBubbleColor(hasWallpaper), PorterDuff.Mode.SRC_IN);
|
||||
footer.setTextColor(ContextCompat.getColor(context, R.color.signal_text_secondary));
|
||||
footer.setIconColor(ContextCompat.getColor(context, R.color.signal_text_secondary));
|
||||
footer.setRevealDotColor(ContextCompat.getColor(context, R.color.signal_text_secondary));
|
||||
footer.setOnlyShowSendingStatus(false, messageRecord);
|
||||
}
|
||||
|
||||
@@ -920,7 +928,13 @@ public final class ConversationItem extends RelativeLayout implements BindableCo
|
||||
thumbnailSlides.get(0) instanceof VideoSlide)
|
||||
{
|
||||
canPlayContent = GiphyMp4PlaybackPolicy.autoplay() || allowedToPlayInline;
|
||||
mediaSource = attachmentMediaSourceFactory.createMediaSource(Objects.requireNonNull(thumbnailSlides.get(0).getUri()));
|
||||
|
||||
Uri uri = thumbnailSlides.get(0).getUri();
|
||||
if (uri != null) {
|
||||
mediaSource = attachmentMediaSourceFactory.createMediaSource(uri);
|
||||
} else {
|
||||
mediaSource = null;
|
||||
}
|
||||
}
|
||||
|
||||
} else {
|
||||
@@ -1105,6 +1119,7 @@ public final class ConversationItem extends RelativeLayout implements BindableCo
|
||||
//noinspection ConstantConditions
|
||||
quoteView.setQuote(glideRequests, quote.getId(), Recipient.live(quote.getAuthor()).get(), quote.getDisplayText(), quote.isOriginalMissing(), quote.getAttachment(), chatColors);
|
||||
quoteView.setVisibility(View.VISIBLE);
|
||||
quoteView.setTextSize(TypedValue.COMPLEX_UNIT_SP, SignalStore.settings().getMessageFontSize());
|
||||
quoteView.getLayoutParams().width = ViewGroup.LayoutParams.WRAP_CONTENT;
|
||||
|
||||
quoteView.setOnClickListener(view -> {
|
||||
@@ -1208,6 +1223,7 @@ public final class ConversationItem extends RelativeLayout implements BindableCo
|
||||
activeFooter.disableBubbleBackground();
|
||||
activeFooter.setTextColor(ContextCompat.getColor(context, R.color.conversation_item_sent_text_secondary_color));
|
||||
activeFooter.setIconColor(ContextCompat.getColor(context, R.color.conversation_item_sent_text_secondary_color));
|
||||
activeFooter.setRevealDotColor(ContextCompat.getColor(context, R.color.conversation_item_sent_text_secondary_color));
|
||||
} else {
|
||||
activeFooter.enableBubbleBackground(R.drawable.wallpaper_bubble_background_tintable_11, getDefaultBubbleColor(hasWallpaper));
|
||||
}
|
||||
@@ -1215,6 +1231,7 @@ public final class ConversationItem extends RelativeLayout implements BindableCo
|
||||
activeFooter.disableBubbleBackground();
|
||||
activeFooter.setTextColor(ContextCompat.getColor(context, R.color.signal_text_secondary));
|
||||
activeFooter.setIconColor(ContextCompat.getColor(context, R.color.signal_icon_tint_secondary));
|
||||
activeFooter.setRevealDotColor(ContextCompat.getColor(context, R.color.signal_icon_tint_secondary));
|
||||
} else {
|
||||
activeFooter.disableBubbleBackground();
|
||||
}
|
||||
@@ -1222,7 +1239,7 @@ public final class ConversationItem extends RelativeLayout implements BindableCo
|
||||
}
|
||||
|
||||
private boolean forceFooter(@NonNull MessageRecord messageRecord) {
|
||||
return FeatureFlags.viewedReceipts() && hasAudio(messageRecord) && messageRecord.getViewedReceiptCount() == 0;
|
||||
return hasAudio(messageRecord) && messageRecord.getViewedReceiptCount() == 0;
|
||||
}
|
||||
|
||||
private ConversationItemFooter getActiveFooter(@NonNull MessageRecord messageRecord) {
|
||||
@@ -1722,6 +1739,7 @@ public final class ConversationItem extends RelativeLayout implements BindableCo
|
||||
intent.putExtra(MediaPreviewActivity.DATE_EXTRA, messageRecord.getTimestamp());
|
||||
intent.putExtra(MediaPreviewActivity.SIZE_EXTRA, slide.asAttachment().getSize());
|
||||
intent.putExtra(MediaPreviewActivity.CAPTION_EXTRA, slide.getCaption().orNull());
|
||||
intent.putExtra(MediaPreviewActivity.IS_VIDEO_GIF, slide.isVideoGif());
|
||||
intent.putExtra(MediaPreviewActivity.LEFT_IS_RECENT_EXTRA, false);
|
||||
|
||||
context.startActivity(intent);
|
||||
|
||||
@@ -3,7 +3,6 @@ package org.thoughtcrime.securesms.conversation;
|
||||
import android.app.Activity;
|
||||
import android.graphics.PointF;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.View;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.appcompat.widget.Toolbar;
|
||||
@@ -55,8 +54,8 @@ final class ConversationReactionDelegate {
|
||||
overlayStub.get().hide();
|
||||
}
|
||||
|
||||
void hideAllButMask() {
|
||||
overlayStub.get().hideAllButMask();
|
||||
void hideForReactWithAny() {
|
||||
overlayStub.get().hideForReactWithAny();
|
||||
}
|
||||
|
||||
void hideMask() {
|
||||
|
||||
@@ -16,7 +16,6 @@ import android.view.animation.DecelerateInterpolator;
|
||||
import android.view.animation.Interpolator;
|
||||
import android.widget.RelativeLayout;
|
||||
|
||||
import androidx.annotation.IdRes;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.widget.Toolbar;
|
||||
@@ -228,8 +227,8 @@ public final class ConversationReactionOverlay extends RelativeLayout {
|
||||
hideInternal(hideAnimatorSet, onHideListener);
|
||||
}
|
||||
|
||||
public void hideAllButMask() {
|
||||
hideInternal(hideAllButMaskAnimatorSet, null);
|
||||
public void hideForReactWithAny() {
|
||||
hideInternal(hideAnimatorSet, null);
|
||||
}
|
||||
|
||||
public void hideMask() {
|
||||
|
||||
@@ -102,6 +102,7 @@ class ConversationRepository {
|
||||
if (SignalStore.settings().getUniversalExpireTimer() != 0 &&
|
||||
conversationRecipient.getExpireMessages() == 0 &&
|
||||
!conversationRecipient.isGroup() &&
|
||||
conversationRecipient.isRegistered() &&
|
||||
(threadId == -1 || !DatabaseFactory.getMmsSmsDatabase(context).hasMeaningfulMessage(threadId)))
|
||||
{
|
||||
showUniversalExpireTimerUpdate = true;
|
||||
|
||||
@@ -8,7 +8,7 @@ import androidx.lifecycle.LiveData;
|
||||
import androidx.lifecycle.MutableLiveData;
|
||||
|
||||
import org.signal.core.util.ThreadUtil;
|
||||
import org.thoughtcrime.securesms.conversationlist.model.MessageResult;
|
||||
import org.thoughtcrime.securesms.search.MessageResult;
|
||||
import org.thoughtcrime.securesms.database.CursorList;
|
||||
import org.thoughtcrime.securesms.search.SearchRepository;
|
||||
import org.thoughtcrime.securesms.util.Debouncer;
|
||||
|
||||
@@ -2,6 +2,10 @@ package org.thoughtcrime.securesms.conversation;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.res.ColorStateList;
|
||||
import android.graphics.Color;
|
||||
import android.graphics.PorterDuff;
|
||||
import android.graphics.PorterDuffColorFilter;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.text.TextUtils;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.View;
|
||||
@@ -25,6 +29,8 @@ import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.util.ExpirationUtil;
|
||||
import org.thoughtcrime.securesms.util.ViewUtil;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
public class ConversationTitleView extends RelativeLayout {
|
||||
|
||||
private AvatarImageView avatar;
|
||||
@@ -78,20 +84,21 @@ public class ConversationTitleView extends RelativeLayout {
|
||||
if (recipient == null) setComposeTitle();
|
||||
else setRecipientTitle(recipient);
|
||||
|
||||
int startDrawable = 0;
|
||||
int endDrawable = 0;
|
||||
Drawable startDrawable = null;
|
||||
Drawable endDrawable = null;
|
||||
|
||||
if (recipient != null && recipient.isBlocked()) {
|
||||
startDrawable = R.drawable.ic_block_white_18dp;
|
||||
startDrawable = ContextCompat.getDrawable(getContext(), R.drawable.ic_block_white_18dp);
|
||||
} else if (recipient != null && recipient.isMuted()) {
|
||||
startDrawable = R.drawable.ic_volume_off_white_18dp;
|
||||
startDrawable = Objects.requireNonNull(ContextCompat.getDrawable(getContext(), R.drawable.ic_bell_disabled_16));
|
||||
startDrawable.setBounds(0, 0, ViewUtil.dpToPx(18), ViewUtil.dpToPx(18));
|
||||
}
|
||||
|
||||
if (recipient != null && recipient.isSystemContact() && !recipient.isSelf()) {
|
||||
endDrawable = R.drawable.ic_profile_circle_outline_16;
|
||||
endDrawable = ContextCompat.getDrawable(getContext(), R.drawable.ic_profile_circle_outline_16);
|
||||
}
|
||||
|
||||
title.setCompoundDrawablesRelativeWithIntrinsicBounds(startDrawable, 0, endDrawable, 0);
|
||||
title.setCompoundDrawablesRelativeWithIntrinsicBounds(startDrawable, null, endDrawable, null);
|
||||
TextViewCompat.setCompoundDrawableTintList(title, ColorStateList.valueOf(ContextCompat.getColor(getContext(), R.color.signal_inverse_transparent_80)));
|
||||
|
||||
if (recipient != null) {
|
||||
|
||||
@@ -292,14 +292,14 @@ public final class ConversationUpdateItem extends FrameLayout
|
||||
eventListener.onGroupMigrationLearnMoreClicked(conversationMessage.getMessageRecord().getGroupV1MigrationMembershipChanges());
|
||||
}
|
||||
});
|
||||
} else if (conversationMessage.getMessageRecord().isFailedDecryptionType() &&
|
||||
(!nextMessageRecord.isPresent() || !nextMessageRecord.get().isFailedDecryptionType()))
|
||||
} else if (conversationMessage.getMessageRecord().isChatSessionRefresh() &&
|
||||
(!nextMessageRecord.isPresent() || !nextMessageRecord.get().isChatSessionRefresh()))
|
||||
{
|
||||
actionButton.setText(R.string.ConversationUpdateItem_learn_more);
|
||||
actionButton.setVisibility(VISIBLE);
|
||||
actionButton.setOnClickListener(v -> {
|
||||
if (batchSelected.isEmpty() && eventListener != null) {
|
||||
eventListener.onDecryptionFailedLearnMoreClicked();
|
||||
eventListener.onChatSessionRefreshLearnMoreClicked();
|
||||
}
|
||||
});
|
||||
} else if (conversationMessage.getMessageRecord().isIdentityUpdate()) {
|
||||
@@ -370,6 +370,16 @@ public final class ConversationUpdateItem extends FrameLayout
|
||||
eventListener.onViewGroupDescriptionChange(conversationRecipient.getGroupId().orNull(), conversationMessage.getMessageRecord().getGroupV2DescriptionUpdate(), isMessageRequestAccepted);
|
||||
}
|
||||
});
|
||||
} else if (conversationMessage.getMessageRecord().isBadDecryptType() &&
|
||||
(!nextMessageRecord.isPresent() || !nextMessageRecord.get().isBadDecryptType()))
|
||||
{
|
||||
actionButton.setText(R.string.ConversationUpdateItem_learn_more);
|
||||
actionButton.setVisibility(VISIBLE);
|
||||
actionButton.setOnClickListener(v -> {
|
||||
if (batchSelected.isEmpty() && eventListener != null) {
|
||||
eventListener.onBadDecryptLearnMoreClicked(conversationMessage.getMessageRecord().getRecipient().getId());
|
||||
}
|
||||
});
|
||||
} else {
|
||||
actionButton.setVisibility(GONE);
|
||||
actionButton.setOnClickListener(null);
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
package org.thoughtcrime.securesms.conversation
|
||||
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import androidx.annotation.VisibleForTesting
|
||||
import androidx.lifecycle.DefaultLifecycleObserver
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
/**
|
||||
* Lifecycle-aware class which will call onTick every 1 minute.
|
||||
* Used to ensure that conversation timestamps are updated appropriately.
|
||||
*/
|
||||
class ConversationUpdateTick(
|
||||
private val onTickListener: OnTickListener
|
||||
) : DefaultLifecycleObserver {
|
||||
|
||||
private val handler = Handler(Looper.getMainLooper())
|
||||
private var isResumed = false
|
||||
|
||||
override fun onResume(owner: LifecycleOwner) {
|
||||
isResumed = true
|
||||
|
||||
handler.removeCallbacksAndMessages(null)
|
||||
onTick()
|
||||
}
|
||||
|
||||
override fun onPause(owner: LifecycleOwner) {
|
||||
isResumed = false
|
||||
|
||||
handler.removeCallbacksAndMessages(null)
|
||||
}
|
||||
|
||||
private fun onTick() {
|
||||
if (isResumed) {
|
||||
onTickListener.onTick()
|
||||
|
||||
handler.removeCallbacksAndMessages(null)
|
||||
handler.postDelayed(this::onTick, TIMEOUT)
|
||||
}
|
||||
}
|
||||
|
||||
interface OnTickListener {
|
||||
fun onTick()
|
||||
}
|
||||
|
||||
companion object {
|
||||
@VisibleForTesting
|
||||
val TIMEOUT = TimeUnit.MINUTES.toMillis(1)
|
||||
}
|
||||
}
|
||||
@@ -44,12 +44,10 @@ import org.whispersystems.libsignal.util.guava.Optional;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
|
||||
public class ConversationViewModel extends ViewModel {
|
||||
|
||||
@@ -224,7 +222,7 @@ public class ConversationViewModel extends ViewModel {
|
||||
return Transformations.map(groupMembers, members -> {
|
||||
List<Recipient> sorted = Stream.of(members)
|
||||
.filter(member -> !Objects.equals(member, Recipient.self()))
|
||||
.sortBy(this::getMemberIdentifier)
|
||||
.sortBy(Recipient::requireStringId)
|
||||
.toList();
|
||||
|
||||
List<NameColor> names = ChatColorsPalette.Names.getAll();
|
||||
@@ -251,13 +249,6 @@ public class ConversationViewModel extends ViewModel {
|
||||
});
|
||||
}
|
||||
|
||||
private @NonNull String getMemberIdentifier(@NonNull Recipient fullMember) {
|
||||
return fullMember.getUuid()
|
||||
.transform(UUID::toString)
|
||||
.or(fullMember.getE164())
|
||||
.or("");
|
||||
}
|
||||
|
||||
long getLastSeen() {
|
||||
return conversationMetadata.getValue() != null ? conversationMetadata.getValue().getLastSeen() : 0;
|
||||
}
|
||||
|
||||
@@ -3,6 +3,8 @@ package org.thoughtcrime.securesms.conversation;
|
||||
import android.content.Context;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.lifecycle.Lifecycle;
|
||||
import androidx.lifecycle.LifecycleOwner;
|
||||
|
||||
import org.signal.core.util.concurrent.SignalExecutors;
|
||||
import org.signal.core.util.logging.Log;
|
||||
@@ -23,18 +25,20 @@ class MarkReadHelper {
|
||||
private static final long DEBOUNCE_TIMEOUT = 100;
|
||||
private static final Executor EXECUTOR = new SerialMonoLifoExecutor(SignalExecutors.BOUNDED);
|
||||
|
||||
private final long threadId;
|
||||
private final Context context;
|
||||
private final Debouncer debouncer = new Debouncer(DEBOUNCE_TIMEOUT);
|
||||
private long latestTimestamp;
|
||||
private final long threadId;
|
||||
private final Context context;
|
||||
private final LifecycleOwner lifecycleOwner;
|
||||
private final Debouncer debouncer = new Debouncer(DEBOUNCE_TIMEOUT);
|
||||
private long latestTimestamp;
|
||||
|
||||
MarkReadHelper(long threadId, @NonNull Context context) {
|
||||
this.threadId = threadId;
|
||||
this.context = context.getApplicationContext();
|
||||
MarkReadHelper(long threadId, @NonNull Context context, @NonNull LifecycleOwner lifecycleOwner) {
|
||||
this.threadId = threadId;
|
||||
this.context = context.getApplicationContext();
|
||||
this.lifecycleOwner = lifecycleOwner;
|
||||
}
|
||||
|
||||
public void onViewsRevealed(long timestamp) {
|
||||
if (timestamp <= latestTimestamp) {
|
||||
if (timestamp <= latestTimestamp || lifecycleOwner.getLifecycle().getCurrentState() != Lifecycle.State.RESUMED) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -134,17 +134,17 @@ final class MenuState {
|
||||
}
|
||||
|
||||
static boolean isActionMessage(@NonNull MessageRecord messageRecord) {
|
||||
return messageRecord.isGroupAction() ||
|
||||
messageRecord.isCallLog() ||
|
||||
messageRecord.isJoined() ||
|
||||
return messageRecord.isGroupAction() ||
|
||||
messageRecord.isCallLog() ||
|
||||
messageRecord.isJoined() ||
|
||||
messageRecord.isExpirationTimerUpdate() ||
|
||||
messageRecord.isEndSession() ||
|
||||
messageRecord.isIdentityUpdate() ||
|
||||
messageRecord.isIdentityVerified() ||
|
||||
messageRecord.isIdentityDefault() ||
|
||||
messageRecord.isProfileChange() ||
|
||||
messageRecord.isEndSession() ||
|
||||
messageRecord.isIdentityUpdate() ||
|
||||
messageRecord.isIdentityVerified() ||
|
||||
messageRecord.isIdentityDefault() ||
|
||||
messageRecord.isProfileChange() ||
|
||||
messageRecord.isGroupV1MigrationEvent() ||
|
||||
messageRecord.isFailedDecryptionType() ||
|
||||
messageRecord.isChatSessionRefresh() ||
|
||||
messageRecord.isInMemoryMessageRecord();
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
package org.thoughtcrime.securesms.conversation
|
||||
|
||||
import android.os.Build
|
||||
import android.os.PowerManager
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.lifecycle.DefaultLifecycleObserver
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import org.thoughtcrime.securesms.util.WakeLockUtil
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
/**
|
||||
* Holds on to and manages a wake-lock for the device proximity sensor.
|
||||
*
|
||||
* This class will register itself as an observe of the given activity's lifecycle and automatically
|
||||
* release the lock if it holds one in onPause
|
||||
*/
|
||||
class VoiceRecorderWakeLock(
|
||||
private val activity: ComponentActivity
|
||||
) : DefaultLifecycleObserver {
|
||||
|
||||
private var wakeLock: PowerManager.WakeLock? = null
|
||||
|
||||
init {
|
||||
activity.lifecycle.addObserver(this)
|
||||
}
|
||||
|
||||
fun acquire() {
|
||||
synchronized(this) {
|
||||
if (wakeLock?.isHeld == true) {
|
||||
return
|
||||
}
|
||||
|
||||
if (Build.VERSION.SDK_INT >= 21) {
|
||||
wakeLock = WakeLockUtil.acquire(activity, PowerManager.PROXIMITY_SCREEN_OFF_WAKE_LOCK, TimeUnit.HOURS.toMillis(1), "voiceRecorder")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun release() {
|
||||
synchronized(this) {
|
||||
if (wakeLock?.isHeld == true) {
|
||||
wakeLock?.release()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onPause(owner: LifecycleOwner) {
|
||||
release()
|
||||
}
|
||||
}
|
||||
@@ -186,19 +186,19 @@ object ChatColorsPalette {
|
||||
@JvmStatic
|
||||
val all = listOf(
|
||||
NameColor(lightColor = 0xFF006DA3.toInt(), darkColor = 0xFF00A7FA.toInt()),
|
||||
NameColor(lightColor = 0xFF007A3D.toInt(), darkColor = 0xFF00B85C.toInt()),
|
||||
NameColor(lightColor = 0xFFC13215.toInt(), darkColor = 0xFFFF6F52.toInt()),
|
||||
NameColor(lightColor = 0xFF067906.toInt(), darkColor = 0xFF0AB80A.toInt()),
|
||||
NameColor(lightColor = 0xFFB814B8.toInt(), darkColor = 0xFFF65AF6.toInt()),
|
||||
NameColor(lightColor = 0xFFC13215.toInt(), darkColor = 0xFFFF6F52.toInt()),
|
||||
NameColor(lightColor = 0xFF5B6976.toInt(), darkColor = 0xFF8BA1B6.toInt()),
|
||||
NameColor(lightColor = 0xFF3D7406.toInt(), darkColor = 0xFF5EB309.toInt()),
|
||||
NameColor(lightColor = 0xFFCC0066.toInt(), darkColor = 0xFFF76EB2.toInt()),
|
||||
NameColor(lightColor = 0xFF2E51FF.toInt(), darkColor = 0xFF8599FF.toInt()),
|
||||
NameColor(lightColor = 0xFF9C5711.toInt(), darkColor = 0xFFD5920B.toInt()),
|
||||
NameColor(lightColor = 0xFF007575.toInt(), darkColor = 0xFF00B2B2.toInt()),
|
||||
NameColor(lightColor = 0xFF9C5711.toInt(), darkColor = 0xFFD5920B.toInt()),
|
||||
NameColor(lightColor = 0xFFD00B4D.toInt(), darkColor = 0xFFFF6B9C.toInt()),
|
||||
NameColor(lightColor = 0xFF8F2AF4.toInt(), darkColor = 0xFFBF80FF.toInt()),
|
||||
NameColor(lightColor = 0xFF3D7406.toInt(), darkColor = 0xFF5EB309.toInt()),
|
||||
NameColor(lightColor = 0xFFD00B0B.toInt(), darkColor = 0xFFFF7070.toInt()),
|
||||
NameColor(lightColor = 0xFF067906.toInt(), darkColor = 0xFF0AB80A.toInt()),
|
||||
NameColor(lightColor = 0xFF007A3D.toInt(), darkColor = 0xFF00B85C.toInt()),
|
||||
NameColor(lightColor = 0xFF5151F6.toInt(), darkColor = 0xFF9494FF.toInt()),
|
||||
NameColor(lightColor = 0xFF866118.toInt(), darkColor = 0xFFD68F00.toInt()),
|
||||
NameColor(lightColor = 0xFF067953.toInt(), darkColor = 0xFF00B87A.toInt()),
|
||||
|
||||
@@ -107,8 +107,8 @@ class ChatColorPreviewView @JvmOverloads constructor(
|
||||
}
|
||||
}
|
||||
|
||||
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
|
||||
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
|
||||
override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
|
||||
super.onLayout(changed, left, top, right, bottom)
|
||||
|
||||
if (chatColors != null) {
|
||||
setChatColors(requireNotNull(chatColors))
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user