Add native iOS notifications (#64)
This commit is contained in:
@@ -174,6 +174,16 @@ pub fn api_date(timestamp: i64) -> String {
|
||||
format!("{year:04}-{month:02}-{day:02}T00:00:00Z")
|
||||
}
|
||||
|
||||
pub fn api_timestamp(timestamp: i64) -> String {
|
||||
let days = timestamp.div_euclid(86_400);
|
||||
let seconds = timestamp.rem_euclid(86_400);
|
||||
let (year, month, day) = civil_from_days(days);
|
||||
let hour = seconds / 3_600;
|
||||
let minute = seconds % 3_600 / 60;
|
||||
let second = seconds % 60;
|
||||
format!("{year:04}-{month:02}-{day:02}T{hour:02}:{minute:02}:{second:02}Z")
|
||||
}
|
||||
|
||||
pub fn parse_api_date(value: &str) -> Option<i64> {
|
||||
let date = value.get(..10)?;
|
||||
let mut parts = date.split('-');
|
||||
@@ -230,6 +240,7 @@ mod tests {
|
||||
assert!(!Page::from_items(vec![(); 29], 30).has_more);
|
||||
let leap_day = parse_api_date("2024-02-29T12:34:56Z").unwrap();
|
||||
assert_eq!(api_date(leap_day), "2024-02-29T00:00:00Z");
|
||||
assert_eq!(api_timestamp(leap_day + 45_296), "2024-02-29T12:34:56Z");
|
||||
assert_eq!(parse_api_date("2023-02-29T00:00:00Z"), None);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ pub mod diff;
|
||||
mod domain;
|
||||
mod issues;
|
||||
mod milestones;
|
||||
pub mod notifications;
|
||||
mod pulls;
|
||||
mod repositories;
|
||||
|
||||
@@ -27,7 +28,7 @@ pub use config::{Config, Selection, ServerProfile, TuiPreferences, server_url};
|
||||
pub use domain::{
|
||||
CreateIssue, DEFAULT_PAGE_SIZE, EditIssue, HistoryCommit, HomeData, IssueDetails, IssueDraft,
|
||||
IssueEditorData, IssueQuery, MilestoneDetails, MilestoneDraft, Page, PullDetails, RepositoryId,
|
||||
api_date, civil_from_days, days_from_civil, parse_api_date,
|
||||
api_date, api_timestamp, civil_from_days, days_from_civil, parse_api_date,
|
||||
};
|
||||
pub use issues::comment_can_edit;
|
||||
pub use pulls::{PullFileSource, pull_file_source, pull_state};
|
||||
|
||||
212
crates/gitea/src/notifications.rs
Normal file
212
crates/gitea/src/notifications.rs
Normal file
@@ -0,0 +1,212 @@
|
||||
use gitea_openapi::apis::notification_api;
|
||||
|
||||
use crate::{Client, Error, Page, Result, models, positive, validate_page};
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub enum NotificationTarget {
|
||||
None,
|
||||
Repository {
|
||||
owner: String,
|
||||
repository: String,
|
||||
},
|
||||
Issue {
|
||||
owner: String,
|
||||
repository: String,
|
||||
number: i64,
|
||||
},
|
||||
Pull {
|
||||
owner: String,
|
||||
repository: String,
|
||||
number: i64,
|
||||
},
|
||||
Commit {
|
||||
owner: String,
|
||||
repository: String,
|
||||
sha: String,
|
||||
},
|
||||
}
|
||||
|
||||
impl Client {
|
||||
pub async fn notifications(
|
||||
&self,
|
||||
unread: bool,
|
||||
page: i32,
|
||||
limit: i32,
|
||||
) -> Result<Page<models::NotificationThread>> {
|
||||
validate_page(page, limit)?;
|
||||
let items = notification_api::notify_get_list(
|
||||
&self.configuration(),
|
||||
Some(!unread),
|
||||
Some(vec![if unread { "unread" } else { "read" }.into()]),
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
Some(page),
|
||||
Some(limit),
|
||||
)
|
||||
.await
|
||||
.map_err(Error::generated)?;
|
||||
Ok(Page::from_items(items, limit))
|
||||
}
|
||||
|
||||
pub async fn notification_updates(
|
||||
&self,
|
||||
since: Option<&str>,
|
||||
) -> Result<Vec<models::NotificationThread>> {
|
||||
let mut notifications = Vec::new();
|
||||
for page in 1.. {
|
||||
let batch = notification_api::notify_get_list(
|
||||
&self.configuration(),
|
||||
Some(false),
|
||||
Some(vec!["unread".into()]),
|
||||
None,
|
||||
since.map(str::to_owned),
|
||||
None,
|
||||
Some(page),
|
||||
Some(100),
|
||||
)
|
||||
.await
|
||||
.map_err(Error::generated)?;
|
||||
let complete = batch.len() < 100;
|
||||
notifications.extend(batch);
|
||||
if complete {
|
||||
break;
|
||||
}
|
||||
}
|
||||
Ok(notifications)
|
||||
}
|
||||
|
||||
pub async fn mark_notification_read(&self, id: i64) -> Result<models::NotificationThread> {
|
||||
positive(id, "notification ID")?;
|
||||
notification_api::notify_read_thread(&self.configuration(), &id.to_string(), Some("read"))
|
||||
.await
|
||||
.map_err(Error::generated)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn target(notification: &models::NotificationThread) -> NotificationTarget {
|
||||
let Some(repository) = notification.repository.as_deref() else {
|
||||
return NotificationTarget::None;
|
||||
};
|
||||
let Some(owner) = repository
|
||||
.owner
|
||||
.as_deref()
|
||||
.and_then(|owner| owner.login.clone())
|
||||
else {
|
||||
return NotificationTarget::None;
|
||||
};
|
||||
let Some(repository) = repository.name.clone() else {
|
||||
return NotificationTarget::None;
|
||||
};
|
||||
let Some(subject) = notification.subject.as_deref() else {
|
||||
return NotificationTarget::Repository { owner, repository };
|
||||
};
|
||||
let value = subject
|
||||
.url
|
||||
.as_deref()
|
||||
.or(subject.html_url.as_deref())
|
||||
.and_then(last_path_component);
|
||||
match subject
|
||||
.r#type
|
||||
.as_deref()
|
||||
.unwrap_or_default()
|
||||
.to_ascii_lowercase()
|
||||
.as_str()
|
||||
{
|
||||
"issue" => {
|
||||
value
|
||||
.and_then(|value| value.parse().ok())
|
||||
.map_or(NotificationTarget::None, |number| {
|
||||
NotificationTarget::Issue {
|
||||
owner,
|
||||
repository,
|
||||
number,
|
||||
}
|
||||
})
|
||||
}
|
||||
"pull" | "pullrequest" | "pull_request" => value
|
||||
.and_then(|value| value.parse().ok())
|
||||
.map_or(NotificationTarget::None, |number| {
|
||||
NotificationTarget::Pull {
|
||||
owner,
|
||||
repository,
|
||||
number,
|
||||
}
|
||||
}),
|
||||
"commit" => value.map_or(NotificationTarget::None, |sha| NotificationTarget::Commit {
|
||||
owner,
|
||||
repository,
|
||||
sha: sha.into(),
|
||||
}),
|
||||
"repository" => NotificationTarget::Repository { owner, repository },
|
||||
_ => NotificationTarget::None,
|
||||
}
|
||||
}
|
||||
|
||||
fn last_path_component(url: &str) -> Option<&str> {
|
||||
url.trim_end_matches('/')
|
||||
.rsplit('/')
|
||||
.next()
|
||||
.filter(|value| !value.is_empty())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn notification(kind: &str, url: &str) -> models::NotificationThread {
|
||||
models::NotificationThread {
|
||||
repository: Some(Box::new(models::Repository {
|
||||
name: Some("demo".into()),
|
||||
owner: Some(Box::new(models::User {
|
||||
login: Some("octo".into()),
|
||||
..Default::default()
|
||||
})),
|
||||
..Default::default()
|
||||
})),
|
||||
subject: Some(Box::new(models::NotificationSubject {
|
||||
r#type: Some(kind.into()),
|
||||
url: Some(url.into()),
|
||||
..Default::default()
|
||||
})),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn maps_notification_subjects_to_native_destinations() {
|
||||
assert_eq!(
|
||||
target(¬ification(
|
||||
"Issue",
|
||||
"https://gitea.example/api/v1/repos/octo/demo/issues/42"
|
||||
)),
|
||||
NotificationTarget::Issue {
|
||||
owner: "octo".into(),
|
||||
repository: "demo".into(),
|
||||
number: 42,
|
||||
}
|
||||
);
|
||||
assert_eq!(
|
||||
target(¬ification(
|
||||
"Pull",
|
||||
"https://gitea.example/api/v1/repos/octo/demo/pulls/7"
|
||||
)),
|
||||
NotificationTarget::Pull {
|
||||
owner: "octo".into(),
|
||||
repository: "demo".into(),
|
||||
number: 7,
|
||||
}
|
||||
);
|
||||
assert_eq!(
|
||||
target(¬ification(
|
||||
"Commit",
|
||||
"https://gitea.example/api/v1/repos/octo/demo/git/commits/deadbeef"
|
||||
)),
|
||||
NotificationTarget::Commit {
|
||||
owner: "octo".into(),
|
||||
repository: "demo".into(),
|
||||
sha: "deadbeef".into(),
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user