diff --git a/couchdb/vodle/_design/vodle/validate_doc_update.js b/couchdb/vodle/_design/vodle/validate_doc_update.js index 672453666..8162de78d 100644 --- a/couchdb/vodle/_design/vodle/validate_doc_update.js +++ b/couchdb/vodle/_design/vodle/validate_doc_update.js @@ -38,9 +38,9 @@ function (newDoc, savedDoc, userCtx) { } */ } else { - // if doc already exists, let noone update or delete it: - if (savedDoc) { - throw ({forbidden: 'Noone may update or delete existing poll documents.'}) + // if doc already exists, let noone update or delete it, unless its delegation map: + if (savedDoc && !_id.endsWith("inverse_indirect_map") && !_id.endsWith("direct_delegation_map")) { + throw ({forbidden: 'Noone may update or delete existing poll documents.'}); } // let only the voters create it: let doc_pid = _id.substring(pollprefix.length, _id.indexOf("§")); diff --git a/src/app/data.service.ts b/src/app/data.service.ts index 684619564..648483e29 100644 --- a/src/app/data.service.ts +++ b/src/app/data.service.ts @@ -214,12 +214,12 @@ function myhash(what): string { export type del_option_spec_t = {type: "+" | "-", oids: Array}; export type del_request_t = {option_spec: del_option_spec_t, public_key: string}; -export type del_response_t = {option_spec: del_option_spec_t}; +export type del_response_t = {option_spec: del_option_spec_t, status: "agreed" | "declined" | "revoked", "decline_cycle", "decline_self"}; export type del_signed_response_t = string; export type del_agreement_t = { // by pid, did client_vid?: string, delegate_vid?: string, - status?: "pending" | "agreed" | "declined" | "revoked", + status?: "pending" | "agreed" | "declined" | "revoked" | "declined_cycle" | "declined_self", accepted_oids?: Set, // oids accepted for delegation by delegate active_oids?: Set // among those, oids currently activated for delegation by client }; @@ -312,13 +312,6 @@ export class DataService implements OnDestroy { delegation_agreements_caches: Record>; // by pid, did - direct_delegation_map_caches: Record>>; // redundant storage of direct delegation data, not stored in database - inv_direct_delegation_map_caches: Record>>>; // redundant storage of inverse direct delegation data, not stored in database - indirect_delegation_map_caches: Record>>>; // redundant storage of indirect delegation data, not stored in database - inv_indirect_delegation_map_caches: Record>>>; // redundant storage of inverse indirect delegation data, not stored in database - effective_delegation_map_caches: Record>>; // redundant storage of effective delegation data, not stored in database - inv_effective_delegation_map_caches: Record>>>; // redundant storage of inverse effective delegation data, not stored in database - tally_caches: Record; // temporary storage of tally data, not stored in database news_keys: Set; @@ -436,12 +429,6 @@ export class DataService implements OnDestroy { this.outgoing_dids_caches = {}; this.incoming_dids_caches = {}; this.delegation_agreements_caches = {}; - this.direct_delegation_map_caches = {}; - this.inv_direct_delegation_map_caches = {}; - this.indirect_delegation_map_caches = {}; - this.inv_indirect_delegation_map_caches = {}; - this.effective_delegation_map_caches = {}; - this.inv_effective_delegation_map_caches = {}; this.proxy_ratings_map_caches = {}; this.max_proxy_ratings_map_caches = {}; this.argmax_proxy_ratings_map_caches = {}; @@ -1522,7 +1509,9 @@ export class DataService implements OnDestroy { } else { this.G.L.error("DataService.setp change option attempted for existing entry", pid, key, value); } - } else { + } else if (key == 'inverse_indirect_map') { + return this._setp_in_polldb(pid, key, value); + }else { this.G.L.error("DataService.setp non-local attempted for non-draft poll", pid, key, value); } } @@ -1568,7 +1557,7 @@ export class DataService implements OnDestroy { // other polls' data is stored in poll's own database. // construct key for poll db: const pkey = this.get_voter_key_prefix(pid, vid) + key; -// this.G.L.trace("getv", pid, key, vid, pkey) + // this.G.L.trace("getv", pid, key, vid, pkey) this.ensure_poll_cache(pid); value = this.poll_caches[pid][pkey] || ''; } @@ -1591,8 +1580,51 @@ export class DataService implements OnDestroy { } } - delv(pid: string, key: string) { - // delete a voter data item + // inverse_indirect_map:- key: voterid, value: [voterid1, voterid2, voterid3] voterids that have effectively delegated to the voterid + set_inverse_indirect_map(pid: string, val: Map) { + const mapKey = `poll.${pid}.inverse_indirect_map`; + this._setp_in_polldb(pid, mapKey, JSON.stringify(Array.from(val.entries()))); + } + + set_effective_delegation(pid: string, vid: string, val: string[]) { + const mapKey = `poll.${pid}.inverse_indirect_map`; + const currentMap = this.get_inverse_indirect_map(pid); + + currentMap.set(vid, JSON.stringify(val)); // Add or update the key-value pair + this._setp_in_polldb(pid, mapKey, JSON.stringify(Array.from(currentMap.entries()))); + } + + get_inverse_indirect_map(pid: string): Map { + const cache = this.poll_caches[pid]['poll.' + pid + '.inverse_indirect_map'] || '[]'; + const ps = cache ? JSON.parse(cache) : {}; + const mp = new Map(ps); + return mp; + } + + // direct_delegation_map:- key: voterid, value: [[voterid, rank, is_current_delegation], [voterid2, rank, is_current_delegate]] of the voter that the key has delegated to. + // When ranked delegation is not allowed, the rank is always 0 and the size of the value is 1. + set_direct_delegation_map(pid: string, val: Map>) { + const mapKey = `poll.${pid}.direct_delegation_map`; + this._setp_in_polldb(pid, mapKey, JSON.stringify(Array.from(val.entries()))); + } + + get_direct_delegation_map(pid: string): Map> { + const cache = this.poll_caches[pid]['poll.' + pid + '.direct_delegation_map'] || '[]'; + const ps = cache ? JSON.parse(cache) : {}; + const mp = new Map>(ps); + return mp; + } + + clear_direct_delegation_map(pid: string) { + const mapKey = `poll.${pid}.direct_delegation_map`; + this._setp_in_polldb(pid, mapKey, JSON.stringify([])); + } + + delv(pid: string, key: string, vid?: string) { + if(!this.getv(pid, key)) { + this.G.L.warn("DataService.delv nothing to delete", pid, key); + return; + } if (this.pid_is_draft(pid)) { const ukey = get_poll_key_prefix(pid) + this.get_voter_key_prefix(pid) + key; delete this.user_cache[ukey]; @@ -1605,6 +1637,10 @@ export class DataService implements OnDestroy { } } + get_ranked_delegation_allowed(pid: string): boolean { + return this.poll_caches[pid]['allow_ranked'] == 'true'; + } + // TODO: delv! get_example_docs(): Promise { @@ -1654,6 +1690,18 @@ export class DataService implements OnDestroy { return this.store_user_data(ukey, this.user_cache, ukey); } + setv_global_for_poll(pid: string, key: string, value: string): boolean { + // set global voter data item in poll db: + value = value || ''; + // construct key for poll db: + // return 'voter.' + (vid ? vid : this.getp(pid, 'myvid')) + "§"; + const pkey = "§"; + this.ensure_poll_cache(pid); + this.G.L.trace("DataService.setv_global_for_poll", pid, key, value); + this.poll_caches[pid][pkey] = value; + return this.store_poll_data(pid, pkey, this.poll_caches[pid], pkey, false); + } + setv_in_polldb(pid: string, key: string, value: string, vid?: string): boolean { /** Set voter data item in poll db. * If necessary, mark the database entry with poll's due date @@ -1770,6 +1818,9 @@ export class DataService implements OnDestroy { this.page.onDataChange(); } } + if (change.docs.some((doc) => doc._id.endsWith('shared_set'))) { + console.log('shared_set has been updated', change.docs); + } this.G.L.exit("DataService.handle_poll_db_change", pid, this.pending_changes); } @@ -2317,7 +2368,7 @@ export class DataService implements OnDestroy { // key existed in poll db, check whether update is allowed. const value = dict[dict_key]; const enc_value = encrypt(value, poll_pw); - if ((key != 'due') && (key != 'state') && (decrypt(doc.value, poll_pw) != value)) { + if ((key != 'due') && (key != 'state') && (decrypt(doc.value, poll_pw) != value) && (key.indexOf("inverse_indirect_map") == -1) && (key.indexOf("direct_delegation_map") == -1)) { // this is not allowed for poll docs! this.G.L.error("DataService.store_poll_data tried changing an existing poll data item", pid, key, value); } else if ((key == 'due') && (doc.due != value)) { @@ -2379,7 +2430,7 @@ export class DataService implements OnDestroy { // check which voter's data this is: const vid_prefix = key.slice(0, key.indexOf("§")), vid = this.user_cache[get_poll_key_prefix(pid) + 'myvid']; - if (vid_prefix != 'voter.' + vid && this.poll_caches[pid]['is_test']!='true') { + if (vid_prefix != 'voter.' + vid && this.poll_caches[pid]['is_test']!='true' && key.indexOf("inverse_indirect_map") == -1) { // it is not allowed to alter other voters' data! this.G.L.error("DataService.store_poll_data tried changing another voter's data item", pid, key); diff --git a/src/app/delegation-dialog/delegation-dialog.page.html b/src/app/delegation-dialog/delegation-dialog.page.html index 594d35d17..351e19482 100644 --- a/src/app/delegation-dialog/delegation-dialog.page.html +++ b/src/app/delegation-dialog/delegation-dialog.page.html @@ -60,6 +60,20 @@

+ + + + + + + {{i}} + + + + + diff --git a/src/app/delegation-dialog/delegation-dialog.page.ts b/src/app/delegation-dialog/delegation-dialog.page.ts index 95747c05d..4c463d5c1 100644 --- a/src/app/delegation-dialog/delegation-dialog.page.ts +++ b/src/app/delegation-dialog/delegation-dialog.page.ts @@ -18,7 +18,7 @@ along with vodle. If not, see . */ import { Component, OnInit, Input, ViewChild } from '@angular/core'; -import { Validators, UntypedFormBuilder, UntypedFormGroup, UntypedFormControl, ValidationErrors, AbstractControl } from '@angular/forms'; +import { Validators, UntypedFormBuilder, UntypedFormGroup, UntypedFormControl, ValidationErrors, AbstractControl, Form } from '@angular/forms'; import { IonInput, PopoverController } from '@ionic/angular'; import { TranslateService } from '@ngx-translate/core'; @@ -59,6 +59,8 @@ export class DelegationDialogPage implements OnInit { message_title: string; message_body: string; mailto_url: string; + rank: number; + rank_options: number[]; constructor( private popover: PopoverController, @@ -75,8 +77,14 @@ export class DelegationDialogPage implements OnInit { this.can_share = Capacitor.isNativePlatform() || this.can_use_web_share; this.formGroup = this.formBuilder.group({ delegate_nickname: new UntypedFormControl('', Validators.required), - from: new UntypedFormControl(this.G.S.email) + from: new UntypedFormControl(this.G.S.email), }); + + // checks if ranked delegation is allowed and if so, initialises the values needed for the drop-down menu + if (this.G.D.get_ranked_delegation_allowed(this.parent.pid)) { + this.initialise_rank_values(); + } + // TODO: what if already some delegation active or pending? // prepare a new delegation: [this.p, this.did, this.request, this.private_key, this.agreement] = this.G.Del.prepare_delegation(this.parent.pid); @@ -92,6 +100,24 @@ export class DelegationDialogPage implements OnInit { setTimeout(() => this.focus_element.setFocus(), 100); } + initialise_rank_values() { + const uid = this.parent.p.myvid; + const dir_del_map = this.G.D.get_direct_delegation_map(this.parent.pid); + const dir_del = dir_del_map.get(uid) || []; + var ranks = Array.from({ length: environment.delegation.max_delegations }, (_, i) => i + 1); + for (const entry of dir_del) { + if (entry === undefined) { + continue; + } + const indexToRemove: number = ranks.indexOf(Number(entry[1])); + if (indexToRemove !== -1) { + ranks.splice(indexToRemove, 1); + } + } + this.rank = ranks[0]; + this.rank_options = ranks; + } + delegate_nickname_changed() { const delegate_nickname = this.formGroup.get('delegate_nickname').value; this.G.D.setp(this.p.pid, "del_nickname." + this.did, delegate_nickname); @@ -105,6 +131,10 @@ export class DelegationDialogPage implements OnInit { this.update_request(); } + rank_changed(e) { + this.rank = e.detail.value; + } + update_request() { this.G.L.entry("DelegationDialogPage.update_request"); this.mailto_url = "mailto:" + encodeURIComponent(this.formGroup.get('delegate_nickname').value) + "?subject=" + encodeURIComponent(this.message_title) + "&body=" + encodeURIComponent(this.message_body); @@ -147,6 +177,7 @@ export class DelegationDialogPage implements OnInit { }).then(res => { this.G.L.info("DelegationDialogPage.share_button_clicked succeeded", res); this.G.Del.after_request_was_sent(this.parent.pid, this.did, this.request, this.private_key, this.agreement); + this.G.Del.set_delegate_rank(this.parent.pid, this.did, this.rank); this.popover.dismiss(); }).catch(err => { this.G.L.error("DelegationDialogPage.share_button_clicked failed", err); @@ -159,6 +190,7 @@ export class DelegationDialogPage implements OnInit { this.from_changed(); window.navigator.clipboard.writeText(this.delegation_link); this.G.Del.after_request_was_sent(this.parent.pid, this.did, this.request, this.private_key, this.agreement); + this.G.Del.set_delegate_rank(this.parent.pid, this.did, this.rank); LocalNotifications.schedule({ notifications: [{ title: this.translate.instant("delegation-request.notification-copied-link-title"), @@ -182,6 +214,7 @@ export class DelegationDialogPage implements OnInit { this.delegate_nickname_changed(); this.from_changed(); this.G.Del.after_request_was_sent(this.parent.pid, this.did, this.request, this.private_key, this.agreement); + this.G.Del.set_delegate_rank(this.parent.pid, this.did, this.rank); this.parent.update_delegation_info(); this.popover.dismiss(); this.G.L.exit("DelegationDialogPage.email_button_clicked"); diff --git a/src/app/delegation.service.ts b/src/app/delegation.service.ts index ca4a00f05..a2f8f8ce5 100644 --- a/src/app/delegation.service.ts +++ b/src/app/delegation.service.ts @@ -36,6 +36,7 @@ import { environment } from '../environments/environment'; import { GlobalService } from './global.service'; import { del_request_t, del_signed_response_t, del_response_t, del_option_spec_t, del_agreement_t } from './data.service'; import { Poll } from './poll.service'; +import { min } from 'rxjs/operators'; @Injectable({ providedIn: 'root' @@ -126,14 +127,11 @@ export class DelegationService { return a.delegate_vid; } - update_my_delegation(pid: string, oid: string, activate: boolean) { + update_my_delegation(pid: string, oid: string, activate: boolean, did? :string) { /** Called when voter toggles an option's delegation switch. * (De)activate an option's delegation */ const p = this.G.P.polls[pid]; - let did = this.get_my_outgoing_dids_cache(pid).get(oid); - if (!did) { - did = this.get_my_outgoing_dids_cache(pid).get("*"); - } + if (!did) { this.G.L.error("DelegationService.update_my_delegation without existing did", pid, oid, activate); } else { @@ -184,9 +182,39 @@ export class DelegationService { } } + set_delegation_pending(pid:string, did:string){ + const a = this.get_agreement(pid, did); + var dm = this.G.D.get_direct_delegation_map(pid); + var list = dm.get(a.client_vid) || []; + var new_list = []; + for (var entry of list) { + if (entry[0] === did) { + entry[2] = '0'; + } + new_list.push(entry); + } + dm.set(a.client_vid, new_list); + this.G.D.set_direct_delegation_map(pid, dm); + + // update inverse map + const sm = this.G.D.get_inverse_indirect_map(pid); + const eff_set = new Set(JSON.parse(sm.get(a.delegate_vid) || "[]")); + const client_set = new Set(JSON.parse(sm.get(a.client_vid) || "[]")); + var new_eff_set = new Set(); + for (let id of eff_set) { + if (id == a.client_vid || client_set.has(id)) { + continue; + } + new_eff_set.add(id); + } + sm.set(a.delegate_vid, JSON.stringify(Array.from(new_eff_set))); + } + revoke_delegation(pid: string, did: string, oid: string) { this.G.L.entry("DelegationService.revoke_delegation", pid, did); - const a = this.get_delegation_agreements_cache(pid).get(did); + // update effectve delegation for delegate_vid + const a = this.get_agreement(pid, did); + const p = this.G.P.polls[pid]; if ((a.client_vid != p.myvid)) { this.G.L.error("DelegationService.revoke_delegation without request from me", pid, did); @@ -195,11 +223,6 @@ export class DelegationService { const acache = this.get_delegation_agreements_cache(pid); if (acache) { const oids = acache.get(did).active_oids; - if (oids) { - for (const oid of oids) { - p.del_delegation(p.myvid, oid); - } - } acache.delete(did); } } @@ -207,70 +230,185 @@ export class DelegationService { if (dcache) { dcache.delete(oid); } + + const sm = this.G.D.get_inverse_indirect_map(pid); + const eff_set = new Set(JSON.parse(sm.get(a.client_vid) || "[]")); + + // gets all voters that are affected by the delegation being deleted + if (!this.G.D.get_ranked_delegation_allowed(pid)){ + var stack = []; + for (let id of sm.keys()) { + if (JSON.parse(sm.get(id) || "[]").includes(a.client_vid)) { + stack.push(id); + } + } + while (stack.length > 0) { + const curr_del = stack.pop(); + const old_delegate_eff_set = new Set(JSON.parse(sm.get(curr_del) || "[]")); + var new_delegate_eff_set = new Set(); + for (let id of old_delegate_eff_set) { + if (id == a.client_vid || eff_set.has(id)) { + continue; + } + new_delegate_eff_set.add(id); + } + sm.set(curr_del, JSON.stringify(Array.from(new_delegate_eff_set))); + } + + this.G.D.set_inverse_indirect_map(pid, sm); + } + + // update direct delegation map + var dir_del_map = this.G.D.get_direct_delegation_map(pid); + var dir_del = dir_del_map.get(a.client_vid) || []; + var new_list = []; + for (var entry of dir_del) { + if (entry[2] === '2') { + continue; + } + new_list.push(entry); + } + dir_del_map.set(a.client_vid, new_list); + this.G.D.set_direct_delegation_map(pid, dir_del_map); + + if (this.G.D.get_ranked_delegation_allowed(pid)){ + this.recalculate_delegation_map(pid); + } + + this.G.L.exit("DelegationService.revoke_delegation"); + } + + recalculate_delegation_map(pid: string) { + const active_delegations = new Map(); + this.find_path(pid); + const dm = this.G.D.get_direct_delegation_map(pid); + for (const [vid, dels] of dm) { + active_delegations.set(vid, ""); + } + for (const [vid, dels] of dm) { + for (const del of dels) { + if (del[2] === '2') { + const a = this.get_agreement(pid, del[0]); + active_delegations.set(a.delegate_vid, a.client_vid); + break; + } + } + } + + let visited = new Set(); + let inverse_map = new Map>(); + for (const vid of active_delegations.keys()) { + if (visited.has(vid)) { + continue; + } + this.bfs(pid, vid, active_delegations, visited, inverse_map); + } + // stringifying the inverse map + let new_sm = new Map(); + for (const [vid, set] of inverse_map) { + new_sm.set(vid, JSON.stringify(Array.from(set))); + } + this.G.D.set_inverse_indirect_map(pid, new_sm); + } + + // recursive function to calculate the inverse indirect map + bfs(pid: string, vid: string, active_delegations: Map, visited: Set, inverse_map: Map>) { + if (visited.has(vid)) { + return; + } + visited.add(vid); + const client_vid = active_delegations.get(vid); + if (!client_vid) { + return; + } + if (!visited.has(client_vid)) { + this.bfs(pid, client_vid, active_delegations, visited, inverse_map); + } + if (!inverse_map.has(client_vid)) { + inverse_map.set(client_vid, new Set()); + } + let s = new Set(inverse_map.get(client_vid) || []); + s.add(client_vid); + inverse_map.set(vid, s); } // RESPONDING TO A DELEGATION REQUEST: get_incoming_request_status(pid: string, did: string): Array { - if (pid in this.G.P.polls) { - const p = this.G.P.polls[pid]; - if (p.state != 'running') { - return ["closed"]; - } else { - // check if request has been retrieved from db: - const agreement = this.G.Del.get_delegation_agreements_cache(pid).get(did); - if (agreement) { - // check if already answered: - if (agreement.status == 'agreed') { - return ["accepted"]; - } - // check if already delegating (in)directly back to client_vid for at least one option: - var status: Array; - const dirdelmap = this.G.D.direct_delegation_map_caches[pid], - effdelmap = this.G.D.effective_delegation_map_caches[pid], - inveffdelmap = this.G.D.inv_effective_delegation_map_caches[pid], - myvid = p.myvid, - client_vid = agreement.client_vid; - if (client_vid == myvid) { - return ["impossible", "is-self"]; - } - let two_way = false, cycle = false, weight_exceeded = false; - for (let oid of p.oids) { - const effdel_vid = effdelmap.get(oid).get(myvid) || myvid; - const thisinveffdelmap = inveffdelmap.get(oid) || new Map(); - if (1 + (thisinveffdelmap.get(client_vid)||new Set([client_vid])).size - + (thisinveffdelmap.get(effdel_vid)||new Set([effdel_vid])).size - > environment.delegation.max_weight) { - weight_exceeded = true; - break; - } - if ((dirdelmap.get(oid) || new Map()).get(myvid) == client_vid) { - two_way = true; - } else if (effdel_vid == client_vid) { - cycle = true; - } - } - if (weight_exceeded) { - status = ["impossible", "weight-exceeded"]; - } else if (two_way) { - status = ["possible", "two-way"]; - } else if (cycle) { - status = ["possible", "cycle"]; - } else { - status = ["possible", "acyclic"]; - } - if (agreement.status == 'declined') { - status[0] = "declined, " + status[0]; - return status; - } else { - return status; - } - } else { - return ["impossible", "not-in-db"]; + if (!(pid in this.G.P.polls)) { + return ["impossible", "poll-unknown"]; + } + const p = this.G.P.polls[pid]; + if (p.state != 'running') { + return ["closed"]; + } + // check if request has been retrieved from db: + const agreement = this.G.Del.get_delegation_agreements_cache(pid).get(did); + if (!agreement) { + return ["impossible", "not-in-db"]; + } + + // check if request has already been revoked + const revoked = this.G.D.getv(pid, "del_status." + did, agreement.client_vid); + if (revoked == "revoked") { + return ["impossible", "revoked"]; + } + + // check if already answered: + if (agreement.status == 'agreed') { + return ["accepted"]; + } + + if (this.G.D.get_ranked_delegation_allowed(pid)){ + return ["ranked"]; + } + + const a = this.get_agreement(pid, did); + + // check if already delegating (in)directly back to client_vid for at least one option: + var status: Array; + const myvid = p.myvid, + client_vid = agreement.client_vid; + if (client_vid == myvid) { + return ["impossible", "is-self"]; + } + let two_way = false, cycle = false, weight_exceeded = false; + const dirdelmap = this.G.D.get_direct_delegation_map(pid); + + if (!this.G.D.get_ranked_delegation_allowed(pid)){ + const list = dirdelmap.get(myvid) || []; + for (const [did2, _, active] of list) { + const a = this.get_agreement(pid, did2); + if (a.client_vid == client_vid) { + two_way = true; + break; } } + } + // check for cycles: + const map = this.G.D.get_inverse_indirect_map(pid); + for (let oid of p.oids){ + const set = new Set(JSON.parse(map.get(client_vid) || "[]")); + if (set.has(myvid)) { + cycle = true; + break; + } + } + + if (weight_exceeded) { + status = ["impossible", "weight-exceeded"]; + } else if (two_way) { + status = ["impossible", "two-way"]; + } else if (cycle) { + status = ["impossible", "cycle"]; } else { - return ["impossible", "poll-unknown"]; + status = ["possible", "acyclic"]; + } + if (agreement.status == 'declined') { + status[0] = "declined, " + status[0]; + return status; + } else { + return status; } } @@ -286,8 +424,11 @@ export class DelegationService { } update_incoming_request_status(pid: string, did: string, status: string) { - const cache = this.G.D.incoming_dids_caches[pid], - [from, url, old_status] = cache.get(did); + const cache = this.G.D.incoming_dids_caches[pid]; + if (!cache){ + return; + } + const [from, url, old_status] = cache.get(did); if (status != old_status[0]) { this.store_incoming_request(pid, did, from, url, status); } @@ -299,6 +440,55 @@ export class DelegationService { signed_response = this.sign_response(response, private_key); this.G.L.info("DelegationService.accept", pid, did, response); this.set_my_signed_response(pid, did, signed_response); + + const a = this.get_agreement(pid, did); + // update effective delegation + if (!this.G.D.get_ranked_delegation_allowed(pid)){ + const sm = this.G.D.get_inverse_indirect_map(pid); + const eff_set = new Set(JSON.parse(sm.get(a.client_vid) || "[]")); + var new_sm = sm; + for (let id of this.G.P.polls[pid].T.all_vids_set) { + if (id === a.client_vid) { + continue; + } + const old_eff_set = new Set(JSON.parse(sm.get(id) || "[]")); + if (id === a.delegate_vid || old_eff_set.has(a.delegate_vid)) { + var new_eff_set = old_eff_set; + for (const id2 of eff_set){ + new_eff_set.add(id2); + } + new_eff_set.add(a.client_vid); + new_sm.set(id, JSON.stringify(Array.from(new_eff_set))); + } + } + this.G.D.set_inverse_indirect_map(pid, new_sm); + + // update direct delegation map + var dir_del_map = this.G.D.get_direct_delegation_map(pid); + var dir_del = dir_del_map.get(a.client_vid) || []; + for (var entry of dir_del) { + if (entry[0] === did) { + entry[2] = '2'; + break; + } + } + dir_del_map.set(a.client_vid, dir_del); + this.G.D.set_direct_delegation_map(pid, dir_del_map); + return; + } + + var dir_del_map = this.G.D.get_direct_delegation_map(pid); + var dir_del = dir_del_map.get(a.client_vid) || []; + for (var entry of dir_del) { + if (entry[0] === did) { + entry[2] = '1'; + break; + } + } + dir_del_map.set(a.client_vid, dir_del); + this.G.D.set_direct_delegation_map(pid, dir_del_map); + + this.recalculate_delegation_map(pid); } decline(pid: string, did: string, private_key?: string) { @@ -310,6 +500,125 @@ export class DelegationService { signed_response = this.sign_response(response, private_key); this.G.L.info("DelegationService.decline", pid, did, response); this.set_my_signed_response(pid, did, signed_response); + + // update map + if (this.G.D.get_ranked_delegation_allowed(pid)){ + this.recalculate_delegation_map(pid); + } + } + + // todo: make this give a different news item to the client + decline_due_to_error(pid: string, did: string, private_key?: string) { + if (!private_key) { + private_key = this.get_private_key(pid, did); + } + const response = {option_spec: {type: "+", oids: []}} as del_response_t, // i.e., accept NO oids + signed_response = this.sign_response(response, private_key); + this.G.L.info("DelegationService.decline_due_to_error", pid, did, response); + this.set_my_signed_response(pid, did, signed_response); + } + + private find_path(pid: string) { + this.min_sum_all(pid); + } + + private delegating_voters(pid: string) : Set { + let del_voters = new Set(); + const dir_del_map = this.G.D.get_direct_delegation_map(pid); + for (const vid of this.G.P.polls[pid].T.all_vids_set) { + for (const [did, rank, active] of dir_del_map.get(vid) || []) { + if (active != '0') { + del_voters.add(vid); + break; + } + } + } + return del_voters; + } + + private is_casting_voter(pid: string, vid: string) { + const dir_del_map = this.G.D.get_direct_delegation_map(pid); + for (const [did, rank, active] of dir_del_map.get(vid) || []) { + if (active == '0') { + continue; + } + return false; + } + return true; + } + + private find_all_paths(pid: string, vid: string, current_path: string[], paths: string[][]) { + const dm = this.G.D.get_direct_delegation_map(pid); + for (const [did, _, active] of dm.get(vid) || []) { + if (active == '0') { + continue; + } + const a = this.get_agreement(pid, did); + let new_path: string[] = [...current_path, did]; + if (this.is_casting_voter(pid, a.delegate_vid)) { + paths.push(new_path); + } else if (!current_path.includes(did)) { + this.find_all_paths(pid, a.delegate_vid, new_path, paths); + } + } + } + + private get_rank_from_did(pid: string, did: string) : number { + const dm = this.G.D.get_direct_delegation_map(pid); + const a = this.get_agreement(pid, did); + for (const [did2, rank, active] of dm.get(a.client_vid) || []) { + if (did2 == did) { + return Number(rank); + } + } + return 0; + } + + private min_sum(pid: string, vid: string) : Array { + const dm = this.G.D.get_direct_delegation_map(pid); + let paths: string[][] = [[]]; + this.find_all_paths(pid, vid, [], paths); + + let minSumPath: string[] = []; + let minSum = Number.MAX_VALUE; + + for (const path of paths) { + if (path.length == 0) { + continue; + } + let pathSum = 0; + for (const did of path) { + pathSum += this.get_rank_from_did(pid, did); + } + if (pathSum < minSum) { + minSum = pathSum; + minSumPath = path; + } + } + return minSumPath; + } + + private min_sum_all(pid: string) { + const dm = this.G.D.get_direct_delegation_map(pid); + for (let vid of this.delegating_voters(pid)) { + const minSumPath = this.min_sum(pid, vid); + for (const did of minSumPath) { + const a = this.get_agreement(pid, did); + const delegations = dm.get(a.client_vid) || []; + var newDelegations = []; + for (const [did2, rank, active] of delegations) { + if (did2 == did) { + newDelegations.push([did2, rank, '2']); + } else if (active === '2') { + newDelegations.push([did2, rank, '1']); + } else { + newDelegations.push([did2, rank, active]); + } + } + dm.set(a.client_vid, newDelegations); + } + } + this.G.D.set_direct_delegation_map(pid, dm); } // DATA HANDLING: @@ -330,6 +639,21 @@ export class DelegationService { this.G.D.setp(pid, "del_private_key." + did, value); } + get_delegate_rank(pid: string, did: string): number { + const rank = Number(this.G.D.getv(pid, "del_rank." + did)); + return rank; + } + + set_delegate_rank(pid: string, did: string, value: number) { + // this.G.D.setv(pid, "del_rank." + did, JSON.stringify(value)); + var direct_del_map = this.G.D.get_direct_delegation_map(pid); + var myvid = this.G.P.polls[pid].myvid; + var dir_del = direct_del_map.get(myvid) || []; + dir_del.push([did, JSON.stringify(value), '0']); + direct_del_map.set(myvid, dir_del); + this.G.D.set_direct_delegation_map(pid, direct_del_map); + } + get_request(pid: string, did: string, client_vid?: string): del_request_t { if (!client_vid) { client_vid = this.get_agreement(pid, did).client_vid; @@ -365,7 +689,7 @@ export class DelegationService { get_agreement(pid: string, did: string): del_agreement_t { const cache = this.get_delegation_agreements_cache(pid); let a = cache.get(did); - this.G.L.entry("DelegationService.get_agreement", pid, did, a); + // this.G.L.entry("DelegationService.get_agreement", pid, did, a); if (!a) { a = { status: "pending", @@ -403,11 +727,6 @@ export class DelegationService { const acache = this.get_delegation_agreements_cache(pid); if (acache) { const oids = acache.get(did).active_oids; - if (oids) { - for (const oid of oids) { - p.del_delegation(client_vid, oid); - } - } acache.delete(did); } } @@ -431,8 +750,7 @@ export class DelegationService { this.update_agreement(pid, did, a, request, signed_response); } - update_agreement(pid: string, did: string, agreement: del_agreement_t, - request: del_request_t, signed_response: del_signed_response_t) { + update_agreement(pid: string, did: string, agreement: del_agreement_t, request: del_request_t, signed_response: del_signed_response_t) { /** after changes to request or response, * compare request and response, set status, extract accepted and active oids */ this.G.L.entry("DelegationService.update_agreement", pid, did, agreement, request, signed_response); @@ -463,8 +781,11 @@ export class DelegationService { } const pair = JSON.parse(this.G.D.open_signed(signed_response, request.public_key)); const response = {option_spec: {type: pair[0], oids: pair[1]}} as del_response_t; - if (!response.option_spec) { - a.status = "declined"; + if (pair.status == "revoked") { + a.status = "revoked"; + return; + } else if (!response.option_spec) { + a.status = "declined" } else { if (response.option_spec.type == "+") { // oids specifies accepted options @@ -517,19 +838,13 @@ export class DelegationService { if (!(a.accepted_oids.has(oid) && request.option_spec.oids.includes(oid))) { // oid no longer active: a.active_oids.delete(oid); - p.del_delegation(a.client_vid, oid); this.G.L.trace("DelegationService.update_agreement deactivated oid", pid, oid); } } for (const oid of request.option_spec.oids) { if (a.accepted_oids.has(oid) && !a.active_oids.has(oid)) { // oid newly active: - if (p.add_delegation(a.client_vid, oid, a.delegate_vid)) { - a.active_oids.add(oid); - this.G.L.trace("DelegationService.update_agreement activated oid", pid, oid); - } else { - this.G.L.warn("DelegationService.update_agreement couldn't activate oid", pid, oid); - } + a.active_oids.add(oid); } } } else if (request.option_spec.type == "-") { @@ -538,27 +853,18 @@ export class DelegationService { if (request.option_spec.oids.includes(oid) || !a.accepted_oids.has(oid)) { // oid no longer active: a.active_oids.delete(oid); - p.del_delegation(a.client_vid, oid); this.G.L.trace("DelegationService.update_agreement deactivated oid", pid, oid); } } for (const oid of a.accepted_oids) { if ((!a.active_oids.has(oid)) && (!request.option_spec.oids.includes(oid))) { - // oid newly active: - if (p.add_delegation(a.client_vid, oid, a.delegate_vid)) { - a.active_oids.add(oid); - this.G.L.trace("DelegationService.update_agreement activated oid", pid, oid); - } else { - this.G.L.warn("DelegationService.update_agreement couldn't activate oid", pid, oid); - } + a.active_oids.add(oid); } } } a.status = (a.accepted_oids.size > 0) ? "agreed" : "declined"; } - } - // if voter affected directly, add news item: if (a.client_vid == p.myvid) { if ((old_status=="pending") && (a.status=="agreed")) { @@ -590,7 +896,6 @@ export class DelegationService { }); } } - // TODO: update tally! this.G.L.exit("DelegationService.update_agreement", a.status, [...a.accepted_oids], [...a.active_oids]); @@ -612,6 +917,10 @@ export class DelegationService { response2string(response: del_response_t): string { /** turn response data without signature deterministically into a string message that can be signed: */ + // if response is a status message, return it as is: + if (response.status) { + return JSON.stringify(response); + } return JSON.stringify([response.option_spec.type, response.option_spec.oids]); } @@ -622,6 +931,13 @@ export class DelegationService { return this.G.D.outgoing_dids_caches[pid]; } + get_my_incoming_dids_cache(pid:string) { + if (!this.G.D.incoming_dids_caches[pid]) { + this.G.D.incoming_dids_caches[pid] = new Map(); + } + return this.G.D.incoming_dids_caches[pid]; + } + get_delegation_agreements_cache(pid:string) { if (!this.G.D.delegation_agreements_caches[pid]) { this.G.D.delegation_agreements_caches[pid] = new Map(); diff --git a/src/app/delrespond/delrespond.page.html b/src/app/delrespond/delrespond.page.html index 9b54b89f0..860d0800c 100644 --- a/src/app/delrespond/delrespond.page.html +++ b/src/app/delrespond/delrespond.page.html @@ -31,6 +31,42 @@ + + + +

+

+

+

+
+
+ + + +   + +    + +   + + + + + + +

 

+

 

+

+ +   + +
+
+
@@ -70,9 +106,9 @@

+

@@ -111,7 +147,7 @@

@@ -202,7 +238,7 @@

+ (click)="revoke()">      @@ -265,8 +301,7 @@

+

@@ -297,7 +332,7 @@

+

@@ -316,7 +351,7 @@

+

@@ -343,8 +378,8 @@

+ + @@ -357,7 +392,7 @@

+   + + + + + +   + + diff --git a/src/app/delrespond/delrespond.page.ts b/src/app/delrespond/delrespond.page.ts index 987529fd1..aa348d7e1 100644 --- a/src/app/delrespond/delrespond.page.ts +++ b/src/app/delrespond/delrespond.page.ts @@ -80,10 +80,16 @@ export class DelrespondPage implements OnInit { onDataReady() { // called when DataService initialization was slower than view initialization if (this.pid in this.G.P.polls) { - this.p = this.G.P.polls[this.pid]; + this.p = this.G.P.polls[this.pid]; + }else{ + this.p = null; } - this.G.L.entry("DelrespondPage.onDataReady", this.pid, this.p.state); this.status = this.G.Del.get_incoming_request_status(this.pid, this.did); + if (this.status[0] === 'impossible' && this.status[1] === 'poll-unknown') { // poll does not exist, prevents further errors + this.ready = true; + return; + } + this.G.L.entry("DelrespondPage.onDataReady", this.pid, this.p.state); this.G.Del.store_incoming_request(this.pid, this.did, this.from, this.url, this.status[0]); this.ready = true; this.G.L.exit("DelrespondPage.onDataReady"); @@ -97,6 +103,7 @@ export class DelrespondPage implements OnInit { // GUI callbacks: + // TODO: verify that it is still possible to accept the request accept() { /** store positive response and go to poll page */ this.G.Del.accept(this.pid, this.did, this.private_key); @@ -111,6 +118,20 @@ export class DelrespondPage implements OnInit { this.router.navigate(["/poll/" + this.pid]); } + // TODO: use to send a different message to the delegator + decline_due_to_error() { + /** store negative response and go to poll page */ + this.G.Del.decline_due_to_error(this.pid, this.did, this.private_key); + this.router.navigate(["/poll/" + this.pid]); + } + + revoke() { + /** store negative response and go to poll page */ + this.G.Del.set_delegation_pending(this.pid, this.did); + this.G.Del.decline(this.pid, this.did, this.private_key); + this.router.navigate(["/poll/" + this.pid]); + } + dismiss() { this.router.navigate(["/mypolls"]); } diff --git a/src/app/draftpoll/draftpoll.page.html b/src/app/draftpoll/draftpoll.page.html index 8318eebe5..906f63042 100644 --- a/src/app/draftpoll/draftpoll.page.html +++ b/src/app/draftpoll/draftpoll.page.html @@ -289,6 +289,13 @@ style="font-size: smaller"> + + + + + diff --git a/src/app/draftpoll/draftpoll.page.ts b/src/app/draftpoll/draftpoll.page.ts index 20f65cb5f..b9483100d 100644 --- a/src/app/draftpoll/draftpoll.page.ts +++ b/src/app/draftpoll/draftpoll.page.ts @@ -83,7 +83,8 @@ export class DraftpollPage implements OnInit { is_test?, type?, language?, title?, desc?, url?, - due_type?, due_custom?, + due_type?, due_custom?, + allow_ranked?, db?, db_from_pid?, db_custom_server_url?, db_custom_password?, options?: option_data_t[] }; @@ -142,7 +143,7 @@ export class DraftpollPage implements OnInit { this.stage = 0; if (!this.pd) { this.G.L.info("DraftpollPage editing new draft"); - this.pd = { db:'default', language:this.G.S.language }; + this.pd = { db:'default', language:this.G.S.language, allow_ranked:false}; } else { this.G.L.info("DraftpollPage editing draft with data", this.pd); this.pd.due_custom = (this.pd.due_custom||'')!=''?(new Date(this.pd.due_custom)):null; @@ -158,7 +159,8 @@ export class DraftpollPage implements OnInit { pid:p.pid, type:p.type, language:p.language, title:p.title, desc:p.desc, url:p.url, - due_type:p.due_type, due_custom:p.due_custom, + due_type:p.due_type, due_custom:p.due_custom, + allow_ranked:p.allow_ranked, db:p.db, db_from_pid:p.db_from_pid, db_custom_server_url:p.db_custom_server_url, db_custom_password:p.db_custom_password, options: [] }; @@ -186,6 +188,7 @@ export class DraftpollPage implements OnInit { poll_url: this.pd.url||'', poll_due_type: this.pd.due_type||'', poll_due_custom: (!this.pd.due_custom)?'':this.pd.due_custom.toISOString(), + poll_allow_ranked_delegation: this.pd.allow_ranked||false, }); if (this.pd.language||this.pd.db_from_pid||this.pd.db_custom_server_url) { this.advanced_expanded = true; @@ -233,6 +236,7 @@ export class DraftpollPage implements OnInit { db_from_pid: this.pd.db_from_pid||'', db_custom_server_url: this.pd.db_custom_server_url||'', db_custom_password: this.pd.db_custom_password||'', + db_allow_ranked: this.pd.allow_ranked||false, }); } } @@ -266,6 +270,7 @@ export class DraftpollPage implements OnInit { p.url = this.pd.url; p.due_type = this.pd.due_type; p.due_custom = this.pd.due_custom; + p.allow_ranked = this.pd.allow_ranked || false; p.set_due(); p.db = this.pd.db; p.db_from_pid = this.pd.db_from_pid; @@ -320,6 +325,7 @@ export class DraftpollPage implements OnInit { this.G.L.warn("DraftpollPage.ionViewWillLeave localNotifications.schedule failed:", err); }); } + console.log("DraftpollPage.ionViewWillLeave p object:", p); this.G.L.trace("DraftpollPage.ionViewWillLeave D.pids:", [...this.G.D.pids]); this.G.L.exit("DraftpollPage.ionViewWillLeave"); } @@ -379,6 +385,12 @@ export class DraftpollPage implements OnInit { if (c.valid) this.pd.due_custom = new Date(c.value); } + set_poll_allow_ranked_delegation(){ + this.G.L.trace("set_poll_allow_ranked_delegation",this.pd.allow_ranked); + this.pd.allow_ranked = this.formGroup.get('poll_allow_ranked_delegation').value; + this.G.L.trace("set_poll_allow_ranked_delegation result",this.pd.allow_ranked); + } + set_option_name(i: number) { let c = this.formGroup.get('option_name'+i); this.G.L.trace("set_option_name",i,c.value); @@ -720,6 +732,7 @@ export class DraftpollPage implements OnInit { poll_url: new UntypedFormControl('', Validators.pattern(this.G.urlRegex)), poll_due_type: new UntypedFormControl('', Validators.required), poll_due_custom: new UntypedFormControl('', this.allowed_date.bind(this)), + poll_allow_ranked_delegation: new UntypedFormControl(false), }); this.G.P.update_ref_date(); } diff --git a/src/app/mypolls/mypolls.page.ts b/src/app/mypolls/mypolls.page.ts index 3caeb53c9..e5722b8f5 100644 --- a/src/app/mypolls/mypolls.page.ts +++ b/src/app/mypolls/mypolls.page.ts @@ -87,14 +87,25 @@ export class MypollsPage implements OnInit { this.unanswered_requests = []; for (const pid in this.G.P.polls) { const cache = this.G.D.incoming_dids_caches[pid]; + var newcache = this.G.D.incoming_dids_caches[pid]; if (cache) { for (let [did, [from, url, status]] of cache) { if (["possible","two-way","cycle"].includes(status)) { + console.log("onDataChange", pid, did, from, url, status); this.G.L.trace("MypollsPage.onDataChange found unanswered request", did, from, url, status); + // check if request has been revoked: + const a = this.G.Del.get_agreement(pid, did); + if (this.G.D.getv(pid, "del_status." + did, a.client_vid) === 'revoked') { + // delete request from cache: + newcache.delete(did); + continue; + } + this.unanswered_requests.push({pid:pid, did:did, from:from, url:url, status:status}); } } } + this.G.D.incoming_dids_caches[pid] = newcache; // update cache to remove revoked requests } } diff --git a/src/app/poll.service.ts b/src/app/poll.service.ts index 02cd2a1a2..ccf458c6d 100644 --- a/src/app/poll.service.ts +++ b/src/app/poll.service.ts @@ -182,6 +182,8 @@ export class Poll { _state: string; // cache for state since it is asked very often syncing: boolean = false; allow_voting: boolean = false; + delegate_id: string = null; + did: string = null; constructor (G:GlobalService, pid?:string) { this.G = G; @@ -429,6 +431,9 @@ export class Poll { get due_type(): poll_due_type_t { return this.G.D.getp(this._pid, 'due_type') as poll_due_type_t; } set due_type(value: poll_due_type_t) { this.G.D.setp(this._pid, 'due_type', value); } + get allow_ranked(): boolean { return this.G.D.getp(this._pid, 'allow_ranked') == 'true'; } + set allow_ranked(value: boolean) { this.G.D.setp(this._pid, 'allow_ranked', value.toString()); } + // Date objects are stored as ISO strings: get start_date(): Date { @@ -469,18 +474,12 @@ export class Poll { // will only be called by the option itself to self-register in its poll! if (o.oid in this._options) { return false; - } else { - this._options[o.oid] = o; - if (!this.own_ratings_map.has(o.oid)) this.own_ratings_map.set(o.oid, new Map()); - if (!this.proxy_ratings_map.has(o.oid)) this.proxy_ratings_map.set(o.oid, new Map()); - if (!this.direct_delegation_map.has(o.oid)) this.direct_delegation_map.set(o.oid, new Map()); - if (!this.inv_direct_delegation_map.has(o.oid)) this.inv_direct_delegation_map.set(o.oid, new Map()); - if (!this.indirect_delegation_map.has(o.oid)) this.indirect_delegation_map.set(o.oid, new Map()); - if (!this.inv_indirect_delegation_map.has(o.oid)) this.inv_indirect_delegation_map.set(o.oid, new Map()); - if (!this.effective_delegation_map.has(o.oid)) this.effective_delegation_map.set(o.oid, new Map()); - if (!this.inv_effective_delegation_map.has(o.oid)) this.inv_effective_delegation_map.set(o.oid, new Map()); - return true; } + + this._options[o.oid] = o; + if (!this.own_ratings_map.has(o.oid)) this.own_ratings_map.set(o.oid, new Map()); + if (!this.proxy_ratings_map.has(o.oid)) this.proxy_ratings_map.set(o.oid, new Map()); + return true; } get options(): Record { return this._options; } @@ -528,7 +527,14 @@ export class Poll { } get_my_proxy_rating(oid: string): number { - return (this.proxy_ratings_map.get(oid) || new Map()).get(this.myvid) || 0; + if (this.delegate_id){ + const agr = this.G.Del.get_agreement(this.pid, this.delegate_id); + if (agr.active_oids.has(oid)){ + return +this.G.D.getv(this.pid, "rating."+oid, this.delegate_id)||0; + } + } + return this.get_my_own_rating(oid); + // return (this.proxy_ratings_map.get(oid) || new Map()).get(this.myvid) || 0; } get_my_effective_rating(oid: string): number { @@ -556,13 +562,15 @@ export class Poll { } get am_abstaining(): boolean { - /** whether or not I'm currently abstaining */ + /** whether or not I'm currently abstaining and haven't delegated */ + if (this.have_delegated) { + return this.T.votes_map.get(this.myvid) === undefined; + } if (!!this.T.votes_map) { const myvote = this.T.votes_map.get(this.myvid); return myvote === undefined; - } else { - return false; } + return false; } get my_n_rated_positive(): number { @@ -588,12 +596,36 @@ export class Poll { } get have_delegated(): boolean { - const did = this.G.Del.get_my_outgoing_dids_cache(this.pid).get("*"); - if (!did) return false; - const agreement = this.G.Del.get_agreement(this.pid, did); - return (agreement.status == "agreed") && (agreement.active_oids.size == agreement.accepted_oids.size); + const dir_del = this.G.D.get_direct_delegation_map(this.pid); + const list = dir_del.get(this.myvid) || []; + for (const entry of list) { + if (entry[2] == "2") { + const a = this.G.Del.get_agreement(this.pid, entry[0]); + this.delegate_id = a.delegate_vid; + return true; + } + } + return false; + } + + getDidFromUrl(url: string): string | null { + // Match the structure of the URL to extract the second item after '/delrespond/' + const regex = /\/delrespond\/[^/]+\/([^/]+)/; + + const match = url.match(regex); + if (match && match[1]) { + return match[1]; // Return the 'did' + } + + return null; // Return null if no match is found } + have_been_delegated(clientVid: string, listOfDelInv: object[]) { + return false; + } + + + ratings_have_changed = false; // OTHER HOOKS: @@ -745,102 +777,6 @@ export class Poll { return this._own_ratings_map; } - // for each oid and vid, the direct delegate's vid (default: null, meaning no delegation): - _direct_delegation_map: Map>; - get direct_delegation_map(): Map> { - if (!this._direct_delegation_map) { - if (this._pid in this.G.D.direct_delegation_map_caches) { - this._direct_delegation_map = this.G.D.direct_delegation_map_caches[this._pid]; - } else { - this.G.D.direct_delegation_map_caches[this._pid] = this._direct_delegation_map = new Map(); - for (const oid of this.oids) { - this._direct_delegation_map.set(oid, new Map()); - } - } - } - return this._direct_delegation_map; - } - - // for each oid and vid, the set of vids who directly delegated to this vid (default: null, meaning no delegation): - _inv_direct_delegation_map: Map>>; - get inv_direct_delegation_map(): Map>> { - if (!this._inv_direct_delegation_map) { - if (this._pid in this.G.D.inv_direct_delegation_map_caches) { - this._inv_direct_delegation_map = this.G.D.inv_direct_delegation_map_caches[this._pid]; - } else { - this.G.D.inv_direct_delegation_map_caches[this._pid] = this._inv_direct_delegation_map = new Map(); - for (const oid of this.oids) { - this._inv_direct_delegation_map.set(oid, new Map()); - } - } - } - return this._inv_direct_delegation_map; - } - - // for each oid and vid, the set of vids who this voter directly or indirectly delegated to (default: null, meaning no delegation): - _indirect_delegation_map: Map>>; - get indirect_delegation_map(): Map>> { - if (!this._indirect_delegation_map) { - if (this._pid in this.G.D.indirect_delegation_map_caches) { - this._indirect_delegation_map = this.G.D.indirect_delegation_map_caches[this._pid]; - } else { - this.G.D.indirect_delegation_map_caches[this._pid] = this._indirect_delegation_map = new Map(); - for (const oid of this.oids) { - this._indirect_delegation_map.set(oid, new Map()); - } - } - } - return this._indirect_delegation_map; - } - - // for each oid and vid, the set of vids who have directly or indirectly delegated to this voter (default: null, meaning no delegation): - _inv_indirect_delegation_map: Map>>; - get inv_indirect_delegation_map(): Map>> { - if (!this._inv_indirect_delegation_map) { - if (this._pid in this.G.D.inv_indirect_delegation_map_caches) { - this._inv_indirect_delegation_map = this.G.D.inv_indirect_delegation_map_caches[this._pid]; - } else { - this.G.D.inv_indirect_delegation_map_caches[this._pid] = this._inv_indirect_delegation_map = new Map(); - for (const oid of this.oids) { - this._inv_indirect_delegation_map.set(oid, new Map()); - } - } - } - return this._inv_indirect_delegation_map; - } - - // for each oid and vid, the effective delegate's vid (default: null, meaning no delegation): - _effective_delegation_map: Map>; - get effective_delegation_map(): Map> { - if (!this._effective_delegation_map) { - if (this._pid in this.G.D.effective_delegation_map_caches) { - this._effective_delegation_map = this.G.D.effective_delegation_map_caches[this._pid]; - } else { - this.G.D.effective_delegation_map_caches[this._pid] = this._effective_delegation_map = new Map(); - for (const oid of this.oids) { - this._effective_delegation_map.set(oid, new Map()); - } - } - } - return this._effective_delegation_map; - } - - // for each oid and vid, the set of vids who effectively delegated to this vid, excluding the vid itself (default: null, meaning no delegation): - _inv_effective_delegation_map: Map>>; - get inv_effective_delegation_map(): Map>> { - if (!this._inv_effective_delegation_map) { - if (this._pid in this.G.D.inv_effective_delegation_map_caches) { - this._inv_effective_delegation_map = this.G.D.inv_effective_delegation_map_caches[this._pid]; - } else { - this.G.D.inv_effective_delegation_map_caches[this._pid] = this._inv_effective_delegation_map = new Map(); - for (const oid of this.oids) { - this._inv_effective_delegation_map.set(oid, new Map()); - } - } - } - return this._inv_effective_delegation_map; - } - // for each oid and vid, the proxy (post-delegation) rating (default: 0): _proxy_ratings_map: Map>; get proxy_ratings_map(): Map> { @@ -913,271 +849,11 @@ export class Poll { // Methods dealing with changes to the delegation graph: - add_delegation(client_vid:string, oid:string, delegate_vid:string): boolean { - if (!environment.delegation.enabled) { - this.G.L.error("PollService.add_delegation when delegation is disabled", this._pid, client_vid, oid, delegate_vid); - return false; - } - /** Called whenever a delegation shall be added. Returns whether this succeeded */ - - this.G.L.debug("add_delegation entry", this.pid, oid, client_vid, delegate_vid); - - const dir_d_map = this.direct_delegation_map.get(oid), - eff_d_map = this.effective_delegation_map.get(oid), - new_eff_d_vid = eff_d_map.get(delegate_vid) || delegate_vid; - - // make sure no delegation exists yet: - // (we no longer require that delegation would not create a cycle) - if (dir_d_map.has(client_vid)) { - - if (dir_d_map.get(client_vid) == delegate_vid) { - this.G.L.warn("PollService.add_delegation of existing delegation", this._pid, client_vid, oid, delegate_vid, dir_d_map.get(client_vid)); - return true; - } else { - this.G.L.error("PollService.add_delegation when delegation already exists", this._pid, client_vid, oid, delegate_vid, dir_d_map.get(client_vid)); - return false; - } - - /* - } else if (new_eff_d_vid == client_vid) { - - this.G.L.error("PollService.add_delegation when this would create a cycle", this._pid, client_vid, oid, delegate_vid); - return false; - */ - } else { - - this.G.L.trace("PollService.add_delegation feasible", this._pid, client_vid, oid, delegate_vid); - - // register DIRECT delegation and inverse: - dir_d_map.set(client_vid, delegate_vid); - const inv_dir_d_map = this.inv_direct_delegation_map.get(oid); - if (!inv_dir_d_map.has(delegate_vid)) { - inv_dir_d_map.set(delegate_vid, new Set()); - } - inv_dir_d_map.get(delegate_vid).add(client_vid); - - // update INDIRECT delegations and inverses: - const ind_d_map = this.indirect_delegation_map.get(oid), - ind_ds_of_delegate = ind_d_map.get(delegate_vid), - inv_ind_d_map = this.inv_indirect_delegation_map.get(oid), - inv_eff_d_map = this.inv_effective_delegation_map.get(oid); - if (!inv_ind_d_map.has(delegate_vid)) { - inv_ind_d_map.set(delegate_vid, new Set()); - } - const inv_ind_ds_of_delegate = inv_ind_d_map.get(delegate_vid), - inv_eff_ds_of_client = inv_eff_d_map.get(client_vid); - // vid: - const ind_ds_of_client = new Set([delegate_vid]); - ind_d_map.set(client_vid, ind_ds_of_client); - inv_ind_ds_of_delegate.add(client_vid); - if (ind_ds_of_delegate) { - for (const vid of ind_ds_of_delegate) { - if (vid != client_vid) { // avoid self-reference entries - ind_ds_of_client.add(vid); - if (!inv_ind_d_map.has(vid)) { - inv_ind_d_map.set(vid, new Set()); - } - inv_ind_d_map.get(vid).add(client_vid); - } - } - } - // voters dependent on client: - if (inv_eff_ds_of_client) { - for (const vid of inv_eff_ds_of_client) { - const ind_ds_of_vid = ind_d_map.get(vid); - if (vid != delegate_vid) { // avoid self-reference entries - ind_ds_of_vid.add(delegate_vid); - inv_ind_ds_of_delegate.add(vid); - } - if (ind_ds_of_delegate) { - for (const vid2 of ind_ds_of_delegate) { - if (vid2 != vid) { // avoid self-reference entries - ind_ds_of_vid.add(vid2); - if (!inv_ind_d_map.has(vid2)) { - inv_ind_d_map.set(vid2, new Set()); - } - inv_ind_d_map.get(vid2).add(vid); - } - } - } - } - } - - // update EFFECTIVE delegations, inverses, and proxy ratings: - if (new_eff_d_vid == client_vid) { - // a cycle is created - const rmap = this.own_ratings_map.get(oid); - let vid = delegate_vid, - new_proxy_rating = rmap.get(vid) || 0, - cycle_len = 0, - includes_me = false; - // loop through cycle members: - for (cycle_len = 1; cycle_len <= this.T.all_vids_set.size; cycle_len++) { - vid = dir_d_map.get(vid); - if (vid == delegate_vid) break; - // use max rating on cycle as new proxy rating: - new_proxy_rating = Math.max(new_proxy_rating, this.own_ratings_map.get(oid).get(vid) || 0); - if (vid == this.myvid) { - includes_me = true; - } - } - this.G.L.info("add_delegation created cycle of length", cycle_len, includes_me, this.pid, oid, client_vid, delegate_vid); - if (includes_me) { - this.T.my_cycle_len = cycle_len; - } - this.update_proxy_rating(client_vid, oid, new_proxy_rating); - // update proxy rating of all dependent voters: - if (inv_eff_ds_of_client) { - for (const vid of inv_eff_ds_of_client) { - this.update_proxy_rating(vid, oid, new_proxy_rating); - } - } - } else { - // determine new proxy rating: - const new_proxy_rating = this.own_ratings_map.get(oid).get(new_eff_d_vid) || 0; - // update eff_d_map and inverse: - if (!inv_eff_d_map.has(new_eff_d_vid)) { - inv_eff_d_map.set(new_eff_d_vid, new Set()); - } - const inv_eff_ds_of_new_eff_d = inv_eff_d_map.get(new_eff_d_vid); - // this vid: - eff_d_map.set(client_vid, new_eff_d_vid); - inv_eff_ds_of_new_eff_d.add(client_vid); - this.update_proxy_rating(client_vid, oid, new_proxy_rating); - // dependent voters: - if (inv_eff_ds_of_client) { - for (const vid of inv_eff_ds_of_client) { - eff_d_map.set(vid, new_eff_d_vid); - inv_eff_ds_of_new_eff_d.add(vid); - this.update_proxy_rating(vid, oid, new_proxy_rating); - } - } - } - this.G.L.debug("add_delegation exit", this.pid, oid, client_vid, delegate_vid); - return true; - } - } - - del_delegation(client_vid: string, oid: string) { - // Called whenever a voter revokes her delegation for some option - const dir_d_map = this.direct_delegation_map.get(oid), - eff_d_map = this.effective_delegation_map.get(oid); - // make sure a delegation exists: - if (!dir_d_map.has(client_vid)) { - this.G.L.error("PollService.del_delegation when no delegation exists", client_vid, oid); - } else { - const old_d_vid = dir_d_map.get(client_vid), - old_eff_d_of_client = eff_d_map.get(client_vid), - inv_dir_d_map = this.inv_direct_delegation_map.get(oid), - ind_d_map = this.indirect_delegation_map.get(oid), - old_ind_ds_of_client = ind_d_map.get(client_vid), - inv_ind_d_map = this.inv_indirect_delegation_map.get(oid), - inv_ind_ds_of_client = inv_ind_d_map.get(client_vid), - inv_eff_d_map = this.inv_effective_delegation_map.get(oid), - inv_eff_ds_of_client = inv_eff_d_map.get(client_vid), - inv_eff_ds_of_old_eff_d_of_client = inv_eff_d_map.get(old_eff_d_of_client), - is_on_cycle = ind_d_map.get(old_d_vid).has(client_vid); - - this.G.L.debug("del_delegation entry", this.pid, oid, client_vid, old_d_vid, is_on_cycle); - - // deregister DIRECT delegation and inverse of vid: - dir_d_map.delete(client_vid); - inv_dir_d_map.get(old_d_vid).delete(client_vid); - - // deregister INDIRECT delegation of client_vid to others: - for (const vid of old_ind_ds_of_client) { - inv_ind_d_map.get(vid).delete(client_vid); - } - ind_d_map.delete(client_vid); - - // deregister INDIRECT delegations no longer valid: - if (is_on_cycle) { - // we're on a cycle, so - // first find cycle elements in correct forward order: - let vid = old_d_vid, - former_cycle = [vid]; - // loop through cycle members: - while (true) { - vid = dir_d_map.get(vid); - former_cycle.push(vid); - if (vid == client_vid) break; - } - if (former_cycle.includes(this.myvid)) { - this.T.my_cycle_len = null; - } - // now for each vid indirectly delegating to client, ... - if (inv_ind_ds_of_client) { - for (const vid of inv_ind_ds_of_client) { - // follow delegation path to cycle: - let cycle_vid = vid, - cycle_pos = -1; - while (true) { - cycle_pos = former_cycle.indexOf(cycle_vid); - if (cycle_pos != -1) { - break; - } - cycle_vid = dir_d_map.get(cycle_vid); - } - // then deregister indirect delegations to all earlier cycle members: - const ind_ds_of_vid = ind_d_map.get(vid); - for (let pos = 0; pos < cycle_pos; pos++) { - const vid2 = former_cycle[pos]; - if (ind_ds_of_vid.has(vid2)) { - ind_ds_of_vid.delete(vid2); - inv_ind_d_map.get(vid2).delete(vid); - } - } - } - } - } else { - // we're not on a cycle, so - // deregister INDIRECT delegation of voters who indirectly delegated to client_vid - // to all old indirect delegates of vid: - if (inv_ind_ds_of_client) { - for (const vid of inv_ind_ds_of_client) { - const ind_ds_of_vid = ind_d_map.get(vid); - for (const vid2 of old_ind_ds_of_client) { - ind_ds_of_vid.delete(vid2); - inv_ind_d_map.get(vid2).delete(vid); - } - } - } - } - - // deregister EFFECTIVE delegation and inverse of vid and reset proxy rating to own rating: - const new_proxy_rating = this.own_ratings_map.get(oid).get(client_vid) || 0; - eff_d_map.delete(client_vid); - inv_eff_ds_of_old_eff_d_of_client.delete(client_vid); - this.update_proxy_rating(client_vid, oid, new_proxy_rating); - - // rewire EFFECTIVE delegation and inverse of voters who indirectly delegated to vid, - // and update proxy ratings: - if (inv_ind_ds_of_client) { - for (const vid of inv_ind_ds_of_client) { - inv_eff_ds_of_old_eff_d_of_client.delete(vid); - eff_d_map.set(vid, client_vid); - inv_eff_ds_of_client.add(vid); - this.update_proxy_rating(vid, oid, new_proxy_rating); - } - } - this.G.L.debug("del_delegation exit", this.pid, oid, client_vid, old_d_vid, is_on_cycle); - } - } - - get_n_indirect_option_clients(vid: string, oid: string): number { - /** count how many voters have indirectly delegated to vid for oid */ - return (this.inv_indirect_delegation_map.get(oid).get(vid)||new Set()).size; - } - get_n_indirect_clients(vid: string): number { /** count how many voters have indirectly delegated to vid for some oid */ - let clients = new Set(); - for (const oid of this.oids) { - for (const vid2 of (this.inv_indirect_delegation_map.get(oid).get(vid)||new Set())) { - clients.add(vid2); - } - } - return clients.size; + const map = this.G.D.get_inverse_indirect_map(this._pid); + const set = new Set(JSON.parse(map.get(vid) || "[]")); + return set.size; } tally_all() { @@ -1282,21 +958,36 @@ export class Poll { rs_map.set(vid, value); this.G.L.trace("Poll.update_own_rating new ratings map", this.pid, oid, [...rs_map.entries()]); // check whether vid has not delegated: - if (!this.direct_delegation_map.get(oid)) { - this.direct_delegation_map.set(oid, new Map()); + const dir_d_map = this.G.D.get_direct_delegation_map(this.pid); + var have_delegated = false; + if (dir_d_map.has(vid)) { + for (const entry of dir_d_map.get(vid)) { + if (entry[2] == "2") { + have_delegated = true; + break; + } + } } - if (!this.direct_delegation_map.get(oid).has(vid)) { - this.G.L.trace("Poll.update_own_rating voter has not delegated", this.pid, vid, oid); + this.update_proxy_rating(vid, oid, value, update_tally); + if (!have_delegated) { // vid has not delegated this rating, // so update all dependent voters' effective ratings: - this.update_proxy_rating(vid, oid, value, update_tally); - const vid2s = (this.inv_effective_delegation_map.get(oid)||new Map()).get(vid); + // const vid2s = (this.G.D.get_inv_effective_delegation_map(this.pid).get(oid)||new Map()).get(vid); + const vid2s = new Set(JSON.parse(this.G.D.get_inverse_indirect_map(this.pid).get(vid) || "[]")); if (vid2s) { for (const vid2 of vid2s) { // vid2 effectively delegates their rating of oid to vid, // hence we store vid's new rating of oid as vid2's effective rating of oid: - this.update_proxy_rating(vid2, oid, value, update_tally); + const list = this.G.D.get_direct_delegation_map(this.pid).get(vid2) || []; + for (const entry of list) { + const a = this.G.Del.get_agreement(this.pid, entry[0]); + if (!(a.delegate_vid == vid) || !a.active_oids.has(oid) || entry[2] != "2") { + continue; + } + this.update_proxy_rating(vid2, oid, value, update_tally); + break; + } } } } diff --git a/src/app/poll/poll.page.html b/src/app/poll/poll.page.html index 7de22cb6b..3374c90fa 100644 --- a/src/app/poll/poll.page.html +++ b/src/app/poll/poll.page.html @@ -88,7 +88,7 @@ - -

@@ -164,7 +163,6 @@

-

- +

  @@ -462,7 +460,7 @@

- @@ -479,7 +477,7 @@

size="smaller" class="ion-no-margin" style="color:black; position: relative; left: -2px; top: 2px;"> - (); + const dir_del_map = this.G.D.get_direct_delegation_map(this.pid); + const list = dir_del_map.get(this.p.myvid) || []; + for (const [did_, rank, status] of list) { + if (status == '2') { + did = did_; + } else if (status == '0') { + pendingSet.add(did_); + } + } if (did) { - this.delegate = this.G.Del.get_delegate_nickname(this.pid, did); + // this.delegate = this.G.Del.get_delegate_nickname(this.pid, did); + this.set_delegate(); const agreement = this.G.Del.get_agreement(this.pid, did); this.G.L.trace("PollPage.update_delegation_info agreement", agreement); - this.delegation_status = agreement.status; + var st = "null"; + const list = dir_del_map.get(this.p.myvid) || []; + for (const [did, rank, status] of list) { + if (status == '2') { + st = "agreed"; + break; + } + } + this.delegation_status = st == "agreed" ? "agreed" : agreement.status; + } else if (pendingSet.size > 0) { + this.delegation_status = "pending"; + this.delegate = this.G.Del.get_delegate_nickname(this.pid, pendingSet.values().next().value); + } else { + this.delegation_status = "none"; } this.update_delegation_toggles(); } update_delegation_toggles() { for (let oid of this.oidsorted) { - let did = this.G.Del.get_my_outgoing_dids_cache(this.pid).get(oid); - if (!did) { - did = this.G.Del.get_my_outgoing_dids_cache(this.pid).get("*"); + // let did = this.G.Del.get_my_outgoing_dids_cache(this.pid).get(oid); + // if (!did) { + // did = this.G.Del.get_my_outgoing_dids_cache(this.pid).get("*"); + // } + var did; + const dm = this.G.D.get_direct_delegation_map(this.pid); + const list = dm.get(this.p.myvid) || []; + for (const [did_, rank, status] of list) { + if (status == '2') { + did = did_; + break; + } } if (did) { const a = this.G.Del.get_agreement(this.pid, did); @@ -295,19 +345,39 @@ export class PollPage implements OnInit { } on_rate_yourself_toggle_change(oid:string) { -// const new_rating = this.p.own_ratings_map.get(oid).get(this.p.myvid); + //const new_rating = this.p.own_ratings_map.get(oid).get(this.p.myvid); // update delegation data: - this.G.Del.update_my_delegation(this.pid, oid, !this.rate_yourself_toggle[oid]); + var did = null; + const dm = this.G.D.get_direct_delegation_map(this.pid); + const list = dm.get(this.p.myvid) || []; + for (const [did_, _, status] of list) { + if (status == '2') { + did = did_; + break; + } + } + + // set rating + if (this.rate_yourself_toggle[oid]) { + this.p.set_my_own_rating(oid, this.p.get_my_effective_rating(oid), true); + } else { + this.p.set_my_own_rating(oid, +this.G.D.getv(this.pid, "rating."+oid, this.p.delegate_id)||0, true); + } + + this.G.Del.update_my_delegation(this.pid, oid, !this.rate_yourself_toggle[oid], did); // update slider value: // this.get_slider(oid).value = new_rating.toString(); this.on_delegate_toggle_change(); + this.G.D.save_state(); } on_delegate_toggle_change() { // update n_delegated: // TODO: make more efficient let sum = 0; for (let [oid, b] of Object.entries(this.rate_yourself_toggle)) { - if (!b) sum++; + if (!b) { + sum++; + } } this.n_delegated = sum; } @@ -356,7 +426,8 @@ export class PollPage implements OnInit { delegate_vid = this.G.Del.get_potential_effective_delegate(this.pid, oid); // this.G.L.trace("PollPage.show_stats needle know delegate_vid", needle, knob, delegate_vid); if (delegate_vid) { - const rating = (this.p.proxy_ratings_map.get(oid)||new Map()).get(delegate_vid)||0; + // const rating = (this.p.proxy_ratings_map.get(oid)||new Map()).get(delegate_vid)||0; + const rating = this.G.D.getv(this.pid, "rating."+oid, this.p.delegate_id)||0; this.G.L.trace("PollPage.show_stats rating", rating); if (needle) { needle.x2.baseVal.valueAsString = (rating).toString() + '%'; @@ -442,6 +513,24 @@ export class PollPage implements OnInit { 'vodlegreen'; } + set_delegate() { + const dm = this.G.D.get_direct_delegation_map(this.pid); + const list = dm.get(this.p.myvid); + var d = null; + if (!list) { + this.delegate = null; + return + } + for (const [did, rank, status] of list) { + if (status == '2') { + d = did; + break; + } + } + this.delegate = d ? this.G.Del.get_delegate_nickname(this.pid, d) : null; + this.p.delegate_id = d? d : null; + } + // CONTROLS: toggle_show_live() { @@ -630,6 +719,23 @@ export class PollPage implements OnInit { return true; } + get_my_rating(oid: string) { + if (this.delegate && !this.rate_yourself_toggle[oid]) { + this.G.D.setv(this.pid, "rating."+oid, "" + this.get_slider_value(oid)); + return this.G.D.getv(this.pid, "rating."+oid, this.p.delegate_id); + } + return this.p.get_my_own_rating(oid); + } + + get_allowed_to_delegate() : boolean { + if (!this.G.D.get_ranked_delegation_allowed(this.pid) && this.delegation_status == 'agreed'){ + return false; + } + const dm = this.G.D.get_direct_delegation_map(this.pid); + const list = dm.get(this.p.myvid) || []; + return list.length < environment.delegation.max_delegations; + } + get_knob_pos(oid: string) { /** get the slider knob position (left and right pixel coordinates) * to be able to compare with click/touch coordinate: @@ -772,9 +878,20 @@ export class PollPage implements OnInit { text: this.translate.instant('OK'), role: 'Ok', handler: () => { - this.G.Del.revoke_delegation(this.pid, this.G.Del.get_my_outgoing_dids_cache(this.pid).get("*"), '*'); - this.delegate = null; - this.delegation_status = 'none'; + console.log('Confirm Ok.'); + var did = null; + const dm = this.G.D.get_direct_delegation_map(this.pid); + const list = dm.get(this.p.myvid) || []; + for (const [did_, _, status] of list) { + if (status == '2') { + did = did_; + break; + } + } + this.G.Del.revoke_delegation(this.pid, did, "*"); + // this.delegate = null; + this.set_delegate(); + this.delegation_status = this.delegate ? 'agreed' : 'none'; this.update_delegation_info(); this.G.D.save_state(); } diff --git a/src/assets/i18n/en.json b/src/assets/i18n/en.json index 852dc261d..d4f9deaf7 100644 --- a/src/assets/i18n/en.json +++ b/src/assets/i18n/en.json @@ -175,6 +175,7 @@ "nickname-placeholder": "Please enter the nickname of that person (or their true name)", "from-label": "The delegate will see the following as the sender of this request:", "from-placeholder": "Enter your own e-mail address, name or nickname", + "rank-label": "Rank of delegation:", "request-options-with-share": "The simplest way to ask that person to act as your delegate is via the share button. Alternatively, you can write them an e-mail. Or you copy the delegation link and send it to them in any way you like.", "request-options-without-share": "The simplest way to ask that person to act as your delegate is via e-mail. Or you copy the delegation link and send it to them in any way you like.", "share": "Use a messenger app", @@ -207,6 +208,7 @@ "header": "Would you act as a delegate for {{from}} in the poll “{{poll_title}}”?", "intro": "Your waps will then also control the other participantʼs waps.", "details": "If you accept, you can still choose whether you want to set your waps yourself or delegate your (and the other participantʼs) waps further to some third person.", + "details_ranked": "Note that this poll allows ranked delegation. [Insert explanation of how cycles and stuff are allowed]", "check-first": "You can also check your waps first and then return here to accept or decline.", "accept": "Accept", "decline": "Decline", @@ -232,7 +234,8 @@ "poll-unknown": "You are not participating in this poll yet. Please check if you have received an invitation link for this poll. If so, use that link first and then try this one again.", "try-again": "We apologize, vodle is still waiting for some data on this request. Please try this link again later.", "closed": "Unfortunately, this poll has already ended.", - "is-self": "This request was created by yourself :-) Please send this link to the person youʼd like to ask for delegation!" + "is-self": "This request was created by yourself :-) Please send this link to the person youʼd like to ask for delegation!", + "been-revoked": "Unfortunately, this request has already been revoked." }, "draftpoll": { "_COMMENT_SECTION_": "[COMMENT] strings used on the Edit Draft Poll page:", @@ -286,7 +289,8 @@ "import-options-msg": "The file must be a .csv file.

Each row must specify one option, either in the format
   \"Name\"
or
   \"Name\", \"Description\"
or
   \"Name\", \"Description\", \"URL\"", "use-example-from-db": "Use an example:", "use-example-from-db-none": "(Donʼt use an example)", - "notification-saved-title": "Draft saved under “My polls”." + "notification-saved-title": "Draft saved under “My polls”.", + "del_ranked": "Allow ranked delegation?" }, "external-link": { "_COMMENT_SECTION_": "[COMMENT] strings concerning external links:", @@ -506,7 +510,9 @@ "delegation_accepted": "{{nickname}} accepted your delegation request.", "delegation_declined": "Unfortunately, {{nickname}} declined your delegation request.", "delegation_accepted_after_all": "{{nickname}} has now accepted your delegation request after all.", - "delegation_revoked": "Unfortunately, {{nickname}} revoked her earlier agreement to your delegation request." + "delegation_revoked": "Unfortunately, {{nickname}} revoked their delegation to you.", + "delegation_declined_cycle": "Unfortunately, {{nickname}} declined your delegation request as it would have created a cycle.", + "delegation_decline_self": "Unfortunately, {{nickname}} declined your delegation request as they are already delegating to you." }, "news-body": { "_COMMENT_SECTION_": "[COMMENT] strings used as bodies of News items:", diff --git a/src/environments/environment.ts b/src/environments/environment.ts index 141287d57..7f1bfb043 100644 --- a/src/environments/environment.ts +++ b/src/environments/environment.ts @@ -52,8 +52,9 @@ export const environment = { // backdoor_public_key: "ea17226c631a8a78c67626136d91980e82328b72e6b536c7df7e68fbb22c2aa7", }, delegation: { - enabled: false, - max_weight: 3 + enabled: true, + max_weight: 300, + max_delegations: 3 }, no_more_options_time_fraction: 1/2, db_put_retry_delay_ms: 100,