Load filtered home activity streams

This commit is contained in:
Georg Bauer
2026-07-31 17:33:45 +02:00
parent 822cf1f769
commit dba6359d30
11 changed files with 258 additions and 118 deletions

View File

@@ -355,8 +355,12 @@ pub async fn load_pull_diff(
Ok(diff::parse_file(&diff, path))
}
pub async fn load_home(server: &Server, page: i32) -> Result<HomeData, String> {
client(server)?.home(page).await.map_err(message)
pub async fn load_home(
server: &Server,
page: i32,
filter: gotcha_gitea::ActivityFilter,
) -> Result<HomeData, String> {
client(server)?.home(page, filter).await.map_err(message)
}
fn client(server: &Server) -> Result<Client, String> {

View File

@@ -171,12 +171,16 @@ impl GotchaCore {
save_preferences(&state.preferences).map_err(Into::into)
}
pub async fn home(&self, page: u32) -> Result<HomePage, GotchaError> {
pub async fn home(
&self,
page: u32,
filter: HomeActivityFilter,
) -> Result<HomePage, GotchaError> {
let server = self.server()?;
let name = server.name.clone();
Ok(home_page(
name,
load_home(&server, valid_page(page)?).await?,
load_home(&server, valid_page(page)?, filter.into()).await?,
))
}

View File

@@ -245,6 +245,23 @@ pub struct ActivityRow {
pub sha: String,
}
#[derive(Clone, Copy, Debug, uniffi::Enum)]
pub enum HomeActivityFilter {
All,
Issues,
PullRequests,
}
impl From<HomeActivityFilter> for gotcha_gitea::ActivityFilter {
fn from(filter: HomeActivityFilter) -> Self {
match filter {
HomeActivityFilter::All => Self::All,
HomeActivityFilter::Issues => Self::Issues,
HomeActivityFilter::PullRequests => Self::PullRequests,
}
}
}
#[derive(Clone, uniffi::Record)]
pub struct HeatCell {
pub level: u32,
@@ -255,11 +272,9 @@ pub struct HeatCell {
pub struct HomePage {
pub server_name: String,
pub activities: Vec<ActivityRow>,
pub issue_activities: Vec<ActivityRow>,
pub pull_activities: Vec<ActivityRow>,
pub heat_cells: Vec<HeatCell>,
pub contribution_count: i64,
pub has_more: bool,
pub next_page: Option<u32>,
}
pub fn repository_rows(
@@ -776,52 +791,13 @@ pub fn home_page(server_name: String, home: HomeData) -> HomePage {
let (heat_cells, contribution_count) = heat_cells(&home.heatmap);
HomePage {
server_name,
issue_activities: home
.activities
.iter()
.filter(|activity| is_issue_activity(activity))
.map(activity_row)
.collect(),
pull_activities: home
.activities
.iter()
.filter(|activity| is_pull_activity(activity))
.map(activity_row)
.collect(),
activities: home.activities.iter().map(activity_row).collect(),
heat_cells,
contribution_count,
has_more: home.has_more,
next_page: home.next_page.map(|page| page as u32),
}
}
fn is_issue_activity(activity: &models::Activity) -> bool {
use models::activity::OpType;
matches!(
activity.op_type,
Some(OpType::CreateIssue | OpType::CloseIssue | OpType::ReopenIssue | OpType::CommentIssue)
)
}
fn is_pull_activity(activity: &models::Activity) -> bool {
use models::activity::OpType;
matches!(
activity.op_type,
Some(
OpType::CreatePullRequest
| OpType::MergePullRequest
| OpType::AutoMergePullRequest
| OpType::ClosePullRequest
| OpType::ReopenPullRequest
| OpType::CommentPull
| OpType::ApprovePullRequest
| OpType::RejectPullRequest
)
)
}
fn activity_row(activity: &models::Activity) -> ActivityRow {
use models::activity::OpType;
@@ -1263,31 +1239,26 @@ mod tests {
}
#[test]
fn categorizes_home_activity_before_crossing_the_bridge() {
fn presents_filtered_home_activity_and_continuation() {
use models::activity::OpType;
let page = home_page(
"Gitea".into(),
HomeData {
activities: [
OpType::CreateIssue,
OpType::CreatePullRequest,
OpType::CreateRepo,
]
.into_iter()
.map(|op_type| models::Activity {
op_type: Some(op_type),
..Default::default()
})
.collect(),
activities: [OpType::CreatePullRequest, OpType::ClosePullRequest]
.into_iter()
.map(|op_type| models::Activity {
op_type: Some(op_type),
..Default::default()
})
.collect(),
heatmap: Vec::new(),
has_more: false,
next_page: Some(4),
},
);
assert_eq!(page.activities.len(), 3);
assert_eq!(page.issue_activities.len(), 1);
assert_eq!(page.pull_activities.len(), 1);
assert_eq!(page.activities.len(), 2);
assert_eq!(page.next_page, Some(4));
}
#[test]

View File

@@ -5,6 +5,48 @@ use crate::{
};
use gitea_openapi::apis;
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
pub enum ActivityFilter {
#[default]
All,
Issues,
PullRequests,
}
impl ActivityFilter {
fn matches(self, activity: &models::Activity) -> bool {
use models::activity::OpType;
match self {
Self::All => true,
Self::Issues => matches!(
activity.op_type,
Some(
OpType::CreateIssue
| OpType::CloseIssue
| OpType::ReopenIssue
| OpType::CommentIssue
)
),
Self::PullRequests => matches!(
activity.op_type,
Some(
OpType::CreatePullRequest
| OpType::MergePullRequest
| OpType::AutoMergePullRequest
| OpType::ClosePullRequest
| OpType::ReopenPullRequest
| OpType::CommentPull
| OpType::ApprovePullRequest
| OpType::RejectPullRequest
| OpType::PullReviewDismissed
| OpType::PullRequestReadyForReview
)
),
}
}
}
#[derive(Debug, Eq, PartialEq)]
pub enum Target {
Repository {
@@ -29,7 +71,7 @@ pub enum Target {
}
impl Client {
pub async fn home(&self, page: i32) -> Result<HomeData> {
pub async fn home(&self, page: i32, filter: ActivityFilter) -> Result<HomeData> {
if page < 1 {
return Err(Error::InvalidInput("page must be positive".into()));
}
@@ -39,27 +81,51 @@ impl Client {
.await?
.login
.ok_or_else(|| Error::Generated("The server account has no username.".into()))?;
let activities = apis::user_api::user_list_activity_feeds(
&configuration,
&login,
Some(true),
None,
Some(page),
Some(DEFAULT_PAGE_SIZE),
);
let activities = activity_page(&configuration, &login, page);
let (activities, heatmap) = tokio::join!(
activities,
apis::user_api::user_get_heatmap_data(&configuration, &login),
);
let activities = activities.map_err(Error::generated)?;
Ok(HomeData {
has_more: activities.len() == DEFAULT_PAGE_SIZE as usize,
activities,
heatmap: heatmap.map_err(Error::generated)?,
})
let mut activities = activities?;
let heatmap = heatmap.map_err(Error::generated)?;
let mut page = page;
loop {
let has_more = activities.len() == DEFAULT_PAGE_SIZE as usize;
let filtered: Vec<_> = activities
.into_iter()
.filter(|activity| filter.matches(activity))
.collect();
if filter == ActivityFilter::All || !filtered.is_empty() || !has_more {
return Ok(HomeData {
activities: filtered,
heatmap,
next_page: has_more.then_some(page + 1),
});
}
page += 1;
activities = activity_page(&configuration, &login, page).await?;
}
}
}
async fn activity_page(
configuration: &apis::configuration::Configuration,
login: &str,
page: i32,
) -> Result<Vec<models::Activity>> {
apis::user_api::user_list_activity_feeds(
configuration,
login,
Some(true),
None,
Some(page),
Some(DEFAULT_PAGE_SIZE),
)
.await
.map_err(Error::generated)
}
pub fn target(activity: &models::Activity) -> Option<Target> {
use models::activity::OpType;
@@ -205,4 +271,29 @@ mod tests {
})
);
}
#[test]
fn filters_issue_and_pull_request_activity() {
let activities = [
activity(OpType::CreateIssue, ""),
activity(OpType::CreatePullRequest, ""),
activity(OpType::PullRequestReadyForReview, ""),
activity(OpType::CreateRepo, ""),
];
assert_eq!(
activities
.iter()
.filter(|activity| ActivityFilter::Issues.matches(activity))
.count(),
1
);
assert_eq!(
activities
.iter()
.filter(|activity| ActivityFilter::PullRequests.matches(activity))
.count(),
2
);
}
}

View File

@@ -164,7 +164,7 @@ pub struct HistoryCommit {
pub struct HomeData {
pub activities: Vec<models::Activity>,
pub heatmap: Vec<models::UserHeatmapData>,
pub has_more: bool,
pub next_page: Option<i32>,
}
pub type PullRefs = HashMap<String, Vec<String>>;

View File

@@ -19,6 +19,7 @@ mod milestones;
mod pulls;
mod repositories;
pub use activity::ActivityFilter;
pub use domain::{
CreateIssue, DEFAULT_PAGE_SIZE, EditIssue, HistoryCommit, HomeData, IssueDetails, IssueDraft,
IssueEditorData, IssueQuery, MilestoneDetails, MilestoneDraft, Page, PullDetails, RepositoryId,