Remove most of Conversation Fragment V1 and friends.

This commit is contained in:
Cody Henthorne 2023-08-02 12:35:42 -04:00 committed by Greyson Parrelli
parent 9c49c84306
commit 67b8f468e4
39 changed files with 187 additions and 9408 deletions

View file

@ -8,6 +8,7 @@ import org.junit.Test
import org.junit.runner.RunWith
import org.signal.core.util.ThreadUtil
import org.thoughtcrime.securesms.attachments.PointerAttachment
import org.thoughtcrime.securesms.conversation.v2.ConversationActivity
import org.thoughtcrime.securesms.database.SignalDatabase
import org.thoughtcrime.securesms.mms.IncomingMediaMessage
import org.thoughtcrime.securesms.mms.OutgoingMessage

View file

@ -7,6 +7,7 @@ import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.thoughtcrime.securesms.contacts.paged.ContactSearchKey
import org.thoughtcrime.securesms.conversation.v2.ConversationActivity
import org.thoughtcrime.securesms.database.IdentityTable
import org.thoughtcrime.securesms.database.SignalDatabase
import org.thoughtcrime.securesms.database.model.DistributionListId

View file

@ -630,22 +630,11 @@
android:value="org.thoughtcrime.securesms.MainActivity" />
</activity>
<activity android:name=".conversation.ConversationActivity"
android:windowSoftInputMode="stateUnchanged"
android:launchMode="singleTask"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
android:parentActivityName=".MainActivity"
android:exported="false">
<meta-data
android:name="android.support.PARENT_ACTIVITY"
android:value="org.thoughtcrime.securesms.MainActivity" />
</activity>
<activity android:name=".conversation.BubbleConversationActivity"
android:theme="@style/Signal.DayNight"
android:allowEmbedded="true"
android:resizeableActivity="true"
android:exported="false"/>
android:theme="@style/Signal.DayNight"
android:allowEmbedded="true"
android:resizeableActivity="true"
android:exported="false"/>
<activity android:name=".conversation.ConversationPopupActivity"
android:windowSoftInputMode="stateVisible"

View file

@ -14,8 +14,6 @@ import org.thoughtcrime.securesms.components.menu.SignalContextMenu
import org.thoughtcrime.securesms.conversation.MessageSendType
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.util.ViewUtil
import java.lang.AssertionError
import java.util.concurrent.CopyOnWriteArrayList
/**
* The send button you see in a conversation.
@ -27,7 +25,6 @@ class SendButton(context: Context, attributeSet: AttributeSet?) : AppCompatImage
private val TAG = Log.tag(SendButton::class.java)
}
private val listeners: MutableList<SendTypeChangedListener> = CopyOnWriteArrayList()
private var scheduledSendListener: ScheduledSendListener? = null
private var availableSendTypes: List<MessageSendType> = MessageSendType.getAllAvailable(context, false)
@ -43,16 +40,10 @@ class SendButton(context: Context, attributeSet: AttributeSet?) : AppCompatImage
ViewUtil.mirrorIfRtl(this, getContext())
}
/**
* @return True if the [selectedSendType] was chosen manually by the user, otherwise false.
*/
val isManualSelection: Boolean
get() = activeMessageSendType != null
/**
* The actively-selected send type.
*/
val selectedSendType: MessageSendType
private val selectedSendType: MessageSendType
get() {
activeMessageSendType?.let {
return it
@ -78,65 +69,33 @@ class SendButton(context: Context, attributeSet: AttributeSet?) : AppCompatImage
if (signalType != null) {
Log.w(TAG, "No options of default type, but Signal type is available. Switching. DefaultTransportType: $defaultTransportType, AllAvailable: ${availableSendTypes.map { it.transportType }}")
defaultTransportType = MessageSendType.TransportType.SIGNAL
onSelectionChanged(signalType, false)
onSelectionChanged(signalType)
return signalType
} else if (availableSendTypes.isEmpty()) {
Log.w(TAG, "No send types available at all! Enabling the Signal transport.")
defaultTransportType = MessageSendType.TransportType.SIGNAL
availableSendTypes = listOf(MessageSendType.SignalMessageSendType)
onSelectionChanged(MessageSendType.SignalMessageSendType, false)
onSelectionChanged(MessageSendType.SignalMessageSendType)
return MessageSendType.SignalMessageSendType
} else {
throw AssertionError("No options of default type! DefaultTransportType: $defaultTransportType, AllAvailable: ${availableSendTypes.map { it.transportType }}")
}
}
fun addOnSelectionChangedListener(listener: SendTypeChangedListener) {
listeners.add(listener)
}
fun triggerSelectedChangedEvent() {
onSelectionChanged(newType = selectedSendType, isManualSelection = false)
onSelectionChanged(newType = selectedSendType)
}
fun setScheduledSendListener(listener: ScheduledSendListener?) {
this.scheduledSendListener = listener
}
fun resetAvailableTransports(isMediaMessage: Boolean) {
availableSendTypes = MessageSendType.getAllAvailable(context, isMediaMessage)
activeMessageSendType = null
defaultTransportType = MessageSendType.TransportType.SIGNAL
defaultSubscriptionId = null
onSelectionChanged(newType = selectedSendType, isManualSelection = false)
}
fun disableTransportType(type: MessageSendType.TransportType) {
availableSendTypes = availableSendTypes.filterNot { it.transportType == type }
}
fun setDefaultTransport(type: MessageSendType.TransportType) {
if (defaultTransportType == type) {
return
}
defaultTransportType = type
onSelectionChanged(newType = selectedSendType, isManualSelection = false)
}
fun setSendType(sendType: MessageSendType?) {
private fun setSendType(sendType: MessageSendType?) {
if (activeMessageSendType == sendType) {
return
}
activeMessageSendType = sendType
onSelectionChanged(newType = selectedSendType, isManualSelection = true)
}
fun setDefaultSubscriptionId(subscriptionId: Int?) {
if (defaultSubscriptionId == subscriptionId) {
return
}
defaultSubscriptionId = subscriptionId
onSelectionChanged(newType = selectedSendType, isManualSelection = false)
onSelectionChanged(newType = selectedSendType)
}
/**
@ -146,25 +105,9 @@ class SendButton(context: Context, attributeSet: AttributeSet?) : AppCompatImage
popupContainer = container
}
private fun onSelectionChanged(newType: MessageSendType, isManualSelection: Boolean) {
private fun onSelectionChanged(newType: MessageSendType) {
setImageResource(newType.buttonDrawableRes)
contentDescription = context.getString(newType.titleRes)
for (listener in listeners) {
listener.onSendTypeChanged(newType, isManualSelection)
}
}
fun showSendTypeMenu(): Boolean {
return if (availableSendTypes.size == 1) {
if (scheduledSendListener == null && snackbarContainer != null && !SignalStore.misc().smsExportPhase.allowSmsFeatures()) {
Snackbar.make(snackbarContainer!!, R.string.InputPanel__sms_messaging_is_no_longer_supported_in_signal, Snackbar.LENGTH_SHORT).show()
}
false
} else {
showSendTypeContextMenu(false)
true
}
}
override fun onLongClick(v: View): Boolean {
@ -216,10 +159,6 @@ class SendButton(context: Context, attributeSet: AttributeSet?) : AppCompatImage
.show(items)
}
fun interface SendTypeChangedListener {
fun onSendTypeChanged(newType: MessageSendType, manuallySelected: Boolean)
}
interface ScheduledSendListener {
fun onSendScheduled()
fun canSchedule(): Boolean

View file

@ -1,30 +0,0 @@
package org.thoughtcrime.securesms.conversation;
import androidx.annotation.NonNull;
import androidx.appcompat.widget.Toolbar;
import org.thoughtcrime.securesms.util.ViewUtil;
/**
* Activity which encapsulates a conversation for a Bubble window.
*
* This activity exists so that we can override some of its manifest parameters
* without clashing with {@link ConversationActivity} and provide an API-level
* independent "is in bubble?" check.
*/
public class BubbleConversationActivity extends ConversationActivity {
@Override
public boolean isInBubble() {
return true;
}
@Override
protected void onPause() {
super.onPause();
ViewUtil.hideKeyboard(this, getComposeText());
}
@Override
public void onInitializeToolbar(@NonNull Toolbar toolbar) {
}
}

View file

@ -0,0 +1,19 @@
package org.thoughtcrime.securesms.conversation
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.conversation.v2.ConversationActivity
import org.thoughtcrime.securesms.util.ViewUtil
/**
* Activity which encapsulates a conversation for a Bubble window.
*
* This activity exists so that we can override some of its manifest parameters
* without clashing with [ConversationActivity] and provide an API-level
* independent "is in bubble?" check.
*/
class BubbleConversationActivity : ConversationActivity() {
override fun onPause() {
super.onPause()
ViewUtil.hideKeyboard(this, findViewById(R.id.fragment_container))
}
}

View file

@ -1,135 +0,0 @@
package org.thoughtcrime.securesms.conversation
import android.content.Intent
import android.os.Bundle
import android.view.MotionEvent
import android.view.View
import android.view.Window
import androidx.appcompat.content.res.AppCompatResources
import androidx.appcompat.widget.Toolbar
import io.reactivex.rxjava3.subjects.PublishSubject
import io.reactivex.rxjava3.subjects.Subject
import org.thoughtcrime.securesms.PassphraseRequiredActivity
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.components.HidingLinearLayout
import org.thoughtcrime.securesms.components.reminder.ReminderView
import org.thoughtcrime.securesms.components.settings.app.subscription.DonationPaymentComponent
import org.thoughtcrime.securesms.components.settings.app.subscription.StripeRepository
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.util.Debouncer
import org.thoughtcrime.securesms.util.DynamicNoActionBarTheme
import org.thoughtcrime.securesms.util.DynamicTheme
import org.thoughtcrime.securesms.util.views.Stub
import java.util.concurrent.TimeUnit
open class ConversationActivity : PassphraseRequiredActivity(), ConversationParentFragment.Callback, DonationPaymentComponent {
companion object {
private const val STATE_WATERMARK = "share_data_watermark"
}
private val transitionDebouncer: Debouncer = Debouncer(150, TimeUnit.MILLISECONDS)
private lateinit var fragment: ConversationParentFragment
private var shareDataTimestamp: Long = -1L
private val dynamicTheme: DynamicTheme = DynamicNoActionBarTheme()
override fun onPreCreate() {
dynamicTheme.onCreate(this)
}
override fun onCreate(savedInstanceState: Bundle?, ready: Boolean) {
supportPostponeEnterTransition()
transitionDebouncer.publish { supportStartPostponedEnterTransition() }
window.requestFeature(Window.FEATURE_ACTIVITY_TRANSITIONS)
if (savedInstanceState != null) {
shareDataTimestamp = savedInstanceState.getLong(STATE_WATERMARK, -1L)
} else if (intent.flags and Intent.FLAG_ACTIVITY_LAUNCHED_FROM_HISTORY != 0) {
shareDataTimestamp = System.currentTimeMillis()
}
setContentView(R.layout.conversation_parent_fragment_container)
if (savedInstanceState == null) {
replaceFragment(intent!!)
} else {
fragment = supportFragmentManager.findFragmentById(R.id.fragment_container) as ConversationParentFragment
}
}
override fun onDestroy() {
super.onDestroy()
transitionDebouncer.clear()
}
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
outState.putLong(STATE_WATERMARK, shareDataTimestamp)
}
override fun onNewIntent(intent: Intent?) {
super.onNewIntent(intent)
setIntent(intent)
replaceFragment(intent!!)
}
@Suppress("DEPRECATION")
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
googlePayResultPublisher.onNext(DonationPaymentComponent.GooglePayResult(requestCode, resultCode, data))
}
private fun replaceFragment(intent: Intent) {
fragment = ConversationParentFragment.create(intent)
supportFragmentManager
.beginTransaction()
.replace(R.id.fragment_container, fragment)
.disallowAddToBackStack()
.commitNowAllowingStateLoss()
}
override fun dispatchTouchEvent(ev: MotionEvent?): Boolean {
return fragment.dispatchTouchEvent(ev) || super.dispatchTouchEvent(ev)
}
override fun onResume() {
super.onResume()
dynamicTheme.onResume(this)
}
override fun getShareDataTimestamp(): Long {
return shareDataTimestamp
}
override fun setShareDataTimestamp(timestamp: Long) {
shareDataTimestamp = timestamp
}
override fun onInitializeToolbar(toolbar: Toolbar) {
toolbar.navigationIcon = AppCompatResources.getDrawable(this, R.drawable.ic_arrow_left_24)
toolbar.setNavigationOnClickListener { finish() }
}
fun getRecipient(): Recipient {
return fragment.recipient
}
fun getTitleView(): View {
return fragment.titleView
}
fun getComposeText(): View {
return fragment.composeText
}
fun getQuickAttachmentToggle(): HidingLinearLayout {
return fragment.quickAttachmentToggle
}
fun getReminderView(): Stub<ReminderView> {
return fragment.reminderView
}
override val stripeRepository: StripeRepository by lazy { StripeRepository(this) }
override val googlePayResultPublisher: Subject<DonationPaymentComponent.GooglePayResult> = PublishSubject.create()
}

View file

@ -1,256 +0,0 @@
package org.thoughtcrime.securesms.conversation;
import android.content.Context;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.annimon.stream.Stream;
import org.signal.core.util.Stopwatch;
import org.signal.core.util.logging.Log;
import org.signal.paging.PagedDataSource;
import org.thoughtcrime.securesms.attachments.DatabaseAttachment;
import org.thoughtcrime.securesms.conversation.ConversationData.MessageRequestData;
import org.thoughtcrime.securesms.conversation.ConversationMessage.ConversationMessageFactory;
import org.thoughtcrime.securesms.conversation.v2.data.AttachmentHelper;
import org.thoughtcrime.securesms.conversation.v2.data.CallHelper;
import org.thoughtcrime.securesms.conversation.v2.data.MentionHelper;
import org.thoughtcrime.securesms.conversation.v2.data.PaymentHelper;
import org.thoughtcrime.securesms.conversation.v2.data.QuotedHelper;
import org.thoughtcrime.securesms.conversation.v2.data.ReactionHelper;
import org.thoughtcrime.securesms.database.CallTable;
import org.thoughtcrime.securesms.database.MessageTable;
import org.thoughtcrime.securesms.database.SignalDatabase;
import org.thoughtcrime.securesms.database.model.InMemoryMessageRecord;
import org.thoughtcrime.securesms.database.model.MediaMmsMessageRecord;
import org.thoughtcrime.securesms.database.model.Mention;
import org.thoughtcrime.securesms.database.model.MessageId;
import org.thoughtcrime.securesms.database.model.MessageRecord;
import org.thoughtcrime.securesms.database.model.ReactionRecord;
import org.thoughtcrime.securesms.database.model.UpdateDescription;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId;
import org.whispersystems.signalservice.api.push.ServiceId;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
/**
* Core data source for loading an individual conversation.
*/
public class ConversationDataSource implements PagedDataSource<MessageId, ConversationMessage> {
private static final String TAG = Log.tag(ConversationDataSource.class);
private final Context context;
private final long threadId;
private final MessageRequestData messageRequestData;
private final boolean showUniversalExpireTimerUpdate;
/** Used once for the initial fetch, then cleared. */
private int baseSize;
private final Recipient threadRecipient;
public ConversationDataSource(
@NonNull Context context,
long threadId,
@NonNull MessageRequestData messageRequestData,
boolean showUniversalExpireTimerUpdate,
int baseSize,
@NonNull Recipient threadRecipient
) {
this.context = context;
this.threadId = threadId;
this.messageRequestData = messageRequestData;
this.showUniversalExpireTimerUpdate = showUniversalExpireTimerUpdate;
this.baseSize = baseSize;
this.threadRecipient = threadRecipient;
}
@Override
public int size() {
long startTime = System.currentTimeMillis();
int size = getSizeInternal() +
(messageRequestData.includeWarningUpdateMessage() ? 1 : 0) +
(messageRequestData.isHidden() ? 1 : 0) +
(showUniversalExpireTimerUpdate ? 1 : 0);
Log.d(TAG, "[size(), thread " + threadId + "] " + (System.currentTimeMillis() - startTime) + " ms");
return size;
}
private int getSizeInternal() {
synchronized (this) {
if (baseSize != -1) {
int size = baseSize;
baseSize = -1;
return size;
}
}
return SignalDatabase.messages().getMessageCountForThread(threadId);
}
@Override
public @NonNull List<ConversationMessage> load(int start, int length, int totalSize, @NonNull CancellationSignal cancellationSignal) {
Stopwatch stopwatch = new Stopwatch("load(" + start + ", " + length + "), thread " + threadId);
List<MessageRecord> records = new ArrayList<>(length);
MentionHelper mentionHelper = new MentionHelper();
QuotedHelper quotedHelper = new QuotedHelper();
AttachmentHelper attachmentHelper = new AttachmentHelper();
ReactionHelper reactionHelper = new ReactionHelper();
PaymentHelper paymentHelper = new PaymentHelper();
CallHelper callHelper = new CallHelper();
Set<ServiceId> referencedIds = new HashSet<>();
try (MessageTable.Reader reader = MessageTable.mmsReaderFor(SignalDatabase.messages().getConversation(threadId, start, length))) {
MessageRecord record;
while ((record = reader.getNext()) != null && !cancellationSignal.isCanceled()) {
records.add(record);
mentionHelper.add(record);
quotedHelper.add(record);
reactionHelper.add(record);
attachmentHelper.add(record);
paymentHelper.add(record);
callHelper.add(record);
UpdateDescription description = record.getUpdateDisplayBody(context, null);
if (description != null) {
referencedIds.addAll(description.getMentioned());
}
}
}
if (messageRequestData.includeWarningUpdateMessage() && (start + length >= totalSize)) {
records.add(new InMemoryMessageRecord.NoGroupsInCommon(threadId, messageRequestData.isGroup()));
}
if (messageRequestData.isHidden() && (start + length >= totalSize)) {
records.add(new InMemoryMessageRecord.RemovedContactHidden(threadId));
}
if (showUniversalExpireTimerUpdate) {
records.add(new InMemoryMessageRecord.UniversalExpireTimerUpdate(threadId));
}
stopwatch.split("messages");
mentionHelper.fetchMentions(context);
stopwatch.split("mentions");
quotedHelper.fetchQuotedState();
stopwatch.split("is-quoted");
reactionHelper.fetchReactions();
stopwatch.split("reactions");
records = reactionHelper.buildUpdatedModels(records);
stopwatch.split("reaction-models");
attachmentHelper.fetchAttachments();
stopwatch.split("attachments");
records = attachmentHelper.buildUpdatedModels(context, records);
stopwatch.split("attachment-models");
paymentHelper.fetchPayments();
stopwatch.split("payments");
records = paymentHelper.buildUpdatedModels(records);
stopwatch.split("payment-models");
callHelper.fetchCalls();
stopwatch.split("calls");
records = callHelper.buildUpdatedModels(records);
stopwatch.split("call-models");
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()), threadRecipient))
.toList();
stopwatch.split("conversion");
stopwatch.stop(TAG);
return messages;
}
@Override
public @Nullable ConversationMessage load(@NonNull MessageId messageId) {
Stopwatch stopwatch = new Stopwatch("load(" + messageId + "), thread " + threadId);
MessageRecord record = SignalDatabase.messages().getMessageRecordOrNull(messageId.getId());
if (record instanceof MediaMmsMessageRecord &&
((MediaMmsMessageRecord) record).getParentStoryId() != null &&
((MediaMmsMessageRecord) record).getParentStoryId().isGroupReply()) {
return null;
}
if (record instanceof MediaMmsMessageRecord && ((MediaMmsMessageRecord) record).getScheduledDate() != -1) {
return null;
}
stopwatch.split("message");
try {
if (record != null) {
List<Mention> mentions = SignalDatabase.mentions().getMentionsForMessage(messageId.getId());
stopwatch.split("mentions");
boolean isQuoted = SignalDatabase.messages().isQuoted(record);
stopwatch.split("is-quoted");
List<ReactionRecord> reactions = SignalDatabase.reactions().getReactions(messageId);
record = ReactionHelper.recordWithReactions(record, reactions);
stopwatch.split("reactions");
List<DatabaseAttachment> attachments = SignalDatabase.attachments().getAttachmentsForMessage(messageId.getId());
if (attachments.size() > 0) {
record = ((MediaMmsMessageRecord) record).withAttachments(context, attachments);
}
stopwatch.split("attachments");
if (record.isPaymentNotification()) {
record = SignalDatabase.payments().updateMessageWithPayment(record);
}
stopwatch.split("payments");
if (record.isCallLog() && !record.isGroupCall()) {
CallTable.Call call = SignalDatabase.calls().getCallByMessageId(record.getId());
if (call != null && record instanceof MediaMmsMessageRecord) {
record = ((MediaMmsMessageRecord) record).withCall(call);
}
}
stopwatch.split("calls");
return ConversationMessage.ConversationMessageFactory.createWithUnresolvedData(ApplicationDependencies.getApplication(),
record,
record.getDisplayBody(ApplicationDependencies.getApplication()),
mentions,
isQuoted,
threadRecipient);
} else {
return null;
}
} finally {
stopwatch.stop(TAG);
}
}
@Override
public @NonNull MessageId getKey(@NonNull ConversationMessage conversationMessage) {
return new MessageId(conversationMessage.getMessageRecord().getId());
}
}

View file

@ -1,293 +0,0 @@
package org.thoughtcrime.securesms.conversation;
import android.app.Application;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.WorkerThread;
import androidx.fragment.app.FragmentManager;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
import androidx.lifecycle.Transformations;
import androidx.lifecycle.ViewModel;
import androidx.lifecycle.ViewModelProvider;
import com.annimon.stream.Stream;
import org.signal.core.util.concurrent.SignalExecutors;
import org.thoughtcrime.securesms.database.GroupTable;
import org.thoughtcrime.securesms.database.model.GroupRecord;
import org.thoughtcrime.securesms.database.SignalDatabase;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.groups.GroupChangeBusyException;
import org.thoughtcrime.securesms.groups.GroupChangeFailedException;
import org.thoughtcrime.securesms.groups.GroupId;
import org.thoughtcrime.securesms.groups.GroupManager;
import org.thoughtcrime.securesms.groups.GroupsV1MigrationUtil;
import org.thoughtcrime.securesms.groups.ui.GroupChangeFailureReason;
import org.thoughtcrime.securesms.groups.ui.invitesandrequests.invite.GroupLinkInviteFriendsBottomSheetDialogFragment;
import org.thoughtcrime.securesms.groups.v2.GroupBlockJoinRequestResult;
import org.thoughtcrime.securesms.groups.v2.GroupManagementRepository;
import org.thoughtcrime.securesms.profiles.spoofing.ReviewRecipient;
import org.thoughtcrime.securesms.profiles.spoofing.ReviewUtil;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.util.AsynchronousCallback;
import org.signal.core.util.concurrent.SimpleTask;
import org.thoughtcrime.securesms.util.livedata.LiveDataUtil;
import java.io.IOException;
import java.util.Collections;
import java.util.List;
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
import io.reactivex.rxjava3.core.Single;
final class ConversationGroupViewModel extends ViewModel {
private final MutableLiveData<Recipient> liveRecipient;
private final LiveData<GroupActiveState> groupActiveState;
private final LiveData<ConversationMemberLevel> selfMembershipLevel;
private final LiveData<Integer> actionableRequestingMembers;
private final LiveData<ReviewState> reviewState;
private final LiveData<List<RecipientId>> gv1MigrationSuggestions;
private final GroupManagementRepository groupManagementRepository;
private boolean firstTimeInviteFriendsTriggered;
private ConversationGroupViewModel() {
this.liveRecipient = new MutableLiveData<>();
this.groupManagementRepository = new GroupManagementRepository();
LiveData<GroupRecord> groupRecord = LiveDataUtil.mapAsync(liveRecipient, ConversationGroupViewModel::getGroupRecordForRecipient);
LiveData<List<Recipient>> duplicates = LiveDataUtil.mapAsync(groupRecord, record -> {
if (record != null && record.isV2Group()) {
return Stream.of(ReviewUtil.getDuplicatedRecipients(record.getId().requireV2()))
.map(ReviewRecipient::getRecipient)
.toList();
} else {
return Collections.emptyList();
}
});
this.groupActiveState = Transformations.distinctUntilChanged(Transformations.map(groupRecord, ConversationGroupViewModel::mapToGroupActiveState));
this.selfMembershipLevel = Transformations.distinctUntilChanged(Transformations.map(groupRecord, ConversationGroupViewModel::mapToSelfMembershipLevel));
this.actionableRequestingMembers = Transformations.distinctUntilChanged(Transformations.map(groupRecord, ConversationGroupViewModel::mapToActionableRequestingMemberCount));
this.gv1MigrationSuggestions = Transformations.distinctUntilChanged(LiveDataUtil.mapAsync(groupRecord, ConversationGroupViewModel::mapToGroupV1MigrationSuggestions));
this.reviewState = LiveDataUtil.combineLatest(groupRecord,
duplicates,
(record, dups) -> dups.isEmpty()
? ReviewState.EMPTY
: new ReviewState(record.getId().requireV2(), dups.get(0), dups.size()));
}
void onRecipientChange(Recipient recipient) {
liveRecipient.setValue(recipient);
}
void onSuggestedMembersBannerDismissed(@NonNull GroupId groupId) {
SignalExecutors.BOUNDED.execute(() -> {
if (groupId.isV2()) {
SignalDatabase.groups().removeUnmigratedV1Members(groupId.requireV2());
liveRecipient.postValue(liveRecipient.getValue());
}
});
}
/**
* The number of pending group join requests that can be actioned by this client.
*/
LiveData<Integer> getActionableRequestingMembers() {
return actionableRequestingMembers;
}
LiveData<GroupActiveState> getGroupActiveState() {
return groupActiveState;
}
LiveData<ConversationMemberLevel> getSelfMemberLevel() {
return selfMembershipLevel;
}
public LiveData<ReviewState> getReviewState() {
return reviewState;
}
@NonNull LiveData<List<RecipientId>> getGroupV1MigrationSuggestions() {
return gv1MigrationSuggestions;
}
boolean isNonAdminInAnnouncementGroup() {
ConversationMemberLevel level = selfMembershipLevel.getValue();
return level != null && level.getMemberLevel() != GroupTable.MemberLevel.ADMINISTRATOR && level.isAnnouncementGroup();
}
private static @Nullable GroupRecord getGroupRecordForRecipient(@Nullable Recipient recipient) {
if (recipient != null && recipient.isGroup()) {
Application context = ApplicationDependencies.getApplication();
GroupTable groupDatabase = SignalDatabase.groups();
return groupDatabase.getGroup(recipient.getId()).orElse(null);
} else {
return null;
}
}
private static int mapToActionableRequestingMemberCount(@Nullable GroupRecord record) {
if (record != null &&
record.isV2Group() &&
record.memberLevel(Recipient.self()) == GroupTable.MemberLevel.ADMINISTRATOR)
{
return record.requireV2GroupProperties()
.getDecryptedGroup()
.getRequestingMembersCount();
} else {
return 0;
}
}
private static GroupActiveState mapToGroupActiveState(@Nullable GroupRecord record) {
if (record == null) {
return null;
}
return new GroupActiveState(record.isActive(), record.isV2Group());
}
private static ConversationMemberLevel mapToSelfMembershipLevel(@Nullable GroupRecord record) {
if (record == null) {
return null;
}
return new ConversationMemberLevel(record.memberLevel(Recipient.self()), record.isAnnouncementGroup());
}
@WorkerThread
private static List<RecipientId> mapToGroupV1MigrationSuggestions(@Nullable GroupRecord record) {
if (record == null ||
!record.isV2Group() ||
!record.isActive() ||
record.isPendingMember(Recipient.self())) {
return Collections.emptyList();
}
return Stream.of(record.getUnmigratedV1Members())
.filterNot(m -> record.getMembers().contains(m))
.map(Recipient::resolved)
.filter(GroupsV1MigrationUtil::isAutoMigratable)
.map(Recipient::getId)
.toList();
}
public static void onCancelJoinRequest(@NonNull Recipient recipient,
@NonNull AsynchronousCallback.WorkerThread<Void, GroupChangeFailureReason> callback)
{
SignalExecutors.UNBOUNDED.execute(() -> {
if (!recipient.isPushV2Group()) {
throw new AssertionError();
}
try {
GroupManager.cancelJoinRequest(ApplicationDependencies.getApplication(), recipient.getGroupId().get().requireV2());
callback.onComplete(null);
} catch (GroupChangeFailedException | GroupChangeBusyException | IOException e) {
callback.onError(GroupChangeFailureReason.fromException(e));
}
});
}
void inviteFriendsOneTimeIfJustSelfInGroup(@NonNull FragmentManager supportFragmentManager, @NonNull GroupId.V2 groupId) {
if (firstTimeInviteFriendsTriggered) {
return;
}
firstTimeInviteFriendsTriggered = true;
SimpleTask.run(() -> SignalDatabase.groups()
.requireGroup(groupId)
.getMembers().equals(Collections.singletonList(Recipient.self().getId())),
justSelf -> {
if (justSelf) {
inviteFriends(supportFragmentManager, groupId);
}
}
);
}
void inviteFriends(@NonNull FragmentManager supportFragmentManager, @NonNull GroupId.V2 groupId) {
GroupLinkInviteFriendsBottomSheetDialogFragment.show(supportFragmentManager, groupId);
}
public Single<GroupBlockJoinRequestResult> blockJoinRequests(@NonNull Recipient groupRecipient, @NonNull Recipient recipient) {
return groupManagementRepository.blockJoinRequests(groupRecipient.requireGroupId().requireV2(), recipient)
.observeOn(AndroidSchedulers.mainThread());
}
static final class ReviewState {
private static final ReviewState EMPTY = new ReviewState(null, Recipient.UNKNOWN, 0);
private final GroupId.V2 groupId;
private final Recipient recipient;
private final int count;
ReviewState(@Nullable GroupId.V2 groupId, @NonNull Recipient recipient, int count) {
this.groupId = groupId;
this.recipient = recipient;
this.count = count;
}
public @Nullable GroupId.V2 getGroupId() {
return groupId;
}
public @NonNull Recipient getRecipient() {
return recipient;
}
public int getCount() {
return count;
}
}
static final class GroupActiveState {
private final boolean isActive;
private final boolean isActiveV2;
public GroupActiveState(boolean isActive, boolean isV2) {
this.isActive = isActive;
this.isActiveV2 = isActive && isV2;
}
public boolean isActiveGroup() {
return isActive;
}
public boolean isActiveV2Group() {
return isActiveV2;
}
}
static final class ConversationMemberLevel {
private final GroupTable.MemberLevel memberLevel;
private final boolean isAnnouncementGroup;
private ConversationMemberLevel(GroupTable.MemberLevel memberLevel, boolean isAnnouncementGroup) {
this.memberLevel = memberLevel;
this.isAnnouncementGroup = isAnnouncementGroup;
}
public @NonNull GroupTable.MemberLevel getMemberLevel() {
return memberLevel;
}
public boolean isAnnouncementGroup() {
return isAnnouncementGroup;
}
}
static class Factory extends ViewModelProvider.NewInstanceFactory {
@Override
public @NonNull <T extends ViewModel> T create(@NonNull Class<T> modelClass) {
//noinspection ConstantConditions
return modelClass.cast(new ConversationGroupViewModel());
}
}
}

View file

@ -16,14 +16,13 @@ import org.thoughtcrime.securesms.conversation.colors.ChatColors;
import org.thoughtcrime.securesms.conversation.v2.ConversationActivity;
import org.thoughtcrime.securesms.database.SignalDatabase;
import org.thoughtcrime.securesms.database.ThreadTable;
import org.thoughtcrime.securesms.keyvalue.SignalStore;
import org.thoughtcrime.securesms.mediasend.Media;
import org.thoughtcrime.securesms.mms.SlideFactory;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.stickers.StickerLocator;
import org.thoughtcrime.securesms.util.FeatureFlags;
import org.thoughtcrime.securesms.wallpaper.ChatWallpaper;
import org.whispersystems.signalservice.api.util.Preconditions;
import java.util.ArrayList;
import java.util.Collection;
@ -65,7 +64,6 @@ public class ConversationIntents {
* @param context Context for Intent creation
* @param recipientId The RecipientId to query the thread ID for if the passed one is invalid.
* @param threadId The threadId, or -1L
*
* @return A Single that will return a builder to create the conversation intent.
*/
@MainThread
@ -81,11 +79,11 @@ public class ConversationIntents {
}
public static @NonNull Builder createPopUpBuilder(@NonNull Context context, @NonNull RecipientId recipientId, long threadId) {
return new Builder(context, ConversationPopupActivity.class, recipientId, threadId);
return new Builder(context, ConversationPopupActivity.class, recipientId, threadId, ConversationScreenType.POPUP);
}
public static @NonNull Intent createBubbleIntent(@NonNull Context context, @NonNull RecipientId recipientId, long threadId) {
return new Builder(context, BubbleConversationActivity.class, recipientId, threadId).build();
return new Builder(context, BubbleConversationActivity.class, recipientId, threadId, ConversationScreenType.BUBBLE).build();
}
/**
@ -95,20 +93,11 @@ public class ConversationIntents {
* @param context Context for Intent creation
* @param recipientId The recipientId, only used if the threadId is not valid
* @param threadId The threadId, required for CFV2.
*
* @return A builder that can be used to create a conversation intent.
*/
public static @NonNull Builder createBuilderSync(@NonNull Context context, @NonNull RecipientId recipientId, long threadId) {
return new Builder(context, recipientId, threadId);
}
static boolean isInvalid(@NonNull Bundle arguments) {
Uri uri = getIntentData(arguments);
if (isBubbleIntentUri(uri)) {
return uri.getQueryParameter(EXTRA_RECIPIENT) == null;
} else {
return !arguments.containsKey(EXTRA_RECIPIENT);
}
Preconditions.checkArgument(threadId > 0, "threadId is invalid");
return new Builder(context, ConversationActivity.class, recipientId, threadId, ConversationScreenType.NORMAL);
}
static @Nullable Uri getIntentData(@NonNull Bundle bundle) {
@ -119,7 +108,7 @@ public class ConversationIntents {
return bundle.getString(INTENT_TYPE);
}
static @NonNull Bundle createParentFragmentArguments(@NonNull Intent intent) {
public static @NonNull Bundle createParentFragmentArguments(@NonNull Intent intent) {
Bundle bundle = new Bundle();
if (intent.getExtras() != null) {
@ -132,7 +121,7 @@ public class ConversationIntents {
return bundle;
}
static boolean isBubbleIntentUri(@Nullable Uri uri) {
public static boolean isBubbleIntentUri(@Nullable Uri uri) {
return uri != null && Objects.equals(uri.getAuthority(), BUBBLE_AUTHORITY);
}
@ -311,6 +300,7 @@ public class ConversationIntents {
private final Class<? extends Activity> conversationActivityClass;
private final RecipientId recipientId;
private final long threadId;
private final ConversationScreenType conversationScreenType;
private String draftText;
private List<Media> media;
@ -324,30 +314,18 @@ public class ConversationIntents {
private boolean withSearchOpen;
private Badge giftBadge;
private long shareDataTimestamp = -1L;
private ConversationScreenType conversationScreenType;
private Builder(@NonNull Context context,
@NonNull RecipientId recipientId,
long threadId)
{
this(
context,
getBaseConversationActivity(),
recipientId,
threadId
);
}
private Builder(@NonNull Context context,
@NonNull Class<? extends Activity> conversationActivityClass,
@NonNull RecipientId recipientId,
long threadId)
long threadId,
@NonNull ConversationScreenType conversationScreenType)
{
this.context = context;
this.conversationActivityClass = conversationActivityClass;
this.recipientId = recipientId;
this.threadId = checkThreadId(threadId);
this.conversationScreenType = ConversationScreenType.fromActivityClass(conversationActivityClass);
this.conversationScreenType = conversationScreenType;
}
public @NonNull Builder withDraftText(@Nullable String draftText) {
@ -419,7 +397,7 @@ public class ConversationIntents {
intent.setAction(Intent.ACTION_DEFAULT);
if (Objects.equals(conversationActivityClass, BubbleConversationActivity.class)) {
if (conversationScreenType.isInBubble()) {
intent.setData(new Uri.Builder().authority(BUBBLE_AUTHORITY)
.appendQueryParameter(EXTRA_RECIPIENT, recipientId.serialize())
.appendQueryParameter(EXTRA_THREAD_ID, String.valueOf(threadId))
@ -459,13 +437,9 @@ public class ConversationIntents {
intent.setType(dataType);
}
if (FeatureFlags.useConversationFragmentV2()) {
Bundle args = ConversationIntents.createParentFragmentArguments(intent);
Bundle args = ConversationIntents.createParentFragmentArguments(intent);
return intent.putExtras(args);
} else {
return intent;
}
return intent.putExtras(args);
}
}
@ -501,31 +475,13 @@ public class ConversationIntents {
return NORMAL;
}
private static @NonNull ConversationScreenType fromActivityClass(Class<? extends Activity> activityClass) {
if (Objects.equals(activityClass, ConversationPopupActivity.class)) {
return POPUP;
} else if (Objects.equals(activityClass, BubbleConversationActivity.class)) {
return BUBBLE;
} else {
return NORMAL;
}
}
}
private static long checkThreadId(long threadId) {
if (threadId < 0 && FeatureFlags.useConversationFragmentV2()) {
if (threadId < 0) {
throw new IllegalArgumentException("ThreadId is a required field in CFV2");
} else {
return threadId;
}
}
private static Class<? extends Activity> getBaseConversationActivity() {
if (FeatureFlags.useConversationFragmentV2()) {
return ConversationActivity.class;
} else {
return org.thoughtcrime.securesms.conversation.ConversationActivity.class;
}
}
}

View file

@ -1,87 +0,0 @@
package org.thoughtcrime.securesms.conversation;
import android.os.Bundle;
import android.view.Display;
import android.view.Gravity;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.View;
import android.view.WindowManager;
import androidx.appcompat.widget.Toolbar;
import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.R;
public class ConversationPopupActivity extends ConversationActivity {
private static final String TAG = Log.tag(ConversationPopupActivity.class);
@Override
protected void onPreCreate() {
super.onPreCreate();
overridePendingTransition(R.anim.slide_from_top, R.anim.slide_to_top);
}
@Override
protected void onCreate(Bundle bundle, boolean ready) {
getWindow().setFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND,
WindowManager.LayoutParams.FLAG_DIM_BEHIND);
WindowManager.LayoutParams params = getWindow().getAttributes();
params.alpha = 1.0f;
params.dimAmount = 0.1f;
params.gravity = Gravity.TOP;
getWindow().setAttributes(params);
Display display = getWindowManager().getDefaultDisplay();
int width = display.getWidth();
int height = display.getHeight();
if (height > width) getWindow().setLayout((int) (width * .85), (int) (height * .5));
else getWindow().setLayout((int) (width * .7), (int) (height * .75));
super.onCreate(bundle, ready);
}
@Override
protected void onResume() {
super.onResume();
getTitleView().setOnClickListener(null);
getComposeText().requestFocus();
getQuickAttachmentToggle().disable();
}
@Override
protected void onPause() {
super.onPause();
if (isFinishing()) overridePendingTransition(R.anim.slide_from_top, R.anim.slide_to_top);
}
@Override
public boolean onPrepareOptionsMenu(Menu menu) {
MenuInflater inflater = this.getMenuInflater();
menu.clear();
inflater.inflate(R.menu.conversation_popup, menu);
return true;
}
@Override
public void onInitializeToolbar(Toolbar toolbar) {
}
@Override
public void onSendComplete(long threadId) {
finish();
}
@Override
public boolean onUpdateReminders() {
if (getReminderView().resolved()) {
getReminderView().get().setVisibility(View.GONE);
}
return false;
}
}

View file

@ -0,0 +1,52 @@
/*
* Copyright 2023 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.conversation
import android.os.Bundle
import android.view.Gravity
import android.view.WindowManager
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.conversation.v2.ConversationActivity
/**
* Flavor of [ConversationActivity] used for quick replies to notifications in pre-API 24 devices.
*/
class ConversationPopupActivity : ConversationActivity() {
override fun onPreCreate() {
super.onPreCreate()
overridePendingTransition(R.anim.slide_from_top, R.anim.slide_to_top)
}
@Suppress("DEPRECATION")
override fun onCreate(bundle: Bundle?, ready: Boolean) {
window.setFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND, WindowManager.LayoutParams.FLAG_DIM_BEHIND)
window.attributes = window.attributes.apply {
alpha = 1.0f
dimAmount = 0.1f
gravity = Gravity.TOP
}
val display = windowManager.defaultDisplay
val width = display.width
val height = display.height
if (height > width) {
window.setLayout((width * .85).toInt(), (height * .5).toInt())
} else {
window.setLayout((width * .7).toInt(), (height * .75).toInt())
}
super.onCreate(bundle, ready)
}
override fun onPause() {
super.onPause()
if (isFinishing) {
overridePendingTransition(R.anim.slide_from_top, R.anim.slide_to_top)
}
}
}

View file

@ -23,7 +23,6 @@ import android.widget.FrameLayout;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.constraintlayout.widget.Barrier;
import androidx.constraintlayout.widget.ConstraintLayout;
import androidx.constraintlayout.widget.ConstraintSet;
import androidx.core.content.ContextCompat;
@ -33,7 +32,6 @@ import androidx.vectordrawable.graphics.drawable.AnimatorInflaterCompat;
import com.annimon.stream.Stream;
import org.signal.core.util.DimensionUnit;
import org.signal.glide.Log;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.animation.AnimationCompleteListener;
import org.thoughtcrime.securesms.components.emoji.EmojiImageView;
@ -178,14 +176,6 @@ public final class ConversationReactionOverlay extends FrameLayout {
bottomNavigationBarHeight = 0;
}
if (!FeatureFlags.useConversationFragmentV2()) {
toolbarShade.setVisibility(VISIBLE);
toolbarShade.setAlpha(1f);
inputShade.setVisibility(VISIBLE);
inputShade.setAlpha(1f);
}
Bitmap conversationItemSnapshot = selectedConversationModel.getBitmap();
conversationItem.setLayoutParams(new LayoutParams(conversationItemSnapshot.getWidth(), conversationItemSnapshot.getHeight()));
@ -211,8 +201,8 @@ public final class ConversationReactionOverlay extends FrameLayout {
@NonNull ConversationMessage conversationMessage,
@NonNull PointF lastSeenDownPoint,
boolean isMessageOnLeft) {
updateToolbarShade(activity);
updateInputShade(activity);
updateToolbarShade();
updateInputShade();
contextMenu = new ConversationContextMenu(dropdownAnchor, getMenuActionItems(conversationMessage));
@ -394,50 +384,18 @@ public final class ConversationReactionOverlay extends FrameLayout {
return Math.max(reactionStartingPoint - reactionBarOffset - reactionBarHeight, spaceNeededBetweenTopOfScreenAndTopOfReactionBar);
}
private void updateToolbarShade(@NonNull Activity activity) {
if (FeatureFlags.useConversationFragmentV2()) {
LayoutParams layoutParams = (LayoutParams) toolbarShade.getLayoutParams();
layoutParams.height = 0;
toolbarShade.setLayoutParams(layoutParams);
return;
}
View toolbar = activity.findViewById(R.id.toolbar);
View bannerContainer = activity.findViewById(FeatureFlags.useConversationFragmentV2() ? R.id.conversation_banner
: R.id.conversation_banner_container);
private void updateToolbarShade() {
LayoutParams layoutParams = (LayoutParams) toolbarShade.getLayoutParams();
layoutParams.height = toolbar.getHeight() + bannerContainer.getHeight();
layoutParams.height = 0;
toolbarShade.setLayoutParams(layoutParams);
}
private void updateInputShade(@NonNull Activity activity) {
if (FeatureFlags.useConversationFragmentV2()) {
LayoutParams layoutParams = (LayoutParams) inputShade.getLayoutParams();
layoutParams.height = 0;
inputShade.setLayoutParams(layoutParams);
return;
}
private void updateInputShade() {
LayoutParams layoutParams = (LayoutParams) inputShade.getLayoutParams();
layoutParams.bottomMargin = bottomNavigationBarHeight;
layoutParams.height = getInputPanelHeight(activity);
layoutParams.height = 0;
inputShade.setLayoutParams(layoutParams);
}
private int getInputPanelHeight(@NonNull Activity activity) {
if (FeatureFlags.useConversationFragmentV2()) {
View bottomPanel = activity.findViewById(R.id.conversation_input_panel);
return bottomPanel.getHeight();
}
View bottomPanel = activity.findViewById(R.id.conversation_activity_panel_parent);
View emojiDrawer = activity.findViewById(R.id.emoji_drawer);
return bottomPanel.getHeight() + (emojiDrawer != null && emojiDrawer.getVisibility() == VISIBLE ? emojiDrawer.getHeight() : 0);
}
/**
* Returns true when the device is in a configuration where the navigation bar doesn't take up
* space at the bottom of the screen.
@ -915,22 +873,6 @@ public final class ConversationReactionOverlay extends FrameLayout {
itemYAnim.setDuration(duration);
animators.add(itemYAnim);
if (!FeatureFlags.useConversationFragmentV2()) {
ObjectAnimator toolbarShadeAnim = new ObjectAnimator();
toolbarShadeAnim.setProperty(View.ALPHA);
toolbarShadeAnim.setFloatValues(0f);
toolbarShadeAnim.setTarget(toolbarShade);
toolbarShadeAnim.setDuration(duration);
animators.add(toolbarShadeAnim);
ObjectAnimator inputShadeAnim = new ObjectAnimator();
inputShadeAnim.setProperty(View.ALPHA);
inputShadeAnim.setFloatValues(0f);
inputShadeAnim.setTarget(inputShade);
inputShadeAnim.setDuration(duration);
animators.add(inputShadeAnim);
}
if (activity != null) {
ValueAnimator statusBarAnim = ValueAnimator.ofArgb(activity.getWindow().getStatusBarColor(), originalStatusBarColor);
statusBarAnim.setDuration(duration);

View file

@ -54,17 +54,6 @@ public class ConversationRepository {
this.context = ApplicationDependencies.getApplication();
}
@WorkerThread
boolean canShowAsBubble(long threadId) {
if (Build.VERSION.SDK_INT >= ConversationUtil.CONVERSATION_SUPPORT_VERSION) {
Recipient recipient = SignalDatabase.threads().getRecipientForThreadId(threadId);
return recipient != null && BubbleUtil.canBubble(context, recipient.getId(), threadId);
} else {
return false;
}
}
@WorkerThread
public @NonNull ConversationData getConversationData(long threadId, @NonNull Recipient conversationRecipient, int jumpToPosition) {
ThreadTable.ConversationMetadata metadata = SignalDatabase.threads().getConversationMetadata(threadId);
@ -143,65 +132,6 @@ public class ConversationRepository {
});
}
@NonNull Single<Boolean> checkIfMmsIsEnabled() {
return Single.fromCallable(() -> Util.isMmsCapable(context)).subscribeOn(Schedulers.io());
}
/**
* Watches the given recipient id for changes, and gets the security info for the recipient
* whenever a change occurs.
*
* @param recipientId The recipient id we are interested in
*
* @return The recipient's security info.
*/
@NonNull Observable<ConversationSecurityInfo> getSecurityInfo(@NonNull RecipientId recipientId) {
return Recipient.observable(recipientId)
.distinctUntilChanged((lhs, rhs) -> lhs.isPushGroup() == rhs.isPushGroup() && lhs.getRegistered().equals(rhs.getRegistered()))
.switchMapSingle(this::getSecurityInfo)
.subscribeOn(Schedulers.io());
}
private @NonNull Single<ConversationSecurityInfo> getSecurityInfo(@NonNull Recipient recipient) {
return Single.fromCallable(() -> {
Log.i(TAG, "Resolving registered state...");
RecipientTable.RegisteredState registeredState;
if (recipient.isPushGroup()) {
Log.i(TAG, "Push group recipient...");
registeredState = RecipientTable.RegisteredState.REGISTERED;
} else {
Log.i(TAG, "Checking through resolved recipient");
registeredState = recipient.getRegistered();
}
Log.i(TAG, "Resolved registered state: " + registeredState);
boolean signalEnabled = Recipient.self().isRegistered();
if (registeredState == RecipientTable.RegisteredState.UNKNOWN) {
try {
Log.i(TAG, "Refreshing directory for user: " + recipient.getId().serialize());
registeredState = ContactDiscovery.refresh(context, recipient, false);
} catch (IOException e) {
Log.w(TAG, e);
}
}
long threadId = SignalDatabase.threads().getThreadIdIfExistsFor(recipient.getId());
boolean hasUnexportedInsecureMessages = threadId != -1 && SignalDatabase.messages().getUnexportedInsecureMessagesCount(threadId) > 0;
Log.i(TAG, "Returning registered state...");
return new ConversationSecurityInfo(recipient.getId(),
registeredState == RecipientTable.RegisteredState.REGISTERED && signalEnabled,
Util.isDefaultSmsProvider(context),
true,
hasUnexportedInsecureMessages,
SignalStore.misc().isClientDeprecated(),
TextSecurePreferences.isUnauthorizedReceived(context));
}).subscribeOn(Schedulers.io());
}
@NonNull
public Single<ConversationMessage> resolveMessageToEdit(@NonNull ConversationMessage message) {
return Single.fromCallable(() -> {
@ -223,42 +153,4 @@ public class ConversationRepository {
}).subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread());
}
Observable<Integer> getUnreadCount(long threadId, long afterTime) {
if (threadId <= -1L || afterTime <= 0L) {
return Observable.just(0);
}
return Observable.<Integer> create(emitter -> {
DatabaseObserver.Observer listener = () -> emitter.onNext(SignalDatabase.messages().getIncomingMeaningfulMessageCountSince(threadId, afterTime));
ApplicationDependencies.getDatabaseObserver().registerConversationObserver(threadId, listener);
emitter.setCancellable(() -> ApplicationDependencies.getDatabaseObserver().unregisterObserver(listener));
listener.onChanged();
}).subscribeOn(Schedulers.io());
}
public void insertSmsExportUpdateEvent(Recipient recipient) {
SignalExecutors.BOUNDED.execute(() -> {
long threadId = SignalDatabase.threads().getThreadIdIfExistsFor(recipient.getId());
if (threadId == -1 || !Util.isDefaultSmsProvider(context)) {
return;
}
if (RecipientUtil.isSmsOnly(threadId, recipient) && (!recipient.isMmsGroup() || Util.isDefaultSmsProvider(context))) {
SignalDatabase.messages().insertSmsExportMessage(recipient.getId(), threadId);
}
});
}
public void setConversationMuted(@NonNull RecipientId recipientId, long until) {
SignalExecutors.BOUNDED.execute(() -> SignalDatabase.recipients().setMuted(recipientId, until));
}
public void setConversationDistributionType(long threadId, int distributionType) {
SignalExecutors.BOUNDED.execute(() -> SignalDatabase.threads().setDistributionType(threadId, distributionType));
}
}

View file

@ -1,79 +0,0 @@
package org.thoughtcrime.securesms.conversation;
import android.app.Application;
import android.text.TextUtils;
import androidx.annotation.NonNull;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
import androidx.lifecycle.ViewModel;
import androidx.lifecycle.ViewModelProvider;
import org.thoughtcrime.securesms.database.DatabaseObserver;
import org.thoughtcrime.securesms.database.model.StickerRecord;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.emoji.EmojiSource;
import org.thoughtcrime.securesms.stickers.StickerSearchRepository;
import org.thoughtcrime.securesms.util.Throttler;
import java.util.Collections;
import java.util.List;
class ConversationStickerViewModel extends ViewModel {
private final StickerSearchRepository repository;
private final MutableLiveData<List<StickerRecord>> stickers;
private final MutableLiveData<Boolean> stickersAvailable;
private final Throttler availabilityThrottler;
private final DatabaseObserver.Observer packObserver;
private ConversationStickerViewModel(@NonNull Application application, @NonNull StickerSearchRepository repository) {
this.repository = repository;
this.stickers = new MutableLiveData<>();
this.stickersAvailable = new MutableLiveData<>();
this.availabilityThrottler = new Throttler(500);
this.packObserver = () -> {
availabilityThrottler.publish(() -> repository.getStickerFeatureAvailability(stickersAvailable::postValue));
};
ApplicationDependencies.getDatabaseObserver().registerStickerPackObserver(packObserver);
}
@NonNull LiveData<List<StickerRecord>> getStickerResults() {
return stickers;
}
@NonNull LiveData<Boolean> getStickersAvailability() {
repository.getStickerFeatureAvailability(stickersAvailable::postValue);
return stickersAvailable;
}
void onInputTextUpdated(@NonNull String text) {
if (TextUtils.isEmpty(text) || text.length() > EmojiSource.getLatest().getMaxEmojiLength()) {
stickers.setValue(Collections.emptyList());
} else {
repository.searchByEmoji(text, stickers::postValue);
}
}
@Override
protected void onCleared() {
ApplicationDependencies.getDatabaseObserver().unregisterObserver(packObserver);
}
static class Factory extends ViewModelProvider.NewInstanceFactory {
private final Application application;
private final StickerSearchRepository repository;
public Factory(@NonNull Application application, @NonNull StickerSearchRepository repository) {
this.application = application;
this.repository = repository;
}
@Override
public @NonNull <T extends ViewModel> T create(@NonNull Class<T> modelClass) {
//noinspection ConstantConditions
return modelClass.cast(new ConversationStickerViewModel(application, repository));
}
}
}

View file

@ -1,513 +0,0 @@
package org.thoughtcrime.securesms.conversation;
import android.app.Application;
import androidx.annotation.MainThread;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.LiveDataReactiveStreams;
import androidx.lifecycle.MutableLiveData;
import androidx.lifecycle.Observer;
import androidx.lifecycle.Transformations;
import androidx.lifecycle.ViewModel;
import androidx.lifecycle.ViewModelProvider;
import org.greenrobot.eventbus.EventBus;
import org.greenrobot.eventbus.Subscribe;
import org.greenrobot.eventbus.ThreadMode;
import org.signal.core.util.logging.Log;
import org.signal.libsignal.protocol.util.Pair;
import org.signal.paging.ObservablePagedData;
import org.signal.paging.PagedData;
import org.signal.paging.PagingConfig;
import org.signal.paging.PagingController;
import org.signal.paging.ProxyPagingController;
import org.thoughtcrime.securesms.components.settings.app.notifications.profiles.NotificationProfilesRepository;
import org.thoughtcrime.securesms.conversation.colors.ChatColors;
import org.thoughtcrime.securesms.conversation.colors.GroupAuthorNameColorHelper;
import org.thoughtcrime.securesms.conversation.colors.NameColor;
import org.thoughtcrime.securesms.database.DatabaseObserver;
import org.thoughtcrime.securesms.database.model.MessageId;
import org.thoughtcrime.securesms.database.model.StoryViewState;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.mediasend.Media;
import org.thoughtcrime.securesms.mediasend.MediaRepository;
import org.thoughtcrime.securesms.notifications.profiles.NotificationProfile;
import org.thoughtcrime.securesms.notifications.profiles.NotificationProfiles;
import org.thoughtcrime.securesms.ratelimit.RecaptchaRequiredEvent;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.util.SignalLocalMetrics;
import org.thoughtcrime.securesms.util.SingleLiveEvent;
import org.thoughtcrime.securesms.util.Util;
import org.thoughtcrime.securesms.util.ViewUtil;
import org.thoughtcrime.securesms.util.livedata.LiveDataUtil;
import org.thoughtcrime.securesms.util.livedata.Store;
import org.thoughtcrime.securesms.util.rx.RxStore;
import org.thoughtcrime.securesms.wallpaper.ChatWallpaper;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
import io.reactivex.rxjava3.core.BackpressureStrategy;
import io.reactivex.rxjava3.core.Flowable;
import io.reactivex.rxjava3.core.Observable;
import io.reactivex.rxjava3.core.Single;
import io.reactivex.rxjava3.disposables.CompositeDisposable;
import io.reactivex.rxjava3.disposables.Disposable;
import io.reactivex.rxjava3.processors.PublishProcessor;
import io.reactivex.rxjava3.schedulers.Schedulers;
import io.reactivex.rxjava3.subjects.BehaviorSubject;
import kotlin.Unit;
public class ConversationViewModel extends ViewModel {
private static final String TAG = Log.tag(ConversationViewModel.class);
private final Application context;
private final MediaRepository mediaRepository;
private final ConversationRepository conversationRepository;
private final ScheduledMessagesRepository scheduledMessagesRepository;
private final MutableLiveData<List<Media>> recentMedia;
private final BehaviorSubject<Long> threadId;
private final Observable<MessageData> messageData;
private final MutableLiveData<Boolean> showScrollButtons;
private final MutableLiveData<Boolean> hasUnreadMentions;
private final Observable<Boolean> canShowAsBubble;
private final ProxyPagingController<MessageId> pagingController;
private final DatabaseObserver.Observer conversationObserver;
private final DatabaseObserver.MessageObserver messageUpdateObserver;
private final DatabaseObserver.MessageObserver messageInsertObserver;
private final BehaviorSubject<RecipientId> recipientId;
private final Observable<Optional<ChatWallpaper>> wallpaper;
private final SingleLiveEvent<Event> events;
private final Observable<ChatColors> chatColors;
private final MutableLiveData<Integer> toolbarBottom;
private final MutableLiveData<Integer> inlinePlayerHeight;
private final LiveData<Integer> conversationTopMargin;
private final Store<ThreadAnimationState> threadAnimationStateStore;
private final Observer<ThreadAnimationState> threadAnimationStateStoreDriver;
private final NotificationProfilesRepository notificationProfilesRepository;
private final MutableLiveData<String> searchQuery;
private final GroupAuthorNameColorHelper groupAuthorNameColorHelper;
private final RxStore<ConversationState> conversationStateStore;
private final CompositeDisposable disposables;
private final BehaviorSubject<Unit> conversationStateTick;
private final PublishProcessor<Long> markReadRequestPublisher;
private final Observable<Integer> scheduledMessageCount;
private ConversationIntents.Args args;
private int jumpToPosition;
private ConversationViewModel() {
this.context = ApplicationDependencies.getApplication();
this.mediaRepository = new MediaRepository();
this.conversationRepository = new ConversationRepository();
this.scheduledMessagesRepository = new ScheduledMessagesRepository();
this.recentMedia = new MutableLiveData<>();
this.showScrollButtons = new MutableLiveData<>(false);
this.hasUnreadMentions = new MutableLiveData<>(false);
this.events = new SingleLiveEvent<>();
this.pagingController = new ProxyPagingController<>();
this.conversationObserver = pagingController::onDataInvalidated;
this.messageUpdateObserver = pagingController::onDataItemChanged;
this.messageInsertObserver = messageId -> pagingController.onDataItemInserted(messageId, 0);
this.toolbarBottom = new MutableLiveData<>();
this.inlinePlayerHeight = new MutableLiveData<>();
this.conversationTopMargin = Transformations.distinctUntilChanged(LiveDataUtil.combineLatest(toolbarBottom, inlinePlayerHeight, Integer::sum));
this.threadAnimationStateStore = new Store<>(new ThreadAnimationState(-1L, null, false));
this.notificationProfilesRepository = new NotificationProfilesRepository();
this.searchQuery = new MutableLiveData<>();
this.recipientId = BehaviorSubject.create();
this.threadId = BehaviorSubject.create();
this.groupAuthorNameColorHelper = new GroupAuthorNameColorHelper();
this.conversationStateStore = new RxStore<>(ConversationState.create(), Schedulers.computation());
this.disposables = new CompositeDisposable();
this.conversationStateTick = BehaviorSubject.createDefault(Unit.INSTANCE);
this.markReadRequestPublisher = PublishProcessor.create();
BehaviorSubject<Recipient> recipientCache = BehaviorSubject.create();
recipientId
.observeOn(Schedulers.io())
.distinctUntilChanged()
.map(Recipient::resolved)
.subscribe(recipientCache);
Disposable disposable = conversationStateStore.update(Observable.combineLatest(recipientId.distinctUntilChanged(), conversationStateTick, (id, tick) -> id)
.switchMap(conversationRepository::getSecurityInfo)
.toFlowable(BackpressureStrategy.LATEST),
(securityInfo, state) -> state.withSecurityInfo(securityInfo));
disposables.add(disposable);
BehaviorSubject<ConversationData> conversationMetadata = BehaviorSubject.create();
Observable.combineLatest(threadId, recipientCache, Pair::new)
.observeOn(Schedulers.io())
.distinctUntilChanged()
.map(threadIdAndRecipient -> {
SignalLocalMetrics.ConversationOpen.onMetadataLoadStarted();
ConversationData conversationData = conversationRepository.getConversationData(threadIdAndRecipient.first(), threadIdAndRecipient.second(), jumpToPosition);
SignalLocalMetrics.ConversationOpen.onMetadataLoaded();
jumpToPosition = -1;
return conversationData;
})
.subscribe(conversationMetadata);
ApplicationDependencies.getDatabaseObserver().registerMessageUpdateObserver(messageUpdateObserver);
messageData = conversationMetadata
.observeOn(Schedulers.io())
.switchMap(data -> {
int startPosition;
ConversationData.MessageRequestData messageRequestData = data.getMessageRequestData();
if (data.shouldJumpToMessage()) {
startPosition = data.getJumpToPosition();
} else if (messageRequestData.isMessageRequestAccepted() && data.shouldScrollToLastSeen()) {
startPosition = data.getLastSeenPosition();
} else if (messageRequestData.isMessageRequestAccepted()) {
startPosition = data.getLastScrolledPosition();
} else {
startPosition = data.getThreadSize();
}
ApplicationDependencies.getDatabaseObserver().unregisterObserver(conversationObserver);
ApplicationDependencies.getDatabaseObserver().unregisterObserver(messageInsertObserver);
ApplicationDependencies.getDatabaseObserver().registerConversationObserver(data.getThreadId(), conversationObserver);
ApplicationDependencies.getDatabaseObserver().registerMessageInsertObserver(data.getThreadId(), messageInsertObserver);
ConversationDataSource dataSource = new ConversationDataSource(context,
data.getThreadId(),
messageRequestData,
data.showUniversalExpireTimerMessage(),
data.getThreadSize(),
data.getThreadRecipient());
PagingConfig config = new PagingConfig.Builder().setPageSize(25)
.setBufferPages(2)
.setStartIndex(Math.max(startPosition, 0))
.build();
Log.d(TAG, "Starting at position: " + startPosition + " || jumpToPosition: " + data.getJumpToPosition() + ", lastSeenPosition: " + data.getLastSeenPosition() + ", lastScrolledPosition: " + data.getLastScrolledPosition());
ObservablePagedData<MessageId, ConversationMessage> pagedData = PagedData.createForObservable(dataSource, config);
pagingController.set(pagedData.getController());
return pagedData.getData();
})
.observeOn(Schedulers.io())
.withLatestFrom(conversationMetadata, (messages, metadata) -> new MessageData(metadata, messages))
.doOnNext(a -> SignalLocalMetrics.ConversationOpen.onDataLoaded());
scheduledMessageCount = threadId
.observeOn(Schedulers.io())
.switchMap(scheduledMessagesRepository::getScheduledMessageCount);
Observable<Recipient> liveRecipient = recipientId.distinctUntilChanged().switchMap(id -> Recipient.live(id).observable());
canShowAsBubble = threadId.observeOn(Schedulers.io()).map(conversationRepository::canShowAsBubble);
wallpaper = liveRecipient.map(r -> Optional.ofNullable(r.getWallpaper())).distinctUntilChanged();
chatColors = liveRecipient.map(Recipient::getChatColors).distinctUntilChanged();
threadAnimationStateStore.update(threadId, (id, state) -> {
if (state.getThreadId() == id) {
return state;
} else {
return new ThreadAnimationState(id, null, false);
}
});
threadAnimationStateStore.update(conversationMetadata, (m, state) -> {
if (state.getThreadId() == m.getThreadId()) {
return state.copy(state.getThreadId(), m, state.getHasCommittedNonEmptyMessageList());
} else {
return state.copy(m.getThreadId(), m, false);
}
});
this.threadAnimationStateStoreDriver = state -> {};
threadAnimationStateStore.getStateLiveData().observeForever(threadAnimationStateStoreDriver);
EventBus.getDefault().register(this);
}
Observable<StoryViewState> getStoryViewState() {
return recipientId
.subscribeOn(Schedulers.io())
.switchMap(StoryViewState::getForRecipientId)
.distinctUntilChanged()
.observeOn(AndroidSchedulers.mainThread());
}
void onMessagesCommitted(@NonNull List<ConversationMessage> conversationMessages) {
if (Util.hasItems(conversationMessages)) {
threadAnimationStateStore.update(state -> {
long threadId = conversationMessages.stream()
.filter(Objects::nonNull)
.findFirst()
.map(c -> c.getMessageRecord().getThreadId())
.orElse(-2L);
if (state.getThreadId() == threadId) {
return state.copy(state.getThreadId(), state.getThreadMetadata(), true);
} else {
return state;
}
});
}
}
void setDistributionType(int distributionType) {
Long threadId = this.threadId.getValue();
if (threadId == null) {
return;
}
conversationRepository.setConversationDistributionType(threadId, distributionType);
}
void submitMarkReadRequest(long timestampSince) {
markReadRequestPublisher.onNext(timestampSince);
}
boolean shouldPlayMessageAnimations() {
return threadAnimationStateStore.getState().shouldPlayMessageAnimations();
}
void setToolbarBottom(int bottom) {
toolbarBottom.setValue(bottom);
}
void setInlinePlayerVisible(boolean isVisible) {
inlinePlayerHeight.setValue(isVisible ? ViewUtil.dpToPx(36) : 0);
}
void onAttachmentKeyboardOpen() {
mediaRepository.getMediaInBucket(context, Media.ALL_MEDIA_BUCKET_ID, recentMedia::postValue);
}
@MainThread
void onConversationDataAvailable(@NonNull RecipientId recipientId, long threadId, int startingPosition) {
Log.d(TAG, "[onConversationDataAvailable] recipientId: " + recipientId + ", threadId: " + threadId + ", startingPosition: " + startingPosition);
this.jumpToPosition = startingPosition;
this.threadId.onNext(threadId);
this.recipientId.onNext(recipientId);
}
void clearThreadId() {
this.jumpToPosition = -1;
this.threadId.onNext(-1L);
}
void setSearchQuery(@Nullable String query) {
searchQuery.setValue(query);
}
void markGiftBadgeRevealed(long messageId) {
conversationRepository.markGiftBadgeRevealed(messageId);
}
void checkIfMmsIsEnabled() {
disposables.add(conversationRepository.checkIfMmsIsEnabled().subscribe(isEnabled -> {
conversationStateStore.update(state -> state.withMmsEnabled(true));
}));
}
@NonNull Flowable<Long> getMarkReadRequests() {
return markReadRequestPublisher.onBackpressureBuffer();
}
@NonNull Observable<Integer> getThreadUnreadCount(long afterTime) {
return threadId.switchMap(id -> conversationRepository.getUnreadCount(id, afterTime));
}
@NonNull Flowable<ConversationState> getConversationState() {
return conversationStateStore.getStateFlowable().observeOn(AndroidSchedulers.mainThread());
}
@NonNull Flowable<ConversationSecurityInfo> getConversationSecurityInfo(@NonNull RecipientId recipientId) {
return getConversationState().map(ConversationState::getSecurityInfo)
.filter(info -> info.isInitialized() && Objects.equals(info.getRecipientId(), recipientId))
.distinctUntilChanged();
}
void updateSecurityInfo() {
conversationStateTick.onNext(Unit.INSTANCE);
}
boolean isDefaultSmsApplication() {
return conversationStateStore.getState().getSecurityInfo().isDefaultSmsApplication();
}
boolean isPushAvailable() {
return conversationStateStore.getState().getSecurityInfo().isPushAvailable();
}
void muteConversation(long until) {
conversationRepository.setConversationMuted(args.getRecipientId(), until);
}
@NonNull ConversationState getConversationStateSnapshot() {
return conversationStateStore.getState();
}
@NonNull LiveData<String> getSearchQuery() {
return searchQuery;
}
@NonNull LiveData<Integer> getConversationTopMargin() {
return conversationTopMargin;
}
@NonNull Observable<Boolean> canShowAsBubble() {
return canShowAsBubble
.observeOn(AndroidSchedulers.mainThread());
}
@NonNull LiveData<Boolean> getShowScrollToBottom() {
return Transformations.distinctUntilChanged(showScrollButtons);
}
@NonNull LiveData<Boolean> getShowMentionsButton() {
return Transformations.distinctUntilChanged(LiveDataUtil.combineLatest(showScrollButtons, hasUnreadMentions, (a, b) -> a && b));
}
@NonNull Observable<Optional<ChatWallpaper>> getWallpaper() {
return wallpaper
.observeOn(AndroidSchedulers.mainThread());
}
@NonNull LiveData<Event> getEvents() {
return events;
}
@NonNull Observable<ChatColors> getChatColors() {
return chatColors
.observeOn(AndroidSchedulers.mainThread());
}
@NonNull Observable<Integer> getScheduledMessageCount() {
return scheduledMessageCount.observeOn(AndroidSchedulers.mainThread());
}
void setHasUnreadMentions(boolean hasUnreadMentions) {
this.hasUnreadMentions.setValue(hasUnreadMentions);
}
boolean getShowScrollButtons() {
return this.showScrollButtons.getValue();
}
void setShowScrollButtons(boolean showScrollButtons) {
this.showScrollButtons.setValue(showScrollButtons);
}
@NonNull LiveData<List<Media>> getRecentMedia() {
return recentMedia;
}
@NonNull Observable<MessageData> getMessageData() {
return messageData
.observeOn(AndroidSchedulers.mainThread());
}
@NonNull PagingController<MessageId> getPagingController() {
return pagingController;
}
@NonNull Observable<Map<RecipientId, NameColor>> getNameColorsMap() {
return recipientId
.observeOn(Schedulers.io())
.distinctUntilChanged()
.map(Recipient::resolved)
.map(recipient -> {
if (recipient.getGroupId().isPresent()) {
return groupAuthorNameColorHelper.getColorMap(recipient.getGroupId().get());
} else {
return Collections.<RecipientId, NameColor>emptyMap();
}
})
.observeOn(AndroidSchedulers.mainThread());
}
@NonNull LiveData<Optional<NotificationProfile>> getActiveNotificationProfile() {
Flowable<Optional<NotificationProfile>> activeProfile = notificationProfilesRepository.getProfiles()
.map(profiles -> Optional.ofNullable(NotificationProfiles.getActiveProfile(profiles)));
return LiveDataReactiveStreams.fromPublisher(activeProfile);
}
@NonNull
public Single<ConversationMessage> resolveMessageToEdit(@NonNull ConversationMessage message) {
return conversationRepository.resolveMessageToEdit(message);
}
void setArgs(@NonNull ConversationIntents.Args args) {
this.args = args;
}
@NonNull ConversationIntents.Args getArgs() {
return Objects.requireNonNull(args);
}
@Subscribe(threadMode = ThreadMode.POSTING)
public void onRecaptchaRequiredEvent(@NonNull RecaptchaRequiredEvent event) {
events.postValue(Event.SHOW_RECAPTCHA);
}
@Override
protected void onCleared() {
super.onCleared();
threadAnimationStateStore.getStateLiveData().removeObserver(threadAnimationStateStoreDriver);
ApplicationDependencies.getDatabaseObserver().unregisterObserver(conversationObserver);
ApplicationDependencies.getDatabaseObserver().unregisterObserver(messageUpdateObserver);
ApplicationDependencies.getDatabaseObserver().unregisterObserver(messageInsertObserver);
disposables.clear();
conversationStateStore.dispose();
EventBus.getDefault().unregister(this);
}
public void insertSmsExportUpdateEvent(@NonNull Recipient recipient) {
conversationRepository.insertSmsExportUpdateEvent(recipient);
}
enum Event {
SHOW_RECAPTCHA
}
static class MessageData {
private final List<ConversationMessage> messages;
private final ConversationData metadata;
MessageData(@NonNull ConversationData metadata, @NonNull List<ConversationMessage> messages) {
this.metadata = metadata;
this.messages = messages;
}
public @NonNull List<ConversationMessage> getMessages() {
return messages;
}
public @NonNull ConversationData getMetadata() {
return metadata;
}
}
static class Factory extends ViewModelProvider.NewInstanceFactory {
@Override
public @NonNull <T extends ViewModel> T create(@NonNull Class<T> modelClass) {
//noinspection ConstantConditions
return modelClass.cast(new ConversationViewModel());
}
}
}

View file

@ -1,64 +0,0 @@
package org.thoughtcrime.securesms.conversation;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.conversation.ConversationAdapter.StickyHeaderViewHolder;
import org.thoughtcrime.securesms.util.StickyHeaderDecoration;
class LastSeenHeader extends StickyHeaderDecoration {
private final ConversationAdapter adapter;
private final long lastSeenTimestamp;
private long unreadCount;
LastSeenHeader(ConversationAdapter adapter, long lastSeenTimestamp) {
super(adapter, false, false, ConversationAdapter.HEADER_TYPE_LAST_SEEN);
this.adapter = adapter;
this.lastSeenTimestamp = lastSeenTimestamp;
}
public void setUnreadCount(long unreadCount) {
this.unreadCount = unreadCount;
}
@Override
protected boolean hasHeader(RecyclerView parent, StickyHeaderAdapter stickyAdapter, int position) {
if (lastSeenTimestamp <= 0 || unreadCount <= 0) {
return false;
}
long currentRecordTimestamp = adapter.getReceivedTimestamp(position);
long previousRecordTimestamp = adapter.getReceivedTimestamp(position + 1);
return currentRecordTimestamp > lastSeenTimestamp && previousRecordTimestamp < lastSeenTimestamp;
}
@Override
protected int getHeaderTop(RecyclerView parent, View child, View header, int adapterPos, int layoutPos) {
return parent.getLayoutManager().getDecoratedTop(child);
}
@Override
protected @NonNull RecyclerView.ViewHolder getHeader(RecyclerView parent, StickyHeaderAdapter stickyAdapter, int position) {
StickyHeaderViewHolder viewHolder = new StickyHeaderViewHolder(LayoutInflater.from(parent.getContext()).inflate(R.layout.conversation_item_last_seen, parent, false));
adapter.onBindLastSeenViewHolder(viewHolder, unreadCount);
int widthSpec = View.MeasureSpec.makeMeasureSpec(parent.getWidth(), View.MeasureSpec.EXACTLY);
int heightSpec = View.MeasureSpec.makeMeasureSpec(parent.getHeight(), View.MeasureSpec.UNSPECIFIED);
int childWidth = ViewGroup.getChildMeasureSpec(widthSpec, parent.getPaddingLeft() + parent.getPaddingRight(), viewHolder.itemView.getLayoutParams().width);
int childHeight = ViewGroup.getChildMeasureSpec(heightSpec, parent.getPaddingTop() + parent.getPaddingBottom(), viewHolder.itemView.getLayoutParams().height);
viewHolder.itemView.measure(childWidth, childHeight);
viewHolder.itemView.layout(0, 0, viewHolder.itemView.getMeasuredWidth(), viewHolder.itemView.getMeasuredHeight());
return viewHolder;
}
}

View file

@ -1,101 +0,0 @@
package org.thoughtcrime.securesms.conversation;
import android.app.Application;
import android.content.Context;
import androidx.annotation.NonNull;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
import androidx.lifecycle.Transformations;
import androidx.lifecycle.ViewModel;
import org.signal.core.util.concurrent.SignalExecutors;
import org.signal.libsignal.protocol.util.Pair;
import org.thoughtcrime.securesms.database.DatabaseObserver;
import org.thoughtcrime.securesms.database.SignalDatabase;
import org.thoughtcrime.securesms.database.model.ThreadRecord;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.util.concurrent.SerialMonoLifoExecutor;
import java.util.concurrent.Executor;
public class MessageCountsViewModel extends ViewModel {
private static final Executor EXECUTOR = new SerialMonoLifoExecutor(SignalExecutors.BOUNDED);
private final Application context;
private final MutableLiveData<Long> threadId = new MutableLiveData<>(-1L);
private final LiveData<Pair<Integer, Integer>> unreadCounts;
private DatabaseObserver.Observer observer;
public MessageCountsViewModel() {
this.context = ApplicationDependencies.getApplication();
this.unreadCounts = Transformations.switchMap(Transformations.distinctUntilChanged(threadId), id -> {
MutableLiveData<Pair<Integer, Integer>> counts = new MutableLiveData<>(new Pair<>(0, 0));
if (id == -1L) {
return counts;
}
if (observer != null) {
ApplicationDependencies.getDatabaseObserver().unregisterObserver(observer);
}
observer = new DatabaseObserver.Observer() {
private int previousUnreadCount = -1;
@Override
public void onChanged() {
EXECUTOR.execute(() -> {
int unreadCount = getUnreadCount(context, id);
if (unreadCount != previousUnreadCount) {
previousUnreadCount = unreadCount;
counts.postValue(new Pair<>(unreadCount, getUnreadMentionsCount(context, id)));
}
});
}
};
observer.onChanged();
ApplicationDependencies.getDatabaseObserver().registerConversationListObserver(observer);
return counts;
});
}
void setThreadId(long threadId) {
this.threadId.setValue(threadId);
}
void clearThreadId() {
this.threadId.postValue(-1L);
}
@NonNull LiveData<Integer> getUnreadMessagesCount() {
return Transformations.map(unreadCounts, Pair::first);
}
@NonNull LiveData<Integer> getUnreadMentionsCount() {
return Transformations.map(unreadCounts, Pair::second);
}
private int getUnreadCount(@NonNull Context context, long threadId) {
ThreadRecord threadRecord = SignalDatabase.threads().getThreadRecord(threadId);
return threadRecord != null ? threadRecord.getUnreadCount() : 0;
}
private int getUnreadMentionsCount(@NonNull Context context, long threadId) {
return SignalDatabase.messages().getUnreadMentionCount(threadId);
}
@Override
protected void onCleared() {
if (observer != null) {
ApplicationDependencies.getDatabaseObserver().unregisterObserver(observer);
}
}
}

View file

@ -5,7 +5,6 @@ import android.net.Uri
import android.text.Spannable
import android.text.SpannableString
import io.reactivex.rxjava3.core.Maybe
import io.reactivex.rxjava3.core.Single
import io.reactivex.rxjava3.schedulers.Schedulers
import org.signal.core.util.StreamUtil
import org.signal.core.util.concurrent.MaybeCompat
@ -167,22 +166,16 @@ class DraftRepository(
}
}
fun saveDrafts(recipient: Recipient?, threadId: Long, distributionType: Int, drafts: Drafts) {
require(threadId != -1L || recipient != null)
fun saveDrafts(threadId: Long, drafts: Drafts) {
require(threadId != -1L)
saveDraftsExecutor.execute {
if (drafts.isNotEmpty()) {
val actualThreadId = if (threadId == -1L) {
threadTable.getOrCreateThreadIdFor(recipient!!, distributionType)
} else {
threadId
}
draftTable.replaceDrafts(actualThreadId, drafts)
draftTable.replaceDrafts(threadId, drafts)
if (drafts.shouldUpdateSnippet()) {
threadTable.updateSnippet(actualThreadId, drafts.getSnippet(context), drafts.getUriSnippet(), System.currentTimeMillis(), MessageTypes.BASE_DRAFT_TYPE, true)
threadTable.updateSnippet(threadId, drafts.getSnippet(context), drafts.getUriSnippet(), System.currentTimeMillis(), MessageTypes.BASE_DRAFT_TYPE, true)
} else {
threadTable.update(actualThreadId, unarchive = false, allowDeletion = false)
threadTable.update(threadId, unarchive = false, allowDeletion = false)
}
} else if (threadId > 0) {
draftTable.clearDrafts(threadId)
@ -191,13 +184,6 @@ class DraftRepository(
}
}
@Deprecated("Not needed for CFv2")
fun loadDrafts(threadId: Long): Single<DatabaseDraft> {
return Single.fromCallable {
loadDraftsInternal(threadId)
}.subscribeOn(Schedulers.io())
}
private fun loadDraftsInternal(threadId: Long): DatabaseDraft {
val drafts: Drafts = draftTable.getDrafts(threadId)
val bodyRangesDraft: DraftTable.Draft? = drafts.getDraftOfType(DraftTable.Draft.BODY_RANGES)
@ -218,11 +204,6 @@ class DraftRepository(
return DatabaseDraft(drafts, updatedText)
}
@Deprecated("Not needed for CFv2")
fun loadDraftQuote(serialized: String): Maybe<ConversationMessage> {
return MaybeCompat.fromCallable { loadDraftQuoteInternal(serialized) }
}
private fun loadDraftQuoteInternal(serialized: String): ConversationMessage? {
val quoteId: QuoteId = QuoteId.deserialize(context, serialized) ?: return null
val messageRecord: MessageRecord = SignalDatabase.messages.getMessageFor(quoteId.id, quoteId.author)?.let {
@ -237,11 +218,6 @@ class DraftRepository(
return ConversationMessageFactory.createWithUnresolvedData(context, messageRecord, threadRecipient)
}
@Deprecated("Not needed for CFv2")
fun loadDraftMessageEdit(serialized: String): Maybe<ConversationMessage> {
return MaybeCompat.fromCallable { loadDraftMessageEditInternal(serialized) }
}
private fun loadDraftMessageEditInternal(serialized: String): ConversationMessage? {
val messageId = MessageId.deserialize(serialized)
val messageRecord: MessageRecord = SignalDatabase.messages.getMessageRecordOrNull(messageId.id) ?: return null

View file

@ -2,7 +2,6 @@ package org.thoughtcrime.securesms.conversation.drafts
import org.thoughtcrime.securesms.database.DraftTable
import org.thoughtcrime.securesms.database.DraftTable.Drafts
import org.thoughtcrime.securesms.recipients.RecipientId
/**
* State object responsible for holding Voice Note draft state. The intention is to allow
@ -10,11 +9,7 @@ import org.thoughtcrime.securesms.recipients.RecipientId
* management pattern going forward for drafts.
*/
data class DraftState(
@Deprecated("Not needed for CFv2")
val recipientId: RecipientId? = null,
val threadId: Long = -1,
@Deprecated("Not needed for CFv2")
val distributionType: Int = 0,
val textDraft: DraftTable.Draft? = null,
val bodyRangesDraft: DraftTable.Draft? = null,
val quoteDraft: DraftTable.Draft? = null,
@ -24,7 +19,7 @@ data class DraftState(
) {
fun copyAndClearDrafts(threadId: Long = this.threadId): DraftState {
return DraftState(recipientId = recipientId, threadId = threadId, distributionType = distributionType)
return DraftState(threadId = threadId)
}
fun toDrafts(): Drafts {

View file

@ -4,17 +4,13 @@ import androidx.lifecycle.ViewModel
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
import io.reactivex.rxjava3.core.Flowable
import io.reactivex.rxjava3.core.Maybe
import io.reactivex.rxjava3.core.Single
import io.reactivex.rxjava3.schedulers.Schedulers
import org.thoughtcrime.securesms.components.location.SignalPlace
import org.thoughtcrime.securesms.conversation.ConversationMessage
import org.thoughtcrime.securesms.database.DraftTable.Draft
import org.thoughtcrime.securesms.database.MentionUtil
import org.thoughtcrime.securesms.database.model.Mention
import org.thoughtcrime.securesms.database.model.MessageId
import org.thoughtcrime.securesms.database.model.databaseprotos.BodyRangeList
import org.thoughtcrime.securesms.mms.QuoteId
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.recipients.RecipientId
import org.thoughtcrime.securesms.util.Base64
import org.thoughtcrime.securesms.util.rx.RxStore
@ -40,16 +36,6 @@ class DraftViewModel @JvmOverloads constructor(
store.dispose()
}
@Deprecated("Not needed for CFv2")
fun setThreadId(threadId: Long) {
store.update { it.copy(threadId = threadId) }
}
@Deprecated("Not needed for CFv2")
fun setDistributionType(distributionType: Int) {
store.update { it.copy(distributionType = distributionType) }
}
fun saveEphemeralVoiceNoteDraft(draft: Draft) {
store.update { draftState ->
saveDrafts(draftState.copy(voiceNoteDraft = draft))
@ -67,11 +53,6 @@ class DraftViewModel @JvmOverloads constructor(
}
}
@Deprecated("Not needed for CFv2")
fun onRecipientChanged(recipient: Recipient) {
store.update { it.copy(recipientId = recipient.id) }
}
fun setMessageEditDraft(messageId: MessageId, text: String, mentions: List<Mention>, styleBodyRanges: BodyRangeList?) {
store.update {
val mentionRanges: BodyRangeList? = MentionUtil.mentionsToBodyRangeList(mentions)
@ -140,34 +121,10 @@ class DraftViewModel @JvmOverloads constructor(
}
private fun saveDrafts(state: DraftState): DraftState {
repository.saveDrafts(state.recipientId?.let { Recipient.resolved(it) }, state.threadId, state.distributionType, state.toDrafts())
repository.saveDrafts(state.threadId, state.toDrafts())
return state
}
@Deprecated("Not needed for CFv2")
fun loadDrafts(threadId: Long): Single<DraftRepository.DatabaseDraft> {
return repository
.loadDrafts(threadId)
.doOnSuccess { drafts ->
store.update { saveDrafts(it.copyAndSetDrafts(threadId, drafts.drafts)) }
}
.observeOn(AndroidSchedulers.mainThread())
}
@Deprecated("Not needed for CFv2")
fun loadDraftQuote(serialized: String): Maybe<ConversationMessage> {
return repository.loadDraftQuote(serialized)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
}
@Deprecated("Not needed for CFv2")
fun loadDraftEditMessage(serialized: String): Maybe<ConversationMessage> {
return repository.loadDraftMessageEdit(serialized)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
}
fun loadShareOrDraftData(lastShareDataTimestamp: Long): Maybe<DraftRepository.ShareOrDraftData> {
return repository.getShareOrDraftData(lastShareDataTimestamp)
.doOnSuccess { (_, drafts) ->

View file

@ -36,7 +36,6 @@ import org.thoughtcrime.securesms.conversation.ConversationAdapterBridge
import org.thoughtcrime.securesms.conversation.ConversationAdapterBridge.PulseRequest
import org.thoughtcrime.securesms.conversation.v2.items.InteractiveConversationElement
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.util.FeatureFlags
import org.thoughtcrime.securesms.util.ThemeUtil
import org.thoughtcrime.securesms.util.ViewUtil
import org.thoughtcrime.securesms.wallpaper.ChatWallpaper
@ -402,12 +401,6 @@ class MultiselectItemDecoration(
}
}
}
if (!FeatureFlags.useConversationFragmentV2()) {
canvas.clipPath(path)
canvas.drawShade()
canvas.restore()
}
}
}
@ -422,12 +415,6 @@ class MultiselectItemDecoration(
path.addRect(child.left.toFloat(), child.top.toFloat(), child.right.toFloat(), child.bottom.toFloat(), Path.Direction.CW)
}
}
if (!FeatureFlags.useConversationFragmentV2()) {
canvas.clipPath(path, Region.Op.DIFFERENCE)
canvas.drawShade()
canvas.restore()
}
}
}

View file

@ -25,7 +25,6 @@ import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.conversation.ConversationFragment;
import org.thoughtcrime.securesms.keyvalue.SignalStore;
import org.thoughtcrime.securesms.notifications.NotificationChannels;
import org.thoughtcrime.securesms.util.DeviceProperties;
@ -123,8 +122,8 @@ public final class EnableCallNotificationSettingsDialog extends DialogFragment {
@Override
public void onDismiss(@NonNull DialogInterface dialog) {
super.onDismiss(dialog);
if (getParentFragment() instanceof ConversationFragment) {
((ConversationFragment) getParentFragment()).refreshList();
if (getParentFragment() instanceof Callback) {
((Callback) getParentFragment()).onCallNotificationSettingsDialogDismissed();
}
}
@ -230,4 +229,8 @@ public final class EnableCallNotificationSettingsDialog extends DialogFragment {
return bitmask;
}
public interface Callback {
void onCallNotificationSettingsDialogDismissed();
}
}

View file

@ -1,90 +0,0 @@
package org.thoughtcrime.securesms.conversation.ui.groupcall;
import android.os.Build;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
import androidx.lifecycle.ViewModel;
import androidx.lifecycle.ViewModelProvider;
import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.events.GroupCallPeekEvent;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.util.livedata.LiveDataUtil;
import java.util.Objects;
public class GroupCallViewModel extends ViewModel {
private static final String TAG = Log.tag(GroupCallViewModel.class);
private final MutableLiveData<Boolean> activeGroup;
private final MutableLiveData<Boolean> ongoingGroupCall;
private final LiveData<Boolean> activeGroupCall;
private final MutableLiveData<Boolean> groupCallHasCapacity;
private @Nullable Recipient currentRecipient;
GroupCallViewModel() {
this.activeGroup = new MutableLiveData<>(false);
this.ongoingGroupCall = new MutableLiveData<>(false);
this.groupCallHasCapacity = new MutableLiveData<>(false);
this.activeGroupCall = LiveDataUtil.combineLatest(activeGroup, ongoingGroupCall, (active, ongoing) -> active && ongoing);
}
public @NonNull LiveData<Boolean> hasActiveGroupCall() {
return activeGroupCall;
}
public @NonNull LiveData<Boolean> groupCallHasCapacity() {
return groupCallHasCapacity;
}
public void onRecipientChange(@Nullable Recipient recipient) {
activeGroup.postValue(recipient != null && recipient.isActiveGroup());
if (Objects.equals(currentRecipient, recipient)) {
return;
}
ongoingGroupCall.postValue(false);
groupCallHasCapacity.postValue(false);
currentRecipient = recipient;
peekGroupCall();
}
public void peekGroupCall() {
if (isGroupCallCapable(currentRecipient)) {
Log.i(TAG, "peek call for " + currentRecipient.getId());
ApplicationDependencies.getSignalCallManager().peekGroupCall(currentRecipient.getId());
}
}
public void onGroupCallPeekEvent(@NonNull GroupCallPeekEvent groupCallPeekEvent) {
if (isGroupCallCapable(currentRecipient) && groupCallPeekEvent.getGroupRecipientId().equals(currentRecipient.getId())) {
Log.i(TAG, "update UI with call event: ongoing call: " + groupCallPeekEvent.isOngoing() + " hasCapacity: " + groupCallPeekEvent.callHasCapacity());
ongoingGroupCall.postValue(groupCallPeekEvent.isOngoing());
groupCallHasCapacity.postValue(groupCallPeekEvent.callHasCapacity());
} else {
Log.i(TAG, "Ignore call event for different recipient.");
}
}
private static boolean isGroupCallCapable(@Nullable Recipient recipient) {
return recipient != null && recipient.isActiveGroup() && recipient.isPushV2Group() && Build.VERSION.SDK_INT > 19;
}
public static final class Factory implements ViewModelProvider.Factory {
@Override
public @NonNull <T extends ViewModel> T create(@NonNull Class<T> modelClass) {
//noinspection ConstantConditions
return modelClass.cast(new GroupCallViewModel());
}
}
}

View file

@ -13,6 +13,7 @@ import org.thoughtcrime.securesms.components.settings.app.subscription.DonationP
import org.thoughtcrime.securesms.components.settings.app.subscription.StripeRepository
import org.thoughtcrime.securesms.components.voice.VoiceNoteMediaController
import org.thoughtcrime.securesms.components.voice.VoiceNoteMediaControllerOwner
import org.thoughtcrime.securesms.conversation.ConversationIntents
import org.thoughtcrime.securesms.util.Debouncer
import org.thoughtcrime.securesms.util.DynamicNoActionBarTheme
import java.util.concurrent.TimeUnit
@ -20,7 +21,7 @@ import java.util.concurrent.TimeUnit
/**
* Wrapper activity for ConversationFragment.
*/
class ConversationActivity : PassphraseRequiredActivity(), VoiceNoteMediaControllerOwner, DonationPaymentComponent {
open class ConversationActivity : PassphraseRequiredActivity(), VoiceNoteMediaControllerOwner, DonationPaymentComponent {
companion object {
private const val STATE_WATERMARK = "share_data_watermark"
@ -88,7 +89,11 @@ class ConversationActivity : PassphraseRequiredActivity(), VoiceNoteMediaControl
private fun replaceFragment() {
val fragment = ConversationFragment().apply {
arguments = intent.extras
arguments = if (ConversationIntents.isBubbleIntentUri(intent.data)) {
ConversationIntents.createParentFragmentArguments(intent)
} else {
intent.extras
}
}
supportFragmentManager

View file

@ -120,6 +120,7 @@ import org.thoughtcrime.securesms.components.menu.SignalBottomActionBar
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.settings.conversation.ConversationSettingsActivity
import org.thoughtcrime.securesms.components.spoiler.SpoilerAnnotation
import org.thoughtcrime.securesms.components.voice.VoiceNoteDraft
import org.thoughtcrime.securesms.components.voice.VoiceNoteMediaControllerOwner
import org.thoughtcrime.securesms.components.voice.VoiceNotePlaybackState
@ -331,7 +332,8 @@ class ConversationFragment :
ScheduleMessageTimePickerBottomSheet.ScheduleCallback,
ScheduleMessageDialogCallback,
ConversationBottomSheetCallback,
SafetyNumberBottomSheet.Callbacks {
SafetyNumberBottomSheet.Callbacks,
EnableCallNotificationSettingsDialog.Callback {
companion object {
private val TAG = Log.tag(ConversationFragment::class.java)
@ -458,7 +460,6 @@ class ConversationFragment :
private lateinit var threadHeaderMarginDecoration: ThreadHeaderMarginDecoration
private lateinit var conversationItemDecorations: ConversationItemDecorations
private lateinit var optionsMenuCallback: ConversationOptionsMenuCallback
private lateinit var menuProvider: ConversationOptionsMenu.Provider
private lateinit var typingIndicatorDecoration: TypingIndicatorDecoration
private lateinit var backPressedCallback: BackPressedDelegate
@ -471,6 +472,7 @@ class ConversationFragment :
private var reShowScheduleMessagesBar: Boolean = false
private var composeTextEventsListener: ComposeTextEventsListener? = null
private var dataObserver: DataObserver? = null
private var menuProvider: ConversationOptionsMenu.Provider? = null
private val jumpAndPulseScrollStrategy = object : ScrollToPositionDelegate.ScrollStrategy {
override fun performScroll(recyclerView: RecyclerView, layoutManager: LinearLayoutManager, position: Int, smooth: Boolean) {
@ -559,6 +561,8 @@ class ConversationFragment :
binding.conversationVideoContainer.setClipToOutline(true)
SpoilerAnnotation.resetRevealedSpoilers()
registerForResults()
}
@ -761,6 +765,10 @@ class ConversationFragment :
override fun onCanceled() = Unit
override fun onCallNotificationSettingsDialogDismissed() {
adapter.notifyDataSetChanged()
}
//endregion
private fun observeConversationThread() {
@ -813,7 +821,7 @@ class ConversationFragment :
backPressedCallback = BackPressedDelegate()
requireActivity().onBackPressedDispatcher.addCallback(viewLifecycleOwner, backPressedCallback)
menuProvider.afterFirstRenderMode = true
menuProvider?.afterFirstRenderMode = true
viewLifecycleOwner.lifecycle.addObserver(LastScrolledPositionUpdater(adapter, layoutManager, viewModel))
@ -858,7 +866,7 @@ class ConversationFragment :
setOnClickListener(sendButtonListener)
setScheduledSendListener(sendButtonListener)
isEnabled = true
post { sendButton.triggerSelectedChangedEvent() }
sendButton.triggerSelectedChangedEvent()
}
sendEditButton.setOnClickListener { handleSendEditMessage() }
@ -981,6 +989,11 @@ class ConversationFragment :
val conversationUpdateTick = ConversationUpdateTick { adapter.updateTimestamps() }
viewLifecycleOwner.lifecycle.addObserver(conversationUpdateTick)
if (args.conversationScreenType.isInPopup) {
composeText.requestFocus()
binding.conversationInputPanel.quickAttachmentToggle.disable()
}
}
private fun initializeInlineSearch() {
@ -1162,15 +1175,17 @@ class ConversationFragment :
}
private fun presentActionBarMenu() {
optionsMenuCallback = ConversationOptionsMenuCallback()
menuProvider = ConversationOptionsMenu.Provider(optionsMenuCallback, disposables)
binding.toolbar.addMenuProvider(menuProvider)
invalidateOptionsMenu()
if (!args.conversationScreenType.isInPopup) {
optionsMenuCallback = ConversationOptionsMenuCallback()
menuProvider = ConversationOptionsMenu.Provider(optionsMenuCallback, disposables)
binding.toolbar.addMenuProvider(menuProvider!!)
invalidateOptionsMenu()
}
when (args.conversationScreenType) {
ConversationScreenType.NORMAL -> presentNavigationIconForNormal()
ConversationScreenType.BUBBLE -> presentNavigationIconForBubble()
ConversationScreenType.POPUP -> Unit
ConversationScreenType.BUBBLE,
ConversationScreenType.POPUP -> presentNavigationIconForBubble()
}
}
@ -1209,8 +1224,10 @@ class ConversationFragment :
titleView.clearExpiring()
}
titleView.setOnClickListener {
optionsMenuCallback.handleConversationSettings()
if (!args.conversationScreenType.isInPopup) {
titleView.setOnClickListener {
optionsMenuCallback.handleConversationSettings()
}
}
}
@ -1751,7 +1768,9 @@ class ConversationFragment :
disposables += send
.doOnSubscribe {
if (clearCompose) {
composeTextEventsListener?.typingStatusEnabled = false
composeText.setText("")
composeTextEventsListener?.typingStatusEnabled = true
attachmentManager.clear(GlideApp.with(this@ConversationFragment), false)
inputPanel.clearQuote()
}
@ -1767,9 +1786,6 @@ class ConversationFragment :
private fun onSendComplete() {
if (isDetached || activity?.isFinishing == true) {
if (args.conversationScreenType.isInPopup) {
activity?.finish()
}
return
}
@ -1781,6 +1797,10 @@ class ConversationFragment :
draftViewModel.onSendComplete()
inputPanel.exitEditMessageMode()
if (args.conversationScreenType.isInPopup) {
activity?.finish()
}
}
private fun handleRecentSafetyNumberChange(changedRecords: List<IdentityRecord>) {
@ -2004,6 +2024,7 @@ class ConversationFragment :
} else if (isSearchRequested) {
searchMenuItem?.collapseActionView()
} else if (args.conversationScreenType.isInBubble) {
isEnabled = false
requireActivity().onBackPressed()
} else {
requireActivity().finish()

View file

@ -11,7 +11,6 @@ import org.signal.core.util.logging.Log
import org.signal.core.util.toInt
import org.signal.paging.PagedDataSource
import org.thoughtcrime.securesms.conversation.ConversationData
import org.thoughtcrime.securesms.conversation.ConversationDataSource
import org.thoughtcrime.securesms.conversation.ConversationMessage
import org.thoughtcrime.securesms.conversation.ConversationMessage.ConversationMessageFactory
import org.thoughtcrime.securesms.database.MessageTable

View file

@ -123,7 +123,6 @@ import org.thoughtcrime.securesms.contacts.paged.ContactSearchMediator;
import org.thoughtcrime.securesms.contacts.paged.ContactSearchState;
import org.thoughtcrime.securesms.contacts.sync.CdsPermanentErrorBottomSheet;
import org.thoughtcrime.securesms.contacts.sync.CdsTemporaryErrorBottomSheet;
import org.thoughtcrime.securesms.conversation.ConversationFragment;
import org.thoughtcrime.securesms.conversationlist.chatfilter.ConversationFilterRequest;
import org.thoughtcrime.securesms.conversationlist.chatfilter.ConversationFilterSource;
import org.thoughtcrime.securesms.conversationlist.chatfilter.ConversationListFilterPullView;
@ -169,6 +168,7 @@ import org.thoughtcrime.securesms.util.AppStartup;
import org.thoughtcrime.securesms.util.BottomSheetUtil;
import org.thoughtcrime.securesms.util.CachedInflater;
import org.thoughtcrime.securesms.util.ConversationUtil;
import org.thoughtcrime.securesms.util.FeatureFlags;
import org.thoughtcrime.securesms.util.PlayStoreUtil;
import org.thoughtcrime.securesms.util.ServiceUtil;
import org.thoughtcrime.securesms.util.SignalLocalMetrics;
@ -979,8 +979,22 @@ public class ConversationListFragment extends MainFragment implements ActionMode
requireCallback().getSearchToolbar().get();
}
if (getContext() != null) {
ConversationFragment.prepare(getContext());
Context context = getContext();
if (context != null) {
FrameLayout parent = new FrameLayout(context);
parent.setLayoutParams(new FrameLayout.LayoutParams(FrameLayout.LayoutParams.MATCH_PARENT, FrameLayout.LayoutParams.WRAP_CONTENT));
if (SignalStore.internalValues().useConversationItemV2()) {
CachedInflater.from(context).cacheUntilLimit(R.layout.v2_conversation_item_text_only_incoming, parent, 25);
CachedInflater.from(context).cacheUntilLimit(R.layout.v2_conversation_item_text_only_outgoing, parent, 25);
} else {
CachedInflater.from(context).cacheUntilLimit(R.layout.conversation_item_received_text_only, parent, 25);
CachedInflater.from(context).cacheUntilLimit(R.layout.conversation_item_sent_text_only, parent, 25);
}
CachedInflater.from(context).cacheUntilLimit(R.layout.conversation_item_received_multimedia, parent, 10);
CachedInflater.from(context).cacheUntilLimit(R.layout.conversation_item_sent_multimedia, parent, 10);
CachedInflater.from(context).cacheUntilLimit(R.layout.conversation_item_update, parent, 5);
CachedInflater.from(context).cacheUntilLimit(R.layout.cursor_adapter_header_footer_view, parent, 2);
}
}

View file

@ -4,8 +4,7 @@ import android.graphics.Canvas
import android.graphics.Rect
import androidx.core.view.children
import androidx.recyclerview.widget.RecyclerView
import org.thoughtcrime.securesms.conversation.ConversationAdapter
import org.thoughtcrime.securesms.conversation.v2.ConversationAdapterV2
import org.thoughtcrime.securesms.conversation.ConversationHeaderView
import kotlin.math.min
/**
@ -28,28 +27,22 @@ class GiphyMp4ItemDecoration(
parent.translationY = 0f
onRecyclerVerticalTranslationSet?.invoke(parent.translationY)
} else {
val threadHeaderViewHolder = parent.children
.map { parent.getChildViewHolder(it) }
.filter { it is ConversationAdapter.FooterViewHolder || it is ConversationAdapterV2.ThreadHeaderViewHolder }
val threadHeaderView: ConversationHeaderView? = parent.children
.filterIsInstance<ConversationHeaderView>()
.firstOrNull()
if (threadHeaderViewHolder == null) {
if (threadHeaderView == null) {
parent.translationY = 0f
onRecyclerVerticalTranslationSet?.invoke(parent.translationY)
return
}
val toolbarMargin = if (threadHeaderViewHolder is ConversationAdapterV2.ThreadHeaderViewHolder) {
// A decorator adds the margin for the toolbar, margin is difference of the bounds "height" and the view height
val bounds = Rect()
parent.getDecoratedBoundsWithMargins(threadHeaderViewHolder.itemView, bounds)
bounds.bottom - bounds.top - threadHeaderViewHolder.itemView.height
} else {
// Deprecated not needed for CFv2
0
}
// A decorator adds the margin for the toolbar, margin is difference of the bounds "height" and the view height
val bounds = Rect()
parent.getDecoratedBoundsWithMargins(threadHeaderView, bounds)
val toolbarMargin = bounds.bottom - bounds.top - threadHeaderView.height
val childTop: Int = threadHeaderViewHolder.itemView.top - toolbarMargin
val childTop: Int = threadHeaderView.top - toolbarMargin
parent.translationY = min(0, -childTop).toFloat()
onRecyclerVerticalTranslationSet?.invoke(parent.translationY)
}

View file

@ -107,7 +107,6 @@ public final class FeatureFlags {
private static final String MAX_ATTACHMENT_SIZE_BYTES = "global.attachments.maxBytes";
private static final String SVR2_KILLSWITCH = "android.svr2.killSwitch";
private static final String CDS_DISABLE_COMPAT_MODE = "cds.disableCompatibilityMode";
private static final String CONVERSATION_FRAGMENT_V2 = "android.conversationFragmentV2.2";
private static final String FCM_MAY_HAVE_MESSAGES_KILL_SWITCH = "android.fcmNotificationFallbackKillSwitch";
private static final String SAFETY_NUMBER_ACI = "global.safetyNumberAci";
@ -169,7 +168,6 @@ public final class FeatureFlags {
AD_HOC_CALLING,
SVR2_KILLSWITCH,
CDS_DISABLE_COMPAT_MODE,
CONVERSATION_FRAGMENT_V2,
SAFETY_NUMBER_ACI,
FCM_MAY_HAVE_MESSAGES_KILL_SWITCH
);
@ -237,7 +235,6 @@ public final class FeatureFlags {
MAX_ATTACHMENT_SIZE_BYTES,
SVR2_KILLSWITCH,
CDS_DISABLE_COMPAT_MODE,
CONVERSATION_FRAGMENT_V2,
SAFETY_NUMBER_ACI,
FCM_MAY_HAVE_MESSAGES_KILL_SWITCH
);
@ -621,11 +618,6 @@ public final class FeatureFlags {
}
}
/** True if the new conversation fragment should be used. */
public static boolean useConversationFragmentV2() {
return getBoolean(CONVERSATION_FRAGMENT_V2, false);
}
/** Only for rendering debug info. */
public static synchronized @NonNull Map<String, Object> getMemoryValues() {
return new TreeMap<>(REMOTE_VALUES);

View file

@ -1,302 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<org.thoughtcrime.securesms.components.InsetAwareConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
tools:viewBindingIgnore="true"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent">
<include layout="@layout/system_ui_guidelines" />
<ImageView
android:id="@+id/conversation_wallpaper"
android:layout_width="0dp"
android:layout_height="match_parent"
android:importantForAccessibility="no"
android:scaleType="centerCrop"
app:layout_constraintEnd_toEndOf="@id/parent_end_guideline"
app:layout_constraintStart_toStartOf="@id/parent_start_guideline" />
<View
android:id="@+id/conversation_wallpaper_dim"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/black"
android:visibility="gone"
tools:alpha="0.2f"
tools:visibility="visible" />
<org.thoughtcrime.securesms.components.InputAwareLayout
android:id="@+id/layout_container"
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintBottom_toBottomOf="@id/keyboard_guideline"
app:layout_constraintEnd_toEndOf="@id/parent_end_guideline"
app:layout_constraintStart_toStartOf="@id/parent_start_guideline"
app:layout_constraintTop_toTopOf="parent">
<LinearLayout
android:id="@+id/conversation_container"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipChildren="false"
android:clipToPadding="false"
android:gravity="bottom"
android:orientation="vertical">
<FrameLayout
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1">
<androidx.coordinatorlayout.widget.CoordinatorLayout
android:id="@+id/fragment_content"
android:layout_width="match_parent"
android:layout_height="match_parent" />
<ViewStub
android:id="@+id/conversation_mention_suggestions_stub"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout="@layout/conversation_mention_suggestions_stub" />
</FrameLayout>
<ViewStub
android:id="@+id/attachment_editor_stub"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inflatedId="@+id/attachment_editor"
android:layout="@layout/conversation_activity_attachment_editor_stub" />
<LinearLayout
android:id="@+id/conversation_activity_panel_parent"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:clipChildren="false"
android:clipToPadding="false">
<ViewStub
android:id="@+id/scheduled_messages_stub"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inflatedId="@+id/scheduled_messages"
android:layout="@layout/conversation_activity_scheduled_messages_stub" />
<include layout="@layout/conversation_search_nav" />
<FrameLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:clipChildren="false"
android:clipToPadding="false">
<include
layout="@layout/conversation_no_longer_a_member"
android:visibility="gone" />
<include
layout="@layout/conversation_requesting_bottom_banner"
android:visibility="gone" />
<include layout="@layout/conversation_input_panel" />
<ViewStub
android:id="@+id/conversation_cannot_send_announcement_stub"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout="@layout/conversation_cannot_send_announcement_group" />
<org.thoughtcrime.securesms.messagerequests.MessageRequestsBottomView
android:id="@+id/conversation_activity_message_request_bottom_bar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:clickable="true"
android:visibility="gone" />
<ViewStub
android:id="@+id/conversation_release_notes_unmute_stub"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout="@layout/conversation_activity_unmute" />
</FrameLayout>
</LinearLayout>
<Button
android:id="@+id/register_button"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:padding="20dp"
android:text="@string/conversation_activity__enable_signal_messages"
android:visibility="gone" />
<Button
android:id="@+id/unblock_button"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:padding="20dp"
android:text="@string/ConversationActivity_unblock"
android:visibility="gone" />
<ViewStub
android:id="@+id/sms_export_stub"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inflatedId="@+id/sms_export_view"
android:layout="@layout/conversation_activity_sms_export_stub" />
<ViewStub
android:id="@+id/logged_out_stub"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inflatedId="@+id/logged_out_view"
android:layout="@layout/conversation_activity_logged_out_stub" />
<TextView
android:id="@+id/space_left"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:background="?android:windowBackground"
android:paddingStart="5dp"
android:visibility="gone"
tools:text="160/160 (1)"
tools:visibility="visible" />
<ViewStub
android:id="@+id/emoji_drawer_stub"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inflatedId="@+id/emoji_drawer"
android:layout="@layout/conversation_activity_emojidrawer_stub" />
<ViewStub
android:id="@+id/attachment_keyboard_stub"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inflatedId="@+id/attachment_keyboard"
android:layout="@layout/conversation_activity_attachment_keyboard_stub" />
</LinearLayout>
</org.thoughtcrime.securesms.components.InputAwareLayout>
<View
android:id="@+id/navbar_background"
android:layout_width="match_parent"
android:layout_height="0dp"
android:background="@color/wallpaper_compose_background"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintTop_toBottomOf="@id/navigation_bar_guideline" />
<View
android:id="@+id/toolbar_background"
android:layout_width="match_parent"
android:layout_height="@dimen/signal_m3_toolbar_height"
android:background="@color/signal_colorBackground"
app:layout_constraintEnd_toEndOf="@id/parent_end_guideline"
app:layout_constraintStart_toStartOf="@id/parent_start_guideline"
app:layout_constraintTop_toTopOf="@id/status_bar_guideline" />
<org.thoughtcrime.securesms.util.views.DarkOverflowToolbar
android:id="@+id/toolbar"
android:layout_width="0dp"
android:layout_height="@dimen/signal_m3_toolbar_height"
android:background="@color/transparent"
android:clipChildren="false"
android:clipToPadding="false"
android:minHeight="@dimen/signal_m3_toolbar_height"
android:theme="?attr/actionBarStyle"
app:contentInsetStart="46dp"
app:contentInsetStartWithNavigation="0dp"
app:layout_constraintEnd_toEndOf="@id/parent_end_guideline"
app:layout_constraintStart_toStartOf="@id/parent_start_guideline"
app:layout_constraintTop_toTopOf="@id/status_bar_guideline">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipChildren="false"
android:clipToPadding="false"
android:orientation="horizontal">
<include
layout="@layout/conversation_title_view"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1" />
<com.google.android.material.button.MaterialButton
android:id="@+id/conversation_group_call_join"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:fontFamily="sans-serif-medium"
android:text="@string/ConversationActivity_join"
android:textAllCaps="false"
android:textColor="@color/core_white"
android:visibility="gone"
app:backgroundTint="@color/core_ultramarine"
app:cornerRadius="@dimen/material_button_full_round_corner_radius"
app:icon="@drawable/ic_video_solid_18"
app:iconGravity="textStart"
app:iconTint="@color/core_white"
tools:visibility="visible" />
</LinearLayout>
</org.thoughtcrime.securesms.util.views.DarkOverflowToolbar>
<LinearLayout
android:id="@+id/conversation_banner_container"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:orientation="vertical"
app:layout_constraintEnd_toEndOf="@id/parent_end_guideline"
app:layout_constraintStart_toStartOf="@id/parent_start_guideline"
app:layout_constraintTop_toBottomOf="@id/toolbar">
<ViewStub
android:id="@+id/voice_note_player_stub"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inflatedId="@+id/voice_note_player"
android:layout="@layout/voice_note_player_stub" />
<ViewStub
android:id="@+id/unverified_banner_stub"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inflatedId="@+id/unverified_banner"
android:layout="@layout/conversation_activity_unverified_banner_stub" />
<ViewStub
android:id="@+id/reminder_stub"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inflatedId="@+id/reminder"
android:layout="@layout/conversation_activity_reminderview_stub" />
<ViewStub
android:id="@+id/review_banner_stub"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inflatedId="@+id/review_banner"
android:layout="@layout/review_banner_view" />
</LinearLayout>
<ViewStub
android:id="@+id/conversation_reaction_scrubber_stub"
android:layout_width="0dp"
android:layout_height="0dp"
android:inflatedId="@+id/conversation_reaction_scrubber"
android:layout="@layout/conversation_reaction_scrubber"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="@+id/parent_end_guideline"
app:layout_constraintStart_toStartOf="@+id/parent_start_guideline"
app:layout_constraintTop_toTopOf="@+id/status_bar_guideline" />
</org.thoughtcrime.securesms.components.InsetAwareConstraintLayout>

View file

@ -1,9 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<org.thoughtcrime.securesms.conversation.AttachmentKeyboard
xmlns:tools="http://schemas.android.com/tools"
tools:viewBindingIgnore="true"
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/attachment_keyboard"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:visibility="gone" />

View file

@ -1,9 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<org.thoughtcrime.securesms.components.emoji.MediaKeyboard
xmlns:tools="http://schemas.android.com/tools"
tools:viewBindingIgnore="true"
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/emoji_drawer"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:visibility="gone" />

View file

@ -1,105 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
tools:viewBindingIgnore="true"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="fill_parent"
android:layout_height="match_parent">
<FrameLayout
android:id="@+id/video_container"
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintBottom_toBottomOf="@android:id/list"
app:layout_constraintEnd_toEndOf="@android:id/list"
app:layout_constraintStart_toStartOf="@android:id/list"
app:layout_constraintTop_toTopOf="@android:id/list" />
<org.thoughtcrime.securesms.conversation.mutiselect.MultiselectRecyclerView
android:id="@android:id/list"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:cacheColorHint="@color/signal_background_primary"
android:clipChildren="false"
android:clipToPadding="false"
android:paddingBottom="@dimen/conversation_bottom_padding"
android:scrollbars="vertical"
android:splitMotionEvents="false"
android:overScrollMode="ifContentScrolls"
app:layout_constraintTop_toTopOf="parent" />
<FrameLayout
android:id="@+id/reactions_shade"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/reactions_screen_light_shade_color"
android:foreground="@color/reactions_screen_dark_shade_color"
android:visibility="gone" />
<TextView
android:id="@+id/scroll_date_header"
style="@style/Signal.Text.BodySmall"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:background="@drawable/sticky_date_header_background"
android:elevation="9dp"
android:gravity="center"
android:paddingStart="12dp"
android:paddingTop="4dp"
android:paddingEnd="12dp"
android:paddingBottom="4dp"
android:textColor="@color/signal_colorOnSurfaceVariant"
android:visibility="gone"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:text="March 1, 2015" />
<View
android:id="@+id/compose_divider"
android:layout_width="match_parent"
android:layout_height="2dp"
android:alpha="1"
android:background="@drawable/compose_divider_background"
android:visibility="invisible"
app:layout_constraintBottom_toBottomOf="parent" />
<org.thoughtcrime.securesms.components.ConversationScrollToView
android:id="@+id/scroll_to_mention"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="10dp"
android:layout_marginBottom="12dp"
android:visibility="invisible"
app:cstv_scroll_button_src="@drawable/ic_at_20"
app:layout_constraintBottom_toTopOf="@id/scroll_to_bottom"
app:layout_constraintEnd_toEndOf="parent"
app:layout_goneMarginBottom="20dp"
tools:visibility="visible" />
<org.thoughtcrime.securesms.components.ConversationScrollToView
android:id="@+id/scroll_to_bottom"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="10dp"
android:layout_marginBottom="16dp"
android:visibility="invisible"
app:cstv_scroll_button_src="@drawable/ic_chevron_down_20"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
tools:visibility="visible" />
<org.thoughtcrime.securesms.components.menu.SignalBottomActionBar
android:id="@+id/conversation_bottom_action_bar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="16dp"
android:layout_marginBottom="16dp"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -1,14 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<org.thoughtcrime.securesms.conversation.ConversationHeaderView
xmlns:tools="http://schemas.android.com/tools"
tools:viewBindingIgnore="true"
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="32dp"
android:paddingStart="16dp"
android:paddingEnd="16dp"
android:paddingTop="24dp"
android:paddingBottom="24dp"
app:layout_constraintTop_toTopOf="parent"/>

View file

@ -1,8 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<fragment xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
tools:viewBindingIgnore="true"
android:tag="mentions_picker_fragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:name="org.thoughtcrime.securesms.conversation.ui.mentions.MentionsPickerFragment"/>

View file

@ -1,7 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.fragment.app.FragmentContainerView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
tools:viewBindingIgnore="true"
android:id="@+id/fragment_container"
android:layout_width="match_parent"
android:layout_height="match_parent" />