Implement drafts for voice notes.

This commit is contained in:
Alex Hart
2021-07-02 10:28:45 -03:00
parent 2d7c043398
commit 5826b0c068
29 changed files with 945 additions and 163 deletions

View File

@@ -76,6 +76,8 @@ import androidx.core.content.pm.ShortcutInfoCompat;
import androidx.core.content.pm.ShortcutManagerCompat;
import androidx.core.graphics.drawable.DrawableCompat;
import androidx.core.graphics.drawable.IconCompat;
import androidx.lifecycle.Lifecycle;
import androidx.lifecycle.Observer;
import androidx.lifecycle.ViewModelProviders;
import com.annimon.stream.Collectors;
@@ -130,6 +132,9 @@ import org.thoughtcrime.securesms.components.reminder.ReminderView;
import org.thoughtcrime.securesms.components.reminder.ServiceOutageReminder;
import org.thoughtcrime.securesms.components.reminder.UnauthorizedReminder;
import org.thoughtcrime.securesms.components.settings.conversation.ConversationSettingsActivity;
import org.thoughtcrime.securesms.components.voice.VoiceNoteDraft;
import org.thoughtcrime.securesms.components.voice.VoiceNoteMediaController;
import org.thoughtcrime.securesms.components.voice.VoiceNotePlaybackState;
import org.thoughtcrime.securesms.contacts.ContactAccessor;
import org.thoughtcrime.securesms.contacts.ContactAccessor.ContactData;
import org.thoughtcrime.securesms.contacts.sync.DirectoryHelper;
@@ -139,6 +144,8 @@ 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.error.SafetyNumberChangeDialog;
import org.thoughtcrime.securesms.conversation.ui.groupcall.GroupCallViewModel;
import org.thoughtcrime.securesms.conversation.ui.mentions.MentionsPickerViewModel;
@@ -402,6 +409,9 @@ public class ConversationActivity extends PassphraseRequiredActivity
private MentionsPickerViewModel mentionsViewModel;
private GroupCallViewModel groupCallViewModel;
private VoiceRecorderWakeLock voiceRecorderWakeLock;
private DraftViewModel draftViewModel;
private VoiceNoteMediaController voiceNoteMediaController;
private LiveRecipient recipient;
private long threadId;
@@ -435,7 +445,8 @@ public class ConversationActivity extends PassphraseRequiredActivity
return;
}
voiceRecorderWakeLock = new VoiceRecorderWakeLock(this);
voiceNoteMediaController = new VoiceNoteMediaController(this);
voiceRecorderWakeLock = new VoiceRecorderWakeLock(this);
new FullscreenHelper(this).showSystemUI();
@@ -462,6 +473,7 @@ public class ConversationActivity extends PassphraseRequiredActivity
initializeGroupViewModel();
initializeMentionsViewModel();
initializeGroupCallViewModel();
initializeDraftViewModel();
initializeEnabledCheck();
initializePendingRequestsBanner();
initializeGroupV1MigrationsBanners();
@@ -520,7 +532,7 @@ public class ConversationActivity extends PassphraseRequiredActivity
}
reactWithAnyEmojiStartPage = -1;
if (!Util.isEmpty(composeText) || attachmentManager.isAttachmentPresent() || inputPanel.getQuote().isPresent()) {
if (!Util.isEmpty(composeText) || attachmentManager.isAttachmentPresent() || inputPanel.hasSaveableContent()) {
saveDraft();
attachmentManager.clear(glideRequests, false);
inputPanel.clearQuote();
@@ -627,6 +639,7 @@ public class ConversationActivity extends PassphraseRequiredActivity
@Override
protected void onStop() {
super.onStop();
saveDraft();
EventBus.getDefault().unregister(this);
}
@@ -647,7 +660,6 @@ public class ConversationActivity extends PassphraseRequiredActivity
@Override
protected void onDestroy() {
saveDraft();
if (securityUpdateReceiver != null) unregisterReceiver(securityUpdateReceiver);
super.onDestroy();
}
@@ -1761,6 +1773,9 @@ public class ConversationActivity extends PassphraseRequiredActivity
SettableFuture<Boolean> quoteResult = new SettableFuture<>();
new QuoteRestorationTask(draft.getValue(), quoteResult).execute();
quoteResult.addListener(listener);
case Draft.VOICE_NOTE:
draftViewModel.setVoiceNoteDraft(recipient.getId(), draft);
voiceNoteMediaController.getVoiceNotePlaybackState().observe(ConversationActivity.this, inputPanel.getPlaybackStateObserver());
break;
}
} catch (IOException e) {
@@ -2277,6 +2292,20 @@ public class ConversationActivity extends PassphraseRequiredActivity
groupCallViewModel.groupCallHasCapacity().observe(this, hasCapacity -> joinGroupCallButton.setText(hasCapacity ? R.string.ConversationActivity_join : R.string.ConversationActivity_full));
}
public void initializeDraftViewModel() {
draftViewModel = ViewModelProviders.of(this, new DraftViewModel.Factory(new DraftRepository(getApplicationContext()))).get(DraftViewModel.class);
recipient.observe(this, r -> {
draftViewModel.onRecipientChanged(r);
});
draftViewModel.getState().observe(this,
state -> {
inputPanel.setVoiceNoteDraft(state.getVoiceNoteDraft());
updateToggleButtonState();
});
}
private void showGroupCallingTooltip() {
if (Build.VERSION.SDK_INT == 19 || !SignalStore.tooltips().shouldShowGroupCallingTooltip() || callingTooltipShown) {
return;
@@ -2416,6 +2445,10 @@ public class ConversationActivity extends PassphraseRequiredActivity
groupCallViewModel.onRecipientChange(recipient);
}
if (draftViewModel != null) {
draftViewModel.onRecipientChanged(recipient);
}
if (this.threadId == -1) {
SimpleTask.run(() -> DatabaseFactory.getThreadDatabase(this).getThreadIdIfExistsFor(recipient.getId()), threadId -> {
if (this.threadId != threadId) {
@@ -2562,6 +2595,11 @@ public class ConversationActivity extends PassphraseRequiredActivity
drafts.add(new Draft(Draft.QUOTE, new QuoteId(quote.get().getId(), quote.get().getAuthor()).serialize()));
}
DraftDatabase.Draft voiceNoteDraft = draftViewModel.getVoiceNoteDraft();
if (voiceNoteDraft != null) {
drafts.add(voiceNoteDraft);
}
return drafts;
}
@@ -2573,13 +2611,25 @@ public class ConversationActivity extends PassphraseRequiredActivity
return future;
}
final Drafts drafts = getDraftsForCurrentState();
final long thisThreadId = this.threadId;
final int thisDistributionType = this.distributionType;
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 = DatabaseFactory.getThreadDatabase(ConversationActivity.this);
DraftDatabase draftDatabase = DatabaseFactory.getDraftDatabase(ConversationActivity.this);
long threadId = params[0];
@@ -2587,7 +2637,7 @@ public class ConversationActivity extends PassphraseRequiredActivity
if (drafts.size() > 0) {
if (threadId == -1) threadId = threadDatabase.getThreadIdFor(getRecipient(), thisDistributionType);
draftDatabase.insertDrafts(threadId, drafts);
draftDatabase.replaceDrafts(threadId, drafts);
threadDatabase.updateSnippet(threadId, drafts.getSnippet(ConversationActivity.this),
drafts.getUriSnippet(),
System.currentTimeMillis(), Types.BASE_DRAFT_TYPE, true);
@@ -2761,6 +2811,15 @@ public class ConversationActivity extends PassphraseRequiredActivity
return;
}
Draft voiceNote = draftViewModel.getVoiceNoteDraft();
if (voiceNote != null) {
AudioSlide audioSlide = AudioSlide.createFromVoiceNoteDraft(this, voiceNote);
sendVoiceNote(Objects.requireNonNull(audioSlide.getUri()), audioSlide.getFileSize());
draftViewModel.clearVoiceNoteDraft();
return;
}
try {
Recipient recipient = getRecipient();
@@ -2975,6 +3034,13 @@ public class ConversationActivity extends PassphraseRequiredActivity
return;
}
if (draftViewModel.hasVoiceNoteDraft()) {
buttonToggle.display(sendButton);
quickAttachmentToggle.hide();
inlineAttachmentToggle.hide();
return;
}
if (composeText.getText().length() == 0 && !attachmentManager.isAttachmentPresent()) {
buttonToggle.display(attachButton);
quickAttachmentToggle.show();
@@ -3063,44 +3129,11 @@ public class ConversationActivity extends PassphraseRequiredActivity
getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED);
ListenableFuture<Pair<Uri, Long>> future = audioRecorder.stopRecording();
future.addListener(new ListenableFuture.Listener<Pair<Uri, Long>>() {
ListenableFuture<VoiceNoteDraft> future = audioRecorder.stopRecording();
future.addListener(new ListenableFuture.Listener<VoiceNoteDraft>() {
@Override
public void onSuccess(final @NonNull Pair<Uri, Long> result) {
boolean forceSms = sendButton.isManualSelection() && sendButton.getSelectedTransport().isSms();
boolean initiating = threadId == -1;
int subscriptionId = sendButton.getSelectedTransport().getSimSubscriptionId().or(-1);
long expiresIn = recipient.get().getExpireMessages() * 1000L;
AudioSlide audioSlide = new AudioSlide(ConversationActivity.this, result.first(), result.second(), MediaUtil.AUDIO_AAC, true);
SlideDeck slideDeck = new SlideDeck();
slideDeck.addSlide(audioSlide);
ListenableFuture<Void> sendResult = sendMediaMessage(recipient.getId(),
forceSms,
"",
slideDeck,
inputPanel.getQuote().orNull(),
Collections.emptyList(),
Collections.emptyList(),
composeText.getMentions(),
expiresIn,
false,
subscriptionId,
initiating,
true);
sendResult.addListener(new AssertedSuccessListener<Void>() {
@Override
public void onSuccess(Void nothing) {
new AsyncTask<Void, Void, Void>() {
@Override
protected Void doInBackground(Void... params) {
BlobProvider.getInstance().delete(ConversationActivity.this, result.first());
return null;
}
}.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
}
});
public void onSuccess(final @NonNull VoiceNoteDraft result) {
sendVoiceNote(result.getUri(), result.getSize());
}
@Override
@@ -3120,22 +3153,12 @@ public class ConversationActivity extends PassphraseRequiredActivity
getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED);
ListenableFuture<Pair<Uri, Long>> future = audioRecorder.stopRecording();
future.addListener(new ListenableFuture.Listener<Pair<Uri, Long>>() {
@Override
public void onSuccess(final Pair<Uri, Long> result) {
new AsyncTask<Void, Void, Void>() {
@Override
protected Void doInBackground(Void... params) {
BlobProvider.getInstance().delete(ConversationActivity.this, result.first());
return null;
}
}.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
}
@Override
public void onFailure(ExecutionException e) {}
});
ListenableFuture<VoiceNoteDraft> future = audioRecorder.stopRecording();
if (getLifecycle().getCurrentState().isAtLeast(Lifecycle.State.RESUMED)) {
future.addListener(new DeleteCanceledVoiceNoteListener());
} else {
draftViewModel.setVoiceNoteDraftFuture(future);
}
}
@Override
@@ -3194,6 +3217,37 @@ public class ConversationActivity extends PassphraseRequiredActivity
container.hideAttachedInput(true);
}
private void sendVoiceNote(@NonNull Uri uri, long size) {
boolean forceSms = sendButton.isManualSelection() && sendButton.getSelectedTransport().isSms();
boolean initiating = threadId == -1;
int subscriptionId = sendButton.getSelectedTransport().getSimSubscriptionId().or(-1);
long expiresIn = recipient.get().getExpireMessages() * 1000L;
AudioSlide audioSlide = new AudioSlide(ConversationActivity.this, uri, size, MediaUtil.AUDIO_AAC, true);
SlideDeck slideDeck = new SlideDeck();
slideDeck.addSlide(audioSlide);
ListenableFuture<Void> sendResult = sendMediaMessage(recipient.getId(),
forceSms,
"",
slideDeck,
inputPanel.getQuote().orNull(),
Collections.emptyList(),
Collections.emptyList(),
composeText.getMentions(),
expiresIn,
false,
subscriptionId,
initiating,
true);
sendResult.addListener(new AssertedSuccessListener<Void>() {
@Override
public void onSuccess(Void nothing) {
draftViewModel.deleteBlob(uri);
}
});
}
private void sendSticker(@NonNull StickerRecord stickerRecord, boolean clearCompose) {
sendSticker(new StickerLocator(stickerRecord.getPackId(), stickerRecord.getPackKey(), stickerRecord.getStickerId(), stickerRecord.getEmoji()), stickerRecord.getContentType(), stickerRecord.getUri(), stickerRecord.getSize(), clearCompose);
@@ -3297,8 +3351,39 @@ public class ConversationActivity extends PassphraseRequiredActivity
}
}
@Override
public void onVoiceNoteDraftPlay(@NonNull Uri audioUri, double progress) {
voiceNoteMediaController.startSinglePlaybackForDraft(audioUri, threadId, progress);
}
@Override
public void onVoiceNoteDraftPause(@NonNull Uri audioUri) {
voiceNoteMediaController.pausePlayback(audioUri);
}
@Override
public void onVoiceNoteDraftSeekTo(@NonNull Uri audioUri, double progress) {
voiceNoteMediaController.seekToPosition(audioUri, progress);
}
@Override
public void onVoiceNoteDraftDelete(@NonNull Uri audioUri) {
voiceNoteMediaController.stopPlaybackAndReset(audioUri);
draftViewModel.deleteVoiceNoteDraft();
}
// Listeners
private final class DeleteCanceledVoiceNoteListener implements ListenableFuture.Listener<VoiceNoteDraft> {
@Override
public void onSuccess(final VoiceNoteDraft result) {
draftViewModel.deleteBlob(result.getUri());
}
@Override
public void onFailure(ExecutionException e) {}
}
private class QuickCameraToggleListener implements OnClickListener {
@Override
public void onClick(View v) {
@@ -3563,6 +3648,36 @@ public class ConversationActivity extends PassphraseRequiredActivity
reactionDelegate.showMask(maskTarget, titleView.getMeasuredHeight(), inputAreaHeight());
}
@Override
public void onVoiceNotePause(@NonNull Uri uri) {
voiceNoteMediaController.pausePlayback(uri);
}
@Override
public void onVoiceNotePlay(@NonNull Uri uri, long messageId, double progress) {
voiceNoteMediaController.startConsecutivePlayback(uri, messageId, progress);
}
@Override
public void onVoiceNoteSeekTo(@NonNull Uri uri, double progress) {
voiceNoteMediaController.seekToPosition(uri, progress);
}
@Override
public void onVoiceNotePlaybackSpeedChanged(@NonNull Uri uri, float speed) {
voiceNoteMediaController.setPlaybackSpeed(uri, speed);
}
@Override
public void onRegisterVoiceNoteCallbacks(@NonNull Observer<VoiceNotePlaybackState> onPlaybackStartObserver) {
voiceNoteMediaController.getVoiceNotePlaybackState().observe(this, onPlaybackStartObserver);
}
@Override
public void onUnregisterVoiceNoteCallbacks(@NonNull Observer<VoiceNotePlaybackState> onPlaybackStartObserver) {
voiceNoteMediaController.getVoiceNotePlaybackState().removeObserver(onPlaybackStartObserver);
}
@Override
public void onCursorChanged() {
if (!reactionDelegate.isShowing()) {

View File

@@ -81,7 +81,6 @@ import org.thoughtcrime.securesms.components.TooltipPopup;
import org.thoughtcrime.securesms.components.TypingStatusRepository;
import org.thoughtcrime.securesms.components.recyclerview.SmoothScrollingLinearLayoutManager;
import org.thoughtcrime.securesms.components.settings.app.AppSettingsActivity;
import org.thoughtcrime.securesms.components.voice.VoiceNoteMediaController;
import org.thoughtcrime.securesms.components.voice.VoiceNotePlaybackState;
import org.thoughtcrime.securesms.contactshare.Contact;
import org.thoughtcrime.securesms.contactshare.ContactUtil;
@@ -214,7 +213,6 @@ public class ConversationFragment extends LoggingFragment {
private Animation mentionButtonOutAnimation;
private OnScrollListener conversationScrollListener;
private int pulsePosition = -1;
private VoiceNoteMediaController voiceNoteMediaController;
private View toolbarShadow;
private ColorizerView colorizerView;
private Stopwatch startupStopwatch;
@@ -408,7 +406,6 @@ public class ConversationFragment extends LoggingFragment {
initializeResources();
initializeMessageRequestViewModel();
initializeListAdapter();
voiceNoteMediaController = new VoiceNoteMediaController((AppCompatActivity) requireActivity());
}
@Override
@@ -1305,6 +1302,12 @@ public class ConversationFragment extends LoggingFragment {
void onListVerticalTranslationChanged(float translationY);
void onMessageWithErrorClicked(@NonNull MessageRecord messageRecord);
void handleReactionDetails(@NonNull MaskView.MaskTarget maskTarget);
void onVoiceNotePause(@NonNull Uri uri);
void onVoiceNotePlay(@NonNull Uri uri, long messageId, double progress);
void onVoiceNoteSeekTo(@NonNull Uri uri, double progress);
void onVoiceNotePlaybackSpeedChanged(@NonNull Uri uri, float speed);
void onRegisterVoiceNoteCallbacks(@NonNull Observer<VoiceNotePlaybackState> onPlaybackStartObserver);
void onUnregisterVoiceNoteCallbacks(@NonNull Observer<VoiceNotePlaybackState> onPlaybackStartObserver);
}
private class ConversationScrollListener extends OnScrollListener {
@@ -1581,32 +1584,32 @@ public class ConversationFragment extends LoggingFragment {
@Override
public void onVoiceNotePause(@NonNull Uri uri) {
voiceNoteMediaController.pausePlayback(uri);
listener.onVoiceNotePause(uri);
}
@Override
public void onVoiceNotePlay(@NonNull Uri uri, long messageId, double progress) {
voiceNoteMediaController.startConsecutivePlayback(uri, messageId, progress);
listener.onVoiceNotePlay(uri, messageId, progress);
}
@Override
public void onVoiceNoteSeekTo(@NonNull Uri uri, double progress) {
voiceNoteMediaController.seekToPosition(uri, progress);
listener.onVoiceNoteSeekTo(uri, progress);
}
@Override
public void onVoiceNotePlaybackSpeedChanged(@NonNull Uri uri, float speed) {
voiceNoteMediaController.setPlaybackSpeed(uri, speed);
listener.onVoiceNotePlaybackSpeedChanged(uri, speed);
}
@Override
public void onRegisterVoiceNoteCallbacks(@NonNull Observer<VoiceNotePlaybackState> onPlaybackStartObserver) {
voiceNoteMediaController.getVoiceNotePlaybackState().observe(getViewLifecycleOwner(), onPlaybackStartObserver);
listener.onRegisterVoiceNoteCallbacks(onPlaybackStartObserver);
}
@Override
public void onUnregisterVoiceNoteCallbacks(@NonNull Observer<VoiceNotePlaybackState> onPlaybackStartObserver) {
voiceNoteMediaController.getVoiceNotePlaybackState().removeObserver(onPlaybackStartObserver);
listener.onUnregisterVoiceNoteCallbacks(onPlaybackStartObserver);
}
@Override

View File

@@ -0,0 +1,91 @@
package org.thoughtcrime.securesms.conversation
import android.content.Context
import android.net.Uri
import android.util.AttributeSet
import android.view.View
import androidx.appcompat.widget.LinearLayoutCompat
import androidx.lifecycle.Observer
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.components.AudioView
import org.thoughtcrime.securesms.components.voice.VoiceNotePlaybackState
import org.thoughtcrime.securesms.database.DraftDatabase
import org.thoughtcrime.securesms.mms.AudioSlide
class VoiceNoteDraftView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : LinearLayoutCompat(context, attrs, defStyleAttr) {
var listener: Listener? = null
var draft: DraftDatabase.Draft? = null
private set
private lateinit var audioView: AudioView
val playbackStateObserver: Observer<VoiceNotePlaybackState>
get() = audioView.playbackStateObserver
init {
inflate(context, R.layout.voice_note_draft_view, this)
val delete: View = findViewById(R.id.voice_note_draft_delete)
delete.setOnClickListener {
if (draft != null) {
val uri = audioView.audioSlideUri
if (uri != null) {
listener?.onVoiceNoteDraftDelete(uri)
}
}
}
audioView = findViewById(R.id.voice_note_audio_view)
}
fun clearDraft() {
this.draft = null
}
fun setDraft(draft: DraftDatabase.Draft) {
audioView.setAudio(
AudioSlide.createFromVoiceNoteDraft(context, draft),
AudioViewCallbacksAdapter(),
true,
false
)
this.draft = draft
}
private inner class AudioViewCallbacksAdapter : AudioView.Callbacks {
override fun onPlay(audioUri: Uri, progress: Double) {
listener?.onVoiceNoteDraftPlay(audioUri, progress)
}
override fun onPause(audioUri: Uri) {
listener?.onVoiceNoteDraftPause(audioUri)
}
override fun onSeekTo(audioUri: Uri, progress: Double) {
listener?.onVoiceNoteDraftSeekTo(audioUri, progress)
}
override fun onStopAndReset(audioUri: Uri) {
throw UnsupportedOperationException()
}
override fun onProgressUpdated(durationMillis: Long, playheadMillis: Long) = Unit
override fun onSpeedChanged(speed: Float, isPlaying: Boolean) = Unit
}
interface Listener {
fun onVoiceNoteDraftPlay(audioUri: Uri, progress: Double)
fun onVoiceNoteDraftPause(audioUri: Uri)
fun onVoiceNoteDraftSeekTo(audioUri: Uri, progress: Double)
fun onVoiceNoteDraftDelete(audioUri: Uri)
}
}

View File

@@ -0,0 +1,19 @@
package org.thoughtcrime.securesms.conversation.drafts
import android.content.Context
import android.net.Uri
import org.signal.core.util.concurrent.SignalExecutors
import org.thoughtcrime.securesms.database.DraftDatabase
import org.thoughtcrime.securesms.providers.BlobProvider
class DraftRepository(private val context: Context) {
fun deleteVoiceNoteDraft(draft: DraftDatabase.Draft) {
deleteBlob(Uri.parse(draft.value).buildUpon().clearQuery().build())
}
fun deleteBlob(uri: Uri) {
SignalExecutors.BOUNDED.execute {
BlobProvider.getInstance().delete(context, uri)
}
}
}

View File

@@ -0,0 +1,15 @@
package org.thoughtcrime.securesms.conversation.drafts
import org.thoughtcrime.securesms.database.DraftDatabase
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.recipients.RecipientId
/**
* State object 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.
*/
data class DraftState(
val recipientId: RecipientId = Recipient.UNKNOWN.id,
val voiceNoteDraft: DraftDatabase.Draft? = null
)

View File

@@ -0,0 +1,87 @@
package org.thoughtcrime.securesms.conversation.drafts
import android.net.Uri
import androidx.lifecycle.LiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import org.thoughtcrime.securesms.components.voice.VoiceNoteDraft
import org.thoughtcrime.securesms.database.DraftDatabase
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.recipients.RecipientId
import org.thoughtcrime.securesms.util.concurrent.ListenableFuture
import org.thoughtcrime.securesms.util.livedata.Store
/**
* 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
) : ViewModel() {
private val store = Store<DraftState>(DraftState())
val state: LiveData<DraftState> = store.stateLiveData
private var voiceNoteDraftFuture: ListenableFuture<VoiceNoteDraft>? = null
val voiceNoteDraft: DraftDatabase.Draft?
get() = store.state.voiceNoteDraft
fun consumeVoiceNoteDraftFuture(): ListenableFuture<VoiceNoteDraft>? {
val future = voiceNoteDraftFuture
voiceNoteDraftFuture = null
return future
}
fun setVoiceNoteDraftFuture(voiceNoteDraftFuture: ListenableFuture<VoiceNoteDraft>) {
this.voiceNoteDraftFuture = voiceNoteDraftFuture
}
fun setVoiceNoteDraft(recipientId: RecipientId, draft: DraftDatabase.Draft) {
store.update {
it.copy(recipientId = recipientId, voiceNoteDraft = draft)
}
}
@get:JvmName("hasVoiceNoteDraft")
val hasVoiceNoteDraft: Boolean
get() = store.state.voiceNoteDraft != null
fun clearVoiceNoteDraft() {
store.update {
it.copy(voiceNoteDraft = null)
}
}
fun deleteVoiceNoteDraft() {
val draft = store.state.voiceNoteDraft
if (draft != null) {
clearVoiceNoteDraft()
repository.deleteVoiceNoteDraft(draft)
}
}
fun onRecipientChanged(recipient: Recipient) {
store.update {
if (recipient.id != it.recipientId) {
it.copy(recipientId = recipient.id, voiceNoteDraft = null)
} else {
it
}
}
}
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)))
}
}
}