mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-11-02 21:33:26 +00:00
Implement link previews for text messages using Apple's LinkPreview framework (#4520)
This commit is contained in:
@@ -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>
|
||||
}
|
||||
+25
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user