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 android.widget.FrameLayout
import org.thoughtcrime.securesms.R import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.components.AvatarImageView import org.thoughtcrime.securesms.components.AvatarImageView
import org.thoughtcrime.securesms.database.model.StoryViewState
import org.thoughtcrime.securesms.keyvalue.SignalStore import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.mms.GlideRequests import org.thoughtcrime.securesms.mms.GlideRequests
import org.thoughtcrime.securesms.recipients.Recipient import org.thoughtcrime.securesms.recipients.Recipient
@ -46,6 +47,14 @@ class AvatarView @JvmOverloads constructor(
avatar.scaleY = 1f avatar.scaleY = 1f
} }
fun setStoryRingFromState(storyViewState: StoryViewState) {
when (storyViewState) {
StoryViewState.NONE -> hideStoryRing()
StoryViewState.UNVIEWED -> showStoryRing(true)
StoryViewState.VIEWED -> showStoryRing(false)
}
}
/** /**
* Displays Note-to-Self * Displays Note-to-Self
*/ */

Wyświetl plik

@ -266,6 +266,7 @@ class ConversationSettingsFragment : DSLSettingsFragment(
customPref( customPref(
AvatarPreference.Model( AvatarPreference.Model(
recipient = state.recipient, recipient = state.recipient,
storyViewState = state.storyViewState,
onAvatarClick = { avatar -> onAvatarClick = { avatar ->
if (!state.recipient.isSelf) { if (!state.recipient.isSelf) {
// startActivity(StoryViewerActivity.createIntent(requireContext(), state.recipient.id)) // startActivity(StoryViewerActivity.createIntent(requireContext(), state.recipient.id))

Wyświetl plik

@ -4,6 +4,8 @@ import android.content.Context
import android.database.Cursor import android.database.Cursor
import androidx.annotation.WorkerThread import androidx.annotation.WorkerThread
import androidx.lifecycle.LiveData 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.concurrent.SignalExecutors
import org.signal.core.util.logging.Log import org.signal.core.util.logging.Log
import org.signal.storageservice.protos.groups.local.DecryptedGroup 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.MediaDatabase
import org.thoughtcrime.securesms.database.SignalDatabase import org.thoughtcrime.securesms.database.SignalDatabase
import org.thoughtcrime.securesms.database.model.IdentityRecord import org.thoughtcrime.securesms.database.model.IdentityRecord
import org.thoughtcrime.securesms.database.model.StoryViewState
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
import org.thoughtcrime.securesms.groups.GroupId import org.thoughtcrime.securesms.groups.GroupId
import org.thoughtcrime.securesms.groups.GroupManager 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) { fun getThreadId(recipientId: RecipientId, consumer: (Long) -> Unit) {
SignalExecutors.BOUNDED.execute { SignalExecutors.BOUNDED.execute {
consumer(SignalDatabase.threads.getThreadIdIfExistsFor(recipientId)) 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.ButtonStripPreference
import org.thoughtcrime.securesms.components.settings.conversation.preferences.LegacyGroupPreference import org.thoughtcrime.securesms.components.settings.conversation.preferences.LegacyGroupPreference
import org.thoughtcrime.securesms.database.model.IdentityRecord 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.GroupId
import org.thoughtcrime.securesms.groups.ui.GroupMemberEntry import org.thoughtcrime.securesms.groups.ui.GroupMemberEntry
import org.thoughtcrime.securesms.recipients.Recipient import org.thoughtcrime.securesms.recipients.Recipient
data class ConversationSettingsState( data class ConversationSettingsState(
val threadId: Long = -1, val threadId: Long = -1,
val storyViewState: StoryViewState = StoryViewState.NONE,
val recipient: Recipient = Recipient.UNKNOWN, val recipient: Recipient = Recipient.UNKNOWN,
val buttonStripState: ButtonStripPreference.State = ButtonStripPreference.State(), val buttonStripState: ButtonStripPreference.State = ButtonStripPreference.State(),
val disappearingMessagesLifespan: Int = 0, val disappearingMessagesLifespan: Int = 0,

Wyświetl plik

@ -6,12 +6,15 @@ import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.Transformations import androidx.lifecycle.Transformations
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider 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.ThreadUtil
import org.signal.core.util.concurrent.SignalExecutors import org.signal.core.util.concurrent.SignalExecutors
import org.thoughtcrime.securesms.components.settings.conversation.preferences.ButtonStripPreference import org.thoughtcrime.securesms.components.settings.conversation.preferences.ButtonStripPreference
import org.thoughtcrime.securesms.components.settings.conversation.preferences.LegacyGroupPreference import org.thoughtcrime.securesms.components.settings.conversation.preferences.LegacyGroupPreference
import org.thoughtcrime.securesms.database.AttachmentDatabase import org.thoughtcrime.securesms.database.AttachmentDatabase
import org.thoughtcrime.securesms.database.RecipientDatabase import org.thoughtcrime.securesms.database.RecipientDatabase
import org.thoughtcrime.securesms.database.model.StoryViewState
import org.thoughtcrime.securesms.groups.GroupId import org.thoughtcrime.securesms.groups.GroupId
import org.thoughtcrime.securesms.groups.LiveGroup import org.thoughtcrime.securesms.groups.LiveGroup
import org.thoughtcrime.securesms.recipients.Recipient import org.thoughtcrime.securesms.recipients.Recipient
@ -46,6 +49,8 @@ sealed class ConversationSettingsViewModel(
val state: LiveData<ConversationSettingsState> = store.stateLiveData val state: LiveData<ConversationSettingsState> = store.stateLiveData
val events: LiveData<ConversationSettingsEvent> = internalEvents val events: LiveData<ConversationSettingsEvent> = internalEvents
protected val disposable = CompositeDisposable()
init { init {
val threadId: LiveData<Long> = Transformations.distinctUntilChanged(Transformations.map(state) { it.threadId }) val threadId: LiveData<Long> = Transformations.distinctUntilChanged(Transformations.map(state) { it.threadId })
val updater: LiveData<Long> = LiveDataUtil.combineLatest(threadId, sharedMediaUpdateTrigger) { tId, _ -> tId } val updater: LiveData<Long> = LiveDataUtil.combineLatest(threadId, sharedMediaUpdateTrigger) { tId, _ -> tId }
@ -105,6 +110,7 @@ sealed class ConversationSettingsViewModel(
cleared = true cleared = true
openedMediaCursors.forEach { it.ensureClosed() } openedMediaCursors.forEach { it.ensureClosed() }
store.clear() store.clear()
disposable.clear()
} }
private fun Cursor?.ensureClosed() { private fun Cursor?.ensureClosed() {
@ -126,6 +132,10 @@ sealed class ConversationSettingsViewModel(
private val liveRecipient = Recipient.live(recipientId) private val liveRecipient = Recipient.live(recipientId)
init { init {
disposable += StoryViewState.getForRecipientId(recipientId).subscribe { storyViewState ->
store.update { it.copy(storyViewState = storyViewState) }
}
store.update(liveRecipient.liveData) { recipient, state -> store.update(liveRecipient.liveData) { recipient, state ->
state.copy( state.copy(
recipient = recipient, recipient = recipient,
@ -240,6 +250,10 @@ sealed class ConversationSettingsViewModel(
private val liveGroup = LiveGroup(groupId) private val liveGroup = LiveGroup(groupId)
init { 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 } val recipientAndIsActive = LiveDataUtil.combineLatest(liveGroup.groupRecipient, liveGroup.isActive) { r, a -> r to a }
store.update(recipientAndIsActive) { (recipient, isActive), state -> store.update(recipientAndIsActive) { (recipient, isActive), state ->
state.copy( 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.components.settings.PreferenceModel
import org.thoughtcrime.securesms.contacts.avatars.FallbackContactPhoto import org.thoughtcrime.securesms.contacts.avatars.FallbackContactPhoto
import org.thoughtcrime.securesms.contacts.avatars.FallbackPhoto import org.thoughtcrime.securesms.contacts.avatars.FallbackPhoto
import org.thoughtcrime.securesms.database.model.StoryViewState
import org.thoughtcrime.securesms.recipients.Recipient import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.util.ViewUtil import org.thoughtcrime.securesms.util.ViewUtil
import org.thoughtcrime.securesms.util.adapter.mapping.LayoutFactory import org.thoughtcrime.securesms.util.adapter.mapping.LayoutFactory
@ -26,6 +27,7 @@ object AvatarPreference {
class Model( class Model(
val recipient: Recipient, val recipient: Recipient,
val storyViewState: StoryViewState,
val onAvatarClick: (View) -> Unit, val onAvatarClick: (View) -> Unit,
val onBadgeClick: (Badge) -> Unit val onBadgeClick: (Badge) -> Unit
) : PreferenceModel<Model>() { ) : PreferenceModel<Model>() {
@ -63,6 +65,7 @@ object AvatarPreference {
} }
} }
avatar.setStoryRingFromState(model.storyViewState)
avatar.displayChatAvatar(model.recipient) avatar.displayChatAvatar(model.recipient)
avatar.disableQuickContact() avatar.disableQuickContact()
avatar.setOnClickListener { model.onAvatarClick(avatar) } avatar.setOnClickListener { model.onAvatarClick(avatar) }

Wyświetl plik

@ -553,6 +553,8 @@ public class ConversationParentFragment extends Fragment
initializeInsightObserver(); initializeInsightObserver();
initializeActionBar(); initializeActionBar();
viewModel.getStoryViewState(getViewLifecycleOwner()).observe(getViewLifecycleOwner(), titleView::setStoryRingFromState);
requireActivity().getOnBackPressedDispatcher().addCallback(getViewLifecycleOwner(), new OnBackPressedCallback(true) { requireActivity().getOnBackPressedDispatcher().addCallback(getViewLifecycleOwner(), new OnBackPressedCallback(true) {
@Override @Override
public void handleOnBackPressed() { 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.avatar.view.AvatarView;
import org.thoughtcrime.securesms.badges.BadgeImageView; import org.thoughtcrime.securesms.badges.BadgeImageView;
import org.thoughtcrime.securesms.components.AvatarImageView; import org.thoughtcrime.securesms.components.AvatarImageView;
import org.thoughtcrime.securesms.database.model.StoryViewState;
import org.thoughtcrime.securesms.mms.GlideRequests; import org.thoughtcrime.securesms.mms.GlideRequests;
import org.thoughtcrime.securesms.recipients.LiveRecipient; import org.thoughtcrime.securesms.recipients.LiveRecipient;
import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.recipients.Recipient;
@ -124,6 +125,10 @@ public class ConversationTitleView extends RelativeLayout {
updateVerifiedSubtitleVisibility(); updateVerifiedSubtitleVisibility();
} }
public void setStoryRingFromState(@NonNull StoryViewState storyViewState) {
avatar.setStoryRingFromState(storyViewState);
}
public void setVerified(boolean verified) { public void setVerified(boolean verified) {
this.verified.setVisibility(verified ? View.VISIBLE : View.GONE); 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.MainThread;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.lifecycle.LifecycleOwner;
import androidx.lifecycle.LiveData; import androidx.lifecycle.LiveData;
import androidx.lifecycle.LiveDataReactiveStreams; import androidx.lifecycle.LiveDataReactiveStreams;
import androidx.lifecycle.MutableLiveData; import androidx.lifecycle.MutableLiveData;
@ -18,6 +19,7 @@ import com.annimon.stream.Stream;
import org.greenrobot.eventbus.EventBus; import org.greenrobot.eventbus.EventBus;
import org.greenrobot.eventbus.Subscribe; import org.greenrobot.eventbus.Subscribe;
import org.greenrobot.eventbus.ThreadMode; import org.greenrobot.eventbus.ThreadMode;
import org.reactivestreams.Publisher;
import org.signal.core.util.MapUtil; import org.signal.core.util.MapUtil;
import org.signal.core.util.logging.Log; import org.signal.core.util.logging.Log;
import org.signal.paging.PagedData; 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.conversation.colors.NameColor;
import org.thoughtcrime.securesms.database.DatabaseObserver; import org.thoughtcrime.securesms.database.DatabaseObserver;
import org.thoughtcrime.securesms.database.model.MessageId; import org.thoughtcrime.securesms.database.model.MessageId;
import org.thoughtcrime.securesms.database.model.StoryViewState;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies; import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.groups.GroupId; import org.thoughtcrime.securesms.groups.GroupId;
import org.thoughtcrime.securesms.groups.LiveGroup; import org.thoughtcrime.securesms.groups.LiveGroup;
@ -61,6 +64,7 @@ import java.util.Set;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import io.reactivex.rxjava3.core.BackpressureStrategy; import io.reactivex.rxjava3.core.BackpressureStrategy;
import io.reactivex.rxjava3.core.Flowable;
import io.reactivex.rxjava3.core.Observable; import io.reactivex.rxjava3.core.Observable;
public class ConversationViewModel extends ViewModel { public class ConversationViewModel extends ViewModel {
@ -198,6 +202,14 @@ public class ConversationViewModel extends ViewModel {
threadAnimationStateStore.getStateLiveData().observeForever(threadAnimationStateStoreDriver); 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) { void onMessagesCommitted(@NonNull List<ConversationMessage> conversationMessages) {
if (Util.hasItems(conversationMessages)) { if (Util.hasItems(conversationMessages)) {
threadAnimationStateStore.update(state -> { 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.MessageId;
import org.thoughtcrime.securesms.database.model.MessageRecord; import org.thoughtcrime.securesms.database.model.MessageRecord;
import org.thoughtcrime.securesms.database.model.SmsMessageRecord; import org.thoughtcrime.securesms.database.model.SmsMessageRecord;
import org.thoughtcrime.securesms.database.model.StoryViewState;
import org.thoughtcrime.securesms.groups.GroupMigrationMembershipChange; import org.thoughtcrime.securesms.groups.GroupMigrationMembershipChange;
import org.thoughtcrime.securesms.insights.InsightsConstants; import org.thoughtcrime.securesms.insights.InsightsConstants;
import org.thoughtcrime.securesms.mms.IncomingMediaMessage; import org.thoughtcrime.securesms.mms.IncomingMediaMessage;
@ -193,6 +194,7 @@ public abstract class MessageDatabase extends Database implements MmsSmsColumns
public abstract long getUnreadStoryCount(); public abstract long getUnreadStoryCount();
public abstract @Nullable Long getOldestStorySendTimestamp(); public abstract @Nullable Long getOldestStorySendTimestamp();
public abstract int deleteStoriesOlderThan(long timestamp); public abstract int deleteStoriesOlderThan(long timestamp);
public abstract @NonNull StoryViewState getStoryViewState(@NonNull RecipientId recipientId);
final @NonNull String getOutgoingTypeClause() { final @NonNull String getOutgoingTypeClause() {
List<String> segments = new ArrayList<>(Types.OUTGOING_MESSAGE_TYPES.length); 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.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import com.annimon.stream.Collectors; import com.annimon.stream.Collectors;
import com.annimon.stream.Stream; 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.NotificationMmsMessageRecord;
import org.thoughtcrime.securesms.database.model.Quote; import org.thoughtcrime.securesms.database.model.Quote;
import org.thoughtcrime.securesms.database.model.SmsMessageRecord; 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.database.model.databaseprotos.BodyRangeList;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies; import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.groups.GroupMigrationMembershipChange; 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.IncomingTextMessage;
import org.thoughtcrime.securesms.sms.OutgoingTextMessage; import org.thoughtcrime.securesms.sms.OutgoingTextMessage;
import org.thoughtcrime.securesms.util.CursorUtil; import org.thoughtcrime.securesms.util.CursorUtil;
import org.thoughtcrime.securesms.util.FeatureFlags;
import org.thoughtcrime.securesms.util.JsonUtils; import org.thoughtcrime.securesms.util.JsonUtils;
import org.thoughtcrime.securesms.util.MediaUtil; import org.thoughtcrime.securesms.util.MediaUtil;
import org.thoughtcrime.securesms.util.SqlUtil; import org.thoughtcrime.securesms.util.SqlUtil;
@ -591,6 +594,46 @@ public class MmsDatabase extends MessageDatabase {
return new Reader(cursor); 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 @Override
public @NonNull MessageId getStoryId(@NonNull RecipientId authorId, long sentTimestamp) throws NoSuchMessageException { public @NonNull MessageId getStoryId(@NonNull RecipientId authorId, long sentTimestamp) throws NoSuchMessageException {
SQLiteDatabase database = databaseHelper.getSignalReadableDatabase(); 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.MessageId;
import org.thoughtcrime.securesms.database.model.MessageRecord; import org.thoughtcrime.securesms.database.model.MessageRecord;
import org.thoughtcrime.securesms.database.model.SmsMessageRecord; 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.GroupCallUpdateDetails;
import org.thoughtcrime.securesms.database.model.databaseprotos.ProfileChangeDetails; import org.thoughtcrime.securesms.database.model.databaseprotos.ProfileChangeDetails;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies; import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
@ -1431,6 +1432,11 @@ public class SmsDatabase extends MessageDatabase {
throw new UnsupportedOperationException(); throw new UnsupportedOperationException();
} }
@Override
public @NonNull StoryViewState getStoryViewState(@NonNull RecipientId recipientId) {
throw new UnsupportedOperationException();
}
@Override @Override
public int deleteStoriesOlderThan(long timestamp) { public int deleteStoriesOlderThan(long timestamp) {
throw new UnsupportedOperationException(); 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.app.Activity;
import android.content.ActivityNotFoundException; import android.content.ActivityNotFoundException;
import android.content.Intent; import android.content.Intent;
import android.content.res.ColorStateList;
import android.graphics.drawable.Drawable; import android.graphics.drawable.Drawable;
import android.os.Bundle; import android.os.Bundle;
import android.text.SpannableString;
import android.text.SpannableStringBuilder; import android.text.SpannableStringBuilder;
import android.text.TextUtils; import android.text.TextUtils;
import android.view.LayoutInflater; import android.view.LayoutInflater;
@ -20,10 +18,9 @@ import android.widget.Toast;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.core.content.ContextCompat; import androidx.core.content.ContextCompat;
import androidx.core.widget.TextViewCompat;
import androidx.fragment.app.DialogFragment; import androidx.fragment.app.DialogFragment;
import androidx.fragment.app.FragmentManager; import androidx.fragment.app.FragmentManager;
import androidx.lifecycle.ViewModelProviders; import androidx.lifecycle.ViewModelProvider;
import com.google.android.material.bottomsheet.BottomSheetDialogFragment; 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.avatar.view.AvatarView;
import org.thoughtcrime.securesms.badges.BadgeImageView; import org.thoughtcrime.securesms.badges.BadgeImageView;
import org.thoughtcrime.securesms.badges.view.ViewBadgeBottomSheetDialogFragment; 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.DSLSettingsIcon;
import org.thoughtcrime.securesms.components.settings.conversation.preferences.ButtonStripPreference; import org.thoughtcrime.securesms.components.settings.conversation.preferences.ButtonStripPreference;
import org.thoughtcrime.securesms.contacts.avatars.FallbackContactPhoto; 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.SpanUtil;
import org.thoughtcrime.securesms.util.ThemeUtil; import org.thoughtcrime.securesms.util.ThemeUtil;
import org.thoughtcrime.securesms.util.Util; import org.thoughtcrime.securesms.util.Util;
import org.thoughtcrime.securesms.util.ViewUtil;
import java.util.Objects; import java.util.Objects;
@ -150,7 +145,11 @@ public final class RecipientBottomSheetDialogFragment extends BottomSheetDialogF
RecipientDialogViewModel.Factory factory = new RecipientDialogViewModel.Factory(requireContext().getApplicationContext(), recipientId, groupId); 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 -> { viewModel.getRecipient().observe(getViewLifecycleOwner(), recipient -> {
interactionsContainer.setVisibility(recipient.isSelf() ? View.GONE : View.VISIBLE); 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.signal.core.util.ThreadUtil;
import org.thoughtcrime.securesms.BlockUnblockDialog; import org.thoughtcrime.securesms.BlockUnblockDialog;
import org.thoughtcrime.securesms.R; import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.verify.VerifyIdentityActivity;
import org.thoughtcrime.securesms.components.settings.conversation.ConversationSettingsActivity; import org.thoughtcrime.securesms.components.settings.conversation.ConversationSettingsActivity;
import org.thoughtcrime.securesms.database.GroupDatabase; import org.thoughtcrime.securesms.database.GroupDatabase;
import org.thoughtcrime.securesms.database.model.IdentityRecord; 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.GroupId;
import org.thoughtcrime.securesms.groups.LiveGroup; import org.thoughtcrime.securesms.groups.LiveGroup;
import org.thoughtcrime.securesms.groups.ui.GroupChangeFailureReason; 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.recipients.RecipientUtil;
import org.thoughtcrime.securesms.util.CommunicationActions; import org.thoughtcrime.securesms.util.CommunicationActions;
import org.thoughtcrime.securesms.util.livedata.LiveDataUtil; import org.thoughtcrime.securesms.util.livedata.LiveDataUtil;
import org.thoughtcrime.securesms.verify.VerifyIdentityActivity;
import java.util.Objects; import java.util.Objects;
import io.reactivex.rxjava3.disposables.CompositeDisposable;
import io.reactivex.rxjava3.disposables.Disposable;
final class RecipientDialogViewModel extends ViewModel { final class RecipientDialogViewModel extends ViewModel {
private final Context context; private final Context context;
@ -44,6 +48,8 @@ final class RecipientDialogViewModel extends ViewModel {
private final LiveData<AdminActionStatus> adminActionStatus; private final LiveData<AdminActionStatus> adminActionStatus;
private final LiveData<Boolean> canAddToAGroup; private final LiveData<Boolean> canAddToAGroup;
private final MutableLiveData<Boolean> adminActionBusy; private final MutableLiveData<Boolean> adminActionBusy;
private final MutableLiveData<StoryViewState> storyViewState;
private final CompositeDisposable disposables;
private RecipientDialogViewModel(@NonNull Context context, private RecipientDialogViewModel(@NonNull Context context,
@NonNull RecipientDialogRepository recipientDialogRepository) @NonNull RecipientDialogRepository recipientDialogRepository)
@ -52,6 +58,8 @@ final class RecipientDialogViewModel extends ViewModel {
this.recipientDialogRepository = recipientDialogRepository; this.recipientDialogRepository = recipientDialogRepository;
this.identity = new MutableLiveData<>(); this.identity = new MutableLiveData<>();
this.adminActionBusy = new MutableLiveData<>(false); this.adminActionBusy = new MutableLiveData<>(false);
this.storyViewState = new MutableLiveData<>();
this.disposables = new CompositeDisposable();
boolean recipientIsSelf = recipientDialogRepository.getRecipientId().equals(Recipient.self().getId()); 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()); (r, count) -> count > 0 && r.isRegistered() && !r.isGroup() && !r.isSelf() && !r.isBlocked());
recipientDialogRepository.getActiveGroupCount(localGroupCount::postValue); 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() { LiveData<Recipient> getRecipient() {

Wyświetl plik

@ -79,7 +79,7 @@ object StoriesLandingItem {
val record = model.data.primaryStory.messageRecord as MediaMmsMessageRecord 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) storyPreview.setImageResource(GlideApp.with(storyPreview), record.slideDeck.thumbnailSlide!!, false, true)
if (model.data.secondaryStory != null) { if (model.data.secondaryStory != null) {

Wyświetl plik

@ -1,13 +1,14 @@
package org.thoughtcrime.securesms.stories.landing package org.thoughtcrime.securesms.stories.landing
import org.thoughtcrime.securesms.conversation.ConversationMessage import org.thoughtcrime.securesms.conversation.ConversationMessage
import org.thoughtcrime.securesms.database.model.StoryViewState
import org.thoughtcrime.securesms.recipients.Recipient import org.thoughtcrime.securesms.recipients.Recipient
/** /**
* Data required by each row of the Stories Landing Page for proper rendering. * Data required by each row of the Stories Landing Page for proper rendering.
*/ */
data class StoriesLandingItemData( data class StoriesLandingItemData(
val hasUnreadStory: Boolean, val storyViewState: StoryViewState,
val hasReplies: Boolean, val hasReplies: Boolean,
val hasRepliesFromSelf: Boolean, val hasRepliesFromSelf: Boolean,
val isHidden: 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.SignalDatabase
import org.thoughtcrime.securesms.database.model.DistributionListId import org.thoughtcrime.securesms.database.model.DistributionListId
import org.thoughtcrime.securesms.database.model.MessageRecord import org.thoughtcrime.securesms.database.model.MessageRecord
import org.thoughtcrime.securesms.database.model.StoryViewState
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
import org.thoughtcrime.securesms.recipients.Recipient import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.recipients.RecipientForeverObserver import org.thoughtcrime.securesms.recipients.RecipientForeverObserver
@ -66,7 +67,7 @@ class StoriesLandingRepository(context: Context) {
fun refresh() { fun refresh() {
val itemData = StoriesLandingItemData( val itemData = StoriesLandingItemData(
storyRecipient = sender, storyRecipient = sender,
hasUnreadStory = messageRecords.any { it.viewedReceiptCount == 0 && !it.isOutgoing }, storyViewState = getStoryViewState(messageRecords),
hasReplies = messageRecords.any { SignalDatabase.mms.getNumberOfStoryReplies(it.id) > 0 }, hasReplies = messageRecords.any { SignalDatabase.mms.getNumberOfStoryReplies(it.id) > 0 },
hasRepliesFromSelf = messageRecords.any { SignalDatabase.mms.hasSelfReplyInStory(it.id) }, hasRepliesFromSelf = messageRecords.any { SignalDatabase.mms.hasSelfReplyInStory(it.id) },
isHidden = Recipient.resolved(messageRecords.first().recipient.id).shouldHideStory(), isHidden = Recipient.resolved(messageRecords.first().recipient.id).shouldHideStory(),
@ -105,4 +106,17 @@ class StoriesLandingRepository(context: Context) {
SignalDatabase.recipients.setHideStory(recipientId, hideStory) SignalDatabase.recipients.setHideStory(recipientId, hideStory)
}.subscribeOn(Schedulers.io()) }.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.RobolectricTestRunner
import org.robolectric.annotation.Config import org.robolectric.annotation.Config
import org.thoughtcrime.securesms.database.MmsSmsColumns.Types import org.thoughtcrime.securesms.database.MmsSmsColumns.Types
import org.thoughtcrime.securesms.database.model.StoryViewState
import org.thoughtcrime.securesms.testing.TestDatabaseUtil import org.thoughtcrime.securesms.testing.TestDatabaseUtil
@RunWith(RobolectricTestRunner::class) @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) 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)) 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, distributionType: Int = ThreadDatabase.DistributionTypes.DEFAULT,
type: Long = MmsSmsColumns.Types.BASE_INBOX_TYPE, type: Long = MmsSmsColumns.Types.BASE_INBOX_TYPE,
unread: Boolean = false, unread: Boolean = false,
threadId: Long = 1 viewed: Boolean = false,
threadId: Long = 1,
isStory: Boolean = false
): Long { ): Long {
val message = OutgoingMediaMessage( val message = OutgoingMediaMessage(
recipient, recipient,
@ -34,7 +36,7 @@ object TestMms {
expiresIn, expiresIn,
viewOnce, viewOnce,
distributionType, distributionType,
false, isStory,
null, null,
null, null,
emptyList(), emptyList(),
@ -50,6 +52,7 @@ object TestMms {
body = body, body = body,
type = type, type = type,
unread = unread, unread = unread,
viewed = viewed,
threadId = threadId, threadId = threadId,
receivedTimestampMillis = receivedTimestampMillis receivedTimestampMillis = receivedTimestampMillis
) )
@ -61,6 +64,7 @@ object TestMms {
body: String = message.body, body: String = message.body,
type: Long = MmsSmsColumns.Types.BASE_INBOX_TYPE, type: Long = MmsSmsColumns.Types.BASE_INBOX_TYPE,
unread: Boolean = false, unread: Boolean = false,
viewed: Boolean = false,
threadId: Long = 1, threadId: Long = 1,
receivedTimestampMillis: Long = System.currentTimeMillis(), receivedTimestampMillis: Long = System.currentTimeMillis(),
): Long { ): Long {
@ -78,6 +82,8 @@ object TestMms {
put(MmsSmsColumns.RECIPIENT_ID, message.recipient.id.serialize()) put(MmsSmsColumns.RECIPIENT_ID, message.recipient.id.serialize())
put(MmsSmsColumns.DELIVERY_RECEIPT_COUNT, 0) put(MmsSmsColumns.DELIVERY_RECEIPT_COUNT, 0)
put(MmsSmsColumns.RECEIPT_TIMESTAMP, 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(MmsSmsColumns.BODY, body)
put(MmsDatabase.PART_COUNT, 0) put(MmsDatabase.PART_COUNT, 0)