diff --git a/app/src/androidTest/java/org/thoughtcrime/securesms/messages/MessageContentProcessorTestV2.kt b/app/src/androidTest/java/org/thoughtcrime/securesms/messages/MessageContentProcessorTestV2.kt index 17c6d35d7f..6a4f014b1e 100644 --- a/app/src/androidTest/java/org/thoughtcrime/securesms/messages/MessageContentProcessorTestV2.kt +++ b/app/src/androidTest/java/org/thoughtcrime/securesms/messages/MessageContentProcessorTestV2.kt @@ -10,7 +10,6 @@ import org.junit.runner.RunWith import org.signal.core.util.ThreadUtil import org.signal.core.util.readToList import org.signal.core.util.select -import org.signal.core.util.toSingleLine import org.signal.core.util.withinTransaction import org.thoughtcrime.securesms.database.AttachmentTable import org.thoughtcrime.securesms.database.MessageTable @@ -370,6 +369,6 @@ class MessageContentProcessorTestV2 { isPaymentsNotificaiton:${type and MessageTypes.SPECIAL_TYPES_MASK == MessageTypes.SPECIAL_TYPE_PAYMENTS_NOTIFICATION} isRequestToActivatePayments:${type and MessageTypes.SPECIAL_TYPES_MASK == MessageTypes.SPECIAL_TYPE_PAYMENTS_ACTIVATE_REQUEST} isPaymentsActivated:${type and MessageTypes.SPECIAL_TYPES_MASK == MessageTypes.SPECIAL_TYPE_PAYMENTS_ACTIVATED} - """.trimIndent().replace(Regex("is[A-Z][A-Za-z0-9]*:false\n?"), "").toSingleLine() + """.trimIndent().replace(Regex("is[A-Z][A-Za-z0-9]*:false\n?"), "").replace("\n", "") } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/backup/BackupCountQueries.kt b/app/src/main/java/org/thoughtcrime/securesms/backup/BackupCountQueries.kt index e9d5a3dde6..8bbba04bab 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/backup/BackupCountQueries.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/backup/BackupCountQueries.kt @@ -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() + """ } diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/UnreadPaymentsView.java b/app/src/main/java/org/thoughtcrime/securesms/components/UnreadPaymentsView.java deleted file mode 100644 index 2d793e4eb3..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/components/UnreadPaymentsView.java +++ /dev/null @@ -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(); - } -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListFragment.java b/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListFragment.java index c482eff6b6..646f7ed055 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListFragment.java +++ b/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListFragment.java @@ -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; - private Stub 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; + private PulsingFloatingActionButton fab; + private PulsingFloatingActionButton cameraFab; + private ConversationListFilterPullView pullView; + private AppBarLayout pullViewAppBarLayout; + private ConversationListViewModel viewModel; + private RecyclerView.Adapter activeAdapter; private ConversationListAdapter defaultAdapter; private PagingMappingAdapter searchAdapter; - private Stub megaphoneContainer; - private SnapToTopDataObserver snapToTopDataObserver; - private Drawable archiveDrawable; - private AppForegroundObserver.Listener appForegroundObserver; - private VoiceNoteMediaControllerOwner mediaControllerOwner; - private Stub voiceNotePlayerViewStub; - private VoiceNotePlayerView voiceNotePlayerView; - private SignalBottomActionBar bottomActionBar; - private SignalContextMenu activeContextMenu; - private LifecycleDisposable lifecycleDisposable; + private Stub megaphoneContainer; + private SnapToTopDataObserver snapToTopDataObserver; + private Drawable archiveDrawable; + private AppForegroundObserver.Listener appForegroundObserver; + private VoiceNoteMediaControllerOwner mediaControllerOwner; + private Stub 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) { - 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; diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListViewModel.java b/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListViewModel.java deleted file mode 100644 index 0b8ff5bc32..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListViewModel.java +++ /dev/null @@ -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; - private final MutableLiveData selectedConversations; - private final MutableLiveData conversationFilterRequest; - private final LiveData conversationListDataSource; - private final Set internalSelection; - private final LiveData> pagedData; - private final LiveData 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 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 hasNoConversations() { - return hasNoConversations; - } - - @NonNull LiveData getMegaphone() { - return megaphone; - } - - @NonNull LiveData> getConversationList() { - return Transformations.switchMap(pagedData, LivePagedData::getData); - } - - @NonNull LiveData> getPagingController() { - return Transformations.map(pagedData, LivePagedData::getController); - } - - @NonNull LiveData> getNotificationProfiles() { - final Observable> activeProfile = Observable.combineLatest(Observable.interval(0, 30, TimeUnit.SECONDS), notificationProfilesRepository.getProfiles(), (interval, profiles) -> profiles); - - return LiveDataReactiveStreams.fromPublisher(activeProfile.toFlowable(BackpressureStrategy.LATEST)); - } - - @NonNull LiveData getPipeState() { - return LiveDataReactiveStreams.fromPublisher(ApplicationDependencies.getSignalWebSocket().getWebSocketState().toFlowable(BackpressureStrategy.LATEST)); - } - - @NonNull LiveData> getUnreadPaymentsLiveData() { - return unreadPaymentsLiveData; - } - - @NonNull LiveData getConversationFilterRequest() { - return conversationFilterRequest; - } - - public int getPinnedCount() { - return pinnedCount; - } - - void onVisible() { - megaphoneRepository.getNextMegaphone(megaphone::postValue); - - if (!coldStart) { - ApplicationDependencies.getDatabaseObserver().notifyConversationListListeners(); - } - - coldStart = false; - } - - @NonNull Set currentSelectedConversations() { - return internalSelection; - } - - @NonNull LiveData getSelectedConversations() { - return selectedConversations; - } - - void startSelection(@NonNull Conversation conversation) { - setSelection(Collections.singleton(conversation)); - } - - void endSelection() { - setSelection(Collections.emptySet()); - } - - void toggleConversationSelected(@NonNull Conversation conversation) { - Set 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 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 create(@NonNull Class modelClass) { - //noinspection ConstantConditions - return modelClass.cast(new ConversationListViewModel(isArchived)); - } - } -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListViewModel.kt new file mode 100644 index 0000000000..93e867f73f --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListViewModel.kt @@ -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 + private val pagingConfig = PagingConfig.Builder() + .setPageSize(15) + .setBufferPages(2) + .build() + + val conversationsState: Flowable> = store.mapDistinctForUi { it.conversations } + val megaphoneState: Flowable = store.mapDistinctForUi { it.megaphone } + val selectedState: Flowable = store.mapDistinctForUi { it.selectedConversations } + val filterRequestState: Flowable = store.mapDistinctForUi { it.filterRequest } + val hasNoConversations: Flowable + + val controller = ProxyPagingController() + + val conversationFilterRequest: ConversationFilterRequest + get() = store.state.filterRequest + val megaphone: Megaphone + get() = store.state.megaphone + val pinnedCount: Int + get() = store.state.pinnedCount + val webSocketState: Observable + get() = ApplicationDependencies.getSignalWebSocket().webSocketState + + @get:JvmName("currentSelectedConversations") + val currentSelectedConversations: Set + 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 = 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> { + return Observable + .combineLatest( + Observable.interval(0, 30, TimeUnit.SECONDS), + notificationProfilesRepository.getProfiles() + ) { _, profiles -> profiles } + .distinctUntilChanged() + } + + private fun setSelection(newSelection: Collection) { + store.update { + val selection = newSelection.toSet() + it.copy(internalSelection = selection, selectedConversations = ConversationSet(selection)) + } + } + + private data class ConversationListState( + val conversations: List = emptyList(), + val megaphone: Megaphone = Megaphone.NONE, + val selectedConversations: ConversationSet = ConversationSet(), + val internalSelection: Set = emptySet(), + val filterRequest: ConversationFilterRequest = ConversationFilterRequest(ConversationFilter.OFF, ConversationFilterSource.DRAG), + val pinnedCount: Int = 0 + ) + + class Factory(private val isArchived: Boolean) : ViewModelProvider.Factory { + override fun create(modelClass: Class): T { + return modelClass.cast(ConversationListViewModel(isArchived))!! + } + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/CallLinkTable.kt b/app/src/main/java/org/thoughtcrime/securesms/database/CallLinkTable.kt index 76678b631d..3493a4ec31 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/CallLinkTable.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/CallLinkTable.kt @@ -19,6 +19,6 @@ class CallLinkTable(context: Context, databaseHelper: SignalDatabase) : Database CREATE TABLE $TABLE_NAME ( $ID INTEGER PRIMARY KEY ) - """.trimIndent() + """ } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/CallTable.kt b/app/src/main/java/org/thoughtcrime/securesms/database/CallTable.kt index 393f2faac3..128d9f7903 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/CallTable.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/CallTable.kt @@ -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, diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/ChatColorsTable.kt b/app/src/main/java/org/thoughtcrime/securesms/database/ChatColorsTable.kt index 35ce273fa7..afa27e8ad9 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/ChatColorsTable.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/ChatColorsTable.kt @@ -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 { diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/DonationReceiptTable.kt b/app/src/main/java/org/thoughtcrime/securesms/database/DonationReceiptTable.kt index 5350896d7d..3f478d725d 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/DonationReceiptTable.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/DonationReceiptTable.kt @@ -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)", diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/GroupTable.kt b/app/src/main/java/org/thoughtcrime/securesms/database/GroupTable.kt index f784c090db..d5b9a373a2 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/GroupTable.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/GroupTable.kt @@ -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) diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/LocalMetricsDatabase.kt b/app/src/main/java/org/thoughtcrime/securesms/database/LocalMetricsDatabase.kt index fbbb620d49..e03ef71a00 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/LocalMetricsDatabase.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/LocalMetricsDatabase.kt @@ -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()) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/LogDatabase.kt b/app/src/main/java/org/thoughtcrime/securesms/database/LogDatabase.kt index 48e9d63067..616df73854 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/LogDatabase.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/LogDatabase.kt @@ -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)", diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/MediaTable.kt b/app/src/main/java/org/thoughtcrime/securesms/database/MediaTable.kt index 687f9c4bcf..5aa7d698b1 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/MediaTable.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/MediaTable.kt @@ -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 diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/MessageTable.kt b/app/src/main/java/org/thoughtcrime/securesms/database/MessageTable.kt index 7d06528cdd..58331533fd 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/MessageTable.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/MessageTable.kt @@ -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 = 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 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 { - 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 { - 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?): List { @@ -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 = 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) diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/NotificationProfileTables.kt b/app/src/main/java/org/thoughtcrime/securesms/database/NotificationProfileTables.kt index 9c66635b99..136a3989c1 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/NotificationProfileTables.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/NotificationProfileTables.kt @@ -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)" } diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/ReactionTable.kt b/app/src/main/java/org/thoughtcrime/securesms/database/ReactionTable.kt index b8ae1890d4..678d719bd8 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/ReactionTable.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/ReactionTable.kt @@ -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( diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/RecipientTable.kt b/app/src/main/java/org/thoughtcrime/securesms/database/RecipientTable.kt index 76a5b79808..f93b0261b8 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/RecipientTable.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/RecipientTable.kt @@ -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 = ArrayList() val columns: Array = 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 = 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 = ?" diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/RemoteMegaphoneTable.kt b/app/src/main/java/org/thoughtcrime/securesms/database/RemoteMegaphoneTable.kt index c350cf1436..6f9a2f7897 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/RemoteMegaphoneTable.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/RemoteMegaphoneTable.kt @@ -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 } diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/RxDatabaseObserver.kt b/app/src/main/java/org/thoughtcrime/securesms/database/RxDatabaseObserver.kt new file mode 100644 index 0000000000..4537f74e36 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/database/RxDatabaseObserver.kt @@ -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 by lazy { conversationListFlowable() } + + private fun conversationListFlowable(): Flowable { + 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) : DatabaseObserver.Observer { + fun prime() { + emitter.onNext(Unit) + } + + override fun onChanged() { + emitter.onNext(Unit) + } + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/SearchTable.kt b/app/src/main/java/org/thoughtcrime/securesms/database/SearchTable.kt index 4eb68124f8..9ab53750b0 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/SearchTable.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/SearchTable.kt @@ -150,7 +150,7 @@ class SearchTable(context: Context, databaseHelper: SignalDatabase) : DatabaseTa WHERE ${MessageTable.ID} >= $i AND ${MessageTable.ID} < ${i + batchSize} - """.trimIndent() + """ ) } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/StorySendTable.kt b/app/src/main/java/org/thoughtcrime/securesms/database/StorySendTable.kt index b15f6450f3..9bb2054a3e 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/StorySendTable.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/StorySendTable.kt @@ -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 = 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 = mutableMapOf() diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/ThreadTable.kt b/app/src/main/java/org/thoughtcrime/securesms/database/ThreadTable.kt index 3c3a75abe9..f27ef3c58c 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/ThreadTable.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/ThreadTable.kt @@ -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 { diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/helpers/migration/V166_ThreadAndMessageForeignKeys.kt b/app/src/main/java/org/thoughtcrime/securesms/database/helpers/migration/V166_ThreadAndMessageForeignKeys.kt index 54b2bf1a23..dc38e56ecf 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/helpers/migration/V166_ThreadAndMessageForeignKeys.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/helpers/migration/V166_ThreadAndMessageForeignKeys.kt @@ -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") diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/helpers/migration/V171_ThreadForeignKeyFix.kt b/app/src/main/java/org/thoughtcrime/securesms/database/helpers/migration/V171_ThreadForeignKeyFix.kt index 265bb558b9..9ca79cfe2b 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/helpers/migration/V171_ThreadForeignKeyFix.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/helpers/migration/V171_ThreadForeignKeyFix.kt @@ -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") diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/helpers/migration/V185_MessageRecipientsMigration.kt b/app/src/main/java/org/thoughtcrime/securesms/database/helpers/migration/V185_MessageRecipientsMigration.kt index 981a573419..3d5941df31 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/helpers/migration/V185_MessageRecipientsMigration.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/helpers/migration/V185_MessageRecipientsMigration.kt @@ -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") diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/model/AvatarPickerDatabase.kt b/app/src/main/java/org/thoughtcrime/securesms/database/model/AvatarPickerDatabase.kt index 8b47d9d335..e42ccca622 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/model/AvatarPickerDatabase.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/model/AvatarPickerDatabase.kt @@ -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 { diff --git a/app/src/main/java/org/thoughtcrime/securesms/main/MainActivityListHostFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/main/MainActivityListHostFragment.kt index e8bf262be3..a945257f6a 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/main/MainActivityListHostFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/main/MainActivityListHostFragment.kt @@ -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(R.id.fragment_container).findNavController() when (controller.currentDestination?.id) { R.id.conversationListFragment -> goToStateFromConversationList(state, controller) diff --git a/app/src/main/java/org/thoughtcrime/securesms/megaphone/Megaphone.java b/app/src/main/java/org/thoughtcrime/securesms/megaphone/Megaphone.java index 16fb641edb..5ba3913005 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/megaphone/Megaphone.java +++ b/app/src/main/java/org/thoughtcrime/securesms/megaphone/Megaphone.java @@ -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; diff --git a/app/src/main/java/org/thoughtcrime/securesms/stories/landing/StoriesLandingRepository.kt b/app/src/main/java/org/thoughtcrime/securesms/stories/landing/StoriesLandingRepository.kt index 8ac6fa17a8..fd4f1cb91b 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/stories/landing/StoriesLandingRepository.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/stories/landing/StoriesLandingRepository.kt @@ -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> { - val storyRecipients: Observable>> = Observable.create { emitter -> - fun refresh() { + val storyRecipients: Observable>> = 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 diff --git a/app/src/main/java/org/thoughtcrime/securesms/stories/my/MyStoriesRepository.kt b/app/src/main/java/org/thoughtcrime/securesms/stories/my/MyStoriesRepository.kt index 283c3efb9d..19196eb5df 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/stories/my/MyStoriesRepository.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/stories/my/MyStoriesRepository.kt @@ -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> { - return Observable.create { emitter -> - fun refresh() { + return RxDatabaseObserver + .conversationList + .toObservable() + .map { val storiesMap = mutableMapOf>() 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): MyStoriesState.DistributionSet { diff --git a/app/src/main/java/org/thoughtcrime/securesms/stories/tabs/ConversationListTabRepository.kt b/app/src/main/java/org/thoughtcrime/securesms/stories/tabs/ConversationListTabRepository.kt index 6a7309e099..e0ea88505e 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/stories/tabs/ConversationListTabRepository.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/stories/tabs/ConversationListTabRepository.kt @@ -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 { + return RxDatabaseObserver.conversationList.map { SignalDatabase.threads.getUnreadMessageCount() } } - fun getNumberOfUnreadMessages(): Observable { - return Observable.create { - 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 { - return Observable.create { 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 { - return Observable.create { 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 { - 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 { + return RxDatabaseObserver.conversationList.map { + SignalDatabase + .messages + .getUnreadStoryThreadRecipientIds() + .map { Recipient.resolved(it) } + .filterNot { it.shouldHideStory() } + .size + .toLong() } } + + fun getHasFailedOutgoingStories(): Flowable { + return RxDatabaseObserver.conversationList.map { SignalDatabase.messages.hasFailedOutgoingStory() } + } + + fun getNumberOfUnseenCalls(): Flowable { + return RxDatabaseObserver.conversationList.map { SignalDatabase.messages.getUnreadMisedCallCount() } + } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/stories/tabs/ConversationListTabsFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/stories/tabs/ConversationListTabsFragment.kt index 2dbde6c54b..a932282e88 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/stories/tabs/ConversationListTabsFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/stories/tabs/ConversationListTabsFragment.kt @@ -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() } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/stories/tabs/ConversationListTabsViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/stories/tabs/ConversationListTabsViewModel.kt index 3b26dfc715..ca01ee603d 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/stories/tabs/ConversationListTabsViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/stories/tabs/ConversationListTabsViewModel.kt @@ -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 = Transformations.distinctUntilChanged(store.stateLiveData) + val state: Flowable = store.stateFlowable.distinctUntilChanged().observeOn(AndroidSchedulers.mainThread()) val disposables = CompositeDisposable() private val internalTabClickEvents: Subject = PublishSubject.create() val tabClickEvents: Observable = 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) } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/rx/RxStore.kt b/app/src/main/java/org/thoughtcrime/securesms/util/rx/RxStore.kt index bab6d2cefa..27e16c2f7b 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/util/rx/RxStore.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/util/rx/RxStore.kt @@ -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( } } + fun addTo(disposable: CompositeDisposable): RxStore { + disposable.add(this) + return this + } + + fun mapDistinctForUi(map: (T) -> R): Flowable { + return stateFlowable + .map(map) + .distinctUntilChanged() + .observeOn(AndroidSchedulers.mainThread()) + } + /** * Dispose of the underlying scan chain. This is terminal. */ diff --git a/app/src/main/res/layout/conversation_list_fragment.xml b/app/src/main/res/layout/conversation_list_fragment.xml index 6d3369a919..7b2d1a4a86 100644 --- a/app/src/main/res/layout/conversation_list_fragment.xml +++ b/app/src/main/res/layout/conversation_list_fragment.xml @@ -25,20 +25,12 @@ android:layout="@layout/conversation_list_reminder_view" app:layout_constraintTop_toTopOf="parent" /> - - + app:constraint_referenced_ids="reminder,voice_note_player" /> - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/core-util/src/main/java/org/signal/core/util/StringExtensions.kt b/core-util/src/main/java/org/signal/core/util/StringExtensions.kt index ab9d7c9fda..854a0ed749 100644 --- a/core-util/src/main/java/org/signal/core/util/StringExtensions.kt +++ b/core-util/src/main/java/org/signal/core/util/StringExtensions.kt @@ -22,23 +22,6 @@ fun String.asListContains(item: String): Boolean { .any { item.startsWith(it) } } -/** - * Turns a multi-line string into a single-line string stripped of indentation, separated by spaces instead of newlines. - * - * e.g. - * - * a - * b - * c - * - * turns into - * - * a b c - */ -fun String.toSingleLine(): String { - return this.trimIndent().split("\n").joinToString(separator = " ") -} - fun String?.emptyIfNull(): String { return this ?: "" } diff --git a/sms-exporter/lib/src/test/java/org/signal/smsexporter/InMemoryContentProvider.kt b/sms-exporter/lib/src/test/java/org/signal/smsexporter/InMemoryContentProvider.kt index 1a0a2a42c6..ed2bb2c392 100644 --- a/sms-exporter/lib/src/test/java/org/signal/smsexporter/InMemoryContentProvider.kt +++ b/sms-exporter/lib/src/test/java/org/signal/smsexporter/InMemoryContentProvider.kt @@ -61,7 +61,7 @@ class InMemoryContentProvider : ContentProvider() { ${Telephony.Sms.TYPE} INTEGER ); - """.trimIndent() + """ ) db.execSQL( @@ -83,7 +83,7 @@ class InMemoryContentProvider : ContentProvider() { ${Telephony.Mms.SEEN} INTEGER, ${Telephony.Mms.TEXT_ONLY} INTEGER ); - """.trimIndent() + """ ) db.execSQL( @@ -95,7 +95,7 @@ class InMemoryContentProvider : ContentProvider() { ${Telephony.Mms.Part.CONTENT_ID} INTEGER, ${Telephony.Mms.Part.TEXT} TEXT ) - """.trimIndent() + """ ) db.execSQL( @@ -106,7 +106,7 @@ class InMemoryContentProvider : ContentProvider() { ${Telephony.Mms.Addr.CHARSET} INTEGER, ${Telephony.Mms.Addr.TYPE} INTEGER ) - """.trimIndent() + """ ) }