Implement Story onboarding download job and message insertion.
This commit is contained in:
parent
2270dfaf21
commit
36ccf9ca54
17 changed files with 365 additions and 56 deletions
|
@ -61,6 +61,7 @@ import org.thoughtcrime.securesms.jobs.PushNotificationReceiveJob;
|
||||||
import org.thoughtcrime.securesms.jobs.RefreshPreKeysJob;
|
import org.thoughtcrime.securesms.jobs.RefreshPreKeysJob;
|
||||||
import org.thoughtcrime.securesms.jobs.RetrieveProfileJob;
|
import org.thoughtcrime.securesms.jobs.RetrieveProfileJob;
|
||||||
import org.thoughtcrime.securesms.jobs.RetrieveRemoteAnnouncementsJob;
|
import org.thoughtcrime.securesms.jobs.RetrieveRemoteAnnouncementsJob;
|
||||||
|
import org.thoughtcrime.securesms.jobs.StoryOnboardingDownloadJob;
|
||||||
import org.thoughtcrime.securesms.jobs.SubscriptionKeepAliveJob;
|
import org.thoughtcrime.securesms.jobs.SubscriptionKeepAliveJob;
|
||||||
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
||||||
import org.thoughtcrime.securesms.logging.CustomSignalProtocolLogger;
|
import org.thoughtcrime.securesms.logging.CustomSignalProtocolLogger;
|
||||||
|
@ -203,6 +204,7 @@ public class ApplicationContext extends MultiDexApplication implements AppForegr
|
||||||
.addPostRender(() -> ApplicationDependencies.getJobManager().add(new FontDownloaderJob()))
|
.addPostRender(() -> ApplicationDependencies.getJobManager().add(new FontDownloaderJob()))
|
||||||
.addPostRender(CheckServiceReachabilityJob::enqueueIfNecessary)
|
.addPostRender(CheckServiceReachabilityJob::enqueueIfNecessary)
|
||||||
.addPostRender(GroupV2UpdateSelfProfileKeyJob::enqueueForGroupsIfNecessary)
|
.addPostRender(GroupV2UpdateSelfProfileKeyJob::enqueueForGroupsIfNecessary)
|
||||||
|
.addPostRender(StoryOnboardingDownloadJob.Companion::enqueueIfNeeded)
|
||||||
.execute();
|
.execute();
|
||||||
|
|
||||||
Log.d(TAG, "onCreate() took " + (System.currentTimeMillis() - startTime) + " ms");
|
Log.d(TAG, "onCreate() took " + (System.currentTimeMillis() - startTime) + " ms");
|
||||||
|
|
|
@ -38,7 +38,7 @@ class InternalSettingsRepository(context: Context) {
|
||||||
val recipientId = SignalStore.releaseChannelValues().releaseChannelRecipientId!!
|
val recipientId = SignalStore.releaseChannelValues().releaseChannelRecipientId!!
|
||||||
val threadId = SignalDatabase.threads.getOrCreateThreadIdFor(Recipient.resolved(recipientId))
|
val threadId = SignalDatabase.threads.getOrCreateThreadIdFor(Recipient.resolved(recipientId))
|
||||||
|
|
||||||
val insertResult: MessageDatabase.InsertResult? = ReleaseChannel.insertAnnouncement(
|
val insertResult: MessageDatabase.InsertResult? = ReleaseChannel.insertReleaseChannelMessage(
|
||||||
recipientId = recipientId,
|
recipientId = recipientId,
|
||||||
body = body,
|
body = body,
|
||||||
threadId = threadId,
|
threadId = threadId,
|
||||||
|
|
|
@ -201,7 +201,7 @@ public abstract class MessageDatabase extends Database implements MmsSmsColumns
|
||||||
public abstract boolean hasSelfReplyInGroupStory(long parentStoryId);
|
public abstract boolean hasSelfReplyInGroupStory(long parentStoryId);
|
||||||
public abstract @NonNull Cursor getStoryReplies(long parentStoryId);
|
public abstract @NonNull Cursor getStoryReplies(long parentStoryId);
|
||||||
public abstract @Nullable Long getOldestStorySendTimestamp();
|
public abstract @Nullable Long getOldestStorySendTimestamp();
|
||||||
public abstract int deleteStoriesOlderThan(long timestamp);
|
public abstract int deleteStoriesOlderThan(long timestamp, boolean hasSeenReleaseChannelStories);
|
||||||
public abstract @NonNull MessageDatabase.Reader getUnreadStories(@NonNull RecipientId recipientId, int limit);
|
public abstract @NonNull MessageDatabase.Reader getUnreadStories(@NonNull RecipientId recipientId, int limit);
|
||||||
public abstract @Nullable ParentStoryId.GroupReply getParentStoryIdForGroupReply(long messageId);
|
public abstract @Nullable ParentStoryId.GroupReply getParentStoryIdForGroupReply(long messageId);
|
||||||
public abstract void deleteGroupStoryReplies(long parentStoryId);
|
public abstract void deleteGroupStoryReplies(long parentStoryId);
|
||||||
|
@ -728,9 +728,38 @@ public abstract class MessageDatabase extends Database implements MmsSmsColumns
|
||||||
void onComplete();
|
void onComplete();
|
||||||
}
|
}
|
||||||
|
|
||||||
public interface Reader extends Closeable {
|
/**
|
||||||
|
* Allows the developer to safely iterate over and close a cursor containing
|
||||||
|
* data for MessageRecord objects. Supports for-each loops as well as try-with-resources
|
||||||
|
* blocks.
|
||||||
|
*
|
||||||
|
* Readers are considered "one-shot" and it's on the caller to decide what needs
|
||||||
|
* to be done with the data. Once read, a reader cannot be read from again. This
|
||||||
|
* is by design, since reading data out of a cursor involves object creations and
|
||||||
|
* lookups, so it is in the best interest of app performance to only read out the
|
||||||
|
* data once. If you need to parse the list multiple times, it is recommended that
|
||||||
|
* you copy the iterable out into a normal List, or use extension methods such as
|
||||||
|
* partition.
|
||||||
|
*
|
||||||
|
* This reader does not support removal, since this would be considered a destructive
|
||||||
|
* database call.
|
||||||
|
*/
|
||||||
|
public interface Reader extends Closeable, Iterable<MessageRecord> {
|
||||||
|
/**
|
||||||
|
* @deprecated Use the Iterable interface instead.
|
||||||
|
*/
|
||||||
|
@Deprecated
|
||||||
MessageRecord getNext();
|
MessageRecord getNext();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @deprecated Use the Iterable interface instead.
|
||||||
|
*/
|
||||||
|
@Deprecated
|
||||||
MessageRecord getCurrent();
|
MessageRecord getCurrent();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* From the {@link Closeable} interface, removing the IOException requirement.
|
||||||
|
*/
|
||||||
void close();
|
void close();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -98,9 +98,11 @@ import java.util.Collection;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
|
import java.util.Iterator;
|
||||||
import java.util.LinkedList;
|
import java.util.LinkedList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import java.util.NoSuchElementException;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
@ -917,26 +919,28 @@ public class MmsDatabase extends MessageDatabase {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int deleteStoriesOlderThan(long timestamp) {
|
public int deleteStoriesOlderThan(long timestamp, boolean hasSeenReleaseChannelStories) {
|
||||||
SQLiteDatabase db = databaseHelper.getSignalWritableDatabase();
|
SQLiteDatabase db = databaseHelper.getSignalWritableDatabase();
|
||||||
|
|
||||||
db.beginTransaction();
|
db.beginTransaction();
|
||||||
try {
|
try {
|
||||||
String storiesBeforeTimestampWhere = IS_STORY_CLAUSE + " AND " + DATE_SENT + " < ?";
|
RecipientId releaseChannelRecipient = hasSeenReleaseChannelStories ? null : SignalStore.releaseChannelValues().getReleaseChannelRecipientId();
|
||||||
String[] sharedArgs = SqlUtil.buildArgs(timestamp);
|
long releaseChannelThreadId = releaseChannelRecipient != null ? SignalDatabase.threads().getOrCreateThreadIdFor(Recipient.resolved(releaseChannelRecipient)) : -1;
|
||||||
String deleteStoryRepliesQuery = "DELETE FROM " + TABLE_NAME + " " +
|
String storiesBeforeTimestampWhere = IS_STORY_CLAUSE + " AND " + DATE_SENT + " < ? AND " + THREAD_ID + " != ?";
|
||||||
"WHERE " + PARENT_STORY_ID + " > 0 AND " + PARENT_STORY_ID + " IN (" +
|
String[] sharedArgs = SqlUtil.buildArgs(timestamp, releaseChannelThreadId);
|
||||||
"SELECT " + ID + " " +
|
String deleteStoryRepliesQuery = "DELETE FROM " + TABLE_NAME + " " +
|
||||||
"FROM " + TABLE_NAME + " " +
|
"WHERE " + PARENT_STORY_ID + " > 0 AND " + PARENT_STORY_ID + " IN (" +
|
||||||
"WHERE " + storiesBeforeTimestampWhere +
|
"SELECT " + ID + " " +
|
||||||
")";
|
"FROM " + TABLE_NAME + " " +
|
||||||
String disassociateQuoteQuery = "UPDATE " + TABLE_NAME + " " +
|
"WHERE " + storiesBeforeTimestampWhere +
|
||||||
"SET " + QUOTE_MISSING + " = 1, " + QUOTE_BODY + " = '' " +
|
")";
|
||||||
"WHERE " + PARENT_STORY_ID + " < 0 AND ABS(" + PARENT_STORY_ID + ") IN (" +
|
String disassociateQuoteQuery = "UPDATE " + TABLE_NAME + " " +
|
||||||
"SELECT " + ID + " " +
|
"SET " + QUOTE_MISSING + " = 1, " + QUOTE_BODY + " = '' " +
|
||||||
"FROM " + TABLE_NAME + " " +
|
"WHERE " + PARENT_STORY_ID + " < 0 AND ABS(" + PARENT_STORY_ID + ") IN (" +
|
||||||
"WHERE " + storiesBeforeTimestampWhere +
|
"SELECT " + ID + " " +
|
||||||
")";
|
"FROM " + TABLE_NAME + " " +
|
||||||
|
"WHERE " + storiesBeforeTimestampWhere +
|
||||||
|
")";
|
||||||
|
|
||||||
db.execSQL(deleteStoryRepliesQuery, sharedArgs);
|
db.execSQL(deleteStoryRepliesQuery, sharedArgs);
|
||||||
db.execSQL(disassociateQuoteQuery, sharedArgs);
|
db.execSQL(disassociateQuoteQuery, sharedArgs);
|
||||||
|
@ -2631,6 +2635,15 @@ public class MmsDatabase extends MessageDatabase {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* MessageRecord reader which implements the Iterable interface. This allows it to
|
||||||
|
* be used with many Kotlin Extension Functions as well as with for-each loops.
|
||||||
|
*
|
||||||
|
* Note that it's the responsibility of the developer using the reader to ensure that:
|
||||||
|
*
|
||||||
|
* 1. They only utilize one of the two interfaces (legacy or iterator)
|
||||||
|
* 1. They close this reader after use, preferably via try-with-resources or a use block.
|
||||||
|
*/
|
||||||
public static class Reader implements MessageDatabase.Reader {
|
public static class Reader implements MessageDatabase.Reader {
|
||||||
|
|
||||||
private final Cursor cursor;
|
private final Cursor cursor;
|
||||||
|
@ -2854,6 +2867,29 @@ public class MmsDatabase extends MessageDatabase {
|
||||||
cursor.close();
|
cursor.close();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
@Override
|
||||||
|
public Iterator<MessageRecord> iterator() {
|
||||||
|
return new ReaderIterator();
|
||||||
|
}
|
||||||
|
|
||||||
|
private class ReaderIterator implements Iterator<MessageRecord> {
|
||||||
|
@Override
|
||||||
|
public boolean hasNext() {
|
||||||
|
return cursor != null && cursor.getCount() != 0 && !cursor.isLast();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public MessageRecord next() {
|
||||||
|
MessageRecord record = getNext();
|
||||||
|
if (record == null) {
|
||||||
|
throw new NoSuchElementException();
|
||||||
|
}
|
||||||
|
|
||||||
|
return record;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private long generatePduCompatTimestamp(long time) {
|
private long generatePduCompatTimestamp(long time) {
|
||||||
|
|
|
@ -1477,7 +1477,7 @@ public class SmsDatabase extends MessageDatabase {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int deleteStoriesOlderThan(long timestamp) {
|
public int deleteStoriesOlderThan(long timestamp, boolean hasSeenReleaseChannelStories) {
|
||||||
throw new UnsupportedOperationException();
|
throw new UnsupportedOperationException();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -184,6 +184,7 @@ public final class JobManagerFactories {
|
||||||
put(StorageSyncJob.KEY, new StorageSyncJob.Factory());
|
put(StorageSyncJob.KEY, new StorageSyncJob.Factory());
|
||||||
put(SubscriptionKeepAliveJob.KEY, new SubscriptionKeepAliveJob.Factory());
|
put(SubscriptionKeepAliveJob.KEY, new SubscriptionKeepAliveJob.Factory());
|
||||||
put(SubscriptionReceiptRequestResponseJob.KEY, new SubscriptionReceiptRequestResponseJob.Factory());
|
put(SubscriptionReceiptRequestResponseJob.KEY, new SubscriptionReceiptRequestResponseJob.Factory());
|
||||||
|
put(StoryOnboardingDownloadJob.KEY, new StoryOnboardingDownloadJob.Factory());
|
||||||
put(SubmitRateLimitPushChallengeJob.KEY, new SubmitRateLimitPushChallengeJob.Factory());
|
put(SubmitRateLimitPushChallengeJob.KEY, new SubmitRateLimitPushChallengeJob.Factory());
|
||||||
put(ThreadUpdateJob.KEY, new ThreadUpdateJob.Factory());
|
put(ThreadUpdateJob.KEY, new ThreadUpdateJob.Factory());
|
||||||
put(TrimThreadJob.KEY, new TrimThreadJob.Factory());
|
put(TrimThreadJob.KEY, new TrimThreadJob.Factory());
|
||||||
|
|
|
@ -199,7 +199,7 @@ class RetrieveRemoteAnnouncementsJob private constructor(private val force: Bool
|
||||||
}
|
}
|
||||||
|
|
||||||
ThreadUtil.sleep(5)
|
ThreadUtil.sleep(5)
|
||||||
val insertResult: MessageDatabase.InsertResult? = ReleaseChannel.insertAnnouncement(
|
val insertResult: MessageDatabase.InsertResult? = ReleaseChannel.insertReleaseChannelMessage(
|
||||||
recipientId = values.releaseChannelRecipientId!!,
|
recipientId = values.releaseChannelRecipientId!!,
|
||||||
body = body,
|
body = body,
|
||||||
threadId = threadId,
|
threadId = threadId,
|
||||||
|
|
|
@ -0,0 +1,196 @@
|
||||||
|
package org.thoughtcrime.securesms.jobs
|
||||||
|
|
||||||
|
import androidx.core.os.LocaleListCompat
|
||||||
|
import com.fasterxml.jackson.core.JsonParseException
|
||||||
|
import org.json.JSONArray
|
||||||
|
import org.json.JSONObject
|
||||||
|
import org.signal.core.util.logging.Log
|
||||||
|
import org.thoughtcrime.securesms.database.MessageDatabase
|
||||||
|
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||||
|
import org.thoughtcrime.securesms.database.model.StoryType
|
||||||
|
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
|
||||||
|
import org.thoughtcrime.securesms.jobmanager.Data
|
||||||
|
import org.thoughtcrime.securesms.jobmanager.Job
|
||||||
|
import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint
|
||||||
|
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||||
|
import org.thoughtcrime.securesms.recipients.Recipient
|
||||||
|
import org.thoughtcrime.securesms.releasechannel.ReleaseChannel
|
||||||
|
import org.thoughtcrime.securesms.s3.S3
|
||||||
|
import org.thoughtcrime.securesms.stories.Stories
|
||||||
|
import org.thoughtcrime.securesms.transport.RetryLaterException
|
||||||
|
import org.whispersystems.signalservice.api.push.exceptions.NonSuccessfulResponseCodeException
|
||||||
|
import java.util.Locale
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Kicks off the necessary work to download the resources for the onboarding story.
|
||||||
|
*/
|
||||||
|
class StoryOnboardingDownloadJob private constructor(parameters: Parameters) : BaseJob(parameters) {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
|
||||||
|
private const val ONBOARDING_MANIFEST_ENDPOINT = "${S3.DYNAMIC_PATH}/android/stories/onboarding/manifest.json"
|
||||||
|
private const val ONBOARDING_IMAGE_PATH = "${S3.STATIC_PATH}/android/stories/onboarding"
|
||||||
|
private const val ONBOARDING_EXTENSION = ".jpg"
|
||||||
|
private const val ONBOARDING_IMAGE_COUNT = 5
|
||||||
|
private const val ONBOARDING_IMAGE_WIDTH = 1125
|
||||||
|
private const val ONBOARDING_IMAGE_HEIGHT = 1998
|
||||||
|
|
||||||
|
const val KEY = "StoryOnboardingDownloadJob"
|
||||||
|
|
||||||
|
private val TAG = Log.tag(StoryOnboardingDownloadJob::class.java)
|
||||||
|
|
||||||
|
private fun create(): Job {
|
||||||
|
return StoryOnboardingDownloadJob(
|
||||||
|
Parameters.Builder()
|
||||||
|
.addConstraint(NetworkConstraint.KEY)
|
||||||
|
.setQueue("StoryOnboardingDownloadJob")
|
||||||
|
.setMaxInstancesForFactory(1)
|
||||||
|
.setMaxAttempts(3)
|
||||||
|
.build()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun enqueueIfNeeded() {
|
||||||
|
if (SignalStore.storyValues().hasDownloadedOnboardingStory || !Stories.isFeatureAvailable()) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
Log.d(TAG, "Attempting to enqueue StoryOnboardingDownloadJob...")
|
||||||
|
ApplicationDependencies.getJobManager()
|
||||||
|
.startChain(CreateReleaseChannelJob.create())
|
||||||
|
.then(create())
|
||||||
|
.enqueue()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun serialize(): Data = Data.EMPTY
|
||||||
|
override fun getFactoryKey(): String = KEY
|
||||||
|
override fun onFailure() = Unit
|
||||||
|
|
||||||
|
override fun onRun() {
|
||||||
|
if (SignalStore.storyValues().hasDownloadedOnboardingStory) {
|
||||||
|
Log.i(TAG, "Already downloaded onboarding story. Exiting.")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val releaseChannelRecipientId = SignalStore.releaseChannelValues().releaseChannelRecipientId
|
||||||
|
if (releaseChannelRecipientId == null) {
|
||||||
|
Log.w(TAG, "Cannot create story onboarding without release channel recipient.")
|
||||||
|
throw Exception("No release channel recipient.")
|
||||||
|
}
|
||||||
|
|
||||||
|
SignalDatabase.mms.getAllStoriesFor(releaseChannelRecipientId).use { reader ->
|
||||||
|
reader.forEach { messageRecord ->
|
||||||
|
SignalDatabase.mms.deleteMessage(messageRecord.id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val manifest: JSONObject = try {
|
||||||
|
JSONObject(S3.getString(ONBOARDING_MANIFEST_ENDPOINT))
|
||||||
|
} catch (e: JsonParseException) {
|
||||||
|
Log.w(TAG, "Returned data could not be parsed into JSON", e)
|
||||||
|
throw e
|
||||||
|
} catch (e: NonSuccessfulResponseCodeException) {
|
||||||
|
Log.w(TAG, "Returned non-successful response code from server.", e)
|
||||||
|
throw RetryLaterException()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!manifest.has("languages")) {
|
||||||
|
Log.w(TAG, "Could not find languages set in manifest.")
|
||||||
|
throw Exception("Could not find languages set in manifest.")
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!manifest.has("version")) {
|
||||||
|
Log.w(TAG, "Could not find version in manifest")
|
||||||
|
throw Exception("Could not find version in manifest.")
|
||||||
|
}
|
||||||
|
|
||||||
|
val version = manifest.getString("version")
|
||||||
|
Log.i(TAG, "Using images for manifest version $version")
|
||||||
|
|
||||||
|
val languages = manifest.getJSONObject("languages")
|
||||||
|
val languageCodeCandidates: List<String> = getLocaleCodes()
|
||||||
|
var candidateArray: JSONArray? = null
|
||||||
|
for (candidate in languageCodeCandidates) {
|
||||||
|
if (languages.has(candidate)) {
|
||||||
|
candidateArray = languages.getJSONArray(candidate)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (candidateArray == null) {
|
||||||
|
Log.w(TAG, "Could not find a candidate for the language set: $languageCodeCandidates")
|
||||||
|
throw Exception("Failed to locate onboarding image set.")
|
||||||
|
}
|
||||||
|
|
||||||
|
val threadId = SignalDatabase.threads.getOrCreateThreadIdFor(Recipient.resolved(releaseChannelRecipientId))
|
||||||
|
|
||||||
|
Log.i(TAG, "Inserting messages...")
|
||||||
|
val insertResults: List<MessageDatabase.InsertResult> = (0 until candidateArray.length()).mapNotNull {
|
||||||
|
val insertResult: MessageDatabase.InsertResult? = ReleaseChannel.insertReleaseChannelMessage(
|
||||||
|
releaseChannelRecipientId,
|
||||||
|
"",
|
||||||
|
threadId,
|
||||||
|
"$ONBOARDING_IMAGE_PATH/$version/${candidateArray.getString(it)}$ONBOARDING_EXTENSION",
|
||||||
|
ONBOARDING_IMAGE_WIDTH,
|
||||||
|
ONBOARDING_IMAGE_HEIGHT,
|
||||||
|
storyType = StoryType.STORY_WITHOUT_REPLIES
|
||||||
|
)
|
||||||
|
|
||||||
|
Thread.sleep(5)
|
||||||
|
|
||||||
|
insertResult
|
||||||
|
}
|
||||||
|
|
||||||
|
if (insertResults.size != ONBOARDING_IMAGE_COUNT) {
|
||||||
|
Log.w(TAG, "Failed to insert some search results. Deleting the ones we added and trying again later.")
|
||||||
|
insertResults.forEach {
|
||||||
|
SignalDatabase.mms.deleteMessage(it.messageId)
|
||||||
|
}
|
||||||
|
|
||||||
|
throw RetryLaterException()
|
||||||
|
}
|
||||||
|
|
||||||
|
Log.d(TAG, "Marking onboarding story downloaded.")
|
||||||
|
SignalStore.storyValues().hasDownloadedOnboardingStory = true
|
||||||
|
|
||||||
|
Log.i(TAG, "Enqueueing download jobs...")
|
||||||
|
insertResults.forEach { insertResult ->
|
||||||
|
SignalDatabase.attachments.getAttachmentsForMessage(insertResult.messageId).forEach {
|
||||||
|
ApplicationDependencies.getJobManager().add(AttachmentDownloadJob(insertResult.messageId, it.attachmentId, true))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onShouldRetry(e: Exception): Boolean = e is RetryLaterException
|
||||||
|
|
||||||
|
private fun getLocaleCodes(): List<String> {
|
||||||
|
val localeList: LocaleListCompat = LocaleListCompat.getDefault()
|
||||||
|
|
||||||
|
val potentialOnboardingUrlLanguages = mutableListOf<String>()
|
||||||
|
|
||||||
|
if (SignalStore.settings().language != "zz") {
|
||||||
|
potentialOnboardingUrlLanguages += SignalStore.settings().language
|
||||||
|
}
|
||||||
|
|
||||||
|
for (index in 0 until localeList.size()) {
|
||||||
|
val locale: Locale = localeList.get(index)
|
||||||
|
if (locale.language.isNotEmpty()) {
|
||||||
|
if (locale.country.isNotEmpty()) {
|
||||||
|
potentialOnboardingUrlLanguages += "${locale.language}_${locale.country}"
|
||||||
|
}
|
||||||
|
potentialOnboardingUrlLanguages += locale.language
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
potentialOnboardingUrlLanguages += "en"
|
||||||
|
|
||||||
|
return potentialOnboardingUrlLanguages
|
||||||
|
}
|
||||||
|
|
||||||
|
class Factory : Job.Factory<StoryOnboardingDownloadJob> {
|
||||||
|
override fun create(parameters: Parameters, data: Data): StoryOnboardingDownloadJob {
|
||||||
|
return StoryOnboardingDownloadJob(parameters)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -39,6 +39,16 @@ internal class StoryValues(store: KeyValueStore) : SignalStoreValues(store) {
|
||||||
* Whether or not the user has see the "Navigation education" view
|
* Whether or not the user has see the "Navigation education" view
|
||||||
*/
|
*/
|
||||||
private const val USER_HAS_SEEN_FIRST_NAV_VIEW = "stories.user.has.seen.first.navigation.view"
|
private const val USER_HAS_SEEN_FIRST_NAV_VIEW = "stories.user.has.seen.first.navigation.view"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether or not the onboarding story has been downloaded.
|
||||||
|
*/
|
||||||
|
private const val HAS_DOWNLOADED_ONBOARDING_STORY = "stories.has.downloaded.onboarding"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Marks whether the user has seen the onboarding story
|
||||||
|
*/
|
||||||
|
private const val USER_HAS_SEEN_ONBOARDING_STORY = "stories.user.has.seen.onboarding"
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onFirstEverAppLaunch() = Unit
|
override fun onFirstEverAppLaunch() = Unit
|
||||||
|
@ -48,7 +58,8 @@ internal class StoryValues(store: KeyValueStore) : SignalStoreValues(store) {
|
||||||
USER_HAS_ADDED_TO_A_STORY,
|
USER_HAS_ADDED_TO_A_STORY,
|
||||||
VIDEO_TOOLTIP_SEEN_MARKER,
|
VIDEO_TOOLTIP_SEEN_MARKER,
|
||||||
CANNOT_SEND_SEEN_MARKER,
|
CANNOT_SEND_SEEN_MARKER,
|
||||||
USER_HAS_SEEN_FIRST_NAV_VIEW
|
USER_HAS_SEEN_FIRST_NAV_VIEW,
|
||||||
|
HAS_DOWNLOADED_ONBOARDING_STORY
|
||||||
)
|
)
|
||||||
|
|
||||||
var isFeatureDisabled: Boolean by booleanValue(MANUAL_FEATURE_DISABLE, false)
|
var isFeatureDisabled: Boolean by booleanValue(MANUAL_FEATURE_DISABLE, false)
|
||||||
|
@ -63,6 +74,10 @@ internal class StoryValues(store: KeyValueStore) : SignalStoreValues(store) {
|
||||||
|
|
||||||
var userHasSeenFirstNavView: Boolean by booleanValue(USER_HAS_SEEN_FIRST_NAV_VIEW, false)
|
var userHasSeenFirstNavView: Boolean by booleanValue(USER_HAS_SEEN_FIRST_NAV_VIEW, false)
|
||||||
|
|
||||||
|
var hasDownloadedOnboardingStory: Boolean by booleanValue(HAS_DOWNLOADED_ONBOARDING_STORY, false)
|
||||||
|
|
||||||
|
var userHasSeenOnboardingStory: Boolean by booleanValue(USER_HAS_SEEN_ONBOARDING_STORY, false)
|
||||||
|
|
||||||
fun setLatestStorySend(storySend: StorySend) {
|
fun setLatestStorySend(storySend: StorySend) {
|
||||||
synchronized(this) {
|
synchronized(this) {
|
||||||
val storySends: List<StorySend> = getList(LATEST_STORY_SENDS, StorySendSerializer)
|
val storySends: List<StorySend> = getList(LATEST_STORY_SENDS, StorySendSerializer)
|
||||||
|
|
|
@ -3,6 +3,7 @@ package org.thoughtcrime.securesms.releasechannel
|
||||||
import org.thoughtcrime.securesms.attachments.PointerAttachment
|
import org.thoughtcrime.securesms.attachments.PointerAttachment
|
||||||
import org.thoughtcrime.securesms.database.MessageDatabase
|
import org.thoughtcrime.securesms.database.MessageDatabase
|
||||||
import org.thoughtcrime.securesms.database.SignalDatabase
|
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||||
|
import org.thoughtcrime.securesms.database.model.StoryType
|
||||||
import org.thoughtcrime.securesms.database.model.databaseprotos.BodyRangeList
|
import org.thoughtcrime.securesms.database.model.databaseprotos.BodyRangeList
|
||||||
import org.thoughtcrime.securesms.mms.IncomingMediaMessage
|
import org.thoughtcrime.securesms.mms.IncomingMediaMessage
|
||||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||||
|
@ -19,7 +20,7 @@ object ReleaseChannel {
|
||||||
|
|
||||||
const val CDN_NUMBER = -1
|
const val CDN_NUMBER = -1
|
||||||
|
|
||||||
fun insertAnnouncement(
|
fun insertReleaseChannelMessage(
|
||||||
recipientId: RecipientId,
|
recipientId: RecipientId,
|
||||||
body: String,
|
body: String,
|
||||||
threadId: Long,
|
threadId: Long,
|
||||||
|
@ -27,7 +28,8 @@ object ReleaseChannel {
|
||||||
imageWidth: Int = 0,
|
imageWidth: Int = 0,
|
||||||
imageHeight: Int = 0,
|
imageHeight: Int = 0,
|
||||||
serverUuid: String? = UUID.randomUUID().toString(),
|
serverUuid: String? = UUID.randomUUID().toString(),
|
||||||
messageRanges: BodyRangeList? = null
|
messageRanges: BodyRangeList? = null,
|
||||||
|
storyType: StoryType = StoryType.NONE
|
||||||
): MessageDatabase.InsertResult? {
|
): MessageDatabase.InsertResult? {
|
||||||
|
|
||||||
val attachments: Optional<List<SignalServiceAttachment>> = if (image != null) {
|
val attachments: Optional<List<SignalServiceAttachment>> = if (image != null) {
|
||||||
|
@ -63,7 +65,8 @@ object ReleaseChannel {
|
||||||
body = body,
|
body = body,
|
||||||
attachments = PointerAttachment.forPointers(attachments),
|
attachments = PointerAttachment.forPointers(attachments),
|
||||||
serverGuid = serverUuid,
|
serverGuid = serverUuid,
|
||||||
messageRanges = messageRanges
|
messageRanges = messageRanges,
|
||||||
|
storyType = storyType
|
||||||
)
|
)
|
||||||
|
|
||||||
return SignalDatabase.mms.insertSecureDecryptedMessageInbox(message, threadId).orElse(null)
|
return SignalDatabase.mms.insertSecureDecryptedMessageInbox(message, threadId).orElse(null)
|
||||||
|
|
|
@ -8,6 +8,7 @@ import androidx.annotation.WorkerThread
|
||||||
import org.signal.core.util.logging.Log
|
import org.signal.core.util.logging.Log
|
||||||
import org.thoughtcrime.securesms.database.SignalDatabase
|
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
|
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
|
||||||
|
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -43,7 +44,7 @@ class ExpiringStoriesManager(
|
||||||
@WorkerThread
|
@WorkerThread
|
||||||
override fun executeEvent(event: Event) {
|
override fun executeEvent(event: Event) {
|
||||||
val threshold = System.currentTimeMillis() - STORY_LIFESPAN
|
val threshold = System.currentTimeMillis() - STORY_LIFESPAN
|
||||||
val deletes = mmsDatabase.deleteStoriesOlderThan(threshold)
|
val deletes = mmsDatabase.deleteStoriesOlderThan(threshold, SignalStore.storyValues().userHasSeenOnboardingStory)
|
||||||
Log.i(TAG, "Deleted $deletes stories before $threshold")
|
Log.i(TAG, "Deleted $deletes stories before $threshold")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -88,6 +88,7 @@ object StoryContextMenu {
|
||||||
anchorView = anchorView,
|
anchorView = anchorView,
|
||||||
isFromSelf = model.data.primaryStory.messageRecord.isOutgoing,
|
isFromSelf = model.data.primaryStory.messageRecord.isOutgoing,
|
||||||
isToGroup = model.data.storyRecipient.isGroup,
|
isToGroup = model.data.storyRecipient.isGroup,
|
||||||
|
isFromReleaseChannel = model.data.storyRecipient.isReleaseNotes,
|
||||||
canHide = !model.data.isHidden,
|
canHide = !model.data.isHidden,
|
||||||
callbacks = object : Callbacks {
|
callbacks = object : Callbacks {
|
||||||
override fun onHide() = model.onHideStory(model)
|
override fun onHide() = model.onHideStory(model)
|
||||||
|
@ -120,6 +121,7 @@ object StoryContextMenu {
|
||||||
anchorView = anchorView,
|
anchorView = anchorView,
|
||||||
isFromSelf = selectedStory.sender.isSelf,
|
isFromSelf = selectedStory.sender.isSelf,
|
||||||
isToGroup = selectedStory.group != null,
|
isToGroup = selectedStory.group != null,
|
||||||
|
isFromReleaseChannel = selectedStory.sender.isReleaseNotes,
|
||||||
canHide = true,
|
canHide = true,
|
||||||
callbacks = object : Callbacks {
|
callbacks = object : Callbacks {
|
||||||
override fun onHide() = onHide(selectedStory)
|
override fun onHide() = onHide(selectedStory)
|
||||||
|
@ -145,6 +147,7 @@ object StoryContextMenu {
|
||||||
anchorView = anchorView,
|
anchorView = anchorView,
|
||||||
isFromSelf = true,
|
isFromSelf = true,
|
||||||
isToGroup = false,
|
isToGroup = false,
|
||||||
|
isFromReleaseChannel = false,
|
||||||
canHide = false,
|
canHide = false,
|
||||||
callbacks = object : Callbacks {
|
callbacks = object : Callbacks {
|
||||||
override fun onHide() = throw NotImplementedError()
|
override fun onHide() = throw NotImplementedError()
|
||||||
|
@ -164,6 +167,7 @@ object StoryContextMenu {
|
||||||
anchorView: View,
|
anchorView: View,
|
||||||
isFromSelf: Boolean,
|
isFromSelf: Boolean,
|
||||||
isToGroup: Boolean,
|
isToGroup: Boolean,
|
||||||
|
isFromReleaseChannel: Boolean,
|
||||||
rootView: ViewGroup = anchorView.rootView as ViewGroup,
|
rootView: ViewGroup = anchorView.rootView as ViewGroup,
|
||||||
canHide: Boolean,
|
canHide: Boolean,
|
||||||
callbacks: Callbacks
|
callbacks: Callbacks
|
||||||
|
@ -208,7 +212,7 @@ object StoryContextMenu {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isToGroup || !isFromSelf) {
|
if ((isToGroup || !isFromSelf) && !isFromReleaseChannel) {
|
||||||
add(
|
add(
|
||||||
ActionItem(R.drawable.ic_open_24_tinted, context.getString(R.string.StoriesLandingItem__go_to_chat)) {
|
ActionItem(R.drawable.ic_open_24_tinted, context.getString(R.string.StoriesLandingItem__go_to_chat)) {
|
||||||
callbacks.onGoToChat()
|
callbacks.onGoToChat()
|
||||||
|
|
|
@ -2,6 +2,7 @@ package org.thoughtcrime.securesms.stories.landing
|
||||||
|
|
||||||
import android.graphics.Color
|
import android.graphics.Color
|
||||||
import android.graphics.drawable.Drawable
|
import android.graphics.drawable.Drawable
|
||||||
|
import android.text.SpannableStringBuilder
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.widget.ImageView
|
import android.widget.ImageView
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
|
@ -19,6 +20,7 @@ import org.thoughtcrime.securesms.mms.GlideApp
|
||||||
import org.thoughtcrime.securesms.recipients.Recipient
|
import org.thoughtcrime.securesms.recipients.Recipient
|
||||||
import org.thoughtcrime.securesms.stories.StoryTextPostModel
|
import org.thoughtcrime.securesms.stories.StoryTextPostModel
|
||||||
import org.thoughtcrime.securesms.stories.dialogs.StoryContextMenu
|
import org.thoughtcrime.securesms.stories.dialogs.StoryContextMenu
|
||||||
|
import org.thoughtcrime.securesms.util.ContextUtil
|
||||||
import org.thoughtcrime.securesms.util.DateUtils
|
import org.thoughtcrime.securesms.util.DateUtils
|
||||||
import org.thoughtcrime.securesms.util.SpanUtil
|
import org.thoughtcrime.securesms.util.SpanUtil
|
||||||
import org.thoughtcrime.securesms.util.adapter.mapping.LayoutFactory
|
import org.thoughtcrime.securesms.util.adapter.mapping.LayoutFactory
|
||||||
|
@ -188,6 +190,7 @@ object StoriesLandingItem {
|
||||||
sender.text = when {
|
sender.text = when {
|
||||||
model.data.storyRecipient.isMyStory -> context.getText(R.string.StoriesLandingFragment__my_stories)
|
model.data.storyRecipient.isMyStory -> context.getText(R.string.StoriesLandingFragment__my_stories)
|
||||||
model.data.storyRecipient.isGroup -> getGroupPresentation(model)
|
model.data.storyRecipient.isGroup -> getGroupPresentation(model)
|
||||||
|
model.data.storyRecipient.isReleaseNotes -> getReleaseNotesPresentation(model)
|
||||||
else -> model.data.storyRecipient.getDisplayName(context)
|
else -> model.data.storyRecipient.getDisplayName(context)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -245,6 +248,15 @@ object StoriesLandingItem {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun getReleaseNotesPresentation(model: Model): CharSequence {
|
||||||
|
val official = ContextUtil.requireDrawable(context, R.drawable.ic_official_20)
|
||||||
|
|
||||||
|
val name = SpannableStringBuilder(model.data.storyRecipient.getDisplayName(context))
|
||||||
|
SpanUtil.appendCenteredImageSpan(name, official, 20, 20)
|
||||||
|
|
||||||
|
return name
|
||||||
|
}
|
||||||
|
|
||||||
private fun getIndividualPresentation(model: Model): String {
|
private fun getIndividualPresentation(model: Model): String {
|
||||||
return if (model.data.primaryStory.messageRecord.isOutgoing) {
|
return if (model.data.primaryStory.messageRecord.isOutgoing) {
|
||||||
context.getString(R.string.Recipient_you)
|
context.getString(R.string.Recipient_you)
|
||||||
|
|
|
@ -21,16 +21,14 @@ data class StoriesLandingItemData(
|
||||||
val failureCount: Long = 0
|
val failureCount: Long = 0
|
||||||
) : Comparable<StoriesLandingItemData> {
|
) : Comparable<StoriesLandingItemData> {
|
||||||
override fun compareTo(other: StoriesLandingItemData): Int {
|
override fun compareTo(other: StoriesLandingItemData): Int {
|
||||||
return if (storyRecipient.isMyStory && !other.storyRecipient.isMyStory) {
|
return when {
|
||||||
-1
|
storyRecipient.isMyStory && !other.storyRecipient.isMyStory -> -1
|
||||||
} else if (!storyRecipient.isMyStory && other.storyRecipient.isMyStory) {
|
!storyRecipient.isMyStory && other.storyRecipient.isMyStory -> 1
|
||||||
1
|
storyRecipient.isReleaseNotes && !other.storyRecipient.isReleaseNotes -> 1
|
||||||
} else if (storyViewState == StoryViewState.UNVIEWED && other.storyViewState != StoryViewState.UNVIEWED) {
|
!storyRecipient.isReleaseNotes && other.storyRecipient.isReleaseNotes -> -1
|
||||||
-1
|
storyViewState == StoryViewState.UNVIEWED && other.storyViewState != StoryViewState.UNVIEWED -> -1
|
||||||
} else if (storyViewState != StoryViewState.UNVIEWED && other.storyViewState == StoryViewState.UNVIEWED) {
|
storyViewState != StoryViewState.UNVIEWED && other.storyViewState == StoryViewState.UNVIEWED -> 1
|
||||||
1
|
else -> -dateInMilliseconds.compareTo(other.dateInMilliseconds)
|
||||||
} else {
|
|
||||||
-dateInMilliseconds.compareTo(other.dateInMilliseconds)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,6 +5,7 @@ import io.reactivex.rxjava3.schedulers.Schedulers
|
||||||
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.StoryViewState
|
import org.thoughtcrime.securesms.database.model.StoryViewState
|
||||||
|
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||||
import org.thoughtcrime.securesms.recipients.Recipient
|
import org.thoughtcrime.securesms.recipients.Recipient
|
||||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||||
|
|
||||||
|
@ -16,6 +17,7 @@ open class StoryViewerRepository {
|
||||||
return Single.create<List<RecipientId>> { emitter ->
|
return Single.create<List<RecipientId>> { emitter ->
|
||||||
val myStoriesId = SignalDatabase.recipients.getOrInsertFromDistributionListId(DistributionListId.MY_STORY)
|
val myStoriesId = SignalDatabase.recipients.getOrInsertFromDistributionListId(DistributionListId.MY_STORY)
|
||||||
val myStories = Recipient.resolved(myStoriesId)
|
val myStories = Recipient.resolved(myStoriesId)
|
||||||
|
val releaseChannelId = SignalStore.releaseChannelValues().releaseChannelRecipientId
|
||||||
val recipientIds = SignalDatabase.mms.orderedStoryRecipientsAndIds.groupBy {
|
val recipientIds = SignalDatabase.mms.orderedStoryRecipientsAndIds.groupBy {
|
||||||
val recipient = Recipient.resolved(it.recipientId)
|
val recipient = Recipient.resolved(it.recipientId)
|
||||||
if (recipient.isDistributionList) {
|
if (recipient.isDistributionList) {
|
||||||
|
@ -42,12 +44,16 @@ open class StoryViewerRepository {
|
||||||
}.map { it.id }
|
}.map { it.id }
|
||||||
|
|
||||||
emitter.onSuccess(
|
emitter.onSuccess(
|
||||||
if (recipientIds.contains(myStoriesId)) {
|
recipientIds.floatToTop(releaseChannelId).floatToTop(myStoriesId)
|
||||||
listOf(myStoriesId) + (recipientIds - myStoriesId)
|
|
||||||
} else {
|
|
||||||
recipientIds
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
}.subscribeOn(Schedulers.io())
|
}.subscribeOn(Schedulers.io())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun List<RecipientId>.floatToTop(recipientId: RecipientId?): List<RecipientId> {
|
||||||
|
return if (recipientId != null && contains(recipientId)) {
|
||||||
|
listOf(recipientId) + (this - recipientId)
|
||||||
|
} else {
|
||||||
|
this
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,6 +19,7 @@ import org.thoughtcrime.securesms.database.model.databaseprotos.StoryTextPost
|
||||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
|
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
|
||||||
import org.thoughtcrime.securesms.jobs.MultiDeviceViewedUpdateJob
|
import org.thoughtcrime.securesms.jobs.MultiDeviceViewedUpdateJob
|
||||||
import org.thoughtcrime.securesms.jobs.SendViewedReceiptJob
|
import org.thoughtcrime.securesms.jobs.SendViewedReceiptJob
|
||||||
|
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||||
import org.thoughtcrime.securesms.recipients.Recipient
|
import org.thoughtcrime.securesms.recipients.Recipient
|
||||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||||
import org.thoughtcrime.securesms.stories.Stories
|
import org.thoughtcrime.securesms.stories.Stories
|
||||||
|
@ -173,19 +174,24 @@ open class StoryViewerPageRepository(context: Context) {
|
||||||
val markedMessageInfo = SignalDatabase.mms.setIncomingMessageViewed(storyPost.id)
|
val markedMessageInfo = SignalDatabase.mms.setIncomingMessageViewed(storyPost.id)
|
||||||
if (markedMessageInfo != null) {
|
if (markedMessageInfo != null) {
|
||||||
ApplicationDependencies.getDatabaseObserver().notifyConversationListListeners()
|
ApplicationDependencies.getDatabaseObserver().notifyConversationListListeners()
|
||||||
ApplicationDependencies.getJobManager().add(
|
|
||||||
SendViewedReceiptJob(
|
|
||||||
markedMessageInfo.threadId,
|
|
||||||
storyPost.sender.id,
|
|
||||||
markedMessageInfo.syncMessageId.timetamp,
|
|
||||||
MessageId(storyPost.id, true)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
MultiDeviceViewedUpdateJob.enqueue(listOf(markedMessageInfo.syncMessageId))
|
|
||||||
|
|
||||||
val recipientId = storyPost.group?.id ?: storyPost.sender.id
|
if (storyPost.sender.isReleaseNotes) {
|
||||||
SignalDatabase.recipients.updateLastStoryViewTimestamp(recipientId)
|
SignalStore.storyValues().userHasSeenOnboardingStory = true
|
||||||
Stories.enqueueNextStoriesForDownload(recipientId, true)
|
} else {
|
||||||
|
ApplicationDependencies.getJobManager().add(
|
||||||
|
SendViewedReceiptJob(
|
||||||
|
markedMessageInfo.threadId,
|
||||||
|
storyPost.sender.id,
|
||||||
|
markedMessageInfo.syncMessageId.timetamp,
|
||||||
|
MessageId(storyPost.id, true)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
MultiDeviceViewedUpdateJob.enqueue(listOf(markedMessageInfo.syncMessageId))
|
||||||
|
|
||||||
|
val recipientId = storyPost.group?.id ?: storyPost.sender.id
|
||||||
|
SignalDatabase.recipients.updateLastStoryViewTimestamp(recipientId)
|
||||||
|
Stories.enqueueNextStoriesForDownload(recipientId, true)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,7 +5,7 @@ import android.graphics.drawable.Drawable;
|
||||||
|
|
||||||
import androidx.annotation.DrawableRes;
|
import androidx.annotation.DrawableRes;
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.core.content.ContextCompat;
|
import androidx.appcompat.content.res.AppCompatResources;
|
||||||
|
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
|
|
||||||
|
@ -13,7 +13,7 @@ public final class ContextUtil {
|
||||||
private ContextUtil() {}
|
private ContextUtil() {}
|
||||||
|
|
||||||
public static @NonNull Drawable requireDrawable(@NonNull Context context, @DrawableRes int drawable) {
|
public static @NonNull Drawable requireDrawable(@NonNull Context context, @DrawableRes int drawable) {
|
||||||
return Objects.requireNonNull(ContextCompat.getDrawable(context, drawable));
|
return Objects.requireNonNull(AppCompatResources.getDrawable(context, drawable));
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Reference in a new issue