Merge branch 'unstable' into codex/extend-tag-pill-rendering-and-add-menu-component

This commit is contained in:
thePR0M3TH3AN authored and GitHub committed 2025-10-25 17:29:39 -04:00
commit 6144a84f7b
14 files changed
+801 -39

No files matched your search

+2 -2
View File
@@ -1,10 +1,10 @@
:root{--surface-base:var(--color-surface);--surface-alt:var(--color-surface-alt);--surface-raised:var(--color-card);--surface-raised-private:var(--color-card-private);--surface-raised-critical:var(--color-card-critical);--surface-panel:var(--color-panel);--surface-panel-hover:var(--color-panel-hover);--surface-sunken:var(--color-surface-sunken);--surface-muted:var(--color-surface-muted);--surface-veil:var(--color-overlay-veil);--surface-inverse:var(--color-page);--surface-overlay:var(--color-overlay);--surface-overlay-muted:var(--color-overlay-muted);--surface-overlay-strong:var(--color-overlay-strong);--surface-overlay-darker:var(--color-overlay-darker);--surface-overlay-black:var(--color-overlay-black);--surface-overlay-banner-base:var(--color-page);--surface-overlay-banner:var(--surface-overlay-banner-base);--surface-overlay-banner-opaque:var(--surface-overlay-banner-base);--surface-overlay-banner-strong:color-mix(in srgb,var(--surface-overlay-banner-base) 82%,transparent);--surface-overlay-banner-bottom:color-mix(in srgb,var(--surface-overlay-banner-base) 50%,transparent);--surface-overlay-banner-channel-mid:color-mix(in srgb,var(--surface-overlay-banner-base) 24%,transparent);--surface-overlay-banner-channel-faint:color-mix(in srgb,var(--surface-overlay-banner-base) 10%,transparent);--surface-overlay-banner-soft:color-mix(in srgb,var(--surface-overlay-banner-base) 56%,transparent);--surface-overlay-banner-faint:color-mix(in srgb,var(--surface-overlay-banner-base) 18%,transparent);--surface-overlay-banner-transparent:transparent;--surface-overlay-banner-gradient-header:linear-gradient(to bottom,var(--surface-overlay-banner-opaque) 0%,var(--surface-overlay-banner-strong) 38%,var(--surface-overlay-banner-soft) 70%,var(--surface-overlay-banner-faint) 88%,var(--surface-overlay-banner-transparent) 100%);--surface-overlay-banner-gradient-channel:linear-gradient(to top,var(--surface-overlay-banner-bottom) 0%,var(--surface-overlay-banner-channel-mid) 28%,var(--surface-overlay-banner-channel-faint) 42%,var(--surface-overlay-banner-transparent) 50%);--surface-overlay-banner-gradient:var(
--surface-overlay-banner-gradient-header
);--surface-overlay-panel:var(--color-overlay-panel);--surface-overlay-panel-soft:var(--color-overlay-panel-soft);--surface-overlay-panel-tint:var(--color-overlay-panel-tint);--surface-overlay-panel-glass:var(--color-overlay-panel-glass);--surface-overlay-panel-glass-soft:var(--color-overlay-panel-glass-soft);--surface-modal-backdrop:var(--color-modal-backdrop);--surface-modal:var(--color-modal-surface);--blur-modal-backdrop:18px;--blur-modal-alert:14px;--blur-thumbnail-blur:18px;--modal-nav-backdrop-blur:6px;--app-header-centerline-offset:calc(var(--space-sm) + var(--space-2xl-plus)/2);--border-default:var(--color-border);--border-subtle:var(--color-border-subtle);--border-strong:var(--color-border-translucent-stronger);--border-overlay:var(--color-border-translucent);--border-overlay-strong:var(--color-border-translucent-strongest);--border-overlay-glow:var(--color-border-translucent-glow);--border-neutral-soft:var(--color-border-neutral-soft);--border-info-soft:var(--color-border-info-soft);--border-sidebar-strong:var(--color-border-sidebar-strong);--border-width-hairline:1px;--border-width-fine:0.5px;--text-primary:var(--color-text);--text-inverse:var(--color-text-inverse);--text-muted:var(--color-text-muted);--text-muted-strong:var(--color-muted-strong);--text-subtle:var(--color-subtle-text);--text-frosted-primary:var(--color-text-frosted);--text-frosted-strong:var(--color-text-frosted-strong);--text-frosted-soft:var(--color-text-frosted-soft);--status-info:var(--color-info);--status-info-strong:var(--color-info-strong);--status-info-surface:var(--color-info-glass);--status-info-border:var(--color-info-border);--status-info-on:var(--color-info-on);--status-success:var(--color-success);--status-success-strong:var(--color-success-strong);--status-success-surface:var(--color-success-surface);--status-success-border:var(--color-success-border);--status-success-on:var(--color-success-on);--status-warning:var(--color-warning);--status-warning-strong:var(--color-warning-strong);--status-warning-surface:var(--color-warning-surface);--status-warning-border:var(--color-warning-border);--status-warning-on:var(--color-warning-on);--status-danger:var(--color-critical);--status-danger-strong:var(--color-critical-strong);--status-danger-surface:var(--color-danger-surface);--status-danger-border:var(--color-danger-border);--status-danger-on:var(--color-danger-on);--status-private:var(--color-private);--status-private-strong:var(--color-private-strong);--status-private-surface:var(--color-private-surface);--status-private-border:var(--color-private-border);--status-private-on:var(--color-private-on);--status-neutral:var(--color-muted);--status-neutral-strong:var(--color-muted-strong);--status-neutral-surface:var(--color-neutral-surface);--status-neutral-border:var(--color-neutral-border);--status-neutral-on:var(--color-neutral-on);--scrollbar-width:0.5rem;--scrollbar-track:var(--color-white-soft);--scrollbar-thumb:var(--color-white-softer);--scrollbar-thumb-hover:var(--color-white-softest);--z-app-header:40;--z-overlay-nav:60;--z-overlay-floating:70;--z-overlay-popover:140;--z-overlay-toast:180;--z-sidebar-overlay:10;--z-sidebar:20;--z-modal-root:100;--z-modal-overlay:110;--z-modal-content:120;--z-modal-player:130;--z-modal-top-root:140;--z-modal-top-overlay:150;--z-modal-top-content:160;--overlay-backdrop-color:var(--color-overlay-backdrop);--overlay-backdrop-blur:12px;--sidebar-width-expanded-base:16rem;--sidebar-dropup-content-width:0px;--sidebar-width-expanded:max(var(--sidebar-width-expanded-base),var(--sidebar-dropup-content-width));--sidebar-width-collapsed:4rem;--sidebar-width:var(--sidebar-width-expanded);--sidebar-control-size:2.75rem;--sidebar-content-gap:1rem;--space-sidebar-shell-inline:var(--space-lg);--space-sidebar-shell-block-start:var(--space-sm);--space-sidebar-shell-block-end:1.75rem;--space-sidebar-scroll-gap:1.75rem;--space-sidebar-panel-gap:1.35rem;--space-sidebar-panel-padding-block:1.35rem;--space-sidebar-panel-padding-inline:1.1rem;--space-sidebar-footer-gap:0.85rem;--space-sidebar-trigger-padding-block:0.78rem;--space-sidebar-popover-offset:0.85rem;--space-sidebar-popover-padding-block:0.85rem;--space-sidebar-popover-padding-inline:0.65rem;--space-sidebar-popover-gap:0.45rem;--space-sidebar-dropup-scroll-reserve:calc(var(--scrollbar-thin-width) + var(--space-sm) + var(--space-2xs));--sidebar-dropup-max-height:18rem;--space-sidebar-nav-padding-block:0.7rem;--space-sidebar-nav-padding-inline-start:var(--space-xs);--space-sidebar-nav-padding-inline-end:calc(var(--space-lg) + var(--space-2xs));--space-sidebar-subnav-padding-block:0.55rem;--space-sidebar-nav-translate:0.35rem;--grid-card-min-width:20rem;--popover-backdrop-blur:12px;--overlay-panel-radius:var(--radius-lg);--overlay-panel-shadow:var(--shadow-lg);--overlay-panel-padding-inline:var(--popup-padding-inline);--overlay-panel-padding-block:var(--popup-padding-block);--toast-translate:-1.5rem;--sidebar-mobile-gutter:3rem;--popup-padding-Line truncated
/*! tailwindcss v3.4.17 | MIT License | https://tailwindcss.com*/*,:after,:before{box-sizing:border-box;border:0 solid #e5e7eb}:after,:before{--tw-content:""}:host,html{line-height:1.5;-webkit-text-size-adjust:100%;-moz-tab-size:4;-o-tab-size:4;tab-size:4;font-family:var(--font-family-sans);font-feature-settings:normal;font-variation-settings:normal;-webkit-tap-highlight-color:transparent}body{line-height:inherit}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,pre,samp{font-family:var(--font-family-mono);font-feature-settings:normal;font-variation-settings:normal;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}button,input,optgroup,select,textarea{font-family:inherit;font-feature-settings:inherit;font-variation-settings:inherit;font-size:100%;font-weight:inherit;line-height:inherit;letter-spacing:inherit;color:inherit;margin:0;padding:0}button,select{text-transform:none}button,input:where([type=button]),input:where([type=reset]),input:where([type=submit]){-webkit-appearance:button;background-color:transparent;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:baseline}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dd,dl,figure,h1,h2,h3,h4,h5,h6,hr,p,pre{margin:0}fieldset{margin:0}fieldset,legend{padding:0}menu,ol,ul{list-style:none;margin:0;padding:0}dialog{padding:0}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{opacity:1;color:#9ca3af}input::placeholder,textarea::placeholder{opacity:1;color:#9ca3af}[role=button],button{cursor:pointer}:disabled{cursor:default}audio,canvas,embed,iframe,img,object,svg,video{display:block;vertical-align:middle}img,video{max-width:100%;height:auto}[hidden]:where(:not([hidden=until-found])){display:none}html{font-size:80%}@media (min-width:768px){html{font-size:100%}}@media (min-width:1024px){html{font-size:75%}}*,:after,:before{scrollbar-width:thin;scrollbar-color:var(--scrollbar-thumb) var(--scrollbar-track)}::-webkit-scrollbar{width:var(--scrollbar-width);height:var(--scrollbar-width)}::-webkit-scrollbar-track{background:var(--scrollbar-track)}::-webkit-scrollbar-thumb{background:var(--scrollbar-thumb);border-radius:var(--radius-sm)}::-webkit-scrollbar-thumb:hover{background:var(--scrollbar-thumb-hover)}body{--app-header-centerline:calc(env(safe-area-inset-top, 0) + var(--app-header-centerline-offset));font-family:var(--font-family-sans);background-color:var(--color-page);color:var(--color-text-strong);line-height:1.5;margin:0;padding:0;overflow-x:hidden}#uiOverlay{position:fixed;inset:0;pointer-events:none;z-index:var(--z-overlay-popover)}#uiOverlay>*{pointer-events:auto}#editNip71TagsFieldset>legend,#nip71TagsFieldset>legend{margin-left:0;padding:0}#app{background-color:var(--color-page);color:inherit}body.modal-open,html.modal-open{overflow:hidden;height:100%}body.modal-open #app{overflow:hidden}body.modal-open #sidebar{pointer-events:none;opacity:0;visibility:hidden;z-index:var(--z-sidebar-overlay)}.ng-cloak,.ng-hide:not(.ng-hide-animate),.x-ng-cloak,[data-ng-cloak],[ng-cloak],[ng\:cloak],[x-ng-cloak]{display:none!important}ng\:form{display:block}.\!container{width:100%!important}.container{width:100%}@media (min-width:25rem){.\!container{max-width:25rem!important}.container{max-width:25rem}}@media (min-width:30rem){.\!container{max-width:30rem!important}.container{max-width:30rem}}@media (min-width:640px){.\!container{max-width:640px!important}.container{max-width:640px}}@media (min-width:768px){.\!container{max-width:768px!important}.container{max-width:768px}}@media (min-width:1024px){.\!container{max-width:1024px!important}.container{max-width:1024px}}@media (min-width:1280px){.\!container{max-width:1280px!important}.container{max-width:1280px}}@media (min-width:1536px){.\!container{max-width:1536px!important}.container{max-width:1536px}}#playerModal.ds-header{display:block}.ds-header{display:flex;align-items:center;justify-content:flex-start;padding-top:var(--space-sm);padding-bottom:var(--space-sm)}.app-shell__header{position:sticky;top:env(safe-area-inset-top,0);z-index:var(--z-app-header);padding-top:calc(var(--space-sm) + env(safe-area-inset-top, 0));padding-bottom:var(--space-sm);background-color:transparent;background-image:var(--surface-overlay-banner-gradient-header);background-repeat:no-repeat;background-size:100% 100%}.ds-metric-probe{position:absolute;tLine truncated
/*! tailwindcss v3.4.17 | MIT License | https://tailwindcss.com*/*,:after,:before{box-sizing:border-box;border:0 solid #e5e7eb}:after,:before{--tw-content:""}:host,html{line-height:1.5;-webkit-text-size-adjust:100%;-moz-tab-size:4;-o-tab-size:4;tab-size:4;font-family:var(--font-family-sans);font-feature-settings:normal;font-variation-settings:normal;-webkit-tap-highlight-color:transparent}body{line-height:inherit}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,pre,samp{font-family:var(--font-family-mono);font-feature-settings:normal;font-variation-settings:normal;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}button,input,optgroup,select,textarea{font-family:inherit;font-feature-settings:inherit;font-variation-settings:inherit;font-size:100%;font-weight:inherit;line-height:inherit;letter-spacing:inherit;color:inherit;margin:0;padding:0}button,select{text-transform:none}button,input:where([type=button]),input:where([type=reset]),input:where([type=submit]){-webkit-appearance:button;background-color:transparent;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:baseline}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dd,dl,figure,h1,h2,h3,h4,h5,h6,hr,p,pre{margin:0}fieldset{margin:0}fieldset,legend{padding:0}menu,ol,ul{list-style:none;margin:0;padding:0}dialog{padding:0}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{opacity:1;color:#9ca3af}input::placeholder,textarea::placeholder{opacity:1;color:#9ca3af}[role=button],button{cursor:pointer}:disabled{cursor:default}audio,canvas,embed,iframe,img,object,svg,video{display:block;vertical-align:middle}img,video{max-width:100%;height:auto}[hidden]:where(:not([hidden=until-found])){display:none}html{font-size:80%}@media (min-width:768px){html{font-size:100%}}@media (min-width:1024px){html{font-size:75%}}*,:after,:before{scrollbar-width:thin;scrollbar-color:var(--scrollbar-thumb) var(--scrollbar-track)}::-webkit-scrollbar{width:var(--scrollbar-width);height:var(--scrollbar-width)}::-webkit-scrollbar-track{background:var(--scrollbar-track)}::-webkit-scrollbar-thumb{background:var(--scrollbar-thumb);border-radius:var(--radius-sm)}::-webkit-scrollbar-thumb:hover{background:var(--scrollbar-thumb-hover)}body{--app-header-centerline:calc(env(safe-area-inset-top, 0) + var(--app-header-centerline-offset));font-family:var(--font-family-sans);background-color:var(--color-page);color:var(--color-text-strong);line-height:1.5;margin:0;padding:0;overflow-x:hidden}#uiOverlay{position:fixed;inset:0;pointer-events:none;z-index:var(--z-overlay-popover)}#uiOverlay>*{pointer-events:auto}#editNip71TagsFieldset>legend,#nip71TagsFieldset>legend{margin-left:0;padding:0}#app{background-color:var(--color-page);color:inherit}body.modal-open,html.modal-open{overflow:hidden;height:100%}body.modal-open #app{overflow:hidden}body.modal-open #sidebar{pointer-events:none;opacity:0;visibility:hidden;z-index:var(--z-sidebar-overlay)}.ng-cloak,.ng-hide:not(.ng-hide-animate),.x-ng-cloak,[data-ng-cloak],[ng-cloak],[ng\:cloak],[x-ng-cloak]{display:none!important}ng\:form{display:block}.\!container{width:100%!important}.container{width:100%}@media (min-width:25rem){.\!container{max-width:25rem!important}.container{max-width:25rem}}@media (min-width:30rem){.\!container{max-width:30rem!important}.container{max-width:30rem}}@media (min-width:640px){.\!container{max-width:640px!important}.container{max-width:640px}}@media (min-width:768px){.\!container{max-width:768px!important}.container{max-width:768px}}@media (min-width:1024px){.\!container{max-width:1024px!important}.container{max-width:1024px}}@media (min-width:1280px){.\!container{max-width:1280px!important}.container{max-width:1280px}}@media (min-width:1536px){.\!container{max-width:1536px!important}.container{max-width:1536px}}#playerModal.ds-header{display:block}.ds-header{display:flex;align-items:center;justify-content:flex-start;padding-top:var(--space-sm);padding-bottom:var(--space-sm)}.app-shell__header{position:sticky;top:env(safe-area-inset-top,0);z-index:var(--z-app-header);padding-top:calc(var(--space-sm) + env(safe-area-inset-top, 0));padding-bottom:var(--space-sm);background-color:transparent;background-image:var(--surface-overlay-banner-gradient-header);background-repeat:no-repeat;background-size:100% 100%}.ds-metric-probe{position:absolute;tLine truncated
--player-modal-inline-padding,clamp(var(--space-md),4vw,var(--space-xl-plus))
);overflow:visible}#playerModal .watch-container.video-modal__similar{padding-inline:clamp(var(--space-3xs),.6vw,var(--space-2xs))}#playerModal .video-modal__primary>.watch-container:first-child{padding-block-start:var(--player-modal-vertical-padding)}#playerModal .video-modal__secondary>.watch-container:last-child{padding-block-end:var(--player-modal-vertical-padding)}#playerModal .modal-nav .watch-container{max-width:none;margin-inline:0}#playerModal .modal-info-panel{display:flex;flex-direction:column;gap:var(--space-md-plus)}#playerModal h2#videoTitle{margin:0;font-size:clamp(var(--font-size-xl),calc(var(--font-size-modal-title-base) + .5vw),var(--font-size-xl-plus));font-weight:600;line-height:1.3;color:var(--color-text)}#playerModal .modal-meta{font-size:var(--font-size-sm);line-height:1.4;color:var(--color-text-muted)}#playerModal .modal-view-count{font-weight:600;color:var(--color-text)}#playerModal .channel-card{align-items:center;gap:var(--space-md);padding:var(--space-md) var(--space-lg)}#playerModal .channel-card,#playerModal .player-modal__module{display:flex;border-radius:var(--radius-xl);border:var(--border-width-hairline) solid var(--color-border-translucent);background-color:var(--color-surface);box-shadow:var(--shadow-sm)}#playerModal .player-modal__module{flex-direction:column;gap:clamp(var(--space-2xs),1vw,var(--space-sm));width:100%;padding:clamp(var(--space-xs),1vw,var(--space-sm))}#playerModal .player-modal__module-title{margin:0;font-size:var(--font-size-lg);font-weight:600;line-height:1.3;color:var(--color-text)}#playerModal .player-modal__module-list{display:flex;flex-direction:column;gap:var(--space-sm);width:100%;margin:0;padding:0;list-style:none}#playerModal .video-modal__similar .player-modal__module{gap:clamp(var(--space-3xs),.6vw,var(--space-xs));padding:clamp(var(--space-2xs),.6vw,var(--space-xs));border-width:0;box-shadow:none;background-color:transparent}#playerModal .video-modal__similar .player-modal__module-title{padding-inline:clamp(var(--space-3xs),.6vw,var(--space-2xs))}#playerModal .video-modal__similar .player-modal__module-list{gap:clamp(var(--space-3xs),.6vw,var(--space-xs));padding-inline:clamp(var(--space-3xs),.6vw,var(--space-2xs));padding-bottom:clamp(var(--space-3xs),.6vw,var(--space-2xs))}#playerModal .player-modal__module-scroll{max-height:clamp(14rem,calc(100vh - var(--player-modal-vertical-padding)*2 - var(--space-2xl)),28rem);overflow-y:auto;overflow-x:hidden;overscroll-behavior:contain;scrollbar-gutter:stable;padding-inline-end:var(--space-xs)}#playerModal .video-modal__similar .player-modal__module-scroll{max-height:none;overflow:visible;padding-inline-end:0;scrollbar-gutter:auto}#playerModal .player-modal__module-scroll>li{scroll-margin-block:var(--space-xs)}@media (max-width:30rem){#playerModal .modal-nav .icon-button{width:var(--space-xl);height:var(--space-xl);min-width:var(--space-xl);min-height:var(--space-xl)}#playerModal .watch-container{padding-inline:var(--space-sm)}#playerModal .channel-card{gap:var(--space-sm);padding:var(--space-sm) var(--space-md)}}#playerModal .channel-card__avatar{flex-shrink:0;width:var(--avatar-size-lg);height:var(--avatar-size-lg);border-radius:var(--radius-full);overflow:hidden;background-color:var(--color-border-subtle)}#playerModal .channel-card__avatar img{display:block;width:100%;height:100%;-o-object-fit:cover;object-fit:cover}#playerModal .channel-card__name{margin:0;font-size:var(--font-size-lg);font-weight:600;color:var(--color-text)}#playerModal .channel-card__npub{margin:0;font-size:var(--font-size-sm);color:var(--color-text-muted)}#playerModal .description-panel{padding:var(--space-md-plus) var(--space-lg);border-radius:var(--radius-xl);border:var(--border-width-hairline) solid var(--color-border-translucent);background-color:var(--color-surface);box-shadow:var(--shadow-sm)}#playerModal .description-panel p{margin:0;font-size:var(--font-size-sm);line-height:1.6;color:var(--color-text)}.player-modal__content{position:relative;z-index:var(--z-modal-content);display:flex;flex:1 1 auto;width:100%;min-height:100%;flex-direction:column;overflow:visible;padding:0;margin:0;border:none;border-radius:0;background-color:var(--color-surface);box-shadow:none}@media (prefers-reduced-motion:reduce){.player-modal__content{transition-property:none}}#playerModal .modal-nav{position:sticky;top:0;z-index:var(--z-overlay-nav);--tw-translate-y:0px;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));transition-property:transform;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s;background-color:var(--surface-overlay-panel);background-image:linear-gradient(180deg,color-mix(in srgb,var(--surface-overlay-panel) 94%,transparent) 0,color-mix(in srgb,var(--surface-overlay-panel) 88%,transparent) 100%);border-bottom:var(--border-width-hairline) solid var(--border-oveLine truncated
--player-modal-primary-inline-padding-lg,clamp(var(--space-sm),2.75vw,var(--space-lg))
)}}.revert-modal__layout{display:grid;gap:1.5rem}@media (min-width:1024px){.revert-modal__layout{grid-template-columns:minmax(var(--revert-modal-sidebar-min),var(--revert-modal-sidebar-width)) minmax(0,1fr)}}#playerModal.video-modal__primary{display:block}.video-modal__primary{display:flex;flex-direction:column;gap:1.5rem;min-width:0;min-height:0}#playerModal.video-modal__secondary{display:block}.video-modal__secondary{display:flex;flex-direction:column;gap:1rem;min-width:0;min-height:0;width:100%;align-self:stretch}.video-modal__similar{display:none}@media (min-width:1024px){#playerModal.video-modal__similar{display:block}.video-modal__similar{display:flex;flex-direction:column;gap:1.5rem;align-self:stretch;flex:1 1 auto;min-height:0}#playerModal .video-modal__similar .player-modal__module{height:100%;display:flex;flex:1 1 auto;min-height:0;flex-direction:column}#playerModal .video-modal__similar .player-modal__module-list{flex:1 1 auto;min-height:0}#playerModal .video-modal__similar .player-modal__module-scroll{height:100%;max-height:none;min-height:0}}#playerModal .player-modal__module-item{width:100%}#playerModal#playerModal .player-modal__module-item>.card:not(.player-modal__similar-card){display:block}#playerModal .player-modal__module-item>.card:not(.player-modal__similar-card){display:flex;width:100%;flex-direction:row;flex-wrap:nowrap;align-items:stretch;gap:var(--space-md)}#playerModal#playerModal .player-modal__module-item>.\!card:not(.player-modal__similar-card){display:block}#playerModal .player-modal__module-item>.\!card:not(.player-modal__similar-card){display:flex;width:100%;flex-direction:row;flex-wrap:nowrap;align-items:stretch;gap:var(--space-md)}#playerModal .player-modal__module-item>.card:not(.player-modal__similar-card) .video-card__media{width:clamp(5.75rem,18vw,8rem)}#playerModal .player-modal__module-item>.\!card:not(.player-modal__similar-card) .video-card__media{width:clamp(5.75rem,18vw,8rem)!important}#playerModal .player-modal__module-item>.\!card:not(.player-modal__similar-card) .video-card__media,#playerModal .player-modal__module-item>.card:not(.player-modal__similar-card) .video-card__media{flex-shrink:0;overflow:hidden;border-radius:var(--radius-lg)}#playerModal .player-modal__module-item>.card:not(.player-modal__similar-card) .video-card__content{padding:clamp(var(--space-sm),1.25vw,var(--space-md))}#playerModal .player-modal__module-item>.\!card:not(.player-modal__similar-card) .video-card__content{padding:clamp(var(--space-sm),1.25vw,var(--space-md))!important}#playerModal#playerModal .player-modal__module-item>.card:not(.player-modal__similar-card) .video-card__content{display:block}#playerModal .player-modal__module-item>.card:not(.player-modal__similar-card) .video-card__content{display:flex;min-width:0;flex:1 1 0%;flex-direction:column;gap:var(--space-sm)}#playerModal#playerModal .player-modal__module-item>.\!card:not(.player-modal__similar-card) .video-card__content{display:block}#playerModal .player-modal__module-item>.\!card:not(.player-modal__similar-card) .video-card__content{display:flex;min-width:0;flex:1 1 0%;flex-direction:column;gap:var(--space-sm)}#playerModal#playerModal .player-modal__module-item>.card:not(.player-modal__similar-card) .video-card__meta{display:block}#playerModal .player-modal__module-item>.card:not(.player-modal__similar-card) .video-card__meta{display:flex;flex-wrap:wrap;align-items:center;gap:.5rem;font-size:var(--font-size-xs);line-height:var(--line-height-tight);color:var(--text-muted-strong)}#playerModal#playerModal .player-modal__module-item>.\!card:not(.player-modal__similar-card) .video-card__meta{display:block}#playerModal .player-modal__module-item>.\!card:not(.player-modal__similar-card) .video-card__meta{display:flex;flex-wrap:wrap;align-items:center;gap:.5rem;font-size:var(--font-size-xs);line-height:var(--line-height-tight);color:var(--text-muted-strong)}#playerModal .video-modal__similar .player-modal__module-list{gap:clamp(var(--space-2xs),.85vw,var(--space-sm))}.player-modal__similar-card{--player-modal-similar-thumb:clamp(11rem,calc(var(--video-modal-secondary-min, 320px)*0.62),17rem);--player-modal-similar-gap:clamp(var(--space-4xs),0.65vw,var(--space-2xs));--player-modal-similar-padding:clamp(var(--space-4xs),0.7vw,var(--space-sm));position:relative;display:grid;grid-template-columns:var(--player-modal-similar-thumb) minmax(0,1fr);align-items:stretch;gap:var(--player-modal-similar-gap);padding:var(--player-modal-similar-padding);width:100%;min-width:0;transform:translate3d(0,var(--video-card-lift-translate-y,0),0)}.player-modal__similar-card:before{content:"";position:absolute;inset:0;z-index:0;opacity:var(--video-card-backdrop-opacity,0);background-image:var(
--similar-card-thumb-url,var(--video-card-thumb-url,none)
);background-position:50%;background-repeat:no-repeat;background-size:cover;transform:translate3d(0,var(--video-card-backdrop-translate-y,0),0) scale(var(--video-card-backdrop-scale,1));filter:blur(var(--video-card-backdrop-blur,0));transition:opacity var(--motion-duration-base) var(--motion-ease-in-out),transform var(--motion-duration-base) var(--motion-ease-in-out),filter var(--motion-duration-base) var(--motion-ease-in-out);pointer-events:none;will-change:opacity,transform,filter}.player-modal__similar-card>*{position:relative;z-index:1}.player-modal__similar-card:focus-within,.player-modal__similar-card:hover{--video-card-backdrop-opacity:0.425;--video-card-backdrop-translate-y:0.15rem;--video-card-backdrop-scale:1.12;--video-card-backdrop-blur:24px}.player-modal__similar-card-media{display:block;position:relative;inline-size:var(--player-modal-similar-thumb);block-size:100%;max-inline-size:clamp(calc(var(--space-2xl-plus)*1.1),28vw,calc(var(--space-2xl-plus)*2.1));border-radius:var(--radius-lg);overflow:hidden;isolation:isolate}.player-modal__similar-card-media img{display:block;width:100%;height:100%;-o-object-fit:cover;object-fit:cover;border-radius:inherit}.player-modal__similar-card-content{display:flex;flex-direction:column;gap:clamp(var(--space-4xs),.6vw,var(--space-2xs));min-width:0}.player-modal__similar-card-title{display:-webkit-box;margin:0;font-size:clamp(var(--font-size-sm),1.05vw,var(--font-size-base));font-weight:600;line-height:1.35;color:var(--color-text);text-decoration:none;overflow:hidden;-webkit-line-clamp:2;-webkit-box-orient:vertical;transition:color var(--motion-duration-base) var(--motion-ease-in-out)}.player-modal__similar-card-title:focus-visible,.player-modal__similar-card-title:hover{color:var(--color-accent)}.player-modal__similar-card-author{display:flex;align-items:center;-moz-column-gap:clamp(var(--space-5xs),.6vw,var(--space-3xs));column-gap:clamp(var(--space-5xs),.6vw,var(--space-3xs));font-size:var(--font-size-xs);color:var(--color-text-muted);min-width:0}.player-modal__similar-card-avatar{flex:0 0 auto;display:block;inline-size:clamp(2.5rem,.8vw + 2.1rem,3rem);block-size:clamp(2.5rem,.8vw + 2.1rem,3rem);border-radius:var(--radius-full);overflow:hidden;background-color:var(--color-surface-alt);box-shadow:0 0 0 1px color-mix(in srgb,var(--color-border-strong) 65%,transparent)}.player-modal__similar-card-avatar-img{display:block;width:100%;height:100%;-o-object-fit:cover;object-fit:cover}.player-modal__similar-card-author-meta{display:flex;flex-direction:column;align-items:flex-start;gap:clamp(var(--space-6xs),.35vw,var(--space-5xs));min-width:0}.player-modal__similar-card-author-name{font-weight:500;color:var(--color-text);min-width:0}.player-modal__similar-card-author-npub{display:inline-flex;padding-inline:var(--space-6xs);border-radius:var(--radius-full);font-family:var(--font-family-mono);line-height:1.2;background-color:color-mix(in srgb,var(--color-surface-alt) 80%,transparent)}.player-modal__similar-card-author-npub,.player-modal__similar-card-meta{align-items:center;font-size:var(--font-size-2xs,.75rem);color:var(--color-muted-strong)}.player-modal__similar-card-meta{display:flex;flex-wrap:wrap;gap:clamp(var(--space-5xs),.5vw,var(--space-4xs));min-width:0}.player-modal__similar-card-timestamp{white-space:nowrap}.player-modal__similar-card-separator{color:inherit;opacity:.8}.player-modal__similar-card-views{white-space:nowrap}.video-modal__stage{position:relative;isolation:isolate;overflow:visible;--video-modal-ambient-bleed:clamp(var(--space-2xl),12vw,var(--space-5xl));--video-modal-ambient-scale:1.38;--video-modal-ambient-blur:clamp(128px,20vw,256px);--video-modal-ambient-opacity:1}.video-modal__video{position:relative;z-index:1;border-radius:var(--radius-xl);padding-block:clamp(var(--space-xs),2.5vw,var(--space-md-plus));background-color:color-mix(in srgb,var(--color-surface) 82%,transparent);box-shadow:0 0 0 1px color-mix(in srgb,var(--color-border-strong) 8%,transparent) inset;overflow:visible}.video-modal__ambient{position:absolute;inset:calc(var(--video-modal-ambient-bleed)*-1);border-radius:calc(var(--radius-3xl) + var(--video-modal-ambient-bleed));pointer-events:none;z-index:0;display:block;overflow:visible}.video-modal__ambient:before{content:"";position:absolute;inset:0;border-radius:inherit;background:radial-gradient(circle at 50% 50%,color-mix(in srgb,var(--color-overlay-strong) 18%,transparent) 0,color-mix(in srgb,var(--color-overlay) 34%,transparent) 62%,transparent 100%);opacity:.9;mix-blend-mode:screen;pointer-events:none}.video-modal__ambient-canvas{position:relative;width:100%;height:100%;display:block;transform:scale(var(--video-modal-ambient-scale));filter:blur(var(--video-modal-ambient-blur)) saturate(2) brightness(1.24);opacity:var(--video-modal-ambient-opacity)}@media (min-width:1024px){.video-modal__video{position:sticky;top:calc(var(--space-2xl) + var(--space-xl))}}#playerModal .video-modal__video video{position:relative;z-index:1Line truncated
);background-position:50%;background-repeat:no-repeat;background-size:cover;transform:translate3d(0,var(--video-card-backdrop-translate-y,0),0) scale(var(--video-card-backdrop-scale,1));filter:blur(var(--video-card-backdrop-blur,0));transition:opacity var(--motion-duration-base) var(--motion-ease-in-out),transform var(--motion-duration-base) var(--motion-ease-in-out),filter var(--motion-duration-base) var(--motion-ease-in-out);pointer-events:none;will-change:opacity,transform,filter}.player-modal__similar-card>*{position:relative;z-index:1}.player-modal__similar-card:focus-within,.player-modal__similar-card:hover{--video-card-backdrop-opacity:0.425;--video-card-backdrop-translate-y:0.15rem;--video-card-backdrop-scale:1.12;--video-card-backdrop-blur:24px}.player-modal__similar-card-media{display:block;position:relative;inline-size:var(--player-modal-similar-thumb);block-size:100%;max-inline-size:clamp(calc(var(--space-2xl-plus)*1.1),28vw,calc(var(--space-2xl-plus)*2.1));border-radius:var(--radius-lg);overflow:hidden;isolation:isolate}.player-modal__similar-card-media img{display:block;width:100%;height:100%;-o-object-fit:cover;object-fit:cover;border-radius:inherit}.player-modal__similar-card-content{display:flex;flex-direction:column;gap:clamp(var(--space-4xs),.6vw,var(--space-2xs));min-width:0}.player-modal__similar-card-title{display:-webkit-box;margin:0;font-size:clamp(var(--font-size-sm),1.05vw,var(--font-size-base));font-weight:600;line-height:1.35;color:var(--color-text);text-decoration:none;overflow:hidden;-webkit-line-clamp:2;-webkit-box-orient:vertical;transition:color var(--motion-duration-base) var(--motion-ease-in-out)}.player-modal__similar-card-title:focus-visible,.player-modal__similar-card-title:hover{color:var(--color-accent)}.player-modal__similar-card-author{display:flex;align-items:center;-moz-column-gap:clamp(var(--space-5xs),.6vw,var(--space-3xs));column-gap:clamp(var(--space-5xs),.6vw,var(--space-3xs));font-size:var(--font-size-xs);color:var(--color-text-muted);min-width:0}.player-modal__similar-card-avatar{flex:0 0 auto;display:block;inline-size:clamp(2.5rem,.8vw + 2.1rem,3rem);block-size:clamp(2.5rem,.8vw + 2.1rem,3rem);border-radius:var(--radius-full);overflow:hidden;background-color:var(--color-surface-alt);box-shadow:0 0 0 1px color-mix(in srgb,var(--color-border-strong) 65%,transparent)}.player-modal__similar-card-avatar-img{display:block;width:100%;height:100%;-o-object-fit:cover;object-fit:cover}.player-modal__similar-card-author-meta{display:flex;flex-direction:column;align-items:flex-start;gap:clamp(var(--space-6xs),.35vw,var(--space-5xs));min-width:0}.player-modal__similar-card-author-name{font-weight:500;color:var(--color-text);min-width:0}.player-modal__similar-card-author-npub{display:inline-flex;padding-inline:var(--space-6xs);border-radius:var(--radius-full);font-family:var(--font-family-mono);line-height:1.2;background-color:color-mix(in srgb,var(--color-surface-alt) 80%,transparent)}.player-modal__similar-card-author-npub,.player-modal__similar-card-meta{align-items:center;font-size:var(--font-size-2xs,.75rem);color:var(--color-muted-strong)}.player-modal__similar-card-meta{display:flex;flex-wrap:wrap;gap:clamp(var(--space-5xs),.5vw,var(--space-4xs));min-width:0}.player-modal__similar-card-timestamp{white-space:nowrap}.player-modal__similar-card-separator{color:inherit;opacity:.8}.player-modal__similar-card-views{white-space:nowrap}.video-modal__stage{position:relative;isolation:isolate;overflow:visible;--video-modal-ambient-bleed:clamp(var(--space-2xl),12vw,var(--space-5xl));--video-modal-ambient-scale:1.38;--video-modal-ambient-blur:clamp(128px,20vw,256px);--video-modal-ambient-opacity:1}.video-modal__video{position:relative;z-index:1;border-radius:var(--radius-xl);padding-block:clamp(var(--space-xs),2.5vw,var(--space-md-plus));background-color:color-mix(in srgb,var(--color-surface) 82%,transparent);box-shadow:0 0 0 1px color-mix(in srgb,var(--color-border-strong) 8%,transparent) inset;overflow:visible}.video-modal__ambient{position:absolute;inset:calc(var(--video-modal-ambient-bleed)*-1);border-radius:calc(var(--radius-3xl) + var(--video-modal-ambient-bleed));pointer-events:none;z-index:0;display:block;overflow:visible}.video-modal__ambient:before{content:"";position:absolute;inset:0;border-radius:inherit;background:radial-gradient(circle at 50% 50%,color-mix(in srgb,var(--color-overlay-strong) 18%,transparent) 0,color-mix(in srgb,var(--color-overlay) 34%,transparent) 62%,transparent 100%);opacity:.9;mix-blend-mode:screen;pointer-events:none}.video-modal__ambient-canvas{position:relative;width:100%;height:100%;display:block;transform:scale(var(--video-modal-ambient-scale));filter:blur(var(--video-modal-ambient-blur)) saturate(2) brightness(1.24);opacity:var(--video-modal-ambient-opacity)}@media (min-width:1024px){.video-modal__video{position:sticky;top:calc(var(--space-2xl) + var(--space-xl))}}#playerModal .video-modal__video video{position:relative;z-index:1Line truncated
+71 -1
View File
@@ -798,6 +798,74 @@
@apply border-status-neutral-border bg-status-neutral-surface text-status-neutral-on;
}
.moderation-badge {
@apply inline-flex items-center gap-2 rounded-xl border px-3 py-1.5 text-xs font-semibold normal-case tracking-tight shadow-sm;
}
.moderation-badge[data-variant="warning"] {
@apply border-status-warning-border bg-status-warning-surface text-status-warning-on;
box-shadow: 0 12px 30px color-mix(in srgb, var(--status-warning) 18%, transparent);
}
.moderation-badge[data-variant="neutral"] {
@apply border-status-neutral-border bg-status-neutral-surface text-status-neutral-on;
box-shadow: 0 12px 30px color-mix(in srgb, var(--status-neutral) 18%, transparent);
}
.moderation-badge__icon {
@apply inline-flex h-5 w-5 items-center justify-center rounded-full;
background-color: color-mix(in srgb, var(--status-warning) 18%, transparent);
color: var(--status-warning-on);
}
.moderation-badge[data-variant="neutral"] .moderation-badge__icon {
background-color: color-mix(in srgb, var(--status-neutral) 18%, transparent);
color: var(--status-neutral-on);
}
.moderation-badge__icon-mark {
@apply h-3.5 w-3.5;
}
.moderation-badge__text {
@apply text-xs font-semibold tracking-tight;
}
.moderation-badge__action {
@apply inline-flex items-center gap-1.5 rounded-full border px-3 py-1 text-xs font-semibold normal-case tracking-tight transition duration-150 ease-out;
background-color: color-mix(in srgb, var(--status-warning) 16%, transparent);
border-color: color-mix(in srgb, var(--status-warning-border) 80%, transparent);
color: var(--status-warning-on);
}
.moderation-badge[data-variant="warning"] .moderation-badge__action:hover {
background-color: color-mix(in srgb, var(--status-warning) 24%, transparent);
}
.moderation-badge[data-variant="neutral"] .moderation-badge__action {
background-color: color-mix(in srgb, var(--status-neutral) 16%, transparent);
border-color: color-mix(in srgb, var(--status-neutral-border) 80%, transparent);
color: var(--status-neutral-on);
}
.moderation-badge[data-variant="neutral"] .moderation-badge__action:hover {
background-color: color-mix(in srgb, var(--status-neutral) 24%, transparent);
}
.moderation-badge__action:focus-visible {
outline: 2px solid color-mix(in srgb, var(--status-warning) 38%, transparent);
outline-offset: 2px;
}
.moderation-badge[data-variant="neutral"] .moderation-badge__action:focus-visible {
outline: 2px solid color-mix(in srgb, var(--status-neutral) 38%, transparent);
}
.moderation-badge__action[disabled] {
cursor: wait;
opacity: 0.7;
}
.pill {
@apply inline-flex items-center gap-2 rounded-full border border-status-neutral-border bg-status-neutral-surface px-3 py-1 text-xs font-medium text-status-neutral-on;
}
@@ -1362,7 +1430,9 @@
@apply border-opacity-60 shadow-lg;
}
.card img[data-thumbnail-state="blurred"] {
.card img[data-thumbnail-state="blurred"],
.card img[data-visual-state="blurred"],
.channel-profile-container img[data-visual-state="blurred"] {
@apply transition duration-base ease-in-out;
filter: blur(var(--blur-thumbnail-blur));
transform: scale(1.05);
+9
View File
@@ -27,6 +27,15 @@ bitvid is follow-centric. Your Home feed comes from people you follow (F1). Disc
- [`bitvidApp.decorateVideoModeration`](../../js/app.js) connects stage output to the UI layer alongside feature-flag plumbing.
- [`VideoCard.refreshModerationUi`](../../js/ui/components/VideoCard.js) applies badges, blur states, hide metadata (`data-moderation-hidden`), and the "show anyway" toggles.
### Moderation badge pipeline
- Every moderation payload now includes a `blurReason` that records why a thumbnail was blurred (`trusted-report`, `trusted-mute`, or a hide reason such as `trusted-mute-hide`). This travels with both the metadata stage output and the decorated `video.moderation.original` snapshot so UI can explain the decision even after a viewer override.
- When a trusted mute forces a blur (without a report threshold firing) the badge copy shortens to **"Muted by a trusted contact"** so we don't repeat the blur state in the label. Reports still surface as "Blurred · {reason}" until the viewer taps "Show anyway".
- The moderation badge now renders as a dedicated yellow chip with an icon and "Show anyway" pill that matches the rest of the design system tokens. Override states flip to the neutral palette and swap the icon for a confirmation check so operators can tell the difference at a glance.
- The badge tooltip and `aria-label` continue to list the specific trusted contacts who muted or reported the content when that metadata is available.
- Viewers who override a trusted mute hide keep a **Hide** pill on the same badge so they can re-hide the clip without hunting through another menu.
- Trusted mute decisions now block autoplay even when report thresholds have not fired, and any associated avatars or channel banners adopt the blurred state alongside the primary thumbnail so the entire card reflects the moderation choice.
## Where to extend
Thread new moderation behaviors through the same service → stage → app → UI flow above. Extending the existing layers keeps overrides, feature flags, and QA hooks consistent—avoid spinning up parallel moderation modules unless the architecture document explicitly calls for it.
+5 -1
View File
@@ -402,7 +402,11 @@
}
const computedBlock = initialBlock && trustedCount >= autoplayThreshold;
const computedBlur = initialBlur && trustedCount >= blurThreshold;
let computedBlur = initialBlur && trustedCount >= blurThreshold;
if (!computedBlur && (trustedMuted || hidden)) {
computedBlur = true;
}
const originalState = {
blockAutoplay: computedBlock,
+91 -3
View File
@@ -144,6 +144,7 @@ import {
getInFlightUrlProbe,
getModerationOverride,
setModerationOverride,
clearModerationOverride,
loadModerationOverridesFromStorage,
getModerationSettings,
getDefaultModerationSettings,
@@ -1350,6 +1351,9 @@ class Application {
this.videoListView.setModerationOverrideHandler((detail = {}) =>
this.handleModerationOverride(detail)
);
this.videoListView.setModerationHideHandler((detail = {}) =>
this.handleModerationHide(detail)
);
if (this.moreMenuController) {
this.moreMenuController.attachVideoListView(this.videoListView);
@@ -6258,7 +6262,9 @@ class Application {
autoplayModalVideo() {
if (this.currentVideo?.moderation?.blockAutoplay) {
this.log("[moderation] Skipping autoplay due to trusted reports.");
this.log(
"[moderation] Skipping autoplay due to trusted reports or trusted mutes.",
);
return;
}
if (!this.modalVideo) return;
@@ -7226,8 +7232,11 @@ class Application {
: this.deriveModerationTrustedCount(summary, reportType);
const thresholds = this.getActiveModerationThresholds();
const computedBlockAutoplay = trustedCount >= thresholds.autoplayBlockThreshold;
const computedBlurThumbnail = trustedCount >= thresholds.blurThreshold;
const computedBlockAutoplay =
trustedCount >= thresholds.autoplayBlockThreshold || trustedMuted;
const blurFromReports = trustedCount >= thresholds.blurThreshold;
let computedBlurThumbnail = blurFromReports;
let computedBlurReason = computedBlurThumbnail ? "trusted-report" : "";
const muteHideThreshold = Number.isFinite(thresholds.trustedMuteHideThreshold)
? Math.max(0, Math.floor(thresholds.trustedMuteHideThreshold))
@@ -7263,6 +7272,23 @@ class Application {
hideTriggered = true;
}
if (!computedBlurThumbnail && (trustedMuted || hideTriggered)) {
computedBlurThumbnail = true;
if (hideTriggered) {
computedBlurReason = hideReason || "trusted-hide";
} else if (trustedMuted) {
computedBlurReason = "trusted-mute";
}
} else if (computedBlurThumbnail) {
if (hideTriggered) {
computedBlurReason = hideReason || "trusted-hide";
} else if (trustedMuted && !blurFromReports) {
computedBlurReason = "trusted-mute";
} else if (!computedBlurReason && blurFromReports) {
computedBlurReason = "trusted-report";
}
}
const hideCounts = hideTriggered
? {
trustedMuteCount,
@@ -7294,6 +7320,7 @@ class Application {
hideCounts: originalHideCounts,
hideBypass,
hideTriggered,
blurReason: computedBlurThumbnail ? computedBlurReason : "",
};
const decoratedModeration = {
@@ -7307,6 +7334,7 @@ class Application {
trustedMuters: rawTrustedMuters,
trustedMuteCount,
trustedMuterDisplayNames,
blurReason: computedBlurThumbnail ? computedBlurReason : "",
original: {
blockAutoplay: originalState.blockAutoplay,
blurThumbnail: originalState.blurThumbnail,
@@ -7315,9 +7343,14 @@ class Application {
hideCounts: originalState.hideCounts,
hideBypass: originalState.hideBypass,
hideTriggered: originalState.hideTriggered,
blurReason: originalState.blurReason,
},
};
if (!computedBlurThumbnail && decoratedModeration.blurReason) {
delete decoratedModeration.blurReason;
}
if (overrideActive) {
decoratedModeration.blockAutoplay = false;
decoratedModeration.blurThumbnail = false;
@@ -7428,6 +7461,61 @@ class Application {
return true;
}
handleModerationHide({ video, card }) {
if (!video || typeof video !== "object" || !video.id) {
return false;
}
try {
clearModerationOverride(video.id);
} catch (error) {
devLogger.warn("[Application] Failed to clear moderation override:", error);
}
const storedVideo =
this.videosMap instanceof Map && video.id ? this.videosMap.get(video.id) : null;
const target = storedVideo || video;
const resetModerationState = (subject) => {
if (!subject || typeof subject !== "object") {
return;
}
const moderation =
subject.moderation && typeof subject.moderation === "object"
? subject.moderation
: null;
if (!moderation) {
return;
}
if (moderation.viewerOverride) {
delete moderation.viewerOverride;
}
if (moderation.hideBypass) {
delete moderation.hideBypass;
}
};
if (target) {
resetModerationState(target);
this.decorateVideoModeration(target);
}
if (this.currentVideo && this.currentVideo.id === video.id) {
resetModerationState(this.currentVideo);
this.decorateVideoModeration(this.currentVideo);
}
if (card && typeof card.refreshModerationUi === "function") {
try {
card.refreshModerationUi();
} catch (error) {
devLogger.warn("[Application] Failed to refresh moderation UI after hide:", error);
}
}
return true;
}
getVideoAddressPointer(video) {
if (
this.discussionCountService &&
+95
View File
@@ -56,6 +56,82 @@ let currentChannelNpub = null;
let currentChannelLightningAddress = "";
let channelZapPendingOpen = false;
function normalizeHex(value) {
if (typeof value !== "string") {
return "";
}
const trimmed = value.trim().toLowerCase();
return /^[0-9a-f]{64}$/.test(trimmed) ? trimmed : "";
}
function shouldBlurChannelMedia(pubkey) {
const normalized = normalizeHex(pubkey);
if (!normalized) {
return false;
}
try {
if (moderationService.isAuthorMutedByViewer(normalized) === true) {
return true;
}
} catch (error) {
devLogger.warn("[ChannelProfile] Failed to resolve viewer mute state", error);
}
const app = getApp();
const videos =
app?.videosMap instanceof Map ? Array.from(app.videosMap.values()) : [];
for (const video of videos) {
if (!video || typeof video !== "object") {
continue;
}
const videoPubkey =
typeof video.pubkey === "string" ? video.pubkey.trim().toLowerCase() : "";
if (videoPubkey !== normalized) {
continue;
}
const moderation =
video.moderation && typeof video.moderation === "object"
? video.moderation
: null;
if (!moderation) {
continue;
}
if (moderation.hidden === true) {
return true;
}
if (moderation.blurThumbnail === true) {
const reason =
typeof moderation.blurReason === "string"
? moderation.blurReason.trim().toLowerCase()
: "";
if (reason.startsWith("trusted-")) {
return true;
}
}
}
return false;
}
function applyChannelVisualBlur({ bannerEl = null, avatarEl = null, pubkey = "" } = {}) {
const shouldBlur = shouldBlurChannelMedia(pubkey);
const applyState = (element) => {
if (!element) {
return;
}
if (shouldBlur) {
element.dataset.visualState = "blurred";
} else if (element.dataset.visualState) {
delete element.dataset.visualState;
}
};
applyState(bannerEl);
applyState(avatarEl);
}
const summarizeZapTracker = (tracker) =>
Array.isArray(tracker)
? tracker.map((entry) => ({
@@ -3283,6 +3359,22 @@ function renderChannelVideosFromList({
startPlayback(detail);
};
videoCard.onModerationOverride = ({ video: overrideVideo, card: overrideCard }) =>
typeof app?.handleModerationOverride === "function"
? app.handleModerationOverride({
video: overrideVideo,
card: overrideCard,
})
: false;
videoCard.onModerationHide = ({ video: hideVideo, card: hideCard }) =>
typeof app?.handleModerationHide === "function"
? app.handleModerationHide({
video: hideVideo,
card: hideCard,
})
: false;
videoCard.onEdit = ({ video: editVideo, index: editIndex }) => {
app?.handleEditVideo?.({
eventId: editVideo?.id || "",
@@ -3468,6 +3560,9 @@ function applyChannelProfileMetadata({
avatarEl.src = normalized.picture || FALLBACK_CHANNEL_AVATAR;
}
const blurPubkey = pubkey || currentChannelHex || "";
applyChannelVisualBlur({ bannerEl, avatarEl, pubkey: blurPubkey });
const nameEl = document.getElementById("channelName");
if (nameEl) {
nameEl.textContent = normalized.name || "Unknown User";
+35 -4
View File
@@ -583,8 +583,11 @@ export function createModerationStage({
trustedMuters = [];
}
const blockAutoplay = trustedCount >= normalizedAutoplayThreshold;
const blurThumbnail = trustedCount >= normalizedBlurThreshold;
const blockAutoplay =
trustedCount >= normalizedAutoplayThreshold || trustedMuted;
const blurFromReports = trustedCount >= normalizedBlurThreshold;
let blurThumbnail = blurFromReports;
let blurReason = blurThumbnail ? "trusted-report" : "";
const adminWhitelist = adminStatus?.whitelisted === true;
const adminWhitelistBypass = false;
@@ -598,7 +601,6 @@ export function createModerationStage({
: {};
metadataModeration.blockAutoplay = blockAutoplay;
metadataModeration.blurThumbnail = blurThumbnail;
metadataModeration.summary = summary;
metadataModeration.trustedCount = trustedCount;
metadataModeration.reportType = normalizedReportType;
@@ -616,7 +618,6 @@ export function createModerationStage({
}
video.moderation.blockAutoplay = blockAutoplay;
video.moderation.blurThumbnail = blurThumbnail;
video.moderation.trustedCount = trustedCount;
video.moderation.reportType = normalizedReportType;
video.moderation.adminWhitelist = adminWhitelist;
@@ -713,6 +714,36 @@ export function createModerationStage({
}
}
if (!blurThumbnail && (trustedMuted || hideTriggered)) {
blurThumbnail = true;
if (hideTriggered) {
blurReason = hideReason || "trusted-hide";
} else if (trustedMuted) {
blurReason = "trusted-mute";
}
} else if (blurThumbnail) {
if (hideTriggered) {
blurReason = hideReason || "trusted-hide";
} else if (trustedMuted && !blurFromReports) {
blurReason = "trusted-mute";
} else if (!blurReason && blurFromReports) {
blurReason = "trusted-report";
}
}
metadataModeration.blurThumbnail = blurThumbnail;
if (blurThumbnail) {
metadataModeration.blurReason = blurReason;
} else if (metadataModeration.blurReason) {
delete metadataModeration.blurReason;
}
video.moderation.blurThumbnail = blurThumbnail;
if (blurThumbnail) {
video.moderation.blurReason = blurReason;
} else if (video.moderation.blurReason) {
delete video.moderation.blurReason;
}
item.metadata.moderation = metadataModeration;
if (IS_DEV_MODE) {
+257 -28
View File
@@ -114,15 +114,20 @@ export class VideoCard {
onRequestSettingsMenu: null,
onCloseSettingsMenu: null,
onModerationOverride: null,
onModerationHide: null,
};
this.moderationBadgeEl = null;
this.moderationBadgeTextEl = null;
this.moderationBadgeIconWrapper = null;
this.moderationBadgeIconSvg = null;
this.moderationActionButton = null;
this.moderationHideButton = null;
this.moderationBadgeId = "";
this.badgesContainerEl = null;
this.hiddenSummaryEl = null;
this.boundShowAnywayHandler = (event) => this.handleShowAnywayClick(event);
this.boundModerationHideHandler = (event) => this.handleModerationHideClick(event);
this.root = null;
this.anchorEl = null;
@@ -198,6 +203,10 @@ export class VideoCard {
this.callbacks.onModerationOverride = typeof fn === "function" ? fn : null;
}
set onModerationHide(fn) {
this.callbacks.onModerationHide = typeof fn === "function" ? fn : null;
}
set onMoreAction(fn) {
this.callbacks.onMoreAction = typeof fn === "function" ? fn : null;
}
@@ -1074,6 +1083,11 @@ export class VideoCard {
const effectiveHideReason = hideReasonActive || originalHideReason;
const effectiveHideCounts = activeHideCounts || originalHideCounts;
const blurReason =
typeof moderation?.blurReason === "string" ? moderation.blurReason.trim() : "";
const originalBlurReason =
typeof original?.blurReason === "string" ? original.blurReason.trim() : "";
const context = {
reportType,
friendlyType: reportType ? reportType.replace(/[_-]+/g, " ").trim() : "",
@@ -1082,6 +1096,8 @@ export class VideoCard {
trustedMuted,
trustedMuteCount,
trustedMuteDisplayNames,
blurReason,
originalBlurReason,
originalBlur: original.blurThumbnail === true,
originalBlockAutoplay: original.blockAutoplay === true,
activeBlur: moderation?.blurThumbnail === true,
@@ -1222,16 +1238,18 @@ export class VideoCard {
return "Hidden";
}
const blurSource = (context.originalBlurReason || context.blurReason || "").toLowerCase();
const suppressBlurLabel =
blurSource === "trusted-mute" || blurSource === "trusted-mute-hide";
const parts = [];
if (context.originalBlur) {
if (context.originalBlur && !suppressBlurLabel) {
parts.push("Blurred");
}
if (context.originalBlockAutoplay) {
parts.push("Autoplay blocked");
}
if (context.trustedMuted && !context.originalBlur && !context.originalBlockAutoplay) {
parts.push(reason || "Muted by trusted contacts");
} else if (reason) {
if (reason) {
parts.push(reason);
}
@@ -1240,28 +1258,7 @@ export class VideoCard {
createModerationOverrideButton() {
const button = this.createElement("button", {
classNames: [
"inline-flex",
"items-center",
"rounded-full",
"border",
"border-status-warning-border",
"px-3",
"py-1",
"text-2xs",
"font-semibold",
"uppercase",
"tracking-extra-wide",
"text-status-warning-on",
"bg-transparent",
"hover:bg-status-warning-surface",
"transition",
"duration-150",
"focus-visible:outline",
"focus-visible:outline-2",
"focus-visible:outline-offset-2",
"focus-visible:outline-status-warning-border",
],
classNames: ["moderation-badge__action", "flex-shrink-0"],
attrs: {
type: "button",
"data-moderation-action": "override",
@@ -1274,6 +1271,80 @@ export class VideoCard {
return button;
}
createModerationHideButton() {
const button = this.createElement("button", {
classNames: ["moderation-badge__action", "flex-shrink-0"],
attrs: {
type: "button",
"data-moderation-action": "hide",
"aria-describedby": this.getModerationBadgeId(),
},
textContent: "Hide",
});
button.addEventListener("click", this.boundModerationHideHandler);
return button;
}
handleModerationHideClick(event) {
if (event) {
if (typeof event.preventDefault === "function") {
event.preventDefault();
}
if (typeof event.stopPropagation === "function") {
event.stopPropagation();
}
}
const button = this.moderationHideButton;
if (button) {
button.disabled = true;
button.setAttribute("aria-busy", "true");
}
if (!this.callbacks.onModerationHide) {
if (button) {
button.disabled = false;
button.removeAttribute("aria-busy");
}
return;
}
let result;
try {
result = this.callbacks.onModerationHide({
event,
video: this.video,
card: this,
});
} catch (error) {
userLogger.warn("[VideoCard] onModerationHide callback threw", error);
if (button) {
button.disabled = false;
button.removeAttribute("aria-busy");
}
return;
}
Promise.resolve(result)
.then((handled) => {
if (handled === false) {
if (button) {
button.disabled = false;
button.removeAttribute("aria-busy");
}
return;
}
this.refreshModerationUi();
})
.catch((error) => {
userLogger.warn("[VideoCard] Moderation hide failed", error);
if (button) {
button.disabled = false;
button.removeAttribute("aria-busy");
}
});
}
handleShowAnywayClick(event) {
if (event) {
if (typeof event.preventDefault === "function") {
@@ -1334,10 +1405,87 @@ export class VideoCard {
});
}
getModerationBadgeIconShape(state) {
if (state === "override") {
return {
d: "M10 18a8 8 0 100-16 8 8 0 000 16zm3.78-9.72a.75.75 0 00-1.06-1.06L9 11.94l-1.72-1.72a.75.75 0 10-1.06 1.06l2.25 2.25a.75.75 0 001.06 0l3.25-3.25z",
fillRule: "evenodd",
clipRule: "evenodd",
};
}
return {
d: "M10 18a8 8 0 100-16 8 8 0 000 16zm-.75-11.75a.75.75 0 011.5 0v4.5a.75.75 0 01-1.5 0v-4.5zm.75 8.5a1 1 0 100-2 1 1 0 000 2z",
fillRule: "evenodd",
clipRule: "evenodd",
};
}
updateModerationBadgeIcon(state) {
if (!this.moderationBadgeIconSvg) {
return;
}
this.moderationBadgeIconSvg.dataset.iconState = state;
const path = this.moderationBadgeIconSvg.firstElementChild;
if (!path) {
return;
}
const { d, fillRule, clipRule } = this.getModerationBadgeIconShape(state);
path.setAttribute("d", d);
if (fillRule) {
path.setAttribute("fill-rule", fillRule);
} else {
path.removeAttribute("fill-rule");
}
if (clipRule) {
path.setAttribute("clip-rule", clipRule);
} else {
path.removeAttribute("clip-rule");
}
}
createModerationBadgeIcon(state) {
const wrapper = this.createElement("span", {
classNames: ["moderation-badge__icon"],
attrs: { "aria-hidden": "true" },
});
const svg = this.document.createElementNS(SVG_NAMESPACE, "svg");
svg.setAttribute("viewBox", "0 0 20 20");
svg.setAttribute("focusable", "false");
svg.setAttribute("aria-hidden", "true");
svg.classList.add("moderation-badge__icon-mark");
const path = this.document.createElementNS(SVG_NAMESPACE, "path");
path.setAttribute("fill", "currentColor");
svg.appendChild(path);
wrapper.appendChild(svg);
this.moderationBadgeIconWrapper = wrapper;
this.moderationBadgeIconSvg = svg;
this.updateModerationBadgeIcon(state);
return wrapper;
}
shouldShowModerationHideButton(context = this.getModerationContext()) {
if (!context || !context.trustedMuted) {
return false;
}
if (context.activeHidden && !context.overrideActive) {
return false;
}
return true;
}
buildModerationBadge(context = this.getModerationContext()) {
if (!context.shouldShow) {
this.moderationBadgeEl = null;
this.moderationBadgeTextEl = null;
this.moderationBadgeIconWrapper = null;
this.moderationBadgeIconSvg = null;
if (this.moderationActionButton) {
this.moderationActionButton.removeEventListener(
"click",
@@ -1345,11 +1493,18 @@ export class VideoCard {
);
}
this.moderationActionButton = null;
if (this.moderationHideButton) {
this.moderationHideButton.removeEventListener(
"click",
this.boundModerationHideHandler,
);
}
this.moderationHideButton = null;
return null;
}
const badge = this.createElement("div", {
classNames: ["badge", "flex", "flex-wrap", "items-center", "gap-sm"],
classNames: ["moderation-badge", "flex", "flex-wrap", "items-center", "gap-sm"],
});
badge.dataset.variant = context.overrideActive ? "neutral" : "warning";
badge.dataset.moderationBadge = "true";
@@ -1362,6 +1517,7 @@ export class VideoCard {
? "trusted-mute"
: "blocked";
badge.dataset.moderationState = state;
this.updateModerationBadgeIcon(state);
if (hiddenActive && context.effectiveHideReason) {
badge.dataset.moderationHideReason = context.effectiveHideReason;
}
@@ -1372,8 +1528,13 @@ export class VideoCard {
badge.setAttribute("aria-live", "polite");
badge.setAttribute("aria-atomic", "true");
const icon = this.createModerationBadgeIcon(state);
if (icon) {
badge.appendChild(icon);
}
const text = this.createElement("span", {
classNames: ["whitespace-nowrap"],
classNames: ["moderation-badge__text", "whitespace-nowrap"],
textContent: this.buildModerationBadgeText(context),
});
badge.appendChild(text);
@@ -1427,6 +1588,24 @@ export class VideoCard {
this.moderationActionButton = null;
}
if (this.shouldShowModerationHideButton(context)) {
if (!this.moderationHideButton) {
const hideButton = this.createModerationHideButton();
badge.appendChild(hideButton);
this.moderationHideButton = hideButton;
} else {
this.moderationHideButton.disabled = false;
this.moderationHideButton.removeAttribute("aria-busy");
}
} else if (this.moderationHideButton) {
this.moderationHideButton.removeEventListener(
"click",
this.boundModerationHideHandler,
);
this.moderationHideButton.remove();
this.moderationHideButton = null;
}
return badge;
}
@@ -1434,6 +1613,21 @@ export class VideoCard {
const badge = this.moderationBadgeEl;
const hiddenActive = context.activeHidden && !context.overrideActive;
if (badge) {
if (!this.moderationBadgeIconWrapper) {
const iconWrapper = badge.querySelector(".moderation-badge__icon");
if (iconWrapper) {
this.moderationBadgeIconWrapper = iconWrapper;
}
}
if (!this.moderationBadgeIconSvg) {
const iconSvg = badge.querySelector(".moderation-badge__icon svg");
if (iconSvg) {
this.moderationBadgeIconSvg = iconSvg;
}
}
}
if (!context.shouldShow) {
if (badge && badge.parentElement) {
badge.parentElement.removeChild(badge);
@@ -1444,8 +1638,17 @@ export class VideoCard {
this.boundShowAnywayHandler,
);
}
if (this.moderationHideButton) {
this.moderationHideButton.removeEventListener(
"click",
this.boundModerationHideHandler,
);
this.moderationHideButton = null;
}
this.moderationBadgeEl = null;
this.moderationBadgeTextEl = null;
this.moderationBadgeIconWrapper = null;
this.moderationBadgeIconSvg = null;
this.moderationActionButton = null;
if (this.hiddenSummaryEl && this.hiddenSummaryEl.parentElement === this.root) {
this.hiddenSummaryEl.remove();
@@ -1590,6 +1793,24 @@ export class VideoCard {
this.moderationActionButton.disabled = false;
this.moderationActionButton.removeAttribute("aria-busy");
}
if (this.shouldShowModerationHideButton(context)) {
if (!this.moderationHideButton) {
const hideButton = this.createModerationHideButton();
badge.appendChild(hideButton);
this.moderationHideButton = hideButton;
} else {
this.moderationHideButton.disabled = false;
this.moderationHideButton.removeAttribute("aria-busy");
}
} else if (this.moderationHideButton) {
this.moderationHideButton.removeEventListener(
"click",
this.boundModerationHideHandler,
);
this.moderationHideButton.remove();
this.moderationHideButton = null;
}
}
ensureHiddenSummaryContainer() {
@@ -2175,6 +2396,14 @@ export class VideoCard {
}
}
if (this.authorPicEl && !this.shouldMaskNsfwForOwner) {
if (context.activeBlur) {
this.authorPicEl.dataset.visualState = "blurred";
} else if (this.authorPicEl.dataset.visualState) {
delete this.authorPicEl.dataset.visualState;
}
}
const reportCount = Math.max(0, Number(context.trustedCount) || 0);
if (reportCount > 0) {
this.root.dataset.moderationReportCount = String(reportCount);
+13
View File
@@ -601,6 +601,19 @@ export class VideoListView {
});
};
videoCard.onModerationHide = ({ event: hideEvent }) => {
if (!this.handlers.moderationHide) {
return false;
}
const trigger = hideEvent?.currentTarget || hideEvent?.target || null;
return this.handlers.moderationHide({
event: hideEvent,
video,
card: videoCard,
trigger,
});
};
videoCard.onEdit = ({ event: editEvent, video: editVideo, index: editIndex }) => {
if (this.handlers.edit) {
const trigger = editEvent?.currentTarget || editEvent?.target || null;
+74
View File
@@ -70,3 +70,77 @@ test("handleModerationOverride decorates stored and current videos then refreshe
assert.equal(currentVideo.moderation.blockAutoplay, false);
assert.equal(currentVideo.moderation.blurThumbnail, false);
});
test("handleModerationHide clears overrides and re-applies hidden state", async (t) => {
withMockedNostrTools(t);
const app = await createModerationAppHarness();
app.getActiveModerationThresholds = () => ({
autoplayBlockThreshold: Number.POSITIVE_INFINITY,
blurThreshold: Number.POSITIVE_INFINITY,
trustedMuteHideThreshold: 1,
trustedSpamHideThreshold: Number.POSITIVE_INFINITY,
});
const videoId = "b".repeat(64);
const incomingVideo = {
id: videoId,
moderation: {
trustedMuted: true,
trustedMuteCount: 1,
trustedMuters: [MUTER_HEX],
trustedCount: 0,
reportType: "nudity",
},
};
const storedVideo = {
id: videoId,
moderation: {
trustedMuted: true,
trustedMuteCount: 1,
trustedMuters: [MUTER_HEX],
trustedCount: 0,
reportType: "nudity",
},
};
const currentVideo = {
id: videoId,
moderation: {
trustedMuted: true,
trustedMuteCount: 1,
trustedMuters: [MUTER_HEX],
trustedCount: 0,
reportType: "nudity",
},
};
app.videosMap.set(videoId, storedVideo);
app.currentVideo = currentVideo;
app.decorateVideoModeration(incomingVideo);
app.decorateVideoModeration(storedVideo);
app.decorateVideoModeration(currentVideo);
const card = {
refreshModerationUi() {
/* noop */
},
};
app.handleModerationOverride({ video: incomingVideo, card });
assert.equal(storedVideo.moderation.viewerOverride?.showAnyway, true);
assert.equal(currentVideo.moderation.viewerOverride?.showAnyway, true);
const result = app.handleModerationHide({ video: incomingVideo, card });
assert.equal(result, true);
assert.equal(incomingVideo.moderation.viewerOverride, undefined);
assert.equal(storedVideo.moderation.viewerOverride, undefined);
assert.equal(currentVideo.moderation.viewerOverride, undefined);
assert.equal(incomingVideo.moderation.hidden, true);
assert.equal(storedVideo.moderation.hidden, true);
assert.equal(currentVideo.moderation.hidden, true);
});
@@ -197,6 +197,7 @@ export async function createModerationAppHarness() {
app.deriveModerationTrustedCount = Application.prototype.deriveModerationTrustedCount;
app.getReporterDisplayName = Application.prototype.getReporterDisplayName;
app.handleModerationOverride = Application.prototype.handleModerationOverride;
app.handleModerationHide = Application.prototype.handleModerationHide;
app.videosMap = new Map();
app.currentVideo = null;
+13
View File
@@ -94,6 +94,8 @@ test("moderation stage enforces admin lists without whitelist bypass", async ()
const whitelistedItem = result[0];
assert.equal(whitelistedItem.video.moderation.blockAutoplay, true);
assert.equal(whitelistedItem.video.moderation.blurThumbnail, true);
assert.equal(whitelistedItem.video.moderation.blurReason, "trusted-report");
assert.equal(whitelistedItem.metadata.moderation.blurReason, "trusted-report");
assert.equal(whitelistedItem.video.moderation.adminWhitelist, true);
assert.equal(whitelistedItem.video.moderation.adminWhitelistBypass, false);
assert.equal(whitelistedItem.metadata.moderation.adminWhitelistBypass, false);
@@ -101,6 +103,8 @@ test("moderation stage enforces admin lists without whitelist bypass", async ()
const normalItem = result[1];
assert.equal(normalItem.video.moderation.blockAutoplay, true);
assert.equal(normalItem.video.moderation.blurThumbnail, true);
assert.equal(normalItem.video.moderation.blurReason, "trusted-report");
assert.equal(normalItem.metadata.moderation.blurReason, "trusted-report");
const reasons = why.map((entry) => entry.reason);
assert(reasons.includes("viewer-block"));
@@ -211,11 +215,14 @@ test("moderation stage applies provided thresholds", async () => {
assert.equal(strictResult.length, 1);
assert.equal(strictResult[0].video.moderation.blockAutoplay, false);
assert.equal(strictResult[0].video.moderation.blurThumbnail, false);
assert.equal(strictResult[0].video.moderation.blurReason, undefined);
const relaxedResult = await relaxedStage(items, {});
assert.equal(relaxedResult.length, 1);
assert.equal(relaxedResult[0].video.moderation.blockAutoplay, true);
assert.equal(relaxedResult[0].video.moderation.blurThumbnail, true);
assert.equal(relaxedResult[0].video.moderation.blurReason, "trusted-report");
assert.equal(relaxedResult[0].metadata.moderation.blurReason, "trusted-report");
});
test("moderation stage propagates whitelist, muters, and threshold updates", async () => {
@@ -315,14 +322,18 @@ test("moderation stage propagates whitelist, muters, and threshold updates", asy
assert.equal(whitelistedItem.metadata.moderation.adminWhitelist, true);
assert.equal(whitelistedItem.metadata.moderation.blockAutoplay, true);
assert.equal(whitelistedItem.metadata.moderation.blurThumbnail, true);
assert.equal(whitelistedItem.metadata.moderation.blurReason, "trusted-report");
assert.equal(whitelistedItem.video.moderation.adminWhitelistBypass, false);
assert.equal(whitelistedItem.video.moderation.blurReason, "trusted-report");
assert.equal(mutedItem.metadata.moderation.trustedMuted, true);
assert.deepEqual(mutedItem.metadata.moderation.trustedMuters, [muterHex]);
assert.equal(mutedItem.metadata.moderation.blockAutoplay, true);
assert.equal(mutedItem.metadata.moderation.blurThumbnail, true);
assert.equal(mutedItem.metadata.moderation.blurReason, "trusted-report");
assert.equal(mutedItem.video.moderation.trustedMuted, true);
assert.deepEqual(mutedItem.video.moderation.trustedMuters, [muterHex]);
assert.equal(mutedItem.video.moderation.blurReason, "trusted-report");
state.summaryById.set("muted-video", {
eventId: "muted-video",
@@ -342,9 +353,11 @@ test("moderation stage propagates whitelist, muters, and threshold updates", asy
assert.deepEqual(updatedMuted.metadata.moderation.trustedMuters, []);
assert.equal(updatedMuted.metadata.moderation.blockAutoplay, false);
assert.equal(updatedMuted.metadata.moderation.blurThumbnail, false);
assert.equal(updatedMuted.metadata.moderation.blurReason, undefined);
assert.equal(updatedMuted.video.moderation.trustedMuted, false);
assert.equal(updatedMuted.video.moderation.blockAutoplay, false);
assert.equal(updatedMuted.video.moderation.blurThumbnail, false);
assert.equal(updatedMuted.video.moderation.blurReason, undefined);
});
test("moderation stage clears cached reporters and muters after service signals", async () => {
+132
View File
@@ -258,3 +258,135 @@ test(
assert.ok(card.moderationBadgeEl.textContent.includes("Showing despite"));
},
);
test("VideoCard blurs thumbnails when trusted mute triggers without reports", async (t) => {
const { document } = setupDom(t);
withMockedNostrTools(t);
const app = await createModerationAppHarness();
app.getActiveModerationThresholds = () => ({
autoplayBlockThreshold: Number.POSITIVE_INFINITY,
blurThreshold: Number.POSITIVE_INFINITY,
trustedMuteHideThreshold: 2,
trustedSpamHideThreshold: Number.POSITIVE_INFINITY,
});
const videoId = "c".repeat(64);
const video = {
id: videoId,
title: "Muted Clip",
pubkey: "d".repeat(64),
moderation: {
trustedMuted: true,
trustedMuteCount: 1,
blockAutoplay: false,
blurThumbnail: false,
trustedCount: 0,
reportType: "nudity",
},
};
app.videosMap.set(video.id, video);
app.currentVideo = video;
app.decorateVideoModeration(video);
const card = new VideoCard({
document,
video,
formatters: {
formatTimeAgo: () => "moments ago",
},
helpers: {
isMagnetSupported: () => false,
},
});
document.body.appendChild(card.getRoot());
assert.equal(video.moderation.blurThumbnail, true);
assert.equal(video.moderation.blurReason, "trusted-mute");
assert.equal(card.getModerationContext().activeBlur, true);
assert.equal(card.thumbnailEl.dataset.thumbnailState, "blurred");
assert.equal(card.authorPicEl.dataset.visualState, "blurred");
assert.equal(card.getRoot().dataset.autoplayPolicy, "blocked");
assert.equal(card.moderationBadgeEl.dataset.moderationState, "trusted-mute");
assert.equal(
card.moderationBadgeTextEl.textContent,
"Autoplay blocked · Muted by a trusted contact",
);
assert.ok(card.moderationHideButton);
assert.equal(
card.moderationHideButton?.dataset?.moderationAction,
"hide",
);
});
test("VideoCard hide action restores trusted mute hide state after override", async (t) => {
const { document } = setupDom(t);
withMockedNostrTools(t);
const app = await createModerationAppHarness();
app.getActiveModerationThresholds = () => ({
autoplayBlockThreshold: Number.POSITIVE_INFINITY,
blurThreshold: Number.POSITIVE_INFINITY,
trustedMuteHideThreshold: 1,
trustedSpamHideThreshold: Number.POSITIVE_INFINITY,
});
const videoId = "d".repeat(64);
const video = {
id: videoId,
title: "Muted Hide Clip",
pubkey: "e".repeat(64),
moderation: {
trustedMuted: true,
trustedMuteCount: 1,
trustedMuters: ["f".repeat(64)],
trustedCount: 0,
reportType: "nudity",
},
};
app.videosMap.set(video.id, video);
app.currentVideo = video;
app.decorateVideoModeration(video);
const card = new VideoCard({
document,
video,
formatters: {
formatTimeAgo: () => "moments ago",
},
helpers: {
isMagnetSupported: () => false,
},
});
document.body.appendChild(card.getRoot());
assert.equal(card.getModerationContext().activeHidden, true);
assert.equal(card.getRoot().dataset.moderationHidden, "true");
card.onModerationOverride = ({ video: overrideVideo, card: overrideCard }) =>
app.handleModerationOverride({ video: overrideVideo, card: overrideCard });
card.moderationActionButton.click();
await new Promise((resolve) => setTimeout(resolve, 0));
const contextAfterOverride = card.getModerationContext();
assert.equal(contextAfterOverride.overrideActive, true);
assert.equal(contextAfterOverride.activeHidden, false);
assert.ok(card.moderationHideButton);
card.onModerationHide = ({ video: targetVideo, card: targetCard }) =>
app.handleModerationHide({ video: targetVideo, card: targetCard });
card.moderationHideButton.click();
await new Promise((resolve) => setTimeout(resolve, 0));
const contextAfterHide = card.getModerationContext();
assert.equal(contextAfterHide.overrideActive, false);
assert.equal(contextAfterHide.activeHidden, true);
assert.equal(card.getRoot().dataset.moderationHidden, "true");
assert.equal(card.moderationHideButton, null);
});
+3
View File
@@ -127,6 +127,7 @@ test.describe("moderation fixtures", () => {
const muteCard = page.locator('[data-test-id="trusted-mute-hide"]');
const badge = muteCard.locator('[data-moderation-badge="true"]');
const thumbnail = muteCard.locator('img[data-video-thumbnail]');
const showAnywayButton = muteCard.getByRole("button", { name: "Show anyway" });
await expect(muteCard).toHaveAttribute("data-moderation-hidden", "true");
@@ -135,6 +136,7 @@ test.describe("moderation fixtures", () => {
await expect(muteCard).toHaveAttribute("data-moderation-trusted-mute", "true");
await expect(muteCard).toHaveAttribute("data-moderation-trusted-mute-count", "1");
await expect(badge).toContainText("Hidden · 1 trusted mute");
await expect(thumbnail).toHaveAttribute("data-thumbnail-state", "blurred");
await expect(showAnywayButton).toBeVisible();
await showAnywayButton.click();
@@ -144,5 +146,6 @@ test.describe("moderation fixtures", () => {
await expect(muteCard.locator('[data-moderation-badge="true"]')).toContainText(
"Showing despite 1 trusted mute",
);
await expect(thumbnail).not.toHaveAttribute("data-thumbnail-state", "blurred");
});
});