diff --git a/prosody-plugins/mod_politictalk_roles.lua b/prosody-plugins/mod_politictalk_roles.lua index f3d1e22..168ef5e 100644 --- a/prosody-plugins/mod_politictalk_roles.lua +++ b/prosody-plugins/mod_politictalk_roles.lua @@ -2,8 +2,11 @@ local st = require "util.stanza"; local http = require "net.http"; local jid = require "util.jid"; local json = require "util.json"; +local array = require "util.array"; local util = module:require "util"; local is_admin = util.is_admin; +local is_focus = util.is_focus; +local internal_room_jid_match_rewrite = util.internal_room_jid_match_rewrite; local ROLE_JOINING = module:shared("politictalk/roles/joining"); local ROOM_INACTIVE_CALLBACK_URL = module:get_option_string( @@ -144,13 +147,48 @@ local function ensure_room_data(room) room._data.politictalk_host_jids = room._data.politictalk_host_jids or {}; room._data.politictalk_participant_jids = room._data.politictalk_participant_jids or {}; room._data.politictalk_jid_user_ids = room._data.politictalk_jid_user_ids or {}; - -- Enables Jitsi AV moderation automatically: hosts can speak, participants - -- must be explicitly allowed by a host before they can unmute. - room._data.av_can_unmute = false; return true; end +local function enable_audio_moderation_only(room, actor_occupant) + if not room or not actor_occupant then + return; + end + + -- Avoid Jitsi's av_can_unmute shortcut because it enables audio, video, and desktop moderation together. + room.av_moderation = room.av_moderation or {}; + room.av_moderation_actors = room.av_moderation_actors or {}; + + if room.av_moderation.audio then + room.av_moderation.video = nil; + room.av_moderation.desktop = nil; + room.av_moderation_actors.video = nil; + room.av_moderation_actors.desktop = nil; + return; + end + + room.av_moderation.audio = array(); + room.av_moderation.video = nil; + room.av_moderation.desktop = nil; + + for _, room_occupant in room:each_occupant() do + if room:get_role(room_occupant.nick) == "moderator" and not is_focus(room_occupant.nick) then + room.av_moderation.audio:push(internal_room_jid_match_rewrite(room_occupant.nick)); + end + end + + room.jitsiMetadata = room.jitsiMetadata or {}; + room.jitsiMetadata.startMuted = room.jitsiMetadata.startMuted or {}; + room.av_moderation_startMuted_restore = room.av_moderation_startMuted_restore or {}; + room.av_moderation_startMuted_restore.audio = room.jitsiMetadata.startMuted.audio; + room.jitsiMetadata.startMuted.audio = true; + + room.av_moderation_actors.audio = actor_occupant.nick; + room.av_moderation_actors.video = nil; + room.av_moderation_actors.desktop = nil; +end + local function update_room_role_lists(room, user_id, is_moderator) if not room or not user_id then return; @@ -613,10 +651,11 @@ module:hook("muc-occupant-joined", function(event) mark_host(room, occupant, role.user_id); update_room_role_lists(room, role.user_id, true); room:set_affiliation(true, occupant.bare_jid, "owner"); + enable_audio_moderation_only(room, occupant); remove_duplicate_user_occupants(room, occupant, role.user_id); module:log( "info", - "PoliticTalk host joined as moderator with AV moderation: %s room=%s", + "PoliticTalk host joined as moderator with audio moderation: %s room=%s", tostring(role.user_id), tostring(room.jid) ); diff --git a/web/plugin.head.html b/web/plugin.head.html index 71fa48f..6c7768d 100644 --- a/web/plugin.head.html +++ b/web/plugin.head.html @@ -466,6 +466,10 @@ display: none !important; } + .politictalk-hidden-av-request { + display: none !important; + } + @media (max-width: 640px) { html:not(.politictalk-direct-access-blocked), html:not(.politictalk-direct-access-blocked) body, @@ -1144,6 +1148,141 @@ }); } + function normalizePoliticTalkText(value) { + return String(value || '').replace(/\s+/g, ' ').trim().toLowerCase(); + } + + function isPoliticTalkModerationAction(text) { + return text.indexOf('allow audio') !== -1 + || text.indexOf('allow video') !== -1 + || text.indexOf('allow screen sharing') !== -1 + || text.indexOf('allow all') !== -1 + || text.indexOf('unmute audio') !== -1 + || text.indexOf('unmute video') !== -1 + || text.indexOf('start screen sharing') !== -1; + } + + function findPoliticTalkNotificationCard(element) { + var current = element; + + while (current && current !== document.body) { + if (isPoliticTalkRaisedHandCard(current)) { + return current; + } + + current = current.parentElement; + } + + return null; + } + + function isPoliticTalkRaisedHandCard(element) { + if (!element || element === document.body) { + return false; + } + + var text = normalizePoliticTalkText(element.textContent); + var rect = element.getBoundingClientRect(); + + return text.indexOf('would like to participate') !== -1 + && rect.width > 120 + && rect.width < 560 + && rect.height > 40 + && rect.height < 280; + } + + function applyPoliticTalkModerationNotificationPolicy() { + if (directAccessBlocked || !document.body) { + return; + } + + var isHost = isPoliticTalkHost(); + var controls = document.querySelectorAll('button, [role="button"], a'); + var handledCards = []; + + controls.forEach(function(control) { + var actionText = normalizePoliticTalkText([ + control.getAttribute('aria-label'), + control.getAttribute('title'), + control.textContent + ].join(' ')); + + if (!isPoliticTalkModerationAction(actionText)) { + return; + } + + var card = findPoliticTalkNotificationCard(control); + + if (!card || handledCards.indexOf(card) !== -1) { + return; + } + + handledCards.push(card); + + var cardText = normalizePoliticTalkText(card.textContent); + var isAudioCard = cardText.indexOf('allow audio') !== -1 + || cardText.indexOf('unmute audio') !== -1; + var isVideoOrDesktopCard = cardText.indexOf('allow video') !== -1 + || cardText.indexOf('allow screen sharing') !== -1 + || cardText.indexOf('unmute video') !== -1 + || cardText.indexOf('start screen sharing') !== -1; + + card.classList.toggle('politictalk-hidden-av-request', !isHost || isVideoOrDesktopCard || !isAudioCard); + card.setAttribute('aria-hidden', (!isHost || isVideoOrDesktopCard || !isAudioCard) ? 'true' : 'false'); + + card.querySelectorAll('button, [role="button"], a').forEach(function(cardControl) { + var cardControlText = normalizePoliticTalkText([ + cardControl.getAttribute('aria-label'), + cardControl.getAttribute('title'), + cardControl.textContent + ].join(' ')); + + if (cardControlText.indexOf('allow all') !== -1) { + cardControl.classList.add('politictalk-hidden-av-request'); + cardControl.setAttribute('aria-hidden', 'true'); + cardControl.setAttribute('tabindex', '-1'); + } + }); + }); + + if (!isHost) { + document.querySelectorAll('[role="alert"], [class*="notification"], [class*="Notification"], div') + .forEach(function(candidate) { + if (isPoliticTalkRaisedHandCard(candidate)) { + candidate.classList.add('politictalk-hidden-av-request'); + candidate.setAttribute('aria-hidden', 'true'); + } + }); + } + } + + function mountPoliticTalkModerationNotificationPolicy() { + applyPoliticTalkModerationNotificationPolicy(); + + if (window.politicTalkModerationNotificationObserver || !document.body) { + return; + } + + window.politicTalkModerationNotificationObserver = new MutationObserver(function() { + window.cancelAnimationFrame(window.politicTalkModerationNotificationFrame); + window.politicTalkModerationNotificationFrame = window.requestAnimationFrame( + applyPoliticTalkModerationNotificationPolicy + ); + }); + window.politicTalkModerationNotificationObserver.observe(document.body, { + attributes: true, + attributeFilter: [ 'aria-label', 'class', 'style', 'title' ], + childList: true, + characterData: true, + subtree: true + }); + + window.clearInterval(window.politicTalkModerationNotificationInterval); + window.politicTalkModerationNotificationInterval = window.setInterval(function() { + applyPoliticTalkModerationNotificationPolicy(); + }, 800); + } + function mountPoliticTalkLogo() { if (!document.body || document.getElementById('politictalk-room-logo')) { return; @@ -1222,6 +1361,7 @@ mountHostHangupPolicy(); mountMobileToolbarPositioning(); mountPoliticTalkStageBrandInset(); + mountPoliticTalkModerationNotificationPolicy(); }); } else { mountPoliticTalkDocumentTitle(); @@ -1230,6 +1370,7 @@ mountHostHangupPolicy(); mountMobileToolbarPositioning(); mountPoliticTalkStageBrandInset(); + mountPoliticTalkModerationNotificationPolicy(); } }());