mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-04-21 09:20:19 +01:00
Add support for starring messages.
This commit is contained in:
committed by
Cody Henthorne
parent
6496f236ea
commit
48374e6950
@@ -170,6 +170,7 @@ import org.thoughtcrime.securesms.notifications.profiles.NotificationProfiles
|
||||
import org.thoughtcrime.securesms.profiles.manage.UsernameEditFragment
|
||||
import org.thoughtcrime.securesms.service.BackupMediaRestoreService
|
||||
import org.thoughtcrime.securesms.service.KeyCachingService
|
||||
import org.thoughtcrime.securesms.starred.StarredMessagesActivity
|
||||
import org.thoughtcrime.securesms.stories.Stories
|
||||
import org.thoughtcrime.securesms.stories.archive.StoryArchiveActivity
|
||||
import org.thoughtcrime.securesms.stories.landing.StoriesLandingFragment
|
||||
@@ -1158,6 +1159,10 @@ class MainActivity :
|
||||
toolbarViewModel.setChatFilter(ConversationFilter.OFF)
|
||||
}
|
||||
|
||||
override fun onStarredMessagesClick() {
|
||||
startActivity(StarredMessagesActivity.createIntent(this@MainActivity))
|
||||
}
|
||||
|
||||
override fun onSettingsClick() {
|
||||
openSettings.launch(AppSettingsActivity.home(this@MainActivity))
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ import android.graphics.Rect;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.ViewStub;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.TextView;
|
||||
|
||||
@@ -24,6 +25,7 @@ import com.airbnb.lottie.LottieAnimationView;
|
||||
import com.airbnb.lottie.LottieProperty;
|
||||
import com.airbnb.lottie.model.KeyPath;
|
||||
|
||||
import org.signal.core.ui.view.Stub;
|
||||
import org.signal.core.util.concurrent.SignalExecutors;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.animation.AnimationCompleteListener;
|
||||
@@ -56,6 +58,8 @@ public class ConversationItemFooter extends ConstraintLayout {
|
||||
private ImageView insecureIndicatorView;
|
||||
private DeliveryStatusView deliveryStatusView;
|
||||
private ImageView pinnedView;
|
||||
private Stub<ImageView> starredStub;
|
||||
private int iconColor;
|
||||
private boolean onlyShowSendingStatus;
|
||||
private TextView audioDuration;
|
||||
private LottieAnimationView revealDot;
|
||||
@@ -100,6 +104,7 @@ public class ConversationItemFooter extends ConstraintLayout {
|
||||
insecureIndicatorView = findViewById(R.id.footer_insecure_indicator);
|
||||
deliveryStatusView = findViewById(R.id.footer_delivery_status);
|
||||
pinnedView = findViewById(R.id.footer_pinned);
|
||||
starredStub = new Stub<>((ViewStub) findViewById(R.id.footer_starred));
|
||||
audioDuration = findViewById(R.id.footer_audio_duration);
|
||||
revealDot = findViewById(R.id.footer_revealed_dot);
|
||||
playbackSpeedToggleTextView = findViewById(R.id.footer_audio_playback_speed_toggle);
|
||||
@@ -146,6 +151,7 @@ public class ConversationItemFooter extends ConstraintLayout {
|
||||
presentDeliveryStatus(messageRecord);
|
||||
presentAudioDuration(messageRecord);
|
||||
presentPinnedIcon(messageRecord);
|
||||
presentStarredIcon(messageRecord);
|
||||
}
|
||||
|
||||
public void setAudioDuration(long totalDurationMillis, long currentPostionMillis) {
|
||||
@@ -174,10 +180,14 @@ public class ConversationItemFooter extends ConstraintLayout {
|
||||
}
|
||||
|
||||
public void setIconColor(int color) {
|
||||
iconColor = color;
|
||||
timerView.setColorFilter(color, PorterDuff.Mode.SRC_IN);
|
||||
insecureIndicatorView.setColorFilter(color);
|
||||
deliveryStatusView.setTint(color);
|
||||
pinnedView.setColorFilter(color, PorterDuff.Mode.SRC_IN);
|
||||
if (starredStub.resolved()) {
|
||||
starredStub.get().setColorFilter(color, PorterDuff.Mode.SRC_IN);
|
||||
}
|
||||
}
|
||||
|
||||
public void setRevealDotColor(int color) {
|
||||
@@ -449,6 +459,16 @@ public class ConversationItemFooter extends ConstraintLayout {
|
||||
}
|
||||
}
|
||||
|
||||
private void presentStarredIcon(@NonNull MessageRecord messageRecord) {
|
||||
if (messageRecord.isStarred()) {
|
||||
ImageView starredView = starredStub.get();
|
||||
starredView.setColorFilter(iconColor, PorterDuff.Mode.SRC_IN);
|
||||
starredView.setVisibility(View.VISIBLE);
|
||||
} else {
|
||||
starredStub.setVisibility(View.GONE);
|
||||
}
|
||||
}
|
||||
|
||||
private void showAudioDurationViews() {
|
||||
audioDuration.setVisibility(View.VISIBLE);
|
||||
revealDot.setVisibility(View.VISIBLE);
|
||||
|
||||
@@ -13,4 +13,5 @@ sealed interface LabsSettingsEvents {
|
||||
data class ToggleBetterSearch(val enabled: Boolean) : LabsSettingsEvents
|
||||
data class ToggleAutoLowerHand(val enabled: Boolean) : LabsSettingsEvents
|
||||
data class ToggleNewApngRenderer(val enabled: Boolean) : LabsSettingsEvents
|
||||
data class ToggleStarredMessages(val enabled: Boolean) : LabsSettingsEvents
|
||||
}
|
||||
|
||||
@@ -151,6 +151,15 @@ private fun LabsSettingsContent(
|
||||
onCheckChanged = { onEvent(LabsSettingsEvents.ToggleNewApngRenderer(it)) }
|
||||
)
|
||||
}
|
||||
|
||||
item {
|
||||
Rows.ToggleRow(
|
||||
checked = state.starredMessages,
|
||||
text = "Starred Messages",
|
||||
label = "Enables starring messages for later reference. Adds star/unstar to the long-press menu and a starred messages screen accessible from conversation settings and the main menu.",
|
||||
onCheckChanged = { onEvent(LabsSettingsEvents.ToggleStarredMessages(it)) }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,5 +15,6 @@ data class LabsSettingsState(
|
||||
val groupSuggestionsForMembers: Boolean = false,
|
||||
val betterSearch: Boolean = false,
|
||||
val autoLowerHand: Boolean = false,
|
||||
val newApngRenderer: Boolean = false
|
||||
val newApngRenderer: Boolean = false,
|
||||
val starredMessages: Boolean = false
|
||||
)
|
||||
|
||||
@@ -45,6 +45,10 @@ class LabsSettingsViewModel : ViewModel() {
|
||||
SignalStore.labs.newApngRenderer = event.enabled
|
||||
_state.value = _state.value.copy(newApngRenderer = event.enabled)
|
||||
}
|
||||
is LabsSettingsEvents.ToggleStarredMessages -> {
|
||||
SignalStore.labs.starredMessages = event.enabled
|
||||
_state.value = _state.value.copy(starredMessages = event.enabled)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -56,7 +60,8 @@ class LabsSettingsViewModel : ViewModel() {
|
||||
groupSuggestionsForMembers = SignalStore.labs.groupSuggestionsForMembers,
|
||||
betterSearch = SignalStore.labs.betterSearch,
|
||||
autoLowerHand = SignalStore.labs.autoLowerHand,
|
||||
newApngRenderer = SignalStore.labs.newApngRenderer
|
||||
newApngRenderer = SignalStore.labs.newApngRenderer,
|
||||
starredMessages = SignalStore.labs.starredMessages
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -89,6 +89,7 @@ import org.thoughtcrime.securesms.groups.ui.invitesandrequests.ManagePendingAndR
|
||||
import org.thoughtcrime.securesms.groups.ui.managegroup.dialogs.GroupDescriptionDialog
|
||||
import org.thoughtcrime.securesms.groups.ui.managegroup.dialogs.GroupInviteSentDialog
|
||||
import org.thoughtcrime.securesms.groups.ui.managegroup.dialogs.GroupsLearnMoreBottomSheetDialogFragment
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.mediaoverview.MediaOverviewActivity
|
||||
import org.thoughtcrime.securesms.mediapreview.MediaIntentFactory
|
||||
import org.thoughtcrime.securesms.mediasend.camerax.CameraXUtil
|
||||
@@ -100,6 +101,7 @@ import org.thoughtcrime.securesms.recipients.RecipientExporter
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
import org.thoughtcrime.securesms.recipients.ui.about.AboutSheet
|
||||
import org.thoughtcrime.securesms.recipients.ui.bottomsheet.RecipientBottomSheetDialogFragment
|
||||
import org.thoughtcrime.securesms.starred.StarredMessagesActivity
|
||||
import org.thoughtcrime.securesms.stories.Stories
|
||||
import org.thoughtcrime.securesms.stories.StoryViewerArgs
|
||||
import org.thoughtcrime.securesms.stories.dialogs.StoryDialogs
|
||||
@@ -634,6 +636,16 @@ class ConversationSettingsFragment :
|
||||
)
|
||||
}
|
||||
|
||||
if (!state.recipient.isReleaseNotes && SignalStore.labs.starredMessages) {
|
||||
clickPref(
|
||||
title = DSLSettingsText.from(R.string.ConversationSettingsFragment__starred_messages),
|
||||
icon = DSLSettingsIcon.from(R.drawable.symbol_star_outline_24),
|
||||
onClick = {
|
||||
startActivity(StarredMessagesActivity.createIntent(requireContext(), state.threadId))
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
state.withRecipientSettingsState { recipientState ->
|
||||
when (recipientState.contactLinkState) {
|
||||
ContactLinkState.OPEN -> {
|
||||
|
||||
@@ -256,6 +256,7 @@ public class ConversationAdapter
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) {
|
||||
switch (getItemViewType(position)) {
|
||||
@@ -268,8 +269,15 @@ public class ConversationAdapter
|
||||
ConversationMessage conversationMessage = Objects.requireNonNull(getItem(position));
|
||||
int adapterPosition = holder.getAdapterPosition();
|
||||
|
||||
ConversationMessage previousMessage = adapterPosition < getItemCount() - 1 && !isFooterPosition(adapterPosition + 1) ? getItem(adapterPosition + 1) : null;
|
||||
ConversationMessage nextMessage = adapterPosition > 0 && !isHeaderPosition(adapterPosition - 1) ? getItem(adapterPosition - 1) : null;
|
||||
|
||||
ConversationMessage previousMessage = null;
|
||||
ConversationMessage nextMessage = null;
|
||||
|
||||
boolean disableClustering = displayMode instanceof ConversationItemDisplayMode.Starred;
|
||||
if (!disableClustering) {
|
||||
previousMessage = adapterPosition < getItemCount() - 1 && !isFooterPosition(adapterPosition + 1) ? getItem(adapterPosition + 1) : null;
|
||||
nextMessage = adapterPosition > 0 && !isHeaderPosition(adapterPosition - 1) ? getItem(adapterPosition - 1) : null;
|
||||
}
|
||||
|
||||
ConversationItemDisplayMode itemDisplayMode = displayMode != null ? displayMode : ConversationItemDisplayMode.Standard.INSTANCE;
|
||||
|
||||
|
||||
@@ -46,6 +46,7 @@ import android.view.MotionEvent;
|
||||
import android.view.TouchDelegate;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.ViewStub;
|
||||
import android.widget.Button;
|
||||
import android.widget.RelativeLayout;
|
||||
import android.widget.TextView;
|
||||
@@ -218,6 +219,10 @@ public final class ConversationItem extends RelativeLayout implements BindableCo
|
||||
@Nullable private ConversationItemFooter stickerFooter;
|
||||
@Nullable private SenderNameWithLabelView senderWithLabelView;
|
||||
@Nullable private View groupSenderHolder;
|
||||
@Nullable private ViewStub starredSourceStub;
|
||||
@Nullable private View starredSourceWrapper;
|
||||
@Nullable private TextView starredSourceView;
|
||||
@Nullable private AvatarImageView starredSourceAvatar;
|
||||
private AvatarImageView contactPhoto;
|
||||
private AlertView alertView;
|
||||
private ReactionsConversationView reactionsView;
|
||||
@@ -340,6 +345,7 @@ public final class ConversationItem extends RelativeLayout implements BindableCo
|
||||
this.stickerFooter = findViewById(R.id.conversation_item_sticker_footer);
|
||||
this.senderWithLabelView = findViewById(R.id.group_sender_name_with_label);
|
||||
this.groupSenderHolder = findViewById(R.id.group_sender_holder);
|
||||
this.starredSourceStub = findViewById(R.id.conversation_item_starred_source_stub);
|
||||
this.alertView = findViewById(R.id.indicators_parent);
|
||||
this.contactPhoto = findViewById(R.id.contact_photo);
|
||||
this.contactPhotoHolder = findViewById(R.id.contact_photo_container);
|
||||
@@ -423,6 +429,7 @@ public final class ConversationItem extends RelativeLayout implements BindableCo
|
||||
setContactPhoto(author.get());
|
||||
setSenderNameAndLabel(author.get());
|
||||
setAuthor(messageRecord, previousMessageRecord, nextMessageRecord, groupThread, hasWallpaper);
|
||||
setStarredSource(messageRecord, conversationMessage);
|
||||
setQuote(messageRecord, previousMessageRecord, nextMessageRecord, groupThread);
|
||||
setMessageSpacing(context, messageRecord, previousMessageRecord, nextMessageRecord, groupThread);
|
||||
setReactions(messageRecord);
|
||||
@@ -467,7 +474,7 @@ public final class ConversationItem extends RelativeLayout implements BindableCo
|
||||
|
||||
@Override
|
||||
public boolean dispatchTouchEvent(MotionEvent ev) {
|
||||
if (isCondensedMode()) return super.dispatchTouchEvent(ev);
|
||||
if (isSuppressedInteractionMode()) return super.dispatchTouchEvent(ev);
|
||||
|
||||
switch (ev.getAction()) {
|
||||
case MotionEvent.ACTION_DOWN:
|
||||
@@ -716,10 +723,11 @@ public final class ConversationItem extends RelativeLayout implements BindableCo
|
||||
}
|
||||
|
||||
if (conversationRecipient.getId().equals(modified.getId())) {
|
||||
setBubbleState(messageRecord, modified, modified.getHasWallpaper(), colorizer);
|
||||
boolean wallpaper = modified.getHasWallpaper() && displayMode.displayWallpaper();
|
||||
setBubbleState(messageRecord, modified, wallpaper, colorizer);
|
||||
|
||||
if (quoteView != null) {
|
||||
quoteView.setWallpaperEnabled(modified.getHasWallpaper());
|
||||
quoteView.setWallpaperEnabled(wallpaper);
|
||||
}
|
||||
|
||||
if (audioViewStub.resolved()) {
|
||||
@@ -1012,6 +1020,13 @@ public final class ConversationItem extends RelativeLayout implements BindableCo
|
||||
return isCondensedMode() && !previousMessage.isPresent();
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether interactions like swipe-to-reply and direct media opening should be suppressed.
|
||||
*/
|
||||
private boolean isSuppressedInteractionMode() {
|
||||
return isCondensedMode() || displayMode instanceof ConversationItemDisplayMode.Starred;
|
||||
}
|
||||
|
||||
private boolean isStoryReaction(MessageRecord messageRecord) {
|
||||
return MessageRecordUtil.isStoryReaction(messageRecord);
|
||||
}
|
||||
@@ -1625,7 +1640,7 @@ public final class ConversationItem extends RelativeLayout implements BindableCo
|
||||
bottomEnd = 0;
|
||||
}
|
||||
|
||||
if (isStartOfMessageCluster(current, previous, isGroupThread) && !current.isOutgoing() && isGroupThread) {
|
||||
if (!(displayMode instanceof ConversationItemDisplayMode.Starred) && isStartOfMessageCluster(current, previous, isGroupThread) && !current.isOutgoing() && isGroupThread) {
|
||||
topStart = 0;
|
||||
topEnd = 0;
|
||||
}
|
||||
@@ -1941,7 +1956,7 @@ public final class ConversationItem extends RelativeLayout implements BindableCo
|
||||
}
|
||||
|
||||
private void setHasBeenQuoted(@NonNull ConversationMessage message) {
|
||||
if (message.hasBeenQuoted() && !isCondensedMode() && quotedIndicator != null && batchSelected.isEmpty() && displayMode != ConversationItemDisplayMode.EditHistory.INSTANCE) {
|
||||
if (message.hasBeenQuoted() && !isSuppressedInteractionMode() && quotedIndicator != null && batchSelected.isEmpty() && displayMode != ConversationItemDisplayMode.EditHistory.INSTANCE) {
|
||||
quotedIndicator.setVisibility(VISIBLE);
|
||||
quotedIndicator.setOnClickListener(quotedIndicatorClickListener);
|
||||
} else if (quotedIndicator != null) {
|
||||
@@ -1975,7 +1990,7 @@ public final class ConversationItem extends RelativeLayout implements BindableCo
|
||||
}
|
||||
|
||||
private boolean forceFooter(@NonNull MessageRecord messageRecord) {
|
||||
return hasAudio(messageRecord) || MessageRecordUtil.isEditMessage(messageRecord) || displayMode == ConversationItemDisplayMode.EditHistory.INSTANCE || messageRecord.getPinnedUntil() > 0;
|
||||
return hasAudio(messageRecord) || MessageRecordUtil.isEditMessage(messageRecord) || displayMode == ConversationItemDisplayMode.EditHistory.INSTANCE || messageRecord.getPinnedUntil() > 0 || messageRecord.isStarred();
|
||||
}
|
||||
|
||||
private boolean forceGroupHeader(@NonNull MessageRecord messageRecord) {
|
||||
@@ -2018,7 +2033,7 @@ public final class ConversationItem extends RelativeLayout implements BindableCo
|
||||
|
||||
@SuppressWarnings("ConstantConditions")
|
||||
private void setAuthor(@NonNull MessageRecord current, @NonNull Optional<MessageRecord> previous, @NonNull Optional<MessageRecord> next, boolean isGroupThread, boolean hasWallpaper) {
|
||||
if (isGroupThread && !current.isOutgoing()) {
|
||||
if (isGroupThread && !current.isOutgoing() && !(displayMode instanceof ConversationItemDisplayMode.Starred)) {
|
||||
contactPhotoHolder.setVisibility(VISIBLE);
|
||||
|
||||
if (!previous.isPresent() || previous.get().isUpdate() || !current.getFromRecipient().equals(previous.get().getFromRecipient()) ||
|
||||
@@ -2058,6 +2073,32 @@ public final class ConversationItem extends RelativeLayout implements BindableCo
|
||||
}
|
||||
}
|
||||
|
||||
private void setStarredSource(@NonNull MessageRecord current, @NonNull ConversationMessage conversationMessage) {
|
||||
if (starredSourceStub == null && starredSourceWrapper == null) return;
|
||||
|
||||
if (displayMode instanceof ConversationItemDisplayMode.Starred) {
|
||||
if (starredSourceWrapper == null) {
|
||||
starredSourceWrapper = starredSourceStub.inflate();
|
||||
starredSourceView = starredSourceWrapper.findViewById(R.id.conversation_item_starred_source);
|
||||
starredSourceAvatar = starredSourceWrapper.findViewById(R.id.conversation_item_starred_source_avatar);
|
||||
starredSourceStub = null;
|
||||
}
|
||||
|
||||
String senderName = current.getFromRecipient().getShortDisplayName(context);
|
||||
String chatName = conversationMessage.getThreadRecipient().getShortDisplayName(context);
|
||||
|
||||
starredSourceView.setText(context.getString(R.string.StarredMessages__s_chevron_s, senderName, chatName));
|
||||
starredSourceWrapper.setVisibility(VISIBLE);
|
||||
if (starredSourceAvatar != null) {
|
||||
starredSourceAvatar.setAvatar(requestManager, current.getFromRecipient(), false);
|
||||
}
|
||||
} else {
|
||||
if (starredSourceWrapper != null) {
|
||||
starredSourceWrapper.setVisibility(GONE);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void adjustMarginsForSenderVisibility() {
|
||||
boolean senderNameVisible = groupSenderHolder != null && groupSenderHolder.getVisibility() == VISIBLE;
|
||||
boolean hasContentAboveBody = (quoteView != null && quoteView.getVisibility() == VISIBLE)
|
||||
@@ -2183,6 +2224,9 @@ public final class ConversationItem extends RelativeLayout implements BindableCo
|
||||
}
|
||||
|
||||
private boolean isStartOfMessageCluster(@NonNull MessageRecord current, @NonNull Optional<MessageRecord> previous, boolean isGroupThread) {
|
||||
if (displayMode instanceof ConversationItemDisplayMode.Starred) {
|
||||
return true;
|
||||
}
|
||||
if (isGroupThread) {
|
||||
return !previous.isPresent() || previous.get().isUpdate() || !DateUtils.isSameDay(current.getTimestamp(), previous.get().getTimestamp()) ||
|
||||
!current.getFromRecipient().equals(previous.get().getFromRecipient()) || !isWithinClusteringTime(current, previous.get()) || MessageRecordUtil.isScheduled(current);
|
||||
@@ -2194,6 +2238,9 @@ public final class ConversationItem extends RelativeLayout implements BindableCo
|
||||
}
|
||||
|
||||
private boolean isEndOfMessageCluster(@NonNull MessageRecord current, @NonNull Optional<MessageRecord> next, boolean isGroupThread) {
|
||||
if (displayMode instanceof ConversationItemDisplayMode.Starred) {
|
||||
return true;
|
||||
}
|
||||
if (isGroupThread) {
|
||||
return !next.isPresent() || next.get().isUpdate() || !DateUtils.isSameDay(current.getTimestamp(), next.get().getTimestamp()) ||
|
||||
!current.getFromRecipient().equals(next.get().getFromRecipient()) || !current.getReactions().isEmpty() || !isWithinClusteringTime(current, next.get()) ||
|
||||
@@ -2206,6 +2253,9 @@ public final class ConversationItem extends RelativeLayout implements BindableCo
|
||||
}
|
||||
|
||||
private boolean isSingularMessage(@NonNull MessageRecord current, @NonNull Optional<MessageRecord> previous, @NonNull Optional<MessageRecord> next, boolean isGroupThread) {
|
||||
if (displayMode instanceof ConversationItemDisplayMode.Starred) {
|
||||
return true;
|
||||
}
|
||||
return isStartOfMessageCluster(current, previous, isGroupThread) && isEndOfMessageCluster(current, next, isGroupThread);
|
||||
}
|
||||
|
||||
@@ -2784,7 +2834,7 @@ public final class ConversationItem extends RelativeLayout implements BindableCo
|
||||
|
||||
private class ThumbnailClickListener implements SlideClickListener {
|
||||
public void onClick(final View v, final Slide slide) {
|
||||
if (shouldInterceptClicks(messageRecord) || !batchSelected.isEmpty() || (isCondensedMode() && (!slide.hasDocument() || (slide.hasDocument() && !MessageRecordUtil.isScheduled(messageRecord))))) {
|
||||
if (shouldInterceptClicks(messageRecord) || !batchSelected.isEmpty() || (isSuppressedInteractionMode() && (!slide.hasDocument() || (slide.hasDocument() && !MessageRecordUtil.isScheduled(messageRecord))))) {
|
||||
performClick();
|
||||
} else if (!canPlayContent && mediaItem != null && eventListener != null) {
|
||||
eventListener.onPlayInlineContent(conversationMessage);
|
||||
|
||||
@@ -13,6 +13,9 @@ sealed class ConversationItemDisplayMode(val messageMode: MessageMode = MessageM
|
||||
/** Less length restrictions. Used to show more info in message details. */
|
||||
object Detailed : ConversationItemDisplayMode()
|
||||
|
||||
/** Standalone messages with starred source labels. Used for starred messages. */
|
||||
object Starred : ConversationItemDisplayMode()
|
||||
|
||||
fun displayWallpaper(): Boolean {
|
||||
return this == Standard || this == Detailed
|
||||
}
|
||||
|
||||
@@ -733,6 +733,14 @@ public final class ConversationReactionOverlay extends FrameLayout {
|
||||
items.add(new ActionItem(R.drawable.symbol_pin_slash_24, getResources().getString(R.string.conversation_selection__menu_unpin_message), () -> handleActionItemClicked(Action.UNPIN_MESSAGE)));
|
||||
}
|
||||
|
||||
if (menuState.shouldShowStarMessage()) {
|
||||
items.add(new ActionItem(R.drawable.symbol_star_outline_24, getResources().getString(R.string.conversation_selection__menu_star), () -> handleActionItemClicked(Action.STAR_MESSAGE)));
|
||||
}
|
||||
|
||||
if (menuState.shouldShowUnstarMessage()) {
|
||||
items.add(new ActionItem(R.drawable.symbol_star_outline_24, getResources().getString(R.string.conversation_selection__menu_unstar), () -> handleActionItemClicked(Action.UNSTAR_MESSAGE)));
|
||||
}
|
||||
|
||||
backgroundView.setVisibility(menuState.shouldShowReactions() ? View.VISIBLE : View.INVISIBLE);
|
||||
foregroundView.setVisibility(menuState.shouldShowReactions() ? View.VISIBLE : View.INVISIBLE);
|
||||
|
||||
@@ -920,6 +928,8 @@ public final class ConversationReactionOverlay extends FrameLayout {
|
||||
DELETE,
|
||||
END_POLL,
|
||||
PIN_MESSAGE,
|
||||
UNPIN_MESSAGE
|
||||
UNPIN_MESSAGE,
|
||||
STAR_MESSAGE,
|
||||
UNSTAR_MESSAGE
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,7 @@ import org.thoughtcrime.securesms.conversation.mutiselect.MultiselectPart;
|
||||
import org.thoughtcrime.securesms.database.model.MmsMessageRecord;
|
||||
import org.thoughtcrime.securesms.database.model.MessageRecord;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
||||
import org.thoughtcrime.securesms.util.MessageRecordUtil;
|
||||
import org.thoughtcrime.securesms.util.MessageConstraintsUtil;
|
||||
import org.thoughtcrime.securesms.util.RemoteConfig;
|
||||
@@ -30,6 +31,8 @@ public final class MenuState {
|
||||
private final boolean pollTerminate;
|
||||
private final boolean pinMessage;
|
||||
private final boolean unpinMessage;
|
||||
private final boolean starMessage;
|
||||
private final boolean unstarMessage;
|
||||
|
||||
private MenuState(@NonNull Builder builder) {
|
||||
forward = builder.forward;
|
||||
@@ -45,6 +48,8 @@ public final class MenuState {
|
||||
pollTerminate = builder.pollTerminate;
|
||||
pinMessage = builder.pinMessage;
|
||||
unpinMessage = builder.unpinMessage;
|
||||
starMessage = builder.starMessage;
|
||||
unstarMessage = builder.unstarMessage;
|
||||
}
|
||||
|
||||
public boolean shouldShowForwardAction() {
|
||||
@@ -99,6 +104,14 @@ public final class MenuState {
|
||||
return unpinMessage;
|
||||
}
|
||||
|
||||
public boolean shouldShowStarMessage() {
|
||||
return starMessage;
|
||||
}
|
||||
|
||||
public boolean shouldShowUnstarMessage() {
|
||||
return unstarMessage;
|
||||
}
|
||||
|
||||
public static MenuState getMenuState(@NonNull Recipient conversationRecipient,
|
||||
@NonNull Set<MultiselectPart> selectedParts,
|
||||
boolean shouldShowMessageRequest,
|
||||
@@ -121,6 +134,8 @@ public final class MenuState {
|
||||
boolean hasPollTerminate = false;
|
||||
boolean canPinMessage = false;
|
||||
boolean canUnpinMessage = false;
|
||||
boolean canStarMessage = false;
|
||||
boolean canUnstarMessage = false;
|
||||
|
||||
for (MultiselectPart part : selectedParts) {
|
||||
MessageRecord messageRecord = part.getMessageRecord();
|
||||
@@ -178,6 +193,14 @@ public final class MenuState {
|
||||
if (messageRecord.getPinnedUntil() != 0 && !conversationRecipient.isReleaseNotes() && canEditGroupInfo && !hasGift && !conversationRecipient.isInactiveGroup()) {
|
||||
canUnpinMessage = true;
|
||||
}
|
||||
|
||||
if (SignalStore.labs().getStarredMessages() && !messageRecord.isUpdate() && !messageRecord.isRemoteDelete() && !messageRecord.isStarred()) {
|
||||
canStarMessage = true;
|
||||
}
|
||||
|
||||
if (SignalStore.labs().getStarredMessages() && messageRecord.isStarred()) {
|
||||
canUnstarMessage = true;
|
||||
}
|
||||
}
|
||||
|
||||
boolean shouldShowForwardAction = !actionMessage &&
|
||||
@@ -204,7 +227,9 @@ public final class MenuState {
|
||||
.shouldShowEdit(false)
|
||||
.shouldShowPollTerminate(false)
|
||||
.shouldShowPinMessage(false)
|
||||
.shouldShowUnpinMessage(false);
|
||||
.shouldShowUnpinMessage(false)
|
||||
.shouldShowStarMessage(false)
|
||||
.shouldShowUnstarMessage(false);
|
||||
} else {
|
||||
MultiselectPart multiSelectRecord = selectedParts.iterator().next();
|
||||
|
||||
@@ -238,6 +263,8 @@ public final class MenuState {
|
||||
.shouldShowPollTerminate(hasPollTerminate)
|
||||
.shouldShowPinMessage(canPinMessage)
|
||||
.shouldShowUnpinMessage(canUnpinMessage)
|
||||
.shouldShowStarMessage(canStarMessage)
|
||||
.shouldShowUnstarMessage(canUnstarMessage)
|
||||
.build();
|
||||
}
|
||||
|
||||
@@ -285,6 +312,8 @@ public final class MenuState {
|
||||
private boolean pollTerminate;
|
||||
private boolean pinMessage;
|
||||
private boolean unpinMessage;
|
||||
private boolean starMessage;
|
||||
private boolean unstarMessage;
|
||||
|
||||
@NonNull Builder shouldShowForwardAction(boolean forward) {
|
||||
this.forward = forward;
|
||||
@@ -351,6 +380,16 @@ public final class MenuState {
|
||||
return this;
|
||||
}
|
||||
|
||||
@NonNull Builder shouldShowStarMessage(boolean starMessage) {
|
||||
this.starMessage = starMessage;
|
||||
return this;
|
||||
}
|
||||
|
||||
@NonNull Builder shouldShowUnstarMessage(boolean unstarMessage) {
|
||||
this.unstarMessage = unstarMessage;
|
||||
return this;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
MenuState build() {
|
||||
return new MenuState(this);
|
||||
|
||||
@@ -1955,6 +1955,22 @@ class ConversationFragment :
|
||||
)
|
||||
}
|
||||
|
||||
private fun handleStarMessages(messageIds: Set<Long>) {
|
||||
disposables += viewModel
|
||||
.setMessagesStarred(messageIds, true)
|
||||
.subscribeBy(
|
||||
onError = { Log.w(TAG, "Error starring message!", it) }
|
||||
)
|
||||
}
|
||||
|
||||
private fun handleUnstarMessages(messageIds: Set<Long>) {
|
||||
disposables += viewModel
|
||||
.setMessagesStarred(messageIds, false)
|
||||
.subscribeBy(
|
||||
onError = { Log.w(TAG, "Error unstarring message!", it) }
|
||||
)
|
||||
}
|
||||
|
||||
private fun handleVideoCall() {
|
||||
val recipient = viewModel.recipientSnapshot ?: return
|
||||
if (!recipient.isGroup) {
|
||||
@@ -2605,6 +2621,24 @@ class ConversationFragment :
|
||||
)
|
||||
}
|
||||
|
||||
if (menuState.shouldShowStarMessage()) {
|
||||
items.add(
|
||||
ActionItem(R.drawable.symbol_star_outline_24, resources.getString(R.string.conversation_selection__menu_star)) {
|
||||
handleStarMessages(selectedParts.map { it.conversationMessage.messageRecord.id }.toSet())
|
||||
finishActionMode()
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
if (menuState.shouldShowUnstarMessage()) {
|
||||
items.add(
|
||||
ActionItem(R.drawable.symbol_star_outline_24, resources.getString(R.string.conversation_selection__menu_unstar)) {
|
||||
handleUnstarMessages(selectedParts.map { it.conversationMessage.messageRecord.id }.toSet())
|
||||
finishActionMode()
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
if (menuState.shouldShowDeleteAction()) {
|
||||
items.add(
|
||||
ActionItem(CoreUiR.drawable.symbol_trash_24, resources.getString(R.string.conversation_selection__menu_delete)) {
|
||||
@@ -4275,6 +4309,8 @@ class ConversationFragment :
|
||||
ConversationReactionOverlay.Action.END_POLL -> handleEndPoll(conversationMessage.messageRecord.getPoll()?.id)
|
||||
ConversationReactionOverlay.Action.PIN_MESSAGE -> handlePinMessage(conversationMessage)
|
||||
ConversationReactionOverlay.Action.UNPIN_MESSAGE -> handleUnpinMessage(conversationMessage.messageRecord.id)
|
||||
ConversationReactionOverlay.Action.STAR_MESSAGE -> handleStarMessages(setOf(conversationMessage.messageRecord.id))
|
||||
ConversationReactionOverlay.Action.UNSTAR_MESSAGE -> handleUnstarMessages(setOf(conversationMessage.messageRecord.id))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -431,6 +431,16 @@ class ConversationRepository(
|
||||
}.subscribeOn(Schedulers.io())
|
||||
}
|
||||
|
||||
fun setMessageStarred(messageId: Long, starred: Boolean): Completable {
|
||||
return setMessagesStarred(setOf(messageId), starred)
|
||||
}
|
||||
|
||||
fun setMessagesStarred(messageIds: Set<Long>, starred: Boolean): Completable {
|
||||
return Completable.fromAction {
|
||||
SignalDatabase.messages.setStarred(messageIds, starred)
|
||||
}.subscribeOn(Schedulers.io())
|
||||
}
|
||||
|
||||
private fun applyUniversalExpireTimerIfNecessary(context: Context, recipient: Recipient, outgoingMessage: OutgoingMessage, threadId: Long): OutgoingMessage {
|
||||
if (!outgoingMessage.isExpirationUpdate && outgoingMessage.expiresIn == 0L) {
|
||||
val expireTimerVersion = RecipientUtil.setAndSendUniversalExpireTimerIfNecessary(context, recipient, threadId)
|
||||
|
||||
@@ -381,6 +381,16 @@ class ConversationViewModel(
|
||||
}
|
||||
}
|
||||
|
||||
fun setMessageStarred(messageId: Long, starred: Boolean): Completable {
|
||||
return setMessagesStarred(setOf(messageId), starred)
|
||||
}
|
||||
|
||||
fun setMessagesStarred(messageIds: Set<Long>, starred: Boolean): Completable {
|
||||
return repository
|
||||
.setMessagesStarred(messageIds, starred)
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
}
|
||||
|
||||
fun updateThreadHeader() {
|
||||
pagingController.onDataItemChanged(ConversationElementKey.threadHeader)
|
||||
}
|
||||
|
||||
@@ -44,7 +44,11 @@ fun V2ConversationItemMediaIncomingBinding.bridge(): V2ConversationItemMediaBind
|
||||
alert = null,
|
||||
footerSpace = null,
|
||||
isIncoming = true,
|
||||
footerPinned = conversationItemFooterPinned
|
||||
footerPinned = conversationItemFooterPinned,
|
||||
footerStarred = conversationItemFooterStarred,
|
||||
starredSource = conversationItemStarredSource,
|
||||
starredSourceWrapper = conversationItemStarredSourceWrapper,
|
||||
starredSourceAvatar = conversationItemStarredSourceAvatar
|
||||
)
|
||||
|
||||
return V2ConversationItemMediaBindingBridge(
|
||||
@@ -75,7 +79,11 @@ fun V2ConversationItemMediaOutgoingBinding.bridge(): V2ConversationItemMediaBind
|
||||
alert = conversationItemAlert,
|
||||
footerSpace = footerEndPad,
|
||||
isIncoming = false,
|
||||
footerPinned = conversationItemFooterPinned
|
||||
footerPinned = conversationItemFooterPinned,
|
||||
footerStarred = conversationItemFooterStarred,
|
||||
starredSource = null,
|
||||
starredSourceWrapper = null,
|
||||
starredSourceAvatar = null
|
||||
)
|
||||
|
||||
return V2ConversationItemMediaBindingBridge(
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
package org.thoughtcrime.securesms.conversation.v2.items
|
||||
|
||||
import org.signal.core.util.dp
|
||||
import org.thoughtcrime.securesms.conversation.ConversationItemDisplayMode
|
||||
import org.thoughtcrime.securesms.database.model.MessageRecord
|
||||
import org.thoughtcrime.securesms.util.DateUtils
|
||||
import org.thoughtcrime.securesms.util.Projection
|
||||
@@ -93,6 +94,9 @@ class V2ConversationItemShape(
|
||||
nextMessage: MessageRecord?,
|
||||
isGroupThread: Boolean
|
||||
): Boolean {
|
||||
if (conversationContext.displayMode is ConversationItemDisplayMode.Starred) {
|
||||
return true
|
||||
}
|
||||
return isStartOfMessageCluster(currentMessage, previousMessage, isGroupThread) && isEndOfMessageCluster(currentMessage, nextMessage)
|
||||
}
|
||||
|
||||
|
||||
@@ -43,7 +43,11 @@ data class V2ConversationItemTextOnlyBindingBridge(
|
||||
val footerSpace: Space?,
|
||||
val alert: AlertView?,
|
||||
val isIncoming: Boolean,
|
||||
val footerPinned: ImageView
|
||||
val footerPinned: ImageView,
|
||||
val footerStarred: ImageView,
|
||||
val starredSource: TextView?,
|
||||
val starredSourceWrapper: View?,
|
||||
val starredSourceAvatar: AvatarImageView?
|
||||
)
|
||||
|
||||
/**
|
||||
@@ -66,7 +70,11 @@ fun V2ConversationItemTextOnlyIncomingBinding.bridge(): V2ConversationItemTextOn
|
||||
alert = null,
|
||||
footerSpace = footerEndPad,
|
||||
isIncoming = true,
|
||||
footerPinned = conversationItemFooterPinned
|
||||
footerPinned = conversationItemFooterPinned,
|
||||
footerStarred = conversationItemFooterStarred,
|
||||
starredSource = conversationItemStarredSource,
|
||||
starredSourceWrapper = conversationItemStarredSourceWrapper,
|
||||
starredSourceAvatar = conversationItemStarredSourceAvatar
|
||||
)
|
||||
}
|
||||
|
||||
@@ -90,6 +98,10 @@ fun V2ConversationItemTextOnlyOutgoingBinding.bridge(): V2ConversationItemTextOn
|
||||
alert = conversationItemAlert,
|
||||
footerSpace = footerEndPad,
|
||||
isIncoming = false,
|
||||
footerPinned = conversationItemFooterPinned
|
||||
footerPinned = conversationItemFooterPinned,
|
||||
footerStarred = conversationItemFooterStarred,
|
||||
starredSource = null,
|
||||
starredSourceWrapper = null,
|
||||
starredSourceAvatar = null
|
||||
)
|
||||
}
|
||||
|
||||
@@ -97,7 +97,8 @@ open class V2ConversationItemTextOnlyViewHolder<Model : MappingModel<Model>>(
|
||||
binding.footerExpiry,
|
||||
binding.deliveryStatus,
|
||||
binding.footerBackground,
|
||||
binding.footerPinned
|
||||
binding.footerPinned,
|
||||
binding.footerStarred
|
||||
)
|
||||
|
||||
override val reactionsView: View = binding.reactions
|
||||
@@ -260,6 +261,8 @@ open class V2ConversationItemTextOnlyViewHolder<Model : MappingModel<Model>>(
|
||||
presentDeliveryStatus()
|
||||
presentFooterBackground()
|
||||
presentFooterPinned()
|
||||
presentFooterStarred()
|
||||
presentStarredSource()
|
||||
presentFooterExpiry()
|
||||
presentFooterEndPadding()
|
||||
presentAlert()
|
||||
@@ -541,6 +544,27 @@ open class V2ConversationItemTextOnlyViewHolder<Model : MappingModel<Model>>(
|
||||
pinned.visible = conversationMessage.messageRecord.pinnedUntil > 0
|
||||
}
|
||||
|
||||
private fun presentFooterStarred() {
|
||||
val starred = binding.footerStarred
|
||||
starred.setColorFilter(themeDelegate.getFooterForegroundColor(conversationMessage), PorterDuff.Mode.SRC_IN)
|
||||
starred.visible = conversationMessage.messageRecord.isStarred
|
||||
}
|
||||
|
||||
private fun presentStarredSource() {
|
||||
val wrapper = binding.starredSourceWrapper ?: return
|
||||
val sourceView = binding.starredSource ?: return
|
||||
|
||||
if (conversationContext.displayMode is ConversationItemDisplayMode.Starred) {
|
||||
val senderName = conversationMessage.messageRecord.fromRecipient.getShortDisplayName(context)
|
||||
val chatName = conversationMessage.threadRecipient.getShortDisplayName(context)
|
||||
sourceView.text = context.getString(R.string.StarredMessages__s_chevron_s, senderName, chatName)
|
||||
wrapper.visible = true
|
||||
binding.starredSourceAvatar?.setAvatar(conversationContext.requestManager, conversationMessage.messageRecord.fromRecipient, false)
|
||||
} else {
|
||||
wrapper.visible = false
|
||||
}
|
||||
}
|
||||
|
||||
private fun presentFooterEndPadding() {
|
||||
binding.footerSpace?.visibility = if (isForcedFooter() || shape.isEndingShape) {
|
||||
View.INVISIBLE
|
||||
@@ -550,7 +574,8 @@ open class V2ConversationItemTextOnlyViewHolder<Model : MappingModel<Model>>(
|
||||
}
|
||||
|
||||
private fun presentSender() {
|
||||
if (conversationMessage.threadRecipient.isGroup) {
|
||||
val isStarredMode = conversationContext.displayMode is ConversationItemDisplayMode.Starred
|
||||
if (conversationMessage.threadRecipient.isGroup && !isStarredMode) {
|
||||
presentSenderPhoto()
|
||||
presentSenderBadge()
|
||||
presentSenderNameWithLabel()
|
||||
@@ -841,7 +866,7 @@ open class V2ConversationItemTextOnlyViewHolder<Model : MappingModel<Model>>(
|
||||
}
|
||||
|
||||
private fun isForcedFooter(): Boolean {
|
||||
return conversationMessage.messageRecord.isEditMessage || conversationMessage.messageRecord.expiresIn > 0L || conversationMessage.messageRecord.pinnedUntil > 0
|
||||
return conversationMessage.messageRecord.isEditMessage || conversationMessage.messageRecord.expiresIn > 0L || conversationMessage.messageRecord.pinnedUntil > 0 || conversationMessage.messageRecord.isStarred
|
||||
}
|
||||
|
||||
private inner class ReactionMeasureListener : V2ConversationItemLayout.OnMeasureListener {
|
||||
|
||||
@@ -36,7 +36,8 @@ class V2FooterPositionDelegate private constructor(
|
||||
binding.deliveryStatus,
|
||||
binding.footerExpiry,
|
||||
binding.footerSpace,
|
||||
binding.footerPinned
|
||||
binding.footerPinned,
|
||||
binding.footerStarred
|
||||
),
|
||||
binding.bodyWrapper,
|
||||
binding.body,
|
||||
|
||||
@@ -48,6 +48,7 @@ public class DatabaseObserver {
|
||||
private static final String KEY_CALL_LINK_UPDATES = "CallLinkUpdates";
|
||||
private static final String KEY_IN_APP_PAYMENTS = "InAppPayments";
|
||||
private static final String KEY_CHAT_FOLDER = "ChatFolder";
|
||||
private static final String KEY_STARRED_MESSAGES = "StarredMessages";
|
||||
|
||||
private final Executor executor;
|
||||
|
||||
@@ -71,6 +72,7 @@ public class DatabaseObserver {
|
||||
private final Map<CallLinkRoomId, Set<Observer>> callLinkObservers;
|
||||
private final Set<InAppPaymentObserver> inAppPaymentObservers;
|
||||
private final Set<Observer> chatFolderObservers;
|
||||
private final Set<Observer> starredMessageObservers;
|
||||
|
||||
public DatabaseObserver() {
|
||||
this.executor = new SerialExecutor(SignalExecutors.BOUNDED);
|
||||
@@ -94,6 +96,7 @@ public class DatabaseObserver {
|
||||
this.callLinkObservers = new HashMap<>();
|
||||
this.inAppPaymentObservers = new HashSet<>();
|
||||
this.chatFolderObservers = new HashSet<>();
|
||||
this.starredMessageObservers = new HashSet<>();
|
||||
}
|
||||
|
||||
public void registerConversationListObserver(@NonNull Observer listener) {
|
||||
@@ -213,6 +216,10 @@ public class DatabaseObserver {
|
||||
executor.execute(() -> chatFolderObservers.add(observer));
|
||||
}
|
||||
|
||||
public void registerStarredMessageObserver(@NonNull Observer observer) {
|
||||
executor.execute(() -> starredMessageObservers.add(observer));
|
||||
}
|
||||
|
||||
public void unregisterObserver(@NonNull Observer listener) {
|
||||
executor.execute(() -> {
|
||||
conversationListObservers.remove(listener);
|
||||
@@ -231,6 +238,7 @@ public class DatabaseObserver {
|
||||
callUpdateObservers.remove(listener);
|
||||
unregisterMapped(callLinkObservers, listener);
|
||||
chatFolderObservers.remove(listener);
|
||||
starredMessageObservers.remove(listener);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -399,6 +407,10 @@ public class DatabaseObserver {
|
||||
runPostSuccessfulTransaction(KEY_CHAT_FOLDER, () -> notifySet(chatFolderObservers));
|
||||
}
|
||||
|
||||
public void notifyStarredMessageObservers() {
|
||||
runPostSuccessfulTransaction(KEY_STARRED_MESSAGES, () -> notifySet(starredMessageObservers));
|
||||
}
|
||||
|
||||
private void runPostSuccessfulTransaction(@NonNull String dedupeKey, @NonNull Runnable runnable) {
|
||||
SignalDatabase.runPostSuccessfulTransaction(dedupeKey, () -> {
|
||||
executor.execute(runnable);
|
||||
|
||||
@@ -227,6 +227,7 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat
|
||||
const val PINNED_AT = "pinned_at"
|
||||
const val DELETED_BY = "deleted_by"
|
||||
const val STORY_ARCHIVED = "story_archived"
|
||||
const val STARRED = "starred"
|
||||
|
||||
const val QUOTE_NOT_PRESENT_ID = 0L
|
||||
const val QUOTE_TARGET_MISSING_ID = -1L
|
||||
@@ -299,7 +300,8 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat
|
||||
$PINNING_MESSAGE_ID INTEGER DEFAULT 0,
|
||||
$PINNED_AT INTEGER DEFAULT 0,
|
||||
$DELETED_BY INTEGER DEFAULT NULL REFERENCES ${RecipientTable.TABLE_NAME} (${RecipientTable.ID}) ON DELETE CASCADE,
|
||||
$STORY_ARCHIVED INTEGER DEFAULT 0
|
||||
$STORY_ARCHIVED INTEGER DEFAULT 0,
|
||||
$STARRED INTEGER DEFAULT 0
|
||||
)
|
||||
"""
|
||||
|
||||
@@ -334,7 +336,8 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat
|
||||
"CREATE INDEX IF NOT EXISTS message_pinned_until_index ON $TABLE_NAME ($PINNED_UNTIL)",
|
||||
"CREATE INDEX IF NOT EXISTS message_pinned_at_index ON $TABLE_NAME ($PINNED_AT)",
|
||||
"CREATE INDEX IF NOT EXISTS message_deleted_by_index ON $TABLE_NAME ($DELETED_BY)",
|
||||
"CREATE INDEX IF NOT EXISTS 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_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"
|
||||
)
|
||||
|
||||
private val MMS_PROJECTION_BASE = arrayOf(
|
||||
@@ -390,7 +393,8 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat
|
||||
VOTES_UNREAD,
|
||||
VOTES_LAST_SEEN,
|
||||
PINNED_UNTIL,
|
||||
DELETED_BY
|
||||
DELETED_BY,
|
||||
STARRED
|
||||
)
|
||||
|
||||
private val MMS_PROJECTION: Array<String> = MMS_PROJECTION_BASE
|
||||
@@ -2150,6 +2154,47 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat
|
||||
}
|
||||
}
|
||||
|
||||
fun setStarred(messageId: Long, starred: Boolean) {
|
||||
setStarred(setOf(messageId), starred)
|
||||
}
|
||||
|
||||
fun setStarred(messageIds: Set<Long>, starred: Boolean) {
|
||||
writableDatabase.withinTransaction { db ->
|
||||
for (messageId in messageIds) {
|
||||
db.update(TABLE_NAME)
|
||||
.values(STARRED to if (starred) 1 else 0)
|
||||
.where("$ID = ?", messageId)
|
||||
.run()
|
||||
}
|
||||
}
|
||||
|
||||
val threadIds = messageIds.map { getThreadIdForMessage(it) }.toSet()
|
||||
for (threadId in threadIds) {
|
||||
notifyConversationListeners(threadId)
|
||||
}
|
||||
for (messageId in messageIds) {
|
||||
AppDependencies.databaseObserver.notifyMessageUpdateObservers(MessageId(messageId))
|
||||
}
|
||||
AppDependencies.databaseObserver.notifyStarredMessageObservers()
|
||||
}
|
||||
|
||||
fun getStarredMessages(threadId: Long? = null): List<MessageRecord> {
|
||||
val where: String
|
||||
val args: Array<String>?
|
||||
|
||||
if (threadId != null) {
|
||||
where = "$STARRED > 0 AND $THREAD_ID = ? AND $LATEST_REVISION_ID IS NULL"
|
||||
args = buildArgs(threadId)
|
||||
} else {
|
||||
where = "$STARRED > 0 AND $LATEST_REVISION_ID IS NULL"
|
||||
args = null
|
||||
}
|
||||
|
||||
return mmsReaderFor(queryMessages(where, args, reverse = true)).use { reader ->
|
||||
reader.mapNotNull { it }
|
||||
}.withAttachments()
|
||||
}
|
||||
|
||||
fun getRecentPendingMessages(): MmsReader {
|
||||
val now = System.currentTimeMillis()
|
||||
val oneDayAgo = now.milliseconds - 1.days
|
||||
@@ -2301,7 +2346,8 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat
|
||||
LINK_PREVIEWS to null,
|
||||
SHARED_CONTACTS to null,
|
||||
ORIGINAL_MESSAGE_ID to null,
|
||||
LATEST_REVISION_ID to null
|
||||
LATEST_REVISION_ID to null,
|
||||
STARRED to 0
|
||||
)
|
||||
.where("$ID = ?", messageId)
|
||||
.run()
|
||||
@@ -2925,6 +2971,7 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat
|
||||
.readToSingleInt(0)
|
||||
|
||||
contentValues.put(NOTIFIED, notified.toInt())
|
||||
contentValues.put(STARRED, if (editedMessage.isStarred) 1 else 0)
|
||||
} else if (MessageTypes.isPinnedMessageUpdate(type)) {
|
||||
contentValues.put(NOTIFIED, 1)
|
||||
}
|
||||
@@ -3343,6 +3390,7 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat
|
||||
contentValues.put(ORIGINAL_MESSAGE_ID, editedMessage.getOriginalOrOwnMessageId().id)
|
||||
contentValues.put(REVISION_NUMBER, editedMessage.revisionNumber + 1)
|
||||
contentValues.put(EXPIRE_STARTED, editedMessage.expireStarted)
|
||||
contentValues.put(STARRED, if (editedMessage.isStarred) 1 else 0)
|
||||
} else {
|
||||
contentValues.putNull(ORIGINAL_MESSAGE_ID)
|
||||
}
|
||||
@@ -6402,6 +6450,7 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat
|
||||
val isRead = cursor.requireBoolean(READ)
|
||||
val pinnedUntil = cursor.requireLong(PINNED_UNTIL)
|
||||
val deletedBy = cursor.requireLongOrNull(DELETED_BY)?.let { RecipientId.from(it) }
|
||||
val isStarred = cursor.requireBoolean(STARRED)
|
||||
val messageExtraBytes = cursor.requireBlob(MESSAGE_EXTRAS)
|
||||
val messageExtras = messageExtraBytes?.let { MessageExtras.ADAPTER.decode(it) }
|
||||
|
||||
@@ -6497,7 +6546,8 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat
|
||||
isRead,
|
||||
pinnedUntil,
|
||||
deletedBy,
|
||||
messageExtras
|
||||
messageExtras,
|
||||
isStarred
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -16,6 +16,7 @@ object RxDatabaseObserver {
|
||||
val conversationList: Flowable<Unit> by lazy { conversationListFlowable() }
|
||||
val notificationProfiles: Flowable<Unit> by lazy { notificationProfilesFlowable() }
|
||||
val chatFolders: Flowable<Unit> by lazy { chatFoldersFlowable() }
|
||||
val starredMessages: Flowable<Unit> by lazy { starredMessagesFlowable() }
|
||||
|
||||
private fun conversationListFlowable(): Flowable<Unit> {
|
||||
return databaseFlowable { listener ->
|
||||
@@ -43,6 +44,12 @@ object RxDatabaseObserver {
|
||||
}
|
||||
}
|
||||
|
||||
private fun starredMessagesFlowable(): Flowable<Unit> {
|
||||
return databaseFlowable { listener ->
|
||||
AppDependencies.databaseObserver.registerStarredMessageObserver(listener)
|
||||
}
|
||||
}
|
||||
|
||||
private fun databaseFlowable(registerObserver: (RxObserver) -> Unit): Flowable<Unit> {
|
||||
val flowable = Flowable.create(
|
||||
{
|
||||
|
||||
@@ -162,6 +162,7 @@ import org.thoughtcrime.securesms.database.helpers.migration.V305_AddStoryArchiv
|
||||
import org.thoughtcrime.securesms.database.helpers.migration.V306_AddRemoteDeletedColumn
|
||||
import org.thoughtcrime.securesms.database.helpers.migration.V308_AddBackRemoteDeletedColumn
|
||||
import org.thoughtcrime.securesms.database.helpers.migration.V309_GroupTerminatedColumnMigration
|
||||
import org.thoughtcrime.securesms.database.helpers.migration.V310_AddStarredColumn
|
||||
import org.thoughtcrime.securesms.database.SQLiteDatabase as SignalSqliteDatabase
|
||||
|
||||
/**
|
||||
@@ -331,10 +332,11 @@ object SignalDatabaseMigrations {
|
||||
306 to V306_AddRemoteDeletedColumn,
|
||||
// 307 to V307_RemoveRemoteDeletedColumn - Removed due to unsolvable OOM crashes. [TODO]: Attempt to fix in the future
|
||||
308 to V308_AddBackRemoteDeletedColumn,
|
||||
309 to V309_GroupTerminatedColumnMigration
|
||||
309 to V309_GroupTerminatedColumnMigration,
|
||||
310 to V310_AddStarredColumn
|
||||
)
|
||||
|
||||
const val DATABASE_VERSION = 309
|
||||
const val DATABASE_VERSION = 310
|
||||
|
||||
@JvmStatic
|
||||
fun migrate(context: Application, db: SignalSqliteDatabase, oldVersion: Int, newVersion: Int) {
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
package org.thoughtcrime.securesms.database.helpers.migration
|
||||
|
||||
import android.app.Application
|
||||
import org.thoughtcrime.securesms.database.SQLiteDatabase
|
||||
|
||||
/**
|
||||
* Adds a column for tracking the starred status of a message.
|
||||
*/
|
||||
@Suppress("ClassName")
|
||||
object V310_AddStarredColumn : SignalDatabaseMigration {
|
||||
|
||||
override fun migrate(context: Application, db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {
|
||||
db.execSQL("ALTER TABLE message ADD COLUMN starred INTEGER DEFAULT 0")
|
||||
db.execSQL("CREATE INDEX IF NOT EXISTS message_starred_index ON message (starred) WHERE starred > 0")
|
||||
}
|
||||
}
|
||||
@@ -60,7 +60,8 @@ public class InMemoryMessageRecord extends MessageRecord {
|
||||
0,
|
||||
0,
|
||||
null,
|
||||
null);
|
||||
null,
|
||||
false);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -116,6 +116,7 @@ public abstract class MessageRecord extends DisplayRecord {
|
||||
private final long pinnedUntil;
|
||||
private final RecipientId deletedBy;
|
||||
private final MessageExtras messageExtras;
|
||||
private final boolean starred;
|
||||
|
||||
protected Boolean isJumboji = null;
|
||||
|
||||
@@ -138,7 +139,8 @@ public abstract class MessageRecord extends DisplayRecord {
|
||||
int revisionNumber,
|
||||
long pinnedUntil,
|
||||
@Nullable RecipientId deletedBy,
|
||||
@Nullable MessageExtras messageExtras)
|
||||
@Nullable MessageExtras messageExtras,
|
||||
boolean starred)
|
||||
{
|
||||
super(body, fromRecipient, toRecipient, dateSent, dateReceived,
|
||||
threadId, deliveryStatus, hasDeliveryReceipt, type,
|
||||
@@ -161,6 +163,7 @@ public abstract class MessageRecord extends DisplayRecord {
|
||||
this.pinnedUntil = pinnedUntil;
|
||||
this.deletedBy = deletedBy;
|
||||
this.messageExtras = messageExtras;
|
||||
this.starred = starred;
|
||||
}
|
||||
|
||||
public abstract boolean isMms();
|
||||
@@ -799,6 +802,10 @@ public abstract class MessageRecord extends DisplayRecord {
|
||||
return deletedBy;
|
||||
}
|
||||
|
||||
public boolean isStarred() {
|
||||
return starred;
|
||||
}
|
||||
|
||||
public boolean isPendingAdminDelete() {
|
||||
return messageExtras != null &&
|
||||
messageExtras.adminDeleteStatus != null &&
|
||||
|
||||
@@ -121,12 +121,13 @@ public class MmsMessageRecord extends MessageRecord {
|
||||
boolean isRead,
|
||||
long pinnedUntil,
|
||||
@Nullable RecipientId deletedBy,
|
||||
@Nullable MessageExtras messageExtras)
|
||||
@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);
|
||||
unidentified, reactions, notifiedTimestamp, viewed, receiptTimestamp, originalMessageId, revisionNumber, pinnedUntil, deletedBy, messageExtras, starred);
|
||||
|
||||
this.slideDeck = slideDeck;
|
||||
this.quote = quote;
|
||||
@@ -334,12 +335,21 @@ public class MmsMessageRecord extends MessageRecord {
|
||||
(parentStoryId == null || parentStoryId.isDirectReply());
|
||||
}
|
||||
|
||||
public @NonNull MmsMessageRecord withIncomingType() {
|
||||
long incomingType = (getType() & ~MessageTypes.BASE_TYPE_MASK) | MessageTypes.BASE_INBOX_TYPE;
|
||||
return new MmsMessageRecord(getId(), getFromRecipient(), getFromDeviceId(), getToRecipient(), getDateSent(), getDateReceived(), getServerTimestamp(), hasDeliveryReceipt(), getThreadId(), getBody(), getSlideDeck(),
|
||||
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());
|
||||
}
|
||||
|
||||
public @NonNull MmsMessageRecord withReactions(@NonNull List<ReactionRecord> reactions) {
|
||||
return new MmsMessageRecord(getId(), getFromRecipient(), getFromDeviceId(), getToRecipient(), getDateSent(), getDateReceived(), getServerTimestamp(), hasDeliveryReceipt(), getThreadId(), getBody(), getSlideDeck(),
|
||||
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());
|
||||
getOriginalMessageId(), getRevisionNumber(), isRead(), getPinnedUntil(), getDeletedBy(), getMessageExtras(), isStarred());
|
||||
}
|
||||
|
||||
public @NonNull MmsMessageRecord withoutQuote() {
|
||||
@@ -347,7 +357,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());
|
||||
getOriginalMessageId(), getRevisionNumber(), isRead(), getPinnedUntil(), getDeletedBy(), getMessageExtras(), isStarred());
|
||||
}
|
||||
|
||||
public @NonNull MmsMessageRecord withAttachments(@NonNull List<DatabaseAttachment> attachments) {
|
||||
@@ -369,7 +379,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());
|
||||
getOriginalMessageId(), getRevisionNumber(), isRead(), getPinnedUntil(), getDeletedBy(), getMessageExtras(), isStarred());
|
||||
}
|
||||
|
||||
public @NonNull MmsMessageRecord withPayment(@NonNull Payment payment) {
|
||||
@@ -377,7 +387,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());
|
||||
getOriginalMessageId(), getRevisionNumber(), isRead(), getPinnedUntil(), getDeletedBy(), getMessageExtras(), isStarred());
|
||||
}
|
||||
|
||||
|
||||
@@ -386,7 +396,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());
|
||||
getOriginalMessageId(), getRevisionNumber(), isRead(), getPinnedUntil(), getDeletedBy(), getMessageExtras(), isStarred());
|
||||
}
|
||||
|
||||
public @NonNull MmsMessageRecord withPoll(@Nullable PollRecord poll) {
|
||||
@@ -394,7 +404,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());
|
||||
getOriginalMessageId(), getRevisionNumber(), isRead(), getPinnedUntil(), getDeletedBy(), getMessageExtras(), isStarred());
|
||||
}
|
||||
|
||||
private static @NonNull List<Contact> updateContacts(@NonNull List<Contact> contacts, @NonNull Map<AttachmentId, DatabaseAttachment> attachmentIdMap) {
|
||||
|
||||
@@ -11,6 +11,7 @@ class LabsValues internal constructor(store: KeyValueStore) : SignalStoreValues(
|
||||
const val BETTER_SEARCH: String = "labs.better_search"
|
||||
const val AUTO_LOWER_HAND: String = "labs.auto_lower_hand"
|
||||
const val NEW_APNG_RENDERER: String = "labs.new_apng_renderer"
|
||||
const val STARRED_MESSAGES: String = "labs.starred_messages"
|
||||
}
|
||||
|
||||
public override fun onFirstEverAppLaunch() = Unit
|
||||
@@ -31,6 +32,8 @@ class LabsValues internal constructor(store: KeyValueStore) : SignalStoreValues(
|
||||
|
||||
var newApngRenderer by booleanValue(NEW_APNG_RENDERER, true).falseForExternalUsers()
|
||||
|
||||
var starredMessages by booleanValue(STARRED_MESSAGES, true).falseForExternalUsers()
|
||||
|
||||
private fun SignalStoreValueDelegate<Boolean>.falseForExternalUsers(): SignalStoreValueDelegate<Boolean> {
|
||||
return this.map { actualValue -> RemoteConfig.internalUser && actualValue }
|
||||
}
|
||||
|
||||
@@ -107,6 +107,7 @@ interface MainToolbarCallback {
|
||||
fun onCloseActionModeClick()
|
||||
fun onSearchQueryUpdated(query: String)
|
||||
fun onSearchFilterClick()
|
||||
fun onStarredMessagesClick()
|
||||
fun onNotificationProfileTooltipDismissed()
|
||||
|
||||
object Empty : MainToolbarCallback {
|
||||
@@ -130,6 +131,7 @@ interface MainToolbarCallback {
|
||||
override fun onCloseActionModeClick() = Unit
|
||||
override fun onSearchQueryUpdated(query: String) = Unit
|
||||
override fun onSearchFilterClick() = Unit
|
||||
override fun onStarredMessagesClick() = Unit
|
||||
override fun onNotificationProfileTooltipDismissed() = Unit
|
||||
}
|
||||
}
|
||||
@@ -723,6 +725,20 @@ private fun ChatDropdownItems(state: MainToolbarState, callback: MainToolbarCall
|
||||
)
|
||||
}
|
||||
|
||||
if (SignalStore.labs.starredMessages) {
|
||||
DropdownMenus.Item(
|
||||
text = {
|
||||
Text(
|
||||
text = stringResource(R.string.text_secure_normal__starred_messages)
|
||||
)
|
||||
},
|
||||
onClick = {
|
||||
callback.onStarredMessagesClick()
|
||||
onOptionSelected()
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
DropdownMenus.Item(
|
||||
text = {
|
||||
Text(
|
||||
|
||||
@@ -0,0 +1,411 @@
|
||||
package org.thoughtcrime.securesms.starred
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.FrameLayout
|
||||
import android.widget.Toast
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TopAppBarDefaults
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.rememberUpdatedState
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.alpha
|
||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.viewinterop.AndroidView
|
||||
import androidx.core.view.doOnNextLayout
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.Observer
|
||||
import androidx.lifecycle.compose.LocalLifecycleOwner
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.bumptech.glide.Glide
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.signal.core.ui.compose.Scaffolds
|
||||
import org.signal.core.ui.compose.SignalIcons
|
||||
import org.signal.core.ui.compose.theme.SignalTheme
|
||||
import org.signal.ringrtc.CallLinkRootKey
|
||||
import org.thoughtcrime.securesms.PassphraseRequiredActivity
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.components.menu.ActionItem
|
||||
import org.thoughtcrime.securesms.components.menu.SignalContextMenu
|
||||
import org.thoughtcrime.securesms.components.recyclerview.SmoothScrollingLinearLayoutManager
|
||||
import org.thoughtcrime.securesms.components.voice.VoiceNotePlaybackState
|
||||
import org.thoughtcrime.securesms.contactshare.Contact
|
||||
import org.thoughtcrime.securesms.conversation.ConversationAdapter
|
||||
import org.thoughtcrime.securesms.conversation.ConversationIntents
|
||||
import org.thoughtcrime.securesms.conversation.ConversationItem
|
||||
import org.thoughtcrime.securesms.conversation.ConversationItemDisplayMode
|
||||
import org.thoughtcrime.securesms.conversation.ConversationMessage
|
||||
import org.thoughtcrime.securesms.conversation.colors.ColorizerV1
|
||||
import org.thoughtcrime.securesms.conversation.mutiselect.MultiselectPart
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||
import org.thoughtcrime.securesms.database.model.InMemoryMessageRecord
|
||||
import org.thoughtcrime.securesms.database.model.MessageRecord
|
||||
import org.thoughtcrime.securesms.database.model.MmsMessageRecord
|
||||
import org.thoughtcrime.securesms.giph.mp4.GiphyMp4ItemDecoration
|
||||
import org.thoughtcrime.securesms.giph.mp4.GiphyMp4PlaybackController
|
||||
import org.thoughtcrime.securesms.giph.mp4.GiphyMp4PlaybackPolicy
|
||||
import org.thoughtcrime.securesms.giph.mp4.GiphyMp4ProjectionPlayerHolder
|
||||
import org.thoughtcrime.securesms.giph.mp4.GiphyMp4ProjectionRecycler
|
||||
import org.thoughtcrime.securesms.groups.GroupId
|
||||
import org.thoughtcrime.securesms.groups.GroupMigrationMembershipChange
|
||||
import org.thoughtcrime.securesms.linkpreview.LinkPreview
|
||||
import org.thoughtcrime.securesms.mediapreview.MediaIntentFactory
|
||||
import org.thoughtcrime.securesms.polls.PollOption
|
||||
import org.thoughtcrime.securesms.polls.PollRecord
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
import org.thoughtcrime.securesms.stickers.StickerLocator
|
||||
import org.thoughtcrime.securesms.util.StickyHeaderDecoration
|
||||
import org.thoughtcrime.securesms.util.viewModel
|
||||
import java.util.Locale
|
||||
import org.signal.core.ui.R as CoreUiR
|
||||
|
||||
class StarredMessagesActivity : PassphraseRequiredActivity() {
|
||||
|
||||
companion object {
|
||||
private const val EXTRA_THREAD_ID = "thread_id"
|
||||
const val NO_THREAD_ID = -1L
|
||||
|
||||
@JvmStatic
|
||||
fun createIntent(context: Context): Intent {
|
||||
return Intent(context, StarredMessagesActivity::class.java)
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun createIntent(context: Context, threadId: Long): Intent {
|
||||
return Intent(context, StarredMessagesActivity::class.java).apply {
|
||||
putExtra(EXTRA_THREAD_ID, threadId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val viewModel by viewModel {
|
||||
val threadId = intent.getLongExtra(EXTRA_THREAD_ID, NO_THREAD_ID)
|
||||
val effectiveThreadId = if (threadId == NO_THREAD_ID) null else threadId
|
||||
StarredMessagesViewModel(effectiveThreadId)
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?, ready: Boolean) {
|
||||
setContent {
|
||||
SignalTheme {
|
||||
StarredMessagesScreen(
|
||||
viewModel = viewModel,
|
||||
onNavigateBack = { supportFinishAfterTransition() },
|
||||
onNavigateToMessage = ::navigateToMessage
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun navigateToMessage(messageRecord: MessageRecord) {
|
||||
lifecycleScope.launch {
|
||||
val (threadRecipient, startingPosition) = withContext(Dispatchers.IO) {
|
||||
val position = SignalDatabase.messages.getMessagePositionInConversation(messageRecord.threadId, messageRecord.dateReceived)
|
||||
val recipient = SignalDatabase.threads.getRecipientForThreadId(messageRecord.threadId)
|
||||
Pair(recipient, maxOf(0, position))
|
||||
}
|
||||
if (threadRecipient != null) {
|
||||
val intent = ConversationIntents.createBuilderSync(this@StarredMessagesActivity, threadRecipient.id, messageRecord.threadId)
|
||||
.withStartingPosition(startingPosition)
|
||||
.build()
|
||||
startActivity(intent)
|
||||
finish()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
private fun StarredMessagesScreen(
|
||||
viewModel: StarredMessagesViewModel,
|
||||
onNavigateBack: () -> Unit,
|
||||
onNavigateToMessage: (MessageRecord) -> Unit
|
||||
) {
|
||||
val messages by viewModel.getMessages().collectAsStateWithLifecycle(initialValue = emptyList())
|
||||
val scope = rememberCoroutineScope()
|
||||
val context = LocalContext.current
|
||||
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior()
|
||||
|
||||
Scaffold(
|
||||
topBar = {
|
||||
Scaffolds.DefaultTopAppBar(
|
||||
title = stringResource(R.string.StarredMessagesActivity__starred_messages),
|
||||
titleContent = { _, title -> Text(text = title, style = MaterialTheme.typography.titleLarge) },
|
||||
navigationIcon = SignalIcons.ArrowStart.imageVector,
|
||||
navigationContentDescription = stringResource(R.string.DefaultTopAppBar__navigate_up_content_description),
|
||||
onNavigationClick = onNavigateBack,
|
||||
scrollBehavior = scrollBehavior
|
||||
)
|
||||
},
|
||||
modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection)
|
||||
) { padding ->
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.padding(padding)
|
||||
.fillMaxSize()
|
||||
) {
|
||||
StarredMessageList(
|
||||
messages = messages,
|
||||
onItemClick = onNavigateToMessage,
|
||||
onQuoteClick = onNavigateToMessage,
|
||||
onUnstarMessage = { messageId ->
|
||||
scope.launch {
|
||||
try {
|
||||
viewModel.unstarMessage(messageId)
|
||||
} catch (e: Exception) {
|
||||
Toast.makeText(context, "Failed to unstar message", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
},
|
||||
modifier = Modifier.fillMaxSize()
|
||||
)
|
||||
|
||||
if (messages.isEmpty()) {
|
||||
EmptyState(modifier = Modifier.fillMaxSize())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("WrongThread")
|
||||
@Composable
|
||||
private fun StarredMessageList(
|
||||
messages: List<ConversationMessage>,
|
||||
onItemClick: (MessageRecord) -> Unit,
|
||||
onQuoteClick: (MessageRecord) -> Unit,
|
||||
onUnstarMessage: (Long) -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val lifecycleOwner = LocalLifecycleOwner.current
|
||||
val onItemClickState = rememberUpdatedState(onItemClick)
|
||||
val onQuoteClickState = rememberUpdatedState(onQuoteClick)
|
||||
val onUnstarMessageState = rememberUpdatedState(onUnstarMessage)
|
||||
|
||||
val adapter = remember {
|
||||
@Suppress("DEPRECATION")
|
||||
val colorizer = ColorizerV1()
|
||||
ConversationAdapter(
|
||||
context,
|
||||
lifecycleOwner,
|
||||
Glide.with(context),
|
||||
Locale.getDefault(),
|
||||
StarredMessageClickListener(
|
||||
onItemClick = { onItemClickState.value(it) },
|
||||
onQuoteClick = { onQuoteClickState.value(it) },
|
||||
onUnstarMessage = { onUnstarMessageState.value(it) },
|
||||
context = context
|
||||
),
|
||||
false,
|
||||
colorizer
|
||||
).apply {
|
||||
setCondensedMode(ConversationItemDisplayMode.Starred)
|
||||
}
|
||||
}
|
||||
|
||||
AndroidView(
|
||||
factory = { ctx ->
|
||||
FrameLayout(ctx).apply {
|
||||
layoutParams = ViewGroup.LayoutParams(
|
||||
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||
ViewGroup.LayoutParams.MATCH_PARENT
|
||||
)
|
||||
|
||||
val videoContainer = FrameLayout(ctx).apply {
|
||||
layoutParams = FrameLayout.LayoutParams(
|
||||
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||
ViewGroup.LayoutParams.MATCH_PARENT
|
||||
)
|
||||
}
|
||||
addView(videoContainer)
|
||||
|
||||
val recyclerView = RecyclerView(ctx).apply {
|
||||
layoutParams = FrameLayout.LayoutParams(
|
||||
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||
ViewGroup.LayoutParams.MATCH_PARENT
|
||||
)
|
||||
clipToPadding = false
|
||||
setPadding(0, 0, 0, (24 * resources.displayMetrics.density).toInt())
|
||||
layoutManager = SmoothScrollingLinearLayoutManager(ctx, true)
|
||||
this.adapter = adapter
|
||||
itemAnimator = null
|
||||
doOnNextLayout {
|
||||
addItemDecoration(StickyHeaderDecoration(adapter, false, false, ConversationAdapter.HEADER_TYPE_INLINE_DATE))
|
||||
}
|
||||
}
|
||||
addView(recyclerView)
|
||||
|
||||
initializeGiphyMp4(lifecycleOwner.lifecycle, videoContainer, recyclerView)
|
||||
}
|
||||
},
|
||||
update = {
|
||||
adapter.submitList(messages)
|
||||
},
|
||||
modifier = modifier
|
||||
)
|
||||
}
|
||||
|
||||
private fun initializeGiphyMp4(lifecycle: Lifecycle, videoContainer: ViewGroup, list: RecyclerView) {
|
||||
val context = list.context
|
||||
val maxPlayback = GiphyMp4PlaybackPolicy.maxSimultaneousPlaybackInConversation()
|
||||
val holders = GiphyMp4ProjectionPlayerHolder.injectVideoViews(context, lifecycle, videoContainer, maxPlayback)
|
||||
val callback = GiphyMp4ProjectionRecycler(holders)
|
||||
|
||||
GiphyMp4PlaybackController.attach(list, callback, maxPlayback)
|
||||
list.addItemDecoration(GiphyMp4ItemDecoration(callback) {}, 0)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun EmptyState(modifier: Modifier = Modifier) {
|
||||
Column(
|
||||
modifier = modifier,
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.Center
|
||||
) {
|
||||
Icon(
|
||||
painter = painterResource(R.drawable.symbol_star_24),
|
||||
contentDescription = null,
|
||||
modifier = Modifier
|
||||
.size(48.dp)
|
||||
.alpha(0.5f),
|
||||
tint = MaterialTheme.colorScheme.onSurface
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
Text(
|
||||
text = stringResource(R.string.StarredMessagesFragment__no_starred_messages),
|
||||
style = MaterialTheme.typography.headlineMedium
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
|
||||
Text(
|
||||
text = stringResource(R.string.StarredMessagesFragment__tap_and_hold_on_a_message_to_star_it),
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
modifier = Modifier.alpha(0.7f)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private class StarredMessageClickListener(
|
||||
private val onItemClick: (MessageRecord) -> Unit,
|
||||
private val onQuoteClick: (MessageRecord) -> Unit,
|
||||
private val onUnstarMessage: (Long) -> Unit,
|
||||
private val context: Context
|
||||
) : ConversationAdapter.ItemClickListener {
|
||||
|
||||
override fun onItemClick(item: MultiselectPart) {
|
||||
onItemClick(item.getMessageRecord())
|
||||
}
|
||||
|
||||
override fun onItemLongClick(itemView: View, item: MultiselectPart) {
|
||||
val messageRecord = item.getMessageRecord()
|
||||
val items = mutableListOf<ActionItem>()
|
||||
|
||||
items.add(
|
||||
ActionItem(R.drawable.symbol_star_outline_24, context.getString(R.string.conversation_selection__menu_unstar)) {
|
||||
onUnstarMessage(messageRecord.id)
|
||||
}
|
||||
)
|
||||
|
||||
SignalContextMenu.Builder(itemView, itemView.rootView as ViewGroup)
|
||||
.preferredHorizontalPosition(SignalContextMenu.HorizontalPosition.START)
|
||||
.show(items)
|
||||
}
|
||||
|
||||
override fun onQuoteClicked(messageRecord: MmsMessageRecord) {
|
||||
onQuoteClick(messageRecord)
|
||||
}
|
||||
|
||||
override fun onLinkPreviewClicked(linkPreview: LinkPreview) = Unit
|
||||
override fun onQuotedIndicatorClicked(messageRecord: MessageRecord) = Unit
|
||||
override fun onMoreTextClicked(conversationRecipientId: RecipientId, messageId: Long, isMms: Boolean) = Unit
|
||||
override fun onStickerClicked(stickerLocator: StickerLocator) = Unit
|
||||
override fun onViewOnceMessageClicked(messageRecord: MmsMessageRecord) = Unit
|
||||
override fun onSharedContactDetailsClicked(contact: Contact, avatarTransitionView: View) = Unit
|
||||
override fun onAddToContactsClicked(contact: Contact) = Unit
|
||||
override fun onMessageSharedContactClicked(choices: MutableList<Recipient>) = Unit
|
||||
override fun onInviteSharedContactClicked(choices: MutableList<Recipient>) = Unit
|
||||
override fun onReactionClicked(multiselectPart: MultiselectPart, messageId: Long, isMms: Boolean) = Unit
|
||||
override fun onGroupMemberClicked(recipientId: RecipientId, groupId: GroupId) = Unit
|
||||
override fun onMessageWithErrorClicked(messageRecord: MessageRecord) = Unit
|
||||
override fun onMessageWithRecaptchaNeededClicked(messageRecord: MessageRecord) = Unit
|
||||
override fun onGroupMigrationLearnMoreClicked(membershipChange: GroupMigrationMembershipChange) = Unit
|
||||
override fun onChatSessionRefreshLearnMoreClicked() = Unit
|
||||
override fun onBadDecryptLearnMoreClicked(author: RecipientId) = Unit
|
||||
override fun onSafetyNumberLearnMoreClicked(recipient: Recipient) = Unit
|
||||
override fun onJoinGroupCallClicked() = Unit
|
||||
override fun onInviteFriendsToGroupClicked(groupId: GroupId.V2) = Unit
|
||||
override fun onEnableCallNotificationsClicked() = Unit
|
||||
override fun onCallToAction(action: String) = Unit
|
||||
override fun onDonateClicked() = Unit
|
||||
override fun onRecipientNameClicked(target: RecipientId) = Unit
|
||||
override fun onViewGiftBadgeClicked(messageRecord: MessageRecord) = Unit
|
||||
override fun onActivatePaymentsClicked() = Unit
|
||||
override fun onSendPaymentClicked(recipientId: RecipientId) = Unit
|
||||
override fun onEditedIndicatorClicked(conversationMessage: ConversationMessage) = Unit
|
||||
override fun onShowSafetyTips(forGroup: Boolean) = Unit
|
||||
override fun onReportSpamLearnMoreClicked() = Unit
|
||||
override fun onMessageRequestAcceptOptionsClicked() = Unit
|
||||
override fun onItemDoubleClick(item: MultiselectPart) = Unit
|
||||
override fun onToggleVote(poll: PollRecord, pollOption: PollOption, isChecked: Boolean?) = Unit
|
||||
override fun onViewResultsClicked(pollId: Long) = Unit
|
||||
override fun onIncomingIdentityMismatchClicked(recipientId: RecipientId) = Unit
|
||||
override fun onRegisterVoiceNoteCallbacks(onPlaybackStartObserver: Observer<VoiceNotePlaybackState>) = Unit
|
||||
override fun onUnregisterVoiceNoteCallbacks(onPlaybackStartObserver: Observer<VoiceNotePlaybackState>) = Unit
|
||||
override fun onVoiceNotePause(uri: Uri) = Unit
|
||||
override fun onVoiceNotePlay(uri: Uri, messageId: Long, position: Double) = Unit
|
||||
override fun onVoiceNoteSeekTo(uri: Uri, position: Double) = Unit
|
||||
override fun onVoiceNotePlaybackSpeedChanged(uri: Uri, speed: Float) = Unit
|
||||
override fun onPlayInlineContent(conversationMessage: ConversationMessage?) = Unit
|
||||
override fun onInMemoryMessageClicked(messageRecord: InMemoryMessageRecord) = Unit
|
||||
override fun onViewGroupDescriptionChange(groupId: GroupId?, description: String, isMessageRequestAccepted: Boolean) = Unit
|
||||
override fun onChangeNumberUpdateContact(recipient: Recipient) = Unit
|
||||
override fun onChangeProfileNameUpdateContact(recipient: Recipient) = Unit
|
||||
override fun onBlockJoinRequest(recipient: Recipient) = Unit
|
||||
override fun onInviteToSignalClicked() = Unit
|
||||
override fun onScheduledIndicatorClicked(view: View, conversationMessage: ConversationMessage) = Unit
|
||||
override fun onUrlClicked(url: String): Boolean = false
|
||||
override fun onGiftBadgeRevealed(messageRecord: MessageRecord) = Unit
|
||||
override fun goToMediaPreview(parent: ConversationItem, sharedElement: View, args: MediaIntentFactory.MediaPreviewArgs) = Unit
|
||||
override fun onShowGroupDescriptionClicked(groupName: String, description: String, shouldLinkifyWebLinks: Boolean) = Unit
|
||||
override fun onJoinCallLink(callLinkRootKey: CallLinkRootKey) = Unit
|
||||
override fun onPaymentTombstoneClicked() = Unit
|
||||
override fun onDisplayMediaNoLongerAvailableSheet() = Unit
|
||||
override fun onShowUnverifiedProfileSheet(forGroup: Boolean) = Unit
|
||||
override fun onUpdateSignalClicked() = Unit
|
||||
override fun onViewPollClicked(messageId: Long) = Unit
|
||||
override fun onViewPinnedMessage(messageId: Long) = Unit
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
package org.thoughtcrime.securesms.starred
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.flowOn
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.rx3.asFlow
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.thoughtcrime.securesms.conversation.ConversationMessage
|
||||
import org.thoughtcrime.securesms.database.RxDatabaseObserver
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||
import org.thoughtcrime.securesms.database.model.MmsMessageRecord
|
||||
import org.thoughtcrime.securesms.dependencies.AppDependencies
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
|
||||
class StarredMessagesViewModel(
|
||||
private val threadId: Long?
|
||||
) : ViewModel() {
|
||||
|
||||
fun getMessages(): Flow<List<ConversationMessage>> {
|
||||
val trigger = if (threadId != null) {
|
||||
RxDatabaseObserver.conversation(threadId)
|
||||
} else {
|
||||
RxDatabaseObserver.starredMessages
|
||||
}
|
||||
|
||||
return trigger.toObservable().asFlow()
|
||||
.map {
|
||||
val messages = SignalDatabase.messages.getStarredMessages(threadId)
|
||||
messages.map { record ->
|
||||
val incomingRecord = if (record is MmsMessageRecord && record.isOutgoing) {
|
||||
record.withIncomingType()
|
||||
} else {
|
||||
record
|
||||
}
|
||||
val threadRecipient = SignalDatabase.threads.getRecipientForThreadId(record.threadId) ?: Recipient.UNKNOWN
|
||||
ConversationMessage.ConversationMessageFactory.createWithUnresolvedData(
|
||||
AppDependencies.application,
|
||||
incomingRecord,
|
||||
threadRecipient
|
||||
)
|
||||
}
|
||||
}
|
||||
.distinctUntilChanged()
|
||||
.flowOn(Dispatchers.IO)
|
||||
}
|
||||
|
||||
suspend fun unstarMessage(messageId: Long) {
|
||||
withContext(Dispatchers.IO) {
|
||||
SignalDatabase.messages.setStarred(messageId, false)
|
||||
}
|
||||
}
|
||||
|
||||
class Factory(
|
||||
private val threadId: Long?
|
||||
) : ViewModelProvider.Factory {
|
||||
override fun <T : ViewModel> create(modelClass: Class<T>): T {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
return StarredMessagesViewModel(threadId) as T
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user