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.RetrieveProfileJob;
|
||||
import org.thoughtcrime.securesms.jobs.RetrieveRemoteAnnouncementsJob;
|
||||
import org.thoughtcrime.securesms.jobs.StoryOnboardingDownloadJob;
|
||||
import org.thoughtcrime.securesms.jobs.SubscriptionKeepAliveJob;
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
||||
import org.thoughtcrime.securesms.logging.CustomSignalProtocolLogger;
|
||||
|
@ -203,6 +204,7 @@ public class ApplicationContext extends MultiDexApplication implements AppForegr
|
|||
.addPostRender(() -> ApplicationDependencies.getJobManager().add(new FontDownloaderJob()))
|
||||
.addPostRender(CheckServiceReachabilityJob::enqueueIfNecessary)
|
||||
.addPostRender(GroupV2UpdateSelfProfileKeyJob::enqueueForGroupsIfNecessary)
|
||||
.addPostRender(StoryOnboardingDownloadJob.Companion::enqueueIfNeeded)
|
||||
.execute();
|
||||
|
||||
Log.d(TAG, "onCreate() took " + (System.currentTimeMillis() - startTime) + " ms");
|
||||
|
|
|
@ -38,7 +38,7 @@ class InternalSettingsRepository(context: Context) {
|
|||
val recipientId = SignalStore.releaseChannelValues().releaseChannelRecipientId!!
|
||||
val threadId = SignalDatabase.threads.getOrCreateThreadIdFor(Recipient.resolved(recipientId))
|
||||
|
||||
val insertResult: MessageDatabase.InsertResult? = ReleaseChannel.insertAnnouncement(
|
||||
val insertResult: MessageDatabase.InsertResult? = ReleaseChannel.insertReleaseChannelMessage(
|
||||
recipientId = recipientId,
|
||||
body = body,
|
||||
threadId = threadId,
|
||||
|
|
|
@ -201,7 +201,7 @@ public abstract class MessageDatabase extends Database implements MmsSmsColumns
|
|||
public abstract boolean hasSelfReplyInGroupStory(long parentStoryId);
|
||||
public abstract @NonNull Cursor getStoryReplies(long parentStoryId);
|
||||
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 @Nullable ParentStoryId.GroupReply getParentStoryIdForGroupReply(long messageId);
|
||||
public abstract void deleteGroupStoryReplies(long parentStoryId);
|
||||
|
@ -728,9 +728,38 @@ public abstract class MessageDatabase extends Database implements MmsSmsColumns
|
|||
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();
|
||||
|
||||
/**
|
||||
* @deprecated Use the Iterable interface instead.
|
||||
*/
|
||||
@Deprecated
|
||||
MessageRecord getCurrent();
|
||||
|
||||
/**
|
||||
* From the {@link Closeable} interface, removing the IOException requirement.
|
||||
*/
|
||||
void close();
|
||||
}
|
||||
|
||||
|
|
|
@ -98,9 +98,11 @@ import java.util.Collection;
|
|||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.Iterator;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.NoSuchElementException;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
|
@ -917,26 +919,28 @@ public class MmsDatabase extends MessageDatabase {
|
|||
}
|
||||
|
||||
@Override
|
||||
public int deleteStoriesOlderThan(long timestamp) {
|
||||
public int deleteStoriesOlderThan(long timestamp, boolean hasSeenReleaseChannelStories) {
|
||||
SQLiteDatabase db = databaseHelper.getSignalWritableDatabase();
|
||||
|
||||
db.beginTransaction();
|
||||
try {
|
||||
String storiesBeforeTimestampWhere = IS_STORY_CLAUSE + " AND " + DATE_SENT + " < ?";
|
||||
String[] sharedArgs = SqlUtil.buildArgs(timestamp);
|
||||
String deleteStoryRepliesQuery = "DELETE FROM " + TABLE_NAME + " " +
|
||||
"WHERE " + PARENT_STORY_ID + " > 0 AND " + PARENT_STORY_ID + " IN (" +
|
||||
"SELECT " + ID + " " +
|
||||
"FROM " + TABLE_NAME + " " +
|
||||
"WHERE " + storiesBeforeTimestampWhere +
|
||||
")";
|
||||
String disassociateQuoteQuery = "UPDATE " + TABLE_NAME + " " +
|
||||
"SET " + QUOTE_MISSING + " = 1, " + QUOTE_BODY + " = '' " +
|
||||
"WHERE " + PARENT_STORY_ID + " < 0 AND ABS(" + PARENT_STORY_ID + ") IN (" +
|
||||
"SELECT " + ID + " " +
|
||||
"FROM " + TABLE_NAME + " " +
|
||||
"WHERE " + storiesBeforeTimestampWhere +
|
||||
")";
|
||||
RecipientId releaseChannelRecipient = hasSeenReleaseChannelStories ? null : SignalStore.releaseChannelValues().getReleaseChannelRecipientId();
|
||||
long releaseChannelThreadId = releaseChannelRecipient != null ? SignalDatabase.threads().getOrCreateThreadIdFor(Recipient.resolved(releaseChannelRecipient)) : -1;
|
||||
String storiesBeforeTimestampWhere = IS_STORY_CLAUSE + " AND " + DATE_SENT + " < ? AND " + THREAD_ID + " != ?";
|
||||
String[] sharedArgs = SqlUtil.buildArgs(timestamp, releaseChannelThreadId);
|
||||
String deleteStoryRepliesQuery = "DELETE FROM " + TABLE_NAME + " " +
|
||||
"WHERE " + PARENT_STORY_ID + " > 0 AND " + PARENT_STORY_ID + " IN (" +
|
||||
"SELECT " + ID + " " +
|
||||
"FROM " + TABLE_NAME + " " +
|
||||
"WHERE " + storiesBeforeTimestampWhere +
|
||||
")";
|
||||
String disassociateQuoteQuery = "UPDATE " + TABLE_NAME + " " +
|
||||
"SET " + QUOTE_MISSING + " = 1, " + QUOTE_BODY + " = '' " +
|
||||
"WHERE " + PARENT_STORY_ID + " < 0 AND ABS(" + PARENT_STORY_ID + ") IN (" +
|
||||
"SELECT " + ID + " " +
|
||||
"FROM " + TABLE_NAME + " " +
|
||||
"WHERE " + storiesBeforeTimestampWhere +
|
||||
")";
|
||||
|
||||
db.execSQL(deleteStoryRepliesQuery, 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 {
|
||||
|
||||
private final Cursor cursor;
|
||||
|
@ -2854,6 +2867,29 @@ public class MmsDatabase extends MessageDatabase {
|
|||
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) {
|
||||
|
|
|
@ -1477,7 +1477,7 @@ public class SmsDatabase extends MessageDatabase {
|
|||
}
|
||||
|
||||
@Override
|
||||
public int deleteStoriesOlderThan(long timestamp) {
|
||||
public int deleteStoriesOlderThan(long timestamp, boolean hasSeenReleaseChannelStories) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
|
|
|
@ -184,6 +184,7 @@ public final class JobManagerFactories {
|
|||
put(StorageSyncJob.KEY, new StorageSyncJob.Factory());
|
||||
put(SubscriptionKeepAliveJob.KEY, new SubscriptionKeepAliveJob.Factory());
|
||||
put(SubscriptionReceiptRequestResponseJob.KEY, new SubscriptionReceiptRequestResponseJob.Factory());
|
||||
put(StoryOnboardingDownloadJob.KEY, new StoryOnboardingDownloadJob.Factory());
|
||||
put(SubmitRateLimitPushChallengeJob.KEY, new SubmitRateLimitPushChallengeJob.Factory());
|
||||
put(ThreadUpdateJob.KEY, new ThreadUpdateJob.Factory());
|
||||
put(TrimThreadJob.KEY, new TrimThreadJob.Factory());
|
||||
|
|
|
@ -199,7 +199,7 @@ class RetrieveRemoteAnnouncementsJob private constructor(private val force: Bool
|
|||
}
|
||||
|
||||
ThreadUtil.sleep(5)
|
||||
val insertResult: MessageDatabase.InsertResult? = ReleaseChannel.insertAnnouncement(
|
||||
val insertResult: MessageDatabase.InsertResult? = ReleaseChannel.insertReleaseChannelMessage(
|
||||
recipientId = values.releaseChannelRecipientId!!,
|
||||
body = body,
|
||||
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
|
||||
*/
|
||||
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
|
||||
|
@ -48,7 +58,8 @@ internal class StoryValues(store: KeyValueStore) : SignalStoreValues(store) {
|
|||
USER_HAS_ADDED_TO_A_STORY,
|
||||
VIDEO_TOOLTIP_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)
|
||||
|
@ -63,6 +74,10 @@ internal class StoryValues(store: KeyValueStore) : SignalStoreValues(store) {
|
|||
|
||||
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) {
|
||||
synchronized(this) {
|
||||
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.database.MessageDatabase
|
||||
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.mms.IncomingMediaMessage
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
|
@ -19,7 +20,7 @@ object ReleaseChannel {
|
|||
|
||||
const val CDN_NUMBER = -1
|
||||
|
||||
fun insertAnnouncement(
|
||||
fun insertReleaseChannelMessage(
|
||||
recipientId: RecipientId,
|
||||
body: String,
|
||||
threadId: Long,
|
||||
|
@ -27,7 +28,8 @@ object ReleaseChannel {
|
|||
imageWidth: Int = 0,
|
||||
imageHeight: Int = 0,
|
||||
serverUuid: String? = UUID.randomUUID().toString(),
|
||||
messageRanges: BodyRangeList? = null
|
||||
messageRanges: BodyRangeList? = null,
|
||||
storyType: StoryType = StoryType.NONE
|
||||
): MessageDatabase.InsertResult? {
|
||||
|
||||
val attachments: Optional<List<SignalServiceAttachment>> = if (image != null) {
|
||||
|
@ -63,7 +65,8 @@ object ReleaseChannel {
|
|||
body = body,
|
||||
attachments = PointerAttachment.forPointers(attachments),
|
||||
serverGuid = serverUuid,
|
||||
messageRanges = messageRanges
|
||||
messageRanges = messageRanges,
|
||||
storyType = storyType
|
||||
)
|
||||
|
||||
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.thoughtcrime.securesms.database.SignalDatabase
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
/**
|
||||
|
@ -43,7 +44,7 @@ class ExpiringStoriesManager(
|
|||
@WorkerThread
|
||||
override fun executeEvent(event: Event) {
|
||||
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")
|
||||
}
|
||||
|
||||
|
|
|
@ -88,6 +88,7 @@ object StoryContextMenu {
|
|||
anchorView = anchorView,
|
||||
isFromSelf = model.data.primaryStory.messageRecord.isOutgoing,
|
||||
isToGroup = model.data.storyRecipient.isGroup,
|
||||
isFromReleaseChannel = model.data.storyRecipient.isReleaseNotes,
|
||||
canHide = !model.data.isHidden,
|
||||
callbacks = object : Callbacks {
|
||||
override fun onHide() = model.onHideStory(model)
|
||||
|
@ -120,6 +121,7 @@ object StoryContextMenu {
|
|||
anchorView = anchorView,
|
||||
isFromSelf = selectedStory.sender.isSelf,
|
||||
isToGroup = selectedStory.group != null,
|
||||
isFromReleaseChannel = selectedStory.sender.isReleaseNotes,
|
||||
canHide = true,
|
||||
callbacks = object : Callbacks {
|
||||
override fun onHide() = onHide(selectedStory)
|
||||
|
@ -145,6 +147,7 @@ object StoryContextMenu {
|
|||
anchorView = anchorView,
|
||||
isFromSelf = true,
|
||||
isToGroup = false,
|
||||
isFromReleaseChannel = false,
|
||||
canHide = false,
|
||||
callbacks = object : Callbacks {
|
||||
override fun onHide() = throw NotImplementedError()
|
||||
|
@ -164,6 +167,7 @@ object StoryContextMenu {
|
|||
anchorView: View,
|
||||
isFromSelf: Boolean,
|
||||
isToGroup: Boolean,
|
||||
isFromReleaseChannel: Boolean,
|
||||
rootView: ViewGroup = anchorView.rootView as ViewGroup,
|
||||
canHide: Boolean,
|
||||
callbacks: Callbacks
|
||||
|
@ -208,7 +212,7 @@ object StoryContextMenu {
|
|||
)
|
||||
}
|
||||
|
||||
if (isToGroup || !isFromSelf) {
|
||||
if ((isToGroup || !isFromSelf) && !isFromReleaseChannel) {
|
||||
add(
|
||||
ActionItem(R.drawable.ic_open_24_tinted, context.getString(R.string.StoriesLandingItem__go_to_chat)) {
|
||||
callbacks.onGoToChat()
|
||||
|
|
|
@ -2,6 +2,7 @@ package org.thoughtcrime.securesms.stories.landing
|
|||
|
||||
import android.graphics.Color
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.text.SpannableStringBuilder
|
||||
import android.view.View
|
||||
import android.widget.ImageView
|
||||
import android.widget.TextView
|
||||
|
@ -19,6 +20,7 @@ import org.thoughtcrime.securesms.mms.GlideApp
|
|||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.stories.StoryTextPostModel
|
||||
import org.thoughtcrime.securesms.stories.dialogs.StoryContextMenu
|
||||
import org.thoughtcrime.securesms.util.ContextUtil
|
||||
import org.thoughtcrime.securesms.util.DateUtils
|
||||
import org.thoughtcrime.securesms.util.SpanUtil
|
||||
import org.thoughtcrime.securesms.util.adapter.mapping.LayoutFactory
|
||||
|
@ -188,6 +190,7 @@ object StoriesLandingItem {
|
|||
sender.text = when {
|
||||
model.data.storyRecipient.isMyStory -> context.getText(R.string.StoriesLandingFragment__my_stories)
|
||||
model.data.storyRecipient.isGroup -> getGroupPresentation(model)
|
||||
model.data.storyRecipient.isReleaseNotes -> getReleaseNotesPresentation(model)
|
||||
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 {
|
||||
return if (model.data.primaryStory.messageRecord.isOutgoing) {
|
||||
context.getString(R.string.Recipient_you)
|
||||
|
|
|
@ -21,16 +21,14 @@ data class StoriesLandingItemData(
|
|||
val failureCount: Long = 0
|
||||
) : Comparable<StoriesLandingItemData> {
|
||||
override fun compareTo(other: StoriesLandingItemData): Int {
|
||||
return if (storyRecipient.isMyStory && !other.storyRecipient.isMyStory) {
|
||||
-1
|
||||
} else if (!storyRecipient.isMyStory && other.storyRecipient.isMyStory) {
|
||||
1
|
||||
} else if (storyViewState == StoryViewState.UNVIEWED && other.storyViewState != StoryViewState.UNVIEWED) {
|
||||
-1
|
||||
} else if (storyViewState != StoryViewState.UNVIEWED && other.storyViewState == StoryViewState.UNVIEWED) {
|
||||
1
|
||||
} else {
|
||||
-dateInMilliseconds.compareTo(other.dateInMilliseconds)
|
||||
return when {
|
||||
storyRecipient.isMyStory && !other.storyRecipient.isMyStory -> -1
|
||||
!storyRecipient.isMyStory && other.storyRecipient.isMyStory -> 1
|
||||
storyRecipient.isReleaseNotes && !other.storyRecipient.isReleaseNotes -> 1
|
||||
!storyRecipient.isReleaseNotes && other.storyRecipient.isReleaseNotes -> -1
|
||||
storyViewState == StoryViewState.UNVIEWED && other.storyViewState != StoryViewState.UNVIEWED -> -1
|
||||
storyViewState != StoryViewState.UNVIEWED && other.storyViewState == StoryViewState.UNVIEWED -> 1
|
||||
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.model.DistributionListId
|
||||
import org.thoughtcrime.securesms.database.model.StoryViewState
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
|
||||
|
@ -16,6 +17,7 @@ open class StoryViewerRepository {
|
|||
return Single.create<List<RecipientId>> { emitter ->
|
||||
val myStoriesId = SignalDatabase.recipients.getOrInsertFromDistributionListId(DistributionListId.MY_STORY)
|
||||
val myStories = Recipient.resolved(myStoriesId)
|
||||
val releaseChannelId = SignalStore.releaseChannelValues().releaseChannelRecipientId
|
||||
val recipientIds = SignalDatabase.mms.orderedStoryRecipientsAndIds.groupBy {
|
||||
val recipient = Recipient.resolved(it.recipientId)
|
||||
if (recipient.isDistributionList) {
|
||||
|
@ -42,12 +44,16 @@ open class StoryViewerRepository {
|
|||
}.map { it.id }
|
||||
|
||||
emitter.onSuccess(
|
||||
if (recipientIds.contains(myStoriesId)) {
|
||||
listOf(myStoriesId) + (recipientIds - myStoriesId)
|
||||
} else {
|
||||
recipientIds
|
||||
}
|
||||
recipientIds.floatToTop(releaseChannelId).floatToTop(myStoriesId)
|
||||
)
|
||||
}.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.jobs.MultiDeviceViewedUpdateJob
|
||||
import org.thoughtcrime.securesms.jobs.SendViewedReceiptJob
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
import org.thoughtcrime.securesms.stories.Stories
|
||||
|
@ -173,19 +174,24 @@ open class StoryViewerPageRepository(context: Context) {
|
|||
val markedMessageInfo = SignalDatabase.mms.setIncomingMessageViewed(storyPost.id)
|
||||
if (markedMessageInfo != null) {
|
||||
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
|
||||
SignalDatabase.recipients.updateLastStoryViewTimestamp(recipientId)
|
||||
Stories.enqueueNextStoriesForDownload(recipientId, true)
|
||||
if (storyPost.sender.isReleaseNotes) {
|
||||
SignalStore.storyValues().userHasSeenOnboardingStory = 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.NonNull;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import androidx.appcompat.content.res.AppCompatResources;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
|
@ -13,7 +13,7 @@ public final class ContextUtil {
|
|||
private ContextUtil() {}
|
||||
|
||||
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