Add paginated panel loading

This commit is contained in:
Georg Bauer
2026-07-31 15:02:21 +02:00
parent 228879da15
commit 33f9eb809c
11 changed files with 1104 additions and 290 deletions

View File

@@ -99,6 +99,20 @@ xcrun simctl launch booted de.rfc1437.gotcha
- [ ] Pull past the top of Home, repository lists, issue lists, pull lists,
commit history, changed files, issue details, pull details, and diffs.
The native refresh control appears, reloads data, and disappears.
- [ ] On accounts/repositories with more than 30 results, Home activity,
repositories, issues, pull requests, milestones, milestone contents,
commit history, and pull-request files initially show one page plus a
**Pull up or tap to load more**
footer. Pull upward past the bottom and verify the footer changes to a
native loading indicator, the next page appends without duplicates, and
the footer disappears after the final page.
- [ ] Tap the load-more footer as an accessibility alternative and repeat while
VoiceOver is running. It announces loading, ignores repeated activation
while a request is active, preserves the current scroll position, and
allows retry after an error.
- [ ] Pull down to refresh after loading multiple pages. The content resets to
the new first page, filtering/branch changes also reset pagination, and
switching History/Files never shows a stale load-more footer.
- [ ] Tap a row, scroll its detail, and use both the navigation-bar Back button
and the left-edge interactive swipe to return.
- [ ] From a list scrolled well away from the top, open a detail and go Back.

View File

@@ -10,12 +10,12 @@ use crate::{
diff,
domain::{
HistoryCommit, HomeData, IssueDetails, IssueDraft, IssueEditorData, MilestoneDetails,
PullDetails, RepositoryData, Server, api_date,
PAGE_SIZE, Page, PullDetails, RepositoryData, Server, api_date,
},
presentation::compact_date,
};
pub async fn load_repositories(server: &Server) -> Result<Vec<RepositoryData>, String> {
pub async fn load_repositories(server: &Server, page: i32) -> Result<Page<RepositoryData>, String> {
let client =
Client::new(&server.url, Some(&server.token)).map_err(|error| error.to_string())?;
let configuration = client.configuration();
@@ -24,36 +24,41 @@ pub async fn load_repositories(server: &Server) -> Result<Vec<RepositoryData>, S
.await
.map_err(|error| error.to_string())?;
let login = user.login.unwrap_or_default();
let repositories = apis::user_api::user_current_list_repos(&configuration, Some(1), Some(100))
.await
.map_err(|error| error.to_string())?;
Ok(repositories
.into_iter()
.filter(|repository| {
repository
.owner
.as_ref()
.and_then(|owner| owner.login.as_deref())
== Some(login.as_str())
})
.filter_map(|repository| {
Some(RepositoryData {
owner: repository.owner?.login?,
name: repository.name?,
description: repository
.description
.filter(|text| !text.is_empty())
.unwrap_or_else(|| "No description".into()),
language: repository
.language
.filter(|text| !text.is_empty())
.unwrap_or_else(|| "Unknown language".into()),
open_issues: repository.open_issues_count.unwrap_or_default(),
updated: compact_date(repository.updated_at.as_deref()),
default_branch: repository.default_branch.unwrap_or_else(|| "main".into()),
let repositories =
apis::user_api::user_current_list_repos(&configuration, Some(page), Some(PAGE_SIZE))
.await
.map_err(|error| error.to_string())?;
let has_more = repositories.len() == PAGE_SIZE as usize;
Ok(Page {
items: repositories
.into_iter()
.filter(|repository| {
repository
.owner
.as_ref()
.and_then(|owner| owner.login.as_deref())
== Some(login.as_str())
})
})
.collect())
.filter_map(|repository| {
Some(RepositoryData {
owner: repository.owner?.login?,
name: repository.name?,
description: repository
.description
.filter(|text| !text.is_empty())
.unwrap_or_else(|| "No description".into()),
language: repository
.language
.filter(|text| !text.is_empty())
.unwrap_or_else(|| "Unknown language".into()),
open_issues: repository.open_issues_count.unwrap_or_default(),
updated: compact_date(repository.updated_at.as_deref()),
default_branch: repository.default_branch.unwrap_or_else(|| "main".into()),
})
})
.collect(),
has_more,
})
}
pub async fn load_issues(
@@ -63,7 +68,8 @@ pub async fn load_issues(
status: &str,
labels: &[String],
milestone: &str,
) -> Result<Vec<models::Issue>, String> {
page: i32,
) -> Result<Page<models::Issue>, String> {
let client =
Client::new(&server.url, Some(&server.token)).map_err(|error| error.to_string())?;
let labels = (!labels.is_empty()).then(|| labels.join(","));
@@ -81,10 +87,33 @@ pub async fn load_issues(
None,
None,
None,
Some(1),
Some(100),
Some(page),
Some(PAGE_SIZE),
)
.await
.map(Page::from_items)
.map_err(|error| error.to_string())
}
pub async fn load_milestones_page(
server: &Server,
owner: &str,
repository: &str,
page: i32,
) -> Result<Page<models::Milestone>, String> {
let client =
Client::new(&server.url, Some(&server.token)).map_err(|error| error.to_string())?;
apis::issue_api::issue_get_milestones_list(
&client.configuration(),
owner,
repository,
Some("all"),
None,
Some(page),
Some(PAGE_SIZE),
)
.await
.map(Page::from_items)
.map_err(|error| error.to_string())
}
@@ -151,6 +180,7 @@ pub async fn load_milestone(
owner: &str,
repository: &str,
id: i64,
page: i32,
) -> Result<MilestoneDetails, String> {
let client =
Client::new(&server.url, Some(&server.token)).map_err(|error| error.to_string())?;
@@ -174,12 +204,14 @@ pub async fn load_milestone(
None,
None,
None,
Some(1),
Some(100),
Some(page),
Some(PAGE_SIZE),
)
};
let (issues, pulls) = tokio::join!(load("issues"), load("pulls"));
let issues = issues.map_err(|error| error.to_string())?;
let mut pulls = pulls.map_err(|error| error.to_string())?;
let has_more = issues.len() == PAGE_SIZE as usize || pulls.len() == PAGE_SIZE as usize;
for pull in &mut pulls {
pull.repository.get_or_insert_with(|| {
Box::new(models::RepositoryMeta {
@@ -191,8 +223,9 @@ pub async fn load_milestone(
}
Ok(MilestoneDetails {
milestone,
issues: issues.map_err(|error| error.to_string())?,
issues,
pulls,
has_more,
})
}
@@ -200,7 +233,8 @@ pub async fn load_pulls(
server: &Server,
status: &str,
milestone: &str,
) -> Result<Vec<models::Issue>, String> {
page: i32,
) -> Result<Page<models::Issue>, String> {
let client =
Client::new(&server.url, Some(&server.token)).map_err(|error| error.to_string())?;
let configuration = client.configuration();
@@ -210,42 +244,35 @@ pub async fn load_pulls(
.map_err(|error| error.to_string())?
.login
.ok_or("The server account has no username.")?;
let mut pulls = Vec::new();
for page in 1.. {
let batch = apis::issue_api::issue_search_issues(
&configuration,
Some(status),
None,
(!milestone.is_empty()).then_some(milestone),
None,
None,
Some("pulls"),
None,
None,
None,
None,
None,
None,
None,
Some(&owner),
None,
Some(page),
Some(100),
)
.await
.map_err(|error| error.to_string())?;
if batch.is_empty() {
break;
}
pulls.extend(batch);
}
Ok(pulls)
apis::issue_api::issue_search_issues(
&configuration,
Some(status),
None,
(!milestone.is_empty()).then_some(milestone),
None,
None,
Some("pulls"),
None,
None,
None,
None,
None,
None,
None,
Some(&owner),
None,
Some(page),
Some(PAGE_SIZE),
)
.await
.map(Page::from_items)
.map_err(|error| error.to_string())
}
pub async fn load_pull_milestones(server: &Server) -> Result<Vec<String>, String> {
let (open, closed) = tokio::try_join!(
load_pulls(server, "open", ""),
load_pulls(server, "closed", "")
load_all_pulls(server, "open"),
load_all_pulls(server, "closed")
)?;
Ok(open
.into_iter()
@@ -256,6 +283,18 @@ pub async fn load_pull_milestones(server: &Server) -> Result<Vec<String>, String
.collect())
}
async fn load_all_pulls(server: &Server, status: &str) -> Result<Vec<models::Issue>, String> {
let mut pulls = Vec::new();
for page in 1.. {
let batch = load_pulls(server, status, "", page).await?;
pulls.extend(batch.items);
if !batch.has_more {
break;
}
}
Ok(pulls)
}
pub async fn load_branches(
server: &Server,
owner: &str,
@@ -336,11 +375,14 @@ pub async fn load_branch_commits(
owner: &str,
repository: &str,
branch: &str,
) -> Result<Vec<HistoryCommit>, String> {
load_commits_for_ref(server, owner, repository, Some(branch))
pages: u32,
) -> Result<Page<HistoryCommit>, String> {
load_commits_for_ref(server, owner, repository, Some(branch), pages)
.await
.map(|commits| {
commits
.map(|page| Page {
has_more: page.has_more,
items: page
.items
.into_iter()
.map(|commit| HistoryCommit {
commit,
@@ -350,7 +392,7 @@ pub async fn load_branch_commits(
connections: Vec::new(),
refs: Vec::new(),
})
.collect()
.collect(),
})
}
@@ -359,13 +401,15 @@ pub async fn load_all_commits(
owner: &str,
repository: &str,
branches: &[String],
) -> Result<Vec<HistoryCommit>, String> {
pages: u32,
) -> Result<Page<HistoryCommit>, String> {
let mut tasks = JoinSet::new();
for (lane, branch) in branches.iter().cloned().enumerate() {
let (server, owner, repository) =
(server.clone(), owner.to_string(), repository.to_string());
tasks.spawn(async move {
let commits = load_commits_for_ref(&server, &owner, &repository, Some(&branch)).await?;
let commits =
load_commits_for_ref(&server, &owner, &repository, Some(&branch), pages).await?;
Ok::<_, String>((lane, branch, commits))
});
}
@@ -375,11 +419,19 @@ pub async fn load_all_commits(
histories.push(result.map_err(|error| error.to_string())??);
}
histories.sort_by_key(|(lane, _, _)| *lane);
let has_more = histories.iter().any(|(_, _, page)| page.has_more);
let histories = histories
.into_iter()
.map(|(lane, branch, page)| (lane, branch, page.items))
.collect();
let pull_refs = load_pull_refs(server, owner, repository)
.await
.unwrap_or_default();
Ok(build_graph(histories, pull_refs))
Ok(Page {
items: build_graph(histories, pull_refs),
has_more,
})
}
pub async fn load_pull_files(
@@ -387,28 +439,21 @@ pub async fn load_pull_files(
owner: &str,
repository: &str,
number: i64,
) -> Result<Vec<models::ChangedFile>, String> {
let mut files = Vec::new();
for page in 1.. {
let batch = apis::repository_api::repo_get_pull_request_files(
configuration,
owner,
repository,
number,
None,
None,
Some(page),
Some(100),
)
.await
.map_err(|error| error.to_string())?;
let done = batch.len() < 100;
files.extend(batch);
if done {
break;
}
}
Ok(files)
page: i32,
) -> Result<Page<models::ChangedFile>, String> {
apis::repository_api::repo_get_pull_request_files(
configuration,
owner,
repository,
number,
None,
None,
Some(page),
Some(PAGE_SIZE),
)
.await
.map(Page::from_items)
.map_err(|error| error.to_string())
}
pub async fn load_issue(
@@ -416,20 +461,37 @@ pub async fn load_issue(
owner: &str,
repository: &str,
number: i64,
page: i32,
) -> Result<IssueDetails, String> {
let client =
Client::new(&server.url, Some(&server.token)).map_err(|error| error.to_string())?;
let configuration = client.configuration();
let (issue, comments) = tokio::join!(
apis::issue_api::issue_get_issue(&configuration, owner, repository, number),
apis::issue_api::issue_get_comments(&configuration, owner, repository, number, None, None,),
load_issue_comments(&configuration, owner, repository, number, page),
);
Ok(IssueDetails {
issue: issue.map_err(|error| error.to_string())?,
comments: comments.map_err(|error| error.to_string())?,
has_more: false,
comments: comments?,
})
}
async fn load_issue_comments(
configuration: &apis::configuration::Configuration,
owner: &str,
repository: &str,
number: i64,
page: i32,
) -> Result<Vec<models::Comment>, String> {
if page > 1 {
return Ok(Vec::new());
}
apis::issue_api::issue_get_comments(configuration, owner, repository, number, None, None)
.await
.map_err(|error| error.to_string())
}
pub async fn load_issue_editor(
server: &Server,
owner: &str,
@@ -544,19 +606,22 @@ pub async fn load_pull(
owner: &str,
repository: &str,
number: i64,
page: i32,
) -> Result<PullDetails, String> {
let client =
Client::new(&server.url, Some(&server.token)).map_err(|error| error.to_string())?;
let configuration = client.configuration();
let (pull, comments, files) = tokio::join!(
apis::repository_api::repo_get_pull_request(&configuration, owner, repository, number,),
apis::issue_api::issue_get_comments(&configuration, owner, repository, number, None, None,),
load_pull_files(&configuration, owner, repository, number),
load_issue_comments(&configuration, owner, repository, number, page),
load_pull_files(&configuration, owner, repository, number, page),
);
let files = files?;
Ok(PullDetails {
pull: pull.map_err(|error| error.to_string())?,
comments: comments.map_err(|error| error.to_string())?,
files: files.map_err(|error| error.to_string())?,
has_more: files.has_more,
comments: comments?,
files: files.items,
})
}
@@ -821,12 +886,14 @@ async fn load_commits_for_ref(
owner: &str,
repository: &str,
branch: Option<&str>,
) -> Result<Vec<models::Commit>, String> {
pages: u32,
) -> Result<Page<models::Commit>, String> {
let client =
Client::new(&server.url, Some(&server.token)).map_err(|error| error.to_string())?;
let configuration = client.configuration();
let mut commits = Vec::new();
for page in 1.. {
let mut has_more = false;
for page in 1..=pages.max(1) {
let batch = apis::repository_api::repo_get_all_commits(
&configuration,
owner,
@@ -838,22 +905,25 @@ async fn load_commits_for_ref(
None,
None,
Some(false),
Some(page),
Some(100),
Some(page as i32),
Some(PAGE_SIZE),
None,
)
.await
.map_err(|error| error.to_string())?;
let done = batch.len() < 100;
has_more = batch.len() == PAGE_SIZE as usize;
commits.extend(batch);
if done {
if !has_more {
break;
}
}
Ok(commits)
Ok(Page {
items: commits,
has_more,
})
}
pub async fn load_home(server: &Server) -> Result<HomeData, String> {
pub async fn load_home(server: &Server, page: i32) -> Result<HomeData, String> {
let client =
Client::new(&server.url, Some(&server.token)).map_err(|error| error.to_string())?;
let configuration = client.configuration();
@@ -863,34 +933,23 @@ pub async fn load_home(server: &Server) -> Result<HomeData, String> {
.map_err(|error| error.to_string())?
.login
.ok_or("The server account has no username.")?;
let activity_configuration = configuration.clone();
let activity_login = login.clone();
let activities = async move {
let mut activities = Vec::new();
for page in 1.. {
let batch = apis::user_api::user_list_activity_feeds(
&activity_configuration,
&activity_login,
Some(true),
None,
Some(page),
Some(100),
)
.await
.map_err(|error| error.to_string())?;
if batch.is_empty() {
break;
}
activities.extend(batch);
}
Ok::<_, String>(activities)
};
let activities = apis::user_api::user_list_activity_feeds(
&configuration,
&login,
Some(true),
None,
Some(page),
Some(PAGE_SIZE),
);
let (activities, heatmap) = tokio::join!(
activities,
apis::user_api::user_get_heatmap_data(&configuration, &login),
);
Ok(HomeData {
activities: activities?,
has_more: activities
.as_ref()
.is_ok_and(|items| items.len() == PAGE_SIZE as usize),
activities: activities.map_err(|error| error.to_string())?,
heatmap: heatmap.map_err(|error| error.to_string())?,
})
}

View File

@@ -3,6 +3,22 @@ use std::collections::{BTreeMap, BTreeSet};
use gotcha_gitea::models;
use serde::{Deserialize, Serialize};
pub const PAGE_SIZE: i32 = 30;
pub struct Page<T> {
pub items: Vec<T>,
pub has_more: bool,
}
impl<T> Page<T> {
pub fn from_items(items: Vec<T>) -> Self {
Self {
has_more: items.len() == PAGE_SIZE as usize,
items,
}
}
}
#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "lowercase")]
pub enum AppearanceMode {
@@ -132,11 +148,13 @@ pub struct State {
pub struct HomeData {
pub activities: Vec<models::Activity>,
pub heatmap: Vec<models::UserHeatmapData>,
pub has_more: bool,
}
pub struct IssueDetails {
pub issue: models::Issue,
pub comments: Vec<models::Comment>,
pub has_more: bool,
}
pub struct IssueEditorData {
@@ -157,12 +175,14 @@ pub struct MilestoneDetails {
pub milestone: models::Milestone,
pub issues: Vec<models::Issue>,
pub pulls: Vec<models::Issue>,
pub has_more: bool,
}
pub struct PullDetails {
pub pull: models::PullRequest,
pub comments: Vec<models::Comment>,
pub files: Vec<models::ChangedFile>,
pub has_more: bool,
}
pub fn open_status() -> String {
@@ -244,4 +264,10 @@ mod tests {
assert_eq!(parse_api_date("2023-02-29T00:00:00Z"), None);
assert_eq!(parse_api_date("not-a-date"), None);
}
#[test]
fn full_api_pages_expose_more_results() {
assert!(Page::from_items(vec![(); PAGE_SIZE as usize]).has_more);
assert!(!Page::from_items(vec![(); PAGE_SIZE as usize - 1]).has_more);
}
}

View File

@@ -173,18 +173,36 @@ impl GotchaCore {
save_preferences(&state.preferences).map_err(Into::into)
}
pub async fn home(&self) -> Result<HomePage, GotchaError> {
pub async fn home(&self, page: u32) -> Result<HomePage, GotchaError> {
let server = self.server()?;
let name = server.name.clone();
Ok(home_page(name, load_home(&server).await?))
Ok(home_page(
name,
load_home(&server, valid_page(page)?).await?,
))
}
pub async fn repositories(&self) -> Result<Vec<RepositoryRow>, GotchaError> {
pub async fn repositories(&self, page: u32) -> Result<RepositoryListPage, GotchaError> {
let server = self.server()?;
let repositories = load_repositories(&server).await?;
let page_number = valid_page(page)?;
let repositories = load_repositories(&server, page_number).await?;
let mut state = self.state.lock().unwrap();
state.repositories = repositories;
Ok(self.repository_rows(&state))
if page_number == 1 {
state.repositories.clear();
}
for repository in repositories.items {
if let Some(existing) = state.repositories.iter_mut().find(|candidate| {
candidate.owner == repository.owner && candidate.name == repository.name
}) {
*existing = repository;
} else {
state.repositories.push(repository);
}
}
Ok(RepositoryListPage {
rows: self.repository_rows(&state),
has_more: repositories.has_more,
})
}
pub fn toggle_favorite(
@@ -209,7 +227,8 @@ impl GotchaCore {
&self,
owner: String,
repository: String,
) -> Result<Vec<IssueRow>, GotchaError> {
page: u32,
) -> Result<IssueListPage, GotchaError> {
let server = self.server()?;
let (status, filter) = {
let state = self.state.lock().unwrap();
@@ -224,17 +243,20 @@ impl GotchaCore {
)
};
let labels: Vec<_> = filter.labels.into_iter().collect();
Ok(issue_rows(
&load_issues(
&server,
&owner,
&repository,
&status,
&labels,
&filter.milestone,
)
.await?,
))
let page = load_issues(
&server,
&owner,
&repository,
&status,
&labels,
&filter.milestone,
valid_page(page)?,
)
.await?;
Ok(IssueListPage {
rows: issue_rows(&page.items),
has_more: page.has_more,
})
}
pub async fn issue_filters(
@@ -317,9 +339,17 @@ impl GotchaCore {
owner: String,
repository: String,
number: i64,
page: u32,
) -> Result<IssuePage, GotchaError> {
Ok(issue_page(
load_issue(&self.server()?, &owner, &repository, number).await?,
load_issue(
&self.server()?,
&owner,
&repository,
number,
valid_page(page)?,
)
.await?,
))
}
@@ -387,10 +417,14 @@ impl GotchaCore {
&self,
owner: String,
repository: String,
) -> Result<Vec<MilestoneRow>, GotchaError> {
Ok(milestone_rows(
&load_milestones(&self.server()?, &owner, &repository).await?,
))
page: u32,
) -> Result<MilestoneListPage, GotchaError> {
let page =
load_milestones_page(&self.server()?, &owner, &repository, valid_page(page)?).await?;
Ok(MilestoneListPage {
rows: milestone_rows(&page.items),
has_more: page.has_more,
})
}
pub async fn milestone(
@@ -398,9 +432,10 @@ impl GotchaCore {
owner: String,
repository: String,
id: i64,
page: u32,
) -> Result<MilestonePage, GotchaError> {
Ok(milestone_page(
load_milestone(&self.server()?, &owner, &repository, id).await?,
load_milestone(&self.server()?, &owner, &repository, id, valid_page(page)?).await?,
))
}
@@ -449,7 +484,7 @@ impl GotchaCore {
save_preferences(&state.preferences).map_err(Into::into)
}
pub async fn pulls(&self) -> Result<Vec<PullRow>, GotchaError> {
pub async fn pulls(&self, page: u32) -> Result<PullListPage, GotchaError> {
let server = self.server()?;
let (status, filter) = {
let state = self.state.lock().unwrap();
@@ -463,9 +498,11 @@ impl GotchaCore {
.unwrap_or_default(),
)
};
Ok(pull_rows(
&load_pulls(&server, &status, &filter.milestone).await?,
))
let page = load_pulls(&server, &status, &filter.milestone, valid_page(page)?).await?;
Ok(PullListPage {
rows: pull_rows(&page.items),
has_more: page.has_more,
})
}
pub async fn pull(
@@ -473,9 +510,17 @@ impl GotchaCore {
owner: String,
repository: String,
number: i64,
page: u32,
) -> Result<PullPage, GotchaError> {
Ok(pull_page(
load_pull(&self.server()?, &owner, &repository, number).await?,
load_pull(
&self.server()?,
&owner,
&repository,
number,
valid_page(page)?,
)
.await?,
))
}
@@ -484,6 +529,7 @@ impl GotchaCore {
owner: String,
repository: String,
branch: Option<String>,
pages: u32,
) -> Result<CommitPage, GotchaError> {
let server = self.server()?;
let default_branch = self
@@ -497,10 +543,17 @@ impl GotchaCore {
.unwrap_or_else(|| "main".into());
let branches = load_branches(&server, &owner, &repository, &default_branch).await?;
let commits = match branch.as_deref() {
Some(branch) => load_branch_commits(&server, &owner, &repository, branch).await?,
None => load_all_commits(&server, &owner, &repository, &branches).await?,
Some(branch) => {
load_branch_commits(&server, &owner, &repository, branch, pages.max(1)).await?
}
None => load_all_commits(&server, &owner, &repository, &branches, pages.max(1)).await?,
};
Ok(commit_page(branches, &commits, branch.is_none()))
Ok(commit_page(
branches,
&commits.items,
branch.is_none(),
commits.has_more,
))
}
pub async fn repository_contents(
@@ -574,6 +627,13 @@ fn validate_repository<'a>(
Ok((owner, repository))
}
fn valid_page(page: u32) -> Result<i32, GotchaError> {
(page > 0)
.then(|| i32::try_from(page).ok())
.flatten()
.ok_or_else(|| "Invalid page number.".into())
}
impl GotchaCore {
fn server(&self) -> Result<Server, GotchaError> {
let state = self.state.lock().unwrap();

View File

@@ -23,6 +23,12 @@ pub struct RepositoryRow {
pub favorite: bool,
}
#[derive(Clone, uniffi::Record)]
pub struct RepositoryListPage {
pub rows: Vec<RepositoryRow>,
pub has_more: bool,
}
#[derive(Clone, uniffi::Record)]
pub struct LabelRow {
pub name: String,
@@ -40,6 +46,12 @@ pub struct IssueRow {
pub labels: Vec<LabelRow>,
}
#[derive(Clone, uniffi::Record)]
pub struct IssueListPage {
pub rows: Vec<IssueRow>,
pub has_more: bool,
}
#[derive(Clone, uniffi::Record)]
pub struct PullRow {
pub number: i64,
@@ -65,6 +77,7 @@ pub struct IssuePage {
pub milestone: String,
pub body: String,
pub comments: Vec<CommentRow>,
pub has_more: bool,
}
#[derive(Clone, uniffi::Record)]
@@ -115,6 +128,13 @@ pub struct MilestonePage {
pub milestone: MilestoneRow,
pub issues: Vec<IssueRow>,
pub pulls: Vec<PullRow>,
pub has_more: bool,
}
#[derive(Clone, uniffi::Record)]
pub struct MilestoneListPage {
pub rows: Vec<MilestoneRow>,
pub has_more: bool,
}
#[derive(Clone, uniffi::Record)]
@@ -131,6 +151,13 @@ pub struct PullPage {
pub files_ref: String,
pub files: Vec<FileRow>,
pub comments: Vec<CommentRow>,
pub has_more: bool,
}
#[derive(Clone, uniffi::Record)]
pub struct PullListPage {
pub rows: Vec<PullRow>,
pub has_more: bool,
}
#[derive(Clone, uniffi::Record)]
@@ -149,6 +176,7 @@ pub struct CommitPage {
pub branches: Vec<String>,
pub commits: Vec<CommitRow>,
pub lane_count: u32,
pub has_more: bool,
}
#[derive(Clone, uniffi::Record)]
@@ -157,6 +185,12 @@ pub struct FileRow {
pub status: String,
}
#[derive(Clone, uniffi::Record)]
pub struct FileListPage {
pub rows: Vec<FileRow>,
pub has_more: bool,
}
#[derive(Clone, uniffi::Record)]
pub struct RepositoryContentRow {
pub name: String,
@@ -216,6 +250,7 @@ pub struct HomePage {
pub pull_activities: Vec<ActivityRow>,
pub heat_cells: Vec<HeatCell>,
pub contribution_count: i64,
pub has_more: bool,
}
pub fn repository_rows(
@@ -323,6 +358,7 @@ pub fn milestone_page(details: MilestoneDetails) -> MilestonePage {
milestone: milestone_row(&details.milestone),
issues: issue_rows(&details.issues),
pulls: pull_rows(&details.pulls),
has_more: details.has_more,
}
}
@@ -389,6 +425,7 @@ pub fn commit_page(
branches: Vec<String>,
commits: &[HistoryCommit],
show_graph: bool,
has_more: bool,
) -> CommitPage {
let lane_count = if show_graph {
commits
@@ -454,6 +491,7 @@ pub fn commit_page(
branches,
commits,
lane_count: lane_count as u32,
has_more,
}
}
@@ -477,6 +515,7 @@ pub fn issue_page(details: IssueDetails) -> IssuePage {
.filter(|body| !body.is_empty())
.unwrap_or_else(|| "No description provided.".into()),
comments: details.comments.iter().map(comment_row).collect(),
has_more: details.has_more,
}
}
@@ -589,6 +628,7 @@ pub fn pull_page(details: PullDetails) -> PullPage {
files_ref: pull_files_ref(pull),
files: details.files.iter().filter_map(file_row).collect(),
comments: details.comments.iter().map(comment_row).collect(),
has_more: details.has_more,
}
}
@@ -722,6 +762,7 @@ pub fn home_page(server_name: String, home: HomeData) -> HomePage {
activities: home.activities.iter().map(activity_row).collect(),
heat_cells,
contribution_count,
has_more: home.has_more,
}
}
@@ -1213,6 +1254,7 @@ mod tests {
})
.collect(),
heatmap: Vec::new(),
has_more: false,
},
);
@@ -1229,6 +1271,7 @@ mod tests {
..Default::default()
},
comments: Vec::new(),
has_more: false,
});
assert_eq!(page.state, "closed");
@@ -1331,6 +1374,7 @@ mod tests {
})),
..Default::default()
}],
has_more: false,
});
assert!(page.issues.is_empty());

View File

@@ -613,11 +613,11 @@ public protocol GotchaCoreProtocol: AnyObject, Sendable {
func commitFiles(owner: String, repository: String, sha: String) async throws -> [FileRow]
func commits(owner: String, repository: String, branch: String?) async throws -> CommitPage
func commits(owner: String, repository: String, branch: String?, pages: UInt32) async throws -> CommitPage
func home() async throws -> HomePage
func home(page: UInt32) async throws -> HomePage
func issue(owner: String, repository: String, number: Int64) async throws -> IssuePage
func issue(owner: String, repository: String, number: Int64, page: UInt32) async throws -> IssuePage
func issueEditor(owner: String, repository: String, number: Int64?) async throws -> IssueEditorPage
@@ -625,13 +625,13 @@ public protocol GotchaCoreProtocol: AnyObject, Sendable {
func issueFiltersActive(owner: String, repository: String) throws -> Bool
func issues(owner: String, repository: String) async throws -> [IssueRow]
func issues(owner: String, repository: String, page: UInt32) async throws -> IssueListPage
func milestone(owner: String, repository: String, id: Int64) async throws -> MilestonePage
func milestone(owner: String, repository: String, id: Int64, page: UInt32) async throws -> MilestonePage
func milestones(owner: String, repository: String) async throws -> [MilestoneRow]
func milestones(owner: String, repository: String, page: UInt32) async throws -> MilestoneListPage
func pull(owner: String, repository: String, number: Int64) async throws -> PullPage
func pull(owner: String, repository: String, number: Int64, page: UInt32) async throws -> PullPage
func pullDiff(owner: String, repository: String, number: Int64, path: String) async throws -> DiffPage
@@ -639,9 +639,9 @@ public protocol GotchaCoreProtocol: AnyObject, Sendable {
func pullFiltersActive() throws -> Bool
func pulls() async throws -> [PullRow]
func pulls(page: UInt32) async throws -> PullListPage
func repositories() async throws -> [RepositoryRow]
func repositories(page: UInt32) async throws -> RepositoryListPage
func repositoryContents(owner: String, repository: String, path: String) async throws -> [RepositoryContentRow]
@@ -797,12 +797,12 @@ open func commitFiles(owner: String, repository: String, sha: String)async throw
)
}
open func commits(owner: String, repository: String, branch: String?)async throws -> CommitPage {
open func commits(owner: String, repository: String, branch: String?, pages: UInt32)async throws -> CommitPage {
return
try await uniffiRustCallAsync(
rustFutureFunc: {
uniffi_gotcha_core_fn_method_gotchacore_commits(
self.uniffiCloneHandle(),FfiConverterString.lower(owner),FfiConverterString.lower(repository),FfiConverterOptionString.lower(branch)
self.uniffiCloneHandle(),FfiConverterString.lower(owner),FfiConverterString.lower(repository),FfiConverterOptionString.lower(branch),FfiConverterUInt32.lower(pages)
)
},
pollFunc: ffi_gotcha_core_rust_future_poll_rust_buffer,
@@ -813,12 +813,12 @@ open func commits(owner: String, repository: String, branch: String?)async throw
)
}
open func home()async throws -> HomePage {
open func home(page: UInt32)async throws -> HomePage {
return
try await uniffiRustCallAsync(
rustFutureFunc: {
uniffi_gotcha_core_fn_method_gotchacore_home(
self.uniffiCloneHandle()
self.uniffiCloneHandle(),FfiConverterUInt32.lower(page)
)
},
pollFunc: ffi_gotcha_core_rust_future_poll_rust_buffer,
@@ -829,12 +829,12 @@ open func home()async throws -> HomePage {
)
}
open func issue(owner: String, repository: String, number: Int64)async throws -> IssuePage {
open func issue(owner: String, repository: String, number: Int64, page: UInt32)async throws -> IssuePage {
return
try await uniffiRustCallAsync(
rustFutureFunc: {
uniffi_gotcha_core_fn_method_gotchacore_issue(
self.uniffiCloneHandle(),FfiConverterString.lower(owner),FfiConverterString.lower(repository),FfiConverterInt64.lower(number)
self.uniffiCloneHandle(),FfiConverterString.lower(owner),FfiConverterString.lower(repository),FfiConverterInt64.lower(number),FfiConverterUInt32.lower(page)
)
},
pollFunc: ffi_gotcha_core_rust_future_poll_rust_buffer,
@@ -888,28 +888,28 @@ open func issueFiltersActive(owner: String, repository: String)throws -> Bool
})
}
open func issues(owner: String, repository: String)async throws -> [IssueRow] {
open func issues(owner: String, repository: String, page: UInt32)async throws -> IssueListPage {
return
try await uniffiRustCallAsync(
rustFutureFunc: {
uniffi_gotcha_core_fn_method_gotchacore_issues(
self.uniffiCloneHandle(),FfiConverterString.lower(owner),FfiConverterString.lower(repository)
self.uniffiCloneHandle(),FfiConverterString.lower(owner),FfiConverterString.lower(repository),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: FfiConverterSequenceTypeIssueRow.lift,
liftFunc: FfiConverterTypeIssueListPage_lift,
errorHandler: FfiConverterTypeGotchaError_lift
)
}
open func milestone(owner: String, repository: String, id: Int64)async throws -> MilestonePage {
open func milestone(owner: String, repository: String, id: Int64, page: UInt32)async throws -> MilestonePage {
return
try await uniffiRustCallAsync(
rustFutureFunc: {
uniffi_gotcha_core_fn_method_gotchacore_milestone(
self.uniffiCloneHandle(),FfiConverterString.lower(owner),FfiConverterString.lower(repository),FfiConverterInt64.lower(id)
self.uniffiCloneHandle(),FfiConverterString.lower(owner),FfiConverterString.lower(repository),FfiConverterInt64.lower(id),FfiConverterUInt32.lower(page)
)
},
pollFunc: ffi_gotcha_core_rust_future_poll_rust_buffer,
@@ -920,28 +920,28 @@ open func milestone(owner: String, repository: String, id: Int64)async throws -
)
}
open func milestones(owner: String, repository: String)async throws -> [MilestoneRow] {
open func milestones(owner: String, repository: String, page: UInt32)async throws -> MilestoneListPage {
return
try await uniffiRustCallAsync(
rustFutureFunc: {
uniffi_gotcha_core_fn_method_gotchacore_milestones(
self.uniffiCloneHandle(),FfiConverterString.lower(owner),FfiConverterString.lower(repository)
self.uniffiCloneHandle(),FfiConverterString.lower(owner),FfiConverterString.lower(repository),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: FfiConverterSequenceTypeMilestoneRow.lift,
liftFunc: FfiConverterTypeMilestoneListPage_lift,
errorHandler: FfiConverterTypeGotchaError_lift
)
}
open func pull(owner: String, repository: String, number: Int64)async throws -> PullPage {
open func pull(owner: String, repository: String, number: Int64, page: UInt32)async throws -> PullPage {
return
try await uniffiRustCallAsync(
rustFutureFunc: {
uniffi_gotcha_core_fn_method_gotchacore_pull(
self.uniffiCloneHandle(),FfiConverterString.lower(owner),FfiConverterString.lower(repository),FfiConverterInt64.lower(number)
self.uniffiCloneHandle(),FfiConverterString.lower(owner),FfiConverterString.lower(repository),FfiConverterInt64.lower(number),FfiConverterUInt32.lower(page)
)
},
pollFunc: ffi_gotcha_core_rust_future_poll_rust_buffer,
@@ -993,34 +993,34 @@ open func pullFiltersActive()throws -> Bool {
})
}
open func pulls()async throws -> [PullRow] {
open func pulls(page: UInt32)async throws -> PullListPage {
return
try await uniffiRustCallAsync(
rustFutureFunc: {
uniffi_gotcha_core_fn_method_gotchacore_pulls(
self.uniffiCloneHandle()
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: FfiConverterSequenceTypePullRow.lift,
liftFunc: FfiConverterTypePullListPage_lift,
errorHandler: FfiConverterTypeGotchaError_lift
)
}
open func repositories()async throws -> [RepositoryRow] {
open func repositories(page: UInt32)async throws -> RepositoryListPage {
return
try await uniffiRustCallAsync(
rustFutureFunc: {
uniffi_gotcha_core_fn_method_gotchacore_repositories(
self.uniffiCloneHandle()
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: FfiConverterSequenceTypeRepositoryRow.lift,
liftFunc: FfiConverterTypeRepositoryListPage_lift,
errorHandler: FfiConverterTypeGotchaError_lift
)
}
@@ -1360,13 +1360,15 @@ public struct CommitPage: Equatable, Hashable {
public var branches: [String]
public var commits: [CommitRow]
public var laneCount: UInt32
public var hasMore: Bool
// Default memberwise initializers are never public by default, so we
// declare one manually.
public init(branches: [String], commits: [CommitRow], laneCount: UInt32) {
public init(branches: [String], commits: [CommitRow], laneCount: UInt32, hasMore: Bool) {
self.branches = branches
self.commits = commits
self.laneCount = laneCount
self.hasMore = hasMore
}
@@ -1387,7 +1389,8 @@ public struct FfiConverterTypeCommitPage: FfiConverterRustBuffer {
try CommitPage(
branches: FfiConverterSequenceString.read(from: &buf),
commits: FfiConverterSequenceTypeCommitRow.read(from: &buf),
laneCount: FfiConverterUInt32.read(from: &buf)
laneCount: FfiConverterUInt32.read(from: &buf),
hasMore: FfiConverterBool.read(from: &buf)
)
}
@@ -1395,6 +1398,7 @@ public struct FfiConverterTypeCommitPage: FfiConverterRustBuffer {
FfiConverterSequenceString.write(value.branches, into: &buf)
FfiConverterSequenceTypeCommitRow.write(value.commits, into: &buf)
FfiConverterUInt32.write(value.laneCount, into: &buf)
FfiConverterBool.write(value.hasMore, into: &buf)
}
}
@@ -1608,6 +1612,60 @@ public func FfiConverterTypeDiffPage_lower(_ value: DiffPage) -> RustBuffer {
}
public struct FileListPage: Equatable, Hashable {
public var rows: [FileRow]
public var hasMore: Bool
// Default memberwise initializers are never public by default, so we
// declare one manually.
public init(rows: [FileRow], hasMore: Bool) {
self.rows = rows
self.hasMore = hasMore
}
}
#if compiler(>=6)
extension FileListPage: Sendable {}
#endif
#if swift(>=5.8)
@_documentation(visibility: private)
#endif
public struct FfiConverterTypeFileListPage: FfiConverterRustBuffer {
public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> FileListPage {
return
try FileListPage(
rows: FfiConverterSequenceTypeFileRow.read(from: &buf),
hasMore: FfiConverterBool.read(from: &buf)
)
}
public static func write(_ value: FileListPage, into buf: inout [UInt8]) {
FfiConverterSequenceTypeFileRow.write(value.rows, into: &buf)
FfiConverterBool.write(value.hasMore, into: &buf)
}
}
#if swift(>=5.8)
@_documentation(visibility: private)
#endif
public func FfiConverterTypeFileListPage_lift(_ buf: RustBuffer) throws -> FileListPage {
return try FfiConverterTypeFileListPage.lift(buf)
}
#if swift(>=5.8)
@_documentation(visibility: private)
#endif
public func FfiConverterTypeFileListPage_lower(_ value: FileListPage) -> RustBuffer {
return FfiConverterTypeFileListPage.lower(value)
}
public struct FileRow: Equatable, Hashable {
public var path: String
public var status: String
@@ -1723,16 +1781,18 @@ public struct HomePage: Equatable, Hashable {
public var pullActivities: [ActivityRow]
public var heatCells: [HeatCell]
public var contributionCount: Int64
public var hasMore: Bool
// Default memberwise initializers are never public by default, so we
// declare one manually.
public init(serverName: String, activities: [ActivityRow], issueActivities: [ActivityRow], pullActivities: [ActivityRow], heatCells: [HeatCell], contributionCount: Int64) {
public init(serverName: String, activities: [ActivityRow], issueActivities: [ActivityRow], pullActivities: [ActivityRow], heatCells: [HeatCell], contributionCount: Int64, hasMore: Bool) {
self.serverName = serverName
self.activities = activities
self.issueActivities = issueActivities
self.pullActivities = pullActivities
self.heatCells = heatCells
self.contributionCount = contributionCount
self.hasMore = hasMore
}
@@ -1756,7 +1816,8 @@ public struct FfiConverterTypeHomePage: FfiConverterRustBuffer {
issueActivities: FfiConverterSequenceTypeActivityRow.read(from: &buf),
pullActivities: FfiConverterSequenceTypeActivityRow.read(from: &buf),
heatCells: FfiConverterSequenceTypeHeatCell.read(from: &buf),
contributionCount: FfiConverterInt64.read(from: &buf)
contributionCount: FfiConverterInt64.read(from: &buf),
hasMore: FfiConverterBool.read(from: &buf)
)
}
@@ -1767,6 +1828,7 @@ public struct FfiConverterTypeHomePage: FfiConverterRustBuffer {
FfiConverterSequenceTypeActivityRow.write(value.pullActivities, into: &buf)
FfiConverterSequenceTypeHeatCell.write(value.heatCells, into: &buf)
FfiConverterInt64.write(value.contributionCount, into: &buf)
FfiConverterBool.write(value.hasMore, into: &buf)
}
}
@@ -2034,6 +2096,60 @@ public func FfiConverterTypeIssueFilterOptions_lower(_ value: IssueFilterOptions
}
public struct IssueListPage: Equatable, Hashable {
public var rows: [IssueRow]
public var hasMore: Bool
// Default memberwise initializers are never public by default, so we
// declare one manually.
public init(rows: [IssueRow], hasMore: Bool) {
self.rows = rows
self.hasMore = hasMore
}
}
#if compiler(>=6)
extension IssueListPage: Sendable {}
#endif
#if swift(>=5.8)
@_documentation(visibility: private)
#endif
public struct FfiConverterTypeIssueListPage: FfiConverterRustBuffer {
public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> IssueListPage {
return
try IssueListPage(
rows: FfiConverterSequenceTypeIssueRow.read(from: &buf),
hasMore: FfiConverterBool.read(from: &buf)
)
}
public static func write(_ value: IssueListPage, into buf: inout [UInt8]) {
FfiConverterSequenceTypeIssueRow.write(value.rows, into: &buf)
FfiConverterBool.write(value.hasMore, into: &buf)
}
}
#if swift(>=5.8)
@_documentation(visibility: private)
#endif
public func FfiConverterTypeIssueListPage_lift(_ buf: RustBuffer) throws -> IssueListPage {
return try FfiConverterTypeIssueListPage.lift(buf)
}
#if swift(>=5.8)
@_documentation(visibility: private)
#endif
public func FfiConverterTypeIssueListPage_lower(_ value: IssueListPage) -> RustBuffer {
return FfiConverterTypeIssueListPage.lower(value)
}
public struct IssuePage: Equatable, Hashable {
public var title: String
public var state: String
@@ -2041,16 +2157,18 @@ public struct IssuePage: Equatable, Hashable {
public var milestone: String
public var body: String
public var comments: [CommentRow]
public var hasMore: Bool
// Default memberwise initializers are never public by default, so we
// declare one manually.
public init(title: String, state: String, meta: String, milestone: String, body: String, comments: [CommentRow]) {
public init(title: String, state: String, meta: String, milestone: String, body: String, comments: [CommentRow], hasMore: Bool) {
self.title = title
self.state = state
self.meta = meta
self.milestone = milestone
self.body = body
self.comments = comments
self.hasMore = hasMore
}
@@ -2074,7 +2192,8 @@ public struct FfiConverterTypeIssuePage: FfiConverterRustBuffer {
meta: FfiConverterString.read(from: &buf),
milestone: FfiConverterString.read(from: &buf),
body: FfiConverterString.read(from: &buf),
comments: FfiConverterSequenceTypeCommentRow.read(from: &buf)
comments: FfiConverterSequenceTypeCommentRow.read(from: &buf),
hasMore: FfiConverterBool.read(from: &buf)
)
}
@@ -2085,6 +2204,7 @@ public struct FfiConverterTypeIssuePage: FfiConverterRustBuffer {
FfiConverterString.write(value.milestone, into: &buf)
FfiConverterString.write(value.body, into: &buf)
FfiConverterSequenceTypeCommentRow.write(value.comments, into: &buf)
FfiConverterBool.write(value.hasMore, into: &buf)
}
}
@@ -2232,17 +2352,73 @@ public func FfiConverterTypeLabelRow_lower(_ value: LabelRow) -> RustBuffer {
}
public struct MilestoneListPage: Equatable, Hashable {
public var rows: [MilestoneRow]
public var hasMore: Bool
// Default memberwise initializers are never public by default, so we
// declare one manually.
public init(rows: [MilestoneRow], hasMore: Bool) {
self.rows = rows
self.hasMore = hasMore
}
}
#if compiler(>=6)
extension MilestoneListPage: Sendable {}
#endif
#if swift(>=5.8)
@_documentation(visibility: private)
#endif
public struct FfiConverterTypeMilestoneListPage: FfiConverterRustBuffer {
public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> MilestoneListPage {
return
try MilestoneListPage(
rows: FfiConverterSequenceTypeMilestoneRow.read(from: &buf),
hasMore: FfiConverterBool.read(from: &buf)
)
}
public static func write(_ value: MilestoneListPage, into buf: inout [UInt8]) {
FfiConverterSequenceTypeMilestoneRow.write(value.rows, into: &buf)
FfiConverterBool.write(value.hasMore, into: &buf)
}
}
#if swift(>=5.8)
@_documentation(visibility: private)
#endif
public func FfiConverterTypeMilestoneListPage_lift(_ buf: RustBuffer) throws -> MilestoneListPage {
return try FfiConverterTypeMilestoneListPage.lift(buf)
}
#if swift(>=5.8)
@_documentation(visibility: private)
#endif
public func FfiConverterTypeMilestoneListPage_lower(_ value: MilestoneListPage) -> RustBuffer {
return FfiConverterTypeMilestoneListPage.lower(value)
}
public struct MilestonePage: Equatable, Hashable {
public var milestone: MilestoneRow
public var issues: [IssueRow]
public var pulls: [PullRow]
public var hasMore: Bool
// Default memberwise initializers are never public by default, so we
// declare one manually.
public init(milestone: MilestoneRow, issues: [IssueRow], pulls: [PullRow]) {
public init(milestone: MilestoneRow, issues: [IssueRow], pulls: [PullRow], hasMore: Bool) {
self.milestone = milestone
self.issues = issues
self.pulls = pulls
self.hasMore = hasMore
}
@@ -2263,7 +2439,8 @@ public struct FfiConverterTypeMilestonePage: FfiConverterRustBuffer {
try MilestonePage(
milestone: FfiConverterTypeMilestoneRow.read(from: &buf),
issues: FfiConverterSequenceTypeIssueRow.read(from: &buf),
pulls: FfiConverterSequenceTypePullRow.read(from: &buf)
pulls: FfiConverterSequenceTypePullRow.read(from: &buf),
hasMore: FfiConverterBool.read(from: &buf)
)
}
@@ -2271,6 +2448,7 @@ public struct FfiConverterTypeMilestonePage: FfiConverterRustBuffer {
FfiConverterTypeMilestoneRow.write(value.milestone, into: &buf)
FfiConverterSequenceTypeIssueRow.write(value.issues, into: &buf)
FfiConverterSequenceTypePullRow.write(value.pulls, into: &buf)
FfiConverterBool.write(value.hasMore, into: &buf)
}
}
@@ -2418,6 +2596,60 @@ public func FfiConverterTypePullFilterOptions_lower(_ value: PullFilterOptions)
}
public struct PullListPage: Equatable, Hashable {
public var rows: [PullRow]
public var hasMore: Bool
// Default memberwise initializers are never public by default, so we
// declare one manually.
public init(rows: [PullRow], hasMore: Bool) {
self.rows = rows
self.hasMore = hasMore
}
}
#if compiler(>=6)
extension PullListPage: Sendable {}
#endif
#if swift(>=5.8)
@_documentation(visibility: private)
#endif
public struct FfiConverterTypePullListPage: FfiConverterRustBuffer {
public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> PullListPage {
return
try PullListPage(
rows: FfiConverterSequenceTypePullRow.read(from: &buf),
hasMore: FfiConverterBool.read(from: &buf)
)
}
public static func write(_ value: PullListPage, into buf: inout [UInt8]) {
FfiConverterSequenceTypePullRow.write(value.rows, into: &buf)
FfiConverterBool.write(value.hasMore, into: &buf)
}
}
#if swift(>=5.8)
@_documentation(visibility: private)
#endif
public func FfiConverterTypePullListPage_lift(_ buf: RustBuffer) throws -> PullListPage {
return try FfiConverterTypePullListPage.lift(buf)
}
#if swift(>=5.8)
@_documentation(visibility: private)
#endif
public func FfiConverterTypePullListPage_lower(_ value: PullListPage) -> RustBuffer {
return FfiConverterTypePullListPage.lower(value)
}
public struct PullPage: Equatable, Hashable {
public var title: String
public var meta: String
@@ -2425,16 +2657,18 @@ public struct PullPage: Equatable, Hashable {
public var filesRef: String
public var files: [FileRow]
public var comments: [CommentRow]
public var hasMore: Bool
// Default memberwise initializers are never public by default, so we
// declare one manually.
public init(title: String, meta: String, body: String, filesRef: String, files: [FileRow], comments: [CommentRow]) {
public init(title: String, meta: String, body: String, filesRef: String, files: [FileRow], comments: [CommentRow], hasMore: Bool) {
self.title = title
self.meta = meta
self.body = body
self.filesRef = filesRef
self.files = files
self.comments = comments
self.hasMore = hasMore
}
@@ -2458,7 +2692,8 @@ public struct FfiConverterTypePullPage: FfiConverterRustBuffer {
body: FfiConverterString.read(from: &buf),
filesRef: FfiConverterString.read(from: &buf),
files: FfiConverterSequenceTypeFileRow.read(from: &buf),
comments: FfiConverterSequenceTypeCommentRow.read(from: &buf)
comments: FfiConverterSequenceTypeCommentRow.read(from: &buf),
hasMore: FfiConverterBool.read(from: &buf)
)
}
@@ -2469,6 +2704,7 @@ public struct FfiConverterTypePullPage: FfiConverterRustBuffer {
FfiConverterString.write(value.filesRef, into: &buf)
FfiConverterSequenceTypeFileRow.write(value.files, into: &buf)
FfiConverterSequenceTypeCommentRow.write(value.comments, into: &buf)
FfiConverterBool.write(value.hasMore, into: &buf)
}
}
@@ -2686,6 +2922,60 @@ public func FfiConverterTypeRepositoryFilePage_lower(_ value: RepositoryFilePage
}
public struct RepositoryListPage: Equatable, Hashable {
public var rows: [RepositoryRow]
public var hasMore: Bool
// Default memberwise initializers are never public by default, so we
// declare one manually.
public init(rows: [RepositoryRow], hasMore: Bool) {
self.rows = rows
self.hasMore = hasMore
}
}
#if compiler(>=6)
extension RepositoryListPage: Sendable {}
#endif
#if swift(>=5.8)
@_documentation(visibility: private)
#endif
public struct FfiConverterTypeRepositoryListPage: FfiConverterRustBuffer {
public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> RepositoryListPage {
return
try RepositoryListPage(
rows: FfiConverterSequenceTypeRepositoryRow.read(from: &buf),
hasMore: FfiConverterBool.read(from: &buf)
)
}
public static func write(_ value: RepositoryListPage, into buf: inout [UInt8]) {
FfiConverterSequenceTypeRepositoryRow.write(value.rows, into: &buf)
FfiConverterBool.write(value.hasMore, into: &buf)
}
}
#if swift(>=5.8)
@_documentation(visibility: private)
#endif
public func FfiConverterTypeRepositoryListPage_lift(_ buf: RustBuffer) throws -> RepositoryListPage {
return try FfiConverterTypeRepositoryListPage.lift(buf)
}
#if swift(>=5.8)
@_documentation(visibility: private)
#endif
public func FfiConverterTypeRepositoryListPage_lower(_ value: RepositoryListPage) -> RustBuffer {
return FfiConverterTypeRepositoryListPage.lower(value)
}
public struct RepositoryRow: Equatable, Hashable {
public var name: String
public var owner: String
@@ -3538,13 +3828,13 @@ private let initializationResult: InitializationResult = {
if (uniffi_gotcha_core_checksum_method_gotchacore_commit_files() != 43926) {
return InitializationResult.apiChecksumMismatch
}
if (uniffi_gotcha_core_checksum_method_gotchacore_commits() != 472) {
if (uniffi_gotcha_core_checksum_method_gotchacore_commits() != 59625) {
return InitializationResult.apiChecksumMismatch
}
if (uniffi_gotcha_core_checksum_method_gotchacore_home() != 5983) {
if (uniffi_gotcha_core_checksum_method_gotchacore_home() != 38990) {
return InitializationResult.apiChecksumMismatch
}
if (uniffi_gotcha_core_checksum_method_gotchacore_issue() != 56735) {
if (uniffi_gotcha_core_checksum_method_gotchacore_issue() != 3108) {
return InitializationResult.apiChecksumMismatch
}
if (uniffi_gotcha_core_checksum_method_gotchacore_issue_editor() != 10078) {
@@ -3556,16 +3846,16 @@ private let initializationResult: InitializationResult = {
if (uniffi_gotcha_core_checksum_method_gotchacore_issue_filters_active() != 51470) {
return InitializationResult.apiChecksumMismatch
}
if (uniffi_gotcha_core_checksum_method_gotchacore_issues() != 1316) {
if (uniffi_gotcha_core_checksum_method_gotchacore_issues() != 8566) {
return InitializationResult.apiChecksumMismatch
}
if (uniffi_gotcha_core_checksum_method_gotchacore_milestone() != 26979) {
if (uniffi_gotcha_core_checksum_method_gotchacore_milestone() != 57186) {
return InitializationResult.apiChecksumMismatch
}
if (uniffi_gotcha_core_checksum_method_gotchacore_milestones() != 27282) {
if (uniffi_gotcha_core_checksum_method_gotchacore_milestones() != 50431) {
return InitializationResult.apiChecksumMismatch
}
if (uniffi_gotcha_core_checksum_method_gotchacore_pull() != 46939) {
if (uniffi_gotcha_core_checksum_method_gotchacore_pull() != 12919) {
return InitializationResult.apiChecksumMismatch
}
if (uniffi_gotcha_core_checksum_method_gotchacore_pull_diff() != 61384) {
@@ -3577,10 +3867,10 @@ private let initializationResult: InitializationResult = {
if (uniffi_gotcha_core_checksum_method_gotchacore_pull_filters_active() != 20260) {
return InitializationResult.apiChecksumMismatch
}
if (uniffi_gotcha_core_checksum_method_gotchacore_pulls() != 13092) {
if (uniffi_gotcha_core_checksum_method_gotchacore_pulls() != 37027) {
return InitializationResult.apiChecksumMismatch
}
if (uniffi_gotcha_core_checksum_method_gotchacore_repositories() != 12256) {
if (uniffi_gotcha_core_checksum_method_gotchacore_repositories() != 31164) {
return InitializationResult.apiChecksumMismatch
}
if (uniffi_gotcha_core_checksum_method_gotchacore_repository_contents() != 8454) {

View File

@@ -286,17 +286,17 @@ uint64_t uniffi_gotcha_core_fn_method_gotchacore_commit_files(uint64_t ptr, Rust
#endif
#ifndef UNIFFI_FFIDEF_UNIFFI_GOTCHA_CORE_FN_METHOD_GOTCHACORE_COMMITS
#define UNIFFI_FFIDEF_UNIFFI_GOTCHA_CORE_FN_METHOD_GOTCHACORE_COMMITS
uint64_t uniffi_gotcha_core_fn_method_gotchacore_commits(uint64_t ptr, RustBuffer owner, RustBuffer repository, RustBuffer branch
uint64_t uniffi_gotcha_core_fn_method_gotchacore_commits(uint64_t ptr, RustBuffer owner, RustBuffer repository, RustBuffer branch, uint32_t pages
);
#endif
#ifndef UNIFFI_FFIDEF_UNIFFI_GOTCHA_CORE_FN_METHOD_GOTCHACORE_HOME
#define UNIFFI_FFIDEF_UNIFFI_GOTCHA_CORE_FN_METHOD_GOTCHACORE_HOME
uint64_t uniffi_gotcha_core_fn_method_gotchacore_home(uint64_t ptr
uint64_t uniffi_gotcha_core_fn_method_gotchacore_home(uint64_t ptr, uint32_t page
);
#endif
#ifndef UNIFFI_FFIDEF_UNIFFI_GOTCHA_CORE_FN_METHOD_GOTCHACORE_ISSUE
#define UNIFFI_FFIDEF_UNIFFI_GOTCHA_CORE_FN_METHOD_GOTCHACORE_ISSUE
uint64_t uniffi_gotcha_core_fn_method_gotchacore_issue(uint64_t ptr, RustBuffer owner, RustBuffer repository, int64_t number
uint64_t uniffi_gotcha_core_fn_method_gotchacore_issue(uint64_t ptr, RustBuffer owner, RustBuffer repository, int64_t number, uint32_t page
);
#endif
#ifndef UNIFFI_FFIDEF_UNIFFI_GOTCHA_CORE_FN_METHOD_GOTCHACORE_ISSUE_EDITOR
@@ -316,22 +316,22 @@ int8_t uniffi_gotcha_core_fn_method_gotchacore_issue_filters_active(uint64_t ptr
#endif
#ifndef UNIFFI_FFIDEF_UNIFFI_GOTCHA_CORE_FN_METHOD_GOTCHACORE_ISSUES
#define UNIFFI_FFIDEF_UNIFFI_GOTCHA_CORE_FN_METHOD_GOTCHACORE_ISSUES
uint64_t uniffi_gotcha_core_fn_method_gotchacore_issues(uint64_t ptr, RustBuffer owner, RustBuffer repository
uint64_t uniffi_gotcha_core_fn_method_gotchacore_issues(uint64_t ptr, RustBuffer owner, RustBuffer repository, uint32_t page
);
#endif
#ifndef UNIFFI_FFIDEF_UNIFFI_GOTCHA_CORE_FN_METHOD_GOTCHACORE_MILESTONE
#define UNIFFI_FFIDEF_UNIFFI_GOTCHA_CORE_FN_METHOD_GOTCHACORE_MILESTONE
uint64_t uniffi_gotcha_core_fn_method_gotchacore_milestone(uint64_t ptr, RustBuffer owner, RustBuffer repository, int64_t id
uint64_t uniffi_gotcha_core_fn_method_gotchacore_milestone(uint64_t ptr, RustBuffer owner, RustBuffer repository, int64_t id, uint32_t page
);
#endif
#ifndef UNIFFI_FFIDEF_UNIFFI_GOTCHA_CORE_FN_METHOD_GOTCHACORE_MILESTONES
#define UNIFFI_FFIDEF_UNIFFI_GOTCHA_CORE_FN_METHOD_GOTCHACORE_MILESTONES
uint64_t uniffi_gotcha_core_fn_method_gotchacore_milestones(uint64_t ptr, RustBuffer owner, RustBuffer repository
uint64_t uniffi_gotcha_core_fn_method_gotchacore_milestones(uint64_t ptr, RustBuffer owner, RustBuffer repository, uint32_t page
);
#endif
#ifndef UNIFFI_FFIDEF_UNIFFI_GOTCHA_CORE_FN_METHOD_GOTCHACORE_PULL
#define UNIFFI_FFIDEF_UNIFFI_GOTCHA_CORE_FN_METHOD_GOTCHACORE_PULL
uint64_t uniffi_gotcha_core_fn_method_gotchacore_pull(uint64_t ptr, RustBuffer owner, RustBuffer repository, int64_t number
uint64_t uniffi_gotcha_core_fn_method_gotchacore_pull(uint64_t ptr, RustBuffer owner, RustBuffer repository, int64_t number, uint32_t page
);
#endif
#ifndef UNIFFI_FFIDEF_UNIFFI_GOTCHA_CORE_FN_METHOD_GOTCHACORE_PULL_DIFF
@@ -351,12 +351,12 @@ int8_t uniffi_gotcha_core_fn_method_gotchacore_pull_filters_active(uint64_t ptr,
#endif
#ifndef UNIFFI_FFIDEF_UNIFFI_GOTCHA_CORE_FN_METHOD_GOTCHACORE_PULLS
#define UNIFFI_FFIDEF_UNIFFI_GOTCHA_CORE_FN_METHOD_GOTCHACORE_PULLS
uint64_t uniffi_gotcha_core_fn_method_gotchacore_pulls(uint64_t ptr
uint64_t uniffi_gotcha_core_fn_method_gotchacore_pulls(uint64_t ptr, uint32_t page
);
#endif
#ifndef UNIFFI_FFIDEF_UNIFFI_GOTCHA_CORE_FN_METHOD_GOTCHACORE_REPOSITORIES
#define UNIFFI_FFIDEF_UNIFFI_GOTCHA_CORE_FN_METHOD_GOTCHACORE_REPOSITORIES
uint64_t uniffi_gotcha_core_fn_method_gotchacore_repositories(uint64_t ptr
uint64_t uniffi_gotcha_core_fn_method_gotchacore_repositories(uint64_t ptr, uint32_t page
);
#endif
#ifndef UNIFFI_FFIDEF_UNIFFI_GOTCHA_CORE_FN_METHOD_GOTCHACORE_REPOSITORY_CONTENTS

View File

@@ -8,6 +8,7 @@ final class IssuesViewController: RefreshingTableViewController {
private var rows: [IssueRow] = []
private var filterOptions: IssueFilterOptions?
private var filterTask: Task<Void, Never>?
private var currentPage: UInt32 = 0
private lazy var filterButton = UIBarButtonItem(
image: context.symbol("line.3.horizontal.decrease.circle")
)
@@ -45,17 +46,38 @@ final class IssuesViewController: RefreshingTableViewController {
}
private func loadIssues(refreshing: Bool) {
beginLoading(refreshing: refreshing)
loadIssues(page: 1, refreshing: refreshing)
}
override func loadMoreContent() {
loadIssues(page: currentPage + 1, refreshing: false)
}
private func loadIssues(page: UInt32, refreshing: Bool) {
if page == 1 {
resetPagination()
beginLoading(refreshing: refreshing)
}
loadingTask?.cancel()
loadingTask = Task {
do {
rows = try await context.core.issues(owner: owner, repository: repository)
let result = try await context.core.issues(
owner: owner,
repository: repository,
page: page
)
if page == 1 { rows = result.rows } else { rows.append(contentsOf: result.rows) }
currentPage = page
finishPagination(hasMore: result.hasMore)
tableView.reloadData()
updateEmptyState()
} catch {
if !Task.isCancelled { show(error: error) }
if !Task.isCancelled {
show(error: error)
failPagination()
}
}
endLoading()
if page == 1 { endLoading() }
}
}
@@ -335,6 +357,7 @@ final class MilestonesViewController: RefreshingTableViewController {
private let owner: String
private let repository: String
private var rows: [MilestoneRow] = []
private var currentPage: UInt32 = 0
init(context: AppContext, owner: String, repository: String) {
self.context = context
@@ -356,11 +379,29 @@ final class MilestonesViewController: RefreshingTableViewController {
}
override func loadContent(refreshing: Bool) {
beginLoading(refreshing: refreshing)
loadPage(1, refreshing: refreshing)
}
override func loadMoreContent() {
loadPage(currentPage + 1, refreshing: false)
}
private func loadPage(_ page: UInt32, refreshing: Bool) {
if page == 1 {
resetPagination()
beginLoading(refreshing: refreshing)
}
loadingTask?.cancel()
loadingTask = Task {
do {
rows = try await context.core.milestones(owner: owner, repository: repository)
let result = try await context.core.milestones(
owner: owner,
repository: repository,
page: page
)
if page == 1 { rows = result.rows } else { rows.append(contentsOf: result.rows) }
currentPage = page
finishPagination(hasMore: result.hasMore)
tableView.reloadData()
tableView.backgroundView = rows.isEmpty
? EmptyBackgroundView(
@@ -369,9 +410,12 @@ final class MilestonesViewController: RefreshingTableViewController {
)
: nil
} catch {
if !Task.isCancelled { show(error: error) }
if !Task.isCancelled {
show(error: error)
failPagination()
}
}
endLoading()
if page == 1 { endLoading() }
}
}
@@ -412,6 +456,7 @@ final class MilestoneViewController: RefreshingTableViewController {
private let repository: String
private let id: Int64
private var page: MilestonePage?
private var currentPage: UInt32 = 0
init(context: AppContext, owner: String, repository: String, id: Int64) {
self.context = context
@@ -435,21 +480,45 @@ final class MilestoneViewController: RefreshingTableViewController {
}
override func loadContent(refreshing: Bool) {
beginLoading(refreshing: refreshing)
loadPage(1, refreshing: refreshing)
}
override func loadMoreContent() {
loadPage(currentPage + 1, refreshing: false)
}
private func loadPage(_ requestedPage: UInt32, refreshing: Bool) {
if requestedPage == 1 {
resetPagination()
beginLoading(refreshing: refreshing)
}
loadingTask?.cancel()
loadingTask = Task {
do {
page = try await context.core.milestone(
let result = try await context.core.milestone(
owner: owner,
repository: repository,
id: id
id: id,
page: requestedPage
)
if requestedPage == 1 {
page = result
} else {
page?.issues.append(contentsOf: result.issues)
page?.pulls.append(contentsOf: result.pulls)
page?.hasMore = result.hasMore
}
currentPage = requestedPage
finishPagination(hasMore: result.hasMore)
title = page?.milestone.title
tableView.reloadData()
} catch {
if !Task.isCancelled { show(error: error) }
if !Task.isCancelled {
show(error: error)
failPagination()
}
}
endLoading()
if requestedPage == 1 { endLoading() }
}
}
@@ -584,6 +653,7 @@ final class PullsViewController: RefreshingTableViewController {
private var rows: [PullRow] = []
private var filterOptions: PullFilterOptions?
private var filterTask: Task<Void, Never>?
private var currentPage: UInt32 = 0
init(context: AppContext) {
self.context = context
@@ -626,11 +696,25 @@ final class PullsViewController: RefreshingTableViewController {
}
private func loadPulls(refreshing: Bool) {
beginLoading(refreshing: refreshing)
loadPulls(page: 1, refreshing: refreshing)
}
override func loadMoreContent() {
loadPulls(page: currentPage + 1, refreshing: false)
}
private func loadPulls(page: UInt32, refreshing: Bool) {
if page == 1 {
resetPagination()
beginLoading(refreshing: refreshing)
}
loadingTask?.cancel()
loadingTask = Task {
do {
rows = try await context.core.pulls()
let result = try await context.core.pulls(page: page)
if page == 1 { rows = result.rows } else { rows.append(contentsOf: result.rows) }
currentPage = page
finishPagination(hasMore: result.hasMore)
tableView.reloadData()
let status = context.core.settings().pullStatus
tableView.backgroundView = rows.isEmpty
@@ -642,9 +726,12 @@ final class PullsViewController: RefreshingTableViewController {
)
: nil
} catch {
if !Task.isCancelled { show(error: error) }
if !Task.isCancelled {
show(error: error)
failPagination()
}
}
endLoading()
if page == 1 { endLoading() }
}
}
@@ -783,6 +870,7 @@ final class CommitsViewController: RefreshingTableViewController {
private var contents: [RepositoryContentRow] = []
private var branch: String?
private var mode = Mode.history
private var currentPage: UInt32 = 0
init(context: AppContext, owner: String, repository: String) {
self.context = context
@@ -808,7 +896,19 @@ final class CommitsViewController: RefreshingTableViewController {
}
override func loadContent(refreshing: Bool) {
beginLoading(refreshing: refreshing)
loadPage(1, refreshing: refreshing)
}
override func loadMoreContent() {
guard mode == .history else { return }
loadPage(currentPage + 1, refreshing: false)
}
private func loadPage(_ requestedPage: UInt32, refreshing: Bool) {
if requestedPage == 1 {
resetPagination()
beginLoading(refreshing: refreshing)
}
loadingTask?.cancel()
loadingTask = Task {
do {
@@ -817,8 +917,11 @@ final class CommitsViewController: RefreshingTableViewController {
page = try await context.core.commits(
owner: owner,
repository: repository,
branch: branch
branch: branch,
pages: requestedPage
)
currentPage = requestedPage
finishPagination(hasMore: page?.hasMore ?? false)
updateBranchMenu()
case .files:
contents = try await context.core.repositoryContents(
@@ -830,9 +933,12 @@ final class CommitsViewController: RefreshingTableViewController {
tableView.reloadData()
updateEmptyView()
} catch {
if !Task.isCancelled { show(error: error) }
if !Task.isCancelled {
show(error: error)
failPagination()
}
}
endLoading()
if requestedPage == 1 { endLoading() }
}
}

View File

@@ -5,12 +5,18 @@ import SwiftUI
import UIKit
@MainActor
class MarkdownPageViewController: UIViewController {
class MarkdownPageViewController: UIViewController, UIScrollViewDelegate {
let context: AppContext
let scrollView = UIScrollView()
let stack = UIStackView()
private let spinner = UIActivityIndicatorView(style: .medium)
var loadingTask: Task<Void, Never>?
private lazy var moreButton = UIButton(
configuration: .plain(),
primaryAction: UIAction { [weak self] _ in self?.requestMoreContent() }
)
private var hasMoreContent = false
private var loadingMore = false
init(context: AppContext) {
self.context = context
@@ -25,6 +31,7 @@ class MarkdownPageViewController: UIViewController {
view.backgroundColor = .systemGroupedBackground
scrollView.translatesAutoresizingMaskIntoConstraints = false
scrollView.alwaysBounceVertical = true
scrollView.delegate = self
scrollView.refreshControl = UIRefreshControl()
scrollView.refreshControl?.addTarget(self, action: #selector(refreshRequested), for: .valueChanged)
view.addSubview(scrollView)
@@ -49,6 +56,8 @@ class MarkdownPageViewController: UIViewController {
func loadContent(refreshing: Bool) {}
func loadMoreContent() {}
func beginLoading(refreshing: Bool) {
if !refreshing { beginNavigationLoading(spinner) }
}
@@ -63,6 +72,54 @@ class MarkdownPageViewController: UIViewController {
views.forEach(stack.addArrangedSubview)
}
func resetPagination() {
hasMoreContent = false
loadingMore = false
stack.removeArrangedSubview(moreButton)
moreButton.removeFromSuperview()
}
func finishPagination(hasMore: Bool) {
hasMoreContent = hasMore
loadingMore = false
guard hasMore else { return }
var configuration = moreButton.configuration
configuration?.title = "Pull up or tap to load more"
configuration?.showsActivityIndicator = false
moreButton.configuration = configuration
moreButton.accessibilityLabel = "Load more results"
if moreButton.superview !== stack { stack.addArrangedSubview(moreButton) }
if moreButton.constraints.isEmpty {
moreButton.heightAnchor.constraint(equalToConstant: 50).isActive = true
}
}
func failPagination() {
loadingMore = false
finishPagination(hasMore: hasMoreContent)
}
func scrollViewDidScroll(_ scrollView: UIScrollView) {
guard scrollView.isDragging, hasMoreContent, !loadingMore else { return }
let bottom = max(
-scrollView.adjustedContentInset.top,
scrollView.contentSize.height
+ scrollView.adjustedContentInset.bottom
- scrollView.bounds.height
)
if scrollView.contentOffset.y > bottom + 60 { requestMoreContent() }
}
private func requestMoreContent() {
guard hasMoreContent, !loadingMore else { return }
loadingMore = true
var configuration = moreButton.configuration
configuration?.title = "Loading more…"
configuration?.showsActivityIndicator = true
moreButton.configuration = configuration
loadMoreContent()
}
@objc private func refreshRequested() {
loadContent(refreshing: true)
}
@@ -73,6 +130,8 @@ final class IssueViewController: MarkdownPageViewController {
private let owner: String
private let repository: String
private let number: Int64
private var page: IssuePage?
private var currentPage: UInt32 = 0
init(context: AppContext, owner: String, repository: String, number: Int64) {
self.owner = owner
@@ -110,27 +169,52 @@ final class IssueViewController: MarkdownPageViewController {
}
override func loadContent(refreshing: Bool) {
beginLoading(refreshing: refreshing)
loadPage(1, refreshing: refreshing)
}
override func loadMoreContent() {
loadPage(currentPage + 1, 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.issue(
let result = try await context.core.issue(
owner: owner,
repository: repository,
number: number
number: number,
page: requestedPage
)
replaceContent(detailViews(
title: page.title,
issueState: page.state,
meta: page.meta,
body: page.body,
comments: page.comments,
milestone: page.milestone
))
if requestedPage == 1 {
page = result
} else {
page?.comments.append(contentsOf: result.comments)
page?.hasMore = result.hasMore
}
currentPage = requestedPage
if let page {
replaceContent(detailViews(
title: page.title,
issueState: page.state,
meta: page.meta,
body: page.body,
comments: page.comments,
milestone: page.milestone
))
finishPagination(hasMore: page.hasMore)
}
} catch {
if !Task.isCancelled { show(error: error) }
if !Task.isCancelled {
show(error: error)
failPagination()
}
}
endLoading()
if requestedPage == 1 { endLoading() }
}
}
}
@@ -140,6 +224,8 @@ final class PullViewController: MarkdownPageViewController {
private let owner: String
private let repository: String
private let number: Int64
private var page: PullPage?
private var currentPage: UInt32 = 0
init(context: AppContext, owner: String, repository: String, number: Int64) {
self.owner = owner
@@ -158,24 +244,45 @@ final class PullViewController: MarkdownPageViewController {
}
override func loadContent(refreshing: Bool) {
beginLoading(refreshing: refreshing)
loadPage(1, refreshing: refreshing)
}
override func loadMoreContent() {
loadPage(currentPage + 1, 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.pull(
let result = try await context.core.pull(
owner: owner,
repository: repository,
number: number
number: number,
page: requestedPage
)
var views = detailViews(
title: page.title,
meta: page.meta,
body: page.body,
comments: []
)
if !page.files.isEmpty {
views.append(sectionLabel(page.filesRef))
views.append(contentsOf: page.files.map { file in
if requestedPage == 1 {
page = result
} else {
page?.files.append(contentsOf: result.files)
page?.comments.append(contentsOf: result.comments)
page?.hasMore = result.hasMore
}
currentPage = requestedPage
if let page {
var views = detailViews(
title: page.title,
meta: page.meta,
body: page.body,
comments: []
)
if !page.files.isEmpty {
views.append(sectionLabel(page.filesRef))
views.append(contentsOf: page.files.map { file in
detailButton(title: file.path, detail: file.status) { [weak self] in
guard let self else { return }
self.navigationController?.pushViewController(
@@ -191,14 +298,19 @@ final class PullViewController: MarkdownPageViewController {
animated: true
)
}
})
})
}
views.append(contentsOf: commentViews(page.comments))
replaceContent(views)
finishPagination(hasMore: page.hasMore)
}
views.append(contentsOf: commentViews(page.comments))
replaceContent(views)
} catch {
if !Task.isCancelled { show(error: error) }
if !Task.isCancelled {
show(error: error)
failPagination()
}
}
endLoading()
if requestedPage == 1 { endLoading() }
}
}
}

View File

@@ -188,6 +188,7 @@ final class RepositoriesViewController: RefreshingTableViewController {
private let context: AppContext
private let mode: RepositoryMode
private var rows: [RepositoryRow] = []
private var currentPage: UInt32 = 0
init(context: AppContext, mode: RepositoryMode) {
self.context = context
@@ -215,11 +216,25 @@ final class RepositoriesViewController: RefreshingTableViewController {
}
override func loadContent(refreshing: Bool) {
beginLoading(refreshing: refreshing)
loadPage(1, refreshing: refreshing)
}
override func loadMoreContent() {
loadPage(currentPage + 1, refreshing: false)
}
private func loadPage(_ page: UInt32, refreshing: Bool) {
if page == 1 {
resetPagination()
beginLoading(refreshing: refreshing)
}
loadingTask?.cancel()
loadingTask = Task {
do {
rows = try await context.core.repositories()
let result = try await context.core.repositories(page: page)
rows = result.rows
currentPage = page
finishPagination(hasMore: result.hasMore)
tableView.reloadData()
tableView.backgroundView = rows.isEmpty
? EmptyBackgroundView(
@@ -228,9 +243,12 @@ final class RepositoriesViewController: RefreshingTableViewController {
)
: nil
} catch {
if !Task.isCancelled { show(error: error) }
if !Task.isCancelled {
show(error: error)
failPagination()
}
}
endLoading()
if page == 1 { endLoading() }
}
}
@@ -308,6 +326,7 @@ final class HomeViewController: RefreshingTableViewController {
private let context: AppContext
private var page: HomePage?
private var filter = ActivityFilter.all
private var currentPage: UInt32 = 0
private var activities: [ActivityRow] {
guard let page else { return [] }
@@ -358,24 +377,48 @@ final class HomeViewController: RefreshingTableViewController {
refreshControl?.endRefreshing()
return
}
beginLoading(refreshing: refreshing)
loadPage(1, refreshing: refreshing)
}
override func loadMoreContent() {
loadPage(currentPage + 1, refreshing: false)
}
private func loadPage(_ requestedPage: UInt32, refreshing: Bool) {
if requestedPage == 1 {
resetPagination()
beginLoading(refreshing: refreshing)
}
loadingTask?.cancel()
loadingTask = Task {
do {
page = try await context.core.home()
let result = try await context.core.home(page: requestedPage)
if requestedPage == 1 {
page = result
} else {
page?.activities.append(contentsOf: result.activities)
page?.issueActivities.append(contentsOf: result.issueActivities)
page?.pullActivities.append(contentsOf: result.pullActivities)
page?.hasMore = result.hasMore
}
currentPage = requestedPage
finishPagination(hasMore: result.hasMore)
title = page?.serverName
tableView.tableHeaderView = page.map { page in
if requestedPage == 1 { tableView.tableHeaderView = page.map { page in
HeatmapView(page: page, selectedFilter: filter.rawValue) { [weak self] index in
guard let self, let filter = ActivityFilter(rawValue: index) else { return }
self.filter = filter
self.updateActivities()
}
}
} }
updateActivities()
} catch {
if !Task.isCancelled { show(error: error) }
if !Task.isCancelled {
show(error: error)
failPagination()
}
}
endLoading()
if requestedPage == 1 { endLoading() }
}
}

View File

@@ -29,6 +29,12 @@ extension UIViewController {
class RefreshingTableViewController: UITableViewController {
private let spinner = UIActivityIndicatorView(style: .medium)
var loadingTask: Task<Void, Never>?
private lazy var moreButton = UIButton(
configuration: .plain(),
primaryAction: UIAction { [weak self] _ in self?.requestMoreContent() }
)
private var hasMoreContent = false
private var loadingMore = false
init() {
super.init(style: .plain)
@@ -46,6 +52,8 @@ class RefreshingTableViewController: UITableViewController {
func loadContent(refreshing: Bool) {}
func loadMoreContent() {}
func beginLoading(refreshing: Bool) {
if !refreshing { beginNavigationLoading(spinner) }
}
@@ -55,6 +63,58 @@ class RefreshingTableViewController: UITableViewController {
refreshControl?.endRefreshing()
}
func resetPagination() {
hasMoreContent = false
loadingMore = false
tableView.tableFooterView = nil
}
func finishPagination(hasMore: Bool) {
hasMoreContent = hasMore
loadingMore = false
var configuration = moreButton.configuration
configuration?.title = "Pull up or tap to load more"
configuration?.showsActivityIndicator = false
moreButton.configuration = configuration
moreButton.accessibilityLabel = "Load more results"
tableView.tableFooterView = hasMore ? paginationFooter() : nil
}
func failPagination() {
loadingMore = false
finishPagination(hasMore: hasMoreContent)
}
override func scrollViewDidScroll(_ scrollView: UIScrollView) {
guard scrollView.isDragging, hasMoreContent, !loadingMore else { return }
let bottom = max(
-scrollView.adjustedContentInset.top,
scrollView.contentSize.height
+ scrollView.adjustedContentInset.bottom
- scrollView.bounds.height
)
if scrollView.contentOffset.y > bottom + 60 { requestMoreContent() }
}
private func requestMoreContent() {
guard hasMoreContent, !loadingMore else { return }
loadingMore = true
var configuration = moreButton.configuration
configuration?.title = "Loading more…"
configuration?.showsActivityIndicator = true
moreButton.configuration = configuration
moreButton.accessibilityLabel = "Loading more results"
loadMoreContent()
}
private func paginationFooter() -> UIView {
let footer = UIView(frame: CGRect(x: 0, y: 0, width: tableView.bounds.width, height: 56))
moreButton.frame = footer.bounds
moreButton.autoresizingMask = [.flexibleWidth, .flexibleHeight]
footer.addSubview(moreButton)
return footer
}
@objc private func refreshRequested() {
loadContent(refreshing: true)
}