Add issue comment editing

This commit is contained in:
Georg Bauer
2026-07-31 15:17:09 +02:00
parent 33f9eb809c
commit da0a3aecb4
10 changed files with 451 additions and 12 deletions

View File

@@ -466,14 +466,16 @@ pub async fn load_issue(
let client =
Client::new(&server.url, Some(&server.token)).map_err(|error| error.to_string())?;
let configuration = client.configuration();
let (issue, comments) = tokio::join!(
let (issue, comments, viewer) = tokio::join!(
apis::issue_api::issue_get_issue(&configuration, owner, repository, number),
load_issue_comments(&configuration, owner, repository, number, page),
client.current_user(),
);
Ok(IssueDetails {
issue: issue.map_err(|error| error.to_string())?,
has_more: false,
comments: comments?,
viewer_id: viewer.map_err(|error| error.to_string())?.id,
})
}
@@ -492,6 +494,67 @@ async fn load_issue_comments(
.map_err(|error| error.to_string())
}
pub async fn save_issue_comment(
server: &Server,
owner: &str,
repository: &str,
number: i64,
comment_id: Option<i64>,
body: String,
) -> Result<(), String> {
let client =
Client::new(&server.url, Some(&server.token)).map_err(|error| error.to_string())?;
let configuration = client.configuration();
match comment_id {
Some(id) => {
let (comment, viewer) = tokio::join!(
apis::issue_api::issue_get_comment(&configuration, owner, repository, id),
client.current_user(),
);
let comment = comment.map_err(|error| error.to_string())?;
let viewer = viewer.map_err(|error| error.to_string())?;
let owned = matches!(
(comment.user.as_ref().and_then(|user| user.id), viewer.id),
(Some(author), Some(viewer)) if author == viewer
);
if !owned || !comment_belongs_to_issue(&comment, number) {
return Err("You can only edit your own comments.".into());
}
apis::issue_api::issue_edit_comment(
&configuration,
owner,
repository,
id,
Some(models::EditIssueCommentOption::new(body)),
)
.await
.map_err(|error| error.to_string())?;
}
None => {
apis::issue_api::issue_create_comment(
&configuration,
owner,
repository,
number,
Some(models::CreateIssueCommentOption::new(body)),
)
.await
.map_err(|error| error.to_string())?;
}
}
Ok(())
}
fn comment_belongs_to_issue(comment: &models::Comment, number: i64) -> bool {
comment
.issue_url
.as_deref()
.map(|url| url.trim_end_matches('/'))
.and_then(|url| url.rsplit('/').next())
.and_then(|index| index.parse().ok())
== Some(number)
}
pub async fn load_issue_editor(
server: &Server,
owner: &str,
@@ -1041,4 +1104,15 @@ mod tests {
assert_eq!(edit.milestone, Some(0));
assert_eq!(edit.unset_due_date, Some(true));
}
#[test]
fn scopes_comment_edits_to_the_selected_issue() {
let comment = models::Comment {
issue_url: Some("https://gitea.example/api/v1/repos/o/r/issues/4/".into()),
..Default::default()
};
assert!(comment_belongs_to_issue(&comment, 4));
assert!(!comment_belongs_to_issue(&comment, 5));
assert!(!comment_belongs_to_issue(&models::Comment::default(), 4));
}
}

View File

@@ -154,6 +154,7 @@ pub struct HomeData {
pub struct IssueDetails {
pub issue: models::Issue,
pub comments: Vec<models::Comment>,
pub viewer_id: Option<i64>,
pub has_more: bool,
}

View File

@@ -353,6 +353,25 @@ impl GotchaCore {
))
}
pub async fn save_issue_comment(
&self,
owner: String,
repository: String,
number: i64,
comment_id: Option<i64>,
body: String,
) -> Result<(), GotchaError> {
let (owner, repository) = validate_repository(&owner, &repository)?;
if number <= 0 || comment_id.is_some_and(|id| id <= 0) {
return Err("Invalid issue comment selection.".into());
}
if body.trim().is_empty() {
return Err("Enter a comment.".into());
}
save_issue_comment(&self.server()?, owner, repository, number, comment_id, body).await?;
Ok(())
}
pub async fn issue_editor(
&self,
owner: String,

View File

@@ -64,9 +64,11 @@ pub struct PullRow {
#[derive(Clone, uniffi::Record)]
pub struct CommentRow {
pub id: i64,
pub author: String,
pub body: String,
pub meta: String,
pub can_edit: bool,
}
#[derive(Clone, uniffi::Record)]
@@ -514,7 +516,11 @@ pub fn issue_page(details: IssueDetails) -> IssuePage {
.body
.filter(|body| !body.is_empty())
.unwrap_or_else(|| "No description provided.".into()),
comments: details.comments.iter().map(comment_row).collect(),
comments: details
.comments
.iter()
.map(|comment| comment_row(comment, details.viewer_id))
.collect(),
has_more: details.has_more,
}
}
@@ -627,7 +633,11 @@ pub fn pull_page(details: PullDetails) -> PullPage {
.unwrap_or_else(|| "No description provided.".into()),
files_ref: pull_files_ref(pull),
files: details.files.iter().filter_map(file_row).collect(),
comments: details.comments.iter().map(comment_row).collect(),
comments: details
.comments
.iter()
.map(|comment| comment_row(comment, None))
.collect(),
has_more: details.has_more,
}
}
@@ -985,8 +995,9 @@ fn label_row(label: &models::Label) -> Option<LabelRow> {
})
}
fn comment_row(comment: &models::Comment) -> CommentRow {
fn comment_row(comment: &models::Comment, viewer_id: Option<i64>) -> CommentRow {
CommentRow {
id: comment.id.unwrap_or_default(),
author: comment
.user
.as_ref()
@@ -1005,6 +1016,14 @@ fn comment_row(comment: &models::Comment) -> CommentRow {
.as_deref()
.or(comment.created_at.as_deref()),
),
can_edit: matches!(
(
comment.id,
comment.user.as_ref().and_then(|user| user.id),
viewer_id,
),
(Some(_), Some(author), Some(viewer)) if author == viewer
),
}
}
@@ -1264,17 +1283,41 @@ mod tests {
}
#[test]
fn issue_page_exposes_state() {
fn issue_page_exposes_state_and_owned_comment_editing() {
let page = issue_page(IssueDetails {
issue: models::Issue {
state: Some("closed".into()),
..Default::default()
},
comments: Vec::new(),
comments: vec![
models::Comment {
id: Some(7),
user: Some(Box::new(models::User {
id: Some(3),
login: Some("viewer".into()),
..Default::default()
})),
body: Some("Comment".into()),
..Default::default()
},
models::Comment {
id: Some(8),
user: Some(Box::new(models::User {
id: Some(4),
login: Some("someone-else".into()),
..Default::default()
})),
..Default::default()
},
],
viewer_id: Some(3),
has_more: false,
});
assert_eq!(page.state, "closed");
assert_eq!(page.comments[0].id, 7);
assert!(page.comments[0].can_edit);
assert!(!page.comments[1].can_edit);
}
#[test]