diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/emoji/EmojiStrings.java b/app/src/main/java/org/thoughtcrime/securesms/components/emoji/EmojiStrings.java index 274390fb09..8e9678175a 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/emoji/EmojiStrings.java +++ b/app/src/main/java/org/thoughtcrime/securesms/components/emoji/EmojiStrings.java @@ -10,4 +10,5 @@ public final class EmojiStrings { public static final String STICKER = "\u2B50"; public static final String GIFT = "\uD83C\uDF81"; public static final String CARD = "\uD83D\uDCB3"; + public static final String FAILED_STORY = "\u2757"; } diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/MessageTable.kt b/app/src/main/java/org/thoughtcrime/securesms/database/MessageTable.kt index 8fe47ef77e..d4e393d792 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/MessageTable.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/MessageTable.kt @@ -1314,6 +1314,17 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat return markedMessageInfos } + fun markAllFailedStoriesNotified() { + val where = "$IS_STORY_CLAUSE AND (${getOutgoingTypeClause()}) AND $NOTIFIED = 0 AND ($TYPE & ${MessageTypes.BASE_TYPE_MASK}) = ${MessageTypes.BASE_SENT_FAILED_TYPE}" + + writableDatabase + .update("$TABLE_NAME INDEXED BY $INDEX_THREAD_DATE") + .values(NOTIFIED to 1) + .where(where) + .run() + notifyConversationListListeners() + } + fun markOnboardingStoryRead() { val recipientId = SignalStore.releaseChannelValues().releaseChannelRecipientId ?: return val where = "$IS_STORY_CLAUSE AND NOT (${getOutgoingTypeClause()}) AND $READ = 0 AND $RECIPIENT_ID = ?" @@ -1459,6 +1470,11 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat .readToList { RecipientId.from(it.getLong(0)) } } + fun hasFailedOutgoingStory(): Boolean { + val where = "$IS_STORY_CLAUSE AND (${getOutgoingTypeClause()}) AND $NOTIFIED = 0 AND ($TYPE & ${MessageTypes.BASE_TYPE_MASK}) = ${MessageTypes.BASE_SENT_FAILED_TYPE}" + return readableDatabase.exists(TABLE_NAME).where(where).run() + } + fun getOrderedStoryRecipientsAndIds(isOutgoingOnly: Boolean): List { val query = """ SELECT @@ -2002,6 +2018,14 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat .run() } + fun markAsNotNotified(id: Long) { + writableDatabase + .update(TABLE_NAME) + .values(NOTIFIED to 0) + .where("$ID = ?", id) + .run() + } + fun setMessagesReadSince(threadId: Long, sinceTimestamp: Long): List { var query = """ $THREAD_ID = ? AND diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/PushSendJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/PushSendJob.java index 38202cdd29..9b0e96004f 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/PushSendJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/PushSendJob.java @@ -26,8 +26,11 @@ import org.thoughtcrime.securesms.blurhash.BlurHash; import org.thoughtcrime.securesms.contactshare.Contact; import org.thoughtcrime.securesms.contactshare.ContactModelMapper; import org.thoughtcrime.securesms.crypto.ProfileKeyUtil; +import org.thoughtcrime.securesms.database.NoSuchMessageException; import org.thoughtcrime.securesms.database.SignalDatabase; import org.thoughtcrime.securesms.database.model.Mention; +import org.thoughtcrime.securesms.database.model.MessageRecord; +import org.thoughtcrime.securesms.database.model.MmsMessageRecord; import org.thoughtcrime.securesms.database.model.ParentStoryId; import org.thoughtcrime.securesms.database.model.StickerRecord; import org.thoughtcrime.securesms.database.model.databaseprotos.BodyRangeList; @@ -41,6 +44,7 @@ import org.thoughtcrime.securesms.keyvalue.CertificateType; import org.thoughtcrime.securesms.keyvalue.SignalStore; import org.thoughtcrime.securesms.linkpreview.LinkPreview; import org.thoughtcrime.securesms.mms.DecryptableStreamUriLoader.DecryptableUri; +import org.thoughtcrime.securesms.mms.MmsException; import org.thoughtcrime.securesms.mms.OutgoingMessage; import org.thoughtcrime.securesms.mms.PartAuthority; import org.thoughtcrime.securesms.mms.QuoteModel; @@ -287,8 +291,23 @@ public abstract class PushSendJob extends SendJob { Recipient recipient = SignalDatabase.threads().getRecipientForThreadId(threadId); ParentStoryId.GroupReply groupReplyStoryId = SignalDatabase.messages().getParentStoryIdForGroupReply(messageId); + boolean isStory = false; + try { + MessageRecord record = SignalDatabase.messages().getMessageRecord(messageId); + if (record instanceof MmsMessageRecord) { + isStory = (((MmsMessageRecord) record).getStoryType().isStory()); + } + } catch (NoSuchMessageException e) { + Log.e(TAG, e); + } + if (threadId != -1 && recipient != null) { - ApplicationDependencies.getMessageNotifier().notifyMessageDeliveryFailed(context, recipient, ConversationId.fromThreadAndReply(threadId, groupReplyStoryId)); + if (isStory) { + SignalDatabase.messages().markAsNotNotified(messageId); + ApplicationDependencies.getMessageNotifier().notifyStoryDeliveryFailed(context, recipient, ConversationId.forConversation(threadId)); + } else { + ApplicationDependencies.getMessageNotifier().notifyMessageDeliveryFailed(context, recipient, ConversationId.fromThreadAndReply(threadId, groupReplyStoryId)); + } } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/notifications/MessageNotifier.java b/app/src/main/java/org/thoughtcrime/securesms/notifications/MessageNotifier.java index ffa98a5aaf..4c37779bb2 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/notifications/MessageNotifier.java +++ b/app/src/main/java/org/thoughtcrime/securesms/notifications/MessageNotifier.java @@ -21,6 +21,7 @@ public interface MessageNotifier { void clearVisibleThread(); void setLastDesktopActivityTimestamp(long timestamp); void notifyMessageDeliveryFailed(@NonNull Context context, @NonNull Recipient recipient, @NonNull ConversationId conversationId); + void notifyStoryDeliveryFailed(@NonNull Context context, @NonNull Recipient recipient, @NonNull ConversationId conversationId); void notifyProofRequired(@NonNull Context context, @NonNull Recipient recipient, @NonNull ConversationId conversationId); void cancelDelayedNotifications(); void updateNotification(@NonNull Context context); diff --git a/app/src/main/java/org/thoughtcrime/securesms/notifications/OptimizedMessageNotifier.java b/app/src/main/java/org/thoughtcrime/securesms/notifications/OptimizedMessageNotifier.java index f6846839ad..235d660f7c 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/notifications/OptimizedMessageNotifier.java +++ b/app/src/main/java/org/thoughtcrime/securesms/notifications/OptimizedMessageNotifier.java @@ -58,6 +58,11 @@ public class OptimizedMessageNotifier implements MessageNotifier { getNotifier().notifyMessageDeliveryFailed(context, recipient, conversationId); } + @Override + public void notifyStoryDeliveryFailed(@NonNull Context context, @NonNull Recipient recipient, @NonNull ConversationId threadId) { + getNotifier().notifyStoryDeliveryFailed(context, recipient, threadId); + } + @Override public void notifyProofRequired(@NonNull Context context, @NonNull Recipient recipient, @NonNull ConversationId conversationId) { getNotifier().notifyProofRequired(context, recipient, conversationId); diff --git a/app/src/main/java/org/thoughtcrime/securesms/notifications/v2/DefaultMessageNotifier.kt b/app/src/main/java/org/thoughtcrime/securesms/notifications/v2/DefaultMessageNotifier.kt index d9c866f45e..3266748983 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/notifications/v2/DefaultMessageNotifier.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/notifications/v2/DefaultMessageNotifier.kt @@ -85,6 +85,10 @@ class DefaultMessageNotifier(context: Application) : MessageNotifier { NotificationFactory.notifyMessageDeliveryFailed(context, recipient, conversationId, visibleThread) } + override fun notifyStoryDeliveryFailed(context: Context, recipient: Recipient, conversationId: ConversationId) { + NotificationFactory.notifyStoryDeliveryFailed(context, recipient, conversationId) + } + override fun notifyProofRequired(context: Context, recipient: Recipient, conversationId: ConversationId) { NotificationFactory.notifyProofRequired(context, recipient, conversationId, visibleThread) } diff --git a/app/src/main/java/org/thoughtcrime/securesms/notifications/v2/NotificationFactory.kt b/app/src/main/java/org/thoughtcrime/securesms/notifications/v2/NotificationFactory.kt index 4fb87e6910..38ec67a6a1 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/notifications/v2/NotificationFactory.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/notifications/v2/NotificationFactory.kt @@ -19,7 +19,10 @@ import org.signal.core.util.concurrent.SignalExecutors import org.signal.core.util.logging.Log import org.thoughtcrime.securesms.MainActivity import org.thoughtcrime.securesms.R +import org.thoughtcrime.securesms.components.emoji.EmojiStrings +import org.thoughtcrime.securesms.contacts.avatars.GeneratedContactPhoto import org.thoughtcrime.securesms.conversation.ConversationIntents +import org.thoughtcrime.securesms.conversation.colors.AvatarColor import org.thoughtcrime.securesms.database.SignalDatabase import org.thoughtcrime.securesms.database.model.InMemoryMessageRecord import org.thoughtcrime.securesms.keyvalue.SignalStore @@ -324,6 +327,46 @@ object NotificationFactory { NotificationManagerCompat.from(context).safelyNotify(recipient, NotificationIds.getNotificationIdForMessageDeliveryFailed(thread), builder.build()) } + fun notifyStoryDeliveryFailed(context: Context, recipient: Recipient, thread: ConversationId) { + val intent = Intent(context, MyStoriesActivity::class.java).makeUniqueToPreventMerging() + + val contentTitle = if (SignalStore.settings().messageNotificationsPrivacy.isDisplayContact) { + if (recipient.isGroup) { + context.getString(R.string.MessageNotifier_group_story_title, recipient.getDisplayName(context)) + } else { + recipient.getDisplayName(context) + } + } else { + context.getString(R.string.SingleRecipientNotificationBuilder_signal) + } + + val largeIcon = if (SignalStore.settings().messageNotificationsPrivacy.isDisplayContact) { + if (recipient.isMyStory) { + Recipient.self().getContactDrawable(context) + } else { + recipient.getContactDrawable(context) + } + } else { + GeneratedContactPhoto("Unknown", R.drawable.ic_profile_outline_40).asDrawable(context, AvatarColor.UNKNOWN) + }.toLargeBitmap(context) + + val builder: NotificationBuilder = NotificationBuilder.create(context) + + builder.apply { + setSmallIcon(R.drawable.ic_notification) + setLargeIcon(largeIcon) + setContentTitle(contentTitle) + setContentText(String.format("%s %s", EmojiStrings.FAILED_STORY, context.getString(R.string.MessageNotifier_story_delivery_failed))) + setTicker(context.getString(R.string.MessageNotifier_story_delivery_failed)) + setContentIntent(NotificationPendingIntentHelper.getActivity(context, 0, intent, PendingIntentFlags.mutable())) + setAutoCancel(true) + setAlarms(recipient) + setChannelId(NotificationChannels.getInstance().FAILURES) + } + + NotificationManagerCompat.from(context).safelyNotify(recipient, NotificationIds.getNotificationIdForMessageDeliveryFailed(thread), builder.build()) + } + fun notifyProofRequired(context: Context, recipient: Recipient, thread: ConversationId, visibleThread: ConversationId?) { if (thread == visibleThread) { notifyInThread(context, recipient, 0) diff --git a/app/src/main/java/org/thoughtcrime/securesms/stories/landing/StoriesLandingRepository.kt b/app/src/main/java/org/thoughtcrime/securesms/stories/landing/StoriesLandingRepository.kt index acbf57d1e6..8ac6fa17a8 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/stories/landing/StoriesLandingRepository.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/stories/landing/StoriesLandingRepository.kt @@ -185,4 +185,13 @@ class StoriesLandingRepository(context: Context) { } } } + + /** + * Marks all failed stories as "notified" by the user (marking them as notified in the database) + */ + fun markFailedStoriesNotified() { + SignalExecutors.BOUNDED_IO.execute { + SignalDatabase.messages.markAllFailedStoriesNotified() + } + } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/stories/landing/StoriesLandingViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/stories/landing/StoriesLandingViewModel.kt index be9387b787..7cb322e8ec 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/stories/landing/StoriesLandingViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/stories/landing/StoriesLandingViewModel.kt @@ -60,6 +60,7 @@ class StoriesLandingViewModel(private val storiesLandingRepository: StoriesLandi fun markStoriesRead() { storiesLandingRepository.markStoriesRead() + storiesLandingRepository.markFailedStoriesNotified() } class Factory(private val storiesLandingRepository: StoriesLandingRepository) : ViewModelProvider.Factory { diff --git a/app/src/main/java/org/thoughtcrime/securesms/stories/tabs/ConversationListTabRepository.kt b/app/src/main/java/org/thoughtcrime/securesms/stories/tabs/ConversationListTabRepository.kt index ede1109853..6a7309e099 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/stories/tabs/ConversationListTabRepository.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/stories/tabs/ConversationListTabRepository.kt @@ -47,6 +47,22 @@ class ConversationListTabRepository { }.subscribeOn(Schedulers.io()) } + fun getHasFailedOutgoingStories(): Observable { + return Observable.create { emitter -> + fun refresh() { + emitter.onNext(SignalDatabase.messages.hasFailedOutgoingStory()) + } + + val listener = DatabaseObserver.Observer { + refresh() + } + + ApplicationDependencies.getDatabaseObserver().registerConversationListObserver(listener) + emitter.setCancellable { ApplicationDependencies.getDatabaseObserver().unregisterObserver(listener) } + refresh() + }.subscribeOn(Schedulers.io()) + } + fun getNumberOfUnseenCalls(): Observable { return Observable.create { emitter -> fun refresh() { diff --git a/app/src/main/java/org/thoughtcrime/securesms/stories/tabs/ConversationListTabsFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/stories/tabs/ConversationListTabsFragment.kt index 6fb857c69c..68c1dc634e 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/stories/tabs/ConversationListTabsFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/stories/tabs/ConversationListTabsFragment.kt @@ -120,8 +120,8 @@ class ConversationListTabsFragment : Fragment(R.layout.conversation_list_tabs) { binding.chatsUnreadIndicator.visible = state.unreadMessagesCount > 0 binding.chatsUnreadIndicator.text = formatCount(state.unreadMessagesCount) - binding.storiesUnreadIndicator.visible = state.unreadStoriesCount > 0 - binding.storiesUnreadIndicator.text = formatCount(state.unreadStoriesCount) + binding.storiesUnreadIndicator.visible = state.unreadStoriesCount > 0 || state.hasFailedStory + binding.storiesUnreadIndicator.text = if (state.hasFailedStory) "!" else formatCount(state.unreadStoriesCount) if (FeatureFlags.callsTab()) { binding.callsUnreadIndicator.visible = state.unreadCallsCount > 0 diff --git a/app/src/main/java/org/thoughtcrime/securesms/stories/tabs/ConversationListTabsState.kt b/app/src/main/java/org/thoughtcrime/securesms/stories/tabs/ConversationListTabsState.kt index 059cbca1a1..e0a71ca174 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/stories/tabs/ConversationListTabsState.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/stories/tabs/ConversationListTabsState.kt @@ -6,6 +6,7 @@ data class ConversationListTabsState( val unreadMessagesCount: Long = 0L, val unreadCallsCount: Long = 0L, val unreadStoriesCount: Long = 0L, + val hasFailedStory: Boolean = false, val visibilityState: VisibilityState = VisibilityState() ) { data class VisibilityState( diff --git a/app/src/main/java/org/thoughtcrime/securesms/stories/tabs/ConversationListTabsViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/stories/tabs/ConversationListTabsViewModel.kt index 45fbaa01eb..9312c38495 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/stories/tabs/ConversationListTabsViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/stories/tabs/ConversationListTabsViewModel.kt @@ -34,6 +34,10 @@ class ConversationListTabsViewModel(repository: ConversationListTabRepository) : disposables += repository.getNumberOfUnseenStories().subscribe { unseenStories -> store.update { it.copy(unreadStoriesCount = unseenStories) } } + + disposables += repository.getHasFailedOutgoingStories().subscribe { hasFailedStories -> + store.update { it.copy(hasFailedStory = hasFailedStories) } + } } override fun onCleared() { diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 69bb06db31..3569801eb2 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -2129,6 +2129,10 @@ Most recent from: %1$s Locked message Message delivery failed. + + Story failed to send + + You to %1$s Failed to deliver message. Error delivering message. Message delivery paused.