This commit is contained in:
smolgrrr 2023-11-12 22:39:51 +11:00
parent 088fe7243a
commit f48d5ff024
8 changed files with 133955 additions and 12 deletions

View File

@ -3,12 +3,16 @@
"version": "0.1.0", "version": "0.1.0",
"private": false, "private": false,
"dependencies": { "dependencies": {
"@emoji-mart/data": "^1.1.2",
"@emoji-mart/react": "^1.1.1",
"@heroicons/react": "^2.0.18", "@heroicons/react": "^2.0.18",
"@types/jest": "^27.5.2", "@types/jest": "^27.5.2",
"@types/node": "^17.0.45", "@types/node": "^17.0.45",
"@types/react": "^18.2.21", "@types/react": "^18.2.21",
"@types/react-dom": "^18.2.7", "@types/react-dom": "^18.2.7",
"@types/react-swipeable-views": "^0.13.2", "@types/react-swipeable-views": "^0.13.2",
"emoji-mart": "^5.5.2",
"emoji-picker-react": "^4.5.15",
"link-preview-js": "^3.0.5", "link-preview-js": "^3.0.5",
"nostr-tools": "latest", "nostr-tools": "latest",
"react": "^18.2.0", "react": "^18.2.0",

View File

@ -0,0 +1,71 @@
import data, { Emoji } from "@emoji-mart/data";
import Picker from "@emoji-mart/react";
import { RefObject } from "react";
import customEmojis from '../custom_emojis.json';
interface EmojiPickerProps {
topOffset: number;
leftOffset: number;
onEmojiSelect: (e: Emoji) => void;
onClickOutside: () => void;
height?: number;
ref: RefObject<HTMLDivElement>;
}
const customCategoryIcons = {
categoryIcons: {
"poast": {
src: 'https://poa.st/emoji/custom/poast_hat.png',
},
},
}
export function EmojiPicker({
topOffset,
leftOffset,
onEmojiSelect,
onClickOutside,
height = 300,
ref,
}: EmojiPickerProps) {
const customEmojiList = customEmojis.map(pack => {
return {
id: pack.id,
name: pack.name,
emojis: pack.emojis.map(e => {
return {
id: e.shortcode,
name: e.shortcode,
skins: [{ src: e.static_url }],
};
}),
};
});
return (
<>
<div
className="z-25"
ref={ref}>
<style>
{`
em-emoji-picker { max-height: ${height}px; }
`}
</style>
<Picker
autoFocus
data={data}
custom={customEmojiList}
perLine={7}
previewPosition="none"
skinTonePosition="search"
theme="dark"
onEmojiSelect={onEmojiSelect}
onClickOutside={onClickOutside}
maxFrequentRows={3}
categoryIcons={customCategoryIcons}
/>
</div>
</>
);
}

View File

@ -2,12 +2,15 @@ import {
ArrowUpTrayIcon, ArrowUpTrayIcon,
CpuChipIcon, CpuChipIcon,
ArrowPathIcon, ArrowPathIcon,
Square2StackIcon
} from "@heroicons/react/24/outline"; } from "@heroicons/react/24/outline";
import { XCircleIcon } from "@heroicons/react/24/solid"; import { XCircleIcon } from "@heroicons/react/24/solid";
import { useState, useEffect, useMemo } from "react"; import { useState, useEffect, useRef } from "react";
import { generatePrivateKey, getPublicKey, finishEvent, UnsignedEvent, Event as NostrEvent, nip19 } from "nostr-tools"; import { generatePrivateKey, getPublicKey, finishEvent, UnsignedEvent, Event as NostrEvent, nip19 } from "nostr-tools";
import { publish } from "../../utils/relays"; import { publish } from "../../utils/relays";
import { renderMedia, attachFile } from "../../utils/FileUpload"; import { renderMedia, attachFile } from "../../utils/FileUpload";
import { EmojiPicker } from "./Emojis/emoji-picker";
import customEmojis from './custom_emojis.json';
const useWorkers = (numCores: number, unsigned: UnsignedEvent, difficulty: string, deps: any[]) => { const useWorkers = (numCores: number, unsigned: UnsignedEvent, difficulty: string, deps: any[]) => {
const [messageFromWorker, setMessageFromWorker] = useState(null); const [messageFromWorker, setMessageFromWorker] = useState(null);
@ -54,6 +57,7 @@ const NewNoteCard = ({
refEvent, refEvent,
tagType tagType
}: FormProps) => { }: FormProps) => {
const ref = useRef<HTMLDivElement | null>(null);
const [comment, setComment] = useState(""); const [comment, setComment] = useState("");
const [file, setFile] = useState(""); const [file, setFile] = useState("");
const [sk, setSk] = useState(generatePrivateKey()); const [sk, setSk] = useState(generatePrivateKey());
@ -77,7 +81,6 @@ const NewNoteCard = ({
const { startWork, messageFromWorker, doingWorkProgress } = useWorkers(numCores, unsigned, difficulty, [unsigned]); const { startWork, messageFromWorker, doingWorkProgress } = useWorkers(numCores, unsigned, difficulty, [unsigned]);
useEffect(() => { useEffect(() => {
if (refEvent && tagType && unsigned.tags.length === 0) { if (refEvent && tagType && unsigned.tags.length === 0) {
const tags = tagMapping[tagType]; const tags = tagMapping[tagType];
@ -134,6 +137,43 @@ const NewNoteCard = ({
} }
}, [messageFromWorker]); }, [messageFromWorker]);
//Emoji stuff
const emojiRef = useRef(null);
const [showEmojiPicker, setShowEmojiPicker] = useState(false);
interface Emoji {
native?: string;
id?: string;
}
const emojiNames = customEmojis.map(p => p.emojis).flat();
function getEmojiById(id: string) {
return emojiNames.find(e => e.shortcode === id);
}
async function onEmojiSelect(emoji: Emoji) {
setShowEmojiPicker(false);
try {
if (emoji.id) {
const e = getEmojiById(emoji.id);
if (e) {
setComment(comment + " :" + e.shortcode + ":");
unsigned.tags.push(['emoji', e.shortcode, e.url]);
};
}
} catch {
//ignore
}
}
const topOffset = ref.current?.getBoundingClientRect().top;
const leftOffset = ref.current?.getBoundingClientRect().left;
function pickEmoji(e: React.MouseEvent) {
e.stopPropagation();
setShowEmojiPicker(!showEmojiPicker);
}
return ( return (
<form <form
name="post" name="post"
@ -151,7 +191,7 @@ const NewNoteCard = ({
id="togglePostFormLink" id="togglePostFormLink"
className="text-lg text-neutral-500 text-center mb-2 font-semibold" className="text-lg text-neutral-500 text-center mb-2 font-semibold"
> >
{tagType === 'Reply' ? 'Reply to thread' : 'Start Thread'} {tagType === 'Reply' ? 'Reply to thread' : 'Start New Thread'}
</div> </div>
<div className="px-4 pt-4 flex flex-col bg-neutral-900 rounded-lg"> <div className="px-4 pt-4 flex flex-col bg-neutral-900 rounded-lg">
<textarea <textarea
@ -181,6 +221,18 @@ const NewNoteCard = ({
</div> </div>
<div> <div>
<div className="flex items-center gap-4"> <div className="flex items-center gap-4">
<div className="items-center">
{showEmojiPicker && (
<EmojiPicker
topOffset={topOffset || 0}
leftOffset={leftOffset || 0}
onEmojiSelect={onEmojiSelect}
onClickOutside={() => setShowEmojiPicker(false)}
ref={emojiRef}
/>
)}
<Square2StackIcon className="h-4 w-4 text-neutral-400 cursor-pointer" onClick={pickEmoji}/>
</div>
<div className="flex items-center"> <div className="flex items-center">
<ArrowUpTrayIcon <ArrowUpTrayIcon
className="h-4 w-4 text-neutral-400 cursor-pointer" className="h-4 w-4 text-neutral-400 cursor-pointer"

File diff suppressed because it is too large Load Diff

View File

@ -55,7 +55,7 @@ const Home = () => {
<div className="w-full px-4 sm:px-0 sm:max-w-xl mx-auto my-2"> <div className="w-full px-4 sm:px-0 sm:max-w-xl mx-auto my-2">
<NewNoteCard /> <NewNoteCard />
</div> </div>
<div className="flex items-center justify-center w-full py-4"> <div className="flex items-center justify-center w-full py-4 z-2">
<label htmlFor="toggleB" className="flex items-center cursor-pointer"> <label htmlFor="toggleB" className="flex items-center cursor-pointer">
<div className="relative"> <div className="relative">
<input <input

View File

@ -62,7 +62,7 @@ const PostCard = ({
<CardContainer> <CardContainer>
<div className={`flex flex-col gap-2`}> <div className={`flex flex-col gap-2`}>
<div className={`flex flex-col break-words ${type !== "OP" ? 'hover:cursor-pointer' : ''}`} onClick={handleClick}> <div className={`flex flex-col break-words ${type !== "OP" ? 'hover:cursor-pointer' : ''}`} onClick={handleClick}>
<ContentPreview key={event.id} comment={comment} /> <ContentPreview key={event.id} eventdata={event} />
</div> </div>
{renderMedia(file)} {renderMedia(file)}
{repliedTo && <div className="flex items-center mt-1" > {repliedTo && <div className="flex items-center mt-1" >

View File

@ -71,7 +71,7 @@ const QuoteEmbed = ({
<div className="p-3 rounded-lg border border-neutral-700 bg-neutral-800"> <div className="p-3 rounded-lg border border-neutral-700 bg-neutral-800">
<div className="flex flex-col"> <div className="flex flex-col">
<div className="flex flex-col break-words"> <div className="flex flex-col break-words">
<ContentPreview key={event.id} comment={comment} /> <ContentPreview key={event.id} eventdata={event} />
</div> </div>
{renderMedia(file)} {renderMedia(file)}
<div className="flex justify-between items-center"> <div className="flex justify-between items-center">

View File

@ -5,12 +5,33 @@ import { useEffect, useState } from "react";
import { subNoteOnce } from "../../../utils/subscriptions"; import { subNoteOnce } from "../../../utils/subscriptions";
import { nip19 } from "nostr-tools"; import { nip19 } from "nostr-tools";
import LinkModal from "./LinkPreview"; import LinkModal from "./LinkPreview";
import { parseContent } from "../../../utils/content";
const ContentPreview = ({ key, comment }: { key: string; comment: string }) => { const RichText = ({ text, isExpanded, emojiMap }: { text: string; isExpanded: boolean; emojiMap: Record<string, any> }) => {
const content = isExpanded ? text.split('\n') : text.slice(0, 350).split('\n');
return (
<>
{content.map((line, i) => (
<div key={i}>
{line.split(' ').map((word, j) =>
emojiMap[word]
? <img className="w-8 h-8 mx-0.5 inline" src={emojiMap[word]} alt={word} key={j} />
: `${word} `
)}
</div>
))}
</>
);
};
const ContentPreview = ({ key, eventdata }: { key: string; eventdata: Event }) => {
const { comment, file } = parseContent(eventdata);
const [finalComment, setFinalComment] = useState(comment); const [finalComment, setFinalComment] = useState(comment);
const [quoteEvents, setQuoteEvents] = useState<Event[]>([]); // Initialize state const [quoteEvents, setQuoteEvents] = useState<Event[]>([]); // Initialize state
const [isExpanded, setIsExpanded] = useState(false); const [isExpanded, setIsExpanded] = useState(false);
const [url, setUrl] = useState(""); const [url, setUrl] = useState("");
const [emojiMap, setEmojiMap] = useState<Record<string, any>>({});
// Define your callback function for subGlobalFeed // Define your callback function for subGlobalFeed
const onEvent = (event: Event, relay: string) => { const onEvent = (event: Event, relay: string) => {
@ -31,6 +52,16 @@ const ContentPreview = ({ key, comment }: { key: string; comment: string }) => {
subNoteOnce(id_to_hex, onEvent); subNoteOnce(id_to_hex, onEvent);
setFinalComment(finalComment.replace("nostr:" + nostrQuoteID, "").trim()); setFinalComment(finalComment.replace("nostr:" + nostrQuoteID, "").trim());
} }
let newEmojiMap: Record<string, any> = {};
eventdata.tags.forEach(tag => {
if (tag[0] === "emoji") {
newEmojiMap[`:${tag[1]}:`] = tag[2];
}
});
// Update the state variable
setEmojiMap(newEmojiMap);
}, [comment, finalComment]); }, [comment, finalComment]);
const getMetadataEvent = (event: Event) => { const getMetadataEvent = (event: Event) => {
@ -44,11 +75,8 @@ const ContentPreview = ({ key, comment }: { key: string; comment: string }) => {
}; };
return ( return (
<div className="gap-2 flex flex-col break-words text-sm"> <div className="gap-2 flex flex-col break-words font-normal">
{isExpanded <RichText text={finalComment} isExpanded={isExpanded} emojiMap={emojiMap} />
? finalComment.split('\n').map((line, i) => <React.Fragment key={i}>{line}<br/></React.Fragment>)
: finalComment.slice(0, 350).split('\n').map((line, i) => <React.Fragment key={i}>{line}<br/></React.Fragment>)
}
{finalComment.length > 350 && ( {finalComment.length > 350 && (
<button <button
className="text-sm text-neutral-500" className="text-sm text-neutral-500"