Compare commits

...

55 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
Greyson Parrelli d6871f8dc2 Bump version to 8.15.3 2026-06-15 19:35:19 -04:00
Greyson Parrelli d93543510f Update baseline profile. 2026-06-15 19:35:19 -04:00
Greyson Parrelli 69f7ad28ec Update translations and other static files. 2026-06-15 19:35:19 -04:00
Greyson Parrelli 8c2ff2f1c2 Improve handling of unlinked device during send. 2026-06-15 19:35:19 -04:00
Cody Henthorne 5e8cebdc87 Bump version to 8.15.2 2026-06-12 15:06:49 -04:00
Cody Henthorne c8f9c41cea Update baseline profile. 2026-06-12 15:00:45 -04:00
Cody Henthorne 647dc23de6 Update translations and other static files. 2026-06-12 14:53:10 -04:00
Cody Henthorne b0531247c3 Prevent crash when building shortcuts for large groups. 2026-06-12 14:35:15 -04:00
Greyson Parrelli f08a20d0a6 Render unread divider when the only unread message is the newest in the thread. 2026-06-12 14:10:40 -03:00
Cody Henthorne 16232e2f9f Fix transfer control showing stale data or not responding. 2026-06-12 13:01:18 -04:00
Michelle Tang fc856dd500 Turn off KT. 2026-06-12 11:22:37 -04:00
Alex Hart 73f81075ce Removes second dialog and adds learnmore. 2026-06-12 11:16:19 -03:00
431 changed files with 7796 additions and 42036 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 = 1705
val canonicalVersionName = "8.15.1"
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()
@@ -241,5 +241,6 @@ class OtherClient(val serviceId: ServiceId, val e164: String, val identityKeyPai
override fun deleteAllStaleOneTimeKyberPreKeys(threshold: Long, minCount: Int) = throw UnsupportedOperationException()
override fun loadLastResortKyberPreKeys(): List<KyberPreKeyRecord> = throw UnsupportedOperationException()
override fun isMultiDevice(): Boolean = throw UnsupportedOperationException()
override fun setMultiDevice(isMultiDevice: Boolean) = throw UnsupportedOperationException()
}
}
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
@@ -157,7 +157,6 @@ fun MessageBackupsKeyRecordScreen(
val url = stringResource(R.string.recovery_key_phishing_support_url)
val events: (RecoveryKeyWarningSheetEvent) -> Unit = {
when (it) {
RecoveryKeyWarningSheetEvent.DoNotShareClick -> error("Not supported")
RecoveryKeyWarningSheetEvent.GotItClick -> {
onCopyToClipboardClick(backupKeyString)
displayRecoveryKeyCopyWarning = false
@@ -167,7 +166,7 @@ fun MessageBackupsKeyRecordScreen(
displayRecoveryKeyCopyWarning = false
}
RecoveryKeyWarningSheetEvent.PasteKeyClick -> error("Not supported")
RecoveryKeyWarningSheetEvent.DoNotShareClick -> error("Not supported")
RecoveryKeyWarningSheetEvent.ShareKeyClick -> error("Not supported")
}
}
@@ -5,22 +5,16 @@
package org.thoughtcrime.securesms.backup.v2.ui.warning
import android.app.Dialog
import android.content.DialogInterface
import android.graphics.Color
import android.graphics.drawable.ColorDrawable
import android.os.Bundle
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.ModalBottomSheet
import androidx.compose.material3.rememberModalBottomSheetState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.fragment.app.setFragmentResult
import org.signal.core.ui.compose.BottomSheets
import org.signal.core.ui.compose.ComposeFullScreenDialogFragment
import org.signal.core.ui.compose.ComposeBottomSheetDialogFragment
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.backup.v2.ui.warning.RecoveryKeyPasteWarningFragment.Companion.REQUEST_KEY
import org.thoughtcrime.securesms.util.CommunicationActions
/**
* Displayed via the [org.thoughtcrime.securesms.components.settings.conversation.ConversationSettingsFragment] whenever the user
@@ -30,7 +24,7 @@ import org.signal.core.ui.compose.ComposeFullScreenDialogFragment
* indicating whether the user chose to proceed with the paste. The host can rely on this firing for
* every dismissal path (paste, decline, or cancel) to restore its own state.
*/
class RecoveryKeyPasteWarningFragment : ComposeFullScreenDialogFragment() {
class RecoveryKeyPasteWarningFragment : ComposeBottomSheetDialogFragment() {
companion object {
const val REQUEST_KEY = "recovery_key_request"
@@ -38,13 +32,6 @@ class RecoveryKeyPasteWarningFragment : ComposeFullScreenDialogFragment() {
private var shouldPaste = false
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
return super.onCreateDialog(savedInstanceState).apply {
window?.setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT))
window?.setWindowAnimations(0)
}
}
override fun onDismiss(dialog: DialogInterface) {
setFragmentResult(
REQUEST_KEY,
@@ -56,10 +43,10 @@ class RecoveryKeyPasteWarningFragment : ComposeFullScreenDialogFragment() {
super.onDismiss(dialog)
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
override fun DialogContent() {
var isDisplayingFinalWarningDialog by remember { mutableStateOf(false) }
override fun SheetContent() {
val context = LocalContext.current
val url = stringResource(R.string.recovery_key_phishing_support_url)
val eventHandler: (RecoveryKeyWarningSheetEvent) -> Unit = {
when (it) {
@@ -67,34 +54,25 @@ class RecoveryKeyPasteWarningFragment : ComposeFullScreenDialogFragment() {
dismissAllowingStateLoss()
}
RecoveryKeyWarningSheetEvent.GotItClick -> error("Not supported for paste")
RecoveryKeyWarningSheetEvent.LearnMoreClick -> error("Not supported for paste")
RecoveryKeyWarningSheetEvent.PasteKeyClick -> {
shouldPaste = true
RecoveryKeyWarningSheetEvent.GotItClick -> {
error("Not supported for paste")
}
RecoveryKeyWarningSheetEvent.LearnMoreClick -> {
CommunicationActions.openBrowserLink(context, url)
dismissAllowingStateLoss()
}
RecoveryKeyWarningSheetEvent.ShareKeyClick -> {
isDisplayingFinalWarningDialog = true
shouldPaste = true
dismissAllowingStateLoss()
}
}
}
if (isDisplayingFinalWarningDialog) {
RecoveryKeyWarningDialog(
events = eventHandler
)
} else {
ModalBottomSheet(
onDismissRequest = { dismissAllowingStateLoss() },
dragHandle = { BottomSheets.Handle() },
sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)
) {
RecoveryKeyWarningSheetContent(
clipStage = ClipStage.PASTE,
events = eventHandler
)
}
}
RecoveryKeyWarningSheetContent(
clipStage = ClipStage.PASTE,
events = eventHandler
)
}
}
@@ -12,6 +12,7 @@ import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
@@ -22,16 +23,17 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.LinkAnnotation
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.TextLinkStyles
import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.withLink
import androidx.compose.ui.text.withStyle
import androidx.compose.ui.unit.dp
import org.signal.core.ui.compose.Buttons
import org.signal.core.ui.compose.DayNightPreviews
import org.signal.core.ui.compose.Dialogs
import org.signal.core.ui.compose.Previews
import org.signal.core.ui.compose.horizontalGutters
import org.thoughtcrime.securesms.R
@@ -64,17 +66,32 @@ fun RecoveryKeyWarningSheetContent(
modifier = Modifier.padding(bottom = 12.dp)
)
val signalWillNeverMessageYou = stringResource(R.string.RecoveryKeyWarningSheetContent__signal_will_never_message_you)
val recoveryKeyWarningBody = stringResource(R.string.RecoveryKeyWarningSheetContent__for_your_recovery_key_never_respond)
Text(
text = buildAnnotatedString {
withStyle(style = SpanStyle(fontWeight = FontWeight.Bold)) {
append(signalWillNeverMessageYou)
append(stringResource(R.string.RecoveryKeyWarningSheetContent__signal_will_never_message_you))
}
append(" ")
append(recoveryKeyWarningBody)
append(stringResource(R.string.RecoveryKeyWarningSheetContent__for_your_recovery_key_never_respond))
if (clipStage == ClipStage.PASTE) {
append(" ")
withLink(
link = LinkAnnotation.Clickable(
tag = "learn-more",
styles = TextLinkStyles(
style = SpanStyle(
color = MaterialTheme.colorScheme.primary
)
)
) {
events(RecoveryKeyWarningSheetEvent.LearnMoreClick)
}
) {
append(stringResource(R.string.RecoveryKeyWarningSheetContent__learn_more_period))
}
}
},
textAlign = TextAlign.Center,
color = MaterialTheme.colorScheme.onSurfaceVariant,
@@ -113,40 +130,18 @@ fun PasteActionButtons(events: (RecoveryKeyWarningSheetEvent) -> Unit) {
Text(text = stringResource(R.string.RecoveryKeyWarningSheetContent__do_not_share_key))
}
TextButton(onClick = {
events(RecoveryKeyWarningSheetEvent.ShareKeyClick)
}) {
TextButton(
colors = ButtonDefaults.textButtonColors(
contentColor = MaterialTheme.colorScheme.error
),
onClick = {
events(RecoveryKeyWarningSheetEvent.ShareKeyClick)
}
) {
Text(text = stringResource(R.string.RecoveryKeyWarningSheetContent__share_key))
}
}
@Composable
fun RecoveryKeyWarningDialog(events: (RecoveryKeyWarningSheetEvent) -> Unit) {
val bodyIntro = stringResource(R.string.RecoveryKeyWarningDialog__do_not_share_your_recovery_key_with_anyone)
val bodyEmphasis = stringResource(R.string.RecoveryKeyWarningDialog__signal_will_never_message_you_for_your_recovery_key)
val bodyOutro = stringResource(R.string.RecoveryKeyWarningDialog__never_respond_to_a_chat)
Dialogs.SimpleAlertDialog(
title = AnnotatedString(stringResource(R.string.RecoveryKeyWarningDialog__do_not_share_recovery_key)),
body = buildAnnotatedString {
append(bodyIntro)
append(" ")
withStyle(style = SpanStyle(fontWeight = FontWeight.Bold)) {
append(bodyEmphasis)
}
append(" ")
append(bodyOutro)
},
confirm = AnnotatedString(stringResource(R.string.RecoveryKeyWarningDialog__paste_key)),
confirmColor = MaterialTheme.colorScheme.error,
dismiss = AnnotatedString(stringResource(R.string.RecoveryKeyWarningDialog__dont_share)),
onConfirm = { events(RecoveryKeyWarningSheetEvent.PasteKeyClick) },
onDeny = { events(RecoveryKeyWarningSheetEvent.DoNotShareClick) }
)
}
enum class ClipStage {
COPY,
PASTE
@@ -175,11 +170,3 @@ private fun RecoveryKeyWarningSheetContentPastePreview() {
)
}
}
@DayNightPreviews
@Composable
private fun RecoveryKeyWarningDialogPreview() {
Previews.Preview {
RecoveryKeyWarningDialog(events = {})
}
}
@@ -8,7 +8,6 @@ package org.thoughtcrime.securesms.backup.v2.ui.warning
sealed interface RecoveryKeyWarningSheetEvent {
data object DoNotShareClick : RecoveryKeyWarningSheetEvent
data object ShareKeyClick : RecoveryKeyWarningSheetEvent
data object PasteKeyClick : RecoveryKeyWarningSheetEvent
data object GotItClick : RecoveryKeyWarningSheetEvent
data object LearnMoreClick : RecoveryKeyWarningSheetEvent
}
@@ -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
@@ -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
}
@@ -90,13 +90,22 @@ class TransferControlView @JvmOverloads constructor(context: Context, attrs: Att
val newRender = TransferControls.deriveRenderState(newState)
state = newState
if (oldRender != newRender) {
verboseLog { "render $oldRender -> $newRender slides=[${slidesAsLogString(newState.slides)}]" }
if (oldRender == newRender) {
return
}
verboseLog { "render $oldRender -> $newRender slides=[${slidesAsLogString(newState.slides)}]" }
if (oldRender is TransferControlsRenderState.InProgress && oldRender.isProgressOnlyDifference(newRender)) {
progressUpdateDebouncer.publish {
renderState = newRender
if (newRender !is TransferControlsRenderState.Gone) {
visibility = VISIBLE
}
visibility = VISIBLE
}
} else {
progressUpdateDebouncer.clear()
renderState = newRender
if (newRender !is TransferControlsRenderState.Gone) {
visibility = VISIBLE
}
}
}
@@ -111,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())
}
}
}
@@ -218,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)
}
@@ -319,7 +321,11 @@ sealed interface TransferControlsRenderState {
val showPlayButton: Boolean,
val cancelable: Boolean,
val label: TransferControls.ProgressLabel?
) : TransferControlsRenderState
) : TransferControlsRenderState {
fun isProgressOnlyDifference(other: TransferControlsRenderState): Boolean {
return other is InProgress && copy(progress = other.progress, label = other.label) == other
}
}
data class Retry(
val isUpload: Boolean
@@ -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 {
@@ -65,10 +65,8 @@ public class ConversationRepository {
firstUnreadPosition = SignalDatabase.messages().getMessagePositionByDateReceivedTimestamp(threadId, firstUnreadDateReceived, false);
}
if (firstUnreadPosition <= 0) {
firstUnreadId = -1;
firstUnreadDateReceived = 0;
}
// A position of 0 means the oldest unread message is the newest message in the thread (e.g. a single unread). That
// is a valid divider anchor, so we keep firstUnreadId; it just means we don't scroll up to reach it.
if (firstUnreadDateReceived == 0 && lastScrolled > 0) {
lastScrolledPosition = SignalDatabase.messages().getMessagePositionByDateReceivedTimestamp(threadId, lastScrolled, true);
@@ -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
@@ -58,6 +58,11 @@ public class SignalServiceAccountDataStoreImpl implements SignalServiceAccountDa
return SignalStore.account().isMultiDevice();
}
@Override
public void setMultiDevice(boolean isMultiDevice) {
SignalStore.account().setMultiDevice(isMultiDevice);
}
@Override
public IdentityKeyPair getIdentityKeyPair() {
return identityKeyStore.getIdentityKeyPair();
@@ -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
@@ -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));

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