Add full-server activity to iOS home (#65)

This commit is contained in:
Georg Bauer
2026-08-15 17:25:25 +02:00
parent 6d271e7402
commit f84ab774e6
15 changed files with 492 additions and 15 deletions

View File

@@ -1,3 +1,5 @@
use std::collections::VecDeque;
use crate::{
Client, Error, Result,
domain::{DEFAULT_PAGE_SIZE, HomeData, Page},
@@ -84,6 +86,18 @@ pub struct ActivityCommit {
pub timestamp: String,
}
pub struct ServerActivityPager {
configuration: apis::configuration::Configuration,
feeds: Vec<UserActivityFeed>,
}
struct UserActivityFeed {
login: String,
activities: VecDeque<models::Activity>,
next_page: i32,
complete: bool,
}
impl Client {
pub async fn activities(
&self,
@@ -123,6 +137,101 @@ impl Client {
next_page: activities.has_more.then_some(page + 1),
})
}
pub async fn server_activity_pager(&self) -> Result<ServerActivityPager> {
let configuration = self.configuration();
let users = activity_users(&configuration).await?;
Ok(ServerActivityPager {
configuration,
feeds: users
.into_iter()
.map(|login| UserActivityFeed {
login,
activities: VecDeque::new(),
next_page: 1,
complete: false,
})
.collect(),
})
}
}
impl ServerActivityPager {
pub async fn next_page(&mut self) -> Result<Page<models::Activity>> {
let mut items = Vec::with_capacity(DEFAULT_PAGE_SIZE as usize);
while items.len() < DEFAULT_PAGE_SIZE as usize {
for feed in &mut self.feeds {
feed.fill(&self.configuration).await?;
}
let Some(feed) = self.feeds.iter_mut().max_by(|left, right| {
activity_order(left.activities.front(), right.activities.front())
}) else {
break;
};
let Some(activity) = feed.activities.pop_front() else {
break;
};
items.push(activity);
}
let has_more = self
.feeds
.iter()
.any(|feed| !feed.complete || !feed.activities.is_empty());
Ok(Page { items, has_more })
}
}
impl UserActivityFeed {
async fn fill(&mut self, configuration: &apis::configuration::Configuration) -> Result<()> {
if self.complete || !self.activities.is_empty() {
return Ok(());
}
let mut activities = activity_page(configuration, &self.login, self.next_page).await?;
self.complete = activities.len() < DEFAULT_PAGE_SIZE as usize;
self.next_page = self
.next_page
.checked_add(1)
.ok_or_else(|| Error::InvalidInput("activity feed has too many pages".into()))?;
activities.sort_unstable_by(|left, right| activity_order(Some(right), Some(left)));
self.activities = activities.into();
Ok(())
}
}
fn activity_order(
left: Option<&models::Activity>,
right: Option<&models::Activity>,
) -> std::cmp::Ordering {
left.map(|activity| (&activity.created, &activity.id))
.cmp(&right.map(|activity| (&activity.created, &activity.id)))
}
async fn activity_users(configuration: &apis::configuration::Configuration) -> Result<Vec<String>> {
let mut users = Vec::new();
let mut page = 1;
loop {
let response = apis::user_api::user_search(
configuration,
None,
None,
Some(page),
Some(DEFAULT_PAGE_SIZE),
)
.await
.map_err(Error::generated)?;
if response.ok == Some(false) {
return Err(Error::Generated("The server user search failed.".into()));
}
let batch = response.data.unwrap_or_default();
let complete = batch.len() < DEFAULT_PAGE_SIZE as usize;
users.extend(batch.into_iter().filter_map(|user| user.login));
if complete {
return Ok(users);
}
page = page
.checked_add(1)
.ok_or_else(|| Error::InvalidInput("user search has too many pages".into()))?;
}
}
async fn filtered_activity_page(
@@ -419,4 +528,42 @@ mod tests {
2
);
}
#[tokio::test]
async fn merges_server_activity_incrementally_in_global_pages() {
let mut feeds = [VecDeque::new(), VecDeque::new()];
for id in 1..=65 {
feeds[id as usize % 2].push_front(models::Activity {
id: Some(id),
created: Some(format!("2026-08-15T12:{:02}:{:02}Z", id / 60, id % 60)),
..Default::default()
});
}
let mut pager = ServerActivityPager {
configuration: Default::default(),
feeds: feeds
.into_iter()
.enumerate()
.map(|(index, activities)| UserActivityFeed {
login: format!("user-{index}"),
activities,
next_page: 1,
complete: true,
})
.collect(),
};
let first = pager.next_page().await.unwrap();
let second = pager.next_page().await.unwrap();
let third = pager.next_page().await.unwrap();
assert_eq!(first.items.first().and_then(|row| row.id), Some(65));
assert_eq!(first.items.last().and_then(|row| row.id), Some(36));
assert!(first.has_more);
assert_eq!(second.items.first().and_then(|row| row.id), Some(35));
assert_eq!(second.items.last().and_then(|row| row.id), Some(6));
assert!(second.has_more);
assert_eq!(third.items.len(), 5);
assert!(!third.has_more);
}
}

View File

@@ -23,7 +23,7 @@ mod pulls;
mod repositories;
pub use actions::{ActionJobLog, ActionRunDetails};
pub use activity::ActivityFilter;
pub use activity::{ActivityFilter, ServerActivityPager};
pub use config::{Config, Selection, ServerProfile, TuiPreferences, server_url};
pub use domain::{
CreateIssue, DEFAULT_PAGE_SIZE, EditIssue, HistoryCommit, HomeData, IssueDetails, IssueDraft,