feature: reactions

This commit is contained in:
Andy Burke 2025-10-14 22:20:54 -07:00
parent 6500d9a9be
commit b8467ec870
16 changed files with 2603 additions and 383 deletions

View file

@ -73,7 +73,7 @@ export async function GET(request: Request, meta: Record<string, any>): Promise<
return false;
}
if (meta.query.type && event_type !== meta.query.type) {
if (meta.query.type && !meta.query.type.split(',').includes(event_type)) {
return false;
}

View file

@ -11,9 +11,12 @@
<link rel="stylesheet" href="./base.css"></link>
<script src="./js/api.js" type="text/javascript"></script>
<script src="./js/audioplayer.js" type="text/javascript"></script>
<script src="./js/datetimeutils.js" type="text/javascript"></script>
<script src="./js/debounce.js" type="text/javascript"></script>
<script src="./js/external/md_to_html.js" type="text/javascript"></script>
<script src="./js/embeds/audio.js" type="text/javascript"></script>
<script src="./js/embeds/gif.js" type="text/javascript"></script>
<script src="./js/embeds/image.js" type="text/javascript"></script>
@ -25,18 +28,16 @@
<script src="./js/embeds/tidal.js" type="text/javascript"></script>
<script src="./js/embeds/vimeo.js" type="text/javascript"></script>
<script src="./js/embeds/youtube.js" type="text/javascript"></script>
<script src="./js/external/md_to_html.js" type="text/javascript"></script>
<script src="./js/emojis/en.js" type="text/javascript"></script>
<script src="./js/htmlify.js" type="text/javascript"></script>
<script src="./js/locationchange.js" type="text/javascript"></script>
<script src="./js/notifications.js" type="text/javascript"></script>
<script src="./js/totp.js" type="text/javascript"></script>
<script src="./js/api.js" type="text/javascript"></script>
<script src="./js/reactions.js" type="text/javascript"></script>
<script src="./js/smartfeeds.js" type="text/javascript"></script>
<script src="./js/smartforms.js" type="text/javascript"></script>
<script src="./js/textareaenhancements.js" type="text/javascript"></script>
<script src="./js/totp.js" type="text/javascript"></script>
</head>
<body>
<!-- #include file="./signup_login_wall.html" -->

7
public/js/debounce.js Normal file
View file

@ -0,0 +1,7 @@
function debounce(fn, delay = 1_000) {
let timer = null;
return (...args) => {
clearTimeout(timer);
timer = setTimeout(() => fn(...args), delay);
};
}

View file

@ -1,4 +1,4 @@
export const EMOJI_MAP = {
const EMOJI_MAP = {
"☔": ["umbrella with rain drops"],
"☕": ["coffee"],
"♈": ["aries"],
@ -38,264 +38,264 @@ export const EMOJI_MAP = {
"🆘": ["sos"],
"🆙": ["up"],
"🆚": ["vs"],
"🇦🇨": ["flag: ac"],
"🇦🇩": ["flag: ad"],
"🇦🇪": ["flag: ae"],
"🇦🇫": ["flag: af"],
"🇦🇬": ["flag: ag"],
"🇦🇮": ["flag: ai"],
"🇦🇱": ["flag: al"],
"🇦🇲": ["flag: am"],
"🇦🇴": ["flag: ao"],
"🇦🇶": ["flag: aq"],
"🇦🇷": ["flag: ar"],
"🇦🇸": ["flag: as"],
"🇦🇹": ["flag: at"],
"🇦🇺": ["flag: au"],
"🇦🇼": ["flag: aw"],
"🇦🇽": ["flag: ax"],
"🇦🇿": ["flag: az"],
"🇧🇦": ["flag: ba"],
"🇧🇧": ["flag: bb"],
"🇧🇩": ["flag: bd"],
"🇧🇪": ["flag: be"],
"🇧🇫": ["flag: bf"],
"🇧🇬": ["flag: bg"],
"🇧🇭": ["flag: bh"],
"🇧🇮": ["flag: bi"],
"🇧🇯": ["flag: bj"],
"🇧🇱": ["flag: bl"],
"🇧🇲": ["flag: bm"],
"🇧🇳": ["flag: bn"],
"🇧🇴": ["flag: bo"],
"🇧🇶": ["flag: bq"],
"🇧🇷": ["flag: br"],
"🇧🇸": ["flag: bs"],
"🇧🇹": ["flag: bt"],
"🇧🇻": ["flag: bv"],
"🇧🇼": ["flag: bw"],
"🇧🇾": ["flag: by"],
"🇧🇿": ["flag: bz"],
"🇨🇦": ["flag: ca"],
"🇨🇨": ["flag: cc"],
"🇨🇩": ["flag: cd"],
"🇨🇫": ["flag: cf"],
"🇨🇬": ["flag: cg"],
"🇨🇭": ["flag: ch"],
"🇨🇮": ["flag: ci"],
"🇨🇰": ["flag: ck"],
"🇨🇱": ["flag: cl"],
"🇨🇲": ["flag: cm"],
"🇨🇳": ["flag: cn"],
"🇨🇴": ["flag: co"],
"🇨🇵": ["flag: cp"],
"🇨🇷": ["flag: cr"],
"🇨🇺": ["flag: cu"],
"🇨🇻": ["flag: cv"],
"🇨🇼": ["flag: cw"],
"🇨🇽": ["flag: cx"],
"🇨🇾": ["flag: cy"],
"🇨🇿": ["flag: cz"],
"🇩🇪": ["flag: de"],
"🇩🇬": ["flag: dg"],
"🇩🇯": ["flag: dj"],
"🇩🇰": ["flag: dk"],
"🇩🇲": ["flag: dm"],
"🇩🇴": ["flag: do"],
"🇩🇿": ["flag: dz"],
"🇪🇦": ["flag: ea"],
"🇪🇨": ["flag: ec"],
"🇪🇪": ["flag: ee"],
"🇪🇬": ["flag: eg"],
"🇪🇭": ["flag: eh"],
"🇪🇷": ["flag: er"],
"🇪🇸": ["flag: es"],
"🇪🇹": ["flag: et"],
"🇪🇺": ["flag: eu"],
"🇫🇮": ["flag: fi"],
"🇫🇯": ["flag: fj"],
"🇫🇰": ["flag: fk"],
"🇫🇲": ["flag: fm"],
"🇫🇴": ["flag: fo"],
"🇫🇷": ["flag: fr"],
"🇬🇦": ["flag: ga"],
"🇬🇧": ["flag: gb", "flag: uk"],
"🇬🇩": ["flag: gd"],
"🇬🇪": ["flag: ge"],
"🇬🇫": ["flag: gf"],
"🇬🇬": ["flag: gg"],
"🇬🇭": ["flag: gh"],
"🇬🇮": ["flag: gi"],
"🇬🇱": ["flag: gl"],
"🇬🇲": ["flag: gm"],
"🇬🇳": ["flag: gn"],
"🇬🇵": ["flag: gp"],
"🇬🇶": ["flag: gq"],
"🇬🇷": ["flag: gr"],
"🇬🇸": ["flag: gs"],
"🇬🇹": ["flag: gt"],
"🇬🇺": ["flag: gu"],
"🇬🇼": ["flag: gw"],
"🇬🇾": ["flag: gy"],
"🇭🇰": ["flag: hk"],
"🇭🇲": ["flag: hm"],
"🇭🇳": ["flag: hn"],
"🇭🇷": ["flag: hr"],
"🇭🇹": ["flag: ht"],
"🇭🇺": ["flag: hu"],
"🇮🇨": ["flag: ic"],
"🇮🇩": ["flag: id"],
"🇮🇪": ["flag: ie"],
"🇮🇱": ["flag: il"],
"🇮🇲": ["flag: im"],
"🇮🇳": ["flag: in"],
"🇮🇴": ["flag: io"],
"🇮🇶": ["flag: iq"],
"🇮🇷": ["flag: ir"],
"🇮🇸": ["flag: is"],
"🇮🇹": ["flag: it"],
"🇯🇪": ["flag: je"],
"🇯🇲": ["flag: jm"],
"🇯🇴": ["flag: jo"],
"🇯🇵": ["flag: jp"],
"🇰🇪": ["flag: ke"],
"🇰🇬": ["flag: kg"],
"🇰🇭": ["flag: kh"],
"🇰🇮": ["flag: ki"],
"🇰🇲": ["flag: km"],
"🇰🇳": ["flag: kn"],
"🇰🇵": ["flag: kp"],
"🇰🇷": ["flag: kr"],
"🇰🇼": ["flag: kw"],
"🇰🇾": ["flag: ky"],
"🇰🇿": ["flag: kz"],
"🇱🇦": ["flag: la"],
"🇱🇧": ["flag: lb"],
"🇱🇨": ["flag: lc"],
"🇱🇮": ["flag: li"],
"🇱🇰": ["flag: lk"],
"🇱🇷": ["flag: lr"],
"🇱🇸": ["flag: ls"],
"🇱🇹": ["flag: lt"],
"🇱🇺": ["flag: lu"],
"🇱🇻": ["flag: lv"],
"🇱🇾": ["flag: ly"],
"🇲🇦": ["flag: ma"],
"🇲🇨": ["flag: mc"],
"🇲🇩": ["flag: md"],
"🇲🇪": ["flag: me"],
"🇲🇫": ["flag: mf"],
"🇲🇬": ["flag: mg"],
"🇲🇭": ["flag: mh"],
"🇲🇰": ["flag: mk"],
"🇲🇱": ["flag: ml"],
"🇲🇲": ["flag: mm"],
"🇲🇳": ["flag: mn"],
"🇲🇴": ["flag: mo"],
"🇲🇵": ["flag: mp"],
"🇲🇶": ["flag: mq"],
"🇲🇷": ["flag: mr"],
"🇲🇸": ["flag: ms"],
"🇲🇹": ["flag: mt"],
"🇲🇺": ["flag: mu"],
"🇲🇻": ["flag: mv"],
"🇲🇼": ["flag: mw"],
"🇲🇽": ["flag: mx"],
"🇲🇾": ["flag: my"],
"🇲🇿": ["flag: mz"],
"🇳🇦": ["flag: na"],
"🇳🇨": ["flag: nc"],
"🇳🇪": ["flag: ne"],
"🇳🇫": ["flag: nf"],
"🇳🇬": ["flag: ng"],
"🇳🇮": ["flag: ni"],
"🇳🇱": ["flag: nl"],
"🇳🇴": ["flag: no"],
"🇳🇵": ["flag: np"],
"🇳🇷": ["flag: nr"],
"🇳🇺": ["flag: nu"],
"🇳🇿": ["flag: nz"],
"🇴🇲": ["flag: om"],
"🇵🇦": ["flag: pa"],
"🇵🇪": ["flag: pe"],
"🇵🇫": ["flag: pf"],
"🇵🇬": ["flag: pg"],
"🇵🇭": ["flag: ph"],
"🇵🇰": ["flag: pk"],
"🇵🇱": ["flag: pl"],
"🇵🇲": ["flag: pm"],
"🇵🇳": ["flag: pn"],
"🇵🇷": ["flag: pr"],
"🇵🇸": ["flag: ps"],
"🇵🇹": ["flag: pt"],
"🇵🇼": ["flag: pw"],
"🇵🇾": ["flag: py"],
"🇶🇦": ["flag: qa"],
"🇷🇪": ["flag: re"],
"🇷🇴": ["flag: ro"],
"🇷🇸": ["flag: rs"],
"🇷🇺": ["flag: ru"],
"🇷🇼": ["flag: rw"],
"🇸🇦": ["flag: sa"],
"🇸🇧": ["flag: sb"],
"🇸🇨": ["flag: sc"],
"🇸🇩": ["flag: sd"],
"🇸🇪": ["flag: se"],
"🇸🇬": ["flag: sg"],
"🇸🇭": ["flag: sh"],
"🇸🇮": ["flag: si"],
"🇸🇯": ["flag: sj"],
"🇸🇰": ["flag: sk"],
"🇸🇱": ["flag: sl"],
"🇸🇲": ["flag: sm"],
"🇸🇳": ["flag: sn"],
"🇸🇴": ["flag: so"],
"🇸🇷": ["flag: sr"],
"🇸🇸": ["flag: ss"],
"🇸🇹": ["flag: st"],
"🇸🇻": ["flag: sv"],
"🇸🇽": ["flag: sx"],
"🇸🇾": ["flag: sy"],
"🇸🇿": ["flag: sz"],
"🇹🇦": ["flag: ta"],
"🇹🇨": ["flag: tc"],
"🇹🇩": ["flag: td"],
"🇹🇫": ["flag: tf"],
"🇹🇬": ["flag: tg"],
"🇹🇭": ["flag: th"],
"🇹🇯": ["flag: tj"],
"🇹🇰": ["flag: tk"],
"🇹🇱": ["flag: tl"],
"🇹🇲": ["flag: tm"],
"🇹🇳": ["flag: tn"],
"🇹🇴": ["flag: to"],
"🇹🇷": ["flag: tr"],
"🇹🇹": ["flag: tt"],
"🇹🇻": ["flag: tv"],
"🇹🇼": ["flag: tw"],
"🇹🇿": ["flag: tz"],
"🇺🇦": ["flag: ua"],
"🇺🇬": ["flag: ug"],
"🇺🇲": ["flag: um"],
"🇺🇳": ["flag: un"],
"🇺🇸": ["flag: us", "flag: usa"],
"🇺🇾": ["flag: uy"],
"🇺🇿": ["flag: uz"],
"🇻🇦": ["flag: va"],
"🇻🇨": ["flag: vc"],
"🇻🇪": ["flag: ve"],
"🇻🇬": ["flag: vg"],
"🇻🇮": ["flag: vi"],
"🇻🇳": ["flag: vn"],
"🇻🇺": ["flag: vu"],
"🇼🇫": ["flag: wf"],
"🇼🇸": ["flag: ws"],
"🇽🇰": ["flag: xk"],
"🇾🇪": ["flag: ye"],
"🇾🇹": ["flag: yt"],
"🇿🇦": ["flag: za"],
"🇿🇲": ["flag: zm"],
"🇿🇼": ["flag: zw"],
"🇦🇨": ["flag ac"],
"🇦🇩": ["flag ad"],
"🇦🇪": ["flag ae"],
"🇦🇫": ["flag af"],
"🇦🇬": ["flag ag"],
"🇦🇮": ["flag ai"],
"🇦🇱": ["flag al"],
"🇦🇲": ["flag am"],
"🇦🇴": ["flag ao"],
"🇦🇶": ["flag aq"],
"🇦🇷": ["flag ar"],
"🇦🇸": ["flag as"],
"🇦🇹": ["flag at"],
"🇦🇺": ["flag au"],
"🇦🇼": ["flag aw"],
"🇦🇽": ["flag ax"],
"🇦🇿": ["flag az"],
"🇧🇦": ["flag ba"],
"🇧🇧": ["flag bb"],
"🇧🇩": ["flag bd"],
"🇧🇪": ["flag be"],
"🇧🇫": ["flag bf"],
"🇧🇬": ["flag bg"],
"🇧🇭": ["flag bh"],
"🇧🇮": ["flag bi"],
"🇧🇯": ["flag bj"],
"🇧🇱": ["flag bl"],
"🇧🇲": ["flag bm"],
"🇧🇳": ["flag bn"],
"🇧🇴": ["flag bo"],
"🇧🇶": ["flag bq"],
"🇧🇷": ["flag br"],
"🇧🇸": ["flag bs"],
"🇧🇹": ["flag bt"],
"🇧🇻": ["flag bv"],
"🇧🇼": ["flag bw"],
"🇧🇾": ["flag by"],
"🇧🇿": ["flag bz"],
"🇨🇦": ["flag ca"],
"🇨🇨": ["flag cc"],
"🇨🇩": ["flag cd"],
"🇨🇫": ["flag cf"],
"🇨🇬": ["flag cg"],
"🇨🇭": ["flag ch"],
"🇨🇮": ["flag ci"],
"🇨🇰": ["flag ck"],
"🇨🇱": ["flag cl"],
"🇨🇲": ["flag cm"],
"🇨🇳": ["flag cn"],
"🇨🇴": ["flag co"],
"🇨🇵": ["flag cp"],
"🇨🇷": ["flag cr"],
"🇨🇺": ["flag cu"],
"🇨🇻": ["flag cv"],
"🇨🇼": ["flag cw"],
"🇨🇽": ["flag cx"],
"🇨🇾": ["flag cy"],
"🇨🇿": ["flag cz"],
"🇩🇪": ["flag de"],
"🇩🇬": ["flag dg"],
"🇩🇯": ["flag dj"],
"🇩🇰": ["flag dk"],
"🇩🇲": ["flag dm"],
"🇩🇴": ["flag do"],
"🇩🇿": ["flag dz"],
"🇪🇦": ["flag ea"],
"🇪🇨": ["flag ec"],
"🇪🇪": ["flag ee"],
"🇪🇬": ["flag eg"],
"🇪🇭": ["flag eh"],
"🇪🇷": ["flag er"],
"🇪🇸": ["flag es"],
"🇪🇹": ["flag et"],
"🇪🇺": ["flag eu"],
"🇫🇮": ["flag fi"],
"🇫🇯": ["flag fj"],
"🇫🇰": ["flag fk"],
"🇫🇲": ["flag fm"],
"🇫🇴": ["flag fo"],
"🇫🇷": ["flag fr"],
"🇬🇦": ["flag ga"],
"🇬🇧": ["flag gb", "flag uk"],
"🇬🇩": ["flag gd"],
"🇬🇪": ["flag ge"],
"🇬🇫": ["flag gf"],
"🇬🇬": ["flag gg"],
"🇬🇭": ["flag gh"],
"🇬🇮": ["flag gi"],
"🇬🇱": ["flag gl"],
"🇬🇲": ["flag gm"],
"🇬🇳": ["flag gn"],
"🇬🇵": ["flag gp"],
"🇬🇶": ["flag gq"],
"🇬🇷": ["flag gr"],
"🇬🇸": ["flag gs"],
"🇬🇹": ["flag gt"],
"🇬🇺": ["flag gu"],
"🇬🇼": ["flag gw"],
"🇬🇾": ["flag gy"],
"🇭🇰": ["flag hk"],
"🇭🇲": ["flag hm"],
"🇭🇳": ["flag hn"],
"🇭🇷": ["flag hr"],
"🇭🇹": ["flag ht"],
"🇭🇺": ["flag hu"],
"🇮🇨": ["flag ic"],
"🇮🇩": ["flag id"],
"🇮🇪": ["flag ie"],
"🇮🇱": ["flag il"],
"🇮🇲": ["flag im"],
"🇮🇳": ["flag in"],
"🇮🇴": ["flag io"],
"🇮🇶": ["flag iq"],
"🇮🇷": ["flag ir"],
"🇮🇸": ["flag is"],
"🇮🇹": ["flag it"],
"🇯🇪": ["flag je"],
"🇯🇲": ["flag jm"],
"🇯🇴": ["flag jo"],
"🇯🇵": ["flag jp"],
"🇰🇪": ["flag ke"],
"🇰🇬": ["flag kg"],
"🇰🇭": ["flag kh"],
"🇰🇮": ["flag ki"],
"🇰🇲": ["flag km"],
"🇰🇳": ["flag kn"],
"🇰🇵": ["flag kp"],
"🇰🇷": ["flag kr"],
"🇰🇼": ["flag kw"],
"🇰🇾": ["flag ky"],
"🇰🇿": ["flag kz"],
"🇱🇦": ["flag la"],
"🇱🇧": ["flag lb"],
"🇱🇨": ["flag lc"],
"🇱🇮": ["flag li"],
"🇱🇰": ["flag lk"],
"🇱🇷": ["flag lr"],
"🇱🇸": ["flag ls"],
"🇱🇹": ["flag lt"],
"🇱🇺": ["flag lu"],
"🇱🇻": ["flag lv"],
"🇱🇾": ["flag ly"],
"🇲🇦": ["flag ma"],
"🇲🇨": ["flag mc"],
"🇲🇩": ["flag md"],
"🇲🇪": ["flag me"],
"🇲🇫": ["flag mf"],
"🇲🇬": ["flag mg"],
"🇲🇭": ["flag mh"],
"🇲🇰": ["flag mk"],
"🇲🇱": ["flag ml"],
"🇲🇲": ["flag mm"],
"🇲🇳": ["flag mn"],
"🇲🇴": ["flag mo"],
"🇲🇵": ["flag mp"],
"🇲🇶": ["flag mq"],
"🇲🇷": ["flag mr"],
"🇲🇸": ["flag ms"],
"🇲🇹": ["flag mt"],
"🇲🇺": ["flag mu"],
"🇲🇻": ["flag mv"],
"🇲🇼": ["flag mw"],
"🇲🇽": ["flag mx"],
"🇲🇾": ["flag my"],
"🇲🇿": ["flag mz"],
"🇳🇦": ["flag na"],
"🇳🇨": ["flag nc"],
"🇳🇪": ["flag ne"],
"🇳🇫": ["flag nf"],
"🇳🇬": ["flag ng"],
"🇳🇮": ["flag ni"],
"🇳🇱": ["flag nl"],
"🇳🇴": ["flag no"],
"🇳🇵": ["flag np"],
"🇳🇷": ["flag nr"],
"🇳🇺": ["flag nu"],
"🇳🇿": ["flag nz"],
"🇴🇲": ["flag om"],
"🇵🇦": ["flag pa"],
"🇵🇪": ["flag pe"],
"🇵🇫": ["flag pf"],
"🇵🇬": ["flag pg"],
"🇵🇭": ["flag ph"],
"🇵🇰": ["flag pk"],
"🇵🇱": ["flag pl"],
"🇵🇲": ["flag pm"],
"🇵🇳": ["flag pn"],
"🇵🇷": ["flag pr"],
"🇵🇸": ["flag ps"],
"🇵🇹": ["flag pt"],
"🇵🇼": ["flag pw"],
"🇵🇾": ["flag py"],
"🇶🇦": ["flag qa"],
"🇷🇪": ["flag re"],
"🇷🇴": ["flag ro"],
"🇷🇸": ["flag rs"],
"🇷🇺": ["flag ru"],
"🇷🇼": ["flag rw"],
"🇸🇦": ["flag sa"],
"🇸🇧": ["flag sb"],
"🇸🇨": ["flag sc"],
"🇸🇩": ["flag sd"],
"🇸🇪": ["flag se"],
"🇸🇬": ["flag sg"],
"🇸🇭": ["flag sh"],
"🇸🇮": ["flag si"],
"🇸🇯": ["flag sj"],
"🇸🇰": ["flag sk"],
"🇸🇱": ["flag sl"],
"🇸🇲": ["flag sm"],
"🇸🇳": ["flag sn"],
"🇸🇴": ["flag so"],
"🇸🇷": ["flag sr"],
"🇸🇸": ["flag ss"],
"🇸🇹": ["flag st"],
"🇸🇻": ["flag sv"],
"🇸🇽": ["flag sx"],
"🇸🇾": ["flag sy"],
"🇸🇿": ["flag sz"],
"🇹🇦": ["flag ta"],
"🇹🇨": ["flag tc"],
"🇹🇩": ["flag td"],
"🇹🇫": ["flag tf"],
"🇹🇬": ["flag tg"],
"🇹🇭": ["flag th"],
"🇹🇯": ["flag tj"],
"🇹🇰": ["flag tk"],
"🇹🇱": ["flag tl"],
"🇹🇲": ["flag tm"],
"🇹🇳": ["flag tn"],
"🇹🇴": ["flag to"],
"🇹🇷": ["flag tr"],
"🇹🇹": ["flag tt"],
"🇹🇻": ["flag tv"],
"🇹🇼": ["flag tw"],
"🇹🇿": ["flag tz"],
"🇺🇦": ["flag ua"],
"🇺🇬": ["flag ug"],
"🇺🇲": ["flag um"],
"🇺🇳": ["flag un"],
"🇺🇸": ["flag us", "flag usa"],
"🇺🇾": ["flag uy"],
"🇺🇿": ["flag uz"],
"🇻🇦": ["flag va"],
"🇻🇨": ["flag vc"],
"🇻🇪": ["flag ve"],
"🇻🇬": ["flag vg"],
"🇻🇮": ["flag vi"],
"🇻🇳": ["flag vn"],
"🇻🇺": ["flag vu"],
"🇼🇫": ["flag wf"],
"🇼🇸": ["flag ws"],
"🇽🇰": ["flag xk"],
"🇾🇪": ["flag ye"],
"🇾🇹": ["flag yt"],
"🇿🇦": ["flag za"],
"🇿🇲": ["flag zm"],
"🇿🇼": ["flag zw"],
"🌀": ["cyclone"],
"🌁": ["foggy"],
"🌂": ["closed umbrella"],
@ -482,7 +482,7 @@ export const EMOJI_MAP = {
"🎾": ["tennis"],
"🎿": ["ski"],
"🏀": ["basketball"],
"🏁": ["flag: checkered", "checkered flag"],
"🏁": ["flag checkered", "checkered flag"],
"🏂": ["snowboarder"],
"🏃‍♀️": ["running"],
"🏃‍♂️": ["running"],
@ -535,14 +535,14 @@ export const EMOJI_MAP = {
"🏮": ["izakaya lantern", "lantern"],
"🏯": ["japanese castle"],
"🏰": ["european castle"],
"🏳️‍🌈": ["flag: rainbow"],
"🏳️‍⚧️": ["flag: transgender"],
"🏳️": ["flag: white", "waving white flag"],
"🏴‍☠️": ["flag: pirate", "pirate flag"],
"🏴󠁧󠁢󠁥󠁮󠁧󠁿": ["flag: england"],
"🏴󠁧󠁢󠁳󠁣󠁴󠁿": ["flag: scotland"],
"🏴󠁧󠁢󠁷󠁬󠁳󠁿": ["flag: wales"],
"🏴": ["flag: black", "waving black flag"],
"🏳️‍🌈": ["flag rainbow"],
"🏳️‍⚧️": ["flag transgender"],
"🏳️": ["flag white", "waving white flag"],
"🏴‍☠️": ["flag pirate", "pirate flag"],
"🏴󠁧󠁢󠁥󠁮󠁧󠁿": ["flag england"],
"🏴󠁧󠁢󠁳󠁣󠁴󠁿": ["flag scotland"],
"🏴󠁧󠁢󠁷󠁬󠁳󠁿": ["flag wales"],
"🏴": ["flag black", "waving black flag"],
"🏵️": ["rosette"],
"🏷️": ["label"],
"🏸": ["badminton racquet and shuttlecock"],
@ -959,30 +959,30 @@ export const EMOJI_MAP = {
"🕌": ["mosque"],
"🕍": ["synagogue"],
"🕎": ["menorah with nine branches"],
"🕐": ["clock 01:00", "clock 13:00", "clock1"],
"🕑": ["clock 02:00", "clock 14:00", "clock2"],
"🕒": ["clock 03:00", "clock 15:00", "clock3"],
"🕓": ["clock 04:00", "clock 16:00", "clock4"],
"🕔": ["clock 05:00", "clock 17:00", "clock5"],
"🕕": ["clock 06:00", "clock 18:00", "clock6"],
"🕖": ["clock 07:00", "clock 19:00", "clock7"],
"🕗": ["clock 08:00", "clock 20:00", "clock8"],
"🕘": ["clock 09:00", "clock 21:00", "clock9"],
"🕙": ["clock 10:00", "clock 22:00", "clock10"],
"🕚": ["clock 11:00", "clock 23:00", "clock11"],
"🕛": ["clock 12:00", "clock 24:00", "clock12"],
"🕜": ["clock 01:30", "clock 13:30", "clock130"],
"🕝": ["clock 02:30", "clock 14:30", "clock230"],
"🕞": ["clock 03:30", "clock 15:30", "clock330"],
"🕟": ["clock 04:30", "clock 16:30", "clock430"],
"🕠": ["clock 05:30", "clock 17:30", "clock530"],
"🕡": ["clock 06:30", "clock 18:30", "clock630"],
"🕢": ["clock 07:30", "clock 19:30", "clock730"],
"🕣": ["clock 08:30", "clock 20:30", "clock830"],
"🕤": ["clock 09:30", "clock 21:30", "clock930"],
"🕥": ["clock 10:30", "clock 22:30", "clock1030"],
"🕦": ["clock 11:30", "clock 23:30", "clock1130"],
"🕧": ["clock 00:30", "clock 12:30", "clock1230"],
"🕐": ["clock 0100", "clock 1300", "clock1"],
"🕑": ["clock 0200", "clock 1400", "clock2"],
"🕒": ["clock 0300", "clock 1500", "clock3"],
"🕓": ["clock 0400", "clock 1600", "clock4"],
"🕔": ["clock 0500", "clock 1700", "clock5"],
"🕕": ["clock 0600", "clock 1800", "clock6"],
"🕖": ["clock 0700", "clock 1900", "clock7"],
"🕗": ["clock 0800", "clock 2000", "clock8"],
"🕘": ["clock 0900", "clock 2100", "clock9"],
"🕙": ["clock 1000", "clock 2200", "clock10"],
"🕚": ["clock 1100", "clock 2300", "clock11"],
"🕛": ["clock 1200", "clock 2400", "clock12"],
"🕜": ["clock 0130", "clock 1330", "clock130"],
"🕝": ["clock 0230", "clock 1430", "clock230"],
"🕞": ["clock 0330", "clock 1530", "clock330"],
"🕟": ["clock 0430", "clock 1630", "clock430"],
"🕠": ["clock 0530", "clock 1730", "clock530"],
"🕡": ["clock 0630", "clock 1830", "clock630"],
"🕢": ["clock 0730", "clock 1930", "clock730"],
"🕣": ["clock 0830", "clock 2030", "clock830"],
"🕤": ["clock 0930", "clock 2130", "clock930"],
"🕥": ["clock 1030", "clock 2230", "clock1030"],
"🕦": ["clock 1130", "clock 2330", "clock1130"],
"🕧": ["clock 0030", "clock 1230", "clock1230"],
"🕯️": ["candle"],
"🕰️": ["mantelpiece clock"],
"🕳️": ["hole"],
@ -1807,53 +1807,52 @@ export const EMOJI_MAP = {
"㊙️": ["secret"],
};
EMOJI_MAP.filter = function (filter) {
const result = {};
if (typeof filter === "function") {
for (const entry of Object.entries(this)) {
if (filter(entry)) {
result[entry[0]] = entry[1];
let _EMOJI_WORDS;
const EMOJIS = {
MAP: EMOJI_MAP,
filter: function (filter) {
const result = {};
if (typeof filter === "function") {
for (const entry of Object.entries(this.MAP)) {
if (filter(entry)) {
result[entry[0]] = entry[1];
}
}
} else if (Array.isArray(filter)) {
const matchers = [];
for (const f of filter) {
matchers.push(
new RegExp(
`[\\u{${parseInt(f[0]).toString(16)}}-\\u{${parseInt(f[1]).toString(16)}}]`,
"u",
),
);
}
for (const entry of Object.entries(this.MAP)) {
const matches = matchers.some((matcher) => matcher.test(entry[0]));
if (matches) {
result[entry[0]] = entry[1];
}
}
} else {
throw new Error("unknown filter type");
}
} else if (Array.isArray(filter)) {
const matchers = [];
for (const f of filter) {
matchers.push(
new RegExp(
`[\\u{${parseInt(f[0]).toString(16)}}-\\u{${parseInt(f[1]).toString(16)}}]`,
"u",
),
return result;
},
autocomplete: function (prompt) {
const results = Object.entries(this.MAP).filter((entry) => {
return entry[1].some(
(name) =>
name.toLowerCase().indexOf(prompt) >= 0 ||
prompt.toLowerCase().indexOf(name) >= 0,
);
}
for (const entry of Object.entries(this)) {
const matches = matchers.some((matcher) => matcher.test(entry[0]));
if (matches) {
result[entry[0]] = entry[1];
}
}
} else {
throw new Error("unknown filter type");
}
return result;
};
let EMOJI_WORDS;
EMOJI_MAP.autocomplete = function (prompt) {
EMOJI_WORDS =
EMOJI_WORDS ??
Object.values(this).reduce((_EMOJI_WORDS, words) => {
if (Array.isArray(words)) {
_EMOJI_WORDS.push(...words);
}
return _EMOJI_WORDS;
}, []);
const results = EMOJI_WORDS.filter((word) => {
return word.toLowerCase().indexOf(prompt) >= 0 || prompt.toLowerCase().indexOf(word) >= 0;
});
return results;
});
return results;
},
};
/* VERY limited testing:

1867
public/js/emojis/en.ts Normal file

File diff suppressed because it is too large Load diff

256
public/js/reactions.js Normal file
View file

@ -0,0 +1,256 @@
const reactions_popup_width = 280;
const reactions_popup_height = 280;
const reactions_popup_styling = `
#reactionspopup {
position: fixed;
width: ${reactions_popup_width}px;
height: ${reactions_popup_height}px;
z-index: 100;
background: inherit;
overflow: hidden;
border: 1px solid var(--border-normal);
padding: 0.5rem;
text-align: center;
}
#reactionspopup .icon.close {
float: right;
margin: 0.5rem;
}
#reactionspopup input[name="search"] {
width: 80%;
}
#reactionspopup ul {
margin-top: 0.5rem;
padding: 0.5rem;
text-align: left;
overflow: scroll;
}
#reactionspopup ul li {
display: inline-block;
cursor: pointer;
}
#reactionspopup ul[data-filtered] li {
display: none;
visibility: hidden;
}
#reactionspopup ul[data-filtered] li[data-filtered] {
display: inline-block;
visibility: visible;
}
#reactionspopup #reactions-names-display {
position: absolute;
left: 0;
bottom: 0;
right: 0;
height: 2rem;
background: inherit;
}
`;
function get_best_coords_for_popup(target, offset = { x: 10, y: 10 }) {
const target_x = target?.getBoundingClientRect().left ?? 0;
const target_y = target?.getBoundingClientRect().top ?? 0;
const viewport_width = document.body.getBoundingClientRect().width;
const viewport_height = document.body.getBoundingClientRect().height;
const best_coords = {
x: target_x + offset.x,
y: target_y + offset.y,
};
if (target_x + offset.x + reactions_popup_width + offset.x > viewport_width) {
best_coords.x = Math.max(0, target_x - reactions_popup_width);
}
if (target_y + offset.y + reactions_popup_height + offset.y > viewport_height) {
best_coords.y = Math.max(0, target_y - reactions_popup_height);
}
return best_coords;
}
let reactions_popup;
let reactions_popup_form;
let reactions_popup_parent_id_input;
let reactions_popup_emojis_list;
let reactions_popup_reaction_input;
function open_reactions_popup(event) {
const parent_event_id = event.target?.closest("[data-event_id]")?.dataset?.event_id;
reactions_popup_parent_id_input.value = parent_event_id ?? "";
const position = get_best_coords_for_popup(event.target.closest("[data-reactions]"), {
x: 25,
y: 25,
});
reactions_popup.style.left = position.x + "px";
reactions_popup.style.top = position.y + "px";
reactions_popup.style.visibility = "visible";
reactions_popup.style.opacity = "1";
reactions_popup.style.display = "block";
}
function clear_reactions_popup() {
if (!reactions_popup) {
return;
}
reactions_popup.style.visibility = "hidden";
reactions_popup.style.opacity = "0";
reactions_popup.style.display = "none";
}
document.addEventListener("DOMContentLoaded", () => {
if (!document.getElementById("reactions-styling")) {
const style = document.createElement("style");
style.id = "reactions-styling";
style.innerHTML = reactions_popup_styling;
document.head.appendChild(style);
}
reactions_popup = document.createElement("div");
reactions_popup.id = "reactionspopup";
reactions_popup.innerHTML = `
<div class="icon close" onclick="clear_reactions_popup()"></div>
<form
id="reactions-selection-form"
data-smart="true"
method="POST"
on_reply="async (event) => { await document.querySelectorAll( '[data-feed]' ).forEach((feed) => feed.__render(event)); }"
on_parsed="async (event) => { await document.querySelectorAll( '[data-feed]' ).forEach((feed) => feed.__render(event)); }"
>
<input id="reactions-search-input" name="search" type="text" placeholder="Search..." data-skip="true" />
<input type="hidden" name="type" value="reaction" />
<input
type="hidden"
name="id"
generator="(_input, form) => 'TEMP-' + form.__submitted_at.toISOString()"
reset-on-submit
/>
<input
type="hidden"
name="meta.temp_id"
generator="(_input, form) => 'TEMP-' + form.__submitted_at.toISOString()"
reset-on-submit
/>
<input
type="hidden"
name="creator_id"
generator="() => { return JSON.parse( document.body.dataset.user ?? '{}' ).id; }"
/>
<input
type="hidden"
name="timestamps.created"
generator="(_input, form) => form.__submitted_at.toISOString()"
reset-on-submit
/>
<input
type="hidden"
name="timestamps.updated"
generator="(_input, form) => form.__submitted_at.toISOString()"
reset-on-submit
/>
<input type="hidden" name="parent_id" reset-on-submit />
<input type="hidden" name="data.reaction" reset-on-submit />
<ul id="reactions-emojis-list">
${Object.keys(EMOJIS.MAP)
.map(
(emoji) =>
`<li data-emoji="${emoji}" data-names="${EMOJIS.MAP[emoji].map((name) => `:${name}:`).join(" ")}">${emoji}</li>`,
)
.join("\n")}
</ul>
</form>
<div id="reactions-names-display"></div>
`;
document.body.appendChild(reactions_popup);
reactions_popup_form = document.getElementById("reactions-selection-form");
document.addEventListener("topic_changed", ({ detail: { topic_id } }) => {
const reaction_topic_id = topic_id ?? document.body.dataset.topic;
reactions_popup_form.action = reaction_topic_id
? `/api/topics/${reaction_topic_id}/events`
: "";
});
reactions_popup_parent_id_input = reactions_popup_form.querySelector('[name="parent_id"]');
reactions_popup_emojis_list = document.getElementById("reactions-emojis-list");
reactions_popup_reaction_input = reactions_popup_form.querySelector('[name="data.reaction"]');
reactions_popup_names_display = document.getElementById("reactions-names-display");
reactions_popup_emojis_list.querySelectorAll("li[data-emoji]").forEach((emoji_selector) => {
emoji_selector.addEventListener("click", (event) => {
event.preventDefault();
const selector = event.target;
const emoji = selector.dataset.emoji;
reactions_popup_reaction_input.value = emoji;
reactions_popup_form.requestSubmit();
});
});
reactions_popup_form.addEventListener("mouseover", (event) => {
reactions_popup_names_display.textContent = event.target.matches("li[data-names]")
? event.target.dataset.names
: "";
});
const reactions_popup_search = debounce((event) => {
const prompt = event.target?.value;
const filtered = EMOJIS.autocomplete(prompt);
delete emojis_list.dataset.filtered;
if (filtered.length) {
emojis_list.dataset.filtered = true;
emojis_list.querySelectorAll("li").forEach((li) => {
if (filtered.some((entry) => entry[0] === li.dataset.emoji)) {
li.dataset.filtered = true;
} else {
delete li.dataset.filtered;
}
});
}
}, 200);
document
.getElementById("reactions-search-input")
.addEventListener("input", reactions_popup_search);
document
.getElementById("reactions-search-input")
.addEventListener("paste", reactions_popup_search);
document
.getElementById("reactions-search-input")
.addEventListener("change", reactions_popup_search);
document.querySelector("body").addEventListener("click", (event) => {
const is_a_data_reactions_child = event?.target?.closest("[data-reactions]");
if (!is_a_data_reactions_child) {
clear_reactions_popup();
return;
}
event.preventDefault();
open_reactions_popup(event);
});
});

View file

@ -14,11 +14,13 @@ function smarten_feeds() {
continue;
}
const feed_item_template = feed.querySelector("template");
if (!feed_item_template) {
console.warn("No template for smart feed: " + feed);
continue;
}
feed.__templates = feed
.querySelectorAll("template[data-for_type]")
.values()
.reduce((_templates, template) => {
_templates[template.dataset.for_type] = template;
return _templates;
}, {});
feed.__start = () => {
feed.__started = true;
@ -65,17 +67,26 @@ function smarten_feeds() {
};
feed.__target = (item) => {
return feed.__target_element?.(item) ?? feed;
if (!feed.__target_element) {
return feed;
}
return feed.__target_element(item);
};
feed.__autoscroll_debounce_timeout = undefined;
feed.__render = async (item) => {
const template = feed.__templates[item.type];
if (!template) {
return;
}
feed.__context =
feed.__context ??
(feed.dataset.context ? new Function(feed.dataset.context) : undefined);
const context = feed.__context ? await feed.__context(item, feed) : {};
const rendered_html = eval("`" + feed_item_template.innerHTML.trim() + "`");
const rendered_html = eval("`" + template.innerHTML.trim() + "`");
const existing_element =
feed.querySelector("#" + item.id?.replace(/([:\.])/g, "\\$1")) ??
@ -106,6 +117,10 @@ function smarten_feeds() {
}
} else {
const target = feed.__target(item);
if (!target) {
return;
}
switch (feed.dataset.insert ?? "append") {
case "prepend":
target.insertAdjacentHTML("afterbegin", rendered_html);
@ -120,7 +135,7 @@ function smarten_feeds() {
break;
}
if (feed.dataset.autoscroll) {
if (target === feed && feed.dataset.autoscroll) {
if (feed.__autoscroll_debounce_timeout) {
clearTimeout(feed.__autoscroll_debounce_timeout);
}

View file

@ -32,6 +32,10 @@ function smarten_forms() {
for (const [key, value] of form_data.entries()) {
const input = form.querySelector(`[name="${key}"]`);
if (input.dataset.skip) {
continue;
}
if (input.type === "file") {
if (input.dataset["smartformsSaveToHome"]) {
form.uploaded = [];

View file

@ -1,27 +1,36 @@
let enhance_textareas_debounce_timeout;
function enhance_textareas() {
const textareas = document.body.querySelectorAll("textarea:not([data-enhanced])");
for (const textarea of textareas) {
const max_length_attr = textarea.getAttribute("maxlength");
if (/^\d+$/.test(max_length_attr)) {
const max_length = parseInt(max_length_attr, 10);
if (enhance_textareas_debounce_timeout) {
clearTimeout(enhance_textareas_debounce_timeout);
}
function on_updated() {
const counters = this.parentElement.querySelectorAll(
`[data-limit-counter-for="${this.name}"]`,
);
for (const counter of counters) {
counter.innerHTML = `${this.value.length} / ${max_length}`;
enhance_textareas_debounce_timeout = setTimeout(() => {
enhance_textareas_debounce_timeout = undefined;
const textareas = document.body.querySelectorAll("textarea:not([data-enhanced])");
for (const textarea of textareas) {
const max_length_attr = textarea.getAttribute("maxlength");
if (/^\d+$/.test(max_length_attr)) {
const max_length = parseInt(max_length_attr, 10);
function on_updated() {
const counters = this.parentElement.querySelectorAll(
`[data-limit-counter-for="${this.name}"]`,
);
for (const counter of counters) {
counter.innerHTML = `${this.value.length} / ${max_length}`;
}
}
textarea.addEventListener("keyup", on_updated);
textarea.addEventListener("paste", on_updated);
textarea.addEventListener("blur", on_updated);
on_updated.call(textarea);
}
textarea.addEventListener("keyup", on_updated);
textarea.addEventListener("paste", on_updated);
textarea.addEventListener("blur", on_updated);
on_updated.call(textarea);
textarea.dataset.enhanced = true;
}
textarea.dataset.enhanced = true;
}
}, 10);
}
const textarea_enhancement_observer = new MutationObserver(enhance_textareas);

View file

@ -174,8 +174,14 @@
};
}
</script>
<template>
<div id="${ context.blurb.id }" class="blurb-container" data-creator_id="${context.creator.id}" data-blurb_id="${context.blurb.id}" data-temp_id="${context.blurb.meta?.temp_id ?? ""}">
<template data-for_type="blurb">
<div
id="${ context.blurb.id }"
class="blurb-container"
data-event_id="${context.blurb.id}"
data-creator_id="${context.creator.id}"
data-blurb_id="${context.blurb.id}"
data-temp_id="${context.blurb.meta?.temp_id ?? ""}">
<div class="media-preview-container">
${context.blurb.data?.media?.length ? context.blurb.data.media.map(function(url) { return `<img src='${url}' />`; }).join('\n') : ''}
</div>

View file

@ -234,10 +234,25 @@
}
#chat .message-container .message-content-container,
#chat .message-container .message-media-container {
#chat .message-container .message-media-container,
#chat .message-container .reactions-container {
padding-left: 8rem;
}
#chat .message-container .reactions-container:has(> .reaction-container) {
margin-top: 0.5rem;
margin-bottom: 0.25rem;
}
#chat .message-container .reactions-container .reaction-container {
display: inline-block;
border: 1px solid var(--border-subtle);
border-radius: var(--border-radius);
margin-right: 0.5rem;
padding: 0.25rem;
font-size: large;
}
#chat .embed-container {
position: relative;
width: 100%;
@ -327,7 +342,8 @@
}
#chat .message-container .message-content-container,
#chat .message-container .message-media-container {
#chat .message-container .message-media-container,
#chat .message-container .reactions-container {
padding-left: 4rem;
}

View file

@ -22,7 +22,7 @@
id="chat-content"
data-feed
data-precheck="!!document.body.dataset.user && document.body.dataset.user.indexOf( 'topics.chat.read' ) !== -1"
data-source="/api/topics/${ document.body.dataset.topic }/events?type=chat&limit=100&sort=newest&wait=true&after_id=${ feed.__newest_id ?? 'chat:able-able-able-able-able-able-able-able-able-able' }"
data-source="/api/topics/${ document.body.dataset.topic }/events?type=chat,reaction&limit=100&sort=newest&wait=true&after_id=${ feed.__newest_id ?? 'chat:able-able-able-able-able-able-able-able-able-able' }"
data-longpolling="true"
data-reverse="true"
data-insert="append"
@ -77,12 +77,33 @@
user_tick_tock_class,
};
};
feed.__target_element = (item) => {
let target = feed;
switch (item.type) {
case "reaction":
target = document.querySelector(
`.message-container[data-event_id='${item.parent_id}'] > .reactions-container`,
);
break;
case "chat":
default:
target =
document.querySelector(
`.message-container[data-event_id='${item.parent_id}'] > .replies-container`,
) ?? feed;
break;
}
return target;
};
}
</script>
<template>
<template data-for_type="chat">
<div
id="${context.event.id}"
class="message-container ${context.user_tick_tock_class} ${context.time_tick_tock_class}"
data-event_id="${context.event.id}"
data-creator_id="${context.creator.id}"
data-temp_id="${context.event.meta?.temp_id ?? ''}"
>
@ -99,7 +120,12 @@
<div class="icon more-borderless"></div>
</label>
<button class="message-action mockup" data-action="react">
<button
class="message-action"
data-action="react"
data-reactions
data-smart
>
<i class="icon circle"></i><span class="action-name">React</span>
</button>
<button
@ -140,6 +166,19 @@
<div class="message-media-container">
${htmlify(context.event.data.media?.join("\n") ?? "")}
</div>
<div class="reactions-container"></div>
<div class="replies-container"></div>
</div>
</template>
<template data-for_type="reaction">
<div
id="${context.event.id}"
class="reaction-container"
data-event_id="${context.event.id}"
data-creator_id="${context.creator.id}"
data-temp_id="${context.event.meta?.temp_id ?? ''}"
>
<span class="reaction">${ context.event.data.reaction }</span>
</div>
</template>
</div>

View file

@ -145,10 +145,11 @@
};
}
</script>
<template>
<template data-for_type="essay">
<div
id="${context.essay.id}"
class="essay-container"
data-event_id="${context.essay.id}"
data-creator_id="${context.creator.id}"
data-essay_id="${context.essay.id}"
data-temp_id="${context.essay.meta?.temp_id ?? ''}"

View file

@ -175,10 +175,11 @@
};
}
</script>
<template>
<template data-for_type="post">
<div
id="${context.post.id}"
class="post-container"
data-event_id="${context.post.id}"
data-creator_id="${context.creator.id}"
data-post_id="${context.post.id}"
>