451 lines
14 KiB
Swift
451 lines
14 KiB
Swift
//
|
|
// Copyright 2023 Signal Messenger, LLC
|
|
// SPDX-License-Identifier: AGPL-3.0-only
|
|
//
|
|
|
|
import LibSignalClient
|
|
import SignalServiceKit
|
|
import SignalUI
|
|
|
|
protocol MessageEditHistoryViewDelegate: AnyObject {
|
|
func editHistoryMessageWasDeleted()
|
|
}
|
|
|
|
class EditHistoryTableSheetViewController: OWSTableSheetViewController {
|
|
|
|
internal enum Constants {
|
|
static let cellSpacing: CGFloat = 12.0
|
|
}
|
|
|
|
weak var delegate: MessageEditHistoryViewDelegate?
|
|
|
|
var parentRenderItems: [CVRenderItem]?
|
|
var renderItems = [CVRenderItem]()
|
|
let spoilerState: SpoilerRenderState
|
|
private var message: TSMessage
|
|
private let database: SDSDatabaseStorage
|
|
private let editManager: EditManager
|
|
|
|
init(
|
|
message: TSMessage,
|
|
spoilerState: SpoilerRenderState,
|
|
editManager: EditManager,
|
|
database: SDSDatabaseStorage
|
|
) {
|
|
self.spoilerState = spoilerState
|
|
self.message = message
|
|
self.database = database
|
|
self.editManager = editManager
|
|
super.init()
|
|
|
|
database.appendDatabaseChangeDelegate(self)
|
|
}
|
|
|
|
override func viewDidAppear(_ animated: Bool) {
|
|
super.viewDidAppear(animated)
|
|
do {
|
|
try self.database.write { tx in
|
|
|
|
guard let thread = TSThread.anyFetch(
|
|
uniqueId: message.uniqueThreadId,
|
|
transaction: tx
|
|
) else { return }
|
|
|
|
try self.editManager.markEditRevisionsAsRead(
|
|
for: self.message,
|
|
thread: thread,
|
|
tx: tx.asV2Write
|
|
)
|
|
}
|
|
} catch {
|
|
owsFailDebug("Failed to update edit read state")
|
|
}
|
|
}
|
|
|
|
// MARK: - Table Update
|
|
|
|
private func loadEditHistory() throws {
|
|
let messageStillExists = try database.read { tx in
|
|
guard let newMessage = TSInteraction.anyFetch(uniqueId: message.uniqueId, transaction: tx) as? TSMessage else {
|
|
return false
|
|
}
|
|
message = newMessage
|
|
|
|
let edits = try DependenciesBridge.shared.editMessageStore.findEditHistory(
|
|
for: message,
|
|
tx: tx.asV2Read
|
|
).compactMap { $1 }
|
|
|
|
guard let thread = TSThread.anyFetch(
|
|
uniqueId: message.uniqueThreadId,
|
|
transaction: tx
|
|
) else {
|
|
owsFailDebug("Missing thread.")
|
|
return false
|
|
}
|
|
|
|
let threadAssociatedData = ThreadAssociatedData.fetchOrDefault(
|
|
for: thread,
|
|
transaction: tx
|
|
)
|
|
|
|
parentRenderItems = buildRenderItem(
|
|
thread: thread,
|
|
threadAssociatedData: threadAssociatedData,
|
|
message: message,
|
|
forceDateHeader: true,
|
|
tx: tx)
|
|
|
|
var renderItems = [CVRenderItem]()
|
|
for edit in edits {
|
|
let items = buildRenderItem(
|
|
thread: thread,
|
|
threadAssociatedData: threadAssociatedData,
|
|
message: edit,
|
|
tx: tx
|
|
)
|
|
renderItems.append(contentsOf: items)
|
|
}
|
|
self.renderItems = renderItems
|
|
|
|
return true
|
|
}
|
|
|
|
if !messageStillExists {
|
|
delegate?.editHistoryMessageWasDeleted()
|
|
}
|
|
}
|
|
|
|
public override func updateTableContents(shouldReload: Bool = true) {
|
|
do {
|
|
try loadEditHistory()
|
|
} catch {
|
|
owsFailDebug("Error reading edit history: \(error)")
|
|
}
|
|
|
|
let contents = OWSTableContents()
|
|
defer { tableViewController.setContents(contents, shouldReload: shouldReload) }
|
|
guard let parentItems = parentRenderItems else { return }
|
|
|
|
let topSection = OWSTableSection()
|
|
topSection.add(createMessageListTableItem(items: parentItems))
|
|
contents.add(topSection)
|
|
|
|
let header = OWSLocalizedString(
|
|
"EDIT_HISTORY_LABEL",
|
|
comment: "Label for Edit History modal"
|
|
)
|
|
|
|
let section = OWSTableSection()
|
|
section.headerAttributedTitle = NSAttributedString(string: header, attributes: [
|
|
.font: UIFont.dynamicTypeBodyClamped.semibold(),
|
|
.foregroundColor: Theme.primaryTextColor
|
|
])
|
|
section.hasBackground = true
|
|
section.hasSeparators = false
|
|
section.add(createMessageListTableItem(items: renderItems))
|
|
contents.add(section)
|
|
}
|
|
|
|
// MARK: - Utility Methods
|
|
|
|
private func createMessageListTableItem(items: [CVRenderItem]) -> OWSTableItem {
|
|
return OWSTableItem { [weak self] in
|
|
guard let self = self else { return UITableViewCell() }
|
|
|
|
let views = items.map { item in
|
|
let cellView = CVCellView()
|
|
cellView.configure(renderItem: item, componentDelegate: self)
|
|
cellView.isCellVisible = true
|
|
cellView.autoSetDimension(.height, toSize: item.cellSize.height)
|
|
return cellView
|
|
}
|
|
|
|
let stack = UIStackView(arrangedSubviews: views)
|
|
stack.spacing = Constants.cellSpacing
|
|
stack.axis = .vertical
|
|
stack.alignment = .fill
|
|
|
|
let cell = OWSTableItem.newCell()
|
|
cell.selectionStyle = .none
|
|
cell.contentView.addSubview(stack)
|
|
stack.autoPinEdgesToSuperviewMargins()
|
|
|
|
return cell
|
|
}
|
|
}
|
|
|
|
var currentDaysBefore: Int = -1
|
|
private func buildRenderItem(
|
|
thread: TSThread,
|
|
threadAssociatedData: ThreadAssociatedData,
|
|
message interaction: TSMessage,
|
|
forceDateHeader: Bool = false,
|
|
tx: SDSAnyReadTransaction
|
|
) -> [CVRenderItem] {
|
|
var results = [CVRenderItem]()
|
|
let cellInsets = tableViewController.cellOuterInsets
|
|
let viewWidth = tableViewController.view.frame.inset(by: cellInsets).width
|
|
let conversationStyle = ConversationStyle(
|
|
type: .messageDetails,
|
|
thread: thread,
|
|
viewWidth: viewWidth,
|
|
hasWallpaper: false,
|
|
isWallpaperPhoto: false,
|
|
chatColor: ChatColors.resolvedChatColor(for: thread, tx: tx)
|
|
)
|
|
|
|
let itemDate = NSDate.ows_date(withMillisecondsSince1970: interaction.timestamp)
|
|
let daysPrior = DateUtil.daysFrom(firstDate: itemDate, toSecondDate: Date())
|
|
if forceDateHeader || daysPrior > currentDaysBefore {
|
|
currentDaysBefore = daysPrior
|
|
|
|
let dateInteraction = DateHeaderInteraction(thread: thread, timestamp: interaction.timestamp)
|
|
if let dateItem = CVLoader.buildStandaloneRenderItem(
|
|
interaction: dateInteraction,
|
|
thread: thread,
|
|
threadAssociatedData: threadAssociatedData,
|
|
conversationStyle: conversationStyle,
|
|
spoilerState: self.spoilerState,
|
|
transaction: tx
|
|
) {
|
|
results.append(dateItem)
|
|
}
|
|
}
|
|
|
|
if let item = CVLoader.buildStandaloneRenderItem(
|
|
interaction: interaction,
|
|
thread: thread,
|
|
threadAssociatedData: threadAssociatedData,
|
|
conversationStyle: conversationStyle,
|
|
spoilerState: self.spoilerState,
|
|
transaction: tx
|
|
) {
|
|
results.append(item)
|
|
}
|
|
return results
|
|
}
|
|
}
|
|
|
|
// MARK: - DatabaseChangeDelegate
|
|
|
|
extension EditHistoryTableSheetViewController: DatabaseChangeDelegate {
|
|
func databaseChangesDidUpdate(databaseChanges: SignalServiceKit.DatabaseChanges) {
|
|
AssertIsOnMainThread()
|
|
|
|
guard databaseChanges.didUpdate(interaction: self.message) else {
|
|
return
|
|
}
|
|
|
|
updateTableContents()
|
|
}
|
|
|
|
func databaseChangesDidUpdateExternally() {
|
|
AssertIsOnMainThread()
|
|
|
|
updateTableContents()
|
|
}
|
|
|
|
func databaseChangesDidReset() {
|
|
AssertIsOnMainThread()
|
|
|
|
updateTableContents()
|
|
}
|
|
}
|
|
|
|
// MARK: - CVComponentDelegate
|
|
|
|
extension EditHistoryTableSheetViewController: CVComponentDelegate {
|
|
|
|
func enqueueReload() {}
|
|
|
|
func enqueueReloadWithoutCaches() {}
|
|
|
|
func didTapBodyTextItem(_ item: CVTextLabel.Item) {}
|
|
|
|
func didLongPressBodyTextItem(_ item: CVTextLabel.Item) {}
|
|
|
|
func didTapSystemMessageItem(_ item: CVTextLabel.Item) {}
|
|
|
|
func didLongPressTextViewItem(
|
|
_ cell: CVCell,
|
|
itemViewModel: CVItemViewModelImpl,
|
|
shouldAllowReply: Bool) {}
|
|
|
|
func didLongPressMediaViewItem(
|
|
_ cell: CVCell,
|
|
itemViewModel: CVItemViewModelImpl,
|
|
shouldAllowReply: Bool) {}
|
|
|
|
func didLongPressQuote(
|
|
_ cell: CVCell,
|
|
itemViewModel: CVItemViewModelImpl,
|
|
shouldAllowReply: Bool) {}
|
|
|
|
func didLongPressSystemMessage(
|
|
_ cell: CVCell,
|
|
itemViewModel: CVItemViewModelImpl) {}
|
|
|
|
func didLongPressSticker(
|
|
_ cell: CVCell,
|
|
itemViewModel: CVItemViewModelImpl,
|
|
shouldAllowReply: Bool) {}
|
|
|
|
func didLongPressPaymentMessage(
|
|
_ cell: CVCell,
|
|
itemViewModel: CVItemViewModelImpl,
|
|
shouldAllowReply: Bool
|
|
) {}
|
|
|
|
func didTapPayment(
|
|
_ paymentModel: TSPaymentModel,
|
|
displayName: String
|
|
) {}
|
|
|
|
func didChangeLongPress(_ itemViewModel: CVItemViewModelImpl) {}
|
|
|
|
func didEndLongPress(_ itemViewModel: CVItemViewModelImpl) {}
|
|
|
|
func didCancelLongPress(_ itemViewModel: CVItemViewModelImpl) {}
|
|
|
|
// MARK: -
|
|
|
|
func didTapReplyToItem(_ itemViewModel: CVItemViewModelImpl) {}
|
|
|
|
func didTapSenderAvatar(_ interaction: TSInteraction) {}
|
|
|
|
func shouldAllowReplyForItem(_ itemViewModel: CVItemViewModelImpl) -> Bool { false }
|
|
|
|
func didTapReactions(
|
|
reactionState: InteractionReactionState,
|
|
message: TSMessage) {}
|
|
|
|
func didTapTruncatedTextMessage(_ itemViewModel: CVItemViewModelImpl) {}
|
|
|
|
func didTapShowEditHistory(_ itemViewModel: CVItemViewModelImpl) {}
|
|
|
|
var hasPendingMessageRequest: Bool { false }
|
|
|
|
func didTapFailedOrPendingDownloads(_ message: TSMessage) {}
|
|
|
|
func didTapBrokenVideo() {}
|
|
|
|
func didTapBodyMedia(
|
|
itemViewModel: CVItemViewModelImpl,
|
|
attachmentStream: ReferencedTSResourceStream,
|
|
imageView: UIView
|
|
) {}
|
|
|
|
func didTapGenericAttachment(
|
|
_ attachment: CVComponentGenericAttachment
|
|
) -> CVAttachmentTapAction { .default }
|
|
|
|
func didTapQuotedReply(_ quotedReply: QuotedReplyModel) {}
|
|
|
|
func didTapLinkPreview(_ linkPreview: OWSLinkPreview) {}
|
|
|
|
func didTapContactShare(_ contactShare: ContactShareViewModel) {}
|
|
|
|
func didTapSendMessage(to phoneNumbers: [String]) {}
|
|
|
|
func didTapSendInvite(toContactShare contactShare: ContactShareViewModel) {}
|
|
|
|
func didTapAddToContacts(contactShare: ContactShareViewModel) {}
|
|
|
|
func didTapStickerPack(_ stickerPackInfo: StickerPackInfo) {}
|
|
|
|
func didTapGroupInviteLink(url: URL) {}
|
|
|
|
func didTapProxyLink(url: URL) {}
|
|
|
|
func didTapShowMessageDetail(_ itemViewModel: CVItemViewModelImpl) {}
|
|
|
|
func willWrapGift(_ messageUniqueId: String) -> Bool { false }
|
|
|
|
func willShakeGift(_ messageUniqueId: String) -> Bool { false }
|
|
|
|
func willUnwrapGift(_ itemViewModel: CVItemViewModelImpl) {}
|
|
|
|
func didTapGiftBadge(
|
|
_ itemViewModel: CVItemViewModelImpl,
|
|
profileBadge: ProfileBadge,
|
|
isExpired: Bool,
|
|
isRedeemed: Bool) {}
|
|
|
|
func prepareMessageDetailForInteractivePresentation(_ itemViewModel: CVItemViewModelImpl) {}
|
|
|
|
func beginCellAnimation(maximumDuration: TimeInterval) -> EndCellAnimation {
|
|
return {}
|
|
}
|
|
|
|
var isConversationPreview: Bool { true }
|
|
|
|
var wallpaperBlurProvider: WallpaperBlurProvider? { nil }
|
|
|
|
public var selectionState: CVSelectionState { CVSelectionState() }
|
|
|
|
func didTapPreviouslyVerifiedIdentityChange(_ address: SignalServiceAddress) {}
|
|
|
|
func didTapUnverifiedIdentityChange(_ address: SignalServiceAddress) {}
|
|
|
|
func didTapInvalidIdentityKeyErrorMessage(_ message: TSInvalidIdentityKeyErrorMessage) {}
|
|
|
|
func didTapCorruptedMessage(_ message: TSErrorMessage) {}
|
|
|
|
func didTapSessionRefreshMessage(_ message: TSErrorMessage) {}
|
|
|
|
func didTapResendGroupUpdateForErrorMessage(_ errorMessage: TSErrorMessage) {}
|
|
|
|
func didTapShowFingerprint(_ address: SignalServiceAddress) {}
|
|
|
|
func didTapIndividualCall(_ call: TSCall) {}
|
|
|
|
func didTapLearnMoreMissedCallFromBlockedContact(_ call: TSCall) {}
|
|
|
|
func didTapGroupCall() {}
|
|
|
|
func didTapPendingOutgoingMessage(_ message: TSOutgoingMessage) {}
|
|
|
|
func didTapFailedOutgoingMessage(_ message: TSOutgoingMessage) {}
|
|
|
|
func didTapGroupMigrationLearnMore() {}
|
|
|
|
func didTapGroupInviteLinkPromotion(groupModel: TSGroupModel) {}
|
|
|
|
func didTapViewGroupDescription(newGroupDescription: String) {}
|
|
|
|
func didTapShowConversationSettings() {}
|
|
|
|
func didTapShowConversationSettingsAndShowMemberRequests() {}
|
|
|
|
func didTapBlockRequest(
|
|
groupModel: TSGroupModelV2,
|
|
requesterName: String,
|
|
requesterAci: Aci) {}
|
|
|
|
func didTapShowUpgradeAppUI() {}
|
|
|
|
func didTapUpdateSystemContact(
|
|
_ address: SignalServiceAddress,
|
|
newNameComponents: PersonNameComponents) {}
|
|
|
|
func didTapPhoneNumberChange(aci: Aci, phoneNumberOld: String, phoneNumberNew: String) {}
|
|
|
|
func didTapViewOnceAttachment(_ interaction: TSInteraction) {}
|
|
|
|
func didTapViewOnceExpired(_ interaction: TSInteraction) {}
|
|
|
|
func didTapContactName(thread: TSContactThread) {}
|
|
|
|
func didTapUnknownThreadWarningGroup() {}
|
|
func didTapUnknownThreadWarningContact() {}
|
|
func didTapDeliveryIssueWarning(_ message: TSErrorMessage) {}
|
|
|
|
func didTapActivatePayments() {}
|
|
func didTapSendPayment() {}
|
|
|
|
func didTapThreadMergeLearnMore(phoneNumber: String) {}
|
|
|
|
func didTapReportSpamLearnMore() {}
|
|
}
|