mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-02-15 07:28:30 +00:00
Flesh out event listeners and add load sequencing to CFV2.
This commit is contained in:
@@ -108,7 +108,7 @@ public interface BindableConversationItem extends Unbindable, GiphyMp4Playable,
|
||||
void onInviteToSignalClicked();
|
||||
void onActivatePaymentsClicked();
|
||||
void onSendPaymentClicked(@NonNull RecipientId recipientId);
|
||||
void onScheduledIndicatorClicked(@NonNull View view, @NonNull MessageRecord messageRecord);
|
||||
void onScheduledIndicatorClicked(@NonNull View view, @NonNull ConversationMessage conversationMessage);
|
||||
/** @return true if handled, false if you want to let the normal url handling continue */
|
||||
boolean onUrlClicked(@NonNull String url);
|
||||
void onViewGiftBadgeClicked(@NonNull MessageRecord messageRecord);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package org.thoughtcrime.securesms.components
|
||||
|
||||
import android.view.View
|
||||
import androidx.annotation.AnyThread
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
||||
@@ -36,7 +37,6 @@ class ScrollToPositionDelegate private constructor(
|
||||
private val EMPTY = ScrollToPositionRequest(
|
||||
position = NO_POSITION,
|
||||
smooth = true,
|
||||
awaitLayout = true,
|
||||
scrollStrategy = DefaultScrollStrategy
|
||||
)
|
||||
}
|
||||
@@ -57,14 +57,8 @@ class ScrollToPositionDelegate private constructor(
|
||||
.filter { it.position >= 0 && canJumpToPosition(it.position) }
|
||||
.map { it.copy(position = mapToTruePosition(it.position)) }
|
||||
.subscribeBy(onNext = { position ->
|
||||
if (position.awaitLayout) {
|
||||
recyclerView.doAfterNextLayout {
|
||||
handleScrollPositionRequest(position, recyclerView)
|
||||
}
|
||||
} else {
|
||||
recyclerView.post {
|
||||
handleScrollPositionRequest(position, recyclerView)
|
||||
}
|
||||
recyclerView.doAfterNextLayout {
|
||||
handleScrollPositionRequest(position, recyclerView)
|
||||
}
|
||||
|
||||
if (!(recyclerView.isLayoutRequested || recyclerView.isInLayout)) {
|
||||
@@ -78,21 +72,21 @@ class ScrollToPositionDelegate private constructor(
|
||||
*
|
||||
* @param position The desired position to jump to. -1 to clear the current request.
|
||||
* @param smooth Whether a smooth scroll will be attempted. Only done if we are within a certain distance.
|
||||
* @param awaitLayout Whether this scroll should await for the next layout to complete before being attempted.
|
||||
* @param scrollStrategy See [ScrollStrategy]
|
||||
*/
|
||||
@AnyThread
|
||||
fun requestScrollPosition(
|
||||
position: Int,
|
||||
smooth: Boolean = true,
|
||||
awaitLayout: Boolean = true,
|
||||
scrollStrategy: ScrollStrategy = DefaultScrollStrategy
|
||||
) {
|
||||
scrollPositionRequested.onNext(ScrollToPositionRequest(position, smooth, awaitLayout, scrollStrategy))
|
||||
scrollPositionRequested.onNext(ScrollToPositionRequest(position, smooth, scrollStrategy))
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset the scroll position to 0
|
||||
*/
|
||||
@AnyThread
|
||||
fun resetScrollPosition() {
|
||||
requestScrollPosition(0, true)
|
||||
}
|
||||
@@ -100,6 +94,7 @@ class ScrollToPositionDelegate private constructor(
|
||||
/**
|
||||
* This should be called every time a list is submitted to the RecyclerView's adapter.
|
||||
*/
|
||||
@AnyThread
|
||||
fun notifyListCommitted() {
|
||||
listCommitted.onNext(Unit)
|
||||
}
|
||||
@@ -135,7 +130,6 @@ class ScrollToPositionDelegate private constructor(
|
||||
private data class ScrollToPositionRequest(
|
||||
val position: Int,
|
||||
val smooth: Boolean,
|
||||
val awaitLayout: Boolean,
|
||||
val scrollStrategy: ScrollStrategy
|
||||
)
|
||||
|
||||
|
||||
@@ -105,8 +105,6 @@ public class ConversationAdapter
|
||||
private final LifecycleOwner lifecycleOwner;
|
||||
private final GlideRequests glideRequests;
|
||||
private final Locale locale;
|
||||
private final Recipient recipient;
|
||||
|
||||
private final Set<MultiselectPart> selected;
|
||||
private final Calendar calendar;
|
||||
|
||||
@@ -129,7 +127,7 @@ public class ConversationAdapter
|
||||
@NonNull GlideRequests glideRequests,
|
||||
@NonNull Locale locale,
|
||||
@Nullable ItemClickListener clickListener,
|
||||
@NonNull Recipient recipient,
|
||||
boolean hasWallpaper,
|
||||
@NonNull Colorizer colorizer)
|
||||
{
|
||||
super(new DiffUtil.ItemCallback<ConversationMessage>() {
|
||||
@@ -150,10 +148,9 @@ public class ConversationAdapter
|
||||
this.glideRequests = glideRequests;
|
||||
this.locale = locale;
|
||||
this.clickListener = clickListener;
|
||||
this.recipient = recipient;
|
||||
this.selected = new HashSet<>();
|
||||
this.calendar = Calendar.getInstance();
|
||||
this.hasWallpaper = recipient.hasWallpaper();
|
||||
this.hasWallpaper = hasWallpaper;
|
||||
this.isMessageRequestAccepted = true;
|
||||
this.colorizer = colorizer;
|
||||
}
|
||||
@@ -292,7 +289,7 @@ public class ConversationAdapter
|
||||
glideRequests,
|
||||
locale,
|
||||
selected,
|
||||
recipient,
|
||||
conversationMessage.getThreadRecipient(),
|
||||
searchQuery,
|
||||
conversationMessage == recordToPulse,
|
||||
hasWallpaper && displayMode.displayWallpaper(),
|
||||
@@ -440,7 +437,8 @@ public class ConversationAdapter
|
||||
}
|
||||
|
||||
public boolean isForRecipientId(@NonNull RecipientId recipientId) {
|
||||
return recipient.getId().equals(recipientId);
|
||||
// TODO [alex] -- This should be fine, since we now have a 1:1 relationship between fragment and recipient.
|
||||
return true;
|
||||
}
|
||||
|
||||
void onBindLastSeenViewHolder(StickyHeaderViewHolder viewHolder, long unreadCount) {
|
||||
@@ -562,7 +560,7 @@ public class ConversationAdapter
|
||||
* Lets the adapter know that the wallpaper state has changed.
|
||||
* @return True if the internal wallpaper state changed, otherwise false.
|
||||
*/
|
||||
boolean onHasWallpaperChanged(boolean hasWallpaper) {
|
||||
public boolean onHasWallpaperChanged(boolean hasWallpaper) {
|
||||
if (this.hasWallpaper != hasWallpaper) {
|
||||
Log.d(TAG, "Resetting adapter due to wallpaper change.");
|
||||
this.hasWallpaper = hasWallpaper;
|
||||
|
||||
@@ -39,6 +39,7 @@ import java.util.HashSet;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import java.util.stream.Collectors;
|
||||
@@ -58,6 +59,8 @@ public class ConversationDataSource implements PagedDataSource<MessageId, Conver
|
||||
/** Used once for the initial fetch, then cleared. */
|
||||
private int baseSize;
|
||||
|
||||
private Recipient threadRecipient;
|
||||
|
||||
public ConversationDataSource(@NonNull Context context, long threadId, @NonNull MessageRequestData messageRequestData, boolean showUniversalExpireTimerUpdate, int baseSize) {
|
||||
this.context = context;
|
||||
this.threadId = threadId;
|
||||
@@ -165,13 +168,15 @@ public class ConversationDataSource implements PagedDataSource<MessageId, Conver
|
||||
records = callHelper.buildUpdatedModels(records);
|
||||
stopwatch.split("call-models");
|
||||
|
||||
ensureThreadRecipient();
|
||||
|
||||
for (ServiceId serviceId : referencedIds) {
|
||||
Recipient.resolved(RecipientId.from(serviceId));
|
||||
}
|
||||
stopwatch.split("recipient-resolves");
|
||||
|
||||
List<ConversationMessage> messages = Stream.of(records)
|
||||
.map(m -> ConversationMessageFactory.createWithUnresolvedData(context, m, m.getDisplayBody(context), mentionHelper.getMentions(m.getId()), quotedHelper.isQuoted(m.getId())))
|
||||
.map(m -> ConversationMessageFactory.createWithUnresolvedData(context, m, m.getDisplayBody(context), mentionHelper.getMentions(m.getId()), quotedHelper.isQuoted(m.getId()), threadRecipient))
|
||||
.toList();
|
||||
|
||||
stopwatch.split("conversion");
|
||||
@@ -229,11 +234,13 @@ public class ConversationDataSource implements PagedDataSource<MessageId, Conver
|
||||
|
||||
stopwatch.split("calls");
|
||||
|
||||
ensureThreadRecipient();
|
||||
return ConversationMessage.ConversationMessageFactory.createWithUnresolvedData(ApplicationDependencies.getApplication(),
|
||||
record,
|
||||
record.getDisplayBody(ApplicationDependencies.getApplication()),
|
||||
mentions,
|
||||
isQuoted);
|
||||
isQuoted,
|
||||
threadRecipient);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
@@ -267,6 +274,12 @@ public class ConversationDataSource implements PagedDataSource<MessageId, Conver
|
||||
}
|
||||
}
|
||||
|
||||
private void ensureThreadRecipient() {
|
||||
if (threadRecipient == null) {
|
||||
threadRecipient = Objects.requireNonNull(SignalDatabase.threads().getRecipientForThreadId(threadId));
|
||||
}
|
||||
}
|
||||
|
||||
private static class QuotedHelper {
|
||||
|
||||
private Collection<MessageRecord> records = new LinkedList<>();
|
||||
|
||||
@@ -112,6 +112,7 @@ import org.thoughtcrime.securesms.conversation.ui.edit.EditMessageHistoryDialog;
|
||||
import org.thoughtcrime.securesms.conversation.ui.error.EnableCallNotificationSettingsDialog;
|
||||
import org.thoughtcrime.securesms.conversation.v2.AddToContactsContract;
|
||||
import org.thoughtcrime.securesms.conversation.v2.BubbleLayoutTransitionListener;
|
||||
import org.thoughtcrime.securesms.conversation.v2.ConversationDialogs;
|
||||
import org.thoughtcrime.securesms.database.DatabaseObserver;
|
||||
import org.thoughtcrime.securesms.database.MessageTable;
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase;
|
||||
@@ -712,7 +713,7 @@ public class ConversationFragment extends LoggingFragment implements Multiselect
|
||||
}
|
||||
|
||||
Log.d(TAG, "Initializing adapter for " + recipient.getId());
|
||||
ConversationAdapter adapter = new ConversationAdapter(requireContext(), this, GlideApp.with(this), locale, selectionClickListener, this.recipient.get(), colorizer);
|
||||
ConversationAdapter adapter = new ConversationAdapter(requireContext(), this, GlideApp.with(this), locale, selectionClickListener, this.recipient.get().hasWallpaper(), colorizer);
|
||||
adapter.setPagingController(conversationViewModel.getPagingController());
|
||||
list.setAdapter(adapter);
|
||||
setInlineDateDecoration(adapter);
|
||||
@@ -1016,7 +1017,7 @@ public class ConversationFragment extends LoggingFragment implements Multiselect
|
||||
|
||||
try (InputStream stream = PartAuthority.getAttachmentStream(requireContext(), textSlide.getUri())) {
|
||||
String body = StreamUtil.readFullyAsString(stream);
|
||||
return ConversationMessage.ConversationMessageFactory.createWithUnresolvedData(requireContext(), message.getMessageRecord(), body)
|
||||
return ConversationMessage.ConversationMessageFactory.createWithUnresolvedData(requireContext(), message.getMessageRecord(), body, message.getThreadRecipient())
|
||||
.getDisplayBody(requireContext());
|
||||
} catch (IOException e) {
|
||||
Log.w(TAG, "Failed to read text slide data.");
|
||||
@@ -1911,16 +1912,7 @@ public class ConversationFragment extends LoggingFragment implements Multiselect
|
||||
|
||||
@Override
|
||||
public void onChatSessionRefreshLearnMoreClicked() {
|
||||
new AlertDialog.Builder(requireContext())
|
||||
.setView(R.layout.decryption_failed_dialog)
|
||||
.setPositiveButton(android.R.string.ok, (d, w) -> {
|
||||
d.dismiss();
|
||||
})
|
||||
.setNeutralButton(R.string.ConversationFragment_contact_us, (d, w) -> {
|
||||
startActivity(AppSettingsActivity.help(requireContext(), 0));
|
||||
d.dismiss();
|
||||
})
|
||||
.show();
|
||||
ConversationDialogs.INSTANCE.displayChatSessionRefreshLearnMoreDialog(requireContext());
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -1932,34 +1924,7 @@ public class ConversationFragment extends LoggingFragment implements Multiselect
|
||||
|
||||
@Override
|
||||
public void onSafetyNumberLearnMoreClicked(@NonNull Recipient recipient) {
|
||||
if (recipient.isGroup()) {
|
||||
throw new AssertionError("Must be individual");
|
||||
}
|
||||
|
||||
AlertDialog dialog = new AlertDialog.Builder(requireContext())
|
||||
.setView(R.layout.safety_number_changed_learn_more_dialog)
|
||||
.setPositiveButton(R.string.ConversationFragment_verify, (d, w) -> {
|
||||
SimpleTask.run(getLifecycle(), () -> {
|
||||
return ApplicationDependencies.getProtocolStore().aci().identities().getIdentityRecord(recipient.getId());
|
||||
}, identityRecord -> {
|
||||
if (identityRecord.isPresent()) {
|
||||
startActivity(VerifyIdentityActivity.newIntent(requireContext(), identityRecord.get()));
|
||||
}});
|
||||
d.dismiss();
|
||||
})
|
||||
.setNegativeButton(R.string.ConversationFragment_not_now, (d, w) -> {
|
||||
d.dismiss();
|
||||
})
|
||||
.create();
|
||||
dialog.setOnShowListener(d -> {
|
||||
TextView title = Objects.requireNonNull(dialog.findViewById(R.id.safety_number_learn_more_title));
|
||||
TextView body = Objects.requireNonNull(dialog.findViewById(R.id.safety_number_learn_more_body));
|
||||
|
||||
title.setText(getString(R.string.ConversationFragment_your_safety_number_with_s_changed, recipient.getDisplayName(requireContext())));
|
||||
body.setText(getString(R.string.ConversationFragment_your_safety_number_with_s_changed_likey_because_they_reinstalled_signal, recipient.getDisplayName(requireContext())));
|
||||
});
|
||||
|
||||
dialog.show();
|
||||
ConversationDialogs.INSTANCE.displaySafetyNumberLearnMoreDialog(ConversationFragment.this, recipient);
|
||||
}
|
||||
@Override
|
||||
public void onJoinGroupCallClicked() {
|
||||
@@ -1988,15 +1953,7 @@ public class ConversationFragment extends LoggingFragment implements Multiselect
|
||||
|
||||
@Override
|
||||
public void onInMemoryMessageClicked(@NonNull InMemoryMessageRecord messageRecord) {
|
||||
if (messageRecord instanceof InMemoryMessageRecord.NoGroupsInCommon) {
|
||||
boolean isGroup = ((InMemoryMessageRecord.NoGroupsInCommon) messageRecord).isGroup();
|
||||
new MaterialAlertDialogBuilder(requireContext(), R.style.ThemeOverlay_Signal_MaterialAlertDialog)
|
||||
.setMessage(isGroup ? R.string.GroupsInCommonMessageRequest__none_of_your_contacts_or_people_you_chat_with_are_in_this_group
|
||||
: R.string.GroupsInCommonMessageRequest__you_have_no_groups_in_common_with_this_person)
|
||||
.setNeutralButton(R.string.GroupsInCommonMessageRequest__about_message_requests, (d, w) -> CommunicationActions.openBrowserLink(requireContext(), getString(R.string.GroupsInCommonMessageRequest__support_article)))
|
||||
.setPositiveButton(R.string.GroupsInCommonMessageRequest__okay, null)
|
||||
.show();
|
||||
}
|
||||
ConversationDialogs.INSTANCE.displayInMemoryMessageDialog(requireContext(), messageRecord);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -2111,7 +2068,7 @@ public class ConversationFragment extends LoggingFragment implements Multiselect
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onScheduledIndicatorClicked(@NonNull View view, @NonNull MessageRecord messageRecord) {
|
||||
public void onScheduledIndicatorClicked(@NonNull View view, @NonNull ConversationMessage conversationMessage) {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2324,7 +2324,7 @@ public final class ConversationItem extends RelativeLayout implements BindableCo
|
||||
private class ScheduledIndicatorClickListener implements View.OnClickListener {
|
||||
public void onClick(final View view) {
|
||||
if (eventListener != null && batchSelected.isEmpty()) {
|
||||
eventListener.onScheduledIndicatorClicked(view, (messageRecord));
|
||||
eventListener.onScheduledIndicatorClicked(view, (conversationMessage));
|
||||
} else {
|
||||
passthroughClickListener.onClick(view);
|
||||
}
|
||||
|
||||
@@ -17,11 +17,13 @@ import org.thoughtcrime.securesms.database.SignalDatabase;
|
||||
import org.thoughtcrime.securesms.database.model.Mention;
|
||||
import org.thoughtcrime.securesms.database.model.MessageRecord;
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.BodyRangeList;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.util.MessageRecordUtil;
|
||||
|
||||
import java.security.MessageDigest;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* A view level model used to pass arbitrary message related information needed
|
||||
@@ -33,18 +35,21 @@ public class ConversationMessage {
|
||||
@Nullable private final SpannableString body;
|
||||
@NonNull private final MultiselectCollection multiselectCollection;
|
||||
@NonNull private final MessageStyler.Result styleResult;
|
||||
@NonNull private final Recipient threadRecipient;
|
||||
private final boolean hasBeenQuoted;
|
||||
|
||||
private ConversationMessage(@NonNull MessageRecord messageRecord,
|
||||
@Nullable CharSequence body,
|
||||
@Nullable List<Mention> mentions,
|
||||
boolean hasBeenQuoted,
|
||||
@Nullable MessageStyler.Result styleResult)
|
||||
@Nullable MessageStyler.Result styleResult,
|
||||
@NonNull Recipient threadRecipient)
|
||||
{
|
||||
this.messageRecord = messageRecord;
|
||||
this.hasBeenQuoted = hasBeenQuoted;
|
||||
this.mentions = mentions != null ? mentions : Collections.emptyList();
|
||||
this.styleResult = styleResult != null ? styleResult : MessageStyler.Result.none();
|
||||
this.messageRecord = messageRecord;
|
||||
this.hasBeenQuoted = hasBeenQuoted;
|
||||
this.mentions = mentions != null ? mentions : Collections.emptyList();
|
||||
this.styleResult = styleResult != null ? styleResult : MessageStyler.Result.none();
|
||||
this.threadRecipient = threadRecipient;
|
||||
|
||||
if (body != null) {
|
||||
this.body = SpannableString.valueOf(body);
|
||||
@@ -119,6 +124,10 @@ public class ConversationMessage {
|
||||
return MessageRecordUtil.isScheduled(messageRecord);
|
||||
}
|
||||
|
||||
@NonNull public Recipient getThreadRecipient() {
|
||||
return threadRecipient;
|
||||
}
|
||||
|
||||
/**
|
||||
* Factory providing multiple ways of creating {@link ConversationMessage}s.
|
||||
*/
|
||||
@@ -135,7 +144,8 @@ public class ConversationMessage {
|
||||
@NonNull MessageRecord messageRecord,
|
||||
@NonNull CharSequence body,
|
||||
@Nullable List<Mention> mentions,
|
||||
boolean hasBeenQuoted)
|
||||
boolean hasBeenQuoted,
|
||||
@NonNull Recipient threadRecipient)
|
||||
{
|
||||
SpannableString styledAndMentionBody = null;
|
||||
MessageStyler.Result styleResult = MessageStyler.Result.none();
|
||||
@@ -157,7 +167,8 @@ public class ConversationMessage {
|
||||
styledAndMentionBody != null ? styledAndMentionBody : mentionsUpdate != null ? mentionsUpdate.getBody() : body,
|
||||
mentionsUpdate != null ? mentionsUpdate.getMentions() : null,
|
||||
hasBeenQuoted,
|
||||
styleResult);
|
||||
styleResult,
|
||||
threadRecipient);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -166,8 +177,8 @@ public class ConversationMessage {
|
||||
* database operations to query for mentions and then to resolve mentions to display names.
|
||||
*/
|
||||
@WorkerThread
|
||||
public static @NonNull ConversationMessage createWithUnresolvedData(@NonNull Context context, @NonNull MessageRecord messageRecord) {
|
||||
return createWithUnresolvedData(context, messageRecord, messageRecord.getDisplayBody(context));
|
||||
public static @NonNull ConversationMessage createWithUnresolvedData(@NonNull Context context, @NonNull MessageRecord messageRecord, @NonNull Recipient threadRecipient) {
|
||||
return createWithUnresolvedData(context, messageRecord, messageRecord.getDisplayBody(context), threadRecipient);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -176,10 +187,10 @@ public class ConversationMessage {
|
||||
* database operations to query for mentions and then to resolve mentions to display names.
|
||||
*/
|
||||
@WorkerThread
|
||||
public static @NonNull ConversationMessage createWithUnresolvedData(@NonNull Context context, @NonNull MessageRecord messageRecord, boolean hasBeenQuoted) {
|
||||
public static @NonNull ConversationMessage createWithUnresolvedData(@NonNull Context context, @NonNull MessageRecord messageRecord, boolean hasBeenQuoted, @NonNull Recipient threadRecipient) {
|
||||
List<Mention> mentions = messageRecord.isMms() ? SignalDatabase.mentions().getMentionsForMessage(messageRecord.getId())
|
||||
: null;
|
||||
return createWithUnresolvedData(context, messageRecord, messageRecord.getDisplayBody(context), mentions, hasBeenQuoted);
|
||||
return createWithUnresolvedData(context, messageRecord, messageRecord.getDisplayBody(context), mentions, hasBeenQuoted, threadRecipient);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -188,11 +199,11 @@ public class ConversationMessage {
|
||||
* database operations to query for mentions and then to resolve mentions to display names.
|
||||
*/
|
||||
@WorkerThread
|
||||
public static @NonNull ConversationMessage createWithUnresolvedData(@NonNull Context context, @NonNull MessageRecord messageRecord, @NonNull CharSequence body) {
|
||||
public static @NonNull ConversationMessage createWithUnresolvedData(@NonNull Context context, @NonNull MessageRecord messageRecord, @NonNull CharSequence body, @NonNull Recipient threadRecipient) {
|
||||
boolean hasBeenQuoted = SignalDatabase.messages().isQuoted(messageRecord);
|
||||
List<Mention> mentions = SignalDatabase.mentions().getMentionsForMessage(messageRecord.getId());
|
||||
|
||||
return createWithUnresolvedData(context, messageRecord, body, mentions, hasBeenQuoted);
|
||||
return createWithUnresolvedData(context, messageRecord, body, mentions, hasBeenQuoted, threadRecipient);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -163,6 +163,7 @@ import org.thoughtcrime.securesms.conversation.ui.inlinequery.InlineQueryChanged
|
||||
import org.thoughtcrime.securesms.conversation.ui.inlinequery.InlineQueryResultsController;
|
||||
import org.thoughtcrime.securesms.conversation.ui.inlinequery.InlineQueryViewModel;
|
||||
import org.thoughtcrime.securesms.conversation.ui.mentions.MentionsPickerViewModel;
|
||||
import org.thoughtcrime.securesms.conversation.v2.ConversationDialogs;
|
||||
import org.thoughtcrime.securesms.crypto.ReentrantSessionLock;
|
||||
import org.thoughtcrime.securesms.crypto.SecurityEvent;
|
||||
import org.thoughtcrime.securesms.database.DraftTable.Draft;
|
||||
@@ -3948,15 +3949,7 @@ public class ConversationParentFragment extends Fragment
|
||||
.forMessageRecord(requireContext(), messageRecord)
|
||||
.show(getChildFragmentManager());
|
||||
} else if (messageRecord.hasFailedWithNetworkFailures()) {
|
||||
new MaterialAlertDialogBuilder(requireContext())
|
||||
.setMessage(R.string.conversation_activity__message_could_not_be_sent)
|
||||
.setNegativeButton(android.R.string.cancel, null)
|
||||
.setPositiveButton(R.string.conversation_activity__send, (dialog, which) -> {
|
||||
SignalExecutors.BOUNDED.execute(() -> {
|
||||
MessageSender.resend(requireContext(), messageRecord);
|
||||
});
|
||||
})
|
||||
.show();
|
||||
ConversationDialogs.INSTANCE.displayMessageCouldNotBeSentDialog(requireContext(), messageRecord);
|
||||
} else {
|
||||
MessageDetailsFragment.create(messageRecord, recipient.getId()).show(getChildFragmentManager(), null);
|
||||
}
|
||||
|
||||
@@ -203,7 +203,7 @@ public class ConversationRepository {
|
||||
|
||||
try (InputStream stream = PartAuthority.getAttachmentStream(context, textSlide.getUri())) {
|
||||
String body = StreamUtil.readFullyAsString(stream);
|
||||
return ConversationMessage.ConversationMessageFactory.createWithUnresolvedData(context, messageRecord, body);
|
||||
return ConversationMessage.ConversationMessageFactory.createWithUnresolvedData(context, messageRecord, body, message.getThreadRecipient());
|
||||
} catch (IOException e) {
|
||||
Log.w(TAG, "Failed to read text slide data.");
|
||||
}
|
||||
|
||||
@@ -92,7 +92,7 @@ class ScheduledMessagesBottomSheet : FixedRoundedCornerBottomSheetDialogFragment
|
||||
|
||||
val colorizer = Colorizer()
|
||||
|
||||
messageAdapter = ConversationAdapter(requireContext(), viewLifecycleOwner, GlideApp.with(this), Locale.getDefault(), ConversationAdapterListener(), conversationRecipient, colorizer).apply {
|
||||
messageAdapter = ConversationAdapter(requireContext(), viewLifecycleOwner, GlideApp.with(this), Locale.getDefault(), ConversationAdapterListener(), conversationRecipient.hasWallpaper(), colorizer).apply {
|
||||
setCondensedMode(ConversationItemDisplayMode.CONDENSED)
|
||||
setScheduledMessagesMode(true)
|
||||
}
|
||||
@@ -147,24 +147,23 @@ class ScheduledMessagesBottomSheet : FixedRoundedCornerBottomSheetDialogFragment
|
||||
return callback
|
||||
}
|
||||
|
||||
private fun showScheduledMessageContextMenu(view: View, messageRecord: MessageRecord) {
|
||||
private fun showScheduledMessageContextMenu(view: View, conversationMessage: ConversationMessage) {
|
||||
SignalContextMenu.Builder(view, requireCoordinatorLayout())
|
||||
.offsetX(12.dp)
|
||||
.offsetY(12.dp)
|
||||
.preferredVerticalPosition(SignalContextMenu.VerticalPosition.ABOVE)
|
||||
.show(getMenuActionItems(messageRecord))
|
||||
.show(getMenuActionItems(conversationMessage))
|
||||
}
|
||||
|
||||
private fun getMenuActionItems(messageRecord: MessageRecord): List<ActionItem> {
|
||||
val message = ConversationMessage.ConversationMessageFactory.createWithUnresolvedData(requireContext(), messageRecord)
|
||||
val canCopy = message.multiselectCollection.toSet().any { it !is Attachments && messageRecord.body.isNotEmpty() }
|
||||
private fun getMenuActionItems(message: ConversationMessage): List<ActionItem> {
|
||||
val canCopy = message.multiselectCollection.toSet().any { it !is Attachments && message.messageRecord.body.isNotEmpty() }
|
||||
val items: MutableList<ActionItem> = ArrayList()
|
||||
items.add(ActionItem(R.drawable.symbol_trash_24, resources.getString(R.string.conversation_selection__menu_delete), action = { handleDeleteMessage(messageRecord) }))
|
||||
items.add(ActionItem(R.drawable.symbol_trash_24, resources.getString(R.string.conversation_selection__menu_delete), action = { handleDeleteMessage(message.messageRecord) }))
|
||||
if (canCopy) {
|
||||
items.add(ActionItem(R.drawable.symbol_copy_android_24, resources.getString(R.string.conversation_selection__menu_copy), action = { handleCopyMessage(message) }))
|
||||
}
|
||||
items.add(ActionItem(R.drawable.symbol_send_24, resources.getString(R.string.ScheduledMessagesBottomSheet_menu_send_now), action = { handleSendMessageNow(messageRecord) }))
|
||||
items.add(ActionItem(R.drawable.symbol_calendar_24, resources.getString(R.string.ScheduledMessagesBottomSheet_menu_reschedule), action = { handleRescheduleMessage(messageRecord) }))
|
||||
items.add(ActionItem(R.drawable.symbol_send_24, resources.getString(R.string.ScheduledMessagesBottomSheet_menu_send_now), action = { handleSendMessageNow(message.messageRecord) }))
|
||||
items.add(ActionItem(R.drawable.symbol_calendar_24, resources.getString(R.string.ScheduledMessagesBottomSheet_menu_reschedule), action = { handleRescheduleMessage(message.messageRecord) }))
|
||||
return items
|
||||
}
|
||||
|
||||
@@ -214,14 +213,14 @@ class ScheduledMessagesBottomSheet : FixedRoundedCornerBottomSheetDialogFragment
|
||||
try {
|
||||
PartAuthority.getAttachmentStream(requireContext(), textSlide.uri!!).use { stream ->
|
||||
val body = StreamUtil.readFullyAsString(stream)
|
||||
return ConversationMessage.ConversationMessageFactory.createWithUnresolvedData(requireContext(), message.messageRecord, body)
|
||||
return ConversationMessage.ConversationMessageFactory.createWithUnresolvedData(requireContext(), message.messageRecord, body, message.threadRecipient)
|
||||
.getDisplayBody(requireContext())
|
||||
}
|
||||
} catch (e: IOException) {
|
||||
Log.w(TAG, "Failed to read text slide data.")
|
||||
}
|
||||
}
|
||||
return ConversationMessage.ConversationMessageFactory.createWithUnresolvedData(requireContext(), message.messageRecord).getDisplayBody(requireContext())
|
||||
return ConversationMessage.ConversationMessageFactory.createWithUnresolvedData(requireContext(), message.messageRecord, message.threadRecipient).getDisplayBody(requireContext())
|
||||
}
|
||||
|
||||
private fun deleteMessage(messageId: Long) {
|
||||
@@ -249,8 +248,8 @@ class ScheduledMessagesBottomSheet : FixedRoundedCornerBottomSheetDialogFragment
|
||||
callback.getConversationAdapterListener().onQuoteClicked(messageRecord)
|
||||
}
|
||||
|
||||
override fun onScheduledIndicatorClicked(view: View, messageRecord: MessageRecord) {
|
||||
showScheduledMessageContextMenu(view, messageRecord)
|
||||
override fun onScheduledIndicatorClicked(view: View, conversationMessage: ConversationMessage) {
|
||||
showScheduledMessageContextMenu(view, conversationMessage)
|
||||
}
|
||||
|
||||
override fun onGroupMemberClicked(recipientId: RecipientId, groupId: GroupId) {
|
||||
|
||||
@@ -8,6 +8,7 @@ import org.thoughtcrime.securesms.database.DatabaseObserver
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||
import org.thoughtcrime.securesms.database.model.MessageRecord
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
|
||||
/**
|
||||
* Handles retrieving scheduled messages data to be shown in [ScheduledMessagesBottomSheet] and [ConversationParentFragment]
|
||||
@@ -32,6 +33,7 @@ class ScheduledMessagesRepository {
|
||||
@WorkerThread
|
||||
private fun getScheduledMessagesSync(context: Context, threadId: Long): List<ConversationMessage> {
|
||||
var scheduledMessages: List<MessageRecord> = SignalDatabase.messages.getScheduledMessagesInThread(threadId)
|
||||
val threadRecipient: Recipient = requireNotNull(SignalDatabase.threads.getRecipientForThreadId(threadId))
|
||||
|
||||
val attachmentHelper = ConversationDataSource.AttachmentHelper()
|
||||
|
||||
@@ -42,7 +44,7 @@ class ScheduledMessagesRepository {
|
||||
scheduledMessages = attachmentHelper.buildUpdatedModels(ApplicationDependencies.getApplication(), scheduledMessages)
|
||||
|
||||
val replies: List<ConversationMessage> = scheduledMessages
|
||||
.map { ConversationMessage.ConversationMessageFactory.createWithUnresolvedData(context, it) }
|
||||
.map { ConversationMessage.ConversationMessageFactory.createWithUnresolvedData(context, it, threadRecipient) }
|
||||
|
||||
return replies
|
||||
}
|
||||
|
||||
@@ -112,7 +112,8 @@ class DraftRepository(
|
||||
}
|
||||
} ?: return@fromCallable null
|
||||
|
||||
ConversationMessageFactory.createWithUnresolvedData(context, messageRecord)
|
||||
val threadRecipient = requireNotNull(SignalDatabase.threads.getRecipientForThreadId(messageRecord.threadId))
|
||||
ConversationMessageFactory.createWithUnresolvedData(context, messageRecord, threadRecipient)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -120,20 +121,21 @@ class DraftRepository(
|
||||
return Maybe.fromCallable {
|
||||
val messageId = MessageId.deserialize(serialized)
|
||||
val messageRecord: MessageRecord = SignalDatabase.messages.getMessageRecordOrNull(messageId.id) ?: return@fromCallable null
|
||||
val threadRecipient: Recipient = requireNotNull(SignalDatabase.threads.getRecipientForThreadId(messageRecord.threadId))
|
||||
if (messageRecord.hasTextSlide()) {
|
||||
val textSlide = messageRecord.requireTextSlide()
|
||||
if (textSlide.uri != null) {
|
||||
try {
|
||||
PartAuthority.getAttachmentStream(context, textSlide.uri!!).use { stream ->
|
||||
val body = StreamUtil.readFullyAsString(stream)
|
||||
return@fromCallable ConversationMessageFactory.createWithUnresolvedData(context, messageRecord, body)
|
||||
return@fromCallable ConversationMessageFactory.createWithUnresolvedData(context, messageRecord, body, threadRecipient)
|
||||
}
|
||||
} catch (e: IOException) {
|
||||
Log.e(TAG, "Failed to load text slide", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
ConversationMessageFactory.createWithUnresolvedData(context, messageRecord)
|
||||
ConversationMessageFactory.createWithUnresolvedData(context, messageRecord, threadRecipient)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -125,7 +125,7 @@ data class MultiselectForwardFragmentArgs @JvmOverloads constructor(
|
||||
if (textSlideUri != null) {
|
||||
PartAuthority.getAttachmentStream(context, textSlideUri).use {
|
||||
val body = StreamUtil.readFullyAsString(it)
|
||||
val msg = ConversationMessage.ConversationMessageFactory.createWithUnresolvedData(context, mediaMessage, body)
|
||||
val msg = ConversationMessage.ConversationMessageFactory.createWithUnresolvedData(context, mediaMessage, body, conversationMessage.threadRecipient)
|
||||
builder.withDraftText(msg.getDisplayBody(context).toString())
|
||||
}
|
||||
} else {
|
||||
|
||||
@@ -72,7 +72,7 @@ class MessageQuotesBottomSheet : FixedRoundedCornerBottomSheetDialogFragment() {
|
||||
|
||||
val colorizer = Colorizer()
|
||||
|
||||
messageAdapter = ConversationAdapter(requireContext(), viewLifecycleOwner, GlideApp.with(this), Locale.getDefault(), ConversationAdapterListener(), conversationRecipient, colorizer).apply {
|
||||
messageAdapter = ConversationAdapter(requireContext(), viewLifecycleOwner, GlideApp.with(this), Locale.getDefault(), ConversationAdapterListener(), conversationRecipient.hasWallpaper(), colorizer).apply {
|
||||
setCondensedMode(ConversationItemDisplayMode.CONDENSED)
|
||||
}
|
||||
|
||||
|
||||
@@ -58,6 +58,7 @@ class MessageQuotesRepository {
|
||||
|
||||
val reactionHelper = ConversationDataSource.ReactionHelper()
|
||||
val attachmentHelper = ConversationDataSource.AttachmentHelper()
|
||||
val threadRecipient = requireNotNull(SignalDatabase.threads.getRecipientForThreadId(originalRecord.threadId))
|
||||
|
||||
reactionHelper.addAll(replyRecords)
|
||||
attachmentHelper.addAll(replyRecords)
|
||||
@@ -77,7 +78,7 @@ class MessageQuotesRepository {
|
||||
replyRecord
|
||||
}
|
||||
}
|
||||
.map { ConversationMessageFactory.createWithUnresolvedData(application, it) }
|
||||
.map { ConversationMessageFactory.createWithUnresolvedData(application, it, threadRecipient) }
|
||||
|
||||
if (originalRecord.isPaymentNotification) {
|
||||
originalRecord = SignalDatabase.payments.updateMessageWithPayment(originalRecord)
|
||||
@@ -99,7 +100,7 @@ class MessageQuotesRepository {
|
||||
.buildUpdatedModels(ApplicationDependencies.getApplication(), listOf(originalRecord))
|
||||
.get(0)
|
||||
|
||||
val originalMessage: ConversationMessage = ConversationMessageFactory.createWithUnresolvedData(application, originalRecord, false)
|
||||
val originalMessage: ConversationMessage = ConversationMessageFactory.createWithUnresolvedData(application, originalRecord, false, threadRecipient)
|
||||
|
||||
return replies + originalMessage
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ import org.thoughtcrime.securesms.components.ViewBinderDelegate
|
||||
import org.thoughtcrime.securesms.conversation.ConversationAdapter
|
||||
import org.thoughtcrime.securesms.conversation.ConversationBottomSheetCallback
|
||||
import org.thoughtcrime.securesms.conversation.ConversationItemDisplayMode
|
||||
import org.thoughtcrime.securesms.conversation.ConversationMessage
|
||||
import org.thoughtcrime.securesms.conversation.colors.Colorizer
|
||||
import org.thoughtcrime.securesms.conversation.colors.RecyclerViewColorizer
|
||||
import org.thoughtcrime.securesms.conversation.mutiselect.MultiselectPart
|
||||
@@ -68,7 +69,7 @@ class EditMessageHistoryDialog : FixedRoundedCornerBottomSheetDialogFragment() {
|
||||
GlideApp.with(this),
|
||||
Locale.getDefault(),
|
||||
ConversationAdapterListener(),
|
||||
conversationRecipient,
|
||||
conversationRecipient.hasWallpaper(),
|
||||
colorizer
|
||||
).apply {
|
||||
setCondensedMode(ConversationItemDisplayMode.EXTRA_CONDENSED)
|
||||
@@ -119,7 +120,7 @@ class EditMessageHistoryDialog : FixedRoundedCornerBottomSheetDialogFragment() {
|
||||
|
||||
private inner class ConversationAdapterListener : ConversationAdapter.ItemClickListener by requireListener<ConversationBottomSheetCallback>().getConversationAdapterListener() {
|
||||
override fun onQuoteClicked(messageRecord: MmsMessageRecord) = Unit
|
||||
override fun onScheduledIndicatorClicked(view: View, messageRecord: MessageRecord) = Unit
|
||||
override fun onScheduledIndicatorClicked(view: View, conversationMessage: ConversationMessage) = Unit
|
||||
override fun onGroupMemberClicked(recipientId: RecipientId, groupId: GroupId) = Unit
|
||||
override fun onItemClick(item: MultiselectPart) = Unit
|
||||
override fun onItemLongClick(itemView: View, item: MultiselectPart) = Unit
|
||||
|
||||
@@ -7,6 +7,7 @@ import org.thoughtcrime.securesms.conversation.ConversationMessage
|
||||
import org.thoughtcrime.securesms.database.DatabaseObserver
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
|
||||
object EditMessageHistoryRepository {
|
||||
|
||||
@@ -41,8 +42,14 @@ object EditMessageHistoryRepository {
|
||||
fetchAttachments()
|
||||
}
|
||||
|
||||
if (records.isEmpty()) {
|
||||
return emptyList()
|
||||
}
|
||||
|
||||
val threadRecipient: Recipient = requireNotNull(SignalDatabase.threads.getRecipientForThreadId(records[0].threadId))
|
||||
|
||||
return attachmentHelper
|
||||
.buildUpdatedModels(context, records)
|
||||
.map { ConversationMessage.ConversationMessageFactory.createWithUnresolvedData(context, it) }
|
||||
.map { ConversationMessage.ConversationMessageFactory.createWithUnresolvedData(context, it, threadRecipient) }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,8 +2,21 @@ package org.thoughtcrime.securesms.conversation.v2
|
||||
|
||||
import android.content.Context
|
||||
import android.content.DialogInterface
|
||||
import android.widget.TextView
|
||||
import androidx.core.app.DialogCompat
|
||||
import androidx.fragment.app.Fragment
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import org.signal.core.util.concurrent.SignalExecutors
|
||||
import org.signal.core.util.concurrent.SimpleTask
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.components.settings.app.AppSettingsActivity
|
||||
import org.thoughtcrime.securesms.database.model.InMemoryMessageRecord.NoGroupsInCommon
|
||||
import org.thoughtcrime.securesms.database.model.MessageRecord
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.sms.MessageSender
|
||||
import org.thoughtcrime.securesms.util.CommunicationActions
|
||||
import org.thoughtcrime.securesms.verify.VerifyIdentityActivity
|
||||
|
||||
/**
|
||||
* Centralized object for displaying dialogs to the user from the
|
||||
@@ -20,4 +33,83 @@ object ConversationDialogs {
|
||||
.setPositiveButton(R.string.ok) { d: DialogInterface, w: Int -> d.dismiss() }
|
||||
.show()
|
||||
}
|
||||
|
||||
fun displayChatSessionRefreshLearnMoreDialog(context: Context) {
|
||||
MaterialAlertDialogBuilder(context)
|
||||
.setView(R.layout.decryption_failed_dialog)
|
||||
.setPositiveButton(android.R.string.ok) { d, _ -> d.dismiss() }
|
||||
.setNeutralButton(R.string.ConversationFragment_contact_us) { d, _ ->
|
||||
context.startActivity(AppSettingsActivity.help(context, 0))
|
||||
d.dismiss()
|
||||
}
|
||||
.show()
|
||||
}
|
||||
|
||||
fun displaySafetyNumberLearnMoreDialog(fragment: Fragment, recipient: Recipient) {
|
||||
check(!recipient.isGroup)
|
||||
val dialog = MaterialAlertDialogBuilder(fragment.requireContext())
|
||||
.setView(R.layout.safety_number_changed_learn_more_dialog)
|
||||
.setPositiveButton(R.string.ConversationFragment_verify) { d, _ ->
|
||||
SimpleTask.run(
|
||||
fragment.lifecycle,
|
||||
{ ApplicationDependencies.getProtocolStore().aci().identities().getIdentityRecord(recipient.id) },
|
||||
{ identityRecord ->
|
||||
identityRecord.ifPresent {
|
||||
fragment.startActivity(VerifyIdentityActivity.newIntent(fragment.requireContext(), identityRecord.get()))
|
||||
}
|
||||
d.dismiss()
|
||||
}
|
||||
)
|
||||
}
|
||||
.setNegativeButton(R.string.ConversationFragment_not_now) { d, _ -> d.dismiss() }
|
||||
.create()
|
||||
|
||||
dialog.setOnShowListener {
|
||||
val title: TextView = DialogCompat.requireViewById(dialog, R.id.safety_number_learn_more_title) as TextView
|
||||
val body: TextView = DialogCompat.requireViewById(dialog, R.id.safety_number_learn_more_body) as TextView
|
||||
|
||||
title.text = fragment.getString(
|
||||
R.string.ConversationFragment_your_safety_number_with_s_changed,
|
||||
recipient.getDisplayName(fragment.requireContext())
|
||||
)
|
||||
|
||||
body.text = fragment.getString(
|
||||
R.string.ConversationFragment_your_safety_number_with_s_changed_likey_because_they_reinstalled_signal,
|
||||
recipient.getDisplayName(fragment.requireContext())
|
||||
)
|
||||
}
|
||||
|
||||
dialog.show()
|
||||
}
|
||||
|
||||
fun displayInMemoryMessageDialog(context: Context, messageRecord: MessageRecord) {
|
||||
if (messageRecord is NoGroupsInCommon) {
|
||||
val isGroup = messageRecord.isGroup
|
||||
MaterialAlertDialogBuilder(context, R.style.ThemeOverlay_Signal_MaterialAlertDialog)
|
||||
.setMessage(
|
||||
if (isGroup) {
|
||||
R.string.GroupsInCommonMessageRequest__none_of_your_contacts_or_people_you_chat_with_are_in_this_group
|
||||
} else {
|
||||
R.string.GroupsInCommonMessageRequest__you_have_no_groups_in_common_with_this_person
|
||||
}
|
||||
)
|
||||
.setNeutralButton(R.string.GroupsInCommonMessageRequest__about_message_requests) { _, _ ->
|
||||
CommunicationActions.openBrowserLink(context, context.getString(R.string.GroupsInCommonMessageRequest__support_article))
|
||||
}
|
||||
.setPositiveButton(R.string.GroupsInCommonMessageRequest__okay, null)
|
||||
.show()
|
||||
}
|
||||
}
|
||||
|
||||
fun displayMessageCouldNotBeSentDialog(context: Context, messageRecord: MessageRecord) {
|
||||
MaterialAlertDialogBuilder(context)
|
||||
.setMessage(R.string.conversation_activity__message_could_not_be_sent)
|
||||
.setNegativeButton(android.R.string.cancel, null)
|
||||
.setPositiveButton(R.string.conversation_activity__send) { _, _ ->
|
||||
SignalExecutors.BOUNDED.execute {
|
||||
MessageSender.resend(context, messageRecord)
|
||||
}
|
||||
}
|
||||
.show()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package org.thoughtcrime.securesms.conversation.v2
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.ActivityOptions
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
@@ -14,6 +15,7 @@ import androidx.core.app.ActivityCompat
|
||||
import androidx.core.app.ActivityOptionsCompat
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.view.ViewCompat
|
||||
import androidx.core.view.doOnNextLayout
|
||||
import androidx.fragment.app.viewModels
|
||||
import androidx.lifecycle.DefaultLifecycleObserver
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
@@ -26,6 +28,7 @@ import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
||||
import io.reactivex.rxjava3.core.Observable
|
||||
import io.reactivex.rxjava3.core.Single
|
||||
import io.reactivex.rxjava3.kotlin.subscribeBy
|
||||
import io.reactivex.rxjava3.schedulers.Schedulers
|
||||
import org.greenrobot.eventbus.EventBus
|
||||
import org.signal.core.util.ThreadUtil
|
||||
import org.signal.core.util.concurrent.LifecycleDisposable
|
||||
@@ -33,16 +36,20 @@ import org.signal.core.util.logging.Log
|
||||
import org.thoughtcrime.securesms.LoggingFragment
|
||||
import org.thoughtcrime.securesms.MainActivity
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.badges.gifts.flow.GiftFlowActivity
|
||||
import org.thoughtcrime.securesms.badges.gifts.viewgift.received.ViewReceivedGiftBottomSheet
|
||||
import org.thoughtcrime.securesms.badges.gifts.viewgift.sent.ViewSentGiftBottomSheet
|
||||
import org.thoughtcrime.securesms.components.ScrollToPositionDelegate
|
||||
import org.thoughtcrime.securesms.components.ViewBinderDelegate
|
||||
import org.thoughtcrime.securesms.components.recyclerview.SmoothScrollingLinearLayoutManager
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.donate.DonateToSignalFragment
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.donate.DonateToSignalType
|
||||
import org.thoughtcrime.securesms.components.voice.VoiceNoteMediaControllerOwner
|
||||
import org.thoughtcrime.securesms.components.voice.VoiceNotePlaybackState
|
||||
import org.thoughtcrime.securesms.contactshare.Contact
|
||||
import org.thoughtcrime.securesms.contactshare.ContactUtil
|
||||
import org.thoughtcrime.securesms.contactshare.SharedContactDetailsActivity
|
||||
import org.thoughtcrime.securesms.conversation.BadDecryptLearnMoreDialog
|
||||
import org.thoughtcrime.securesms.conversation.ConversationAdapter
|
||||
import org.thoughtcrime.securesms.conversation.ConversationIntents
|
||||
import org.thoughtcrime.securesms.conversation.ConversationIntents.ConversationScreenType
|
||||
@@ -56,9 +63,13 @@ import org.thoughtcrime.securesms.conversation.colors.RecyclerViewColorizer
|
||||
import org.thoughtcrime.securesms.conversation.mutiselect.ConversationItemAnimator
|
||||
import org.thoughtcrime.securesms.conversation.mutiselect.MultiselectItemDecoration
|
||||
import org.thoughtcrime.securesms.conversation.mutiselect.MultiselectPart
|
||||
import org.thoughtcrime.securesms.conversation.quotes.MessageQuotesBottomSheet
|
||||
import org.thoughtcrime.securesms.conversation.ui.edit.EditMessageHistoryDialog
|
||||
import org.thoughtcrime.securesms.conversation.ui.error.EnableCallNotificationSettingsDialog
|
||||
import org.thoughtcrime.securesms.conversation.v2.groups.ConversationGroupCallViewModel
|
||||
import org.thoughtcrime.securesms.conversation.v2.groups.ConversationGroupViewModel
|
||||
import org.thoughtcrime.securesms.database.model.InMemoryMessageRecord
|
||||
import org.thoughtcrime.securesms.database.model.MessageId
|
||||
import org.thoughtcrime.securesms.database.model.MessageRecord
|
||||
import org.thoughtcrime.securesms.database.model.MmsMessageRecord
|
||||
import org.thoughtcrime.securesms.database.model.Quote
|
||||
@@ -72,6 +83,9 @@ import org.thoughtcrime.securesms.giph.mp4.GiphyMp4ProjectionRecycler
|
||||
import org.thoughtcrime.securesms.groups.GroupId
|
||||
import org.thoughtcrime.securesms.groups.GroupMigrationMembershipChange
|
||||
import org.thoughtcrime.securesms.groups.ui.GroupErrors
|
||||
import org.thoughtcrime.securesms.groups.ui.invitesandrequests.invite.GroupLinkInviteFriendsBottomSheetDialogFragment
|
||||
import org.thoughtcrime.securesms.groups.ui.managegroup.dialogs.GroupDescriptionDialog
|
||||
import org.thoughtcrime.securesms.groups.ui.migration.GroupsV1MigrationInfoBottomSheetDialogFragment
|
||||
import org.thoughtcrime.securesms.groups.v2.GroupBlockJoinRequestResult
|
||||
import org.thoughtcrime.securesms.invites.InviteActions
|
||||
import org.thoughtcrime.securesms.linkpreview.LinkPreview
|
||||
@@ -79,6 +93,7 @@ import org.thoughtcrime.securesms.longmessage.LongMessageFragment
|
||||
import org.thoughtcrime.securesms.mediapreview.MediaIntentFactory
|
||||
import org.thoughtcrime.securesms.mediapreview.MediaIntentFactory.create
|
||||
import org.thoughtcrime.securesms.mediapreview.MediaPreviewV2Activity
|
||||
import org.thoughtcrime.securesms.messagedetails.MessageDetailsFragment
|
||||
import org.thoughtcrime.securesms.mms.AttachmentManager
|
||||
import org.thoughtcrime.securesms.mms.GlideApp
|
||||
import org.thoughtcrime.securesms.notifications.v2.ConversationId
|
||||
@@ -86,6 +101,7 @@ import org.thoughtcrime.securesms.payments.preferences.PaymentsActivity
|
||||
import org.thoughtcrime.securesms.ratelimit.RecaptchaProofBottomSheetFragment
|
||||
import org.thoughtcrime.securesms.reactions.ReactionsBottomSheetDialogFragment
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.recipients.RecipientExporter
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
import org.thoughtcrime.securesms.recipients.ui.bottomsheet.RecipientBottomSheetDialogFragment
|
||||
import org.thoughtcrime.securesms.safety.SafetyNumberBottomSheet
|
||||
@@ -98,7 +114,9 @@ import org.thoughtcrime.securesms.util.CommunicationActions
|
||||
import org.thoughtcrime.securesms.util.ContextUtil
|
||||
import org.thoughtcrime.securesms.util.DrawableUtil
|
||||
import org.thoughtcrime.securesms.util.FullscreenHelper
|
||||
import org.thoughtcrime.securesms.util.SignalLocalMetrics
|
||||
import org.thoughtcrime.securesms.util.WindowUtil
|
||||
import org.thoughtcrime.securesms.util.doAfterNextLayout
|
||||
import org.thoughtcrime.securesms.util.fragments.requireListener
|
||||
import org.thoughtcrime.securesms.util.hasGiftBadge
|
||||
import org.thoughtcrime.securesms.util.visible
|
||||
@@ -140,6 +158,7 @@ class ConversationFragment : LoggingFragment(R.layout.v2_conversation_fragment)
|
||||
)
|
||||
|
||||
private val conversationTooltips = ConversationTooltips(this)
|
||||
private val colorizer = Colorizer()
|
||||
|
||||
private lateinit var conversationOptionsMenuProvider: ConversationOptionsMenu.Provider
|
||||
private lateinit var layoutManager: SmoothScrollingLinearLayoutManager
|
||||
@@ -150,6 +169,8 @@ class ConversationFragment : LoggingFragment(R.layout.v2_conversation_fragment)
|
||||
private lateinit var adapter: ConversationAdapter
|
||||
private lateinit var recyclerViewColorizer: RecyclerViewColorizer
|
||||
|
||||
private var animationsAllowed = false
|
||||
|
||||
private val jumpAndPulseScrollStrategy = object : ScrollToPositionDelegate.ScrollStrategy {
|
||||
override fun performScroll(recyclerView: RecyclerView, layoutManager: LinearLayoutManager, position: Int, smooth: Boolean) {
|
||||
ScrollToPositionDelegate.JumpToPositionStrategy.performScroll(recyclerView, layoutManager, position, smooth)
|
||||
@@ -157,32 +178,20 @@ class ConversationFragment : LoggingFragment(R.layout.v2_conversation_fragment)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
SignalLocalMetrics.ConversationOpen.start()
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
registerForResults()
|
||||
disposables.bindTo(viewLifecycleOwner)
|
||||
FullscreenHelper(requireActivity()).showSystemUI()
|
||||
|
||||
conversationOptionsMenuProvider = ConversationOptionsMenu.Provider(ConversationOptionsMenuCallback(), disposables)
|
||||
markReadHelper = MarkReadHelper(ConversationId.forConversation(args.threadId), requireContext(), viewLifecycleOwner)
|
||||
|
||||
FullscreenHelper(requireActivity()).showSystemUI()
|
||||
|
||||
layoutManager = SmoothScrollingLinearLayoutManager(requireContext(), true)
|
||||
binding.conversationItemRecycler.setHasFixedSize(false)
|
||||
binding.conversationItemRecycler.layoutManager = layoutManager
|
||||
binding.conversationItemRecycler.addOnScrollListener(ScrollListener())
|
||||
|
||||
binding.scrollToBottom.setOnClickListener {
|
||||
scrollToPositionDelegate.resetScrollPosition()
|
||||
}
|
||||
|
||||
binding.scrollToMention.setOnClickListener {
|
||||
scrollToNextMention()
|
||||
}
|
||||
|
||||
val layoutTransitionListener = BubbleLayoutTransitionListener(binding.conversationItemRecycler)
|
||||
viewLifecycleOwner.lifecycle.addObserver(layoutTransitionListener)
|
||||
|
||||
recyclerViewColorizer = RecyclerViewColorizer(binding.conversationItemRecycler)
|
||||
recyclerViewColorizer.setChatColors(args.chatColors)
|
||||
initializeConversationThreadUi()
|
||||
|
||||
val conversationToolbarOnScrollHelper = ConversationToolbarOnScrollHelper(
|
||||
requireActivity(),
|
||||
@@ -190,29 +199,10 @@ class ConversationFragment : LoggingFragment(R.layout.v2_conversation_fragment)
|
||||
viewModel::wallpaperSnapshot
|
||||
)
|
||||
conversationToolbarOnScrollHelper.attach(binding.conversationItemRecycler)
|
||||
|
||||
disposables.bindTo(viewLifecycleOwner)
|
||||
disposables += viewModel.recipient
|
||||
.firstOrError()
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribeBy(onSuccess = {
|
||||
onFirstRecipientLoad(it)
|
||||
})
|
||||
|
||||
presentWallpaper(args.wallpaper)
|
||||
disposables += viewModel.recipient
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribeBy(onNext = this::onRecipientChanged)
|
||||
presentActionBarMenu()
|
||||
|
||||
disposables += viewModel.markReadRequests
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribeBy(onNext = markReadHelper::onViewsRevealed)
|
||||
|
||||
disposables += viewModel.scrollButtonState
|
||||
.subscribeBy(onNext = this::presentScrollButtons)
|
||||
|
||||
EventBus.getDefault().registerForLifecycle(groupCallViewModel, viewLifecycleOwner)
|
||||
presentGroupCallJoinButton()
|
||||
observeConversationThread()
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
@@ -227,75 +217,57 @@ class ConversationFragment : LoggingFragment(R.layout.v2_conversation_fragment)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
super.onPause()
|
||||
ApplicationDependencies.getMessageNotifier().clearVisibleThread()
|
||||
}
|
||||
|
||||
private fun registerForResults() {
|
||||
addToContactsLauncher = registerForActivityResult(AddToContactsContract()) {}
|
||||
}
|
||||
|
||||
private fun onFirstRecipientLoad(recipient: Recipient) {
|
||||
Log.d(TAG, "onFirstRecipientLoad")
|
||||
|
||||
val colorizer = Colorizer()
|
||||
adapter = ConversationAdapter(
|
||||
requireContext(),
|
||||
viewLifecycleOwner,
|
||||
GlideApp.with(this),
|
||||
Locale.getDefault(),
|
||||
ConversationItemClickListener(),
|
||||
recipient,
|
||||
colorizer
|
||||
)
|
||||
|
||||
scrollToPositionDelegate = ScrollToPositionDelegate(
|
||||
binding.conversationItemRecycler,
|
||||
adapter::canJumpToPosition,
|
||||
adapter::getAdapterPositionForMessagePosition
|
||||
)
|
||||
|
||||
binding.conversationItemRecycler.itemAnimator = ConversationItemAnimator(
|
||||
isInMultiSelectMode = adapter.selectedItems::isNotEmpty,
|
||||
shouldPlayMessageAnimations = {
|
||||
scrollToPositionDelegate.isListCommitted() && binding.conversationItemRecycler.scrollState == RecyclerView.SCROLL_STATE_IDLE
|
||||
},
|
||||
isParentFilled = {
|
||||
binding.conversationItemRecycler.canScrollVertically(1) || binding.conversationItemRecycler.canScrollVertically(-1)
|
||||
}
|
||||
)
|
||||
|
||||
ConversationAdapter.initializePool(binding.conversationItemRecycler.recycledViewPool)
|
||||
adapter.setPagingController(viewModel.pagingController)
|
||||
adapter.registerAdapterDataObserver(DataObserver(scrollToPositionDelegate))
|
||||
viewLifecycleOwner.lifecycle.addObserver(LastSeenPositionUpdater(adapter, layoutManager, viewModel))
|
||||
binding.conversationItemRecycler.adapter = adapter
|
||||
giphyMp4ProjectionRecycler = initializeGiphyMp4()
|
||||
|
||||
val multiselectItemDecoration = MultiselectItemDecoration(
|
||||
requireContext()
|
||||
) { viewModel.wallpaperSnapshot }
|
||||
|
||||
binding.conversationItemRecycler.addItemDecoration(multiselectItemDecoration)
|
||||
viewLifecycleOwner.lifecycle.addObserver(multiselectItemDecoration)
|
||||
|
||||
private fun observeConversationThread() {
|
||||
var firstRender = true
|
||||
disposables += viewModel
|
||||
.conversationThreadState
|
||||
.doOnSuccess {
|
||||
scrollToPositionDelegate.requestScrollPosition(
|
||||
position = it.meta.getStartPosition(),
|
||||
smooth = false,
|
||||
awaitLayout = false
|
||||
)
|
||||
.subscribeOn(Schedulers.io())
|
||||
.doOnSuccess { state ->
|
||||
SignalLocalMetrics.ConversationOpen.onDataLoaded()
|
||||
binding.conversationItemRecycler.doOnNextLayout {
|
||||
layoutManager.scrollToPositionWithOffset(
|
||||
adapter.getAdapterPositionForMessagePosition(state.meta.getStartPosition()),
|
||||
binding.conversationItemRecycler.height
|
||||
)
|
||||
}
|
||||
}
|
||||
.flatMapObservable { it.items.data }
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribeBy(onNext = {
|
||||
SignalLocalMetrics.ConversationOpen.onDataPostedToMain()
|
||||
|
||||
adapter.submitList(it) {
|
||||
scrollToPositionDelegate.notifyListCommitted()
|
||||
|
||||
binding.conversationItemRecycler.doAfterNextLayout {
|
||||
SignalLocalMetrics.ConversationOpen.onRenderFinished()
|
||||
|
||||
if (firstRender) {
|
||||
firstRender = false
|
||||
doAfterFirstRender()
|
||||
animationsAllowed = true
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private fun doAfterFirstRender() {
|
||||
Log.d(TAG, "doAfterFirstRender")
|
||||
|
||||
EventBus.getDefault().registerForLifecycle(groupCallViewModel, viewLifecycleOwner)
|
||||
viewLifecycleOwner.lifecycle.addObserver(LastSeenPositionUpdater(adapter, layoutManager, viewModel))
|
||||
|
||||
disposables += viewModel.recipient
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribeBy(onNext = this::onRecipientChanged)
|
||||
|
||||
disposables += viewModel.markReadRequests
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribeBy(onNext = markReadHelper::onViewsRevealed)
|
||||
|
||||
disposables += viewModel.scrollButtonState
|
||||
.subscribeBy(onNext = this::presentScrollButtons)
|
||||
|
||||
disposables += viewModel
|
||||
.nameColorsMap
|
||||
@@ -305,7 +277,26 @@ class ConversationFragment : LoggingFragment(R.layout.v2_conversation_fragment)
|
||||
adapter.notifyItemRangeChanged(0, adapter.itemCount)
|
||||
})
|
||||
|
||||
presentActionBarMenu()
|
||||
presentGroupCallJoinButton()
|
||||
|
||||
binding.scrollToBottom.setOnClickListener {
|
||||
scrollToPositionDelegate.resetScrollPosition()
|
||||
}
|
||||
|
||||
binding.scrollToMention.setOnClickListener {
|
||||
scrollToNextMention()
|
||||
}
|
||||
|
||||
adapter.registerAdapterDataObserver(DataObserver(scrollToPositionDelegate))
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
super.onPause()
|
||||
ApplicationDependencies.getMessageNotifier().clearVisibleThread()
|
||||
}
|
||||
|
||||
private fun registerForResults() {
|
||||
addToContactsLauncher = registerForActivityResult(AddToContactsContract()) {}
|
||||
}
|
||||
|
||||
private fun onRecipientChanged(recipient: Recipient) {
|
||||
@@ -364,6 +355,7 @@ class ConversationFragment : LoggingFragment(R.layout.v2_conversation_fragment)
|
||||
binding.conversationWallpaper.visible = chatWallpaper != null
|
||||
binding.scrollToBottom.setWallpaperEnabled(chatWallpaper != null)
|
||||
binding.scrollToMention.setWallpaperEnabled(chatWallpaper != null)
|
||||
adapter.onHasWallpaperChanged(chatWallpaper != null)
|
||||
}
|
||||
|
||||
private fun presentChatColors(chatColors: ChatColors) {
|
||||
@@ -428,6 +420,58 @@ class ConversationFragment : LoggingFragment(R.layout.v2_conversation_fragment)
|
||||
|
||||
private fun getVoiceNoteMediaController() = requireListener<VoiceNoteMediaControllerOwner>().voiceNoteMediaController
|
||||
|
||||
private fun initializeConversationThreadUi() {
|
||||
layoutManager = SmoothScrollingLinearLayoutManager(requireContext(), true)
|
||||
binding.conversationItemRecycler.setHasFixedSize(false)
|
||||
binding.conversationItemRecycler.layoutManager = layoutManager
|
||||
binding.conversationItemRecycler.addOnScrollListener(ScrollListener())
|
||||
|
||||
adapter = ConversationAdapter(
|
||||
requireContext(),
|
||||
viewLifecycleOwner,
|
||||
GlideApp.with(this),
|
||||
Locale.getDefault(),
|
||||
ConversationItemClickListener(),
|
||||
args.wallpaper != null,
|
||||
colorizer
|
||||
)
|
||||
|
||||
scrollToPositionDelegate = ScrollToPositionDelegate(
|
||||
binding.conversationItemRecycler,
|
||||
adapter::canJumpToPosition,
|
||||
adapter::getAdapterPositionForMessagePosition
|
||||
)
|
||||
|
||||
ConversationAdapter.initializePool(binding.conversationItemRecycler.recycledViewPool)
|
||||
adapter.setPagingController(viewModel.pagingController)
|
||||
|
||||
binding.conversationItemRecycler.adapter = adapter
|
||||
giphyMp4ProjectionRecycler = initializeGiphyMp4()
|
||||
|
||||
val multiselectItemDecoration = MultiselectItemDecoration(
|
||||
requireContext()
|
||||
) { viewModel.wallpaperSnapshot }
|
||||
|
||||
binding.conversationItemRecycler.addItemDecoration(multiselectItemDecoration)
|
||||
viewLifecycleOwner.lifecycle.addObserver(multiselectItemDecoration)
|
||||
|
||||
val layoutTransitionListener = BubbleLayoutTransitionListener(binding.conversationItemRecycler)
|
||||
viewLifecycleOwner.lifecycle.addObserver(layoutTransitionListener)
|
||||
|
||||
recyclerViewColorizer = RecyclerViewColorizer(binding.conversationItemRecycler)
|
||||
recyclerViewColorizer.setChatColors(args.chatColors)
|
||||
|
||||
binding.conversationItemRecycler.itemAnimator = ConversationItemAnimator(
|
||||
isInMultiSelectMode = adapter.selectedItems::isNotEmpty,
|
||||
shouldPlayMessageAnimations = {
|
||||
animationsAllowed && scrollToPositionDelegate.isListCommitted() && binding.conversationItemRecycler.scrollState == RecyclerView.SCROLL_STATE_IDLE
|
||||
},
|
||||
isParentFilled = {
|
||||
binding.conversationItemRecycler.canScrollVertically(1) || binding.conversationItemRecycler.canScrollVertically(-1)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
private fun initializeGiphyMp4(): GiphyMp4ProjectionRecycler {
|
||||
val maxPlayback = GiphyMp4PlaybackPolicy.maxSimultaneousPlaybackInConversation()
|
||||
val holders = GiphyMp4ProjectionPlayerHolder.injectVideoViews(
|
||||
@@ -553,7 +597,15 @@ class ConversationFragment : LoggingFragment(R.layout.v2_conversation_fragment)
|
||||
}
|
||||
|
||||
override fun onQuotedIndicatorClicked(messageRecord: MessageRecord) {
|
||||
// TODO [alex] - ("Not yet implemented")
|
||||
context ?: return
|
||||
activity ?: return
|
||||
val recipientId = viewModel.recipientSnapshot?.id ?: return
|
||||
|
||||
MessageQuotesBottomSheet.show(
|
||||
childFragmentManager,
|
||||
MessageId(messageRecord.id),
|
||||
recipientId
|
||||
)
|
||||
}
|
||||
|
||||
override fun onMoreTextClicked(conversationRecipientId: RecipientId, messageId: Long, isMms: Boolean) {
|
||||
@@ -614,7 +666,16 @@ class ConversationFragment : LoggingFragment(R.layout.v2_conversation_fragment)
|
||||
}
|
||||
|
||||
override fun onMessageWithErrorClicked(messageRecord: MessageRecord) {
|
||||
// TODO [alex] - ("Not yet implemented")
|
||||
val recipientId = viewModel.recipientSnapshot?.id ?: return
|
||||
if (messageRecord.isIdentityMismatchFailure) {
|
||||
SafetyNumberBottomSheet
|
||||
.forMessageRecord(requireContext(), messageRecord)
|
||||
.show(childFragmentManager)
|
||||
} else if (messageRecord.hasFailedWithNetworkFailures()) {
|
||||
ConversationDialogs.displayMessageCouldNotBeSentDialog(requireContext(), messageRecord)
|
||||
} else {
|
||||
MessageDetailsFragment.create(messageRecord, recipientId).show(childFragmentManager, null)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onMessageWithRecaptchaNeededClicked(messageRecord: MessageRecord) {
|
||||
@@ -654,55 +715,73 @@ class ConversationFragment : LoggingFragment(R.layout.v2_conversation_fragment)
|
||||
}
|
||||
|
||||
override fun onGroupMigrationLearnMoreClicked(membershipChange: GroupMigrationMembershipChange) {
|
||||
// TODO [alex] -- ("Not yet implemented")
|
||||
GroupsV1MigrationInfoBottomSheetDialogFragment.show(parentFragmentManager, membershipChange)
|
||||
}
|
||||
|
||||
override fun onChatSessionRefreshLearnMoreClicked() {
|
||||
// TODO [alex] -- ("Not yet implemented")
|
||||
ConversationDialogs.displayChatSessionRefreshLearnMoreDialog(requireContext())
|
||||
}
|
||||
|
||||
override fun onBadDecryptLearnMoreClicked(author: RecipientId) {
|
||||
// TODO [alex] -- ("Not yet implemented")
|
||||
val isGroup = viewModel.recipientSnapshot?.isGroup ?: return
|
||||
val recipientName = Recipient.resolved(author).getDisplayName(requireContext())
|
||||
BadDecryptLearnMoreDialog.show(parentFragmentManager, recipientName, isGroup)
|
||||
}
|
||||
|
||||
override fun onSafetyNumberLearnMoreClicked(recipient: Recipient) {
|
||||
// TODO [alex] -- ("Not yet implemented")
|
||||
ConversationDialogs.displaySafetyNumberLearnMoreDialog(this@ConversationFragment, recipient)
|
||||
}
|
||||
|
||||
override fun onJoinGroupCallClicked() {
|
||||
// TODO [alex] -- ("Not yet implemented")
|
||||
val activity = activity ?: return
|
||||
val recipient = viewModel.recipientSnapshot ?: return
|
||||
CommunicationActions.startVideoCall(activity, recipient)
|
||||
}
|
||||
|
||||
override fun onInviteFriendsToGroupClicked(groupId: GroupId.V2) {
|
||||
// TODO [alex] -- ("Not yet implemented")
|
||||
GroupLinkInviteFriendsBottomSheetDialogFragment.show(requireActivity().supportFragmentManager, groupId)
|
||||
}
|
||||
|
||||
@SuppressLint("NotifyDataSetChanged")
|
||||
override fun onEnableCallNotificationsClicked() {
|
||||
// TODO [alex] -- ("Not yet implemented")
|
||||
EnableCallNotificationSettingsDialog.fixAutomatically(requireContext())
|
||||
if (EnableCallNotificationSettingsDialog.shouldShow(requireContext())) {
|
||||
EnableCallNotificationSettingsDialog.show(childFragmentManager)
|
||||
} else {
|
||||
adapter.notifyDataSetChanged()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onPlayInlineContent(conversationMessage: ConversationMessage?) {
|
||||
// TODO [alex] - ("Not yet implemented")
|
||||
adapter.playInlineContent(conversationMessage)
|
||||
}
|
||||
|
||||
override fun onInMemoryMessageClicked(messageRecord: InMemoryMessageRecord) {
|
||||
// TODO [alex] - ("Not yet implemented")
|
||||
ConversationDialogs.displayInMemoryMessageDialog(requireContext(), messageRecord)
|
||||
}
|
||||
|
||||
override fun onViewGroupDescriptionChange(groupId: GroupId?, description: String, isMessageRequestAccepted: Boolean) {
|
||||
// TODO [alex] - ("Not yet implemented")
|
||||
if (groupId != null) {
|
||||
GroupDescriptionDialog.show(childFragmentManager, groupId, description, isMessageRequestAccepted)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onChangeNumberUpdateContact(recipient: Recipient) {
|
||||
// TODO [alex] - ("Not yet implemented")
|
||||
startActivity(RecipientExporter.export(recipient).asAddContactIntent())
|
||||
}
|
||||
|
||||
override fun onCallToAction(action: String) {
|
||||
// TODO [alex] - ("Not yet implemented")
|
||||
if ("gift_badge" == action) {
|
||||
startActivity(Intent(requireContext(), GiftFlowActivity::class.java))
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDonateClicked() {
|
||||
// TODO [alex] - ("Not yet implemented")
|
||||
requireActivity()
|
||||
.supportFragmentManager
|
||||
.beginTransaction()
|
||||
.add(DonateToSignalFragment.Dialog.create(DonateToSignalType.ONE_TIME), "one_time_nav")
|
||||
.commitNow()
|
||||
}
|
||||
|
||||
override fun onBlockJoinRequest(recipient: Recipient) {
|
||||
@@ -728,7 +807,7 @@ class ConversationFragment : LoggingFragment(R.layout.v2_conversation_fragment)
|
||||
InviteActions.inviteUserToSignal(
|
||||
requireContext(),
|
||||
recipient,
|
||||
{}, // TODO [alex] -- append to compose
|
||||
binding.conversationInputPanel.embeddedTextEditor::appendInvite,
|
||||
this@ConversationFragment::startActivity
|
||||
)
|
||||
}
|
||||
@@ -738,17 +817,11 @@ class ConversationFragment : LoggingFragment(R.layout.v2_conversation_fragment)
|
||||
}
|
||||
|
||||
override fun onSendPaymentClicked(recipientId: RecipientId) {
|
||||
disposables += viewModel.recipient
|
||||
.firstOrError()
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribeBy {
|
||||
AttachmentManager.selectPayment(this@ConversationFragment, it)
|
||||
}
|
||||
val recipient = viewModel.recipientSnapshot ?: return
|
||||
AttachmentManager.selectPayment(this@ConversationFragment, recipient)
|
||||
}
|
||||
|
||||
override fun onScheduledIndicatorClicked(view: View, messageRecord: MessageRecord) {
|
||||
// TODO [alex] -- ("Not yet implemented")
|
||||
}
|
||||
override fun onScheduledIndicatorClicked(view: View, conversationMessage: ConversationMessage) = Unit
|
||||
|
||||
override fun onUrlClicked(url: String): Boolean {
|
||||
return CommunicationActions.handlePotentialGroupLinkUrl(requireActivity(), url) ||
|
||||
@@ -793,7 +866,11 @@ class ConversationFragment : LoggingFragment(R.layout.v2_conversation_fragment)
|
||||
}
|
||||
|
||||
override fun onEditedIndicatorClicked(messageRecord: MessageRecord) {
|
||||
// TODO [alex] -- ("Not yet implemented")
|
||||
if (messageRecord.isOutgoing) {
|
||||
EditMessageHistoryDialog.show(childFragmentManager, messageRecord.toRecipient.id, messageRecord.id)
|
||||
} else {
|
||||
EditMessageHistoryDialog.show(childFragmentManager, messageRecord.fromRecipient.id, messageRecord.id)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onItemClick(item: MultiselectPart?) {
|
||||
|
||||
@@ -18,6 +18,7 @@ import org.thoughtcrime.securesms.database.SignalDatabase.Companion.threads
|
||||
import org.thoughtcrime.securesms.database.model.Quote
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
import org.thoughtcrime.securesms.util.SignalLocalMetrics
|
||||
import kotlin.math.max
|
||||
|
||||
class ConversationRepository(context: Context) {
|
||||
@@ -52,6 +53,7 @@ class ConversationRepository(context: Context) {
|
||||
*/
|
||||
fun getConversationThreadState(threadId: Long, requestedStartPosition: Int): Single<ConversationThreadState> {
|
||||
return Single.fromCallable {
|
||||
SignalLocalMetrics.ConversationOpen.onMetadataLoadStarted()
|
||||
val recipient = threads.getRecipientForThreadId(threadId)!!
|
||||
val metadata = oldConversationRepository.getConversationData(threadId, recipient, requestedStartPosition)
|
||||
val messageRequestData = metadata.messageRequestData
|
||||
@@ -70,8 +72,10 @@ class ConversationRepository(context: Context) {
|
||||
ConversationThreadState(
|
||||
items = PagedData.createForObservable(dataSource, config),
|
||||
meta = metadata
|
||||
)
|
||||
}
|
||||
).apply {
|
||||
SignalLocalMetrics.ConversationOpen.onMetadataLoaded()
|
||||
}
|
||||
}.subscribeOn(Schedulers.io())
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -10,6 +10,7 @@ import io.reactivex.rxjava3.disposables.CompositeDisposable
|
||||
import io.reactivex.rxjava3.kotlin.plusAssign
|
||||
import io.reactivex.rxjava3.kotlin.subscribeBy
|
||||
import io.reactivex.rxjava3.processors.PublishProcessor
|
||||
import io.reactivex.rxjava3.schedulers.Schedulers
|
||||
import io.reactivex.rxjava3.subjects.BehaviorSubject
|
||||
import io.reactivex.rxjava3.subjects.Subject
|
||||
import org.signal.paging.ProxyPagingController
|
||||
@@ -67,7 +68,9 @@ class ConversationViewModel(
|
||||
|
||||
init {
|
||||
disposables += repository.observeRecipientForThread(threadId)
|
||||
.subscribeBy(onNext = _recipient::onNext)
|
||||
.subscribeBy(onNext = {
|
||||
_recipient.onNext(it)
|
||||
})
|
||||
|
||||
disposables += repository.getConversationThreadState(threadId, requestedStartingPosition)
|
||||
.subscribeBy(onSuccess = {
|
||||
@@ -75,7 +78,7 @@ class ConversationViewModel(
|
||||
_conversationThreadState.onNext(it)
|
||||
})
|
||||
|
||||
disposables += _conversationThreadState.firstOrError().flatMapObservable { threadState ->
|
||||
disposables += conversationThreadState.flatMapObservable { threadState ->
|
||||
Observable.create<Unit> { emitter ->
|
||||
val controller = threadState.items.controller
|
||||
val messageUpdateObserver = DatabaseObserver.MessageObserver {
|
||||
@@ -98,7 +101,7 @@ class ConversationViewModel(
|
||||
ApplicationDependencies.getDatabaseObserver().unregisterObserver(conversationObserver)
|
||||
}
|
||||
}
|
||||
}.subscribe()
|
||||
}.subscribeOn(Schedulers.io()).subscribe()
|
||||
|
||||
disposables += scrollButtonStateStore.update(
|
||||
repository.getMessageCounts(threadId)
|
||||
|
||||
@@ -5,8 +5,10 @@ import android.net.Uri
|
||||
import org.signal.core.util.StreamUtil
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.thoughtcrime.securesms.conversation.ConversationMessage
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||
import org.thoughtcrime.securesms.database.model.MmsMessageRecord
|
||||
import org.thoughtcrime.securesms.mms.PartAuthority
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import java.io.IOException
|
||||
|
||||
const val TAG = "LongMessageResolver"
|
||||
@@ -21,11 +23,12 @@ fun readFullBody(context: Context, uri: Uri): String {
|
||||
}
|
||||
|
||||
fun MmsMessageRecord.resolveBody(context: Context): ConversationMessage {
|
||||
val threadRecipient: Recipient = requireNotNull(SignalDatabase.threads.getRecipientForThreadId(threadId))
|
||||
val textSlide = slideDeck.textSlide
|
||||
val textSlideUri = textSlide?.uri
|
||||
return if (textSlide != null && textSlideUri != null) {
|
||||
ConversationMessage.ConversationMessageFactory.createWithUnresolvedData(context, this, readFullBody(context, textSlideUri))
|
||||
ConversationMessage.ConversationMessageFactory.createWithUnresolvedData(context, this, readFullBody(context, textSlideUri), threadRecipient)
|
||||
} else {
|
||||
ConversationMessage.ConversationMessageFactory.createWithUnresolvedData(context, this)
|
||||
ConversationMessage.ConversationMessageFactory.createWithUnresolvedData(context, this, threadRecipient)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -131,7 +131,8 @@ public final class MessageDetailsRepository {
|
||||
}
|
||||
}
|
||||
|
||||
return new MessageDetails(ConversationMessageFactory.createWithUnresolvedData(context, messageRecord), recipients);
|
||||
Recipient threadRecipient = Objects.requireNonNull(SignalDatabase.threads().getRecipientForThreadId(messageRecord.getThreadId()));
|
||||
return new MessageDetails(ConversationMessageFactory.createWithUnresolvedData(context, messageRecord, threadRecipient), recipients);
|
||||
}
|
||||
|
||||
private @Nullable NetworkFailure getNetworkFailure(MessageRecord messageRecord, Recipient recipient) {
|
||||
|
||||
@@ -112,10 +112,10 @@ class StoriesLandingRepository(context: Context) {
|
||||
hasReplies = messageRecords.any { SignalDatabase.messages.getNumberOfStoryReplies(it.id) > 0 },
|
||||
hasRepliesFromSelf = messageRecords.any { SignalDatabase.messages.hasSelfReplyInStory(it.id) },
|
||||
isHidden = sender.shouldHideStory(),
|
||||
primaryStory = ConversationMessage.ConversationMessageFactory.createWithUnresolvedData(context, messageRecords[primaryIndex]),
|
||||
primaryStory = ConversationMessage.ConversationMessageFactory.createWithUnresolvedData(context, messageRecords[primaryIndex], sender),
|
||||
secondaryStory = if (sender.isMyStory) {
|
||||
messageRecords.drop(1).firstOrNull()?.let {
|
||||
ConversationMessage.ConversationMessageFactory.createWithUnresolvedData(context, it)
|
||||
ConversationMessage.ConversationMessageFactory.createWithUnresolvedData(context, it, sender)
|
||||
}
|
||||
} else {
|
||||
null
|
||||
|
||||
@@ -42,7 +42,7 @@ class MyStoriesRepository(context: Context) {
|
||||
return MyStoriesState.DistributionSet(
|
||||
label = recipient.getDisplayName(context),
|
||||
stories = messageRecords.map {
|
||||
ConversationMessage.ConversationMessageFactory.createWithUnresolvedData(context, it)
|
||||
ConversationMessage.ConversationMessageFactory.createWithUnresolvedData(context, it, recipient)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@@ -86,7 +86,7 @@ open class StoryViewerPageRepository(context: Context, private val storyViewStat
|
||||
replyCount = SignalDatabase.messages.getNumberOfStoryReplies(record.id),
|
||||
dateInMilliseconds = record.dateSent,
|
||||
content = getContent(record as MmsMessageRecord),
|
||||
conversationMessage = ConversationMessage.ConversationMessageFactory.createWithUnresolvedData(context, record),
|
||||
conversationMessage = ConversationMessage.ConversationMessageFactory.createWithUnresolvedData(context, record, recipient),
|
||||
allowsReplies = record.storyType.isStoryWithReplies,
|
||||
hasSelfViewed = storyViewStateCache.getOrPut(record.id, if (record.isOutgoing) true else record.viewedReceiptCount > 0)
|
||||
)
|
||||
|
||||
@@ -8,6 +8,7 @@ import org.thoughtcrime.securesms.database.SignalDatabase
|
||||
import org.thoughtcrime.securesms.database.model.MessageId
|
||||
import org.thoughtcrime.securesms.database.model.MmsMessageRecord
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
|
||||
class StoryGroupReplyDataSource(private val parentStoryId: Long) : PagedDataSource<MessageId, ReplyBody> {
|
||||
override fun size(): Int {
|
||||
@@ -36,11 +37,12 @@ class StoryGroupReplyDataSource(private val parentStoryId: Long) : PagedDataSour
|
||||
}
|
||||
|
||||
private fun readRowFromRecord(record: MmsMessageRecord): ReplyBody {
|
||||
val threadRecipient: Recipient = requireNotNull(SignalDatabase.threads.getRecipientForThreadId(record.threadId))
|
||||
return when {
|
||||
record.isRemoteDelete -> ReplyBody.RemoteDelete(record)
|
||||
MessageTypes.isStoryReaction(record.type) -> ReplyBody.Reaction(record)
|
||||
else -> ReplyBody.Text(
|
||||
ConversationMessage.ConversationMessageFactory.createWithUnresolvedData(ApplicationDependencies.getApplication(), record)
|
||||
ConversationMessage.ConversationMessageFactory.createWithUnresolvedData(ApplicationDependencies.getApplication(), record, threadRecipient)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,8 +8,7 @@
|
||||
android:background="@color/signal_background_primary"
|
||||
android:clipChildren="false"
|
||||
android:clipToPadding="false"
|
||||
android:orientation="vertical"
|
||||
tools:viewBindingIgnore="true">
|
||||
android:orientation="vertical">
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/input_panel_sticker_suggestion"
|
||||
|
||||
Reference in New Issue
Block a user