Improve conversation open speed.
Co-authored-by: Cody Henthorne <cody@signal.org>
This commit is contained in:
parent
d3049a3433
commit
666218773c
27 changed files with 462 additions and 395 deletions
|
@ -7,6 +7,7 @@ import androidx.lifecycle.ViewModel
|
|||
import androidx.lifecycle.ViewModelProvider
|
||||
import io.reactivex.rxjava3.disposables.CompositeDisposable
|
||||
import io.reactivex.rxjava3.kotlin.plusAssign
|
||||
import org.signal.paging.LivePagedData
|
||||
import org.signal.paging.PagedData
|
||||
import org.signal.paging.PagingConfig
|
||||
import org.signal.paging.PagingController
|
||||
|
@ -30,7 +31,7 @@ class ContactSearchViewModel(
|
|||
.setStartIndex(0)
|
||||
.build()
|
||||
|
||||
private val pagedData = MutableLiveData<PagedData<ContactSearchKey, ContactSearchData>>()
|
||||
private val pagedData = MutableLiveData<LivePagedData<ContactSearchKey, ContactSearchData>>()
|
||||
private val configurationStore = Store(ContactSearchState())
|
||||
private val selectionStore = Store<Set<ContactSearchKey>>(emptySet())
|
||||
|
||||
|
@ -45,7 +46,7 @@ class ContactSearchViewModel(
|
|||
|
||||
fun setConfiguration(contactSearchConfiguration: ContactSearchConfiguration) {
|
||||
val pagedDataSource = ContactSearchPagedDataSource(contactSearchConfiguration)
|
||||
pagedData.value = PagedData.create(pagedDataSource, pagingConfig)
|
||||
pagedData.value = PagedData.createForLiveData(pagedDataSource, pagingConfig)
|
||||
}
|
||||
|
||||
fun setQuery(query: String?) {
|
||||
|
|
|
@ -1,112 +0,0 @@
|
|||
package org.thoughtcrime.securesms.conversation;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
/**
|
||||
* Represents metadata about a conversation.
|
||||
*/
|
||||
public final class ConversationData {
|
||||
private final long threadId;
|
||||
private final long lastSeen;
|
||||
private final int lastSeenPosition;
|
||||
private final int lastScrolledPosition;
|
||||
private final boolean hasSent;
|
||||
private final int jumpToPosition;
|
||||
private final int threadSize;
|
||||
private final MessageRequestData messageRequestData;
|
||||
private final boolean showUniversalExpireTimerMessage;
|
||||
|
||||
ConversationData(long threadId,
|
||||
long lastSeen,
|
||||
int lastSeenPosition,
|
||||
int lastScrolledPosition,
|
||||
boolean hasSent,
|
||||
int jumpToPosition,
|
||||
int threadSize,
|
||||
@NonNull MessageRequestData messageRequestData,
|
||||
boolean showUniversalExpireTimerMessage)
|
||||
{
|
||||
this.threadId = threadId;
|
||||
this.lastSeen = lastSeen;
|
||||
this.lastSeenPosition = lastSeenPosition;
|
||||
this.lastScrolledPosition = lastScrolledPosition;
|
||||
this.hasSent = hasSent;
|
||||
this.jumpToPosition = jumpToPosition;
|
||||
this.threadSize = threadSize;
|
||||
this.messageRequestData = messageRequestData;
|
||||
this.showUniversalExpireTimerMessage = showUniversalExpireTimerMessage;
|
||||
}
|
||||
|
||||
public long getThreadId() {
|
||||
return threadId;
|
||||
}
|
||||
|
||||
long getLastSeen() {
|
||||
return lastSeen;
|
||||
}
|
||||
|
||||
int getLastSeenPosition() {
|
||||
return lastSeenPosition;
|
||||
}
|
||||
|
||||
int getLastScrolledPosition() {
|
||||
return lastScrolledPosition;
|
||||
}
|
||||
|
||||
boolean hasSent() {
|
||||
return hasSent;
|
||||
}
|
||||
|
||||
boolean shouldJumpToMessage() {
|
||||
return jumpToPosition >= 0;
|
||||
}
|
||||
|
||||
boolean shouldScrollToLastSeen() {
|
||||
return lastSeenPosition > 0;
|
||||
}
|
||||
|
||||
int getJumpToPosition() {
|
||||
return jumpToPosition;
|
||||
}
|
||||
|
||||
int getThreadSize() {
|
||||
return threadSize;
|
||||
}
|
||||
|
||||
@NonNull MessageRequestData getMessageRequestData() {
|
||||
return messageRequestData;
|
||||
}
|
||||
|
||||
public boolean showUniversalExpireTimerMessage() {
|
||||
return showUniversalExpireTimerMessage;
|
||||
}
|
||||
|
||||
static final class MessageRequestData {
|
||||
|
||||
private final boolean messageRequestAccepted;
|
||||
private final boolean groupsInCommon;
|
||||
private final boolean isGroup;
|
||||
|
||||
public MessageRequestData(boolean messageRequestAccepted) {
|
||||
this(messageRequestAccepted, false, false);
|
||||
}
|
||||
|
||||
public MessageRequestData(boolean messageRequestAccepted, boolean groupsInCommon, boolean isGroup) {
|
||||
this.messageRequestAccepted = messageRequestAccepted;
|
||||
this.groupsInCommon = groupsInCommon;
|
||||
this.isGroup = isGroup;
|
||||
}
|
||||
|
||||
public boolean isMessageRequestAccepted() {
|
||||
return messageRequestAccepted;
|
||||
}
|
||||
|
||||
public boolean includeWarningUpdateMessage() {
|
||||
return !messageRequestAccepted && !groupsInCommon;
|
||||
}
|
||||
|
||||
public boolean isGroup() {
|
||||
return isGroup;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,35 @@
|
|||
package org.thoughtcrime.securesms.conversation
|
||||
|
||||
/**
|
||||
* Represents metadata about a conversation.
|
||||
*/
|
||||
data class ConversationData(
|
||||
val threadId: Long,
|
||||
val lastSeen: Long,
|
||||
val lastSeenPosition: Int,
|
||||
val lastScrolledPosition: Int,
|
||||
val jumpToPosition: Int,
|
||||
val threadSize: Int,
|
||||
val messageRequestData: MessageRequestData,
|
||||
@get:JvmName("showUniversalExpireTimerMessage") val showUniversalExpireTimerMessage: Boolean
|
||||
) {
|
||||
|
||||
fun shouldJumpToMessage(): Boolean {
|
||||
return jumpToPosition >= 0
|
||||
}
|
||||
|
||||
fun shouldScrollToLastSeen(): Boolean {
|
||||
return lastSeenPosition > 0
|
||||
}
|
||||
|
||||
data class MessageRequestData @JvmOverloads constructor(
|
||||
val isMessageRequestAccepted: Boolean,
|
||||
private val groupsInCommon: Boolean = false,
|
||||
val isGroup: Boolean = false
|
||||
) {
|
||||
|
||||
fun includeWarningUpdateMessage(): Boolean {
|
||||
return !isMessageRequestAccepted && !groupsInCommon
|
||||
}
|
||||
}
|
||||
}
|
|
@ -47,25 +47,41 @@ class ConversationDataSource implements PagedDataSource<MessageId, ConversationM
|
|||
private final MessageRequestData messageRequestData;
|
||||
private final boolean showUniversalExpireTimerUpdate;
|
||||
|
||||
ConversationDataSource(@NonNull Context context, long threadId, @NonNull MessageRequestData messageRequestData, boolean showUniversalExpireTimerUpdate) {
|
||||
/** Used once for the initial fetch, then cleared. */
|
||||
private int baseSize;
|
||||
|
||||
ConversationDataSource(@NonNull Context context, long threadId, @NonNull MessageRequestData messageRequestData, boolean showUniversalExpireTimerUpdate, int baseSize) {
|
||||
this.context = context;
|
||||
this.threadId = threadId;
|
||||
this.messageRequestData = messageRequestData;
|
||||
this.showUniversalExpireTimerUpdate = showUniversalExpireTimerUpdate;
|
||||
this.baseSize = baseSize;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int size() {
|
||||
long startTime = System.currentTimeMillis();
|
||||
int size = SignalDatabase.mmsSms().getConversationCount(threadId) +
|
||||
int size = getSizeInternal() +
|
||||
(messageRequestData.includeWarningUpdateMessage() ? 1 : 0) +
|
||||
(showUniversalExpireTimerUpdate ? 1 : 0);
|
||||
|
||||
Log.d(TAG, "size() for thread " + threadId + ": " + (System.currentTimeMillis() - startTime) + " ms");
|
||||
Log.d(TAG, "[size(), thread " + threadId + "] " + (System.currentTimeMillis() - startTime) + " ms");
|
||||
|
||||
return size;
|
||||
}
|
||||
|
||||
private int getSizeInternal() {
|
||||
synchronized (this) {
|
||||
if (baseSize != -1) {
|
||||
int size = baseSize;
|
||||
baseSize = -1;
|
||||
return size;
|
||||
}
|
||||
}
|
||||
|
||||
return SignalDatabase.mmsSms().getConversationCount(threadId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull List<ConversationMessage> load(int start, int length, @NonNull CancellationSignal cancellationSignal) {
|
||||
Stopwatch stopwatch = new Stopwatch("load(" + start + ", " + length + "), thread " + threadId);
|
||||
|
|
|
@ -195,8 +195,9 @@ public class ConversationFragment extends LoggingFragment implements Multiselect
|
|||
private static final int SCROLL_ANIMATION_THRESHOLD = 50;
|
||||
private static final int CODE_ADD_EDIT_CONTACT = 77;
|
||||
|
||||
private final ActionModeCallback actionModeCallback = new ActionModeCallback();
|
||||
private final ItemClickListener selectionClickListener = new ConversationFragmentItemClickListener();
|
||||
private final ActionModeCallback actionModeCallback = new ActionModeCallback();
|
||||
private final ItemClickListener selectionClickListener = new ConversationFragmentItemClickListener();
|
||||
private final LifecycleDisposable disposables = new LifecycleDisposable();
|
||||
|
||||
private ConversationFragmentListener listener;
|
||||
|
||||
|
@ -241,6 +242,9 @@ public class ConversationFragment extends LoggingFragment implements Multiselect
|
|||
private MultiselectItemDecoration multiselectItemDecoration;
|
||||
private LifecycleDisposable lifecycleDisposable;
|
||||
|
||||
private @Nullable ConversationData conversationData;
|
||||
private @Nullable ChatWallpaper chatWallpaper;
|
||||
|
||||
public static void prepare(@NonNull Context context) {
|
||||
FrameLayout parent = new FrameLayout(context);
|
||||
parent.setLayoutParams(new FrameLayout.LayoutParams(FrameLayout.LayoutParams.MATCH_PARENT, FrameLayout.LayoutParams.WRAP_CONTENT));
|
||||
|
@ -263,6 +267,8 @@ public class ConversationFragment extends LoggingFragment implements Multiselect
|
|||
|
||||
@Override
|
||||
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle bundle) {
|
||||
disposables.bindTo(getViewLifecycleOwner());
|
||||
|
||||
final View view = inflater.inflate(R.layout.conversation_fragment, container, false);
|
||||
videoContainer = view.findViewById(R.id.video_container);
|
||||
list = view.findViewById(android.R.id.list);
|
||||
|
@ -291,8 +297,7 @@ public class ConversationFragment extends LoggingFragment implements Multiselect
|
|||
() -> conversationViewModel.shouldPlayMessageAnimations() && list.getScrollState() == RecyclerView.SCROLL_STATE_IDLE,
|
||||
() -> list.canScrollVertically(1) || list.canScrollVertically(-1));
|
||||
|
||||
multiselectItemDecoration = new MultiselectItemDecoration(requireContext(),
|
||||
() -> conversationViewModel.getWallpaper().getValue());
|
||||
multiselectItemDecoration = new MultiselectItemDecoration(requireContext(), () -> chatWallpaper);
|
||||
|
||||
list.setHasFixedSize(false);
|
||||
list.setLayoutManager(layoutManager);
|
||||
|
@ -333,17 +338,22 @@ public class ConversationFragment extends LoggingFragment implements Multiselect
|
|||
this.messageCountsViewModel = new ViewModelProvider(getParentFragment()).get(MessageCountsViewModel.class);
|
||||
this.conversationViewModel = new ViewModelProvider(getParentFragment(), new ConversationViewModel.Factory()).get(ConversationViewModel.class);
|
||||
|
||||
conversationViewModel.getChatColors().observe(getViewLifecycleOwner(), recyclerViewColorizer::setChatColors);
|
||||
conversationViewModel.getMessages().observe(getViewLifecycleOwner(), messages -> {
|
||||
disposables.add(conversationViewModel.getChatColors().subscribe(recyclerViewColorizer::setChatColors));
|
||||
disposables.add(conversationViewModel.getMessageData().subscribe(messageData -> {
|
||||
ConversationAdapter adapter = getListAdapter();
|
||||
if (adapter != null) {
|
||||
List<ConversationMessage> messages = messageData.getMessages();
|
||||
getListAdapter().submitList(messages, () -> {
|
||||
list.post(() -> conversationViewModel.onMessagesCommitted(messages));
|
||||
list.post(() -> {
|
||||
conversationViewModel.onMessagesCommitted(messages);
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
conversationViewModel.getConversationMetadata().observe(getViewLifecycleOwner(), this::presentConversationMetadata);
|
||||
presentConversationMetadata(messageData.getMetadata());
|
||||
}));
|
||||
|
||||
disposables.add(conversationViewModel.getWallpaper().subscribe(w -> chatWallpaper = w.orElse(null)));
|
||||
|
||||
conversationViewModel.getShowMentionsButton().observe(getViewLifecycleOwner(), shouldShow -> {
|
||||
if (shouldShow) {
|
||||
|
@ -367,14 +377,14 @@ public class ConversationFragment extends LoggingFragment implements Multiselect
|
|||
updateToolbarDependentMargins();
|
||||
|
||||
colorizer = new Colorizer();
|
||||
conversationViewModel.getNameColorsMap().observe(getViewLifecycleOwner(), nameColorsMap -> {
|
||||
disposables.add(conversationViewModel.getNameColorsMap().subscribe(nameColorsMap -> {
|
||||
colorizer.onNameColorsChanged(nameColorsMap);
|
||||
|
||||
ConversationAdapter adapter = getListAdapter();
|
||||
if (adapter != null) {
|
||||
adapter.notifyItemRangeChanged(0, adapter.getItemCount(), ConversationAdapter.PAYLOAD_NAME_COLORS);
|
||||
}
|
||||
});
|
||||
}));
|
||||
|
||||
conversationUpdateTick = new ConversationUpdateTick(this::updateConversationItemTimestamps);
|
||||
getViewLifecycleOwner().getLifecycle().addObserver(conversationUpdateTick);
|
||||
|
@ -491,7 +501,8 @@ public class ConversationFragment extends LoggingFragment implements Multiselect
|
|||
}
|
||||
|
||||
public void moveToLastSeen() {
|
||||
if (conversationViewModel.getLastSeenPosition() <= 0) {
|
||||
int lastSeenPosition = conversationData != null ? conversationData.getLastSeenPosition() : 0;
|
||||
if (lastSeenPosition <= 0) {
|
||||
Log.i(TAG, "No need to move to last seen.");
|
||||
return;
|
||||
}
|
||||
|
@ -501,7 +512,7 @@ public class ConversationFragment extends LoggingFragment implements Multiselect
|
|||
return;
|
||||
}
|
||||
|
||||
int position = getListAdapter().getAdapterPositionForMessagePosition(conversationViewModel.getLastSeenPosition());
|
||||
int position = getListAdapter().getAdapterPositionForMessagePosition(lastSeenPosition);
|
||||
snapToTopDataObserver.requestScrollPosition(position);
|
||||
}
|
||||
|
||||
|
@ -631,9 +642,8 @@ public class ConversationFragment extends LoggingFragment implements Multiselect
|
|||
}
|
||||
|
||||
private void initializeResources() {
|
||||
long oldThreadId = threadId;
|
||||
|
||||
int startingPosition = getStartPosition();
|
||||
long oldThreadId = threadId;
|
||||
int startingPosition = getStartPosition();
|
||||
|
||||
this.recipient = Recipient.live(conversationViewModel.getArgs().getRecipientId());
|
||||
this.threadId = conversationViewModel.getArgs().getThreadId();
|
||||
|
@ -678,14 +688,14 @@ public class ConversationFragment extends LoggingFragment implements Multiselect
|
|||
adapter.registerAdapterDataObserver(snapToTopDataObserver);
|
||||
adapter.registerAdapterDataObserver(new CheckExpirationDataObserver());
|
||||
|
||||
setLastSeen(conversationViewModel.getLastSeen());
|
||||
setLastSeen(conversationData != null ? conversationData.getLastSeen() : 0);
|
||||
|
||||
adapter.registerAdapterDataObserver(new RecyclerView.AdapterDataObserver() {
|
||||
@Override
|
||||
public void onItemRangeInserted(int positionStart, int itemCount) {
|
||||
adapter.unregisterAdapterDataObserver(this);
|
||||
startupStopwatch.split("data-set");
|
||||
SignalLocalMetrics.ConversationOpen.onDataLoaded();
|
||||
adapter.unregisterAdapterDataObserver(this);
|
||||
list.post(() -> {
|
||||
startupStopwatch.split("first-render");
|
||||
startupStopwatch.stop(TAG);
|
||||
|
@ -1100,6 +1110,13 @@ public class ConversationFragment extends LoggingFragment implements Multiselect
|
|||
}
|
||||
|
||||
private void presentConversationMetadata(@NonNull ConversationData conversation) {
|
||||
if (conversationData != null && conversationData.getThreadId() == conversation.getThreadId()) {
|
||||
Log.d(TAG, "Already presented conversation data for thread " + threadId);
|
||||
return;
|
||||
}
|
||||
|
||||
conversationData = conversation;
|
||||
|
||||
ConversationAdapter adapter = getListAdapter();
|
||||
if (adapter == null) {
|
||||
return;
|
||||
|
|
|
@ -108,6 +108,9 @@ import org.thoughtcrime.securesms.PromptMmsActivity;
|
|||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.ShortcutLauncherActivity;
|
||||
import org.thoughtcrime.securesms.TransportOption;
|
||||
import org.thoughtcrime.securesms.components.emoji.RecentEmojiPageModel;
|
||||
import org.thoughtcrime.securesms.util.LifecycleDisposable;
|
||||
import org.thoughtcrime.securesms.verify.VerifyIdentityActivity;
|
||||
import org.thoughtcrime.securesms.attachments.Attachment;
|
||||
import org.thoughtcrime.securesms.attachments.TombstoneAttachment;
|
||||
import org.thoughtcrime.securesms.audio.AudioRecorder;
|
||||
|
@ -439,6 +442,8 @@ public class ConversationParentFragment extends Fragment
|
|||
private boolean isSecurityInitialized = false;
|
||||
private boolean isSearchRequested = false;
|
||||
|
||||
private final LifecycleDisposable disposables = new LifecycleDisposable();
|
||||
|
||||
private volatile boolean screenInitialized = false;
|
||||
|
||||
private IdentityRecordList identityRecords = new IdentityRecordList(Collections.emptyList());
|
||||
|
@ -452,6 +457,8 @@ public class ConversationParentFragment extends Fragment
|
|||
|
||||
@Override
|
||||
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
|
||||
disposables.bindTo(getViewLifecycleOwner());
|
||||
|
||||
if (requireActivity() instanceof Callback) {
|
||||
callback = (Callback) requireActivity();
|
||||
} else if (getParentFragment() instanceof Callback) {
|
||||
|
@ -552,7 +559,7 @@ public class ConversationParentFragment extends Fragment
|
|||
initializeInsightObserver();
|
||||
initializeActionBar();
|
||||
|
||||
viewModel.getStoryViewState(getViewLifecycleOwner()).observe(getViewLifecycleOwner(), titleView::setStoryRingFromState);
|
||||
disposables.add(viewModel.getStoryViewState().subscribe(titleView::setStoryRingFromState));
|
||||
|
||||
requireActivity().getOnBackPressedDispatcher().addCallback(getViewLifecycleOwner(), new OnBackPressedCallback(true) {
|
||||
@Override
|
||||
|
@ -1026,13 +1033,13 @@ public class ConversationParentFragment extends Fragment
|
|||
}
|
||||
|
||||
hideMenuItem(menu, R.id.menu_create_bubble);
|
||||
viewModel.canShowAsBubble().observe(getViewLifecycleOwner(), canShowAsBubble -> {
|
||||
disposables.add(viewModel.canShowAsBubble().subscribe(canShowAsBubble -> {
|
||||
MenuItem item = menu.findItem(R.id.menu_create_bubble);
|
||||
|
||||
if (item != null) {
|
||||
item.setVisible(canShowAsBubble && !isInBubble());
|
||||
}
|
||||
});
|
||||
}));
|
||||
|
||||
if (threadId == -1L) {
|
||||
hideMenuItem(menu, R.id.menu_view_media);
|
||||
|
@ -2300,8 +2307,8 @@ public class ConversationParentFragment extends Fragment
|
|||
this.viewModel = new ViewModelProvider(this, new ConversationViewModel.Factory()).get(ConversationViewModel.class);
|
||||
|
||||
this.viewModel.setArgs(args);
|
||||
this.viewModel.getWallpaper().observe(getViewLifecycleOwner(), this::updateWallpaper);
|
||||
this.viewModel.getEvents().observe(getViewLifecycleOwner(), this::onViewModelEvent);
|
||||
disposables.add(this.viewModel.getWallpaper().subscribe(w -> updateWallpaper(w.orElse(null))));
|
||||
}
|
||||
|
||||
private void initializeGroupViewModel() {
|
||||
|
|
|
@ -5,10 +5,9 @@ import android.os.Build;
|
|||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.WorkerThread;
|
||||
import androidx.lifecycle.LiveData;
|
||||
import androidx.lifecycle.MutableLiveData;
|
||||
|
||||
import org.signal.core.util.concurrent.SignalExecutors;
|
||||
import org.signal.core.util.logging.Log;
|
||||
import org.thoughtcrime.securesms.database.GroupDatabase;
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase;
|
||||
import org.thoughtcrime.securesms.database.ThreadDatabase;
|
||||
|
@ -21,26 +20,15 @@ import org.thoughtcrime.securesms.util.ConversationUtil;
|
|||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.Executor;
|
||||
|
||||
class ConversationRepository {
|
||||
|
||||
private static final String TAG = Log.tag(ConversationRepository.class);
|
||||
|
||||
private final Context context;
|
||||
private final Executor executor;
|
||||
|
||||
ConversationRepository() {
|
||||
this.context = ApplicationDependencies.getApplication();
|
||||
this.executor = SignalExecutors.BOUNDED;
|
||||
}
|
||||
|
||||
LiveData<ConversationData> getConversationData(long threadId, @NonNull Recipient recipient, int jumpToPosition) {
|
||||
MutableLiveData<ConversationData> liveData = new MutableLiveData<>();
|
||||
|
||||
executor.execute(() -> {
|
||||
liveData.postValue(getConversationDataInternal(threadId, recipient, jumpToPosition));
|
||||
});
|
||||
|
||||
return liveData;
|
||||
this.context = ApplicationDependencies.getApplication();
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
|
@ -54,11 +42,11 @@ class ConversationRepository {
|
|||
}
|
||||
}
|
||||
|
||||
private @NonNull ConversationData getConversationDataInternal(long threadId, @NonNull Recipient conversationRecipient, int jumpToPosition) {
|
||||
@WorkerThread
|
||||
public @NonNull ConversationData getConversationData(long threadId, @NonNull Recipient conversationRecipient, int jumpToPosition) {
|
||||
ThreadDatabase.ConversationMetadata metadata = SignalDatabase.threads().getConversationMetadata(threadId);
|
||||
int threadSize = SignalDatabase.mmsSms().getConversationCount(threadId);
|
||||
long lastSeen = metadata.getLastSeen();
|
||||
boolean hasSent = metadata.hasSent();
|
||||
int lastSeenPosition = 0;
|
||||
long lastScrolled = metadata.getLastScrolled();
|
||||
int lastScrolledPosition = 0;
|
||||
|
@ -108,6 +96,6 @@ class ConversationRepository {
|
|||
showUniversalExpireTimerUpdate = true;
|
||||
}
|
||||
|
||||
return new ConversationData(threadId, lastSeen, lastSeenPosition, lastScrolledPosition, hasSent, jumpToPosition, threadSize, messageRequestData, showUniversalExpireTimerUpdate);
|
||||
return new ConversationData(threadId, lastSeen, lastSeenPosition, lastScrolledPosition, jumpToPosition, threadSize, messageRequestData, showUniversalExpireTimerUpdate);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,7 +5,6 @@ import android.app.Application;
|
|||
import androidx.annotation.MainThread;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.lifecycle.LifecycleOwner;
|
||||
import androidx.lifecycle.LiveData;
|
||||
import androidx.lifecycle.LiveDataReactiveStreams;
|
||||
import androidx.lifecycle.MutableLiveData;
|
||||
|
@ -19,9 +18,9 @@ import com.annimon.stream.Stream;
|
|||
import org.greenrobot.eventbus.EventBus;
|
||||
import org.greenrobot.eventbus.Subscribe;
|
||||
import org.greenrobot.eventbus.ThreadMode;
|
||||
import org.reactivestreams.Publisher;
|
||||
import org.signal.core.util.MapUtil;
|
||||
import org.signal.core.util.logging.Log;
|
||||
import org.signal.paging.ObservablePagedData;
|
||||
import org.signal.paging.PagedData;
|
||||
import org.signal.paging.PagingConfig;
|
||||
import org.signal.paging.PagingController;
|
||||
|
@ -31,12 +30,12 @@ import org.thoughtcrime.securesms.conversation.colors.ChatColors;
|
|||
import org.thoughtcrime.securesms.conversation.colors.ChatColorsPalette;
|
||||
import org.thoughtcrime.securesms.conversation.colors.NameColor;
|
||||
import org.thoughtcrime.securesms.database.DatabaseObserver;
|
||||
import org.thoughtcrime.securesms.database.GroupDatabase;
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase;
|
||||
import org.thoughtcrime.securesms.database.model.MessageId;
|
||||
import org.thoughtcrime.securesms.database.model.StoryViewState;
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.groups.GroupId;
|
||||
import org.thoughtcrime.securesms.groups.LiveGroup;
|
||||
import org.thoughtcrime.securesms.groups.ui.GroupMemberEntry;
|
||||
import org.thoughtcrime.securesms.mediasend.Media;
|
||||
import org.thoughtcrime.securesms.mediasend.MediaRepository;
|
||||
import org.thoughtcrime.securesms.notifications.profiles.NotificationProfile;
|
||||
|
@ -44,7 +43,7 @@ import org.thoughtcrime.securesms.notifications.profiles.NotificationProfiles;
|
|||
import org.thoughtcrime.securesms.ratelimit.RecaptchaRequiredEvent;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
import org.thoughtcrime.securesms.util.DefaultValueLiveData;
|
||||
import org.thoughtcrime.securesms.util.SignalLocalMetrics;
|
||||
import org.thoughtcrime.securesms.util.SingleLiveEvent;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
import org.thoughtcrime.securesms.util.ViewUtil;
|
||||
|
@ -63,39 +62,41 @@ 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.Flowable;
|
||||
import io.reactivex.rxjava3.core.Observable;
|
||||
import io.reactivex.rxjava3.disposables.CompositeDisposable;
|
||||
import io.reactivex.rxjava3.schedulers.Schedulers;
|
||||
import io.reactivex.rxjava3.subjects.BehaviorSubject;
|
||||
|
||||
public class ConversationViewModel extends ViewModel {
|
||||
|
||||
private static final String TAG = Log.tag(ConversationViewModel.class);
|
||||
|
||||
private final Application context;
|
||||
private final MediaRepository mediaRepository;
|
||||
private final ConversationRepository conversationRepository;
|
||||
private final MutableLiveData<List<Media>> recentMedia;
|
||||
private final MutableLiveData<Long> threadId;
|
||||
private final LiveData<List<ConversationMessage>> messages;
|
||||
private final LiveData<ConversationData> conversationMetadata;
|
||||
private final MutableLiveData<Boolean> showScrollButtons;
|
||||
private final MutableLiveData<Boolean> hasUnreadMentions;
|
||||
private final LiveData<Boolean> canShowAsBubble;
|
||||
private final ProxyPagingController<MessageId> pagingController;
|
||||
private final DatabaseObserver.Observer conversationObserver;
|
||||
private final DatabaseObserver.MessageObserver messageUpdateObserver;
|
||||
private final DatabaseObserver.MessageObserver messageInsertObserver;
|
||||
private final MutableLiveData<RecipientId> recipientId;
|
||||
private final LiveData<ChatWallpaper> wallpaper;
|
||||
private final SingleLiveEvent<Event> events;
|
||||
private final LiveData<ChatColors> chatColors;
|
||||
private final MutableLiveData<Integer> toolbarBottom;
|
||||
private final MutableLiveData<Integer> inlinePlayerHeight;
|
||||
private final LiveData<Integer> conversationTopMargin;
|
||||
private final Store<ThreadAnimationState> threadAnimationStateStore;
|
||||
private final Observer<ThreadAnimationState> threadAnimationStateStoreDriver;
|
||||
private final NotificationProfilesRepository notificationProfilesRepository;
|
||||
private final MutableLiveData<String> searchQuery;
|
||||
private final Application context;
|
||||
private final MediaRepository mediaRepository;
|
||||
private final ConversationRepository conversationRepository;
|
||||
private final MutableLiveData<List<Media>> recentMedia;
|
||||
private final BehaviorSubject<Long> threadId;
|
||||
private final Observable<MessageData> messageData;
|
||||
private final MutableLiveData<Boolean> showScrollButtons;
|
||||
private final MutableLiveData<Boolean> hasUnreadMentions;
|
||||
private final Observable<Boolean> canShowAsBubble;
|
||||
private final ProxyPagingController<MessageId> pagingController;
|
||||
private final DatabaseObserver.Observer conversationObserver;
|
||||
private final DatabaseObserver.MessageObserver messageUpdateObserver;
|
||||
private final DatabaseObserver.MessageObserver messageInsertObserver;
|
||||
private final BehaviorSubject<RecipientId> recipientId;
|
||||
private final Observable<Optional<ChatWallpaper>> wallpaper;
|
||||
private final SingleLiveEvent<Event> events;
|
||||
private final Observable<ChatColors> chatColors;
|
||||
private final MutableLiveData<Integer> toolbarBottom;
|
||||
private final MutableLiveData<Integer> inlinePlayerHeight;
|
||||
private final LiveData<Integer> conversationTopMargin;
|
||||
private final Store<ThreadAnimationState> threadAnimationStateStore;
|
||||
private final Observer<ThreadAnimationState> threadAnimationStateStoreDriver;
|
||||
private final NotificationProfilesRepository notificationProfilesRepository;
|
||||
private final MutableLiveData<String> searchQuery;
|
||||
|
||||
private final Map<GroupId, Set<Recipient>> sessionMemberCache = new HashMap<>();
|
||||
|
||||
|
@ -107,10 +108,8 @@ public class ConversationViewModel extends ViewModel {
|
|||
this.mediaRepository = new MediaRepository();
|
||||
this.conversationRepository = new ConversationRepository();
|
||||
this.recentMedia = new MutableLiveData<>();
|
||||
this.threadId = new MutableLiveData<>();
|
||||
this.showScrollButtons = new MutableLiveData<>(false);
|
||||
this.hasUnreadMentions = new MutableLiveData<>(false);
|
||||
this.recipientId = new MutableLiveData<>();
|
||||
this.events = new SingleLiveEvent<>();
|
||||
this.pagingController = new ProxyPagingController<>();
|
||||
this.conversationObserver = pagingController::onDataInvalidated;
|
||||
|
@ -122,65 +121,75 @@ public class ConversationViewModel extends ViewModel {
|
|||
this.threadAnimationStateStore = new Store<>(new ThreadAnimationState(-1L, null, false));
|
||||
this.notificationProfilesRepository = new NotificationProfilesRepository();
|
||||
this.searchQuery = new MutableLiveData<>();
|
||||
this.recipientId = BehaviorSubject.create();
|
||||
this.threadId = BehaviorSubject.create();
|
||||
|
||||
LiveData<Recipient> recipientLiveData = LiveDataUtil.mapAsync(recipientId, Recipient::resolved);
|
||||
LiveData<ThreadAndRecipient> threadAndRecipient = LiveDataUtil.combineLatest(threadId, recipientLiveData, ThreadAndRecipient::new);
|
||||
BehaviorSubject<Recipient> recipientCache = BehaviorSubject.create();
|
||||
|
||||
LiveData<ConversationData> metadata = Transformations.switchMap(threadAndRecipient, d -> {
|
||||
LiveData<ConversationData> conversationData = conversationRepository.getConversationData(d.threadId, d.recipient, jumpToPosition);
|
||||
recipientId
|
||||
.observeOn(Schedulers.io())
|
||||
.distinctUntilChanged()
|
||||
.map(Recipient::resolved)
|
||||
.subscribe(recipientCache);
|
||||
|
||||
jumpToPosition = -1;
|
||||
BehaviorSubject<ConversationData> conversationMetadata = BehaviorSubject.create();
|
||||
|
||||
return conversationData;
|
||||
});
|
||||
Observable.combineLatest(threadId, recipientCache, Pair::new)
|
||||
.observeOn(Schedulers.io())
|
||||
.distinctUntilChanged()
|
||||
.map(threadIdAndRecipient -> {
|
||||
SignalLocalMetrics.ConversationOpen.onMetadataLoadStarted();
|
||||
ConversationData conversationData = conversationRepository.getConversationData(threadIdAndRecipient.first(), threadIdAndRecipient.second(), jumpToPosition);
|
||||
SignalLocalMetrics.ConversationOpen.onMetadataLoaded();
|
||||
|
||||
jumpToPosition = -1;
|
||||
|
||||
return conversationData;
|
||||
})
|
||||
.subscribe(conversationMetadata);
|
||||
|
||||
ApplicationDependencies.getDatabaseObserver().registerMessageUpdateObserver(messageUpdateObserver);
|
||||
|
||||
LiveData<Pair<Long, PagedData<MessageId, ConversationMessage>>> pagedDataForThreadId = Transformations.map(metadata, data -> {
|
||||
int startPosition;
|
||||
ConversationData.MessageRequestData messageRequestData = data.getMessageRequestData();
|
||||
messageData = conversationMetadata
|
||||
.observeOn(Schedulers.io())
|
||||
.switchMap(data -> {
|
||||
int startPosition;
|
||||
|
||||
if (data.shouldJumpToMessage()) {
|
||||
startPosition = data.getJumpToPosition();
|
||||
} else if (messageRequestData.isMessageRequestAccepted() && data.shouldScrollToLastSeen()) {
|
||||
startPosition = data.getLastSeenPosition();
|
||||
} else if (messageRequestData.isMessageRequestAccepted()) {
|
||||
startPosition = data.getLastScrolledPosition();
|
||||
} else {
|
||||
startPosition = data.getThreadSize();
|
||||
}
|
||||
ConversationData.MessageRequestData messageRequestData = data.getMessageRequestData();
|
||||
|
||||
ApplicationDependencies.getDatabaseObserver().unregisterObserver(conversationObserver);
|
||||
ApplicationDependencies.getDatabaseObserver().unregisterObserver(messageInsertObserver);
|
||||
ApplicationDependencies.getDatabaseObserver().registerConversationObserver(data.getThreadId(), conversationObserver);
|
||||
ApplicationDependencies.getDatabaseObserver().registerMessageInsertObserver(data.getThreadId(), messageInsertObserver);
|
||||
if (data.shouldJumpToMessage()) {
|
||||
startPosition = data.getJumpToPosition();
|
||||
} else if (messageRequestData.isMessageRequestAccepted() && data.shouldScrollToLastSeen()) {
|
||||
startPosition = data.getLastSeenPosition();
|
||||
} else if (messageRequestData.isMessageRequestAccepted()) {
|
||||
startPosition = data.getLastScrolledPosition();
|
||||
} else {
|
||||
startPosition = data.getThreadSize();
|
||||
}
|
||||
|
||||
ConversationDataSource dataSource = new ConversationDataSource(context, data.getThreadId(), messageRequestData, data.showUniversalExpireTimerMessage());
|
||||
PagingConfig config = new PagingConfig.Builder().setPageSize(25)
|
||||
.setBufferPages(3)
|
||||
.setStartIndex(Math.max(startPosition, 0))
|
||||
.build();
|
||||
ApplicationDependencies.getDatabaseObserver().unregisterObserver(conversationObserver);
|
||||
ApplicationDependencies.getDatabaseObserver().unregisterObserver(messageInsertObserver);
|
||||
ApplicationDependencies.getDatabaseObserver().registerConversationObserver(data.getThreadId(), conversationObserver);
|
||||
ApplicationDependencies.getDatabaseObserver().registerMessageInsertObserver(data.getThreadId(), messageInsertObserver);
|
||||
|
||||
Log.d(TAG, "Starting at position: " + startPosition + " || jumpToPosition: " + data.getJumpToPosition() + ", lastSeenPosition: " + data.getLastSeenPosition() + ", lastScrolledPosition: " + data.getLastScrolledPosition());
|
||||
return new Pair<>(data.getThreadId(), PagedData.create(dataSource, config));
|
||||
});
|
||||
ConversationDataSource dataSource = new ConversationDataSource(context, data.getThreadId(), messageRequestData, data.showUniversalExpireTimerMessage(), data.getThreadSize());
|
||||
PagingConfig config = new PagingConfig.Builder().setPageSize(25)
|
||||
.setBufferPages(2)
|
||||
.setStartIndex(Math.max(startPosition, 0))
|
||||
.build();
|
||||
|
||||
this.messages = Transformations.switchMap(pagedDataForThreadId, pair -> {
|
||||
pagingController.set(pair.second().getController());
|
||||
return pair.second().getData();
|
||||
});
|
||||
Log.d(TAG, "Starting at position: " + startPosition + " || jumpToPosition: " + data.getJumpToPosition() + ", lastSeenPosition: " + data.getLastSeenPosition() + ", lastScrolledPosition: " + data.getLastScrolledPosition());
|
||||
ObservablePagedData<MessageId, ConversationMessage> pagedData = PagedData.createForObservable(dataSource, config);
|
||||
|
||||
conversationMetadata = Transformations.switchMap(messages, m -> metadata);
|
||||
canShowAsBubble = LiveDataUtil.mapAsync(threadId, conversationRepository::canShowAsBubble);
|
||||
wallpaper = LiveDataUtil.mapDistinct(Transformations.switchMap(recipientId,
|
||||
id -> Recipient.live(id).getLiveData()),
|
||||
Recipient::getWallpaper);
|
||||
pagingController.set(pagedData.getController());
|
||||
return pagedData.getData();
|
||||
})
|
||||
.observeOn(Schedulers.io())
|
||||
.withLatestFrom(conversationMetadata, (messages, metadata) -> new MessageData(metadata, messages));
|
||||
|
||||
EventBus.getDefault().register(this);
|
||||
|
||||
chatColors = LiveDataUtil.mapDistinct(Transformations.switchMap(recipientId,
|
||||
id -> Recipient.live(id).getLiveData()),
|
||||
Recipient::getChatColors);
|
||||
canShowAsBubble = threadId.observeOn(Schedulers.io()).map(conversationRepository::canShowAsBubble);
|
||||
wallpaper = recipientCache.map(r -> Optional.ofNullable(r.getWallpaper())).distinctUntilChanged();
|
||||
chatColors = recipientCache.map(Recipient::getChatColors).distinctUntilChanged();
|
||||
|
||||
threadAnimationStateStore.update(threadId, (id, state) -> {
|
||||
if (state.getThreadId() == id) {
|
||||
|
@ -190,7 +199,7 @@ public class ConversationViewModel extends ViewModel {
|
|||
}
|
||||
});
|
||||
|
||||
threadAnimationStateStore.update(metadata, (m, state) -> {
|
||||
threadAnimationStateStore.update(conversationMetadata, (m, state) -> {
|
||||
if (state.getThreadId() == m.getThreadId()) {
|
||||
return state.copy(state.getThreadId(), m, state.getHasCommittedNonEmptyMessageList());
|
||||
} else {
|
||||
|
@ -200,14 +209,16 @@ public class ConversationViewModel extends ViewModel {
|
|||
|
||||
this.threadAnimationStateStoreDriver = state -> {};
|
||||
threadAnimationStateStore.getStateLiveData().observeForever(threadAnimationStateStoreDriver);
|
||||
|
||||
EventBus.getDefault().register(this);
|
||||
}
|
||||
|
||||
LiveData<StoryViewState> getStoryViewState(@NonNull LifecycleOwner lifecycle) {
|
||||
Publisher<RecipientId> recipientIdPublisher = LiveDataReactiveStreams.toPublisher(lifecycle, recipientId);
|
||||
Flowable<StoryViewState> storyViewState = Flowable.fromPublisher(recipientIdPublisher)
|
||||
.flatMap(id -> StoryViewState.getForRecipientId(id).toFlowable(BackpressureStrategy.LATEST));
|
||||
|
||||
return LiveDataReactiveStreams.fromPublisher(storyViewState);
|
||||
Observable<StoryViewState> getStoryViewState() {
|
||||
return recipientId
|
||||
.subscribeOn(Schedulers.io())
|
||||
.switchMap(StoryViewState::getForRecipientId)
|
||||
.distinctUntilChanged()
|
||||
.observeOn(AndroidSchedulers.mainThread());
|
||||
}
|
||||
|
||||
void onMessagesCommitted(@NonNull List<ConversationMessage> conversationMessages) {
|
||||
|
@ -249,13 +260,13 @@ public class ConversationViewModel extends ViewModel {
|
|||
Log.d(TAG, "[onConversationDataAvailable] recipientId: " + recipientId + ", threadId: " + threadId + ", startingPosition: " + startingPosition);
|
||||
this.jumpToPosition = startingPosition;
|
||||
|
||||
this.threadId.setValue(threadId);
|
||||
this.recipientId.setValue(recipientId);
|
||||
this.threadId.onNext(threadId);
|
||||
this.recipientId.onNext(recipientId);
|
||||
}
|
||||
|
||||
void clearThreadId() {
|
||||
this.jumpToPosition = -1;
|
||||
this.threadId.postValue(-1L);
|
||||
this.threadId.onNext(-1L);
|
||||
}
|
||||
|
||||
void setSearchQuery(@Nullable String query) {
|
||||
|
@ -270,8 +281,9 @@ public class ConversationViewModel extends ViewModel {
|
|||
return conversationTopMargin;
|
||||
}
|
||||
|
||||
@NonNull LiveData<Boolean> canShowAsBubble() {
|
||||
return canShowAsBubble;
|
||||
@NonNull Observable<Boolean> canShowAsBubble() {
|
||||
return canShowAsBubble
|
||||
.observeOn(AndroidSchedulers.mainThread());
|
||||
}
|
||||
|
||||
@NonNull LiveData<Boolean> getShowScrollToBottom() {
|
||||
|
@ -282,16 +294,18 @@ public class ConversationViewModel extends ViewModel {
|
|||
return Transformations.distinctUntilChanged(LiveDataUtil.combineLatest(showScrollButtons, hasUnreadMentions, (a, b) -> a && b));
|
||||
}
|
||||
|
||||
@NonNull LiveData<ChatWallpaper> getWallpaper() {
|
||||
return wallpaper;
|
||||
@NonNull Observable<Optional<ChatWallpaper>> getWallpaper() {
|
||||
return wallpaper
|
||||
.observeOn(AndroidSchedulers.mainThread());
|
||||
}
|
||||
|
||||
@NonNull LiveData<Event> getEvents() {
|
||||
return events;
|
||||
}
|
||||
|
||||
@NonNull LiveData<ChatColors> getChatColors() {
|
||||
return chatColors;
|
||||
@NonNull Observable<ChatColors> getChatColors() {
|
||||
return chatColors
|
||||
.observeOn(AndroidSchedulers.mainThread());
|
||||
}
|
||||
|
||||
void setHasUnreadMentions(boolean hasUnreadMentions) {
|
||||
|
@ -310,55 +324,45 @@ public class ConversationViewModel extends ViewModel {
|
|||
return recentMedia;
|
||||
}
|
||||
|
||||
@NonNull LiveData<ConversationData> getConversationMetadata() {
|
||||
return conversationMetadata;
|
||||
@NonNull Observable<MessageData> getMessageData() {
|
||||
return messageData
|
||||
.observeOn(AndroidSchedulers.mainThread());
|
||||
}
|
||||
|
||||
@NonNull LiveData<List<ConversationMessage>> getMessages() {
|
||||
return messages;
|
||||
}
|
||||
|
||||
@NonNull PagingController getPagingController() {
|
||||
@NonNull PagingController<MessageId> getPagingController() {
|
||||
return pagingController;
|
||||
}
|
||||
|
||||
@NonNull LiveData<Map<RecipientId, NameColor>> getNameColorsMap() {
|
||||
LiveData<Recipient> recipient = Transformations.switchMap(recipientId, r -> Recipient.live(r).getLiveData());
|
||||
LiveData<Optional<GroupId>> group = Transformations.map(recipient, Recipient::getGroupId);
|
||||
LiveData<Set<Recipient>> groupMembers = Transformations.switchMap(group, g -> {
|
||||
//noinspection CodeBlock2Expr
|
||||
return g.map(this::getSessionGroupRecipients)
|
||||
.orElseGet(() -> new DefaultValueLiveData<>(Collections.emptySet()));
|
||||
});
|
||||
@NonNull Observable<Map<RecipientId, NameColor>> getNameColorsMap() {
|
||||
return recipientId.map(Recipient::resolved)
|
||||
.map(Recipient::getGroupId)
|
||||
.map(groupId -> {
|
||||
if (groupId.isPresent()) {
|
||||
List<Recipient> fullMembers = SignalDatabase.groups().getGroupMembers(groupId.get(), GroupDatabase.MemberSet.FULL_MEMBERS_INCLUDING_SELF);
|
||||
Set<Recipient> cachedMembers = MapUtil.getOrDefault(sessionMemberCache, groupId.get(), new HashSet<>());
|
||||
cachedMembers.addAll(fullMembers);
|
||||
sessionMemberCache.put(groupId.get(), cachedMembers);
|
||||
return cachedMembers;
|
||||
} else {
|
||||
return Collections.<Recipient>emptySet();
|
||||
}
|
||||
})
|
||||
.map(members -> {
|
||||
List<Recipient> sorted = Stream.of(members)
|
||||
.filter(member -> !Objects.equals(member, Recipient.self()))
|
||||
.sortBy(Recipient::requireStringId)
|
||||
.toList();
|
||||
|
||||
return Transformations.map(groupMembers, members -> {
|
||||
List<Recipient> sorted = Stream.of(members)
|
||||
.filter(member -> !Objects.equals(member, Recipient.self()))
|
||||
.sortBy(Recipient::requireStringId)
|
||||
.toList();
|
||||
List<NameColor> names = ChatColorsPalette.Names.getAll();
|
||||
Map<RecipientId, NameColor> colors = new HashMap<>();
|
||||
for (int i = 0; i < sorted.size(); i++) {
|
||||
colors.put(sorted.get(i).getId(), names.get(i % names.size()));
|
||||
}
|
||||
|
||||
List<NameColor> names = ChatColorsPalette.Names.getAll();
|
||||
Map<RecipientId, NameColor> colors = new HashMap<>();
|
||||
for (int i = 0; i < sorted.size(); i++) {
|
||||
colors.put(sorted.get(i).getId(), names.get(i % names.size()));
|
||||
}
|
||||
|
||||
return colors;
|
||||
});
|
||||
}
|
||||
|
||||
private @NonNull LiveData<Set<Recipient>> getSessionGroupRecipients(@NonNull GroupId groupId) {
|
||||
LiveData<List<Recipient>> fullMembers = Transformations.map(new LiveGroup(groupId).getFullMembers(),
|
||||
members -> Stream.of(members)
|
||||
.map(GroupMemberEntry.FullMember::getMember)
|
||||
.toList());
|
||||
|
||||
return Transformations.map(fullMembers, currentMembership -> {
|
||||
Set<Recipient> cachedMembers = MapUtil.getOrDefault(sessionMemberCache, groupId, new HashSet<>());
|
||||
cachedMembers.addAll(currentMembership);
|
||||
sessionMemberCache.put(groupId, cachedMembers);
|
||||
return cachedMembers;
|
||||
});
|
||||
return colors;
|
||||
})
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread());
|
||||
}
|
||||
|
||||
@NonNull LiveData<Optional<NotificationProfile>> getActiveNotificationProfile() {
|
||||
|
@ -368,14 +372,6 @@ public class ConversationViewModel extends ViewModel {
|
|||
return LiveDataReactiveStreams.fromPublisher(activeProfile.toFlowable(BackpressureStrategy.LATEST));
|
||||
}
|
||||
|
||||
long getLastSeen() {
|
||||
return conversationMetadata.getValue() != null ? conversationMetadata.getValue().getLastSeen() : 0;
|
||||
}
|
||||
|
||||
int getLastSeenPosition() {
|
||||
return conversationMetadata.getValue() != null ? conversationMetadata.getValue().getLastSeenPosition() : 0;
|
||||
}
|
||||
|
||||
void setArgs(@NonNull ConversationIntents.Args args) {
|
||||
this.args = args;
|
||||
}
|
||||
|
@ -403,14 +399,21 @@ public class ConversationViewModel extends ViewModel {
|
|||
SHOW_RECAPTCHA
|
||||
}
|
||||
|
||||
private static class ThreadAndRecipient {
|
||||
static class MessageData {
|
||||
private final List<ConversationMessage> messages;
|
||||
private final ConversationData metadata;
|
||||
|
||||
private final long threadId;
|
||||
private final Recipient recipient;
|
||||
MessageData(@NonNull ConversationData metadata, @NonNull List<ConversationMessage> messages) {
|
||||
this.metadata = metadata;
|
||||
this.messages = messages;
|
||||
}
|
||||
|
||||
public ThreadAndRecipient(long threadId, Recipient recipient) {
|
||||
this.threadId = threadId;
|
||||
this.recipient = recipient;
|
||||
public @NonNull List<ConversationMessage> getMessages() {
|
||||
return messages;
|
||||
}
|
||||
|
||||
public @NonNull ConversationData getMetadata() {
|
||||
return metadata;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -11,6 +11,7 @@ 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;
|
||||
|
@ -56,24 +57,24 @@ class ConversationListViewModel extends ViewModel {
|
|||
|
||||
private static boolean coldStart = true;
|
||||
|
||||
private final MutableLiveData<Megaphone> megaphone;
|
||||
private final MutableLiveData<SearchResult> searchResult;
|
||||
private final MutableLiveData<ConversationSet> selectedConversations;
|
||||
private final Set<Conversation> internalSelection;
|
||||
private final ConversationListDataSource conversationListDataSource;
|
||||
private final PagedData<Long, Conversation> pagedData;
|
||||
private final LiveData<Boolean> hasNoConversations;
|
||||
private final SearchRepository searchRepository;
|
||||
private final MegaphoneRepository megaphoneRepository;
|
||||
private final Debouncer messageSearchDebouncer;
|
||||
private final Debouncer contactSearchDebouncer;
|
||||
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 final MutableLiveData<Megaphone> megaphone;
|
||||
private final MutableLiveData<SearchResult> searchResult;
|
||||
private final MutableLiveData<ConversationSet> selectedConversations;
|
||||
private final Set<Conversation> internalSelection;
|
||||
private final ConversationListDataSource conversationListDataSource;
|
||||
private final LivePagedData<Long, Conversation> pagedData;
|
||||
private final LiveData<Boolean> hasNoConversations;
|
||||
private final SearchRepository searchRepository;
|
||||
private final MegaphoneRepository megaphoneRepository;
|
||||
private final Debouncer messageSearchDebouncer;
|
||||
private final Debouncer contactSearchDebouncer;
|
||||
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 String activeQuery;
|
||||
private SearchResult activeSearchResult;
|
||||
|
@ -95,8 +96,8 @@ class ConversationListViewModel extends ViewModel {
|
|||
this.invalidator = new Invalidator();
|
||||
this.disposables = new CompositeDisposable();
|
||||
this.conversationListDataSource = ConversationListDataSource.create(application, isArchived);
|
||||
this.pagedData = PagedData.create(conversationListDataSource,
|
||||
new PagingConfig.Builder()
|
||||
this.pagedData = PagedData.createForLiveData(conversationListDataSource,
|
||||
new PagingConfig.Builder()
|
||||
.setPageSize(15)
|
||||
.setBufferPages(2)
|
||||
.build());
|
||||
|
|
|
@ -1379,10 +1379,10 @@ open class RecipientDatabase(context: Context, databaseHelper: SignalDatabase) :
|
|||
return DeviceLastResetTime.newBuilder().build()
|
||||
}
|
||||
|
||||
fun setBadges(id: RecipientId, badges: List<Badge?>) {
|
||||
fun setBadges(id: RecipientId, badges: List<Badge>) {
|
||||
val badgeListBuilder = BadgeList.newBuilder()
|
||||
for (badge in badges) {
|
||||
badgeListBuilder.addBadges(toDatabaseBadge(badge!!))
|
||||
badgeListBuilder.addBadges(toDatabaseBadge(badge))
|
||||
}
|
||||
|
||||
val values = ContentValues(1).apply {
|
||||
|
@ -1390,7 +1390,6 @@ open class RecipientDatabase(context: Context, databaseHelper: SignalDatabase) :
|
|||
}
|
||||
|
||||
if (update(id, values)) {
|
||||
ApplicationDependencies.getDatabaseObserver().notifyNotificationProfileObservers()
|
||||
ApplicationDependencies.getDatabaseObserver().notifyRecipientChanged(id)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -12,6 +12,7 @@ import androidx.lifecycle.ViewModelProvider;
|
|||
|
||||
import com.annimon.stream.Stream;
|
||||
|
||||
import org.signal.paging.LivePagedData;
|
||||
import org.signal.paging.PagedData;
|
||||
import org.signal.paging.PagingConfig;
|
||||
import org.signal.paging.PagingController;
|
||||
|
@ -28,12 +29,12 @@ import java.util.Objects;
|
|||
*/
|
||||
public final class GiphyMp4ViewModel extends ViewModel {
|
||||
|
||||
private final GiphyMp4Repository repository;
|
||||
private final MutableLiveData<PagedData<String, GiphyImage>> pagedData;
|
||||
private final LiveData<MappingModelList> images;
|
||||
private final LiveData<PagingController<String>> pagingController;
|
||||
private final SingleLiveEvent<GiphyMp4SaveResult> saveResultEvents;
|
||||
private final boolean isForMms;
|
||||
private final GiphyMp4Repository repository;
|
||||
private final MutableLiveData<LivePagedData<String, GiphyImage>> pagedData;
|
||||
private final LiveData<MappingModelList> images;
|
||||
private final LiveData<PagingController<String>> pagingController;
|
||||
private final SingleLiveEvent<GiphyMp4SaveResult> saveResultEvents;
|
||||
private final boolean isForMms;
|
||||
|
||||
private String query;
|
||||
|
||||
|
@ -52,7 +53,7 @@ public final class GiphyMp4ViewModel extends ViewModel {
|
|||
.collect(MappingModelList.toMappingModelList())));
|
||||
}
|
||||
|
||||
LiveData<PagedData<String, GiphyImage>> getPagedData() {
|
||||
LiveData<LivePagedData<String, GiphyImage>> getPagedData() {
|
||||
return pagedData;
|
||||
}
|
||||
|
||||
|
@ -81,9 +82,9 @@ public final class GiphyMp4ViewModel extends ViewModel {
|
|||
return pagingController;
|
||||
}
|
||||
|
||||
private PagedData<String, GiphyImage> getGiphyImagePagedData(@Nullable String query) {
|
||||
return PagedData.create(new GiphyMp4PagedDataSource(query),
|
||||
new PagingConfig.Builder().setPageSize(20)
|
||||
private LivePagedData<String, GiphyImage> getGiphyImagePagedData(@Nullable String query) {
|
||||
return PagedData.createForLiveData(new GiphyMp4PagedDataSource(query),
|
||||
new PagingConfig.Builder().setPageSize(20)
|
||||
.setBufferPages(1)
|
||||
.build());
|
||||
}
|
||||
|
|
|
@ -10,6 +10,7 @@ import androidx.lifecycle.ViewModelProvider;
|
|||
import org.signal.core.util.ThreadUtil;
|
||||
import org.signal.core.util.logging.Log;
|
||||
import org.signal.core.util.tracing.Tracer;
|
||||
import org.signal.paging.LivePagedData;
|
||||
import org.signal.paging.PagedData;
|
||||
import org.signal.paging.PagingConfig;
|
||||
import org.signal.paging.PagingController;
|
||||
|
@ -53,7 +54,7 @@ public class SubmitDebugLogViewModel extends ViewModel {
|
|||
.setStartIndex(0)
|
||||
.build();
|
||||
|
||||
PagedData<Long, LogLine> pagedData = PagedData.create(dataSource, config);
|
||||
LivePagedData<Long, LogLine> pagedData = PagedData.createForLiveData(dataSource, config);
|
||||
|
||||
ThreadUtil.runOnMain(() -> {
|
||||
pagingController.set(pagedData.getController());
|
||||
|
|
|
@ -155,7 +155,6 @@ public class MessageRequestViewModel extends ViewModel {
|
|||
private void loadRecipient() {
|
||||
liveRecipient.observeForever(recipientObserver);
|
||||
SignalExecutors.BOUNDED.execute(() -> {
|
||||
liveRecipient.refresh();
|
||||
recipient.postValue(liveRecipient.get());
|
||||
});
|
||||
}
|
||||
|
|
|
@ -3,6 +3,7 @@ package org.thoughtcrime.securesms.stories.viewer.reply.group
|
|||
import io.reactivex.rxjava3.core.Observable
|
||||
import io.reactivex.rxjava3.core.Single
|
||||
import io.reactivex.rxjava3.schedulers.Schedulers
|
||||
import org.signal.paging.LivePagedData
|
||||
import org.signal.paging.PagedData
|
||||
import org.signal.paging.PagingConfig
|
||||
import org.thoughtcrime.securesms.database.DatabaseObserver
|
||||
|
@ -12,10 +13,10 @@ import org.thoughtcrime.securesms.recipients.RecipientId
|
|||
|
||||
class StoryGroupReplyRepository {
|
||||
|
||||
fun getPagedReplies(parentStoryId: Long): Observable<PagedData<StoryGroupReplyItemData.Key, StoryGroupReplyItemData>> {
|
||||
return Observable.create<PagedData<StoryGroupReplyItemData.Key, StoryGroupReplyItemData>> { emitter ->
|
||||
fun getPagedReplies(parentStoryId: Long): Observable<LivePagedData<StoryGroupReplyItemData.Key, StoryGroupReplyItemData>> {
|
||||
return Observable.create<LivePagedData<StoryGroupReplyItemData.Key, StoryGroupReplyItemData>> { emitter ->
|
||||
fun refresh() {
|
||||
emitter.onNext(PagedData.create(StoryGroupReplyDataSource(parentStoryId), PagingConfig.Builder().build()))
|
||||
emitter.onNext(PagedData.createForLiveData(StoryGroupReplyDataSource(parentStoryId), PagingConfig.Builder().build()))
|
||||
}
|
||||
|
||||
val observer = DatabaseObserver.Observer {
|
||||
|
|
|
@ -8,7 +8,7 @@ import androidx.lifecycle.ViewModelProvider
|
|||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
||||
import io.reactivex.rxjava3.disposables.CompositeDisposable
|
||||
import io.reactivex.rxjava3.kotlin.plusAssign
|
||||
import org.signal.paging.PagedData
|
||||
import org.signal.paging.LivePagedData
|
||||
import org.signal.paging.PagingController
|
||||
import org.thoughtcrime.securesms.conversation.colors.NameColors
|
||||
import org.thoughtcrime.securesms.groups.GroupId
|
||||
|
@ -23,7 +23,7 @@ class StoryGroupReplyViewModel(storyId: Long, repository: StoryGroupReplyReposit
|
|||
|
||||
val state: LiveData<StoryGroupReplyState> = store.stateLiveData
|
||||
|
||||
private val pagedData: MutableLiveData<PagedData<StoryGroupReplyItemData.Key, StoryGroupReplyItemData>> = MutableLiveData()
|
||||
private val pagedData: MutableLiveData<LivePagedData<StoryGroupReplyItemData.Key, StoryGroupReplyItemData>> = MutableLiveData()
|
||||
|
||||
val pagingController: LiveData<PagingController<StoryGroupReplyItemData.Key>>
|
||||
val pageData: LiveData<List<StoryGroupReplyItemData>>
|
||||
|
|
|
@ -68,8 +68,8 @@ object LocalMetrics {
|
|||
|
||||
executor.execute {
|
||||
val lastTime: Long? = lastSplitTimeById[id]
|
||||
|
||||
if (lastTime != null) {
|
||||
val splitDoesNotExist: Boolean = eventsById[id]?.splits?.none { it.name == split } ?: true
|
||||
if (lastTime != null && splitDoesNotExist) {
|
||||
eventsById[id]?.splits?.add(LocalMetricsSplit(split, time - lastTime))
|
||||
lastSplitTimeById[id] = time
|
||||
}
|
||||
|
|
|
@ -72,8 +72,10 @@ public final class SignalLocalMetrics {
|
|||
public static final class ConversationOpen {
|
||||
private static final String NAME = "conversation-open";
|
||||
|
||||
private static final String SPLIT_DATA_LOADED = "data-loaded";
|
||||
private static final String SPLIT_RENDER = "render";
|
||||
private static final String SPLIT_VIEWMODEL_INIT = "viewmodel-init";
|
||||
private static final String SPLIT_METADATA_LOADED = "metadata-loaded";
|
||||
private static final String SPLIT_DATA_LOADED = "data-loaded";
|
||||
private static final String SPLIT_RENDER = "render";
|
||||
|
||||
private static String id;
|
||||
|
||||
|
@ -82,6 +84,14 @@ public final class SignalLocalMetrics {
|
|||
LocalMetrics.getInstance().start(id, NAME);
|
||||
}
|
||||
|
||||
public static void onMetadataLoadStarted() {
|
||||
LocalMetrics.getInstance().split(id, SPLIT_VIEWMODEL_INIT);
|
||||
}
|
||||
|
||||
public static void onMetadataLoaded() {
|
||||
LocalMetrics.getInstance().split(id, SPLIT_METADATA_LOADED);
|
||||
}
|
||||
|
||||
public static void onDataLoaded() {
|
||||
LocalMetrics.getInstance().split(id, SPLIT_DATA_LOADED);
|
||||
}
|
||||
|
|
|
@ -4,6 +4,7 @@ import androidx.annotation.AnyThread;
|
|||
import androidx.annotation.MainThread;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.lifecycle.LiveData;
|
||||
import androidx.lifecycle.LiveDataReactiveStreams;
|
||||
import androidx.lifecycle.MediatorLiveData;
|
||||
|
||||
import com.annimon.stream.function.Function;
|
||||
|
@ -15,6 +16,9 @@ import java.util.HashSet;
|
|||
import java.util.Set;
|
||||
import java.util.concurrent.Executor;
|
||||
|
||||
import io.reactivex.rxjava3.core.BackpressureStrategy;
|
||||
import io.reactivex.rxjava3.core.Observable;
|
||||
|
||||
/**
|
||||
* Manages a state to be updated from a view model and provide direct and live access. Updates
|
||||
* occur serially on the same executor to allow updating in a thread safe way. While not
|
||||
|
@ -46,6 +50,11 @@ public class Store<State> {
|
|||
liveStore.update(source, action);
|
||||
}
|
||||
|
||||
@MainThread
|
||||
public <Input> void update(@NonNull Observable<Input> source, @NonNull Action<Input, State> action) {
|
||||
liveStore.update(LiveDataReactiveStreams.fromPublisher(source.toFlowable(BackpressureStrategy.LATEST)), action);
|
||||
}
|
||||
|
||||
@MainThread
|
||||
public void clear() {
|
||||
liveStore.clear();
|
||||
|
|
|
@ -2302,6 +2302,11 @@ https://docs.gradle.org/current/userguide/dependency_verification.html
|
|||
<sha256 value="5b3582a1e9fd9e9037ee933ab9486ff323d6244b5f1b6ff6ebadc2bfa957ce5b" origin="Generated by Gradle"/>
|
||||
</artifact>
|
||||
</component>
|
||||
<component group="io.reactivex.rxjava3" name="rxjava" version="3.0.0">
|
||||
<artifact name="rxjava-3.0.0.jar">
|
||||
<sha256 value="dd1eebeb292ddc40429e5b3e72f6a61facbec21ad5493f9f54ffb2bdf254bf57" origin="Generated by Gradle"/>
|
||||
</artifact>
|
||||
</component>
|
||||
<component group="io.reactivex.rxjava3" name="rxjava" version="3.0.13">
|
||||
<artifact name="rxjava-3.0.13.jar">
|
||||
<sha256 value="598abaf71dbc970dd0727e6d5f4f786dc999df5b972cbf261316a32e155b2c69" origin="Generated by Gradle"/>
|
||||
|
|
|
@ -4,6 +4,7 @@ import androidx.annotation.NonNull;
|
|||
import androidx.lifecycle.LiveData;
|
||||
import androidx.lifecycle.ViewModel;
|
||||
|
||||
import org.signal.paging.LivePagedData;
|
||||
import org.signal.paging.PagingController;
|
||||
import org.signal.paging.PagingConfig;
|
||||
import org.signal.paging.PagedData;
|
||||
|
@ -12,14 +13,14 @@ import java.util.List;
|
|||
|
||||
public class MainViewModel extends ViewModel {
|
||||
|
||||
private final PagedData<String, Item> pagedData;
|
||||
private final MainDataSource dataSource;
|
||||
private final LivePagedData<String, Item> pagedData;
|
||||
private final MainDataSource dataSource;
|
||||
|
||||
public MainViewModel() {
|
||||
this.dataSource = new MainDataSource(1000);
|
||||
this.pagedData = PagedData.create(dataSource, new PagingConfig.Builder().setBufferPages(3)
|
||||
.setPageSize(25)
|
||||
.build());
|
||||
this.pagedData = PagedData.createForLiveData(dataSource, new PagingConfig.Builder().setBufferPages(3)
|
||||
.setPageSize(25)
|
||||
.build());
|
||||
}
|
||||
|
||||
public void onItemClicked(@NonNull String key) {
|
||||
|
|
|
@ -18,6 +18,8 @@ android {
|
|||
dependencies {
|
||||
implementation libs.androidx.appcompat
|
||||
implementation libs.material.material
|
||||
implementation libs.rxjava3.rxandroid
|
||||
implementation libs.rxjava3.rxjava
|
||||
implementation project(':core-util')
|
||||
testImplementation testLibs.junit.junit
|
||||
}
|
|
@ -1,9 +1,7 @@
|
|||
package org.signal.paging;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.lifecycle.MutableLiveData;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.concurrent.Executor;
|
||||
import java.util.concurrent.Executors;
|
||||
|
||||
|
@ -24,16 +22,19 @@ class BufferedPagingController<Key, Data> implements PagingController<Key> {
|
|||
|
||||
private final PagedDataSource<Key, Data> dataSource;
|
||||
private final PagingConfig config;
|
||||
private final MutableLiveData<List<Data>> liveData;
|
||||
private final DataStream<Data> dataStream;
|
||||
private final Executor serializationExecutor;
|
||||
|
||||
private PagingController<Key> activeController;
|
||||
private int lastRequestedIndex;
|
||||
|
||||
BufferedPagingController(PagedDataSource<Key, Data> dataSource, PagingConfig config, @NonNull MutableLiveData<List<Data>> liveData) {
|
||||
BufferedPagingController(@NonNull PagedDataSource<Key, Data> dataSource,
|
||||
@NonNull PagingConfig config,
|
||||
@NonNull DataStream<Data> dataStream)
|
||||
{
|
||||
this.dataSource = dataSource;
|
||||
this.config = config;
|
||||
this.liveData = liveData;
|
||||
this.dataStream = dataStream;
|
||||
this.serializationExecutor = Executors.newSingleThreadExecutor();
|
||||
|
||||
this.activeController = null;
|
||||
|
@ -57,7 +58,7 @@ class BufferedPagingController<Key, Data> implements PagingController<Key> {
|
|||
activeController.onDataInvalidated();
|
||||
}
|
||||
|
||||
activeController = new FixedSizePagingController<>(dataSource, config, liveData, dataSource.size());
|
||||
activeController = new FixedSizePagingController<>(dataSource, config, dataStream, dataSource.size());
|
||||
activeController.onDataNeededAroundIndex(lastRequestedIndex);
|
||||
});
|
||||
}
|
||||
|
|
10
paging/lib/src/main/java/org/signal/paging/DataStream.java
Normal file
10
paging/lib/src/main/java/org/signal/paging/DataStream.java
Normal file
|
@ -0,0 +1,10 @@
|
|||
package org.signal.paging;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* An abstraction over different types of ways the paging lib can provide data, e.g. Observables vs LiveData.
|
||||
*/
|
||||
interface DataStream<Data> {
|
||||
void next(List<Data> data);
|
||||
}
|
|
@ -1,7 +1,6 @@
|
|||
package org.signal.paging;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.lifecycle.MutableLiveData;
|
||||
|
||||
import org.signal.core.util.concurrent.SignalExecutors;
|
||||
import org.signal.core.util.logging.Log;
|
||||
|
@ -29,7 +28,7 @@ class FixedSizePagingController<Key, Data> implements PagingController<Key> {
|
|||
|
||||
private final PagedDataSource<Key, Data> dataSource;
|
||||
private final PagingConfig config;
|
||||
private final MutableLiveData<List<Data>> liveData;
|
||||
private final DataStream<Data> dataStream;
|
||||
private final DataStatus loadState;
|
||||
private final Map<Key, Integer> keyToPosition;
|
||||
|
||||
|
@ -39,15 +38,17 @@ class FixedSizePagingController<Key, Data> implements PagingController<Key> {
|
|||
|
||||
FixedSizePagingController(@NonNull PagedDataSource<Key, Data> dataSource,
|
||||
@NonNull PagingConfig config,
|
||||
@NonNull MutableLiveData<List<Data>> liveData,
|
||||
@NonNull DataStream<Data> dataStream,
|
||||
int size)
|
||||
{
|
||||
this.dataSource = dataSource;
|
||||
this.config = config;
|
||||
this.liveData = liveData;
|
||||
this.dataStream = dataStream;
|
||||
this.loadState = DataStatus.obtain(size);
|
||||
this.data = new CompressedList<>(loadState.size());
|
||||
this.keyToPosition = new HashMap<>();
|
||||
|
||||
if (DEBUG) Log.d(TAG, "[Constructor] Creating with size " + size + " (loadState.size() = " + loadState.size() + ")");
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -58,7 +59,7 @@ class FixedSizePagingController<Key, Data> implements PagingController<Key> {
|
|||
@Override
|
||||
public void onDataNeededAroundIndex(int aroundIndex) {
|
||||
if (invalidated) {
|
||||
Log.w(TAG, buildLog(aroundIndex, "Invalidated! At very beginning."));
|
||||
Log.w(TAG, buildDataNeededLog(aroundIndex, "Invalidated! At very beginning."));
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -67,7 +68,7 @@ class FixedSizePagingController<Key, Data> implements PagingController<Key> {
|
|||
|
||||
synchronized (loadState) {
|
||||
if (loadState.size() == 0) {
|
||||
liveData.postValue(Collections.emptyList());
|
||||
dataStream.next(Collections.emptyList());
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -81,14 +82,14 @@ class FixedSizePagingController<Key, Data> implements PagingController<Key> {
|
|||
loadStart = loadState.getEarliestUnmarkedIndexInRange(leftLoadBoundary, rightLoadBoundary);
|
||||
|
||||
if (loadStart < 0) {
|
||||
if (DEBUG) Log.i(TAG, buildLog(aroundIndex, "loadStart < 0"));
|
||||
if (DEBUG) Log.i(TAG, buildDataNeededLog(aroundIndex, "loadStart < 0"));
|
||||
return;
|
||||
}
|
||||
|
||||
loadEnd = loadState.getLatestUnmarkedIndexInRange(Math.max(leftLoadBoundary, loadStart), rightLoadBoundary) + 1;
|
||||
|
||||
if (loadEnd <= loadStart) {
|
||||
if (DEBUG) Log.i(TAG, buildLog(aroundIndex, "loadEnd <= loadStart, loadEnd: " + loadEnd + ", loadStart: " + loadStart));
|
||||
if (DEBUG) Log.i(TAG, buildDataNeededLog(aroundIndex, "loadEnd <= loadStart, loadEnd: " + loadEnd + ", loadStart: " + loadStart));
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -96,19 +97,19 @@ class FixedSizePagingController<Key, Data> implements PagingController<Key> {
|
|||
|
||||
loadState.markRange(loadStart, loadEnd);
|
||||
|
||||
if (DEBUG) Log.i(TAG, buildLog(aroundIndex, "start: " + loadStart + ", end: " + loadEnd + ", totalSize: " + totalSize));
|
||||
if (DEBUG) Log.i(TAG, buildDataNeededLog(aroundIndex, "start: " + loadStart + ", end: " + loadEnd + ", totalSize: " + totalSize));
|
||||
}
|
||||
|
||||
FETCH_EXECUTOR.execute(() -> {
|
||||
if (invalidated) {
|
||||
Log.w(TAG, buildLog(aroundIndex, "Invalidated! At beginning of load task."));
|
||||
Log.w(TAG, buildDataNeededLog(aroundIndex, "Invalidated! At beginning of load task."));
|
||||
return;
|
||||
}
|
||||
|
||||
List<Data> loaded = dataSource.load(loadStart, loadEnd - loadStart, () -> invalidated);
|
||||
|
||||
if (invalidated) {
|
||||
Log.w(TAG, buildLog(aroundIndex, "Invalidated! Just after data was loaded."));
|
||||
Log.w(TAG, buildDataNeededLog(aroundIndex, "Invalidated! Just after data was loaded."));
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -123,7 +124,7 @@ class FixedSizePagingController<Key, Data> implements PagingController<Key> {
|
|||
}
|
||||
|
||||
data = updated;
|
||||
liveData.postValue(updated);
|
||||
dataStream.next(updated);
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -139,6 +140,8 @@ class FixedSizePagingController<Key, Data> implements PagingController<Key> {
|
|||
|
||||
@Override
|
||||
public void onDataItemChanged(Key key) {
|
||||
if (DEBUG) Log.d(TAG, buildItemChangedLog(key, ""));
|
||||
|
||||
FETCH_EXECUTOR.execute(() -> {
|
||||
Integer position = keyToPosition.get(key);
|
||||
|
||||
|
@ -172,12 +175,16 @@ class FixedSizePagingController<Key, Data> implements PagingController<Key> {
|
|||
|
||||
updatedList.set(position, item);
|
||||
data = updatedList;
|
||||
liveData.postValue(updatedList);
|
||||
dataStream.next(updatedList);
|
||||
|
||||
if (DEBUG) Log.d(TAG, buildItemChangedLog(key, "Published updated data"));
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDataItemInserted(Key key, int position) {
|
||||
if (DEBUG) Log.d(TAG, buildItemInsertedLog(key, position, ""));
|
||||
|
||||
FETCH_EXECUTOR.execute(() -> {
|
||||
if (keyToPosition.containsKey(key)) {
|
||||
Log.w(TAG, "Notified of key " + key + " being inserted at " + position + ", but the item already exists!");
|
||||
|
@ -191,6 +198,7 @@ class FixedSizePagingController<Key, Data> implements PagingController<Key> {
|
|||
|
||||
synchronized (loadState) {
|
||||
loadState.insertState(position, true);
|
||||
if (DEBUG) Log.d(TAG, buildItemInsertedLog(key, position, "Size of loadState updated to " + loadState.size()));
|
||||
}
|
||||
|
||||
Data item = dataSource.load(key);
|
||||
|
@ -211,7 +219,9 @@ class FixedSizePagingController<Key, Data> implements PagingController<Key> {
|
|||
rebuildKeyToPositionMap(keyToPosition, updatedList, dataSource);
|
||||
|
||||
data = updatedList;
|
||||
liveData.postValue(updatedList);
|
||||
dataStream.next(updatedList);
|
||||
|
||||
if (DEBUG) Log.d(TAG, buildItemInsertedLog(key, position, "Published updated data"));
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -226,7 +236,15 @@ class FixedSizePagingController<Key, Data> implements PagingController<Key> {
|
|||
}
|
||||
}
|
||||
|
||||
private static String buildLog(int aroundIndex, String message) {
|
||||
return "onDataNeededAroundIndex(" + aroundIndex + ") " + message;
|
||||
private String buildDataNeededLog(int aroundIndex, String message) {
|
||||
return "[onDataNeededAroundIndex(" + aroundIndex + "), size: " + loadState.size() + "] " + message;
|
||||
}
|
||||
|
||||
private String buildItemInsertedLog(Key key, int position, String message) {
|
||||
return "[onDataItemInserted(" + key + ", " + position + "), size: " + loadState.size() + "] " + message;
|
||||
}
|
||||
|
||||
private String buildItemChangedLog(Key key, String message) {
|
||||
return "[onDataItemInserted(" + key + "), size: " + loadState.size() + "] " + message;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,25 @@
|
|||
package org.signal.paging;
|
||||
|
||||
import androidx.annotation.AnyThread;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.lifecycle.LiveData;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* An implementation of {@link PagedData} that will provide data as a {@link LiveData}.
|
||||
*/
|
||||
public class LivePagedData<Key, Data> extends PagedData<Key> {
|
||||
|
||||
private final LiveData<List<Data>> data;
|
||||
|
||||
LivePagedData(@NonNull LiveData<List<Data>> data, @NonNull PagingController<Key> controller) {
|
||||
super(controller);
|
||||
this.data = data;
|
||||
}
|
||||
|
||||
@AnyThread
|
||||
public @NonNull LiveData<List<Data>> getData() {
|
||||
return data;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,27 @@
|
|||
package org.signal.paging;
|
||||
|
||||
import androidx.annotation.AnyThread;
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import io.reactivex.rxjava3.core.Observable;
|
||||
import io.reactivex.rxjava3.subjects.Subject;
|
||||
|
||||
/**
|
||||
* An implementation of {@link PagedData} that will provide data as an {@link Observable}.
|
||||
*/
|
||||
public class ObservablePagedData<Key, Data> extends PagedData<Key> {
|
||||
|
||||
private final Observable<List<Data>> data;
|
||||
|
||||
ObservablePagedData(@NonNull Observable<List<Data>> data, @NonNull PagingController<Key> controller) {
|
||||
super(controller);
|
||||
this.data = data;
|
||||
}
|
||||
|
||||
@AnyThread
|
||||
public @NonNull Observable<List<Data>> getData() {
|
||||
return data;
|
||||
}
|
||||
}
|
|
@ -2,39 +2,41 @@ package org.signal.paging;
|
|||
|
||||
import androidx.annotation.AnyThread;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.lifecycle.LiveData;
|
||||
import androidx.lifecycle.MutableLiveData;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import io.reactivex.rxjava3.subjects.BehaviorSubject;
|
||||
import io.reactivex.rxjava3.subjects.Subject;
|
||||
|
||||
/**
|
||||
* The primary entry point for creating paged data.
|
||||
*/
|
||||
public final class PagedData<Key, Data> {
|
||||
public class PagedData<Key> {
|
||||
|
||||
private final LiveData<List<Data>> data;
|
||||
private final PagingController<Key> controller;
|
||||
|
||||
@AnyThread
|
||||
public static <Key, Data> PagedData<Key, Data> create(@NonNull PagedDataSource<Key, Data> dataSource, @NonNull PagingConfig config) {
|
||||
MutableLiveData<List<Data>> liveData = new MutableLiveData<>();
|
||||
PagingController<Key> controller = new BufferedPagingController<>(dataSource, config, liveData);
|
||||
|
||||
return new PagedData<>(liveData, controller);
|
||||
}
|
||||
|
||||
private PagedData(@NonNull LiveData<List<Data>> data, @NonNull PagingController<Key> controller) {
|
||||
this.data = data;
|
||||
protected PagedData(PagingController<Key> controller) {
|
||||
this.controller = controller;
|
||||
}
|
||||
|
||||
@AnyThread
|
||||
public @NonNull LiveData<List<Data>> getData() {
|
||||
return data;
|
||||
public static <Key, Data> LivePagedData<Key, Data> createForLiveData(@NonNull PagedDataSource<Key, Data> dataSource, @NonNull PagingConfig config) {
|
||||
MutableLiveData<List<Data>> liveData = new MutableLiveData<>();
|
||||
PagingController<Key> controller = new BufferedPagingController<>(dataSource, config, liveData::postValue);
|
||||
|
||||
return new LivePagedData<>(liveData, controller);
|
||||
}
|
||||
|
||||
@AnyThread
|
||||
public @NonNull PagingController<Key> getController() {
|
||||
public static <Key, Data> ObservablePagedData<Key, Data> createForObservable(@NonNull PagedDataSource<Key, Data> dataSource, @NonNull PagingConfig config) {
|
||||
Subject<List<Data>> subject = BehaviorSubject.create();
|
||||
PagingController<Key> controller = new BufferedPagingController<>(dataSource, config, subject::onNext);
|
||||
|
||||
return new ObservablePagedData<>(subject, controller);
|
||||
}
|
||||
|
||||
public PagingController<Key> getController() {
|
||||
return controller;
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue