kopia lustrzana https://github.com/ryukoposting/Signal-Android
Add save-as-you-compose drafts.
rodzic
192509f762
commit
0a76eb81e6
|
@ -50,6 +50,7 @@ import org.thoughtcrime.securesms.mms.GlideRequests;
|
|||
import org.thoughtcrime.securesms.mms.QuoteModel;
|
||||
import org.thoughtcrime.securesms.mms.SlideDeck;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
import org.thoughtcrime.securesms.util.ViewUtil;
|
||||
import org.thoughtcrime.securesms.util.concurrent.AssertedSuccessListener;
|
||||
import org.thoughtcrime.securesms.util.concurrent.ListenableFuture;
|
||||
|
@ -210,6 +211,10 @@ public class InputPanel extends LinearLayout
|
|||
int cornerRadius = readDimen(R.dimen.message_corner_collapse_radius);
|
||||
this.linkPreview.setCorners(cornerRadius, cornerRadius);
|
||||
}
|
||||
|
||||
if (listener != null) {
|
||||
listener.onQuoteChanged(id, author.getId());
|
||||
}
|
||||
}
|
||||
|
||||
public void clearQuote() {
|
||||
|
@ -230,6 +235,10 @@ public class InputPanel extends LinearLayout
|
|||
});
|
||||
|
||||
quoteAnimator.start();
|
||||
|
||||
if (listener != null) {
|
||||
listener.onQuoteCleared();
|
||||
}
|
||||
}
|
||||
|
||||
private static ValueAnimator createHeightAnimator(@NonNull View view,
|
||||
|
@ -572,6 +581,8 @@ public class InputPanel extends LinearLayout
|
|||
void onEmojiToggle();
|
||||
void onLinkPreviewCanceled();
|
||||
void onStickerSuggestionSelected(@NonNull StickerRecord sticker);
|
||||
void onQuoteChanged(long id, @NonNull RecipientId author);
|
||||
void onQuoteCleared();
|
||||
}
|
||||
|
||||
private static class SlideToCancel {
|
||||
|
|
|
@ -3,6 +3,8 @@ package org.thoughtcrime.securesms.contactshare;
|
|||
import android.text.Editable;
|
||||
import android.text.TextWatcher;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
public abstract class SimpleTextWatcher implements TextWatcher {
|
||||
|
||||
@Override
|
||||
|
@ -10,11 +12,15 @@ public abstract class SimpleTextWatcher implements TextWatcher {
|
|||
|
||||
@Override
|
||||
public void onTextChanged(CharSequence s, int start, int before, int count) {
|
||||
onTextChanged(s.toString());
|
||||
onTextChanged(s);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void afterTextChanged(Editable s) { }
|
||||
|
||||
public abstract void onTextChanged(String text);
|
||||
public void onTextChanged(@NonNull CharSequence text) {
|
||||
onTextChanged(text.toString());
|
||||
}
|
||||
|
||||
public void onTextChanged(@NonNull String text) { }
|
||||
}
|
||||
|
|
|
@ -17,7 +17,6 @@ import org.thoughtcrime.securesms.components.settings.app.subscription.DonationP
|
|||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.util.DynamicNoActionBarTheme
|
||||
import org.thoughtcrime.securesms.util.DynamicTheme
|
||||
import org.thoughtcrime.securesms.util.concurrent.ListenableFuture
|
||||
import org.thoughtcrime.securesms.util.views.Stub
|
||||
|
||||
open class ConversationActivity : PassphraseRequiredActivity(), ConversationParentFragment.Callback, DonationPaymentComponent {
|
||||
|
@ -89,10 +88,6 @@ open class ConversationActivity : PassphraseRequiredActivity(), ConversationPare
|
|||
toolbar.setNavigationOnClickListener { finish() }
|
||||
}
|
||||
|
||||
fun saveDraft(): ListenableFuture<Long> {
|
||||
return fragment.saveDraft()
|
||||
}
|
||||
|
||||
fun getRecipient(): Recipient {
|
||||
return fragment.recipient
|
||||
}
|
||||
|
|
|
@ -43,8 +43,6 @@ import android.provider.Browser;
|
|||
import android.provider.ContactsContract;
|
||||
import android.provider.Settings;
|
||||
import android.text.Editable;
|
||||
import android.text.Spannable;
|
||||
import android.text.SpannableString;
|
||||
import android.text.SpannableStringBuilder;
|
||||
import android.text.TextWatcher;
|
||||
import android.text.method.LinkMovementMethod;
|
||||
|
@ -103,7 +101,7 @@ import com.google.android.material.dialog.MaterialAlertDialogBuilder;
|
|||
import org.greenrobot.eventbus.EventBus;
|
||||
import org.greenrobot.eventbus.Subscribe;
|
||||
import org.greenrobot.eventbus.ThreadMode;
|
||||
import org.signal.core.util.DimensionUnit;
|
||||
import org.signal.core.util.StringUtil;
|
||||
import org.signal.core.util.ThreadUtil;
|
||||
import org.signal.core.util.concurrent.SignalExecutors;
|
||||
import org.signal.core.util.concurrent.SimpleTask;
|
||||
|
@ -138,8 +136,6 @@ import org.thoughtcrime.securesms.components.emoji.RecentEmojiPageModel;
|
|||
import org.thoughtcrime.securesms.components.identity.UnverifiedBannerView;
|
||||
import org.thoughtcrime.securesms.components.location.SignalPlace;
|
||||
import org.thoughtcrime.securesms.components.mention.MentionAnnotation;
|
||||
import org.thoughtcrime.securesms.components.menu.ActionItem;
|
||||
import org.thoughtcrime.securesms.components.menu.SignalContextMenu;
|
||||
import org.thoughtcrime.securesms.components.reminder.BubbleOptOutReminder;
|
||||
import org.thoughtcrime.securesms.components.reminder.ExpiredBuildReminder;
|
||||
import org.thoughtcrime.securesms.components.reminder.GroupsV1MigrationSuggestionsReminder;
|
||||
|
@ -163,25 +159,19 @@ import org.thoughtcrime.securesms.contactshare.ContactUtil;
|
|||
import org.thoughtcrime.securesms.contactshare.SimpleTextWatcher;
|
||||
import org.thoughtcrime.securesms.conversation.ConversationGroupViewModel.GroupActiveState;
|
||||
import org.thoughtcrime.securesms.conversation.ConversationMessage.ConversationMessageFactory;
|
||||
import org.thoughtcrime.securesms.conversation.drafts.DraftRepository;
|
||||
import org.thoughtcrime.securesms.conversation.drafts.DraftViewModel;
|
||||
import org.thoughtcrime.securesms.conversation.ui.groupcall.GroupCallViewModel;
|
||||
import org.thoughtcrime.securesms.conversation.ui.inlinequery.InlineQuery;
|
||||
import org.thoughtcrime.securesms.conversation.ui.inlinequery.InlineQueryChangedListener;
|
||||
import org.thoughtcrime.securesms.conversation.ui.inlinequery.InlineQueryResultsController;
|
||||
import org.thoughtcrime.securesms.conversation.ui.inlinequery.InlineQueryResultsPopup;
|
||||
import org.thoughtcrime.securesms.conversation.ui.inlinequery.InlineQueryViewModel;
|
||||
import org.thoughtcrime.securesms.conversation.ui.mentions.MentionsPickerViewModel;
|
||||
import org.thoughtcrime.securesms.crypto.ReentrantSessionLock;
|
||||
import org.thoughtcrime.securesms.crypto.SecurityEvent;
|
||||
import org.thoughtcrime.securesms.database.DraftDatabase;
|
||||
import org.thoughtcrime.securesms.database.DraftDatabase.Draft;
|
||||
import org.thoughtcrime.securesms.database.DraftDatabase.Drafts;
|
||||
import org.thoughtcrime.securesms.database.GroupDatabase;
|
||||
import org.thoughtcrime.securesms.database.IdentityDatabase.VerifiedStatus;
|
||||
import org.thoughtcrime.securesms.database.MentionUtil;
|
||||
import org.thoughtcrime.securesms.database.MentionUtil.UpdatedBodyAndMentions;
|
||||
import org.thoughtcrime.securesms.database.MmsSmsColumns.Types;
|
||||
import org.thoughtcrime.securesms.database.RecipientDatabase;
|
||||
import org.thoughtcrime.securesms.database.RecipientDatabase.RegisteredState;
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase;
|
||||
|
@ -242,7 +232,6 @@ import org.thoughtcrime.securesms.mms.GifSlide;
|
|||
import org.thoughtcrime.securesms.mms.GlideApp;
|
||||
import org.thoughtcrime.securesms.mms.GlideRequests;
|
||||
import org.thoughtcrime.securesms.mms.ImageSlide;
|
||||
import org.thoughtcrime.securesms.mms.LocationSlide;
|
||||
import org.thoughtcrime.securesms.mms.MediaConstraints;
|
||||
import org.thoughtcrime.securesms.mms.OutgoingMediaMessage;
|
||||
import org.thoughtcrime.securesms.mms.OutgoingSecureMediaMessage;
|
||||
|
@ -285,7 +274,6 @@ import org.thoughtcrime.securesms.stickers.StickerSearchRepository;
|
|||
import org.thoughtcrime.securesms.stories.StoryViewerArgs;
|
||||
import org.thoughtcrime.securesms.stories.viewer.StoryViewerActivity;
|
||||
import org.thoughtcrime.securesms.util.AsynchronousCallback;
|
||||
import org.thoughtcrime.securesms.util.Base64;
|
||||
import org.thoughtcrime.securesms.util.BitmapUtil;
|
||||
import org.thoughtcrime.securesms.util.BubbleUtil;
|
||||
import org.thoughtcrime.securesms.util.CharacterCalculator.CharacterState;
|
||||
|
@ -417,7 +405,7 @@ public class ConversationParentFragment extends Fragment
|
|||
protected Stub<ReminderView> reminderView;
|
||||
private Stub<UnverifiedBannerView> unverifiedBannerView;
|
||||
private Stub<ReviewBannerView> reviewBanner;
|
||||
private TypingStatusTextWatcher typingTextWatcher;
|
||||
private ComposeTextWatcher typingTextWatcher;
|
||||
private ConversationSearchBottomBar searchNav;
|
||||
private MenuItem searchViewItem;
|
||||
private MessageRequestsBottomView messageRequestBottomView;
|
||||
|
@ -466,11 +454,12 @@ public class ConversationParentFragment extends Fragment
|
|||
private LiveRecipient recipient;
|
||||
private long threadId;
|
||||
private int distributionType;
|
||||
private int reactWithAnyEmojiStartPage = -1;
|
||||
private boolean isSearchRequested = false;
|
||||
private int reactWithAnyEmojiStartPage = -1;
|
||||
private boolean isSearchRequested = false;
|
||||
|
||||
private final LifecycleDisposable disposables = new LifecycleDisposable();
|
||||
private final Debouncer optionsMenuDebouncer = new Debouncer(50);
|
||||
private final LifecycleDisposable disposables = new LifecycleDisposable();
|
||||
private final Debouncer optionsMenuDebouncer = new Debouncer(50);
|
||||
private final Debouncer textDraftSaveDebouncer = new Debouncer(500);
|
||||
|
||||
private IdentityRecordList identityRecords = new IdentityRecordList(Collections.emptyList());
|
||||
private Callback callback;
|
||||
|
@ -640,7 +629,6 @@ public class ConversationParentFragment extends Fragment
|
|||
@Override
|
||||
public void onStop() {
|
||||
super.onStop();
|
||||
saveDraft();
|
||||
EventBus.getDefault().unregister(this);
|
||||
}
|
||||
|
||||
|
@ -730,6 +718,7 @@ public class ConversationParentFragment extends Fragment
|
|||
case PICK_LOCATION:
|
||||
SignalPlace place = new SignalPlace(PlacePickerActivity.addressFromData(data));
|
||||
attachmentManager.setLocation(place, getCurrentMediaConstraints());
|
||||
draftViewModel.setLocationDraft(place);
|
||||
break;
|
||||
case SMS_DEFAULT:
|
||||
viewModel.updateSecurityInfo();
|
||||
|
@ -1423,6 +1412,7 @@ public class ConversationParentFragment extends Fragment
|
|||
|
||||
private void handleDistributionBroadcastEnabled(MenuItem item) {
|
||||
distributionType = ThreadDatabase.DistributionTypes.BROADCAST;
|
||||
draftViewModel.setDistributionType(distributionType);
|
||||
item.setChecked(true);
|
||||
|
||||
if (threadId != -1) {
|
||||
|
@ -1438,6 +1428,7 @@ public class ConversationParentFragment extends Fragment
|
|||
|
||||
private void handleDistributionConversationEnabled(MenuItem item) {
|
||||
distributionType = ThreadDatabase.DistributionTypes.CONVERSATION;
|
||||
draftViewModel.setDistributionType(distributionType);
|
||||
item.setChecked(true);
|
||||
|
||||
if (threadId != -1) {
|
||||
|
@ -1772,90 +1763,68 @@ public class ConversationParentFragment extends Fragment
|
|||
private ListenableFuture<Boolean> initializeDraftFromDatabase() {
|
||||
SettableFuture<Boolean> future = new SettableFuture<>();
|
||||
|
||||
final Context context = requireContext().getApplicationContext();
|
||||
Disposable disposable = draftViewModel
|
||||
.loadDrafts(threadId)
|
||||
.subscribe(databaseDrafts -> {
|
||||
Drafts drafts = databaseDrafts.getDrafts();
|
||||
CharSequence updatedText = databaseDrafts.getUpdatedText();
|
||||
|
||||
new AsyncTask<Void, Void, Pair<Drafts, CharSequence>>() {
|
||||
@Override
|
||||
protected Pair<Drafts, CharSequence> doInBackground(Void... params) {
|
||||
DraftDatabase draftDatabase = SignalDatabase.drafts();
|
||||
Drafts results = draftDatabase.getDrafts(threadId);
|
||||
Draft mentionsDraft = results.getDraftOfType(Draft.MENTION);
|
||||
Spannable updatedText = null;
|
||||
if (drafts.isEmpty()) {
|
||||
future.set(false);
|
||||
updateToggleButtonState();
|
||||
return;
|
||||
}
|
||||
|
||||
if (mentionsDraft != null) {
|
||||
String text = results.getDraftOfType(Draft.TEXT).getValue();
|
||||
List<Mention> mentions = MentionUtil.bodyRangeListToMentions(context, Base64.decodeOrThrow(mentionsDraft.getValue()));
|
||||
UpdatedBodyAndMentions updated = MentionUtil.updateBodyAndMentionsWithDisplayNames(context, text, mentions);
|
||||
AtomicInteger draftsRemaining = new AtomicInteger(drafts.size());
|
||||
AtomicBoolean success = new AtomicBoolean(false);
|
||||
ListenableFuture.Listener<Boolean> listener = new AssertedSuccessListener<Boolean>() {
|
||||
@Override
|
||||
public void onSuccess(Boolean result) {
|
||||
success.compareAndSet(false, result);
|
||||
|
||||
updatedText = new SpannableString(updated.getBody());
|
||||
MentionAnnotation.setMentionAnnotations(updatedText, updated.getMentions());
|
||||
}
|
||||
if (draftsRemaining.decrementAndGet() <= 0) {
|
||||
future.set(success.get());
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
draftDatabase.clearDrafts(threadId);
|
||||
for (Draft draft : drafts) {
|
||||
try {
|
||||
switch (draft.getType()) {
|
||||
case Draft.TEXT:
|
||||
composeText.setText(updatedText == null ? draft.getValue() : updatedText);
|
||||
listener.onSuccess(true);
|
||||
break;
|
||||
case Draft.LOCATION:
|
||||
attachmentManager.setLocation(SignalPlace.deserialize(draft.getValue()), getCurrentMediaConstraints()).addListener(listener);
|
||||
break;
|
||||
case Draft.IMAGE:
|
||||
setMedia(Uri.parse(draft.getValue()), MediaType.IMAGE).addListener(listener);
|
||||
break;
|
||||
case Draft.AUDIO:
|
||||
setMedia(Uri.parse(draft.getValue()), MediaType.AUDIO).addListener(listener);
|
||||
break;
|
||||
case Draft.VIDEO:
|
||||
setMedia(Uri.parse(draft.getValue()), MediaType.VIDEO).addListener(listener);
|
||||
break;
|
||||
case Draft.QUOTE:
|
||||
SettableFuture<Boolean> quoteResult = new SettableFuture<>();
|
||||
new QuoteRestorationTask(draft.getValue(), quoteResult).execute();
|
||||
quoteResult.addListener(listener);
|
||||
break;
|
||||
case Draft.VOICE_NOTE:
|
||||
listener.onSuccess(true);
|
||||
break;
|
||||
}
|
||||
} catch (IOException e) {
|
||||
Log.w(TAG, e);
|
||||
}
|
||||
}
|
||||
|
||||
return new Pair<>(results, updatedText);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPostExecute(Pair<Drafts, CharSequence> draftsWithUpdatedMentions) {
|
||||
Drafts drafts = Objects.requireNonNull(draftsWithUpdatedMentions.first());
|
||||
CharSequence updatedText = draftsWithUpdatedMentions.second();
|
||||
|
||||
if (drafts.isEmpty()) {
|
||||
future.set(false);
|
||||
updateToggleButtonState();
|
||||
return;
|
||||
}
|
||||
});
|
||||
|
||||
AtomicInteger draftsRemaining = new AtomicInteger(drafts.size());
|
||||
AtomicBoolean success = new AtomicBoolean(false);
|
||||
ListenableFuture.Listener<Boolean> listener = new AssertedSuccessListener<Boolean>() {
|
||||
@Override
|
||||
public void onSuccess(Boolean result) {
|
||||
success.compareAndSet(false, result);
|
||||
|
||||
if (draftsRemaining.decrementAndGet() <= 0) {
|
||||
future.set(success.get());
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
for (Draft draft : drafts) {
|
||||
try {
|
||||
switch (draft.getType()) {
|
||||
case Draft.TEXT:
|
||||
composeText.setText(updatedText == null ? draft.getValue() : updatedText);
|
||||
listener.onSuccess(true);
|
||||
break;
|
||||
case Draft.LOCATION:
|
||||
attachmentManager.setLocation(SignalPlace.deserialize(draft.getValue()), getCurrentMediaConstraints()).addListener(listener);
|
||||
break;
|
||||
case Draft.IMAGE:
|
||||
setMedia(Uri.parse(draft.getValue()), MediaType.IMAGE).addListener(listener);
|
||||
break;
|
||||
case Draft.AUDIO:
|
||||
setMedia(Uri.parse(draft.getValue()), MediaType.AUDIO).addListener(listener);
|
||||
break;
|
||||
case Draft.VIDEO:
|
||||
setMedia(Uri.parse(draft.getValue()), MediaType.VIDEO).addListener(listener);
|
||||
break;
|
||||
case Draft.QUOTE:
|
||||
SettableFuture<Boolean> quoteResult = new SettableFuture<>();
|
||||
new QuoteRestorationTask(draft.getValue(), quoteResult).execute();
|
||||
quoteResult.addListener(listener);
|
||||
break;
|
||||
case Draft.VOICE_NOTE:
|
||||
draftViewModel.setVoiceNoteDraft(recipient.getId(), draft);
|
||||
break;
|
||||
}
|
||||
} catch (IOException e) {
|
||||
Log.w(TAG, e);
|
||||
}
|
||||
}
|
||||
|
||||
updateToggleButtonState();
|
||||
}
|
||||
}.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
|
||||
disposables.add(disposable);
|
||||
|
||||
return future;
|
||||
}
|
||||
|
@ -2062,7 +2031,7 @@ public class ConversationParentFragment extends Fragment
|
|||
|
||||
attachmentManager = new AttachmentManager(requireActivity(), this);
|
||||
audioRecorder = new AudioRecorder(requireContext());
|
||||
typingTextWatcher = new TypingStatusTextWatcher();
|
||||
typingTextWatcher = new ComposeTextWatcher();
|
||||
|
||||
SendButtonListener sendButtonListener = new SendButtonListener();
|
||||
ComposeKeyPressedListener composeKeyPressedListener = new ComposeKeyPressedListener();
|
||||
|
@ -2437,17 +2406,24 @@ public class ConversationParentFragment extends Fragment
|
|||
}
|
||||
|
||||
public void initializeDraftViewModel() {
|
||||
draftViewModel = new ViewModelProvider(this, new DraftViewModel.Factory(new DraftRepository(requireContext().getApplicationContext()))).get(DraftViewModel.class);
|
||||
draftViewModel = new ViewModelProvider(this).get(DraftViewModel.class);
|
||||
|
||||
recipient.observe(getViewLifecycleOwner(), r -> {
|
||||
draftViewModel.onRecipientChanged(r);
|
||||
});
|
||||
|
||||
draftViewModel.getState().observe(getViewLifecycleOwner(),
|
||||
state -> {
|
||||
inputPanel.setVoiceNoteDraft(state.getVoiceNoteDraft());
|
||||
updateToggleButtonState();
|
||||
});
|
||||
draftViewModel.setThreadId(threadId);
|
||||
draftViewModel.setDistributionType(distributionType);
|
||||
|
||||
disposables.add(
|
||||
draftViewModel
|
||||
.getState()
|
||||
.distinctUntilChanged(state -> state.getVoiceNoteDraft())
|
||||
.subscribe(state -> {
|
||||
inputPanel.setVoiceNoteDraft(state.getVoiceNoteDraft());
|
||||
updateToggleButtonState();
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
private void showGroupCallingTooltip() {
|
||||
|
@ -2605,6 +2581,7 @@ public class ConversationParentFragment extends Fragment
|
|||
this.threadId = threadId;
|
||||
fragment.reload(recipient, this.threadId);
|
||||
setVisibleThread(this.threadId);
|
||||
draftViewModel.setThreadId(this.threadId);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
@ -2715,102 +2692,6 @@ public class ConversationParentFragment extends Fragment
|
|||
builder.show();
|
||||
}
|
||||
|
||||
private Drafts getDraftsForCurrentState() {
|
||||
Drafts drafts = new Drafts();
|
||||
|
||||
if (recipient.get().isGroup() && !recipient.get().isActiveGroup()) {
|
||||
return drafts;
|
||||
}
|
||||
|
||||
if (!Util.isEmpty(composeText)) {
|
||||
drafts.add(new Draft(Draft.TEXT, composeText.getTextTrimmed().toString()));
|
||||
List<Mention> draftMentions = composeText.getMentions();
|
||||
if (!draftMentions.isEmpty()) {
|
||||
drafts.add(new Draft(Draft.MENTION, Base64.encodeBytes(MentionUtil.mentionsToBodyRangeList(draftMentions).toByteArray())));
|
||||
}
|
||||
}
|
||||
|
||||
for (Slide slide : attachmentManager.buildSlideDeck().getSlides()) {
|
||||
if (slide.hasAudio() && slide.getUri() != null) drafts.add(new Draft(Draft.AUDIO, slide.getUri().toString()));
|
||||
else if (slide.hasVideo() && slide.getUri() != null) drafts.add(new Draft(Draft.VIDEO, slide.getUri().toString()));
|
||||
else if (slide.hasLocation()) drafts.add(new Draft(Draft.LOCATION, ((LocationSlide)slide).getPlace().serialize()));
|
||||
else if (slide.hasImage() && slide.getUri() != null) drafts.add(new Draft(Draft.IMAGE, slide.getUri().toString()));
|
||||
}
|
||||
|
||||
Optional<QuoteModel> quote = inputPanel.getQuote();
|
||||
|
||||
if (quote.isPresent()) {
|
||||
drafts.add(new Draft(Draft.QUOTE, new QuoteId(quote.get().getId(), quote.get().getAuthor()).serialize()));
|
||||
}
|
||||
|
||||
Draft voiceNoteDraft = draftViewModel.getVoiceNoteDraft();
|
||||
if (voiceNoteDraft != null) {
|
||||
drafts.add(voiceNoteDraft);
|
||||
}
|
||||
|
||||
return drafts;
|
||||
}
|
||||
|
||||
protected ListenableFuture<Long> saveDraft() {
|
||||
final SettableFuture<Long> future = new SettableFuture<>();
|
||||
|
||||
if (this.recipient == null) {
|
||||
future.set(threadId);
|
||||
return future;
|
||||
}
|
||||
|
||||
final Context context = requireContext().getApplicationContext();
|
||||
final Drafts drafts = getDraftsForCurrentState();
|
||||
final long thisThreadId = this.threadId;
|
||||
final RecipientId recipientId = this.recipient.getId();
|
||||
final int thisDistributionType = this.distributionType;
|
||||
final ListenableFuture<VoiceNoteDraft> voiceNoteDraftFuture = draftViewModel.consumeVoiceNoteDraftFuture();
|
||||
|
||||
new AsyncTask<Long, Void, Long>() {
|
||||
@Override
|
||||
protected Long doInBackground(Long... params) {
|
||||
if (voiceNoteDraftFuture != null) {
|
||||
try {
|
||||
Draft voiceNoteDraft = voiceNoteDraftFuture.get().asDraft();
|
||||
draftViewModel.setVoiceNoteDraft(recipientId, voiceNoteDraft);
|
||||
drafts.add(voiceNoteDraft);
|
||||
} catch (ExecutionException | InterruptedException e) {
|
||||
Log.w(TAG, "Could not extract voice note draft data.", e);
|
||||
}
|
||||
}
|
||||
|
||||
ThreadDatabase threadDatabase = SignalDatabase.threads();
|
||||
DraftDatabase draftDatabase = SignalDatabase.drafts();
|
||||
long threadId = params[0];
|
||||
|
||||
if (drafts.size() > 0) {
|
||||
if (threadId == -1) threadId = threadDatabase.getOrCreateThreadIdFor(getRecipient(), thisDistributionType);
|
||||
|
||||
draftDatabase.replaceDrafts(threadId, drafts);
|
||||
threadDatabase.updateSnippet(threadId, drafts.getSnippet(context),
|
||||
drafts.getUriSnippet(),
|
||||
System.currentTimeMillis(), Types.BASE_DRAFT_TYPE, true);
|
||||
} else if (threadId > 0) {
|
||||
threadDatabase.update(threadId, false);
|
||||
}
|
||||
|
||||
if (drafts.isEmpty()) {
|
||||
draftDatabase.clearDrafts(threadId);
|
||||
}
|
||||
|
||||
return threadId;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPostExecute(Long result) {
|
||||
future.set(result);
|
||||
}
|
||||
|
||||
}.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, thisThreadId);
|
||||
|
||||
return future;
|
||||
}
|
||||
|
||||
private void setBlockedUserState(Recipient recipient, @NonNull ConversationSecurityInfo conversationSecurityInfo) {
|
||||
if (!conversationSecurityInfo.isInitialized()) {
|
||||
Log.i(TAG, "Ignoring blocked state update for uninitialized security info.");
|
||||
|
@ -2972,6 +2853,8 @@ public class ConversationParentFragment extends Fragment
|
|||
|
||||
updateLinkPreviewState();
|
||||
callback.onSendComplete(threadId);
|
||||
|
||||
draftViewModel.onSendComplete(threadId);
|
||||
}
|
||||
|
||||
private void sendMessage(@Nullable String metricId) {
|
||||
|
@ -2985,7 +2868,6 @@ public class ConversationParentFragment extends Fragment
|
|||
AudioSlide audioSlide = AudioSlide.createFromVoiceNoteDraft(requireContext(), voiceNote);
|
||||
|
||||
sendVoiceNote(Objects.requireNonNull(audioSlide.getUri()), audioSlide.getFileSize());
|
||||
draftViewModel.clearVoiceNoteDraft();
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -3205,7 +3087,7 @@ public class ConversationParentFragment extends Fragment
|
|||
return;
|
||||
}
|
||||
|
||||
if (draftViewModel.hasVoiceNoteDraft()) {
|
||||
if (draftViewModel.getVoiceNoteDraft() != null) {
|
||||
buttonToggle.display(sendButton);
|
||||
quickAttachmentToggle.hide();
|
||||
inlineAttachmentToggle.hide();
|
||||
|
@ -3329,7 +3211,7 @@ public class ConversationParentFragment extends Fragment
|
|||
if (getLifecycle().getCurrentState().isAtLeast(Lifecycle.State.RESUMED)) {
|
||||
future.addListener(new DeleteCanceledVoiceNoteListener());
|
||||
} else {
|
||||
draftViewModel.setVoiceNoteDraftFuture(future);
|
||||
draftViewModel.saveEphemeralVoiceNoteDraft(future);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -3359,6 +3241,16 @@ public class ConversationParentFragment extends Fragment
|
|||
sendSticker(sticker, true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onQuoteChanged(long id, @NonNull RecipientId author) {
|
||||
draftViewModel.setQuoteDraft(id, author);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onQuoteCleared() {
|
||||
draftViewModel.clearQuoteDraft();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onMediaSelected(@NonNull Uri uri, String contentType) {
|
||||
if (MediaUtil.isGif(contentType) || MediaUtil.isImageType(contentType)) {
|
||||
|
@ -3389,32 +3281,26 @@ public class ConversationParentFragment extends Fragment
|
|||
}
|
||||
|
||||
private void sendVoiceNote(@NonNull Uri uri, long size) {
|
||||
boolean initiating = threadId == -1;
|
||||
long expiresIn = TimeUnit.SECONDS.toMillis(recipient.get().getExpiresInSeconds());
|
||||
AudioSlide audioSlide = new AudioSlide(requireContext(), uri, size, MediaUtil.AUDIO_AAC, true);
|
||||
SlideDeck slideDeck = new SlideDeck();
|
||||
boolean initiating = threadId == -1;
|
||||
long expiresIn = TimeUnit.SECONDS.toMillis(recipient.get().getExpiresInSeconds());
|
||||
AudioSlide audioSlide = new AudioSlide(requireContext(), uri, size, MediaUtil.AUDIO_AAC, true);
|
||||
SlideDeck slideDeck = new SlideDeck();
|
||||
|
||||
slideDeck.addSlide(audioSlide);
|
||||
|
||||
ListenableFuture<Void> sendResult = sendMediaMessage(recipient.getId(),
|
||||
sendButton.getSelectedSendType(),
|
||||
"",
|
||||
slideDeck,
|
||||
inputPanel.getQuote().orElse(null),
|
||||
Collections.emptyList(),
|
||||
Collections.emptyList(),
|
||||
composeText.getMentions(),
|
||||
expiresIn,
|
||||
false,
|
||||
initiating,
|
||||
true,
|
||||
null);
|
||||
|
||||
sendResult.addListener(new AssertedSuccessListener<Void>() {
|
||||
@Override
|
||||
public void onSuccess(Void nothing) {
|
||||
draftViewModel.deleteBlob(uri);
|
||||
}
|
||||
});
|
||||
sendMediaMessage(recipient.getId(),
|
||||
sendButton.getSelectedSendType(),
|
||||
"",
|
||||
slideDeck,
|
||||
inputPanel.getQuote().orElse(null),
|
||||
Collections.emptyList(),
|
||||
Collections.emptyList(),
|
||||
composeText.getMentions(),
|
||||
expiresIn,
|
||||
false,
|
||||
initiating,
|
||||
true,
|
||||
null);
|
||||
}
|
||||
|
||||
private void sendSticker(@NonNull StickerRecord stickerRecord, boolean clearCompose) {
|
||||
|
@ -3446,9 +3332,9 @@ public class ConversationParentFragment extends Fragment
|
|||
}
|
||||
|
||||
private void silentlySetComposeText(String text) {
|
||||
typingTextWatcher.setEnabled(false);
|
||||
typingTextWatcher.setTypingStatusEnabled(false);
|
||||
composeText.setText(text);
|
||||
typingTextWatcher.setEnabled(true);
|
||||
typingTextWatcher.setTypingStatusEnabled(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -3584,7 +3470,7 @@ public class ConversationParentFragment extends Fragment
|
|||
private final class DeleteCanceledVoiceNoteListener implements ListenableFuture.Listener<VoiceNoteDraft> {
|
||||
@Override
|
||||
public void onSuccess(final VoiceNoteDraft result) {
|
||||
draftViewModel.deleteBlob(result.getUri());
|
||||
draftViewModel.cancelEphemeralVoiceNoteDraft(result.asDraft());
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -3691,15 +3577,24 @@ public class ConversationParentFragment extends Fragment
|
|||
}
|
||||
}
|
||||
|
||||
private class TypingStatusTextWatcher extends SimpleTextWatcher {
|
||||
private class ComposeTextWatcher extends SimpleTextWatcher {
|
||||
|
||||
private boolean enabled = true;
|
||||
private boolean typingStatusEnabled = true;
|
||||
|
||||
private String previousText = "";
|
||||
|
||||
@Override
|
||||
public void onTextChanged(String text) {
|
||||
if (enabled && threadId > 0 && viewModel.isPushAvailable() && !isSmsForced() && !recipient.get().isBlocked() && !recipient.get().isSelf()) {
|
||||
public void onTextChanged(@NonNull CharSequence text) {
|
||||
handleSaveDraftOnTextChange(text);
|
||||
handleTypingIndicatorOnTextChange(text.toString());
|
||||
}
|
||||
|
||||
private void handleSaveDraftOnTextChange(@NonNull CharSequence text) {
|
||||
textDraftSaveDebouncer.publish(() -> draftViewModel.setTextDraft(StringUtil.trimSequence(text).toString(), MentionAnnotation.getMentionsFromAnnotations(text)));
|
||||
}
|
||||
|
||||
private void handleTypingIndicatorOnTextChange(@NonNull String text) {
|
||||
if (typingStatusEnabled && threadId > 0 && viewModel.isPushAvailable() && !isSmsForced() && !recipient.get().isBlocked() && !recipient.get().isSelf()) {
|
||||
TypingStatusSender typingStatusSender = ApplicationDependencies.getTypingStatusSender();
|
||||
|
||||
if (text.length() == 0) {
|
||||
|
@ -3714,8 +3609,8 @@ public class ConversationParentFragment extends Fragment
|
|||
}
|
||||
}
|
||||
|
||||
public void setEnabled(boolean enabled) {
|
||||
this.enabled = enabled;
|
||||
public void setTypingStatusEnabled(boolean enabled) {
|
||||
this.typingStatusEnabled = enabled;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -3931,6 +3826,7 @@ public class ConversationParentFragment extends Fragment
|
|||
@Override
|
||||
public void setThreadId(long threadId) {
|
||||
this.threadId = threadId;
|
||||
draftViewModel.setThreadId(threadId);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -4028,6 +3924,11 @@ public class ConversationParentFragment extends Fragment
|
|||
updateLinkPreviewState();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLocationRemoved() {
|
||||
draftViewModel.clearLocationDraft();
|
||||
}
|
||||
|
||||
private void onMessageRequestDeleteClicked(@NonNull MessageRequestViewModel requestModel) {
|
||||
Recipient recipient = requestModel.getRecipient().getValue();
|
||||
if (recipient == null) {
|
||||
|
|
|
@ -1,23 +1,17 @@
|
|||
package org.thoughtcrime.securesms.conversation;
|
||||
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
import android.view.Display;
|
||||
import android.view.Gravity;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuInflater;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.view.WindowManager;
|
||||
|
||||
import androidx.appcompat.widget.Toolbar;
|
||||
import androidx.core.app.ActivityOptionsCompat;
|
||||
|
||||
import org.signal.core.util.logging.Log;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.util.concurrent.ListenableFuture;
|
||||
|
||||
import java.util.concurrent.ExecutionException;
|
||||
|
||||
public class ConversationPopupActivity extends ConversationActivity {
|
||||
|
||||
|
@ -73,33 +67,6 @@ public class ConversationPopupActivity extends ConversationActivity {
|
|||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem item) {
|
||||
switch (item.getItemId()) {
|
||||
case R.id.menu_expand:
|
||||
saveDraft().addListener(new ListenableFuture.Listener<Long>() {
|
||||
@Override
|
||||
public void onSuccess(Long result) {
|
||||
ActivityOptionsCompat transition = ActivityOptionsCompat.makeScaleUpAnimation(getWindow().getDecorView(), 0, 0, getWindow().getAttributes().width, getWindow().getAttributes().height);
|
||||
Intent intent = ConversationIntents.createBuilder(ConversationPopupActivity.this, getRecipient().getId(), result)
|
||||
.build();
|
||||
|
||||
startActivity(intent, transition.toBundle());
|
||||
|
||||
finish();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(ExecutionException e) {
|
||||
Log.w(TAG, e);
|
||||
}
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onInitializeToolbar(Toolbar toolbar) {
|
||||
}
|
||||
|
|
|
@ -2,18 +2,75 @@ package org.thoughtcrime.securesms.conversation.drafts
|
|||
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import android.text.Spannable
|
||||
import android.text.SpannableString
|
||||
import io.reactivex.rxjava3.core.Single
|
||||
import io.reactivex.rxjava3.schedulers.Schedulers
|
||||
import org.signal.core.util.concurrent.SignalExecutors
|
||||
import org.thoughtcrime.securesms.components.mention.MentionAnnotation
|
||||
import org.thoughtcrime.securesms.database.DraftDatabase
|
||||
import org.thoughtcrime.securesms.database.DraftDatabase.Drafts
|
||||
import org.thoughtcrime.securesms.database.MentionUtil
|
||||
import org.thoughtcrime.securesms.database.MmsSmsColumns
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||
import org.thoughtcrime.securesms.database.ThreadDatabase
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
|
||||
import org.thoughtcrime.securesms.providers.BlobProvider
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.util.Base64
|
||||
import org.thoughtcrime.securesms.util.concurrent.SerialMonoLifoExecutor
|
||||
import java.util.concurrent.Executor
|
||||
|
||||
class DraftRepository(private val context: Context) {
|
||||
fun deleteVoiceNoteDraft(draft: DraftDatabase.Draft) {
|
||||
deleteBlob(Uri.parse(draft.value).buildUpon().clearQuery().build())
|
||||
}
|
||||
class DraftRepository(
|
||||
private val context: Context = ApplicationDependencies.getApplication(),
|
||||
private val threadDatabase: ThreadDatabase = SignalDatabase.threads,
|
||||
private val draftDatabase: DraftDatabase = SignalDatabase.drafts,
|
||||
private val saveDraftsExecutor: Executor = SerialMonoLifoExecutor(SignalExecutors.BOUNDED)
|
||||
) {
|
||||
|
||||
fun deleteBlob(uri: Uri) {
|
||||
SignalExecutors.BOUNDED.execute {
|
||||
BlobProvider.getInstance().delete(context, uri)
|
||||
fun deleteVoiceNoteDraftData(draft: DraftDatabase.Draft?) {
|
||||
if (draft != null) {
|
||||
SignalExecutors.BOUNDED.execute {
|
||||
BlobProvider.getInstance().delete(context, Uri.parse(draft.value).buildUpon().clearQuery().build())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun saveDrafts(recipient: Recipient, threadId: Long, distributionType: Int, drafts: Drafts) {
|
||||
saveDraftsExecutor.execute {
|
||||
if (drafts.isNotEmpty()) {
|
||||
val actualThreadId = if (threadId == -1L) {
|
||||
threadDatabase.getOrCreateThreadIdFor(recipient, distributionType)
|
||||
} else {
|
||||
threadId
|
||||
}
|
||||
|
||||
draftDatabase.replaceDrafts(actualThreadId, drafts)
|
||||
threadDatabase.updateSnippet(actualThreadId, drafts.getSnippet(context), drafts.uriSnippet, System.currentTimeMillis(), MmsSmsColumns.Types.BASE_DRAFT_TYPE, true)
|
||||
} else if (threadId > 0) {
|
||||
threadDatabase.update(threadId, false)
|
||||
draftDatabase.clearDrafts(threadId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun loadDrafts(threadId: Long): Single<DatabaseDraft> {
|
||||
return Single.fromCallable {
|
||||
val drafts: Drafts = draftDatabase.getDrafts(threadId)
|
||||
val mentionsDraft = drafts.getDraftOfType(DraftDatabase.Draft.MENTION)
|
||||
var updatedText: Spannable? = null
|
||||
|
||||
if (mentionsDraft != null) {
|
||||
val text = drafts.getDraftOfType(DraftDatabase.Draft.TEXT)!!.value
|
||||
val mentions = MentionUtil.bodyRangeListToMentions(context, Base64.decodeOrThrow(mentionsDraft.value))
|
||||
val updated = MentionUtil.updateBodyAndMentionsWithDisplayNames(context, text, mentions)
|
||||
updatedText = SpannableString(updated.body)
|
||||
MentionAnnotation.setMentionAnnotations(updatedText, updated.mentions)
|
||||
}
|
||||
|
||||
DatabaseDraft(drafts, updatedText)
|
||||
}.subscribeOn(Schedulers.io())
|
||||
}
|
||||
|
||||
data class DatabaseDraft(val drafts: Drafts, val updatedText: CharSequence?)
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
package org.thoughtcrime.securesms.conversation.drafts
|
||||
|
||||
import org.thoughtcrime.securesms.database.DraftDatabase
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.database.DraftDatabase.Drafts
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
|
||||
/**
|
||||
|
@ -10,6 +10,38 @@ import org.thoughtcrime.securesms.recipients.RecipientId
|
|||
* management pattern going forward for drafts.
|
||||
*/
|
||||
data class DraftState(
|
||||
val recipientId: RecipientId = Recipient.UNKNOWN.id,
|
||||
val voiceNoteDraft: DraftDatabase.Draft? = null
|
||||
)
|
||||
val recipientId: RecipientId = RecipientId.UNKNOWN,
|
||||
val threadId: Long = -1,
|
||||
val distributionType: Int = 0,
|
||||
val textDraft: DraftDatabase.Draft? = null,
|
||||
val mentionsDraft: DraftDatabase.Draft? = null,
|
||||
val quoteDraft: DraftDatabase.Draft? = null,
|
||||
val locationDraft: DraftDatabase.Draft? = null,
|
||||
val voiceNoteDraft: DraftDatabase.Draft? = null,
|
||||
) {
|
||||
|
||||
fun copyAndClearDrafts(threadId: Long = this.threadId): DraftState {
|
||||
return DraftState(recipientId = recipientId, threadId = threadId, distributionType = distributionType)
|
||||
}
|
||||
|
||||
fun toDrafts(): Drafts {
|
||||
return Drafts().apply {
|
||||
addIfNotNull(textDraft)
|
||||
addIfNotNull(mentionsDraft)
|
||||
addIfNotNull(quoteDraft)
|
||||
addIfNotNull(locationDraft)
|
||||
addIfNotNull(voiceNoteDraft)
|
||||
}
|
||||
}
|
||||
|
||||
fun copyAndSetDrafts(threadId: Long, drafts: Drafts): DraftState {
|
||||
return copy(
|
||||
threadId = threadId,
|
||||
textDraft = drafts.getDraftOfType(DraftDatabase.Draft.TEXT),
|
||||
mentionsDraft = drafts.getDraftOfType(DraftDatabase.Draft.MENTION),
|
||||
quoteDraft = drafts.getDraftOfType(DraftDatabase.Draft.QUOTE),
|
||||
locationDraft = drafts.getDraftOfType(DraftDatabase.Draft.LOCATION),
|
||||
voiceNoteDraft = drafts.getDraftOfType(DraftDatabase.Draft.VOICE_NOTE),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,87 +1,126 @@
|
|||
package org.thoughtcrime.securesms.conversation.drafts
|
||||
|
||||
import android.net.Uri
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
||||
import io.reactivex.rxjava3.core.Flowable
|
||||
import io.reactivex.rxjava3.core.Single
|
||||
import org.thoughtcrime.securesms.components.location.SignalPlace
|
||||
import org.thoughtcrime.securesms.components.voice.VoiceNoteDraft
|
||||
import org.thoughtcrime.securesms.database.DraftDatabase
|
||||
import org.thoughtcrime.securesms.database.DraftDatabase.Draft
|
||||
import org.thoughtcrime.securesms.database.MentionUtil
|
||||
import org.thoughtcrime.securesms.database.model.Mention
|
||||
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.concurrent.ListenableFuture
|
||||
import org.thoughtcrime.securesms.util.livedata.Store
|
||||
import org.thoughtcrime.securesms.util.rx.RxStore
|
||||
|
||||
/**
|
||||
* ViewModel responsible for holding Voice Note draft state. The intention is to allow
|
||||
* other pieces of draft state to be held here as well in the future, and to serve as a
|
||||
* management pattern going forward for drafts.
|
||||
*/
|
||||
class DraftViewModel(
|
||||
private val repository: DraftRepository
|
||||
class DraftViewModel @JvmOverloads constructor(
|
||||
private val repository: DraftRepository = DraftRepository()
|
||||
) : ViewModel() {
|
||||
|
||||
private val store = Store<DraftState>(DraftState())
|
||||
private val store = RxStore(DraftState())
|
||||
|
||||
val state: LiveData<DraftState> = store.stateLiveData
|
||||
val state: Flowable<DraftState> = store.stateFlowable.observeOn(AndroidSchedulers.mainThread())
|
||||
|
||||
private var voiceNoteDraftFuture: ListenableFuture<VoiceNoteDraft>? = null
|
||||
|
||||
val voiceNoteDraft: DraftDatabase.Draft?
|
||||
val voiceNoteDraft: Draft?
|
||||
get() = store.state.voiceNoteDraft
|
||||
|
||||
fun consumeVoiceNoteDraftFuture(): ListenableFuture<VoiceNoteDraft>? {
|
||||
val future = voiceNoteDraftFuture
|
||||
voiceNoteDraftFuture = null
|
||||
|
||||
return future
|
||||
fun setThreadId(threadId: Long) {
|
||||
store.update { it.copy(threadId = threadId) }
|
||||
}
|
||||
|
||||
fun setVoiceNoteDraftFuture(voiceNoteDraftFuture: ListenableFuture<VoiceNoteDraft>) {
|
||||
this.voiceNoteDraftFuture = voiceNoteDraftFuture
|
||||
fun setDistributionType(distributionType: Int) {
|
||||
store.update { it.copy(distributionType = distributionType) }
|
||||
}
|
||||
|
||||
fun setVoiceNoteDraft(recipientId: RecipientId, draft: DraftDatabase.Draft) {
|
||||
fun saveEphemeralVoiceNoteDraft(voiceNoteDraftFuture: ListenableFuture<VoiceNoteDraft>) {
|
||||
store.update {
|
||||
it.copy(recipientId = recipientId, voiceNoteDraft = draft)
|
||||
saveDrafts(it.copy(voiceNoteDraft = voiceNoteDraftFuture.get().asDraft()))
|
||||
}
|
||||
}
|
||||
|
||||
@get:JvmName("hasVoiceNoteDraft")
|
||||
val hasVoiceNoteDraft: Boolean
|
||||
get() = store.state.voiceNoteDraft != null
|
||||
|
||||
fun clearVoiceNoteDraft() {
|
||||
store.update {
|
||||
it.copy(voiceNoteDraft = null)
|
||||
}
|
||||
fun cancelEphemeralVoiceNoteDraft(draft: Draft) {
|
||||
repository.deleteVoiceNoteDraftData(draft)
|
||||
}
|
||||
|
||||
fun deleteVoiceNoteDraft() {
|
||||
val draft = store.state.voiceNoteDraft
|
||||
if (draft != null) {
|
||||
clearVoiceNoteDraft()
|
||||
repository.deleteVoiceNoteDraft(draft)
|
||||
store.update {
|
||||
repository.deleteVoiceNoteDraftData(it.voiceNoteDraft)
|
||||
saveDrafts(it.copy(voiceNoteDraft = null))
|
||||
}
|
||||
}
|
||||
|
||||
fun onRecipientChanged(recipient: Recipient) {
|
||||
store.update { it.copy(recipientId = recipient.id) }
|
||||
}
|
||||
|
||||
fun setTextDraft(text: String, mentions: List<Mention>) {
|
||||
store.update {
|
||||
if (recipient.id != it.recipientId) {
|
||||
it.copy(recipientId = recipient.id, voiceNoteDraft = null)
|
||||
} else {
|
||||
it
|
||||
saveDrafts(it.copy(textDraft = text.toTextDraft(), mentionsDraft = mentions.toMentionsDraft()))
|
||||
}
|
||||
}
|
||||
|
||||
fun setLocationDraft(place: SignalPlace) {
|
||||
store.update {
|
||||
saveDrafts(it.copy(locationDraft = Draft(Draft.LOCATION, place.serialize())))
|
||||
}
|
||||
}
|
||||
|
||||
fun clearLocationDraft() {
|
||||
store.update {
|
||||
saveDrafts(it.copy(locationDraft = null))
|
||||
}
|
||||
}
|
||||
|
||||
fun setQuoteDraft(id: Long, author: RecipientId) {
|
||||
store.update {
|
||||
saveDrafts(it.copy(quoteDraft = Draft(Draft.QUOTE, QuoteId(id, author).serialize())))
|
||||
}
|
||||
}
|
||||
|
||||
fun clearQuoteDraft() {
|
||||
store.update {
|
||||
saveDrafts(it.copy(quoteDraft = null))
|
||||
}
|
||||
}
|
||||
|
||||
fun onSendComplete(threadId: Long) {
|
||||
repository.deleteVoiceNoteDraftData(store.state.voiceNoteDraft)
|
||||
store.update { saveDrafts(it.copyAndClearDrafts(threadId)) }
|
||||
}
|
||||
|
||||
private fun saveDrafts(state: DraftState): DraftState {
|
||||
repository.saveDrafts(Recipient.resolved(state.recipientId), state.threadId, state.distributionType, state.toDrafts())
|
||||
return state
|
||||
}
|
||||
|
||||
fun loadDrafts(threadId: Long): Single<DraftRepository.DatabaseDraft> {
|
||||
return repository
|
||||
.loadDrafts(threadId)
|
||||
.doOnSuccess { drafts ->
|
||||
store.update { it.copyAndSetDrafts(threadId, drafts.drafts) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun deleteBlob(uri: Uri) {
|
||||
repository.deleteBlob(uri)
|
||||
}
|
||||
|
||||
class Factory(private val repository: DraftRepository) : ViewModelProvider.Factory {
|
||||
|
||||
override fun <T : ViewModel> create(modelClass: Class<T>): T {
|
||||
return requireNotNull(modelClass.cast(DraftViewModel(repository)))
|
||||
}
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
}
|
||||
}
|
||||
|
||||
private fun String.toTextDraft(): Draft? {
|
||||
return if (isNotEmpty()) Draft(Draft.TEXT, this) else null
|
||||
}
|
||||
|
||||
private fun List<Mention>.toMentionsDraft(): Draft? {
|
||||
val mentions: BodyRangeList? = MentionUtil.mentionsToBodyRangeList(this)
|
||||
return if (mentions != null) {
|
||||
Draft(Draft.MENTION, Base64.encodeBytes(mentions.toByteArray()))
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
|
|
@ -164,6 +164,12 @@ public class DraftDatabase extends Database {
|
|||
}
|
||||
|
||||
public static class Drafts extends LinkedList<Draft> {
|
||||
public void addIfNotNull(@Nullable Draft draft) {
|
||||
if (draft != null) {
|
||||
add(draft);
|
||||
}
|
||||
}
|
||||
|
||||
public @Nullable Draft getDraftOfType(String type) {
|
||||
for (Draft draft : this) {
|
||||
if (type.equals(draft.getType())) {
|
||||
|
|
|
@ -489,6 +489,12 @@ public class AttachmentManager {
|
|||
private class RemoveButtonListener implements View.OnClickListener {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
slide.ifPresent(oldSlide -> {
|
||||
if (oldSlide instanceof LocationSlide) {
|
||||
attachmentListener.onLocationRemoved();
|
||||
}
|
||||
});
|
||||
|
||||
cleanup();
|
||||
clear(GlideApp.with(context.getApplicationContext()), true);
|
||||
}
|
||||
|
@ -496,6 +502,7 @@ public class AttachmentManager {
|
|||
|
||||
public interface AttachmentListener {
|
||||
void onAttachmentChanged();
|
||||
void onLocationRemoved();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
Ładowanie…
Reference in New Issue