Ignore:
Timestamp:
07/01/2026 01:32:22 PM (2 months ago)
Author:
roboburr <roboburr@…>
Branches:
main
Children:
c45c187
Parents:
bb3f67c
Message:

feat(polls): vote on any fediverse poll from the interact page

The interact page (paste any post URL) now detects a Question and shows a ballot
(radio/checkbox + Vote) so you can vote on any fediverse poll by URL — not only
polls from accounts you follow (which vote via /news). Casts the Mastodon-standard
ballot straight to the poll's author, no timeline cache needed.

  • src/services/ActivityPubService.js — voteOnRemotePoll(site, url, choices) fetches the Question fresh, validates the choice(s) and delivers the ballot to the author; resolveRemoteNote now returns the parsed poll so the view can render options.
  • src/routes/posts.js — POST /authorize_interaction/vote; GET passes voted and skips re-resolving the target on the confirmation view.
  • src/views/pages/authorize-interaction.ejs — ballot (open) / results (closed) above the like/boost/reply actions, a "vote sent" confirmation, and scoped .auth-poll styles.
  • src/services/i18n.js — poll.voted_title/_done (nl/en/de).

Co-Authored-By: Claude <noreply@…>

File:
1 edited

Legend:

Unmodified
Added
Removed
  • src/views/pages/authorize-interaction.ejs

    rbb3f67c r667fb41  
    9393      <a class="btn" href="/"><%= t('fedi.remote_back') %></a>
    9494    </p>
     95  <% } else if (typeof voted !== 'undefined' && voted) { %>
     96    <h1 class="auth-interact-title">📊 <%= t('poll.voted_title') %></h1>
     97    <p class="auth-interact-note"><%= t('poll.voted_done') %></p>
     98    <p class="auth-interact-actions">
     99      <% if (uri) { %><a class="btn btn-primary" href="<%= uri %>" rel="nofollow noopener"><%= t('fedi.remote_view_original') %></a><% } %>
     100      <a class="btn" href="/"><%= t('fedi.remote_back') %></a>
     101    </p>
    95102  <% } else if (typeof followTarget !== 'undefined' && followTarget) { %>
    96103  <h1 class="auth-interact-title"><%= t('fedi.follow_heading') %></h1>
     
    135142        <span><%= t('fedi.remote_view_original') %></span>
    136143      </a>
     144    <% } %>
     145    <% if (target.poll) { var _pl = target.poll; var _tot = _pl.options.reduce(function(s,o){return s+(o.count||0);},0); %>
     146      <% if (!_pl.closed) { %>
     147        <form method="post" action="/authorize_interaction/vote" class="auth-poll">
     148          <input type="hidden" name="uri" value="<%= uri %>">
     149          <% _pl.options.forEach(function(o){ %>
     150            <label class="auth-poll-choice"><input type="<%= _pl.multiple ? 'checkbox' : 'radio' %>" name="choice" value="<%= o.name %>"><span><%= o.name %></span></label>
     151          <% }); %>
     152          <div class="auth-interact-actions">
     153            <button type="submit" class="btn btn-primary"><%= t('poll.vote') %></button>
     154          </div>
     155          <p class="auth-poll-foot"><% if (_pl.multiple) { %><%= t('poll.multiple') %> · <% } %><%= (_pl.voters!=null?_pl.voters:_tot) %> <%= t('poll.votes') %></p>
     156        </form>
     157      <% } else { %>
     158        <div class="auth-poll">
     159          <% _pl.options.forEach(function(o){ var _pct = _tot ? Math.round((o.count||0)*100/_tot) : 0; %>
     160            <div class="auth-poll-res"><span class="auth-poll-fill" style="width:<%= _pct %>%"></span><span class="auth-poll-name"><%= o.name %></span><span class="auth-poll-pct"><%= _pct %>%</span></div>
     161          <% }); %>
     162          <p class="auth-poll-foot"><%= (_pl.voters!=null?_pl.voters:_tot) %> <%= t('poll.votes') %> · <%= t('poll.closed') %></p>
     163        </div>
     164      <% } %>
    137165    <% } %>
    138166    <% var _liked = (typeof reacted !== 'undefined' && reacted.liked); var _boosted = (typeof reacted !== 'undefined' && reacted.boosted); %>
     
    251279  .fedi-edit-save { padding: .4rem 1rem; border: 0; border-radius: 999px; background: var(--accent); color: var(--paper, #fff);
    252280    font: inherit; font-weight: 600; font-size: .82rem; cursor: pointer; }
     281  /* Poll ballot / results on the interact page */
     282  .auth-poll { display: flex; flex-direction: column; gap: 8px; margin: 0 0 1rem; }
     283  .auth-poll-choice { display: flex; align-items: center; gap: 10px; padding: 10px 12px; border-radius: 10px; cursor: pointer;
     284    border: 1px solid color-mix(in srgb, var(--ink, #000) 14%, transparent); }
     285  .auth-poll-choice:hover { border-color: var(--accent); }
     286  .auth-poll-choice input { accent-color: var(--accent); }
     287  .auth-poll-res { position: relative; padding: 9px 12px; border-radius: 10px; overflow: hidden; display: flex; align-items: center; gap: 8px;
     288    background: color-mix(in srgb, var(--ink, #000) 6%, transparent); }
     289  .auth-poll-fill { position: absolute; inset: 0 auto 0 0; background: color-mix(in srgb, var(--accent) 22%, transparent); z-index: 0; }
     290  .auth-poll-name { position: relative; z-index: 1; flex: 1; font-size: .95rem; }
     291  .auth-poll-pct { position: relative; z-index: 1; font-variant-numeric: tabular-nums; font-weight: 500; }
     292  .auth-poll-foot { font-size: .82rem; color: var(--ink-soft, #888); margin: .1rem 0 0; }
    253293</style>
    254294
Note: See TracChangeset for help on using the changeset viewer.