Profile pages
This commit is contained in:
parent
4b3d186a2e
commit
05c895db88
|
@ -10,11 +10,17 @@
|
||||||
04DACE8A212CA6B7009840C4 /* Timeline.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04DACE89212CA6B7009840C4 /* Timeline.swift */; };
|
04DACE8A212CA6B7009840C4 /* Timeline.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04DACE89212CA6B7009840C4 /* Timeline.swift */; };
|
||||||
04DACE8C212CB14B009840C4 /* MainTabBarViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04DACE8B212CB14B009840C4 /* MainTabBarViewController.swift */; };
|
04DACE8C212CB14B009840C4 /* MainTabBarViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04DACE8B212CB14B009840C4 /* MainTabBarViewController.swift */; };
|
||||||
04DACE8E212CC7CC009840C4 /* AvatarCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04DACE8D212CC7CC009840C4 /* AvatarCache.swift */; };
|
04DACE8E212CC7CC009840C4 /* AvatarCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04DACE8D212CC7CC009840C4 /* AvatarCache.swift */; };
|
||||||
D64A0CD32132153900640E3B /* StatusContentLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D64A0CD22132153900640E3B /* StatusContentLabel.swift */; };
|
D64A0CD32132153900640E3B /* HTMLContentLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D64A0CD22132153900640E3B /* HTMLContentLabel.swift */; };
|
||||||
D64D0AAD2128D88B005A6F37 /* LocalData.swift in Sources */ = {isa = PBXBuildFile; fileRef = D64D0AAC2128D88B005A6F37 /* LocalData.swift */; };
|
D64D0AAD2128D88B005A6F37 /* LocalData.swift in Sources */ = {isa = PBXBuildFile; fileRef = D64D0AAC2128D88B005A6F37 /* LocalData.swift */; };
|
||||||
D64D0AAF2128D954005A6F37 /* Onboarding.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = D64D0AAE2128D954005A6F37 /* Onboarding.storyboard */; };
|
D64D0AAF2128D954005A6F37 /* Onboarding.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = D64D0AAE2128D954005A6F37 /* Onboarding.storyboard */; };
|
||||||
D64D0AB12128D9AE005A6F37 /* OnboardingViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D64D0AB02128D9AE005A6F37 /* OnboardingViewController.swift */; };
|
D64D0AB12128D9AE005A6F37 /* OnboardingViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D64D0AB02128D9AE005A6F37 /* OnboardingViewController.swift */; };
|
||||||
D667E5E12134937B0057A976 /* StatusTableViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = D667E5E02134937B0057A976 /* StatusTableViewCell.xib */; };
|
D667E5E12134937B0057A976 /* StatusTableViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = D667E5E02134937B0057A976 /* StatusTableViewCell.xib */; };
|
||||||
|
D667E5E3213499F70057A976 /* Profile.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = D667E5E2213499F70057A976 /* Profile.storyboard */; };
|
||||||
|
D667E5E721349D4C0057A976 /* ProfileTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D667E5E621349D4C0057A976 /* ProfileTableViewController.swift */; };
|
||||||
|
D667E5E921349EE50057A976 /* ProfileHeaderTableViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = D667E5E821349EE50057A976 /* ProfileHeaderTableViewCell.xib */; };
|
||||||
|
D667E5EB21349EF80057A976 /* ProfileHeaderTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D667E5EA21349EF80057A976 /* ProfileHeaderTableViewCell.swift */; };
|
||||||
|
D667E5EF2134C39F0057A976 /* StatusContentLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D667E5EE2134C39F0057A976 /* StatusContentLabel.swift */; };
|
||||||
|
D667E5F12134D5050057A976 /* UIViewController+StatusTableViewCellDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = D667E5F02134D5050057A976 /* UIViewController+StatusTableViewCellDelegate.swift */; };
|
||||||
D6BED16F212663DA00F02DA0 /* SwiftSoup.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D6BED16E212663DA00F02DA0 /* SwiftSoup.framework */; };
|
D6BED16F212663DA00F02DA0 /* SwiftSoup.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D6BED16E212663DA00F02DA0 /* SwiftSoup.framework */; };
|
||||||
D6BED170212663DA00F02DA0 /* SwiftSoup.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = D6BED16E212663DA00F02DA0 /* SwiftSoup.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
|
D6BED170212663DA00F02DA0 /* SwiftSoup.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = D6BED16E212663DA00F02DA0 /* SwiftSoup.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
|
||||||
D6BED174212667E900F02DA0 /* StatusTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6BED173212667E900F02DA0 /* StatusTableViewCell.swift */; };
|
D6BED174212667E900F02DA0 /* StatusTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6BED173212667E900F02DA0 /* StatusTableViewCell.swift */; };
|
||||||
|
@ -67,11 +73,17 @@
|
||||||
04DACE89212CA6B7009840C4 /* Timeline.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Timeline.swift; sourceTree = "<group>"; };
|
04DACE89212CA6B7009840C4 /* Timeline.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Timeline.swift; sourceTree = "<group>"; };
|
||||||
04DACE8B212CB14B009840C4 /* MainTabBarViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainTabBarViewController.swift; sourceTree = "<group>"; };
|
04DACE8B212CB14B009840C4 /* MainTabBarViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainTabBarViewController.swift; sourceTree = "<group>"; };
|
||||||
04DACE8D212CC7CC009840C4 /* AvatarCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AvatarCache.swift; sourceTree = "<group>"; };
|
04DACE8D212CC7CC009840C4 /* AvatarCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AvatarCache.swift; sourceTree = "<group>"; };
|
||||||
D64A0CD22132153900640E3B /* StatusContentLabel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusContentLabel.swift; sourceTree = "<group>"; };
|
D64A0CD22132153900640E3B /* HTMLContentLabel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HTMLContentLabel.swift; sourceTree = "<group>"; };
|
||||||
D64D0AAC2128D88B005A6F37 /* LocalData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalData.swift; sourceTree = "<group>"; };
|
D64D0AAC2128D88B005A6F37 /* LocalData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalData.swift; sourceTree = "<group>"; };
|
||||||
D64D0AAE2128D954005A6F37 /* Onboarding.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = Onboarding.storyboard; sourceTree = "<group>"; };
|
D64D0AAE2128D954005A6F37 /* Onboarding.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = Onboarding.storyboard; sourceTree = "<group>"; };
|
||||||
D64D0AB02128D9AE005A6F37 /* OnboardingViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingViewController.swift; sourceTree = "<group>"; };
|
D64D0AB02128D9AE005A6F37 /* OnboardingViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingViewController.swift; sourceTree = "<group>"; };
|
||||||
D667E5E02134937B0057A976 /* StatusTableViewCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = StatusTableViewCell.xib; sourceTree = "<group>"; };
|
D667E5E02134937B0057A976 /* StatusTableViewCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = StatusTableViewCell.xib; sourceTree = "<group>"; };
|
||||||
|
D667E5E2213499F70057A976 /* Profile.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = Profile.storyboard; sourceTree = "<group>"; };
|
||||||
|
D667E5E621349D4C0057A976 /* ProfileTableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileTableViewController.swift; sourceTree = "<group>"; };
|
||||||
|
D667E5E821349EE50057A976 /* ProfileHeaderTableViewCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = ProfileHeaderTableViewCell.xib; sourceTree = "<group>"; };
|
||||||
|
D667E5EA21349EF80057A976 /* ProfileHeaderTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileHeaderTableViewCell.swift; sourceTree = "<group>"; };
|
||||||
|
D667E5EE2134C39F0057A976 /* StatusContentLabel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusContentLabel.swift; sourceTree = "<group>"; };
|
||||||
|
D667E5F02134D5050057A976 /* UIViewController+StatusTableViewCellDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIViewController+StatusTableViewCellDelegate.swift"; sourceTree = "<group>"; };
|
||||||
D6BED16E212663DA00F02DA0 /* SwiftSoup.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = SwiftSoup.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
D6BED16E212663DA00F02DA0 /* SwiftSoup.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = SwiftSoup.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
D6BED173212667E900F02DA0 /* StatusTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusTableViewCell.swift; sourceTree = "<group>"; };
|
D6BED173212667E900F02DA0 /* StatusTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusTableViewCell.swift; sourceTree = "<group>"; };
|
||||||
D6D4DDCC212518A000E1C4BB /* Tusker.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Tusker.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
D6D4DDCC212518A000E1C4BB /* Tusker.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Tusker.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
|
@ -122,9 +134,13 @@
|
||||||
D6BED1722126661300F02DA0 /* Views */ = {
|
D6BED1722126661300F02DA0 /* Views */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
D6BED173212667E900F02DA0 /* StatusTableViewCell.swift */,
|
D64A0CD22132153900640E3B /* HTMLContentLabel.swift */,
|
||||||
D64A0CD22132153900640E3B /* StatusContentLabel.swift */,
|
D667E5EE2134C39F0057A976 /* StatusContentLabel.swift */,
|
||||||
D667E5E02134937B0057A976 /* StatusTableViewCell.xib */,
|
D667E5E02134937B0057A976 /* StatusTableViewCell.xib */,
|
||||||
|
D6BED173212667E900F02DA0 /* StatusTableViewCell.swift */,
|
||||||
|
D667E5F02134D5050057A976 /* UIViewController+StatusTableViewCellDelegate.swift */,
|
||||||
|
D667E5E821349EE50057A976 /* ProfileHeaderTableViewCell.xib */,
|
||||||
|
D667E5EA21349EF80057A976 /* ProfileHeaderTableViewCell.swift */,
|
||||||
);
|
);
|
||||||
path = Views;
|
path = Views;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
|
@ -193,6 +209,7 @@
|
||||||
D6F953EB212519E700CF0F2B /* TimelineTableViewController.swift */,
|
D6F953EB212519E700CF0F2B /* TimelineTableViewController.swift */,
|
||||||
D64D0AB02128D9AE005A6F37 /* OnboardingViewController.swift */,
|
D64D0AB02128D9AE005A6F37 /* OnboardingViewController.swift */,
|
||||||
04DACE8B212CB14B009840C4 /* MainTabBarViewController.swift */,
|
04DACE8B212CB14B009840C4 /* MainTabBarViewController.swift */,
|
||||||
|
D667E5E621349D4C0057A976 /* ProfileTableViewController.swift */,
|
||||||
);
|
);
|
||||||
path = "View Controllers";
|
path = "View Controllers";
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
|
@ -203,6 +220,7 @@
|
||||||
D6D4DDD3212518A000E1C4BB /* Main.storyboard */,
|
D6D4DDD3212518A000E1C4BB /* Main.storyboard */,
|
||||||
D6F953ED21251A0700CF0F2B /* Timeline.storyboard */,
|
D6F953ED21251A0700CF0F2B /* Timeline.storyboard */,
|
||||||
D64D0AAE2128D954005A6F37 /* Onboarding.storyboard */,
|
D64D0AAE2128D954005A6F37 /* Onboarding.storyboard */,
|
||||||
|
D667E5E2213499F70057A976 /* Profile.storyboard */,
|
||||||
);
|
);
|
||||||
path = Storyboards;
|
path = Storyboards;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
|
@ -320,11 +338,13 @@
|
||||||
isa = PBXResourcesBuildPhase;
|
isa = PBXResourcesBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
|
D667E5E921349EE50057A976 /* ProfileHeaderTableViewCell.xib in Resources */,
|
||||||
D6D4DDDA212518A200E1C4BB /* LaunchScreen.storyboard in Resources */,
|
D6D4DDDA212518A200E1C4BB /* LaunchScreen.storyboard in Resources */,
|
||||||
D64D0AAF2128D954005A6F37 /* Onboarding.storyboard in Resources */,
|
D64D0AAF2128D954005A6F37 /* Onboarding.storyboard in Resources */,
|
||||||
D6D4DDD7212518A200E1C4BB /* Assets.xcassets in Resources */,
|
D6D4DDD7212518A200E1C4BB /* Assets.xcassets in Resources */,
|
||||||
D6F953EE21251A0700CF0F2B /* Timeline.storyboard in Resources */,
|
D6F953EE21251A0700CF0F2B /* Timeline.storyboard in Resources */,
|
||||||
D6D4DDD5212518A000E1C4BB /* Main.storyboard in Resources */,
|
D6D4DDD5212518A000E1C4BB /* Main.storyboard in Resources */,
|
||||||
|
D667E5E3213499F70057A976 /* Profile.storyboard in Resources */,
|
||||||
D667E5E12134937B0057A976 /* StatusTableViewCell.xib in Resources */,
|
D667E5E12134937B0057A976 /* StatusTableViewCell.xib in Resources */,
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
@ -356,9 +376,13 @@
|
||||||
04DACE8E212CC7CC009840C4 /* AvatarCache.swift in Sources */,
|
04DACE8E212CC7CC009840C4 /* AvatarCache.swift in Sources */,
|
||||||
D6BED174212667E900F02DA0 /* StatusTableViewCell.swift in Sources */,
|
D6BED174212667E900F02DA0 /* StatusTableViewCell.swift in Sources */,
|
||||||
D64D0AAD2128D88B005A6F37 /* LocalData.swift in Sources */,
|
D64D0AAD2128D88B005A6F37 /* LocalData.swift in Sources */,
|
||||||
|
D667E5EF2134C39F0057A976 /* StatusContentLabel.swift in Sources */,
|
||||||
D64D0AB12128D9AE005A6F37 /* OnboardingViewController.swift in Sources */,
|
D64D0AB12128D9AE005A6F37 /* OnboardingViewController.swift in Sources */,
|
||||||
D6F953EC212519E700CF0F2B /* TimelineTableViewController.swift in Sources */,
|
D6F953EC212519E700CF0F2B /* TimelineTableViewController.swift in Sources */,
|
||||||
D64A0CD32132153900640E3B /* StatusContentLabel.swift in Sources */,
|
D667E5EB21349EF80057A976 /* ProfileHeaderTableViewCell.swift in Sources */,
|
||||||
|
D64A0CD32132153900640E3B /* HTMLContentLabel.swift in Sources */,
|
||||||
|
D667E5F12134D5050057A976 /* UIViewController+StatusTableViewCellDelegate.swift in Sources */,
|
||||||
|
D667E5E721349D4C0057A976 /* ProfileTableViewController.swift in Sources */,
|
||||||
D6D4DDD0212518A000E1C4BB /* AppDelegate.swift in Sources */,
|
D6D4DDD0212518A000E1C4BB /* AppDelegate.swift in Sources */,
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
|
|
@ -35,18 +35,18 @@ class AvatarCache {
|
||||||
guard error == nil,
|
guard error == nil,
|
||||||
let data = data,
|
let data = data,
|
||||||
let image = UIImage(data: data) else {
|
let image = UIImage(data: data) else {
|
||||||
let callbacks = self.requestCallbacks.removeValue(forKey: url)!
|
let callbacks = self.requestCallbacks.removeValue(forKey: url)
|
||||||
for callback in callbacks {
|
callbacks?.forEach({ callback in
|
||||||
// todo: default avatar for failed requests
|
// todo: default avatar for failed requests
|
||||||
callback(nil)
|
callback(nil)
|
||||||
}
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
let callbacks = self.requestCallbacks.removeValue(forKey: url)!
|
let callbacks = self.requestCallbacks.removeValue(forKey: url)
|
||||||
for callback in callbacks {
|
callbacks?.forEach({ callback in
|
||||||
callback(image)
|
callback(image)
|
||||||
}
|
})
|
||||||
self.cache.setObject(image, forKey: key)
|
self.cache.setObject(image, forKey: key)
|
||||||
}
|
}
|
||||||
task.resume()
|
task.resume()
|
||||||
|
|
|
@ -0,0 +1,37 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="14313.13.2" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="HMn-Wn-5Ab">
|
||||||
|
<device id="retina4_7" orientation="portrait">
|
||||||
|
<adaptation id="fullscreen"/>
|
||||||
|
</device>
|
||||||
|
<dependencies>
|
||||||
|
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="14283.9"/>
|
||||||
|
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||||
|
</dependencies>
|
||||||
|
<scenes>
|
||||||
|
<!--Profile Table View Controller-->
|
||||||
|
<scene sceneID="b2p-rd-ie4">
|
||||||
|
<objects>
|
||||||
|
<tableViewController id="HMn-Wn-5Ab" customClass="ProfileTableViewController" customModule="Tusker" customModuleProvider="target" sceneMemberID="viewController">
|
||||||
|
<tableView key="view" clipsSubviews="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" dataMode="prototypes" style="plain" separatorStyle="default" rowHeight="-1" estimatedRowHeight="-1" sectionHeaderHeight="28" sectionFooterHeight="28" id="O5e-jX-tC3">
|
||||||
|
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
|
||||||
|
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||||
|
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||||
|
<connections>
|
||||||
|
<outlet property="dataSource" destination="HMn-Wn-5Ab" id="nAT-a9-DHC"/>
|
||||||
|
<outlet property="delegate" destination="HMn-Wn-5Ab" id="PbX-SY-1Qw"/>
|
||||||
|
</connections>
|
||||||
|
</tableView>
|
||||||
|
<navigationItem key="navigationItem" id="bvr-ex-deZ"/>
|
||||||
|
<refreshControl key="refreshControl" opaque="NO" multipleTouchEnabled="YES" contentMode="center" insetsLayoutMarginsFromSafeArea="NO" enabled="NO" contentHorizontalAlignment="center" contentVerticalAlignment="center" id="aAC-id-7YO">
|
||||||
|
<autoresizingMask key="autoresizingMask"/>
|
||||||
|
<connections>
|
||||||
|
<action selector="refreshStatuses:" destination="HMn-Wn-5Ab" eventType="valueChanged" id="uPt-pK-1iL"/>
|
||||||
|
</connections>
|
||||||
|
</refreshControl>
|
||||||
|
</tableViewController>
|
||||||
|
<placeholder placeholderIdentifier="IBFirstResponder" id="C4T-mP-V8U" userLabel="First Responder" sceneMemberID="firstResponder"/>
|
||||||
|
</objects>
|
||||||
|
<point key="canvasLocation" x="-482" y="90"/>
|
||||||
|
</scene>
|
||||||
|
</scenes>
|
||||||
|
</document>
|
|
@ -0,0 +1,132 @@
|
||||||
|
//
|
||||||
|
// ProfileTableViewController.swift
|
||||||
|
// Tusker
|
||||||
|
//
|
||||||
|
// Created by Shadowfacts on 8/27/18.
|
||||||
|
// Copyright © 2018 Shadowfacts. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
import MastodonKit
|
||||||
|
import SafariServices
|
||||||
|
|
||||||
|
class ProfileTableViewController: UITableViewController {
|
||||||
|
|
||||||
|
static func create(for account: Account) -> UIViewController {
|
||||||
|
guard let profileController = UIStoryboard(name: "Profile", bundle: nil).instantiateInitialViewController() as? ProfileTableViewController else { fatalError() }
|
||||||
|
profileController.account = account
|
||||||
|
return profileController
|
||||||
|
}
|
||||||
|
|
||||||
|
var account: Account!
|
||||||
|
|
||||||
|
var statuses: [Status] = [] {
|
||||||
|
didSet {
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
self.tableView.reloadData()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var newer: RequestRange?
|
||||||
|
var older: RequestRange?
|
||||||
|
|
||||||
|
func request(for range: RequestRange? = .default) -> Request<[Status]> {
|
||||||
|
let range = range ?? .default
|
||||||
|
return Accounts.statuses(id: account.id, mediaOnly: false, pinnedOnly: false, excludeReplies: true, range: range)
|
||||||
|
}
|
||||||
|
|
||||||
|
override func viewDidLoad() {
|
||||||
|
super.viewDidLoad()
|
||||||
|
|
||||||
|
tableView.rowHeight = UITableView.automaticDimension
|
||||||
|
tableView.estimatedRowHeight = 140
|
||||||
|
|
||||||
|
tableView.register(UINib(nibName: "StatusTableViewCell", bundle: nil), forCellReuseIdentifier: "statusCell")
|
||||||
|
tableView.register(UINib(nibName: "ProfileHeaderTableViewCell", bundle: nil), forCellReuseIdentifier: "headerCell")
|
||||||
|
|
||||||
|
navigationItem.title = account.displayName
|
||||||
|
|
||||||
|
MastodonController.shared.client.run(request()) { result in
|
||||||
|
guard case let .success(statuses, pagination) = result else { fatalError() }
|
||||||
|
self.statuses = statuses
|
||||||
|
self.newer = pagination?.previous
|
||||||
|
self.older = pagination?.next
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
// MARK: - Navigation
|
||||||
|
|
||||||
|
// In a storyboard-based application, you will often want to do a little preparation before navigation
|
||||||
|
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
|
||||||
|
// Get the new view controller using segue.destination.
|
||||||
|
// Pass the selected object to the new view controller.
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
// MARK: - Table view data source
|
||||||
|
|
||||||
|
override func numberOfSections(in tableView: UITableView) -> Int {
|
||||||
|
return 2
|
||||||
|
}
|
||||||
|
|
||||||
|
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
|
||||||
|
switch section {
|
||||||
|
case 0:
|
||||||
|
return 1
|
||||||
|
case 1:
|
||||||
|
return statuses.count
|
||||||
|
default:
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
|
||||||
|
switch indexPath.section {
|
||||||
|
case 0:
|
||||||
|
let cell = tableView.dequeueReusableCell(withIdentifier: "headerCell", for: indexPath) as! ProfileHeaderTableViewCell
|
||||||
|
cell.selectionStyle = .none
|
||||||
|
cell.updateUI(for: account)
|
||||||
|
cell.delegate = self
|
||||||
|
return cell
|
||||||
|
case 1:
|
||||||
|
let cell = tableView.dequeueReusableCell(withIdentifier: "statusCell", for: indexPath) as! StatusTableViewCell
|
||||||
|
let status = statuses[indexPath.row]
|
||||||
|
cell.updateUI(for: status)
|
||||||
|
cell.delegate = self
|
||||||
|
return cell
|
||||||
|
default:
|
||||||
|
fatalError("Invalid section \(indexPath.section) for profile VC")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
|
||||||
|
if indexPath.section == 1 && indexPath.row == statuses.count - 1 {
|
||||||
|
guard let older = older else { return }
|
||||||
|
|
||||||
|
MastodonController.shared.client.run(request(for: older)) { result in
|
||||||
|
guard case let .success(newStatuses, pagination) = result else { fatalError() }
|
||||||
|
self.older = pagination?.next
|
||||||
|
self.statuses.append(contentsOf: newStatuses)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@IBAction func refreshStatuses(_ sender: Any) {
|
||||||
|
guard let newer = newer else { return }
|
||||||
|
|
||||||
|
MastodonController.shared.client.run(request(for: newer)) { result in
|
||||||
|
guard case let .success(newStatuses, pagination) = result else { fatalError() }
|
||||||
|
self.newer = pagination?.previous
|
||||||
|
self.statuses.insert(contentsOf: newStatuses, at: 0)
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
self.refreshControl?.endRefreshing()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension ProfileTableViewController: ProfileHeaderTableViewCellDelegate {
|
||||||
|
|
||||||
|
}
|
|
@ -8,7 +8,6 @@
|
||||||
|
|
||||||
import UIKit
|
import UIKit
|
||||||
import MastodonKit
|
import MastodonKit
|
||||||
import SwiftSoup
|
|
||||||
import SafariServices
|
import SafariServices
|
||||||
|
|
||||||
class TimelineTableViewController: UITableViewController {
|
class TimelineTableViewController: UITableViewController {
|
||||||
|
@ -52,18 +51,13 @@ class TimelineTableViewController: UITableViewController {
|
||||||
tableView.estimatedRowHeight = 140
|
tableView.estimatedRowHeight = 140
|
||||||
|
|
||||||
tableView.register(UINib(nibName: "StatusTableViewCell", bundle: nil), forCellReuseIdentifier: "statusCell")
|
tableView.register(UINib(nibName: "StatusTableViewCell", bundle: nil), forCellReuseIdentifier: "statusCell")
|
||||||
}
|
|
||||||
|
|
||||||
override func viewWillAppear(_ animated: Bool) {
|
|
||||||
guard MastodonController.shared.client?.accessToken != nil else { return }
|
guard MastodonController.shared.client?.accessToken != nil else { return }
|
||||||
MastodonController.shared.client.run(timeline.request()) { result in
|
MastodonController.shared.client.run(timeline.request()) { result in
|
||||||
guard case let .success(statuses, pagination) = result else { fatalError() }
|
guard case let .success(statuses, pagination) = result else { fatalError() }
|
||||||
self.statuses = statuses
|
self.statuses = statuses
|
||||||
self.newer = pagination?.previous
|
self.newer = pagination?.previous
|
||||||
self.older = pagination?.next
|
self.older = pagination?.next
|
||||||
DispatchQueue.main.async {
|
|
||||||
self.tableView.reloadData()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -89,9 +83,7 @@ class TimelineTableViewController: UITableViewController {
|
||||||
|
|
||||||
|
|
||||||
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
|
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
|
||||||
guard let cell = tableView.dequeueReusableCell(withIdentifier: "statusCell", for: indexPath) as? StatusTableViewCell else {
|
let cell = tableView.dequeueReusableCell(withIdentifier: "statusCell", for: indexPath) as! StatusTableViewCell
|
||||||
fatalError()
|
|
||||||
}
|
|
||||||
|
|
||||||
let status = statuses[indexPath.row]
|
let status = statuses[indexPath.row]
|
||||||
|
|
||||||
|
@ -131,18 +123,3 @@ class TimelineTableViewController: UITableViewController {
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
extension TimelineTableViewController: StatusTableViewCellDelegate {
|
|
||||||
func selected(mention: Mention) {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
func selected(tag: MastodonKit.Tag) {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
func selected(url: URL) {
|
|
||||||
let vc = SFSafariViewController(url: url)
|
|
||||||
present(vc, animated: true)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -0,0 +1,302 @@
|
||||||
|
//
|
||||||
|
// StatusContentLabel.swift
|
||||||
|
// Tusker
|
||||||
|
//
|
||||||
|
// Created by Shadowfacts on 8/25/18.
|
||||||
|
// Copyright © 2018 Shadowfacts. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
import MastodonKit
|
||||||
|
import SwiftSoup
|
||||||
|
|
||||||
|
protocol HTMLContentLabelDelegate {
|
||||||
|
|
||||||
|
func selected(mention: Mention)
|
||||||
|
|
||||||
|
func selected(tag: MastodonKit.Tag)
|
||||||
|
|
||||||
|
func selected(url: URL)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
class HTMLContentLabel: UILabel {
|
||||||
|
|
||||||
|
var delegate: HTMLContentLabelDelegate?
|
||||||
|
|
||||||
|
override var text: String? {
|
||||||
|
didSet {
|
||||||
|
parseHTML()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override var attributedText: NSAttributedString? {
|
||||||
|
didSet {
|
||||||
|
updateTextStorage()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override var numberOfLines: Int {
|
||||||
|
didSet {
|
||||||
|
textContainer.maximumNumberOfLines = numberOfLines
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override var lineBreakMode: NSLineBreakMode {
|
||||||
|
didSet {
|
||||||
|
textContainer.lineBreakMode = lineBreakMode
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// var status: Status! {
|
||||||
|
// didSet {
|
||||||
|
// text = status.content
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
private var _customizing = true
|
||||||
|
|
||||||
|
private lazy var textStorage = NSTextStorage()
|
||||||
|
private lazy var layoutManager = NSLayoutManager()
|
||||||
|
private lazy var textContainer = NSTextContainer()
|
||||||
|
|
||||||
|
private var selectedLink: (range: NSRange, url: URL)?
|
||||||
|
|
||||||
|
private var links: [NSRange: URL] = [:]
|
||||||
|
|
||||||
|
override init(frame: CGRect) {
|
||||||
|
super.init(frame: frame)
|
||||||
|
_customizing = false
|
||||||
|
setupLabel()
|
||||||
|
}
|
||||||
|
|
||||||
|
required init?(coder aDecoder: NSCoder) {
|
||||||
|
super.init(coder: aDecoder)
|
||||||
|
_customizing = false
|
||||||
|
setupLabel()
|
||||||
|
}
|
||||||
|
|
||||||
|
private func setupLabel() {
|
||||||
|
textStorage.addLayoutManager(layoutManager)
|
||||||
|
layoutManager.addTextContainer(textContainer)
|
||||||
|
textContainer.lineFragmentPadding = 0
|
||||||
|
textContainer.lineBreakMode = lineBreakMode
|
||||||
|
textContainer.maximumNumberOfLines = numberOfLines
|
||||||
|
isUserInteractionEnabled = true
|
||||||
|
}
|
||||||
|
|
||||||
|
override func awakeFromNib() {
|
||||||
|
super.awakeFromNib()
|
||||||
|
updateTextStorage()
|
||||||
|
}
|
||||||
|
|
||||||
|
override var intrinsicContentSize: CGSize {
|
||||||
|
let superSize = super.intrinsicContentSize
|
||||||
|
textContainer.size = CGSize(width: superSize.width, height: .greatestFiniteMagnitude)
|
||||||
|
let size = layoutManager.usedRect(for: textContainer)
|
||||||
|
return CGSize(width: ceil(size.width), height: ceil(size.height))
|
||||||
|
}
|
||||||
|
|
||||||
|
override func drawText(in rect: CGRect) {
|
||||||
|
let range = NSRange(location: 0, length: textStorage.length)
|
||||||
|
|
||||||
|
textContainer.size = rect.size
|
||||||
|
let origin = rect.origin
|
||||||
|
|
||||||
|
layoutManager.drawBackground(forGlyphRange: range, at: origin)
|
||||||
|
layoutManager.drawGlyphs(forGlyphRange: range, at: origin)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - HTML parsing
|
||||||
|
|
||||||
|
private func parseHTML() {
|
||||||
|
if _customizing { return }
|
||||||
|
guard let text = text else { return }
|
||||||
|
|
||||||
|
let doc = try! SwiftSoup.parse(text)
|
||||||
|
let body = doc.body()!
|
||||||
|
|
||||||
|
let (attributedText, links) = attributedTextForHTMLNode(body)
|
||||||
|
self.links = links
|
||||||
|
let mutAttrString = NSMutableAttributedString(attributedString: attributedText)
|
||||||
|
mutAttrString.addAttribute(.font, value: font, range: NSRange(location: 0, length: mutAttrString.length))
|
||||||
|
self.attributedText = mutAttrString
|
||||||
|
}
|
||||||
|
|
||||||
|
private func attributedTextForHTMLNode(_ node: Node) -> (NSAttributedString, [NSRange: URL]) {
|
||||||
|
switch node {
|
||||||
|
case let node as TextNode:
|
||||||
|
return (NSAttributedString(string: node.text()), [:])
|
||||||
|
case let node as Element:
|
||||||
|
var links = [NSRange: URL]()
|
||||||
|
let attributed = NSMutableAttributedString()
|
||||||
|
node.getChildNodes().forEach { child in
|
||||||
|
let (text, childLinks) = attributedTextForHTMLNode(child)
|
||||||
|
childLinks.forEach { range, url in
|
||||||
|
let newRange = NSRange(location: range.location + attributed.length, length: range.length)
|
||||||
|
links[newRange] = url
|
||||||
|
}
|
||||||
|
attributed.append(text)
|
||||||
|
}
|
||||||
|
|
||||||
|
switch node.tagName() {
|
||||||
|
case "br":
|
||||||
|
attributed.append(NSAttributedString(string: "\n"))
|
||||||
|
case "a":
|
||||||
|
if let link = try? node.attr("href"),
|
||||||
|
let url = URL(string: link) {
|
||||||
|
let linkRange = NSRange(location: 0, length: attributed.length)
|
||||||
|
let linkAttributes: [NSAttributedString.Key: Any] = [
|
||||||
|
.foregroundColor: UIColor.blue
|
||||||
|
]
|
||||||
|
attributed.setAttributes(linkAttributes, range: linkRange)
|
||||||
|
|
||||||
|
links[linkRange] = url
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
return (attributed, links)
|
||||||
|
default:
|
||||||
|
fatalError("Unexpected node type: \(type(of: node))")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Text Storage
|
||||||
|
|
||||||
|
private func updateTextStorage() {
|
||||||
|
if _customizing { return }
|
||||||
|
guard let attributedText = attributedText,
|
||||||
|
attributedText.length > 0 else {
|
||||||
|
links = [:]
|
||||||
|
textStorage.setAttributedString(NSAttributedString())
|
||||||
|
setNeedsDisplay()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
textStorage.setAttributedString(attributedText)
|
||||||
|
_customizing = true
|
||||||
|
text = attributedText.string
|
||||||
|
_customizing = false
|
||||||
|
setNeedsDisplay()
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Interaction
|
||||||
|
|
||||||
|
func getMention(for url: URL, text: String) -> Mention? {
|
||||||
|
// todo: figure out how to get account IDs
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func getTag(for url: URL, text: String) -> MastodonKit.Tag? {
|
||||||
|
if text.starts(with: "#") {
|
||||||
|
let tag = String(text.dropFirst())
|
||||||
|
return MastodonKit.Tag(name: tag, url: url.absoluteString)
|
||||||
|
} else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func onTouch(_ touch: UITouch) -> Bool {
|
||||||
|
let location = touch.location(in: self)
|
||||||
|
var avoidSuperCall = false
|
||||||
|
|
||||||
|
switch touch.phase {
|
||||||
|
case .began, .moved:
|
||||||
|
if let link = link(at: location) {
|
||||||
|
if link.range.location != selectedLink?.range.location || link.range.length != selectedLink?.range.length {
|
||||||
|
updateAttributesWhenSelected(false)
|
||||||
|
selectedLink = link
|
||||||
|
updateAttributesWhenSelected(true)
|
||||||
|
}
|
||||||
|
avoidSuperCall = true
|
||||||
|
} else {
|
||||||
|
updateAttributesWhenSelected(false)
|
||||||
|
selectedLink = nil
|
||||||
|
}
|
||||||
|
case .ended:
|
||||||
|
guard let selectedLink = selectedLink else { return avoidSuperCall }
|
||||||
|
|
||||||
|
let text = String(self.text![Range(selectedLink.range, in: self.text!)!])
|
||||||
|
if let delegate = delegate {
|
||||||
|
if let mention = getMention(for: selectedLink.url, text: text) {
|
||||||
|
delegate.selected(mention: mention)
|
||||||
|
} else if let tag = getTag(for: selectedLink.url, text: text) {
|
||||||
|
delegate.selected(tag: tag)
|
||||||
|
} else {
|
||||||
|
delegate.selected(url: selectedLink.url)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let when = DispatchTime.now() + Double(Int64(0.25 * Double(NSEC_PER_SEC))) / Double(NSEC_PER_SEC)
|
||||||
|
DispatchQueue.main.asyncAfter(deadline: when) {
|
||||||
|
self.updateAttributesWhenSelected(false)
|
||||||
|
self.selectedLink = nil
|
||||||
|
}
|
||||||
|
case .cancelled:
|
||||||
|
updateAttributesWhenSelected(false)
|
||||||
|
selectedLink = nil
|
||||||
|
case .stationary:
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
return avoidSuperCall
|
||||||
|
}
|
||||||
|
|
||||||
|
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
|
||||||
|
guard let touch = touches.first else { return }
|
||||||
|
if onTouch(touch) { return }
|
||||||
|
super.touchesBegan(touches, with: event)
|
||||||
|
}
|
||||||
|
|
||||||
|
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
|
||||||
|
guard let touch = touches.first else { return }
|
||||||
|
if onTouch(touch) { return }
|
||||||
|
super.touchesMoved(touches, with: event)
|
||||||
|
}
|
||||||
|
|
||||||
|
override func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent?) {
|
||||||
|
guard let touch = touches.first else { return }
|
||||||
|
if onTouch(touch) { return }
|
||||||
|
super.touchesCancelled(touches, with: event)
|
||||||
|
}
|
||||||
|
|
||||||
|
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
|
||||||
|
guard let touch = touches.first else { return }
|
||||||
|
if onTouch(touch) { return }
|
||||||
|
super.touchesEnded(touches, with: event)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func link(at location: CGPoint) -> (range: NSRange, url: URL)? {
|
||||||
|
guard textStorage.length > 0 else { return nil }
|
||||||
|
|
||||||
|
let boundingRect = layoutManager.boundingRect(forGlyphRange: NSRange(location: 0, length: textStorage.length), in: textContainer)
|
||||||
|
guard boundingRect.contains(location) else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
let index = layoutManager.glyphIndex(for: location, in: textContainer)
|
||||||
|
|
||||||
|
for (range, url) in links {
|
||||||
|
if index >= range.location && index <= range.location + range.length {
|
||||||
|
return (range, url)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
private func updateAttributesWhenSelected(_ isSelected: Bool) {
|
||||||
|
guard let selectedLink = selectedLink else { return }
|
||||||
|
|
||||||
|
var attributes = textStorage.attributes(at: 0, effectiveRange: nil)
|
||||||
|
|
||||||
|
attributes[.foregroundColor] = isSelected ? nil : UIColor.blue
|
||||||
|
|
||||||
|
textStorage.addAttributes(attributes, range: selectedLink.range)
|
||||||
|
|
||||||
|
setNeedsDisplay()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,96 @@
|
||||||
|
//
|
||||||
|
// ProfileHeaderTableViewCell.swift
|
||||||
|
// Tusker
|
||||||
|
//
|
||||||
|
// Created by Shadowfacts on 8/27/18.
|
||||||
|
// Copyright © 2018 Shadowfacts. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
import MastodonKit
|
||||||
|
|
||||||
|
protocol ProfileHeaderTableViewCellDelegate: StatusTableViewCellDelegate {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
class ProfileHeaderTableViewCell: UITableViewCell {
|
||||||
|
|
||||||
|
var delegate: ProfileHeaderTableViewCellDelegate?
|
||||||
|
|
||||||
|
@IBOutlet weak var displayNameLabel: UILabel!
|
||||||
|
@IBOutlet weak var usernameLabel: UILabel!
|
||||||
|
@IBOutlet weak var noteLabel: HTMLContentLabel!
|
||||||
|
@IBOutlet weak var avatarContainerView: UIView!
|
||||||
|
@IBOutlet weak var avatarImageView: UIImageView!
|
||||||
|
@IBOutlet weak var headerImageView: UIImageView!
|
||||||
|
|
||||||
|
var account: Account!
|
||||||
|
|
||||||
|
var avatarURL: URL?
|
||||||
|
|
||||||
|
var headerImageDownloadTask: URLSessionDataTask?
|
||||||
|
|
||||||
|
override func awakeFromNib() {
|
||||||
|
avatarContainerView.layer.cornerRadius = 12
|
||||||
|
avatarContainerView.layer.masksToBounds = true
|
||||||
|
avatarImageView.layer.cornerRadius = 11.6
|
||||||
|
avatarImageView.layer.masksToBounds = true
|
||||||
|
}
|
||||||
|
|
||||||
|
func updateUI(for account: Account) {
|
||||||
|
self.account = account
|
||||||
|
|
||||||
|
displayNameLabel.text = account.displayName
|
||||||
|
usernameLabel.text = "@\(account.acct)"
|
||||||
|
|
||||||
|
avatarImageView.image = nil
|
||||||
|
if let url = URL(string: account.avatar) {
|
||||||
|
avatarURL = url
|
||||||
|
AvatarCache.shared.get(url) { image in
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
self.avatarImageView.image = image
|
||||||
|
self.avatarURL = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let url = URL(string: account.header) {
|
||||||
|
headerImageDownloadTask = URLSession.shared.dataTask(with: url) { data, response, error in
|
||||||
|
guard error == nil,
|
||||||
|
let data = data,
|
||||||
|
let image = UIImage(data: data) else { return }
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
self.headerImageView.image = image
|
||||||
|
self.headerImageDownloadTask = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
headerImageDownloadTask!.resume()
|
||||||
|
}
|
||||||
|
|
||||||
|
// todo: HTML parsing
|
||||||
|
noteLabel.text = account.note
|
||||||
|
noteLabel.delegate = self
|
||||||
|
}
|
||||||
|
|
||||||
|
override func prepareForReuse() {
|
||||||
|
if let url = avatarURL {
|
||||||
|
AvatarCache.shared.cancel(url)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
extension ProfileHeaderTableViewCell: HTMLContentLabelDelegate {
|
||||||
|
|
||||||
|
func selected(mention: Mention) {
|
||||||
|
delegate?.selected(mention: mention)
|
||||||
|
}
|
||||||
|
|
||||||
|
func selected(tag: Tag) {
|
||||||
|
delegate?.selected(tag: tag)
|
||||||
|
}
|
||||||
|
|
||||||
|
func selected(url: URL) {
|
||||||
|
delegate?.selected(url: url)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,93 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="14313.13.2" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
|
||||||
|
<device id="retina4_7" orientation="portrait">
|
||||||
|
<adaptation id="fullscreen"/>
|
||||||
|
</device>
|
||||||
|
<dependencies>
|
||||||
|
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="14283.9"/>
|
||||||
|
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
|
||||||
|
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||||
|
</dependencies>
|
||||||
|
<objects>
|
||||||
|
<placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner"/>
|
||||||
|
<placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder"/>
|
||||||
|
<view contentMode="scaleToFill" id="iN0-l3-epB" customClass="ProfileHeaderTableViewCell" customModule="Tusker" customModuleProvider="target">
|
||||||
|
<rect key="frame" x="0.0" y="0.0" width="375" height="270"/>
|
||||||
|
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||||
|
<subviews>
|
||||||
|
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFill" horizontalHuggingPriority="251" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="Fw7-OL-iy5">
|
||||||
|
<rect key="frame" x="0.0" y="0.0" width="375" height="150"/>
|
||||||
|
<constraints>
|
||||||
|
<constraint firstAttribute="height" constant="150" id="y43-4B-slK"/>
|
||||||
|
</constraints>
|
||||||
|
</imageView>
|
||||||
|
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Display name" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="LjK-72-Bez">
|
||||||
|
<rect key="frame" x="144" y="158" width="215" height="24"/>
|
||||||
|
<fontDescription key="fontDescription" type="system" pointSize="20"/>
|
||||||
|
<nil key="textColor"/>
|
||||||
|
<nil key="highlightedColor"/>
|
||||||
|
</label>
|
||||||
|
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" text="Note" lineBreakMode="tailTruncation" numberOfLines="0" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="I0n-aP-dJP" customClass="HTMLContentLabel" customModule="Tusker" customModuleProvider="target">
|
||||||
|
<rect key="frame" x="16" y="215" width="343" height="39"/>
|
||||||
|
<fontDescription key="fontDescription" type="system" pointSize="17"/>
|
||||||
|
<nil key="textColor"/>
|
||||||
|
<nil key="highlightedColor"/>
|
||||||
|
</label>
|
||||||
|
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="@username" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="MIj-OR-NOR">
|
||||||
|
<rect key="frame" x="144" y="190" width="215" height="17"/>
|
||||||
|
<fontDescription key="fontDescription" type="system" pointSize="14"/>
|
||||||
|
<color key="textColor" cocoaTouchSystemColor="darkTextColor"/>
|
||||||
|
<nil key="highlightedColor"/>
|
||||||
|
</label>
|
||||||
|
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="KyB-ey-l11">
|
||||||
|
<rect key="frame" x="16" y="90" width="120" height="120"/>
|
||||||
|
<subviews>
|
||||||
|
<imageView userInteractionEnabled="NO" contentMode="scaleToFill" horizontalHuggingPriority="251" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="tH8-sR-DHC">
|
||||||
|
<rect key="frame" x="2" y="2" width="116" height="116"/>
|
||||||
|
<constraints>
|
||||||
|
<constraint firstAttribute="width" constant="116" id="k9v-I0-Aoz"/>
|
||||||
|
<constraint firstAttribute="height" constant="116" id="sz5-86-5Iq"/>
|
||||||
|
</constraints>
|
||||||
|
</imageView>
|
||||||
|
</subviews>
|
||||||
|
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||||
|
<constraints>
|
||||||
|
<constraint firstItem="tH8-sR-DHC" firstAttribute="centerX" secondItem="KyB-ey-l11" secondAttribute="centerX" id="KT6-FP-LsA"/>
|
||||||
|
<constraint firstAttribute="height" constant="120" id="LVm-OC-cGm"/>
|
||||||
|
<constraint firstAttribute="width" constant="120" id="Obt-ZN-POD"/>
|
||||||
|
<constraint firstItem="tH8-sR-DHC" firstAttribute="centerY" secondItem="KyB-ey-l11" secondAttribute="centerY" id="nYu-RE-MfA"/>
|
||||||
|
</constraints>
|
||||||
|
</view>
|
||||||
|
</subviews>
|
||||||
|
<color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||||
|
<constraints>
|
||||||
|
<constraint firstItem="Fw7-OL-iy5" firstAttribute="top" secondItem="vUN-kp-3ea" secondAttribute="top" id="0fI-0y-cXG"/>
|
||||||
|
<constraint firstItem="KyB-ey-l11" firstAttribute="centerY" secondItem="Fw7-OL-iy5" secondAttribute="bottom" id="AXr-6X-FJ8"/>
|
||||||
|
<constraint firstItem="vUN-kp-3ea" firstAttribute="trailing" secondItem="MIj-OR-NOR" secondAttribute="trailing" constant="16" id="AwT-Vi-CLa"/>
|
||||||
|
<constraint firstItem="LjK-72-Bez" firstAttribute="leading" secondItem="KyB-ey-l11" secondAttribute="trailing" constant="8" id="CIO-tn-hJC"/>
|
||||||
|
<constraint firstItem="I0n-aP-dJP" firstAttribute="leading" secondItem="vUN-kp-3ea" secondAttribute="leading" constant="16" id="JlW-us-7Wd"/>
|
||||||
|
<constraint firstItem="LjK-72-Bez" firstAttribute="top" secondItem="Fw7-OL-iy5" secondAttribute="bottom" constant="8" id="Kvl-sz-Lv3"/>
|
||||||
|
<constraint firstItem="Fw7-OL-iy5" firstAttribute="trailing" secondItem="vUN-kp-3ea" secondAttribute="trailing" id="LqH-lE-AIe"/>
|
||||||
|
<constraint firstItem="KyB-ey-l11" firstAttribute="leading" secondItem="vUN-kp-3ea" secondAttribute="leading" constant="16" id="NN7-5B-k1Q"/>
|
||||||
|
<constraint firstItem="vUN-kp-3ea" firstAttribute="trailing" secondItem="I0n-aP-dJP" secondAttribute="trailing" constant="16" id="aaC-xJ-vvs"/>
|
||||||
|
<constraint firstItem="Fw7-OL-iy5" firstAttribute="leading" secondItem="vUN-kp-3ea" secondAttribute="leading" id="d1j-6d-hBb"/>
|
||||||
|
<constraint firstItem="vUN-kp-3ea" firstAttribute="trailing" secondItem="LjK-72-Bez" secondAttribute="trailing" constant="16" id="hn9-c3-iNH"/>
|
||||||
|
<constraint firstItem="MIj-OR-NOR" firstAttribute="leading" secondItem="KyB-ey-l11" secondAttribute="trailing" constant="8" id="iG7-yZ-9u3"/>
|
||||||
|
<constraint firstItem="MIj-OR-NOR" firstAttribute="top" secondItem="LjK-72-Bez" secondAttribute="bottom" constant="8" id="nMM-6t-bjX"/>
|
||||||
|
<constraint firstItem="vUN-kp-3ea" firstAttribute="bottom" secondItem="I0n-aP-dJP" secondAttribute="bottom" constant="16" id="nPu-bH-pLA"/>
|
||||||
|
<constraint firstItem="I0n-aP-dJP" firstAttribute="top" secondItem="MIj-OR-NOR" secondAttribute="bottom" constant="8" id="upb-uc-3BZ"/>
|
||||||
|
</constraints>
|
||||||
|
<freeformSimulatedSizeMetrics key="simulatedDestinationMetrics"/>
|
||||||
|
<viewLayoutGuide key="safeArea" id="vUN-kp-3ea"/>
|
||||||
|
<connections>
|
||||||
|
<outlet property="avatarContainerView" destination="KyB-ey-l11" id="45s-jV-l8L"/>
|
||||||
|
<outlet property="avatarImageView" destination="tH8-sR-DHC" id="6ll-yL-g1o"/>
|
||||||
|
<outlet property="displayNameLabel" destination="LjK-72-Bez" id="nIU-ey-H1C"/>
|
||||||
|
<outlet property="headerImageView" destination="Fw7-OL-iy5" id="6sv-E5-D73"/>
|
||||||
|
<outlet property="noteLabel" destination="I0n-aP-dJP" id="7yW-mE-jxY"/>
|
||||||
|
<outlet property="usernameLabel" destination="MIj-OR-NOR" id="e1I-N7-rKx"/>
|
||||||
|
</connections>
|
||||||
|
<point key="canvasLocation" x="40.799999999999997" y="98.950524737631198"/>
|
||||||
|
</view>
|
||||||
|
</objects>
|
||||||
|
</document>
|
|
@ -2,51 +2,14 @@
|
||||||
// StatusContentLabel.swift
|
// StatusContentLabel.swift
|
||||||
// Tusker
|
// Tusker
|
||||||
//
|
//
|
||||||
// Created by Shadowfacts on 8/25/18.
|
// Created by Shadowfacts on 8/27/18.
|
||||||
// Copyright © 2018 Shadowfacts. All rights reserved.
|
// Copyright © 2018 Shadowfacts. All rights reserved.
|
||||||
//
|
//
|
||||||
|
|
||||||
import UIKit
|
import UIKit
|
||||||
import MastodonKit
|
import MastodonKit
|
||||||
import SwiftSoup
|
|
||||||
|
|
||||||
protocol StatusContentLabelDelegate {
|
class StatusContentLabel: HTMLContentLabel {
|
||||||
|
|
||||||
func selected(mention: Mention)
|
|
||||||
|
|
||||||
func selected(tag: MastodonKit.Tag)
|
|
||||||
|
|
||||||
func selected(url: URL)
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
class StatusContentLabel: UILabel {
|
|
||||||
|
|
||||||
var delegate: StatusContentLabelDelegate?
|
|
||||||
|
|
||||||
override var text: String? {
|
|
||||||
didSet {
|
|
||||||
parseHTML()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override var attributedText: NSAttributedString? {
|
|
||||||
didSet {
|
|
||||||
updateTextStorage()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override var numberOfLines: Int {
|
|
||||||
didSet {
|
|
||||||
textContainer.maximumNumberOfLines = numberOfLines
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override var lineBreakMode: NSLineBreakMode {
|
|
||||||
didSet {
|
|
||||||
textContainer.lineBreakMode = lineBreakMode
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var status: Status! {
|
var status: Status! {
|
||||||
didSet {
|
didSet {
|
||||||
|
@ -54,265 +17,20 @@ class StatusContentLabel: UILabel {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private var _customizing = true
|
override func getMention(for url: URL, text: String) -> Mention? {
|
||||||
|
|
||||||
private lazy var textStorage = NSTextStorage()
|
|
||||||
private lazy var layoutManager = NSLayoutManager()
|
|
||||||
private lazy var textContainer = NSTextContainer()
|
|
||||||
|
|
||||||
private var selectedLink: (range: NSRange, url: URL)?
|
|
||||||
|
|
||||||
private var links: [NSRange: URL] = [:]
|
|
||||||
|
|
||||||
override init(frame: CGRect) {
|
|
||||||
super.init(frame: frame)
|
|
||||||
_customizing = false
|
|
||||||
setupLabel()
|
|
||||||
}
|
|
||||||
|
|
||||||
required init?(coder aDecoder: NSCoder) {
|
|
||||||
super.init(coder: aDecoder)
|
|
||||||
_customizing = false
|
|
||||||
setupLabel()
|
|
||||||
}
|
|
||||||
|
|
||||||
private func setupLabel() {
|
|
||||||
textStorage.addLayoutManager(layoutManager)
|
|
||||||
layoutManager.addTextContainer(textContainer)
|
|
||||||
textContainer.lineFragmentPadding = 0
|
|
||||||
textContainer.lineBreakMode = lineBreakMode
|
|
||||||
textContainer.maximumNumberOfLines = numberOfLines
|
|
||||||
isUserInteractionEnabled = true
|
|
||||||
}
|
|
||||||
|
|
||||||
override func awakeFromNib() {
|
|
||||||
super.awakeFromNib()
|
|
||||||
updateTextStorage()
|
|
||||||
}
|
|
||||||
|
|
||||||
override var intrinsicContentSize: CGSize {
|
|
||||||
let superSize = super.intrinsicContentSize
|
|
||||||
textContainer.size = CGSize(width: superSize.width, height: .greatestFiniteMagnitude)
|
|
||||||
let size = layoutManager.usedRect(for: textContainer)
|
|
||||||
return CGSize(width: ceil(size.width), height: ceil(size.height))
|
|
||||||
}
|
|
||||||
|
|
||||||
override func drawText(in rect: CGRect) {
|
|
||||||
let range = NSRange(location: 0, length: textStorage.length)
|
|
||||||
|
|
||||||
textContainer.size = rect.size
|
|
||||||
let origin = rect.origin
|
|
||||||
|
|
||||||
layoutManager.drawBackground(forGlyphRange: range, at: origin)
|
|
||||||
layoutManager.drawGlyphs(forGlyphRange: range, at: origin)
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - HTML parsing
|
|
||||||
|
|
||||||
private func parseHTML() {
|
|
||||||
if _customizing { return }
|
|
||||||
guard let text = text else { return }
|
|
||||||
|
|
||||||
let doc = try! SwiftSoup.parse(text)
|
|
||||||
let body = doc.body()!
|
|
||||||
|
|
||||||
let (attributedText, links) = attributedTextForHTMLNode(body)
|
|
||||||
self.links = links
|
|
||||||
let mutAttrString = NSMutableAttributedString(attributedString: attributedText)
|
|
||||||
mutAttrString.addAttribute(.font, value: font, range: NSRange(location: 0, length: mutAttrString.length))
|
|
||||||
self.attributedText = mutAttrString
|
|
||||||
}
|
|
||||||
|
|
||||||
private func attributedTextForHTMLNode(_ node: Node) -> (NSAttributedString, [NSRange: URL]) {
|
|
||||||
switch node {
|
|
||||||
case let node as TextNode:
|
|
||||||
return (NSAttributedString(string: node.text()), [:])
|
|
||||||
case let node as Element:
|
|
||||||
var links = [NSRange: URL]()
|
|
||||||
let attributed = NSMutableAttributedString()
|
|
||||||
node.getChildNodes().forEach { child in
|
|
||||||
let (text, childLinks) = attributedTextForHTMLNode(child)
|
|
||||||
childLinks.forEach { range, url in
|
|
||||||
let newRange = NSRange(location: range.location + attributed.length, length: range.length)
|
|
||||||
links[newRange] = url
|
|
||||||
}
|
|
||||||
attributed.append(text)
|
|
||||||
}
|
|
||||||
|
|
||||||
switch node.tagName() {
|
|
||||||
case "br":
|
|
||||||
attributed.append(NSAttributedString(string: "\n"))
|
|
||||||
case "a":
|
|
||||||
if let link = try? node.attr("href"),
|
|
||||||
let url = URL(string: link) {
|
|
||||||
let linkRange = NSRange(location: 0, length: attributed.length)
|
|
||||||
let linkAttributes: [NSAttributedString.Key: Any] = [
|
|
||||||
.foregroundColor: UIColor.blue
|
|
||||||
]
|
|
||||||
attributed.setAttributes(linkAttributes, range: linkRange)
|
|
||||||
|
|
||||||
links[linkRange] = url
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
return (attributed, links)
|
|
||||||
default:
|
|
||||||
fatalError("Unexpected node type: \(type(of: node))")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - Text Storage
|
|
||||||
|
|
||||||
private func updateTextStorage() {
|
|
||||||
if _customizing { return }
|
|
||||||
guard let attributedText = attributedText,
|
|
||||||
attributedText.length > 0 else {
|
|
||||||
links = [:]
|
|
||||||
textStorage.setAttributedString(NSAttributedString())
|
|
||||||
setNeedsDisplay()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
textStorage.setAttributedString(attributedText)
|
|
||||||
_customizing = true
|
|
||||||
text = attributedText.string
|
|
||||||
_customizing = false
|
|
||||||
setNeedsDisplay()
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - Interaction
|
|
||||||
|
|
||||||
private func getMention(for url: URL, text: String) -> Mention? {
|
|
||||||
return status.mentions.first(where: { mention -> Bool in
|
return status.mentions.first(where: { mention -> Bool in
|
||||||
(text.dropFirst() == mention.username || text == mention.username) && url.host == URL(string: mention.url)!.host
|
(text.dropFirst() == mention.username || text == mention.username) && url.host == URL(string: mention.url)!.host
|
||||||
})
|
}) ?? super.getMention(for: url, text: text)
|
||||||
}
|
}
|
||||||
|
|
||||||
private func getTag(for url: URL, text: String) -> MastodonKit.Tag? {
|
override func getTag(for url: URL, text: String) -> MastodonKit.Tag? {
|
||||||
if let tag = status.tags.first(where: { tag -> Bool in
|
if let tag = status.tags.first(where: { tag -> Bool in
|
||||||
tag.url == url.absoluteString
|
tag.url == url.absoluteString
|
||||||
}) {
|
}) {
|
||||||
return tag
|
return tag
|
||||||
} else if text.starts(with: "#") {
|
|
||||||
let tag = String(text.dropFirst())
|
|
||||||
return MastodonKit.Tag(name: tag, url: url.absoluteString)
|
|
||||||
} else {
|
} else {
|
||||||
return nil
|
return super.getTag(for: url, text: text)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func getEntity(for url: URL, text: String) -> Any {
|
|
||||||
if let mention = getMention(for: url, text: text) {
|
|
||||||
return mention
|
|
||||||
} else if let tag = getTag(for: url, text: text) {
|
|
||||||
return tag
|
|
||||||
} else {
|
|
||||||
return url
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private func onTouch(_ touch: UITouch) -> Bool {
|
|
||||||
let location = touch.location(in: self)
|
|
||||||
var avoidSuperCall = false
|
|
||||||
|
|
||||||
switch touch.phase {
|
|
||||||
case .began, .moved:
|
|
||||||
if let link = link(at: location) {
|
|
||||||
if link.range.location != selectedLink?.range.location || link.range.length != selectedLink?.range.length {
|
|
||||||
updateAttributesWhenSelected(false)
|
|
||||||
selectedLink = link
|
|
||||||
updateAttributesWhenSelected(true)
|
|
||||||
}
|
|
||||||
avoidSuperCall = true
|
|
||||||
} else {
|
|
||||||
updateAttributesWhenSelected(false)
|
|
||||||
selectedLink = nil
|
|
||||||
}
|
|
||||||
case .ended:
|
|
||||||
guard let selectedLink = selectedLink else { return avoidSuperCall }
|
|
||||||
|
|
||||||
let text = String(self.text![Range(selectedLink.range, in: self.text!)!])
|
|
||||||
// print(getEntity(for: selectedLink.url, text: text))
|
|
||||||
if let delegate = delegate {
|
|
||||||
if let mention = getMention(for: selectedLink.url, text: text) {
|
|
||||||
delegate.selected(mention: mention)
|
|
||||||
} else if let tag = getTag(for: selectedLink.url, text: text) {
|
|
||||||
delegate.selected(tag: tag)
|
|
||||||
} else {
|
|
||||||
delegate.selected(url: selectedLink.url)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let when = DispatchTime.now() + Double(Int64(0.25 * Double(NSEC_PER_SEC))) / Double(NSEC_PER_SEC)
|
|
||||||
DispatchQueue.main.asyncAfter(deadline: when) {
|
|
||||||
self.updateAttributesWhenSelected(false)
|
|
||||||
self.selectedLink = nil
|
|
||||||
}
|
|
||||||
case .cancelled:
|
|
||||||
updateAttributesWhenSelected(false)
|
|
||||||
selectedLink = nil
|
|
||||||
case .stationary:
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
return avoidSuperCall
|
|
||||||
}
|
|
||||||
|
|
||||||
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
|
|
||||||
guard let touch = touches.first else { return }
|
|
||||||
if onTouch(touch) { return }
|
|
||||||
super.touchesBegan(touches, with: event)
|
|
||||||
}
|
|
||||||
|
|
||||||
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
|
|
||||||
guard let touch = touches.first else { return }
|
|
||||||
if onTouch(touch) { return }
|
|
||||||
super.touchesMoved(touches, with: event)
|
|
||||||
}
|
|
||||||
|
|
||||||
override func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent?) {
|
|
||||||
guard let touch = touches.first else { return }
|
|
||||||
if onTouch(touch) { return }
|
|
||||||
super.touchesCancelled(touches, with: event)
|
|
||||||
}
|
|
||||||
|
|
||||||
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
|
|
||||||
guard let touch = touches.first else { return }
|
|
||||||
if onTouch(touch) { return }
|
|
||||||
super.touchesEnded(touches, with: event)
|
|
||||||
}
|
|
||||||
|
|
||||||
private func link(at location: CGPoint) -> (range: NSRange, url: URL)? {
|
|
||||||
guard textStorage.length > 0 else { return nil }
|
|
||||||
|
|
||||||
let boundingRect = layoutManager.boundingRect(forGlyphRange: NSRange(location: 0, length: textStorage.length), in: textContainer)
|
|
||||||
guard boundingRect.contains(location) else {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
let index = layoutManager.glyphIndex(for: location, in: textContainer)
|
|
||||||
|
|
||||||
for (range, url) in links {
|
|
||||||
if index >= range.location && index <= range.location + range.length {
|
|
||||||
return (range, url)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
private func updateAttributesWhenSelected(_ isSelected: Bool) {
|
|
||||||
guard let selectedLink = selectedLink else { return }
|
|
||||||
|
|
||||||
var attributes = textStorage.attributes(at: 0, effectiveRange: nil)
|
|
||||||
|
|
||||||
attributes[.foregroundColor] = isSelected ? nil : UIColor.blue
|
|
||||||
|
|
||||||
textStorage.addAttributes(attributes, range: selectedLink.range)
|
|
||||||
|
|
||||||
setNeedsDisplay()
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,6 +12,8 @@ import SwiftSoup
|
||||||
|
|
||||||
protocol StatusTableViewCellDelegate {
|
protocol StatusTableViewCellDelegate {
|
||||||
|
|
||||||
|
func selected(account: Account)
|
||||||
|
|
||||||
func selected(mention: Mention)
|
func selected(mention: Mention)
|
||||||
|
|
||||||
func selected(tag: MastodonKit.Tag)
|
func selected(tag: MastodonKit.Tag)
|
||||||
|
@ -30,6 +32,7 @@ class StatusTableViewCell: UITableViewCell {
|
||||||
@IBOutlet weak var avatarImageView: UIImageView!
|
@IBOutlet weak var avatarImageView: UIImageView!
|
||||||
|
|
||||||
var status: Status!
|
var status: Status!
|
||||||
|
var account: Account!
|
||||||
|
|
||||||
var avatarURL: URL?
|
var avatarURL: URL?
|
||||||
|
|
||||||
|
@ -39,6 +42,17 @@ class StatusTableViewCell: UITableViewCell {
|
||||||
|
|
||||||
var links: [NSRange: URL] = [:]
|
var links: [NSRange: URL] = [:]
|
||||||
|
|
||||||
|
override func awakeFromNib() {
|
||||||
|
displayNameLabel.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(accountPressed)))
|
||||||
|
displayNameLabel.isUserInteractionEnabled = true
|
||||||
|
usernameLabel.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(accountPressed)))
|
||||||
|
usernameLabel.isUserInteractionEnabled = true
|
||||||
|
avatarImageView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(accountPressed)))
|
||||||
|
avatarImageView.isUserInteractionEnabled = true
|
||||||
|
avatarImageView.layer.cornerRadius = 5
|
||||||
|
avatarImageView.layer.masksToBounds = true
|
||||||
|
}
|
||||||
|
|
||||||
func updateUI(for status: Status) {
|
func updateUI(for status: Status) {
|
||||||
self.status = status
|
self.status = status
|
||||||
|
|
||||||
|
@ -48,15 +62,17 @@ class StatusTableViewCell: UITableViewCell {
|
||||||
} else {
|
} else {
|
||||||
account = status.account
|
account = status.account
|
||||||
}
|
}
|
||||||
|
self.account = account
|
||||||
|
|
||||||
displayNameLabel.text = account.displayName
|
displayNameLabel.text = account.displayName
|
||||||
usernameLabel.text = "@\(account.acct)"
|
usernameLabel.text = "@\(account.acct)"
|
||||||
avatarImageView.layer.cornerRadius = 5
|
|
||||||
avatarImageView.layer.masksToBounds = true
|
|
||||||
avatarImageView.image = nil
|
avatarImageView.image = nil
|
||||||
if let url = URL(string: account.avatar) {
|
if let url = URL(string: account.avatar) {
|
||||||
|
avatarURL = url
|
||||||
AvatarCache.shared.get(url) { image in
|
AvatarCache.shared.get(url) { image in
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
self.avatarImageView.image = image
|
self.avatarImageView.image = image
|
||||||
|
self.avatarURL = nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -71,9 +87,13 @@ class StatusTableViewCell: UITableViewCell {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@objc func accountPressed() {
|
||||||
|
delegate?.selected(account: account)
|
||||||
}
|
}
|
||||||
|
|
||||||
extension StatusTableViewCell: StatusContentLabelDelegate {
|
}
|
||||||
|
|
||||||
|
extension StatusTableViewCell: HTMLContentLabelDelegate {
|
||||||
|
|
||||||
func selected(mention: Mention) {
|
func selected(mention: Mention) {
|
||||||
delegate?.selected(mention: mention)
|
delegate?.selected(mention: mention)
|
||||||
|
|
|
@ -17,6 +17,7 @@
|
||||||
<subviews>
|
<subviews>
|
||||||
<imageView userInteractionEnabled="NO" contentMode="scaleToFill" horizontalHuggingPriority="251" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="QMP-j2-HLn">
|
<imageView userInteractionEnabled="NO" contentMode="scaleToFill" horizontalHuggingPriority="251" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="QMP-j2-HLn">
|
||||||
<rect key="frame" x="16" y="8" width="50" height="50"/>
|
<rect key="frame" x="16" y="8" width="50" height="50"/>
|
||||||
|
<gestureRecognizers/>
|
||||||
<constraints>
|
<constraints>
|
||||||
<constraint firstAttribute="width" constant="50" id="KZ8-d7-8UK"/>
|
<constraint firstAttribute="width" constant="50" id="KZ8-d7-8UK"/>
|
||||||
<constraint firstAttribute="height" constant="50" id="nMi-Gq-JyV"/>
|
<constraint firstAttribute="height" constant="50" id="nMi-Gq-JyV"/>
|
||||||
|
@ -24,12 +25,14 @@
|
||||||
</imageView>
|
</imageView>
|
||||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" horizontalCompressionResistancePriority="751" text="Display name" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="gll-xe-FSr">
|
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" horizontalCompressionResistancePriority="751" text="Display name" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="gll-xe-FSr">
|
||||||
<rect key="frame" x="74" y="8" width="103" height="27.5"/>
|
<rect key="frame" x="74" y="8" width="103" height="27.5"/>
|
||||||
|
<gestureRecognizers/>
|
||||||
<fontDescription key="fontDescription" type="system" pointSize="17"/>
|
<fontDescription key="fontDescription" type="system" pointSize="17"/>
|
||||||
<nil key="textColor"/>
|
<nil key="textColor"/>
|
||||||
<nil key="highlightedColor"/>
|
<nil key="highlightedColor"/>
|
||||||
</label>
|
</label>
|
||||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" verticalHuggingPriority="251" text="@username" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="j89-zc-SFa">
|
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" verticalHuggingPriority="251" text="@username" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="j89-zc-SFa">
|
||||||
<rect key="frame" x="185" y="8" width="174" height="21"/>
|
<rect key="frame" x="185" y="8" width="174" height="21"/>
|
||||||
|
<gestureRecognizers/>
|
||||||
<fontDescription key="fontDescription" type="system" pointSize="17"/>
|
<fontDescription key="fontDescription" type="system" pointSize="17"/>
|
||||||
<color key="textColor" white="0.33333333333333331" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
<color key="textColor" white="0.33333333333333331" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||||
<nil key="highlightedColor"/>
|
<nil key="highlightedColor"/>
|
||||||
|
|
|
@ -0,0 +1,36 @@
|
||||||
|
//
|
||||||
|
// UIViewController+StatusTableViewCellDelegate.swift
|
||||||
|
// Tusker
|
||||||
|
//
|
||||||
|
// Created by Shadowfacts on 8/27/18.
|
||||||
|
// Copyright © 2018 Shadowfacts. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
import MastodonKit
|
||||||
|
import SafariServices
|
||||||
|
|
||||||
|
extension UIViewController: StatusTableViewCellDelegate {
|
||||||
|
|
||||||
|
func selected(account: Account) {
|
||||||
|
guard let navigationController = navigationController else {
|
||||||
|
fatalError("Can't show profile VC when not in navigation controller")
|
||||||
|
}
|
||||||
|
let vc = ProfileTableViewController.create(for: account)
|
||||||
|
navigationController.pushViewController(vc, animated: true)
|
||||||
|
}
|
||||||
|
|
||||||
|
func selected(mention: Mention) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func selected(tag: Tag) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func selected(url: URL) {
|
||||||
|
let vc = SFSafariViewController(url: url)
|
||||||
|
present(vc, animated: true)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
Loading…
Reference in New Issue