Skip to content

Commit

Permalink
Merge pull request #113 from TootSDK/wip/TootStreamUpdates
Browse files Browse the repository at this point in the history
Toot Stream Updates
  • Loading branch information
davidgarywood committed Mar 14, 2023
2 parents c4673f4 + 05f59a4 commit 25683c6
Show file tree
Hide file tree
Showing 18 changed files with 488 additions and 411 deletions.
Expand Up @@ -17,9 +17,9 @@ struct FeedSelectionView: View {

@State private var selection: SelectionOptions = .home

@StateObject var timeLineHomeViewModel = FeedViewModel(streamType: .timeLineHome)
@StateObject var timeLineLocalViewModel = FeedViewModel(streamType: .timeLineLocal)
@StateObject var timeLineFederatedViewModel = FeedViewModel(streamType: .timeLineFederated)
@StateObject var timeLineHomeViewModel = FeedViewModel(streamType: .home)
@StateObject var timeLineLocalViewModel = FeedViewModel(streamType: .local)
@StateObject var timeLineFederatedViewModel = FeedViewModel(streamType: .federated)

var body: some View {
VStack {
Expand Down
Expand Up @@ -56,6 +56,6 @@ struct FeedView: View {

struct FeedView_Previews: PreviewProvider {
static var previews: some View {
FeedView(viewModel: FeedViewModel(streamType: .timeLineHome))
FeedView(viewModel: FeedViewModel(streamType: .home))
}
}
Expand Up @@ -20,9 +20,9 @@ actor FeedViewModel: ObservableObject {
@MainActor private var tootManager: TootManager?
private var lastClient: TootClient?

private var streamType: PostTootStreams
private var streamType: Timeline

public init(streamType: PostTootStreams) {
public init(streamType: Timeline) {
self.streamType = streamType
}

Expand Down
Expand Up @@ -56,7 +56,7 @@ struct PostOperationsView: View {
self.postID = nil
debugPrint(context ?? "")

try await self.tootManager.currentClient?.data.refresh(.timeLineHome)
try await self.tootManager.currentClient?.data.refresh(.home)

self.path.removeLast()
}
Expand Down
78 changes: 39 additions & 39 deletions Examples/vaportoot/Sources/App/Controllers/TootController+me.swift
Expand Up @@ -7,50 +7,50 @@ import TootSDK
import Vapor

extension TootController {
func me(req: Request) async throws -> Response {
guard let client = try await getAuthenticatedClient(req: req) else {
return try await logout(req: req)
func me(req: Request) async throws -> Response {
guard let client = try await getAuthenticatedClient(req: req) else {
return try await logout(req: req)
}

client.debugOn()
guard let account = try? await client.verifyCredentials() else {
throw Abort(.notFound)
}

// if replying to a post, add the content to context
let query = try req.query.decode(MeQuery.self)
var replyText = ""
if let replyPostId = query.replyTo {
let replyToPost = try await client.getPost(id: replyPostId)
replyText = TootHTML.extractAsPlainText(html: replyToPost.displayPost.content) ?? ""
}
let posts = try await client.getTimeline(.home)
let nameWithEmojis = try UniversalRenderer().render(
html: account.displayName, emojis: account.emojis
).wrappedValue
let context = MeContext(
note: account.note,
name: nameWithEmojis,
avatar: account.avatar,
posts: posts.result.map({ PostItem(post: $0) }),
replyText: replyText,
replyId: query.replyTo)
return try await req.view.render(
"user",
context
).encodeResponse(for: req)
}

client.debugOn()
guard let account = try? await client.verifyCredentials() else {
throw Abort(.notFound)
}

// if replying to a post, add the content to context
let query = try req.query.decode(MeQuery.self)
var replyText = ""
if let replyPostId = query.replyTo {
let replyToPost = try await client.getPost(id: replyPostId)
replyText = TootHTML.extractAsPlainText(html: replyToPost.displayPost.content) ?? ""
}
let posts = try await client.getHomeTimeline()
let nameWithEmojis = try UniversalRenderer().render(
html: account.displayName, emojis: account.emojis
).wrappedValue
let context = MeContext(
note: account.note,
name: nameWithEmojis,
avatar: account.avatar,
posts: posts.result.map({ PostItem(post: $0) }),
replyText: replyText,
replyId: query.replyTo)
return try await req.view.render(
"user",
context
).encodeResponse(for: req)
}
}

struct MeContext: Encodable {
var note: String
var name: String
var avatar: String
var posts: [PostItem]
var replyText: String?
var replyId: String?
var note: String
var name: String
var avatar: String
var posts: [PostItem]
var replyText: String?
var replyId: String?
}

struct MeQuery: Content {
var replyTo: String?
var replyTo: String?
}
92 changes: 88 additions & 4 deletions README.md
Expand Up @@ -22,7 +22,7 @@ You can use TootSDK to build a client for Apple operating systems, or Linux with

## Why make TootSDK?

When app developers build apps for Mastodon and the Fediverse, every developer ends up having to solved the same set of problems when it comes to the API and data model.
When app developers build apps for Mastodon and the Fediverse, every developer ends up having to solve the same set of problems when it comes to the API and data model.

[Konstantin](https://m.iamkonstantin.eu/konstantin) and [Dave](https://social.davidgarywood.com/@davidgarywood) decided to share this effort.
TootSDK is a shared Swift Package that any client app can be built on.
Expand Down Expand Up @@ -107,13 +107,96 @@ let accessToken = client.collectToken(returnUrl: url, callbackURI: callbackURI)

We recommend keeping the accessToken somewhere secure, for example the Keychain.

### Accessing a user's home feedfeed
## Usage and key concepts

Once you have your client connected, you're going to want to use it. Our example apps and reference docs will help you get into the nitty gritty of it all, but some key concepts are highlighted here.

<details>
<summary>Accessing a user's timeline</summary>


There are several different types of timeline in TootSDK that you can access, for example their home timeline, the local timeline of their instance, or the federated timeline. These are all enumerated in the `Timeline` enum.

You can retrieve the latest posts (up to 40 on Mastodon) with a call like so:

```swift
let items = try await client.getTimeline(.home)
let posts = items.result
```
TootSDK returns Posts, Accounts, Lists and DomainBblocks as `PagedResult`. In our code, `items` is a PagedResult struct. It contains a property called `result` which will be the type of data request (in this case an array of `Post`).
</details>

<details>
<summary>Paging requests</summary>


Some requests in TootSDK are paging. This means that we can send and receive PagedInfo structs when making these requests. The properties in this struct are:

- maxId (Return results older than ID)
- minId (Return results immediately newer than ID)
- sinceId (Return results newer than ID)

So for example, if we want all posts from the user's home timeline that are newer than post ID 100, we could write:

```swift
let items = try await client.getTimeline(.home, PagedInfo(minId: 100))
let posts = items.result
```

Paged requests also deliver a PagedInfo struct as a property of the `PagedResult` returned, which means you can use that for subsequent requests of the same type.

```swift

var pagedInfo: PagedInfo?
var posts: [Post] = []

func retrievePosts() async {
let items = try await client.getTimeline(.home, pagedInfo)
posts.append(contentsOf: items.result)
self.pagedInfo = items.pagedInfo
}

```

</details>

<details>
<summary>Streaming timelines</summary>


In TootSDK it is possible to subscribe to some types of content with AsyncSequences, a concept we've wrapped up in our `TootStream` object.

```swift
for posts in try await client.data.stream(.home) {
print(posts)
}
```

Underneath the hood, this uses our Paging mechanism. This means that when you ask the client to refresh that stream, it will deliver you new results, from after the ones you requested.

```swift
let posts = try await client.data.stream(.timeLineHome)
client.data.refresh(.home)
```

### Creating an account
You can also pass an initial PagedInfo value to the stream call. For example, to start steaming all posts from the user's home timeline that are newer than post ID 100:

```swift
for posts in try await client.data.stream(.home, PagedInfo(minId: 100) {
```

Some timelines require associated query parameters to configure. Luckily these are associated values that their timeline enumeration require when creating - so you can't miss them!

```swift

for posts in try await client.data.stream(HashtagTimelineQuery(tag: "iOSDev") {
print(posts)
}
```

</details>

<details>
<summary>Creating an account</summary>

* Register the app with the following scopes `["read", "write:accounts"]`.

Expand All @@ -135,6 +218,7 @@ let params = RegisterAccountParams(
username: name, email: email, password: password, agreement: true, locale: "en")
let token = try await client.registerAccount(params: params)
```
</details>

## Further Documentation 📖

Expand Down
14 changes: 0 additions & 14 deletions Sources/TootSDK/Models/LocalTimelineQuery.swift

This file was deleted.

Expand Up @@ -12,3 +12,17 @@ public struct FederatedTimelineQuery: Codable, Sendable {
/// Return only statuses with media attachments
public var onlyMedia: Bool?
}

extension FederatedTimelineQuery: TimelineQuery {

public func getQueryItems() -> [URLQueryItem] {
var queryItems: [URLQueryItem] = []

if let onlyMedia = onlyMedia {
queryItems.append(.init(name: "only_media", value: String(onlyMedia)))
}

return queryItems
}

}
@@ -1,14 +1,11 @@
//
// HashtagTimelineQuery.swift
//
//
// Created by Konstantin on 09/03/2023.
//

import Foundation

/// Specifies the parameters for a hashtag timeline request
public struct HashtagTimelineQuery: Codable, Sendable {

public init(tag: String, anyTags: [String]? = nil, allTags: [String]? = nil, noneTags: [String]? = nil, onlyMedia: Bool? = nil, local: Bool? = nil, remote: Bool? = nil) {
self.tag = tag
self.anyTags = anyTags
Expand Down Expand Up @@ -40,3 +37,35 @@ public struct HashtagTimelineQuery: Codable, Sendable {
/// Return only remote statuses
public var remote: Bool?
}

extension HashtagTimelineQuery: TimelineQuery {

public func getQueryItems() -> [URLQueryItem] {
var queryItems: [URLQueryItem] = []

if let anyTags {
queryItems.append(contentsOf: anyTags.map({ URLQueryItem(name: "any[]", value: $0) }))
}
if let allTags {
queryItems.append(contentsOf: allTags.map({ URLQueryItem(name: "all[]", value: $0) }))
}
if let noneTags {
queryItems.append(contentsOf: noneTags.map({ URLQueryItem(name: "none[]", value: $0) }))
}

if let onlyMedia {
queryItems.append(.init(name: "only_media", value: String(onlyMedia)))
}

if let local {
queryItems.append(.init(name: "local", value: String(local)))
}

if let remote {
queryItems.append(.init(name: "remote", value: String(remote)))
}

return queryItems
}

}
29 changes: 29 additions & 0 deletions Sources/TootSDK/Models/Timeline/LocalTimelineQuery.swift
@@ -0,0 +1,29 @@
// LocalTimelineQuery.swift
// Created by Konstantin on 09/03/2023.

import Foundation

/// Specifies the parameters for a local timeline request
public struct LocalTimelineQuery: Hashable, Codable, Sendable {
public init(onlyMedia: Bool? = nil) {
self.onlyMedia = onlyMedia
}

/// Return only statuses with media attachments
public var onlyMedia: Bool?
}

extension LocalTimelineQuery: TimelineQuery {

public func getQueryItems() -> [URLQueryItem] {
var queryItems: [URLQueryItem] = []

if let onlyMedia {
queryItems.append(.init(name: "only_media", value: String(onlyMedia)))
}

queryItems.append(.init(name: "local", value: String(true)))

return queryItems
}
}

0 comments on commit 25683c6

Please sign in to comment.