diff --git a/docs/push-notifications.md b/docs/push-notifications.md index c2c4201..5792a51 100644 --- a/docs/push-notifications.md +++ b/docs/push-notifications.md @@ -46,7 +46,7 @@ membership amendments, rate changes, etc.). |-------|-------| | Trigger | `INSERT` on `public.group_events` (`on_group_event_push` trigger) | | Recipient | Active group members with push enabled, excluding the actor | -| Title | `{group name}` (or `"FairShare"` if the group has no name) | +| Title | `{group name}` (or `"Union"` if the group has no name) | | Body | `NEW.summary` from the event row | | URL | `/?group={groupUuid}` | | Source | [sql/fairshare-schema.sql](../sql/fairshare-schema.sql) 1720–1739 | @@ -100,19 +100,18 @@ When someone sponsors you into a group via the `offer_group_membership` RPC. ## 4. Handshake completed — new contact -Sent to the QR/meet issuer when another user completes the handshake. Only -active if [sql/handshake-push-schema.sql](../sql/handshake-push-schema.sql) has -been applied; that file replaces the earlier `complete_meet` definition with one -that pushes. +Sent to the QR/meet issuer when another user completes the handshake. The +current `complete_meet(p_token, p_meet_source)` records how the pair met +(`met_via`) and pushes the issuer. | Field | Value | |-------|-------| | Trigger | RPC `complete_meet(p_token, p_meet_source)` after pairing (`p_meet_source` defaults to `URL`; in-app QR scans use `F2F`) | | Recipient | The meet issuer (`v_meet_request.user_id`) | -| Title | `"FairShare"` | -| Body | `"{name} just joined FairShare via your handshake."` if the newcomer signed up via that token, otherwise `"{name} is now your contact."` | +| Title | `"Union"` | +| Body | `"{name} just joined Union via your handshake."` if the newcomer signed up via that token, otherwise `"{name} is now your contact."` | | URL | `/?action=view_contact&contact={newContactUuid}` | -| Source | [sql/handshake-push-schema.sql](../sql/handshake-push-schema.sql) 47–145; optional `p_meet_source` and `contacts.met_via` in [sql/contact-intro-and-met-via-migration.sql](../sql/contact-intro-and-met-via-migration.sql) | +| Source | `complete_meet(p_token, p_meet_source)` — current definition in [sql/meet-token-reserve-at-signup.sql](../sql/meet-token-reserve-at-signup.sql); `p_meet_source` / `contacts.met_via` introduced in [sql/contact-intro-and-met-via-migration.sql](../sql/contact-intro-and-met-via-migration.sql) | Also writes a `contact_notifications` row of type `new_contact` for in-app display. @@ -127,7 +126,7 @@ When a contact saves a selfie of the two of you via `add_contact_selfie`. |-------|-------| | Trigger | RPC `add_contact_selfie(...)` after mirrored selfie rows are inserted | | Recipient | The other party (`p_contact_id`) | -| Title | `"FairShare"` | +| Title | `"Union"` | | Body | `"{name} took a new selfie with you."` | | URL | `/?action=view_contact&contact={callerUuid}` | | Source | [sql/contact-list-schema.sql](../sql/contact-list-schema.sql) 147–155 | @@ -144,7 +143,7 @@ Tap opens the intro dialog (`handleNotificationNavigation` in |-------|-------| | Trigger | RPC `send_contact_intro(p_contact_a, p_contact_b, p_message)` | | Recipient | Each of the two parties (`p_contact_a`, `p_contact_b`), excluding the actor | -| Title | `"FairShare"` | +| Title | `"Union"` | | Body | `"{introducer} wants you to meet {other}"` — `other` is the opposite party for that recipient | | URL | `/?action=contact_intro&intro={introUuid}` | | Source | [sql/contact-intro-and-met-via-migration.sql](../sql/contact-intro-and-met-via-migration.sql) `send_contact_intro` | @@ -161,7 +160,7 @@ Fanned out to every user who has the actor in their `contacts` list. |-------|-------| | Trigger | RPC `notify_contacts_of_profile_picture_change(p_actor_id)`; called from [js/modals.js](../js/modals.js) after a successful upload | | Recipient | All users whose `contacts` includes `p_actor_id`, excluding the actor | -| Title | `"FairShare"` | +| Title | `"Union"` | | Body | `"{name} updated their profile picture"` | | URL | `/?action=view_contact&contact={actorUuid}` | | Source | [sql/fairshare-schema.sql](../sql/fairshare-schema.sql) 1864–1904 | @@ -179,7 +178,7 @@ When the user saves a new display name in preferences (`savePreferences` in |-------|-------| | Trigger | RPC `notify_contacts_of_display_name_change(p_actor_id, p_old_display_name, p_new_display_name)`; called from [js/modals.js](../js/modals.js) after a successful profile update when the trimmed name changed | | Recipient | All users whose `contacts` includes `p_actor_id`, excluding the actor | -| Title | `"FairShare"` | +| Title | `"Union"` | | Body | `"{old} changed their name to {new}"` — empty/whitespace-only old or new side falls back to `"Someone"` | | URL | `/?action=view_contact&contact={actorUuid}` | | Source | [sql/fairshare-schema.sql](../sql/fairshare-schema.sql) 1948–1998; constraint + one-shot deploy [sql/display-name-change-push-schema.sql](../sql/display-name-change-push-schema.sql) | @@ -197,7 +196,7 @@ the sponsor share dialog), every user who lists them as a contact is notified. |-------|-------| | Trigger | RPC `notify_contacts_of_profile_update(p_actor_id, p_message)`; called from [js/modals.js](../js/modals.js) (`persistProfileEmailPhone`, `savePreferences`) | | Recipient | All users whose `contacts` includes `p_actor_id`, excluding the actor | -| Title | `"FairShare"` | +| Title | `"Union"` | | Body | `p_message` (e.g. `"{name} updated their email."`) | | URL | `/?action=view_contact&contact={actorUuid}` | | Source | [sql/fairshare-schema.sql](../sql/fairshare-schema.sql) 1909–1945 | @@ -247,7 +246,7 @@ When a contact records the date you two met via `set_first_met_date`. |-------|-------| | Trigger | RPC `set_first_met_date(p_contact_id, p_met_date)` when `p_met_date` is non-null | | Recipient | The contact (`p_contact_id`) | -| Title | `"FairShare"` | +| Title | `"Union"` | | Body | `"{name} says you met on {formatted date}"` | | URL | `/` (no deep link wired — could route to the contact view) | | Source | [sql/fairshare-schema.sql](../sql/fairshare-schema.sql) 1952–2002 | diff --git a/docs/transitive-trust.md b/docs/transitive-trust.md new file mode 100644 index 0000000..8c648ce --- /dev/null +++ b/docs/transitive-trust.md @@ -0,0 +1,144 @@ +# Transitive Trust: Propagation, Discovery, and Group Coherence + +**Status: proposal.** Nothing in this document is implemented. It sketches a single +mathematical foundation for four of the Future Directions listed in +[web-of-trust.md](web-of-trust.md) — Discovery, Transitive trust, Group reputation, +and Decay — built entirely from conventions already in the codebase. + +## What we have today + +Two building blocks already exist in `get_contact_trust_summary` +([contact-details-schema.sql](../sql/contact-details-schema.sql)): + +- **Time decay.** Every vouch contributes `2^(-age / 2 years)` — a vouch from two + years ago is worth half of one made today. +- **A fixed two-hop signal.** The Trusted component sums vouches sent to a contact + by mutuals the caller has personally given an "I trust you" vouch. + +The Trusted component is the seed of transitive trust, but it has three limits: it +stops at exactly two hops; it treats every trusted mutual identically, no matter how +much the caller actually trusts them; and it is an unbounded sum, so quantity of +vouches can outweigh quality of the people making them. + +## The idea: trust attenuates with distance the way it fades with age + +Extend the halving rule from time to the graph. A vouch that reaches you through one +extra hop of trust is worth half of one made by someone you trust directly: + +``` +contribution of a vouch = 2^(-age / T_half) x 2^(-hops / D_half) + +T_half = 2 years (existing time half-life) +D_half = 1 hop (proposed distance half-life, tunable) +``` + +The **transitive trust** the caller holds for a person `x` is the sum, over every +attester `a` who ever vouched for `x`, of `a`'s time-decayed vouches weighted by the +caller's (attenuated) trust in `a` — computed along 'trust' vouch chains out to a +bounded depth (3 hops). Depth 1 is exactly today's Trusted component; this is its +natural continuation, not a replacement. + +Formally this is a truncated diffusion on the vouch graph — the same family of +computation as random-walk trust (personalized PageRank) and heat-kernel +propagation — but at Union's scale it is implementable as one bounded recursive SQL +query inside a `SECURITY DEFINER` function. No new infrastructure. + +## Why this exact shape + +### 1. Sybils cannot manufacture trust + +The load-bearing property: **trust flowing into any cluster of accounts is bounded +by the vouch weight crossing the boundary into that cluster.** A thousand fake +accounts vouching for each other in a ring gain nothing from any real user's +perspective unless real people vouch across the boundary — and each real vouch +admits at most a fixed, decaying amount of flow. Trust becomes something you can +only obtain from humans, never mint. + +This matters beyond contact screens. If FairShare currency is ever gated or priced +by trust, the trust quantity must be one that cannot be inflated from inside a +cluster of colluding accounts. Diffusion with per-hop attenuation has exactly this +cut-bound property; raw counts (today's sums) do not. + +### 2. It degrades gracefully + +Attenuation is exponential, so distant vouches contribute vanishingly little and +truncating at depth 3 discards at most a `2^-3` fraction of the signal. No cliffs, +no surprises when the graph grows. + +### 3. It stays explainable in one sentence + +The app already teaches users one rule: *worth half after two years.* This adds the +same rule in a second dimension: *worth half per extra hop.* The Trust Details +dialog explanation grows by one clause, not a paragraph. + +## What it unlocks + +| Future direction (web-of-trust.md) | With diffusion | +|---|---| +| Transitive trust | The score itself: "trusted by people you trust," to depth 3, weighted by how much you trust the intermediaries. | +| Discovery | Rank people you haven't met by transitive trust from your position in the graph. Show the score — never the path. | +| Group reputation | The Candidates screen shows each candidate's transitive trust from existing members, giving endorsers a principled signal beyond the sponsor's message. | +| Decay | Already inside the kernel — one constant per dimension, both user-legible. | + +## Group Coherence: a related, separate number + +Groups admit a complementary metric. Build the graph of vouches *among a group's +active members* and compute its **algebraic connectivity** (the second-smallest +eigenvalue of the graph Laplacian, the Fiedler value). This is the standard measure +of whether a network is one community or two cliques joined by a thread: + +- **High** when vouching spans the whole membership — the group moves as one flock. +- **Low** when the group is quietly fragmenting into sub-cliques, *before* it is + visible in chat activity or membership churn. + +Normalized 0–100, this is a "Group Coherence" stat on the group page. It is cheap — +for groups of realistic size it computes in milliseconds by power iteration, nightly +and cached. Later it could inform constitution mechanics (e.g. admission thresholds +that keep coherence above a floor), but the observable stat alone is valuable. + +## Privacy model + +These are constraints, not features. Every mechanism above must preserve the +guarantees in [web-of-trust.md](web-of-trust.md): + +1. **No path enumeration, ever.** Every RPC returns a single aggregate number. + Nothing reveals *via whom* trust flows — a "trust path" display is explicitly + rejected here, because any path rendering exposes private vouch and contact + edges of third parties. +2. **No new readable rows.** The `attestations` table keeps no SELECT policy; all + computation stays inside `SECURITY DEFINER` functions. +3. **Bucketed display.** Transitive scores follow the heart-dialog rules: fully + hidden below a threshold, floored to buckets above it, so a single new vouch + cannot be detected by watching a number move. +4. **Recipients never learn attesters.** Transitive trust is always the *caller's* + view of a *third party*; the reverse direction (who feeds *my* score) is never + queryable, matching `get_contact_history`'s one-direction rule. + +## Implementation sketch + +Three phases, each independently shippable: + +1. **Deepen the Trusted component.** Inside `get_contact_trust_summary`, replace + the fixed two-hop Trusted query with a recursive CTE over 'trust' attestations, + applying `2^(-d / D_half)` per hop, depth capped at 3, fan-out capped per node. + Reuses the existing `contacts.trust_score` cache. New constants sit next to + `c_decay_a`: `c_hop_half`, `c_max_depth`. +2. **Discovery / group reputation RPC.** A function returning bucketed transitive + trust for the sponsorship and endorsement flows (candidate screens first — the + moment members most need a signal about someone they haven't met). +3. **`get_group_coherence(group_id)`.** Laplacian power iteration in plpgsql or an + edge function, computed nightly per group and cached. + +## Open questions + +- **Constants.** `D_half`, depth cap, and bucket sizes should be picked by + simulating on production-shaped graphs, not by intuition. +- **Low-degree inference.** For users with very few contacts, even bucketed + aggregates can leak who vouched. The "fully hidden below N" rule likely needs a + larger N for transitive scores than for direct counts. +- **Which vouch types propagate?** Proposal: only 'trust' carries flow across hops; + other types ('love', 'respect', 'help') contribute at depth 1 only, since their + meanings are not transitive. +- **Cost.** A depth-3 recursive query over `attestations` is fine at current scale + but wants the same caching pattern `trust_score` already uses; worth measuring + before Phase 2. diff --git a/sql/contact-intro-and-met-via-migration.sql b/sql/contact-intro-and-met-via-migration.sql index 11e1e78..67f9a3d 100644 --- a/sql/contact-intro-and-met-via-migration.sql +++ b/sql/contact-intro-and-met-via-migration.sql @@ -164,7 +164,7 @@ BEGIN v_push_msg := COALESCE(v_caller_name, 'Someone') || CASE WHEN v_is_new_account - THEN ' just joined FairShare via your handshake.' + THEN ' just joined Union via your handshake.' ELSE ' is now your contact.' END; @@ -176,7 +176,7 @@ BEGIN PERFORM public.send_push_to_users( ARRAY[v_meet_request.user_id], v_caller_id, - 'FairShare', + 'Union', v_push_msg, '/?action=view_contact&contact=' || v_caller_id::text ); @@ -468,7 +468,7 @@ BEGIN PERFORM public.send_push_to_users( ARRAY[p_contact_a], v_uid, - 'FairShare', + 'Union', v_body_a, v_url ); @@ -476,7 +476,7 @@ BEGIN PERFORM public.send_push_to_users( ARRAY[p_contact_b], v_uid, - 'FairShare', + 'Union', v_body_b, v_url ); diff --git a/sql/contact-list-schema.sql b/sql/contact-list-schema.sql index 081b5b6..9770bdb 100644 --- a/sql/contact-list-schema.sql +++ b/sql/contact-list-schema.sql @@ -150,7 +150,7 @@ BEGIN -- Deep-link the push so tapping the OS notification opens the caller's -- contact details screen (handled in push.js handleNotificationNavigation). PERFORM public.send_push_to_users( - ARRAY[p_contact_id], v_caller_id, 'FairShare', v_msg, + ARRAY[p_contact_id], v_caller_id, 'Union', v_msg, '/?action=view_contact&contact=' || v_caller_id::text ); END; diff --git a/sql/display-name-change-push-schema.sql b/sql/display-name-change-push-schema.sql index c493b99..27ca709 100644 --- a/sql/display-name-change-push-schema.sql +++ b/sql/display-name-change-push-schema.sql @@ -2,7 +2,7 @@ -- lists this user as a contact. -- -- Run in Supabase SQL Editor after migrations that define the current --- `contact_notifications_notification_type_check` (e.g. handshake-push-schema.sql). +-- `contact_notifications_notification_type_check` (e.g. contact-intro-and-met-via-migration.sql). ALTER TABLE public.contact_notifications DROP CONSTRAINT IF EXISTS contact_notifications_notification_type_check; @@ -69,7 +69,7 @@ BEGIN PERFORM public.send_push_to_users( v_contact_ids, p_actor_id, - 'FairShare', + 'Union', v_msg, '/?action=view_contact&contact=' || p_actor_id::text ); diff --git a/sql/fairshare-schema.sql b/sql/fairshare-schema.sql index f1d159d..a557dcc 100644 --- a/sql/fairshare-schema.sql +++ b/sql/fairshare-schema.sql @@ -2367,7 +2367,7 @@ begin perform public.send_push_to_group( NEW.group_id, NEW.actor_id, - coalesce(v_group_name, 'FairShare'), + coalesce(v_group_name, 'Union'), NEW.summary, '/?group=' || NEW.group_id::text ); @@ -2404,7 +2404,7 @@ begin perform public.send_push_to_group( NEW.group_id, NEW.user_id, - coalesce(v_group_name, 'FairShare') || ' Chat', + coalesce(v_group_name, 'Union') || ' Chat', v_body, '/?group=' || NEW.group_id::text || '&tab=chat' ); @@ -2546,7 +2546,7 @@ begin -- tap on the OS notification deep-links straight to that contact's details -- screen (via handleNotificationNavigation). perform public.send_push_to_users( - v_contact_ids, p_actor_id, 'FairShare', v_msg, + v_contact_ids, p_actor_id, 'Union', v_msg, '/?action=view_contact&contact=' || p_actor_id::text ); end; @@ -2586,7 +2586,7 @@ begin end loop; perform public.send_push_to_users( - v_contact_ids, p_actor_id, 'FairShare', p_message, + v_contact_ids, p_actor_id, 'Union', p_message, '/?action=view_contact&contact=' || p_actor_id::text ); end; @@ -2640,7 +2640,7 @@ begin end loop; perform public.send_push_to_users( - v_contact_ids, p_actor_id, 'FairShare', v_msg, + v_contact_ids, p_actor_id, 'Union', v_msg, '/?action=view_contact&contact=' || p_actor_id::text ); end; @@ -2701,7 +2701,7 @@ begin values (p_contact_id, v_caller_id, 'met_date_set', v_msg, jsonb_build_object('met_date', p_met_date)); -- Send Web Push. - perform public.send_push_to_users(ARRAY[p_contact_id], v_caller_id, 'FairShare', v_msg); + perform public.send_push_to_users(ARRAY[p_contact_id], v_caller_id, 'Union', v_msg); end; $$ language plpgsql security definer; diff --git a/sql/handshake-push-schema.sql b/sql/handshake-push-schema.sql deleted file mode 100644 index 68fc772..0000000 --- a/sql/handshake-push-schema.sql +++ /dev/null @@ -1,209 +0,0 @@ --- Handshake push notification schema for FairShare --- Run this in Supabase SQL Editor. --- --- Goal: when a contact connects with you via your handshake QR / meet link --- (whether they're a brand-new signup or an existing user), send a push --- notification to you (the QR holder). Previously complete_meet only --- inserted the bidirectional contacts row, so the issuer learned about --- the connection only via Realtime while the meet overlay was open or --- via subscribeToContactEvents in auth.js (foreground only). Backgrounded --- or terminated apps got nothing because no push was being emitted. --- --- This migration: --- 1. Adds 'new_contact' to the contact_notifications.notification_type --- CHECK constraint, so we can write a row of that type. --- 2. Replaces complete_meet to insert a contact_notifications row and --- call send_push_to_users for the meet issuer. Push delivery uses --- the same APNs / Web Push plumbing as every other notification in --- the app, which is configured for alert-style delivery so iOS --- shows a banner even when the app is backgrounded or terminated. --- 3. Differentiates the message slightly when the caller signed up via --- this exact handshake token (signup_token = p_token, set by --- handle_new_user), so the issuer sees "X just joined FairShare via --- your handshake" rather than the generic "X is now your contact". - - --- 1. Allow 'new_contact' notification_type. --- The constraint has been replaced multiple times across previous --- migrations (nearby-schema.sql, location-sharing-schema.sql), so we --- drop and re-create with the full superset to keep the DB consistent. -ALTER TABLE public.contact_notifications - DROP CONSTRAINT IF EXISTS contact_notifications_notification_type_check; - -ALTER TABLE public.contact_notifications - ADD CONSTRAINT contact_notifications_notification_type_check - CHECK (notification_type IN ( - 'profile_picture_updated', - 'profile_updated', - 'met_date_set', - 'profile_picture_suggested', - 'nearby_alert', - 'new_selfie', - 'location_share_started', - 'new_contact', - 'display_name_changed' - )); - - --- 2. Replace complete_meet so it sends a push to the meet issuer. -CREATE OR REPLACE FUNCTION public.complete_meet(p_token text) -RETURNS json AS $$ -DECLARE - v_caller_id uuid := auth.uid(); - v_meet_request record; - v_contact_name text; - v_caller_name text; - v_caller_signup_token text; - v_caller_sponsor uuid; - v_group_name text; - v_admitted boolean; - v_is_new_account boolean; - v_push_msg text; - v_already_contact boolean; -BEGIN - -- Look up the meet request by token, locking the row so a concurrent - -- call cannot also pass the used_by check before we mark it used. - SELECT * INTO v_meet_request - FROM public.meet_requests - WHERE token = p_token - AND expires_at > now() - FOR UPDATE; - - IF NOT FOUND THEN - RAISE EXCEPTION 'Meet request not found or expired'; - END IF; - - IF v_meet_request.used_by IS NOT NULL THEN - RAISE EXCEPTION 'This meet link has already been used'; - END IF; - - IF v_meet_request.user_id = v_caller_id THEN - RAISE EXCEPTION 'Cannot create a contact with yourself'; - END IF; - - SELECT EXISTS ( - SELECT 1 FROM public.contacts - WHERE user_id = v_caller_id AND contact_id = v_meet_request.user_id - ) INTO v_already_contact; - - -- Mark the token as used - UPDATE public.meet_requests - SET used_by = v_caller_id - WHERE id = v_meet_request.id; - - -- If the caller has no sponsor yet, set the meet issuer as their sponsor - SELECT sponsor_id INTO v_caller_sponsor - FROM public.profiles - WHERE id = v_caller_id; - - IF v_caller_sponsor IS NULL THEN - UPDATE public.profiles - SET sponsor_id = v_meet_request.user_id - WHERE id = v_caller_id; - END IF; - - -- Insert bidirectional contacts, or update met_at if already exist - INSERT INTO public.contacts (user_id, contact_id, met_at) - VALUES (v_caller_id, v_meet_request.user_id, now()) - ON CONFLICT (user_id, contact_id) DO UPDATE SET met_at = now(); - - INSERT INTO public.contacts (user_id, contact_id, met_at) - VALUES (v_meet_request.user_id, v_caller_id, now()) - ON CONFLICT (user_id, contact_id) DO UPDATE SET met_at = now(); - - -- Other person's name (the issuer, used in the return JSON) - SELECT display_name INTO v_contact_name - FROM public.profiles - WHERE id = v_meet_request.user_id; - - -- Caller's name + whether they signed up via this exact handshake. - -- handle_new_user() stamps profiles.signup_token = meet_token at signup. - SELECT display_name, signup_token - INTO v_caller_name, v_caller_signup_token - FROM public.profiles - WHERE id = v_caller_id; - - v_is_new_account := (v_caller_signup_token IS NOT NULL - AND v_caller_signup_token = p_token); - - v_push_msg := COALESCE(v_caller_name, 'Someone') - || CASE - WHEN v_is_new_account - THEN ' just joined FairShare via your handshake.' - ELSE ' is now your contact.' - END; - - -- Notify the meet issuer (the QR holder). Inserting into - -- contact_notifications gives us a Realtime in-app toast when the - -- issuer is foregrounded; send_push_to_users delivers an APNs alert - -- (and Web Push) so they also get a banner when backgrounded / - -- terminated. Excludes the actor automatically. - INSERT INTO public.contact_notifications - (to_user_id, from_user_id, notification_type, message) - VALUES - (v_meet_request.user_id, v_caller_id, 'new_contact', v_push_msg); - - PERFORM public.send_push_to_users( - ARRAY[v_meet_request.user_id], - v_caller_id, - 'FairShare', - v_push_msg, - '/?action=view_contact&contact=' || v_caller_id::text - ); - - -- If this meet carries group context, also perform sponsorship - IF v_meet_request.group_id IS NOT NULL THEN - - IF EXISTS ( - SELECT 1 FROM public.members - WHERE group_id = v_meet_request.group_id - AND user_id = v_caller_id - AND status IN ('active', 'pending') - ) THEN - RAISE EXCEPTION 'You are already a member or pending candidate of this group'; - END IF; - - INSERT INTO public.members (group_id, user_id, status, balance) - VALUES (v_meet_request.group_id, v_caller_id, 'pending', 0); - - INSERT INTO public.group_events (group_id, event_type, summary, actor_id, metadata) - VALUES ( - v_meet_request.group_id, - 'member_sponsored', - 'New candidate ' - || (SELECT display_name FROM public.profiles WHERE id = v_caller_id) - || ', sponsored by ' - || COALESCE(v_contact_name, 'Unknown'), - v_meet_request.user_id, - json_build_object('sponsor_id', v_meet_request.user_id, 'candidate_id', v_caller_id)::jsonb - ); - - INSERT INTO public.endorsements (group_id, candidate_id, endorser_id) - VALUES (v_meet_request.group_id, v_caller_id, v_meet_request.user_id); - - PERFORM public.check_endorsements(v_meet_request.group_id, v_caller_id); - - SELECT name INTO v_group_name - FROM public.groups WHERE id = v_meet_request.group_id; - - SELECT (status = 'active') INTO v_admitted - FROM public.members - WHERE group_id = v_meet_request.group_id AND user_id = v_caller_id; - - RETURN json_build_object( - 'contact_id', v_meet_request.user_id, - 'contact_name', COALESCE(v_contact_name, 'Unknown'), - 'group_id', v_meet_request.group_id, - 'group_name', v_group_name, - 'admitted', COALESCE(v_admitted, false), - 'already_contact', COALESCE(v_already_contact, false) - ); - END IF; - - RETURN json_build_object( - 'contact_id', v_meet_request.user_id, - 'contact_name', COALESCE(v_contact_name, 'Unknown'), - 'already_contact', COALESCE(v_already_contact, false) - ); -END; -$$ LANGUAGE plpgsql SECURITY DEFINER; diff --git a/sql/meet-token-reserve-at-signup.sql b/sql/meet-token-reserve-at-signup.sql index a19dd57..735a52c 100644 --- a/sql/meet-token-reserve-at-signup.sql +++ b/sql/meet-token-reserve-at-signup.sql @@ -145,7 +145,7 @@ BEGIN v_push_msg := COALESCE(v_caller_name, 'Someone') || CASE WHEN v_is_new_account - THEN ' just joined FairShare via your handshake.' + THEN ' just joined Union via your handshake.' ELSE ' is now your contact.' END; @@ -157,7 +157,7 @@ BEGIN PERFORM public.send_push_to_users( ARRAY[v_meet_request.user_id], v_caller_id, - 'FairShare', + 'Union', v_push_msg, '/?action=view_contact&contact=' || v_caller_id::text );