progress today

This commit is contained in:
Luna D. 2024-05-07 19:33:56 +02:00
parent 0de3d98c82
commit f04666037c
No known key found for this signature in database
GPG key ID: 4B1C63448394F688
46 changed files with 672 additions and 321 deletions

View file

@ -32,7 +32,7 @@
@import "elements/form";
@import "elements/heading";
@import "elements/input";
@import "elements/interaction";
@import "elements/label";
@import "elements/layout";
@import "elements/list";
@import "elements/media";
@ -48,9 +48,11 @@
@import "views/footer";
@import "views/header";
@import "views/image";
@import "views/interaction";
@import "views/markdown";
@import "views/metabar";
@import "views/pagination";
@import "views/staff";
@import "views/statistics";
@import "views/tag";
@import "views/user";

View file

@ -51,13 +51,16 @@ $font-family-monospace: "Droid Sans Mono", monospace;
--media-container-width: 225px;
--media-tiny-container-width: 50px;
--media-small-container-width: 125px;
--media-small-container-width: 150px;
--media-medium-container-width: 250px;
--media-large-container-width: 500px;
--media-full-container-width: 100%;
--media-featured-width: 358px;
--media-header-height: 2rem;
--badge-small-size: 1.1rem;
--badge-normal-size: 2rem;
--number-badge-size: 0.6rem;
--number-badge-padding: 0.1rem;
--number-badge-border: 2px;

View file

@ -8,11 +8,11 @@
.button--$(type).button--important {
color: var(--text-color) !important;
background: var(--$(type)-color);
box-shadow: 0 -1px var(--$(type)-dark-color) inset;
}
.button--$(type):hover {
background: var(--$(type)-muted-color) !important;
.button--$(type):hover, .button--$(type):active, .button--$(type).selected {
background: var(--$(type)-dark-color) !important;
border-radius: var(--border-radius-inner);
}
.button__group--$(type) {
@ -28,6 +28,7 @@
@mixin animated-transition;
color: var(--text-color);
background: var(--$(type)-color);
border-radius: var(--border-radius-inner);
}
}
@ -40,17 +41,18 @@
font-size: var(--font-size);
background: var(--primary-dark-color);
color: var(--text-color);
border: 0;
border-radius: var(--border-radius-inner);
padding: 0 var(--padding-small);
overflow: hidden;
line-height: var(--button-height);
align-items: center;
border-width: 0;
}
.button:hover {
.button:hover, .button:active {
@mixin animated-transition;
background: var(--primary-muted-color);
background: var(--primary-dark-color);
border-radius: var(--border-radius-inner);
cursor: pointer;
}
@ -70,7 +72,6 @@
.button--important {
background: var(--primary-color);
box-shadow: 0 -1px var(--primary-dark-color) inset;
}
.button__row {
@ -85,7 +86,6 @@
.button__group, .button__group--single, .button__group--standalone {
display: flex;
flex-direction: row;
border: 0;
border-radius: var(--border-radius-inner);
margin-right: var(--padding-normal);
background: var(--secondary-dark-color);
@ -126,6 +126,7 @@
.button__group--standalone a:hover {
@mixin animated-transition;
background: var(--secondary-muted-color);
border-radius: var(--border-radius-inner);
}
.button--borderless {
@ -139,6 +140,7 @@
.block__header__buttons .button:hover {
background: var(--primary-muted-color);
border-radius: var(--border-radius-inner);
}
@mixin button-type primary;

View file

@ -67,6 +67,18 @@
justify-content: space-between;
}
.flex--small-gap {
gap: var(--padding-small);
}
.flex--normal-gap {
gap: var(--padding-normal);
}
.flex--large-gap {
gap: var(--padding-large);
}
.flex--start-bunched {
justify-content: flex-start;
}

View file

@ -17,6 +17,36 @@ form .form--two-column > .field, form .form--two-column > li {
gap: var(--padding-normal);
}
form .form--three-items {
display: grid;
grid: inherit;
grid-template-columns: 1 / -1;
gap: var(--padding-normal);
}
form .form--three-items > .field {
display: grid;
grid-template-columns: auto 1fr;
grid-template-rows: 1fr auto;
gap: var(--padding-normal);
}
form .form--three-items > .field > *:nth-child(1) {
grid-area: 1 / 1 / 2 / 2;
}
form .form--three-items > .field > *:nth-child(2) {
grid-area: 1 / 2 / 2 / 3;
}
form .form--three-items > .field > *:nth-child(3) {
grid-area: 2 / 1 / 3 / 3;
}
form .form--three-items select {
width: fit-content;
}
form .with-error {
display: block;
}

View file

@ -0,0 +1,26 @@
@define-mixin label-type $type {
.label--$(type) {
background-color: var(--$(type)-color);
}
}
.label {
display: flex;
background: var(--primary-color);
padding: var(--padding-tiny) var(--padding-small);
border-radius: var(--border-radius-inner);
width: fit-content;
gap: var(--padding-tiny);
white-space: nowrap;
}
.label--block {
margin: var(--padding-small) 0;
}
@mixin label-type secondary;
@mixin label-type danger;
@mixin label-type warning;
@mixin label-type success;
@mixin label-type special;
@mixin label-type information;

View file

@ -0,0 +1,38 @@
/* An attempt to make stylesheets less broken on
* pre-2020 browsers like Chrome 67.
*
* Note: browsers newer than Chrome 84 will not need this.
*
* The main thing about those is that they don't support
* the "gap" property in flexboxes, so that has to be polyfilled
* with margin tags.
*/
@import "common/measurements";
@import "common/mixins";
@define-mixin legacy-flex-gap $classname, $size {
.$(classname) > * {
margin-bottom: $(size);
margin-right: $(size);
}
}
header > *, nav.header__secondary > * {
margin-left: var(--padding-large);
}
@mixin if-mobile {
header > * {
margin-left: var(--padding-normal);
}
}
@mixin legacy-flex-gap block__header__buttons, var(--padding-normal);
@mixin legacy-flex-gap block__header--js-tabbed, var(--padding-normal);
@mixin legacy-flex-gap field, var(--padding-normal);
@mixin legacy-flex-gap horizontal-list, var(--padding-normal);
@mixin legacy-flex-gap communication-edit__actions, var(--padding-normal);
@mixin legacy-flex-gap header__link--user, var(--padding-normal);
@mixin legacy-flex-gap tag-list, var(--padding-small);
@mixin legacy-flex-gap tagsinput, var(--padding-small);

View file

@ -4,7 +4,7 @@ $text-color: #e0e0e0;
$primary-color: #284371;
$secondary-color: #546c99;
$danger-color: #6d2a20;
$warning-color: #715227;
$warning-color: #6d421a;
$success-color: #25603e;
$information-color: #1c606a;
$special-color: #65206e;

View file

@ -0,0 +1,92 @@
$background-color: #15121a;
$text-color: #e0e0e0;
$primary-color: #36274e;
$secondary-color: #785b99;
$danger-color: #6d2a20;
$warning-color: #715227;
$success-color: #25603e;
$information-color: #1c606a;
$special-color: #65206e;
$upvote-color: #5b9b26;
$downvote-color: #da3412;
$fave-color: #a18e27;
$comment-color: #b099dd;
$hide-color: #da3412;
$tag-default-color: #1b3c21;
$tag-error-color: #4f181d;
$tag-rating-color: #113456;
$tag-origin-color: #1d1858;
$tag-character-color: #193f47;
$tag-oc-color: #451f47;
$tag-species-color: #362118;
$tag-body-type-color: #393939;
$tag-content-fanmade-color: #622c4e;
$tag-content-official-color: #4b491c;
$tag-spoiler-color: #4f3811;
$spoiler-color: #0f0f0f;
@define-mixin tag-color $tagname, $color, $text-percentage: 35, $border-percentage: 15 {
--tag-$(tagname)-color: $(color);
--tag-$(tagname)-border-color: hsl(from $color h s calc(l + $border-percentage));
--tag-$(tagname)-text-color: hsl(from $color h s calc(l + $text-percentage));
}
@define-mixin type-color $type, $color {
--$(type)-color: $color;
--$(type)-border-color: hsl(from $color h calc(s - 20) calc(l + 10));
--$(type)-muted-color: hsl(from $color h calc(s - 10) calc(l - 7));
--$(type)-dark-color: hsl(from $color h calc(s - 30) calc(l - 11));
--$(type)-link-color: hsl(from $color h calc(s + 10) calc(l + 45));
}
:root {
--background-color: $background-color;
--text-color: $text-color;
--text-light-color: $text-color;
--link-color: hsl(from $primary-color h calc(s + 10) calc(l + 50));
--link-hover-color: $text-color;
--primary-color: $primary-color;
--primary-border-color: hsl(from $primary-color h calc(s - 20) calc(l + 5));
--primary-muted-color: hsl(from $primary-color h calc(s - 10) calc(l - 5));
--primary-dark-color: hsl(from $primary-color h calc(s - 15) calc(l - 9));
--primary-link-color: var(--link-color); /* for consistency */
--secondary-color: $secondary-color;
--secondary-border-color: hsl(from $secondary-color h s calc(l + 5));
--secondary-muted-color: hsl(from $secondary-color h s calc(l - 17));
--secondary-dark-color: hsl(from $secondary-color h calc(s - 5) calc(l - 25));
--secondary-link-color: hsl(from $secondary-color h s calc(l + 40));
--upvote-color: $upvote-color;
--downvote-color: $downvote-color;
--fave-color: $fave-color;
--comment-color: $comment-color;
--hide-color: $hide-color;
--spoiler-color: $spoiler-color;
--spoiler-revealed-color: hsl(from $spoiler-color h s calc(l + 20));
@mixin type-color success, $success-color;
@mixin type-color warning, $warning-color;
@mixin type-color danger, $danger-color;
@mixin type-color information, $information-color;
@mixin type-color special, $special-color;
@mixin tag-color default, $tag-default-color;
@mixin tag-color error, $tag-error-color, 37;
@mixin tag-color rating, $tag-rating-color, 37;
@mixin tag-color origin, $tag-origin-color, 42;
@mixin tag-color character, $tag-character-color;
@mixin tag-color oc, $tag-oc-color, 40;
@mixin tag-color species, $tag-species-color, 37;
@mixin tag-color body-type, $tag-body-type-color, 45, 12;
@mixin tag-color content-fanmade, $tag-content-fanmade-color, 40;
@mixin tag-color content-official, $tag-content-official-color;
@mixin tag-color spoiler, $tag-spoiler-color;
}

View file

@ -45,3 +45,101 @@
.communication__anonymous--label {
padding: 0 var(--padding-small);
}
.communication > .block__content:first-of-type {
border-bottom-left-radius: 0;
border-bottom-right-radius: 0;
}
.communication > .block__content:last-of-type {
background: var(--primary-muted-color);
border-top-left-radius: 0;
border-top-right-radius: 0;
}
.communication__body {
display: block;
overflow: hidden;
}
.communication__body__text {
border-radius: var(--border-radius-inner);
word-wrap: break-word;
}
.communication__sender-name {
display: flex;
align-items: center;
gap: var(--padding-normal);
}
.communication__sender-block {
display: flex;
flex-direction: column;
background: var(--primary-muted-color);
border-radius: var(--border-radius-inner);
padding: var(--padding-small);
}
@mixin if-mobile {
.communication__body__text {
margin-top: var(--padding-normal);
}
.communication__sender-block {
flex-direction: row;
gap: var(--padding-normal);
align-items: center;
}
.communication__sender-block > .image-constrained {
min-height: var(--avatar-small-size);
min-width: var(--avatar-small-size);
}
}
.communication__interaction {
display: inline-block;
background: var(--secondary-muted-color);
color: var(--secondary-link-color) !important;
padding: var(--padding-tiny);
border-radius: var(--border-radius-inner);
margin-left: var(--padding-small);
}
.communication__interaction:first-child {
margin: 0;
}
.communication__info {
display: inline-block;
background: var(--information-muted-color);
padding: var(--padding-tiny);
border-radius: var(--border-radius-inner);
margin-left: var(--padding-small);
}
.communication__info > a {
color: var(--information-link-color);
}
.togglable-delete-form, .togglable-delete-form-link {
margin-top: var(--padding-small) !important;
}
.togglable-delete-form-link {
background: var(--danger-muted-color);
color: var(--danger-link-color) !important;
}
.owner-options {
margin-left: var(--padding-small);
}
@mixin if-mobile {
.communication__options > *:first-child {
display: flex;
flex-direction: column;
gap: var(--padding-small);
}
}

View file

@ -30,3 +30,7 @@
.spoiler-revealed a, .spoiler:hover a {
color: var(--link-color);
}
.walloftext {
line-height: var(--readable-line-height);
}

View file

@ -4,6 +4,16 @@
}
}
.metabar__interactions {
display: block;
}
.metabar__interactions .comments_count {
background: 0;
border: 0;
position: static;
}
@mixin if-phone {
.metabar__interactions {
display: grid;
@ -55,7 +65,25 @@
}
.metabar__user-credit {
display: flex;
box-sizing: border-box;
gap: var(--padding-small);
}
.metabar__user-credit .image_uploader {
display: flex;
align-self: center;
align-items: center;
vertical-align: center;
gap: var(--padding-small);
}
/* For some bizarre reason the icon appears to have
* comically large gap on desktop */
@mixin if-desktop {
.image_uploader > .username-with-icon {
gap: 0;
}
}
.metabar__mobile-info td {

View file

@ -12,7 +12,7 @@
font-size: 0.8rem !important;
}
.pagination a {
.pagination a, .pagination span {
display: grid;
grid-template-columns: auto;
gap: var(--padding-tiny);

35
assets/css/views/user.css Normal file
View file

@ -0,0 +1,35 @@
.badges {
display: grid;
grid-template-columns: repeat(6, var(--badge-small-size));
gap: var(--padding-small);
}
.badge {
width: var(--badge-small-size);
height: var(--badge-small-size);
}
.badge__overflow {
background: var(--information-color);
border-radius: var(--border-radius-inner);
text-align: center;
vertical-align: center;
line-height: calc(var(--badge-small-size) * 1.2);
width: calc(var(--badge-small-size) * 1.2);
height: calc(var(--badge-small-size) * 1.2);
font-size: calc(var(--font-size) * 0.8);
}
.username-with-icon {
display: inline-flex;
gap: var(--padding-tiny);
align-items: center;
}
.user-title {
display: flex;
flex-direction: row;
gap: var(--padding-small);
flex-wrap: wrap;
margin-top: var(--padding-small);
}

View file

@ -1,5 +1,5 @@
/**
* Fingerprints
* Thanks uBlock for breaking our JS!
*/
// http://stackoverflow.com/a/34842797
@ -8,7 +8,7 @@ function hashCode(str) {
((prevHash << 5) - prevHash) + currVal.charCodeAt(0), 0) >>> 0;
}
function createFingerprint() {
function createFp() {
const prints = [
navigator.userAgent,
navigator.cpuClass,
@ -33,19 +33,19 @@ function createFingerprint() {
return hashCode(prints.join(''));
}
function setFingerprintCookie() {
let fingerprint;
function setFpCookie() {
let fp;
// The prepended 'c' acts as a crude versioning mechanism.
try {
fingerprint = `c${createFingerprint()}`;
fp = `c${createFp()}`;
}
// If fingerprinting fails, use fakeprint "c1836832948" as a last resort.
// If it fails, use fakeprint "c1836832948" as a last resort.
catch (err) {
fingerprint = 'c1836832948';
fp = 'c1836832948';
}
document.cookie = `_ses=${fingerprint}; path=/; SameSite=Lax`;
document.cookie = `_ses=${fp}; path=/; SameSite=Lax`;
}
export { setFingerprintCookie };
export { setFpCookie };

View file

@ -14,7 +14,7 @@ import { setupBurgerMenu } from './burger';
import { bindCaptchaLinks } from './captcha';
import { setupComments } from './comment';
import { setupDupeReports } from './duplicate_reports';
import { setFingerprintCookie } from './fingerprint';
import { setFpCookie } from './fp';
import { setupGalleryEditing } from './galleries';
import { initImagesClientside } from './imagesclientside';
import { bindImageTarget } from './image_expansion';
@ -48,7 +48,7 @@ whenReady(() => {
initImagesClientside();
setupComments();
setupDupeReports();
setFingerprintCookie();
setFpCookie();
setupGalleryEditing();
bindImageTarget();
setupEvents();

126
assets/package-lock.json generated
View file

@ -2682,29 +2682,6 @@
"node": ">=0.10.0"
}
},
"node_modules/execa": {
"version": "8.0.1",
"resolved": "https://registry.npmjs.org/execa/-/execa-8.0.1.tgz",
"integrity": "sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==",
"dev": true,
"dependencies": {
"cross-spawn": "^7.0.3",
"get-stream": "^8.0.1",
"human-signals": "^5.0.0",
"is-stream": "^3.0.0",
"merge-stream": "^2.0.0",
"npm-run-path": "^5.1.0",
"onetime": "^6.0.0",
"signal-exit": "^4.1.0",
"strip-final-newline": "^3.0.0"
},
"engines": {
"node": ">=16.17"
},
"funding": {
"url": "https://github.com/sindresorhus/execa?sponsor=1"
}
},
"node_modules/fast-deep-equal": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
@ -2861,18 +2838,6 @@
"node": "*"
}
},
"node_modules/get-stream": {
"version": "8.0.1",
"resolved": "https://registry.npmjs.org/get-stream/-/get-stream-8.0.1.tgz",
"integrity": "sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==",
"dev": true,
"engines": {
"node": ">=16"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/glob": {
"version": "7.2.3",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
@ -3018,15 +2983,6 @@
"node": ">= 14"
}
},
"node_modules/human-signals": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/human-signals/-/human-signals-5.0.0.tgz",
"integrity": "sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==",
"dev": true,
"engines": {
"node": ">=16.17.0"
}
},
"node_modules/iconv-lite": {
"version": "0.6.3",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
@ -3132,18 +3088,6 @@
"resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz",
"integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ=="
},
"node_modules/is-stream": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/is-stream/-/is-stream-3.0.0.tgz",
"integrity": "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==",
"dev": true,
"engines": {
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/isexe": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
@ -3846,33 +3790,6 @@
"resolved": "https://registry.npmjs.org/normalize.css/-/normalize.css-8.0.1.tgz",
"integrity": "sha512-qizSNPO93t1YUuUhP22btGOo3chcvDFqFaj2TRybP0DMxkHOCTYwp3n34fel4a31ORXy4m1Xq0Gyqpb5m33qIg=="
},
"node_modules/npm-run-path": {
"version": "5.3.0",
"resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.3.0.tgz",
"integrity": "sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ==",
"dev": true,
"dependencies": {
"path-key": "^4.0.0"
},
"engines": {
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/npm-run-path/node_modules/path-key": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz",
"integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==",
"dev": true,
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/nwsapi": {
"version": "2.2.9",
"resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.9.tgz",
@ -3886,21 +3803,6 @@
"wrappy": "1"
}
},
"node_modules/onetime": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/onetime/-/onetime-6.0.0.tgz",
"integrity": "sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==",
"dev": true,
"dependencies": {
"mimic-fn": "^4.0.0"
},
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/optionator": {
"version": "0.9.4",
"resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz",
@ -4210,17 +4112,6 @@
"integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==",
"dev": true
},
"node_modules/readdirp": {
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
"integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
"dependencies": {
"picomatch": "^2.2.1"
},
"engines": {
"node": ">=8.10.0"
}
},
"node_modules/redent": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz",
@ -4393,18 +4284,6 @@
"integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==",
"dev": true
},
"node_modules/signal-exit": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz",
"integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==",
"dev": true,
"engines": {
"node": ">=14"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/slash": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz",
@ -5254,6 +5133,11 @@
"resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz",
"integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw=="
},
"node_modules/yallist": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="
},
"node_modules/yocto-queue": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",

View file

@ -353,7 +353,10 @@ defmodule Philomena.Users.User do
:show_sidebar_and_watched_images
])
|> TagList.propagate_tag_list(:watched_tag_list, :watched_tag_ids)
|> validate_inclusion(:theme, ~W(default dark red))
|> validate_inclusion(
:theme,
~W(dark-blue dark-red dark-green dark-purple dark-pink dark-yellow dark-cyan dark-grey light-blue light-red light-green light-purple light-pink light-yellow light-cyan light-grey)
)
|> validate_inclusion(:images_per_page, 1..50)
|> validate_inclusion(:comments_per_page, 1..100)
|> validate_inclusion(:scale_large_images, ["false", "partscaled", "true"])

View file

@ -56,10 +56,18 @@ defmodule PhilomenaWeb.SettingController do
)
end
defp determine_theme(%{"theme" => "light", "light_theme" => name} = attrs) when name != nil,
do: Map.replace(attrs, "theme", name)
defp determine_theme(%{"dark_theme" => name} = attrs) when name != nil,
do: Map.replace(attrs, "theme", name)
defp determine_theme(attrs), do: Map.replace(attrs, "theme", "dark-blue")
defp maybe_update_user(conn, nil, _user_params), do: {:ok, conn}
defp maybe_update_user(conn, user, user_params) do
case Users.update_settings(user, user_params) do
case Users.update_settings(user, determine_theme(user_params)) do
{:ok, _user} ->
{:ok, conn}

View file

@ -70,12 +70,12 @@ defmodule PhilomenaWeb.ContentSecurityPolicyPlug do
defp cdn_uri, do: Application.get_env(:philomena, :cdn_host) |> to_uri()
defp camo_uri, do: Application.get_env(:philomena, :camo_host) |> to_uri()
defp default_script_src, do: vite_hmr?(do: "'self' localhost:5173", else: "'self'")
defp default_script_src, do: vite_hmr?(do: "*", else: "'self'")
defp default_connect_src,
do: vite_hmr?(do: "'self' localhost:5173 ws://localhost:5173", else: "'self'")
do: vite_hmr?(do: "*", else: "'self'")
defp default_style_src, do: vite_hmr?(do: "'self' 'unsafe-inline'", else: "'self'")
defp default_style_src, do: vite_hmr?(do: "*", else: "'self'")
defp to_uri(host) when host in [nil, ""], do: ""
defp to_uri(host), do: URI.to_string(%URI{scheme: "https", host: host})

View file

@ -4,14 +4,7 @@ p
article.block.communication
.block__content.flex.flex--no-wrap
.flex__fixed.spacing--right
= render PhilomenaWeb.UserAttributionView, "_anon_user_avatar.html", object: @report, conn: @conn
.flex__grow.communication__body
span.communication__body__sender-name = render PhilomenaWeb.UserAttributionView, "_anon_user.html", object: @report, awards: true, conn: @conn
br
= render PhilomenaWeb.UserAttributionView, "_anon_user_title.html", object: @report, conn: @conn
.communication__body__text
=<> @body
= render PhilomenaWeb.CommunicationView, "_body.html", object: @report, body: @body, conn: @conn
.block__content.communication__options
.flex.flex--wrap.flex--spaced-out

View file

@ -22,33 +22,7 @@ article.block.communication id="comment_#{@comment.id}"
= submit "Delete", class: "button"
.block__content.flex.flex--no-wrap class=communication_body_class(@comment)
.flex__fixed.spacing--right
= render PhilomenaWeb.UserAttributionView, "_anon_user_avatar.html", object: @comment, conn: @conn
.flex__grow.communication__body
span.communication__body__sender-name = render PhilomenaWeb.UserAttributionView, "_anon_user.html", object: @comment, awards: true, conn: @conn
br
= render PhilomenaWeb.UserAttributionView, "_anon_user_title.html", object: @comment, conn: @conn
.communication__body__text
= if @comment.hidden_from_users do
strong.comment_deleted
' Deletion reason:
=<> @comment.deletion_reason
= if can?(@conn, :hide, @comment) and not is_nil(@comment.deleted_by) do
| (
= @comment.deleted_by.name
| )
= if can?(@conn, :hide, @comment) do
= if @comment.destroyed_content do
br
strong.comment_deleted>
| This comment's contents have been destroyed.
- else
br
=<> @body
- else
=<> @body
= render PhilomenaWeb.CommunicationView, "_body.html", object: @comment, body: @body, conn: @conn, name: "comment"
.block__content.communication__options
.flex.flex--wrap.flex--spaced-out

View file

@ -1,37 +1,6 @@
article.block.communication id="comment_#{@comment.id}"
.block__content.flex.flex--no-wrap class=communication_body_class(@comment)
.flex__fixed.spacing--right
.post-image-container
= render PhilomenaWeb.ImageView, "_image_container.html", image: @comment.image, size: :thumb_tiny, conn: @conn
.flex__grow.communication__body
span.communication__body__sender-name = render PhilomenaWeb.UserAttributionView, "_anon_user.html", object: @comment, awards: true, conn: @conn
br
= render PhilomenaWeb.UserAttributionView, "_anon_user_title.html", object: @comment, conn: @conn
.communication__body__text
= if @comment.hidden_from_users do
strong.comment_deleted
' Deletion reason:
=<> @comment.deletion_reason
= if can?(@conn, :hide, @comment) and not is_nil(@comment.deleted_by) do
| (
= @comment.deleted_by.name
| )
= if can?(@conn, :hide, @comment) do
= if @comment.destroyed_content do
br
strong.comment_deleted>
| This comment's contents have been destroyed.
- else
br
=<> @body
- else
=<> @body
= render PhilomenaWeb.CommunicationView, "_body.html", object: @comment, image: @comment.image, body: @body, conn: @conn, name: "comment"
.block__content.communication__options
.flex.flex--wrap.flex--spaced-out

View file

@ -0,0 +1,60 @@
- anon = is_nil(assigns[:noanon]) or @noanon == false
- avatar = cond do
- not is_nil(assigns[:image]) ->
.post-image-container
= render PhilomenaWeb.ImageView, "_image_container.html", image: @image, size: :thumb_tiny, conn: @conn
- anon ->
= render PhilomenaWeb.UserAttributionView, "_anon_user_avatar.html", object: @object, conn: @conn
- true ->
= render PhilomenaWeb.UserAttributionView, "_user_avatar.html", object: @object, conn: @conn, class: "avatar--small"
- username = if anon do
= render PhilomenaWeb.UserAttributionView, "_anon_user.html", object: @object, awards: true, conn: @conn
- else
= render PhilomenaWeb.UserAttributionView, "_user.html", object: @object, badges: true, conn: @conn
- title = if anon do
= render PhilomenaWeb.UserAttributionView, "_anon_user_title.html", object: @object, conn: @conn
- else
= render PhilomenaWeb.UserAttributionView, "_user_title.html", object: @object, conn: @conn
- contents = if Map.has_key?(@object, :hidden_from_users) and @object.hidden_from_users == true do
strong.comment_deleted
' Deletion reason:
=<> @object.deletion_reason
= if can?(@conn, :hide, @object) and not is_nil(@object.deleted_by) do
| (
= @object.deleted_by.name
| )
= if can?(@conn, :hide, @object) do
= if @object.destroyed_content do
br
strong.comment_deleted>
| This #{@name}'s contents have been destroyed.
- else
br
=<> @body
- else
=<> @body
.flex.flex__grow.hidden--mobile
.flex.flex__fixed.spacing--right
= avatar
.flex__grow.communication__body
.communication__sender-block
span.communication__sender-name
= username
= title
.communication__body__text
= contents
.flex.flex__column.flex__grow.hidden--desktop
.communication__sender-block
= avatar
.flex__column.flex--small-gap
span.communication__sender-name
= username
= title
.communication__body.communication__body__text
= contents

View file

@ -9,7 +9,7 @@ html lang="en"
=> @status
| - Philomena
link rel="stylesheet" href=stylesheet_path(@conn, nil)
link rel="stylesheet" href=dark_stylesheet_path(@conn) media="(prefers-color-scheme: dark)"
link rel="stylesheet" href=light_stylesheet_path(@conn) media="(prefers-color-scheme: light)"
link rel="icon" href="/favicon.ico" type="image/x-icon"
link rel="icon" href="/favicon.svg" type="image/svg+xml"

View file

@ -61,10 +61,9 @@
a href="#{pretty_url(@image, true, true)}" title="Download (no tags in filename)"
i.fa.fa-download
.metabar.metabar__user-credit.hidden--phone.layout--centered#extrameta
div
' Uploaded
=> pretty_time(@image.created_at)
= render PhilomenaWeb.ImageView, "_uploader.html", assigns
' Uploaded
=> pretty_time(@image.created_at)
= render PhilomenaWeb.ImageView, "_uploader.html", assigns
span.image-size
| &nbsp;

View file

@ -14,7 +14,7 @@ elixir:
.block__header.page__header
=> header
.block.block--borderless.flex__row.flex--spaced-out
.block.block--borderless.flex__row.flex--spaced-out.flex--wrap
= if @images.total_pages > 1 do
.button__group--standalone
.page__pagination = pagination
@ -41,17 +41,15 @@ elixir:
- image ->
= render PhilomenaWeb.ImageView, "_image_box.html", image: image, link: image_url.(image), size: assigns[:size] || :thumb, conn: @conn
.block.block--borderless.block--spaced-top.flex__row
br
.block.block--borderless.flex__row.flex--normal-gap.flex--wrap
.button__group--standalone
.page__pagination = pagination
span.page__info
= info
.flex__spacer
.page__info.button__group--standalone
a href="/settings/edit" title="Display Settings"
i.fa.fa-cog
span.hidden--mobile<>
' Display Settings
span.page__info
= info

View file

@ -13,7 +13,7 @@ html lang="en"
link rel="stylesheet" href="/css/application.css"
link rel="stylesheet" href=stylesheet_path(@conn, @current_user)
= if is_nil(@current_user) do
link rel="stylesheet" href=dark_stylesheet_path(@conn) media="(prefers-color-scheme: dark)"
link rel="stylesheet" href=light_stylesheet_path(@conn) media="(prefers-color-scheme: light)"
link rel="icon" href="/favicon.ico" type="image/x-icon"
link rel="icon" href="/favicon.svg" type="image/svg+xml"
meta name="generator" content="philomena"

View file

@ -7,7 +7,7 @@ html lang="en"
title Two Factor Authentication - Derpibooru
link rel="stylesheet" href=stylesheet_path(@conn, nil)
link rel="stylesheet" href=dark_stylesheet_path(@conn) media="(prefers-color-scheme: dark)"
link rel="stylesheet" href=light_stylesheet_path(@conn) media="(prefers-color-scheme: light)"
link rel="icon" href="/favicon.ico" type="image/x-icon"
link rel="icon" href="/favicon.svg" type="image/svg+xml"

View file

@ -14,18 +14,7 @@ article.block.communication
' Approve
.block__content.flex.flex--no-wrap
.flex__fixed.spacing--right
= render PhilomenaWeb.UserAttributionView, "_user_avatar.html", object: %{user: @message.from}, conn: @conn, class: "avatar--small"
.flex__grow.communication__body
span.communication__body__sender-name = render PhilomenaWeb.UserAttributionView, "_user.html", object: %{user: @message.from}, badges: true, conn: @conn
br
= render PhilomenaWeb.UserAttributionView, "_user_title.html", object: %{user: @message.from}, conn: @conn
.communication__body__text
= @body
= render PhilomenaWeb.CommunicationView, "_body.html", object: %{user: @message.from}, noanon: true, body: @body, conn: @conn, name: "message"
.block__content.communication__options
.flex.flex--wrap.flex--spaced-out

View file

@ -40,11 +40,11 @@
nav.pagination.hidden--desktop
= if first_page?(@page) do
span
span.with-icon
i.fa.fa-backward>
' First
.separator--vertical.separator--secondary
span
span.with-icon
i.fa.fa-chevron-left>
' Prev
.separator--vertical.separator--secondary
@ -52,12 +52,14 @@
= link to: first_page_path(@page, @route, params), class: "with-icon" do
i.fa.fa-backward>
' First
.separator--vertical.separator--secondary
= link to: prev_page_path(@page, @route, params), class: "js-prev with-icon" do
i.fa.fa-chevron-left>
' Prev
.separator--vertical.separator--secondary
.dropdown
a.page-current.pagination__dropdown
a.with-icon.page-current.pagination__dropdown
=> @page.page_number
i.fa.fa-caret-down
@ -80,11 +82,11 @@
= if last_page?(@page) do
.separator--vertical.separator--secondary
span
span.with-icon
' Next
i.fa.fa-chevron-right
.separator--vertical.separator--secondary
span
span.with-icon
' Last
i.fa.fa-fast-forward
- else

View file

@ -22,33 +22,7 @@ article.block.communication id="post_#{@post.id}"
= submit "Delete", class: "button"
.block__content.flex.flex--no-wrap class=communication_body_class(@post)
.flex__fixed.spacing--right
= render PhilomenaWeb.UserAttributionView, "_anon_user_avatar.html", object: @post, conn: @conn
.flex__grow.communication__body
span.communication__body__sender-name = render PhilomenaWeb.UserAttributionView, "_anon_user.html", object: @post, awards: true, conn: @conn
br
= render PhilomenaWeb.UserAttributionView, "_anon_user_title.html", object: @post, conn: @conn
.communication__body__text
= if @post.hidden_from_users do
strong.comment_deleted
' Deletion reason:
=> @post.deletion_reason
= if can?(@conn, :hide, @post) and not is_nil(@post.deleted_by) do
| (
= @post.deleted_by.name
| )
= if can?(@conn, :hide, @post) do
= if @post.destroyed_content do
br
strong.comment_deleted>
| This post's contents have been destroyed.
- else
br
=<> @body
- else
=<> @body
= render PhilomenaWeb.CommunicationView, "_body.html", object: @post, body: @body, conn: @conn, name: "post"
.block__content.communication__options
.flex.flex--wrap.flex--spaced-out

View file

@ -2,6 +2,8 @@
= cond do
- @user.description not in [nil, ""] ->
= @about_me
- true ->
| No description provided.
= if can?(@conn, :edit_description, @user) do
= if @user.description not in [nil, ""] do

View file

@ -1,6 +1,6 @@
.badges
- awards = award_order(@awards)
- {awards, overflow} = Enum.split(awards, 10)
- {awards, overflow} = Enum.split(awards, 5)
= for award <- awards do
- title = [award_title(award), award.label] |> Enum.join(" - ")
@ -8,11 +8,6 @@
= badge_image(award.badge, alt: title, title: title, width: "18", height: "18")
= if Enum.any?(overflow) do
.dropdown
i.fa.fa-caret-down
.dropdown__content.block__header
.badges.flex--column
= for award <- overflow do
- title = [award_title(award), award.label] |> Enum.join(" - ")
.badge
= badge_image(award.badge, alt: title, title: title, width: "18", height: "18")
span.badge__overflow
| +
= Enum.count(overflow)

View file

@ -56,36 +56,54 @@ h1 Content Settings
' Do not share this URL with anyone, it may allow an attacker to compromise your account.
.block__tab.hidden.flex.flex--maybe-wrap data-tab="display"
div
.form--three-items
.field
=> label f, :use_centered_layout
=> checkbox f, :use_centered_layout, class: "checkbox"
.with-error
=> checkbox f, :use_centered_layout, class: "checkbox"
.fieldlabel: i Align content to the center of the page - try this option out if you browse the site on a tablet or a fairly wide screen.
.field
=> label f, :show_sidebar_and_watched_images
=> checkbox f, :show_sidebar_and_watched_images, class: "checkbox"
.with-error
=> checkbox f, :show_sidebar_and_watched_images, class: "checkbox"
.fieldlabel: i Show the sidebar and new watched images on the homepage (the default) or hide it.
.field
=> label f, :hide_vote_counts
=> checkbox f, :hide_vote_counts, class: "checkbox"
.with-error
=> checkbox f, :hide_vote_counts, class: "checkbox"
.fieldlabel: i Hide upvote and downvote counts on images, showing only the overall score
.field
=> label f, :images_per_page
=> number_input f, :images_per_page, min: 1, max: 50, step: 1, class: "input"
= error_tag f, :images_per_page
.with-error
=> number_input f, :images_per_page, min: 1, max: 50, step: 1, class: "input"
= error_tag f, :images_per_page
.fieldlabel
i
' This is the number of images per page that are displayed on image listings and searches, up to a maximum of 50.
' For 1080p monitors, try 24.
.field
=> label f, :theme
=> select f, :theme, theme_options(@conn), class: "input"
= error_tag f, :theme
label Theme
select.input#js-theme-selector name="user[theme]" id="user_theme"
option value="dark" Dark
option value="light" Light
.fieldlabel: i General appearance of the theme
.field#js-theme-dark
label Theme color
.with-error
=> select f, :dark_theme, theme_options(@conn), class: "input"
= error_tag f, :dark_theme
.fieldlabel: i Color of the theme, don't forget to save settings to apply the theme
.field.hidden#js-theme-light
label Theme color
.with-error
=> select f, :light_theme, light_theme_options(@conn), class: "input"
= error_tag f, :light_theme
.fieldlabel: i Preview themes by selecting one from the dropdown. Saving sets the currently selected theme.
.field
=> label f, :scale_large_images
=> select f, :scale_large_images, scale_options(), class: "input"
= error_tag f, :scale_large_images
.with-error
=> select f, :scale_large_images, scale_options(), class: "input"
= error_tag f, :scale_large_images
.block__tab.hidden.flex.flex--maybe-wrap data-tab="comments"
div

View file

@ -1,6 +1,9 @@
= cond do
- not is_nil(@object.user) and not anonymous?(@object) ->
strong<>
strong.username-with-icon<>
- icon = user_icon(@object.user)
= if icon do
i class="fa #{icon}"
= link(@object.user.name, to: Routes.profile_path(@conn, :show, @object.user))
= if assigns[:awards] do
= render PhilomenaWeb.ProfileView, "_awards.html", awards: @object.user.awards

View file

@ -1,6 +1,7 @@
= if !!@object.user and !anonymous?(@object) do
= for {class, label} <- user_labels(@object) do
= if assigns[:large] do
.label.label--block class=class = label
- else
.label.label--block.label--small class=class = label
.user-title
= for {class, label} <- user_labels(@object) do
= if assigns[:large] do
.label class=class = label
- else
.label.label--small class=class = label

View file

@ -1,5 +1,8 @@
= if !!@object.user do
strong<>
- icon = user_icon(@object.user)
= if icon do
i class="fa #{icon}">
= link(@object.user.name, to: Routes.profile_path(@conn, :show, @object.user))
= if assigns[:awards] do
= render PhilomenaWeb.ProfileView, "_awards.html", awards: @object.user.awards
= render PhilomenaWeb.ProfileView, "_awards.html", awards: @object.user.awards

View file

@ -1,5 +1,6 @@
= for {class, label} <- user_labels(@object) do
= if assigns[:large] do
.label.label--block class=class = label
- else
.label.label--block.label--small class=class = label
.user-title
= for {class, label} <- user_labels(@object) do
= if assigns[:large] do
.label class=class = label
- else
.label.label--small class=class = label

View file

@ -46,7 +46,9 @@ defmodule PhilomenaWeb.Admin.UserView do
def description("moderator", "Tag"), do: "Manage tag details"
def description("admin", "Tag"), do: "Alias tags"
def description("batch_update", "Tag"), do: "Update tags in batches (do not issue to staff members)"
def description("batch_update", "Tag"),
do: "Update tags in batches (do not issue to staff members)"
def description("moderator", "User"), do: "Manage users and wipe votes"
def description("admin", "Role"), do: "Manage permissions"

View file

@ -0,0 +1,3 @@
defmodule PhilomenaWeb.CommunicationView do
use PhilomenaWeb, :view
end

View file

@ -4,7 +4,7 @@ defmodule PhilomenaWeb.ErrorView do
import PhilomenaWeb.LayoutView,
only: [
stylesheet_path: 2,
dark_stylesheet_path: 1,
light_stylesheet_path: 1,
viewport_meta_tag: 1
]

View file

@ -69,17 +69,32 @@ defmodule PhilomenaWeb.LayoutView do
Config.get(:footer)
end
# def stylesheet_path(conn, %{theme: "dark"}),
# do: Routes.static_path(conn, "/css/dark.css")
# def stylesheet_path(conn, %{theme: "red"}),
# do: Routes.static_path(conn, "/css/red.css")
def stylesheet_path(conn, %{theme: theme})
when theme in [
"dark-blue",
"dark-red",
"dark-green",
"dark-purple",
"dark-pink",
"dark-yellow",
"dark-cyan",
"dark-grey",
"light-blue",
"light-red",
"light-green",
"light-purple",
"light-pink",
"light-yellow",
"light-cyan",
"light-grey"
],
do: Routes.static_path(conn, "/css/#{theme}.css")
def stylesheet_path(conn, _user),
do: Routes.static_path(conn, "/css/dark-blue.css")
def dark_stylesheet_path(conn),
do: Routes.static_path(conn, "/css/dark-blue.css")
def light_stylesheet_path(conn),
do: Routes.static_path(conn, "/css/light-blue.css")
def theme_name(%{theme: theme}), do: theme
def theme_name(_user), do: "default"

View file

@ -4,12 +4,90 @@ defmodule PhilomenaWeb.SettingView do
def theme_options(conn) do
[
[
key: "Default",
value: "default",
data: [theme_path: Routes.static_path(conn, "/css/default.css")]
key: "Blue (default)",
value: "dark-blue",
data: [theme_path: Routes.static_path(conn, "/css/dark-blue.css")]
],
[key: "Dark", value: "dark", data: [theme_path: Routes.static_path(conn, "/css/dark.css")]],
[key: "Red", value: "red", data: [theme_path: Routes.static_path(conn, "/css/red.css")]]
[
key: "Red",
value: "dark-red",
data: [theme_path: Routes.static_path(conn, "/css/dark-red.css")]
],
[
key: "Green",
value: "dark-green",
data: [theme_path: Routes.static_path(conn, "/css/dark-green.css")]
],
[
key: "Purple",
value: "dark-purple",
data: [theme_path: Routes.static_path(conn, "/css/dark-purple.css")]
],
[
key: "Pink",
value: "dark-pink",
data: [theme_path: Routes.static_path(conn, "/css/dark-pink.css")]
],
[
key: "Yellow",
value: "dark-yellow",
data: [theme_path: Routes.static_path(conn, "/css/dark-yellow.css")]
],
[
key: "Cyan",
value: "dark-cyan",
data: [theme_path: Routes.static_path(conn, "/css/dark-cyan.css")]
],
[
key: "Grey",
value: "dark-grey",
data: [theme_path: Routes.static_path(conn, "/css/dark-grey.css")]
]
]
end
def light_theme_options(conn) do
[
[
key: "Blue (default)",
value: "light-blue",
data: [theme_path: Routes.static_path(conn, "/css/light-blue.css")]
],
[
key: "Red",
value: "light-red",
data: [theme_path: Routes.static_path(conn, "/css/light-red.css")]
],
[
key: "Green",
value: "light-green",
data: [theme_path: Routes.static_path(conn, "/css/light-green.css")]
],
[
key: "Purple",
value: "light-purple",
data: [theme_path: Routes.static_path(conn, "/css/light-purple.css")]
],
[
key: "Pink",
value: "light-pink",
data: [theme_path: Routes.static_path(conn, "/css/light-pink.css")]
],
[
key: "Yellow",
value: "light-yellow",
data: [theme_path: Routes.static_path(conn, "/css/light-yellow.css")]
],
[
key: "Cyan",
value: "light-cyan",
data: [theme_path: Routes.static_path(conn, "/css/light-cyan.css")]
],
[
key: "Grey",
value: "light-grey",
data: [theme_path: Routes.static_path(conn, "/css/light-grey.css")]
]
]
end

View file

@ -81,6 +81,13 @@ defmodule PhilomenaWeb.UserAttributionView do
"data:image/svg+xml;base64," <> Base.encode64(svg)
end
def user_icon(%{secondary_role: sr}) when sr in ["Site Developer", "Devops"], do: "fa-screwdriver-wrench"
def user_icon(%{secondary_role: sr}) when sr in ["Public Relations"], do: "fa-bullhorn"
def user_icon(%{hide_default_role: true}), do: nil
def user_icon(%{role: role}) when role in ["admin", "moderator"], do: "fa-gavel"
def user_icon(%{role: "assistant"}), do: "fa-handshake-angle"
def user_icon(_), do: nil
def user_labels(%{user: user}) do
[]
|> personal_title(user)