Add notification for failed story messages.

This commit is contained in:
Clark 2023-03-17 12:23:02 -04:00 committed by Greyson Parrelli
parent 069b707d9d
commit 150c42c590
14 changed files with 135 additions and 3 deletions

View file

@ -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";
}

View file

@ -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<StoryResult> {
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<MarkedMessageInfo> {
var query = """
$THREAD_ID = ? AND

View file

@ -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,10 +291,25 @@ 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) {
if (isStory) {
SignalDatabase.messages().markAsNotNotified(messageId);
ApplicationDependencies.getMessageNotifier().notifyStoryDeliveryFailed(context, recipient, ConversationId.forConversation(threadId));
} else {
ApplicationDependencies.getMessageNotifier().notifyMessageDeliveryFailed(context, recipient, ConversationId.fromThreadAndReply(threadId, groupReplyStoryId));
}
}
}
protected Optional<SignalServiceDataMessage.Quote> getQuoteFor(OutgoingMessage message) throws IOException {
if (message.getOutgoingQuote() == null) return Optional.empty();

View file

@ -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);

View file

@ -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);

View file

@ -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)
}

View file

@ -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)

View file

@ -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()
}
}
}

View file

@ -60,6 +60,7 @@ class StoriesLandingViewModel(private val storiesLandingRepository: StoriesLandi
fun markStoriesRead() {
storiesLandingRepository.markStoriesRead()
storiesLandingRepository.markFailedStoriesNotified()
}
class Factory(private val storiesLandingRepository: StoriesLandingRepository) : ViewModelProvider.Factory {

View file

@ -47,6 +47,22 @@ class ConversationListTabRepository {
}.subscribeOn(Schedulers.io())
}
fun getHasFailedOutgoingStories(): Observable<Boolean> {
return Observable.create<Boolean> { 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<Long> {
return Observable.create { emitter ->
fun refresh() {

View file

@ -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

View file

@ -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(

View file

@ -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() {

View file

@ -2129,6 +2129,10 @@
<string name="MessageNotifier_most_recent_from_s">Most recent from: %1$s</string>
<string name="MessageNotifier_locked_message">Locked message</string>
<string name="MessageNotifier_message_delivery_failed">Message delivery failed.</string>
<!-- Shown in a notification when a story the user tries to send fails to be sent -->
<string name="MessageNotifier_story_delivery_failed">Story failed to send</string>
<!-- Shown as notification title for when a notification about a story sent to a group story %1$s replaced with the group name -->
<string name="MessageNotifier_group_story_title">You to %1$s</string>
<string name="MessageNotifier_failed_to_deliver_message">Failed to deliver message.</string>
<string name="MessageNotifier_error_delivering_message">Error delivering message.</string>
<string name="MessageNotifier_message_delivery_paused">Message delivery paused.</string>