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
8 changes: 7 additions & 1 deletion openvtc/src/state_handler/main_page/content.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ use std::sync::Arc;

use dtg_credentials::DTGCredential;
use openvtc_core::community_access::{DEFAULT_EXPIRY, DeviceGrant};
use openvtc_core::config::account::PersonaId;
use openvtc_core::config::account::{PersonaId, RelationshipIdentifierDefault};
use openvtc_core::config::community_context::{
ContextDeletion, ContextDeletionPreview, ContextOption, PersonaTakenAlong,
};
Expand Down Expand Up @@ -2490,6 +2490,12 @@ pub enum RelationshipsMode {
reason_input: String,
/// Whether to generate a random relationship DID (privacy)
generate_r_did: bool,
/// The working community's declared `relationshipIdentifierDefault`, if
/// any — what seeded [`generate_r_did`](Self::NewRequest::generate_r_did).
/// Carried only so the form can explain *why* the toggle defaulted where
/// it did ("your community prefers …"); `None` when there is no working
/// community or it declared nothing (issue #241 follow-up).
community_default: Option<RelationshipIdentifierDefault>,
/// Which form field is currently focused (0=DID, 1=Alias, 2=Reason, 3=R-DID toggle)
active_field: usize,
},
Expand Down
104 changes: 62 additions & 42 deletions openvtc/src/state_handler/relationship_actions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1049,7 +1049,10 @@ fn handle_open_detail(state: &mut State, index: usize) {
};
}

fn handle_start_new_request(state: &mut State, generate_r_did: bool) {
fn handle_start_new_request(
state: &mut State,
community_default: Option<RelationshipIdentifierDefault>,
) {
state.main_page.content_panel.relationships.mode = RelationshipsMode::NewRequest {
did_input: String::new(),
alias_input: String::new(),
Expand All @@ -1061,36 +1064,36 @@ fn handle_start_new_request(state: &mut State, generate_r_did: bool) {
// it costs.
//
// A community that declares `relationshipIdentifierDefault: attributed`
// (a public community that wants a legible graph) flips this to `false`
// — see [`default_generate_r_did`]. The user can still toggle it.
generate_r_did,
// (a public community that wants a legible graph) seeds the persona DID
// instead; the form explains that from `community_default`. The user can
// still toggle it.
generate_r_did: !matches!(
community_default,
Some(RelationshipIdentifierDefault::Attributed)
),
community_default,
active_field: 0,
};
}

/// The mint default for a new relationship, seeded from the working community's
/// declared `relationshipIdentifierDefault` (issue #241).
///
/// `Attributed` ⇒ default to the persona DID (`false`); `Pairwise`, undeclared,
/// or no working community ⇒ pairwise (`true`), the codebase-wide default. It is
/// only a *default* — the form's field-3 toggle overrides it either way. The
/// working community is the selected one, falling back to the account's default
/// working membership; in State-A there is none, so pairwise stands.
fn default_generate_r_did(config: &Config, state: &State) -> bool {
let membership = state
/// The working community's declared `relationshipIdentifierDefault`, which seeds
/// the new-relationship form's default and the "your community prefers …" hint
/// (issue #241). The working community is the selected one, falling back to the
/// account's default working membership; `None` in State-A (no working
/// community) or when the community declared nothing — both of which leave the
/// pairwise default standing.
fn working_community_relationship_default(
config: &Config,
state: &State,
) -> Option<RelationshipIdentifierDefault> {
let (vtc, persona) = state
.selected_community
.clone()
.or_else(|| config.account.default_working_membership());
let Some((vtc, persona)) = membership else {
return true;
};
!matches!(
config
.account
.membership(&vtc, persona)
.and_then(|c| c.relationship_identifier_default),
Some(RelationshipIdentifierDefault::Attributed)
)
.or_else(|| config.account.default_working_membership())?;
config
.account
.membership(&vtc, persona)
.and_then(|c| c.relationship_identifier_default)
}

fn handle_cancel_or_back(state: &mut State) {
Expand Down Expand Up @@ -1492,7 +1495,7 @@ pub(crate) async fn dispatch(
}
RelationshipAction::OpenDetail(index) => handle_open_detail(state, index),
RelationshipAction::StartNewRequest => {
handle_start_new_request(state, default_generate_r_did(config, state))
handle_start_new_request(state, working_community_relationship_default(config, state))
}
RelationshipAction::CancelNewRequest | RelationshipAction::Back => {
handle_cancel_or_back(state)
Expand Down Expand Up @@ -1741,30 +1744,46 @@ mod tests {
(config, state)
};

// The lookup reports the community's declaration…
let (config, state) = seed(Some(RelationshipIdentifierDefault::Attributed));
assert!(
!default_generate_r_did(&config, &state),
"an attributed community defaults to the persona DID"
assert_eq!(
working_community_relationship_default(&config, &state),
Some(RelationshipIdentifierDefault::Attributed)
);
// …and it seeds the form: attributed ⇒ persona DID, and the form carries
// the declaration so it can explain the choice.
let mut s = state.clone();
handle_start_new_request(
&mut s,
working_community_relationship_default(&config, &state),
);
assert!(matches!(
s.main_page.content_panel.relationships.mode,
RelationshipsMode::NewRequest {
generate_r_did: false,
community_default: Some(RelationshipIdentifierDefault::Attributed),
..
}
));

let (config, state) = seed(Some(RelationshipIdentifierDefault::Pairwise));
assert!(
default_generate_r_did(&config, &state),
"a pairwise community keeps the pairwise default"
assert_eq!(
working_community_relationship_default(&config, &state),
Some(RelationshipIdentifierDefault::Pairwise)
);

let (config, state) = seed(None);
assert!(
default_generate_r_did(&config, &state),
"an undeclared community keeps the pairwise default"
assert_eq!(
working_community_relationship_default(&config, &state),
None
);

// State-A: no working community at all → pairwise.
// State-A: no working community at all → None → pairwise stands.
let config = test_config();
let state = State::default();
assert!(
default_generate_r_did(&config, &state),
"with no working community, pairwise stands"
assert_eq!(
working_community_relationship_default(&config, &state),
None
);
}

Expand Down Expand Up @@ -2156,7 +2175,7 @@ mod tests {
#[test]
fn start_new_request_and_cancel_back() {
let mut state = State::default();
handle_start_new_request(&mut state, true);
handle_start_new_request(&mut state, None);
// Pairwise is the default (#241) — a new request mints an R-DID unless
// the operator deliberately turns it off.
assert!(matches!(
Expand All @@ -2175,6 +2194,7 @@ mod tests {
alias_input: String::new(),
reason_input: String::new(),
generate_r_did: true,
community_default: None,
active_field: 2,
},
RelationshipsMode::Detail {
Expand Down Expand Up @@ -2233,7 +2253,7 @@ mod tests {
];
for (field, (did, alias, reason)) in cases {
let mut state = State::default();
handle_start_new_request(&mut state, true);
handle_start_new_request(&mut state, None);
let value = match field {
0 => "the-did",
1 => "the-alias",
Expand Down Expand Up @@ -2265,7 +2285,7 @@ mod tests {
#[test]
fn toggle_r_did_flips_flag() {
let mut state = State::default();
handle_start_new_request(&mut state, true);
handle_start_new_request(&mut state, None);
// Starts pairwise (#241), so the first toggle is the opt-out.
handle_toggle_r_did(&mut state);
assert!(matches!(
Expand Down
71 changes: 70 additions & 1 deletion openvtc/src/ui/pages/main/components/relationships_panel.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ use crate::state_handler::{
main_page::content::{ContentPanelState, RelationshipsMode, RelationshipsState},
state::ConnectionState,
};
use openvtc_core::config::account::RelationshipIdentifierDefault;
use openvtc_core::display::display_identifier;
use ratatui::{
style::{Style, Stylize},
Expand Down Expand Up @@ -46,12 +47,14 @@ pub fn render(state: &RelationshipsState) -> Vec<Line<'static>> {
alias_input,
reason_input,
generate_r_did,
community_default,
active_field,
} => render_form(
did_input,
alias_input,
reason_input,
*generate_r_did,
*community_default,
*active_field,
),
RelationshipsMode::List => render_list(state),
Expand Down Expand Up @@ -419,6 +422,7 @@ fn render_form(
alias_input: &str,
reason_input: &str,
generate_r_did: bool,
community_default: Option<RelationshipIdentifierDefault>,
active_field: usize,
) -> Vec<Line<'static>> {
let mut lines = vec![Line::from("")];
Expand Down Expand Up @@ -469,8 +473,19 @@ fn render_form(
} else {
Style::new().fg(COLOR_DARK_GRAY)
};
// "(recommended)" is the general privacy recommendation (pairwise), except
// where the working community declares it wants attributed edges — there the
// persona DID is the community's default, and the hint below says why.
let community_prefers_attributed =
community_default == Some(RelationshipIdentifierDefault::Attributed);
let toggle_value = if generate_r_did {
"Pairwise R-DID (recommended)"
if community_prefers_attributed {
"Pairwise R-DID (more private)"
} else {
"Pairwise R-DID (recommended)"
}
} else if community_prefers_attributed {
"Your persona DID (your community's default)"
} else {
"Your persona DID"
};
Expand All @@ -480,6 +495,28 @@ fn render_form(
Span::styled(toggle_value.to_string(), value_style),
]));

// Explain where the default came from, when the community declared one — so a
// toggle that started on the persona DID does not look like an odd choice
// (issue #241 follow-up).
match community_default {
Some(RelationshipIdentifierDefault::Attributed) => {
lines.push(
Line::from(
" Your community publishes relationships under members' persona DIDs, so \
this defaults there.",
)
.fg(COLOR_DARK_GRAY),
);
}
Some(RelationshipIdentifierDefault::Pairwise) => {
lines.push(
Line::from(" Your community recommends a pairwise DID (the default).")
.fg(COLOR_DARK_GRAY),
);
}
None => {}
}

// Spell out the trade-off at the point of choice — the cost of reusing the
// persona DID is invisible at the moment the decision is made (#241).
lines.push(Line::from(""));
Expand Down Expand Up @@ -542,6 +579,38 @@ mod tests {
.collect()
}

/// The new-relationship form explains where its identifier default came
/// from when the working community declared one (issue #241 follow-up), and
/// says nothing extra when it did not.
#[test]
fn the_form_explains_a_community_relationship_default() {
let rendered = |cd: Option<RelationshipIdentifierDefault>| {
let generate_r_did = !matches!(cd, Some(RelationshipIdentifierDefault::Attributed));
text(&render_form("", "", "", generate_r_did, cd, 0)).join("\n")
};

let attributed = rendered(Some(RelationshipIdentifierDefault::Attributed));
assert!(
attributed.contains("Your persona DID (your community's default)"),
"{attributed}"
);
assert!(
attributed.contains("publishes relationships under members' persona DIDs"),
"{attributed}"
);

let pairwise = rendered(Some(RelationshipIdentifierDefault::Pairwise));
assert!(
pairwise.contains("Your community recommends a pairwise DID"),
"{pairwise}"
);

let none = rendered(None);
assert!(none.contains("Pairwise R-DID (recommended)"), "{none}");
assert!(!none.contains("your community's default"), "{none}");
assert!(!none.contains("Your community"), "{none}");
}

fn raw() -> RawCredential {
RawCredential::Value(Arc::new(serde_json::json!({})))
}
Expand Down
1 change: 1 addition & 0 deletions openvtc/src/ui/pages/main/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1510,6 +1510,7 @@ impl MainPage {
reason_input,
generate_r_did,
active_field,
..
} => {
// Form input handling
let active_field = *active_field;
Expand Down
Loading