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