Add full-server activity to iOS home (#65)
This commit is contained in:
@@ -683,6 +683,8 @@ public protocol GotchaCoreProtocol: AnyObject, Sendable {
|
||||
|
||||
func selectServer(index: UInt32) throws
|
||||
|
||||
func serverActivity(page: UInt32) async throws -> ServerActivityPage
|
||||
|
||||
func serverEditor(index: UInt32) throws -> ServerEditor
|
||||
|
||||
func servers() -> [ServerRow]
|
||||
@@ -1348,6 +1350,22 @@ open func selectServer(index: UInt32)throws {try rustCallWithError(FfiConverte
|
||||
}
|
||||
}
|
||||
|
||||
open func serverActivity(page: UInt32)async throws -> ServerActivityPage {
|
||||
return
|
||||
try await uniffiRustCallAsync(
|
||||
rustFutureFunc: {
|
||||
uniffi_gotcha_core_fn_method_gotchacore_server_activity(
|
||||
self.uniffiCloneHandle(),FfiConverterUInt32.lower(page)
|
||||
)
|
||||
},
|
||||
pollFunc: ffi_gotcha_core_rust_future_poll_rust_buffer,
|
||||
completeFunc: ffi_gotcha_core_rust_future_complete_rust_buffer,
|
||||
freeFunc: ffi_gotcha_core_rust_future_free_rust_buffer,
|
||||
liftFunc: FfiConverterTypeServerActivityPage_lift,
|
||||
errorHandler: FfiConverterTypeGotchaError_lift
|
||||
)
|
||||
}
|
||||
|
||||
open func serverEditor(index: UInt32)throws -> ServerEditor {
|
||||
return try FfiConverterTypeServerEditor_lift(try rustCallWithError(FfiConverterTypeGotchaError_lift) {
|
||||
uniffiCallStatus in
|
||||
@@ -3709,6 +3727,60 @@ public func FfiConverterTypeRepositoryRow_lower(_ value: RepositoryRow) -> RustB
|
||||
}
|
||||
|
||||
|
||||
public struct ServerActivityPage: Equatable, Hashable {
|
||||
public var rows: [ActivityRow]
|
||||
public var hasMore: Bool
|
||||
|
||||
// Default memberwise initializers are never public by default, so we
|
||||
// declare one manually.
|
||||
public init(rows: [ActivityRow], hasMore: Bool) {
|
||||
self.rows = rows
|
||||
self.hasMore = hasMore
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
#if compiler(>=6)
|
||||
extension ServerActivityPage: Sendable {}
|
||||
#endif
|
||||
|
||||
#if swift(>=5.8)
|
||||
@_documentation(visibility: private)
|
||||
#endif
|
||||
public struct FfiConverterTypeServerActivityPage: FfiConverterRustBuffer {
|
||||
public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> ServerActivityPage {
|
||||
return
|
||||
try ServerActivityPage(
|
||||
rows: FfiConverterSequenceTypeActivityRow.read(from: &buf),
|
||||
hasMore: FfiConverterBool.read(from: &buf)
|
||||
)
|
||||
}
|
||||
|
||||
public static func write(_ value: ServerActivityPage, into buf: inout [UInt8]) {
|
||||
FfiConverterSequenceTypeActivityRow.write(value.rows, into: &buf)
|
||||
FfiConverterBool.write(value.hasMore, into: &buf)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#if swift(>=5.8)
|
||||
@_documentation(visibility: private)
|
||||
#endif
|
||||
public func FfiConverterTypeServerActivityPage_lift(_ buf: RustBuffer) throws -> ServerActivityPage {
|
||||
return try FfiConverterTypeServerActivityPage.lift(buf)
|
||||
}
|
||||
|
||||
#if swift(>=5.8)
|
||||
@_documentation(visibility: private)
|
||||
#endif
|
||||
public func FfiConverterTypeServerActivityPage_lower(_ value: ServerActivityPage) -> RustBuffer {
|
||||
return FfiConverterTypeServerActivityPage.lower(value)
|
||||
}
|
||||
|
||||
|
||||
public struct ServerEditor: Equatable, Hashable {
|
||||
public var name: String
|
||||
public var url: String
|
||||
@@ -5589,6 +5661,9 @@ private let initializationResult: InitializationResult = {
|
||||
if (uniffi_gotcha_core_checksum_method_gotchacore_select_server() != 10204) {
|
||||
return InitializationResult.apiChecksumMismatch
|
||||
}
|
||||
if (uniffi_gotcha_core_checksum_method_gotchacore_server_activity() != 35959) {
|
||||
return InitializationResult.apiChecksumMismatch
|
||||
}
|
||||
if (uniffi_gotcha_core_checksum_method_gotchacore_server_editor() != 36193) {
|
||||
return InitializationResult.apiChecksumMismatch
|
||||
}
|
||||
|
||||
@@ -458,6 +458,11 @@ uint64_t uniffi_gotcha_core_fn_method_gotchacore_home(uint64_t ptr, uint32_t pag
|
||||
void uniffi_gotcha_core_fn_method_gotchacore_select_server(uint64_t ptr, uint32_t index, RustCallStatus *_Nonnull out_status
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_UNIFFI_GOTCHA_CORE_FN_METHOD_GOTCHACORE_SERVER_ACTIVITY
|
||||
#define UNIFFI_FFIDEF_UNIFFI_GOTCHA_CORE_FN_METHOD_GOTCHACORE_SERVER_ACTIVITY
|
||||
uint64_t uniffi_gotcha_core_fn_method_gotchacore_server_activity(uint64_t ptr, uint32_t page
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_UNIFFI_GOTCHA_CORE_FN_METHOD_GOTCHACORE_SERVER_EDITOR
|
||||
#define UNIFFI_FFIDEF_UNIFFI_GOTCHA_CORE_FN_METHOD_GOTCHACORE_SERVER_EDITOR
|
||||
RustBuffer uniffi_gotcha_core_fn_method_gotchacore_server_editor(uint64_t ptr, uint32_t index, RustCallStatus *_Nonnull out_status
|
||||
@@ -1011,6 +1016,12 @@ uint16_t uniffi_gotcha_core_checksum_method_gotchacore_home(void
|
||||
#define UNIFFI_FFIDEF_UNIFFI_GOTCHA_CORE_CHECKSUM_METHOD_GOTCHACORE_SELECT_SERVER
|
||||
uint16_t uniffi_gotcha_core_checksum_method_gotchacore_select_server(void
|
||||
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_UNIFFI_GOTCHA_CORE_CHECKSUM_METHOD_GOTCHACORE_SERVER_ACTIVITY
|
||||
#define UNIFFI_FFIDEF_UNIFFI_GOTCHA_CORE_CHECKSUM_METHOD_GOTCHACORE_SERVER_ACTIVITY
|
||||
uint16_t uniffi_gotcha_core_checksum_method_gotchacore_server_activity(void
|
||||
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_UNIFFI_GOTCHA_CORE_CHECKSUM_METHOD_GOTCHACORE_SERVER_EDITOR
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
|
||||
/* Begin PBXBuildFile section */
|
||||
0020E57CC7B0C5CBBF04291C /* HomeScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = D35C7ECBC3EEC8B4659234AE /* HomeScreen.swift */; };
|
||||
1279D8E7C8A76F0D7F45F97E /* ServerActivityScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = C028DA45E7CB9E1CD56754AC /* ServerActivityScreen.swift */; };
|
||||
130339B2D7AEAC791E50140F /* RepositoryDirectoryScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD6BB62D12708255650508B2 /* RepositoryDirectoryScreen.swift */; };
|
||||
1EDCCB5DE286C1DA407F00F1 /* MilestoneEditorViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA582099DD57D35C748EFB02 /* MilestoneEditorViewController.swift */; };
|
||||
25B30DCE869158C89021EFC6 /* DiffScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 41E0F726C9668B50418D9ADF /* DiffScreen.swift */; };
|
||||
@@ -86,6 +87,7 @@
|
||||
9C0921C76676A14A024BA417 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
|
||||
ACD6449327E1A51FCD8E3BD4 /* IssueScreens.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IssueScreens.swift; sourceTree = "<group>"; };
|
||||
BA5F9B46F07F3EB2333CD4DF /* NotificationCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationCoordinator.swift; sourceTree = "<group>"; };
|
||||
C028DA45E7CB9E1CD56754AC /* ServerActivityScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerActivityScreen.swift; sourceTree = "<group>"; };
|
||||
C13A39F3C39C1F353D58C307 /* ServerScreens.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerScreens.swift; sourceTree = "<group>"; };
|
||||
CA582099DD57D35C748EFB02 /* MilestoneEditorViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MilestoneEditorViewController.swift; sourceTree = "<group>"; };
|
||||
D35C7ECBC3EEC8B4659234AE /* HomeScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeScreen.swift; sourceTree = "<group>"; };
|
||||
@@ -140,6 +142,7 @@
|
||||
FD6BB62D12708255650508B2 /* RepositoryDirectoryScreen.swift */,
|
||||
181AE294D07DB4EAC9C9A0FF /* RepositoryFileScreens.swift */,
|
||||
000E810FCB39B9916CA5CAB8 /* RepositoryListScreen.swift */,
|
||||
C028DA45E7CB9E1CD56754AC /* ServerActivityScreen.swift */,
|
||||
C13A39F3C39C1F353D58C307 /* ServerScreens.swift */,
|
||||
8F145F13B8ED83A5AB0009A4 /* SettingsViewController.swift */,
|
||||
F75B3E4FFB9C9992517C4D69 /* Support.swift */,
|
||||
@@ -363,6 +366,7 @@
|
||||
130339B2D7AEAC791E50140F /* RepositoryDirectoryScreen.swift in Sources */,
|
||||
DC155F5DEB86D95FAF709E16 /* RepositoryFileScreens.swift in Sources */,
|
||||
DFB34862C386662D6EE7E6BE /* RepositoryListScreen.swift in Sources */,
|
||||
1279D8E7C8A76F0D7F45F97E /* ServerActivityScreen.swift in Sources */,
|
||||
9B2206DF1263080B9B25B82C /* ServerScreens.swift in Sources */,
|
||||
96C4AFC206A1DBAE3D3E00CE /* SettingsViewController.swift in Sources */,
|
||||
F55A89489B2758D694F3B27D /* Support.swift in Sources */,
|
||||
|
||||
@@ -119,6 +119,13 @@ final class HomeViewController: RefreshingTableViewController {
|
||||
NotificationsViewController(context: self.context),
|
||||
animated: true
|
||||
)
|
||||
},
|
||||
onServerActivity: { [weak self] in
|
||||
guard let self else { return }
|
||||
self.navigationController?.pushViewController(
|
||||
ServerActivityViewController(context: self.context),
|
||||
animated: true
|
||||
)
|
||||
}
|
||||
)
|
||||
} }
|
||||
@@ -148,7 +155,7 @@ final class HomeViewController: RefreshingTableViewController {
|
||||
cell,
|
||||
title: row.title,
|
||||
detail: "\(row.detail)\n\(row.meta)",
|
||||
image: context.symbol(symbolName(for: row.icon))
|
||||
image: context.symbol(activitySymbolName(for: row.icon))
|
||||
)
|
||||
cell.accessoryType = row.target == .none ? .none : .disclosureIndicator
|
||||
return cell
|
||||
@@ -173,16 +180,17 @@ final class HomeViewController: RefreshingTableViewController {
|
||||
: nil
|
||||
}
|
||||
|
||||
private func symbolName(for icon: ActivityIcon) -> String {
|
||||
switch icon {
|
||||
case .pullRequest: return "arrow.triangle.pull"
|
||||
case .issue: return "exclamationmark.circle"
|
||||
case .branch: return "arrow.triangle.branch"
|
||||
case .tag: return "tag"
|
||||
case .push: return "arrow.up.circle"
|
||||
case .release: return "shippingbox"
|
||||
case .repository: return "books.vertical"
|
||||
}
|
||||
}
|
||||
|
||||
func activitySymbolName(for icon: ActivityIcon) -> String {
|
||||
switch icon {
|
||||
case .pullRequest: return "arrow.triangle.pull"
|
||||
case .issue: return "exclamationmark.circle"
|
||||
case .branch: return "arrow.triangle.branch"
|
||||
case .tag: return "tag"
|
||||
case .push: return "arrow.up.circle"
|
||||
case .release: return "shippingbox"
|
||||
case .repository: return "books.vertical"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -194,7 +202,8 @@ final class HeatmapView: UIView {
|
||||
page: HomePage,
|
||||
selectedFilter: Int,
|
||||
onFilter: @escaping (Int) -> Void,
|
||||
onNotifications: @escaping () -> Void
|
||||
onNotifications: @escaping () -> Void,
|
||||
onServerActivity: @escaping () -> Void
|
||||
) {
|
||||
cells = page.heatCells
|
||||
super.init(frame: CGRect(x: 0, y: 0, width: 0, height: 180))
|
||||
@@ -241,6 +250,15 @@ final class HeatmapView: UIView {
|
||||
button.widthAnchor.constraint(equalToConstant: 44).isActive = true
|
||||
filters.addArrangedSubview(button)
|
||||
}
|
||||
let serverActivity = UIButton(
|
||||
type: .custom,
|
||||
primaryAction: UIAction { _ in onServerActivity() }
|
||||
)
|
||||
serverActivity.setImage(UIImage(systemName: "person.3"), for: .normal)
|
||||
serverActivity.tintColor = .secondaryLabel
|
||||
serverActivity.accessibilityLabel = "Server Activity"
|
||||
serverActivity.widthAnchor.constraint(equalToConstant: 44).isActive = true
|
||||
filters.addArrangedSubview(serverActivity)
|
||||
let notifications = UIButton(
|
||||
type: .custom,
|
||||
primaryAction: UIAction { _ in onNotifications() }
|
||||
|
||||
104
ios/Sources/ServerActivityScreen.swift
Normal file
104
ios/Sources/ServerActivityScreen.swift
Normal file
@@ -0,0 +1,104 @@
|
||||
import UIKit
|
||||
|
||||
@MainActor
|
||||
final class ServerActivityViewController: RefreshingTableViewController {
|
||||
private let context: AppContext
|
||||
private var rows: [ActivityRow] = []
|
||||
private var nextPage: UInt32?
|
||||
private var loaded = false
|
||||
|
||||
init(context: AppContext) {
|
||||
self.context = context
|
||||
super.init()
|
||||
title = "Server Activity"
|
||||
}
|
||||
|
||||
@available(*, unavailable)
|
||||
required init?(coder: NSCoder) { fatalError("init(coder:) is not supported") }
|
||||
|
||||
override func viewWillAppear(_ animated: Bool) {
|
||||
super.viewWillAppear(animated)
|
||||
guard !loaded else { return }
|
||||
loadContent(refreshing: false)
|
||||
}
|
||||
|
||||
override func loadContent(refreshing: Bool) {
|
||||
loadPage(1, refreshing: refreshing)
|
||||
}
|
||||
|
||||
override func loadMoreContent() {
|
||||
guard let nextPage else { return }
|
||||
loadPage(nextPage, refreshing: false)
|
||||
}
|
||||
|
||||
private func loadPage(_ requestedPage: UInt32, refreshing: Bool) {
|
||||
if requestedPage == 1 {
|
||||
resetPagination()
|
||||
beginLoading(refreshing: refreshing)
|
||||
}
|
||||
loadingTask?.cancel()
|
||||
loadingTask = Task {
|
||||
do {
|
||||
let page = try await context.core.serverActivity(page: requestedPage)
|
||||
if requestedPage == 1 {
|
||||
rows = page.rows
|
||||
loaded = true
|
||||
} else {
|
||||
rows.append(contentsOf: page.rows)
|
||||
}
|
||||
nextPage = page.hasMore ? requestedPage + 1 : nil
|
||||
finishPagination(hasMore: page.hasMore)
|
||||
updateRows()
|
||||
} catch {
|
||||
if !Task.isCancelled {
|
||||
show(error: error)
|
||||
failPagination()
|
||||
}
|
||||
}
|
||||
if requestedPage == 1 { endLoading() }
|
||||
}
|
||||
}
|
||||
|
||||
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
|
||||
rows.count
|
||||
}
|
||||
|
||||
override func tableView(
|
||||
_ tableView: UITableView,
|
||||
cellForRowAt indexPath: IndexPath
|
||||
) -> UITableViewCell {
|
||||
let cell = tableView.dequeueReusableCell(withIdentifier: "server-activity")
|
||||
?? UITableViewCell(style: .subtitle, reuseIdentifier: "server-activity")
|
||||
let row = rows[indexPath.row]
|
||||
configureTextCell(
|
||||
cell,
|
||||
title: row.title,
|
||||
detail: "\(row.detail)\n\(row.meta)",
|
||||
image: context.symbol(activitySymbolName(for: row.icon))
|
||||
)
|
||||
cell.accessoryType = row.target == .none ? .none : .disclosureIndicator
|
||||
cell.selectionStyle = row.target == .none ? .none : .default
|
||||
return cell
|
||||
}
|
||||
|
||||
override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
|
||||
92
|
||||
}
|
||||
|
||||
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
|
||||
let row = rows[indexPath.row]
|
||||
guard row.target != .none else { return }
|
||||
tableView.deselectRow(at: indexPath, animated: true)
|
||||
context.route(row)
|
||||
}
|
||||
|
||||
private func updateRows() {
|
||||
tableView.reloadData()
|
||||
tableView.backgroundView = rows.isEmpty
|
||||
? EmptyBackgroundView(
|
||||
title: "No server activity",
|
||||
detail: "No activity is visible to this server account."
|
||||
)
|
||||
: nil
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user