Merge branch 'frost' of https://github.com/Aevann1/rDrama into frost

master
Aevann1 2022-09-26 03:15:41 +02:00
commit 2dc7805df8
28 changed files with 355 additions and 92 deletions

View File

@ -1,11 +1,13 @@
{
"name": "chat",
"version": "0.0.24",
"version": "0.1.10",
"main": "./src/index.tsx",
"license": "MIT",
"dependencies": {
"@types/humanize-duration": "^3.27.1",
"@types/lodash.clonedeep": "^4.5.7",
"@types/lodash.debounce": "^4.0.7",
"@types/lodash.throttle": "^4.1.7",
"@types/lozad": "^1.16.1",
"@types/react": "^18.0.20",
"@types/react-dom": "^18.0.6",
@ -15,7 +17,9 @@
"dotenv": "^16.0.2",
"esbuild": "^0.15.7",
"humanize-duration": "^3.27.3",
"lodash.clonedeep": "^4.5.0",
"lodash.debounce": "^4.0.8",
"lodash.throttle": "^4.1.1",
"lozad": "^1.16.0",
"react": "^18.2.0",
"react-dnd": "^16.0.1",

View File

@ -1,5 +1,6 @@
html,
body {
overflow: hidden;
overscroll-behavior-y: none;
}
@ -9,6 +10,8 @@ html {
body {
min-height: 100vh;
min-height: calc(var(--vh, 1vh) * 100);
overflow: hidden;
/* mobile viewport bug fix */
min-height: -webkit-fill-available;
}
@ -25,8 +28,8 @@ body {
overflow: hidden;
display: flex;
flex-direction: column;
margin: 0 auto;
max-width: 1000px;
justify-content: center;
margin: 0 2rem;
}
.App-heading {
@ -37,6 +40,7 @@ body {
.App-heading small {
opacity: 0.2;
font-size: 10px;
}
.App-side {
@ -50,6 +54,7 @@ body {
position: relative;
flex: 3;
height: 62vh;
height: calc(var(--vh, 1vh) * 72);
max-height: 1000px;
overflow: auto;
-ms-overflow-style: none;
@ -94,6 +99,10 @@ body {
/* On mobile, hide the sidebar and make the input full-width. */
@media screen and (max-width: 1100px) {
.App-wrapper {
margin: 0 auto;
}
.App-heading {
padding: 0 1rem;
}
@ -112,7 +121,7 @@ body {
}
.App-content__reduced {
height: 58vh;
height: calc(var(--vh, 1vh) * 65);
}
}
@ -122,4 +131,9 @@ lite-youtube {
.btn-secondary {
border: none !important;
}
.btn-secondary:focus {
border: none !important;
box-shadow: none !important;
}

View File

@ -2,6 +2,7 @@ import React, { useEffect, useRef } from "react";
import { DndProvider, useDrop } from "react-dnd";
import { HTML5Backend } from "react-dnd-html5-backend";
import cx from "classnames";
import throttle from "lodash.throttle";
import {
ChatHeading,
ChatMessageList,
@ -14,6 +15,7 @@ import { ChatProvider, DrawerProvider, useChat, useDrawer } from "./hooks";
import "./App.css";
const SCROLL_CANCEL_THRESHOLD = 500;
const WINDOW_RESIZE_THROTTLE_WAIT = 250;
export function App() {
return (
@ -36,6 +38,26 @@ function AppInner() {
const initiallyScrolledDown = useRef(false);
const { messages, quote } = useChat();
// See: https://css-tricks.com/the-trick-to-viewport-units-on-mobile/
useEffect(() => {
const updateViewportHeightUnit = () => {
const vh = window.innerHeight * 0.01;
document.documentElement.style.setProperty("--vh", `${vh}px`);
};
const throttledResizeHandler = throttle(
updateViewportHeightUnit,
WINDOW_RESIZE_THROTTLE_WAIT
);
throttledResizeHandler();
window.addEventListener("resize", throttledResizeHandler);
return () => {
window.removeEventListener("resize", throttledResizeHandler);
};
}, []);
useEffect(() => {
if (messages.length > 0) {
if (initiallyScrolledDown.current) {
@ -95,7 +117,7 @@ function AppInner() {
<div className="App-bottom">
{quote && (
<div className="App-bottom-extra">
{quote && <QuotedMessage />}
<QuotedMessage />
</div>
)}
<UserInput />

View File

@ -1,17 +1,11 @@
@keyframes fading-in {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
.ChatMessage {
position: relative;
animation: fading-in 0.3s ease-in-out forwards;
padding: 1rem;
padding-right: 3rem;
padding-right: 1.5rem;
min-height: 28px;
}
.ChatMessage p {
margin: 0;
}
.ChatMessage .btn {
@ -25,6 +19,8 @@
.ChatMessage-timestamp {
margin-left: 0.5rem;
opacity: 0.5;
font-size: 10px;
}
.ChatMessage-bottom {
@ -41,20 +37,78 @@
}
.ChatMessage-button {
background: transparent !important;
margin: 0 0.5rem;
}
.ChatMessage-button__confirmed i {
.ChatMessage-button i {
margin-right: 0.5rem;
}
.ChatMessage-button__confirmed {
color: red !important;
}
.ChatMessage-delete {
.ChatMessage-quoted-link {
padding-left: 2rem;
}
.ChatMessage-actions-button {
position: absolute;
top: 4px;
right: 4px;
top: 0;
right: 0;
cursor: pointer;
z-index: 5;
background: none !important;
border: none !important;
box-shadow: none !important;
display: flex;
align-items: center;
}
.ChatMessage-actions-button button {
background: none !important;
border: none !important;
padding: 0 !important;
}
.ChatMessage-actions-button button i {
position: relative;
top: 3px;
margin-right: 1rem;
}
.ChatMessage-actions {
z-index: 1;
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
background: rgba(20, 20, 20, 0.85);
display: flex;
align-items: center;
justify-content: flex-end;
padding: 1rem;
padding-right: 3rem;
animation: fading-in 0.3s ease-in-out forwards;
}
.ChatMessage-actions button {
font-size: 10px;
background: none !important;
}
/* List */
.ChatMessageList {
flex: 1;
}
}
.ChatMessageList-group {
margin-bottom: 1rem;
padding: 0.3rem;
border-radius: 8px;
}
.ChatMessageList-group:nth-child(even) {
background: rgba(255, 255, 255, 0.025);
}

View File

@ -1,12 +1,15 @@
import React, {
useCallback,
useEffect,
useLayoutEffect,
useMemo,
useRef,
useState,
} from "react";
import cx from "classnames";
import key from "weak-key";
import humanizeDuration from "humanize-duration";
import cloneDeep from "lodash.clonedeep";
import { Username } from "./Username";
import { useChat, useRootContext } from "../../hooks";
import { QuotedMessageLink } from "./QuotedMessageLink";
@ -16,6 +19,8 @@ interface ChatMessageProps {
message: IChatMessage;
timestampUpdates: number;
showUser?: boolean;
actionsOpen: boolean;
onToggleActions(messageId: string): void;
}
const TIMESTAMP_UPDATE_INTERVAL = 20000;
@ -24,6 +29,8 @@ export function ChatMessage({
message,
showUser = true,
timestampUpdates,
actionsOpen,
onToggleActions,
}: ChatMessageProps) {
const {
id,
@ -38,19 +45,25 @@ export function ChatMessage({
quotes,
} = message;
const {
username: loggedInUsername,
id: userId,
username: userUsername,
admin,
censored,
themeColor,
} = useRootContext();
const { messageLookup, deleteMessage, quoteMessage } = useChat();
const [confirmedDelete, setConfirmedDelete] = useState(false);
const quotedMessage = messageLookup[quotes];
const content = censored ? text_censored : text_html;
const hasMention = content.includes(loggedInUsername);
const mentionStyle = hasMention
? { backgroundColor: `#${themeColor}55` }
: {};
const [confirmedDelete, setConfirmedDelete] = useState(false);
const isMention =
quotedMessage?.username === userUsername ||
(text_html.includes(`/id/${userId}`) &&
userUsername &&
username !== userUsername);
const timestamp = useMemo(
() => formatTimeAgo(time),
[time, timestampUpdates]
);
const handleDeleteMessage = useCallback(() => {
if (confirmedDelete) {
deleteMessage(text);
@ -58,13 +71,76 @@ export function ChatMessage({
setConfirmedDelete(true);
}
}, [text, confirmedDelete]);
const timestamp = useMemo(
() => formatTimeAgo(time),
[time, timestampUpdates]
);
const handleQuoteMessageAction = useCallback(() => {
quoteMessage(message);
onToggleActions(message.id);
}, [message, onToggleActions]);
useEffect(() => {
if (!actionsOpen) {
setConfirmedDelete(false);
}
}, [actionsOpen]);
return (
<div className="ChatMessage" style={mentionStyle} id={id}>
<div
className={cx("ChatMessage", {
ChatMessage__showingUser: showUser,
ChatMessage__isMention: isMention,
})}
id={id}
style={
isMention
? {
background: `#${themeColor}25`,
borderLeft: `1px solid #${themeColor}`,
}
: {}
}
>
{!actionsOpen && (
<div className="ChatMessage-actions-button">
<button
className="btn btn-secondary"
onClick={() => quoteMessage(message)}
>
<i className="fas fa-reply" />
</button>
<button
className="btn btn-secondary"
onClick={() => onToggleActions(id)}
>
...
</button>
</div>
)}
{actionsOpen && (
<div className="ChatMessage-actions">
<button
className="btn btn-secondary ChatMessage-button"
onClick={handleQuoteMessageAction}
>
<i className="fas fa-reply" /> Reply
</button>
{admin && (
<button
className={cx("btn btn-secondary ChatMessage-button", {
"ChatMessage-button__confirmed": confirmedDelete,
})}
onClick={handleDeleteMessage}
>
<i className="fas fa-trash-alt" />{" "}
{confirmedDelete ? "Are you sure?" : "Delete"}
</button>
)}
<button
className="btn btn-secondary ChatMessage-button"
onClick={() => onToggleActions(id)}
>
<i>X</i> Close
</button>
</div>
)}
{showUser && (
<div className="ChatMessage-top">
<Username
@ -76,7 +152,11 @@ export function ChatMessage({
<div className="ChatMessage-timestamp">{timestamp}</div>
</div>
)}
{quotes && quotedMessage && <QuotedMessageLink message={quotedMessage} />}
{quotes && quotedMessage && (
<div className="ChatMessage-quoted-link">
<QuotedMessageLink message={quotedMessage} />
</div>
)}
<div className="ChatMessage-bottom">
<div>
<span
@ -86,31 +166,27 @@ export function ChatMessage({
__html: content,
}}
/>
<button
className="ChatMessage-button quote btn"
onClick={() => quoteMessage(message)}
>
<i className="fas fa-reply"></i>
</button>
</div>
{admin && (
<button
className={cx("ChatMessage-button ChatMessage-delete btn", {
"ChatMessage-button__confirmed": confirmedDelete,
})}
onClick={handleDeleteMessage}
>
<i className="fas fa-trash-alt"></i>
</button>
)}
</div>
</div>
);
}
export function ChatMessageList() {
const listRef = useRef<HTMLDivElement>(null);
const { messages } = useChat();
const [timestampUpdates, setTimestampUpdates] = useState(0);
const groupedMessages = useMemo(() => groupMessages(messages), [messages]);
const [actionsOpenForMessage, setActionsOpenForMessage] = useState<
string | null
>(null);
const handleToggleActionsForMessage = useCallback(
(messageId: string) =>
setActionsOpenForMessage(
messageId === actionsOpenForMessage ? null : messageId
),
[actionsOpenForMessage]
);
useEffect(() => {
const updatingTimestamps = setInterval(
@ -123,27 +199,86 @@ export function ChatMessageList() {
};
}, []);
useLayoutEffect(() => {
const images = Array.from(
listRef.current.getElementsByTagName("img")
).filter((image) => image.dataset.src);
for (const image of images) {
image.src = image.dataset.src;
image.dataset.src = undefined;
}
}, [messages]);
return (
<div className="ChatMessageList">
{messages.map((message, index) => (
<ChatMessage
key={key(message)}
message={message}
timestampUpdates={timestampUpdates}
showUser={message.username !== messages[index - 1]?.username}
/>
<div className="ChatMessageList" ref={listRef}>
{groupedMessages.map((group) => (
<div key={key(group)} className="ChatMessageList-group">
{group.map((message, index) => (
<ChatMessage
key={key(message)}
message={message}
timestampUpdates={timestampUpdates}
showUser={index === 0}
actionsOpen={actionsOpenForMessage === message.id}
onToggleActions={handleToggleActionsForMessage}
/>
))}
</div>
))}
</div>
);
}
function formatTimeAgo(time: number) {
const now = new Date().getTime();
const humanized = `${humanizeDuration(time * 1000 - now, {
const shortEnglishHumanizer = humanizeDuration.humanizer({
language: "shortEn",
languages: {
shortEn: {
y: () => "y",
mo: () => "mo",
w: () => "w",
d: () => "d",
h: () => "h",
m: () => "m",
s: () => "s",
ms: () => "ms",
},
},
round: true,
units: ["h", "m", "s"],
largest: 2,
})} ago`;
spacer: "",
delimiter: ", ",
});
const now = new Date().getTime();
const humanized = `${shortEnglishHumanizer(time * 1000 - now)} ago`;
return humanized === "0 seconds ago" ? "just now" : humanized;
return humanized === "0s ago" ? "just now" : humanized;
}
function groupMessages(messages: IChatMessage[]) {
const grouped: IChatMessage[][] = [];
let lastUsername = "";
let temp: IChatMessage[] = [];
for (const message of messages) {
if (!lastUsername) {
lastUsername = message.username;
}
if (message.username === lastUsername) {
temp.push(message);
} else {
grouped.push(cloneDeep(temp));
lastUsername = message.username;
temp = [message];
}
}
if (temp.length > 0) {
grouped.push(cloneDeep(temp));
}
return grouped;
}

View File

@ -0,0 +1,3 @@
.QuotedMessageLink {
font-size: 10px;
}

View File

@ -1,5 +1,6 @@
import React, { useCallback, useMemo } from "react";
import { useRootContext } from "../../hooks";
import "./QuotedMessageLink.css";
const SCROLL_TO_QUOTED_OVERFLOW = 250;
const QUOTED_MESSAGE_CONTEXTUAL_HIGHLIGHTING_DURATION = 2500;
@ -40,8 +41,8 @@ export function QuotedMessageLink({ message }: { message: IChatMessage }) {
}, [message, censored]);
return (
<a href="#" onClick={handleLinkClick}>
Replying to @{message.username}:{" "}
<a className="QuotedMessageLink" href="#" onClick={handleLinkClick}>
<i className="fas fa-reply" /> @{message.username}:{" "}
<em>"{replyText}"</em>
</a>
);

View File

@ -47,7 +47,7 @@ export function UserInput() {
);
const handleKeyUp = useCallback(
(event: KeyboardEvent<HTMLTextAreaElement>) => {
if (event.key === "Enter") {
if (event.key === "Enter" && !event.shiftKey) {
handleSendMessage();
}
},

View File

@ -1,4 +1,5 @@
.UsersTyping {
height: 18px;
display: inline-block;
font-size: 10px;
}

View File

@ -9,7 +9,6 @@ import React, {
useState,
} from "react";
import { io, Socket } from "socket.io-client";
import lozad from "lozad";
import debounce from "lodash.debounce";
import { useRootContext } from "./useRootContext";
import { useWindowFocus } from "./useWindowFocus";
@ -178,17 +177,6 @@ export function ChatProvider({ children }: PropsWithChildren) {
title.innerHTML = alertedWhileAway ? `[+${notifications}] Chat` : "Chat";
}, [notifications, focused]);
// Setup Lozad
useEffect(() => {
const { observe, observer } = lozad();
observe();
return () => {
observer.disconnect();
};
}, []);
return (
<ChatContext.Provider value={context}>{children}</ChatContext.Provider>
);

View File

@ -39,6 +39,13 @@
resolved "https://registry.yarnpkg.com/@types/humanize-duration/-/humanize-duration-3.27.1.tgz#f14740d1f585a0a8e3f46359b62fda8b0eaa31e7"
integrity sha512-K3e+NZlpCKd6Bd/EIdqjFJRFHbrq5TzPPLwREk5Iv/YoIjQrs6ljdAUCo+Lb2xFlGNOjGSE0dqsVD19cZL137w==
"@types/lodash.clonedeep@^4.5.7":
version "4.5.7"
resolved "https://registry.yarnpkg.com/@types/lodash.clonedeep/-/lodash.clonedeep-4.5.7.tgz#0e119f582ed6f9e6b373c04a644651763214f197"
integrity sha512-ccNqkPptFIXrpVqUECi60/DFxjNKsfoQxSQsgcBJCX/fuX1wgyQieojkcWH/KpE3xzLoWN/2k+ZeGqIN3paSvw==
dependencies:
"@types/lodash" "*"
"@types/lodash.debounce@^4.0.7":
version "4.0.7"
resolved "https://registry.yarnpkg.com/@types/lodash.debounce/-/lodash.debounce-4.0.7.tgz#0285879defb7cdb156ae633cecd62d5680eded9f"
@ -46,6 +53,13 @@
dependencies:
"@types/lodash" "*"
"@types/lodash.throttle@^4.1.7":
version "4.1.7"
resolved "https://registry.yarnpkg.com/@types/lodash.throttle/-/lodash.throttle-4.1.7.tgz#4ef379eb4f778068022310ef166625f420b6ba58"
integrity sha512-znwGDpjCHQ4FpLLx19w4OXDqq8+OvREa05H89obtSyXyOFKL3dDjCslsmfBz0T2FU8dmf5Wx1QvogbINiGIu9g==
dependencies:
"@types/lodash" "*"
"@types/lodash@*":
version "4.14.185"
resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.185.tgz#c9843f5a40703a8f5edfd53358a58ae729816908"
@ -390,11 +404,21 @@ inherits@2:
resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499"
integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==
lodash.clonedeep@^4.5.0:
version "4.5.0"
resolved "https://registry.yarnpkg.com/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz#e23f3f9c4f8fbdde872529c1071857a086e5ccef"
integrity sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ==
lodash.debounce@^4.0.8:
version "4.0.8"
resolved "https://registry.yarnpkg.com/lodash.debounce/-/lodash.debounce-4.0.8.tgz#82d79bff30a67c4005ffd5e2515300ad9ca4d7af"
integrity sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==
lodash.throttle@^4.1.1:
version "4.1.1"
resolved "https://registry.yarnpkg.com/lodash.throttle/-/lodash.throttle-4.1.1.tgz#c23e91b710242ac70c37f1e1cda9274cc39bf2f4"
integrity sha512-wIkUCfVKpVsWo3JSZlc+8MB5it+2AN5W8J7YVMST30UrvcQNZ1Okbj+rbVniijTWE6FGYy4XJq/rHkas8qJMLQ==
lodash@^4.17.21:
version "4.17.21"
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c"

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

View File

@ -313,14 +313,27 @@ function showmore() {
btn.innerHTML = 'SHOW LESS'
}
function formatDate(d) {
var year = d.getFullYear();
var monthAbbr = d.toLocaleDateString('en-us', {month: 'short'});
var day = d.getDate();
var hour = ("0" + d.getHours()).slice(-2);
var minute = ("0" + d.getMinutes()).slice(-2);
var second = ("0" + d.getSeconds()).slice(-2);
var tzAbbr = d.toLocaleTimeString('en-us', {timeZoneName: 'short'}).split(' ')[2];
return (day + " " + monthAbbr + " " + year + " "
+ hour + ":" + minute + ":" + second + " " + tzAbbr);
}
const timestamps = document.querySelectorAll('[data-time]');
for (const e of timestamps) {
const date = new Date(e.dataset.time*1000);
e.innerHTML = date.toString();
e.innerHTML = formatDate(date);
};
function timestamp(str, ti) {
const date = new Date(ti*1000);
document.getElementById(str).setAttribute("data-bs-original-title", date.toString());
};
document.getElementById(str).setAttribute("data-bs-original-title", formatDate(date));
};

View File

@ -113,7 +113,7 @@ def settings_profile_post(v):
v.spider = int(request.values.get("spider") == 'true')
if v.spider: badge_grant(user=v, badge_id=179)
else:
badge = v.has_badge(170)
badge = v.has_badge(179)
if badge: g.db.delete(badge)
elif request.values.get("bio") == "":

View File

@ -222,16 +222,16 @@ INSERT INTO public.hat_defs VALUES (722, 'Darth Vader Helmet', 'Cool-looking guy
INSERT INTO public.hat_defs VALUES (725, 'New Years Glasses', 'Happy New Year!', 2, 500, NULL, 1663892467);
INSERT INTO public.hat_defs VALUES (726, 'Operator', 'This classical variables has been quantized and is now a Hermitian operator on a Hillbert space.', 2, 500, NULL, 1663893198);
INSERT INTO public.hat_defs VALUES (1, 'Ushanka', 'The People''s Hat', 2, 500, NULL, 1662167687);
INSERT INTO public.hat_defs VALUES (736, 'Marsey Slime Hat', 'Let this cute little slime marsey climb on your crusty head.', 2, 500, NULL, 1664085993);
INSERT INTO public.hat_defs VALUES (728, 'Kanohi Akaku (Kopaka)', 'Toa of Ice. The obligatory lone wolf character. You''re Lego Sasuke.', 2, 500, NULL, 1663969562);
INSERT INTO public.hat_defs VALUES (729, 'Kanohi Kaukau (Gali)', 'Toa of Water. Lmao, you picked the girl one!', 2, 500, NULL, 1663969637);
INSERT INTO public.hat_defs VALUES (730, 'Kanohi Miru (Lewa)', 'Toa of Air. You''re the funny one of the group, or maybe you just know that the axe was the coolest weapon.', 2, 500, NULL, 1663969803);
INSERT INTO public.hat_defs VALUES (731, 'Kanohi Kakama (Pohatu)', 'Toa of Stone. You''re everybody''s pal and just want the team to stick together. Why is your element separate from Earth? It''s anybody''s guess!', 2, 500, NULL, 1663969919);
INSERT INTO public.hat_defs VALUES (732, 'Kanohi Avohkii (Takanuva)', 'Toa of Light. Well aren''t you just a special snowflake? Fuckin'' think you''re the Chosen One over here or something.', 2, 500, NULL, 1663970033);
INSERT INTO public.hat_defs VALUES (733, 'Kanohi Ignika', 'The Mask of Life. Matoro died for you, and you''re using your life to shitpost about trans people?', 2, 500, NULL, 1663970127);
INSERT INTO public.hat_defs VALUES (734, 'Kanohi Pakari (Onua)', 'Toa of Earth. The wisdom of the group, but wisdom is useless if you don''t share it. Why is your element separate from Stone? Answer me that, wise guy!', 2, 500, NULL, 1663970191);
INSERT INTO public.hat_defs VALUES (735, 'The Yakub', 'Proof that science has, in fact, gone too far', 2, 500, NULL, 1664054894);
INSERT INTO public.hat_defs VALUES (737, 'Duel Wielding', 'Oh shit you''re packing? My bad carry on', 2, 500, NULL, 1664088304);
INSERT INTO public.hat_defs VALUES (3, 'Cat Ears I', 'Mew :3', 2, 500, NULL, 1662167687);
INSERT INTO public.hat_defs VALUES (723, 'Hohol', 'Мій предок :)', 2, 500, NULL, 1663892328);
INSERT INTO public.hat_defs VALUES (92, 'Top Hat (black)', 'Traditional. Classy. Elegant.', 2, 500, NULL, 1662167687);
@ -1177,7 +1177,7 @@ INSERT INTO public.marseys (name, author_id, tags, created_utc) VALUES
('marseycarp',2,'beard mohawk reaction carpathianflorist monster',NULL),
('marseycarp2',2,'harm cutting selfharm cutter reaction carpathianflorist self animated',NULL),
('marseycarp3',2,'reaction fish carpathianflorist catfish',NULL),
('marseycarp4',2,'sick sad world ban hammer pin awards janny mod admin',1664034021),
('marseycarp4',2,'sicksadworld banhammer pin awards janny mod admin',1664034021),
('marseycarpasian',2,'carpathianflorist fish azn racist hat paddy gook nip ching chong chink',NULL),
('marseycarpautism',2,'carpathianflorist special assburgers aspergers retarded janitor jannie',NULL),
('marseycarpboobs',2,'carpathianflorist boobs booba titties tiddies tits boobs breasts censored fish administrator jannie janny janitor',1663771699),
@ -1193,6 +1193,7 @@ INSERT INTO public.marseys (name, author_id, tags, created_utc) VALUES
('marseycarphug2',2,'fish love bottomfeeder carpathianflorist heart blow admin cute',NULL),
('marseycarpina',2,'carpathianflorist drag transgender admin jannie',NULL),
('marseycarplazy',2,'carpathianflorist fish couch sleeping slacker idc antiwork janitor',NULL),
('marseycarpler',2,'carp hitler nazi',1664137088),
('marseycarpmerchant',2,'jewish money redbubble merch carpathianflorist money yid heeb sheeny sheenie greedy handrubbery rubbing hands kike israeli',1663465891),
('marseycarpmerchant2',2,'jew fish greedy',1663548215),
('marseycarpmermaid',2,'carp mermaid merman mercarp siren sexy legs temptress',NULL),
@ -1770,6 +1771,7 @@ INSERT INTO public.marseys (name, author_id, tags, created_utc) VALUES
('marseyliathomas',2,'tranny transgender athlete lia thomas athletics woman valid lgbt swimmer aquamaam aqua maam',NULL),
('marseyliberty',2,'usa burger america statue lady republican democrat biden trump rightoid leftoid',NULL),
('marseyliberty2',2,'usa burger america statue lady republican democrat biden trump rightoid leftoid',NULL),
('marseylibertyfireworks',2,'independence forth july america usa eagle republican democrat united states patriot statue animated',1664123152),
('marseylibleft',2,'unemployed protest riot anarcho capitalist antifa anarchist anarchy',NULL),
('marseylibright',2,'libertarian anarcho wagecuck pedophile capitalist lolbert',NULL),
('marseylicking',2,'spongebob tongue taste',1663284181),
@ -2324,6 +2326,7 @@ INSERT INTO public.marseys (name, author_id, tags, created_utc) VALUES
('marseyspa',2,'spa towel cucumber facial relax calm selfcare foid hygiene beauty',NULL),
('marseyspecial',2,'retard reaction slow special needs sped',NULL),
('marseysperm',2,'cum swim vasectomy jizz semen spunk penis sex pregnant coom animated',NULL),
('marseysphericalcow',2,'sphere science moo physics',1664131121),
('marseysphinx',2,'sphinx egypt ancient antiquity wonder pharaoh myth riddle puzzle',NULL),
('marseyspider',2,'insect halloween arachnid holiday bug',NULL),
('marseyspider2',2,'insect halloween spiderweb arachnid holiday bug',NULL),
@ -2542,6 +2545,7 @@ INSERT INTO public.marseys (name, author_id, tags, created_utc) VALUES
('marseyyeti',2,'scary monster myth winter ice mountain himalaya asia predator giant',NULL),
('marseyyikes',2,'reaction judgment disgust oof cringe',NULL),
('marseyyinzer',2,'pittsburgh pennsylvania pens penguins steelers stillers pirates buccos buckos terrible towel pierogo yuengling beer city hat baseball football hockey nfl mlb nhl happy',NULL),
('marseyyoshi',2,'super mario nintendo snes retro video game dinosaur italian plumber pipes',1664142051),
('marseyyugi',2,'yugioh yu-gi-oh! cards trap anime',NULL),
('marseyza',2,'antlers flowers',NULL),
('marseyzaku',2,'gundam mecha robot helmet mask',NULL),