Compare commits

...

43 Commits

Author SHA1 Message Date
Greyson Parrelli 5929866ae0 Bump version to 8.16.0 2026-06-17 13:49:25 -04:00
Greyson Parrelli d706fb0c4b Update baseline profile. 2026-06-17 13:26:59 -04:00
Greyson Parrelli f4185d2868 Update translations and other static files. 2026-06-17 13:17:21 -04:00
Greyson Parrelli 9430c27e64 Setup basic compose screenshot testing infra for regV5. 2026-06-17 13:08:36 -04:00
Michelle Tang b724f2b01a Turn on KT. 2026-06-17 13:08:36 -04:00
Greyson Parrelli 1e6d575ec9 Allow a backoffInterval of zero. 2026-06-17 13:08:36 -04:00
Greyson Parrelli 4c7cf5212e Remove the first PIN reminder interval. 2026-06-17 13:08:36 -04:00
Michelle Tang 33ca1132dc Update deleted messages UI. 2026-06-17 13:08:36 -04:00
andrew-signal a5e11abdc9 Bump to libsignal v0.94.5. 2026-06-17 13:08:36 -04:00
Michelle Tang 3924f65cbe Update group update margins. 2026-06-17 13:08:35 -04:00
Greyson Parrelli c500d8ecbd Prevent crash when popping back stack from a detached EnterCodeFragment. 2026-06-17 13:08:35 -04:00
Greyson Parrelli cd98fd894d Prevent crash when parsing an invalid custom donation amount. 2026-06-17 13:08:35 -04:00
Greyson Parrelli 45a3c44d0c Prevent crash when opting out of PIN after fragment is detached. 2026-06-17 13:08:35 -04:00
Greyson Parrelli 8ddec63e31 Make long text selectable. 2026-06-17 13:08:35 -04:00
Greyson Parrelli 2d2a871194 Always render message details bubbles in no-wallpaper mode. 2026-06-17 13:08:35 -04:00
Greyson Parrelli 2ef0032a33 Revert "Manually draw location on google map."
This reverts commit 02d245ac0c.
2026-06-17 13:08:35 -04:00
Greyson Parrelli 570a310e2e Do not show websocket notification for unregistered users. 2026-06-17 13:08:35 -04:00
Greyson Parrelli 930a263174 Don't show remote mute in 1:1 calls. 2026-06-17 13:08:35 -04:00
Greyson Parrelli 7df015ceef Improve registered check for CheckServiceReachabilityJob. 2026-06-17 13:08:35 -04:00
Greyson Parrelli 4c1555bc7b Add more checks to avoid unnecessary websocket connects. 2026-06-17 13:08:35 -04:00
Greyson Parrelli e877f43dde Fix body range bounds validation for long text messages. 2026-06-16 11:03:55 -04:00
Greyson Parrelli 52dcbb8bc6 Add a separate 'internal issues' notification channel. 2026-06-16 09:56:45 -04:00
Alex Hart e0dd576cb1 Update lint baseline. 2026-06-16 09:56:12 -04:00
Alex Hart fb746b1ad5 Add AAPT OSX verification metadata. 2026-06-16 09:56:12 -04:00
Michelle Tang ef35efe34e Update sync msg disappearing timers for calls. 2026-06-16 09:56:12 -04:00
dependabot[bot] 6a30caff87 Bump gradle/actions from 6.1.0 to 6.2.0 2026-06-16 09:56:12 -04:00
Alex Hart cb2816362c Fix crash when group story has more than 100 replies or reactions. 2026-06-16 09:56:12 -04:00
Alex Hart 5f67c9363e Fix back navigation when opening group settings from the chat list. 2026-06-16 09:56:12 -04:00
Alex Hart ba76a8323e Fix back navigation from conversation settings sub-screens popping to the chat. 2026-06-16 09:56:12 -04:00
Alex Hart aa9c7f7d7b Use detail navigation router instead of finishing activity when deleting conversation. 2026-06-16 09:56:12 -04:00
Greyson Parrelli 411a0198b4 Show media preview controls immediately. 2026-06-16 09:56:12 -04:00
Greyson Parrelli 39679ebfc3 Inline useNewLinkifier flag. 2026-06-16 09:56:12 -04:00
Alex Hart 933b799266 Utilize events instead of callbacks in MediaSend feature module. 2026-06-16 09:56:12 -04:00
Cody Henthorne d22a2c0a50 Fix transfer control progress reporting bugs. 2026-06-16 09:56:12 -04:00
Greyson Parrelli 3f682be609 Upgrade AGP to 9.2.1 2026-06-16 09:56:12 -04:00
Greyson Parrelli b16481616a Fix a bunch of lint issues. 2026-06-16 09:56:12 -04:00
Greyson Parrelli d44bef0eda Move backup status operations off the main thread. 2026-06-16 09:56:12 -04:00
Cody Henthorne f02b8001e4 Increase tap area for start/retry download. 2026-06-16 09:56:12 -04:00
Cody Henthorne fa258dcef2 Use indexes for story viewed-receipt lookup and pinned messages queries. 2026-06-16 09:56:12 -04:00
Michelle Tang fc547218d1 Turn on capability for KT username syncs. 2026-06-16 09:56:12 -04:00
Alex Hart eea29813fa Move DatabaseId and AttachmentId to core.models. 2026-06-16 09:56:12 -04:00
Alex Hart 276d71d365 Decouple add message dialog from old view model. 2026-06-16 09:56:12 -04:00
Cody Henthorne 539276673a Fix flakey BackupDeleteJobTest. 2026-06-16 09:56:12 -04:00
373 changed files with 5732 additions and 40082 deletions
+1
View File
@@ -1 +1,2 @@
*.ai binary
**/src/screenshotTest*/reference/**/*.png filter=lfs diff=lfs merge=lfs -text
+3 -2
View File
@@ -20,6 +20,7 @@ jobs:
# gh api repos/actions/checkout/commits/v6 --jq '.sha'
with:
submodules: true
lfs: true
- name: set up JDK 17
uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5
@@ -29,11 +30,11 @@ jobs:
java-version: 17
- name: Validate Gradle Wrapper
uses: gradle/actions/wrapper-validation@50e97c2cd7a37755bbfafc9c5b7cafaece252f6e # v6
uses: gradle/actions/wrapper-validation@3f131e8634966bd73d06cc69884922b02e6faf92 # v6
# gh api repos/gradle/actions/commits/v6 --jq '.sha'
- name: Set up Gradle
uses: gradle/actions/setup-gradle@50e97c2cd7a37755bbfafc9c5b7cafaece252f6e # v6
uses: gradle/actions/setup-gradle@3f131e8634966bd73d06cc69884922b02e6faf92 # v6
# gh api repos/gradle/actions/commits/v6 --jq '.sha'
with:
# Only 8.** branch builds write to the cache; everything else (PRs, etc.) reads only.
+2 -2
View File
@@ -30,7 +30,7 @@ jobs:
java-version: 17
- name: Set up Gradle
uses: gradle/actions/setup-gradle@50e97c2cd7a37755bbfafc9c5b7cafaece252f6e # v6
uses: gradle/actions/setup-gradle@3f131e8634966bd73d06cc69884922b02e6faf92 # v6
# gh api repos/gradle/actions/commits/v6 --jq '.sha'
with:
# PR-only workflow: always read from the cache, never write.
@@ -42,7 +42,7 @@ jobs:
run: echo "y" | ${ANDROID_SDK_ROOT}/cmdline-tools/latest/bin/sdkmanager --install "ndk;${{ env.NDK_VERSION }}"
- name: Validate Gradle Wrapper
uses: gradle/actions/wrapper-validation@50e97c2cd7a37755bbfafc9c5b7cafaece252f6e # v6
uses: gradle/actions/wrapper-validation@3f131e8634966bd73d06cc69884922b02e6faf92 # v6
# gh api repos/gradle/actions/commits/v6 --jq '.sha'
- name: Cache base apk
+2 -2
View File
@@ -27,8 +27,8 @@ plugins {
val staticIps = Properties().apply { file("static-ips.properties").reader().use { load(it) } }
staticIps.stringPropertyNames().forEach { rootProject.extra[it] = staticIps.getProperty(it) }
val canonicalVersionCode = 1707
val canonicalVersionName = "8.15.3"
val canonicalVersionCode = 1708
val canonicalVersionName = "8.16.0"
val currentHotfixVersion = 0
val maxHotfixVersions = 100
+423 -36814
View File
File diff suppressed because one or more lines are too long
@@ -20,6 +20,7 @@ import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.signal.core.models.backup.MediaName
import org.signal.core.models.database.AttachmentId
import org.signal.core.models.media.TransformProperties
import org.signal.core.util.Base64
import org.signal.core.util.Base64.decodeBase64OrThrow
@@ -27,7 +28,6 @@ import org.signal.core.util.copyTo
import org.signal.core.util.stream.NullOutputStream
import org.thoughtcrime.securesms.attachments.ArchivedAttachment
import org.thoughtcrime.securesms.attachments.Attachment
import org.thoughtcrime.securesms.attachments.AttachmentId
import org.thoughtcrime.securesms.attachments.PointerAttachment
import org.thoughtcrime.securesms.attachments.UriAttachment
import org.thoughtcrime.securesms.backup.v2.ArchivedMediaObject
@@ -5,10 +5,10 @@
package org.thoughtcrime.securesms.database
import org.signal.core.models.database.AttachmentId
import org.signal.core.util.Base64
import org.signal.core.util.Util
import org.signal.network.api.AttachmentUploadResult
import org.thoughtcrime.securesms.attachments.AttachmentId
import org.thoughtcrime.securesms.attachments.Cdn
import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentRemoteId
import kotlin.random.Random
@@ -12,13 +12,13 @@ import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.signal.core.models.ServiceId
import org.signal.core.models.database.AttachmentId
import org.signal.core.models.media.TransformProperties
import org.signal.core.util.Base64
import org.signal.core.util.Util
import org.signal.core.util.readFully
import org.signal.core.util.stream.LimitedInputStream
import org.signal.core.util.update
import org.thoughtcrime.securesms.attachments.AttachmentId
import org.thoughtcrime.securesms.attachments.Cdn
import org.thoughtcrime.securesms.attachments.PointerAttachment
import org.thoughtcrime.securesms.keyvalue.SignalStore
@@ -19,16 +19,22 @@ import org.junit.After
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.signal.core.util.Base64
import org.signal.core.util.Util
import org.signal.network.NetworkResult
import org.signal.network.exceptions.NonSuccessfulResponseCodeException
import org.thoughtcrime.securesms.attachments.Cdn
import org.thoughtcrime.securesms.attachments.PointerAttachment
import org.thoughtcrime.securesms.backup.DeletionState
import org.thoughtcrime.securesms.backup.v2.BackupRepository
import org.thoughtcrime.securesms.backup.v2.MessageBackupTier
import org.thoughtcrime.securesms.database.AttachmentTable
import org.thoughtcrime.securesms.database.SignalDatabase
import org.thoughtcrime.securesms.jobs.protos.BackupDeleteJobData
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.testing.SignalActivityRule
import org.thoughtcrime.securesms.util.RemoteConfig
import java.util.UUID
class BackupDeleteJobTest {
@@ -155,10 +161,7 @@ class BackupDeleteJobTest {
@Test
fun givenMediaOffloaded_whenIRun_thenIExpectAwaitingMediaDownload() {
mockkObject(SignalDatabase)
every { SignalDatabase.attachments.getRemainingRestorableAttachmentSize() } returns 1
every { SignalDatabase.attachments.getOptimizedMediaAttachmentSize() } returns 1
every { SignalDatabase.attachments.clearAllArchiveData() } returns Unit
insertOffloadedAttachment()
SignalStore.backup.deletionState = DeletionState.CLEAR_LOCAL_STATE
@@ -252,4 +255,39 @@ class BackupDeleteJobTest {
assertThat(result.isRetry).isTrue()
}
private fun insertOffloadedAttachment(size: Long = 100) {
SignalDatabase.attachments.insertAttachmentsForMessage(
mmsId = 1,
attachments = listOf(
PointerAttachment(
contentType = "image/jpeg",
transferState = AttachmentTable.TRANSFER_RESTORE_OFFLOADED,
size = size,
fileName = null,
cdn = Cdn.CDN_3,
location = "somelocation",
key = Base64.encodeWithPadding(Util.getSecretBytes(64)),
iv = null,
digest = Util.getSecretBytes(64),
incrementalDigest = null,
incrementalMacChunkSize = 0,
fastPreflightId = null,
voiceNote = false,
borderless = false,
videoGif = false,
width = 100,
height = 100,
uploadTimestamp = System.currentTimeMillis(),
caption = null,
stickerLocator = null,
blurHash = null,
uuid = UUID.randomUUID(),
quote = false,
quoteTargetContentType = null
)
),
quoteAttachment = emptyList()
)
}
}
@@ -143,6 +143,7 @@ class BackupSubscriptionCheckJobTest {
@Test
fun givenUserIsNotRegistered_whenIRun_thenIExpectSuccessAndEarlyExit() {
mockkObject(SignalStore.account) {
every { SignalStore.account.e164 } returns "+15555550101"
every { SignalStore.account.isRegistered } returns false
val job = BackupSubscriptionCheckJob.create()
@@ -155,6 +156,7 @@ class BackupSubscriptionCheckJobTest {
@Test
fun givenIsLinkedDevice_whenIRun_thenIExpectSuccessAndEarlyExit() {
mockkObject(SignalStore.account) {
every { SignalStore.account.e164 } returns "+15555550101"
every { SignalStore.account.isLinkedDevice } returns true
val job = BackupSubscriptionCheckJob.create()
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -14,7 +14,7 @@ object AppCapabilities {
versionedExpirationTimer = true,
attachmentBackfill = true,
spqr = true,
usernameChangeSyncMessage = false // TODO(michelle): Turn on once all clients support it and add a migration
usernameChangeSyncMessage = true
)
}
}
@@ -600,7 +600,18 @@ public final class ContactSelectionListFragment extends LoggingFragment {
boolean isUnknown = contact instanceof ContactSearchKey.UnknownRecipientKey;
SelectedContact selectedContact = contact.requireSelectedContact();
if (!canSelectSelf && !selectedContact.hasUsername() && Recipient.self().getId().equals(selectedContact.getOrCreateRecipientId())) {
boolean needsSelfCheck = !canSelectSelf && !selectedContact.hasUsername();
if (needsSelfCheck) {
lifecycleDisposable.add(contactChipViewModel.isSelf(selectedContact)
.subscribe(isSelf -> onItemClickResolved(contact, selectedContact, isUnknown, isSelf)));
} else {
onItemClickResolved(contact, selectedContact, isUnknown, false);
}
}
private void onItemClickResolved(ContactSearchKey contact, SelectedContact selectedContact, boolean isUnknown, boolean isSelf) {
if (isSelf) {
Toast.makeText(requireContext(), R.string.ContactSelectionListFragment_you_do_not_need_to_add_yourself_to_the_group, Toast.LENGTH_SHORT).show();
return;
}
@@ -561,6 +561,7 @@ class MainActivity :
val scope = rememberCoroutineScope()
BackHandler(paneExpansionState.currentAnchor == detailOnlyAnchor) {
mainNavigationViewModel.goTo(MainNavigationDetailLocation.Empty)
scope.launch {
paneExpansionState.animateTo(listOnlyAnchor)
}
@@ -1,107 +1,27 @@
package org.thoughtcrime.securesms;
import android.app.Activity;
import android.content.Intent;
import android.database.Cursor;
import android.os.Bundle;
import android.provider.ContactsContract;
import android.text.TextUtils;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.activity.ComponentActivity;
import androidx.lifecycle.ViewModelProvider;
import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.conversation.ConversationIntents;
import org.thoughtcrime.securesms.conversation.NewConversationActivity;
import org.thoughtcrime.securesms.database.SignalDatabase;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.util.Rfc5724Uri;
import java.net.URISyntaxException;
public class SystemContactsEntrypointActivity extends Activity {
private static final String TAG = Log.tag(SystemContactsEntrypointActivity.class);
public class SystemContactsEntrypointActivity extends ComponentActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
startActivity(getNextIntent(getIntent()));
finish();
super.onCreate(savedInstanceState);
}
private Intent getNextIntent(Intent original) {
DestinationAndBody destination;
SystemContactsEntrypointViewModel viewModel = new ViewModelProvider(this).get(SystemContactsEntrypointViewModel.class);
if (original.getData() != null && "content".equals(original.getData().getScheme())) {
destination = getDestinationForSyncAdapter(original);
} else {
destination = getDestinationForView(original);
}
final Intent nextIntent;
if (TextUtils.isEmpty(destination.destination)) {
nextIntent = NewConversationActivity.createIntent(this, destination.getBody());
Toast.makeText(this, R.string.ConversationActivity_specify_recipient, Toast.LENGTH_LONG).show();
} else {
Recipient recipient = Recipient.external(destination.getDestination());
if (recipient != null) {
long threadId = SignalDatabase.threads().getOrCreateThreadIdFor(recipient);
nextIntent = ConversationIntents.createBuilderSync(this, recipient.getId(), threadId)
.withDraftText(destination.getBody())
.build();
} else {
nextIntent = NewConversationActivity.createIntent(this, destination.getBody());
viewModel.getContactAction().observe(this, nextStep -> {
if (nextStep.getShowSpecifyRecipientToast()) {
Toast.makeText(this, R.string.ConversationActivity_specify_recipient, Toast.LENGTH_LONG).show();
}
}
return nextIntent;
}
startActivity(nextStep.getIntent());
finish();
});
private @NonNull DestinationAndBody getDestinationForView(Intent intent) {
try {
Rfc5724Uri smsUri = new Rfc5724Uri(intent.getData().toString());
return new DestinationAndBody(smsUri.getPath(), smsUri.getQueryParams().get("body"));
} catch (URISyntaxException e) {
Log.w(TAG, "unable to parse RFC5724 URI from intent", e);
return new DestinationAndBody("", "");
}
}
private @NonNull DestinationAndBody getDestinationForSyncAdapter(Intent intent) {
Cursor cursor = null;
try {
cursor = getContentResolver().query(intent.getData(), null, null, null, null);
if (cursor != null && cursor.moveToNext()) {
return new DestinationAndBody(cursor.getString(cursor.getColumnIndexOrThrow(ContactsContract.RawContacts.Data.DATA1)), "");
}
return new DestinationAndBody("", "");
} finally {
if (cursor != null) cursor.close();
}
}
private static class DestinationAndBody {
private final String destination;
private final String body;
private DestinationAndBody(String destination, String body) {
this.destination = destination;
this.body = body;
}
public String getDestination() {
return destination;
}
public String getBody() {
return body;
}
viewModel.resolveNextStep(getIntent());
}
}
@@ -0,0 +1,100 @@
package org.thoughtcrime.securesms
import android.content.Context
import android.content.Intent
import android.provider.ContactsContract
import android.text.TextUtils
import androidx.annotation.WorkerThread
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.signal.core.util.logging.Log
import org.thoughtcrime.securesms.conversation.ConversationIntents
import org.thoughtcrime.securesms.conversation.NewConversationActivity
import org.thoughtcrime.securesms.database.SignalDatabase
import org.thoughtcrime.securesms.dependencies.AppDependencies
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.util.Rfc5724Uri
import java.net.URISyntaxException
class SystemContactsEntrypointViewModel : ViewModel() {
companion object {
private val TAG = Log.tag(SystemContactsEntrypointViewModel::class.java)
}
private val internalContactAction = MutableLiveData<ContactAction>()
val contactAction: LiveData<ContactAction> = internalContactAction
fun resolveNextStep(original: Intent) {
viewModelScope.launch {
val result = withContext(Dispatchers.IO) {
getContactAction(AppDependencies.application, original)
}
internalContactAction.value = result
}
}
@WorkerThread
private fun getContactAction(context: Context, original: Intent): ContactAction {
val destination = if (original.data != null && "content" == original.data?.scheme) {
getDestinationForSyncAdapter(context, original)
} else {
getDestinationForView(original)
}
val destinationAddress = destination.destination
if (TextUtils.isEmpty(destinationAddress)) {
return ContactAction(NewConversationActivity.createIntent(context, destination.body), true)
}
val recipient = Recipient.external(destinationAddress!!)
if (recipient != null) {
val threadId = SignalDatabase.threads.getOrCreateThreadIdFor(recipient)
val nextIntent = ConversationIntents.createBuilderSync(context, recipient.id, threadId)
.withDraftText(destination.body)
.build()
return ContactAction(nextIntent, false)
}
return ContactAction(NewConversationActivity.createIntent(context, destination.body), true)
}
private fun getDestinationForView(intent: Intent): DestinationAndBody {
return try {
val smsUri = Rfc5724Uri(intent.data.toString())
DestinationAndBody(smsUri.path, smsUri.queryParams["body"])
} catch (e: URISyntaxException) {
Log.w(TAG, "unable to parse RFC5724 URI from intent", e)
DestinationAndBody("", "")
}
}
private fun getDestinationForSyncAdapter(context: Context, intent: Intent): DestinationAndBody {
context.contentResolver.query(intent.data!!, null, null, null, null).use { cursor ->
if (cursor != null && cursor.moveToNext()) {
return DestinationAndBody(cursor.getString(cursor.getColumnIndexOrThrow(ContactsContract.RawContacts.Data.DATA1)), "")
}
return DestinationAndBody("", "")
}
}
data class ContactAction(
val intent: Intent,
val showSpecifyRecipientToast: Boolean
)
private data class DestinationAndBody(
val destination: String?,
val body: String?
)
}
@@ -4,6 +4,7 @@ import android.net.Uri
import android.os.Parcel
import androidx.core.os.ParcelCompat
import org.signal.blurhash.BlurHash
import org.signal.core.models.database.AttachmentId
import org.signal.core.models.media.TransformProperties
import org.signal.core.util.ParcelUtil
import org.thoughtcrime.securesms.audio.AudioHash
@@ -7,9 +7,9 @@ import androidx.annotation.AnyThread
import io.reactivex.rxjava3.core.Single
import io.reactivex.rxjava3.schedulers.Schedulers
import io.reactivex.rxjava3.subjects.SingleSubject
import org.signal.core.models.database.AttachmentId
import org.signal.core.util.logging.Log
import org.thoughtcrime.securesms.attachments.Attachment
import org.thoughtcrime.securesms.attachments.AttachmentId
import org.thoughtcrime.securesms.attachments.DatabaseAttachment
import org.thoughtcrime.securesms.database.SignalDatabase
import org.thoughtcrime.securesms.database.model.databaseprotos.AudioWaveFormData
@@ -19,11 +19,11 @@ import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.onStart
import kotlinx.coroutines.flow.shareIn
import kotlinx.coroutines.withContext
import org.signal.core.models.database.AttachmentId
import org.signal.core.util.bytes
import org.signal.core.util.logging.Log
import org.signal.core.util.throttleLatest
import org.thoughtcrime.securesms.BuildConfig
import org.thoughtcrime.securesms.attachments.AttachmentId
import org.thoughtcrime.securesms.backup.v2.BackupRepository
import org.thoughtcrime.securesms.database.SignalDatabase
import org.thoughtcrime.securesms.dependencies.AppDependencies
@@ -11,7 +11,7 @@ import org.signal.core.util.Conversions;
import org.signal.core.util.logging.Log;
import org.signal.libsignal.protocol.kdf.HKDF;
import org.signal.libsignal.protocol.util.ByteUtil;
import org.thoughtcrime.securesms.attachments.AttachmentId;
import org.signal.core.models.database.AttachmentId;
import org.thoughtcrime.securesms.backup.proto.Attachment;
import org.thoughtcrime.securesms.backup.proto.Avatar;
import org.thoughtcrime.securesms.backup.proto.BackupFrame;
@@ -19,7 +19,7 @@ import org.signal.core.util.SetUtil;
import org.signal.core.util.SqlUtil;
import org.signal.core.util.Stopwatch;
import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.attachments.AttachmentId;
import org.signal.core.models.database.AttachmentId;
import org.thoughtcrime.securesms.backup.proto.KeyValue;
import org.thoughtcrime.securesms.backup.proto.SharedPreference;
import org.thoughtcrime.securesms.backup.proto.SqlStatement;
@@ -16,13 +16,13 @@ import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.update
import org.signal.core.models.database.AttachmentId
import org.signal.core.util.bytes
import org.signal.core.util.concurrent.SignalExecutors
import org.signal.core.util.logging.Log
import org.signal.core.util.safeUnregisterReceiver
import org.signal.core.util.throttleLatest
import org.thoughtcrime.securesms.BuildConfig
import org.thoughtcrime.securesms.attachments.AttachmentId
import org.thoughtcrime.securesms.backup.RestoreState
import org.thoughtcrime.securesms.database.DatabaseObserver
import org.thoughtcrime.securesms.database.SignalDatabase
@@ -34,6 +34,7 @@ import org.signal.core.models.backup.BackupId
import org.signal.core.models.backup.MediaName
import org.signal.core.models.backup.MediaRootBackupKey
import org.signal.core.models.backup.MessageBackupKey
import org.signal.core.models.database.AttachmentId
import org.signal.core.util.Base64
import org.signal.core.util.Base64.decodeBase64OrThrow
import org.signal.core.util.CursorUtil
@@ -72,9 +73,7 @@ import org.signal.network.NetworkResult
import org.signal.network.StatusCodeErrorAction
import org.signal.network.api.SvrBApi
import org.signal.network.exceptions.NonSuccessfulResponseCodeException
import org.signal.network.rest.toNetworkResult
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.attachments.AttachmentId
import org.thoughtcrime.securesms.attachments.Cdn
import org.thoughtcrime.securesms.attachments.DatabaseAttachment
import org.thoughtcrime.securesms.backup.ArchiveUploadProgress
@@ -5,8 +5,8 @@
package org.thoughtcrime.securesms.backup.v2
import org.signal.core.models.database.AttachmentId
import org.signal.core.util.concurrent.SignalExecutors
import org.thoughtcrime.securesms.attachments.AttachmentId
import org.thoughtcrime.securesms.attachments.DatabaseAttachment
import org.thoughtcrime.securesms.database.AttachmentTable
import org.thoughtcrime.securesms.database.model.MessageRecord
@@ -5,7 +5,7 @@
package org.thoughtcrime.securesms.backup.v2
import org.thoughtcrime.securesms.attachments.AttachmentId
import org.signal.core.models.database.AttachmentId
import org.whispersystems.signalservice.api.archive.BatchArchiveMediaResponse
/**
@@ -5,8 +5,8 @@
package org.thoughtcrime.securesms.backup.v2.database
import org.signal.core.models.database.AttachmentId
import org.thoughtcrime.securesms.attachments.Attachment
import org.thoughtcrime.securesms.attachments.AttachmentId
import org.thoughtcrime.securesms.database.AttachmentTable
fun AttachmentTable.restoreWallpaperAttachment(attachment: Attachment): AttachmentId? {
@@ -42,6 +42,7 @@ import org.signal.archive.proto.Text
import org.signal.archive.proto.ThreadMergeChatUpdate
import org.signal.archive.proto.ViewOnceMessage
import org.signal.core.models.ServiceId
import org.signal.core.models.database.AttachmentId
import org.signal.core.util.Base64
import org.signal.core.util.EventTimer
import org.signal.core.util.Hex
@@ -68,7 +69,6 @@ import org.signal.core.util.requireLong
import org.signal.core.util.requireLongOrNull
import org.signal.core.util.requireString
import org.signal.core.util.toByteArray
import org.thoughtcrime.securesms.attachments.AttachmentId
import org.thoughtcrime.securesms.attachments.DatabaseAttachment
import org.thoughtcrime.securesms.backup.v2.BackupMode
import org.thoughtcrime.securesms.backup.v2.ExportOddities
@@ -7,10 +7,10 @@ package org.thoughtcrime.securesms.backup.v2.importer
import androidx.core.content.contentValuesOf
import org.signal.archive.proto.Chat
import org.signal.core.models.database.AttachmentId
import org.signal.core.util.SqlUtil
import org.signal.core.util.insertInto
import org.signal.core.util.toInt
import org.thoughtcrime.securesms.attachments.AttachmentId
import org.thoughtcrime.securesms.backup.v2.ImportState
import org.thoughtcrime.securesms.backup.v2.database.restoreWallpaperAttachment
import org.thoughtcrime.securesms.backup.v2.util.parseChatWallpaper
@@ -16,6 +16,7 @@ import org.signal.archive.stream.EncryptedBackupReader
import org.signal.core.models.backup.BackupId
import org.signal.core.models.backup.MediaName
import org.signal.core.models.backup.MessageBackupKey
import org.signal.core.models.database.AttachmentId
import org.signal.core.util.Stopwatch
import org.signal.core.util.StreamUtil
import org.signal.core.util.Util
@@ -23,7 +24,6 @@ import org.signal.core.util.logging.Log
import org.signal.core.util.readFully
import org.signal.core.util.toJson
import org.signal.libsignal.crypto.Aes256Ctr32
import org.thoughtcrime.securesms.attachments.AttachmentId
import org.thoughtcrime.securesms.backup.LocalExportProgress
import org.thoughtcrime.securesms.backup.v2.BackupRepository
import org.thoughtcrime.securesms.database.AttachmentTable
@@ -12,11 +12,11 @@ import org.signal.archive.proto.AccountData
import org.signal.archive.proto.ChatStyle
import org.signal.archive.proto.Frame
import org.signal.archive.stream.BackupFrameEmitter
import org.signal.core.models.database.AttachmentId
import org.signal.core.util.UuidUtil
import org.signal.core.util.logging.Log
import org.signal.core.util.toByteArray
import org.signal.libsignal.zkgroup.backups.BackupLevel
import org.thoughtcrime.securesms.attachments.AttachmentId
import org.thoughtcrime.securesms.backup.v2.ExportState
import org.thoughtcrime.securesms.backup.v2.ImportState
import org.thoughtcrime.securesms.backup.v2.MessageBackupTier
@@ -7,8 +7,8 @@ package org.thoughtcrime.securesms.backup.v2.util
import org.signal.archive.proto.ChatStyle
import org.signal.archive.proto.FilePointer
import org.signal.core.models.database.AttachmentId
import org.signal.core.util.logging.Log
import org.thoughtcrime.securesms.attachments.AttachmentId
import org.thoughtcrime.securesms.backup.v2.BackupMode
import org.thoughtcrime.securesms.backup.v2.ImportState
import org.thoughtcrime.securesms.conversation.colors.ChatColors
@@ -27,6 +27,7 @@ import org.signal.archive.stream.EncryptedBackupReader
import org.signal.archive.stream.EncryptedBackupReader.Companion.MAC_SIZE
import org.signal.core.models.ServiceId
import org.signal.core.models.backup.MessageBackupKey
import org.signal.core.models.database.AttachmentId
import org.signal.core.util.Hex
import org.signal.core.util.ThreadUtil
import org.signal.core.util.bytes
@@ -39,7 +40,6 @@ import org.signal.core.util.stream.LimitedInputStream
import org.signal.libsignal.zkgroup.profiles.ProfileKey
import org.signal.network.NetworkResult
import org.signal.network.api.SvrBApi
import org.thoughtcrime.securesms.attachments.AttachmentId
import org.thoughtcrime.securesms.attachments.DatabaseAttachment
import org.thoughtcrime.securesms.backup.ArchiveUploadProgress
import org.thoughtcrime.securesms.backup.LocalExportProgress
@@ -45,7 +45,6 @@ import org.signal.core.ui.compose.Texts
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.compose.rememberStatusBarColorNestedScrollModifier
import org.thoughtcrime.securesms.util.CommunicationActions
import org.thoughtcrime.securesms.util.RemoteConfig
import org.thoughtcrime.securesms.util.viewModel
/**
@@ -299,31 +298,29 @@ private fun AdvancedPrivacySettingsScreen(
)
}
if (RemoteConfig.internalUser) {
item {
Dividers.Default()
}
item {
Dividers.Default()
}
item {
val label = buildAnnotatedString {
append(stringResource(R.string.preferences_automatic_key_verification_body))
append(" ")
withLink(
LinkAnnotation.Clickable("learn-more", linkInteractionListener = {
callbacks.onAutomaticVerificationLearnMoreClick()
})
) {
append(stringResource(R.string.LearnMoreTextView_learn_more))
}
item {
val label = buildAnnotatedString {
append(stringResource(R.string.preferences_automatic_key_verification_body))
append(" ")
withLink(
LinkAnnotation.Clickable("learn-more", linkInteractionListener = {
callbacks.onAutomaticVerificationLearnMoreClick()
})
) {
append(stringResource(R.string.LearnMoreTextView_learn_more))
}
Rows.ToggleRow(
checked = state.allowAutomaticKeyVerification,
text = AnnotatedString(stringResource(R.string.preferences_automatic_key_verification)),
label = label,
onCheckChanged = callbacks::onAllowAutomaticVerificationChanged
)
}
Rows.ToggleRow(
checked = state.allowAutomaticKeyVerification,
text = AnnotatedString(stringResource(R.string.preferences_automatic_key_verification)),
label = label,
onCheckChanged = callbacks::onAllowAutomaticVerificationChanged
)
}
}
}
@@ -42,6 +42,7 @@ import org.whispersystems.signalservice.api.subscriptions.SubscriberId
import java.math.BigDecimal
import java.text.DecimalFormat
import java.text.DecimalFormatSymbols
import java.text.ParseException
import java.util.Currency
import java.util.Optional
@@ -170,6 +171,8 @@ class DonateToSignalViewModel(
decimalFormat.parse(amount) as BigDecimal
} catch (e: NumberFormatException) {
BigDecimal.ZERO
} catch (e: ParseException) {
BigDecimal.ZERO
}
}
@@ -9,13 +9,17 @@ import android.os.Bundle
import androidx.core.os.bundleOf
import androidx.navigation.fragment.NavHostFragment
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.withContext
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.components.settings.DSLSettingsActivity
import org.thoughtcrime.securesms.compose.FragmentBackPressedInfo
import org.thoughtcrime.securesms.compose.FragmentBackPressedInfoProvider
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.recipients.RecipientId
class ConversationSettingsNavHostFragment : NavHostFragment() {
class ConversationSettingsNavHostFragment : NavHostFragment(), FragmentBackPressedInfoProvider {
companion object {
suspend fun createArgs(recipientId: RecipientId): Bundle {
@@ -36,4 +40,14 @@ class ConversationSettingsNavHostFragment : NavHostFragment() {
navController.setGraph(R.navigation.conversation_settings, args)
super.onCreate(savedInstanceState)
}
override fun getFragmentBackPressedInfo(): Flow<FragmentBackPressedInfo> {
return navController.currentBackStackEntryFlow.map {
if (navController.previousBackStackEntry != null) {
FragmentBackPressedInfo.Enabled { navController.popBackStack() }
} else {
FragmentBackPressedInfo.Disabled
}
}
}
}
@@ -20,7 +20,7 @@ object ConversationSettingsNavigator {
recipient: Recipient
) {
if (activity is MainNavigationChatDetailRouter) {
activity.goToChatDetail(MainNavigationDetailLocation.Chats.ConversationSettings(recipient.id))
activity.goToChatDetail(MainNavigationDetailLocation.Chats.ConversationSettings(recipient.id, isContentRoot = true))
return
}
@@ -120,25 +120,13 @@ class TransferControlView @JvmOverloads constructor(context: Context, attrs: Att
}
if (event.type == PartProgressEvent.Type.COMPRESSION) {
val mutableMap = it.compressionProgress.toMutableMap()
val updateEvent = Progress.fromEvent(event)
val existingEvent = mutableMap[attachment]
if (existingEvent == null || updateEvent.completed > existingEvent.completed) {
mutableMap[attachment] = updateEvent
} else if (updateEvent.completed < 0.bytes) {
mutableMap.remove(attachment)
}
return@updateState it.copy(compressionProgress = mutableMap.toMap())
val progress = it.compressionProgress.toMutableMap()
progress.applyProgress(attachment, Progress.fromEvent(event))
return@updateState it.copy(compressionProgress = progress.toMap())
} else {
val mutableMap = it.networkProgress.toMutableMap()
val updateEvent = Progress.fromEvent(event)
val existingEvent = mutableMap[attachment]
if (existingEvent == null || updateEvent.completed > existingEvent.completed) {
mutableMap[attachment] = updateEvent
} else if (updateEvent.completed < 0.bytes) {
mutableMap.remove(attachment)
}
return@updateState it.copy(networkProgress = mutableMap.toMap())
val progress = it.networkProgress.toMutableMap()
progress.applyProgress(attachment, Progress.fromEvent(event))
return@updateState it.copy(networkProgress = progress.toMap())
}
}
}
@@ -227,6 +215,14 @@ class TransferControlView @JvmOverloads constructor(context: Context, attrs: Att
updateState { it.copy(isClickable = clickable) }
}
private fun MutableMap<Attachment, Progress>.applyProgress(attachment: Attachment, update: Progress) {
if (update.completed < 0.bytes) {
remove(attachment)
} else {
put(attachment, update)
}
}
private inline fun verboseLog(message: () -> String) {
if (VERBOSE_DEVELOPMENT_LOGGING) {
Log.d(TAG, "[$viewId] ${message()}")
@@ -11,6 +11,8 @@ import org.thoughtcrime.securesms.attachments.Attachment
import org.thoughtcrime.securesms.database.AttachmentTable
import org.thoughtcrime.securesms.mms.Slide
import org.thoughtcrime.securesms.util.MediaUtil
import org.whispersystems.signalservice.api.crypto.AttachmentCipherStreamUtil
import org.whispersystems.signalservice.internal.crypto.PaddingInputStream
/**
* Pure, Android-View-free logic for the transfer controls UI.
@@ -152,7 +154,7 @@ object TransferControls {
} else if (state.isUpload) {
ProgressLabel.Bytes(state.networkProgress.sumCompleted(), state.networkProgress.sumTotal())
} else {
val total = state.slides.sumOf { it.fileSize }.bytes
val total = state.slides.sumOf { AttachmentCipherStreamUtil.getCiphertextLength(PaddingInputStream.getPaddedSize(it.fileSize)) }.bytes
val completed = state.networkProgress.sumCompleted().let { if (it > total) total else it }
ProgressLabel.Bytes(completed, total)
}
@@ -65,7 +65,7 @@ fun TransferControls(
is TransferControlsRenderState.Gone -> Unit
is TransferControlsRenderState.Pending -> Content(
control = TransferProgressState.Ready(
state = TransferProgressState.Ready(
icon = arrowIcon(state.isUpload),
startButtonContentDesc = startContentDescription(state.isUpload),
startButtonOnClickLabel = startContentDescription(state.isUpload),
@@ -79,7 +79,7 @@ fun TransferControls(
)
is TransferControlsRenderState.Retry -> Content(
control = TransferProgressState.Ready(
state = TransferProgressState.Ready(
icon = arrowIcon(state.isUpload),
startButtonContentDesc = startContentDescription(state.isUpload),
startButtonOnClickLabel = startContentDescription(state.isUpload),
@@ -104,7 +104,7 @@ fun TransferControls(
}
Content(
control = TransferProgressState.InProgress(
state = TransferProgressState.InProgress(
progress = state.progress,
cancelAction = if (state.cancelable) {
TransferProgressState.InProgress.CancelAction(
@@ -130,7 +130,7 @@ fun TransferControls(
@Composable
private fun BoxScope.Content(
control: TransferProgressState,
state: TransferProgressState,
placement: TransferControls.Placement,
showPlayButton: Boolean,
centerLabel: String?,
@@ -142,12 +142,16 @@ private fun BoxScope.Content(
val controlInCorner = placement == TransferControls.Placement.CORNER
if (controlInCenter || showPlayButton || centerLabel != null) {
val centerStartReadyState = if (controlInCenter) state as? TransferProgressState.Ready else null
Pill(
modifier = Modifier.align(Alignment.Center),
cornerRadius = 24.dp
cornerRadius = 24.dp,
onClick = centerStartReadyState?.onStartClick,
onClickContentDescription = centerStartReadyState?.startButtonContentDesc,
onClickLabel = centerStartReadyState?.startButtonOnClickLabel
) {
if (controlInCenter) {
OnMediaIndicator(control, CENTER_CONTROL_SIZE)
OnMediaIndicator(centerStartReadyState?.copy(onStartClick = null) ?: state, CENTER_CONTROL_SIZE)
}
if (showPlayButton) {
@@ -167,14 +171,18 @@ private fun BoxScope.Content(
}
if (controlInCorner || cornerText != null) {
val cornerStartReadyState = if (controlInCorner) state as? TransferProgressState.Ready else null
Pill(
modifier = Modifier
.align(Alignment.TopStart)
.padding(4.dp),
cornerRadius = 16.dp
cornerRadius = 16.dp,
onClick = cornerStartReadyState?.onStartClick,
onClickContentDescription = cornerStartReadyState?.startButtonContentDesc,
onClickLabel = cornerStartReadyState?.startButtonOnClickLabel
) {
if (controlInCorner) {
OnMediaIndicator(control, 32.dp)
OnMediaIndicator(cornerStartReadyState?.copy(onStartClick = null) ?: state, 32.dp)
}
if (cornerText != null) {
@@ -195,12 +203,26 @@ private fun BoxScope.Content(
private fun Pill(
modifier: Modifier = Modifier,
cornerRadius: Dp,
onClick: (() -> Unit)? = null,
onClickContentDescription: String? = null,
onClickLabel: String? = null,
content: @Composable RowScope.() -> Unit
) {
Row(
modifier = modifier
.clip(RoundedCornerShape(cornerRadius))
.background(colorResource(CoreUiR.color.signal_colorTransparentInverse4)),
.background(colorResource(CoreUiR.color.signal_colorTransparentInverse4))
.then(
if (onClick != null) {
Modifier.clickableContainer(
contentDescription = onClickContentDescription,
onClickLabel = onClickLabel ?: "",
onClick = onClick
)
} else {
Modifier
}
),
verticalAlignment = Alignment.CenterVertically
) {
content()
@@ -84,10 +84,16 @@ private fun StartTransferButton(
) {
Box(
modifier = modifier
.clickableContainer(
contentDescription = state.startButtonContentDesc,
onClickLabel = state.startButtonOnClickLabel,
onClick = state.onStartClick
.then(
if (state.onStartClick != null) {
Modifier.clickableContainer(
contentDescription = state.startButtonContentDesc,
onClickLabel = state.startButtonOnClickLabel,
onClick = state.onStartClick
)
} else {
Modifier
}
)
) {
Icon(
@@ -165,8 +171,6 @@ private fun ProgressIndicator(
)
}
// When cancelable, draw the filled "stop" square in the center of the ring (matches the legacy view's
// IN_PROGRESS_CANCELABLE state). Sized as a fraction of the control so it scales with center/corner placements.
if (state.cancelAction != null) {
Box(
modifier = Modifier
@@ -198,7 +202,7 @@ sealed interface TransferProgressState {
val icon: ImageVector,
val startButtonContentDesc: String,
val startButtonOnClickLabel: String,
val onStartClick: () -> Unit
val onStartClick: (() -> Unit)?
) : TransferProgressState
data class InProgress(
@@ -8,7 +8,6 @@ import android.content.pm.ResolveInfo;
import android.media.AudioManager;
import android.net.Uri;
import android.os.Bundle;
import android.os.Process;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
@@ -26,7 +25,7 @@ import androidx.media3.session.MediaSessionService;
import org.signal.core.util.concurrent.SignalExecutors;
import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.attachments.AttachmentId;
import org.signal.core.models.database.AttachmentId;
import org.thoughtcrime.securesms.attachments.DatabaseAttachment;
import org.thoughtcrime.securesms.database.DatabaseObserver;
import org.thoughtcrime.securesms.database.MessageTable;
@@ -89,6 +89,7 @@ object CallInfoView {
ParticipantsState(
inCallLobby = state.callState == WebRtcViewModel.State.CALL_PRE_JOIN,
ringGroup = state.ringGroup,
isGroupOrAdHocCall = state.groupCallState.isNotIdle,
includeSelf = state.groupCallState === WebRtcViewModel.GroupCallState.CONNECTED_AND_JOINED || state.groupCallState === WebRtcViewModel.GroupCallState.IDLE,
participantCount = if (state.participantCount.isPresent) state.participantCount.get().toInt() else 0,
remoteParticipants = state.allRemoteParticipants.sortedBy { it.callParticipantId.recipientId },
@@ -345,6 +346,7 @@ private fun CallInfo(
callParticipant = participant,
isSelfAdmin = controlAndInfoState.isSelfAdmin(),
isCallLink = controlAndInfoState.callLink != null,
canRemoteMute = participantsState.isGroupOrAdHocCall,
onDismiss = { selectedParticipant = null },
onMuteAudio = onMuteAudio,
onRemoveFromCall = onRemoveFromCall,
@@ -702,6 +704,7 @@ private fun UnknownMembersRowPreview() {
private data class ParticipantsState(
val inCallLobby: Boolean = false,
val ringGroup: Boolean = true,
val isGroupOrAdHocCall: Boolean = false,
val includeSelf: Boolean = false,
val participantCount: Int = 0,
val remoteParticipants: List<CallParticipant> = emptyList(),
@@ -46,6 +46,7 @@ fun ParticipantActionsSheet(
callParticipant: CallParticipant,
isSelfAdmin: Boolean,
isCallLink: Boolean,
canRemoteMute: Boolean,
onDismiss: () -> Unit,
onMuteAudio: (CallParticipant) -> Unit,
onRemoveFromCall: (CallParticipant) -> Unit,
@@ -71,6 +72,7 @@ fun ParticipantActionsSheet(
callParticipant = callParticipant,
isSelfAdmin = isSelfAdmin,
isCallLink = isCallLink,
canRemoteMute = canRemoteMute,
onDismiss = onDismiss,
onMuteAudio = onMuteAudio,
onRemoveFromCall = onRemoveFromCall,
@@ -87,6 +89,7 @@ private fun ParticipantActionsSheetContent(
callParticipant: CallParticipant,
isSelfAdmin: Boolean,
isCallLink: Boolean,
canRemoteMute: Boolean,
onDismiss: () -> Unit,
onMuteAudio: (CallParticipant) -> Unit,
onRemoveFromCall: (CallParticipant) -> Unit,
@@ -96,7 +99,7 @@ private fun ParticipantActionsSheetContent(
) {
ParticipantHeader(recipient = recipient)
if (callParticipant.isMicrophoneEnabled) {
if (canRemoteMute && callParticipant.isMicrophoneEnabled) {
Dividers.Default()
Rows.TextRow(
@@ -110,7 +113,7 @@ private fun ParticipantActionsSheetContent(
}
if (isSelfAdmin && isCallLink) {
if (!callParticipant.isMicrophoneEnabled) {
if (!(canRemoteMute && callParticipant.isMicrophoneEnabled)) {
Dividers.Default()
}
@@ -207,6 +210,7 @@ private fun ParticipantActionsSheetAdminPreview() {
),
isSelfAdmin = true,
isCallLink = true,
canRemoteMute = true,
onDismiss = {},
onMuteAudio = {},
onRemoveFromCall = {},
@@ -228,6 +232,7 @@ private fun ParticipantActionsSheetNonAdminPreview() {
),
isSelfAdmin = false,
isCallLink = false,
canRemoteMute = true,
onDismiss = {},
onMuteAudio = {},
onRemoveFromCall = {},
@@ -72,6 +72,12 @@ class ContactChipViewModel : ViewModel() {
}
}
fun isSelf(selectedContact: SelectedContact): Single<Boolean> {
return Single.fromCallable { Recipient.self().id == selectedContact.getOrCreateRecipientId() }
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
}
private fun getOrCreateRecipientId(selectedContact: SelectedContact): Single<RecipientId> {
return Single.fromCallable {
selectedContact.getOrCreateRecipientId()
@@ -13,7 +13,7 @@ import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonProperty;
import org.thoughtcrime.securesms.attachments.Attachment;
import org.thoughtcrime.securesms.attachments.AttachmentId;
import org.signal.core.models.database.AttachmentId;
import org.thoughtcrime.securesms.attachments.UriAttachment;
import org.thoughtcrime.securesms.database.AttachmentTable;
import org.signal.core.util.JsonUtils;
@@ -429,7 +429,7 @@ public final class ConversationItem extends RelativeLayout implements BindableCo
setGutterSizes(messageRecord, groupThread);
setMessageShape(messageRecord, previousMessageRecord, nextMessageRecord, groupThread);
setMediaAttributes(messageRecord, previousMessageRecord, nextMessageRecord, groupThread, hasWallpaper, isMessageRequestAccepted, allowedToPlayInline);
setBodyText(messageRecord, searchQuery, isMessageRequestAccepted);
setBodyText(messageRecord, searchQuery, isMessageRequestAccepted, hasWallpaper);
setBubbleState(messageRecord, messageRecord.getFromRecipient(), hasWallpaper, colorizer);
setInteractionState(conversationMessage, pulse);
setStatusIcons(messageRecord, hasWallpaper);
@@ -941,14 +941,22 @@ public final class ConversationItem extends RelativeLayout implements BindableCo
footer.setRevealDotColor(colorizer.getOutgoingFooterIconColor(context));
footer.setOnlyShowSendingStatus(false, messageRecord);
} else if (messageRecord.isRemoteDelete()) {
if (hasWallpaper) {
bodyBubble.getBackground().setColorFilter(ContextCompat.getColor(context, R.color.wallpaper_bubble_color), PorterDuff.Mode.SRC_IN);
if (messageRecord.isOutgoing() && hasWallpaper) {
bodyBubble.getBackground().setColorFilter(recipient.getChatColors().getChatBubbleColorFilter());
footer.setTextColor(colorizer.getOutgoingFooterTextColor(context));
footer.setIconColor(colorizer.getOutgoingFooterIconColor(context));
footer.setRevealDotColor(colorizer.getOutgoingFooterIconColor(context));
} else if (hasWallpaper) {
bodyBubble.getBackground().setColorFilter(getDefaultBubbleColor(true), PorterDuff.Mode.SRC_IN);
footer.setTextColor(ContextCompat.getColor(context, R.color.signal_text_secondary));
footer.setIconColor(ContextCompat.getColor(context, org.signal.core.ui.R.color.signal_colorNeutralVariantInverse));
footer.setRevealDotColor(ContextCompat.getColor(context, org.signal.core.ui.R.color.signal_colorNeutralVariantInverse));
} else {
bodyBubble.getBackground().setColorFilter(ContextCompat.getColor(context, R.color.signal_background_primary), PorterDuff.Mode.MULTIPLY);
footer.setTextColor(ContextCompat.getColor(context, R.color.signal_text_secondary));
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);
} else {
bodyBubble.getBackground().setColorFilter(getDefaultBubbleColor(hasWallpaper), PorterDuff.Mode.SRC_IN);
@@ -1143,7 +1151,8 @@ public final class ConversationItem extends RelativeLayout implements BindableCo
private void setBodyText(@NonNull MessageRecord messageRecord,
@Nullable String searchQuery,
boolean messageRequestAccepted)
boolean messageRequestAccepted,
boolean hasWallpaper)
{
bodyText.setClickable(false);
bodyText.setFocusable(false);
@@ -1154,14 +1163,16 @@ public final class ConversationItem extends RelativeLayout implements BindableCo
bodyText.setMaxLength(-1);
if (RemoteConfig.receiveAdminDelete() && conversationMessage.getDeletedByRecipient() != null) {
bodyText.setText(getDeletedMessageText(conversationMessage));
bodyText.setText(getDeletedMessageText(conversationMessage, hasWallpaper));
bodyText.setVisibility(View.VISIBLE);
bodyText.setOverflowText(null);
} else if (messageRecord.isRemoteDelete()) {
String deletedMessage = context.getString(messageRecord.isOutgoing() ? R.string.ConversationItem_you_deleted_this_message : R.string.ConversationItem_this_message_was_deleted);
SpannableString italics = new SpannableString(deletedMessage);
italics.setSpan(new StyleSpan(android.graphics.Typeface.ITALIC), 0, deletedMessage.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
italics.setSpan(new ForegroundColorSpan(ContextCompat.getColor(context, R.color.signal_text_primary)),
int textColor = messageRecord.isOutgoing() && hasWallpaper ? colorizer.getOutgoingDeleteTextColor(context)
: ContextCompat.getColor(context, R.color.signal_text_primary);
italics.setSpan(new ForegroundColorSpan(textColor),
0,
deletedMessage.length(),
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
@@ -1220,40 +1231,44 @@ public final class ConversationItem extends RelativeLayout implements BindableCo
}
}
private SpannableStringBuilder getDeletedMessageText(@NonNull ConversationMessage message) {
private SpannableStringBuilder getDeletedMessageText(@NonNull ConversationMessage message, boolean hasWallpaper) {
boolean isAdminDelete = !message.getDeletedByRecipient().equals(message.getMessageRecord().getFromRecipient());
boolean useOutgoing = message.getMessageRecord().isOutgoing() && hasWallpaper;
int textColor = useOutgoing ? colorizer.getOutgoingDeleteTextColor(context)
: ContextCompat.getColor(context, org.signal.core.ui.R.color.signal_colorOnSurfaceVariant);
int nameColor = useOutgoing ? colorizer.getOutgoingDeleteNameColor(context)
: colorizer.getIncomingGroupSenderColor(getContext(), message.getDeletedByRecipient());
CharSequence body;
if (message.getDeletedByRecipient().equals(Recipient.self())) {
body = formatDeletedText(context.getString(R.string.ConversationItem_you_deleted_this_message));
body = formatDeletedText(context.getString(R.string.ConversationItem_you_deleted_this_message), textColor);
} else if (!isAdminDelete) {
body = formatDeletedText(context.getString(R.string.ConversationItem_s_deleted_this_message, message.getDeletedByRecipient().getShortDisplayName(context)));
body = formatDeletedText(context.getString(R.string.ConversationItem_s_deleted_this_message, message.getDeletedByRecipient().getShortDisplayName(context)), textColor);
} else {
String template = context.getString(R.string.ConversationItem_admin_s_deleted_this_message, SpanUtil.SPAN_PLACE_HOLDER);
int start = template.indexOf(SpanUtil.SPAN_PLACE_HOLDER);
int nameColor = colorizer.getIncomingGroupSenderColor(getContext(), message.getDeletedByRecipient());
SpannableString name = new SpannableString(message.getDeletedByRecipient().getShortDisplayName(context));
SpannableString name = new SpannableString(message.getDeletedByRecipient().getShortDisplayName(context));
name.setSpan(new ForegroundColorSpan(nameColor), 0, name.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
name.setSpan(new RecipientClickableSpan(conversationMessage.getDeletedByRecipient().getId()), 0, name.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
name.setSpan(new StyleSpan(Typeface.BOLD), 0, name.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
SpannableStringBuilder builder = new SpannableStringBuilder(template);
builder.setSpan(new ForegroundColorSpan(ContextCompat.getColor(context, org.signal.core.ui.R.color.signal_colorOnSurfaceVariant)), 0, builder.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
builder.setSpan(new ForegroundColorSpan(textColor), 0, builder.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
builder.replace(start, start + SpanUtil.SPAN_PLACE_HOLDER.length(), name);
body = builder;
}
return new SpannableStringBuilder()
.append(SignalSymbols.getSpannedString(getContext(), SignalSymbols.Weight.REGULAR, SignalSymbols.Glyph.X_CIRCLE, org.signal.core.ui.R.color.signal_colorOnSurfaceVariant))
.append(SpanUtil.color(textColor, SignalSymbols.getSpannedString(getContext(), SignalSymbols.Weight.REGULAR, SignalSymbols.Glyph.X_CIRCLE, -1)))
.append(" ")
.append(body);
}
private SpannableString formatDeletedText(String text) {
private SpannableString formatDeletedText(String text, int textColor) {
SpannableString spannableString = new SpannableString(text);
spannableString.setSpan(new ForegroundColorSpan(ContextCompat.getColor(context, org.signal.core.ui.R.color.signal_colorOnSurfaceVariant)), 0, text.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
spannableString.setSpan(new ForegroundColorSpan(textColor), 0, text.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
return spannableString;
}
@@ -2487,7 +2502,7 @@ public final class ConversationItem extends RelativeLayout implements BindableCo
if ((messageRecord.isOutgoing() || !outgoingOnly) &&
!hasNoBubble(messageRecord) &&
!messageRecord.isRemoteDelete() &&
(!messageRecord.isRemoteDelete() || (hasWallpaper && messageRecord.isOutgoing())) &&
bodyBubbleCorners != null &&
bodyBubble.getVisibility() == VISIBLE)
{
@@ -2954,10 +2969,16 @@ public final class ConversationItem extends RelativeLayout implements BindableCo
MediaIntentFactory.MediaPreviewArgs args = new MediaIntentFactory.MediaPreviewArgs(
messageRecord.getThreadId(),
messageRecord.getTimestamp(),
messageRecord.getId(),
messageRecord.getFromRecipient().getId(),
conversationRecipient.getId(),
messageRecord.isOutgoing(),
mediaUri,
slide.getUri(),
slide.getContentType(),
slide.asAttachment().size,
slide.getCaption().orElse(null),
conversationMessage.getDisplayBody(getContext()),
false,
false,
false,
@@ -17,7 +17,7 @@ sealed class ConversationItemDisplayMode(val messageMode: MessageMode = MessageM
object Starred : ConversationItemDisplayMode()
fun displayWallpaper(): Boolean {
return this == Standard || this == Detailed
return this == Standard
}
enum class MessageMode {
@@ -94,6 +94,7 @@ public final class ConversationUpdateItem extends FrameLayout
private TextView body;
private MaterialButton actionButton;
private View background;
private View bodyBackground;
private ConversationMessage conversationMessage;
private Recipient conversationRecipient;
private Optional<MessageRecord> previousMessageRecord;
@@ -119,6 +120,7 @@ public final class ConversationUpdateItem extends FrameLayout
private int latestFrame;
private SpannableString displayBody;
private ExpirationTimer timer;
private boolean hasWallpaper;
private final PassthroughClickListener passthroughClickListener = new PassthroughClickListener();
@@ -138,6 +140,7 @@ public final class ConversationUpdateItem extends FrameLayout
this.body = findViewById(R.id.conversation_update_body);
this.actionButton = findViewById(R.id.conversation_update_action);
this.background = findViewById(R.id.conversation_update_background);
this.bodyBackground = findViewById(R.id.conversation_update_body_background);
this.collapsedButton = findViewById(R.id.conversation_update_collapsed);
body.setOnClickListener(v -> {
@@ -198,6 +201,7 @@ public final class ConversationUpdateItem extends FrameLayout
this.nextMessageRecord = nextMessageRecord;
this.conversationRecipient = conversationRecipient;
this.isMessageRequestAccepted = isMessageRequestAccepted;
this.hasWallpaper = hasWallpaper;
senderObserver.observe(lifecycleOwner, messageRecord.getFromRecipient());
@@ -209,9 +213,11 @@ public final class ConversationUpdateItem extends FrameLayout
groupObserver.observe(lifecycleOwner, null);
}
int textColor = ContextCompat.getColor(getContext(), R.color.conversation_item_update_text_color);
if (ThemeUtil.isDarkTheme(getContext()) && hasWallpaper) {
textColor = ContextCompat.getColor(getContext(), R.color.core_grey_15);
int textColor;
if (hasWallpaper) {
textColor = ContextCompat.getColor(getContext(), org.signal.core.ui.R.color.signal_colorOnSurfaceVariant);
} else {
textColor = ContextCompat.getColor(getContext(), R.color.conversation_item_update_text_color);
}
UpdateDescription updateDescription = Objects.requireNonNull(messageRecord.getUpdateDisplayBody(getContext(), eventListener::onRecipientNameClicked));
@@ -227,13 +233,10 @@ public final class ConversationUpdateItem extends FrameLayout
present(conversationMessage, nextMessageRecord, conversationRecipient, isMessageRequestAccepted);
presentTimer(updateDescription);
presentBackground(shouldCollapse(messageRecord, previousMessageRecord),
shouldCollapse(messageRecord, nextMessageRecord),
hasWallpaper,
donationRequest);
presentBackground(hasWallpaper, donationRequest);
presentActionButton(hasWallpaper, donationRequest);
presentCollapsedHead(conversationMessage.getMessageRecord().getCollapsedState());
presentCollapsedHead(hasWallpaper, conversationMessage.getMessageRecord().getCollapsedState());
updateSelectedState();
}
@@ -247,12 +250,11 @@ public final class ConversationUpdateItem extends FrameLayout
}
}
private static boolean shouldCollapse(@NonNull MessageRecord current, @NonNull Optional<MessageRecord> candidate)
private static boolean isSameDayUpdate(@NonNull MessageRecord current, @NonNull Optional<MessageRecord> candidate)
{
return candidate.isPresent() &&
candidate.get().isUpdate() &&
DateUtils.isSameDay(current.getTimestamp(), candidate.get().getTimestamp()) &&
isSameType(current, candidate.get());
DateUtils.isSameDay(current.getTimestamp(), candidate.get().getTimestamp());
}
/** After a short delay, if the main data hasn't shown yet, then a loading message is displayed. */
@@ -414,6 +416,7 @@ public final class ConversationUpdateItem extends FrameLayout
private void update() {
present(conversationMessage, nextMessageRecord, conversationRecipient, isMessageRequestAccepted);
presentBackground(hasWallpaper, conversationMessage.getMessageRecord().isReleaseChannelDonationRequest());
}
}
@@ -791,72 +794,43 @@ public final class ConversationUpdateItem extends FrameLayout
(messageRecord.isGroupV2JoinRequest(toBlock.requireServiceId()) && previousMessageRecord.map(m -> m.isCollapsedGroupV2JoinUpdate(toBlock.requireServiceId())).orElse(false));
}
private void presentBackground(boolean collapseAbove, boolean collapseBelow, boolean hasWallpaper, boolean isDonationRequest) {
int marginDefault = getContext().getResources().getDimensionPixelOffset(R.dimen.conversation_update_vertical_margin);
int marginCollapsed = 0;
int paddingDefault = getContext().getResources().getDimensionPixelOffset(R.dimen.conversation_update_vertical_padding);
int paddingCollapsed = getContext().getResources().getDimensionPixelOffset(R.dimen.conversation_update_vertical_padding_collapsed);
private void presentBackground(boolean hasWallpaper, boolean isDonationRequest) {
int marginCompact;
int marginDefault;
int topMargin;
int bottomMargin;
if (!hasWallpaper) {
marginCompact = getContext().getResources().getDimensionPixelOffset(R.dimen.conversation_update_margin_compact);
marginDefault = getContext().getResources().getDimensionPixelOffset(R.dimen.conversation_update_margin);
} else {
marginCompact = getContext().getResources().getDimensionPixelOffset(R.dimen.conversation_update_margin_compact_wallpaper);
marginDefault = getContext().getResources().getDimensionPixelOffset(R.dimen.conversation_update_margin_wallpaper);
}
topMargin = isSameDayUpdate(messageRecord, previousMessageRecord) ? marginCompact : marginDefault;
bottomMargin = isSameDayUpdate(messageRecord, nextMessageRecord) ? marginCompact : marginDefault;
if (collapseAbove && collapseBelow) {
ViewUtil.setTopMargin(background, marginCollapsed);
ViewUtil.setBottomMargin(background, marginCollapsed);
int verticalPadding;
if (actionButton.getVisibility() == View.VISIBLE) {
verticalPadding = getContext().getResources().getDimensionPixelOffset(R.dimen.conversation_update_vertical_margin_action);
} else if (hasWallpaper) {
verticalPadding = getContext().getResources().getDimensionPixelOffset(R.dimen.conversation_update_vertical_margin);
} else {
verticalPadding = 0;
}
ViewUtil.setPaddingTop(background, paddingCollapsed);
ViewUtil.setPaddingBottom(background, paddingCollapsed);
ViewUtil.setTopMargin(background, topMargin);
ViewUtil.setBottomMargin(background, bottomMargin);
ViewUtil.setPaddingTop(bodyBackground, verticalPadding);
ViewUtil.setPaddingBottom(bodyBackground, verticalPadding);
ViewUtil.updateLayoutParams(background, ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);
if (hasWallpaper) {
background.setBackgroundResource(R.drawable.conversation_update_wallpaper_background_middle);
if (hasWallpaper && !conversationMessage.isActiveCollapsedHead()) {
if (isDonationRequest) {
bodyBackground.setBackgroundResource(R.drawable.conversation_update_release_note_background);
} else {
background.setBackground(null);
}
} else if (collapseAbove) {
ViewUtil.setTopMargin(background, marginCollapsed);
ViewUtil.setBottomMargin(background, marginDefault);
ViewUtil.setPaddingTop(background, paddingDefault);
ViewUtil.setPaddingBottom(background, paddingDefault);
ViewUtil.updateLayoutParams(background, ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);
if (hasWallpaper) {
background.setBackgroundResource(R.drawable.conversation_update_wallpaper_background_bottom);
} else {
background.setBackground(null);
}
} else if (collapseBelow) {
ViewUtil.setTopMargin(background, marginDefault);
ViewUtil.setBottomMargin(background, marginCollapsed);
ViewUtil.setPaddingTop(background, paddingDefault);
ViewUtil.setPaddingBottom(background, paddingCollapsed);
ViewUtil.updateLayoutParams(background, ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);
if (hasWallpaper) {
background.setBackgroundResource(R.drawable.conversation_update_wallpaper_background_top);
} else {
background.setBackground(null);
bodyBackground.setBackgroundResource(R.drawable.conversation_update_wallpaper_background_singular);
}
} else {
ViewUtil.setTopMargin(background, marginDefault);
ViewUtil.setBottomMargin(background, marginDefault);
ViewUtil.setPaddingTop(background, paddingDefault);
ViewUtil.setPaddingBottom(background, paddingDefault);
ViewUtil.updateLayoutParams(background, ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
if (hasWallpaper) {
if (isDonationRequest) {
background.setBackgroundResource(R.drawable.conversation_update_release_note_background);
} else {
background.setBackgroundResource(R.drawable.conversation_update_wallpaper_background_singular);
}
} else {
background.setBackground(null);
}
bodyBackground.setBackground(null);
}
}
@@ -873,7 +847,7 @@ public final class ConversationUpdateItem extends FrameLayout
}
}
private void presentCollapsedHead(CollapsedState collapsedState) {
private void presentCollapsedHead(boolean hasWallpaper, CollapsedState collapsedState) {
if (!conversationMessage.isActiveCollapsibleHead()) {
collapsedButton.setVisibility(GONE);
} else {
@@ -898,6 +872,15 @@ public final class ConversationUpdateItem extends FrameLayout
}
});
ViewUtil.setBottomMargin(collapsedButton, (int) DimensionUnit.DP.toPixels(conversationMessage.isActiveCollapsedHead() ? 0 : 12));
if (hasWallpaper) {
collapsedButton.setBackgroundResource(R.drawable.conversation_update_wallpaper_background_singular);
collapsedButton.setBackgroundTintList(null);
} else {
collapsedButton.setBackgroundResource(R.drawable.rounded_rectangle_38);
collapsedButton.setBackgroundTintList(AppCompatResources.getColorStateList(getContext(), org.signal.core.ui.R.color.signal_colorSurface1));
}
collapsedButton.setVisibility(VISIBLE);
} else {
Log.w(TAG, "Found a message that is a collapsible head but does not have a collapsible type.");
@@ -925,14 +908,6 @@ public final class ConversationUpdateItem extends FrameLayout
};
}
private static boolean isSameType(@NonNull MessageRecord current, @NonNull MessageRecord candidate) {
return (current.isGroupUpdate() && candidate.isGroupUpdate()) ||
(current.isProfileChange() && candidate.isProfileChange()) ||
(current.isGroupCall() && candidate.isGroupCall()) ||
(current.isExpirationTimerUpdate() && candidate.isExpirationTimerUpdate()) ||
(current.isChangeNumber() && candidate.isChangeNumber());
}
private void presentTimer(UpdateDescription updateDescription) {
if (updateDescription.hasExpiration() && messageRecord.getExpiresIn() > 0 && messageRecord.getExpireStarted() > 0) {
timer = new ExpirationTimer(messageRecord.getExpireStarted(), messageRecord.getExpiresIn());
@@ -984,6 +959,7 @@ public final class ConversationUpdateItem extends FrameLayout
if (recipient.getId() == conversationRecipient.getId() && (conversationRecipient == null || !conversationRecipient.hasSameContent(recipient))) {
conversationRecipient = recipient;
present(conversationMessage, nextMessageRecord, conversationRecipient, isMessageRequestAccepted);
presentBackground(hasWallpaper, conversationMessage.getMessageRecord().isReleaseChannelDonationRequest());
}
}
}
@@ -4,6 +4,7 @@ import android.content.Context
import androidx.annotation.ColorInt
import androidx.core.content.ContextCompat
import org.signal.core.models.ServiceId
import org.signal.core.ui.util.ThemeUtil
import org.signal.core.util.orNull
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.recipients.Recipient
@@ -58,6 +59,24 @@ interface Colorizer {
}
}
@ColorInt
fun getOutgoingDeleteTextColor(context: Context): Int {
return if (ThemeUtil.isDarkTheme(context)) {
ContextCompat.getColor(context, CoreUiR.color.signal_colorNeutralVariantInverse)
} else {
ContextCompat.getColor(context, CoreUiR.color.signal_colorNeutralVariant)
}
}
@ColorInt
fun getOutgoingDeleteNameColor(context: Context): Int {
return if (ThemeUtil.isDarkTheme(context)) {
ContextCompat.getColor(context, CoreUiR.color.signal_colorNeutralInverse)
} else {
ContextCompat.getColor(context, CoreUiR.color.signal_colorNeutral)
}
}
@ColorInt
fun getIncomingGroupSenderColor(context: Context, recipient: Recipient): Int {
return getNameColor(context, recipient).getColor(context)
@@ -2975,7 +2975,7 @@ class ConversationFragment :
ConversationDialogs.displayDeleteDialog(requireContext(), recipient) {
messageRequestViewModel
.onDelete()
.doAfterSuccess { activity?.finish() }
.doAfterSuccess { chatRouter.exitDetailLocation() }
.subscribeWithShowProgress("delete message request")
}
}
@@ -11,10 +11,10 @@ import android.text.Spanned
import android.text.style.URLSpan
import android.text.util.Linkify
import androidx.core.text.util.LinkifyCompat
import org.signal.core.util.addDetectedLinks
import org.thoughtcrime.securesms.database.model.MessageRecord
import org.thoughtcrime.securesms.util.InterceptableLongClickCopyLinkSpan
import org.thoughtcrime.securesms.util.LinkUtil
import org.thoughtcrime.securesms.util.Linkification
import org.thoughtcrime.securesms.util.UrlClickHandler
import org.thoughtcrime.securesms.util.hasOnlyThumbnail
@@ -34,7 +34,7 @@ object V2ConversationItemUtils {
}
LinkifyCompat.addLinks(messageBody, Linkify.EMAIL_ADDRESSES or Linkify.PHONE_NUMBERS)
Linkification.applyWebUrlSpans(messageBody)
messageBody.addDetectedLinks()
messageBody.getSpans(0, messageBody.length, URLSpan::class.java).forEach { urlSpan ->
val url = urlSpan.url
@@ -29,6 +29,7 @@ import org.signal.archive.proto.BackupDebugInfo
import org.signal.blurhash.BlurHash
import org.signal.core.models.backup.MediaId
import org.signal.core.models.backup.MediaName
import org.signal.core.models.database.AttachmentId
import org.signal.core.models.media.TransformProperties
import org.signal.core.ui.util.StorageUtil
import org.signal.core.util.Base64
@@ -69,7 +70,6 @@ import org.signal.glide.decryptableuri.DecryptableUri
import org.signal.network.api.AttachmentUploadResult
import org.thoughtcrime.securesms.attachments.ArchivedAttachment
import org.thoughtcrime.securesms.attachments.Attachment
import org.thoughtcrime.securesms.attachments.AttachmentId
import org.thoughtcrime.securesms.attachments.Cdn
import org.thoughtcrime.securesms.attachments.DatabaseAttachment
import org.thoughtcrime.securesms.attachments.LocalBackupKey
@@ -110,7 +110,7 @@ class CallTable(context: Context, databaseHelper: SignalDatabase) : DatabaseTabl
)
}
fun markAllCallEventsRead(timestamp: Long = Long.MAX_VALUE) {
val now = System.currentTimeMillis()
val proposedExpireStarted = if (timestamp == Long.MAX_VALUE) System.currentTimeMillis() else timestamp
val allUnreadMissedCalls = readableDatabase
.select(MESSAGE_ID)
@@ -131,9 +131,9 @@ class CallTable(context: Context, databaseHelper: SignalDatabase) : DatabaseTabl
if (expiringCalls.isNotEmpty()) {
Log.i(TAG, "Found ${expiringCalls.size} calls that needs expiring.")
SignalDatabase.messages.markExpireStarted(expiringCalls.map { it.key to now })
SignalDatabase.messages.markExpireStarted(expiringCalls.map { it.key to proposedExpireStarted })
for ((messageId, expiresIn) in expiringCalls) {
AppDependencies.expiringMessageManager.scheduleDeletion(messageId, true, now, expiresIn)
AppDependencies.expiringMessageManager.scheduleDeletion(messageId, true, proposedExpireStarted, expiresIn)
}
}
@@ -143,13 +143,13 @@ class CallTable(context: Context, databaseHelper: SignalDatabase) : DatabaseTabl
}
fun markAllCallEventsWithPeerBeforeTimestampRead(peer: RecipientId, timestamp: Long): Call? {
val now = System.currentTimeMillis()
val proposedExpireStarted = if (timestamp == Long.MAX_VALUE) System.currentTimeMillis() else timestamp
val latestCallAsOfTimestamp = writableDatabase.withinTransaction { db ->
val unreadMissedCalls = db
.select(MESSAGE_ID)
.from(TABLE_NAME)
.where("$PEER = ? AND $TIMESTAMP <= ? AND $READ != ? AND $EVENT = ?", peer.toLong(), timestamp, ReadState.serialize(ReadState.READ), Event.serialize(Event.MISSED))
.where("$PEER = ? AND $TIMESTAMP <= ? AND $READ != ? AND $EVENT = ? AND $GROUP_CALL_ACTIVE = 0", peer.toLong(), timestamp, ReadState.serialize(ReadState.READ), Event.serialize(Event.MISSED))
.run()
.readToList { cursor ->
cursor.requireLong(MESSAGE_ID)
@@ -164,9 +164,9 @@ class CallTable(context: Context, databaseHelper: SignalDatabase) : DatabaseTabl
if (expiring.isNotEmpty()) {
Log.i(TAG, "Found ${expiring.size} calls that needs expiring.")
SignalDatabase.messages.markExpireStarted(expiring.map { it.key to now })
SignalDatabase.messages.markExpireStarted(expiring.map { it.key to proposedExpireStarted })
for ((messageId, expiresIn) in expiring) {
AppDependencies.expiringMessageManager.scheduleDeletion(messageId, true, now, expiresIn)
AppDependencies.expiringMessageManager.scheduleDeletion(messageId, true, proposedExpireStarted, expiresIn)
}
}
@@ -196,11 +196,11 @@ class CallTable(context: Context, databaseHelper: SignalDatabase) : DatabaseTabl
.readToSingleLong()
}
fun insertOneToOneCall(callId: Long, timestamp: Long, peer: RecipientId, type: Type, direction: Direction, event: Event) {
fun insertOneToOneCall(callId: Long, timestamp: Long, peer: RecipientId, type: Type, direction: Direction, event: Event, fromSync: Boolean = false) {
val messageType: Long = Call.getMessageType(type, direction, event)
writableDatabase.withinTransaction {
val result = SignalDatabase.messages.insertOneToOneCallLog(peer, messageType, timestamp, direction == Direction.OUTGOING)
val result = SignalDatabase.messages.insertOneToOneCallLog(peer, messageType, timestamp, direction == Direction.OUTGOING, fromSync)
val values = contentValuesOf(
CALL_ID to callId,
MESSAGE_ID to result.messageId,
@@ -14,7 +14,7 @@ import androidx.core.content.contentValuesOf
import kotlinx.parcelize.IgnoredOnParcel
import kotlinx.parcelize.Parcelize
import kotlinx.parcelize.TypeParceler
import org.signal.core.util.DatabaseId
import org.signal.core.models.database.DatabaseId
import org.signal.core.util.DatabaseSerializer
import org.signal.core.util.Serializer
import org.signal.core.util.delete
@@ -152,7 +152,7 @@ object IssueReporter {
return
}
val notification: Notification = NotificationCompat.Builder(context, NotificationChannels.getInstance().FAILURES)
val notification: Notification = NotificationCompat.Builder(context, NotificationChannels.getInstance().INTERNAL_ISSUES)
.setSmallIcon(R.drawable.ic_notification)
.setContentTitle("[Internal-only] Issue detected")
.setContentText("$name (${priority.label}). Please tap to get a debug log.")
@@ -28,6 +28,7 @@ import org.json.JSONArray
import org.json.JSONException
import org.json.JSONObject
import org.signal.core.models.ServiceId
import org.signal.core.models.database.AttachmentId
import org.signal.core.util.Base64
import org.signal.core.util.CursorUtil
import org.signal.core.util.JsonUtils
@@ -67,7 +68,6 @@ import org.signal.core.util.update
import org.signal.core.util.withinTransaction
import org.signal.libsignal.protocol.IdentityKey
import org.thoughtcrime.securesms.attachments.Attachment
import org.thoughtcrime.securesms.attachments.AttachmentId
import org.thoughtcrime.securesms.attachments.DatabaseAttachment
import org.thoughtcrime.securesms.attachments.DatabaseAttachment.DisplayOrderComparator
import org.thoughtcrime.securesms.backup.v2.exporters.ChatItemArchiveExporter
@@ -322,6 +322,7 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat
private const val INDEX_NOTIFICATION_STATE = "message_notification_state_index"
private const val INDEX_RATE_LIMITED = "message_rate_limited_index"
private const val INDEX_SCHEDULED_NON_STORY = "message_scheduled_non_story_index"
private const val INDEX_MESSAGE_PINNED_UNTIL = "message_pinned_until_index"
@JvmField
val CREATE_INDEXS = arrayOf(
@@ -348,7 +349,7 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat
// Partial index for marking messages read in a thread (see setMessagesReadSince). Only contains unread/unseen rows.
"CREATE INDEX IF NOT EXISTS $INDEX_THREAD_DATE_RECEIVED_UNREAD ON $TABLE_NAME ($THREAD_ID, $DATE_RECEIVED) WHERE $STORY_TYPE = 0 AND $PARENT_STORY_ID <= 0 AND ($READ = 0 OR $REACTIONS_UNREAD = 1 OR $VOTES_UNREAD = 1)",
"CREATE INDEX IF NOT EXISTS message_votes_unread_index ON $TABLE_NAME ($VOTES_UNREAD)",
"CREATE INDEX IF NOT EXISTS message_pinned_until_index ON $TABLE_NAME ($PINNED_UNTIL)",
"CREATE INDEX IF NOT EXISTS $INDEX_MESSAGE_PINNED_UNTIL ON $TABLE_NAME ($PINNED_UNTIL)",
"CREATE INDEX IF NOT EXISTS message_pinned_at_index ON $TABLE_NAME ($PINNED_AT)",
"CREATE INDEX IF NOT EXISTS message_deleted_by_index ON $TABLE_NAME ($DELETED_BY)",
"CREATE INDEX IF NOT EXISTS $INDEX_ARCHIVED_STORY ON $TABLE_NAME ($STORY_ARCHIVED, $STORY_TYPE, $DATE_SENT) WHERE $STORY_TYPE > 0 AND $STORY_ARCHIVED > 0",
@@ -908,7 +909,7 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat
return results
}
fun insertOneToOneCallLog(recipientId: RecipientId, type: Long, timestamp: Long, outgoing: Boolean): InsertResult {
fun insertOneToOneCallLog(recipientId: RecipientId, type: Long, timestamp: Long, outgoing: Boolean, fromSync: Boolean = false): InsertResult {
val recipient = Recipient.resolved(recipientId)
val threadIdResult = threads.getOrCreateThreadIdResultFor(recipient.id, recipient.isGroup)
val threadId = threadIdResult.threadId
@@ -937,6 +938,13 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat
notifyConversationListeners(threadId)
TrimThreadJob.enqueueAsync(threadId)
// If inserting an outgoing call from a sync message, automatically start timer
if (expiresIn != 0L && outgoing && fromSync) {
Log.i(TAG, "Starting expiration timer after inserting a call from a sync message.")
markExpireStarted(messageId, timestamp)
AppDependencies.expiringMessageManager.scheduleDeletion(messageId, true, timestamp, expiresIn)
}
return InsertResult(
messageId = messageId,
threadId = threadId,
@@ -1582,7 +1590,7 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat
writableDatabase.withinTransaction { db ->
db.select(FROM_RECIPIENT_ID)
.from(TABLE_NAME)
.from("$TABLE_NAME INDEXED BY $INDEX_DATE_SENT_FROM_TO_THREAD")
.where("$IS_STORY_CLAUSE AND $DATE_SENT IN ($timestamps) AND NOT ($outgoingTypeClause) AND $VIEWED_COLUMN > 0")
.run()
.readToList { cursor -> RecipientId.from(cursor.requireLong(FROM_RECIPIENT_ID)) }
@@ -2179,7 +2187,8 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat
where = "$THREAD_ID = ? AND $PINNED_UNTIL > 0",
arguments = buildArgs(threadId),
reverse = true,
orderBy = if (orderByPinned) "$PINNED_AT ASC" else ""
orderBy = if (orderByPinned) "$PINNED_AT ASC" else "",
index = INDEX_MESSAGE_PINNED_UNTIL
)
return mmsReaderFor(cursor).use { reader ->
@@ -6,7 +6,7 @@ import android.os.Parcelable;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.signal.core.util.DatabaseId;
import org.signal.core.models.database.DatabaseId;
import java.util.Objects;
@@ -1,6 +1,6 @@
package org.thoughtcrime.securesms.database.model
import org.signal.core.util.DatabaseId
import org.signal.core.models.database.DatabaseId
import org.signal.core.util.IntSerializer
/**
@@ -15,7 +15,7 @@ import androidx.core.content.ContextCompat;
import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.attachments.Attachment;
import org.thoughtcrime.securesms.attachments.AttachmentId;
import org.signal.core.models.database.AttachmentId;
import org.thoughtcrime.securesms.attachments.DatabaseAttachment;
import org.thoughtcrime.securesms.contactshare.Contact;
import org.thoughtcrime.securesms.database.CallTable;
@@ -399,7 +399,7 @@ public class ApplicationDependencyProvider implements AppDependencies.Provider {
};
SignalWebSocket.AuthenticatedWebSocket webSocket = new SignalWebSocket.AuthenticatedWebSocket(authFactory,
() -> !SignalStore.misc().isClientDeprecated() && !DeviceTransferBlockingInterceptor.getInstance().isBlockingNetwork() && !Environment.IS_INSTRUMENTATION,
() -> !SignalStore.misc().isClientDeprecated() && SignalStore.account().isRegistered() && !TextSecurePreferences.isUnauthorizedReceived(context) && !DeviceTransferBlockingInterceptor.getInstance().isBlockingNetwork() && !Environment.IS_INSTRUMENTATION,
sleepTimer,
TimeUnit.SECONDS.toMillis(30));
if (AppForegroundObserver.isForegrounded()) {
@@ -45,7 +45,6 @@ object JumboEmoji {
private var currentVersion: Int = -1
@JvmStatic
@MainThread
fun updateCurrentVersion(context: Context) {
SignalExecutors.BOUNDED.execute {
val version: EmojiFiles.Version = EmojiFiles.Version.readVersion(context, true) ?: return@execute
@@ -15,7 +15,7 @@ import com.bumptech.glide.load.data.StreamLocalUriFetcher;
import org.signal.core.util.logging.Log;
import org.signal.glide.common.io.GlideStreamConfig;
import org.thoughtcrime.securesms.attachments.AttachmentId;
import org.signal.core.models.database.AttachmentId;
import org.thoughtcrime.securesms.mms.PartAuthority;
import org.thoughtcrime.securesms.providers.BlobProvider;
import org.thoughtcrime.securesms.util.BitmapDecodingException;
@@ -11,7 +11,7 @@ import kotlinx.parcelize.Parcelize
import kotlinx.serialization.Serializable
import kotlinx.serialization.Transient
import okio.ByteString
import org.signal.core.util.DatabaseId
import org.signal.core.models.database.DatabaseId
import org.signal.core.util.Hex
import org.signal.core.util.LRUCache
import org.signal.core.util.Util
@@ -15,10 +15,10 @@ import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.text.util.LinkifyCompat;
import org.signal.core.util.LinkifierSpannableExtensionsKt;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.components.emoji.EmojiTextView;
import org.thoughtcrime.securesms.util.LinkUtil;
import org.thoughtcrime.securesms.util.Linkification;
import org.thoughtcrime.securesms.util.LongClickCopySpan;
public final class GroupDescriptionUtil {
@@ -38,7 +38,7 @@ public final class GroupDescriptionUtil {
if (linkify) {
LinkifyCompat.addLinks(descriptionSpannable, Linkify.EMAIL_ADDRESSES | Linkify.PHONE_NUMBERS);
Linkification.applyWebUrlSpans(descriptionSpannable);
LinkifierSpannableExtensionsKt.addDetectedLinks(descriptionSpannable);
for (URLSpan urlSpan : descriptionSpannable.getSpans(0, descriptionSpannable.length(), URLSpan.class)) {
String url = urlSpan.getURL();
@@ -296,7 +296,7 @@ class JobController {
@WorkerThread
synchronized void onRetry(@NonNull Job job, long backoffInterval) {
if (backoffInterval <= 0) {
if (backoffInterval < 0) {
throw new IllegalArgumentException("Invalid backoff interval! " + backoffInterval);
}
@@ -5,12 +5,12 @@
package org.thoughtcrime.securesms.jobs
import org.signal.core.models.database.AttachmentId
import org.signal.core.util.Util
import org.signal.core.util.logging.Log
import org.signal.glide.decryptableuri.DecryptableUri
import org.signal.network.NetworkResult
import org.signal.network.api.AttachmentUploadResult
import org.thoughtcrime.securesms.attachments.AttachmentId
import org.thoughtcrime.securesms.attachments.AttachmentUploadUtil
import org.thoughtcrime.securesms.attachments.DatabaseAttachment
import org.thoughtcrime.securesms.backup.v2.ArchiveDatabaseExecutor
@@ -13,7 +13,7 @@ import org.greenrobot.eventbus.EventBus;
import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.attachments.Attachment;
import org.thoughtcrime.securesms.attachments.AttachmentId;
import org.signal.core.models.database.AttachmentId;
import org.thoughtcrime.securesms.attachments.DatabaseAttachment;
import org.thoughtcrime.securesms.crypto.AttachmentSecret;
import org.thoughtcrime.securesms.crypto.AttachmentSecretProvider;
@@ -3,7 +3,7 @@ package org.thoughtcrime.securesms.jobs;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.thoughtcrime.securesms.attachments.AttachmentId;
import org.signal.core.models.database.AttachmentId;
import org.thoughtcrime.securesms.database.AttachmentTable;
import org.thoughtcrime.securesms.database.SignalDatabase;
import org.thoughtcrime.securesms.jobmanager.JsonJobData;
@@ -8,6 +8,7 @@ import androidx.annotation.MainThread
import okio.Source
import okio.buffer
import org.greenrobot.eventbus.EventBus
import org.signal.core.models.database.AttachmentId
import org.signal.core.util.Base64
import org.signal.core.util.Hex
import org.signal.core.util.Util
@@ -17,7 +18,6 @@ import org.signal.libsignal.protocol.InvalidMessageException
import org.signal.network.exceptions.NonSuccessfulResponseCodeException
import org.signal.network.exceptions.PushNetworkException
import org.thoughtcrime.securesms.attachments.Attachment
import org.thoughtcrime.securesms.attachments.AttachmentId
import org.thoughtcrime.securesms.attachments.Cdn
import org.thoughtcrime.securesms.attachments.DatabaseAttachment
import org.thoughtcrime.securesms.attachments.InvalidAttachmentException
@@ -5,10 +5,10 @@
package org.thoughtcrime.securesms.jobs
import org.signal.core.models.database.AttachmentId
import org.signal.core.util.ThreadUtil
import org.signal.core.util.drain
import org.signal.core.util.logging.Log
import org.thoughtcrime.securesms.attachments.AttachmentId
import org.thoughtcrime.securesms.database.SignalDatabase
import org.thoughtcrime.securesms.dependencies.AppDependencies
import org.thoughtcrime.securesms.jobmanager.Job
@@ -7,6 +7,7 @@ package org.thoughtcrime.securesms.jobs
import android.text.TextUtils
import okhttp3.internal.http2.StreamResetException
import org.greenrobot.eventbus.EventBus
import org.signal.core.models.database.AttachmentId
import org.signal.core.util.Base64
import org.signal.core.util.Util
import org.signal.core.util.concurrent.SignalExecutors
@@ -20,7 +21,6 @@ import org.signal.network.api.AttachmentUploadResult
import org.signal.protos.resumableuploads.ResumableUpload
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.attachments.Attachment
import org.thoughtcrime.securesms.attachments.AttachmentId
import org.thoughtcrime.securesms.attachments.AttachmentUploadUtil
import org.thoughtcrime.securesms.attachments.DatabaseAttachment
import org.thoughtcrime.securesms.backup.v2.BackupRepository
@@ -5,10 +5,10 @@
package org.thoughtcrime.securesms.jobs
import org.signal.core.models.database.AttachmentId
import org.signal.core.util.logging.Log
import org.signal.core.util.withinTransaction
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.attachments.AttachmentId
import org.thoughtcrime.securesms.backup.v2.ArchiveRestoreProgress
import org.thoughtcrime.securesms.database.AttachmentTable
import org.thoughtcrime.securesms.database.SignalDatabase
@@ -87,10 +87,7 @@ class CheckKeyTransparencyJob private constructor(
}
private fun canRunJob(): Boolean {
return if (!RemoteConfig.internalUser) {
Log.i(TAG, "Remote config is not on. Exiting.")
false
} else if (!SignalStore.account.isRegistered) {
return if (!SignalStore.account.isRegistered) {
Log.i(TAG, "Account not registered. Exiting.")
false
} else if (TextSecurePreferences.isUnauthorizedReceived(AppDependencies.application)) {
@@ -7,6 +7,7 @@ import org.thoughtcrime.securesms.jobmanager.Job
import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.stories.Stories
import org.thoughtcrime.securesms.util.TextSecurePreferences
import org.whispersystems.signalservice.api.websocket.WebSocketConnectionState
import org.whispersystems.signalservice.internal.util.StaticCredentialsProvider
import org.whispersystems.signalservice.internal.websocket.OkHttpWebSocketConnection
@@ -34,8 +35,9 @@ class CheckServiceReachabilityJob private constructor(params: Parameters) : Base
@JvmStatic
fun enqueueIfNecessary() {
val isCensored = AppDependencies.signalServiceNetworkAccess.isCensored()
val context = AppDependencies.application
val timeSinceLastCheck = System.currentTimeMillis() - SignalStore.misc.lastCensorshipServiceReachabilityCheckTime
if (SignalStore.account.isRegistered && isCensored && timeSinceLastCheck > TimeUnit.DAYS.toMillis(1)) {
if (SignalStore.account.isRegistered && !TextSecurePreferences.isUnauthorizedReceived(context) && isCensored && timeSinceLastCheck > TimeUnit.DAYS.toMillis(1)) {
AppDependencies.jobManager.add(CheckServiceReachabilityJob())
}
}
@@ -56,6 +58,12 @@ class CheckServiceReachabilityJob private constructor(params: Parameters) : Base
return
}
if (TextSecurePreferences.isUnauthorizedReceived(context)) {
Log.w(TAG, "Unauthorized received, skipping.")
SignalStore.misc.lastCensorshipServiceReachabilityCheckTime = System.currentTimeMillis()
return
}
if (!AppDependencies.signalServiceNetworkAccess.isCensored()) {
Log.w(TAG, "Not currently censored, skipping.")
SignalStore.misc.lastCensorshipServiceReachabilityCheckTime = System.currentTimeMillis()
@@ -2,6 +2,7 @@ package org.thoughtcrime.securesms.jobs
import kotlinx.coroutines.runBlocking
import org.signal.core.models.backup.MediaName
import org.signal.core.models.database.AttachmentId
import org.signal.core.util.Base64.decodeBase64
import org.signal.core.util.ByteSize
import org.signal.core.util.bytes
@@ -9,7 +10,6 @@ import org.signal.core.util.logging.Log
import org.signal.core.util.logging.logW
import org.signal.libsignal.zkgroup.VerificationFailedException
import org.signal.network.NetworkResult
import org.thoughtcrime.securesms.attachments.AttachmentId
import org.thoughtcrime.securesms.attachments.Cdn
import org.thoughtcrime.securesms.attachments.DatabaseAttachment
import org.thoughtcrime.securesms.backup.ArchiveUploadProgress
@@ -1,8 +1,8 @@
package org.thoughtcrime.securesms.jobs
import org.signal.core.models.database.AttachmentId
import org.signal.core.util.concurrent.safeBlockingGet
import org.signal.core.util.logging.Log
import org.thoughtcrime.securesms.attachments.AttachmentId
import org.thoughtcrime.securesms.attachments.DatabaseAttachment
import org.thoughtcrime.securesms.audio.AudioWaveForms
import org.thoughtcrime.securesms.database.SignalDatabase
@@ -75,6 +75,7 @@ import org.thoughtcrime.securesms.migrations.EmojiSearchIndexCheckMigrationJob;
import org.thoughtcrime.securesms.migrations.FixChangeNumberErrorMigrationJob;
import org.thoughtcrime.securesms.migrations.GooglePlayBillingPurchaseTokenMigrationJob;
import org.thoughtcrime.securesms.migrations.IdentityTableCleanupMigrationJob;
import org.thoughtcrime.securesms.migrations.KeyTransparencyUsernameMigrationJob;
import org.thoughtcrime.securesms.migrations.LegacyMigrationJob;
import org.thoughtcrime.securesms.migrations.MigrationCompleteJob;
import org.thoughtcrime.securesms.migrations.OptimizeMessageSearchIndexMigrationJob;
@@ -340,6 +341,7 @@ public final class JobManagerFactories {
put(FixChangeNumberErrorMigrationJob.KEY, new FixChangeNumberErrorMigrationJob.Factory());
put(GooglePlayBillingPurchaseTokenMigrationJob.KEY, new GooglePlayBillingPurchaseTokenMigrationJob.Factory());
put(IdentityTableCleanupMigrationJob.KEY, new IdentityTableCleanupMigrationJob.Factory());
put(KeyTransparencyUsernameMigrationJob.KEY, new KeyTransparencyUsernameMigrationJob.Factory());
put(LegacyMigrationJob.KEY, new LegacyMigrationJob.Factory());
put(MigrationCompleteJob.KEY, new MigrationCompleteJob.Factory());
put(OptimizeMessageSearchIndexMigrationJob.KEY, new OptimizeMessageSearchIndexMigrationJob.Factory());
@@ -6,6 +6,7 @@
package org.thoughtcrime.securesms.jobs
import android.net.Uri
import org.signal.core.models.database.AttachmentId
import org.signal.core.util.logging.Log
import org.signal.core.util.readToSingleObject
import org.signal.core.util.requireLong
@@ -14,7 +15,6 @@ import org.signal.core.util.requireString
import org.signal.core.util.select
import org.signal.core.util.update
import org.signal.glide.decryptableuri.DecryptableUri
import org.thoughtcrime.securesms.attachments.AttachmentId
import org.thoughtcrime.securesms.database.AttachmentTable
import org.thoughtcrime.securesms.database.AttachmentTable.Companion.CONTENT_TYPE
import org.thoughtcrime.securesms.database.AttachmentTable.Companion.DATA_FILE
@@ -13,6 +13,7 @@ import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import androidx.core.content.ContextCompat
import org.greenrobot.eventbus.EventBus
import org.signal.core.models.database.AttachmentId
import org.signal.core.util.Base64.decodeBase64OrThrow
import org.signal.core.util.PendingIntentFlags
import org.signal.core.util.isNotNullOrBlank
@@ -22,7 +23,6 @@ import org.signal.libsignal.protocol.InvalidMessageException
import org.signal.network.exceptions.NonSuccessfulResponseCodeException
import org.signal.network.exceptions.PushNetworkException
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.attachments.AttachmentId
import org.thoughtcrime.securesms.attachments.DatabaseAttachment
import org.thoughtcrime.securesms.attachments.InvalidAttachmentException
import org.thoughtcrime.securesms.backup.v2.ArchiveDatabaseExecutor
@@ -4,10 +4,10 @@
*/
package org.thoughtcrime.securesms.jobs
import org.signal.core.models.database.AttachmentId
import org.signal.core.util.logging.Log
import org.signal.libsignal.protocol.InvalidMessageException
import org.signal.network.exceptions.NonSuccessfulResponseCodeException
import org.thoughtcrime.securesms.attachments.AttachmentId
import org.thoughtcrime.securesms.attachments.InvalidAttachmentException
import org.thoughtcrime.securesms.backup.v2.ArchiveDatabaseExecutor
import org.thoughtcrime.securesms.backup.v2.BackupRepository
@@ -6,13 +6,13 @@ package org.thoughtcrime.securesms.jobs
import android.net.Uri
import org.signal.core.models.backup.MediaName
import org.signal.core.models.database.AttachmentId
import org.signal.core.util.Base64
import org.signal.core.util.StreamUtil
import org.signal.core.util.androidx.DocumentFileInfo
import org.signal.core.util.logging.Log
import org.signal.libsignal.protocol.InvalidMacException
import org.signal.libsignal.protocol.InvalidMessageException
import org.thoughtcrime.securesms.attachments.AttachmentId
import org.thoughtcrime.securesms.backup.v2.ArchiveRestoreProgress
import org.thoughtcrime.securesms.backup.v2.local.ArchiveFileSystem
import org.thoughtcrime.securesms.database.AttachmentTable
@@ -6,6 +6,7 @@
package org.thoughtcrime.securesms.jobs
import org.signal.core.models.backup.MediaName
import org.signal.core.models.database.AttachmentId
import org.signal.core.util.Base64
import org.signal.core.util.Base64.decodeBase64
import org.signal.core.util.Util
@@ -17,7 +18,6 @@ import org.signal.network.NetworkResult
import org.signal.network.api.AttachmentUploadResult
import org.signal.protos.resumableuploads.ResumableUpload
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.attachments.AttachmentId
import org.thoughtcrime.securesms.attachments.AttachmentUploadUtil
import org.thoughtcrime.securesms.attachments.DatabaseAttachment
import org.thoughtcrime.securesms.backup.ArchiveUploadProgress
@@ -0,0 +1,18 @@
/*
* Copyright 2026 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.keyboard.emoji
import android.view.KeyEvent
/**
* Mapping of [EmojiKeyboardCallback] methods into a sealed event class
*/
sealed interface EmojiKeyboardEvent {
object OpenEmojiSearch : EmojiKeyboardEvent
object CloseEmojiSearch : EmojiKeyboardEvent
data class EmojiInsert(val emoji: String?) : EmojiKeyboardEvent
data class EmojiKeyEvent(val keyEvent: KeyEvent?) : EmojiKeyboardEvent
}
@@ -0,0 +1,28 @@
/*
* Copyright 2026 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.keyboard.emoji
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.receiveAsFlow
import kotlinx.coroutines.launch
/**
* Glue ViewModel that allows a component to dispatch emoji events to subcomponents.
*/
class EmojiKeyboardEventViewModel : ViewModel() {
private val eventChannel = Channel<EmojiKeyboardEvent>(Channel.BUFFERED)
val events: Flow<EmojiKeyboardEvent> = eventChannel.receiveAsFlow()
fun onEvent(event: EmojiKeyboardEvent) {
viewModelScope.launch {
eventChannel.send(event)
}
}
}
@@ -12,7 +12,7 @@ import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonProperty;
import org.thoughtcrime.securesms.attachments.Attachment;
import org.thoughtcrime.securesms.attachments.AttachmentId;
import org.signal.core.models.database.AttachmentId;
import org.thoughtcrime.securesms.attachments.DatabaseAttachment;
import org.signal.core.util.JsonUtils;
@@ -11,7 +11,6 @@ import java.util.stream.Collectors;
import org.signal.core.util.Linkifier;
import org.thoughtcrime.securesms.util.DateUtils;
import org.thoughtcrime.securesms.util.LinkUtil;
import org.thoughtcrime.securesms.util.Linkification;
import org.signal.core.util.Util;
import org.whispersystems.signalservice.api.util.OptionalUtil;
@@ -51,7 +50,7 @@ public final class LinkPreviewUtil {
* @return All URLs allowed as previews in the source text.
*/
public static @NonNull Links findValidPreviewUrls(@NonNull String text) {
List<Linkifier.DetectedLink> detected = Linkification.findWebLinks(text);
List<Linkifier.DetectedLink> detected = Linkifier.findLinks(text);
if (detected.isEmpty()) {
return Links.EMPTY;
}
@@ -1,6 +1,5 @@
package org.thoughtcrime.securesms.linkpreview;
import android.content.Context;
import android.text.TextUtils;
import androidx.annotation.NonNull;
@@ -12,7 +11,7 @@ import androidx.lifecycle.ViewModel;
import androidx.lifecycle.ViewModelProvider;
import org.signal.core.util.ThreadUtil;
import org.thoughtcrime.securesms.attachments.AttachmentId;
import org.signal.core.models.database.AttachmentId;
import org.thoughtcrime.securesms.dependencies.AppDependencies;
import org.thoughtcrime.securesms.keyvalue.SignalStore;
import org.thoughtcrime.securesms.net.RequestController;
@@ -25,7 +25,6 @@ public class SignalPinReminders {
private static final long FOUR_WEEKS = TimeUnit.DAYS.toMillis(28);
private static final NavigableSet<Long> INTERVALS = new TreeSet<Long>() {{
add(ONE_DAY);
add(THREE_DAYS);
add(ONE_WEEK);
add(TWO_WEEKS);
@@ -200,7 +200,7 @@ public abstract class BaseSvrPinFragment<ViewModel extends BaseSvrPinViewModel>
}
private void onPinSkipped() {
PinOptOutDialog.show(requireContext(), false, () -> {
PinOptOutDialog.show(requireContext(), getViewLifecycleOwner(), false, () -> {
RegistrationUtil.maybeMarkRegistrationComplete();
closeNavGraphBranch();
});
@@ -125,6 +125,6 @@ public final class SvrSplashFragment extends Fragment {
}
private void onPinSkipped() {
PinOptOutDialog.show(requireContext(), false, () -> requireActivity().finish());
PinOptOutDialog.show(requireContext(), getViewLifecycleOwner(), false, () -> requireActivity().finish());
}
}
@@ -28,7 +28,8 @@ final class LogSectionNotifications implements LogSection {
.append("New contact alerts : ").append(SignalStore.settings().isNotifyWhenContactJoinsSignal()).append("\n")
.append("In-chat sounds : ").append(SignalStore.settings().isMessageNotificationsInChatSoundsEnabled()).append("\n")
.append("Repeat alerts : ").append(SignalStore.settings().getMessageNotificationsRepeatAlerts()).append("\n")
.append("Notification display : ").append(SignalStore.settings().getMessageNotificationsPrivacy()).append("\n\n");
.append("Notification display : ").append(SignalStore.settings().getMessageNotificationsPrivacy()).append("\n")
.append("Force websocket : ").append(SignalStore.settings().getForceWebsocketMode()).append("\n\n");
if (Build.VERSION.SDK_INT >= 26) {
NotificationManager manager = ServiceUtil.getNotificationManager(context);
@@ -34,7 +34,6 @@ import org.thoughtcrime.securesms.components.ConversationSearchBottomBar;
import org.thoughtcrime.securesms.components.ProgressCard;
import org.thoughtcrime.securesms.components.SearchView;
import org.thoughtcrime.securesms.util.DynamicTheme;
import org.thoughtcrime.securesms.util.Linkification;
import org.thoughtcrime.securesms.util.LongClickCopySpan;
import org.thoughtcrime.securesms.util.LongClickMovementMethod;
import org.signal.core.ui.util.ThemeUtil;
@@ -458,7 +457,7 @@ public class SubmitDebugLogActivity extends BaseActivity {
TextView dialogView = new TextView(builder.getContext());
LongClickCopySpan longClickUrl = new LongClickCopySpan(url);
for (Linkifier.DetectedLink link : Linkification.findWebLinks(dialogText)) {
for (Linkifier.DetectedLink link : Linkifier.findLinks(dialogText)) {
spannableDialogText.setSpan(longClickUrl, link.getStart(), link.getEnd(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
}
@@ -3,6 +3,7 @@ package org.thoughtcrime.securesms.longmessage;
import android.content.Context;
import androidx.annotation.NonNull;
import androidx.annotation.WorkerThread;
import org.thoughtcrime.securesms.conversation.ConversationMessage;
import org.thoughtcrime.securesms.database.model.MessageRecord;
@@ -14,16 +15,19 @@ import org.thoughtcrime.securesms.database.model.MessageRecord;
class LongMessage {
private final ConversationMessage conversationMessage;
private final CharSequence fullBody;
LongMessage(@NonNull ConversationMessage conversationMessage) {
@WorkerThread
LongMessage(@NonNull ConversationMessage conversationMessage, @NonNull Context context) {
this.conversationMessage = conversationMessage;
this.fullBody = conversationMessage.getDisplayBody(context);
}
@NonNull MessageRecord getMessageRecord() {
return conversationMessage.getMessageRecord();
}
@NonNull CharSequence getFullBody(@NonNull Context context) {
return conversationMessage.getDisplayBody(context);
@NonNull CharSequence getFullBody() {
return fullBody;
}
}
@@ -122,16 +122,17 @@ public class LongMessageFragment extends FullScreenDialogFragment {
EmojiTextView text = bubble.findViewById(R.id.longmessage_text);
ConversationItemFooter footer = bubble.findViewById(R.id.longmessage_footer);
SpannableString body = new SpannableString(getTrimmedBody(message.get().getFullBody(requireContext())));
SpannableString body = new SpannableString(getTrimmedBody(message.get().getFullBody()));
V2ConversationItemUtils.linkifyUrlLinks(body,
true,
url -> CommunicationActions.handlePotentialGroupLinkUrl(requireActivity(), url) ||
CommunicationActions.handlePotentialProxyLinkUrl(requireActivity(), url));
bubble.setVisibility(View.VISIBLE);
text.setMovementMethod(LongClickMovementMethod.getInstance(getContext()));
text.setTextSize(TypedValue.COMPLEX_UNIT_SP, SignalStore.settings().getMessageFontSize());
text.setTextAsync(body);
text.setTextIsSelectable(true);
text.setMovementMethod(LongClickMovementMethod.getInstance(getContext()));
if (!message.get().getMessageRecord().isOutgoing()) {
text.setMentionBackgroundTint(ContextCompat.getColor(requireContext(), ThemeUtil.isDarkTheme(requireActivity()) ? R.color.core_grey_60 : R.color.core_grey_20));
@@ -37,7 +37,7 @@ class LongMessageRepository {
Optional<MmsMessageRecord> record = getMmsMessage(mmsDatabase, messageId);
if (record.isPresent()) {
final ConversationMessage resolvedMessage = LongMessageResolveerKt.resolveBody(record.get(), context);
return Optional.of(new LongMessage(resolvedMessage));
return Optional.of(new LongMessage(resolvedMessage, context));
} else {
return Optional.empty();
}
@@ -197,6 +197,9 @@ fun NavGraphBuilder.chatNavGraphBuilder(
}
arguments?.let { args ->
val backPressedState = remember { FragmentBackPressedState() }
FragmentBackHandler(backPressedState)
AndroidFragment(
clazz = ConversationSettingsNavHostFragment::class.java,
fragmentState = fragmentState,
@@ -206,7 +209,9 @@ fun NavGraphBuilder.chatNavGraphBuilder(
.background(MaterialTheme.colorScheme.background)
.statusBarsPadding()
.navigationBarsPadding()
)
) { fragment ->
backPressedState.attach(fragment)
}
}
}
}
@@ -90,7 +90,10 @@ sealed interface MainNavigationDetailLocation : Parcelable {
}
@Serializable
data class ConversationSettings(val recipientId: RecipientId) : Chats {
data class ConversationSettings(
val recipientId: RecipientId,
override val isContentRoot: Boolean = false
) : Chats {
@Transient
@IgnoredOnParcel
override val controllerKey: RecipientId = recipientId
@@ -12,6 +12,7 @@ import android.location.Address;
import android.location.Geocoder;
import android.net.Uri;
import android.os.AsyncTask;
import android.os.Build;
import android.os.Bundle;
import android.view.View;
import android.view.animation.OvershootInterpolator;
@@ -24,11 +25,11 @@ import androidx.core.content.ContextCompat;
import androidx.core.view.ViewCompat;
import androidx.fragment.app.Fragment;
import com.google.accompanist.permissions.PermissionsUtilKt;
import com.google.android.gms.maps.CameraUpdateFactory;
import com.google.android.gms.maps.GoogleMap;
import com.google.android.gms.maps.MapView;
import com.google.android.gms.maps.SupportMapFragment;
import com.google.android.gms.maps.model.CircleOptions;
import com.google.android.gms.maps.model.LatLng;
import com.google.android.gms.maps.model.MapStyleOptions;
@@ -72,7 +73,6 @@ public final class PlacePickerActivity extends AppCompatActivity {
private Address currentAddress;
private LatLng initialLocation;
private LatLng currentLocation = new LatLng(0, 0);
private LatLng userLocation;
private AddressLookup addressLookup;
private GoogleMap googleMap;
@@ -103,10 +103,7 @@ public final class PlacePickerActivity extends AppCompatActivity {
ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_COARSE_LOCATION) == PackageManager.PERMISSION_GRANTED)
{
new LocationRetriever(this, this, location -> {
LatLng latLng = new LatLng(location.getLatitude(), location.getLongitude());
userLocation = latLng;
setInitialLocation(latLng);
drawUserLocationIfPossible();
setInitialLocation(new LatLng(location.getLatitude(), location.getLongitude()));
}, () -> {
Log.w(TAG, "Failed to get location.");
setInitialLocation(PRIME_MERIDIAN);
@@ -133,6 +130,8 @@ public final class PlacePickerActivity extends AppCompatActivity {
}
}
enableMyLocationButtonIfHaveThePermission(googleMap);
googleMap.setOnCameraMoveStartedListener(i -> {
markerImage.animate()
.translationY(-75f)
@@ -171,18 +170,6 @@ public final class PlacePickerActivity extends AppCompatActivity {
this.googleMap = googleMap;
moveMapToInitialIfPossible();
drawUserLocationIfPossible();
}
private void drawUserLocationIfPossible() {
if (userLocation != null && googleMap != null) {
googleMap.addCircle(new CircleOptions()
.center(userLocation)
.radius(12)
.strokeWidth(4f)
.strokeColor(Color.WHITE)
.fillColor(Color.parseColor("#4285F4")));
}
}
private void moveMapToInitialIfPossible() {
@@ -229,6 +216,12 @@ public final class PlacePickerActivity extends AppCompatActivity {
});
}
private void enableMyLocationButtonIfHaveThePermission(GoogleMap googleMap) {
if (checkSelfPermission(Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED || checkSelfPermission(Manifest.permission.ACCESS_COARSE_LOCATION) == PackageManager.PERMISSION_GRANTED) {
googleMap.setMyLocationEnabled(true);
}
}
private void lookupAddress(@Nullable LatLng target) {
if (addressLookup != null) {
addressLookup.cancel(true);
@@ -237,9 +230,13 @@ public final class PlacePickerActivity extends AppCompatActivity {
addressLookup.execute(target);
}
@SuppressLint("MissingPermission")
@Override
protected void onPause() {
super.onPause();
if (googleMap != null && (checkSelfPermission(Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED || checkSelfPermission(Manifest.permission.ACCESS_COARSE_LOCATION) == PackageManager.PERMISSION_GRANTED)) {
googleMap.setMyLocationEnabled(false);
}
if (addressLookup != null) {
addressLookup.cancel(true);
}
@@ -338,10 +338,16 @@ public final class MediaOverviewPageFragment extends LoggingFragment
MediaIntentFactory.MediaPreviewArgs args = new MediaIntentFactory.MediaPreviewArgs(
threadId,
mediaRecord.getDate(),
mediaRecord.getMessageId(),
mediaRecord.getRecipientId(),
mediaRecord.getThreadRecipientId(),
mediaRecord.isOutgoing(),
Objects.requireNonNull(mediaRecord.getAttachment().getDisplayUri()),
mediaRecord.getAttachment().getUri(),
mediaRecord.getContentType(),
mediaRecord.getAttachment().size,
mediaRecord.getAttachment().caption,
null,
true,
true,
threadId == MediaTable.ALL_THREADS,

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