feat: pubkeys-to-users api, finished the addComment component functionality, show/hide reply

This commit is contained in:
MTG2000
2022-07-21 19:03:29 +03:00
parent 41b8ff40c6
commit 8c5eac030b
8 changed files with 126 additions and 58 deletions

View File

@@ -0,0 +1,62 @@
const serverless = require('serverless-http');
const { createExpressApp } = require('../../modules');
const express = require('express');
const extractKeyFromCookie = require('../../utils/extractKeyFromCookie');
const { getUserByPubKey } = require('../../auth/utils/helperFuncs');
const { prisma } = require('../../prisma');
const mapPubkeysToUsers = async (req, res) => {
try {
const pubkeys = req.body.pubkeys ?? [];
const usersArr = await prisma.user.findMany({
where: {
nostr_pub_key: {
in: pubkeys
}
},
select: {
id: true,
name: true,
avatar: true,
nostr_pub_key: true,
lightning_address: true
}
})
let pubkeysToUsers = {}
usersArr.forEach(user => {
pubkeysToUsers[user.nostr_pub_key] = user;
});
return res
.status(200)
.json({ pubkeysToUsers });
} catch (error) {
console.log(error);
res.status(500).send("Unexpected error happened, please try again")
}
}
let app;
if (process.env.LOCAL) {
app = createExpressApp()
app.post('/pubkeys-to-users', mapPubkeysToUsers);
}
else {
const router = express.Router();
router.post('/pubkeys-to-users', mapPubkeysToUsers)
app = createExpressApp(router)
}
const handler = serverless(app);
exports.handler = async (event, context) => {
return await handler(event, context);
};

View File

@@ -51,9 +51,12 @@ functions:
sign-event: sign-event:
handler: api/functions/sign-event/sign-event.handler handler: api/functions/sign-event/sign-event.handler
events: events:
- http:
path: sign-event
method: get
- http: - http:
path: sign-event path: sign-event
method: post method: post
pubkeys-to-users:
handler: api/functions/pubkeys-to-users/pubkeys-to-users.handler
events:
- http:
path: pubkeys-to-users
method: post

View File

@@ -15,7 +15,8 @@ const Template: ComponentStory<typeof AddComment> = (args) => <div className="ma
export const Default = Template.bind({}); export const Default = Template.bind({});
Default.args = { Default.args = {
placeholder: "Leave a comment..." placeholder: "Leave a comment...",
avatar: "https://i.pravatar.cc/150?img=8"
} }

View File

@@ -24,13 +24,13 @@ import { InvalidContentHandler } from 'remirror';
interface Props { interface Props {
initialContent?: string; initialContent?: string;
placeholder?: string; placeholder?: string;
name?: string; avatar: string;
autoFocus?: boolean autoFocus?: boolean
onSubmit?: (comment: string) => void; onSubmit?: (comment: string) => void;
} }
export default function AddComment({ initialContent, placeholder, name, autoFocus, onSubmit }: Props) { export default function AddComment({ initialContent, placeholder, avatar, autoFocus, onSubmit }: Props) {
const containerRef = useRef<HTMLDivElement>(null) const containerRef = useRef<HTMLDivElement>(null)
const linkExtension = useMemo(() => { const linkExtension = useMemo(() => {
@@ -104,7 +104,7 @@ export default function AddComment({ initialContent, placeholder, name, autoFocu
autoFocus={autoFocus} autoFocus={autoFocus}
> >
<div className="flex gap-16 items-start pb-24 border-b border-gray-200 focus-within:border-primary-500"> <div className="flex gap-16 items-start pb-24 border-b border-gray-200 focus-within:border-primary-500">
<div className="hidden sm:block mt-16 shrink-0"><Avatar width={48} src='https://i.pravatar.cc/150?img=1' /></div> <div className="hidden sm:block mt-16 shrink-0"><Avatar width={48} src={avatar} /></div>
<div className="flex-grow"> <div className="flex-grow">
<EditorComponent <EditorComponent
/> />

View File

@@ -1,5 +1,6 @@
import { useState } from "react"; import { useState } from "react";
import { useAppSelector } from "src/utils/hooks";
import AddComment from "../AddComment/AddComment"; import AddComment from "../AddComment/AddComment";
import CommentCard from "../CommentCard/CommentCard"; import CommentCard from "../CommentCard/CommentCard";
import { CommentWithReplies } from "../types"; import { CommentWithReplies } from "../types";
@@ -8,13 +9,14 @@ import { CommentWithReplies } from "../types";
interface Props { interface Props {
comment: CommentWithReplies comment: CommentWithReplies
isRoot?: boolean; isRoot?: boolean;
canReply: boolean;
onClickedReply?: () => void onClickedReply?: () => void
} }
export default function Comment({ comment, isRoot, onClickedReply }: Props) { export default function Comment({ comment, canReply, isRoot, onClickedReply }: Props) {
const [replyOpen, setReplyOpen] = useState(false) const [replyOpen, setReplyOpen] = useState(false)
const user = useAppSelector(s => s.user.me)
const clickReply = () => { const clickReply = () => {
if (isRoot) if (isRoot)
@@ -25,7 +27,7 @@ export default function Comment({ comment, isRoot, onClickedReply }: Props) {
return ( return (
<div > <div >
<CommentCard comment={comment} onReply={clickReply} /> <CommentCard canReply={canReply} comment={comment} onReply={clickReply} />
{(comment.replies.length > 0 || replyOpen) && <div className="flex mt-16 gap-8 md:gap-20 pl-8"> {(comment.replies.length > 0 || replyOpen) && <div className="flex mt-16 gap-8 md:gap-20 pl-8">
<div className="border-l border-b border-gray-200 w-16 md:w-24 h-40 rounded-bl-8 flex-shrink-0"></div> <div className="border-l border-b border-gray-200 w-16 md:w-24 h-40 rounded-bl-8 flex-shrink-0"></div>
<div className="flex flex-col w-full gap-16"> <div className="flex flex-col w-full gap-16">
@@ -33,8 +35,9 @@ export default function Comment({ comment, isRoot, onClickedReply }: Props) {
key={reply.id} key={reply.id}
comment={reply} comment={reply}
onClickedReply={clickReply} onClickedReply={clickReply}
canReply={false}
/>)} />)}
{replyOpen && <AddComment autoFocus placeholder="Leave a reply..." />} {replyOpen && <AddComment avatar={user?.avatar!} autoFocus placeholder="Leave a reply..." />}
</div> </div>
</div>} </div>}
</div> </div>

View File

@@ -7,10 +7,11 @@ import { Comment } from "../types";
interface Props { interface Props {
comment: Comment comment: Comment
canReply?: boolean;
onReply?: () => void onReply?: () => void
} }
export default function CommentCard({ comment, onReply }: Props) { export default function CommentCard({ comment, canReply, onReply }: Props) {
return ( return (
<div className="border rounded-12 p-24"> <div className="border rounded-12 p-24">
<Header author={comment.author} date={new Date(comment.created_at).toISOString()} /> <Header author={comment.author} date={new Date(comment.created_at).toISOString()} />
@@ -19,12 +20,12 @@ export default function CommentCard({ comment, onReply }: Props) {
</p> </p>
<div className="flex gap-24 mt-16 items-center"> <div className="flex gap-24 mt-16 items-center">
<VotesCount count={0} /> <VotesCount count={0} />
<button {canReply && <button
className="text-gray-600 font-medium hover:bg-gray-100 py-8 px-12 rounded-8" className="text-gray-600 font-medium hover:bg-gray-100 py-8 px-12 rounded-8"
onClick={onReply} onClick={onReply}
> >
<BiComment /> <span className="align-middle text-body5">Reply</span> <BiComment /> <span className="align-middle text-body5">Reply</span>
</button> </button>}
</div> </div>
</div> </div>
) )

View File

@@ -1,9 +1,9 @@
import React, { useEffect, useMemo, useState } from 'react' import React, { useEffect, useMemo, useState } from 'react'
import CommentRoot from '../Comment/Comment' import CommentRoot from '../Comment/Comment'
import AddComment from '../AddComment/AddComment' import AddComment from '../AddComment/AddComment'
import { } from '../helpers'
import { Comment, } from '../types' import { Comment, } from '../types'
import { createWorkerFactory, useWorker } from '@shopify/react-web-worker' import { createWorkerFactory, useWorker } from '@shopify/react-web-worker'
import { useAppSelector } from "src/utils/hooks";
import * as CommentsWorker from './comments.worker' import * as CommentsWorker from './comments.worker'
import { Post_Type } from 'src/graphql' import { Post_Type } from 'src/graphql'
@@ -22,7 +22,7 @@ export default function CommentsSection({ type, id }: Props) {
// const commentsTree = useMemo(() => convertCommentsToTree(comments), [comments]) // const commentsTree = useMemo(() => convertCommentsToTree(comments), [comments])
const [commentsTree, setCommentsTree] = useState<Comment[]>([]) const [commentsTree, setCommentsTree] = useState<Comment[]>([])
const user = useAppSelector(state => state.user.me)
const filter = useMemo(() => `boltfun ${type}_comment ${id}` + (process.env.NODE_ENV === 'development' ? 'dev' : ""), [id, type]) const filter = useMemo(() => `boltfun ${type}_comment ${id}` + (process.env.NODE_ENV === 'development' ? 'dev' : ""), [id, type])
useEffect(() => { useEffect(() => {
@@ -44,11 +44,21 @@ export default function CommentsSection({ type, id }: Props) {
return ( return (
<div className="border border-gray-200 rounded-10 p-32 bg-white"> <div className="border border-gray-200 rounded-10 p-32 bg-white">
<h6 className="text-body2 font-bolder">Discussion</h6> <h6 className="text-body2 font-bolder">Discussion</h6>
<div className="mt-24"> {!!user && <div className="mt-24">
<AddComment placeholder='Leave a comment...' onSubmit={handleNewComment} /> <AddComment
</div> placeholder='Leave a comment...'
onSubmit={handleNewComment}
avatar={user.avatar}
/>
</div>}
<div className='flex flex-col gap-16 mt-32'> <div className='flex flex-col gap-16 mt-32'>
{commentsTree.map(comment => <CommentRoot key={comment.id} comment={comment} />)} {commentsTree.map(comment =>
<CommentRoot
key={comment.id}
comment={comment}
isRoot
canReply={!!user}
/>)}
</div> </div>
</div> </div>
) )

View File

@@ -1,33 +1,20 @@
import dayjs from 'dayjs' import debounce from 'lodash.debounce';
import { relayPool } from 'nostr-tools'
import { generatePrivateKey, getPublicKey, relayPool } from 'nostr-tools'
import { Nullable } from 'remirror'; import { Nullable } from 'remirror';
import { CONSTS } from 'src/utils'; import { CONSTS } from 'src/utils';
import { Comment } from '../types'; import { Comment } from '../types';
import { mapPubkeysToUsers, } from './comment.server';
type Author = { type Author = {
id: number; id: number;
name: string; name: string;
avatar: string; avatar: string;
lightning_address?: string
} }
const pool = relayPool(); const pool = relayPool();
const globalKeys = {
prvkey: '',
pubkey: ''
}
export function now(prefix: string) {
const hell = window.localStorage.getItem('test');
if (!hell) window.localStorage.setItem('test', 'test');
return hell + prefix + dayjs()
};
export function connect() { export function connect() {
const RELAYS = [ const RELAYS = [
@@ -45,35 +32,37 @@ export function connect() {
}) })
}; };
const events: Record<string, Required<NostrEvent>> = {}; let events: Record<string, Required<NostrEvent>> = {};
export function sub(filter: string, cb: (data: Comment[]) => void) { export function sub(filter: string, cb: (data: Comment[]) => void) {
const reconstructTree = debounce(async () => {
const newComments = await constructTree();
cb(newComments)
}, 1000)
let sub = pool.sub({ let sub = pool.sub({
filter: { filter: {
"#r": [filter] "#r": [filter]
}, },
cb: async (event: Required<NostrEvent>) => { cb: async (event: Required<NostrEvent>) => {
//Got a new event //Got a new event
console.log(event);
if (!event.id) return; if (!event.id) return;
if (event.id in events) return if (event.id in events) return
events[event.id] = event events[event.id] = event
const newComments = await constructTree();
cb(newComments)
reconstructTree()
} }
}); });
return () => { return () => {
sub.unsub(); sub.unsub();
events = {};
}; };
} }
const getSignedEvents = async (event: any) => { async function getSignedEvents(event: any) {
const res = await fetch(CONSTS.apiEndpoint + '/sign-event', { const res = await fetch(CONSTS.apiEndpoint + '/sign-event', {
method: "post", method: "post",
body: JSON.stringify({ event }), body: JSON.stringify({ event }),
@@ -86,21 +75,20 @@ const getSignedEvents = async (event: any) => {
return data.event; return data.event;
} }
function setKeys() { async function mapPubkeysToUsers(pubkeys: string[]) {
if (globalKeys.prvkey) return; const res = await fetch(CONSTS.apiEndpoint + '/pubkeys-to-users', {
method: "post",
let privateKey = localStorage.getItem('nostrkey') body: JSON.stringify({ pubkeys }),
if (!privateKey) { credentials: 'include',
privateKey = generatePrivateKey() headers: {
localStorage.setItem('nostrkey', privateKey) 'Content-Type': 'application/json'
} },
pool.setPrivateKey(privateKey) });
const pubkey = getPublicKey(privateKey) const data = await res.json()
globalKeys.prvkey = privateKey return data.pubkeysToUsers as Record<string, Author>;
globalKeys.pubkey = pubkey;
} }
export async function post(data: string, filter: string) { export async function post(data: string, filter: string) {
// setKeys(); // setKeys();
@@ -162,7 +150,7 @@ export async function constructTree() {
// Extract the pubkeys used // Extract the pubkeys used
const pubkeysSet = new Set(); const pubkeysSet = new Set<string>();
sortedEvenets.forEach(e => pubkeysSet.add(e.pubkey)); sortedEvenets.forEach(e => pubkeysSet.add(e.pubkey));