Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
21 changes: 21 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,34 @@ uses semantic versioning (`MAJOR.MINOR.PATCH`).

### Added

- **Slider custom fields.** A field can now be a slider, matching the type the
server and the web app gained: either a labelled one from 0 to 100 between
two words of your choosing ("Drained" to "Wired", read as a percentage), or a
number with its own range, step and unit ("172.5 cm"). Either can paint a
colour gradient across the track, picked from nine named ones or built from
your own colours with the colour picker the app already uses for members and
tags. Settings gets the whole setup with a live preview; the member editor
gets a draggable track with the reading beside it and a tap to clear; a
member's profile shows the reading. The reading is the same string the server
puts on a public profile, so a slider reads the same everywhere.

- **Announcements can be aimed at particular clients.** Admin only. An
announcement can now be shown only on Web, Android, iOS or the watch, with a
"Show on" section in the editor and a marker in the list; leaving it unticked
shows it to everyone, as before. The app also names itself when asking, so an
announcement aimed at Android reaches it. Needs a server that supports
targeting; on an older one everything carries on as it was.

### Fixed

- **A field type this build has not heard of no longer reads as an error.** A
custom field whose type the server knows and the app does not showed "Unknown
field type" in red where its value should be, so the value a member had put
there was hidden by a message that looked like a fault. It now shows the value
as text with a note to update the app, which is what the API asks clients to
do, and is why a slider field on an older build will have looked broken rather
than merely unsupported.

## [1.5.0] - 2026-10-12

### Added
Expand Down
33 changes: 31 additions & 2 deletions sheaf/app/src/main/java/systems/lupine/sheaf/data/model/Models.kt
Original file line number Diff line number Diff line change
Expand Up @@ -804,15 +804,43 @@ data class TagDeletePending(

// ── Custom Fields ─────────────────────────────────────────────────────────────

/** The two ends of a slider, and optionally its middle. Each up to 50 chars. */
@JsonClass(generateAdapter = true)
data class SliderLabels(
val left: String? = null,
val center: String? = null,
val right: String? = null,
)

/**
* Per-field-type options. For SELECT / MULTISELECT, [choices] carries
* the predefined values the user can pick from; null = freeform tag
* mode (any string accepted server-side). Other field types don't
* carry options today.
* mode (any string accepted server-side).
*
* The rest belong to SLIDER, which the server always stores in full canonical
* shape, so every key below comes back populated on a read. On a write the
* rules are the opposite of the PATCH bodies next door: the server rebuilds a
* slider's options from scratch rather than merging, so a key left out means
* "the default for that key", not "leave it alone". Moshi omitting nulls is
* therefore exactly right here, and it is what clears a label, a unit or a
* gradient. Do not reach for a hand-written adapter: it would send explicit
* nulls, which say the same thing, less clearly.
*
* [mode] is "labeled" (fixed 0..100, read as a percentage) or "numeric"
* ([min]/[max] are the field's own, [step] may be null for any value, [unit]
* is shown after the number). [gradient] is null or 2 to 8 #RRGGBB colours
* painted left to right across the track.
*/
@JsonClass(generateAdapter = true)
data class CustomFieldOptions(
val choices: List<String>? = null,
val mode: String? = null,
val min: Double? = null,
val max: Double? = null,
val step: Double? = null,
val unit: String? = null,
val labels: SliderLabels? = null,
val gradient: List<String>? = null,
)

@JsonClass(generateAdapter = true)
Expand Down Expand Up @@ -868,6 +896,7 @@ data class CustomFieldUpdate(
// boolean -> Boolean
// select -> String (must be in choices when choices are set)
// multiselect -> List<String> (each must be in choices when set)
// slider -> Number, inside [options.min, options.max], never a string
//
// Members the viewer isn't permitted to see fields for return an
// empty list; the per-field privacy gate is enforced server-side.
Expand Down
Loading
Loading