Skip to content
Closed
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -364,7 +364,6 @@ public struct CVLongPressHandler {

switch gestureLocation {
case .`default`:
// TODO: Rename from "Text view item" to "default"?
delegate.didLongPressTextViewItem(cell,
itemViewModel: itemViewModel,
shouldAllowReply: shouldAllowReply)
Expand Down
83 changes: 77 additions & 6 deletions Signal/Notifications/NotificationActionHandler.swift
Original file line number Diff line number Diff line change
Expand Up @@ -120,9 +120,13 @@ public class NotificationActionHandler {
private class func reply(userInfo: [AnyHashable: Any], replyText: String) throws -> Promise<Void> {
return firstly { () -> Promise<NotificationMessage> in
self.notificationMessage(forUserInfo: userInfo)
}.then(on: DispatchQueue.global()) { (notificationMessage: NotificationMessage) -> Promise<Void> in
let thread = notificationMessage.thread
let interaction = notificationMessage.interaction
}.then(on: DispatchQueue.global()) { (notificationMessage: NotificationMessage) -> Promise<(DraftQuotedReplyModel?, NotificationMessage)> in
return getDraftQuotedReplyModelTupleFromIncomingMessage(notificationMessage: notificationMessage)
}.then(on: DispatchQueue.global()) { (informationTuple: (draft: DraftQuotedReplyModel?, notificationMessage: NotificationMessage)) -> Promise<(DraftQuotedReplyModel.ForSending?, NotificationMessage)> in
return getDraftQuotedReplyModelForSendingTupleFromDraftAndIncomingMessage(optionalDraftModel: informationTuple.draft, notificationMessage: informationTuple.notificationMessage)
}.then(on: DispatchQueue.global()) { (informationTuple: (draft: DraftQuotedReplyModel.ForSending?, notificationMessage: NotificationMessage)) -> Promise<Void> in
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's no need for these to be their own Promises. We can construct the reply inline here:

...
let thread = notificationMessage.thread
let interaction = notificationMessage.interaction

let draftQuotedReplyModelForSending = try SKEnvironment.shared.databaseStorageRef.read { tx -> DraftQuotedReplyModel.ForSending? in
  if
    let incomingMessage = interaction as? TSIncomingMessage,
    let draftQuotedReplyModel = DependenciesBridge.shared.quotedReplyManager.buildDraftQuotedReply(originalMessage: incomingMessage, tx: txasV2Read)
  {
    return try DependenciesBridge.shared.quotedReplyManager.prepareDraftForSending(draftQuotedReplyModel)
  }

  return nil
}

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IIRC If you put DependenciesBridge.shared.quotedReplyManager.prepareDraftForSending inside of a SSKEnvironment.shared.databaseStorageRef.read or SSKEnvironment.shared.databaseStorageRef.write block it will cause a database reentrant error because prepareDraftForSending is also reading from the database.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, annoying. Then something like:

...
let thread = notificationMessage.thread
let interaction = notificationMessage.interaction

let draftQuotedReplyModel = try SKEnvironment.shared.databaseStorageRef.read { tx -> DraftQuotedReplyModel? in
  if let incomingMessage = interaction as? TSIncomingMessage {
    return DependenciesBridge.shared.quotedReplyManager.buildDraftQuotedReply(originalMessage: incomingMessage, tx: txasV2Read)
  }

  return nil
}

let draftQuotedReplyModelForSending = try draftQuotedReplyModel.map { try DependenciesBridge.shared.quotedReplyManager.prepareDraftForSending($0) }

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see you already did something similar, thanks.

let thread = informationTuple.notificationMessage.thread
let interaction = informationTuple.notificationMessage.interaction
guard (interaction is TSOutgoingMessage) || (interaction is TSIncomingMessage) else {
throw OWSAssertionError("Unexpected interaction type.")
}
Expand All @@ -135,7 +139,7 @@ public class NotificationActionHandler {
// If we're replying to a group story reply, keep the reply within that context.
if
let incomingMessage = interaction as? TSIncomingMessage,
notificationMessage.isGroupStoryReply,
informationTuple.notificationMessage.isGroupStoryReply,
let storyTimestamp = incomingMessage.storyTimestamp,
let storyAuthorAci = incomingMessage.storyAuthorAci
{
Expand All @@ -156,7 +160,7 @@ public class NotificationActionHandler {
explicitRecipients: [],
skippedRecipients: [],
transaction: transaction
))
), quotedReplyDraft: informationTuple.draft)
do {
let preparedMessage = try unpreparedMessage.prepare(tx: transaction)
return ThreadUtil.enqueueMessagePromise(message: preparedMessage, transaction: transaction)
Expand All @@ -169,7 +173,74 @@ public class NotificationActionHandler {
SSKEnvironment.shared.notificationPresenterRef.notifyUserOfFailedSend(inThread: thread)
throw error
}.then(on: DispatchQueue.global()) { () -> Promise<Void> in
self.markMessageAsRead(notificationMessage: notificationMessage)
self.markMessageAsRead(notificationMessage: informationTuple.notificationMessage)
}
}
}

private class func getDraftQuotedReplyModelTupleFromIncomingMessage(notificationMessage: NotificationMessage) -> Promise<(DraftQuotedReplyModel?, NotificationMessage)> {
let (promise, future) = Promise<(DraftQuotedReplyModel?, NotificationMessage)>.pending()
SSKEnvironment.shared.databaseStorageRef.read { readTransaction in
if
let incomingMessage = notificationMessage.interaction as? TSIncomingMessage,
let draftQuotedReplyModel = DependenciesBridge.shared.quotedReplyManager.buildDraftQuotedReply(originalMessage: incomingMessage, tx: readTransaction.asV2Read) {
future.resolve((draftQuotedReplyModel, notificationMessage))
}
return future.resolve((nil, notificationMessage))
}
return promise
}

private class func getDraftQuotedReplyModelForSendingTupleFromDraftAndIncomingMessage(optionalDraftModel: DraftQuotedReplyModel?, notificationMessage: NotificationMessage) -> Promise<(DraftQuotedReplyModel.ForSending?, NotificationMessage)> {
guard let draftModel = optionalDraftModel else {
return Promise.value((nil, notificationMessage))
}

do {
let draftForSending = try DependenciesBridge.shared.quotedReplyManager.prepareDraftForSending(draftModel)
return Promise.value((draftForSending, notificationMessage))
} catch {
return Promise.value((nil, notificationMessage))
}
}

private class func sendReplyToNoficationMessageWithMessageToReplyTo(replyText: String, messageBeingRespondedTo: DraftQuotedReplyModel.ForSending?, notificationMessage: NotificationMessage) throws -> Promise<Void> {
return SSKEnvironment.shared.databaseStorageRef.write { transaction in
let builder: TSOutgoingMessageBuilder = .withDefaultValues(thread: notificationMessage.thread)
builder.messageBody = replyText

// If we're replying to a group story reply, keep the reply within that context.
if
let incomingMessage = notificationMessage.interaction as? TSIncomingMessage,
notificationMessage.isGroupStoryReply,
let storyTimestamp = incomingMessage.storyTimestamp,
let storyAuthorAci = incomingMessage.storyAuthorAci
{
builder.storyTimestamp = storyTimestamp
builder.storyAuthorAci = storyAuthorAci
} else {
// We only use the thread's DM timer for normal messages & 1:1 story
// replies -- group story replies last for the lifetime of the story.
let dmConfigurationStore = DependenciesBridge.shared.disappearingMessagesConfigurationStore
let dmConfig = dmConfigurationStore.fetchOrBuildDefault(for: .thread(notificationMessage.thread), tx: transaction.asV2Read)
builder.expiresInSeconds = dmConfig.durationSeconds
builder.expireTimerVersion = NSNumber(value: dmConfig.timerVersion)
}

let outgoingMessage = TSOutgoingMessage(
outgoingMessageWith: builder,
additionalRecipients: [],
explicitRecipients: [],
skippedRecipients: [],
transaction: transaction
)

let unpreparedMessage = UnpreparedOutgoingMessage.forMessage(outgoingMessage, quotedReplyDraft: messageBeingRespondedTo)
do {
let preparedMessage = try unpreparedMessage.prepare(tx: transaction)
return ThreadUtil.enqueueMessagePromise(message: preparedMessage, transaction: transaction)
} catch {
return Promise(error: error)
}
}
}
Expand Down