Don't auto-download attachments from untrusted contacts
This commit is contained in:
parent
e170bfedf1
commit
960e500acd
|
@ -288,6 +288,8 @@
|
||||||
B8F5F58325EC94A6003BF8D4 /* Collection+Subscripting.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8F5F58225EC94A6003BF8D4 /* Collection+Subscripting.swift */; };
|
B8F5F58325EC94A6003BF8D4 /* Collection+Subscripting.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8F5F58225EC94A6003BF8D4 /* Collection+Subscripting.swift */; };
|
||||||
B8F5F60325EDE16F003BF8D4 /* DataExtractionNotification.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8F5F60225EDE16F003BF8D4 /* DataExtractionNotification.swift */; };
|
B8F5F60325EDE16F003BF8D4 /* DataExtractionNotification.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8F5F60225EDE16F003BF8D4 /* DataExtractionNotification.swift */; };
|
||||||
B8F5F61B25EDE4BF003BF8D4 /* DataExtractionNotificationInfoMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8F5F61A25EDE4BF003BF8D4 /* DataExtractionNotificationInfoMessage.swift */; };
|
B8F5F61B25EDE4BF003BF8D4 /* DataExtractionNotificationInfoMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8F5F61A25EDE4BF003BF8D4 /* DataExtractionNotificationInfoMessage.swift */; };
|
||||||
|
B8F5F71A25F1B35C003BF8D4 /* MediaPlaceholderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8F5F71925F1B35C003BF8D4 /* MediaPlaceholderView.swift */; };
|
||||||
|
B8F5F72325F1B4CA003BF8D4 /* DownloadAttachmentModal.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8F5F72225F1B4CA003BF8D4 /* DownloadAttachmentModal.swift */; };
|
||||||
B8FF8DAE25C0D00F004D1F22 /* SessionMessagingKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C3C2A6F025539DE700C340D1 /* SessionMessagingKit.framework */; };
|
B8FF8DAE25C0D00F004D1F22 /* SessionMessagingKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C3C2A6F025539DE700C340D1 /* SessionMessagingKit.framework */; };
|
||||||
B8FF8DAF25C0D00F004D1F22 /* SessionUtilitiesKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C3C2A679255388CC00C340D1 /* SessionUtilitiesKit.framework */; };
|
B8FF8DAF25C0D00F004D1F22 /* SessionUtilitiesKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C3C2A679255388CC00C340D1 /* SessionUtilitiesKit.framework */; };
|
||||||
B8FF8E6225C10DA5004D1F22 /* GeoLite2-Country-Blocks-IPv4 in Resources */ = {isa = PBXBuildFile; fileRef = B8FF8E6125C10DA5004D1F22 /* GeoLite2-Country-Blocks-IPv4 */; };
|
B8FF8E6225C10DA5004D1F22 /* GeoLite2-Country-Blocks-IPv4 in Resources */ = {isa = PBXBuildFile; fileRef = B8FF8E6125C10DA5004D1F22 /* GeoLite2-Country-Blocks-IPv4 */; };
|
||||||
|
@ -1283,6 +1285,8 @@
|
||||||
B8F5F58225EC94A6003BF8D4 /* Collection+Subscripting.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Collection+Subscripting.swift"; sourceTree = "<group>"; };
|
B8F5F58225EC94A6003BF8D4 /* Collection+Subscripting.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Collection+Subscripting.swift"; sourceTree = "<group>"; };
|
||||||
B8F5F60225EDE16F003BF8D4 /* DataExtractionNotification.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataExtractionNotification.swift; sourceTree = "<group>"; };
|
B8F5F60225EDE16F003BF8D4 /* DataExtractionNotification.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataExtractionNotification.swift; sourceTree = "<group>"; };
|
||||||
B8F5F61A25EDE4BF003BF8D4 /* DataExtractionNotificationInfoMessage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataExtractionNotificationInfoMessage.swift; sourceTree = "<group>"; };
|
B8F5F61A25EDE4BF003BF8D4 /* DataExtractionNotificationInfoMessage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataExtractionNotificationInfoMessage.swift; sourceTree = "<group>"; };
|
||||||
|
B8F5F71925F1B35C003BF8D4 /* MediaPlaceholderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaPlaceholderView.swift; sourceTree = "<group>"; };
|
||||||
|
B8F5F72225F1B4CA003BF8D4 /* DownloadAttachmentModal.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DownloadAttachmentModal.swift; sourceTree = "<group>"; };
|
||||||
B8FF8E6125C10DA5004D1F22 /* GeoLite2-Country-Blocks-IPv4 */ = {isa = PBXFileReference; lastKnownFileType = file.bplist; name = "GeoLite2-Country-Blocks-IPv4"; path = "Countries/GeoLite2-Country-Blocks-IPv4"; sourceTree = "<group>"; };
|
B8FF8E6125C10DA5004D1F22 /* GeoLite2-Country-Blocks-IPv4 */ = {isa = PBXFileReference; lastKnownFileType = file.bplist; name = "GeoLite2-Country-Blocks-IPv4"; path = "Countries/GeoLite2-Country-Blocks-IPv4"; sourceTree = "<group>"; };
|
||||||
B8FF8E7325C10FC3004D1F22 /* GeoLite2-Country-Locations-English */ = {isa = PBXFileReference; lastKnownFileType = file.bplist; name = "GeoLite2-Country-Locations-English"; path = "Countries/GeoLite2-Country-Locations-English"; sourceTree = "<group>"; };
|
B8FF8E7325C10FC3004D1F22 /* GeoLite2-Country-Locations-English */ = {isa = PBXFileReference; lastKnownFileType = file.bplist; name = "GeoLite2-Country-Locations-English"; path = "Countries/GeoLite2-Country-Locations-English"; sourceTree = "<group>"; };
|
||||||
B8FF8EA525C11FEF004D1F22 /* IPv4.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IPv4.swift; sourceTree = "<group>"; };
|
B8FF8EA525C11FEF004D1F22 /* IPv4.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IPv4.swift; sourceTree = "<group>"; };
|
||||||
|
@ -2158,6 +2162,7 @@
|
||||||
B8569AD225CBA13D00DBA3DB /* MediaTextOverlayView.swift */,
|
B8569AD225CBA13D00DBA3DB /* MediaTextOverlayView.swift */,
|
||||||
3488F9352191CC4000E524CC /* MediaView.swift */,
|
3488F9352191CC4000E524CC /* MediaView.swift */,
|
||||||
B8041A9425C8FA1D003C2166 /* MediaLoaderView.swift */,
|
B8041A9425C8FA1D003C2166 /* MediaLoaderView.swift */,
|
||||||
|
B8F5F71925F1B35C003BF8D4 /* MediaPlaceholderView.swift */,
|
||||||
C328251E25CA3A900062D0A7 /* QuoteView.swift */,
|
C328251E25CA3A900062D0A7 /* QuoteView.swift */,
|
||||||
34B6A902218B3F62007C4606 /* TypingIndicatorView.swift */,
|
34B6A902218B3F62007C4606 /* TypingIndicatorView.swift */,
|
||||||
C328250E25CA06020062D0A7 /* VoiceMessageView.swift */,
|
C328250E25CA06020062D0A7 /* VoiceMessageView.swift */,
|
||||||
|
@ -2172,6 +2177,7 @@
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
B897621B25D201F7004F83B2 /* ScrollToBottomButton.swift */,
|
B897621B25D201F7004F83B2 /* ScrollToBottomButton.swift */,
|
||||||
|
B8F5F72225F1B4CA003BF8D4 /* DownloadAttachmentModal.swift */,
|
||||||
C3A76A8C25DB83F90074CB90 /* PermissionMissingModal.swift */,
|
C3A76A8C25DB83F90074CB90 /* PermissionMissingModal.swift */,
|
||||||
B821494525D4D6FF009C0F2A /* URLModal.swift */,
|
B821494525D4D6FF009C0F2A /* URLModal.swift */,
|
||||||
B821494E25D4E163009C0F2A /* BodyTextView.swift */,
|
B821494E25D4E163009C0F2A /* BodyTextView.swift */,
|
||||||
|
@ -4917,6 +4923,7 @@
|
||||||
3496957121A301A100DCFE74 /* OWSBackupImportJob.m in Sources */,
|
3496957121A301A100DCFE74 /* OWSBackupImportJob.m in Sources */,
|
||||||
34BECE301F7ABCF800D7438D /* GifPickerLayout.swift in Sources */,
|
34BECE301F7ABCF800D7438D /* GifPickerLayout.swift in Sources */,
|
||||||
C331FFFE2558FF3B00070591 /* ConversationCell.swift in Sources */,
|
C331FFFE2558FF3B00070591 /* ConversationCell.swift in Sources */,
|
||||||
|
B8F5F72325F1B4CA003BF8D4 /* DownloadAttachmentModal.swift in Sources */,
|
||||||
C3DFFAC623E96F0D0058DAF8 /* Sheet.swift in Sources */,
|
C3DFFAC623E96F0D0058DAF8 /* Sheet.swift in Sources */,
|
||||||
C31FFE57254A5FFE00F19441 /* KeyPairUtilities.swift in Sources */,
|
C31FFE57254A5FFE00F19441 /* KeyPairUtilities.swift in Sources */,
|
||||||
B8D84EA325DF745A005A043E /* LinkPreviewState.swift in Sources */,
|
B8D84EA325DF745A005A043E /* LinkPreviewState.swift in Sources */,
|
||||||
|
@ -4933,6 +4940,7 @@
|
||||||
C328254925CA60E60062D0A7 /* ContextMenuVC+Action.swift in Sources */,
|
C328254925CA60E60062D0A7 /* ContextMenuVC+Action.swift in Sources */,
|
||||||
4542DF54208D40AC007B4E76 /* LoadingViewController.swift in Sources */,
|
4542DF54208D40AC007B4E76 /* LoadingViewController.swift in Sources */,
|
||||||
34D5CCA91EAE3D30005515DB /* AvatarViewHelper.m in Sources */,
|
34D5CCA91EAE3D30005515DB /* AvatarViewHelper.m in Sources */,
|
||||||
|
B8F5F71A25F1B35C003BF8D4 /* MediaPlaceholderView.swift in Sources */,
|
||||||
341341EF2187467A00192D59 /* ConversationViewModel.m in Sources */,
|
341341EF2187467A00192D59 /* ConversationViewModel.m in Sources */,
|
||||||
4C21D5D8223AC60F00EF8A77 /* PhotoCapture.swift in Sources */,
|
4C21D5D8223AC60F00EF8A77 /* PhotoCapture.swift in Sources */,
|
||||||
C331FFF32558FF0300070591 /* PathStatusView.swift in Sources */,
|
C331FFF32558FF0300070591 /* PathStatusView.swift in Sources */,
|
||||||
|
|
|
@ -373,30 +373,37 @@ extension ConversationVC : InputViewDelegate, MessageCellDelegate, ContextMenuAc
|
||||||
case .audio: playOrPauseAudio(for: viewItem)
|
case .audio: playOrPauseAudio(for: viewItem)
|
||||||
case .mediaMessage:
|
case .mediaMessage:
|
||||||
guard let index = viewItems.firstIndex(where: { $0 === viewItem }),
|
guard let index = viewItems.firstIndex(where: { $0 === viewItem }),
|
||||||
let cell = messagesTableView.cellForRow(at: IndexPath(row: index, section: 0)) as? VisibleMessageCell, let albumView = cell.albumView else { return }
|
let cell = messagesTableView.cellForRow(at: IndexPath(row: index, section: 0)) as? VisibleMessageCell else { return }
|
||||||
let locationInCell = gestureRecognizer.location(in: cell)
|
if let thread = viewItem.interaction.thread as? TSContactThread,
|
||||||
// Figure out whether the "read more" button was tapped
|
Storage.shared.getContact(with: thread.contactIdentifier())?.isTrusted != true {
|
||||||
if let overlayView = cell.mediaTextOverlayView {
|
// Ask the user whether they want to download this attachment
|
||||||
let locationInOverlayView = cell.convert(locationInCell, to: overlayView)
|
|
||||||
if let readMoreButton = overlayView.readMoreButton, readMoreButton.frame.contains(locationInOverlayView) {
|
} else {
|
||||||
return showFullText(viewItem) // HACK: This is a dirty way to do this
|
guard let albumView = cell.albumView else { return }
|
||||||
|
let locationInCell = gestureRecognizer.location(in: cell)
|
||||||
|
// Figure out whether the "read more" button was tapped
|
||||||
|
if let overlayView = cell.mediaTextOverlayView {
|
||||||
|
let locationInOverlayView = cell.convert(locationInCell, to: overlayView)
|
||||||
|
if let readMoreButton = overlayView.readMoreButton, readMoreButton.frame.contains(locationInOverlayView) {
|
||||||
|
return showFullText(viewItem) // HACK: This is a dirty way to do this
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
// Otherwise, figure out which of the media views was tapped
|
||||||
// Otherwise, figure out which of the media views was tapped
|
let locationInAlbumView = cell.convert(locationInCell, to: albumView)
|
||||||
let locationInAlbumView = cell.convert(locationInCell, to: albumView)
|
guard let mediaView = albumView.mediaView(forLocation: locationInAlbumView) else { return }
|
||||||
guard let mediaView = albumView.mediaView(forLocation: locationInAlbumView) else { return }
|
if albumView.isMoreItemsView(mediaView: mediaView) && viewItem.mediaAlbumHasFailedAttachment() {
|
||||||
if albumView.isMoreItemsView(mediaView: mediaView) && viewItem.mediaAlbumHasFailedAttachment() {
|
|
||||||
// TODO: Tapped a failed incoming attachment
|
|
||||||
}
|
|
||||||
let attachment = mediaView.attachment
|
|
||||||
if let pointer = attachment as? TSAttachmentPointer {
|
|
||||||
if pointer.state == .failed {
|
|
||||||
// TODO: Tapped a failed incoming attachment
|
// TODO: Tapped a failed incoming attachment
|
||||||
}
|
}
|
||||||
|
let attachment = mediaView.attachment
|
||||||
|
if let pointer = attachment as? TSAttachmentPointer {
|
||||||
|
if pointer.state == .failed {
|
||||||
|
// TODO: Tapped a failed incoming attachment
|
||||||
|
}
|
||||||
|
}
|
||||||
|
guard let stream = attachment as? TSAttachmentStream else { return }
|
||||||
|
let gallery = MediaGallery(thread: thread, options: [ .sliderEnabled, .showAllMediaButton ])
|
||||||
|
gallery.presentDetailView(fromViewController: self, mediaAttachment: stream)
|
||||||
}
|
}
|
||||||
guard let stream = attachment as? TSAttachmentStream else { return }
|
|
||||||
let gallery = MediaGallery(thread: thread, options: [ .sliderEnabled, .showAllMediaButton ])
|
|
||||||
gallery.presentDetailView(fromViewController: self, mediaAttachment: stream)
|
|
||||||
case .genericAttachment:
|
case .genericAttachment:
|
||||||
// Open the document if possible
|
// Open the document if possible
|
||||||
guard let url = viewItem.attachmentStream?.originalMediaURL else { return }
|
guard let url = viewItem.attachmentStream?.originalMediaURL else { return }
|
||||||
|
|
|
@ -0,0 +1,50 @@
|
||||||
|
|
||||||
|
final class MediaPlaceholderView : UIView {
|
||||||
|
private let viewItem: ConversationViewItem
|
||||||
|
private let textColor: UIColor
|
||||||
|
|
||||||
|
// MARK: Settings
|
||||||
|
private static let iconSize: CGFloat = 24
|
||||||
|
private static let iconImageViewSize: CGFloat = 40
|
||||||
|
|
||||||
|
// MARK: Lifecycle
|
||||||
|
init(viewItem: ConversationViewItem, textColor: UIColor) {
|
||||||
|
self.viewItem = viewItem
|
||||||
|
self.textColor = textColor
|
||||||
|
super.init(frame: CGRect.zero)
|
||||||
|
setUpViewHierarchy()
|
||||||
|
}
|
||||||
|
|
||||||
|
override init(frame: CGRect) {
|
||||||
|
preconditionFailure("Use init(viewItem:textColor:) instead.")
|
||||||
|
}
|
||||||
|
|
||||||
|
required init?(coder: NSCoder) {
|
||||||
|
preconditionFailure("Use init(viewItem:textColor:) instead.")
|
||||||
|
}
|
||||||
|
|
||||||
|
private func setUpViewHierarchy() {
|
||||||
|
// Image view
|
||||||
|
let iconSize = MediaPlaceholderView.iconSize
|
||||||
|
let icon = UIImage(named: "actionsheet_camera_roll_black")?.withTint(textColor)?.resizedImage(to: CGSize(width: iconSize, height: iconSize))
|
||||||
|
let imageView = UIImageView(image: icon)
|
||||||
|
imageView.contentMode = .center
|
||||||
|
let iconImageViewSize = MediaPlaceholderView.iconImageViewSize
|
||||||
|
imageView.set(.width, to: iconImageViewSize)
|
||||||
|
imageView.set(.height, to: iconImageViewSize)
|
||||||
|
// Body label
|
||||||
|
let titleLabel = UILabel()
|
||||||
|
titleLabel.lineBreakMode = .byTruncatingTail
|
||||||
|
titleLabel.text = "Download Media?"
|
||||||
|
titleLabel.textColor = textColor
|
||||||
|
titleLabel.font = .systemFont(ofSize: Values.mediumFontSize)
|
||||||
|
// Stack view
|
||||||
|
let stackView = UIStackView(arrangedSubviews: [ imageView, titleLabel ])
|
||||||
|
stackView.axis = .horizontal
|
||||||
|
stackView.alignment = .center
|
||||||
|
stackView.isLayoutMarginsRelativeArrangement = true
|
||||||
|
stackView.layoutMargins = UIEdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 12)
|
||||||
|
addSubview(stackView)
|
||||||
|
stackView.pin(to: self, withInset: Values.smallSpacing)
|
||||||
|
}
|
||||||
|
}
|
|
@ -331,25 +331,32 @@ final class VisibleMessageCell : MessageCell, LinkPreviewViewDelegate {
|
||||||
stackView.pin(to: snContentView, withInset: inset)
|
stackView.pin(to: snContentView, withInset: inset)
|
||||||
}
|
}
|
||||||
case .mediaMessage:
|
case .mediaMessage:
|
||||||
guard let cache = delegate?.getMediaCache() else { preconditionFailure() }
|
if let thread = viewItem.interaction.thread as? TSContactThread,
|
||||||
let maxMessageWidth = VisibleMessageCell.getMaxWidth(for: viewItem)
|
Storage.shared.getContact(with: thread.contactIdentifier())?.isTrusted != true {
|
||||||
let albumView = MediaAlbumView(mediaCache: cache, items: viewItem.mediaAlbumItems!, isOutgoing: isOutgoing, maxMessageWidth: maxMessageWidth)
|
let mediaPlaceholderView = MediaPlaceholderView(viewItem: viewItem, textColor: bodyLabelTextColor)
|
||||||
self.albumView = albumView
|
snContentView.addSubview(mediaPlaceholderView)
|
||||||
snContentView.addSubview(albumView)
|
mediaPlaceholderView.pin(to: snContentView)
|
||||||
let size = getSize(for: viewItem)
|
} else {
|
||||||
albumView.set(.width, to: size.width)
|
guard let cache = delegate?.getMediaCache() else { preconditionFailure() }
|
||||||
albumView.set(.height, to: size.height)
|
let maxMessageWidth = VisibleMessageCell.getMaxWidth(for: viewItem)
|
||||||
albumView.pin(to: snContentView)
|
let albumView = MediaAlbumView(mediaCache: cache, items: viewItem.mediaAlbumItems!, isOutgoing: isOutgoing, maxMessageWidth: maxMessageWidth)
|
||||||
albumView.loadMedia()
|
self.albumView = albumView
|
||||||
albumView.layer.mask = bubbleViewMaskLayer
|
snContentView.addSubview(albumView)
|
||||||
if let message = viewItem.interaction as? TSMessage, let body = message.body, body.count > 0,
|
let size = getSize(for: viewItem)
|
||||||
let delegate = delegate { // delegate should always be set at this point
|
albumView.set(.width, to: size.width)
|
||||||
let overlayView = MediaTextOverlayView(viewItem: viewItem, albumViewWidth: size.width, delegate: delegate)
|
albumView.set(.height, to: size.height)
|
||||||
self.mediaTextOverlayView = overlayView
|
albumView.pin(to: snContentView)
|
||||||
snContentView.addSubview(overlayView)
|
albumView.loadMedia()
|
||||||
overlayView.pin([ UIView.HorizontalEdge.left, UIView.VerticalEdge.bottom, UIView.HorizontalEdge.right ], to: snContentView)
|
albumView.layer.mask = bubbleViewMaskLayer
|
||||||
|
if let message = viewItem.interaction as? TSMessage, let body = message.body, body.count > 0,
|
||||||
|
let delegate = delegate { // delegate should always be set at this point
|
||||||
|
let overlayView = MediaTextOverlayView(viewItem: viewItem, albumViewWidth: size.width, delegate: delegate)
|
||||||
|
self.mediaTextOverlayView = overlayView
|
||||||
|
snContentView.addSubview(overlayView)
|
||||||
|
overlayView.pin([ UIView.HorizontalEdge.left, UIView.VerticalEdge.bottom, UIView.HorizontalEdge.right ], to: snContentView)
|
||||||
|
}
|
||||||
|
unloadContent = { albumView.unloadMedia() }
|
||||||
}
|
}
|
||||||
unloadContent = { albumView.unloadMedia() }
|
|
||||||
case .audio:
|
case .audio:
|
||||||
let voiceMessageView = VoiceMessageView(viewItem: viewItem)
|
let voiceMessageView = VoiceMessageView(viewItem: viewItem)
|
||||||
snContentView.addSubview(voiceMessageView)
|
snContentView.addSubview(voiceMessageView)
|
||||||
|
|
|
@ -0,0 +1,79 @@
|
||||||
|
|
||||||
|
final class DownloadAttachmentModal : Modal {
|
||||||
|
private let viewItem: ConversationViewItem
|
||||||
|
|
||||||
|
// MARK: Lifecycle
|
||||||
|
init(viewItem: ConversationViewItem) {
|
||||||
|
self.viewItem = viewItem
|
||||||
|
super.init(nibName: nil, bundle: nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
override init(nibName: String?, bundle: Bundle?) {
|
||||||
|
preconditionFailure("Use init(viewItem:) instead.")
|
||||||
|
}
|
||||||
|
|
||||||
|
required init?(coder: NSCoder) {
|
||||||
|
preconditionFailure("Use init(viewItem:) instead.")
|
||||||
|
}
|
||||||
|
|
||||||
|
override func populateContentView() {
|
||||||
|
guard let publicKey = (viewItem.interaction as? TSIncomingMessage)?.authorId else { return }
|
||||||
|
// Name
|
||||||
|
let name = Storage.shared.getContact(with: publicKey)?.displayName(for: .regular) ?? publicKey
|
||||||
|
// Title
|
||||||
|
let titleLabel = UILabel()
|
||||||
|
titleLabel.textColor = Colors.text
|
||||||
|
titleLabel.font = .boldSystemFont(ofSize: Values.largeFontSize)
|
||||||
|
titleLabel.text = "Trust \(name)?"
|
||||||
|
titleLabel.textAlignment = .center
|
||||||
|
// Message
|
||||||
|
let messageLabel = UILabel()
|
||||||
|
messageLabel.textColor = Colors.text
|
||||||
|
messageLabel.font = .systemFont(ofSize: Values.smallFontSize)
|
||||||
|
let message = "Are you sure you want to download media sent by \(name)?"
|
||||||
|
let attributedMessage = NSMutableAttributedString(string: message)
|
||||||
|
attributedMessage.addAttributes([ .font : UIFont.boldSystemFont(ofSize: Values.smallFontSize) ], range: (message as NSString).range(of: name))
|
||||||
|
messageLabel.attributedText = attributedMessage
|
||||||
|
messageLabel.numberOfLines = 0
|
||||||
|
messageLabel.lineBreakMode = .byWordWrapping
|
||||||
|
messageLabel.textAlignment = .center
|
||||||
|
// Unblock button
|
||||||
|
let unblockButton = UIButton()
|
||||||
|
unblockButton.set(.height, to: Values.mediumButtonHeight)
|
||||||
|
unblockButton.layer.cornerRadius = Modal.buttonCornerRadius
|
||||||
|
unblockButton.backgroundColor = Colors.buttonBackground
|
||||||
|
unblockButton.titleLabel!.font = .systemFont(ofSize: Values.smallFontSize)
|
||||||
|
unblockButton.setTitleColor(Colors.text, for: UIControl.State.normal)
|
||||||
|
unblockButton.setTitle("Download", for: UIControl.State.normal)
|
||||||
|
unblockButton.addTarget(self, action: #selector(trust), for: UIControl.Event.touchUpInside)
|
||||||
|
// Button stack view
|
||||||
|
let buttonStackView = UIStackView(arrangedSubviews: [ cancelButton, unblockButton ])
|
||||||
|
buttonStackView.axis = .horizontal
|
||||||
|
buttonStackView.spacing = Values.mediumSpacing
|
||||||
|
buttonStackView.distribution = .fillEqually
|
||||||
|
// Main stack view
|
||||||
|
let mainStackView = UIStackView(arrangedSubviews: [ titleLabel, messageLabel, buttonStackView ])
|
||||||
|
mainStackView.axis = .vertical
|
||||||
|
mainStackView.spacing = Values.largeSpacing
|
||||||
|
contentView.addSubview(mainStackView)
|
||||||
|
mainStackView.pin(.leading, to: .leading, of: contentView, withInset: Values.largeSpacing)
|
||||||
|
mainStackView.pin(.top, to: .top, of: contentView, withInset: Values.largeSpacing)
|
||||||
|
contentView.pin(.trailing, to: .trailing, of: mainStackView, withInset: Values.largeSpacing)
|
||||||
|
contentView.pin(.bottom, to: .bottom, of: mainStackView, withInset: Values.largeSpacing)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: Interaction
|
||||||
|
@objc private func trust() {
|
||||||
|
guard let message = viewItem.interaction as? TSIncomingMessage else { return }
|
||||||
|
let publicKey = message.authorId
|
||||||
|
let contact = Storage.shared.getContact(with: publicKey) ?? Contact(sessionID: publicKey)
|
||||||
|
contact.isTrusted = true
|
||||||
|
Storage.write(with: { transaction in
|
||||||
|
Storage.shared.setContact(contact, using: transaction)
|
||||||
|
message.touch(with: transaction)
|
||||||
|
}, completion: {
|
||||||
|
Storage.shared.resumeAttachmentDownloadJobsIfNeeded(for: message.uniqueId!)
|
||||||
|
})
|
||||||
|
presentingViewController?.dismiss(animated: true, completion: nil)
|
||||||
|
}
|
||||||
|
}
|
|
@ -10,6 +10,8 @@ public class Contact : NSObject, NSCoding { // NSObject/NSCoding conformance is
|
||||||
@objc public var profilePictureEncryptionKey: OWSAES256Key?
|
@objc public var profilePictureEncryptionKey: OWSAES256Key?
|
||||||
/// The ID of the thread associated with this contact.
|
/// The ID of the thread associated with this contact.
|
||||||
@objc public var threadID: String?
|
@objc public var threadID: String?
|
||||||
|
/// This flag is used to determine whether we should auto-download files sent by this contact.
|
||||||
|
@objc public var isTrusted = false
|
||||||
|
|
||||||
// MARK: Name
|
// MARK: Name
|
||||||
/// The name of the contact. Use this whenever you need the "real", underlying name of a user (e.g. when sending a message).
|
/// The name of the contact. Use this whenever you need the "real", underlying name of a user (e.g. when sending a message).
|
||||||
|
@ -54,8 +56,10 @@ public class Contact : NSObject, NSCoding { // NSObject/NSCoding conformance is
|
||||||
|
|
||||||
// MARK: Coding
|
// MARK: Coding
|
||||||
public required init?(coder: NSCoder) {
|
public required init?(coder: NSCoder) {
|
||||||
guard let sessionID = coder.decodeObject(forKey: "sessionID") as! String? else { return nil }
|
guard let sessionID = coder.decodeObject(forKey: "sessionID") as! String?,
|
||||||
|
let isTrusted = coder.decodeObject(forKey: "isTrusted") as! Bool? else { return nil }
|
||||||
self.sessionID = sessionID
|
self.sessionID = sessionID
|
||||||
|
self.isTrusted = isTrusted
|
||||||
if let name = coder.decodeObject(forKey: "displayName") as! String? { self.name = name }
|
if let name = coder.decodeObject(forKey: "displayName") as! String? { self.name = name }
|
||||||
if let nickname = coder.decodeObject(forKey: "nickname") as! String? { self.nickname = nickname }
|
if let nickname = coder.decodeObject(forKey: "nickname") as! String? { self.nickname = nickname }
|
||||||
if let profilePictureURL = coder.decodeObject(forKey: "profilePictureURL") as! String? { self.profilePictureURL = profilePictureURL }
|
if let profilePictureURL = coder.decodeObject(forKey: "profilePictureURL") as! String? { self.profilePictureURL = profilePictureURL }
|
||||||
|
@ -72,6 +76,7 @@ public class Contact : NSObject, NSCoding { // NSObject/NSCoding conformance is
|
||||||
coder.encode(profilePictureFileName, forKey: "profilePictureFileName")
|
coder.encode(profilePictureFileName, forKey: "profilePictureFileName")
|
||||||
coder.encode(profilePictureEncryptionKey, forKey: "profilePictureEncryptionKey")
|
coder.encode(profilePictureEncryptionKey, forKey: "profilePictureEncryptionKey")
|
||||||
coder.encode(threadID, forKey: "threadID")
|
coder.encode(threadID, forKey: "threadID")
|
||||||
|
coder.encode(isTrusted, forKey: "isTrusted")
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: Equality
|
// MARK: Equality
|
||||||
|
|
|
@ -9,12 +9,18 @@ extension Storage {
|
||||||
Storage.read { transaction in
|
Storage.read { transaction in
|
||||||
result = transaction.object(forKey: sessionID, inCollection: Storage.contactCollection) as? Contact
|
result = transaction.object(forKey: sessionID, inCollection: Storage.contactCollection) as? Contact
|
||||||
}
|
}
|
||||||
|
if let result = result, result.sessionID == getUserHexEncodedPublicKey() {
|
||||||
|
result.isTrusted = true // Always trust ourselves
|
||||||
|
}
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc(setContact:usingTransaction:)
|
@objc(setContact:usingTransaction:)
|
||||||
public func setContact(_ contact: Contact, using transaction: Any) {
|
public func setContact(_ contact: Contact, using transaction: Any) {
|
||||||
let transaction = transaction as! YapDatabaseReadWriteTransaction
|
let transaction = transaction as! YapDatabaseReadWriteTransaction
|
||||||
|
if contact.sessionID == getUserHexEncodedPublicKey() {
|
||||||
|
contact.isTrusted = true // Always trust ourselves
|
||||||
|
}
|
||||||
transaction.setObject(contact, forKey: contact.sessionID, inCollection: Storage.contactCollection)
|
transaction.setObject(contact, forKey: contact.sessionID, inCollection: Storage.contactCollection)
|
||||||
transaction.addCompletionQueue(DispatchQueue.main) {
|
transaction.addCompletionQueue(DispatchQueue.main) {
|
||||||
let notificationCenter = NotificationCenter.default
|
let notificationCenter = NotificationCenter.default
|
||||||
|
|
|
@ -72,6 +72,25 @@ extension Storage {
|
||||||
#endif
|
#endif
|
||||||
return result.first
|
return result.first
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public func getAttachmentDownloadJobs(for tsMessageID: String) -> [AttachmentDownloadJob] {
|
||||||
|
var result: [AttachmentDownloadJob] = []
|
||||||
|
Storage.read { transaction in
|
||||||
|
transaction.enumerateRows(inCollection: AttachmentDownloadJob.collection) { _, object, _, _ in
|
||||||
|
guard let job = object as? AttachmentDownloadJob, job.tsMessageID == tsMessageID else { return }
|
||||||
|
result.append(job)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
public func resumeAttachmentDownloadJobsIfNeeded(for tsMessageID: String) {
|
||||||
|
let jobs = getAttachmentDownloadJobs(for: tsMessageID)
|
||||||
|
jobs.forEach { job in
|
||||||
|
job.delegate = JobQueue.shared
|
||||||
|
job.execute()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public func getMessageSendJob(for messageSendJobID: String) -> MessageSendJob? {
|
public func getMessageSendJob(for messageSendJobID: String) -> MessageSendJob? {
|
||||||
var result: MessageSendJob?
|
var result: MessageSendJob?
|
||||||
|
|
|
@ -8,6 +8,7 @@ public final class AttachmentDownloadJob : NSObject, Job, NSCoding { // NSObject
|
||||||
public var delegate: JobDelegate?
|
public var delegate: JobDelegate?
|
||||||
public var id: String?
|
public var id: String?
|
||||||
public var failureCount: UInt = 0
|
public var failureCount: UInt = 0
|
||||||
|
public var isDeferred = false
|
||||||
|
|
||||||
public enum Error : LocalizedError {
|
public enum Error : LocalizedError {
|
||||||
case noAttachment
|
case noAttachment
|
||||||
|
@ -33,11 +34,13 @@ public final class AttachmentDownloadJob : NSObject, Job, NSCoding { // NSObject
|
||||||
public init?(coder: NSCoder) {
|
public init?(coder: NSCoder) {
|
||||||
guard let attachmentID = coder.decodeObject(forKey: "attachmentID") as! String?,
|
guard let attachmentID = coder.decodeObject(forKey: "attachmentID") as! String?,
|
||||||
let tsMessageID = coder.decodeObject(forKey: "tsIncomingMessageID") as! String?,
|
let tsMessageID = coder.decodeObject(forKey: "tsIncomingMessageID") as! String?,
|
||||||
let id = coder.decodeObject(forKey: "id") as! String? else { return nil }
|
let id = coder.decodeObject(forKey: "id") as! String?,
|
||||||
|
let isDeferred = coder.decodeObject(forKey: "isDeferred") as! Bool? else { return nil }
|
||||||
self.attachmentID = attachmentID
|
self.attachmentID = attachmentID
|
||||||
self.tsMessageID = tsMessageID
|
self.tsMessageID = tsMessageID
|
||||||
self.id = id
|
self.id = id
|
||||||
self.failureCount = coder.decodeObject(forKey: "failureCount") as! UInt? ?? 0
|
self.failureCount = coder.decodeObject(forKey: "failureCount") as! UInt? ?? 0
|
||||||
|
self.isDeferred = isDeferred
|
||||||
}
|
}
|
||||||
|
|
||||||
public func encode(with coder: NSCoder) {
|
public func encode(with coder: NSCoder) {
|
||||||
|
@ -45,10 +48,12 @@ public final class AttachmentDownloadJob : NSObject, Job, NSCoding { // NSObject
|
||||||
coder.encode(tsMessageID, forKey: "tsIncomingMessageID")
|
coder.encode(tsMessageID, forKey: "tsIncomingMessageID")
|
||||||
coder.encode(id, forKey: "id")
|
coder.encode(id, forKey: "id")
|
||||||
coder.encode(failureCount, forKey: "failureCount")
|
coder.encode(failureCount, forKey: "failureCount")
|
||||||
|
coder.encode(isDeferred, forKey: "isDeferred")
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: Running
|
// MARK: Running
|
||||||
public func execute() {
|
public func execute() {
|
||||||
|
guard !isDeferred else { return }
|
||||||
if TSAttachment.fetch(uniqueId: attachmentID) is TSAttachmentStream {
|
if TSAttachment.fetch(uniqueId: attachmentID) is TSAttachmentStream {
|
||||||
// FIXME: It's not clear * how * this happens, but apparently we can get to this point
|
// FIXME: It's not clear * how * this happens, but apparently we can get to this point
|
||||||
// from time to time with an already downloaded attachment.
|
// from time to time with an already downloaded attachment.
|
||||||
|
|
|
@ -276,8 +276,11 @@ extension MessageReceiver {
|
||||||
groupPublicKey: message.groupPublicKey, openGroupID: openGroupID, using: transaction) else { throw Error.noThread }
|
groupPublicKey: message.groupPublicKey, openGroupID: openGroupID, using: transaction) else { throw Error.noThread }
|
||||||
message.threadID = threadID
|
message.threadID = threadID
|
||||||
// Start attachment downloads if needed
|
// Start attachment downloads if needed
|
||||||
|
let isContactTrusted = Storage.shared.getContact(with: message.sender!)?.isTrusted ?? false
|
||||||
|
let isGroup = message.groupPublicKey != nil || openGroupID != nil
|
||||||
attachmentsToDownload.forEach { attachmentID in
|
attachmentsToDownload.forEach { attachmentID in
|
||||||
let downloadJob = AttachmentDownloadJob(attachmentID: attachmentID, tsMessageID: tsMessageID)
|
let downloadJob = AttachmentDownloadJob(attachmentID: attachmentID, tsMessageID: tsMessageID)
|
||||||
|
downloadJob.isDeferred = !isContactTrusted && !isGroup
|
||||||
if isMainAppAndActive {
|
if isMainAppAndActive {
|
||||||
JobQueue.shared.add(downloadJob, using: transaction)
|
JobQueue.shared.add(downloadJob, using: transaction)
|
||||||
} else {
|
} else {
|
||||||
|
|
Loading…
Reference in New Issue