Changeset 6053c6c in Klonkt for src/views/pages


Ignore:
Timestamp:
07/01/2026 07:38:33 AM (2 months ago)
Author:
Robin Genis <roboburr@…>
Branches:
main
Children:
55a73f0
Parents:
f70e010
Message:

feat(fediverse): show + vote on remote polls (Question) in the News feed

Inbound ActivityPub polls, phase 1. A Question (the Mastodon-standard poll)
from a followed account is now cached and rendered in the News feed with its
options, vote counts and close state, and the owner can vote — a ballot is a
Create(Note) carrying only the chosen option's name + inReplyTo the Question,
addressed to the poll's author (the fediverse-standard vote). The author's
Update(Question) refreshes the authoritative counts; our own vote is preserved.

  • config/database.js — ap_timeline.poll_json column
  • services/ActivityPubService.js — parsePoll(); handle Create/Update(Question); voteOnPoll() sends the ballot + optimistic local tally
  • routes/posts.js — parse poll_json for the feed; POST /news/vote (owner-only)
  • views/pages/news.ejs — poll UI (votable options / result bars) + styles
  • services/i18n.js — poll.vote/votes/closed (nl/en/de)

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

File:
1 edited

Legend:

Unmodified
Added
Removed
  • src/views/pages/news.ejs

    rf70e010 r6053c6c  
    132132          <% if (p.embedUrl) { %><a class="tl-embed-open" href="<%= p.embedUrl %>" target="_blank" rel="noopener">▶ <%= t('tl.open_player') %></a><% } %>
    133133
     134          <% if (p.poll) { var _pl=p.poll; var _tot=_pl.options.reduce(function(s,o){return s+(o.count||0);},0); var _voteable=!_pl.closed&&!_pl.voted; %>
     135          <div class="tl-poll">
     136            <% if (_voteable) { %>
     137              <form method="post" action="/news/vote" class="tl-poll-form">
     138                <input type="hidden" name="note" value="<%= p.id %>">
     139                <% _pl.options.forEach(function(o){ %><label class="tl-poll-choice"><input type="<%= _pl.multiple?'checkbox':'radio' %>" name="choice" value="<%= o.name %>"><span><%= o.name %></span></label><% }); %>
     140                <button type="submit" class="btn btn-primary tl-poll-btn"><%= t('poll.vote') %></button>
     141              </form>
     142            <% } else { _pl.options.forEach(function(o){ var _pct=_tot?Math.round((o.count||0)*100/_tot):0; var _mine=_pl.voted&&(Array.isArray(_pl.voted)?_pl.voted.indexOf(o.name)>=0:_pl.voted===o.name); %><div class="tl-poll-res<%= _mine?' is-mine':'' %>"><span class="tl-poll-fill" style="width:<%= _pct %>%"></span><span class="tl-poll-name"><%= _mine?'✓ ':'' %><%= o.name %></span><span class="tl-poll-pct"><%= _pct %>%</span></div><% }); } %>
     143            <div class="tl-poll-foot"><%= (_pl.voters!=null?_pl.voters:_tot) %> <%= t('poll.votes') %><% if(_pl.closed){ %> · <%= t('poll.closed') %><% } %></div>
     144          </div>
     145          <% } %>
     146
    134147          <% if (p.url) { %><a class="tl-orig" href="<%= p.url %>" target="_blank" rel="nofollow noopener"><%= t('tl.view_original') %></a><% } %>
    135148
     
    199212  .tl-media-video, .tl-media-audio { width: 100%; margin: .75rem 0 0; border-radius: 12px; display: block; }
    200213  .tl-media-video { max-height: 480px; background: #000; }
     214  .tl-poll { margin: .75rem 0 0; display: flex; flex-direction: column; gap: 8px; }
     215  .tl-poll-form { display: flex; flex-direction: column; gap: 8px; }
     216  .tl-poll-choice { display: flex; align-items: center; gap: 10px; padding: 10px 12px; border: 1px solid var(--line, rgba(128,128,128,.3)); border-radius: 10px; cursor: pointer; }
     217  .tl-poll-choice:hover { border-color: var(--accent); }
     218  .tl-poll-choice input { accent-color: var(--accent); }
     219  .tl-poll-btn { align-self: flex-start; margin-top: 2px; }
     220  .tl-poll-res { position: relative; padding: 9px 12px; border-radius: 10px; overflow: hidden; background: var(--paper-2, rgba(128,128,128,.08)); display: flex; align-items: center; gap: 8px; }
     221  .tl-poll-fill { position: absolute; inset: 0 auto 0 0; background: color-mix(in srgb, var(--accent) 22%, transparent); z-index: 0; }
     222  .tl-poll-res.is-mine .tl-poll-fill { background: color-mix(in srgb, var(--accent) 40%, transparent); }
     223  .tl-poll-name { position: relative; z-index: 1; flex: 1; font-size: .95rem; }
     224  .tl-poll-pct { position: relative; z-index: 1; font-variant-numeric: tabular-nums; font-weight: 500; }
     225  .tl-poll-foot { font-size: .82rem; color: var(--ink-soft, #888); }
    201226
    202227  .tl-embed { margin: .75rem 0 0; border-radius: 12px; overflow: hidden; background: var(--paper-2, #111); }
Note: See TracChangeset for help on using the changeset viewer.