Improve performance from thread being updated to data available to render.

This commit is contained in:
Cody Henthorne
2023-04-14 16:28:56 -04:00
parent 9d17bf473c
commit 4f06a0d27c
39 changed files with 497 additions and 815 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -19,6 +19,6 @@ class CallLinkTable(context: Context, databaseHelper: SignalDatabase) : Database
CREATE TABLE $TABLE_NAME (
$ID INTEGER PRIMARY KEY
)
""".trimIndent()
"""
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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 = ?"

View File

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

View File

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

View File

@@ -150,7 +150,7 @@ class SearchTable(context: Context, databaseHelper: SignalDatabase) : DatabaseTa
WHERE
${MessageTable.ID} >= $i AND
${MessageTable.ID} < ${i + batchSize}
""".trimIndent()
"""
)
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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