Add image to call link previews for sent messages

This commit is contained in:
Marissa Le Coz 2024-09-24 17:40:34 -04:00 committed by GitHub
parent d30f9be555
commit 752dffb2c1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 240 additions and 19 deletions

View File

@ -2670,6 +2670,7 @@
E18C4A7729EF2ECC007534D4 /* SignalAccountTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = E18C4A7629EF2ECC007534D4 /* SignalAccountTest.swift */; };
E1A090382A4B909B00F2BE8B /* RecipientHidingManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1A090372A4B909B00F2BE8B /* RecipientHidingManager.swift */; };
E1C2A54B2A8FCB0D00AEC4DA /* DeleteSystemContactViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1C2A54A2A8FCB0D00AEC4DA /* DeleteSystemContactViewController.swift */; };
E1CFAAA32C9DD2B1003145C3 /* LinkPreviewCallLink.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1CFAAA22C9DD2B1003145C3 /* LinkPreviewCallLink.swift */; };
E1D827D52BD9B6E50022C1AF /* ReactionsBurstView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1D827D42BD9B6E50022C1AF /* ReactionsBurstView.swift */; };
E1D827D72BD9DA4D0022C1AF /* ReactionsSink.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1D827D62BD9DA4D0022C1AF /* ReactionsSink.swift */; };
E1D827DA2BDC1F7B0022C1AF /* ReactionBurstManagerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1D827D82BDC1F6B0022C1AF /* ReactionBurstManagerTests.swift */; };
@ -6301,6 +6302,7 @@
E1A090372A4B909B00F2BE8B /* RecipientHidingManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecipientHidingManager.swift; sourceTree = "<group>"; };
E1A0AD8B16E13FDD0071E604 /* CoreFoundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreFoundation.framework; path = System/Library/Frameworks/CoreFoundation.framework; sourceTree = SDKROOT; };
E1C2A54A2A8FCB0D00AEC4DA /* DeleteSystemContactViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeleteSystemContactViewController.swift; sourceTree = "<group>"; };
E1CFAAA22C9DD2B1003145C3 /* LinkPreviewCallLink.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LinkPreviewCallLink.swift; sourceTree = "<group>"; };
E1D827D42BD9B6E50022C1AF /* ReactionsBurstView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReactionsBurstView.swift; sourceTree = "<group>"; };
E1D827D62BD9DA4D0022C1AF /* ReactionsSink.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReactionsSink.swift; sourceTree = "<group>"; };
E1D827D82BDC1F6B0022C1AF /* ReactionBurstManagerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReactionBurstManagerTests.swift; sourceTree = "<group>"; };
@ -7436,6 +7438,7 @@
348815C5255346A500D4F4C4 /* CVNode.swift */,
D9170EE9290C57BF00CD813A /* CVViewState+Banners.swift */,
341D392825472F3B00996E7B /* CVViewState.swift */,
E1CFAAA22C9DD2B1003145C3 /* LinkPreviewCallLink.swift */,
34A95517271B510400B05242 /* LinkPreviewGroupLink.swift */,
346EAA13250199A300E8AB6F /* MemberRequestView.swift */,
4CB5F26820F7D060004D1B42 /* MessageActions.swift */,
@ -16122,6 +16125,7 @@
505C2ED42997015800C23FB2 /* LinkDeviceViewController.swift in Sources */,
3437F63A2512835300AC1767 /* LinkedDevicesTableViewController.swift in Sources */,
76C87FE128BE8E2400BD8709 /* LinkPreviewAttachmentViewController.swift in Sources */,
E1CFAAA32C9DD2B1003145C3 /* LinkPreviewCallLink.swift in Sources */,
507B69122C5044F800F1C6D7 /* LinkPreviewGroupLink.swift in Sources */,
4542DF54208D40AC007B4E76 /* LoadingViewController.swift in Sources */,
4C25768A23AD510800E0398D /* LoadMoreMessagesView.swift in Sources */,

View File

@ -376,16 +376,7 @@ private class _CreateCallLinkViewController: OWSTableViewController2 {
private class CallLinkCardView: UIView {
private lazy var circleView: UIView = {
let circleView = CircleView()
circleView.backgroundColor = UIColor(rgbHex: Constants.iconBackgroundColor)
circleView.autoSetDimensions(to: CGSize(width: Constants.circleViewDimension, height: Constants.circleViewDimension))
let iconImageView = UIImageView(image: UIImage(named: "video-compact"))
iconImageView.tintColor = UIColor(rgbHex: Constants.iconTintColor)
iconImageView.autoSetDimensions(to: CGSize(width: Constants.iconDimension, height: Constants.iconDimension))
circleView.addSubview(iconImageView)
iconImageView.autoCenterInSuperview()
return circleView
return SignalUI.CallLinkComponentFactory.callLinkIconView()
}()
private lazy var textStack: UIStackView = {
@ -502,9 +493,5 @@ private class CallLinkCardView: UIView {
static let spacingTextToButton: CGFloat = 16
static let spacingIconToText: CGFloat = 12
static let textStackSpacing: CGFloat = 2
static let circleViewDimension: CGFloat = 64
static let iconDimension: CGFloat = 36
static let iconBackgroundColor: UInt32 = 0xE4E4FD
static let iconTintColor: UInt32 = 0x5151F6
}
}

View File

@ -1574,6 +1574,16 @@ fileprivate extension CVComponentState.Builder {
state: state
)
}
} else if let _ = CallLink(url: url) {
let state = LinkPreviewCallLink(
linkPreview: linkPreview,
conversationStyle: conversationStyle
)
self.linkPreview = LinkPreview(
linkPreview: linkPreview,
linkPreviewAttachment: nil,
state: state
)
} else {
let linkPreviewAttachment = { () -> TSResource? in
guard

View File

@ -0,0 +1,72 @@
//
// Copyright 2024 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
//
import SignalServiceKit
import SignalUI
// MARK: -
class LinkPreviewCallLink: LinkPreviewState {
private let linkPreview: OWSLinkPreview
public let conversationStyle: ConversationStyle?
public init(
linkPreview: OWSLinkPreview,
conversationStyle: ConversationStyle?
) {
self.linkPreview = linkPreview
self.conversationStyle = conversationStyle
}
public var isLoaded: Bool { true }
public var urlString: String? {
guard let urlString = linkPreview.urlString else {
owsFailDebug("Missing url")
return nil
}
return urlString
}
public var displayDomain: String? {
guard let displayDomain = linkPreview.displayDomain else {
Logger.error("Missing display domain")
return nil
}
return displayDomain
}
public var title: String? {
linkPreview.title?.filterForDisplay.nilIfEmpty ?? CallStrings.signalCall
}
public var imageState: LinkPreviewImageState {
// Image is a local asset.
return .loaded
}
public func imageAsync(thumbnailQuality: AttachmentThumbnailQuality, completion: @escaping (UIImage) -> Void) {}
public func imageCacheKey(thumbnailQuality: AttachmentThumbnailQuality) -> LinkPreviewImageCacheKey? { return nil }
public var imagePixelSize: CGSize {
// This value does not matter since the image is local for call links.
return .zero
}
public var previewDescription: String? {
return linkPreview.previewDescription?.filterForDisplay.nilIfEmpty ?? CallStrings.callLinkDescription
}
public var date: Date? { linkPreview.date }
public let isGroupInviteLink = false
public var isCallLink = true
public var activityIndicatorStyle: UIActivityIndicatorView.Style {
LinkPreviewView.defaultActivityIndicatorStyle
}
}

View File

@ -9,7 +9,6 @@ import SignalUI
// MARK: -
class LinkPreviewGroupLink: LinkPreviewState {
private let linkPreview: OWSLinkPreview
public let linkType: LinkPreviewLinkType
private let groupInviteLinkViewModel: GroupInviteLinkViewModel
@ -122,6 +121,7 @@ class LinkPreviewGroupLink: LinkPreviewState {
var date: Date? { linkPreview.date }
let isGroupInviteLink = true
public let isCallLink = false
var activityIndicatorStyle: UIActivityIndicatorView.Style {
switch linkType {

View File

@ -476,6 +476,13 @@ public enum CallStrings {
comment: "Shown in the header when the user hasn't provided a custom name for a call."
)
}
public static var callLinkDescription: String {
return OWSLocalizedString(
"CALL_LINK_LINK_PREVIEW_DESCRIPTION",
comment: "Shown in a message bubble when you send a call link in a Signal chat"
)
}
}
// MARK: -

View File

@ -310,10 +310,7 @@ public class LinkPreviewFetcherImpl: LinkPreviewFetcher {
let callLinkState = try await CallLinkFetcherImpl().readCallLink(callLink.rootKey, authCredential: authCredential)
return (
callLinkState.localizedName,
OWSLocalizedString(
"CALL_LINK_LINK_PREVIEW_DESCRIPTION",
comment: "Shown in a message bubble when you send a call link in a Signal chat"
)
CallStrings.callLinkDescription
)
}
}

View File

@ -42,6 +42,7 @@ public protocol LinkPreviewState: AnyObject {
var previewDescription: String? { get }
var date: Date? { get }
var isGroupInviteLink: Bool { get }
var isCallLink: Bool { get }
var activityIndicatorStyle: UIActivityIndicatorView.Style { get }
var conversationStyle: ConversationStyle? { get }
}
@ -108,6 +109,8 @@ public class LinkPreviewLoading: LinkPreviewState {
}
}
public let isCallLink: Bool = false
public var activityIndicatorStyle: UIActivityIndicatorView.Style {
switch linkType {
case .incomingMessageGroupInviteLink:
@ -203,6 +206,7 @@ public class LinkPreviewDraft: LinkPreviewState {
public var date: Date? { linkPreviewDraft.date }
public let isGroupInviteLink = false
public let isCallLink: Bool = false
public var activityIndicatorStyle: UIActivityIndicatorView.Style {
LinkPreviewView.defaultActivityIndicatorStyle
@ -332,6 +336,7 @@ public class LinkPreviewSent: LinkPreviewState {
public var date: Date? { linkPreview.date }
public let isGroupInviteLink = false
public var isCallLink = false
public var activityIndicatorStyle: UIActivityIndicatorView.Style {
LinkPreviewView.defaultActivityIndicatorStyle

View File

@ -139,6 +139,8 @@ public class LinkPreviewView: ManualStackViewWithLayer {
return LinkPreviewViewAdapterDraft(state: state)
} else if state.isGroupInviteLink {
return LinkPreviewViewAdapterGroupLink(state: state)
} else if state.isCallLink {
return LinkPreviewViewAdapterCallLink(state: state)
} else {
if state.hasLoadedImage {
if Self.sentIsHero(state: state) {
@ -912,6 +914,143 @@ private class LinkPreviewViewAdapterGroupLink: LinkPreviewViewAdapter {
// MARK: -
private class LinkPreviewViewAdapterCallLink: LinkPreviewViewAdapter {
let state: LinkPreviewState
init(state: LinkPreviewState) {
self.state = state
}
var rootStackConfig: ManualStackView.Config {
ManualStackView.Config(
axis: .horizontal,
alignment: .fill,
spacing: LinkPreviewView.sentNonHeroHSpacing,
layoutMargins: LinkPreviewView.sentNonHeroLayoutMargins
)
}
var textStackConfig: ManualStackView.Config {
return ManualStackView.Config(
axis: .vertical,
alignment: .leading,
spacing: LinkPreviewView.sentVSpacing,
layoutMargins: .zero
)
}
func configureForRendering(
linkPreviewView: LinkPreviewView,
hasAsymmetricalRounding: Bool,
cellMeasurement: CVCellMeasurement
) {
linkPreviewView.backgroundColor = Theme.secondaryBackgroundColor
var rootStackSubviews = [UIView]()
let iconView = CallLinkComponentFactory.callLinkIconView()
iconView.contentMode = .scaleAspectFill
iconView.clipsToBounds = true
rootStackSubviews.append(iconView)
let textStack = linkPreviewView.textStack
var textStackSubviews = [UIView]()
if let titleLabel = sentTitleLabel(state: state) {
textStackSubviews.append(titleLabel)
}
if let descriptionLabel = sentDescriptionLabel(state: state) {
textStackSubviews.append(descriptionLabel)
}
textStack.configure(
config: textStackConfig,
cellMeasurement: cellMeasurement,
measurementKey: Self.measurementKey_textStack,
subviews: textStackSubviews
)
rootStackSubviews.append(textStack)
linkPreviewView.configure(
config: rootStackConfig,
cellMeasurement: cellMeasurement,
measurementKey: Self.measurementKey_rootStack,
subviews: rootStackSubviews
)
}
func measure(
maxWidth: CGFloat,
measurementBuilder: CVCellMeasurement.Builder,
state: LinkPreviewState
) -> CGSize {
var maxLabelWidth = (maxWidth - (
textStackConfig.layoutMargins.totalWidth + rootStackConfig.layoutMargins.totalWidth
))
var rootStackSubviewInfos = [ManualStackSubviewInfo]()
let imageSize = LinkPreviewView.sentNonHeroImageSize
rootStackSubviewInfos.append(CGSize.square(imageSize).asManualSubviewInfo(hasFixedSize: true))
maxLabelWidth -= imageSize + rootStackConfig.spacing
maxLabelWidth = max(0, maxLabelWidth)
var textStackSubviewInfos = [ManualStackSubviewInfo]()
if let labelConfig = sentTitleLabelConfig(state: state) {
let labelSize = CVText.measureLabel(config: labelConfig, maxWidth: maxLabelWidth)
textStackSubviewInfos.append(labelSize.asManualSubviewInfo)
}
if let labelConfig = sentDescriptionLabelConfig(state: state) {
let labelSize = CVText.measureLabel(config: labelConfig, maxWidth: maxLabelWidth)
textStackSubviewInfos.append(labelSize.asManualSubviewInfo)
}
let textStackMeasurement = ManualStackView.measure(
config: textStackConfig,
measurementBuilder: measurementBuilder,
measurementKey: Self.measurementKey_textStack,
subviewInfos: textStackSubviewInfos
)
rootStackSubviewInfos.append(textStackMeasurement.measuredSize.asManualSubviewInfo)
let rootStackMeasurement = ManualStackView.measure(
config: rootStackConfig,
measurementBuilder: measurementBuilder,
measurementKey: Self.measurementKey_rootStack,
subviewInfos: rootStackSubviewInfos,
maxWidth: maxWidth
)
return rootStackMeasurement.measuredSize
}
}
public class CallLinkComponentFactory {
public static func callLinkIconView() -> UIView {
let circleView = CircleView()
circleView.backgroundColor = UIColor(rgbHex: Constants.iconBackgroundColor)
circleView.autoSetDimensions(to: CGSize(width: Constants.circleViewDimension, height: Constants.circleViewDimension))
let iconImageView = UIImageView(image: UIImage(named: "video-compact"))
iconImageView.tintColor = UIColor(rgbHex: Constants.iconTintColor)
iconImageView.autoSetDimensions(to: CGSize(width: Constants.iconDimension, height: Constants.iconDimension))
circleView.addSubview(iconImageView)
iconImageView.autoCenterInSuperview()
return circleView
}
private enum Constants {
static let circleViewDimension: CGFloat = 64
static let iconDimension: CGFloat = 36
static let iconBackgroundColor: UInt32 = 0xE4E4FD
static let iconTintColor: UInt32 = 0x5151F6
}
}
// MARK: -
private class LinkPreviewViewAdapterSentHero: LinkPreviewViewAdapter {
let state: LinkPreviewState