Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
e811591
Add media_edit_context table and DbTable variant
crazytonyli May 13, 2026
139b8c8
Add MediaEditContext EntityType variant
crazytonyli May 13, 2026
46f1249
Add MediaRepository with edit-context upsert/select/delete
crazytonyli May 13, 2026
9f65dcb
Add MediaBuilder test fixture and use it from MediaRepository tests
crazytonyli May 13, 2026
a97db2c
Add MediaListFilter and media_list_filter_cache_key
crazytonyli May 13, 2026
82727a8
Add MediaService with sync, fetch, and delete APIs
crazytonyli May 13, 2026
9e25a66
Order service accessors alphabetically in WpService
crazytonyli May 13, 2026
54d7752
Add MediaMetadataCollectionWithEditContext and wire factory on MediaS…
crazytonyli May 13, 2026
5757897
Add integration test for MediaMetadataCollectionWithEditContext
crazytonyli May 13, 2026
92e963b
Add failing test: load_media_by_ids should send all attachment statuses
crazytonyli May 13, 2026
dd713c7
Fix load_media_by_ids: send all attachment statuses to bypass REST de…
crazytonyli May 13, 2026
0c42025
Add failing test: delete_media_permanently should scrub list metadata
crazytonyli May 13, 2026
7c483fb
Scrub deleted media from list metadata in delete_media_permanently
crazytonyli May 13, 2026
2cb9228
Rename ALL_ATTACHMENT_STATUSES to ALL_CORE_ATTACHMENT_STATUSES and do…
crazytonyli May 13, 2026
82e59a6
Add failing test: load_media_by_ids should include custom statuses fr…
crazytonyli May 13, 2026
c121d7b
Thread caller filter statuses through load_media_by_ids
crazytonyli May 13, 2026
369abb8
Derive Hashable on MediaDetails and MediaWithEditContext for Swift bi…
crazytonyli May 13, 2026
3dc5631
Add change logs
crazytonyli May 13, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- [Global Styles](https://developer.wordpress.org/rest-api/reference/wp_global_styles/) endpoint
- [Pattern Directory Items](https://developer.wordpress.org/rest-api/reference/pattern-directory-items/) endpoint
- [Rendered Blocks](https://developer.wordpress.org/rest-api/reference/rendered-blocks/) endpoint
- `MediaService` on `WpService` (sync, fetch, state tracking, `delete_media_permanently`) and `MediaMetadataCollectionWithEditContext`, mirroring the existing `PostService` / `PostMetadataCollectionWithEditContext` pattern for a cached, paginated, observable media list
- `MediaListFilter`, the subset of `MediaListParams` that backs `MediaService.create_media_metadata_collection_with_edit_context` (excludes pagination, include/exclude, and date ranges)
- `wp_mobile_cache` storage for media: `media_edit_context` table (migration 0014), `DbTable::MediaEditContext`, `EntityType::MediaEditContext`, and a `MediaRepository<EditContext>` mirroring `PostRepository` minus term relationships
- `MetadataService::remove_entity_from_lists_with_key_prefix` so service-level deletes can scrub a deleted entity from every cached list for a site without waiting for a refresh

### Changed

- `MediaDetails` now derives `Eq + Hash` (raw-JSON-string comparison, FIXME left in place) and is exported via `#[uniffi::export(Eq, Hash)]`; `SparseMedia`'s `#[WpContextualDontDerivePartialEq]` opt-out is removed so `MediaWithEditContext` and the generated `FullEntityMediaWithEditContext` Swift wrapper synthesize `Equatable + Hashable`

### Removed

Expand Down
6 changes: 6 additions & 0 deletions scripts/swift-bindings.sh
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,12 @@ import SQLite3\
extension AnyPostWithEditContext: Hashable {}
extension AnyPostWithEmbedContext: Hashable {}
extension AnyPostWithViewContext: Hashable {}
// MediaWith*Context types contain `MediaDetails` (a reference type) which
// prevents automatic Hashable synthesis. Add the conformance manually.
extension MediaWithEditContext: Hashable {}
extension MediaWithEmbedContext: Hashable {}
extension MediaWithViewContext: Hashable {}
PATCH
}

Expand Down
16 changes: 15 additions & 1 deletion wp_api/src/media.rs
Original file line number Diff line number Diff line change
Expand Up @@ -359,7 +359,6 @@ impl From<MediaCreateParams> for HashMap<String, String> {
}

#[derive(Debug, Serialize, Deserialize, uniffi::Record, WpContextual)]
#[WpContextualDontDerivePartialEq]
pub struct SparseMedia {
#[WpContext(edit, embed, view)]
pub id: Option<MediaId>,
Expand Down Expand Up @@ -428,6 +427,7 @@ pub struct SparseMedia {

#[derive(Debug, Serialize, Deserialize, uniffi::Object)]
#[serde(transparent)]
#[uniffi::export(Eq, Hash)]
pub struct MediaDetails {
pub payload: Box<RawValue>,
}
Expand Down Expand Up @@ -457,6 +457,20 @@ impl MediaDetails {
}
}

impl PartialEq for MediaDetails {
fn eq(&self, other: &Self) -> bool {
self.payload.get() == other.payload.get()
}
}

impl Eq for MediaDetails {}

impl std::hash::Hash for MediaDetails {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
self.payload.get().hash(state);
}
}

#[derive(Debug, uniffi::Enum)]
pub enum MediaDetailsPayload {
Audio(AudioMediaDetails),
Expand Down
71 changes: 68 additions & 3 deletions wp_mobile/src/cache_key.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,11 @@

use url::Url;
use wp_api::{
posts::PostListParamsField, request::endpoint::posts_endpoint::PostEndpointType,
url_query::QueryPairsExtension,
media::MediaListParamsField, posts::PostListParamsField,
request::endpoint::posts_endpoint::PostEndpointType, url_query::QueryPairsExtension,
};

use crate::filters::PostListFilter;
use crate::filters::{MediaListFilter, PostListFilter};

/// Generates a cache key segment from a `PostEndpointType`.
///
Expand Down Expand Up @@ -103,6 +103,38 @@ pub fn post_list_filter_cache_key(filter: &PostListFilter) -> String {
url.query().unwrap_or("").to_string()
}

/// Generates a deterministic cache key from `MediaListFilter`.
///
/// All fields in `MediaListFilter` are included in the cache key since it only
/// contains filter-relevant fields (pagination, instance-specific, and date
/// range fields are excluded by design in `MediaListFilter`).
pub fn media_list_filter_cache_key(filter: &MediaListFilter) -> String {
let mut url = Url::parse("https://cache-key-generator.local").expect("valid base URL");

{
let mut q = url.query_pairs_mut();

// Alphabetically ordered for determinism.
q.append_vec_query_value_pair(MediaListParamsField::Author, &filter.author);
q.append_vec_query_value_pair(MediaListParamsField::AuthorExclude, &filter.author_exclude);
q.append_option_query_value_pair(
MediaListParamsField::MediaType,
filter.media_type.as_ref(),
);
q.append_option_query_value_pair(MediaListParamsField::MimeType, filter.mime_type.as_ref());
q.append_option_query_value_pair(MediaListParamsField::Order, filter.order.as_ref());
q.append_option_query_value_pair(MediaListParamsField::Orderby, filter.orderby.as_ref());
q.append_vec_query_value_pair(MediaListParamsField::Parent, &filter.parent);
q.append_vec_query_value_pair(MediaListParamsField::ParentExclude, &filter.parent_exclude);
q.append_option_query_value_pair(MediaListParamsField::Search, filter.search.as_ref());
q.append_vec_query_value_pair(MediaListParamsField::SearchColumns, &filter.search_columns);
q.append_vec_query_value_pair(MediaListParamsField::Slug, &filter.slug);
q.append_vec_query_value_pair(MediaListParamsField::Status, &filter.status);
}

url.query().unwrap_or("").to_string()
}

#[cfg(test)]
mod tests {
use super::*;
Expand Down Expand Up @@ -167,4 +199,37 @@ mod tests {
let key = endpoint_type_cache_key(&PostEndpointType::Custom("products".to_string()));
assert_eq!(key, "post_type_custom_products");
}

#[test]
fn media_empty_filter_produces_empty_key() {
let filter = MediaListFilter::default();
let key = media_list_filter_cache_key(&filter);
assert_eq!(key, "");
}

#[test]
fn media_status_filter() {
use wp_api::media::MediaStatus;
let filter = MediaListFilter {
status: vec![MediaStatus::Inherit],
..Default::default()
};
let key = media_list_filter_cache_key(&filter);
assert_eq!(key, "status=inherit");
}

#[test]
fn media_multi_field_sorted() {
use wp_api::media::{MediaStatus, MediaTypeParam};
use wp_api::users::UserId;
let filter = MediaListFilter {
status: vec![MediaStatus::Inherit],
author: vec![UserId(5)],
media_type: Some(MediaTypeParam::Image),
..Default::default()
};
let key = media_list_filter_cache_key(&filter);
// Fields in alphabetical order: author, media_type, status
assert_eq!(key, "author=5&media_type=image&status=inherit");
}
}
Loading