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

View File

@ -1,5 +1,6 @@
html, html,
body { body {
overflow: hidden;
overscroll-behavior-y: none; overscroll-behavior-y: none;
} }
@ -9,6 +10,8 @@ html {
body { body {
min-height: 100vh; min-height: 100vh;
min-height: calc(var(--vh, 1vh) * 100);
overflow: hidden;
/* mobile viewport bug fix */ /* mobile viewport bug fix */
min-height: -webkit-fill-available; min-height: -webkit-fill-available;
} }
@ -25,8 +28,8 @@ body {
overflow: hidden; overflow: hidden;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
margin: 0 auto; justify-content: center;
max-width: 1000px; margin: 0 2rem;
} }
.App-heading { .App-heading {
@ -37,6 +40,7 @@ body {
.App-heading small { .App-heading small {
opacity: 0.2; opacity: 0.2;
font-size: 10px;
} }
.App-side { .App-side {
@ -50,6 +54,7 @@ body {
position: relative; position: relative;
flex: 3; flex: 3;
height: 62vh; height: 62vh;
height: calc(var(--vh, 1vh) * 72);
max-height: 1000px; max-height: 1000px;
overflow: auto; overflow: auto;
-ms-overflow-style: none; -ms-overflow-style: none;
@ -94,6 +99,10 @@ body {
/* On mobile, hide the sidebar and make the input full-width. */ /* On mobile, hide the sidebar and make the input full-width. */
@media screen and (max-width: 1100px) { @media screen and (max-width: 1100px) {
.App-wrapper {
margin: 0 auto;
}
.App-heading { .App-heading {
padding: 0 1rem; padding: 0 1rem;
} }
@ -112,7 +121,7 @@ body {
} }
.App-content__reduced { .App-content__reduced {
height: 58vh; height: calc(var(--vh, 1vh) * 65);
} }
} }
@ -122,4 +131,9 @@ lite-youtube {
.btn-secondary { .btn-secondary {
border: none !important; 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 { DndProvider, useDrop } from "react-dnd";
import { HTML5Backend } from "react-dnd-html5-backend"; import { HTML5Backend } from "react-dnd-html5-backend";
import cx from "classnames"; import cx from "classnames";
import throttle from "lodash.throttle";
import { import {
ChatHeading, ChatHeading,
ChatMessageList, ChatMessageList,
@ -14,6 +15,7 @@ import { ChatProvider, DrawerProvider, useChat, useDrawer } from "./hooks";
import "./App.css"; import "./App.css";
const SCROLL_CANCEL_THRESHOLD = 500; const SCROLL_CANCEL_THRESHOLD = 500;
const WINDOW_RESIZE_THROTTLE_WAIT = 250;
export function App() { export function App() {
return ( return (
@ -36,6 +38,26 @@ function AppInner() {
const initiallyScrolledDown = useRef(false); const initiallyScrolledDown = useRef(false);
const { messages, quote } = useChat(); 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(() => { useEffect(() => {
if (messages.length > 0) { if (messages.length > 0) {
if (initiallyScrolledDown.current) { if (initiallyScrolledDown.current) {
@ -95,7 +117,7 @@ function AppInner() {
<div className="App-bottom"> <div className="App-bottom">
{quote && ( {quote && (
<div className="App-bottom-extra"> <div className="App-bottom-extra">
{quote && <QuotedMessage />} <QuotedMessage />
</div> </div>
)} )}
<UserInput /> <UserInput />

View File

@ -1,17 +1,11 @@
@keyframes fading-in {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
.ChatMessage { .ChatMessage {
position: relative; position: relative;
animation: fading-in 0.3s ease-in-out forwards; padding-right: 1.5rem;
padding: 1rem; min-height: 28px;
padding-right: 3rem; }
.ChatMessage p {
margin: 0;
} }
.ChatMessage .btn { .ChatMessage .btn {
@ -25,6 +19,8 @@
.ChatMessage-timestamp { .ChatMessage-timestamp {
margin-left: 0.5rem; margin-left: 0.5rem;
opacity: 0.5;
font-size: 10px;
} }
.ChatMessage-bottom { .ChatMessage-bottom {
@ -41,20 +37,78 @@
} }
.ChatMessage-button { .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; color: red !important;
} }
.ChatMessage-delete { .ChatMessage-quoted-link {
padding-left: 2rem;
}
.ChatMessage-actions-button {
position: absolute; position: absolute;
top: 4px; top: 0;
right: 4px; 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 */ /* List */
.ChatMessageList { .ChatMessageList {
flex: 1; 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, { import React, {
useCallback, useCallback,
useEffect, useEffect,
useLayoutEffect,
useMemo, useMemo,
useRef,
useState, useState,
} from "react"; } from "react";
import cx from "classnames"; import cx from "classnames";
import key from "weak-key"; import key from "weak-key";
import humanizeDuration from "humanize-duration"; import humanizeDuration from "humanize-duration";
import cloneDeep from "lodash.clonedeep";
import { Username } from "./Username"; import { Username } from "./Username";
import { useChat, useRootContext } from "../../hooks"; import { useChat, useRootContext } from "../../hooks";
import { QuotedMessageLink } from "./QuotedMessageLink"; import { QuotedMessageLink } from "./QuotedMessageLink";
@ -16,6 +19,8 @@ interface ChatMessageProps {
message: IChatMessage; message: IChatMessage;
timestampUpdates: number; timestampUpdates: number;
showUser?: boolean; showUser?: boolean;
actionsOpen: boolean;
onToggleActions(messageId: string): void;
} }
const TIMESTAMP_UPDATE_INTERVAL = 20000; const TIMESTAMP_UPDATE_INTERVAL = 20000;
@ -24,6 +29,8 @@ export function ChatMessage({
message, message,
showUser = true, showUser = true,
timestampUpdates, timestampUpdates,
actionsOpen,
onToggleActions,
}: ChatMessageProps) { }: ChatMessageProps) {
const { const {
id, id,
@ -38,19 +45,25 @@ export function ChatMessage({
quotes, quotes,
} = message; } = message;
const { const {
username: loggedInUsername, id: userId,
username: userUsername,
admin, admin,
censored, censored,
themeColor, themeColor,
} = useRootContext(); } = useRootContext();
const { messageLookup, deleteMessage, quoteMessage } = useChat(); const { messageLookup, deleteMessage, quoteMessage } = useChat();
const [confirmedDelete, setConfirmedDelete] = useState(false);
const quotedMessage = messageLookup[quotes]; const quotedMessage = messageLookup[quotes];
const content = censored ? text_censored : text_html; const content = censored ? text_censored : text_html;
const hasMention = content.includes(loggedInUsername); const isMention =
const mentionStyle = hasMention quotedMessage?.username === userUsername ||
? { backgroundColor: `#${themeColor}55` } (text_html.includes(`/id/${userId}`) &&
: {}; userUsername &&
const [confirmedDelete, setConfirmedDelete] = useState(false); username !== userUsername);
const timestamp = useMemo(
() => formatTimeAgo(time),
[time, timestampUpdates]
);
const handleDeleteMessage = useCallback(() => { const handleDeleteMessage = useCallback(() => {
if (confirmedDelete) { if (confirmedDelete) {
deleteMessage(text); deleteMessage(text);
@ -58,13 +71,76 @@ export function ChatMessage({
setConfirmedDelete(true); setConfirmedDelete(true);
} }
}, [text, confirmedDelete]); }, [text, confirmedDelete]);
const timestamp = useMemo( const handleQuoteMessageAction = useCallback(() => {
() => formatTimeAgo(time), quoteMessage(message);
[time, timestampUpdates] onToggleActions(message.id);
); }, [message, onToggleActions]);
useEffect(() => {
if (!actionsOpen) {
setConfirmedDelete(false);
}
}, [actionsOpen]);
return ( 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 && ( {showUser && (
<div className="ChatMessage-top"> <div className="ChatMessage-top">
<Username <Username
@ -76,7 +152,11 @@ export function ChatMessage({
<div className="ChatMessage-timestamp">{timestamp}</div> <div className="ChatMessage-timestamp">{timestamp}</div>
</div> </div>
)} )}
{quotes && quotedMessage && <QuotedMessageLink message={quotedMessage} />} {quotes && quotedMessage && (
<div className="ChatMessage-quoted-link">
<QuotedMessageLink message={quotedMessage} />
</div>
)}
<div className="ChatMessage-bottom"> <div className="ChatMessage-bottom">
<div> <div>
<span <span
@ -86,31 +166,27 @@ export function ChatMessage({
__html: content, __html: content,
}} }}
/> />
<button
className="ChatMessage-button quote btn"
onClick={() => quoteMessage(message)}
>
<i className="fas fa-reply"></i>
</button>
</div> </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>
</div> </div>
); );
} }
export function ChatMessageList() { export function ChatMessageList() {
const listRef = useRef<HTMLDivElement>(null);
const { messages } = useChat(); const { messages } = useChat();
const [timestampUpdates, setTimestampUpdates] = useState(0); 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(() => { useEffect(() => {
const updatingTimestamps = setInterval( 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 ( return (
<div className="ChatMessageList"> <div className="ChatMessageList" ref={listRef}>
{messages.map((message, index) => ( {groupedMessages.map((group) => (
<ChatMessage <div key={key(group)} className="ChatMessageList-group">
key={key(message)} {group.map((message, index) => (
message={message} <ChatMessage
timestampUpdates={timestampUpdates} key={key(message)}
showUser={message.username !== messages[index - 1]?.username} message={message}
/> timestampUpdates={timestampUpdates}
showUser={index === 0}
actionsOpen={actionsOpenForMessage === message.id}
onToggleActions={handleToggleActionsForMessage}
/>
))}
</div>
))} ))}
</div> </div>
); );
} }
function formatTimeAgo(time: number) { function formatTimeAgo(time: number) {
const now = new Date().getTime(); const shortEnglishHumanizer = humanizeDuration.humanizer({
const humanized = `${humanizeDuration(time * 1000 - now, { language: "shortEn",
languages: {
shortEn: {
y: () => "y",
mo: () => "mo",
w: () => "w",
d: () => "d",
h: () => "h",
m: () => "m",
s: () => "s",
ms: () => "ms",
},
},
round: true, round: true,
units: ["h", "m", "s"], units: ["h", "m", "s"],
largest: 2, 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 React, { useCallback, useMemo } from "react";
import { useRootContext } from "../../hooks"; import { useRootContext } from "../../hooks";
import "./QuotedMessageLink.css";
const SCROLL_TO_QUOTED_OVERFLOW = 250; const SCROLL_TO_QUOTED_OVERFLOW = 250;
const QUOTED_MESSAGE_CONTEXTUAL_HIGHLIGHTING_DURATION = 2500; const QUOTED_MESSAGE_CONTEXTUAL_HIGHLIGHTING_DURATION = 2500;
@ -40,8 +41,8 @@ export function QuotedMessageLink({ message }: { message: IChatMessage }) {
}, [message, censored]); }, [message, censored]);
return ( return (
<a href="#" onClick={handleLinkClick}> <a className="QuotedMessageLink" href="#" onClick={handleLinkClick}>
Replying to @{message.username}:{" "} <i className="fas fa-reply" /> @{message.username}:{" "}
<em>"{replyText}"</em> <em>"{replyText}"</em>
</a> </a>
); );

View File

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

View File

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

View File

@ -9,7 +9,6 @@ import React, {
useState, useState,
} from "react"; } from "react";
import { io, Socket } from "socket.io-client"; import { io, Socket } from "socket.io-client";
import lozad from "lozad";
import debounce from "lodash.debounce"; import debounce from "lodash.debounce";
import { useRootContext } from "./useRootContext"; import { useRootContext } from "./useRootContext";
import { useWindowFocus } from "./useWindowFocus"; import { useWindowFocus } from "./useWindowFocus";
@ -178,17 +177,6 @@ export function ChatProvider({ children }: PropsWithChildren) {
title.innerHTML = alertedWhileAway ? `[+${notifications}] Chat` : "Chat"; title.innerHTML = alertedWhileAway ? `[+${notifications}] Chat` : "Chat";
}, [notifications, focused]); }, [notifications, focused]);
// Setup Lozad
useEffect(() => {
const { observe, observer } = lozad();
observe();
return () => {
observer.disconnect();
};
}, []);
return ( return (
<ChatContext.Provider value={context}>{children}</ChatContext.Provider> <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" resolved "https://registry.yarnpkg.com/@types/humanize-duration/-/humanize-duration-3.27.1.tgz#f14740d1f585a0a8e3f46359b62fda8b0eaa31e7"
integrity sha512-K3e+NZlpCKd6Bd/EIdqjFJRFHbrq5TzPPLwREk5Iv/YoIjQrs6ljdAUCo+Lb2xFlGNOjGSE0dqsVD19cZL137w== 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": "@types/lodash.debounce@^4.0.7":
version "4.0.7" version "4.0.7"
resolved "https://registry.yarnpkg.com/@types/lodash.debounce/-/lodash.debounce-4.0.7.tgz#0285879defb7cdb156ae633cecd62d5680eded9f" resolved "https://registry.yarnpkg.com/@types/lodash.debounce/-/lodash.debounce-4.0.7.tgz#0285879defb7cdb156ae633cecd62d5680eded9f"
@ -46,6 +53,13 @@
dependencies: dependencies:
"@types/lodash" "*" "@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@*": "@types/lodash@*":
version "4.14.185" version "4.14.185"
resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.185.tgz#c9843f5a40703a8f5edfd53358a58ae729816908" 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" resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499"
integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ== 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: lodash.debounce@^4.0.8:
version "4.0.8" version "4.0.8"
resolved "https://registry.yarnpkg.com/lodash.debounce/-/lodash.debounce-4.0.8.tgz#82d79bff30a67c4005ffd5e2515300ad9ca4d7af" resolved "https://registry.yarnpkg.com/lodash.debounce/-/lodash.debounce-4.0.8.tgz#82d79bff30a67c4005ffd5e2515300ad9ca4d7af"
integrity sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow== 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: lodash@^4.17.21:
version "4.17.21" version "4.17.21"
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" 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' 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]'); const timestamps = document.querySelectorAll('[data-time]');
for (const e of timestamps) { for (const e of timestamps) {
const date = new Date(e.dataset.time*1000); const date = new Date(e.dataset.time*1000);
e.innerHTML = date.toString(); e.innerHTML = formatDate(date);
}; };
function timestamp(str, ti) { function timestamp(str, ti) {
const date = new Date(ti*1000); 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') v.spider = int(request.values.get("spider") == 'true')
if v.spider: badge_grant(user=v, badge_id=179) if v.spider: badge_grant(user=v, badge_id=179)
else: else:
badge = v.has_badge(170) badge = v.has_badge(179)
if badge: g.db.delete(badge) if badge: g.db.delete(badge)
elif request.values.get("bio") == "": 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 (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 (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 (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 (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 (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 (723, 'Hohol', 'Мій предок :)', 2, 500, NULL, 1663892328);
INSERT INTO public.hat_defs VALUES (92, 'Top Hat (black)', 'Traditional. Classy. Elegant.', 2, 500, NULL, 1662167687); 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), ('marseycarp',2,'beard mohawk reaction carpathianflorist monster',NULL),
('marseycarp2',2,'harm cutting selfharm cutter reaction carpathianflorist self animated',NULL), ('marseycarp2',2,'harm cutting selfharm cutter reaction carpathianflorist self animated',NULL),
('marseycarp3',2,'reaction fish carpathianflorist catfish',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), ('marseycarpasian',2,'carpathianflorist fish azn racist hat paddy gook nip ching chong chink',NULL),
('marseycarpautism',2,'carpathianflorist special assburgers aspergers retarded janitor jannie',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), ('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), ('marseycarphug2',2,'fish love bottomfeeder carpathianflorist heart blow admin cute',NULL),
('marseycarpina',2,'carpathianflorist drag transgender admin jannie',NULL), ('marseycarpina',2,'carpathianflorist drag transgender admin jannie',NULL),
('marseycarplazy',2,'carpathianflorist fish couch sleeping slacker idc antiwork janitor',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), ('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), ('marseycarpmerchant2',2,'jew fish greedy',1663548215),
('marseycarpmermaid',2,'carp mermaid merman mercarp siren sexy legs temptress',NULL), ('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), ('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), ('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), ('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), ('marseylibleft',2,'unemployed protest riot anarcho capitalist antifa anarchist anarchy',NULL),
('marseylibright',2,'libertarian anarcho wagecuck pedophile capitalist lolbert',NULL), ('marseylibright',2,'libertarian anarcho wagecuck pedophile capitalist lolbert',NULL),
('marseylicking',2,'spongebob tongue taste',1663284181), ('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), ('marseyspa',2,'spa towel cucumber facial relax calm selfcare foid hygiene beauty',NULL),
('marseyspecial',2,'retard reaction slow special needs sped',NULL), ('marseyspecial',2,'retard reaction slow special needs sped',NULL),
('marseysperm',2,'cum swim vasectomy jizz semen spunk penis sex pregnant coom animated',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), ('marseysphinx',2,'sphinx egypt ancient antiquity wonder pharaoh myth riddle puzzle',NULL),
('marseyspider',2,'insect halloween arachnid holiday bug',NULL), ('marseyspider',2,'insect halloween arachnid holiday bug',NULL),
('marseyspider2',2,'insect halloween spiderweb 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), ('marseyyeti',2,'scary monster myth winter ice mountain himalaya asia predator giant',NULL),
('marseyyikes',2,'reaction judgment disgust oof cringe',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), ('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), ('marseyyugi',2,'yugioh yu-gi-oh! cards trap anime',NULL),
('marseyza',2,'antlers flowers',NULL), ('marseyza',2,'antlers flowers',NULL),
('marseyzaku',2,'gundam mecha robot helmet mask',NULL), ('marseyzaku',2,'gundam mecha robot helmet mask',NULL),