Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,8 @@ If you want to test it with me on testflight or think we can get it on the app s
<!-- USAGE EXAMPLES -->
## Usage

In a thread, use **Jump → First Post / Latest Reply** to move through long discussions without repeated scrolling. Hidden posts are skipped.

Use **Settings → Favorites Backup** to export saved thread links and named generals as JSON. Importing a backup adds missing entries while keeping existing names. Backups do not include media files, settings, or reading progress.

Use **Open Link** on the Boards tab to jump to a board by entering its shortcut, such as `/biz/`, or paste a full board or thread URL.
Expand Down
27 changes: 27 additions & 0 deletions swiftchan/Views/Boards/Catalog/Thread/ThreadView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,33 @@ struct ThreadView: View {
.onScrollPhaseChange { _, phase in
if phase == .idle { ThreadReadingStore.shared.flush() }
}
.safeAreaInset(edge: .bottom, spacing: 0) {
if !isSearching, viewModel.searchText.isEmpty, viewModel.searchFilters == SearchFilters(),
readablePostIDs.count > 1 {
HStack {
Spacer()
Menu {
Button("First Post", systemImage: "arrow.up.to.line") {
if let first = readablePostIDs.first { reader.scrollTo(first, anchor: .top) }
}
.accessibilityIdentifier("Jump To First Post")
Button("Latest Reply", systemImage: "arrow.down.to.line") {
if let last = readablePostIDs.last { reader.scrollTo(last, anchor: .bottom) }
}
.accessibilityIdentifier("Jump To Latest Reply")
} label: {
Label("Jump", systemImage: "arrow.up.arrow.down")
.font(.subheadline.weight(.semibold))
.padding(.horizontal, 14)
.padding(.vertical, 10)
.background(.regularMaterial, in: Capsule())
}
.accessibilityLabel("Jump within thread")
.accessibilityIdentifier("Thread Jump Menu")
.padding(12)
}
}
}
.safeAreaInset(edge: .top, spacing: 0) {
if rememberThreadPositions, !isSearching, viewModel.searchText.isEmpty, viewModel.searchFilters == SearchFilters(),
let firstUnread = unreadPostIDs.first {
Expand Down
25 changes: 25 additions & 0 deletions swiftchanUITests/BrowsingUITests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,31 @@ import XCTest

@MainActor
final class BrowsingUITests: XCTestCase {
func testJumpBetweenFirstPostAndLatestReply() {
let app = launchThreadFixture(rememberProgress: false)
openFixtureThread(in: app)
XCTAssertTrue(app.staticTexts["#100"].isHittable)
app.buttons["Thread Jump Menu"].tap()
app.buttons["Jump To Latest Reply"].tap()
XCTAssertTrue(app.staticTexts["#114"].waitForExistence(timeout: 5))
XCTAssertTrue(app.staticTexts["#114"].isHittable)
XCTAssertFalse(app.staticTexts["#100"].isHittable)
attachScreenshot("Jump To Latest Reply", app: app)
app.buttons["Post Options 114"].tap()
app.buttons["Hide Selected Post"].tap()
XCTAssertTrue(app.buttons["Undo Hide"].waitForExistence(timeout: 5))
app.buttons["Thread Jump Menu"].tap()
app.buttons["Jump To First Post"].tap()
XCTAssertTrue(app.staticTexts["#100"].waitForExistence(timeout: 5))
XCTAssertTrue(app.staticTexts["#100"].isHittable)
XCTAssertFalse(app.staticTexts["#114"].isHittable)
app.buttons["Thread Jump Menu"].tap()
app.buttons["Jump To Latest Reply"].tap()
XCTAssertTrue(app.staticTexts["#113"].waitForExistence(timeout: 5))
XCTAssertTrue(app.staticTexts["#113"].isHittable)
XCTAssertFalse(app.staticTexts["#114"].exists)
}

func testFavoritesBackupShowsSavedGeneralAndOpensExport() {
let app = launchApp()
app.buttons["Settings"].tap()
Expand Down
Loading