mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-04-21 09:20:19 +01:00
Improve performance from thread being updated to data available to render.
This commit is contained in:
@@ -16,12 +16,12 @@ object BackupCountQueries {
|
||||
SELECT COUNT(*) FROM ${GroupReceiptTable.TABLE_NAME}
|
||||
INNER JOIN ${MessageTable.TABLE_NAME} ON ${GroupReceiptTable.TABLE_NAME}.${GroupReceiptTable.MMS_ID} = ${MessageTable.TABLE_NAME}.${MessageTable.ID}
|
||||
WHERE ${MessageTable.TABLE_NAME}.${MessageTable.EXPIRES_IN} <= 0 AND ${MessageTable.TABLE_NAME}.${MessageTable.VIEW_ONCE} <= 0
|
||||
""".trimIndent()
|
||||
"""
|
||||
|
||||
@get:JvmStatic
|
||||
val attachmentCount: String = """
|
||||
SELECT COUNT(*) FROM ${AttachmentTable.TABLE_NAME}
|
||||
INNER JOIN ${MessageTable.TABLE_NAME} ON ${AttachmentTable.TABLE_NAME}.${AttachmentTable.MMS_ID} = ${MessageTable.TABLE_NAME}.${MessageTable.ID}
|
||||
WHERE ${MessageTable.TABLE_NAME}.${MessageTable.EXPIRES_IN} <= 0 AND ${MessageTable.TABLE_NAME}.${MessageTable.VIEW_ONCE} <= 0
|
||||
""".trimIndent()
|
||||
"""
|
||||
}
|
||||
|
||||
@@ -1,74 +0,0 @@
|
||||
package org.thoughtcrime.securesms.components;
|
||||
|
||||
import android.content.Context;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.View;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.constraintlayout.widget.ConstraintLayout;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.conversationlist.model.UnreadPayments;
|
||||
|
||||
/**
|
||||
* Displays the data in a given UnreadPayments object in a banner.
|
||||
*/
|
||||
public class UnreadPaymentsView extends ConstraintLayout {
|
||||
|
||||
private TextView title;
|
||||
private AvatarImageView avatar;
|
||||
private Listener listener;
|
||||
|
||||
public UnreadPaymentsView(@NonNull Context context) {
|
||||
super(context);
|
||||
}
|
||||
|
||||
public UnreadPaymentsView(@NonNull Context context, @Nullable AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
}
|
||||
|
||||
public UnreadPaymentsView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
|
||||
super(context, attrs, defStyleAttr);
|
||||
}
|
||||
|
||||
public UnreadPaymentsView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
|
||||
super(context, attrs, defStyleAttr, defStyleRes);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onFinishInflate() {
|
||||
super.onFinishInflate();
|
||||
|
||||
title = findViewById(R.id.payment_notification_title);
|
||||
avatar = findViewById(R.id.payment_notification_avatar);
|
||||
|
||||
View open = findViewById(R.id.payment_notification_touch_target);
|
||||
View close = findViewById(R.id.payment_notification_close_touch_target);
|
||||
|
||||
open.setOnClickListener(v -> {
|
||||
if (listener != null) listener.onOpenPaymentsNotificationClicked();
|
||||
});
|
||||
|
||||
close.setOnClickListener(v -> {
|
||||
if (listener != null) listener.onClosePaymentsNotificationClicked();
|
||||
});
|
||||
}
|
||||
|
||||
public void setListener(@NonNull Listener listener) {
|
||||
this.listener = listener;
|
||||
}
|
||||
|
||||
public void setUnreadPayments(@NonNull UnreadPayments unreadPayments) {
|
||||
title.setText(unreadPayments.getDescription(getContext()));
|
||||
avatar.setAvatar(unreadPayments.getRecipient());
|
||||
avatar.setVisibility(unreadPayments.getRecipient() == null ? GONE : VISIBLE);
|
||||
}
|
||||
|
||||
public interface Listener {
|
||||
void onOpenPaymentsNotificationClicked();
|
||||
|
||||
void onClosePaymentsNotificationClicked();
|
||||
}
|
||||
}
|
||||
@@ -94,7 +94,6 @@ import org.thoughtcrime.securesms.badges.self.expired.ExpiredBadgeBottomSheetDia
|
||||
import org.thoughtcrime.securesms.components.Material3SearchToolbar;
|
||||
import org.thoughtcrime.securesms.components.RatingManager;
|
||||
import org.thoughtcrime.securesms.components.SignalProgressDialog;
|
||||
import org.thoughtcrime.securesms.components.UnreadPaymentsView;
|
||||
import org.thoughtcrime.securesms.components.menu.ActionItem;
|
||||
import org.thoughtcrime.securesms.components.menu.SignalBottomActionBar;
|
||||
import org.thoughtcrime.securesms.components.menu.SignalContextMenu;
|
||||
@@ -130,7 +129,6 @@ import org.thoughtcrime.securesms.conversationlist.chatfilter.ConversationListFi
|
||||
import org.thoughtcrime.securesms.conversationlist.chatfilter.FilterLerp;
|
||||
import org.thoughtcrime.securesms.conversationlist.model.Conversation;
|
||||
import org.thoughtcrime.securesms.conversationlist.model.ConversationFilter;
|
||||
import org.thoughtcrime.securesms.conversationlist.model.UnreadPayments;
|
||||
import org.thoughtcrime.securesms.database.MessageTable.MarkedMessageInfo;
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase;
|
||||
import org.thoughtcrime.securesms.database.ThreadTable;
|
||||
@@ -154,7 +152,6 @@ import org.thoughtcrime.securesms.megaphone.SmsExportMegaphoneActivity;
|
||||
import org.thoughtcrime.securesms.mms.GlideApp;
|
||||
import org.thoughtcrime.securesms.notifications.MarkReadReceiver;
|
||||
import org.thoughtcrime.securesms.notifications.profiles.NotificationProfile;
|
||||
import org.thoughtcrime.securesms.payments.preferences.PaymentsActivity;
|
||||
import org.thoughtcrime.securesms.permissions.Permissions;
|
||||
import org.thoughtcrime.securesms.profiles.manage.ManageProfileActivity;
|
||||
import org.thoughtcrime.securesms.ratelimit.RecaptchaProofBottomSheetFragment;
|
||||
@@ -199,7 +196,6 @@ import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import kotlin.Unit;
|
||||
@@ -224,29 +220,28 @@ public class ConversationListFragment extends MainFragment implements ActionMode
|
||||
private static final int MAX_CONTACTS_ABOVE_FOLD = 5;
|
||||
private static final int MAX_GROUP_MEMBERSHIPS_ABOVE_FOLD = 5;
|
||||
|
||||
private ActionMode actionMode;
|
||||
private View coordinator;
|
||||
private RecyclerView list;
|
||||
private Stub<ReminderView> reminderView;
|
||||
private Stub<UnreadPaymentsView> paymentNotificationView;
|
||||
private PulsingFloatingActionButton fab;
|
||||
private PulsingFloatingActionButton cameraFab;
|
||||
private ConversationListFilterPullView pullView;
|
||||
private AppBarLayout pullViewAppBarLayout;
|
||||
private ConversationListViewModel viewModel;
|
||||
private RecyclerView.Adapter activeAdapter;
|
||||
private ActionMode actionMode;
|
||||
private View coordinator;
|
||||
private RecyclerView list;
|
||||
private Stub<ReminderView> reminderView;
|
||||
private PulsingFloatingActionButton fab;
|
||||
private PulsingFloatingActionButton cameraFab;
|
||||
private ConversationListFilterPullView pullView;
|
||||
private AppBarLayout pullViewAppBarLayout;
|
||||
private ConversationListViewModel viewModel;
|
||||
private RecyclerView.Adapter activeAdapter;
|
||||
private ConversationListAdapter defaultAdapter;
|
||||
private PagingMappingAdapter<ContactSearchKey> searchAdapter;
|
||||
private Stub<ViewGroup> megaphoneContainer;
|
||||
private SnapToTopDataObserver snapToTopDataObserver;
|
||||
private Drawable archiveDrawable;
|
||||
private AppForegroundObserver.Listener appForegroundObserver;
|
||||
private VoiceNoteMediaControllerOwner mediaControllerOwner;
|
||||
private Stub<FrameLayout> voiceNotePlayerViewStub;
|
||||
private VoiceNotePlayerView voiceNotePlayerView;
|
||||
private SignalBottomActionBar bottomActionBar;
|
||||
private SignalContextMenu activeContextMenu;
|
||||
private LifecycleDisposable lifecycleDisposable;
|
||||
private Stub<ViewGroup> megaphoneContainer;
|
||||
private SnapToTopDataObserver snapToTopDataObserver;
|
||||
private Drawable archiveDrawable;
|
||||
private AppForegroundObserver.Listener appForegroundObserver;
|
||||
private VoiceNoteMediaControllerOwner mediaControllerOwner;
|
||||
private Stub<FrameLayout> voiceNotePlayerViewStub;
|
||||
private VoiceNotePlayerView voiceNotePlayerView;
|
||||
private SignalBottomActionBar bottomActionBar;
|
||||
private SignalContextMenu activeContextMenu;
|
||||
private LifecycleDisposable lifecycleDisposable;
|
||||
|
||||
protected ConversationListArchiveItemDecoration archiveDecoration;
|
||||
protected ConversationListItemAnimator itemAnimator;
|
||||
@@ -283,12 +278,14 @@ public class ConversationListFragment extends MainFragment implements ActionMode
|
||||
|
||||
@Override
|
||||
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
|
||||
lifecycleDisposable = new LifecycleDisposable();
|
||||
lifecycleDisposable.bindTo(getViewLifecycleOwner());
|
||||
|
||||
coordinator = view.findViewById(R.id.coordinator);
|
||||
list = view.findViewById(R.id.list);
|
||||
bottomActionBar = view.findViewById(R.id.conversation_list_bottom_action_bar);
|
||||
reminderView = new Stub<>(view.findViewById(R.id.reminder));
|
||||
megaphoneContainer = new Stub<>(view.findViewById(R.id.megaphone_container));
|
||||
paymentNotificationView = new Stub<>(view.findViewById(R.id.payments_notification));
|
||||
voiceNotePlayerViewStub = new Stub<>(view.findViewById(R.id.voice_note_player));
|
||||
fab = view.findViewById(R.id.fab);
|
||||
cameraFab = view.findViewById(R.id.camera_fab);
|
||||
@@ -428,7 +425,6 @@ public class ConversationListFragment extends MainFragment implements ActionMode
|
||||
}
|
||||
});
|
||||
|
||||
lifecycleDisposable = new LifecycleDisposable();
|
||||
conversationListTabsViewModel = new ViewModelProvider(requireActivity()).get(ConversationListTabsViewModel.class);
|
||||
|
||||
lifecycleDisposable.bindTo(getViewLifecycleOwner());
|
||||
@@ -453,7 +449,6 @@ public class ConversationListFragment extends MainFragment implements ActionMode
|
||||
bottomActionBar = null;
|
||||
reminderView = null;
|
||||
megaphoneContainer = null;
|
||||
paymentNotificationView = null;
|
||||
voiceNotePlayerViewStub = null;
|
||||
fab = null;
|
||||
cameraFab = null;
|
||||
@@ -569,7 +564,7 @@ public class ConversationListFragment extends MainFragment implements ActionMode
|
||||
menu.findItem(R.id.menu_insights).setVisible(Util.isDefaultSmsProvider(requireContext()));
|
||||
menu.findItem(R.id.menu_clear_passphrase).setVisible(!TextSecurePreferences.isPasswordDisabled(requireContext()));
|
||||
|
||||
ConversationFilterRequest request = viewModel.getConversationFilterRequest().getValue();
|
||||
ConversationFilterRequest request = viewModel.getConversationFilterRequest();
|
||||
boolean isChatFilterEnabled = request != null && request.getFilter() == ConversationFilter.UNREAD;
|
||||
|
||||
menu.findItem(R.id.menu_filter_unread_chats).setVisible(FeatureFlags.chatFilters() && !isChatFilterEnabled);
|
||||
@@ -617,7 +612,7 @@ public class ConversationListFragment extends MainFragment implements ActionMode
|
||||
@Override
|
||||
public void onConfigurationChanged(@NonNull Configuration newConfig) {
|
||||
super.onConfigurationChanged(newConfig);
|
||||
onMegaphoneChanged(viewModel.getMegaphone().getValue());
|
||||
onMegaphoneChanged(viewModel.getMegaphone());
|
||||
}
|
||||
|
||||
private ContactSearchConfiguration mapSearchStateToConfiguration(@NonNull ContactSearchState state) {
|
||||
@@ -817,8 +812,12 @@ public class ConversationListFragment extends MainFragment implements ActionMode
|
||||
}
|
||||
|
||||
private void initializeSearchListener() {
|
||||
viewModel.getConversationFilterRequest().observe(getViewLifecycleOwner(), this::updateSearchToolbarHint);
|
||||
viewModel.getConversationFilterRequest().observe(getViewLifecycleOwner(), contactSearchMediator::onConversationFilterRequestChanged);
|
||||
lifecycleDisposable.add(
|
||||
viewModel.getFilterRequestState().subscribe(request -> {
|
||||
updateSearchToolbarHint(request);
|
||||
contactSearchMediator.onConversationFilterRequestChanged(request);
|
||||
})
|
||||
);
|
||||
|
||||
requireCallback().getSearchAction().setOnClickListener(v -> {
|
||||
fadeOutButtonsAndMegaphone(250);
|
||||
@@ -853,7 +852,7 @@ public class ConversationListFragment extends MainFragment implements ActionMode
|
||||
fadeInButtonsAndMegaphone(250);
|
||||
}
|
||||
});
|
||||
updateSearchToolbarHint(Objects.requireNonNull(viewModel.getConversationFilterRequest().getValue()));
|
||||
updateSearchToolbarHint(Objects.requireNonNull(viewModel.getConversationFilterRequest()));
|
||||
});
|
||||
}
|
||||
|
||||
@@ -924,9 +923,7 @@ public class ConversationListFragment extends MainFragment implements ActionMode
|
||||
}
|
||||
|
||||
if (adapter instanceof ConversationListAdapter) {
|
||||
viewModel.getPagingController()
|
||||
.observe(getViewLifecycleOwner(),
|
||||
controller -> ((ConversationListAdapter) adapter).setPagingController(controller));
|
||||
((ConversationListAdapter) adapter).setPagingController(viewModel.getController());
|
||||
}
|
||||
|
||||
list.setAdapter(adapter);
|
||||
@@ -953,15 +950,13 @@ public class ConversationListFragment extends MainFragment implements ActionMode
|
||||
}
|
||||
|
||||
private void initializeViewModel() {
|
||||
ConversationListViewModel.Factory viewModelFactory = new ConversationListViewModel.Factory(isArchived());
|
||||
viewModel = new ViewModelProvider(this, new ConversationListViewModel.Factory(isArchived())).get(ConversationListViewModel.class);
|
||||
|
||||
viewModel = new ViewModelProvider(this, (ViewModelProvider.Factory) viewModelFactory).get(ConversationListViewModel.class);
|
||||
|
||||
viewModel.getMegaphone().observe(getViewLifecycleOwner(), this::onMegaphoneChanged);
|
||||
viewModel.getConversationList().observe(getViewLifecycleOwner(), this::onConversationListChanged);
|
||||
viewModel.hasNoConversations().observe(getViewLifecycleOwner(), this::updateEmptyState);
|
||||
viewModel.getNotificationProfiles().observe(getViewLifecycleOwner(), profiles -> requireCallback().updateNotificationProfileStatus(profiles));
|
||||
viewModel.getPipeState().observe(getViewLifecycleOwner(), pipeState -> requireCallback().updateProxyStatus(pipeState));
|
||||
lifecycleDisposable.add(viewModel.getMegaphoneState().subscribe(this::onMegaphoneChanged));
|
||||
lifecycleDisposable.add(viewModel.getConversationsState().subscribe(this::onConversationListChanged));
|
||||
lifecycleDisposable.add(viewModel.getHasNoConversations().subscribe(this::updateEmptyState));
|
||||
lifecycleDisposable.add(viewModel.getNotificationProfiles().subscribe(profiles -> requireCallback().updateNotificationProfileStatus(profiles)));
|
||||
lifecycleDisposable.add(viewModel.getWebSocketState().subscribe(pipeState -> requireCallback().updateProxyStatus(pipeState)));
|
||||
|
||||
appForegroundObserver = new AppForegroundObserver.Listener() {
|
||||
@Override
|
||||
@@ -973,12 +968,12 @@ public class ConversationListFragment extends MainFragment implements ActionMode
|
||||
public void onBackground() {}
|
||||
};
|
||||
|
||||
viewModel.getUnreadPaymentsLiveData().observe(getViewLifecycleOwner(), this::onUnreadPaymentsChanged);
|
||||
|
||||
viewModel.getSelectedConversations().observe(getViewLifecycleOwner(), conversations -> {
|
||||
defaultAdapter.setSelectedConversations(conversations);
|
||||
updateMultiSelectState();
|
||||
});
|
||||
lifecycleDisposable.add(
|
||||
viewModel.getSelectedState().subscribe(conversations -> {
|
||||
defaultAdapter.setSelectedConversations(conversations);
|
||||
updateMultiSelectState();
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
private void onFirstRender() {
|
||||
@@ -1012,31 +1007,8 @@ public class ConversationListFragment extends MainFragment implements ActionMode
|
||||
});
|
||||
}
|
||||
|
||||
private void onUnreadPaymentsChanged(@NonNull Optional<UnreadPayments> unreadPayments) {
|
||||
if (unreadPayments.isPresent()) {
|
||||
paymentNotificationView.get().setListener(new PaymentNotificationListener(unreadPayments.get()));
|
||||
paymentNotificationView.get().setUnreadPayments(unreadPayments.get());
|
||||
animatePaymentUnreadStatusIn();
|
||||
} else {
|
||||
animatePaymentUnreadStatusOut();
|
||||
}
|
||||
}
|
||||
|
||||
private void animatePaymentUnreadStatusIn() {
|
||||
paymentNotificationView.get().setVisibility(View.VISIBLE);
|
||||
requireCallback().getUnreadPaymentsDot().animate().alpha(1);
|
||||
}
|
||||
|
||||
private void animatePaymentUnreadStatusOut() {
|
||||
if (paymentNotificationView.resolved()) {
|
||||
paymentNotificationView.get().setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
requireCallback().getUnreadPaymentsDot().animate().alpha(0);
|
||||
}
|
||||
|
||||
private void onMegaphoneChanged(@Nullable Megaphone megaphone) {
|
||||
if (megaphone == null || isArchived() || getResources().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE) {
|
||||
private void onMegaphoneChanged(@NonNull Megaphone megaphone) {
|
||||
if (megaphone == Megaphone.NONE || isArchived() || getResources().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE) {
|
||||
if (megaphoneContainer.resolved()) {
|
||||
megaphoneContainer.get().setVisibility(View.GONE);
|
||||
megaphoneContainer.get().removeAllViews();
|
||||
@@ -1678,39 +1650,6 @@ public class ConversationListFragment extends MainFragment implements ActionMode
|
||||
pullViewAppBarLayout.setExpanded(false, true);
|
||||
}
|
||||
|
||||
private class PaymentNotificationListener implements UnreadPaymentsView.Listener {
|
||||
|
||||
private final UnreadPayments unreadPayments;
|
||||
|
||||
private PaymentNotificationListener(@NonNull UnreadPayments unreadPayments) {
|
||||
this.unreadPayments = unreadPayments;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onOpenPaymentsNotificationClicked() {
|
||||
UUID paymentId = unreadPayments.getPaymentUuid();
|
||||
|
||||
if (paymentId == null) {
|
||||
goToPaymentsHome();
|
||||
} else {
|
||||
goToSinglePayment(paymentId);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onClosePaymentsNotificationClicked() {
|
||||
viewModel.onUnreadPaymentsClosed();
|
||||
}
|
||||
|
||||
private void goToPaymentsHome() {
|
||||
startActivity(new Intent(requireContext(), PaymentsActivity.class));
|
||||
}
|
||||
|
||||
private void goToSinglePayment(@NonNull UUID paymentId) {
|
||||
startActivity(PaymentsActivity.navigateToPaymentDetails(requireContext(), paymentId));
|
||||
}
|
||||
}
|
||||
|
||||
private class ArchiveListenerCallback extends ItemTouchHelper.SimpleCallback {
|
||||
|
||||
private static final long SWIPE_ANIMATION_DURATION = 175;
|
||||
|
||||
@@ -1,267 +0,0 @@
|
||||
package org.thoughtcrime.securesms.conversationlist;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.lifecycle.LiveData;
|
||||
import androidx.lifecycle.LiveDataReactiveStreams;
|
||||
import androidx.lifecycle.MutableLiveData;
|
||||
import androidx.lifecycle.Transformations;
|
||||
import androidx.lifecycle.ViewModel;
|
||||
import androidx.lifecycle.ViewModelProvider;
|
||||
|
||||
import org.signal.core.util.logging.Log;
|
||||
import org.signal.paging.LivePagedData;
|
||||
import org.signal.paging.PagedData;
|
||||
import org.signal.paging.PagingConfig;
|
||||
import org.signal.paging.PagingController;
|
||||
import org.thoughtcrime.securesms.components.settings.app.notifications.profiles.NotificationProfilesRepository;
|
||||
import org.thoughtcrime.securesms.conversationlist.chatfilter.ConversationFilterRequest;
|
||||
import org.thoughtcrime.securesms.conversationlist.chatfilter.ConversationFilterSource;
|
||||
import org.thoughtcrime.securesms.conversationlist.model.Conversation;
|
||||
import org.thoughtcrime.securesms.conversationlist.model.ConversationFilter;
|
||||
import org.thoughtcrime.securesms.conversationlist.model.ConversationSet;
|
||||
import org.thoughtcrime.securesms.conversationlist.model.UnreadPayments;
|
||||
import org.thoughtcrime.securesms.conversationlist.model.UnreadPaymentsLiveData;
|
||||
import org.thoughtcrime.securesms.database.DatabaseObserver;
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase;
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
||||
import org.thoughtcrime.securesms.megaphone.Megaphone;
|
||||
import org.thoughtcrime.securesms.megaphone.MegaphoneRepository;
|
||||
import org.thoughtcrime.securesms.megaphone.Megaphones;
|
||||
import org.thoughtcrime.securesms.notifications.profiles.NotificationProfile;
|
||||
import org.thoughtcrime.securesms.payments.UnreadPaymentsRepository;
|
||||
import org.thoughtcrime.securesms.search.SearchRepository;
|
||||
import org.thoughtcrime.securesms.search.SearchResult;
|
||||
import org.thoughtcrime.securesms.util.ThrottledDebouncer;
|
||||
import org.thoughtcrime.securesms.util.livedata.LiveDataUtil;
|
||||
import org.thoughtcrime.securesms.util.paging.Invalidator;
|
||||
import org.whispersystems.signalservice.api.websocket.WebSocketConnectionState;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
|
||||
import io.reactivex.rxjava3.core.BackpressureStrategy;
|
||||
import io.reactivex.rxjava3.core.Observable;
|
||||
import io.reactivex.rxjava3.core.Single;
|
||||
import io.reactivex.rxjava3.disposables.CompositeDisposable;
|
||||
import io.reactivex.rxjava3.schedulers.Schedulers;
|
||||
import kotlin.Pair;
|
||||
|
||||
class ConversationListViewModel extends ViewModel {
|
||||
|
||||
private static final String TAG = Log.tag(ConversationListViewModel.class);
|
||||
|
||||
private static boolean coldStart = true;
|
||||
|
||||
private final MutableLiveData<Megaphone> megaphone;
|
||||
private final MutableLiveData<ConversationSet> selectedConversations;
|
||||
private final MutableLiveData<ConversationFilterRequest> conversationFilterRequest;
|
||||
private final LiveData<ConversationListDataSource> conversationListDataSource;
|
||||
private final Set<Conversation> internalSelection;
|
||||
private final LiveData<LivePagedData<Long, Conversation>> pagedData;
|
||||
private final LiveData<Boolean> hasNoConversations;
|
||||
private final MegaphoneRepository megaphoneRepository;
|
||||
private final ThrottledDebouncer updateDebouncer;
|
||||
private final DatabaseObserver.Observer observer;
|
||||
private final Invalidator invalidator;
|
||||
private final CompositeDisposable disposables;
|
||||
private final UnreadPaymentsLiveData unreadPaymentsLiveData;
|
||||
private final UnreadPaymentsRepository unreadPaymentsRepository;
|
||||
private final NotificationProfilesRepository notificationProfilesRepository;
|
||||
private int pinnedCount;
|
||||
|
||||
private ConversationListViewModel(boolean isArchived) {
|
||||
this.megaphone = new MutableLiveData<>();
|
||||
this.internalSelection = new HashSet<>();
|
||||
this.selectedConversations = new MutableLiveData<>(new ConversationSet());
|
||||
this.megaphoneRepository = ApplicationDependencies.getMegaphoneRepository();
|
||||
this.unreadPaymentsRepository = new UnreadPaymentsRepository();
|
||||
this.notificationProfilesRepository = new NotificationProfilesRepository();
|
||||
this.updateDebouncer = new ThrottledDebouncer(500);
|
||||
this.invalidator = new Invalidator();
|
||||
this.disposables = new CompositeDisposable();
|
||||
this.conversationFilterRequest = new MutableLiveData<>(new ConversationFilterRequest(ConversationFilter.OFF, ConversationFilterSource.DRAG));
|
||||
this.conversationListDataSource = Transformations.map(Transformations.distinctUntilChanged(conversationFilterRequest),
|
||||
request -> ConversationListDataSource.create(request.getFilter(),
|
||||
isArchived,
|
||||
SignalStore.uiHints().canDisplayPullToFilterTip() && request.getSource() == ConversationFilterSource.OVERFLOW));
|
||||
this.pagedData = Transformations.map(conversationListDataSource, source -> PagedData.createForLiveData(source,
|
||||
new PagingConfig.Builder()
|
||||
.setPageSize(15)
|
||||
.setBufferPages(2)
|
||||
.build()));
|
||||
this.unreadPaymentsLiveData = new UnreadPaymentsLiveData();
|
||||
this.observer = () -> {
|
||||
updateDebouncer.publish(() -> {
|
||||
LivePagedData<Long, Conversation> data = pagedData.getValue();
|
||||
if (data == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
data.getController().onDataInvalidated();
|
||||
});
|
||||
};
|
||||
|
||||
this.hasNoConversations = LiveDataUtil.mapAsync(LiveDataUtil.combineLatest(conversationFilterRequest, getConversationList(), Pair::new), filterAndData -> {
|
||||
pinnedCount = SignalDatabase.threads().getPinnedConversationListCount(ConversationFilter.OFF);
|
||||
|
||||
if (filterAndData.getSecond().size() > 0) {
|
||||
return false;
|
||||
} else {
|
||||
return SignalDatabase.threads().getArchivedConversationListCount(filterAndData.getFirst().getFilter()) == 0;
|
||||
}
|
||||
});
|
||||
|
||||
ApplicationDependencies.getDatabaseObserver().registerConversationListObserver(observer);
|
||||
}
|
||||
|
||||
public LiveData<Boolean> hasNoConversations() {
|
||||
return hasNoConversations;
|
||||
}
|
||||
|
||||
@NonNull LiveData<Megaphone> getMegaphone() {
|
||||
return megaphone;
|
||||
}
|
||||
|
||||
@NonNull LiveData<List<Conversation>> getConversationList() {
|
||||
return Transformations.switchMap(pagedData, LivePagedData::getData);
|
||||
}
|
||||
|
||||
@NonNull LiveData<PagingController<Long>> getPagingController() {
|
||||
return Transformations.map(pagedData, LivePagedData::getController);
|
||||
}
|
||||
|
||||
@NonNull LiveData<List<NotificationProfile>> getNotificationProfiles() {
|
||||
final Observable<List<NotificationProfile>> activeProfile = Observable.combineLatest(Observable.interval(0, 30, TimeUnit.SECONDS), notificationProfilesRepository.getProfiles(), (interval, profiles) -> profiles);
|
||||
|
||||
return LiveDataReactiveStreams.fromPublisher(activeProfile.toFlowable(BackpressureStrategy.LATEST));
|
||||
}
|
||||
|
||||
@NonNull LiveData<WebSocketConnectionState> getPipeState() {
|
||||
return LiveDataReactiveStreams.fromPublisher(ApplicationDependencies.getSignalWebSocket().getWebSocketState().toFlowable(BackpressureStrategy.LATEST));
|
||||
}
|
||||
|
||||
@NonNull LiveData<Optional<UnreadPayments>> getUnreadPaymentsLiveData() {
|
||||
return unreadPaymentsLiveData;
|
||||
}
|
||||
|
||||
@NonNull LiveData<ConversationFilterRequest> getConversationFilterRequest() {
|
||||
return conversationFilterRequest;
|
||||
}
|
||||
|
||||
public int getPinnedCount() {
|
||||
return pinnedCount;
|
||||
}
|
||||
|
||||
void onVisible() {
|
||||
megaphoneRepository.getNextMegaphone(megaphone::postValue);
|
||||
|
||||
if (!coldStart) {
|
||||
ApplicationDependencies.getDatabaseObserver().notifyConversationListListeners();
|
||||
}
|
||||
|
||||
coldStart = false;
|
||||
}
|
||||
|
||||
@NonNull Set<Conversation> currentSelectedConversations() {
|
||||
return internalSelection;
|
||||
}
|
||||
|
||||
@NonNull LiveData<ConversationSet> getSelectedConversations() {
|
||||
return selectedConversations;
|
||||
}
|
||||
|
||||
void startSelection(@NonNull Conversation conversation) {
|
||||
setSelection(Collections.singleton(conversation));
|
||||
}
|
||||
|
||||
void endSelection() {
|
||||
setSelection(Collections.emptySet());
|
||||
}
|
||||
|
||||
void toggleConversationSelected(@NonNull Conversation conversation) {
|
||||
Set<Conversation> newSelection = new HashSet<>(internalSelection);
|
||||
if (newSelection.contains(conversation)) {
|
||||
newSelection.remove(conversation);
|
||||
} else {
|
||||
newSelection.add(conversation);
|
||||
}
|
||||
|
||||
setSelection(newSelection);
|
||||
}
|
||||
|
||||
void setFiltered(boolean isFiltered, @NonNull ConversationFilterSource conversationFilterSource) {
|
||||
if (isFiltered) {
|
||||
conversationFilterRequest.setValue(new ConversationFilterRequest(ConversationFilter.UNREAD, conversationFilterSource));
|
||||
} else {
|
||||
conversationFilterRequest.setValue(new ConversationFilterRequest(ConversationFilter.OFF, conversationFilterSource));
|
||||
}
|
||||
}
|
||||
|
||||
private void setSelection(@NonNull Collection<Conversation> newSelection) {
|
||||
internalSelection.clear();
|
||||
internalSelection.addAll(newSelection);
|
||||
selectedConversations.setValue(new ConversationSet(internalSelection));
|
||||
}
|
||||
|
||||
void onSelectAllClick() {
|
||||
ConversationListDataSource dataSource = conversationListDataSource.getValue();
|
||||
if (dataSource == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
disposables.add(
|
||||
Single.fromCallable(() -> dataSource.load(0, dataSource.size(), disposables::isDisposed))
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(this::setSelection)
|
||||
);
|
||||
}
|
||||
|
||||
void onMegaphoneCompleted(@NonNull Megaphones.Event event) {
|
||||
megaphone.postValue(null);
|
||||
megaphoneRepository.markFinished(event);
|
||||
}
|
||||
|
||||
void onMegaphoneSnoozed(@NonNull Megaphones.Event event) {
|
||||
megaphoneRepository.markSeen(event);
|
||||
megaphone.postValue(null);
|
||||
}
|
||||
|
||||
void onMegaphoneVisible(@NonNull Megaphone visible) {
|
||||
megaphoneRepository.markVisible(visible.getEvent());
|
||||
}
|
||||
|
||||
void onUnreadPaymentsClosed() {
|
||||
unreadPaymentsRepository.markAllPaymentsSeen();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onCleared() {
|
||||
invalidator.invalidate();
|
||||
disposables.dispose();
|
||||
updateDebouncer.clear();
|
||||
ApplicationDependencies.getDatabaseObserver().unregisterObserver(observer);
|
||||
}
|
||||
|
||||
public static class Factory extends ViewModelProvider.NewInstanceFactory {
|
||||
|
||||
private final boolean isArchived;
|
||||
|
||||
public Factory(boolean isArchived) {
|
||||
this.isArchived = isArchived;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull <T extends ViewModel> T create(@NonNull Class<T> modelClass) {
|
||||
//noinspection ConstantConditions
|
||||
return modelClass.cast(new ConversationListViewModel(isArchived));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,218 @@
|
||||
package org.thoughtcrime.securesms.conversationlist
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import io.reactivex.rxjava3.core.BackpressureStrategy
|
||||
import io.reactivex.rxjava3.core.Flowable
|
||||
import io.reactivex.rxjava3.core.Observable
|
||||
import io.reactivex.rxjava3.disposables.CompositeDisposable
|
||||
import io.reactivex.rxjava3.kotlin.addTo
|
||||
import io.reactivex.rxjava3.schedulers.Schedulers
|
||||
import org.signal.paging.PagedData
|
||||
import org.signal.paging.PagingConfig
|
||||
import org.signal.paging.ProxyPagingController
|
||||
import org.thoughtcrime.securesms.components.settings.app.notifications.profiles.NotificationProfilesRepository
|
||||
import org.thoughtcrime.securesms.conversationlist.chatfilter.ConversationFilterRequest
|
||||
import org.thoughtcrime.securesms.conversationlist.chatfilter.ConversationFilterSource
|
||||
import org.thoughtcrime.securesms.conversationlist.model.Conversation
|
||||
import org.thoughtcrime.securesms.conversationlist.model.ConversationFilter
|
||||
import org.thoughtcrime.securesms.conversationlist.model.ConversationSet
|
||||
import org.thoughtcrime.securesms.database.RxDatabaseObserver
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.megaphone.Megaphone
|
||||
import org.thoughtcrime.securesms.megaphone.MegaphoneRepository
|
||||
import org.thoughtcrime.securesms.megaphone.Megaphones
|
||||
import org.thoughtcrime.securesms.notifications.profiles.NotificationProfile
|
||||
import org.thoughtcrime.securesms.util.rx.RxStore
|
||||
import org.whispersystems.signalservice.api.websocket.WebSocketConnectionState
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
class ConversationListViewModel(
|
||||
private val isArchived: Boolean,
|
||||
private val megaphoneRepository: MegaphoneRepository = ApplicationDependencies.getMegaphoneRepository(),
|
||||
private val notificationProfilesRepository: NotificationProfilesRepository = NotificationProfilesRepository()
|
||||
) : ViewModel() {
|
||||
|
||||
companion object {
|
||||
private var coldStart = true
|
||||
}
|
||||
|
||||
private val disposables: CompositeDisposable = CompositeDisposable()
|
||||
|
||||
private val store = RxStore(ConversationListState()).addTo(disposables)
|
||||
private val conversationListDataSource: Flowable<ConversationListDataSource>
|
||||
private val pagingConfig = PagingConfig.Builder()
|
||||
.setPageSize(15)
|
||||
.setBufferPages(2)
|
||||
.build()
|
||||
|
||||
val conversationsState: Flowable<List<Conversation>> = store.mapDistinctForUi { it.conversations }
|
||||
val megaphoneState: Flowable<Megaphone> = store.mapDistinctForUi { it.megaphone }
|
||||
val selectedState: Flowable<ConversationSet> = store.mapDistinctForUi { it.selectedConversations }
|
||||
val filterRequestState: Flowable<ConversationFilterRequest> = store.mapDistinctForUi { it.filterRequest }
|
||||
val hasNoConversations: Flowable<Boolean>
|
||||
|
||||
val controller = ProxyPagingController<Long>()
|
||||
|
||||
val conversationFilterRequest: ConversationFilterRequest
|
||||
get() = store.state.filterRequest
|
||||
val megaphone: Megaphone
|
||||
get() = store.state.megaphone
|
||||
val pinnedCount: Int
|
||||
get() = store.state.pinnedCount
|
||||
val webSocketState: Observable<WebSocketConnectionState>
|
||||
get() = ApplicationDependencies.getSignalWebSocket().webSocketState
|
||||
|
||||
@get:JvmName("currentSelectedConversations")
|
||||
val currentSelectedConversations: Set<Conversation>
|
||||
get() = store.state.internalSelection
|
||||
|
||||
init {
|
||||
conversationListDataSource = store
|
||||
.stateFlowable
|
||||
.subscribeOn(Schedulers.io())
|
||||
.map { it.filterRequest }
|
||||
.distinctUntilChanged()
|
||||
.map {
|
||||
ConversationListDataSource.create(
|
||||
it.filter,
|
||||
isArchived,
|
||||
SignalStore.uiHints().canDisplayPullToFilterTip() && it.source === ConversationFilterSource.OVERFLOW
|
||||
)
|
||||
}
|
||||
.replay(1)
|
||||
.refCount()
|
||||
|
||||
val pagedData = conversationListDataSource
|
||||
.map { PagedData.createForObservable(it, pagingConfig) }
|
||||
.doOnNext { controller.set(it.controller) }
|
||||
.switchMap { it.data.toFlowable(BackpressureStrategy.LATEST) }
|
||||
|
||||
store.update(pagedData) { conversations, state -> state.copy(conversations = conversations) }
|
||||
.addTo(disposables)
|
||||
|
||||
RxDatabaseObserver
|
||||
.conversationList
|
||||
.throttleLatest(500, TimeUnit.MILLISECONDS)
|
||||
.subscribe { controller.onDataInvalidated() }
|
||||
.addTo(disposables)
|
||||
|
||||
val pinnedCount = RxDatabaseObserver
|
||||
.conversationList
|
||||
.map { SignalDatabase.threads.getPinnedConversationListCount(ConversationFilter.OFF) }
|
||||
.distinctUntilChanged()
|
||||
|
||||
store.update(pinnedCount) { pinned, state -> state.copy(pinnedCount = pinned) }
|
||||
.addTo(disposables)
|
||||
|
||||
hasNoConversations = store
|
||||
.stateFlowable
|
||||
.map { it.filterRequest to it.conversations }
|
||||
.distinctUntilChanged()
|
||||
.map { (filterRequest, conversations) ->
|
||||
if (conversations.isNotEmpty()) {
|
||||
false
|
||||
} else {
|
||||
SignalDatabase.threads.getArchivedConversationListCount(filterRequest.filter) == 0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCleared() {
|
||||
disposables.dispose()
|
||||
super.onCleared()
|
||||
}
|
||||
|
||||
fun onVisible() {
|
||||
megaphoneRepository.getNextMegaphone { next ->
|
||||
store.update { it.copy(megaphone = next ?: Megaphone.NONE) }
|
||||
}
|
||||
|
||||
if (!coldStart) {
|
||||
ApplicationDependencies.getDatabaseObserver().notifyConversationListListeners()
|
||||
}
|
||||
coldStart = false
|
||||
}
|
||||
|
||||
fun startSelection(conversation: Conversation) {
|
||||
setSelection(setOf(conversation))
|
||||
}
|
||||
|
||||
fun endSelection() {
|
||||
setSelection(emptySet())
|
||||
}
|
||||
|
||||
fun onSelectAllClick() {
|
||||
conversationListDataSource
|
||||
.subscribeOn(Schedulers.io())
|
||||
.firstOrError()
|
||||
.map { dataSource -> dataSource.load(0, dataSource.size()) { disposables.isDisposed } }
|
||||
.subscribe { newSelection -> setSelection(newSelection) }
|
||||
.addTo(disposables)
|
||||
}
|
||||
|
||||
fun toggleConversationSelected(conversation: Conversation) {
|
||||
val newSelection: MutableSet<Conversation> = store.state.internalSelection.toMutableSet()
|
||||
|
||||
if (newSelection.contains(conversation)) {
|
||||
newSelection.remove(conversation)
|
||||
} else {
|
||||
newSelection.add(conversation)
|
||||
}
|
||||
|
||||
setSelection(newSelection)
|
||||
}
|
||||
|
||||
fun setFiltered(isFiltered: Boolean, conversationFilterSource: ConversationFilterSource) {
|
||||
store.update {
|
||||
it.copy(filterRequest = ConversationFilterRequest(if (isFiltered) ConversationFilter.UNREAD else ConversationFilter.OFF, conversationFilterSource))
|
||||
}
|
||||
}
|
||||
|
||||
fun onMegaphoneCompleted(event: Megaphones.Event) {
|
||||
store.update { it.copy(megaphone = Megaphone.NONE) }
|
||||
megaphoneRepository.markFinished(event)
|
||||
}
|
||||
|
||||
fun onMegaphoneSnoozed(event: Megaphones.Event) {
|
||||
megaphoneRepository.markSeen(event)
|
||||
store.update { it.copy(megaphone = Megaphone.NONE) }
|
||||
}
|
||||
|
||||
fun onMegaphoneVisible(visible: Megaphone) {
|
||||
megaphoneRepository.markVisible(visible.event)
|
||||
}
|
||||
|
||||
fun getNotificationProfiles(): Observable<List<NotificationProfile>> {
|
||||
return Observable
|
||||
.combineLatest(
|
||||
Observable.interval(0, 30, TimeUnit.SECONDS),
|
||||
notificationProfilesRepository.getProfiles()
|
||||
) { _, profiles -> profiles }
|
||||
.distinctUntilChanged()
|
||||
}
|
||||
|
||||
private fun setSelection(newSelection: Collection<Conversation>) {
|
||||
store.update {
|
||||
val selection = newSelection.toSet()
|
||||
it.copy(internalSelection = selection, selectedConversations = ConversationSet(selection))
|
||||
}
|
||||
}
|
||||
|
||||
private data class ConversationListState(
|
||||
val conversations: List<Conversation> = emptyList(),
|
||||
val megaphone: Megaphone = Megaphone.NONE,
|
||||
val selectedConversations: ConversationSet = ConversationSet(),
|
||||
val internalSelection: Set<Conversation> = emptySet(),
|
||||
val filterRequest: ConversationFilterRequest = ConversationFilterRequest(ConversationFilter.OFF, ConversationFilterSource.DRAG),
|
||||
val pinnedCount: Int = 0
|
||||
)
|
||||
|
||||
class Factory(private val isArchived: Boolean) : ViewModelProvider.Factory {
|
||||
override fun <T : ViewModel> create(modelClass: Class<T>): T {
|
||||
return modelClass.cast(ConversationListViewModel(isArchived))!!
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -19,6 +19,6 @@ class CallLinkTable(context: Context, databaseHelper: SignalDatabase) : Database
|
||||
CREATE TABLE $TABLE_NAME (
|
||||
$ID INTEGER PRIMARY KEY
|
||||
)
|
||||
""".trimIndent()
|
||||
"""
|
||||
}
|
||||
}
|
||||
|
||||
@@ -76,7 +76,7 @@ class CallTable(context: Context, databaseHelper: SignalDatabase) : DatabaseTabl
|
||||
UNIQUE ($CALL_ID, $PEER, $CALL_LINK) ON CONFLICT FAIL,
|
||||
CHECK (($PEER IS NULL AND $CALL_LINK IS NOT NULL) OR ($PEER IS NOT NULL AND $CALL_LINK IS NULL))
|
||||
)
|
||||
""".trimIndent()
|
||||
"""
|
||||
|
||||
val CREATE_INDEXES = arrayOf(
|
||||
"CREATE INDEX call_call_id_index ON $TABLE_NAME ($CALL_ID)",
|
||||
@@ -760,7 +760,7 @@ class CallTable(context: Context, databaseHelper: SignalDatabase) : DatabaseTabl
|
||||
${RecipientTable.TABLE_NAME}.${RecipientTable.PHONE} GLOB ? OR
|
||||
${RecipientTable.TABLE_NAME}.${RecipientTable.EMAIL} GLOB ?
|
||||
)
|
||||
""".trimIndent()
|
||||
"""
|
||||
SqlUtil.buildQuery(selection, 0, 0, glob, glob, glob, glob)
|
||||
} else {
|
||||
SqlUtil.buildQuery("")
|
||||
@@ -854,7 +854,7 @@ class CallTable(context: Context, databaseHelper: SignalDatabase) : DatabaseTabl
|
||||
LEFT JOIN ${GroupTable.TABLE_NAME} ON ${GroupTable.TABLE_NAME}.${GroupTable.RECIPIENT_ID} = ${RecipientTable.TABLE_NAME}.${RecipientTable.ID}
|
||||
WHERE true_parent = p.$ID ${if (queryClause.where.isNotEmpty()) "AND ${queryClause.where}" else ""}
|
||||
$offsetLimit
|
||||
""".trimIndent()
|
||||
"""
|
||||
|
||||
return readableDatabase.query(
|
||||
statement,
|
||||
|
||||
@@ -23,7 +23,7 @@ class ChatColorsTable(context: Context, databaseHelper: SignalDatabase) : Databa
|
||||
$ID INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
$CHAT_COLORS BLOB
|
||||
)
|
||||
""".trimIndent()
|
||||
"""
|
||||
}
|
||||
|
||||
fun getById(chatColorsId: ChatColors.Id): ChatColors {
|
||||
|
||||
@@ -31,7 +31,7 @@ class DonationReceiptTable(context: Context, databaseHelper: SignalDatabase) : D
|
||||
$CURRENCY TEXT NOT NULL,
|
||||
$SUBSCRIPTION_LEVEL INTEGER NOT NULL
|
||||
)
|
||||
""".trimIndent()
|
||||
"""
|
||||
|
||||
val CREATE_INDEXS = arrayOf(
|
||||
"CREATE INDEX IF NOT EXISTS donation_receipt_type_index ON $TABLE_NAME ($TYPE)",
|
||||
|
||||
@@ -28,7 +28,6 @@ import org.signal.core.util.requireLong
|
||||
import org.signal.core.util.requireNonNullString
|
||||
import org.signal.core.util.requireString
|
||||
import org.signal.core.util.select
|
||||
import org.signal.core.util.toSingleLine
|
||||
import org.signal.core.util.update
|
||||
import org.signal.core.util.withinTransaction
|
||||
import org.signal.libsignal.zkgroup.groups.GroupMasterKey
|
||||
@@ -171,7 +170,7 @@ class GroupTable(context: Context?, databaseHelper: SignalDatabase?) : DatabaseT
|
||||
WHERE ${MembershipTable.TABLE_NAME}.${MembershipTable.GROUP_ID} = $TABLE_NAME.$GROUP_ID
|
||||
) as $MEMBER_GROUP_CONCAT
|
||||
FROM $TABLE_NAME
|
||||
""".toSingleLine()
|
||||
"""
|
||||
|
||||
val CREATE_TABLES = arrayOf(CREATE_TABLE, MembershipTable.CREATE_TABLE)
|
||||
}
|
||||
@@ -193,7 +192,7 @@ class GroupTable(context: Context?, databaseHelper: SignalDatabase?) : DatabaseT
|
||||
$RECIPIENT_ID INTEGER NOT NULL,
|
||||
UNIQUE($GROUP_ID, $RECIPIENT_ID)
|
||||
)
|
||||
""".toSingleLine()
|
||||
"""
|
||||
}
|
||||
}
|
||||
|
||||
@@ -371,7 +370,7 @@ class GroupTable(context: Context?, databaseHelper: SignalDatabase?) : DatabaseT
|
||||
WHERE $ACTIVE = 1 AND ${MembershipTable.TABLE_NAME}.${MembershipTable.RECIPIENT_ID} IN (${subquery.where})
|
||||
GROUP BY ${MembershipTable.TABLE_NAME}.${MembershipTable.GROUP_ID}
|
||||
ORDER BY $TITLE COLLATE NOCASE ASC
|
||||
""".toSingleLine()
|
||||
"""
|
||||
|
||||
return databaseHelper.signalReadableDatabase.query(statement, subquery.whereArgs)
|
||||
}
|
||||
@@ -383,7 +382,7 @@ class GroupTable(context: Context?, databaseHelper: SignalDatabase?) : DatabaseT
|
||||
$JOINED_GROUP_SELECT
|
||||
WHERE ${query.where}
|
||||
ORDER BY $TITLE COLLATE NOCASE ASC
|
||||
""".trimIndent()
|
||||
"""
|
||||
|
||||
val cursor = databaseHelper.signalReadableDatabase.query(statement, query.whereArgs)
|
||||
return Reader(cursor)
|
||||
@@ -432,7 +431,7 @@ class GroupTable(context: Context?, databaseHelper: SignalDatabase?) : DatabaseT
|
||||
FROM ${MembershipTable.TABLE_NAME}
|
||||
INNER JOIN $TABLE_NAME ON ${MembershipTable.TABLE_NAME}.${MembershipTable.GROUP_ID} = $TABLE_NAME.$GROUP_ID
|
||||
WHERE $query
|
||||
""".trimIndent()
|
||||
"""
|
||||
|
||||
return Reader(readableDatabase.query(selection, queryArgs))
|
||||
}
|
||||
@@ -444,7 +443,7 @@ class GroupTable(context: Context?, databaseHelper: SignalDatabase?) : DatabaseT
|
||||
INNER JOIN ${ThreadTable.TABLE_NAME} ON ${ThreadTable.TABLE_NAME}.${ThreadTable.RECIPIENT_ID} = $TABLE_NAME.$RECIPIENT_ID
|
||||
WHERE ${query.where}
|
||||
ORDER BY ${ThreadTable.TABLE_NAME}.${ThreadTable.DATE} DESC
|
||||
""".toSingleLine()
|
||||
"""
|
||||
|
||||
return Reader(databaseHelper.signalReadableDatabase.rawQuery(sql, query.whereArgs))
|
||||
}
|
||||
@@ -534,7 +533,7 @@ class GroupTable(context: Context?, databaseHelper: SignalDatabase?) : DatabaseT
|
||||
) as $MEMBER_GROUP_CONCAT
|
||||
FROM $TABLE_NAME
|
||||
WHERE $MEMBER_GROUP_CONCAT = ?
|
||||
""".toSingleLine()
|
||||
"""
|
||||
|
||||
return readableDatabase.rawQuery(statement, buildArgs(joinedTestMembers)).use { cursor ->
|
||||
if (cursor.moveToNext()) {
|
||||
@@ -577,7 +576,7 @@ class GroupTable(context: Context?, databaseHelper: SignalDatabase?) : DatabaseT
|
||||
FROM ${MembershipTable.TABLE_NAME}
|
||||
INNER JOIN $TABLE_NAME ON ${MembershipTable.TABLE_NAME}.${MembershipTable.GROUP_ID} = $TABLE_NAME.$GROUP_ID
|
||||
LEFT JOIN ${ThreadTable.TABLE_NAME} ON $TABLE_NAME.$RECIPIENT_ID = ${ThreadTable.TABLE_NAME}.${ThreadTable.RECIPIENT_ID}
|
||||
""".toSingleLine()
|
||||
"""
|
||||
|
||||
var query = "${MembershipTable.TABLE_NAME}.${MembershipTable.RECIPIENT_ID} = ?"
|
||||
var args = buildArgs(recipientId)
|
||||
@@ -1291,7 +1290,7 @@ class GroupTable(context: Context?, databaseHelper: SignalDatabase?) : DatabaseT
|
||||
)
|
||||
)
|
||||
ORDER BY active_timestamp DESC
|
||||
""".toSingleLine()
|
||||
"""
|
||||
|
||||
return readableDatabase
|
||||
.query(query)
|
||||
|
||||
@@ -65,7 +65,7 @@ class LocalMetricsDatabase private constructor(
|
||||
$SPLIT_NAME TEXT NOT NULL,
|
||||
$DURATION INTEGER NOT NULL
|
||||
)
|
||||
""".trimIndent()
|
||||
"""
|
||||
|
||||
private val CREATE_INDEXES = arrayOf(
|
||||
"CREATE INDEX events_create_at_index ON $TABLE_NAME ($CREATED_AT)",
|
||||
@@ -99,7 +99,7 @@ class LocalMetricsDatabase private constructor(
|
||||
SELECT $EVENT_ID, $EVENT_NAME, SUM($DURATION) AS $DURATION
|
||||
FROM $TABLE_NAME
|
||||
GROUP BY $EVENT_ID
|
||||
""".trimIndent()
|
||||
"""
|
||||
}
|
||||
|
||||
override fun onCreate(db: SQLiteDatabase) {
|
||||
@@ -231,7 +231,7 @@ class LocalMetricsDatabase private constructor(
|
||||
OFFSET (SELECT COUNT(*)
|
||||
FROM $table
|
||||
WHERE $where) * $percent / 100 - 1
|
||||
""".trimIndent()
|
||||
"""
|
||||
|
||||
readableDatabase.rawQuery(query, null).use { cursor ->
|
||||
return if (cursor.moveToFirst()) {
|
||||
|
||||
@@ -70,7 +70,7 @@ class LogDatabase private constructor(
|
||||
$BODY TEXT,
|
||||
$SIZE INTEGER
|
||||
)
|
||||
""".trimIndent()
|
||||
"""
|
||||
|
||||
private val CREATE_INDEXES = arrayOf(
|
||||
"CREATE INDEX keep_longer_index ON $TABLE_NAME ($KEEP_LONGER)",
|
||||
|
||||
@@ -6,7 +6,6 @@ import android.database.Cursor
|
||||
import org.signal.core.util.requireInt
|
||||
import org.signal.core.util.requireLong
|
||||
import org.signal.core.util.requireNonNullString
|
||||
import org.signal.core.util.toSingleLine
|
||||
import org.thoughtcrime.securesms.attachments.DatabaseAttachment
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
import org.thoughtcrime.securesms.util.MediaUtil
|
||||
@@ -80,7 +79,7 @@ class MediaTable internal constructor(context: Context?, databaseHelper: SignalD
|
||||
${AttachmentTable.STICKER_PACK_ID} IS NULL AND
|
||||
${MessageTable.TABLE_NAME}.${MessageTable.FROM_RECIPIENT_ID} > 0 AND
|
||||
$THREAD_RECIPIENT_ID > 0
|
||||
""".toSingleLine()
|
||||
"""
|
||||
|
||||
private val UNIQUE_MEDIA_QUERY = """
|
||||
SELECT
|
||||
@@ -92,14 +91,14 @@ class MediaTable internal constructor(context: Context?, databaseHelper: SignalD
|
||||
${AttachmentTable.STICKER_PACK_ID} IS NULL AND
|
||||
${AttachmentTable.TRANSFER_STATE} = ${AttachmentTable.TRANSFER_PROGRESS_DONE}
|
||||
GROUP BY ${AttachmentTable.DATA}
|
||||
""".toSingleLine()
|
||||
"""
|
||||
|
||||
private val GALLERY_MEDIA_QUERY = String.format(
|
||||
BASE_MEDIA_QUERY,
|
||||
"""
|
||||
${AttachmentTable.CONTENT_TYPE} NOT LIKE 'image/svg%' AND
|
||||
(${AttachmentTable.CONTENT_TYPE} LIKE 'image/%' OR ${AttachmentTable.CONTENT_TYPE} LIKE 'video/%')
|
||||
""".toSingleLine()
|
||||
"""
|
||||
)
|
||||
|
||||
private val AUDIO_MEDIA_QUERY = String.format(BASE_MEDIA_QUERY, "${AttachmentTable.CONTENT_TYPE} LIKE 'audio/%'")
|
||||
@@ -113,7 +112,7 @@ class MediaTable internal constructor(context: Context?, databaseHelper: SignalD
|
||||
${AttachmentTable.CONTENT_TYPE} NOT LIKE 'video/%' AND
|
||||
${AttachmentTable.CONTENT_TYPE} NOT LIKE 'audio/%' AND
|
||||
${AttachmentTable.CONTENT_TYPE} NOT LIKE 'text/x-signal-plain'
|
||||
)""".toSingleLine()
|
||||
)"""
|
||||
)
|
||||
|
||||
private fun applyEqualityOperator(threadId: Long, query: String): String {
|
||||
@@ -224,20 +223,20 @@ class MediaTable internal constructor(context: Context?, databaseHelper: SignalD
|
||||
${AttachmentTable.TABLE_NAME}.${AttachmentTable.MMS_ID} DESC,
|
||||
${AttachmentTable.TABLE_NAME}.${AttachmentTable.DISPLAY_ORDER} DESC,
|
||||
${AttachmentTable.TABLE_NAME}.${AttachmentTable.ROW_ID} DESC
|
||||
""".toSingleLine()
|
||||
"""
|
||||
),
|
||||
Oldest(
|
||||
"""
|
||||
${AttachmentTable.TABLE_NAME}.${AttachmentTable.MMS_ID} ASC,
|
||||
${AttachmentTable.TABLE_NAME}.${AttachmentTable.DISPLAY_ORDER} DESC,
|
||||
${AttachmentTable.TABLE_NAME}.${AttachmentTable.ROW_ID} ASC
|
||||
""".toSingleLine()
|
||||
"""
|
||||
),
|
||||
Largest(
|
||||
"""
|
||||
${AttachmentTable.TABLE_NAME}.${AttachmentTable.SIZE} DESC,
|
||||
${AttachmentTable.TABLE_NAME}.${AttachmentTable.DISPLAY_ORDER} DESC
|
||||
""".toSingleLine()
|
||||
"""
|
||||
);
|
||||
|
||||
private val postFix: String
|
||||
|
||||
@@ -55,7 +55,6 @@ import org.signal.core.util.requireNonNullString
|
||||
import org.signal.core.util.requireString
|
||||
import org.signal.core.util.select
|
||||
import org.signal.core.util.toOptional
|
||||
import org.signal.core.util.toSingleLine
|
||||
import org.signal.core.util.update
|
||||
import org.signal.core.util.withinTransaction
|
||||
import org.signal.libsignal.protocol.IdentityKey
|
||||
@@ -363,7 +362,7 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat
|
||||
'${AttachmentTable.UPLOAD_TIMESTAMP}', ${AttachmentTable.TABLE_NAME}.${AttachmentTable.UPLOAD_TIMESTAMP}
|
||||
)
|
||||
) AS ${AttachmentTable.ATTACHMENT_JSON_ALIAS}
|
||||
""".toSingleLine()
|
||||
"""
|
||||
|
||||
private const val IS_STORY_CLAUSE = "$STORY_TYPE > 0 AND $REMOTE_DELETED = 0"
|
||||
private const val RAW_ID_WHERE = "$TABLE_NAME.$ID = ?"
|
||||
@@ -390,7 +389,7 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat
|
||||
${MessageTypes.SMS_EXPORT_TYPE}
|
||||
)
|
||||
ORDER BY $DATE_RECEIVED DESC LIMIT 1
|
||||
""".toSingleLine()
|
||||
"""
|
||||
|
||||
private val IS_CALL_TYPE_CLAUSE = """(
|
||||
($TYPE = ${MessageTypes.INCOMING_AUDIO_CALL_TYPE})
|
||||
@@ -406,7 +405,13 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat
|
||||
($TYPE = ${MessageTypes.MISSED_VIDEO_CALL_TYPE})
|
||||
OR
|
||||
($TYPE = ${MessageTypes.GROUP_CALL_TYPE})
|
||||
)""".toSingleLine()
|
||||
)"""
|
||||
|
||||
private val outgoingTypeClause: String by lazy {
|
||||
MessageTypes.OUTGOING_MESSAGE_TYPES
|
||||
.map { "($TABLE_NAME.$TYPE & ${MessageTypes.BASE_TYPE_MASK} = $it)" }
|
||||
.joinToString(" OR ")
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun mmsReaderFor(cursor: Cursor): MmsReader {
|
||||
@@ -620,7 +625,7 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat
|
||||
UPDATE $TABLE_NAME
|
||||
SET $TYPE = ($TYPE & ${MessageTypes.TOTAL_MASK - maskOff} | $maskOn )
|
||||
WHERE $ID = ?
|
||||
""".toSingleLine(),
|
||||
""",
|
||||
buildArgs(id)
|
||||
)
|
||||
|
||||
@@ -640,7 +645,7 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat
|
||||
$BODY = ?,
|
||||
$TYPE = ($TYPE & ${MessageTypes.TOTAL_MASK - maskOff} | $maskOn)
|
||||
WHERE $ID = ?
|
||||
""".toSingleLine(),
|
||||
""",
|
||||
arrayOf(body, messageId.toString() + "")
|
||||
)
|
||||
|
||||
@@ -729,7 +734,7 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat
|
||||
val results: List<MarkedMessageInfo> = readableDatabase
|
||||
.select(ID, TO_RECIPIENT_ID, DATE_SENT, THREAD_ID, STORY_TYPE)
|
||||
.from(TABLE_NAME)
|
||||
.where("""$ID IN (${Util.join(messageIds, ",")}) AND (${getOutgoingTypeClause()}) AND ($TYPE & ${MessageTypes.SPECIAL_TYPES_MASK} = ${MessageTypes.SPECIAL_TYPE_GIFT_BADGE}) AND $VIEWED_RECEIPT_COUNT = 0""")
|
||||
.where("""$ID IN (${Util.join(messageIds, ",")}) AND ($outgoingTypeClause) AND ($TYPE & ${MessageTypes.SPECIAL_TYPES_MASK} = ${MessageTypes.SPECIAL_TYPE_GIFT_BADGE}) AND $VIEWED_RECEIPT_COUNT = 0""")
|
||||
.run()
|
||||
.readToList { it.toMarkedMessageInfo(outgoing = true) }
|
||||
|
||||
@@ -1294,7 +1299,7 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat
|
||||
null
|
||||
}
|
||||
|
||||
var where = "$IS_STORY_CLAUSE AND (${getOutgoingTypeClause()})"
|
||||
var where = "$IS_STORY_CLAUSE AND ($outgoingTypeClause)"
|
||||
val whereArgs: Array<String>
|
||||
|
||||
if (threadId == null) {
|
||||
@@ -1309,12 +1314,12 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat
|
||||
}
|
||||
|
||||
fun getAllOutgoingStories(reverse: Boolean, limit: Int): Reader {
|
||||
val where = "$IS_STORY_CLAUSE AND (${getOutgoingTypeClause()})"
|
||||
val where = "$IS_STORY_CLAUSE AND ($outgoingTypeClause)"
|
||||
return MmsReader(rawQueryWithAttachments(where, null, reverse, limit.toLong()))
|
||||
}
|
||||
|
||||
fun markAllIncomingStoriesRead(): List<MarkedMessageInfo> {
|
||||
val where = "$IS_STORY_CLAUSE AND NOT (${getOutgoingTypeClause()}) AND $READ = 0"
|
||||
val where = "$IS_STORY_CLAUSE AND NOT ($outgoingTypeClause) AND $READ = 0"
|
||||
val markedMessageInfos = setMessagesRead(where, null)
|
||||
notifyConversationListListeners()
|
||||
return markedMessageInfos
|
||||
@@ -1328,7 +1333,7 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat
|
||||
}
|
||||
|
||||
fun markAllFailedStoriesNotified() {
|
||||
val where = "$IS_STORY_CLAUSE AND (${getOutgoingTypeClause()}) AND $NOTIFIED = 0 AND ($TYPE & ${MessageTypes.BASE_TYPE_MASK}) = ${MessageTypes.BASE_SENT_FAILED_TYPE}"
|
||||
val where = "$IS_STORY_CLAUSE AND ($outgoingTypeClause) AND $NOTIFIED = 0 AND ($TYPE & ${MessageTypes.BASE_TYPE_MASK}) = ${MessageTypes.BASE_SENT_FAILED_TYPE}"
|
||||
|
||||
writableDatabase
|
||||
.update("$TABLE_NAME INDEXED BY $INDEX_THREAD_DATE")
|
||||
@@ -1340,7 +1345,7 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat
|
||||
|
||||
fun markOnboardingStoryRead() {
|
||||
val recipientId = SignalStore.releaseChannelValues().releaseChannelRecipientId ?: return
|
||||
val where = "$IS_STORY_CLAUSE AND NOT (${getOutgoingTypeClause()}) AND $READ = 0 AND $FROM_RECIPIENT_ID = ?"
|
||||
val where = "$IS_STORY_CLAUSE AND NOT ($outgoingTypeClause) AND $READ = 0 AND $FROM_RECIPIENT_ID = ?"
|
||||
val markedMessageInfos = setMessagesRead(where, buildArgs(recipientId))
|
||||
|
||||
if (markedMessageInfos.isNotEmpty()) {
|
||||
@@ -1358,7 +1363,7 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat
|
||||
|
||||
fun getUnreadStories(recipientId: RecipientId, limit: Int): Reader {
|
||||
val threadId = threads.getThreadIdIfExistsFor(recipientId)
|
||||
val query = "$IS_STORY_CLAUSE AND NOT (${getOutgoingTypeClause()}) AND $THREAD_ID = ? AND $VIEWED_RECEIPT_COUNT = ?"
|
||||
val query = "$IS_STORY_CLAUSE AND NOT ($outgoingTypeClause) AND $THREAD_ID = ? AND $VIEWED_RECEIPT_COUNT = ?"
|
||||
val args = buildArgs(threadId, 0)
|
||||
return MmsReader(rawQueryWithAttachments(query, args, false, limit.toLong()))
|
||||
}
|
||||
@@ -1411,7 +1416,7 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat
|
||||
writableDatabase.withinTransaction { db ->
|
||||
db.select(FROM_RECIPIENT_ID)
|
||||
.from(TABLE_NAME)
|
||||
.where("$IS_STORY_CLAUSE AND $DATE_SENT IN ($timestamps) AND NOT (${getOutgoingTypeClause()}) AND $VIEWED_RECEIPT_COUNT > 0")
|
||||
.where("$IS_STORY_CLAUSE AND $DATE_SENT IN ($timestamps) AND NOT ($outgoingTypeClause) AND $VIEWED_RECEIPT_COUNT > 0")
|
||||
.run()
|
||||
.readToList { cursor -> RecipientId.from(cursor.requireLong(FROM_RECIPIENT_ID)) }
|
||||
.forEach { id -> recipients.updateLastStoryViewTimestamp(id) }
|
||||
@@ -1431,7 +1436,7 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat
|
||||
|
||||
val hasUnviewedStories = readableDatabase
|
||||
.exists(TABLE_NAME)
|
||||
.where("$IS_STORY_CLAUSE AND $THREAD_ID = ? AND $VIEWED_RECEIPT_COUNT = ? AND NOT (${getOutgoingTypeClause()})", threadId, 0)
|
||||
.where("$IS_STORY_CLAUSE AND $THREAD_ID = ? AND $VIEWED_RECEIPT_COUNT = ? AND NOT ($outgoingTypeClause)", threadId, 0)
|
||||
.run()
|
||||
|
||||
return if (hasUnviewedStories) {
|
||||
@@ -1444,7 +1449,7 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat
|
||||
fun isOutgoingStoryAlreadyInDatabase(recipientId: RecipientId, sentTimestamp: Long): Boolean {
|
||||
return readableDatabase
|
||||
.exists(TABLE_NAME)
|
||||
.where("$TO_RECIPIENT_ID = ? AND $STORY_TYPE > 0 AND $DATE_SENT = ? AND (${getOutgoingTypeClause()})", recipientId, sentTimestamp)
|
||||
.where("$TO_RECIPIENT_ID = ? AND $STORY_TYPE > 0 AND $DATE_SENT = ? AND ($outgoingTypeClause)", recipientId, sentTimestamp)
|
||||
.run()
|
||||
}
|
||||
|
||||
@@ -1467,10 +1472,10 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat
|
||||
JOIN ${ThreadTable.TABLE_NAME} ON $TABLE_NAME.$THREAD_ID = ${ThreadTable.TABLE_NAME}.${ThreadTable.ID}
|
||||
WHERE
|
||||
$IS_STORY_CLAUSE AND
|
||||
(${getOutgoingTypeClause()}) = 0 AND
|
||||
($outgoingTypeClause) = 0 AND
|
||||
$VIEWED_RECEIPT_COUNT = 0 AND
|
||||
$TABLE_NAME.$READ = 0
|
||||
""".toSingleLine()
|
||||
"""
|
||||
|
||||
return readableDatabase
|
||||
.rawQuery(query, null)
|
||||
@@ -1478,7 +1483,7 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat
|
||||
}
|
||||
|
||||
fun hasFailedOutgoingStory(): Boolean {
|
||||
val where = "$IS_STORY_CLAUSE AND (${getOutgoingTypeClause()}) AND $NOTIFIED = 0 AND ($TYPE & ${MessageTypes.BASE_TYPE_MASK}) = ${MessageTypes.BASE_SENT_FAILED_TYPE}"
|
||||
val where = "$IS_STORY_CLAUSE AND ($outgoingTypeClause) AND $NOTIFIED = 0 AND ($TYPE & ${MessageTypes.BASE_TYPE_MASK}) = ${MessageTypes.BASE_SENT_FAILED_TYPE}"
|
||||
return readableDatabase.exists(TABLE_NAME).where(where).run()
|
||||
}
|
||||
|
||||
@@ -1488,11 +1493,11 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat
|
||||
$TABLE_NAME.$DATE_SENT AS sent_timestamp,
|
||||
$TABLE_NAME.$ID AS mms_id,
|
||||
${ThreadTable.TABLE_NAME}.${ThreadTable.RECIPIENT_ID},
|
||||
(${getOutgoingTypeClause()}) AS is_outgoing,
|
||||
($outgoingTypeClause) AS is_outgoing,
|
||||
$VIEWED_RECEIPT_COUNT,
|
||||
$TABLE_NAME.$DATE_SENT,
|
||||
$RECEIPT_TIMESTAMP,
|
||||
(${getOutgoingTypeClause()}) = 0 AND $VIEWED_RECEIPT_COUNT = 0 AS is_unread
|
||||
($outgoingTypeClause) = 0 AND $VIEWED_RECEIPT_COUNT = 0 AS is_unread
|
||||
FROM $TABLE_NAME
|
||||
JOIN ${ThreadTable.TABLE_NAME} ON $TABLE_NAME.$THREAD_ID = ${ThreadTable.TABLE_NAME}.${ThreadTable.ID}
|
||||
WHERE
|
||||
@@ -1506,7 +1511,7 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat
|
||||
WHEN is_outgoing = 0 AND viewed_receipt_count > 0 THEN $RECEIPT_TIMESTAMP
|
||||
WHEN is_outgoing = 1 THEN $TABLE_NAME.$DATE_SENT
|
||||
END DESC
|
||||
""".toSingleLine()
|
||||
"""
|
||||
|
||||
return readableDatabase
|
||||
.rawQuery(query, null)
|
||||
@@ -1545,7 +1550,7 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat
|
||||
fun hasSelfReplyInStory(parentStoryId: Long): Boolean {
|
||||
return readableDatabase
|
||||
.exists(TABLE_NAME)
|
||||
.where("$PARENT_STORY_ID = ? AND (${getOutgoingTypeClause()})", -parentStoryId)
|
||||
.where("$PARENT_STORY_ID = ? AND ($outgoingTypeClause)", -parentStoryId)
|
||||
.run()
|
||||
}
|
||||
|
||||
@@ -1589,7 +1594,7 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat
|
||||
FROM $TABLE_NAME
|
||||
WHERE $storiesBeforeTimestampWhere
|
||||
)
|
||||
""".toSingleLine()
|
||||
"""
|
||||
|
||||
val disassociateQuoteQuery = """
|
||||
UPDATE $TABLE_NAME
|
||||
@@ -1603,7 +1608,7 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat
|
||||
FROM $TABLE_NAME
|
||||
WHERE $storiesBeforeTimestampWhere
|
||||
)
|
||||
""".toSingleLine()
|
||||
"""
|
||||
|
||||
db.execSQL(deleteStoryRepliesQuery, sharedArgs)
|
||||
db.execSQL(disassociateQuoteQuery, sharedArgs)
|
||||
@@ -1726,7 +1731,7 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat
|
||||
|
||||
fun getIncomingMeaningfulMessageCountSince(threadId: Long, afterTime: Long): Int {
|
||||
val meaningfulMessagesQuery = buildMeaningfulMessagesQuery(threadId)
|
||||
val where = "${meaningfulMessagesQuery.where} AND $DATE_RECEIVED >= ? AND NOT (${getOutgoingTypeClause()})"
|
||||
val where = "${meaningfulMessagesQuery.where} AND $DATE_RECEIVED >= ? AND NOT ($outgoingTypeClause)"
|
||||
val whereArgs = appendArg(meaningfulMessagesQuery.whereArgs, afterTime.toString())
|
||||
|
||||
return readableDatabase
|
||||
@@ -1750,7 +1755,7 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat
|
||||
$TYPE != ${MessageTypes.BOOST_REQUEST_TYPE} AND
|
||||
$TYPE & ${MessageTypes.GROUP_V2_LEAVE_BITS} != ${MessageTypes.GROUP_V2_LEAVE_BITS}
|
||||
)
|
||||
""".toSingleLine()
|
||||
"""
|
||||
|
||||
return SqlUtil.buildQuery(query, threadId)
|
||||
}
|
||||
@@ -1809,7 +1814,7 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat
|
||||
$where
|
||||
GROUP BY
|
||||
$TABLE_NAME.$ID
|
||||
""".toSingleLine()
|
||||
"""
|
||||
|
||||
if (reverse) {
|
||||
rawQueryString += " ORDER BY $TABLE_NAME.$ID DESC"
|
||||
@@ -1851,7 +1856,7 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat
|
||||
UPDATE $TABLE_NAME
|
||||
SET $TYPE = ($TYPE & ${MessageTypes.TOTAL_MASK - maskOff} | $maskOn )
|
||||
WHERE $ID = ?
|
||||
""".toSingleLine(),
|
||||
""",
|
||||
buildArgs(id)
|
||||
)
|
||||
|
||||
@@ -2059,10 +2064,10 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat
|
||||
$READ = 0 OR
|
||||
(
|
||||
$REACTIONS_UNREAD = 1 AND
|
||||
(${getOutgoingTypeClause()})
|
||||
($outgoingTypeClause)
|
||||
)
|
||||
)
|
||||
""".toSingleLine()
|
||||
"""
|
||||
|
||||
val args = mutableListOf(threadId.toString())
|
||||
|
||||
@@ -2083,10 +2088,10 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat
|
||||
$READ = 0 OR
|
||||
(
|
||||
$REACTIONS_UNREAD = 1 AND
|
||||
(${getOutgoingTypeClause()})
|
||||
($outgoingTypeClause)
|
||||
)
|
||||
)
|
||||
""".toSingleLine()
|
||||
"""
|
||||
|
||||
val args = mutableListOf(threadId.toString(), groupStoryId.toString())
|
||||
|
||||
@@ -2133,7 +2138,7 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat
|
||||
}
|
||||
|
||||
fun setAllMessagesRead(): List<MarkedMessageInfo> {
|
||||
return setMessagesRead("$STORY_TYPE = 0 AND $PARENT_STORY_ID <= 0 AND ($READ = 0 OR ($REACTIONS_UNREAD = 1 AND (${getOutgoingTypeClause()})))", null)
|
||||
return setMessagesRead("$STORY_TYPE = 0 AND $PARENT_STORY_ID <= 0 AND ($READ = 0 OR ($REACTIONS_UNREAD = 1 AND ($outgoingTypeClause)))", null)
|
||||
}
|
||||
|
||||
private fun setMessagesRead(where: String, arguments: Array<String>?): List<MarkedMessageInfo> {
|
||||
@@ -3306,7 +3311,7 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat
|
||||
$TABLE_NAME INNER JOIN ${AttachmentTable.TABLE_NAME} ON $TABLE_NAME.$ID = ${AttachmentTable.TABLE_NAME}.${AttachmentTable.MMS_ID}
|
||||
WHERE
|
||||
${getInsecureMessageClause()} AND $EXPORTED < ${MessageExportStatus.EXPORTED.serialize()}
|
||||
""".toSingleLine(),
|
||||
""",
|
||||
null
|
||||
).readToSingleLong()
|
||||
|
||||
@@ -3415,7 +3420,7 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat
|
||||
WHERE
|
||||
$VIEW_ONCE > 0 AND
|
||||
(${AttachmentTable.DATA} NOT NULL OR ${AttachmentTable.TRANSFER_STATE} != ?)
|
||||
""".toSingleLine()
|
||||
"""
|
||||
|
||||
val args = buildArgs(AttachmentTable.TRANSFER_PROGRESS_DONE)
|
||||
|
||||
@@ -3503,16 +3508,6 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat
|
||||
return result.toOptional()
|
||||
}
|
||||
|
||||
private fun getOutgoingTypeClause(): String {
|
||||
val segments: MutableList<String> = ArrayList(MessageTypes.OUTGOING_MESSAGE_TYPES.size)
|
||||
|
||||
for (outgoingMessageType in MessageTypes.OUTGOING_MESSAGE_TYPES) {
|
||||
segments.add("($TABLE_NAME.$TYPE & ${MessageTypes.BASE_TYPE_MASK} = $outgoingMessageType)")
|
||||
}
|
||||
|
||||
return segments.joinToString(" OR ")
|
||||
}
|
||||
|
||||
fun getInsecureMessageCount(): Int {
|
||||
return readableDatabase
|
||||
.select("COUNT(*)")
|
||||
@@ -4247,7 +4242,7 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat
|
||||
($TO_RECIPIENT_ID = ? OR EXISTS (SELECT 1 FROM ${GroupTable.TABLE_NAME} WHERE ${GroupTable.TABLE_NAME}.${GroupTable.RECIPIENT_ID} = $TO_RECIPIENT_ID))
|
||||
$qualifierWhere
|
||||
RETURNING $ID, $THREAD_ID, $STORY_TYPE
|
||||
""".toSingleLine(),
|
||||
""",
|
||||
buildArgs(receiptSentTimestamp, targetTimestamp, Recipient.self().id, receiptAuthor)
|
||||
).forEach { cursor ->
|
||||
val messageId = cursor.requireLong(ID)
|
||||
@@ -4336,7 +4331,7 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat
|
||||
readableDatabase
|
||||
.select(ID, THREAD_ID, EXPIRES_IN, EXPIRE_STARTED)
|
||||
.from(TABLE_NAME)
|
||||
.where("$DATE_SENT = ? AND ($FROM_RECIPIENT_ID = ? OR ($FROM_RECIPIENT_ID = ? AND ${getOutgoingTypeClause()}))", messageId.timetamp, messageId.recipientId, Recipient.self().id)
|
||||
.where("$DATE_SENT = ? AND ($FROM_RECIPIENT_ID = ? OR ($FROM_RECIPIENT_ID = ? AND $outgoingTypeClause))", messageId.timetamp, messageId.recipientId, Recipient.self().id)
|
||||
.run()
|
||||
.forEach { cursor ->
|
||||
val id = cursor.requireLong(ID)
|
||||
|
||||
@@ -53,7 +53,7 @@ class NotificationProfileDatabase(context: Context, databaseHelper: SignalDataba
|
||||
$ALLOW_ALL_CALLS INTEGER NOT NULL DEFAULT 0,
|
||||
$ALLOW_ALL_MENTIONS INTEGER NOT NULL DEFAULT 0
|
||||
)
|
||||
""".trimIndent()
|
||||
"""
|
||||
}
|
||||
|
||||
private object NotificationProfileScheduleTable {
|
||||
@@ -77,7 +77,7 @@ class NotificationProfileDatabase(context: Context, databaseHelper: SignalDataba
|
||||
$END INTEGER NOT NULL,
|
||||
$DAYS_ENABLED TEXT NOT NULL
|
||||
)
|
||||
""".trimIndent()
|
||||
"""
|
||||
|
||||
const val CREATE_INDEX = "CREATE INDEX notification_profile_schedule_profile_index ON $TABLE_NAME ($NOTIFICATION_PROFILE_ID)"
|
||||
}
|
||||
@@ -96,7 +96,7 @@ class NotificationProfileDatabase(context: Context, databaseHelper: SignalDataba
|
||||
$RECIPIENT_ID INTEGER NOT NULL,
|
||||
UNIQUE($NOTIFICATION_PROFILE_ID, $RECIPIENT_ID) ON CONFLICT REPLACE
|
||||
)
|
||||
""".trimIndent()
|
||||
"""
|
||||
|
||||
const val CREATE_INDEX = "CREATE INDEX notification_profile_allowed_members_profile_index ON $TABLE_NAME ($NOTIFICATION_PROFILE_ID)"
|
||||
}
|
||||
|
||||
@@ -37,7 +37,7 @@ class ReactionTable(context: Context, databaseHelper: SignalDatabase) : Database
|
||||
$DATE_RECEIVED INTEGER NOT NULL,
|
||||
UNIQUE($MESSAGE_ID, $AUTHOR_ID) ON CONFLICT REPLACE
|
||||
)
|
||||
""".trimIndent()
|
||||
"""
|
||||
|
||||
private fun readReaction(cursor: Cursor): ReactionRecord {
|
||||
return ReactionRecord(
|
||||
|
||||
@@ -32,7 +32,6 @@ import org.signal.core.util.requireLong
|
||||
import org.signal.core.util.requireNonNullString
|
||||
import org.signal.core.util.requireString
|
||||
import org.signal.core.util.select
|
||||
import org.signal.core.util.toSingleLine
|
||||
import org.signal.core.util.update
|
||||
import org.signal.core.util.withinTransaction
|
||||
import org.signal.libsignal.protocol.IdentityKey
|
||||
@@ -254,7 +253,7 @@ open class RecipientTable(context: Context, databaseHelper: SignalDatabase) : Da
|
||||
$REPORTING_TOKEN BLOB DEFAULT NULL,
|
||||
$SYSTEM_NICKNAME TEXT DEFAULT NULL
|
||||
)
|
||||
""".trimIndent()
|
||||
"""
|
||||
|
||||
val CREATE_INDEXS = arrayOf(
|
||||
"CREATE INDEX IF NOT EXISTS recipient_group_type_index ON $TABLE_NAME ($GROUP_TYPE);",
|
||||
@@ -343,7 +342,7 @@ open class RecipientTable(context: Context, databaseHelper: SignalDatabase) : Da
|
||||
NULLIF($USERNAME, '')
|
||||
)
|
||||
) AS $SORT_NAME
|
||||
""".trimIndent()
|
||||
"""
|
||||
)
|
||||
|
||||
@JvmField
|
||||
@@ -385,7 +384,7 @@ open class RecipientTable(context: Context, databaseHelper: SignalDatabase) : Da
|
||||
' ',
|
||||
''
|
||||
) AS $SORT_NAME
|
||||
""".trimIndent()
|
||||
"""
|
||||
)
|
||||
|
||||
private val INSIGHTS_INVITEE_LIST =
|
||||
@@ -1079,7 +1078,7 @@ open class RecipientTable(context: Context, databaseHelper: SignalDatabase) : Da
|
||||
$TABLE_NAME LEFT OUTER JOIN ${IdentityTable.TABLE_NAME} ON $TABLE_NAME.$SERVICE_ID = ${IdentityTable.TABLE_NAME}.${IdentityTable.ADDRESS}
|
||||
LEFT OUTER JOIN ${GroupTable.TABLE_NAME} ON $TABLE_NAME.$GROUP_ID = ${GroupTable.TABLE_NAME}.${GroupTable.GROUP_ID}
|
||||
LEFT OUTER JOIN ${ThreadTable.TABLE_NAME} ON $TABLE_NAME.$ID = ${ThreadTable.TABLE_NAME}.${ThreadTable.RECIPIENT_ID}
|
||||
""".trimIndent()
|
||||
"""
|
||||
val out: MutableList<RecipientRecord> = ArrayList()
|
||||
val columns: Array<String> = TYPED_RECIPIENT_PROJECTION + arrayOf(
|
||||
SYSTEM_NICKNAME,
|
||||
@@ -1129,7 +1128,7 @@ open class RecipientTable(context: Context, databaseHelper: SignalDatabase) : Da
|
||||
FROM ${DistributionListTables.ListTable.TABLE_NAME}
|
||||
)
|
||||
)
|
||||
""".toSingleLine(),
|
||||
""",
|
||||
GroupType.NONE.id,
|
||||
Recipient.self().id,
|
||||
GroupType.SIGNAL_V1.id
|
||||
@@ -3158,7 +3157,7 @@ open class RecipientTable(context: Context, databaseHelper: SignalDatabase) : Da
|
||||
ORDER BY $SORT_NAME, $SYSTEM_JOINED_NAME, $SEARCH_PROFILE_NAME, $PHONE
|
||||
)
|
||||
GROUP BY letter_header
|
||||
""".trimIndent(),
|
||||
""",
|
||||
searchSelection.args
|
||||
).use { cursor ->
|
||||
if (cursor.count == 0) {
|
||||
@@ -3263,7 +3262,7 @@ open class RecipientTable(context: Context, databaseHelper: SignalDatabase) : Da
|
||||
$PHONE GLOB ? OR
|
||||
$EMAIL GLOB ?
|
||||
)
|
||||
""".trimIndent()
|
||||
"""
|
||||
val args = SqlUtil.buildArgs(0, 0, query, query, query, query)
|
||||
return readableDatabase.query(TABLE_NAME, SEARCH_PROJECTION, selection, args, null, null, null)
|
||||
}
|
||||
@@ -3284,7 +3283,7 @@ open class RecipientTable(context: Context, databaseHelper: SignalDatabase) : Da
|
||||
$PHONE GLOB ? OR
|
||||
$EMAIL GLOB ?
|
||||
))
|
||||
""".toSingleLine()
|
||||
"""
|
||||
|
||||
return SqlUtil.Query(subquery, SqlUtil.buildArgs(0, 0, query, query, query, query))
|
||||
}
|
||||
@@ -3302,7 +3301,7 @@ open class RecipientTable(context: Context, databaseHelper: SignalDatabase) : Da
|
||||
$PHONE GLOB ? OR
|
||||
$EMAIL GLOB ?
|
||||
)
|
||||
""".toSingleLine()
|
||||
"""
|
||||
|
||||
return readableDatabase.query(subquery, SqlUtil.buildArgs(0, 0, query, query, query, query))
|
||||
}
|
||||
@@ -3479,7 +3478,7 @@ open class RecipientTable(context: Context, databaseHelper: SignalDatabase) : Da
|
||||
r.$PROFILE_SHARING = 0 AND (
|
||||
EXISTS(SELECT 1 FROM ${MessageTable.TABLE_NAME} WHERE ${MessageTable.THREAD_ID} = t.${ThreadTable.ID} AND ${MessageTable.DATE_RECEIVED} < ?)
|
||||
)
|
||||
""".trimIndent()
|
||||
"""
|
||||
|
||||
val idsToUpdate: MutableList<Long> = ArrayList()
|
||||
readableDatabase.rawQuery(select, whereArgs).use { cursor ->
|
||||
@@ -3998,7 +3997,7 @@ open class RecipientTable(context: Context, databaseHelper: SignalDatabase) : Da
|
||||
UPDATE $TABLE_NAME
|
||||
SET $SERVICE_ID = $PNI_COLUMN
|
||||
WHERE $ID = ? AND $PNI_COLUMN NOT NULL
|
||||
""".toSingleLine(),
|
||||
""",
|
||||
SqlUtil.buildArgs(recipientId)
|
||||
)
|
||||
|
||||
@@ -4466,7 +4465,7 @@ open class RecipientTable(context: Context, databaseHelper: SignalDatabase) : Da
|
||||
INNER JOIN ${GroupTable.TABLE_NAME} ON ${GroupTable.TABLE_NAME}.${GroupTable.GROUP_ID} = ${GroupTable.MembershipTable.TABLE_NAME}.${GroupTable.MembershipTable.GROUP_ID}
|
||||
WHERE ${GroupTable.MembershipTable.TABLE_NAME}.${GroupTable.MembershipTable.RECIPIENT_ID} = $TABLE_NAME.$ID AND ${GroupTable.TABLE_NAME}.${GroupTable.ACTIVE} = 1 AND ${GroupTable.TABLE_NAME}.${GroupTable.MMS} = 0
|
||||
)
|
||||
""".toSingleLine()
|
||||
"""
|
||||
const val FILTER_GROUPS = " AND $GROUP_ID IS NULL"
|
||||
const val FILTER_ID = " AND $ID != ?"
|
||||
const val FILTER_BLOCKED = " AND $BLOCKED = ?"
|
||||
|
||||
@@ -80,7 +80,7 @@ class RemoteMegaphoneTable(context: Context, databaseHelper: SignalDatabase) : D
|
||||
$SNOOZED_AT INTEGER DEFAULT 0,
|
||||
$SEEN_COUNT INTEGER DEFAULT 0
|
||||
)
|
||||
""".trimIndent()
|
||||
"""
|
||||
|
||||
const val VERSION_FINISHED = Int.MAX_VALUE
|
||||
}
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
package org.thoughtcrime.securesms.database
|
||||
|
||||
import io.reactivex.rxjava3.core.BackpressureStrategy
|
||||
import io.reactivex.rxjava3.core.Emitter
|
||||
import io.reactivex.rxjava3.core.Flowable
|
||||
import io.reactivex.rxjava3.schedulers.Schedulers
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
|
||||
|
||||
/**
|
||||
* Provide a shared Rx interface to listen to database updates and ensure listeners
|
||||
* execute on [Schedulers.io].
|
||||
*/
|
||||
object RxDatabaseObserver {
|
||||
|
||||
val conversationList: Flowable<Unit> by lazy { conversationListFlowable() }
|
||||
|
||||
private fun conversationListFlowable(): Flowable<Unit> {
|
||||
val flowable = Flowable.create(
|
||||
{
|
||||
val listener = RxObserver(it)
|
||||
|
||||
ApplicationDependencies.getDatabaseObserver().registerConversationListObserver(listener)
|
||||
it.setCancellable { ApplicationDependencies.getDatabaseObserver().unregisterObserver(listener) }
|
||||
|
||||
listener.prime()
|
||||
},
|
||||
BackpressureStrategy.LATEST
|
||||
)
|
||||
|
||||
return flowable
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(Schedulers.io())
|
||||
.replay(1)
|
||||
.refCount()
|
||||
.observeOn(Schedulers.io())
|
||||
}
|
||||
|
||||
private class RxObserver(private val emitter: Emitter<Unit>) : DatabaseObserver.Observer {
|
||||
fun prime() {
|
||||
emitter.onNext(Unit)
|
||||
}
|
||||
|
||||
override fun onChanged() {
|
||||
emitter.onNext(Unit)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -150,7 +150,7 @@ class SearchTable(context: Context, databaseHelper: SignalDatabase) : DatabaseTa
|
||||
WHERE
|
||||
${MessageTable.ID} >= $i AND
|
||||
${MessageTable.ID} < ${i + batchSize}
|
||||
""".trimIndent()
|
||||
"""
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -42,7 +42,7 @@ class StorySendTable(context: Context, databaseHelper: SignalDatabase) : Databas
|
||||
$ALLOWS_REPLIES INTEGER NOT NULL,
|
||||
$DISTRIBUTION_ID TEXT NOT NULL REFERENCES ${DistributionListTables.LIST_TABLE_NAME} (${DistributionListTables.DISTRIBUTION_ID}) ON DELETE CASCADE
|
||||
)
|
||||
""".trimIndent()
|
||||
"""
|
||||
|
||||
val CREATE_INDEXS = arrayOf(
|
||||
"CREATE INDEX story_sends_recipient_id_sent_timestamp_allows_replies_index ON $TABLE_NAME ($RECIPIENT_ID, $SENT_TIMESTAMP, $ALLOWS_REPLIES)",
|
||||
@@ -110,7 +110,7 @@ class StorySendTable(context: Context, databaseHelper: SignalDatabase) : Databas
|
||||
AND $MESSAGE_ID > $messageId
|
||||
AND $ALLOWS_REPLIES > ${allowsReplies.toInt()}
|
||||
)
|
||||
""".trimIndent()
|
||||
"""
|
||||
|
||||
readableDatabase.rawQuery(query, null).use { cursor ->
|
||||
while (cursor.moveToNext()) {
|
||||
@@ -146,7 +146,7 @@ class StorySendTable(context: Context, databaseHelper: SignalDatabase) : Databas
|
||||
WHERE ${MessageTable.REMOTE_DELETED} = 0
|
||||
)
|
||||
)
|
||||
""".trimIndent()
|
||||
"""
|
||||
|
||||
readableDatabase.rawQuery(query, null).use { cursor ->
|
||||
while (cursor.moveToNext()) {
|
||||
@@ -227,7 +227,7 @@ class StorySendTable(context: Context, databaseHelper: SignalDatabase) : Databas
|
||||
"""
|
||||
$SENT_TIMESTAMP = ? AND
|
||||
(SELECT ${MessageTable.REMOTE_DELETED} FROM ${MessageTable.TABLE_NAME} WHERE ${MessageTable.ID} = $MESSAGE_ID) = 0
|
||||
""".trimIndent(),
|
||||
""",
|
||||
sentTimestamp
|
||||
)
|
||||
.orderBy(MESSAGE_ID)
|
||||
@@ -272,7 +272,7 @@ class StorySendTable(context: Context, databaseHelper: SignalDatabase) : Databas
|
||||
FROM ${MessageTable.TABLE_NAME}
|
||||
INNER JOIN ${DistributionListTables.LIST_TABLE_NAME} ON ${DistributionListTables.LIST_TABLE_NAME}.${DistributionListTables.RECIPIENT_ID} = ${MessageTable.TABLE_NAME}.${MessageTable.TO_RECIPIENT_ID}
|
||||
WHERE ${MessageTable.DATE_SENT} = $sentTimestamp AND ${DistributionListTables.DISTRIBUTION_ID} IS NOT NULL
|
||||
""".trimIndent()
|
||||
"""
|
||||
|
||||
val distributionIdToMessageId = readableDatabase.query(query).use { cursor ->
|
||||
val results: MutableMap<DistributionId, Long> = mutableMapOf()
|
||||
@@ -351,7 +351,7 @@ class StorySendTable(context: Context, databaseHelper: SignalDatabase) : Databas
|
||||
FROM $TABLE_NAME
|
||||
INNER JOIN ${MessageTable.TABLE_NAME} ON ${MessageTable.TABLE_NAME}.${MessageTable.ID} = $TABLE_NAME.$MESSAGE_ID
|
||||
WHERE $TABLE_NAME.$SENT_TIMESTAMP = ?
|
||||
""".trimIndent(),
|
||||
""",
|
||||
arrayOf(sentTimestamp)
|
||||
).use { cursor ->
|
||||
val results: MutableMap<RecipientId, SentStorySyncManifest.Entry> = mutableMapOf()
|
||||
|
||||
@@ -23,7 +23,6 @@ import org.signal.core.util.requireInt
|
||||
import org.signal.core.util.requireLong
|
||||
import org.signal.core.util.requireString
|
||||
import org.signal.core.util.select
|
||||
import org.signal.core.util.toSingleLine
|
||||
import org.signal.core.util.update
|
||||
import org.signal.core.util.withinTransaction
|
||||
import org.signal.libsignal.zkgroup.InvalidInputException
|
||||
@@ -135,7 +134,7 @@ class ThreadTable(context: Context, databaseHelper: SignalDatabase) : DatabaseTa
|
||||
$PINNED INTEGER DEFAULT 0,
|
||||
$UNREAD_SELF_MENTION_COUNT INTEGER DEFAULT 0
|
||||
)
|
||||
""".trimIndent()
|
||||
"""
|
||||
|
||||
@JvmField
|
||||
val CREATE_INDEXS = arrayOf(
|
||||
@@ -607,7 +606,7 @@ class ThreadTable(context: Context, databaseHelper: SignalDatabase) : DatabaseTa
|
||||
$UNREAD_SELF_MENTION_COUNT = $UNREAD_SELF_MENTION_COUNT + ?,
|
||||
$LAST_SCROLLED = ?
|
||||
WHERE $ID = ?
|
||||
""".toSingleLine(),
|
||||
""",
|
||||
SqlUtil.buildArgs(unreadAmount, unreadSelfMentionAmount, 0, threadId)
|
||||
)
|
||||
}
|
||||
@@ -1683,7 +1682,7 @@ class ThreadTable(context: Context, databaseHelper: SignalDatabase) : DatabaseTa
|
||||
) as MembershipAlias ON MembershipAlias.${GroupTable.MembershipTable.GROUP_ID} = ${GroupTable.TABLE_NAME}.${GroupTable.GROUP_ID}
|
||||
WHERE $where
|
||||
ORDER BY $orderBy
|
||||
""".trimIndent()
|
||||
"""
|
||||
|
||||
if (limit > 0) {
|
||||
query += " LIMIT $limit"
|
||||
@@ -1693,7 +1692,7 @@ class ThreadTable(context: Context, databaseHelper: SignalDatabase) : DatabaseTa
|
||||
query += " OFFSET $offset"
|
||||
}
|
||||
|
||||
return query.toSingleLine()
|
||||
return query
|
||||
}
|
||||
|
||||
private fun isSilentType(type: Long): Boolean {
|
||||
|
||||
@@ -8,7 +8,6 @@ import org.signal.core.util.delete
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.signal.core.util.readToList
|
||||
import org.signal.core.util.requireLong
|
||||
import org.signal.core.util.toSingleLine
|
||||
import org.signal.core.util.update
|
||||
|
||||
/**
|
||||
@@ -58,7 +57,7 @@ object V166_ThreadAndMessageForeignKeys : SignalDatabaseMigration {
|
||||
COUNT(*) AS thread_count
|
||||
FROM thread
|
||||
GROUP BY thread_recipient_id HAVING thread_count > 1
|
||||
""".toSingleLine()
|
||||
"""
|
||||
).use { cursor ->
|
||||
while (cursor.moveToNext()) {
|
||||
val recipientId = cursor.requireLong("thread_recipient_id")
|
||||
|
||||
@@ -7,7 +7,6 @@ import org.signal.core.util.delete
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.signal.core.util.readToList
|
||||
import org.signal.core.util.requireLong
|
||||
import org.signal.core.util.toSingleLine
|
||||
import org.signal.core.util.update
|
||||
|
||||
/**
|
||||
@@ -38,7 +37,7 @@ object V171_ThreadForeignKeyFix : SignalDatabaseMigration {
|
||||
COUNT(*) AS thread_count
|
||||
FROM thread
|
||||
GROUP BY recipient_id HAVING thread_count > 1
|
||||
""".toSingleLine()
|
||||
"""
|
||||
).use { cursor ->
|
||||
while (cursor.moveToNext()) {
|
||||
val recipientId = cursor.requireLong("recipient_id")
|
||||
|
||||
@@ -12,7 +12,6 @@ import org.signal.core.util.readToSingleObject
|
||||
import org.signal.core.util.requireLong
|
||||
import org.signal.core.util.requireNonNullString
|
||||
import org.signal.core.util.requireString
|
||||
import org.signal.core.util.toSingleLine
|
||||
import org.thoughtcrime.securesms.database.KeyValueDatabase
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
@@ -182,7 +181,7 @@ object V185_MessageRecipientsMigration : SignalDatabaseMigration {
|
||||
from_recipient_id = ${selfId.toLong()},
|
||||
from_device_id = 1
|
||||
WHERE $outgoingClause
|
||||
""".toSingleLine()
|
||||
"""
|
||||
)
|
||||
}
|
||||
stopwatch.split("update-data")
|
||||
|
||||
@@ -33,7 +33,7 @@ class AvatarPickerDatabase(context: Context, databaseHelper: SignalDatabase) : D
|
||||
$GROUP_ID TEXT DEFAULT NULL,
|
||||
$AVATAR BLOB NOT NULL
|
||||
)
|
||||
""".trimIndent()
|
||||
"""
|
||||
}
|
||||
|
||||
fun saveAvatarForSelf(avatar: Avatar): Avatar {
|
||||
|
||||
@@ -19,6 +19,8 @@ import androidx.navigation.Navigator
|
||||
import androidx.navigation.findNavController
|
||||
import androidx.navigation.fragment.FragmentNavigatorExtras
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import io.reactivex.rxjava3.kotlin.subscribeBy
|
||||
import org.signal.core.util.concurrent.LifecycleDisposable
|
||||
import org.signal.core.util.concurrent.SimpleTask
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.thoughtcrime.securesms.MainActivity
|
||||
@@ -56,6 +58,7 @@ class MainActivityListHostFragment : Fragment(R.layout.main_activity_list_host_f
|
||||
}
|
||||
|
||||
private val conversationListTabsViewModel: ConversationListTabsViewModel by viewModels(ownerProducer = { requireActivity() })
|
||||
private val disposables: LifecycleDisposable = LifecycleDisposable()
|
||||
|
||||
private lateinit var _toolbarBackground: View
|
||||
private lateinit var _toolbar: Toolbar
|
||||
@@ -77,6 +80,8 @@ class MainActivityListHostFragment : Fragment(R.layout.main_activity_list_host_f
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
disposables.bindTo(viewLifecycleOwner)
|
||||
|
||||
_toolbarBackground = view.findViewById(R.id.toolbar_background)
|
||||
_toolbar = view.findViewById(R.id.toolbar)
|
||||
_basicToolbar = Stub(view.findViewById(R.id.toolbar_basic_stub))
|
||||
@@ -93,7 +98,7 @@ class MainActivityListHostFragment : Fragment(R.layout.main_activity_list_host_f
|
||||
|
||||
(requireActivity() as AppCompatActivity).setSupportActionBar(_toolbar)
|
||||
|
||||
conversationListTabsViewModel.state.observe(viewLifecycleOwner) { state ->
|
||||
disposables += conversationListTabsViewModel.state.subscribeBy { state ->
|
||||
val controller: NavController = requireView().findViewById<View>(R.id.fragment_container).findNavController()
|
||||
when (controller.currentDestination?.id) {
|
||||
R.id.conversationListFragment -> goToStateFromConversationList(state, controller)
|
||||
|
||||
@@ -17,6 +17,9 @@ import org.thoughtcrime.securesms.mms.GlideRequest;
|
||||
*/
|
||||
public class Megaphone {
|
||||
|
||||
@SuppressWarnings("ConstantConditions")
|
||||
public static final Megaphone NONE = new Megaphone.Builder(null, null).build();
|
||||
|
||||
private final Event event;
|
||||
private final Style style;
|
||||
private final boolean canSnooze;
|
||||
|
||||
@@ -8,6 +8,7 @@ import org.signal.core.util.concurrent.SignalExecutors
|
||||
import org.thoughtcrime.securesms.conversation.ConversationMessage
|
||||
import org.thoughtcrime.securesms.database.DatabaseObserver
|
||||
import org.thoughtcrime.securesms.database.MessageTable
|
||||
import org.thoughtcrime.securesms.database.RxDatabaseObserver
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||
import org.thoughtcrime.securesms.database.model.DistributionListId
|
||||
import org.thoughtcrime.securesms.database.model.MessageRecord
|
||||
@@ -34,8 +35,10 @@ class StoriesLandingRepository(context: Context) {
|
||||
|
||||
@Suppress("UsePropertyAccessSyntax")
|
||||
fun getStories(): Observable<List<StoriesLandingItemData>> {
|
||||
val storyRecipients: Observable<Map<Recipient, List<StoryResult>>> = Observable.create { emitter ->
|
||||
fun refresh() {
|
||||
val storyRecipients: Observable<Map<Recipient, List<StoryResult>>> = RxDatabaseObserver
|
||||
.conversationList
|
||||
.toObservable()
|
||||
.map {
|
||||
val myStoriesId = SignalDatabase.recipients.getOrInsertFromDistributionListId(DistributionListId.MY_STORY)
|
||||
val myStories = Recipient.resolved(myStoriesId)
|
||||
|
||||
@@ -55,21 +58,9 @@ class StoriesLandingRepository(context: Context) {
|
||||
}
|
||||
}
|
||||
|
||||
emitter.onNext(mapping)
|
||||
mapping
|
||||
}
|
||||
|
||||
val observer = DatabaseObserver.Observer {
|
||||
refresh()
|
||||
}
|
||||
|
||||
ApplicationDependencies.getDatabaseObserver().registerConversationListObserver(observer)
|
||||
emitter.setCancellable {
|
||||
ApplicationDependencies.getDatabaseObserver().unregisterObserver(observer)
|
||||
}
|
||||
|
||||
refresh()
|
||||
}
|
||||
|
||||
return storyRecipients.switchMap { map ->
|
||||
val observables = map.map { (recipient, results) ->
|
||||
val messages = results
|
||||
|
||||
@@ -5,10 +5,9 @@ import io.reactivex.rxjava3.core.Completable
|
||||
import io.reactivex.rxjava3.core.Observable
|
||||
import io.reactivex.rxjava3.schedulers.Schedulers
|
||||
import org.thoughtcrime.securesms.conversation.ConversationMessage
|
||||
import org.thoughtcrime.securesms.database.DatabaseObserver
|
||||
import org.thoughtcrime.securesms.database.RxDatabaseObserver
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||
import org.thoughtcrime.securesms.database.model.MessageRecord
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.sms.MessageSender
|
||||
|
||||
@@ -23,8 +22,10 @@ class MyStoriesRepository(context: Context) {
|
||||
}
|
||||
|
||||
fun getMyStories(): Observable<List<MyStoriesState.DistributionSet>> {
|
||||
return Observable.create { emitter ->
|
||||
fun refresh() {
|
||||
return RxDatabaseObserver
|
||||
.conversationList
|
||||
.toObservable()
|
||||
.map {
|
||||
val storiesMap = mutableMapOf<Recipient, List<MessageRecord>>()
|
||||
SignalDatabase.messages.getAllOutgoingStories(true, -1).use {
|
||||
for (messageRecord in it) {
|
||||
@@ -33,20 +34,8 @@ class MyStoriesRepository(context: Context) {
|
||||
}
|
||||
}
|
||||
|
||||
emitter.onNext(storiesMap.toSortedMap(MyStoryBiasComparator()).map { (r, m) -> createDistributionSet(r, m) })
|
||||
storiesMap.toSortedMap(MyStoryBiasComparator()).map { (r, m) -> createDistributionSet(r, m) }
|
||||
}
|
||||
|
||||
val observer = DatabaseObserver.Observer {
|
||||
refresh()
|
||||
}
|
||||
|
||||
ApplicationDependencies.getDatabaseObserver().registerConversationListObserver(observer)
|
||||
emitter.setCancellable {
|
||||
ApplicationDependencies.getDatabaseObserver().unregisterObserver(observer)
|
||||
}
|
||||
|
||||
refresh()
|
||||
}
|
||||
}
|
||||
|
||||
private fun createDistributionSet(recipient: Recipient, messageRecords: List<MessageRecord>): MyStoriesState.DistributionSet {
|
||||
|
||||
@@ -1,81 +1,33 @@
|
||||
package org.thoughtcrime.securesms.stories.tabs
|
||||
|
||||
import io.reactivex.rxjava3.core.Observable
|
||||
import io.reactivex.rxjava3.schedulers.Schedulers
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.thoughtcrime.securesms.database.DatabaseObserver
|
||||
import io.reactivex.rxjava3.core.Flowable
|
||||
import org.thoughtcrime.securesms.database.RxDatabaseObserver
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
|
||||
class ConversationListTabRepository {
|
||||
|
||||
companion object {
|
||||
private val TAG = Log.tag(ConversationListTabRepository::class.java)
|
||||
fun getNumberOfUnreadMessages(): Flowable<Long> {
|
||||
return RxDatabaseObserver.conversationList.map { SignalDatabase.threads.getUnreadMessageCount() }
|
||||
}
|
||||
|
||||
fun getNumberOfUnreadMessages(): Observable<Long> {
|
||||
return Observable.create<Long> {
|
||||
fun refresh() {
|
||||
it.onNext(SignalDatabase.threads.getUnreadMessageCount())
|
||||
}
|
||||
|
||||
val listener = DatabaseObserver.Observer {
|
||||
refresh()
|
||||
}
|
||||
|
||||
ApplicationDependencies.getDatabaseObserver().registerConversationListObserver(listener)
|
||||
it.setCancellable { ApplicationDependencies.getDatabaseObserver().unregisterObserver(listener) }
|
||||
|
||||
refresh()
|
||||
}.subscribeOn(Schedulers.io())
|
||||
}
|
||||
|
||||
fun getNumberOfUnseenStories(): Observable<Long> {
|
||||
return Observable.create<Long> { emitter ->
|
||||
fun refresh() {
|
||||
emitter.onNext(SignalDatabase.messages.getUnreadStoryThreadRecipientIds().map { Recipient.resolved(it) }.filterNot { it.shouldHideStory() }.size.toLong())
|
||||
}
|
||||
|
||||
val listener = DatabaseObserver.Observer {
|
||||
refresh()
|
||||
}
|
||||
|
||||
ApplicationDependencies.getDatabaseObserver().registerConversationListObserver(listener)
|
||||
emitter.setCancellable { ApplicationDependencies.getDatabaseObserver().unregisterObserver(listener) }
|
||||
refresh()
|
||||
}.subscribeOn(Schedulers.io())
|
||||
}
|
||||
|
||||
fun getHasFailedOutgoingStories(): Observable<Boolean> {
|
||||
return Observable.create<Boolean> { emitter ->
|
||||
fun refresh() {
|
||||
emitter.onNext(SignalDatabase.messages.hasFailedOutgoingStory())
|
||||
}
|
||||
|
||||
val listener = DatabaseObserver.Observer {
|
||||
refresh()
|
||||
}
|
||||
|
||||
ApplicationDependencies.getDatabaseObserver().registerConversationListObserver(listener)
|
||||
emitter.setCancellable { ApplicationDependencies.getDatabaseObserver().unregisterObserver(listener) }
|
||||
refresh()
|
||||
}.subscribeOn(Schedulers.io())
|
||||
}
|
||||
|
||||
fun getNumberOfUnseenCalls(): Observable<Long> {
|
||||
return Observable.create { emitter ->
|
||||
fun refresh() {
|
||||
emitter.onNext(SignalDatabase.messages.getUnreadMisedCallCount())
|
||||
}
|
||||
|
||||
val listener = DatabaseObserver.Observer {
|
||||
refresh()
|
||||
}
|
||||
|
||||
ApplicationDependencies.getDatabaseObserver().registerConversationListObserver(listener)
|
||||
emitter.setCancellable { ApplicationDependencies.getDatabaseObserver().unregisterObserver(listener) }
|
||||
refresh()
|
||||
fun getNumberOfUnseenStories(): Flowable<Long> {
|
||||
return RxDatabaseObserver.conversationList.map {
|
||||
SignalDatabase
|
||||
.messages
|
||||
.getUnreadStoryThreadRecipientIds()
|
||||
.map { Recipient.resolved(it) }
|
||||
.filterNot { it.shouldHideStory() }
|
||||
.size
|
||||
.toLong()
|
||||
}
|
||||
}
|
||||
|
||||
fun getHasFailedOutgoingStories(): Flowable<Boolean> {
|
||||
return RxDatabaseObserver.conversationList.map { SignalDatabase.messages.hasFailedOutgoingStory() }
|
||||
}
|
||||
|
||||
fun getNumberOfUnseenCalls(): Flowable<Long> {
|
||||
return RxDatabaseObserver.conversationList.map { SignalDatabase.messages.getUnreadMisedCallCount() }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,7 +14,9 @@ import androidx.fragment.app.viewModels
|
||||
import com.airbnb.lottie.LottieAnimationView
|
||||
import com.airbnb.lottie.LottieProperty
|
||||
import com.airbnb.lottie.model.KeyPath
|
||||
import io.reactivex.rxjava3.kotlin.subscribeBy
|
||||
import org.signal.core.util.DimensionUnit
|
||||
import org.signal.core.util.concurrent.LifecycleDisposable
|
||||
import org.signal.core.util.dp
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.components.ViewBinderDelegate
|
||||
@@ -23,7 +25,6 @@ import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.stories.Stories
|
||||
import org.thoughtcrime.securesms.util.FeatureFlags
|
||||
import org.thoughtcrime.securesms.util.visible
|
||||
import java.text.NumberFormat
|
||||
|
||||
/**
|
||||
* Displays the "Chats" and "Stories" tab to a user.
|
||||
@@ -31,6 +32,7 @@ import java.text.NumberFormat
|
||||
class ConversationListTabsFragment : Fragment(R.layout.conversation_list_tabs) {
|
||||
|
||||
private val viewModel: ConversationListTabsViewModel by viewModels(ownerProducer = { requireActivity() })
|
||||
private val disposables: LifecycleDisposable = LifecycleDisposable()
|
||||
private val binding by ViewBinderDelegate(ConversationListTabsBinding::bind)
|
||||
private var shouldBeImmediate = true
|
||||
private var pillAnimator: Animator? = null
|
||||
@@ -39,6 +41,8 @@ class ConversationListTabsFragment : Fragment(R.layout.conversation_list_tabs) {
|
||||
private val smallConstraintSet: ConstraintSet = ConstraintSet()
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
disposables.bindTo(viewLifecycleOwner)
|
||||
|
||||
val iconTint = ContextCompat.getColor(requireContext(), R.color.signal_colorOnSecondaryContainer)
|
||||
|
||||
largeConstraintSet.clone(binding.root)
|
||||
@@ -73,7 +77,7 @@ class ConversationListTabsFragment : Fragment(R.layout.conversation_list_tabs) {
|
||||
|
||||
updateTabsVisibility()
|
||||
|
||||
viewModel.state.observe(viewLifecycleOwner) {
|
||||
disposables += viewModel.state.subscribeBy {
|
||||
update(it, shouldBeImmediate)
|
||||
shouldBeImmediate = false
|
||||
}
|
||||
@@ -245,6 +249,6 @@ class ConversationListTabsFragment : Fragment(R.layout.conversation_list_tabs) {
|
||||
if (count > 99L) {
|
||||
return getString(R.string.ConversationListTabs__99p)
|
||||
}
|
||||
return NumberFormat.getInstance().format(count)
|
||||
return count.toString()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,44 +1,44 @@
|
||||
package org.thoughtcrime.securesms.stories.tabs
|
||||
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.Transformations
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
||||
import io.reactivex.rxjava3.core.Flowable
|
||||
import io.reactivex.rxjava3.core.Observable
|
||||
import io.reactivex.rxjava3.disposables.CompositeDisposable
|
||||
import io.reactivex.rxjava3.kotlin.plusAssign
|
||||
import io.reactivex.rxjava3.subjects.PublishSubject
|
||||
import io.reactivex.rxjava3.subjects.Subject
|
||||
import org.thoughtcrime.securesms.stories.Stories
|
||||
import org.thoughtcrime.securesms.util.livedata.Store
|
||||
import org.thoughtcrime.securesms.util.rx.RxStore
|
||||
|
||||
class ConversationListTabsViewModel(repository: ConversationListTabRepository) : ViewModel() {
|
||||
private val store = Store(ConversationListTabsState())
|
||||
private val store = RxStore(ConversationListTabsState())
|
||||
|
||||
val stateSnapshot: ConversationListTabsState
|
||||
get() = store.state
|
||||
|
||||
val state: LiveData<ConversationListTabsState> = Transformations.distinctUntilChanged(store.stateLiveData)
|
||||
val state: Flowable<ConversationListTabsState> = store.stateFlowable.distinctUntilChanged().observeOn(AndroidSchedulers.mainThread())
|
||||
val disposables = CompositeDisposable()
|
||||
|
||||
private val internalTabClickEvents: Subject<ConversationListTab> = PublishSubject.create()
|
||||
val tabClickEvents: Observable<ConversationListTab> = internalTabClickEvents.filter { Stories.isFeatureEnabled() }
|
||||
|
||||
init {
|
||||
disposables += repository.getNumberOfUnreadMessages().subscribe { unreadChats ->
|
||||
store.update { it.copy(unreadMessagesCount = unreadChats) }
|
||||
disposables += store.update(repository.getNumberOfUnreadMessages()) { unreadChats, state ->
|
||||
state.copy(unreadMessagesCount = unreadChats)
|
||||
}
|
||||
|
||||
disposables += repository.getNumberOfUnseenCalls().subscribe { unseenCalls ->
|
||||
store.update { it.copy(unreadCallsCount = unseenCalls) }
|
||||
disposables += store.update(repository.getNumberOfUnseenCalls()) { unseenCalls, state ->
|
||||
state.copy(unreadCallsCount = unseenCalls)
|
||||
}
|
||||
|
||||
disposables += repository.getNumberOfUnseenStories().subscribe { unseenStories ->
|
||||
store.update { it.copy(unreadStoriesCount = unseenStories) }
|
||||
disposables += store.update(repository.getNumberOfUnseenStories()) { unseenStories, state ->
|
||||
state.copy(unreadStoriesCount = unseenStories)
|
||||
}
|
||||
|
||||
disposables += repository.getHasFailedOutgoingStories().subscribe { hasFailedStories ->
|
||||
store.update { it.copy(hasFailedStory = hasFailedStories) }
|
||||
disposables += store.update(repository.getHasFailedOutgoingStories()) { hasFailedStories, state ->
|
||||
state.copy(hasFailedStory = hasFailedStories)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
package org.thoughtcrime.securesms.util.rx
|
||||
|
||||
import androidx.annotation.CheckResult
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
||||
import io.reactivex.rxjava3.core.Flowable
|
||||
import io.reactivex.rxjava3.core.Scheduler
|
||||
import io.reactivex.rxjava3.disposables.CompositeDisposable
|
||||
import io.reactivex.rxjava3.disposables.Disposable
|
||||
import io.reactivex.rxjava3.kotlin.plusAssign
|
||||
import io.reactivex.rxjava3.processors.BehaviorProcessor
|
||||
import io.reactivex.rxjava3.schedulers.Schedulers
|
||||
import io.reactivex.rxjava3.subjects.PublishSubject
|
||||
@@ -43,6 +44,18 @@ class RxStore<T : Any>(
|
||||
}
|
||||
}
|
||||
|
||||
fun addTo(disposable: CompositeDisposable): RxStore<T> {
|
||||
disposable.add(this)
|
||||
return this
|
||||
}
|
||||
|
||||
fun <R : Any> mapDistinctForUi(map: (T) -> R): Flowable<R> {
|
||||
return stateFlowable
|
||||
.map(map)
|
||||
.distinctUntilChanged()
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispose of the underlying scan chain. This is terminal.
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user