Load and store URLPreviewViewData in RoomBubbleCellData.

Implement close button and store the action in Core Data. Hide the preview image view when no image is received. Remove line breaks in description text.
This commit is contained in:
Doug
2021-09-01 10:37:37 +01:00
parent 660b95b20a
commit 4331994d72
28 changed files with 716 additions and 376 deletions
@@ -21,36 +21,49 @@ import Reusable
class URLPreviewView: UIView, NibLoadable, Themable {
// MARK: - Constants
private enum Constants { }
private static let sizingView = URLPreviewView.instantiate()
private enum Constants {
// URL Previews
static let maxHeight: CGFloat = 247.0
static let width: CGFloat = 267.0
}
// MARK: - Properties
var viewModel: URLPreviewViewModel! {
var preview: URLPreviewViewData? {
didSet {
viewModel.viewDelegate = self
guard let preview = preview else { return }
renderLoaded(preview)
}
}
weak var delegate: URLPreviewViewDelegate?
@IBOutlet weak var imageContainer: UIView!
@IBOutlet weak var imageView: UIImageView!
@IBOutlet weak var faviconImageView: UIImageView!
@IBOutlet weak var closeButton: UIButton!
@IBOutlet weak var siteNameLabel: UILabel!
@IBOutlet weak var titleLabel: UILabel!
@IBOutlet weak var descriptionLabel: UILabel!
/// The constraint that pins the top of the text container to the top of the view.
@IBOutlet weak var textContainerViewConstraint: NSLayoutConstraint!
/// The constraint that pins the top of the text container to the bottom of the image container.
@IBOutlet weak var textContainerImageConstraint: NSLayoutConstraint!
override var intrinsicContentSize: CGSize {
CGSize(width: RoomBubbleCellLayout.urlPreviewViewWidth, height: RoomBubbleCellLayout.urlPreviewViewHeight)
CGSize(width: Constants.width, height: Constants.maxHeight)
}
// MARK: - Setup
static func instantiate(viewModel: URLPreviewViewModel) -> Self {
static func instantiate() -> Self {
let view = Self.loadFromNib()
view.update(theme: ThemeService.shared().theme)
view.viewModel = viewModel
viewModel.process(viewAction: .loadData)
return view
}
@@ -63,14 +76,10 @@ class URLPreviewView: UIView, NibLoadable, Themable {
layer.masksToBounds = true
imageView.contentMode = .scaleAspectFill
faviconImageView.layer.cornerRadius = 6
siteNameLabel.isUserInteractionEnabled = false
titleLabel.isUserInteractionEnabled = false
descriptionLabel.isUserInteractionEnabled = false
#warning("Debugging for previews - to be removed")
faviconImageView.backgroundColor = .systemBlue.withAlphaComponent(0.7)
}
// MARK: - Public
@@ -86,9 +95,19 @@ class URLPreviewView: UIView, NibLoadable, Themable {
descriptionLabel.textColor = theme.colors.secondaryContent
descriptionLabel.font = theme.fonts.caption1
let closeButtonAsset = ThemeService.shared().isCurrentThemeDark() ? Asset.Images.urlPreviewCloseDark : Asset.Images.urlPreviewClose
closeButton.setImage(closeButtonAsset.image, for: .normal)
}
static func contentViewHeight(for preview: URLPreviewViewData) -> CGFloat {
sizingView.renderLoaded(preview)
return sizingView.systemLayoutSizeFitting(sizingView.intrinsicContentSize).height
}
// MARK: - Private
#warning("Check whether we should show a loading state.")
private func renderLoading(_ url: URL) {
imageView.image = nil
@@ -98,48 +117,49 @@ class URLPreviewView: UIView, NibLoadable, Themable {
}
private func renderLoaded(_ preview: URLPreviewViewData) {
imageView.image = preview.image
if let image = preview.image {
imageView.image = image
showImageContainer()
} else {
imageView.image = nil
hideImageContainer()
}
siteNameLabel.text = preview.siteName ?? preview.url.host
titleLabel.text = preview.title
descriptionLabel.text = preview.text
}
private func renderError(_ error: Error) {
imageView.image = nil
private func showImageContainer() {
// When the image container has a superview it is already visible
guard imageContainer.superview == nil else { return }
siteNameLabel.text = "Error"
titleLabel.text = descriptionLabel.text
descriptionLabel.text = error.localizedDescription
textContainerViewConstraint.isActive = false
addSubview(imageContainer)
textContainerImageConstraint.isActive = true
// Ensure the close button remains visible
bringSubviewToFront(closeButton)
}
private func hideImageContainer() {
textContainerImageConstraint.isActive = false
imageContainer.removeFromSuperview()
textContainerViewConstraint.isActive = true
}
// MARK: - Action
@IBAction private func openURL(_ sender: Any) {
MXLog.debug("[URLPreviewView] Link was tapped.")
viewModel.process(viewAction: .openURL)
guard let preview = preview else { return }
// Ask the delegate to open the URL for the event, as the bubble component
// has the original un-sanitized URL that needs to be opened.
delegate?.didOpenURLFromPreviewView(self, for: preview.eventID, in: preview.roomID)
}
@IBAction private func close(_ sender: Any) {
}
}
// MARK: URLPreviewViewModelViewDelegate
extension URLPreviewView: URLPreviewViewModelViewDelegate {
func urlPreviewViewModel(_ viewModel: URLPreviewViewModelType, didUpdateViewState viewState: URLPreviewViewState) {
DispatchQueue.main.async {
switch viewState {
case .loading(let url):
self.renderLoading(url)
case .loaded(let preview):
self.renderLoaded(preview)
case .error(let error):
self.renderError(error)
case .hidden:
self.frame.size.height = 0
}
}
guard let preview = preview else { return }
delegate?.didCloseURLPreviewView(self, for: preview.eventID, in: preview.roomID)
}
}
@@ -14,7 +14,7 @@
<rect key="frame" x="0.0" y="0.0" width="267" height="247"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="sV7-z8-2ZW">
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="sV7-z8-2ZW" userLabel="Image Container">
<rect key="frame" x="0.0" y="0.0" width="267" height="140"/>
<subviews>
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="9ew-qc-5BO">
@@ -23,7 +23,6 @@
</subviews>
<color key="backgroundColor" systemColor="systemBackgroundColor"/>
<constraints>
<constraint firstAttribute="width" constant="267" id="1cA-Oe-Ffr"/>
<constraint firstItem="9ew-qc-5BO" firstAttribute="top" secondItem="sV7-z8-2ZW" secondAttribute="top" id="PkK-wn-hNC"/>
<constraint firstItem="9ew-qc-5BO" firstAttribute="leading" secondItem="sV7-z8-2ZW" secondAttribute="leading" id="bQt-a9-prT"/>
<constraint firstAttribute="bottom" secondItem="9ew-qc-5BO" secondAttribute="bottom" id="dfh-UN-9f8"/>
@@ -34,35 +33,48 @@
<outletCollection property="gestureRecognizers" destination="rSB-1V-Kev" appends="YES" id="LLc-zz-Ooa"/>
</connections>
</view>
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="78m-NG-Oe7">
<rect key="frame" x="8" y="108" width="24" height="24"/>
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="Q0C-fE-Qie" userLabel="Text Container">
<rect key="frame" x="0.0" y="140" width="267" height="107"/>
<subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Site Name" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="2D0-pg-81F">
<rect key="frame" x="8" y="8" width="56" height="13.5"/>
<fontDescription key="fontDescription" type="system" weight="semibold" pointSize="11"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" horizontalCompressionResistancePriority="250" text="Title" textAlignment="natural" lineBreakMode="tailTruncation" numberOfLines="2" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="bfQ-4X-PGU">
<rect key="frame" x="8" y="23.5" width="33.5" height="19.5"/>
<fontDescription key="fontDescription" type="system" weight="semibold" pointSize="16"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" horizontalCompressionResistancePriority="250" text="Description" textAlignment="natural" lineBreakMode="tailTruncation" numberOfLines="3" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="cSk-qu-c4j">
<rect key="frame" x="8" y="45" width="65" height="14.5"/>
<fontDescription key="fontDescription" type="system" pointSize="12"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
</subviews>
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<constraints>
<constraint firstAttribute="width" constant="24" id="54e-3E-QhC"/>
<constraint firstAttribute="height" constant="24" id="Ms9-sP-ceF"/>
<constraint firstAttribute="trailing" relation="greaterThanOrEqual" secondItem="2D0-pg-81F" secondAttribute="trailing" constant="8" id="2zQ-XH-oQR"/>
<constraint firstAttribute="width" constant="267" id="LRy-qw-2b8"/>
<constraint firstAttribute="bottom" relation="greaterThanOrEqual" secondItem="cSk-qu-c4j" secondAttribute="bottom" constant="8" id="NR5-XL-Tai"/>
<constraint firstItem="cSk-qu-c4j" firstAttribute="leading" secondItem="bfQ-4X-PGU" secondAttribute="leading" id="PX7-CK-i5a"/>
<constraint firstAttribute="height" relation="lessThanOrEqual" constant="107" id="QSQ-YM-7r3"/>
<constraint firstItem="bfQ-4X-PGU" firstAttribute="leading" secondItem="2D0-pg-81F" secondAttribute="leading" id="UQW-5z-5gV"/>
<constraint firstItem="bfQ-4X-PGU" firstAttribute="top" secondItem="2D0-pg-81F" secondAttribute="bottom" constant="2" id="UpA-Eq-vOM"/>
<constraint firstAttribute="trailing" relation="greaterThanOrEqual" secondItem="cSk-qu-c4j" secondAttribute="trailing" constant="8" id="ay3-i8-Tm3"/>
<constraint firstAttribute="trailing" relation="greaterThanOrEqual" secondItem="bfQ-4X-PGU" secondAttribute="trailing" constant="8" id="orX-8Y-YeT"/>
<constraint firstItem="2D0-pg-81F" firstAttribute="leading" secondItem="Q0C-fE-Qie" secondAttribute="leading" constant="8" id="quM-cg-gTu"/>
<constraint firstItem="2D0-pg-81F" firstAttribute="top" secondItem="Q0C-fE-Qie" secondAttribute="top" constant="8" id="rOX-ft-oNx"/>
<constraint firstItem="cSk-qu-c4j" firstAttribute="top" secondItem="bfQ-4X-PGU" secondAttribute="bottom" constant="2" id="uUe-4x-y4M"/>
</constraints>
</imageView>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Site Name" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="2D0-pg-81F">
<rect key="frame" x="8" y="148" width="56" height="13.5"/>
<fontDescription key="fontDescription" type="system" weight="semibold" pointSize="11"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" horizontalCompressionResistancePriority="250" text="Title" textAlignment="natural" lineBreakMode="tailTruncation" numberOfLines="2" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="bfQ-4X-PGU">
<rect key="frame" x="8" y="163.5" width="33.5" height="19.5"/>
<fontDescription key="fontDescription" type="system" weight="semibold" pointSize="16"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" horizontalCompressionResistancePriority="250" text="Description" textAlignment="natural" lineBreakMode="tailTruncation" numberOfLines="3" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="cSk-qu-c4j">
<rect key="frame" x="8" y="185" width="251" height="14.5"/>
<fontDescription key="fontDescription" type="system" pointSize="12"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
<button opaque="NO" contentMode="scaleToFill" fixedFrame="YES" contentHorizontalAlignment="center" contentVerticalAlignment="center" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="CL2-R3-bYG">
<rect key="frame" x="242" y="9" width="10" height="22"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<state key="normal" image="close_banner">
</view>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="CL2-R3-bYG">
<rect key="frame" x="227" y="0.0" width="40" height="40"/>
<inset key="contentEdgeInsets" minX="8" minY="8" maxX="8" maxY="8"/>
<state key="normal" image="url_preview_close">
<preferredSymbolConfiguration key="preferredSymbolConfiguration" scale="large"/>
</state>
<connections>
@@ -73,28 +85,31 @@
<color key="backgroundColor" systemColor="tertiarySystemFillColor"/>
<gestureRecognizers/>
<constraints>
<constraint firstAttribute="trailing" secondItem="cSk-qu-c4j" secondAttribute="trailing" constant="8" id="1QZ-IV-b1E"/>
<constraint firstAttribute="top" secondItem="sV7-z8-2ZW" secondAttribute="top" id="8AQ-S7-Fm0"/>
<constraint firstItem="2D0-pg-81F" firstAttribute="leading" secondItem="iN0-l3-epB" secondAttribute="leading" constant="8" id="A6d-x8-mDG"/>
<constraint firstItem="cSk-qu-c4j" firstAttribute="leading" secondItem="bfQ-4X-PGU" secondAttribute="leading" id="IT4-7h-n5y"/>
<constraint firstItem="bfQ-4X-PGU" firstAttribute="leading" secondItem="2D0-pg-81F" secondAttribute="leading" id="Qxt-SR-d1M"/>
<constraint firstItem="bfQ-4X-PGU" firstAttribute="top" secondItem="2D0-pg-81F" secondAttribute="bottom" constant="2" id="ZBc-MM-pl2"/>
<constraint firstAttribute="trailing" relation="greaterThanOrEqual" secondItem="bfQ-4X-PGU" secondAttribute="trailing" constant="8" id="bk2-2s-hgS"/>
<constraint firstAttribute="bottom" secondItem="Q0C-fE-Qie" secondAttribute="bottom" id="AMK-Mj-Bth"/>
<constraint firstAttribute="trailing" secondItem="Q0C-fE-Qie" secondAttribute="trailing" id="FwW-Yj-RX8"/>
<constraint firstItem="Q0C-fE-Qie" firstAttribute="top" secondItem="iN0-l3-epB" secondAttribute="top" id="RJG-FK-55c"/>
<constraint firstItem="Q0C-fE-Qie" firstAttribute="leading" secondItem="iN0-l3-epB" secondAttribute="leading" id="U7n-8S-2Oc"/>
<constraint firstItem="sV7-z8-2ZW" firstAttribute="leading" secondItem="iN0-l3-epB" secondAttribute="leading" id="cfa-wh-EzA"/>
<constraint firstItem="78m-NG-Oe7" firstAttribute="leading" secondItem="sV7-z8-2ZW" secondAttribute="leading" constant="8" id="ecO-IN-wTm"/>
<constraint firstItem="CL2-R3-bYG" firstAttribute="top" secondItem="iN0-l3-epB" secondAttribute="top" id="er2-K6-UxP"/>
<constraint firstAttribute="trailing" secondItem="sV7-z8-2ZW" secondAttribute="trailing" id="gUP-5s-qWI"/>
<constraint firstItem="2D0-pg-81F" firstAttribute="top" secondItem="sV7-z8-2ZW" secondAttribute="bottom" constant="8" id="neh-8P-aFx"/>
<constraint firstAttribute="trailing" relation="greaterThanOrEqual" secondItem="2D0-pg-81F" secondAttribute="trailing" constant="8" id="tnX-Fo-b79"/>
<constraint firstItem="cSk-qu-c4j" firstAttribute="top" secondItem="bfQ-4X-PGU" secondAttribute="bottom" constant="2" id="xp1-2j-xl8"/>
<constraint firstAttribute="bottom" relation="greaterThanOrEqual" secondItem="cSk-qu-c4j" secondAttribute="bottom" constant="8" id="yHd-Fp-jPo"/>
<constraint firstItem="sV7-z8-2ZW" firstAttribute="bottom" secondItem="78m-NG-Oe7" secondAttribute="bottom" constant="8" id="zsc-zO-UkD"/>
<constraint firstItem="Q0C-fE-Qie" firstAttribute="top" secondItem="sV7-z8-2ZW" secondAttribute="bottom" id="jLX-fo-dQS"/>
<constraint firstAttribute="trailing" secondItem="CL2-R3-bYG" secondAttribute="trailing" id="wNl-Nu-qrw"/>
</constraints>
<freeformSimulatedSizeMetrics key="simulatedDestinationMetrics"/>
<variation key="default">
<mask key="constraints">
<exclude reference="RJG-FK-55c"/>
</mask>
</variation>
<connections>
<outlet property="closeButton" destination="CL2-R3-bYG" id="Ajz-iV-YeT"/>
<outlet property="descriptionLabel" destination="cSk-qu-c4j" id="gC7-Pu-nHx"/>
<outlet property="faviconImageView" destination="78m-NG-Oe7" id="u26-zc-JwX"/>
<outlet property="imageContainer" destination="sV7-z8-2ZW" id="swm-BL-00E"/>
<outlet property="imageView" destination="9ew-qc-5BO" id="nzz-xV-mae"/>
<outlet property="siteNameLabel" destination="2D0-pg-81F" id="72o-l1-f7x"/>
<outlet property="textContainerImageConstraint" destination="jLX-fo-dQS" id="fRa-ak-dUy"/>
<outlet property="textContainerViewConstraint" destination="RJG-FK-55c" id="tsp-0o-sXt"/>
<outlet property="titleLabel" destination="bfQ-4X-PGU" id="Jzt-75-caa"/>
<outletCollection property="gestureRecognizers" destination="rSB-1V-Kev" appends="YES" id="qPD-mR-YpO"/>
</connections>
@@ -107,7 +122,7 @@
</tapGestureRecognizer>
</objects>
<resources>
<image name="close_banner" width="10" height="10"/>
<image name="url_preview_close" width="24" height="24"/>
<systemColor name="systemBackgroundColor">
<color white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
</systemColor>
@@ -16,12 +16,18 @@
import Foundation
@objc
@objcMembers
class URLPreviewViewData: NSObject {
/// The URL that's represented by the preview data. This may have been sanitized.
/// Note: The original URL, is stored in ``URLPreviewViewModel``.
/// 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?
@@ -34,10 +40,13 @@ class URLPreviewViewData: NSObject {
/// The OpenGraph image for the URL.
var image: UIImage?
init(url: URL, siteName: String?, title: String?, text: String?) {
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
self.text = text
// Remove line breaks from the description text
self.text = text?.replacingOccurrences(of: "\n", with: " ")
}
}
@@ -16,9 +16,8 @@
import Foundation
/// URLPreviewView actions exposed to view model
enum URLPreviewViewAction {
case loadData
case openURL
case close
@objc
protocol URLPreviewViewDelegate: AnyObject {
func didOpenURLFromPreviewView(_ previewView: URLPreviewView, for eventID: String, in roomID: String)
func didCloseURLPreviewView(_ previewView: URLPreviewView, for eventID: String, in roomID: String)
}
@@ -1,91 +0,0 @@
//
// 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
import MatrixSDK
@objcMembers
class URLPreviewViewModel: NSObject, URLPreviewViewModelType {
// MARK: - Properties
// MARK: Private
/// The original (un-sanitized) URL to be previewed.
private let url: URL
private let session: MXSession
private var currentOperation: MXHTTPOperation?
private var urlPreview: MXURLPreview?
// MARK: Public
weak var viewDelegate: URLPreviewViewModelViewDelegate?
// MARK: - Setup
init(url: URL, session: MXSession) {
self.url = url
self.session = session
}
deinit {
cancelOperations()
}
// MARK: - Public
func process(viewAction: URLPreviewViewAction) {
switch viewAction {
case .loadData:
loadData()
case .openURL:
openURL()
case .close:
cancelOperations()
}
}
// MARK: - Private
private func loadData() {
update(viewState: .loading(url))
AppDelegate.theDelegate().previewManager.preview(for: url) { [weak self] preview in
guard let self = self else { return }
self.update(viewState: .loaded(preview))
} failure: { error in
#warning("REALLY?!")
if let error = error {
self.update(viewState: .error(error))
}
}
}
private func openURL() {
// Open the original (un-sanitized) URL stored in the view model.
UIApplication.shared.open(url)
}
private func update(viewState: URLPreviewViewState) {
viewDelegate?.urlPreviewViewModel(self, didUpdateViewState: viewState)
}
private func cancelOperations() {
currentOperation?.cancel()
}
}
@@ -1,29 +0,0 @@
//
// 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
protocol URLPreviewViewModelViewDelegate: AnyObject {
func urlPreviewViewModel(_ viewModel: URLPreviewViewModelType, didUpdateViewState viewState: URLPreviewViewState)
}
/// Protocol describing the view model used by `URLPreviewView`
protocol URLPreviewViewModelType {
var viewDelegate: URLPreviewViewModelViewDelegate? { get set }
func process(viewAction: URLPreviewViewAction)
}
@@ -1,25 +0,0 @@
//
// 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
/// URLPreviewView state
enum URLPreviewViewState {
case loading(_ url: URL)
case loaded(_ preview: URLPreviewViewData)
case error(Error)
case hidden
}