Files
Morgan Pretty 1817fbb45d Merge branch 'session-pro-settings' into feature/libsession-message-wrapping
# Conflicts:
#	Session.xcodeproj/project.pbxproj
#	Session/Closed Groups/EditGroupViewModel.swift
#	Session/Conversations/ConversationVC+Interaction.swift
#	Session/Conversations/ConversationVC.swift
#	Session/Conversations/ConversationViewModel.swift
#	Session/Conversations/Input View/ExpandingAttachmentsButton.swift
#	Session/Conversations/Input View/InputView.swift
#	Session/Conversations/Message Cells/Content Views/SwiftUI/QuoteView_SwiftUI.swift
#	Session/Conversations/Message Cells/VisibleMessageCell.swift
#	Session/Conversations/Settings/ThreadSettingsViewModel.swift
#	Session/Media Viewing & Editing/MessageInfoScreen.swift
#	Session/Meta/AppDelegate.swift
#	Session/Settings/BlockedContactsViewModel.swift
#	Session/Settings/DeveloperSettings/DeveloperSettingsProViewModel.swift
#	Session/Settings/SettingsViewModel.swift
#	Session/Settings/Views/ThemeMessagePreviewView.swift
#	Session/Shared/BaseVC.swift
#	Session/Shared/UserListViewModel.swift
#	SessionMessagingKit/Database/Models/LinkPreview.swift
#	SessionMessagingKit/LibSession/Config Handling/LibSession+Pro.swift
#	SessionMessagingKit/Open Groups/OpenGroupManager.swift
#	SessionMessagingKit/Sending & Receiving/Link Previews/LinkPreviewDraft.swift
#	SessionMessagingKit/Shared Models/MessageViewModel.swift
#	SessionMessagingKit/Shared Models/SessionThreadViewModel.swift
#	SessionMessagingKit/Utilities/ProfilePictureView+Convenience.swift
#	SessionMessagingKit/Utilities/SessionProState.swift
#	SessionShareExtension/ThreadPickerVC.swift
#	SessionUIKit/Components/ProfilePictureView.swift
#	SessionUIKit/Components/QuoteView.swift
#	SessionUIKit/Components/SwiftUI/ProCTAModal.swift
#	SessionUIKit/Utilities/MentionUtilities.swift
#	SessionUtilitiesKit/General/Feature.swift
#	SignalUtilitiesKit/Media Viewing & Editing/Attachment Approval/AttachmentApprovalViewController.swift
#	SignalUtilitiesKit/Media Viewing & Editing/Attachment Approval/AttachmentTextToolbar.swift
2025-11-21 11:07:05 +11:00

153 lines
5.3 KiB
Swift

// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
//
// stringlint:disable
import Foundation
import Combine
import UniformTypeIdentifiers
import GRDB
import SessionUIKit
import SessionUtilitiesKit
import SessionNetworkingKit
public struct LinkPreview: Sendable, Codable, Equatable, Hashable, FetchableRecord, PersistableRecord, TableRecord, ColumnExpressible {
public static var databaseTableName: String { "linkPreview" }
/// We want to cache url previews to the nearest 100,000 seconds (~28 hours - simpler than 86,400) to ensure the user isn't shown a preview that is too stale
public static let timstampResolution: Double = 100000
internal static let maxImageDimension: CGFloat = 600
public typealias Columns = CodingKeys
public enum CodingKeys: String, CodingKey, ColumnExpression {
case url
case timestamp
case variant
case title
case attachmentId
}
public enum Variant: Int, Sendable, Codable, Hashable, CaseIterable, DatabaseValueConvertible {
case standard
case openGroupInvitation
}
/// The url for the link preview
public let url: String
/// The number of seconds since epoch rounded down to the nearest 100,000 seconds (~day) - This
/// allows us to optimise against duplicate urls without having "stale" data last too long
public let timestamp: TimeInterval
/// The type of link preview
public let variant: Variant
/// The title for the link
public let title: String?
/// The id for the attachment for the link preview image
public let attachmentId: String?
// MARK: - Initialization
public init(
url: String,
timestamp: TimeInterval? = nil,
variant: Variant = .standard,
title: String?,
attachmentId: String? = nil,
using dependencies: Dependencies
) {
self.url = url
self.timestamp = (timestamp ?? LinkPreview.timestampFor(
sentTimestampMs: dependencies[cache: .snodeAPI].currentOffsetTimestampMs() // Default to now
))
self.variant = variant
self.title = title
self.attachmentId = attachmentId
}
}
// MARK: - Protobuf
public extension LinkPreview {
init?(
_ db: ObservingDatabase,
linkPreview: VisibleMessage.VMLinkPreview,
sentTimestampMs: UInt64
) throws {
guard LinkPreviewManager.isValidLinkUrl(linkPreview.url) else { throw LinkPreviewError.invalidInput }
// Try to get an existing link preview first
let timestamp: TimeInterval = LinkPreview.timestampFor(sentTimestampMs: sentTimestampMs)
let maybeLinkPreview: LinkPreview? = try? LinkPreview
.filter(LinkPreview.Columns.url == linkPreview.url)
.filter(LinkPreview.Columns.timestamp == timestamp)
.fetchOne(db)
if let linkPreview: LinkPreview = maybeLinkPreview {
self = linkPreview
return
}
self.url = linkPreview.url
self.timestamp = timestamp
self.variant = .standard
self.title = LinkPreviewManager.normalizeTitle(title: linkPreview.title)
if let attachment: Attachment = linkPreview.nonInsertedAttachment {
try attachment.insert(db)
self.attachmentId = attachment.id
}
else {
self.attachmentId = nil
}
// Make sure the quote is valid before completing
guard self.title != nil || self.attachmentId != nil else { throw LinkPreviewError.invalidInput }
}
}
// MARK: - Convenience
public extension LinkPreview {
struct URLMatchResult {
let urlString: String
let matchRange: NSRange
}
static func timestampFor(sentTimestampMs: UInt64) -> TimeInterval {
// We want to round the timestamp down to the nearest 100,000 seconds (~28 hours - simpler
// than 86,400) to optimise LinkPreview storage without having too stale data
return (floor(Double(sentTimestampMs) / 1000 / LinkPreview.timstampResolution) * LinkPreview.timstampResolution)
}
static func prepareAttachmentIfPossible(
urlString: String,
imageSource: ImageDataManager.DataSource?,
using dependencies: Dependencies
) async throws -> PreparedAttachment? {
guard let imageSource: ImageDataManager.DataSource = imageSource, imageSource.contentExists else {
return nil
}
let pendingAttachment: PendingAttachment = PendingAttachment(
source: .media(imageSource),
using: dependencies
)
let targetFormat: PendingAttachment.ConversionFormat = (dependencies[feature: .usePngInsteadOfWebPForFallbackImageType] ?
.png(maxDimension: LinkPreview.maxImageDimension) : .webPLossy(maxDimension: LinkPreview.maxImageDimension)
)
return try await pendingAttachment.prepare(
operations: [
.convert(to: targetFormat),
.stripImageMetadata
],
/// We only call `prepareAttachmentIfPossible` before sending so always store at the pending upload path
storeAtPendingAttachmentUploadPath: true,
using: dependencies
)
}
}