kopia lustrzana https://github.com/ryukoposting/Signal-Android
Implement story ring support.
rodzic
fe088c39c7
commit
2d7655a6bb
|
@ -6,6 +6,7 @@ import android.view.View
|
|||
import android.widget.FrameLayout
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.components.AvatarImageView
|
||||
import org.thoughtcrime.securesms.database.model.StoryViewState
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.mms.GlideRequests
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
|
@ -46,6 +47,14 @@ class AvatarView @JvmOverloads constructor(
|
|||
avatar.scaleY = 1f
|
||||
}
|
||||
|
||||
fun setStoryRingFromState(storyViewState: StoryViewState) {
|
||||
when (storyViewState) {
|
||||
StoryViewState.NONE -> hideStoryRing()
|
||||
StoryViewState.UNVIEWED -> showStoryRing(true)
|
||||
StoryViewState.VIEWED -> showStoryRing(false)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays Note-to-Self
|
||||
*/
|
||||
|
|
|
@ -266,6 +266,7 @@ class ConversationSettingsFragment : DSLSettingsFragment(
|
|||
customPref(
|
||||
AvatarPreference.Model(
|
||||
recipient = state.recipient,
|
||||
storyViewState = state.storyViewState,
|
||||
onAvatarClick = { avatar ->
|
||||
if (!state.recipient.isSelf) {
|
||||
// startActivity(StoryViewerActivity.createIntent(requireContext(), state.recipient.id))
|
||||
|
|
|
@ -4,6 +4,8 @@ import android.content.Context
|
|||
import android.database.Cursor
|
||||
import androidx.annotation.WorkerThread
|
||||
import androidx.lifecycle.LiveData
|
||||
import io.reactivex.rxjava3.core.Observable
|
||||
import io.reactivex.rxjava3.schedulers.Schedulers
|
||||
import org.signal.core.util.concurrent.SignalExecutors
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.signal.storageservice.protos.groups.local.DecryptedGroup
|
||||
|
@ -13,6 +15,7 @@ import org.thoughtcrime.securesms.database.GroupDatabase
|
|||
import org.thoughtcrime.securesms.database.MediaDatabase
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||
import org.thoughtcrime.securesms.database.model.IdentityRecord
|
||||
import org.thoughtcrime.securesms.database.model.StoryViewState
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
|
||||
import org.thoughtcrime.securesms.groups.GroupId
|
||||
import org.thoughtcrime.securesms.groups.GroupManager
|
||||
|
@ -44,6 +47,14 @@ class ConversationSettingsRepository(
|
|||
}
|
||||
}
|
||||
|
||||
fun getStoryViewState(groupId: GroupId): Observable<StoryViewState> {
|
||||
return Observable.fromCallable {
|
||||
SignalDatabase.recipients.getByGroupId(groupId)
|
||||
}.flatMap {
|
||||
StoryViewState.getForRecipientId(it.get())
|
||||
}.observeOn(Schedulers.io())
|
||||
}
|
||||
|
||||
fun getThreadId(recipientId: RecipientId, consumer: (Long) -> Unit) {
|
||||
SignalExecutors.BOUNDED.execute {
|
||||
consumer(SignalDatabase.threads.getThreadIdIfExistsFor(recipientId))
|
||||
|
|
|
@ -4,12 +4,14 @@ import android.database.Cursor
|
|||
import org.thoughtcrime.securesms.components.settings.conversation.preferences.ButtonStripPreference
|
||||
import org.thoughtcrime.securesms.components.settings.conversation.preferences.LegacyGroupPreference
|
||||
import org.thoughtcrime.securesms.database.model.IdentityRecord
|
||||
import org.thoughtcrime.securesms.database.model.StoryViewState
|
||||
import org.thoughtcrime.securesms.groups.GroupId
|
||||
import org.thoughtcrime.securesms.groups.ui.GroupMemberEntry
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
|
||||
data class ConversationSettingsState(
|
||||
val threadId: Long = -1,
|
||||
val storyViewState: StoryViewState = StoryViewState.NONE,
|
||||
val recipient: Recipient = Recipient.UNKNOWN,
|
||||
val buttonStripState: ButtonStripPreference.State = ButtonStripPreference.State(),
|
||||
val disappearingMessagesLifespan: Int = 0,
|
||||
|
|
|
@ -6,12 +6,15 @@ import androidx.lifecycle.MutableLiveData
|
|||
import androidx.lifecycle.Transformations
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import io.reactivex.rxjava3.disposables.CompositeDisposable
|
||||
import io.reactivex.rxjava3.kotlin.plusAssign
|
||||
import org.signal.core.util.ThreadUtil
|
||||
import org.signal.core.util.concurrent.SignalExecutors
|
||||
import org.thoughtcrime.securesms.components.settings.conversation.preferences.ButtonStripPreference
|
||||
import org.thoughtcrime.securesms.components.settings.conversation.preferences.LegacyGroupPreference
|
||||
import org.thoughtcrime.securesms.database.AttachmentDatabase
|
||||
import org.thoughtcrime.securesms.database.RecipientDatabase
|
||||
import org.thoughtcrime.securesms.database.model.StoryViewState
|
||||
import org.thoughtcrime.securesms.groups.GroupId
|
||||
import org.thoughtcrime.securesms.groups.LiveGroup
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
|
@ -46,6 +49,8 @@ sealed class ConversationSettingsViewModel(
|
|||
val state: LiveData<ConversationSettingsState> = store.stateLiveData
|
||||
val events: LiveData<ConversationSettingsEvent> = internalEvents
|
||||
|
||||
protected val disposable = CompositeDisposable()
|
||||
|
||||
init {
|
||||
val threadId: LiveData<Long> = Transformations.distinctUntilChanged(Transformations.map(state) { it.threadId })
|
||||
val updater: LiveData<Long> = LiveDataUtil.combineLatest(threadId, sharedMediaUpdateTrigger) { tId, _ -> tId }
|
||||
|
@ -105,6 +110,7 @@ sealed class ConversationSettingsViewModel(
|
|||
cleared = true
|
||||
openedMediaCursors.forEach { it.ensureClosed() }
|
||||
store.clear()
|
||||
disposable.clear()
|
||||
}
|
||||
|
||||
private fun Cursor?.ensureClosed() {
|
||||
|
@ -126,6 +132,10 @@ sealed class ConversationSettingsViewModel(
|
|||
private val liveRecipient = Recipient.live(recipientId)
|
||||
|
||||
init {
|
||||
disposable += StoryViewState.getForRecipientId(recipientId).subscribe { storyViewState ->
|
||||
store.update { it.copy(storyViewState = storyViewState) }
|
||||
}
|
||||
|
||||
store.update(liveRecipient.liveData) { recipient, state ->
|
||||
state.copy(
|
||||
recipient = recipient,
|
||||
|
@ -240,6 +250,10 @@ sealed class ConversationSettingsViewModel(
|
|||
private val liveGroup = LiveGroup(groupId)
|
||||
|
||||
init {
|
||||
disposable += repository.getStoryViewState(groupId).subscribe { storyViewState ->
|
||||
store.update { it.copy(storyViewState = storyViewState) }
|
||||
}
|
||||
|
||||
val recipientAndIsActive = LiveDataUtil.combineLatest(liveGroup.groupRecipient, liveGroup.isActive) { r, a -> r to a }
|
||||
store.update(recipientAndIsActive) { (recipient, isActive), state ->
|
||||
state.copy(
|
||||
|
|
|
@ -9,6 +9,7 @@ import org.thoughtcrime.securesms.badges.models.Badge
|
|||
import org.thoughtcrime.securesms.components.settings.PreferenceModel
|
||||
import org.thoughtcrime.securesms.contacts.avatars.FallbackContactPhoto
|
||||
import org.thoughtcrime.securesms.contacts.avatars.FallbackPhoto
|
||||
import org.thoughtcrime.securesms.database.model.StoryViewState
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.util.ViewUtil
|
||||
import org.thoughtcrime.securesms.util.adapter.mapping.LayoutFactory
|
||||
|
@ -26,6 +27,7 @@ object AvatarPreference {
|
|||
|
||||
class Model(
|
||||
val recipient: Recipient,
|
||||
val storyViewState: StoryViewState,
|
||||
val onAvatarClick: (View) -> Unit,
|
||||
val onBadgeClick: (Badge) -> Unit
|
||||
) : PreferenceModel<Model>() {
|
||||
|
@ -63,6 +65,7 @@ object AvatarPreference {
|
|||
}
|
||||
}
|
||||
|
||||
avatar.setStoryRingFromState(model.storyViewState)
|
||||
avatar.displayChatAvatar(model.recipient)
|
||||
avatar.disableQuickContact()
|
||||
avatar.setOnClickListener { model.onAvatarClick(avatar) }
|
||||
|
|
|
@ -553,6 +553,8 @@ public class ConversationParentFragment extends Fragment
|
|||
initializeInsightObserver();
|
||||
initializeActionBar();
|
||||
|
||||
viewModel.getStoryViewState(getViewLifecycleOwner()).observe(getViewLifecycleOwner(), titleView::setStoryRingFromState);
|
||||
|
||||
requireActivity().getOnBackPressedDispatcher().addCallback(getViewLifecycleOwner(), new OnBackPressedCallback(true) {
|
||||
@Override
|
||||
public void handleOnBackPressed() {
|
||||
|
|
|
@ -20,6 +20,7 @@ import org.thoughtcrime.securesms.R;
|
|||
import org.thoughtcrime.securesms.avatar.view.AvatarView;
|
||||
import org.thoughtcrime.securesms.badges.BadgeImageView;
|
||||
import org.thoughtcrime.securesms.components.AvatarImageView;
|
||||
import org.thoughtcrime.securesms.database.model.StoryViewState;
|
||||
import org.thoughtcrime.securesms.mms.GlideRequests;
|
||||
import org.thoughtcrime.securesms.recipients.LiveRecipient;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
|
@ -124,6 +125,10 @@ public class ConversationTitleView extends RelativeLayout {
|
|||
updateVerifiedSubtitleVisibility();
|
||||
}
|
||||
|
||||
public void setStoryRingFromState(@NonNull StoryViewState storyViewState) {
|
||||
avatar.setStoryRingFromState(storyViewState);
|
||||
}
|
||||
|
||||
public void setVerified(boolean verified) {
|
||||
this.verified.setVisibility(verified ? View.VISIBLE : View.GONE);
|
||||
|
||||
|
|
|
@ -5,6 +5,7 @@ import android.app.Application;
|
|||
import androidx.annotation.MainThread;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.lifecycle.LifecycleOwner;
|
||||
import androidx.lifecycle.LiveData;
|
||||
import androidx.lifecycle.LiveDataReactiveStreams;
|
||||
import androidx.lifecycle.MutableLiveData;
|
||||
|
@ -18,6 +19,7 @@ import com.annimon.stream.Stream;
|
|||
import org.greenrobot.eventbus.EventBus;
|
||||
import org.greenrobot.eventbus.Subscribe;
|
||||
import org.greenrobot.eventbus.ThreadMode;
|
||||
import org.reactivestreams.Publisher;
|
||||
import org.signal.core.util.MapUtil;
|
||||
import org.signal.core.util.logging.Log;
|
||||
import org.signal.paging.PagedData;
|
||||
|
@ -30,6 +32,7 @@ import org.thoughtcrime.securesms.conversation.colors.ChatColorsPalette;
|
|||
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.groups.GroupId;
|
||||
import org.thoughtcrime.securesms.groups.LiveGroup;
|
||||
|
@ -61,6 +64,7 @@ import java.util.Set;
|
|||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import io.reactivex.rxjava3.core.BackpressureStrategy;
|
||||
import io.reactivex.rxjava3.core.Flowable;
|
||||
import io.reactivex.rxjava3.core.Observable;
|
||||
|
||||
public class ConversationViewModel extends ViewModel {
|
||||
|
@ -198,6 +202,14 @@ public class ConversationViewModel extends ViewModel {
|
|||
threadAnimationStateStore.getStateLiveData().observeForever(threadAnimationStateStoreDriver);
|
||||
}
|
||||
|
||||
LiveData<StoryViewState> getStoryViewState(@NonNull LifecycleOwner lifecycle) {
|
||||
Publisher<RecipientId> recipientIdPublisher = LiveDataReactiveStreams.toPublisher(lifecycle, recipientId);
|
||||
Flowable<StoryViewState> storyViewState = Flowable.fromPublisher(recipientIdPublisher)
|
||||
.flatMap(id -> StoryViewState.getForRecipientId(id).toFlowable(BackpressureStrategy.LATEST));
|
||||
|
||||
return LiveDataReactiveStreams.fromPublisher(storyViewState);
|
||||
}
|
||||
|
||||
void onMessagesCommitted(@NonNull List<ConversationMessage> conversationMessages) {
|
||||
if (Util.hasItems(conversationMessages)) {
|
||||
threadAnimationStateStore.update(state -> {
|
||||
|
|
|
@ -20,6 +20,7 @@ import org.thoughtcrime.securesms.database.documents.NetworkFailure;
|
|||
import org.thoughtcrime.securesms.database.model.MessageId;
|
||||
import org.thoughtcrime.securesms.database.model.MessageRecord;
|
||||
import org.thoughtcrime.securesms.database.model.SmsMessageRecord;
|
||||
import org.thoughtcrime.securesms.database.model.StoryViewState;
|
||||
import org.thoughtcrime.securesms.groups.GroupMigrationMembershipChange;
|
||||
import org.thoughtcrime.securesms.insights.InsightsConstants;
|
||||
import org.thoughtcrime.securesms.mms.IncomingMediaMessage;
|
||||
|
@ -193,6 +194,7 @@ public abstract class MessageDatabase extends Database implements MmsSmsColumns
|
|||
public abstract long getUnreadStoryCount();
|
||||
public abstract @Nullable Long getOldestStorySendTimestamp();
|
||||
public abstract int deleteStoriesOlderThan(long timestamp);
|
||||
public abstract @NonNull StoryViewState getStoryViewState(@NonNull RecipientId recipientId);
|
||||
|
||||
final @NonNull String getOutgoingTypeClause() {
|
||||
List<String> segments = new ArrayList<>(Types.OUTGOING_MESSAGE_TYPES.length);
|
||||
|
|
|
@ -23,6 +23,7 @@ import android.text.TextUtils;
|
|||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.VisibleForTesting;
|
||||
|
||||
import com.annimon.stream.Collectors;
|
||||
import com.annimon.stream.Stream;
|
||||
|
@ -52,6 +53,7 @@ import org.thoughtcrime.securesms.database.model.MessageRecord;
|
|||
import org.thoughtcrime.securesms.database.model.NotificationMmsMessageRecord;
|
||||
import org.thoughtcrime.securesms.database.model.Quote;
|
||||
import org.thoughtcrime.securesms.database.model.SmsMessageRecord;
|
||||
import org.thoughtcrime.securesms.database.model.StoryViewState;
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.BodyRangeList;
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.groups.GroupMigrationMembershipChange;
|
||||
|
@ -74,6 +76,7 @@ import org.thoughtcrime.securesms.revealable.ViewOnceUtil;
|
|||
import org.thoughtcrime.securesms.sms.IncomingTextMessage;
|
||||
import org.thoughtcrime.securesms.sms.OutgoingTextMessage;
|
||||
import org.thoughtcrime.securesms.util.CursorUtil;
|
||||
import org.thoughtcrime.securesms.util.FeatureFlags;
|
||||
import org.thoughtcrime.securesms.util.JsonUtils;
|
||||
import org.thoughtcrime.securesms.util.MediaUtil;
|
||||
import org.thoughtcrime.securesms.util.SqlUtil;
|
||||
|
@ -591,6 +594,46 @@ public class MmsDatabase extends MessageDatabase {
|
|||
return new Reader(cursor);
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull StoryViewState getStoryViewState(@NonNull RecipientId recipientId) {
|
||||
if (!FeatureFlags.stories() || SignalStore.storyValues().isFeatureDisabled()) {
|
||||
return StoryViewState.NONE;
|
||||
}
|
||||
|
||||
long threadId = SignalDatabase.threads().getThreadIdIfExistsFor(recipientId);
|
||||
|
||||
return getStoryViewState(threadId);
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
@NonNull StoryViewState getStoryViewState(long threadId) {
|
||||
final String hasStoryQuery = "SELECT EXISTS(SELECT 1 FROM " + TABLE_NAME + " WHERE " + IS_STORY_CLAUSE + " AND " + THREAD_ID_WHERE + " LIMIT 1)";
|
||||
final String[] hasStoryArgs = SqlUtil.buildArgs(1, 0, threadId);
|
||||
final boolean hasStories;
|
||||
|
||||
try (Cursor cursor = getReadableDatabase().rawQuery(hasStoryQuery, hasStoryArgs)) {
|
||||
hasStories = cursor != null && cursor.moveToFirst() && !cursor.isNull(0) && cursor.getInt(0) == 1;
|
||||
}
|
||||
|
||||
if (!hasStories) {
|
||||
return StoryViewState.NONE;
|
||||
}
|
||||
|
||||
final String hasUnviewedStoriesQuery = "SELECT EXISTS(SELECT 1 FROM " + TABLE_NAME + " WHERE " + IS_STORY_CLAUSE + " AND " + THREAD_ID_WHERE + " AND " + VIEWED_RECEIPT_COUNT + " = ? " + "AND NOT (" + getOutgoingTypeClause() + ") LIMIT 1)";
|
||||
final String[] hasUnviewedStoriesArgs = SqlUtil.buildArgs(1, 0, threadId, 0);
|
||||
final boolean hasUnviewedStories;
|
||||
|
||||
try (Cursor cursor = getReadableDatabase().rawQuery(hasUnviewedStoriesQuery, hasUnviewedStoriesArgs)) {
|
||||
hasUnviewedStories = cursor != null && cursor.moveToFirst() && !cursor.isNull(0) && cursor.getInt(0) == 1;
|
||||
}
|
||||
|
||||
if (hasUnviewedStories) {
|
||||
return StoryViewState.UNVIEWED;
|
||||
} else {
|
||||
return StoryViewState.VIEWED;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull MessageId getStoryId(@NonNull RecipientId authorId, long sentTimestamp) throws NoSuchMessageException {
|
||||
SQLiteDatabase database = databaseHelper.getSignalReadableDatabase();
|
||||
|
|
|
@ -39,6 +39,7 @@ import org.thoughtcrime.securesms.database.model.GroupCallUpdateDetailsUtil;
|
|||
import org.thoughtcrime.securesms.database.model.MessageId;
|
||||
import org.thoughtcrime.securesms.database.model.MessageRecord;
|
||||
import org.thoughtcrime.securesms.database.model.SmsMessageRecord;
|
||||
import org.thoughtcrime.securesms.database.model.StoryViewState;
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.GroupCallUpdateDetails;
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.ProfileChangeDetails;
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
|
@ -1431,6 +1432,11 @@ public class SmsDatabase extends MessageDatabase {
|
|||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull StoryViewState getStoryViewState(@NonNull RecipientId recipientId) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int deleteStoriesOlderThan(long timestamp) {
|
||||
throw new UnsupportedOperationException();
|
||||
|
|
|
@ -0,0 +1,39 @@
|
|||
package org.thoughtcrime.securesms.database.model
|
||||
|
||||
import io.reactivex.rxjava3.core.Observable
|
||||
import io.reactivex.rxjava3.schedulers.Schedulers
|
||||
import org.thoughtcrime.securesms.database.DatabaseObserver
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
|
||||
/**
|
||||
* Denotes whether a given recipient has stories, and whether those stories are viewed or unviewed.
|
||||
*/
|
||||
enum class StoryViewState {
|
||||
NONE,
|
||||
UNVIEWED,
|
||||
VIEWED;
|
||||
|
||||
companion object {
|
||||
@JvmStatic
|
||||
fun getForRecipientId(recipientId: RecipientId): Observable<StoryViewState> {
|
||||
return Observable.create<StoryViewState> { emitter ->
|
||||
fun refresh() {
|
||||
emitter.onNext(SignalDatabase.mms.getStoryViewState(recipientId))
|
||||
}
|
||||
|
||||
val storyObserver = DatabaseObserver.Observer {
|
||||
refresh()
|
||||
}
|
||||
|
||||
ApplicationDependencies.getDatabaseObserver().registerStoryObserver(recipientId, storyObserver)
|
||||
emitter.setCancellable {
|
||||
ApplicationDependencies.getDatabaseObserver().unregisterObserver(storyObserver)
|
||||
}
|
||||
|
||||
refresh()
|
||||
}.observeOn(Schedulers.io())
|
||||
}
|
||||
}
|
||||
}
|
|
@ -3,10 +3,8 @@ package org.thoughtcrime.securesms.recipients.ui.bottomsheet;
|
|||
import android.app.Activity;
|
||||
import android.content.ActivityNotFoundException;
|
||||
import android.content.Intent;
|
||||
import android.content.res.ColorStateList;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.os.Bundle;
|
||||
import android.text.SpannableString;
|
||||
import android.text.SpannableStringBuilder;
|
||||
import android.text.TextUtils;
|
||||
import android.view.LayoutInflater;
|
||||
|
@ -20,10 +18,9 @@ import android.widget.Toast;
|
|||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import androidx.core.widget.TextViewCompat;
|
||||
import androidx.fragment.app.DialogFragment;
|
||||
import androidx.fragment.app.FragmentManager;
|
||||
import androidx.lifecycle.ViewModelProviders;
|
||||
import androidx.lifecycle.ViewModelProvider;
|
||||
|
||||
import com.google.android.material.bottomsheet.BottomSheetDialogFragment;
|
||||
|
||||
|
@ -32,7 +29,6 @@ import org.thoughtcrime.securesms.R;
|
|||
import org.thoughtcrime.securesms.avatar.view.AvatarView;
|
||||
import org.thoughtcrime.securesms.badges.BadgeImageView;
|
||||
import org.thoughtcrime.securesms.badges.view.ViewBadgeBottomSheetDialogFragment;
|
||||
import org.thoughtcrime.securesms.components.AvatarImageView;
|
||||
import org.thoughtcrime.securesms.components.settings.DSLSettingsIcon;
|
||||
import org.thoughtcrime.securesms.components.settings.conversation.preferences.ButtonStripPreference;
|
||||
import org.thoughtcrime.securesms.contacts.avatars.FallbackContactPhoto;
|
||||
|
@ -50,7 +46,6 @@ import org.thoughtcrime.securesms.util.ServiceUtil;
|
|||
import org.thoughtcrime.securesms.util.SpanUtil;
|
||||
import org.thoughtcrime.securesms.util.ThemeUtil;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
import org.thoughtcrime.securesms.util.ViewUtil;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
|
@ -150,7 +145,11 @@ public final class RecipientBottomSheetDialogFragment extends BottomSheetDialogF
|
|||
|
||||
RecipientDialogViewModel.Factory factory = new RecipientDialogViewModel.Factory(requireContext().getApplicationContext(), recipientId, groupId);
|
||||
|
||||
viewModel = ViewModelProviders.of(this, factory).get(RecipientDialogViewModel.class);
|
||||
viewModel = new ViewModelProvider(this, factory).get(RecipientDialogViewModel.class);
|
||||
|
||||
viewModel.getStoryViewState().observe(getViewLifecycleOwner(), state -> {
|
||||
avatar.setStoryRingFromState(state);
|
||||
});
|
||||
|
||||
viewModel.getRecipient().observe(getViewLifecycleOwner(), recipient -> {
|
||||
interactionsContainer.setVisibility(recipient.isSelf() ? View.GONE : View.VISIBLE);
|
||||
|
|
|
@ -18,10 +18,10 @@ import androidx.lifecycle.ViewModelProvider;
|
|||
import org.signal.core.util.ThreadUtil;
|
||||
import org.thoughtcrime.securesms.BlockUnblockDialog;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.verify.VerifyIdentityActivity;
|
||||
import org.thoughtcrime.securesms.components.settings.conversation.ConversationSettingsActivity;
|
||||
import org.thoughtcrime.securesms.database.GroupDatabase;
|
||||
import org.thoughtcrime.securesms.database.model.IdentityRecord;
|
||||
import org.thoughtcrime.securesms.database.model.StoryViewState;
|
||||
import org.thoughtcrime.securesms.groups.GroupId;
|
||||
import org.thoughtcrime.securesms.groups.LiveGroup;
|
||||
import org.thoughtcrime.securesms.groups.ui.GroupChangeFailureReason;
|
||||
|
@ -32,9 +32,13 @@ import org.thoughtcrime.securesms.recipients.RecipientId;
|
|||
import org.thoughtcrime.securesms.recipients.RecipientUtil;
|
||||
import org.thoughtcrime.securesms.util.CommunicationActions;
|
||||
import org.thoughtcrime.securesms.util.livedata.LiveDataUtil;
|
||||
import org.thoughtcrime.securesms.verify.VerifyIdentityActivity;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
import io.reactivex.rxjava3.disposables.CompositeDisposable;
|
||||
import io.reactivex.rxjava3.disposables.Disposable;
|
||||
|
||||
final class RecipientDialogViewModel extends ViewModel {
|
||||
|
||||
private final Context context;
|
||||
|
@ -44,6 +48,8 @@ final class RecipientDialogViewModel extends ViewModel {
|
|||
private final LiveData<AdminActionStatus> adminActionStatus;
|
||||
private final LiveData<Boolean> canAddToAGroup;
|
||||
private final MutableLiveData<Boolean> adminActionBusy;
|
||||
private final MutableLiveData<StoryViewState> storyViewState;
|
||||
private final CompositeDisposable disposables;
|
||||
|
||||
private RecipientDialogViewModel(@NonNull Context context,
|
||||
@NonNull RecipientDialogRepository recipientDialogRepository)
|
||||
|
@ -52,6 +58,8 @@ final class RecipientDialogViewModel extends ViewModel {
|
|||
this.recipientDialogRepository = recipientDialogRepository;
|
||||
this.identity = new MutableLiveData<>();
|
||||
this.adminActionBusy = new MutableLiveData<>(false);
|
||||
this.storyViewState = new MutableLiveData<>();
|
||||
this.disposables = new CompositeDisposable();
|
||||
|
||||
boolean recipientIsSelf = recipientDialogRepository.getRecipientId().equals(Recipient.self().getId());
|
||||
|
||||
|
@ -87,6 +95,20 @@ final class RecipientDialogViewModel extends ViewModel {
|
|||
(r, count) -> count > 0 && r.isRegistered() && !r.isGroup() && !r.isSelf() && !r.isBlocked());
|
||||
|
||||
recipientDialogRepository.getActiveGroupCount(localGroupCount::postValue);
|
||||
|
||||
Disposable storyViewStateDisposable = StoryViewState.getForRecipientId(recipientDialogRepository.getRecipientId())
|
||||
.subscribe(storyViewState::postValue);
|
||||
|
||||
disposables.add(storyViewStateDisposable);
|
||||
}
|
||||
|
||||
@Override protected void onCleared() {
|
||||
super.onCleared();
|
||||
disposables.clear();
|
||||
}
|
||||
|
||||
LiveData<StoryViewState> getStoryViewState() {
|
||||
return storyViewState;
|
||||
}
|
||||
|
||||
LiveData<Recipient> getRecipient() {
|
||||
|
|
|
@ -79,7 +79,7 @@ object StoriesLandingItem {
|
|||
|
||||
val record = model.data.primaryStory.messageRecord as MediaMmsMessageRecord
|
||||
|
||||
avatarView.showStoryRing(model.data.hasUnreadStory)
|
||||
avatarView.setStoryRingFromState(model.data.storyViewState)
|
||||
storyPreview.setImageResource(GlideApp.with(storyPreview), record.slideDeck.thumbnailSlide!!, false, true)
|
||||
|
||||
if (model.data.secondaryStory != null) {
|
||||
|
|
|
@ -1,13 +1,14 @@
|
|||
package org.thoughtcrime.securesms.stories.landing
|
||||
|
||||
import org.thoughtcrime.securesms.conversation.ConversationMessage
|
||||
import org.thoughtcrime.securesms.database.model.StoryViewState
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
|
||||
/**
|
||||
* Data required by each row of the Stories Landing Page for proper rendering.
|
||||
*/
|
||||
data class StoriesLandingItemData(
|
||||
val hasUnreadStory: Boolean,
|
||||
val storyViewState: StoryViewState,
|
||||
val hasReplies: Boolean,
|
||||
val hasRepliesFromSelf: Boolean,
|
||||
val isHidden: Boolean,
|
||||
|
|
|
@ -9,6 +9,7 @@ import org.thoughtcrime.securesms.database.DatabaseObserver
|
|||
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||
import org.thoughtcrime.securesms.database.model.DistributionListId
|
||||
import org.thoughtcrime.securesms.database.model.MessageRecord
|
||||
import org.thoughtcrime.securesms.database.model.StoryViewState
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.recipients.RecipientForeverObserver
|
||||
|
@ -66,7 +67,7 @@ class StoriesLandingRepository(context: Context) {
|
|||
fun refresh() {
|
||||
val itemData = StoriesLandingItemData(
|
||||
storyRecipient = sender,
|
||||
hasUnreadStory = messageRecords.any { it.viewedReceiptCount == 0 && !it.isOutgoing },
|
||||
storyViewState = getStoryViewState(messageRecords),
|
||||
hasReplies = messageRecords.any { SignalDatabase.mms.getNumberOfStoryReplies(it.id) > 0 },
|
||||
hasRepliesFromSelf = messageRecords.any { SignalDatabase.mms.hasSelfReplyInStory(it.id) },
|
||||
isHidden = Recipient.resolved(messageRecords.first().recipient.id).shouldHideStory(),
|
||||
|
@ -105,4 +106,17 @@ class StoriesLandingRepository(context: Context) {
|
|||
SignalDatabase.recipients.setHideStory(recipientId, hideStory)
|
||||
}.subscribeOn(Schedulers.io())
|
||||
}
|
||||
|
||||
private fun getStoryViewState(messageRecords: List<MessageRecord>): StoryViewState {
|
||||
val incoming = messageRecords.filterNot { it.isOutgoing }
|
||||
if (incoming.isEmpty()) {
|
||||
return StoryViewState.NONE
|
||||
}
|
||||
|
||||
if (incoming.any { it.viewedReceiptCount == 0 }) {
|
||||
return StoryViewState.UNVIEWED
|
||||
}
|
||||
|
||||
return StoryViewState.VIEWED
|
||||
}
|
||||
}
|
||||
|
|
|
@ -13,6 +13,7 @@ import org.junit.runner.RunWith
|
|||
import org.robolectric.RobolectricTestRunner
|
||||
import org.robolectric.annotation.Config
|
||||
import org.thoughtcrime.securesms.database.MmsSmsColumns.Types
|
||||
import org.thoughtcrime.securesms.database.model.StoryViewState
|
||||
import org.thoughtcrime.securesms.testing.TestDatabaseUtil
|
||||
|
||||
@RunWith(RobolectricTestRunner::class)
|
||||
|
@ -71,4 +72,43 @@ class MmsDatabaseTest {
|
|||
TestMms.insert(db, threadId = 1, sentTimeMillis = 3, type = Types.BASE_SENDING_TYPE or Types.SECURE_MESSAGE_BIT or Types.PUSH_MESSAGE_BIT or Types.GROUP_LEAVE_BIT or Types.GROUP_V2_BIT or Types.GROUP_UPDATE_BIT)
|
||||
assertEquals(-1, mmsDatabase.getLatestGroupQuitTimestamp(1, 4))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Given no stories in database, when I getStoryViewState, then I expect NONE`() {
|
||||
assertEquals(StoryViewState.NONE, mmsDatabase.getStoryViewState(1))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Given stories in database not in thread 1, when I getStoryViewState for thread 1, then I expect NONE`() {
|
||||
TestMms.insert(db, threadId = 2, isStory = true)
|
||||
TestMms.insert(db, threadId = 2, isStory = true)
|
||||
assertEquals(StoryViewState.NONE, mmsDatabase.getStoryViewState(1))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Given viewed incoming stories in database, when I getStoryViewState, then I expect VIEWED`() {
|
||||
TestMms.insert(db, threadId = 1, isStory = true, viewed = true)
|
||||
TestMms.insert(db, threadId = 1, isStory = true, viewed = true)
|
||||
assertEquals(StoryViewState.VIEWED, mmsDatabase.getStoryViewState(1))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Given unviewed incoming stories in database, when I getStoryViewState, then I expect UNVIEWED`() {
|
||||
TestMms.insert(db, threadId = 1, isStory = true, viewed = false)
|
||||
TestMms.insert(db, threadId = 1, isStory = true, viewed = false)
|
||||
assertEquals(StoryViewState.UNVIEWED, mmsDatabase.getStoryViewState(1))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Given mix of viewed and unviewed incoming stories in database, when I getStoryViewState, then I expect UNVIEWED`() {
|
||||
TestMms.insert(db, threadId = 1, isStory = true, viewed = true)
|
||||
TestMms.insert(db, threadId = 1, isStory = true, viewed = false)
|
||||
assertEquals(StoryViewState.UNVIEWED, mmsDatabase.getStoryViewState(1))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Given only outgoing story in database, when I getStoryViewState, then I expect VIEWED`() {
|
||||
TestMms.insert(db, threadId = 1, isStory = true, type = Types.BASE_OUTBOX_TYPE)
|
||||
assertEquals(StoryViewState.VIEWED, mmsDatabase.getStoryViewState(1))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -23,7 +23,9 @@ object TestMms {
|
|||
distributionType: Int = ThreadDatabase.DistributionTypes.DEFAULT,
|
||||
type: Long = MmsSmsColumns.Types.BASE_INBOX_TYPE,
|
||||
unread: Boolean = false,
|
||||
threadId: Long = 1
|
||||
viewed: Boolean = false,
|
||||
threadId: Long = 1,
|
||||
isStory: Boolean = false
|
||||
): Long {
|
||||
val message = OutgoingMediaMessage(
|
||||
recipient,
|
||||
|
@ -34,7 +36,7 @@ object TestMms {
|
|||
expiresIn,
|
||||
viewOnce,
|
||||
distributionType,
|
||||
false,
|
||||
isStory,
|
||||
null,
|
||||
null,
|
||||
emptyList(),
|
||||
|
@ -50,6 +52,7 @@ object TestMms {
|
|||
body = body,
|
||||
type = type,
|
||||
unread = unread,
|
||||
viewed = viewed,
|
||||
threadId = threadId,
|
||||
receivedTimestampMillis = receivedTimestampMillis
|
||||
)
|
||||
|
@ -61,6 +64,7 @@ object TestMms {
|
|||
body: String = message.body,
|
||||
type: Long = MmsSmsColumns.Types.BASE_INBOX_TYPE,
|
||||
unread: Boolean = false,
|
||||
viewed: Boolean = false,
|
||||
threadId: Long = 1,
|
||||
receivedTimestampMillis: Long = System.currentTimeMillis(),
|
||||
): Long {
|
||||
|
@ -78,6 +82,8 @@ object TestMms {
|
|||
put(MmsSmsColumns.RECIPIENT_ID, message.recipient.id.serialize())
|
||||
put(MmsSmsColumns.DELIVERY_RECEIPT_COUNT, 0)
|
||||
put(MmsSmsColumns.RECEIPT_TIMESTAMP, 0)
|
||||
put(MmsSmsColumns.VIEWED_RECEIPT_COUNT, if (viewed) 1 else 0)
|
||||
put(MmsDatabase.IS_STORY, if (message.isStory) 1 else 0)
|
||||
|
||||
put(MmsSmsColumns.BODY, body)
|
||||
put(MmsDatabase.PART_COUNT, 0)
|
||||
|
|
Ładowanie…
Reference in New Issue