Skip to content

Commit a6de118

Browse files
authored
Replace the Getting Started guide with a Walkthrough example. (#29)
1 parent 480d2a7 commit a6de118

File tree

4 files changed

+315
-132
lines changed

4 files changed

+315
-132
lines changed

Examples/Package.swift

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
// swift-tools-version: 6.1
2+
// The swift-tools-version declares the minimum version of Swift required to build this package.
3+
4+
import PackageDescription
5+
6+
let package = Package(
7+
name: "MatrixRustComponentsSwiftExamples",
8+
platforms: [.macOS(.v15)],
9+
products: [
10+
.executable(name: "Walkthrough", targets: ["Walkthrough"])
11+
],
12+
dependencies: [
13+
.package(path: "../"), // matrix-rust-components-swift
14+
.package(url: "https://github.com/apple/swift-argument-parser", from: "1.6.1"),
15+
.package(url: "https://github.com/kishikawakatsumi/KeychainAccess", from: "4.2.0")
16+
],
17+
targets: [
18+
.executableTarget(name: "Walkthrough",
19+
dependencies: [
20+
.product(name: "MatrixRustSDK", package: "matrix-rust-components-swift"),
21+
.product(name: "ArgumentParser", package: "swift-argument-parser"),
22+
.product(name: "KeychainAccess", package: "KeychainAccess")
23+
]),
24+
]
25+
)
Lines changed: 287 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,287 @@
1+
import ArgumentParser
2+
import Foundation
3+
import MatrixRustSDK
4+
import KeychainAccess
5+
6+
class Walkthrough {
7+
// MARK: - Step 1
8+
// Authenticate the user.
9+
10+
var client: Client!
11+
12+
func step1Login() async throws -> WalkthroughUser {
13+
let storeID = UUID().uuidString
14+
15+
// Create a client for a particular homeserver.
16+
// Note that we can pass a server name (the second part of a Matrix user ID) instead of the direct URL.
17+
// This allows the SDK to discover the homeserver's well-known configuration for Sliding Sync support.
18+
let client = try await ClientBuilder()
19+
.serverNameOrHomeserverUrl(serverNameOrUrl: "matrix.org")
20+
.sessionPaths(dataPath: URL.sessionData(for: storeID).path(percentEncoded: false),
21+
cachePath: URL.sessionCaches(for: storeID).path(percentEncoded: false))
22+
.slidingSyncVersionBuilder(versionBuilder: .discoverNative)
23+
.build()
24+
25+
// Login using password authentication.
26+
try await client.login(username: "alice", password: "secret", initialDeviceName: nil, deviceId: nil)
27+
28+
self.client = client
29+
30+
// This data should be stored securely in the keychain.
31+
return try WalkthroughUser(session: client.session(), storeID: storeID)
32+
}
33+
34+
// Or, if the user has previously authenticated we can restore their session instead.
35+
36+
func step1Restore(_ walkthroughUser: WalkthroughUser) async throws {
37+
let session = walkthroughUser.session
38+
let sessionID = walkthroughUser.storeID
39+
40+
// Build a client for the homeserver.
41+
let client = try await ClientBuilder()
42+
.sessionPaths(dataPath: URL.sessionData(for: sessionID).path(percentEncoded: false),
43+
cachePath: URL.sessionCaches(for: sessionID).path(percentEncoded: false))
44+
.homeserverUrl(url: session.homeserverUrl)
45+
.build()
46+
47+
// Restore the client using the session.
48+
try await client.restoreSession(session: session)
49+
50+
self.client = client
51+
}
52+
53+
// MARK: - Step 2
54+
// Build the room list.
55+
56+
class AllRoomsListener: RoomListEntriesListener {
57+
/// The user's list of rooms.
58+
var rooms: [Room] = []
59+
60+
func onUpdate(roomEntriesUpdate: [RoomListEntriesUpdate]) {
61+
// Update the user's room list on each update.
62+
for update in roomEntriesUpdate {
63+
switch update {
64+
case .append(let values):
65+
rooms.append(contentsOf: values)
66+
case .clear:
67+
rooms.removeAll()
68+
case .pushFront(let room):
69+
rooms.insert(room, at: 0)
70+
case .pushBack(let room):
71+
rooms.append(room)
72+
case .popFront:
73+
rooms.removeFirst()
74+
case .popBack:
75+
rooms.removeLast()
76+
case .insert(let index, let room):
77+
rooms.insert(room, at: Int(index))
78+
case .set(let index, let room):
79+
rooms[Int(index)] = room
80+
case .remove(let index):
81+
rooms.remove(at: Int(index))
82+
case .truncate(let length):
83+
rooms.removeSubrange(Int(length)..<rooms.count)
84+
case .reset(values: let values):
85+
rooms = values
86+
}
87+
}
88+
}
89+
}
90+
91+
var syncService: SyncService!
92+
var roomListService: RoomListService!
93+
var allRoomsListener: AllRoomsListener!
94+
var roomListEntriesHandle: RoomListEntriesWithDynamicAdaptersResult!
95+
96+
func step2StartSync() async throws {
97+
// Create a sync service which controls the sync loop.
98+
syncService = try await client.syncService().finish()
99+
100+
// Listen to room list updates.
101+
allRoomsListener = AllRoomsListener()
102+
roomListService = syncService.roomListService()
103+
roomListEntriesHandle = try await roomListService.allRooms().entriesWithDynamicAdapters(pageSize: 100, listener: allRoomsListener)
104+
_ = roomListEntriesHandle.controller().setFilter(kind: .all(filters: []))
105+
106+
// Start the sync loop.
107+
await syncService.start()
108+
}
109+
110+
// MARK: - Step 3
111+
// Create a timeline.
112+
113+
class TimelineItemListener: TimelineListener {
114+
/// The loaded items for this room's timeline
115+
var timelineItems: [TimelineItem] = []
116+
117+
func onUpdate(diff: [TimelineDiff]) {
118+
// Update the timeline items on each update.
119+
for update in diff {
120+
switch update {
121+
case .append(let values):
122+
timelineItems.append(contentsOf: values)
123+
case .clear:
124+
timelineItems.removeAll()
125+
case .pushFront(let room):
126+
timelineItems.insert(room, at: 0)
127+
case .pushBack(let room):
128+
timelineItems.append(room)
129+
case .popFront:
130+
timelineItems.removeFirst()
131+
case .popBack:
132+
timelineItems.removeLast()
133+
case .insert(let index, let room):
134+
timelineItems.insert(room, at: Int(index))
135+
case .set(let index, let room):
136+
timelineItems[Int(index)] = room
137+
case .remove(let index):
138+
timelineItems.remove(at: Int(index))
139+
case .truncate(let length):
140+
timelineItems.removeSubrange(Int(length)..<timelineItems.count)
141+
case .reset(values: let values):
142+
timelineItems = values
143+
}
144+
}
145+
}
146+
}
147+
148+
var timeline: Timeline!
149+
var timelineItemsListener: TimelineItemListener!
150+
var timelineHandle: TaskHandle!
151+
152+
func step3LoadRoomTimeline() async throws {
153+
let roomID = "!someroomid:matrix.org"
154+
155+
// Wait for the rooms array to contain the desired room…
156+
while !allRoomsListener.rooms.contains(where: { $0.id() == roomID }) {
157+
try await Task.sleep(for: .milliseconds(250))
158+
}
159+
160+
// Fetch the room from the listener and initialise it's timeline.
161+
let room = allRoomsListener.rooms.first { $0.id() == roomID }!
162+
timeline = try await room.timeline()
163+
164+
// Listen to timeline item updates.
165+
timelineItemsListener = TimelineItemListener()
166+
timelineHandle = await timeline.addListener(listener: timelineItemsListener)
167+
168+
// Wait for the items array to be updated…
169+
while timelineItemsListener.timelineItems.isEmpty {
170+
try await Task.sleep(for: .milliseconds(250))
171+
}
172+
173+
// Get the event contents from an item.
174+
let timelineItem = timelineItemsListener.timelineItems.last!
175+
if case let .msgLike(content: messageEvent) = timelineItem.asEvent()?.content,
176+
case let .message(content: messageContent) = messageEvent.kind {
177+
print(messageContent.body)
178+
}
179+
}
180+
181+
// MARK: - Step 4
182+
// Sending events.
183+
184+
var sendHandle: SendHandle?
185+
186+
func step4SendMessage() async throws {
187+
// Create the message content from a markdown string.
188+
let message = messageEventContentFromMarkdown(md: "Hello, World!")
189+
190+
// Send the message content via the room's timeline (so that we show a local echo).
191+
sendHandle = try await timeline.send(msg: message)
192+
}
193+
}
194+
195+
// MARK: - @main
196+
197+
let applicationID = "org.matrix.swift.walkthrough"
198+
let keychainSessionKey = "WalkthroughUser"
199+
200+
@main
201+
struct WalkthroughCommand: AsyncParsableCommand {
202+
static let configuration = CommandConfiguration(abstract: "A basic example of using Matrix Rust SDK in Swift.")
203+
204+
func run() async throws {
205+
let walkthrough = Walkthrough()
206+
207+
if let walkthroughUser = try loadUserFromKeychain() {
208+
try await walkthrough.step1Restore(walkthroughUser)
209+
} else {
210+
let walkthroughUser = try await walkthrough.step1Login()
211+
try saveUserToKeychain(walkthroughUser)
212+
}
213+
214+
try await walkthrough.step2StartSync()
215+
try await walkthrough.step3LoadRoomTimeline()
216+
try await walkthrough.step4SendMessage()
217+
218+
// Don't exit immediately otherwise the message won't be sent (the await only suspends until the event is queued).
219+
_ = readLine()
220+
}
221+
222+
func saveUserToKeychain(_ walkthroughUser: WalkthroughUser) throws {
223+
let keychainData = try JSONEncoder().encode(walkthroughUser)
224+
let keychain = Keychain(service: applicationID)
225+
try keychain.set(keychainData, key: keychainSessionKey)
226+
}
227+
228+
func loadUserFromKeychain() throws -> WalkthroughUser? {
229+
let keychain = Keychain(service: applicationID)
230+
guard let keychainData = try keychain.getData(keychainSessionKey) else { return nil }
231+
return try JSONDecoder().decode(WalkthroughUser.self, from: keychainData)
232+
}
233+
234+
private func reset() throws {
235+
if let walkthroughUser = try loadUserFromKeychain() {
236+
try? FileManager.default.removeItem(at: .sessionData(for: walkthroughUser.storeID))
237+
try? FileManager.default.removeItem(at: .sessionCaches(for: walkthroughUser.storeID))
238+
let keychain = Keychain(service: applicationID)
239+
try keychain.removeAll()
240+
}
241+
}
242+
}
243+
244+
struct WalkthroughUser: Codable {
245+
let accessToken: String
246+
let refreshToken: String?
247+
let userID: String
248+
let deviceID: String
249+
let homeserverURL: String
250+
let oidcData: String?
251+
let storeID: String
252+
253+
init(session: Session, storeID: String) {
254+
self.accessToken = session.accessToken
255+
self.refreshToken = session.refreshToken
256+
self.userID = session.userId
257+
self.deviceID = session.deviceId
258+
self.homeserverURL = session.homeserverUrl
259+
self.oidcData = session.oidcData
260+
self.storeID = storeID
261+
}
262+
263+
var session: Session {
264+
Session(accessToken: accessToken,
265+
refreshToken: refreshToken,
266+
userId: userID,
267+
deviceId: deviceID,
268+
homeserverUrl: homeserverURL,
269+
oidcData: oidcData,
270+
slidingSyncVersion: .native)
271+
272+
}
273+
}
274+
275+
extension URL {
276+
static func sessionData(for sessionID: String) -> URL {
277+
applicationSupportDirectory
278+
.appending(component: applicationID)
279+
.appending(component: sessionID)
280+
}
281+
282+
static func sessionCaches(for sessionID: String) -> URL {
283+
cachesDirectory
284+
.appending(component: applicationID)
285+
.appending(component: sessionID)
286+
}
287+
}

README.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,9 @@ This repository is a Swift Package for distributing releases of the [Matrix Rust
44

55
## Usage
66

7-
For more information about using the package, please read the [Getting Started](docs/Getting%20Started.md) guide.
7+
A brief walkthrough of the SDK is available in the [Examples](Examples/) directory. You can find more information about the API by browsing the [FFI crate](https://github.com/matrix-org/matrix-rust-sdk/tree/main/bindings/matrix-sdk-ffi/src) in the SDK's repository.
8+
9+
Please note: The Swift components for the Rust SDK are unstable meaning that the API could change at any point.
810

911
## Releasing
1012

0 commit comments

Comments
 (0)