Implement Stories feature behind flag.

Co-Authored-By: Greyson Parrelli <37311915+greyson-signal@users.noreply.github.com>
Co-Authored-By: Rashad Sookram <95182499+rashad-signal@users.noreply.github.com>
This commit is contained in:
Alex Hart
2022-02-24 13:40:28 -04:00
parent 765185952e
commit 174cd860a0
416 changed files with 19506 additions and 857 deletions

View File

@@ -13,11 +13,13 @@ import com.annimon.stream.Stream;
import org.signal.core.util.ThreadUtil;
import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.database.DistributionListDatabase;
import org.thoughtcrime.securesms.database.GroupDatabase;
import org.thoughtcrime.securesms.database.GroupDatabase.GroupRecord;
import org.thoughtcrime.securesms.database.RecipientDatabase;
import org.thoughtcrime.securesms.database.model.RecipientRecord;
import org.thoughtcrime.securesms.database.SignalDatabase;
import org.thoughtcrime.securesms.database.model.DistributionListRecord;
import org.thoughtcrime.securesms.database.model.RecipientRecord;
import org.thoughtcrime.securesms.util.livedata.LiveDataUtil;
import org.whispersystems.libsignal.util.guava.Optional;
@@ -40,16 +42,18 @@ public final class LiveRecipient {
private final AtomicReference<Recipient> recipient;
private final RecipientDatabase recipientDatabase;
private final GroupDatabase groupDatabase;
private final DistributionListDatabase distributionListDatabase;
private final MutableLiveData<Object> refreshForceNotify;
LiveRecipient(@NonNull Context context, @NonNull Recipient defaultRecipient) {
this.context = context.getApplicationContext();
this.liveData = new MutableLiveData<>(defaultRecipient);
this.recipient = new AtomicReference<>(defaultRecipient);
this.recipientDatabase = SignalDatabase.recipients();
this.groupDatabase = SignalDatabase.groups();
this.observers = new CopyOnWriteArraySet<>();
this.foreverObserver = recipient -> {
this.context = context.getApplicationContext();
this.liveData = new MutableLiveData<>(defaultRecipient);
this.recipient = new AtomicReference<>(defaultRecipient);
this.recipientDatabase = SignalDatabase.recipients();
this.groupDatabase = SignalDatabase.groups();
this.distributionListDatabase = SignalDatabase.distributionLists();
this.observers = new CopyOnWriteArraySet<>();
this.foreverObserver = recipient -> {
ThreadUtil.postToMain(() -> {
for (RecipientForeverObserver o : observers) {
o.onRecipientChanged(recipient);
@@ -192,9 +196,15 @@ public final class LiveRecipient {
}
private @NonNull Recipient fetchAndCacheRecipientFromDisk(@NonNull RecipientId id) {
RecipientRecord settings = recipientDatabase.getRecord(id);
RecipientDetails details = settings.getGroupId() != null ? getGroupRecipientDetails(settings)
: RecipientDetails.forIndividual(context, settings);
RecipientRecord record = recipientDatabase.getRecord(id);
RecipientDetails details;
if (record.getGroupId() != null) {
details = getGroupRecipientDetails(record);
} else if (record.getDistributionListId() != null) {
details = getDistributionListRecipientDetails(record);
} else {
details = RecipientDetails.forIndividual(context, record);
}
Recipient recipient = new Recipient(id, details, true);
RecipientIdCache.INSTANCE.put(recipient);
@@ -202,8 +212,8 @@ public final class LiveRecipient {
}
@WorkerThread
private @NonNull RecipientDetails getGroupRecipientDetails(@NonNull RecipientRecord settings) {
Optional<GroupRecord> groupRecord = groupDatabase.getGroup(settings.getId());
private @NonNull RecipientDetails getGroupRecipientDetails(@NonNull RecipientRecord record) {
Optional<GroupRecord> groupRecord = groupDatabase.getGroup(record.getId());
if (groupRecord.isPresent()) {
String title = groupRecord.get().getTitle();
@@ -214,10 +224,25 @@ public final class LiveRecipient {
avatarId = Optional.of(groupRecord.get().getAvatarId());
}
return new RecipientDetails(title, null, avatarId, false, false, settings.getRegistered(), settings, members, false);
return new RecipientDetails(title, null, avatarId, false, false, record.getRegistered(), record, members, false);
}
return new RecipientDetails(null, null, Optional.absent(), false, false, settings.getRegistered(), settings, null, false);
return new RecipientDetails(null, null, Optional.absent(), false, false, record.getRegistered(), record, null, false);
}
@WorkerThread
private @NonNull RecipientDetails getDistributionListRecipientDetails(@NonNull RecipientRecord record) {
DistributionListRecord groupRecord = distributionListDatabase.getList(Objects.requireNonNull(record.getDistributionListId()));
// TODO [stories] We'll have to see what the perf is like for very large distribution lists. We may not be able to support fetching all the members.
if (groupRecord != null) {
String title = groupRecord.getName();
List<Recipient> members = Stream.of(groupRecord.getMembers()).filterNot(RecipientId::isUnknown).map(this::fetchAndCacheRecipientFromDisk).toList();
return RecipientDetails.forDistributionList(title, members, record);
}
return RecipientDetails.forDistributionList(null, null, record);
}
synchronized void set(@NonNull Recipient recipient) {

View File

@@ -33,6 +33,7 @@ import org.thoughtcrime.securesms.database.RecipientDatabase.RegisteredState;
import org.thoughtcrime.securesms.database.RecipientDatabase.UnidentifiedAccessMode;
import org.thoughtcrime.securesms.database.RecipientDatabase.VibrateState;
import org.thoughtcrime.securesms.database.SignalDatabase;
import org.thoughtcrime.securesms.database.model.DistributionListId;
import org.thoughtcrime.securesms.database.model.databaseprotos.RecipientExtras;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.groups.GroupId;
@@ -48,8 +49,8 @@ import org.thoughtcrime.securesms.util.Util;
import org.thoughtcrime.securesms.wallpaper.ChatWallpaper;
import org.whispersystems.libsignal.util.guava.Optional;
import org.whispersystems.libsignal.util.guava.Preconditions;
import org.whispersystems.signalservice.api.push.ServiceId;
import org.whispersystems.signalservice.api.push.PNI;
import org.whispersystems.signalservice.api.push.ServiceId;
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
import org.whispersystems.signalservice.api.util.UuidUtil;
@@ -70,7 +71,7 @@ public class Recipient {
private static final String TAG = Log.tag(Recipient.class);
public static final Recipient UNKNOWN = new Recipient(RecipientId.UNKNOWN, new RecipientDetails(), true);
public static final Recipient UNKNOWN = new Recipient(RecipientId.UNKNOWN, RecipientDetails.forUnknown(), true);
public static final FallbackPhotoProvider DEFAULT_FALLBACK_PHOTO_PROVIDER = new FallbackPhotoProvider();
@@ -84,6 +85,7 @@ public class Recipient {
private final String e164;
private final String email;
private final GroupId groupId;
private final DistributionListId distributionListId;
private final List<Recipient> participants;
private final Optional<Long> groupAvatarId;
private final boolean isSelf;
@@ -115,6 +117,7 @@ public class Recipient {
private final Capability senderKeyCapability;
private final Capability announcementGroupCapability;
private final Capability changeNumberCapability;
private final Capability storiesCapability;
private final InsightsBannerTier insightsBannerTier;
private final byte[] storageId;
private final MentionSetting mentionSetting;
@@ -162,6 +165,12 @@ public class Recipient {
return recipients;
}
@WorkerThread
public static @NonNull Recipient distributionList(@NonNull DistributionListId distributionListId) {
RecipientId id = SignalDatabase.recipients().getOrInsertFromDistributionListId(distributionListId);
return resolved(id);
}
/**
* Returns a fully-populated {@link Recipient} and associates it with the provided username.
*/
@@ -343,6 +352,7 @@ public class Recipient {
this.e164 = null;
this.email = null;
this.groupId = null;
this.distributionListId = null;
this.participants = Collections.emptyList();
this.groupAvatarId = Optional.absent();
this.isSelf = false;
@@ -375,6 +385,7 @@ public class Recipient {
this.senderKeyCapability = Capability.UNKNOWN;
this.announcementGroupCapability = Capability.UNKNOWN;
this.changeNumberCapability = Capability.UNKNOWN;
this.storiesCapability = Capability.UNKNOWN;
this.storageId = null;
this.mentionSetting = MentionSetting.ALWAYS_NOTIFY;
this.wallpaper = null;
@@ -399,6 +410,7 @@ public class Recipient {
this.e164 = details.e164;
this.email = details.email;
this.groupId = details.groupId;
this.distributionListId = details.distributionListId;
this.participants = details.participants;
this.groupAvatarId = details.groupAvatarId;
this.isSelf = details.isSelf;
@@ -431,6 +443,7 @@ public class Recipient {
this.senderKeyCapability = details.senderKeyCapability;
this.announcementGroupCapability = details.announcementGroupCapability;
this.changeNumberCapability = details.changeNumberCapability;
this.storiesCapability = details.storiesCapability;
this.storageId = details.storageId;
this.mentionSetting = details.mentionSetting;
this.wallpaper = details.wallpaper;
@@ -492,6 +505,8 @@ public class Recipient {
}
return Util.join(names, ", ");
} else if (isMyStory()) {
return context.getString(R.string.Recipient_my_story);
} else {
return this.groupName;
}
@@ -642,6 +657,10 @@ public class Recipient {
return Optional.fromNullable(groupId);
}
public @NonNull Optional<DistributionListId> getDistributionListId() {
return Optional.fromNullable(distributionListId);
}
public @NonNull Optional<String> getSmsAddress() {
return Optional.fromNullable(e164).or(Optional.fromNullable(email));
}
@@ -704,6 +723,10 @@ public class Recipient {
return hasServiceId() && !hasSmsAddress();
}
public boolean shouldHideStory() {
return extras.transform(Extras::hideStory).or(false);
}
public @NonNull GroupId requireGroupId() {
GroupId resolved = resolving ? resolve().groupId : groupId;
@@ -714,6 +737,16 @@ public class Recipient {
return resolved;
}
public @NonNull DistributionListId requireDistributionListId() {
DistributionListId resolved = resolving ? resolve().distributionListId : distributionListId;
if (resolved == null) {
throw new MissingAddressError(id);
}
return resolved;
}
/**
* The {@link ServiceId} of the user if available, otherwise throw.
*/
@@ -796,6 +829,14 @@ public class Recipient {
return groupId != null && groupId.isV2();
}
public boolean isDistributionList() {
return resolve().distributionListId != null;
}
public boolean isMyStory() {
return Objects.equals(resolve().distributionListId, DistributionListId.from(DistributionListId.MY_STORY_ID));
}
public boolean isActiveGroup() {
return Stream.of(getParticipants()).anyMatch(Recipient::isSelf);
}
@@ -835,6 +876,7 @@ public class Recipient {
public @NonNull FallbackContactPhoto getFallbackContactPhoto(@NonNull FallbackPhotoProvider fallbackPhotoProvider, int targetSize) {
if (isSelf) return fallbackPhotoProvider.getPhotoForLocalNumber();
else if (isResolving()) return fallbackPhotoProvider.getPhotoForResolvingRecipient();
else if (isDistributionList()) return fallbackPhotoProvider.getPhotoForDistributionList();
else if (isGroupInternal()) return fallbackPhotoProvider.getPhotoForGroup();
else if (isGroup()) return fallbackPhotoProvider.getPhotoForGroup();
else if (!TextUtils.isEmpty(groupName)) return fallbackPhotoProvider.getPhotoForRecipientWithName(groupName, targetSize);
@@ -947,6 +989,10 @@ public class Recipient {
return changeNumberCapability;
}
public @NonNull Capability getStoriesCapability() {
return storiesCapability;
}
/**
* True if this recipient supports the message retry system, or false if we should use the legacy session reset system.
*/
@@ -1170,17 +1216,21 @@ public class Recipient {
return recipientExtras.getManuallyShownAvatar();
}
public boolean hideStory() {
return recipientExtras.getHideStory();
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
final Extras that = (Extras) o;
return manuallyShownAvatar() == that.manuallyShownAvatar();
return manuallyShownAvatar() == that.manuallyShownAvatar() && hideStory() == that.hideStory();
}
@Override
public int hashCode() {
return Objects.hash(manuallyShownAvatar());
return Objects.hash(manuallyShownAvatar(), hideStory());
}
}
@@ -1269,6 +1319,10 @@ public class Recipient {
public @NonNull FallbackContactPhoto getPhotoForRecipientWithoutName() {
return new ResourceContactPhoto(R.drawable.ic_profile_outline_40, R.drawable.ic_profile_outline_20, R.drawable.ic_profile_outline_48);
}
public @NonNull FallbackContactPhoto getPhotoForDistributionList() {
return new ResourceContactPhoto(R.drawable.ic_group_outline_34, R.drawable.ic_group_outline_20, R.drawable.ic_group_outline_48);
}
}
private static class MissingAddressError extends AssertionError {

View File

@@ -12,6 +12,7 @@ import org.thoughtcrime.securesms.conversation.colors.AvatarColor;
import org.thoughtcrime.securesms.conversation.colors.ChatColors;
import org.thoughtcrime.securesms.database.RecipientDatabase.InsightsBannerTier;
import org.thoughtcrime.securesms.database.RecipientDatabase.MentionSetting;
import org.thoughtcrime.securesms.database.model.DistributionListId;
import org.thoughtcrime.securesms.database.model.RecipientRecord;
import org.thoughtcrime.securesms.database.RecipientDatabase.RegisteredState;
import org.thoughtcrime.securesms.database.RecipientDatabase.UnidentifiedAccessMode;
@@ -39,6 +40,7 @@ public class RecipientDetails {
final String e164;
final String email;
final GroupId groupId;
final DistributionListId distributionListId;
final String groupName;
final String systemContactName;
final String customLabel;
@@ -72,6 +74,7 @@ public class RecipientDetails {
final Recipient.Capability senderKeyCapability;
final Recipient.Capability announcementGroupCapability;
final Recipient.Capability changeNumberCapability;
final Recipient.Capability storiesCapability;
final InsightsBannerTier insightsBannerTier;
final byte[] storageId;
final MentionSetting mentionSetting;
@@ -106,6 +109,7 @@ public class RecipientDetails {
this.e164 = record.getE164();
this.email = record.getEmail();
this.groupId = record.getGroupId();
this.distributionListId = record.getDistributionListId();
this.messageRingtone = record.getMessageRingtone();
this.callRingtone = record.getCallRingtone();
this.mutedUntil = record.getMuteUntil();
@@ -133,6 +137,7 @@ public class RecipientDetails {
this.senderKeyCapability = record.getSenderKeyCapability();
this.announcementGroupCapability = record.getAnnouncementGroupCapability();
this.changeNumberCapability = record.getChangeNumberCapability();
this.storiesCapability = record.getStoriesCapability();
this.insightsBannerTier = record.getInsightsBannerTier();
this.storageId = record.getStorageId();
this.mentionSetting = record.getMentionSetting();
@@ -150,10 +155,7 @@ public class RecipientDetails {
this.isReleaseChannel = isReleaseChannel;
}
/**
* Only used for {@link Recipient#UNKNOWN}.
*/
RecipientDetails() {
private RecipientDetails() {
this.groupAvatarId = null;
this.systemContactPhoto = null;
this.customLabel = null;
@@ -164,6 +166,7 @@ public class RecipientDetails {
this.e164 = null;
this.email = null;
this.groupId = null;
this.distributionListId = null;
this.messageRingtone = null;
this.callRingtone = null;
this.mutedUntil = 0;
@@ -193,6 +196,7 @@ public class RecipientDetails {
this.senderKeyCapability = Recipient.Capability.UNKNOWN;
this.announcementGroupCapability = Recipient.Capability.UNKNOWN;
this.changeNumberCapability = Recipient.Capability.UNKNOWN;
this.storiesCapability = Recipient.Capability.UNKNOWN;
this.storageId = null;
this.mentionSetting = MentionSetting.ALWAYS_NOTIFY;
this.wallpaper = null;
@@ -226,4 +230,12 @@ public class RecipientDetails {
return new RecipientDetails(null, settings.getSystemDisplayName(), Optional.absent(), systemContact, isSelf, registeredState, settings, null, isReleaseChannel);
}
public static @NonNull RecipientDetails forDistributionList(String title, @Nullable List<Recipient> members, @NonNull RecipientRecord record) {
return new RecipientDetails(title, null, Optional.absent(), false, false, record.getRegistered(), record, members, false);
}
public static @NonNull RecipientDetails forUnknown() {
return new RecipientDetails();
}
}

View File

@@ -11,6 +11,7 @@ import androidx.annotation.Nullable;
import com.annimon.stream.Stream;
import org.thoughtcrime.securesms.database.model.DatabaseId;
import org.thoughtcrime.securesms.util.DelimiterUtil;
import org.thoughtcrime.securesms.util.Util;
import org.whispersystems.signalservice.api.push.ServiceId;
@@ -22,7 +23,7 @@ import java.util.Collection;
import java.util.List;
import java.util.regex.Pattern;
public class RecipientId implements Parcelable, Comparable<RecipientId> {
public class RecipientId implements Parcelable, Comparable<RecipientId>, DatabaseId {
private static final long UNKNOWN_ID = -1;
private static final char DELIMITER = ',';
@@ -141,6 +142,7 @@ public class RecipientId implements Parcelable, Comparable<RecipientId> {
return id == UNKNOWN_ID;
}
@Override
public @NonNull String serialize() {
return String.valueOf(id);
}

View File

@@ -29,6 +29,7 @@ import com.google.android.material.bottomsheet.BottomSheetDialogFragment;
import org.signal.core.util.logging.Log;
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;
@@ -69,7 +70,7 @@ public final class RecipientBottomSheetDialogFragment extends BottomSheetDialogF
private static final String ARGS_GROUP_ID = "GROUP_ID";
private RecipientDialogViewModel viewModel;
private AvatarImageView avatar;
private AvatarView avatar;
private TextView fullName;
private TextView about;
private TextView usernameNumber;
@@ -160,7 +161,7 @@ public final class RecipientBottomSheetDialogFragment extends BottomSheetDialogF
return new FallbackPhoto80dp(R.drawable.ic_note_80, recipient.getAvatarColor());
}
});
avatar.setAvatar(recipient);
avatar.displayChatAvatar(recipient);
if (!recipient.isSelf()) {
badgeImageView.setBadgeFromRecipient(recipient);