Implement link previews for text messages using Apple's LinkPreview framework (#4520)

This commit is contained in:
Stefan Ceriu
2025-09-19 15:48:09 +03:00
committed by GitHub
parent 94db3adf3a
commit bfaca7cff9
48 changed files with 347 additions and 31 deletions
@@ -0,0 +1,27 @@
//
// Copyright 2025 New Vector Ltd.
//
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
// Please see LICENSE files in the repository root for full details.
//
import LinkPresentation
class LinkMetadataProvider: LinkMetadataProviderProtocol {
private(set) var metadataItems = [URL: LinkMetadataProviderItem]()
func fetchMetadataFor(url: URL) async -> Result<LinkMetadataProviderItem, Error> {
if let item = metadataItems[url] {
return .success(item)
}
do {
let metadata = try await LPMetadataProvider().startFetchingMetadata(for: url)
let item = LinkMetadataProviderItem(url: url, metadata: metadata)
metadataItems[url] = item
return .success(item)
} catch {
return .failure(error)
}
}
}
@@ -0,0 +1,20 @@
//
// Copyright 2025 New Vector Ltd.
//
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
// Please see LICENSE files in the repository root for full details.
//
import Foundation
import LinkPresentation
struct LinkMetadataProviderItem {
let url: URL
let metadata: LPLinkMetadata?
}
protocol LinkMetadataProviderProtocol {
var metadataItems: [URL: LinkMetadataProviderItem] { get }
func fetchMetadataFor(url: URL) async -> Result<LinkMetadataProviderItem, Error>
}
@@ -5,6 +5,7 @@
// Please see LICENSE files in the repository root for full details.
//
import Algorithms
import UIKit
struct TextRoomTimelineItem: TextBasedRoomTimelineItem, Equatable {
@@ -28,4 +29,28 @@ struct TextRoomTimelineItem: TextBasedRoomTimelineItem, Equatable {
var contentType: EventBasedMessageTimelineItemContentType {
.text(content)
}
var links: [URL] {
guard let attributedString = content.formattedBody else {
return []
}
let links = attributedString.runs.compactMap { (run: AttributedString.Runs.Run) -> URL? in
if run.link == nil {
return nil
}
guard run.elementX.eventOnRoomAlias == nil,
run.elementX.eventOnRoomID == nil,
run.elementX.roomAlias == nil,
run.elementX.roomID == nil,
run.elementX.userID == nil else {
return nil
}
return run.link
}
return Array(links.uniqued())
}
}
@@ -6,6 +6,8 @@
//
import SwiftUI
import OrderedCollections
struct RoomTimelineItemView: View {
@Environment(\.timelineContext) var context
@ObservedObject var viewState: RoomTimelineItemViewState
@@ -26,7 +28,7 @@ struct RoomTimelineItemView: View {
@ViewBuilder private var timelineView: some View {
switch viewState.type {
case .text(let item):
TextRoomTimelineView(timelineItem: item)
TextRoomTimelineView(timelineItem: item, linkMetadata: linkMetadataForItem(item))
case .separator(let item):
SeparatorRoomTimelineView(timelineItem: item)
case .image(let item):
@@ -64,13 +66,26 @@ struct RoomTimelineItemView: View {
case .poll(let item):
PollRoomTimelineView(timelineItem: item)
case .voice(let item):
VoiceMessageRoomTimelineView(timelineItem: item, playerState: context?.viewState.audioPlayerStateProvider?(item.id) ?? AudioPlayerState(id: .timelineItemIdentifier(item.id),
title: L10n.commonVoiceMessage,
duration: 0))
let playerState = context?.viewState.audioPlayerStateProvider?(item.id) ?? AudioPlayerState(id: .timelineItemIdentifier(item.id),
title: L10n.commonVoiceMessage,
duration: 0)
VoiceMessageRoomTimelineView(timelineItem: item, playerState: playerState)
case .callInvite(let item):
CallInviteRoomTimelineView(timelineItem: item)
case .callNotification(let item):
CallNotificationRoomTimelineView(timelineItem: item)
}
}
private func linkMetadataForItem(_ item: TextRoomTimelineItem) -> OrderedDictionary<URL, LinkMetadataProviderItem> {
var linkMetadata = OrderedDictionary<URL, LinkMetadataProviderItem>()
for url in item.links.prefix(TextRoomTimelineView.maxLinkPreviewsToRender) {
if let item = context?.viewState.linkMetadataProvider?.metadataItems[url] {
linkMetadata[url] = item
} else {
linkMetadata[url] = LinkMetadataProviderItem(url: url, metadata: nil)
}
}
return linkMetadata
}
}