Turn on collapsing chat events for internal users.

This commit is contained in:
Michelle Tang
2026-03-23 12:05:35 -04:00
committed by Cody Henthorne
parent 94e3dabc20
commit c3b8768570
32 changed files with 977 additions and 42 deletions

View File

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

View File

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

View File

@@ -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()) ||

View File

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

View File

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

View File

@@ -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.

View File

@@ -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

View File

@@ -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) {

View File

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

View File

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

View File

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

View File

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

View File

@@ -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) {

View File

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

View File

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

View File

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

View File

@@ -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) {

View File

@@ -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 {

View File

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

View File

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