Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add URL previews as a Labs feature #4790

Merged
merged 32 commits into from
Sep 8, 2021
Merged
Show file tree
Hide file tree
Changes from 25 commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
eacb8a4
Begin adding link detection to RoomBubbleCellData.
pixlwave Jul 21, 2021
661b536
Merge branch 'develop' into doug/888_add_url_previews
pixlwave Aug 17, 2021
22382e9
Merge branch 'develop' into doug/888_add_url_previews
pixlwave Aug 18, 2021
29758d1
Add PreviewManger with Core Data cache and a URLPreviewView with a vi…
pixlwave Aug 23, 2021
5f598a9
Add comments about the un-sanitized URL.
pixlwave Aug 24, 2021
59e5416
Load and store URLPreviewViewData in RoomBubbleCellData.
pixlwave Sep 1, 2021
a81ebbd
Refactoring and tidy up.
pixlwave Sep 1, 2021
042eb8e
Use stack views for layout.
pixlwave Sep 2, 2021
9fb13b7
Update layout for text only previews.
pixlwave Sep 2, 2021
1831b61
Show an activity indicator until the preview has loaded.
pixlwave Sep 2, 2021
4924110
Merge remote-tracking branch 'origin/develop' into doug/888_add_url_p…
pixlwave Sep 2, 2021
6a5b12a
Ensure correct font is used.
pixlwave Sep 2, 2021
2e04123
Add setting to disable URL previews.
pixlwave Sep 3, 2021
be83d8e
Fix edits to previewable links not working.
pixlwave Sep 3, 2021
80f8cc6
Hide the loading state on error.
pixlwave Sep 3, 2021
7db81cc
Break-up cell data after a link even if the new event isn't a message.
pixlwave Sep 3, 2021
434657e
Fix reactions beneath URL previews.
pixlwave Sep 3, 2021
cf3733c
Clear the URL preview manager's store when clearing caches.
pixlwave Sep 3, 2021
55df930
Fix potentially redundant table reloading.
pixlwave Sep 3, 2021
7448ca1
Observe URL preview update notification in RoomViewController.
pixlwave Sep 7, 2021
1c7adf0
Fix unsatisfiable constraints messages.
pixlwave Sep 7, 2021
0094add
Move url preview setting under labs section.
pixlwave Sep 7, 2021
4ad0416
Remove "Loading preview..." label.
pixlwave Sep 7, 2021
c007bc5
Fix settings toggle not enabled.
pixlwave Sep 7, 2021
08d548c
Add changelog entry.
pixlwave Sep 7, 2021
1c7cef5
Merge branch 'develop' into doug/888_add_url_previews
pixlwave Sep 7, 2021
ea14ed9
Add more docs and comments.
pixlwave Sep 8, 2021
24afc7a
Update for PR feedback.
pixlwave Sep 8, 2021
206017c
Rename Core Data objects.
pixlwave Sep 8, 2021
2656746
Revert height computation for now.
pixlwave Sep 8, 2021
ad618b4
Add matrix.to to firstURLDetectionIgnoredHosts.
pixlwave Sep 8, 2021
dfb63e1
Revert "Add matrix.to to firstURLDetectionIgnoredHosts."
pixlwave Sep 8, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions Config/CommonConfiguration.swift
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,9 @@ class CommonConfiguration: NSObject, Configurable {
settings.messageDetailsAllowCopyingMedia = BuildSettings.messageDetailsAllowCopyMedia
settings.messageDetailsAllowPastingMedia = BuildSettings.messageDetailsAllowPasteMedia

// Enable link detection if url preview are enabled
settings.enableBubbleComponentLinkDetection = true

MXKContactManager.shared().allowLocalContactsAccess = BuildSettings.allowLocalContactsAccess
}

Expand Down
6 changes: 6 additions & 0 deletions Riot/Assets/Images.xcassets/Room/URLPreviews/Contents.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"info" : {
"author" : "xcode",
"version" : 1
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"images" : [
{
"filename" : "url_preview_close.pdf",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"preserves-vector-representation" : true
}
}
Binary file not shown.
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"images" : [
{
"filename" : "url_preview_close_dark.pdf",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"preserves-vector-representation" : true
}
}
Binary file not shown.
3 changes: 3 additions & 0 deletions Riot/Assets/en.lproj/Vector.strings
Original file line number Diff line number Diff line change
Expand Up @@ -537,6 +537,9 @@ Tap the + to start adding people.";
"settings_ui_theme_picker_message_invert_colours" = "\"Auto\" uses your device's \"Invert Colours\" settings";
"settings_ui_theme_picker_message_match_system_theme" = "\"Auto\" matches your device's system theme";

"settings_show_url_previews" = "Show inline URL previews";
"settings_show_url_previews_description" = "Previews will only be shown in unencrypted rooms.";

"settings_unignore_user" = "Show all messages from %@?";

"settings_contacts_discover_matrix_users" = "Use emails and phone numbers to discover users";
Expand Down
2 changes: 2 additions & 0 deletions Riot/Generated/Images.swift
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,8 @@ internal enum Asset {
internal static let videoCall = ImageAsset(name: "video_call")
internal static let voiceCallHangonIcon = ImageAsset(name: "voice_call_hangon_icon")
internal static let voiceCallHangupIcon = ImageAsset(name: "voice_call_hangup_icon")
internal static let urlPreviewClose = ImageAsset(name: "url_preview_close")
internal static let urlPreviewCloseDark = ImageAsset(name: "url_preview_close_dark")
internal static let voiceMessageCancelGradient = ImageAsset(name: "voice_message_cancel_gradient")
internal static let voiceMessageLockChevron = ImageAsset(name: "voice_message_lock_chevron")
internal static let voiceMessageLockIconLocked = ImageAsset(name: "voice_message_lock_icon_locked")
Expand Down
8 changes: 8 additions & 0 deletions Riot/Generated/Strings.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4554,6 +4554,14 @@ internal enum VectorL10n {
internal static var settingsShowNSFWPublicRooms: String {
return VectorL10n.tr("Vector", "settings_show_NSFW_public_rooms")
}
/// Show inline URL previews
internal static var settingsShowUrlPreviews: String {
return VectorL10n.tr("Vector", "settings_show_url_previews")
}
/// Previews will only be shown in unencrypted rooms.
internal static var settingsShowUrlPreviewsDescription: String {
return VectorL10n.tr("Vector", "settings_show_url_previews_description")
}
/// Sign Out
internal static var settingsSignOut: String {
return VectorL10n.tr("Vector", "settings_sign_out")
Expand Down
4 changes: 4 additions & 0 deletions Riot/Managers/Settings/RiotSettings.swift
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,10 @@ final class RiotSettings: NSObject {
@UserDefault(key: "roomScreenAllowFilesAction", defaultValue: BuildSettings.roomScreenAllowFilesAction, storage: defaults)
var roomScreenAllowFilesAction

// labs prefix added to the key can be dropped when default value becomes true
@UserDefault(key: "labsRoomScreenShowsURLPreviews", defaultValue: false, storage: defaults)
var roomScreenShowsURLPreviews

// MARK: - Room Contextual Menu

@UserDefault(key: "roomContextualMenuShowMoreOptionForMessages", defaultValue: BuildSettings.roomContextualMenuShowMoreOptionForMessages, storage: defaults)
Expand Down
25 changes: 25 additions & 0 deletions Riot/Managers/URLPreviews/ClosedURLPreview.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
//
// Copyright 2021 New Vector Ltd
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//

import CoreData

extension ClosedURLPreview {
pixlwave marked this conversation as resolved.
Show resolved Hide resolved
convenience init(context: NSManagedObjectContext, eventID: String, roomID: String) {
self.init(context: context)
self.eventID = eventID
self.roomID = roomID
}
}
48 changes: 48 additions & 0 deletions Riot/Managers/URLPreviews/URLPreviewCacheData.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
//
// Copyright 2021 New Vector Ltd
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//

import CoreData

extension URLPreviewCacheData {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm wondering if we will have more Core Data entities which prefix or suffix we can use to distinguish the managed object from the memory object. A convention like CDURLPreviewData for Core Data and URLPreviewData for the memory representation?

convenience init(context: NSManagedObjectContext, preview: URLPreviewData, creationDate: Date) {
self.init(context: context)
update(from: preview, on: creationDate)
}

func update(from preview: URLPreviewData, on date: Date) {
url = preview.url
siteName = preview.siteName
title = preview.title
text = preview.text
image = preview.image

creationDate = date
}

func preview(for event: MXEvent) -> URLPreviewData? {
guard let url = url else { return nil }

let viewData = URLPreviewData(url: url,
eventID: event.eventId,
roomID: event.roomId,
siteName: siteName,
title: title,
text: text)
viewData.image = image as? UIImage

return viewData
}
}
52 changes: 52 additions & 0 deletions Riot/Managers/URLPreviews/URLPreviewData.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
//
// Copyright 2021 New Vector Ltd
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//

import Foundation

@objcMembers
class URLPreviewData: NSObject {
/// The URL that's represented by the preview data. This may have been sanitized.
/// Note: The original URL, can be found in the bubble components with `eventID` and `roomID`.
let url: URL

/// The ID of the event that created this preview.
let eventID: String

/// The ID of the room that this preview is from.
let roomID: String

/// The OpenGraph site name for the URL.
let siteName: String?

/// The OpenGraph title for the URL.
let title: String?

/// The OpenGraph description for the URL.
let text: String?

/// The OpenGraph image for the URL.
var image: UIImage?

init(url: URL, eventID: String, roomID: String, siteName: String?, title: String?, text: String?) {
self.url = url
self.eventID = eventID
self.roomID = roomID
self.siteName = siteName
self.title = title
// Remove line breaks from the description text
self.text = text?.replacingOccurrences(of: "\n", with: " ")
pixlwave marked this conversation as resolved.
Show resolved Hide resolved
}
}
45 changes: 45 additions & 0 deletions Riot/Managers/URLPreviews/URLPreviewImageTransformer.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
//
// Copyright 2021 New Vector Ltd
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//

import CoreData

/// A `ValueTransformer` for ``URLPreviewCacheData``'s `image` field.
/// This class transforms between `UIImage` and it's `pngData()` representation.
class URLPreviewImageTransformer: ValueTransformer {
override class func transformedValueClass() -> AnyClass {
UIImage.self
}

override class func allowsReverseTransformation() -> Bool {
true
}

/// Transforms a `UIImage` into it's `pngData()` representation.
override func transformedValue(_ value: Any?) -> Any? {
guard let image = value as? UIImage else { return nil }
return image.pngData()
}

/// Transforms `Data` into a `UIImage`
override func reverseTransformedValue(_ value: Any?) -> Any? {
guard let data = value as? Data else { return nil }
return UIImage(data: data)
}
}

extension NSValueTransformerName {
static let urlPreviewImageTransformer = NSValueTransformerName("URLPreviewImageTransformer")
}
116 changes: 116 additions & 0 deletions Riot/Managers/URLPreviews/URLPreviewManager.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
//
// Copyright 2021 New Vector Ltd
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//

import Foundation

@objcMembers
class URLPreviewManager: NSObject {
pixlwave marked this conversation as resolved.
Show resolved Hide resolved
static let shared = URLPreviewManager()

// Core Data store to reduce network requests
private let store = URLPreviewStore()

private override init() { }
pixlwave marked this conversation as resolved.
Show resolved Hide resolved

func preview(for url: URL,
and event: MXEvent,
with session: MXSession,
success: @escaping (URLPreviewData) -> Void,
failure: @escaping (Error?) -> Void) {
// Sanitize the URL before checking the store or performing lookup
let sanitizedURL = sanitize(url)

if let preview = store.preview(for: sanitizedURL, and: event) {
MXLog.debug("[URLPreviewManager] Using cached preview.")
success(preview)
return
}

session.matrixRestClient.preview(for: sanitizedURL, success: { previewResponse in
MXLog.debug("[URLPreviewManager] Cached preview not found. Requesting from homeserver.")

if let previewResponse = previewResponse {
pixlwave marked this conversation as resolved.
Show resolved Hide resolved
self.makePreviewData(from: previewResponse, for: sanitizedURL, and: event, with: session) { previewData in
self.store.store(previewData)
success(previewData)
}
}

}, failure: failure)
}

func makePreviewData(from previewResponse: MXURLPreview,
for url: URL,
and event: MXEvent,
with session: MXSession,
completion: @escaping (URLPreviewData) -> Void) {
let previewData = URLPreviewData(url: url,
eventID: event.eventId,
roomID: event.roomId,
siteName: previewResponse.siteName,
title: previewResponse.title,
text: previewResponse.text)

guard let imageURL = previewResponse.imageURL else {
completion(previewData)
return
}

if let cachePath = MXMediaManager.cachePath(forMatrixContentURI: imageURL, andType: previewResponse.imageType, inFolder: nil),
let image = MXMediaManager.loadThroughCache(withFilePath: cachePath) {
previewData.image = image
completion(previewData)
return
}

// Don't de-dupe image downloads as the manager should de-dupe preview generation.

session.mediaManager.downloadMedia(fromMatrixContentURI: imageURL, withType: previewResponse.imageType, inFolder: nil) { path in
guard let image = MXMediaManager.loadThroughCache(withFilePath: path) else {
completion(previewData)
return
}
previewData.image = image
completion(previewData)
} failure: { error in
completion(previewData)
}
}

func removeExpiredCacheData() {
store.removeExpiredItems()
}

func clearStore() {
store.deleteAll()
}

func closePreview(for eventID: String, in roomID: String) {
store.closePreview(for: eventID, in: roomID)
}

func hasClosedPreview(from event: MXEvent) -> Bool {
store.hasClosedPreview(for: event.eventId, in: event.roomId)
}

private func sanitize(_ url: URL) -> URL {
// Remove the fragment from the URL.
var components = URLComponents(url: url, resolvingAgainstBaseURL: false)
components?.fragment = nil

return components?.url ?? url
}
}
Loading