Flesh out event listeners and add load sequencing to CFV2.

This commit is contained in:
Alex Hart
2023-04-21 12:57:56 -03:00
parent 694d8f1984
commit 30fc6d94c5
28 changed files with 423 additions and 264 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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