Changeset 0403187 in Klonkt for src/views/pages/post-edit.ejs


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

feat(fediverse): host your own polls (federate as AS2 Question)

A post can carry a poll that federates as an AS2 Question so remote (Mastodon)
followers vote from their own app; votes are tallied server-side and the fresh
counts are pushed back as Update(Question). Voting is fediverse-only; the site
shows live, read-only results. Complements the existing inbound poll support.

  • src/config/database.js — posts.poll_json (our poll definition) + poll_votes table (post_id, actor_uri, choice; UNIQUE) backing the tally + per-actor dedupe.
  • src/services/ActivityPubService.js — parseOwnPoll/pollTally/ownPollView helpers; buildNote emits a Question (oneOf/anyOf + replies.totalItems + endTime/closed + votersCount) for a poll post; handleInbox records a ballot (Note with name + inReplyTo our poll) before the reply path, deduped per actor; a debounced Update(Question) pushes fresh counts to followers; votersCount added to AP_CONTEXT.
  • src/services/Scheduler.js — closeExpiredPolls() marks a poll closed once its endTime passes and pushes the final tally; runs on the existing 60s tick.
  • src/routes/posts.js — parsePollForm() turns the editor fields into poll_json on create/save (a poll with votes is frozen), passes poll_json to the federation hooks, and hands the post page a render-ready ownPollView.
  • src/views/pages/post-edit.ejs — poll section (options, multiple-choice, duration); disabled once the poll has votes.
  • src/views/pages/post.ejs — display-only poll with result bars + voter/close meta.
  • src/services/i18n.js — poll.* + pedit.poll_* strings (nl/en/de).
  • test/polls.test.js — Question shape, tally, percentages, closed state, AS2 term.
  • CHANGELOG(.nl/.de).md — "Create your own polls" under Unreleased.

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

File:
1 edited

Legend:

Unmodified
Added
Removed
  • src/views/pages/post-edit.ejs

    r731e431 r0403187  
    257257          if (cw && nsfw && !cw.__nsfwWired) { cw.__nsfwWired = true;
    258258            cw.addEventListener('input', function () { if (cw.value.trim()) nsfw.checked = true; });
     259          }
     260        })();
     261        </script>
     262        <% // Poll (federates as an AS2 Question). Free feature. A poll with votes is frozen.
     263           var _poll = null; try { _poll = post.poll_json ? JSON.parse(post.poll_json) : null; } catch (e) { _poll = null; }
     264           var _pollLocked = (typeof pollLocked !== 'undefined' && pollLocked);
     265           var _pollOpts = (_poll && Array.isArray(_poll.options) && _poll.options.length) ? _poll.options : [{ name: '' }, { name: '' }]; %>
     266        <label class="pe-checkbox" style="margin-top:8px">
     267          <input type="checkbox" name="poll_enabled" value="1" id="pe-poll-toggle" <%= _poll ? 'checked' : '' %> <%= _pollLocked ? 'disabled' : '' %>>
     268          <span>📊 <%= t('pedit.poll_label') %></span>
     269        </label>
     270        <div id="pe-poll-fields" style="margin-top:6px<%= _poll ? '' : ';display:none' %>">
     271          <% if (_pollLocked) { %><p style="font-size:12px;opacity:.7;margin:0 0 6px"><%= t('pedit.poll_locked') %></p><% } %>
     272          <div id="pe-poll-opts">
     273            <% _pollOpts.forEach(function (o) { %>
     274              <input type="text" name="poll_option" class="pe-poll-opt" maxlength="100" value="<%= (o && o.name) || '' %>" placeholder="<%= t('pedit.poll_option_ph') %>" <%= _pollLocked ? 'disabled' : '' %>
     275                     style="display:block;width:100%;box-sizing:border-box;margin-bottom:5px;font-size:13px;padding:7px 9px;border-radius:7px;border:1px solid var(--rule,rgba(128,128,128,.35));background:transparent;color:inherit">
     276            <% }); %>
     277          </div>
     278          <button type="button" id="pe-poll-add" class="btn" data-ph="<%= t('pedit.poll_option_ph') %>" <%= _pollLocked ? 'disabled' : '' %> style="font-size:12.5px;padding:5px 10px">+ <%= t('pedit.poll_add') %></button>
     279          <label class="pe-checkbox" style="margin-top:8px">
     280            <input type="checkbox" name="poll_multiple" value="1" <%= (_poll && _poll.multiple) ? 'checked' : '' %> <%= _pollLocked ? 'disabled' : '' %>>
     281            <span><%= t('pedit.poll_multiple') %></span>
     282          </label>
     283          <label style="display:block;font-size:12.5px;opacity:.8;margin:6px 0 4px"><%= t('pedit.poll_duration') %></label>
     284          <select name="poll_duration" <%= _pollLocked ? 'disabled' : '' %> style="padding:7px 9px;border-radius:7px;border:1px solid var(--rule,rgba(128,128,128,.35));background:transparent;color:inherit;font-size:13px">
     285            <% [['300','5m'],['1800','30m'],['3600','1h'],['21600','6h'],['43200','12h'],['86400','1d'],['259200','3d'],['604800','7d']].forEach(function (d) { %>
     286              <option value="<%= d[0] %>" <%= d[0] === '86400' ? 'selected' : '' %>><%= t('pedit.poll_dur_' + d[1]) %></option>
     287            <% }); %>
     288          </select>
     289        </div>
     290        <script>
     291        (function () {
     292          var tog = document.getElementById('pe-poll-toggle'), box = document.getElementById('pe-poll-fields');
     293          if (tog && box && !tog.__wired) { tog.__wired = true; tog.addEventListener('change', function () { box.style.display = tog.checked ? '' : 'none'; }); }
     294          var add = document.getElementById('pe-poll-add'), opts = document.getElementById('pe-poll-opts');
     295          if (add && opts && !add.__wired) { add.__wired = true;
     296            add.addEventListener('click', function () {
     297              if (opts.querySelectorAll('.pe-poll-opt').length >= 8) return;
     298              var i = document.createElement('input');
     299              i.type = 'text'; i.name = 'poll_option'; i.className = 'pe-poll-opt'; i.maxLength = 100;
     300              i.placeholder = add.getAttribute('data-ph') || '';
     301              i.setAttribute('style', 'display:block;width:100%;box-sizing:border-box;margin-bottom:5px;font-size:13px;padding:7px 9px;border-radius:7px;border:1px solid var(--rule,rgba(128,128,128,.35));background:transparent;color:inherit');
     302              opts.appendChild(i);
     303            });
    259304          }
    260305        })();
Note: See TracChangeset for help on using the changeset viewer.