Implement story ring support.

fork-5.53.8
Alex Hart 2022-02-25 15:06:12 -04:00
rodzic fe088c39c7
commit 2d7655a6bb
20 zmienionych plików z 244 dodań i 13 usunięć

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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