Browse Source

Development

master
Dwayne Harris 6 years ago
parent
commit
bb78a91df5
  1. 16
      elpha-ios.xcodeproj/project.pbxproj
  2. 97
      elpha-ios/AuthenticateViewController.swift
  3. 13
      elpha-ios/AuthenticationManager.swift
  4. 137
      elpha-ios/Base.lproj/Main.storyboard
  5. 77
      elpha-ios/Date+TimeAgo.swift
  6. 75
      elpha-ios/Elpha.xcdatamodeld/Elpha.xcdatamodel/contents
  7. 6
      elpha-ios/InstancesDataManager.swift
  8. 8
      elpha-ios/InstancesTableViewCell.swift
  9. 12
      elpha-ios/InstancesTableViewController.swift
  10. 231
      elpha-ios/MastodonDataManager.swift
  11. 18
      elpha-ios/TimelineTableViewCell.swift
  12. 210
      elpha-ios/TimelineTableViewController.swift

16
elpha-ios.xcodeproj/project.pbxproj

@ -11,6 +11,8 @@
157405B12151A5DA00EEAAEB /* README.md in Resources */ = {isa = PBXBuildFile; fileRef = 157405AF2151A5DA00EEAAEB /* README.md */; };
157405B42151A93E00EEAAEB /* InstancesDataManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 157405B32151A93E00EEAAEB /* InstancesDataManager.swift */; };
157405D1215890D700EEAAEB /* Alamofire.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 157405C3215890BC00EEAAEB /* Alamofire.framework */; };
159026AE2162CF5600D362DD /* TimelineTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 159026AD2162CF5600D362DD /* TimelineTableViewCell.swift */; };
159026D02163069600D362DD /* Date+TimeAgo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 159026CF2163069600D362DD /* Date+TimeAgo.swift */; };
159048AF214F5015004F4014 /* InstancesTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 159048AE214F5015004F4014 /* InstancesTableViewCell.swift */; };
15960E5B213145E100C38CE9 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 15960E5A213145E100C38CE9 /* AppDelegate.swift */; };
15960E5F213145E100C38CE9 /* SecondViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 15960E5E213145E100C38CE9 /* SecondViewController.swift */; };
@ -31,6 +33,8 @@
15A79B20215B439A007A326E /* OAuthSwift.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 15A79B13215B438C007A326E /* OAuthSwift.framework */; };
15A79B2E215C63B6007A326E /* AlamofireImage.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1517EA842159D72200DE80D6 /* AlamofireImage.framework */; };
15A79B43215EB959007A326E /* CoreDataManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 15A79B42215EB959007A326E /* CoreDataManager.swift */; };
15F9981721629965009E58DA /* TimelineTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 15F9981621629965009E58DA /* TimelineTableViewController.swift */; };
15F998352162C0E8009E58DA /* MastodonDataManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 15F998342162C0E8009E58DA /* MastodonDataManager.swift */; };
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
@ -224,6 +228,8 @@
157405AF2151A5DA00EEAAEB /* README.md */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = "<group>"; };
157405B32151A93E00EEAAEB /* InstancesDataManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstancesDataManager.swift; sourceTree = "<group>"; };
157405B7215890BC00EEAAEB /* Alamofire.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = Alamofire.xcodeproj; path = Frameworks/Alamofire/Alamofire.xcodeproj; sourceTree = "<group>"; };
159026AD2162CF5600D362DD /* TimelineTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineTableViewCell.swift; sourceTree = "<group>"; };
159026CF2163069600D362DD /* Date+TimeAgo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Date+TimeAgo.swift"; sourceTree = "<group>"; };
159048AE214F5015004F4014 /* InstancesTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstancesTableViewCell.swift; sourceTree = "<group>"; };
15960E57213145E100C38CE9 /* elpha-ios.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "elpha-ios.app"; sourceTree = BUILT_PRODUCTS_DIR; };
15960E5A213145E100C38CE9 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
@ -245,6 +251,8 @@
15A79AE7215B3CC5007A326E /* MastodonKit.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = MastodonKit.xcodeproj; path = Frameworks/MastodonKit/MastodonKit.xcodeproj; sourceTree = "<group>"; };
15A79B08215B438C007A326E /* OAuthSwift.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = OAuthSwift.xcodeproj; path = Frameworks/OAuthSwift/OAuthSwift.xcodeproj; sourceTree = "<group>"; };
15A79B42215EB959007A326E /* CoreDataManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoreDataManager.swift; sourceTree = "<group>"; };
15F9981621629965009E58DA /* TimelineTableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineTableViewController.swift; sourceTree = "<group>"; };
15F998342162C0E8009E58DA /* MastodonDataManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonDataManager.swift; sourceTree = "<group>"; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
@ -328,7 +336,9 @@
15960E7B213272CD00C38CE9 /* AuthenticationManager.swift */,
15960E7621322C6F00C38CE9 /* Configuration.swift */,
15A79B42215EB959007A326E /* CoreDataManager.swift */,
159026CF2163069600D362DD /* Date+TimeAgo.swift */,
157405B32151A93E00EEAAEB /* InstancesDataManager.swift */,
15F998342162C0E8009E58DA /* MastodonDataManager.swift */,
15960E63213145E200C38CE9 /* Assets.xcassets */,
15960E6E21321FA500C38CE9 /* Elpha.xcdatamodeld */,
15960E7121322B9F00C38CE9 /* Keychain Helper */,
@ -358,6 +368,8 @@
15960E792132387A00C38CE9 /* MainTabBarController.swift */,
15960E5E213145E100C38CE9 /* SecondViewController.swift */,
15960E812136668500C38CE9 /* TimelinesNavigationController.swift */,
159026AD2162CF5600D362DD /* TimelineTableViewCell.swift */,
15F9981621629965009E58DA /* TimelineTableViewController.swift */,
15960E7F21353DCF00C38CE9 /* TimelineViewController.swift */,
);
name = "View Controllers";
@ -667,8 +679,10 @@
159048AF214F5015004F4014 /* InstancesTableViewCell.swift in Sources */,
15960E84213774FC00C38CE9 /* InstancesTableViewController.swift in Sources */,
15960E7021321FA500C38CE9 /* Elpha.xcdatamodeld in Sources */,
15F9981721629965009E58DA /* TimelineTableViewController.swift in Sources */,
157405B42151A93E00EEAAEB /* InstancesDataManager.swift in Sources */,
15960E5F213145E100C38CE9 /* SecondViewController.swift in Sources */,
15F998352162C0E8009E58DA /* MastodonDataManager.swift in Sources */,
15960E8021353DCF00C38CE9 /* TimelineViewController.swift in Sources */,
15960E7A2132387A00C38CE9 /* MainTabBarController.swift in Sources */,
15960E7C213272CD00C38CE9 /* AuthenticationManager.swift in Sources */,
@ -678,8 +692,10 @@
15960E7521322BF800C38CE9 /* KeychainWrapper.swift in Sources */,
157405A82150588A00EEAAEB /* InstanceViewController.swift in Sources */,
15960E7321322BC700C38CE9 /* KeychainItemAccessibility.swift in Sources */,
159026D02163069600D362DD /* Date+TimeAgo.swift in Sources */,
15A79B43215EB959007A326E /* CoreDataManager.swift in Sources */,
15960E822136668500C38CE9 /* TimelinesNavigationController.swift in Sources */,
159026AE2162CF5600D362DD /* TimelineTableViewCell.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};

97
elpha-ios/AuthenticateViewController.swift

@ -28,84 +28,35 @@ class AuthenticateViewController: UIViewController {
return false
}
func saveApp(id: String, clientID: String, clientSecret: String, url: String) -> AppMO {
let app = AppMO(context: CoreDataManager.shared.getContext())
app.id = id
app.clientID = clientID
app.clientSecret = clientSecret
app.url = url
CoreDataManager.shared.saveContext()
return app
}
func upsertAccount(_ remoteAccount: Account) -> AccountMO? {
func saveAccount(_ account: AccountMO) -> AccountMO? {
account.id = remoteAccount.id
account.username = remoteAccount.username
account.acct = remoteAccount.acct
account.displayName = remoteAccount.displayName
account.note = remoteAccount.note
account.url = remoteAccount.url
account.avatarURL = URL(string: remoteAccount.avatar)
account.avatarStaticURL = URL(string: remoteAccount.avatarStatic)
account.headerURL = URL(string: remoteAccount.header)
account.headerStaticURL = URL(string: remoteAccount.headerStatic)
account.locked = remoteAccount.locked
account.createdAt = remoteAccount.createdAt
account.followersCount = Int32(remoteAccount.followersCount)
account.followingCount = Int32(remoteAccount.followingCount)
account.statusesCount = Int32(remoteAccount.statusesCount)
CoreDataManager.shared.saveContext()
return account
}
let context = CoreDataManager.shared.getContext()
let request = NSFetchRequest<AccountMO>(entityName: "Account")
request.predicate = NSPredicate(format: "acct == %@", remoteAccount.acct)
do {
let results = try context.fetch(request)
if let account = results.first {
return saveAccount(account)
} else {
return saveAccount(AccountMO(context: context))
}
} catch {
print("\(error)")
return nil
}
}
func authorize(app: AppMO) {
func authorize(client: ClientMO) {
let oauthswift = OAuth2Swift(
consumerKey: app.clientID!,
consumerSecret: app.clientSecret!,
authorizeUrl: "https://\(app.url!)/oauth/authorize",
accessTokenUrl: "https://\(app.url!)/oauth/token",
consumerKey: client.clientID!,
consumerSecret: client.clientSecret!,
authorizeUrl: "https://\(client.url!)/oauth/authorize",
accessTokenUrl: "https://\(client.url!)/oauth/token",
responseType: "code"
)
self.oauthswift = oauthswift
oauthswift.authorizeURLHandler = OAuthSwiftOpenURLExternally.sharedInstance
self.oauthswift = oauthswift
let _ = oauthswift.authorize(
withCallbackURL: URL(string: "elpha://oauth")!,
scope: "read write follow",
state: NSUUID().uuidString,
success: { credential, _, _ in
let client = Client(
baseURL: "https://\(app.url!)",
let mkClient = Client(
baseURL: "https://\(client.url!)",
accessToken: credential.oauthToken
)
let request = Accounts.currentUser()
client.run(request) { result in
mkClient.run(request) { result in
switch result {
case .success(let value):
let account = self.upsertAccount(value.0)
let _ = AuthenticationManager.shared.saveSession(app: app, account: account!, token: credential.oauthToken)
case .success(let account, _):
let account = MastodonDataManager.upsertAccount(account)
let _ = AuthenticationManager.shared.saveSession(client: client, account: account!, token: credential.oauthToken)
self.dismiss(animated: true)
case .failure(let error):
print("\(error)")
}
@ -140,16 +91,16 @@ class AuthenticateViewController: UIViewController {
signInButton.isEnabled = true
}
let request = NSFetchRequest<AppMO>(entityName: "App")
let request = NSFetchRequest<ClientMO>(entityName: "Client")
request.predicate = NSPredicate(format: "url == %@", url)
do {
let response = try CoreDataManager.shared.getContext().fetch(request)
if let app = response.first {
self.authorize(app: app)
self.dismiss(animated: true)
if let client = response.first {
self.authorize(client: client)
} else {
let client = Client(baseURL: "https://\(url)")
let request = Clients.register(
clientName: Config.clientDisplayName,
redirectURI: "elpha://oauth",
@ -159,17 +110,15 @@ class AuthenticateViewController: UIViewController {
client.run(request) { result in
switch result {
case .success(let value):
let remoteApp = value.0
let app = self.saveApp(
id: remoteApp.id,
clientID: remoteApp.clientID,
clientSecret: remoteApp.clientSecret,
case .success(let remoteClient, _):
let client = MastodonDataManager.saveClient(
id: remoteClient.id,
clientID: remoteClient.clientID,
clientSecret: remoteClient.clientSecret,
url: url
)
self.authorize(app: app)
self.dismiss(animated: true)
self.authorize(client: client)
case .failure(let error):
print("\(error)")
}

13
elpha-ios/AuthenticationManager.swift

@ -8,6 +8,7 @@
import CoreData
import Foundation
import MastodonKit
import UIKit
class AuthenticationManager {
@ -50,13 +51,21 @@ class AuthenticationManager {
}
}
func saveSession(app: AppMO, account: AccountMO, token: String) -> SessionMO? {
func getMKClientForSelectedSession() -> Client? {
guard let session = selectedSession, let client = session.client else {
return nil
}
return Client(baseURL: "https://\(client.url!)", accessToken: selectedSessionToken)
}
func saveSession(client: ClientMO, account: AccountMO, token: String) -> SessionMO? {
if let selectedSession = selectedSession {
selectedSession.selected = false
}
let session = SessionMO(context: CoreDataManager.shared.getContext())
session.app = app
session.client = client
session.account = account
session.order = 0
session.createdAt = Date()

137
elpha-ios/Base.lproj/Main.storyboard

@ -9,22 +9,6 @@
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<scenes>
<!--Timeline View Controller-->
<scene sceneID="hNz-n2-bh7">
<objects>
<viewController id="9pv-A4-QxB" customClass="TimelineViewController" customModule="elpha_ios" customModuleProvider="target" sceneMemberID="viewController">
<view key="view" contentMode="scaleToFill" id="tsR-hK-woN">
<rect key="frame" x="0.0" y="0.0" width="414" height="896"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<viewLayoutGuide key="safeArea" id="PQr-Ze-W5v"/>
</view>
<navigationItem key="navigationItem" id="fYO-hh-Wv6"/>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="W5J-7L-Pyd" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="1688.8" y="-320.68965517241384"/>
</scene>
<!--Authenticate View Controller-->
<scene sceneID="ti2-vn-Sq9">
<objects>
@ -552,19 +536,132 @@
<navigationController automaticallyAdjustsScrollViewInsets="NO" id="tkS-C2-D40" customClass="TimelinesNavigationController" customModule="elpha_ios" customModuleProvider="target" sceneMemberID="viewController">
<tabBarItem key="tabBarItem" title="Timelines" image="Timelines" id="acW-dT-cKf"/>
<toolbarItems/>
<navigationBar key="navigationBar" contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" id="MdH-tW-gfk">
<rect key="frame" x="0.0" y="44" width="414" height="44"/>
<navigationBar key="navigationBar" contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" largeTitles="YES" id="MdH-tW-gfk">
<rect key="frame" x="0.0" y="44" width="414" height="96"/>
<autoresizingMask key="autoresizingMask"/>
<color key="tintColor" red="0.090196078430000007" green="0.047058823530000002" blue="0.28627450980000002" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<color key="barTintColor" red="0.72941176470000002" green="0.6705882353" blue="0.83921568629999999" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<textAttributes key="titleTextAttributes">
<color key="textColor" red="0.090196078430000007" green="0.047058823530000002" blue="0.28627450980000002" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
</textAttributes>
<textAttributes key="largeTitleTextAttributes">
<color key="textColor" red="0.090196078430000007" green="0.047058823530000002" blue="0.28627450980000002" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
</textAttributes>
</navigationBar>
<nil name="viewControllers"/>
<connections>
<segue destination="9pv-A4-QxB" kind="relationship" relationship="rootViewController" id="ein-3N-ZQu"/>
<segue destination="9RY-uh-Cwh" kind="relationship" relationship="rootViewController" id="byv-oj-HeD"/>
</connections>
</navigationController>
<placeholder placeholderIdentifier="IBFirstResponder" id="lyd-tU-mSo" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="749.60000000000002" y="-320.68965517241384"/>
</scene>
<!--Timeline-->
<scene sceneID="jGL-v8-K0I">
<objects>
<tableViewController id="9RY-uh-Cwh" customClass="TimelineTableViewController" customModule="elpha_ios" 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="fgT-hp-jpg">
<rect key="frame" x="0.0" y="0.0" width="414" height="896"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<prototypes>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" reuseIdentifier="TimelineTableViewCell" rowHeight="150" id="ltf-IW-WJt" customClass="TimelineTableViewCell" customModule="elpha_ios" customModuleProvider="target">
<rect key="frame" x="0.0" y="28" width="414" height="150"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="ltf-IW-WJt" id="n6I-gD-BmS">
<rect key="frame" x="0.0" y="0.0" width="414" height="149.5"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<imageView userInteractionEnabled="NO" contentMode="scaleToFill" horizontalHuggingPriority="251" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="oQB-FK-89W">
<rect key="frame" x="20" y="11" width="50" height="50"/>
<constraints>
<constraint firstAttribute="width" constant="50" id="8ca-j9-jUQ"/>
<constraint firstAttribute="height" constant="50" id="rs8-rd-h8m"/>
</constraints>
</imageView>
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="oRN-Cg-Tuh">
<rect key="frame" x="78" y="16" width="258" height="50"/>
<subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Label" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="WfV-kx-uwj">
<rect key="frame" x="0.0" y="0.0" width="258" height="18"/>
<fontDescription key="fontDescription" type="system" pointSize="15"/>
<color key="textColor" red="0.090196078430000007" green="0.047058823530000002" blue="0.28627450980000002" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<nil key="highlightedColor"/>
</label>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Label" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="Jzk-kb-RSG">
<rect key="frame" x="0.0" y="23" width="258" height="14.5"/>
<fontDescription key="fontDescription" type="system" pointSize="12"/>
<color key="textColor" red="0.090196078430000007" green="0.047058823530000002" blue="0.28627450980000002" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<nil key="highlightedColor"/>
</label>
</subviews>
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<constraints>
<constraint firstItem="WfV-kx-uwj" firstAttribute="top" secondItem="oRN-Cg-Tuh" secondAttribute="top" id="CF4-vW-ksc"/>
<constraint firstAttribute="height" constant="50" id="FKR-wN-R79"/>
<constraint firstAttribute="trailing" secondItem="Jzk-kb-RSG" secondAttribute="trailing" id="NSm-0a-Puo"/>
<constraint firstItem="Jzk-kb-RSG" firstAttribute="leading" secondItem="oRN-Cg-Tuh" secondAttribute="leading" id="e07-cg-ilX"/>
<constraint firstItem="Jzk-kb-RSG" firstAttribute="top" secondItem="WfV-kx-uwj" secondAttribute="bottom" constant="5" id="kt5-Ce-afJ"/>
<constraint firstAttribute="trailing" secondItem="WfV-kx-uwj" secondAttribute="trailing" id="orG-rZ-afg"/>
<constraint firstItem="WfV-kx-uwj" firstAttribute="leading" secondItem="oRN-Cg-Tuh" secondAttribute="leading" id="zTG-Wl-wrg"/>
</constraints>
</view>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Label" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="u1N-eJ-oKk">
<rect key="frame" x="344" y="11" width="50" height="50"/>
<constraints>
<constraint firstAttribute="width" constant="50" id="0pw-Ep-hi5"/>
<constraint firstAttribute="height" constant="50" id="dMz-Rn-S3b"/>
</constraints>
<fontDescription key="fontDescription" type="system" pointSize="11"/>
<color key="textColor" red="0.090196078430000007" green="0.047058823530000002" blue="0.28627450980000002" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<nil key="highlightedColor"/>
</label>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Label" textAlignment="natural" lineBreakMode="wordWrap" numberOfLines="0" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="Uxx-lg-rdv">
<rect key="frame" x="20" y="76" width="374" height="63"/>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
<color key="textColor" red="0.090196078430000007" green="0.047058823530000002" blue="0.28627450980000002" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<nil key="highlightedColor"/>
</label>
</subviews>
<constraints>
<constraint firstItem="oQB-FK-89W" firstAttribute="top" secondItem="n6I-gD-BmS" secondAttribute="topMargin" id="0Q6-R0-IwX"/>
<constraint firstItem="Uxx-lg-rdv" firstAttribute="leading" secondItem="n6I-gD-BmS" secondAttribute="leadingMargin" id="3js-ld-GRK"/>
<constraint firstItem="u1N-eJ-oKk" firstAttribute="trailing" secondItem="n6I-gD-BmS" secondAttribute="trailingMargin" id="Ekk-lS-cal"/>
<constraint firstItem="Uxx-lg-rdv" firstAttribute="top" secondItem="oQB-FK-89W" secondAttribute="bottom" constant="15" id="Idj-Tv-uBu"/>
<constraint firstItem="oQB-FK-89W" firstAttribute="leading" secondItem="n6I-gD-BmS" secondAttribute="leadingMargin" id="Kq3-2O-M8V"/>
<constraint firstItem="oRN-Cg-Tuh" firstAttribute="leading" secondItem="oQB-FK-89W" secondAttribute="trailing" constant="8" id="Lhi-K2-Odq"/>
<constraint firstItem="u1N-eJ-oKk" firstAttribute="leading" secondItem="oRN-Cg-Tuh" secondAttribute="trailing" constant="8" id="nwg-gI-09m"/>
<constraint firstItem="u1N-eJ-oKk" firstAttribute="top" secondItem="n6I-gD-BmS" secondAttribute="topMargin" id="smr-C3-RRE"/>
<constraint firstAttribute="trailingMargin" secondItem="Uxx-lg-rdv" secondAttribute="trailing" id="uDD-HN-8e1"/>
<constraint firstItem="oRN-Cg-Tuh" firstAttribute="top" secondItem="n6I-gD-BmS" secondAttribute="topMargin" constant="5" id="v8J-VK-mN3"/>
<constraint firstAttribute="bottomMargin" secondItem="Uxx-lg-rdv" secondAttribute="bottom" id="vrh-TW-fBX"/>
</constraints>
</tableViewCellContentView>
<connections>
<outlet property="acctLabel" destination="Jzk-kb-RSG" id="h1c-eN-uYC"/>
<outlet property="avatarImageView" destination="oQB-FK-89W" id="ui0-Hf-fxP"/>
<outlet property="contentLabel" destination="Uxx-lg-rdv" id="gZl-GN-45S"/>
<outlet property="displayNameLabel" destination="WfV-kx-uwj" id="qDC-lL-bRb"/>
<outlet property="timestampLabel" destination="u1N-eJ-oKk" id="GEV-Rv-sWI"/>
</connections>
</tableViewCell>
</prototypes>
<connections>
<outlet property="dataSource" destination="9RY-uh-Cwh" id="o0C-ny-A0b"/>
<outlet property="delegate" destination="9RY-uh-Cwh" id="BBP-UD-Mnx"/>
</connections>
</tableView>
<navigationItem key="navigationItem" title="Timeline" id="Ffm-5G-rCg"/>
<refreshControl key="refreshControl" opaque="NO" multipleTouchEnabled="YES" contentMode="center" insetsLayoutMarginsFromSafeArea="NO" enabled="NO" contentHorizontalAlignment="center" contentVerticalAlignment="center" id="CMZ-BQ-MbK">
<autoresizingMask key="autoresizingMask"/>
<attributedString key="attributedTitle"/>
</refreshControl>
</tableViewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="KTy-Tn-EdD" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="1634.7826086956522" y="-321.42857142857139"/>
</scene>
<!--Navigation Controller-->
<scene sceneID="UVY-Um-jQm">
<objects>
@ -594,7 +691,7 @@
</scenes>
<resources>
<image name="Globe" width="25" height="25"/>
<image name="Instance Placeholder" width="60" height="60"/>
<image name="Instance Placeholder" width="135" height="135"/>
<image name="Logo" width="400" height="400"/>
<image name="Timelines" width="25" height="25"/>
<image name="second" width="30" height="30"/>

77
elpha-ios/Date+TimeAgo.swift

@ -0,0 +1,77 @@
//
// Date+TimeAgo.swift
// elpha-ios
//
// Created by Dwayne Harris on 10/1/18.
// Copyright © 2018 Elpha. All rights reserved.
//
import Foundation
extension Date {
func timeAgo() -> String {
let calendar = Calendar.current
let now = Date()
let earliest = self < now ? self : now
let latest = self > now ? self : now
let unitFlags: Set<Calendar.Component> = [.second, .minute, .hour, .day, .weekOfMonth, .month, .year]
let components: DateComponents = calendar.dateComponents(unitFlags, from: earliest, to: latest)
if let year = components.year {
if (year >= 2) {
return "\(year) years"
} else if (year >= 1) {
return "Last year"
}
}
if let month = components.month {
if (month >= 2) {
return "\(month) months"
} else if (month >= 1) {
return "Last month"
}
}
if let weekOfMonth = components.weekOfMonth {
if (weekOfMonth >= 2) {
return "\(weekOfMonth) weeks"
} else if (weekOfMonth >= 1) {
return "Last week"
}
}
if let day = components.day {
if (day >= 2) {
return "\(day) days"
} else if (day >= 1) {
return "Yesterday"
}
}
if let hour = components.hour {
if (hour >= 2) {
return "\(hour) hours"
} else if (hour >= 1) {
return "An hour ago"
}
}
if let minute = components.minute {
if (minute >= 2) {
return "\(minute) mins"
} else if (minute >= 1) {
return "A minute ago"
}
}
if let second = components.second {
if (second >= 3) {
return "\(second) secs"
}
}
return "Just now"
}
}

75
elpha-ios/Elpha.xcdatamodeld/Elpha.xcdatamodel/contents

@ -2,7 +2,6 @@
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="14315.18" systemVersion="18A391" minimumToolsVersion="Automatic" sourceLanguage="Swift" userDefinedModelVersionIdentifier="">
<entity name="Account" representedClassName="AccountMO" syncable="YES" codeGenerationType="class">
<attribute name="acct" attributeType="String" syncable="YES"/>
<attribute name="avatarData" optional="YES" attributeType="Binary" syncable="YES"/>
<attribute name="avatarStaticURL" optional="YES" attributeType="URI" syncable="YES"/>
<attribute name="avatarURL" optional="YES" attributeType="URI" syncable="YES"/>
<attribute name="bot" optional="YES" attributeType="Boolean" usesScalarValueType="YES" syncable="YES"/>
@ -10,7 +9,6 @@
<attribute name="displayName" attributeType="String" syncable="YES"/>
<attribute name="followersCount" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES" syncable="YES"/>
<attribute name="followingCount" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES" syncable="YES"/>
<attribute name="headerData" optional="YES" attributeType="Binary" syncable="YES"/>
<attribute name="headerStaticURL" optional="YES" attributeType="URI" syncable="YES"/>
<attribute name="headerURL" optional="YES" attributeType="URI" syncable="YES"/>
<attribute name="id" attributeType="String" syncable="YES"/>
@ -21,13 +19,29 @@
<attribute name="url" attributeType="String" syncable="YES"/>
<attribute name="username" attributeType="String" syncable="YES"/>
<relationship name="sessions" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Session" inverseName="account" inverseEntity="Session" syncable="YES"/>
<relationship name="statuses" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="Status" inverseName="account" inverseEntity="Status" syncable="YES"/>
<relationship name="timelines" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="Timeline" inverseName="account" inverseEntity="Timeline" syncable="YES"/>
</entity>
<entity name="App" representedClassName="AppMO" syncable="YES" codeGenerationType="class">
<attribute name="name" attributeType="String" syncable="YES"/>
<attribute name="website" optional="YES" attributeType="URI" syncable="YES"/>
<relationship name="statuses" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="Status" inverseName="app" inverseEntity="Status" syncable="YES"/>
</entity>
<entity name="Attachment" representedClassName="AttachmentMO" syncable="YES" codeGenerationType="class">
<attribute name="id" attributeType="String" syncable="YES"/>
<attribute name="previewURL" attributeType="URI" syncable="YES"/>
<attribute name="remoteURL" optional="YES" attributeType="String" syncable="YES"/>
<attribute name="textURL" optional="YES" attributeType="String" syncable="YES"/>
<attribute name="type" attributeType="String" syncable="YES"/>
<attribute name="url" attributeType="URI" syncable="YES"/>
<relationship name="status" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Status" inverseName="attachments" inverseEntity="Status" syncable="YES"/>
</entity>
<entity name="Client" representedClassName="ClientMO" syncable="YES" codeGenerationType="class">
<attribute name="clientID" attributeType="String" syncable="YES"/>
<attribute name="clientSecret" attributeType="String" syncable="YES"/>
<attribute name="id" attributeType="String" syncable="YES"/>
<attribute name="url" attributeType="String" syncable="YES"/>
<relationship name="sessions" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Session" inverseName="app" inverseEntity="Session" syncable="YES"/>
<relationship name="sessions" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Session" inverseName="client" inverseEntity="Session" syncable="YES"/>
</entity>
<entity name="ISCategory" representedClassName="ISCategoryMO" syncable="YES" codeGenerationType="class">
<attribute name="id" attributeType="String" syncable="YES"/>
@ -75,20 +89,67 @@
</uniquenessConstraint>
</uniquenessConstraints>
</entity>
<entity name="Mention" representedClassName="MentionMO" syncable="YES" codeGenerationType="class">
<attribute name="acct" attributeType="String" syncable="YES"/>
<attribute name="id" attributeType="String" syncable="YES"/>
<attribute name="url" attributeType="URI" syncable="YES"/>
<attribute name="username" attributeType="String" syncable="YES"/>
<relationship name="status" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Status" inverseName="mentions" inverseEntity="Status" syncable="YES"/>
</entity>
<entity name="Session" representedClassName="SessionMO" syncable="YES" codeGenerationType="class">
<attribute name="color" optional="YES" attributeType="String" syncable="YES"/>
<attribute name="createdAt" attributeType="Date" usesScalarValueType="NO" syncable="YES"/>
<attribute name="order" attributeType="Integer 16" defaultValueString="0" usesScalarValueType="YES" syncable="YES"/>
<attribute name="selected" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/>
<relationship name="account" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Account" inverseName="sessions" inverseEntity="Account" syncable="YES"/>
<relationship name="app" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="App" inverseName="sessions" inverseEntity="App" syncable="YES"/>
<relationship name="client" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Client" inverseName="sessions" inverseEntity="Client" syncable="YES"/>
<relationship name="selectedTimeline" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Timeline" inverseName="session" inverseEntity="Timeline" syncable="YES"/>
</entity>
<entity name="Status" representedClassName="StatusMO" syncable="YES" codeGenerationType="class">
<attribute name="content" optional="YES" attributeType="String" syncable="YES"/>
<attribute name="createdAt" attributeType="Date" usesScalarValueType="NO" syncable="YES"/>
<attribute name="favourited" optional="YES" attributeType="Boolean" usesScalarValueType="YES" syncable="YES"/>
<attribute name="favouritesCount" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES" syncable="YES"/>
<attribute name="id" attributeType="String" syncable="YES"/>
<attribute name="inReplyToAccountID" optional="YES" attributeType="String" syncable="YES"/>
<attribute name="inReplyToID" optional="YES" attributeType="String" syncable="YES"/>
<attribute name="reblogged" optional="YES" attributeType="Boolean" usesScalarValueType="YES" syncable="YES"/>
<attribute name="reblogsCount" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES" syncable="YES"/>
<attribute name="sensitive" optional="YES" attributeType="Boolean" usesScalarValueType="YES" syncable="YES"/>
<attribute name="spoilerText" optional="YES" attributeType="String" syncable="YES"/>
<attribute name="uri" attributeType="URI" syncable="YES"/>
<attribute name="url" optional="YES" attributeType="URI" syncable="YES"/>
<attribute name="visibility" optional="YES" attributeType="String" syncable="YES"/>
<relationship name="account" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Account" inverseName="statuses" inverseEntity="Account" syncable="YES"/>
<relationship name="app" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="App" inverseName="statuses" inverseEntity="App" syncable="YES"/>
<relationship name="attachments" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="Attachment" inverseName="status" inverseEntity="Attachment" syncable="YES"/>
<relationship name="mentions" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="Mention" inverseName="status" inverseEntity="Mention" syncable="YES"/>
<relationship name="tags" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="Tag" inverseName="statuses" inverseEntity="Tag" syncable="YES"/>
<relationship name="timelines" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="Timeline" inverseName="statuses" inverseEntity="Timeline" syncable="YES"/>
</entity>
<entity name="Tag" representedClassName="TagMO" syncable="YES" codeGenerationType="class">
<attribute name="name" attributeType="String" syncable="YES"/>
<attribute name="url" attributeType="URI" syncable="YES"/>
<relationship name="statuses" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="Status" inverseName="tags" inverseEntity="Status" syncable="YES"/>
</entity>
<entity name="Timeline" representedClassName="TimelineMO" syncable="YES" codeGenerationType="class">
<attribute name="name" attributeType="String" syncable="YES"/>
<relationship name="account" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Account" inverseName="timelines" inverseEntity="Account" syncable="YES"/>
<relationship name="session" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Session" inverseName="selectedTimeline" inverseEntity="Session" syncable="YES"/>
<relationship name="statuses" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="Status" inverseName="timelines" inverseEntity="Status" syncable="YES"/>
</entity>
<elements>
<element name="Account" positionX="-669.49609375" positionY="52.046875" width="128" height="30"/>
<element name="App" positionX="-244.4140625" positionY="307.203125" width="128" height="120"/>
<element name="Account" positionX="-669.49609375" positionY="52.046875" width="128" height="345"/>
<element name="App" positionX="-423" positionY="252" width="128" height="90"/>
<element name="Attachment" positionX="-450" positionY="225" width="128" height="150"/>
<element name="Client" positionX="-244.4140625" positionY="307.203125" width="128" height="120"/>
<element name="ISCategory" positionX="196.8984375" positionY="498.03515625" width="128" height="75"/>
<element name="ISInstance" positionX="-18" positionY="153" width="128" height="435"/>
<element name="ISLanguage" positionX="-286.21875" positionY="512.6171875" width="128" height="75"/>
<element name="Session" positionX="-445.046875" positionY="277.31640625" width="128" height="135"/>
<element name="Mention" positionX="-441" positionY="234" width="128" height="120"/>
<element name="Session" positionX="-445.046875" positionY="277.31640625" width="128" height="150"/>
<element name="Status" positionX="-459" positionY="216" width="128" height="345"/>
<element name="Tag" positionX="-432" positionY="243" width="128" height="90"/>
<element name="Timeline" positionX="-468" positionY="207" width="128" height="105"/>
</elements>
</model>

6
elpha-ios/InstancesDataManager.swift

@ -27,14 +27,13 @@ class InstancesDataManager {
request.predicate = NSPredicate(format: "id == %@", string)
do {
let results: [ISLanguageMO] = try context.fetch(request)
let results = try context.fetch(request)
if let language = results.first {
return language
} else {
let language = ISLanguageMO(context: context)
language.id = string
CoreDataManager.shared.saveContext()
return language
}
} catch {
@ -51,14 +50,13 @@ class InstancesDataManager {
request.predicate = NSPredicate(format: "id == %@", string)
do {
let results: [ISCategoryMO] = try context.fetch(request)
let results = try context.fetch(request)
if let category = results.first {
return category
} else {
let category = ISCategoryMO(context: context)
category.id = string
CoreDataManager.shared.saveContext()
return category
}
} catch {

8
elpha-ios/InstancesTableViewCell.swift

@ -15,12 +15,4 @@ class InstancesTableViewCell: UITableViewCell {
@IBOutlet var instanceNameLabel: UILabel!
@IBOutlet var statusesLabel: UILabel!
@IBOutlet var usersLabel: UILabel!
var thumbnailURL: URL? {
didSet {
if let thumbnailURL = thumbnailURL {
thumbnailImageView.af_setImage(withURL: thumbnailURL)
}
}
}
}

12
elpha-ios/InstancesTableViewController.swift

@ -43,10 +43,10 @@ class InstancesTableViewController: UITableViewController, UIViewControllerPrevi
mainNavigationItem.leftBarButtonItem = UIBarButtonItem(barButtonSystemItem: .done, target: self, action: #selector(done))
registerForPreviewing(with: self, sourceView: tableView)
refreshControl?.addTarget(self, action: #selector(self.reloadInstances), for: .valueChanged)
refreshControl?.addTarget(self, action: #selector(self.refetchInstances), for: .valueChanged)
if instanceCount == 0 {
loadInstances()
fetchInstances()
}
}
@ -54,7 +54,7 @@ class InstancesTableViewController: UITableViewController, UIViewControllerPrevi
dismiss(animated: true, completion: nil)
}
@objc func reloadInstances() {
@objc func refetchInstances() {
loading = true
DispatchQueue.main.async {
@ -75,7 +75,7 @@ class InstancesTableViewController: UITableViewController, UIViewControllerPrevi
}
}
func loadInstances() {
func fetchInstances() {
loading = true
InstancesDataManager.shared.loadInstances() { error in
@ -130,11 +130,11 @@ class InstancesTableViewController: UITableViewController, UIViewControllerPrevi
cell.usersLabel.text = NumberFormatter.localizedString(from: NSNumber(value: instance.users), number: .decimal)
if let thumbnail = instance.thumbnail {
cell.thumbnailURL = thumbnail
cell.thumbnailImageView.af_setImage(withURL: thumbnail)
}
if indexPath.row == instances.count - 1 && !loading && !InstancesDataManager.shared.finished {
loadInstances()
fetchInstances()
}
return cell

231
elpha-ios/MastodonDataManager.swift

@ -0,0 +1,231 @@
//
// MastodonDataManager.swift
// elpha-ios
//
// Created by Dwayne Harris on 10/1/18.
// Copyright © 2018 Elpha. All rights reserved.
//
import CoreData
import Foundation
import MastodonKit
public class MastodonDataManager {
static func upsertAccount(_ remoteAccount: Account) -> AccountMO? {
func saveAccount(_ account: AccountMO) -> AccountMO? {
account.id = remoteAccount.id
account.username = remoteAccount.username
account.acct = remoteAccount.acct
account.displayName = remoteAccount.displayName
account.note = remoteAccount.note
account.url = remoteAccount.url
account.avatarURL = URL(string: remoteAccount.avatar)
account.avatarStaticURL = URL(string: remoteAccount.avatarStatic)
account.headerURL = URL(string: remoteAccount.header)
account.headerStaticURL = URL(string: remoteAccount.headerStatic)
account.locked = remoteAccount.locked
account.createdAt = remoteAccount.createdAt
account.followersCount = Int32(remoteAccount.followersCount)
account.followingCount = Int32(remoteAccount.followingCount)
account.statusesCount = Int32(remoteAccount.statusesCount)
return account
}
let context = CoreDataManager.shared.getContext()
let request = NSFetchRequest<AccountMO>(entityName: "Account")
request.predicate = NSPredicate(format: "acct == %@", remoteAccount.acct)
do {
let results = try context.fetch(request)
if let account = results.first {
return saveAccount(account)
} else {
return saveAccount(AccountMO(context: context))
}
} catch {
print("\(error)")
return nil
}
}
static func upsertAttachment(_ remoteAttachment: Attachment) -> AttachmentMO? {
func saveAttachment(_ attachment: AttachmentMO) -> AttachmentMO? {
attachment.id = remoteAttachment.id
attachment.type = remoteAttachment.type.rawValue
attachment.url = URL(string: remoteAttachment.url)
attachment.previewURL = URL(string: remoteAttachment.previewURL)
attachment.remoteURL = remoteAttachment.remoteURL
attachment.textURL = remoteAttachment.textURL
return attachment
}
let context = CoreDataManager.shared.getContext()
let request = NSFetchRequest<AttachmentMO>(entityName: "Attachment")
request.predicate = NSPredicate(format: "id == %@", remoteAttachment.id)
do {
let results = try context.fetch(request)
if let attachment = results.first {
return saveAttachment(attachment)
} else {
return saveAttachment(AttachmentMO(context: context))
}
} catch {
print("\(error)")
return nil
}
}
static func upsertMention(_ remoteMention: Mention) -> MentionMO? {
func saveMention(_ mention: MentionMO) -> MentionMO? {
mention.id = remoteMention.id
mention.username = remoteMention.username
mention.acct = remoteMention.acct
mention.url = URL(string: remoteMention.url)
return mention
}
let context = CoreDataManager.shared.getContext()
let request = NSFetchRequest<MentionMO>(entityName: "Mention")
request.predicate = NSPredicate(format: "id == %@", remoteMention.id)
do {
let results = try context.fetch(request)
if let mention = results.first {
return saveMention(mention)
} else {
return saveMention(MentionMO(context: context))
}
} catch {
print("\(error)")
return nil
}
}
static func upsertTag(_ remoteTag: Tag) -> TagMO? {
func saveTag(_ tag: TagMO) -> TagMO? {
tag.name = remoteTag.name
tag.url = URL(string: remoteTag.url)
return tag
}
let context = CoreDataManager.shared.getContext()
let request = NSFetchRequest<TagMO>(entityName: "Tag")
request.predicate = NSPredicate(format: "name == %@", remoteTag.name)
do {
let results = try context.fetch(request)
if let tag = results.first {
return saveTag(tag)
} else {
return saveTag(TagMO(context: context))
}
} catch {
print("\(error)")
return nil
}
}
static func upsertApp(_ remoteApp: Application) -> AppMO? {
func saveApp(_ app: AppMO) -> AppMO? {
app.name = remoteApp.name
if let website = remoteApp.website {
app.website = URL(string: website)
}
return app
}
let context = CoreDataManager.shared.getContext()
let request = NSFetchRequest<AppMO>(entityName: "App")
request.predicate = NSPredicate(format: "name == %@", remoteApp.name)
do {
let results = try context.fetch(request)
if let tag = results.first {
return saveApp(tag)
} else {
return saveApp(AppMO(context: context))
}
} catch {
print("\(error)")
return nil
}
}
static func upsertStatus(remoteStatus: Status) -> StatusMO? {
func saveStatus(_ status: StatusMO) throws -> StatusMO? {
status.id = remoteStatus.id
status.uri = URL(string: remoteStatus.uri)
status.url = remoteStatus.url
status.account = MastodonDataManager.upsertAccount(remoteStatus.account)
status.inReplyToID = remoteStatus.inReplyToID
status.inReplyToAccountID = remoteStatus.inReplyToAccountID
status.content = remoteStatus.content
status.createdAt = remoteStatus.createdAt
status.reblogsCount = Int32(remoteStatus.reblogsCount)
status.favouritesCount = Int32(remoteStatus.favouritesCount)
status.reblogged = remoteStatus.reblogged ?? false
status.favourited = remoteStatus.favourited ?? false
status.sensitive = remoteStatus.sensitive ?? false
status.spoilerText = remoteStatus.spoilerText
status.visibility = remoteStatus.visibility.rawValue
if let app = remoteStatus.application {
status.app = MastodonDataManager.upsertApp(app)
}
remoteStatus.mediaAttachments.forEach { attachment in
if let attachment = MastodonDataManager.upsertAttachment(attachment) {
status.mutableSetValue(forKey: "attachments").add(attachment)
}
}
remoteStatus.mentions.forEach { mention in
if let mention = MastodonDataManager.upsertMention(mention) {
status.mutableSetValue(forKey: "mentions").add(mention)
}
}
remoteStatus.tags.forEach { tag in
if let tag = MastodonDataManager.upsertTag(tag) {
status.mutableSetValue(forKey: "tags").add(tag)
}
}
return status
}
let context = CoreDataManager.shared.getContext()
let request = NSFetchRequest<StatusMO>(entityName: "Status")
request.predicate = NSPredicate(format: "id == %@", remoteStatus.id)
do {
let results = try context.fetch(request)
if let status = results.first {
return try saveStatus(status)
} else {
return try saveStatus(StatusMO(context: context))
}
} catch {
print("\(error)")
return nil
}
}
static func saveClient(id: String, clientID: String, clientSecret: String, url: String) -> ClientMO {
let client = ClientMO(context: CoreDataManager.shared.getContext())
client.id = id
client.clientID = clientID
client.clientSecret = clientSecret
client.url = url
CoreDataManager.shared.saveContext()
return client
}
}

18
elpha-ios/TimelineTableViewCell.swift

@ -0,0 +1,18 @@
//
// TimelineTableViewCell.swift
// elpha-ios
//
// Created by Dwayne Harris on 10/1/18.
// Copyright © 2018 Elpha. All rights reserved.
//
import Foundation
import UIKit
class TimelineTableViewCell: UITableViewCell {
@IBOutlet var avatarImageView: UIImageView!
@IBOutlet var displayNameLabel: UILabel!
@IBOutlet var acctLabel: UILabel!
@IBOutlet var timestampLabel: UILabel!
@IBOutlet var contentLabel: UILabel!
}

210
elpha-ios/TimelineTableViewController.swift

@ -0,0 +1,210 @@
//
// TimelineTableViewController.swift
// elpha-ios
//
// Created by Dwayne Harris on 10/1/18.
// Copyright © 2018 Elpha. All rights reserved.
//
import AlamofireImage
import CoreData
import Foundation
import MastodonKit
import UIKit
class TimelineTableViewController: UITableViewController {
var loading: Bool = false {
didSet {
DispatchQueue.main.async {
if self.loading {
UIApplication.shared.isNetworkActivityIndicatorVisible = true
self.refreshControl?.beginRefreshing()
} else {
UIApplication.shared.isNetworkActivityIndicatorVisible = false
self.refreshControl?.endRefreshing()
}
}
}
}
override func viewDidLoad() {
super.viewDidLoad()
refreshControl?.addTarget(self, action: #selector(self.fetchTimeline), for: .valueChanged)
let statusesCount = getTimelineStatusesCount()
fetchTimeline()
if statusesCount == 0 {
self.tableView.reloadData()
} else {
print("More data to load...")
self.tableView.reloadData()
}
}
func createDefaultTimelines(account: AccountMO) {
let context = CoreDataManager.shared.getContext()
let timelineNames = [
"Home",
"Local",
"Federated",
]
timelineNames.forEach { timelineName in
let timeline = TimelineMO(context: context)
timeline.name = timelineName
timeline.account = account
}
CoreDataManager.shared.saveContext()
}
func fetchStatuses(request: Request<[Status]>, forTimeline timeline: TimelineMO) {
if let client = AuthenticationManager.shared.getMKClientForSelectedSession() {
client.run(request) { result in
switch result {
case .success(let remoteStatuses, _):
let statuses = remoteStatuses.compactMap { status in
return MastodonDataManager.upsertStatus(remoteStatus: status)
}
timeline.addToStatuses(NSSet(array: statuses))
CoreDataManager.shared.saveContext()
case .failure(let error):
print("\(error)")
}
}
}
}
@objc func fetchTimeline() {
guard let session = AuthenticationManager.shared.selectedSession, let account = session.account else {
return
}
if session.selectedTimeline == nil {
if let timelines = account.timelines, timelines.count == 0 {
createDefaultTimelines(account: account)
}
let request = NSFetchRequest<TimelineMO>(entityName: "Timeline")
request.predicate = NSPredicate(format: "name == %@", "Home")
do {
let results = try CoreDataManager.shared.getContext().fetch(request)
session.selectedTimeline = results.first
CoreDataManager.shared.saveContext()
} catch {
print("\(error)")
}
}
loading = true
defer {
loading = false
}
if let selectedTimeline = session.selectedTimeline {
switch selectedTimeline.name {
case "Home":
fetchStatuses(request: Timelines.home(), forTimeline: selectedTimeline)
case "Local":
fetchStatuses(request: Timelines.public(local: true), forTimeline: selectedTimeline)
case "Federated":
fetchStatuses(request: Timelines.public(local: false), forTimeline: selectedTimeline)
default:
print("Tags not implemented")
}
}
}
func getTimelineRequest() -> NSFetchRequest<TimelineMO>? {
guard let session = AuthenticationManager.shared.selectedSession, let selectedTimeline = session.selectedTimeline else {
return nil
}
let request = NSFetchRequest<TimelineMO>(entityName: "Timeline")
request.predicate = NSPredicate(format: "name == %@", selectedTimeline.name!)
return request
}
func getTimelineStatuses() -> [StatusMO]? {
guard let request = getTimelineRequest() else {
return []
}
do {
let response = try CoreDataManager.shared.getContext().fetch(request)
guard let timeline = response.first, let statuses = timeline.statuses else {
return []
}
let sortDescriptor = NSSortDescriptor(key: "createdAt", ascending: false)
return statuses.sortedArray(using: [sortDescriptor]) as? [StatusMO]
} catch {
print("\(error)")
return []
}
}
func getTimelineStatusesCount() -> Int {
guard let statuses = getTimelineStatuses() else {
return 0
}
return statuses.count
}
override func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return getTimelineStatusesCount()
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
guard let cell = tableView.dequeueReusableCell(withIdentifier: "TimelineTableViewCell", for: indexPath) as? TimelineTableViewCell else {
fatalError("Unable to find reusable cell")
}
if let statuses = getTimelineStatuses() {
let status = statuses[indexPath.row]
if let account = status.account {
let filter = AspectScaledToFillSizeWithRoundedCornersFilter(
size: CGSize(width: 40.0, height: 40.0),
radius: 20.0,
divideRadiusByImageScale: true
)
cell.avatarImageView.af_setImage(withURL: account.avatarURL!, filter: filter)
cell.displayNameLabel.text = account.displayName
cell.acctLabel.text = account.acct
}
if let content = status.content {
do {
let styledContent = "<style>html * { font-size: 16px; color: #170c49; font-family: -apple-system }</style> \(content)"
let attributedText = try NSAttributedString(
data: styledContent.data(using: String.Encoding.unicode, allowLossyConversion: true)!,
options: [.documentType: NSAttributedString.DocumentType.html],
documentAttributes: nil
)
cell.contentLabel.attributedText = attributedText
} catch {
print("\(error)")
}
}
cell.timestampLabel.text = status.createdAt!.timeAgo()
return cell
}
return cell
}
}
Loading…
Cancel
Save