mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-07-13 17:23:56 +01:00
Turn on collapsing chat events for internal users.
This commit is contained in:
committed by
Cody Henthorne
parent
94e3dabc20
commit
c3b8768570
+4
@@ -358,5 +358,9 @@ class V2ConversationItemShapeTest {
|
||||
override fun onToggleVote(poll: PollRecord, pollOption: PollOption, isChecked: Boolean) = Unit
|
||||
|
||||
override fun onViewPinnedMessage(messageId: Long) = Unit
|
||||
|
||||
override fun onExpandEvents(messageId: Long) = Unit
|
||||
|
||||
override fun onCollapseEvents(messageId: Long) = Unit
|
||||
}
|
||||
}
|
||||
|
||||
+294
@@ -0,0 +1,294 @@
|
||||
package org.thoughtcrime.securesms.database
|
||||
|
||||
import androidx.core.content.contentValuesOf
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Before
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.signal.core.models.ServiceId.ACI
|
||||
import org.thoughtcrime.securesms.mms.OutgoingMessage
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
import org.thoughtcrime.securesms.testing.SignalDatabaseRule
|
||||
import java.util.UUID
|
||||
import kotlin.time.Duration.Companion.days
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class CollapsingMessagesTests {
|
||||
|
||||
private lateinit var message: MessageTable
|
||||
private lateinit var thread: ThreadTable
|
||||
|
||||
@Rule
|
||||
@JvmField
|
||||
val databaseRule = SignalDatabaseRule()
|
||||
|
||||
private lateinit var alice: RecipientId
|
||||
private var aliceThread: Long = 0
|
||||
|
||||
private lateinit var bob: RecipientId
|
||||
|
||||
@Before
|
||||
fun setUp() {
|
||||
message = SignalDatabase.messages
|
||||
message.deleteAllThreads()
|
||||
|
||||
thread = SignalDatabase.threads
|
||||
|
||||
alice = SignalDatabase.recipients.getOrInsertFromServiceId(ACI.from(UUID.randomUUID()))
|
||||
aliceThread = SignalDatabase.threads.getOrCreateThreadIdFor(Recipient.resolved(alice))
|
||||
bob = SignalDatabase.recipients.getOrInsertFromServiceId(ACI.from(UUID.randomUUID()))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenCollapsibleMessage_whenIInsert_thenItBecomesHead() {
|
||||
val messageId = message.insertCallLog(alice, MessageTypes.INCOMING_AUDIO_CALL_TYPE, 1000L, false).messageId
|
||||
|
||||
val msg = message.getMessageRecord(messageId)
|
||||
assertEquals(CollapsedState.HEAD_COLLAPSED, msg.collapsedState)
|
||||
assertEquals(messageId, msg.collapsedHeadId)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenSameCollapsibleTypes_whenIInsert_thenAllCollapseUnderHead() {
|
||||
val messageId1 = message.insertCallLog(alice, MessageTypes.INCOMING_AUDIO_CALL_TYPE, 1000L, false).messageId
|
||||
val messageId2 = message.insertCallLog(alice, MessageTypes.INCOMING_AUDIO_CALL_TYPE, 2000L, false).messageId
|
||||
val messageId3 = message.insertCallLog(alice, MessageTypes.INCOMING_AUDIO_CALL_TYPE, 3000L, false).messageId
|
||||
|
||||
val msg1 = message.getMessageRecord(messageId1)
|
||||
val msg2 = message.getMessageRecord(messageId2)
|
||||
val msg3 = message.getMessageRecord(messageId3)
|
||||
|
||||
assertEquals(CollapsedState.HEAD_COLLAPSED, msg1.collapsedState)
|
||||
assertEquals(messageId1, msg1.collapsedHeadId)
|
||||
|
||||
assertEquals(CollapsedState.PENDING_COLLAPSED, msg2.collapsedState)
|
||||
assertEquals(messageId1, msg2.collapsedHeadId)
|
||||
|
||||
assertEquals(CollapsedState.PENDING_COLLAPSED, msg3.collapsedState)
|
||||
assertEquals(messageId1, msg3.collapsedHeadId)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenDifferentCollapsedTypes_whenIInsert_thenNoCollapsing() {
|
||||
val messageId1 = message.insertCallLog(alice, MessageTypes.INCOMING_AUDIO_CALL_TYPE, 1000L, false).messageId
|
||||
val messageId2 = MmsHelper.insert(message = OutgoingMessage.identityVerifiedMessage(Recipient.resolved(alice), 2000L), threadId = aliceThread)
|
||||
|
||||
val msg1 = message.getMessageRecord(messageId1)
|
||||
val msg2 = message.getMessageRecord(messageId2)
|
||||
|
||||
assertEquals(CollapsedState.HEAD_COLLAPSED, msg1.collapsedState)
|
||||
assertEquals(messageId1, msg1.collapsedHeadId)
|
||||
|
||||
assertEquals(CollapsedState.HEAD_COLLAPSED, msg2.collapsedState)
|
||||
assertEquals(messageId2, msg2.collapsedHeadId)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenNonCollapsibleTypes_whenIInsert_thenNoCollapsing() {
|
||||
val messageId = MmsHelper.insert(recipient = Recipient.resolved(alice), sentTimeMillis = 1000L)
|
||||
|
||||
val msg = message.getMessageRecord(messageId)
|
||||
assertEquals(CollapsedState.NONE, msg.collapsedState)
|
||||
assertEquals(0, msg.collapsedHeadId)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenMessagesOnDifferentDays_whenIInsert_thenNoCollapsing() {
|
||||
val messageId1 = message.insertCallLog(alice, MessageTypes.INCOMING_AUDIO_CALL_TYPE, 1000L, false).messageId
|
||||
|
||||
message.writableDatabase.update(
|
||||
MessageTable.TABLE_NAME,
|
||||
contentValuesOf(MessageTable.DATE_RECEIVED to (System.currentTimeMillis() - 1.days.inWholeMilliseconds)),
|
||||
"${MessageTable.ID} = ?",
|
||||
arrayOf(messageId1.toString())
|
||||
)
|
||||
|
||||
val messageId2 = message.insertCallLog(alice, MessageTypes.INCOMING_AUDIO_CALL_TYPE, 2000L, false).messageId
|
||||
|
||||
val msg2 = message.getMessageRecord(messageId2)
|
||||
assertEquals(CollapsedState.HEAD_COLLAPSED, msg2.collapsedState)
|
||||
assertEquals(messageId2, msg2.collapsedHeadId)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenRegularMessageBetweenCollapsed_whenIInsertCollapsed_thenNoCollapsing() {
|
||||
val messageId1 = message.insertCallLog(alice, MessageTypes.INCOMING_AUDIO_CALL_TYPE, 1000L, false).messageId
|
||||
val messageId2 = MmsHelper.insert(recipient = Recipient.resolved(alice), sentTimeMillis = 2000L)
|
||||
val messageId3 = message.insertCallLog(alice, MessageTypes.INCOMING_AUDIO_CALL_TYPE, 3000L, false).messageId
|
||||
|
||||
val msg1 = message.getMessageRecord(messageId1)
|
||||
val msg2 = message.getMessageRecord(messageId2)
|
||||
val msg3 = message.getMessageRecord(messageId3)
|
||||
|
||||
assertEquals(CollapsedState.HEAD_COLLAPSED, msg1.collapsedState)
|
||||
assertEquals(messageId1, msg1.collapsedHeadId)
|
||||
|
||||
assertEquals(CollapsedState.NONE, msg2.collapsedState)
|
||||
assertEquals(0, msg2.collapsedHeadId)
|
||||
|
||||
assertEquals(CollapsedState.HEAD_COLLAPSED, msg3.collapsedState)
|
||||
assertEquals(messageId3, msg3.collapsedHeadId)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenDifferentThreads_whenIInsertCollapsed_thenNoCollapsing() {
|
||||
val messageId1 = message.insertCallLog(alice, MessageTypes.INCOMING_AUDIO_CALL_TYPE, 1000L, false).messageId
|
||||
val messageId2 = message.insertCallLog(bob, MessageTypes.INCOMING_AUDIO_CALL_TYPE, 2000L, false).messageId
|
||||
|
||||
val msg1 = message.getMessageRecord(messageId1)
|
||||
val msg2 = message.getMessageRecord(messageId2)
|
||||
|
||||
assertEquals(CollapsedState.HEAD_COLLAPSED, msg1.collapsedState)
|
||||
assertEquals(messageId1, msg1.collapsedHeadId)
|
||||
|
||||
assertEquals(CollapsedState.HEAD_COLLAPSED, msg2.collapsedState)
|
||||
assertEquals(messageId2, msg2.collapsedHeadId)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenCollapsedMessages_whenIDeleteFirstMessage_thenNextMessageBecomesHead() {
|
||||
val messageId1 = message.insertCallLog(alice, MessageTypes.INCOMING_AUDIO_CALL_TYPE, 1000L, false).messageId
|
||||
val messageId2 = message.insertCallLog(alice, MessageTypes.INCOMING_AUDIO_CALL_TYPE, 2000L, false).messageId
|
||||
val messageId3 = message.insertCallLog(alice, MessageTypes.INCOMING_AUDIO_CALL_TYPE, 3000L, false).messageId
|
||||
|
||||
message.deleteMessage(messageId1, aliceThread)
|
||||
|
||||
val msg2 = message.getMessageRecord(messageId2)
|
||||
val msg3 = message.getMessageRecord(messageId3)
|
||||
|
||||
assertEquals(CollapsedState.HEAD_COLLAPSED, msg2.collapsedState)
|
||||
assertEquals(messageId2, msg2.collapsedHeadId)
|
||||
|
||||
assertEquals(CollapsedState.PENDING_COLLAPSED, msg3.collapsedState)
|
||||
assertEquals(messageId2, msg3.collapsedHeadId)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenCollapsedMessages_whenIDeleteNonFirstMessage_thenFirstMessageStaysHead() {
|
||||
val messageId1 = message.insertCallLog(alice, MessageTypes.INCOMING_AUDIO_CALL_TYPE, 1000L, false).messageId
|
||||
val messageId2 = message.insertCallLog(alice, MessageTypes.INCOMING_AUDIO_CALL_TYPE, 2000L, false).messageId
|
||||
val messageId3 = message.insertCallLog(alice, MessageTypes.INCOMING_AUDIO_CALL_TYPE, 3000L, false).messageId
|
||||
|
||||
message.deleteMessage(messageId2, aliceThread)
|
||||
|
||||
val msg1 = message.getMessageRecord(messageId1)
|
||||
val msg3 = message.getMessageRecord(messageId3)
|
||||
|
||||
assertEquals(CollapsedState.HEAD_COLLAPSED, msg1.collapsedState)
|
||||
assertEquals(messageId1, msg1.collapsedHeadId)
|
||||
|
||||
assertEquals(CollapsedState.PENDING_COLLAPSED, msg3.collapsedState)
|
||||
assertEquals(messageId1, msg3.collapsedHeadId)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenTwoCollapsingTypes_whenIDeleteHeadOfFirstGroup_thenSecondGroupIsUnchanged() {
|
||||
val call1 = message.insertCallLog(alice, MessageTypes.INCOMING_AUDIO_CALL_TYPE, 1000L, false)
|
||||
val call2 = message.insertCallLog(alice, MessageTypes.INCOMING_AUDIO_CALL_TYPE, 2000L, false)
|
||||
|
||||
val recipient = Recipient.resolved(alice)
|
||||
val identity1Id = MmsHelper.insert(message = OutgoingMessage.identityVerifiedMessage(recipient, 3000L), threadId = call1.threadId)
|
||||
val identity2Id = MmsHelper.insert(message = OutgoingMessage.identityVerifiedMessage(recipient, 4000L), threadId = call1.threadId)
|
||||
|
||||
message.deleteMessage(call1.messageId, call1.threadId)
|
||||
|
||||
val msgCall2 = message.getMessageRecord(call2.messageId)
|
||||
assertEquals(CollapsedState.HEAD_COLLAPSED, msgCall2.collapsedState)
|
||||
assertEquals(call2.messageId, msgCall2.collapsedHeadId)
|
||||
|
||||
val msgIdentity1 = message.getMessageRecord(identity1Id)
|
||||
val msgIdentity2 = message.getMessageRecord(identity2Id)
|
||||
assertEquals(CollapsedState.HEAD_COLLAPSED, msgIdentity1.collapsedState)
|
||||
assertEquals(identity1Id, msgIdentity1.collapsedHeadId)
|
||||
assertEquals(CollapsedState.PENDING_COLLAPSED, msgIdentity2.collapsedState)
|
||||
assertEquals(identity1Id, msgIdentity2.collapsedHeadId)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenPendingCollapsingEvents_whenIMarkSeenAtASpecificTime_thenEverythingBeforeThatTimeIsCollapsed() {
|
||||
val call1 = message.insertCallLog(alice, MessageTypes.INCOMING_AUDIO_CALL_TYPE, 1000L, false)
|
||||
val call2 = message.insertCallLog(alice, MessageTypes.INCOMING_AUDIO_CALL_TYPE, 2000L, false)
|
||||
|
||||
message.collapsePendingCollapsibleEvents(aliceThread, System.currentTimeMillis())
|
||||
|
||||
val call3 = message.insertCallLog(alice, MessageTypes.INCOMING_AUDIO_CALL_TYPE, 3000L, false)
|
||||
|
||||
val msgCall1 = message.getMessageRecord(call1.messageId)
|
||||
val msgCall2 = message.getMessageRecord(call2.messageId)
|
||||
val msgCall3 = message.getMessageRecord(call3.messageId)
|
||||
|
||||
assertEquals(CollapsedState.HEAD_COLLAPSED, msgCall1.collapsedState)
|
||||
assertEquals(CollapsedState.COLLAPSED, msgCall2.collapsedState)
|
||||
assertEquals(CollapsedState.PENDING_COLLAPSED, msgCall3.collapsedState)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenPendingCollapsingEvents_whenIMarkAllAsSeen_thenEverythingIsCollapsed() {
|
||||
val call1 = message.insertCallLog(alice, MessageTypes.INCOMING_AUDIO_CALL_TYPE, 1000L, false)
|
||||
val call2 = message.insertCallLog(alice, MessageTypes.INCOMING_AUDIO_CALL_TYPE, 2000L, false)
|
||||
|
||||
message.collapseAllPendingCollapsibleEvents()
|
||||
|
||||
val call3 = message.insertCallLog(alice, MessageTypes.INCOMING_AUDIO_CALL_TYPE, 3000L, false)
|
||||
|
||||
val msgCall1 = message.getMessageRecord(call1.messageId)
|
||||
val msgCall2 = message.getMessageRecord(call2.messageId)
|
||||
val msgCall3 = message.getMessageRecord(call3.messageId)
|
||||
|
||||
assertEquals(CollapsedState.HEAD_COLLAPSED, msgCall1.collapsedState)
|
||||
assertEquals(CollapsedState.COLLAPSED, msgCall2.collapsedState)
|
||||
assertEquals(CollapsedState.PENDING_COLLAPSED, msgCall3.collapsedState)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenCollapsedEvents_whenITrimTheThreadByCount_thenIExpectANewHead() {
|
||||
val call1 = message.insertCallLog(alice, MessageTypes.INCOMING_AUDIO_CALL_TYPE, 1000L, false)
|
||||
val call2 = message.insertCallLog(alice, MessageTypes.INCOMING_AUDIO_CALL_TYPE, 2000L, false)
|
||||
val call3 = message.insertCallLog(alice, MessageTypes.INCOMING_AUDIO_CALL_TYPE, 3000L, false)
|
||||
val call4 = message.insertCallLog(alice, MessageTypes.INCOMING_AUDIO_CALL_TYPE, 4000L, false)
|
||||
|
||||
val msgCall1 = message.getMessageRecord(call1.messageId)
|
||||
val msgCall2 = message.getMessageRecord(call2.messageId)
|
||||
|
||||
assertEquals(CollapsedState.HEAD_COLLAPSED, msgCall1.collapsedState)
|
||||
assertEquals(CollapsedState.PENDING_COLLAPSED, msgCall2.collapsedState)
|
||||
|
||||
thread.trimThread(threadId = aliceThread, syncThreadTrimDeletes = false, length = 2)
|
||||
|
||||
val msgCall3 = message.getMessageRecord(call3.messageId)
|
||||
val msgCall4 = message.getMessageRecord(call4.messageId)
|
||||
|
||||
assertEquals(CollapsedState.HEAD_COLLAPSED, msgCall3.collapsedState)
|
||||
assertEquals(CollapsedState.PENDING_COLLAPSED, msgCall4.collapsedState)
|
||||
assertEquals(call3.messageId, msgCall4.collapsedHeadId)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenCollapsedEvents_whenITrimTheThreadByDate_thenIExpectANewHead() {
|
||||
val call1 = message.insertCallLog(alice, MessageTypes.INCOMING_AUDIO_CALL_TYPE, 1000L, false)
|
||||
val call2 = message.insertCallLog(alice, MessageTypes.INCOMING_AUDIO_CALL_TYPE, 2000L, false)
|
||||
val trimBeforeDate = System.currentTimeMillis()
|
||||
val call3 = message.insertCallLog(alice, MessageTypes.INCOMING_AUDIO_CALL_TYPE, 3000L, false)
|
||||
val call4 = message.insertCallLog(alice, MessageTypes.INCOMING_AUDIO_CALL_TYPE, 4000L, false)
|
||||
|
||||
message.collapsePendingCollapsibleEvents(aliceThread, System.currentTimeMillis())
|
||||
|
||||
val msgCall1 = message.getMessageRecord(call1.messageId)
|
||||
val msgCall2 = message.getMessageRecord(call2.messageId)
|
||||
|
||||
assertEquals(CollapsedState.HEAD_COLLAPSED, msgCall1.collapsedState)
|
||||
assertEquals(CollapsedState.COLLAPSED, msgCall2.collapsedState)
|
||||
|
||||
thread.trimThread(threadId = aliceThread, syncThreadTrimDeletes = false, trimBeforeDate = trimBeforeDate)
|
||||
|
||||
val msgCall3 = message.getMessageRecord(call3.messageId)
|
||||
val msgCall4 = message.getMessageRecord(call4.messageId)
|
||||
|
||||
assertEquals(CollapsedState.HEAD_COLLAPSED, msgCall3.collapsedState)
|
||||
assertEquals(CollapsedState.COLLAPSED, msgCall4.collapsedState)
|
||||
assertEquals(call3.messageId, msgCall4.collapsedHeadId)
|
||||
}
|
||||
}
|
||||
+3
@@ -9,6 +9,7 @@ import org.thoughtcrime.securesms.conversation.ConversationMessage.ConversationM
|
||||
import org.thoughtcrime.securesms.conversation.v2.data.ConversationElementKey
|
||||
import org.thoughtcrime.securesms.conversation.v2.data.IncomingTextOnly
|
||||
import org.thoughtcrime.securesms.conversation.v2.data.OutgoingTextOnly
|
||||
import org.thoughtcrime.securesms.database.CollapsedState
|
||||
import org.thoughtcrime.securesms.database.MessageTypes
|
||||
import org.thoughtcrime.securesms.database.model.MmsMessageRecord
|
||||
import org.thoughtcrime.securesms.database.model.StoryType
|
||||
@@ -122,6 +123,8 @@ class ConversationElementGenerator {
|
||||
false,
|
||||
0,
|
||||
null,
|
||||
CollapsedState.NONE,
|
||||
0,
|
||||
null,
|
||||
false
|
||||
)
|
||||
|
||||
+8
@@ -353,5 +353,13 @@ class InternalConversationTestFragment : Fragment(R.layout.conversation_test_fra
|
||||
override fun onViewPinnedMessage(messageId: Long) {
|
||||
Toast.makeText(requireContext(), "Can't touch this.", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
|
||||
override fun onExpandEvents(messageId: Long) {
|
||||
Toast.makeText(requireContext(), "Can't touch this.", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
|
||||
override fun onCollapseEvents(messageId: Long) {
|
||||
Toast.makeText(requireContext(), "Can't touch this.", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -148,5 +148,7 @@ public interface BindableConversationItem extends Unbindable, GiphyMp4Playable,
|
||||
void onViewPollClicked(long messageId);
|
||||
void onToggleVote(@NonNull PollRecord poll, @NonNull PollOption pollOption, Boolean isChecked);
|
||||
void onViewPinnedMessage(long messageId);
|
||||
void onExpandEvents(long messageId);
|
||||
void onCollapseEvents(long messageId);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ import org.thoughtcrime.securesms.conversation.mutiselect.Multiselect;
|
||||
import org.thoughtcrime.securesms.conversation.mutiselect.MultiselectCollection;
|
||||
import org.thoughtcrime.securesms.conversation.v2.computed.FormattedDate;
|
||||
import org.thoughtcrime.securesms.database.BodyRangeUtil;
|
||||
import org.thoughtcrime.securesms.database.CollapsedState;
|
||||
import org.thoughtcrime.securesms.database.MentionUtil;
|
||||
import org.thoughtcrime.securesms.database.NoSuchMessageException;
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase;
|
||||
@@ -54,6 +55,7 @@ public class ConversationMessage {
|
||||
@Nullable private final MemberLabel memberLabel;
|
||||
@Nullable private final MemberLabel quoteMemberLabel;
|
||||
@Nullable private final Recipient deletedByRecipient;
|
||||
private final int collapsedSize;
|
||||
|
||||
private ConversationMessage(@NonNull MessageRecord messageRecord,
|
||||
@Nullable CharSequence body,
|
||||
@@ -65,7 +67,8 @@ public class ConversationMessage {
|
||||
@NonNull ComputedProperties computedProperties,
|
||||
@Nullable MemberLabel memberLabel,
|
||||
@Nullable MemberLabel quoteMemberLabel,
|
||||
@Nullable Recipient deletedByRecipient)
|
||||
@Nullable Recipient deletedByRecipient,
|
||||
int collapsedSize)
|
||||
{
|
||||
this.messageRecord = messageRecord;
|
||||
this.hasBeenQuoted = hasBeenQuoted;
|
||||
@@ -77,6 +80,7 @@ public class ConversationMessage {
|
||||
this.memberLabel = memberLabel;
|
||||
this.quoteMemberLabel = quoteMemberLabel;
|
||||
this.deletedByRecipient = deletedByRecipient;
|
||||
this.collapsedSize = collapsedSize;
|
||||
|
||||
if (body != null) {
|
||||
this.body = SpannableString.valueOf(body);
|
||||
@@ -125,6 +129,10 @@ public class ConversationMessage {
|
||||
return deletedByRecipient;
|
||||
}
|
||||
|
||||
public int getCollapsedSize() {
|
||||
return collapsedSize;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
@@ -282,6 +290,11 @@ public class ConversationMessage {
|
||||
MemberLabel quoteMemberLabel = getQuoteMemberLabel(messageRecord, threadRecipient, prefetchedLabels);
|
||||
Recipient deletedBy = messageRecord.getDeletedBy() != null ? Recipient.resolved(messageRecord.getDeletedBy()) : null;
|
||||
|
||||
int collapsedSize = 0;
|
||||
if (CollapsedState.isHead(messageRecord.getCollapsedState())) {
|
||||
collapsedSize = SignalDatabase.messages().getCollapsedCount(messageRecord.getId());
|
||||
}
|
||||
|
||||
return new ConversationMessage(messageRecord,
|
||||
styledAndMentionBody != null ? styledAndMentionBody : mentionsUpdate != null ? mentionsUpdate.getBody() : body,
|
||||
mentionsUpdate != null ? mentionsUpdate.getMentions() : null,
|
||||
@@ -292,7 +305,8 @@ public class ConversationMessage {
|
||||
new ComputedProperties(formattedDate),
|
||||
memberLabel,
|
||||
quoteMemberLabel,
|
||||
deletedBy);
|
||||
deletedBy,
|
||||
collapsedSize);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+61
-5
@@ -12,11 +12,13 @@ import android.text.method.LinkMovementMethod;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.Button;
|
||||
import android.widget.FrameLayout;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.StringRes;
|
||||
import androidx.appcompat.content.res.AppCompatResources;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import androidx.lifecycle.LifecycleOwner;
|
||||
@@ -36,6 +38,8 @@ import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.conversation.colors.Colorizer;
|
||||
import org.thoughtcrime.securesms.conversation.mutiselect.MultiselectPart;
|
||||
import org.thoughtcrime.securesms.conversation.ui.error.EnableCallNotificationSettingsDialog;
|
||||
import org.thoughtcrime.securesms.database.CollapsibleEvents;
|
||||
import org.thoughtcrime.securesms.database.CollapsedState;
|
||||
import org.thoughtcrime.securesms.database.model.GroupCallUpdateDetailsUtil;
|
||||
import org.thoughtcrime.securesms.database.model.IdentityRecord;
|
||||
import org.thoughtcrime.securesms.database.model.InMemoryMessageRecord;
|
||||
@@ -43,6 +47,7 @@ import org.thoughtcrime.securesms.database.model.LiveUpdateMessage;
|
||||
import org.thoughtcrime.securesms.database.model.MessageRecord;
|
||||
import org.thoughtcrime.securesms.database.model.UpdateDescription;
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.GroupCallUpdateDetails;
|
||||
import org.thoughtcrime.securesms.fonts.SignalSymbols;
|
||||
import org.thoughtcrime.securesms.groups.LiveGroup;
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
||||
import org.thoughtcrime.securesms.recipients.LiveRecipient;
|
||||
@@ -93,6 +98,7 @@ public final class ConversationUpdateItem extends FrameLayout
|
||||
private MessageRecord messageRecord;
|
||||
private boolean isMessageRequestAccepted;
|
||||
private EventListener eventListener;
|
||||
private Button collapsedButton;
|
||||
|
||||
private final UpdateObserver updateObserver = new UpdateObserver();
|
||||
|
||||
@@ -124,9 +130,10 @@ public final class ConversationUpdateItem extends FrameLayout
|
||||
@Override
|
||||
public void onFinishInflate() {
|
||||
super.onFinishInflate();
|
||||
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.body = findViewById(R.id.conversation_update_body);
|
||||
this.actionButton = findViewById(R.id.conversation_update_action);
|
||||
this.background = findViewById(R.id.conversation_update_background);
|
||||
this.collapsedButton = findViewById(R.id.conversation_update_collapsed);
|
||||
|
||||
body.setOnClickListener(v -> performClick());
|
||||
body.setOnLongClickListener(v -> performLongClick());
|
||||
@@ -210,6 +217,7 @@ public final class ConversationUpdateItem extends FrameLayout
|
||||
hasWallpaper);
|
||||
|
||||
presentActionButton(hasWallpaper, conversationMessage.getMessageRecord().isReleaseChannelDonationRequest());
|
||||
presentCollapsedHead(conversationMessage.getMessageRecord().getCollapsedState());
|
||||
|
||||
updateSelectedState();
|
||||
}
|
||||
@@ -442,7 +450,9 @@ public final class ConversationUpdateItem extends FrameLayout
|
||||
}
|
||||
|
||||
private void setBodyText(@Nullable CharSequence text) {
|
||||
if (text == null) {
|
||||
if (CollapsedState.isCollapsed(conversationMessage.getMessageRecord().getCollapsedState()) && conversationMessage.getCollapsedSize() > 1) {
|
||||
body.setVisibility(GONE);
|
||||
} else if (text == null) {
|
||||
body.setVisibility(INVISIBLE);
|
||||
} else {
|
||||
body.setText(text);
|
||||
@@ -459,7 +469,10 @@ public final class ConversationUpdateItem extends FrameLayout
|
||||
|
||||
setSelected(!Sets.intersection(multiselectParts, batchSelected).isEmpty());
|
||||
|
||||
if (conversationMessage.getMessageRecord().isGroupV1MigrationEvent() &&
|
||||
if (CollapsedState.isCollapsed(conversationMessage.getMessageRecord().getCollapsedState()) && conversationMessage.getCollapsedSize() > 1) {
|
||||
actionButton.setVisibility(GONE);
|
||||
actionButton.setOnClickListener(null);
|
||||
} else if (conversationMessage.getMessageRecord().isGroupV1MigrationEvent() &&
|
||||
(!nextMessageRecord.isPresent() || !nextMessageRecord.get().isGroupV1MigrationEvent()))
|
||||
{
|
||||
actionButton.setText(R.string.ConversationUpdateItem_learn_more);
|
||||
@@ -807,6 +820,49 @@ public final class ConversationUpdateItem extends FrameLayout
|
||||
}
|
||||
}
|
||||
|
||||
private void presentCollapsedHead(CollapsedState collapsedState) {
|
||||
CollapsibleEvents.CollapsibleType collapsibleType = CollapsibleEvents.getCollapsibleType(messageRecord.getType(), messageRecord.getMessageExtras());
|
||||
if (CollapsedState.isHead(collapsedState) && conversationMessage.getCollapsedSize() > 1 && collapsibleType != null) {
|
||||
SpannableStringBuilder text = new SpannableStringBuilder()
|
||||
.append(SignalSymbols.getSpannedString(getContext(), SignalSymbols.Weight.BOLD, getCollapsibleSymbol(collapsibleType), org.signal.core.ui.R.color.signal_colorOnSurfaceVariant))
|
||||
.append(" ")
|
||||
.append(getContext().getString(getCollapsibleString(collapsibleType), conversationMessage.getCollapsedSize()))
|
||||
.append(" ")
|
||||
.append(SignalSymbols.getSpannedString(getContext(), SignalSymbols.Weight.BOLD, collapsedState == CollapsedState.HEAD_EXPANDED ? SignalSymbols.Glyph.CHEVRON_UP : SignalSymbols.Glyph.CHEVRON_DOWN, org.signal.core.ui.R.color.signal_colorOnSurfaceVariant));
|
||||
collapsedButton.setText(text);
|
||||
collapsedButton.setOnClickListener(v -> {
|
||||
if (eventListener != null) {
|
||||
if (CollapsedState.isCollapsed(collapsedState)) {
|
||||
eventListener.onExpandEvents(conversationMessage.getMessageRecord().getId());
|
||||
} else {
|
||||
eventListener.onCollapseEvents(conversationMessage.getMessageRecord().getId());
|
||||
}
|
||||
} else {
|
||||
passthroughClickListener.onClick(v);
|
||||
}
|
||||
});
|
||||
collapsedButton.setVisibility(VISIBLE);
|
||||
} else {
|
||||
collapsedButton.setVisibility(GONE);
|
||||
}
|
||||
}
|
||||
|
||||
private @StringRes int getCollapsibleString(CollapsibleEvents.CollapsibleType type) {
|
||||
return switch (type) {
|
||||
case CALL_EVENT -> R.string.CollapsedEvent__call_event;
|
||||
case DISAPPEARING_TIMER -> R.string.CollapsedEvent__disappearing_timer;
|
||||
case GROUP_UPDATE -> R.string.CollapsedEvent__group_update;
|
||||
};
|
||||
}
|
||||
|
||||
private SignalSymbols.Glyph getCollapsibleSymbol(CollapsibleEvents.CollapsibleType type) {
|
||||
return switch (type) {
|
||||
case CALL_EVENT -> SignalSymbols.Glyph.PHONE;
|
||||
case DISAPPEARING_TIMER -> SignalSymbols.Glyph.TIMER;
|
||||
case GROUP_UPDATE -> SignalSymbols.Glyph.GROUP;
|
||||
};
|
||||
}
|
||||
|
||||
private static boolean isSameType(@NonNull MessageRecord current, @NonNull MessageRecord candidate) {
|
||||
return (current.isGroupUpdate() && candidate.isGroupUpdate()) ||
|
||||
(current.isProfileChange() && candidate.isProfileChange()) ||
|
||||
|
||||
+2
@@ -94,4 +94,6 @@ object EmptyConversationAdapterListener : ConversationAdapter.ItemClickListener
|
||||
override fun onViewPollClicked(messageId: Long) = Unit
|
||||
override fun onToggleVote(poll: PollRecord, pollOption: PollOption, isChecked: Boolean?) = Unit
|
||||
override fun onViewPinnedMessage(messageId: Long) = Unit
|
||||
override fun onExpandEvents(messageId: Long) = Unit
|
||||
override fun onCollapseEvents(messageId: Long) = Unit
|
||||
}
|
||||
|
||||
@@ -811,6 +811,7 @@ class ConversationFragment :
|
||||
}
|
||||
|
||||
override fun onDestroyView() {
|
||||
viewModel.collapseAllEvents()
|
||||
keyboardEvents?.let {
|
||||
container.removeInputListener(it)
|
||||
container.removeKeyboardStateListener(it)
|
||||
@@ -3767,6 +3768,14 @@ class ConversationFragment :
|
||||
}
|
||||
}
|
||||
|
||||
override fun onExpandEvents(messageId: Long) {
|
||||
viewModel.onExpandEvents(messageId)
|
||||
}
|
||||
|
||||
override fun onCollapseEvents(messageId: Long) {
|
||||
viewModel.onCollapseEvents(messageId)
|
||||
}
|
||||
|
||||
override fun onItemClick(item: MultiselectPart) {
|
||||
if (isActionModeStarted()) {
|
||||
adapter.toggleSelection(item)
|
||||
|
||||
+12
@@ -842,6 +842,18 @@ class ConversationRepository(
|
||||
.subscribeOn(Schedulers.io())
|
||||
}
|
||||
|
||||
fun collapseEvents(messageId: Long) {
|
||||
SignalDatabase.messages.collapseEvents(messageId)
|
||||
}
|
||||
|
||||
fun collapseAllEvents() {
|
||||
SignalDatabase.messages.collapseAllEvents()
|
||||
}
|
||||
|
||||
fun expandEvents(messageId: Long) {
|
||||
SignalDatabase.messages.expandEvents(messageId)
|
||||
}
|
||||
|
||||
/**
|
||||
* Glide target for a contact photo which expects an error drawable, and publishes
|
||||
* the result to the given emitter.
|
||||
|
||||
@@ -43,6 +43,7 @@ import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.rx3.asFlow
|
||||
import org.signal.core.models.ServiceId
|
||||
import org.signal.core.util.concurrent.SignalDispatchers
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.signal.core.util.orNull
|
||||
import org.signal.paging.ProxyPagingController
|
||||
@@ -433,6 +434,20 @@ class ConversationViewModel(
|
||||
.flowOn(Dispatchers.IO)
|
||||
}
|
||||
|
||||
fun onCollapseEvents(messageId: Long) {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
repository.collapseEvents(messageId)
|
||||
pagingController.onDataInvalidated()
|
||||
}
|
||||
}
|
||||
|
||||
fun onExpandEvents(messageId: Long) {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
repository.expandEvents(messageId)
|
||||
pagingController.onDataInvalidated()
|
||||
}
|
||||
}
|
||||
|
||||
fun onChatBoundsChanged(bounds: Rect) {
|
||||
chatBounds.onNext(bounds)
|
||||
}
|
||||
@@ -785,6 +800,12 @@ class ConversationViewModel(
|
||||
_plaintextExportState.value = PlaintextExportState.None
|
||||
}
|
||||
|
||||
fun collapseAllEvents() {
|
||||
viewModelScope.launch(SignalDispatchers.IO) {
|
||||
repository.collapseAllEvents()
|
||||
}
|
||||
}
|
||||
|
||||
sealed interface PlaintextExportState {
|
||||
data object None : PlaintextExportState
|
||||
data object Preparing : PlaintextExportState
|
||||
|
||||
+1
-1
@@ -97,7 +97,7 @@ class ConversationDataSource(
|
||||
val stopwatch = Stopwatch(title = "load($start, $length), thread $threadId", decimalPlaces = 2)
|
||||
var records: MutableList<MessageRecord> = ArrayList(length)
|
||||
|
||||
MessageTable.mmsReaderFor(SignalDatabase.messages.getConversation(threadId, start.toLong(), length.toLong()))
|
||||
MessageTable.mmsReaderFor(SignalDatabase.messages.getConversation(threadId, start.toLong(), length.toLong(), filterCollapsed = true))
|
||||
.use { reader ->
|
||||
reader.forEach { record ->
|
||||
if (cancellationSignal.isCanceled) {
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
package org.thoughtcrime.securesms.database
|
||||
|
||||
import org.signal.core.util.LongSerializer
|
||||
|
||||
/**
|
||||
* Tracks the collapsed state of a message. Non-update messages are always NONE, while
|
||||
* update messages can either be the first update message of a collapsed set (HEAD_*)
|
||||
* or part of the collapsed set (COLLAPSED/EXPANDED)
|
||||
*
|
||||
* eg in the message table:
|
||||
* id | msg | collapsed_state | collapsed_head_id
|
||||
* 1 | [Group Update 1] | HEAD_COLLAPSED | 1
|
||||
* 2 | [Group Update 3] | COLLAPSED | 1
|
||||
* 3 | Regular message | NONE | null
|
||||
* 4 | [Group Update 4] | HEAD_COLLAPSED | 4
|
||||
*
|
||||
* and when expanded,
|
||||
* id | msg | collapsed_state | collapsed_head_id
|
||||
* 1 | [Group Update 1] | HEAD_EXPANDED | 1
|
||||
* 2 | [Group Update 3] | EXPANDED | 1
|
||||
* 3 | Regular message | NONE | null
|
||||
* 4 | [Group Update 4] | HEAD_COLLAPSED | 4
|
||||
*/
|
||||
enum class CollapsedState(val id: Long) {
|
||||
NONE(0),
|
||||
HEAD_COLLAPSED(1),
|
||||
HEAD_EXPANDED(2),
|
||||
COLLAPSED(3),
|
||||
EXPANDED(4),
|
||||
PENDING_COLLAPSED(5);
|
||||
|
||||
companion object Serializer : LongSerializer<CollapsedState> {
|
||||
override fun serialize(data: CollapsedState): Long {
|
||||
return data.id
|
||||
}
|
||||
|
||||
override fun deserialize(input: Long): CollapsedState {
|
||||
return CollapsedState.entries.firstOrNull { it.id == input } ?: NONE
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun isHead(state: CollapsedState): Boolean {
|
||||
return state == HEAD_COLLAPSED || state == HEAD_EXPANDED
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun isCollapsed(state: CollapsedState): Boolean {
|
||||
return state == HEAD_COLLAPSED || state == COLLAPSED
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
package org.thoughtcrime.securesms.database
|
||||
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.MessageExtras
|
||||
|
||||
/**
|
||||
* Utility functions to track the different collapsing types and what type a message is
|
||||
*/
|
||||
object CollapsibleEvents {
|
||||
|
||||
@JvmStatic
|
||||
fun isCollapsibleType(type: Long, messageExtras: MessageExtras?): Boolean {
|
||||
return getCollapsibleType(type, messageExtras) != null
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun getCollapsibleType(type: Long, messageExtras: MessageExtras?): CollapsibleType? {
|
||||
if (MessageTypes.isCallLog(type)) {
|
||||
return CollapsibleType.CALL_EVENT
|
||||
}
|
||||
|
||||
if (MessageTypes.isExpirationTimerUpdate(type)) {
|
||||
return CollapsibleType.DISAPPEARING_TIMER
|
||||
}
|
||||
|
||||
if (messageExtras?.gv2UpdateDescription != null) {
|
||||
val groupChangeUpdate = messageExtras.gv2UpdateDescription.groupChangeUpdate
|
||||
return if (groupChangeUpdate?.updates?.any { it.groupExpirationTimerUpdate != null } == true) {
|
||||
CollapsibleType.DISAPPEARING_TIMER
|
||||
} else if (groupChangeUpdate?.updates?.none { it.groupTerminateChangeUpdate != null } == true) {
|
||||
CollapsibleType.GROUP_UPDATE
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
if (MessageTypes.isProfileChange(type)) {
|
||||
return CollapsibleType.GROUP_UPDATE
|
||||
}
|
||||
|
||||
if (MessageTypes.isIdentityUpdate(type) || MessageTypes.isIdentityVerified(type) || MessageTypes.isIdentityDefault(type)) {
|
||||
return CollapsibleType.GROUP_UPDATE
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
enum class CollapsibleType {
|
||||
DISAPPEARING_TIMER,
|
||||
GROUP_UPDATE,
|
||||
CALL_EVENT
|
||||
}
|
||||
}
|
||||
@@ -141,6 +141,7 @@ import org.thoughtcrime.securesms.revealable.ViewOnceExpirationInfo
|
||||
import org.thoughtcrime.securesms.revealable.ViewOnceUtil
|
||||
import org.thoughtcrime.securesms.sms.GroupV2UpdateMessageUtil
|
||||
import org.thoughtcrime.securesms.stories.Stories.isFeatureEnabled
|
||||
import org.thoughtcrime.securesms.util.DateUtils
|
||||
import org.thoughtcrime.securesms.util.JsonUtils
|
||||
import org.thoughtcrime.securesms.util.MediaUtil
|
||||
import org.thoughtcrime.securesms.util.MessageConstraintsUtil
|
||||
@@ -228,6 +229,8 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat
|
||||
const val DELETED_BY = "deleted_by"
|
||||
const val STORY_ARCHIVED = "story_archived"
|
||||
const val STARRED = "starred"
|
||||
const val COLLAPSED_STATE = "collapsed_state"
|
||||
const val COLLAPSED_HEAD_ID = "collapsed_head_id"
|
||||
|
||||
const val QUOTE_NOT_PRESENT_ID = 0L
|
||||
const val QUOTE_TARGET_MISSING_ID = -1L
|
||||
@@ -301,7 +304,9 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat
|
||||
$PINNED_AT INTEGER DEFAULT 0,
|
||||
$DELETED_BY INTEGER DEFAULT NULL REFERENCES ${RecipientTable.TABLE_NAME} (${RecipientTable.ID}) ON DELETE CASCADE,
|
||||
$STORY_ARCHIVED INTEGER DEFAULT 0,
|
||||
$STARRED INTEGER DEFAULT 0
|
||||
$STARRED INTEGER DEFAULT 0,
|
||||
$COLLAPSED_STATE INTEGER DEFAULT 0,
|
||||
$COLLAPSED_HEAD_ID INTEGER DEFAULT 0
|
||||
)
|
||||
"""
|
||||
|
||||
@@ -329,7 +334,7 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat
|
||||
"CREATE INDEX IF NOT EXISTS message_to_recipient_id_index ON $TABLE_NAME ($TO_RECIPIENT_ID)",
|
||||
"CREATE UNIQUE INDEX IF NOT EXISTS message_unique_sent_from_thread ON $TABLE_NAME ($DATE_SENT, $FROM_RECIPIENT_ID, $THREAD_ID)",
|
||||
// This index is created specifically for getting the number of messages in a thread and therefore needs to be kept in sync with that query
|
||||
"CREATE INDEX IF NOT EXISTS $INDEX_THREAD_COUNT ON $TABLE_NAME ($THREAD_ID) WHERE $STORY_TYPE = 0 AND $PARENT_STORY_ID <= 0 AND $SCHEDULED_DATE = -1 AND $LATEST_REVISION_ID IS NULL",
|
||||
"CREATE INDEX IF NOT EXISTS $INDEX_THREAD_COUNT ON $TABLE_NAME ($THREAD_ID) WHERE $STORY_TYPE = 0 AND $PARENT_STORY_ID <= 0 AND $SCHEDULED_DATE = -1 AND $LATEST_REVISION_ID IS NULL AND $COLLAPSED_STATE != ${CollapsedState.COLLAPSED.id}",
|
||||
// This index is created specifically for getting the number of unread messages in a thread and therefore needs to be kept in sync with that query
|
||||
"CREATE INDEX IF NOT EXISTS $INDEX_THREAD_UNREAD_COUNT ON $TABLE_NAME ($THREAD_ID) WHERE $STORY_TYPE = 0 AND $PARENT_STORY_ID <= 0 AND $SCHEDULED_DATE = -1 AND $ORIGINAL_MESSAGE_ID IS NULL AND $READ = 0",
|
||||
"CREATE INDEX IF NOT EXISTS message_votes_unread_index ON $TABLE_NAME ($VOTES_UNREAD)",
|
||||
@@ -337,7 +342,9 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat
|
||||
"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 message_story_archived_index ON $TABLE_NAME ($STORY_ARCHIVED, $STORY_TYPE, $DATE_SENT) WHERE $STORY_TYPE > 0 AND $STORY_ARCHIVED > 0",
|
||||
"CREATE INDEX IF NOT EXISTS message_starred_index ON $TABLE_NAME ($STARRED) WHERE $STARRED > 0"
|
||||
"CREATE INDEX IF NOT EXISTS message_starred_index ON $TABLE_NAME ($STARRED) WHERE $STARRED > 0",
|
||||
"CREATE INDEX IF NOT EXISTS message_collapsed_state_index ON $TABLE_NAME ($COLLAPSED_STATE)",
|
||||
"CREATE INDEX IF NOT EXISTS message_collapsed_head_id_index ON $TABLE_NAME ($COLLAPSED_HEAD_ID)"
|
||||
)
|
||||
|
||||
private val MMS_PROJECTION_BASE = arrayOf(
|
||||
@@ -394,7 +401,9 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat
|
||||
VOTES_LAST_SEEN,
|
||||
PINNED_UNTIL,
|
||||
DELETED_BY,
|
||||
STARRED
|
||||
STARRED,
|
||||
COLLAPSED_STATE,
|
||||
COLLAPSED_HEAD_ID
|
||||
)
|
||||
|
||||
private val MMS_PROJECTION: Array<String> = MMS_PROJECTION_BASE
|
||||
@@ -862,12 +871,13 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat
|
||||
val recipient = Recipient.resolved(recipientId)
|
||||
val threadIdResult = threads.getOrCreateThreadIdResultFor(recipient.id, recipient.isGroup)
|
||||
val threadId = threadIdResult.threadId
|
||||
val dateReceived = System.currentTimeMillis()
|
||||
|
||||
val values = contentValuesOf(
|
||||
FROM_RECIPIENT_ID to if (outgoing) Recipient.self().id.serialize() else recipientId.serialize(),
|
||||
FROM_DEVICE_ID to 1,
|
||||
TO_RECIPIENT_ID to if (outgoing) recipientId.serialize() else Recipient.self().id.serialize(),
|
||||
DATE_RECEIVED to System.currentTimeMillis(),
|
||||
DATE_RECEIVED to dateReceived,
|
||||
DATE_SENT to timestamp,
|
||||
READ to 1,
|
||||
TYPE to type,
|
||||
@@ -876,6 +886,8 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat
|
||||
|
||||
val messageId = writableDatabase.insert(TABLE_NAME, null, values)
|
||||
|
||||
maybeCollapseMessage(db = writableDatabase, messageId = messageId, threadId = threadId, dateReceived = dateReceived, messageExtras = null, messageType = type)
|
||||
|
||||
threads.update(threadId, true)
|
||||
|
||||
notifyConversationListeners(threadId)
|
||||
@@ -889,6 +901,7 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat
|
||||
}
|
||||
|
||||
fun updateCallLog(messageId: Long, type: Long) {
|
||||
val message = getMessageRecordOrNull(messageId = messageId)
|
||||
writableDatabase
|
||||
.update(TABLE_NAME)
|
||||
.values(
|
||||
@@ -902,6 +915,10 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat
|
||||
|
||||
threads.update(threadId, true)
|
||||
|
||||
if (message?.collapsedState == CollapsedState.NONE) {
|
||||
maybeCollapseMessage(db = writableDatabase, messageId = messageId, threadId = threadId, dateReceived = message.dateReceived, messageExtras = message.messageExtras, messageType = type)
|
||||
}
|
||||
|
||||
notifyConversationListeners(threadId)
|
||||
AppDependencies.databaseObserver.notifyMessageUpdateObservers(MessageId(messageId))
|
||||
}
|
||||
@@ -943,6 +960,12 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat
|
||||
)
|
||||
|
||||
val messageId = MessageId(db.insert(TABLE_NAME, null, values))
|
||||
|
||||
val isActiveCall = joinedUuids.isNotEmpty() || isIncomingGroupCallRingingOnLocalDevice
|
||||
if (!isActiveCall) {
|
||||
maybeCollapseMessage(db = db, messageId = messageId.id, threadId = threadId, dateReceived = timestamp, messageExtras = null, messageType = MessageTypes.GROUP_CALL_TYPE)
|
||||
}
|
||||
|
||||
threads.incrementUnread(threadId, 1, 0)
|
||||
threads.update(threadId, true)
|
||||
|
||||
@@ -1044,6 +1067,10 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat
|
||||
val query = buildTrueUpdateQuery(ID_WHERE, buildArgs(messageId), contentValues)
|
||||
val updated = db.update(TABLE_NAME, contentValues, query.where, query.whereArgs) > 0
|
||||
|
||||
if (inCallUuids.isEmpty() && message.collapsedState == CollapsedState.NONE) {
|
||||
maybeCollapseMessage(db = db, messageId = messageId, threadId = message.threadId, dateReceived = message.dateReceived, messageExtras = message.messageExtras, messageType = message.type)
|
||||
}
|
||||
|
||||
if (updated) {
|
||||
notifyConversationListeners(message.threadId)
|
||||
}
|
||||
@@ -1091,6 +1118,10 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat
|
||||
val query = buildTrueUpdateQuery(ID_WHERE, buildArgs(record.id), contentValues)
|
||||
val updated = db.update(TABLE_NAME, contentValues, query.where, query.whereArgs) > 0
|
||||
|
||||
if (inCallUuids.isEmpty() && record.collapsedState == CollapsedState.NONE) {
|
||||
maybeCollapseMessage(db = db, messageId = record.id, threadId = record.threadId, dateReceived = record.dateReceived, messageExtras = record.messageExtras, messageType = record.type)
|
||||
}
|
||||
|
||||
if (updated) {
|
||||
notifyConversationListeners(threadId)
|
||||
}
|
||||
@@ -1142,18 +1173,20 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat
|
||||
threadIdsToUpdate
|
||||
.filterNotNull()
|
||||
.forEach { threadId ->
|
||||
val now = System.currentTimeMillis()
|
||||
val values = contentValuesOf(
|
||||
FROM_RECIPIENT_ID to recipient.id.serialize(),
|
||||
FROM_DEVICE_ID to 1,
|
||||
TO_RECIPIENT_ID to Recipient.self().id.serialize(),
|
||||
DATE_RECEIVED to System.currentTimeMillis(),
|
||||
DATE_SENT to System.currentTimeMillis(),
|
||||
DATE_RECEIVED to now,
|
||||
DATE_SENT to now,
|
||||
READ to 1,
|
||||
TYPE to MessageTypes.PROFILE_CHANGE_TYPE,
|
||||
THREAD_ID to threadId,
|
||||
MESSAGE_EXTRAS to extras.encode()
|
||||
)
|
||||
db.insert(TABLE_NAME, null, values)
|
||||
val messageId = db.insert(TABLE_NAME, null, values)
|
||||
maybeCollapseMessage(db = db, messageId = messageId, threadId = threadId, dateReceived = now, messageExtras = extras, messageType = MessageTypes.PROFILE_CHANGE_TYPE)
|
||||
notifyConversationListeners(threadId)
|
||||
TrimThreadJob.enqueueAsync(threadId)
|
||||
}
|
||||
@@ -1173,18 +1206,19 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat
|
||||
val threadId: Long? = SignalDatabase.threads.getThreadIdFor(recipient.id)
|
||||
|
||||
if (threadId != null) {
|
||||
val now = System.currentTimeMillis()
|
||||
val extras = MessageExtras(
|
||||
profileChangeDetails = ProfileChangeDetails(learnedProfileName = ProfileChangeDetails.LearnedProfileName(e164 = e164, username = username))
|
||||
)
|
||||
|
||||
writableDatabase
|
||||
val messageId = writableDatabase
|
||||
.insertInto(TABLE_NAME)
|
||||
.values(
|
||||
FROM_RECIPIENT_ID to recipient.id.serialize(),
|
||||
FROM_DEVICE_ID to 1,
|
||||
TO_RECIPIENT_ID to Recipient.self().id.serialize(),
|
||||
DATE_RECEIVED to System.currentTimeMillis(),
|
||||
DATE_SENT to System.currentTimeMillis(),
|
||||
DATE_RECEIVED to now,
|
||||
DATE_SENT to now,
|
||||
READ to 1,
|
||||
TYPE to MessageTypes.PROFILE_CHANGE_TYPE,
|
||||
THREAD_ID to threadId,
|
||||
@@ -1192,6 +1226,8 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat
|
||||
)
|
||||
.run()
|
||||
|
||||
maybeCollapseMessage(db = writableDatabase, messageId = messageId, threadId = threadId, dateReceived = now, messageExtras = extras, messageType = MessageTypes.PROFILE_CHANGE_TYPE)
|
||||
|
||||
notifyConversationListeners(threadId)
|
||||
}
|
||||
}
|
||||
@@ -1946,7 +1982,7 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat
|
||||
return readableDatabase
|
||||
.select("COUNT(*)")
|
||||
.from("$TABLE_NAME INDEXED BY $INDEX_THREAD_COUNT")
|
||||
.where("$THREAD_ID = $threadId AND $STORY_TYPE = 0 AND $PARENT_STORY_ID <= 0 AND $SCHEDULED_DATE = -1 AND $LATEST_REVISION_ID IS NULL")
|
||||
.where("$THREAD_ID = $threadId AND $STORY_TYPE = 0 AND $PARENT_STORY_ID <= 0 AND $SCHEDULED_DATE = -1 AND $LATEST_REVISION_ID IS NULL AND $COLLAPSED_STATE != ${CollapsedState.COLLAPSED.id}")
|
||||
.run()
|
||||
.readToSingleInt()
|
||||
}
|
||||
@@ -3002,6 +3038,8 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat
|
||||
return Optional.empty()
|
||||
}
|
||||
|
||||
maybeCollapseMessage(db = writableDatabase, messageId = messageId, threadId = threadId, dateReceived = retrieved.receivedTimeMillis, messageExtras = retrieved.messageExtras, messageType = type)
|
||||
|
||||
if (editedMessage != null) {
|
||||
writableDatabase.update(TABLE_NAME)
|
||||
.values(QUOTE_ID to retrieved.sentTimeMillis)
|
||||
@@ -3512,10 +3550,14 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat
|
||||
movePinnedDetailsToNewMessage(newMessageId = messageId, previousId = message.messageToEdit)
|
||||
}
|
||||
|
||||
threads.updateLastSeenAndMarkSentAndLastScrolledSilenty(threadId, dateReceived)
|
||||
val hasCollapsed = maybeCollapseMessage(db = writableDatabase, messageId = messageId, threadId = threadId, dateReceived = dateReceived, messageExtras = message.messageExtras, messageType = type)
|
||||
|
||||
if (!message.isIdentityVerified && !message.isIdentityDefault) {
|
||||
threads.updateLastSeenAndMarkSentAndLastScrolledSilenty(threadId, dateReceived)
|
||||
}
|
||||
|
||||
if (!message.storyType.isStory) {
|
||||
if (message.outgoingQuote == null && editedMessage == null) {
|
||||
if (message.outgoingQuote == null && editedMessage == null && !hasCollapsed) {
|
||||
AppDependencies.databaseObserver.notifyMessageInsertObservers(threadId, MessageId(messageId))
|
||||
} else {
|
||||
AppDependencies.databaseObserver.notifyConversationListeners(threadId)
|
||||
@@ -3543,6 +3585,54 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Conditionally collapses a new message if it is the same [CollapsibleEvents.CollapsibleType] as the previous message on the same day.
|
||||
* If it is not, but the new message is a collapsing type, mark it as a new collapsed head. Returns whether a message was collapsed.
|
||||
*/
|
||||
fun maybeCollapseMessage(db: SQLiteDatabase, messageId: Long, threadId: Long, dateReceived: Long, messageExtras: MessageExtras?, messageType: Long): Boolean {
|
||||
if (!RemoteConfig.collapseEvents || !CollapsibleEvents.isCollapsibleType(messageType, messageExtras)) {
|
||||
return false
|
||||
}
|
||||
|
||||
val currentType = CollapsibleEvents.getCollapsibleType(messageType, messageExtras)!!
|
||||
val previousMessage = getMessageDirectlyBefore(messageId, threadId, dateReceived)
|
||||
val previousType = previousMessage?.let { CollapsibleEvents.getCollapsibleType(previousMessage.type, previousMessage.messageExtras) }
|
||||
|
||||
return if (previousType == currentType) {
|
||||
db.update(TABLE_NAME)
|
||||
.values(
|
||||
COLLAPSED_STATE to CollapsedState.PENDING_COLLAPSED.id,
|
||||
COLLAPSED_HEAD_ID to previousMessage.collapsedHeadId
|
||||
)
|
||||
.where("$ID = ?", messageId)
|
||||
.run()
|
||||
true
|
||||
} else {
|
||||
db.update(TABLE_NAME)
|
||||
.values(
|
||||
COLLAPSED_STATE to CollapsedState.HEAD_COLLAPSED.id,
|
||||
COLLAPSED_HEAD_ID to messageId
|
||||
)
|
||||
.where("$ID = ?", messageId)
|
||||
.run()
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
// TODO(michelle): Maybe reduce to the fields you actually need instead of everything
|
||||
private fun getMessageDirectlyBefore(messageId: Long, threadId: Long, dateReceived: Long): MessageRecord? {
|
||||
val message = readableDatabase
|
||||
.select(*MMS_PROJECTION)
|
||||
.from(TABLE_NAME)
|
||||
.where("$ID < ? AND $THREAD_ID = ?", messageId, threadId)
|
||||
.orderBy("$DATE_RECEIVED DESC")
|
||||
.limit(1)
|
||||
.run()
|
||||
.readToSingleObject { MmsReader(it).getCurrent() }
|
||||
|
||||
return message?.takeIf { DateUtils.isSameDay(message.dateReceived, dateReceived) }
|
||||
}
|
||||
|
||||
private fun hasAudioAttachment(attachments: List<Attachment>): Boolean {
|
||||
return attachments.any { MediaUtil.isAudio(it) }
|
||||
}
|
||||
@@ -3761,6 +3851,44 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat
|
||||
return deleteMessage(messageId, threadId)
|
||||
}
|
||||
|
||||
/**
|
||||
* When an update gets deleted, check if it was the head of a set of collapsed events. If so,
|
||||
* set the next element to be the new head, and change all elements' head reference to the new one.
|
||||
*/
|
||||
private fun reassignCollapsedHead(messageId: Long) {
|
||||
val collapsedState = readableDatabase
|
||||
.select(COLLAPSED_STATE)
|
||||
.from(TABLE_NAME)
|
||||
.where("$ID = ?", messageId)
|
||||
.run()
|
||||
.readToSingleObject { cursor -> CollapsedState.deserialize(cursor.requireLong(COLLAPSED_STATE)) } ?: CollapsedState.NONE
|
||||
|
||||
if (CollapsedState.isHead(collapsedState)) {
|
||||
val nextHead = readableDatabase
|
||||
.select(ID)
|
||||
.from(TABLE_NAME)
|
||||
.where("$ID > ? AND $COLLAPSED_HEAD_ID = ?", messageId, messageId)
|
||||
.orderBy("$DATE_RECEIVED ASC")
|
||||
.limit(1)
|
||||
.run()
|
||||
.readToSingleLongOrNull()
|
||||
|
||||
if (nextHead != null) {
|
||||
writableDatabase.withinTransaction { db ->
|
||||
db.update(TABLE_NAME)
|
||||
.values(COLLAPSED_STATE to collapsedState.id)
|
||||
.where("$ID = ?", nextHead)
|
||||
.run()
|
||||
|
||||
db.update(TABLE_NAME)
|
||||
.values(COLLAPSED_HEAD_ID to nextHead)
|
||||
.where("$COLLAPSED_HEAD_ID = ?", messageId)
|
||||
.run()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
fun deleteMessage(messageId: Long, threadId: Long, notify: Boolean = true, updateThread: Boolean = true): Boolean {
|
||||
Log.d(TAG, "deleteMessage($messageId)")
|
||||
@@ -3770,6 +3898,7 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat
|
||||
mentions.deleteMentionsForMessage(messageId)
|
||||
disassociatePollFromPollTerminate(polls.getPollTerminateMessageId(messageId))
|
||||
disassociatePinnedMessage(messageId)
|
||||
reassignCollapsedHead(messageId)
|
||||
|
||||
writableDatabase
|
||||
.delete(TABLE_NAME)
|
||||
@@ -4531,6 +4660,65 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat
|
||||
.run()
|
||||
}
|
||||
|
||||
fun collapsePendingCollapsibleEvents(threadId: Long, sinceTimestamp: Long) {
|
||||
val where = if (sinceTimestamp > -1) {
|
||||
"$THREAD_ID = ? AND $COLLAPSED_STATE = ? AND $DATE_RECEIVED <= $sinceTimestamp"
|
||||
} else {
|
||||
"$THREAD_ID = ? AND $COLLAPSED_STATE = ?"
|
||||
}
|
||||
|
||||
writableDatabase
|
||||
.update(TABLE_NAME)
|
||||
.values(COLLAPSED_STATE to CollapsedState.COLLAPSED.id)
|
||||
.where(where, threadId, CollapsedState.PENDING_COLLAPSED.id)
|
||||
.run()
|
||||
}
|
||||
|
||||
fun collapseAllPendingCollapsibleEvents() {
|
||||
writableDatabase
|
||||
.update(TABLE_NAME)
|
||||
.values(COLLAPSED_STATE to CollapsedState.COLLAPSED.id)
|
||||
.where("$COLLAPSED_STATE = ?", CollapsedState.PENDING_COLLAPSED.id)
|
||||
.run()
|
||||
}
|
||||
|
||||
/**
|
||||
* If the oldest message in a thread is [CollapsedState.COLLAPSED], [CollapsedState.PENDING_COLLAPSED], or [CollapsedState.EXPANDED],
|
||||
* that means its head reference has been deleted in a previous operation. In that case, we promote the
|
||||
* oldest message to be the HEAD and update any existing events that previously had the deleted head as a reference.
|
||||
*/
|
||||
fun fixPotentialDanglingCollapsibleEvent(threadId: Long) {
|
||||
writableDatabase.withinTransaction { db ->
|
||||
db.select(ID, COLLAPSED_STATE, COLLAPSED_HEAD_ID)
|
||||
.from(TABLE_NAME)
|
||||
.where("$THREAD_ID = ?", threadId)
|
||||
.orderBy("$DATE_RECEIVED ASC")
|
||||
.limit(1)
|
||||
.run()
|
||||
.use { cursor ->
|
||||
if (cursor.moveToFirst()) {
|
||||
val id = cursor.requireLong(ID)
|
||||
val collapsedState = CollapsedState.deserialize(cursor.requireLong(COLLAPSED_STATE))
|
||||
val deletedHeadId = cursor.requireLong(COLLAPSED_HEAD_ID)
|
||||
if (collapsedState == CollapsedState.COLLAPSED || collapsedState == CollapsedState.EXPANDED || collapsedState == CollapsedState.PENDING_COLLAPSED) {
|
||||
val newState = if (collapsedState == CollapsedState.EXPANDED) CollapsedState.HEAD_EXPANDED.id else CollapsedState.HEAD_COLLAPSED.id
|
||||
val updated = db.update(TABLE_NAME)
|
||||
.values(COLLAPSED_STATE to newState)
|
||||
.where("$ID = ?", id)
|
||||
.run()
|
||||
|
||||
db.update(TABLE_NAME)
|
||||
.values(COLLAPSED_HEAD_ID to id)
|
||||
.where("$COLLAPSED_HEAD_ID = ?", deletedHeadId)
|
||||
.run()
|
||||
|
||||
Log.i(TAG, "Found dangling collapsed set, reset head: $updated")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun setNotifiedTimestamp(timestamp: Long, ids: List<Long>) {
|
||||
if (ids.isEmpty()) {
|
||||
return
|
||||
@@ -4781,7 +4969,7 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat
|
||||
return readableDatabase
|
||||
.select("COUNT(*)")
|
||||
.from(TABLE_NAME)
|
||||
.where("$THREAD_ID = $threadId AND $STORY_TYPE = 0 AND $PARENT_STORY_ID <= 0 AND $SCHEDULED_DATE = -1 AND $LATEST_REVISION_ID IS NULL AND $DATE_RECEIVED > $targetMessageDateReceived")
|
||||
.where("$THREAD_ID = $threadId AND $STORY_TYPE = 0 AND $PARENT_STORY_ID <= 0 AND $SCHEDULED_DATE = -1 AND $LATEST_REVISION_ID IS NULL AND $DATE_RECEIVED > $targetMessageDateReceived AND $COLLAPSED_STATE != ${CollapsedState.COLLAPSED.id}")
|
||||
.run()
|
||||
.readToSingleInt()
|
||||
}
|
||||
@@ -4799,7 +4987,7 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat
|
||||
return readableDatabase
|
||||
.select("COUNT(*)")
|
||||
.from(TABLE_NAME)
|
||||
.where("$THREAD_ID = $threadId AND $STORY_TYPE = 0 AND $PARENT_STORY_ID <= 0 AND $SCHEDULED_DATE = -1 AND $LATEST_REVISION_ID IS NULL AND $DATE_RECEIVED > $receivedTimestamp")
|
||||
.where("$THREAD_ID = $threadId AND $STORY_TYPE = 0 AND $PARENT_STORY_ID <= 0 AND $SCHEDULED_DATE = -1 AND $LATEST_REVISION_ID IS NULL AND $DATE_RECEIVED > $receivedTimestamp AND $COLLAPSED_STATE != ${CollapsedState.COLLAPSED.id}")
|
||||
.run()
|
||||
.readToSingleInt(-1)
|
||||
}
|
||||
@@ -4843,9 +5031,9 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat
|
||||
*/
|
||||
fun getMessagePositionInConversation(threadId: Long, groupStoryId: Long, receivedTimestamp: Long): Int {
|
||||
val selection = if (groupStoryId > 0) {
|
||||
"$THREAD_ID = $threadId AND $DATE_RECEIVED < $receivedTimestamp AND $STORY_TYPE = 0 AND $PARENT_STORY_ID = $groupStoryId AND $SCHEDULED_DATE = -1 AND $LATEST_REVISION_ID IS NULL"
|
||||
"$THREAD_ID = $threadId AND $DATE_RECEIVED < $receivedTimestamp AND $STORY_TYPE = 0 AND $PARENT_STORY_ID = $groupStoryId AND $SCHEDULED_DATE = -1 AND $LATEST_REVISION_ID IS NULL AND $COLLAPSED_STATE != ${CollapsedState.COLLAPSED.id}"
|
||||
} else {
|
||||
"$THREAD_ID = $threadId AND $DATE_RECEIVED > $receivedTimestamp AND $STORY_TYPE = 0 AND $PARENT_STORY_ID <= 0 AND $SCHEDULED_DATE = -1 AND $LATEST_REVISION_ID IS NULL"
|
||||
"$THREAD_ID = $threadId AND $DATE_RECEIVED > $receivedTimestamp AND $STORY_TYPE = 0 AND $PARENT_STORY_ID <= 0 AND $SCHEDULED_DATE = -1 AND $LATEST_REVISION_ID IS NULL AND $COLLAPSED_STATE != ${CollapsedState.COLLAPSED.id}"
|
||||
}
|
||||
|
||||
return readableDatabase
|
||||
@@ -4871,7 +5059,7 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat
|
||||
return readableDatabase
|
||||
.select("COUNT(*)")
|
||||
.from(TABLE_NAME)
|
||||
.where("$DATE_RECEIVED < $date AND $SCHEDULED_DATE = -1 AND $LATEST_REVISION_ID IS NULL")
|
||||
.where("$DATE_RECEIVED < $date AND $SCHEDULED_DATE = -1 AND $LATEST_REVISION_ID IS NULL AND $COLLAPSED_STATE != ${CollapsedState.COLLAPSED.id}")
|
||||
.run()
|
||||
.readToSingleInt()
|
||||
}
|
||||
@@ -4889,7 +5077,7 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat
|
||||
return readableDatabase
|
||||
.select("COUNT(*)")
|
||||
.from(TABLE_NAME)
|
||||
.where("$THREAD_ID = $threadId AND $DATE_RECEIVED ${if (inclusive) ">=" else ">"} $timestamp AND $STORY_TYPE = 0 AND $PARENT_STORY_ID <= 0 AND $SCHEDULED_DATE = -1 AND $LATEST_REVISION_ID IS NULL")
|
||||
.where("$THREAD_ID = $threadId AND $DATE_RECEIVED ${if (inclusive) ">=" else ">"} $timestamp AND $STORY_TYPE = 0 AND $PARENT_STORY_ID <= 0 AND $SCHEDULED_DATE = -1 AND $LATEST_REVISION_ID IS NULL AND $COLLAPSED_STATE != ${CollapsedState.COLLAPSED.id}")
|
||||
.run()
|
||||
.readToSingleInt()
|
||||
}
|
||||
@@ -5380,15 +5568,23 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat
|
||||
|
||||
/**
|
||||
* A cursor containing all of the messages in a given thread, in the proper order, respecting offset/limit.
|
||||
* This does *not* have attachments in it.
|
||||
* This does *not* have attachments in it. Use [filterCollapsed] to exclude collapsed events.
|
||||
*/
|
||||
fun getConversation(threadId: Long, offset: Long = 0, limit: Long = 0, dateReceiveOrderBy: String = "DESC"): Cursor {
|
||||
fun getConversation(threadId: Long, offset: Long = 0, limit: Long = 0, dateReceiveOrderBy: String = "DESC", filterCollapsed: Boolean = false): Cursor {
|
||||
val limitStr: String = if (limit > 0 || offset > 0) "$offset, $limit" else ""
|
||||
|
||||
var query = "$THREAD_ID = ? AND $STORY_TYPE = ? AND $PARENT_STORY_ID <= ? AND $SCHEDULED_DATE = ? AND $LATEST_REVISION_ID IS NULL"
|
||||
val args = mutableListOf(threadId.toString(), 0.toString(), 0.toString(), (-1).toString())
|
||||
|
||||
if (filterCollapsed) {
|
||||
query += " AND $COLLAPSED_STATE != ?"
|
||||
args.add(CollapsedState.COLLAPSED.id.toString())
|
||||
}
|
||||
|
||||
return readableDatabase
|
||||
.select(*MMS_PROJECTION)
|
||||
.from("$TABLE_NAME INDEXED BY $INDEX_THREAD_STORY_SCHEDULED_DATE_LATEST_REVISION_ID")
|
||||
.where("$THREAD_ID = ? AND $STORY_TYPE = ? AND $PARENT_STORY_ID <= ? AND $SCHEDULED_DATE = ? AND $LATEST_REVISION_ID IS NULL", threadId, 0, 0, -1)
|
||||
.where(query, args.toTypedArray())
|
||||
.orderBy("$DATE_RECEIVED $dateReceiveOrderBy")
|
||||
.limit(limitStr)
|
||||
.run()
|
||||
@@ -5979,6 +6175,70 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the number of updates that belong in a collapsed update set where [messageId] is the head (first update) in that set
|
||||
* If an event is [PENDING_COLLAPSED], we do not want to consider it part of the count until it is seen.
|
||||
*/
|
||||
fun getCollapsedCount(messageId: Long): Int {
|
||||
return readableDatabase
|
||||
.count()
|
||||
.from(TABLE_NAME)
|
||||
.where("$COLLAPSED_HEAD_ID = ? AND $COLLAPSED_STATE != ?", messageId, CollapsedState.PENDING_COLLAPSED.id)
|
||||
.run()
|
||||
.readToSingleInt()
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a collapsed head, set it and all of the updates in that set, to expanded
|
||||
*/
|
||||
fun expandEvents(messageId: Long) {
|
||||
writableDatabase.withinTransaction { db ->
|
||||
db.update(TABLE_NAME)
|
||||
.values(COLLAPSED_STATE to CollapsedState.HEAD_EXPANDED.id)
|
||||
.where("$ID = ?", messageId)
|
||||
.run()
|
||||
|
||||
db.update(TABLE_NAME)
|
||||
.values(COLLAPSED_STATE to CollapsedState.EXPANDED.id)
|
||||
.where("$COLLAPSED_HEAD_ID = ? AND $COLLAPSED_STATE = ?", messageId, CollapsedState.COLLAPSED.id)
|
||||
.run()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Given an expanded head, set it and all of the updates in that set, to collapsed
|
||||
*/
|
||||
fun collapseEvents(messageId: Long) {
|
||||
writableDatabase.withinTransaction { db ->
|
||||
db.update(TABLE_NAME)
|
||||
.values(COLLAPSED_STATE to CollapsedState.HEAD_COLLAPSED.id)
|
||||
.where("$ID = ?", messageId)
|
||||
.run()
|
||||
|
||||
db.update(TABLE_NAME)
|
||||
.values(COLLAPSED_STATE to CollapsedState.COLLAPSED.id)
|
||||
.where("$COLLAPSED_HEAD_ID = ? AND $COLLAPSED_STATE = ?", messageId, CollapsedState.EXPANDED.id)
|
||||
.run()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Collapses any expanded events in a thread
|
||||
*/
|
||||
fun collapseAllEvents() {
|
||||
writableDatabase.withinTransaction { db ->
|
||||
db.update(TABLE_NAME)
|
||||
.values(COLLAPSED_STATE to CollapsedState.HEAD_COLLAPSED.id)
|
||||
.where("$COLLAPSED_STATE = ?", CollapsedState.HEAD_EXPANDED.id)
|
||||
.run()
|
||||
|
||||
db.update(TABLE_NAME)
|
||||
.values(COLLAPSED_STATE to CollapsedState.COLLAPSED.id)
|
||||
.where("$COLLAPSED_STATE = ?", CollapsedState.EXPANDED.id)
|
||||
.run()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove duplicate messages that were imported from a backup without the same sql constraint on the this table.
|
||||
*
|
||||
@@ -6451,6 +6711,8 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat
|
||||
val pinnedUntil = cursor.requireLong(PINNED_UNTIL)
|
||||
val deletedBy = cursor.requireLongOrNull(DELETED_BY)?.let { RecipientId.from(it) }
|
||||
val isStarred = cursor.requireBoolean(STARRED)
|
||||
val collapsedState = CollapsedState.deserialize(cursor.requireLong(COLLAPSED_STATE))
|
||||
val collapsedHeadId = cursor.requireLong(COLLAPSED_HEAD_ID)
|
||||
val messageExtraBytes = cursor.requireBlob(MESSAGE_EXTRAS)
|
||||
val messageExtras = messageExtraBytes?.let { MessageExtras.ADAPTER.decode(it) }
|
||||
|
||||
@@ -6546,6 +6808,8 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat
|
||||
isRead,
|
||||
pinnedUntil,
|
||||
deletedBy,
|
||||
collapsedState,
|
||||
collapsedHeadId,
|
||||
messageExtras,
|
||||
isStarred
|
||||
)
|
||||
|
||||
@@ -450,6 +450,7 @@ class ThreadTable(context: Context, databaseHelper: SignalDatabase) : DatabaseTa
|
||||
|
||||
if (deletes > 0) {
|
||||
Log.i(TAG, "Trimming deleted $deletes messages thread: $threadId")
|
||||
messages.fixPotentialDanglingCollapsibleEvent(threadId)
|
||||
setLastScrolled(threadId, 0)
|
||||
val threadDeleted = update(threadId = threadId, unarchive = false, syncThreadDelete = syncThreadTrimDeletes)
|
||||
notifyConversationListeners(threadId)
|
||||
@@ -499,6 +500,7 @@ class ThreadTable(context: Context, databaseHelper: SignalDatabase) : DatabaseTa
|
||||
|
||||
messages.setAllReactionsSeen()
|
||||
messages.setAllVotesSeen()
|
||||
messages.collapseAllPendingCollapsibleEvents()
|
||||
notifyConversationListListeners()
|
||||
|
||||
return messageRecords
|
||||
@@ -561,6 +563,7 @@ class ThreadTable(context: Context, databaseHelper: SignalDatabase) : DatabaseTa
|
||||
|
||||
messages.setReactionsSeen(threadId, sinceTimestamp)
|
||||
messages.setVoteSeen(threadId, sinceTimestamp)
|
||||
messages.collapsePendingCollapsibleEvents(threadId, sinceTimestamp)
|
||||
|
||||
val unreadCount = messages.getUnreadCount(threadId)
|
||||
val unreadMentionsCount = messages.getUnreadMentionCount(threadId)
|
||||
|
||||
+4
-2
@@ -165,6 +165,7 @@ import org.thoughtcrime.securesms.database.helpers.migration.V309_GroupTerminate
|
||||
import org.thoughtcrime.securesms.database.helpers.migration.V310_AddStarredColumn
|
||||
import org.thoughtcrime.securesms.database.helpers.migration.V311_AddAttachmentMediaOverviewSizeIndex
|
||||
import org.thoughtcrime.securesms.database.helpers.migration.V312_RefactorNameCollisionTables
|
||||
import org.thoughtcrime.securesms.database.helpers.migration.V313_AddCollapsingUpdateColumns
|
||||
import org.thoughtcrime.securesms.database.SQLiteDatabase as SignalSqliteDatabase
|
||||
|
||||
/**
|
||||
@@ -337,10 +338,11 @@ object SignalDatabaseMigrations {
|
||||
309 to V309_GroupTerminatedColumnMigration,
|
||||
310 to V310_AddStarredColumn,
|
||||
311 to V311_AddAttachmentMediaOverviewSizeIndex,
|
||||
312 to V312_RefactorNameCollisionTables
|
||||
312 to V312_RefactorNameCollisionTables,
|
||||
313 to V313_AddCollapsingUpdateColumns
|
||||
)
|
||||
|
||||
const val DATABASE_VERSION = 312
|
||||
const val DATABASE_VERSION = 313
|
||||
|
||||
@JvmStatic
|
||||
fun migrate(context: Application, db: SignalSqliteDatabase, oldVersion: Int, newVersion: Int) {
|
||||
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
package org.thoughtcrime.securesms.database.helpers.migration
|
||||
|
||||
import android.app.Application
|
||||
import org.thoughtcrime.securesms.database.SQLiteDatabase
|
||||
|
||||
/**
|
||||
* Adds the columns and indexes necessary for collapsing updates
|
||||
*/
|
||||
@Suppress("ClassName")
|
||||
object V313_AddCollapsingUpdateColumns : SignalDatabaseMigration {
|
||||
override fun migrate(context: Application, db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {
|
||||
db.execSQL("ALTER TABLE message ADD COLUMN collapsed_state INTEGER DEFAULT 0")
|
||||
db.execSQL("ALTER TABLE message ADD COLUMN collapsed_head_id INTEGER DEFAULT 0")
|
||||
|
||||
db.execSQL("CREATE INDEX IF NOT EXISTS message_collapsed_state_index ON message (collapsed_state)")
|
||||
db.execSQL("CREATE INDEX message_collapsed_head_id_index ON message (collapsed_head_id)")
|
||||
|
||||
// Adjust existing index to disregard collapsed updates from the thread count
|
||||
db.execSQL("DROP INDEX IF EXISTS message_thread_count_index")
|
||||
db.execSQL("CREATE INDEX message_thread_count_index ON message (thread_id) WHERE story_type = 0 AND parent_story_id <= 0 AND scheduled_date = -1 AND latest_revision_id IS NULL AND collapsed_state != 3")
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,7 @@ import androidx.annotation.Nullable;
|
||||
import androidx.annotation.StringRes;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.database.CollapsedState;
|
||||
import org.thoughtcrime.securesms.fonts.SignalSymbols.Glyph;
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
@@ -60,6 +61,8 @@ public class InMemoryMessageRecord extends MessageRecord {
|
||||
0,
|
||||
0,
|
||||
null,
|
||||
CollapsedState.NONE,
|
||||
0,
|
||||
null,
|
||||
false);
|
||||
}
|
||||
|
||||
@@ -43,6 +43,7 @@ import org.signal.archive.proto.GroupCreationUpdate;
|
||||
import org.thoughtcrime.securesms.components.emoji.EmojiProvider;
|
||||
import org.thoughtcrime.securesms.components.emoji.parsing.EmojiParser;
|
||||
import org.thoughtcrime.securesms.components.transfercontrols.TransferControlView;
|
||||
import org.thoughtcrime.securesms.database.CollapsedState;
|
||||
import org.thoughtcrime.securesms.database.MessageTypes;
|
||||
import org.thoughtcrime.securesms.database.documents.IdentityKeyMismatch;
|
||||
import org.thoughtcrime.securesms.database.documents.NetworkFailure;
|
||||
@@ -115,6 +116,8 @@ public abstract class MessageRecord extends DisplayRecord {
|
||||
private final int revisionNumber;
|
||||
private final long pinnedUntil;
|
||||
private final RecipientId deletedBy;
|
||||
private final CollapsedState collapsedState;
|
||||
private final long collapsedHeadId;
|
||||
private final MessageExtras messageExtras;
|
||||
private final boolean starred;
|
||||
|
||||
@@ -139,6 +142,8 @@ public abstract class MessageRecord extends DisplayRecord {
|
||||
int revisionNumber,
|
||||
long pinnedUntil,
|
||||
@Nullable RecipientId deletedBy,
|
||||
CollapsedState collapsedState,
|
||||
long collapsedHeadId,
|
||||
@Nullable MessageExtras messageExtras,
|
||||
boolean starred)
|
||||
{
|
||||
@@ -162,6 +167,8 @@ public abstract class MessageRecord extends DisplayRecord {
|
||||
this.revisionNumber = revisionNumber;
|
||||
this.pinnedUntil = pinnedUntil;
|
||||
this.deletedBy = deletedBy;
|
||||
this.collapsedState = collapsedState;
|
||||
this.collapsedHeadId = collapsedHeadId;
|
||||
this.messageExtras = messageExtras;
|
||||
this.starred = starred;
|
||||
}
|
||||
@@ -818,6 +825,14 @@ public abstract class MessageRecord extends DisplayRecord {
|
||||
messageExtras.adminDeleteStatus.status == AdminDeleteStatus.Status.FAILED;
|
||||
}
|
||||
|
||||
public CollapsedState getCollapsedState() {
|
||||
return collapsedState;
|
||||
}
|
||||
|
||||
public long getCollapsedHeadId() {
|
||||
return collapsedHeadId;
|
||||
}
|
||||
|
||||
public boolean isInMemoryMessageRecord() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ import org.thoughtcrime.securesms.attachments.AttachmentId;
|
||||
import org.thoughtcrime.securesms.attachments.DatabaseAttachment;
|
||||
import org.thoughtcrime.securesms.contactshare.Contact;
|
||||
import org.thoughtcrime.securesms.database.CallTable;
|
||||
import org.thoughtcrime.securesms.database.CollapsedState;
|
||||
import org.thoughtcrime.securesms.database.MessageTable;
|
||||
import org.thoughtcrime.securesms.database.MessageTable.Status;
|
||||
import org.thoughtcrime.securesms.database.MessageTypes;
|
||||
@@ -121,13 +122,15 @@ public class MmsMessageRecord extends MessageRecord {
|
||||
boolean isRead,
|
||||
long pinnedUntil,
|
||||
@Nullable RecipientId deletedBy,
|
||||
@NonNull CollapsedState collapsedState,
|
||||
long collapsedHeadId,
|
||||
@Nullable MessageExtras messageExtras,
|
||||
boolean starred)
|
||||
{
|
||||
super(id, body, fromRecipient, fromDeviceId, toRecipient,
|
||||
dateSent, dateReceived, dateServer, threadId, Status.STATUS_NONE, hasDeliveryReceipt,
|
||||
mailbox, mismatches, failures, subscriptionId, expiresIn, expireStarted, expireTimerVersion, hasReadReceipt,
|
||||
unidentified, reactions, notifiedTimestamp, viewed, receiptTimestamp, originalMessageId, revisionNumber, pinnedUntil, deletedBy, messageExtras, starred);
|
||||
unidentified, reactions, notifiedTimestamp, viewed, receiptTimestamp, originalMessageId, revisionNumber, pinnedUntil, deletedBy, collapsedState, collapsedHeadId, messageExtras, starred);
|
||||
|
||||
this.slideDeck = slideDeck;
|
||||
this.quote = quote;
|
||||
@@ -341,7 +344,7 @@ public class MmsMessageRecord extends MessageRecord {
|
||||
incomingType, getIdentityKeyMismatches(), getNetworkFailures(), getSubscriptionId(), getExpiresIn(), getExpireStarted(), getExpireTimerVersion(), isViewOnce(),
|
||||
hasReadReceipt(), getQuote(), getSharedContacts(), getLinkPreviews(), isUnidentified(), getReactions(), mentionsSelf,
|
||||
getNotifiedTimestamp(), isViewed(), getReceiptTimestamp(), getMessageRanges(), getStoryType(), getParentStoryId(), getGiftBadge(), getPayment(), getCall(), getPoll(), getScheduledDate(), getLatestRevisionId(),
|
||||
getOriginalMessageId(), getRevisionNumber(), isRead(), getPinnedUntil(), getDeletedBy(), getMessageExtras(), isStarred());
|
||||
getOriginalMessageId(), getRevisionNumber(), isRead(), getPinnedUntil(), getDeletedBy(), getCollapsedState(), getCollapsedHeadId(), getMessageExtras(), isStarred());
|
||||
}
|
||||
|
||||
public @NonNull MmsMessageRecord withReactions(@NonNull List<ReactionRecord> reactions) {
|
||||
@@ -349,7 +352,7 @@ public class MmsMessageRecord extends MessageRecord {
|
||||
getType(), getIdentityKeyMismatches(), getNetworkFailures(), getSubscriptionId(), getExpiresIn(), getExpireStarted(), getExpireTimerVersion(), isViewOnce(),
|
||||
hasReadReceipt(), getQuote(), getSharedContacts(), getLinkPreviews(), isUnidentified(), reactions, mentionsSelf,
|
||||
getNotifiedTimestamp(), isViewed(), getReceiptTimestamp(), getMessageRanges(), getStoryType(), getParentStoryId(), getGiftBadge(), getPayment(), getCall(), getPoll(), getScheduledDate(), getLatestRevisionId(),
|
||||
getOriginalMessageId(), getRevisionNumber(), isRead(), getPinnedUntil(), getDeletedBy(), getMessageExtras(), isStarred());
|
||||
getOriginalMessageId(), getRevisionNumber(), isRead(), getPinnedUntil(), getDeletedBy(), getCollapsedState(), getCollapsedHeadId(), getMessageExtras(), isStarred());
|
||||
}
|
||||
|
||||
public @NonNull MmsMessageRecord withoutQuote() {
|
||||
@@ -357,7 +360,7 @@ public class MmsMessageRecord extends MessageRecord {
|
||||
getType(), getIdentityKeyMismatches(), getNetworkFailures(), getSubscriptionId(), getExpiresIn(), getExpireStarted(), getExpireTimerVersion(), isViewOnce(),
|
||||
hasReadReceipt(), null, getSharedContacts(), getLinkPreviews(), isUnidentified(), getReactions(), mentionsSelf,
|
||||
getNotifiedTimestamp(), isViewed(), getReceiptTimestamp(), getMessageRanges(), getStoryType(), getParentStoryId(), getGiftBadge(), getPayment(), getCall(), getPoll(), getScheduledDate(), getLatestRevisionId(),
|
||||
getOriginalMessageId(), getRevisionNumber(), isRead(), getPinnedUntil(), getDeletedBy(), getMessageExtras(), isStarred());
|
||||
getOriginalMessageId(), getRevisionNumber(), isRead(), getPinnedUntil(), getDeletedBy(), getCollapsedState(), getCollapsedHeadId(), getMessageExtras(), isStarred());
|
||||
}
|
||||
|
||||
public @NonNull MmsMessageRecord withAttachments(@NonNull List<DatabaseAttachment> attachments) {
|
||||
@@ -379,7 +382,7 @@ public class MmsMessageRecord extends MessageRecord {
|
||||
getType(), getIdentityKeyMismatches(), getNetworkFailures(), getSubscriptionId(), getExpiresIn(), getExpireStarted(), getExpireTimerVersion(), isViewOnce(),
|
||||
hasReadReceipt(), quote, contacts, linkPreviews, isUnidentified(), getReactions(), mentionsSelf,
|
||||
getNotifiedTimestamp(), isViewed(), getReceiptTimestamp(), getMessageRanges(), getStoryType(), getParentStoryId(), getGiftBadge(), getPayment(), getCall(), getPoll(), getScheduledDate(), getLatestRevisionId(),
|
||||
getOriginalMessageId(), getRevisionNumber(), isRead(), getPinnedUntil(), getDeletedBy(), getMessageExtras(), isStarred());
|
||||
getOriginalMessageId(), getRevisionNumber(), isRead(), getPinnedUntil(), getDeletedBy(), getCollapsedState(), getCollapsedHeadId(), getMessageExtras(), isStarred());
|
||||
}
|
||||
|
||||
public @NonNull MmsMessageRecord withPayment(@NonNull Payment payment) {
|
||||
@@ -387,7 +390,7 @@ public class MmsMessageRecord extends MessageRecord {
|
||||
getType(), getIdentityKeyMismatches(), getNetworkFailures(), getSubscriptionId(), getExpiresIn(), getExpireStarted(), getExpireTimerVersion(), isViewOnce(),
|
||||
hasReadReceipt(), getQuote(), getSharedContacts(), getLinkPreviews(), isUnidentified(), getReactions(), mentionsSelf,
|
||||
getNotifiedTimestamp(), isViewed(), getReceiptTimestamp(), getMessageRanges(), getStoryType(), getParentStoryId(), getGiftBadge(), payment, getCall(), getPoll(), getScheduledDate(), getLatestRevisionId(),
|
||||
getOriginalMessageId(), getRevisionNumber(), isRead(), getPinnedUntil(), getDeletedBy(), getMessageExtras(), isStarred());
|
||||
getOriginalMessageId(), getRevisionNumber(), isRead(), getPinnedUntil(), getDeletedBy(), getCollapsedState(), getCollapsedHeadId(), getMessageExtras(), isStarred());
|
||||
}
|
||||
|
||||
|
||||
@@ -396,7 +399,7 @@ public class MmsMessageRecord extends MessageRecord {
|
||||
getType(), getIdentityKeyMismatches(), getNetworkFailures(), getSubscriptionId(), getExpiresIn(), getExpireStarted(), getExpireTimerVersion(), isViewOnce(),
|
||||
hasReadReceipt(), getQuote(), getSharedContacts(), getLinkPreviews(), isUnidentified(), getReactions(), mentionsSelf,
|
||||
getNotifiedTimestamp(), isViewed(), getReceiptTimestamp(), getMessageRanges(), getStoryType(), getParentStoryId(), getGiftBadge(), getPayment(), call, getPoll(), getScheduledDate(), getLatestRevisionId(),
|
||||
getOriginalMessageId(), getRevisionNumber(), isRead(), getPinnedUntil(), getDeletedBy(), getMessageExtras(), isStarred());
|
||||
getOriginalMessageId(), getRevisionNumber(), isRead(), getPinnedUntil(), getDeletedBy(), getCollapsedState(), getCollapsedHeadId(), getMessageExtras(), isStarred());
|
||||
}
|
||||
|
||||
public @NonNull MmsMessageRecord withPoll(@Nullable PollRecord poll) {
|
||||
@@ -404,7 +407,7 @@ public class MmsMessageRecord extends MessageRecord {
|
||||
getType(), getIdentityKeyMismatches(), getNetworkFailures(), getSubscriptionId(), getExpiresIn(), getExpireStarted(), getExpireTimerVersion(), isViewOnce(),
|
||||
hasReadReceipt(), getQuote(), getSharedContacts(), getLinkPreviews(), isUnidentified(), getReactions(), mentionsSelf,
|
||||
getNotifiedTimestamp(), isViewed(), getReceiptTimestamp(), getMessageRanges(), getStoryType(), getParentStoryId(), getGiftBadge(), getPayment(), getCall(), poll, getScheduledDate(), getLatestRevisionId(),
|
||||
getOriginalMessageId(), getRevisionNumber(), isRead(), getPinnedUntil(), getDeletedBy(), getMessageExtras(), isStarred());
|
||||
getOriginalMessageId(), getRevisionNumber(), isRead(), getPinnedUntil(), getDeletedBy(), getCollapsedState(), getCollapsedHeadId(), getMessageExtras(), isStarred());
|
||||
}
|
||||
|
||||
private static @NonNull List<Contact> updateContacts(@NonNull List<Contact> contacts, @NonNull Map<AttachmentId, DatabaseAttachment> attachmentIdMap) {
|
||||
|
||||
@@ -417,6 +417,14 @@ class MessageDetailsFragment : Fragment(), MessageDetailsAdapter.Callbacks {
|
||||
Toast.makeText(requireContext(), "Can't touch this.", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
|
||||
override fun onExpandEvents(messageId: Long) {
|
||||
Toast.makeText(requireContext(), "Can't touch this.", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
|
||||
override fun onCollapseEvents(messageId: Long) {
|
||||
Toast.makeText(requireContext(), "Can't touch this.", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
|
||||
class Dialog : WrapperDialogFragment() {
|
||||
override fun getWrappedFragment(): Fragment {
|
||||
return MessageDetailsFragment().apply {
|
||||
|
||||
@@ -421,4 +421,6 @@ private class StarredMessageClickListener(
|
||||
override fun onUpdateSignalClicked() = Unit
|
||||
override fun onViewPollClicked(messageId: Long) = Unit
|
||||
override fun onViewPinnedMessage(messageId: Long) = Unit
|
||||
override fun onCollapseEvents(messageId: Long) = Unit
|
||||
override fun onExpandEvents(messageId: Long) = Unit
|
||||
}
|
||||
|
||||
@@ -1315,5 +1315,16 @@ object RemoteConfig {
|
||||
hotSwappable = true
|
||||
)
|
||||
|
||||
/**
|
||||
* Whether to collapse update events
|
||||
*/
|
||||
@JvmStatic
|
||||
@get:JvmName("collapseEvents")
|
||||
val collapseEvents: Boolean by remoteBoolean(
|
||||
key = "android.collapseEvents",
|
||||
defaultValue = false,
|
||||
hotSwappable = true
|
||||
)
|
||||
|
||||
// endregion
|
||||
}
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="16dp"
|
||||
android:height="16dp"
|
||||
android:viewportWidth="16"
|
||||
android:viewportHeight="16">
|
||||
<path
|
||||
android:pathData="M13.46,5.79C13.714,6.044 13.714,6.456 13.46,6.71L8.46,11.71C8.338,11.832 8.172,11.9 8,11.9C7.828,11.9 7.662,11.832 7.54,11.71L2.54,6.71C2.287,6.456 2.287,6.044 2.54,5.79C2.794,5.537 3.206,5.537 3.46,5.79L8,10.331L12.54,5.79C12.794,5.537 13.206,5.537 13.46,5.79Z"
|
||||
android:fillColor="#FF000000"/>
|
||||
</vector>
|
||||
@@ -0,0 +1,9 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="16dp"
|
||||
android:height="16dp"
|
||||
android:viewportWidth="16"
|
||||
android:viewportHeight="16">
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M2.54 10.46c-0.25-0.25-0.25-0.67 0-0.92l5-5C7.66 4.42 7.83 4.35 8 4.35c0.17 0 0.34 0.07 0.46 0.19l5 5c0.25 0.25 0.25 0.67 0 0.92s-0.67 0.25-0.92 0L8 5.92l-4.54 4.54c-0.25 0.25-0.67 0.25-0.92 0Z"/>
|
||||
</vector>
|
||||
@@ -0,0 +1,12 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="16dp"
|
||||
android:height="16dp"
|
||||
android:viewportWidth="16"
|
||||
android:viewportHeight="16">
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M8 4.25c0.41 0 0.75 0.34 0.75 0.75v3c0.3 0.23 0.5 0.6 0.5 1 0 0.69-0.56 1.25-1.25 1.25S6.75 9.69 6.75 9c0-0.4 0.2-0.77 0.5-1V5c0-0.41 0.34-0.75 0.75-0.75Z"/>
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M8 0.25c0.41 0 0.75 0.34 0.75 0.75v0.8c1.3 0.14 2.5 0.65 3.46 1.43l0.63-0.64c0.3-0.29 0.77-0.29 1.07 0 0.29 0.3 0.29 0.77 0 1.07l-0.64 0.63c0.93 1.15 1.48 2.62 1.48 4.21 0 3.73-3.02 6.75-6.75 6.75S1.25 12.23 1.25 8.5c0-3.47 2.63-6.34 6-6.7V1c0-0.41 0.34-0.75 0.75-0.75ZM2.75 8.5c0 2.9 2.35 5.25 5.25 5.25s5.25-2.35 5.25-5.25S10.9 3.25 8 3.25 2.75 5.6 2.75 8.5Z"/>
|
||||
</vector>
|
||||
@@ -24,6 +24,20 @@
|
||||
android:paddingStart="16dp"
|
||||
android:paddingEnd="16dp">
|
||||
|
||||
<androidx.appcompat.widget.AppCompatButton
|
||||
android:id="@+id/conversation_update_collapsed"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:minHeight="32dp"
|
||||
android:layout_marginBottom="12dp"
|
||||
android:background="@drawable/rounded_rectangle_38"
|
||||
android:paddingHorizontal="16dp"
|
||||
android:textColor="@color/signal_colorOnSurfaceVariant"
|
||||
android:textAllCaps="false"
|
||||
style="?android:attr/borderlessButtonStyle"
|
||||
android:backgroundTint="@color/signal_colorSurfaceVariant"
|
||||
tools:text="12 group updates" />
|
||||
|
||||
<org.thoughtcrime.securesms.components.emoji.EmojiTextView
|
||||
android:id="@+id/conversation_update_body"
|
||||
style="@style/Signal.Text.BodySmall"
|
||||
|
||||
@@ -3690,6 +3690,12 @@
|
||||
<string name="ConversationUpdateItem_options">Options</string>
|
||||
<!-- Update item button text shown for a profile name change. -->
|
||||
<string name="ConversationUpdateItem_update">Update</string>
|
||||
<!-- Update item button text to show how many group updates there are. -->
|
||||
<string name="CollapsedEvent__group_update">%1$d group updates</string>
|
||||
<!-- Update item button text to show how many disappearing message timer changes are. -->
|
||||
<string name="CollapsedEvent__disappearing_timer">%1$d disappearing message timer changes</string>
|
||||
<!-- Update item button text to show how many call events are. -->
|
||||
<string name="CollapsedEvent__call_event">%1$d call events</string>
|
||||
|
||||
<!-- audio_view -->
|
||||
<string name="audio_view__play_pause_accessibility_description">Play … Pause</string>
|
||||
|
||||
@@ -8,6 +8,7 @@ import org.signal.spinner.Spinner
|
||||
import org.signal.spinner.Spinner.DatabaseConfig
|
||||
import org.signal.spinner.SpinnerLogger
|
||||
import org.thoughtcrime.securesms.database.AttachmentTransformer
|
||||
import org.thoughtcrime.securesms.database.CollapsedStateTransformer
|
||||
import org.thoughtcrime.securesms.database.DatabaseMonitor
|
||||
import org.thoughtcrime.securesms.database.GV2Transformer
|
||||
import org.thoughtcrime.securesms.database.GV2UpdateTransformer
|
||||
@@ -74,7 +75,8 @@ class SpinnerApplicationContext : ApplicationContext() {
|
||||
RecipientTransformer,
|
||||
AttachmentTransformer,
|
||||
PollTransformer,
|
||||
IdPopupTransformer
|
||||
IdPopupTransformer,
|
||||
CollapsedStateTransformer
|
||||
)
|
||||
),
|
||||
"jobmanager" to DatabaseConfig(db = { JobDatabase.getInstance(this).sqlCipherDatabase }, columnTransformers = listOf(TimestampTransformer)),
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
package org.thoughtcrime.securesms.database
|
||||
|
||||
import android.database.Cursor
|
||||
import org.signal.core.util.requireInt
|
||||
import org.signal.core.util.requireLong
|
||||
import org.signal.spinner.ColumnTransformer
|
||||
|
||||
/**
|
||||
* Transforms enum integers for [CollapsedState] into a human readable state
|
||||
*/
|
||||
object CollapsedStateTransformer : ColumnTransformer {
|
||||
override fun matches(tableName: String?, columnName: String): Boolean {
|
||||
return columnName == MessageTable.COLLAPSED_STATE && (tableName == null || tableName == MessageTable.TABLE_NAME)
|
||||
}
|
||||
|
||||
override fun transform(tableName: String?, columnName: String, cursor: Cursor): String? {
|
||||
val state = CollapsedState.deserialize(cursor.requireLong(MessageTable.COLLAPSED_STATE))
|
||||
return "${cursor.requireInt(MessageTable.COLLAPSED_STATE)}<br><br>$state"
|
||||
}
|
||||
}
|
||||
@@ -210,6 +210,8 @@ object FakeMessageRecords {
|
||||
false,
|
||||
0,
|
||||
deletedBy,
|
||||
CollapsedState.NONE,
|
||||
0,
|
||||
null,
|
||||
false
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user