mirror of
https://github.com/yzernik/squeaknode.git
synced 2026-08-13 12:33:25 +02:00
Upgrade squeaklib to v013 (#2165)
* Upgrade to squeaklib v0.13 * Allow different squeak types in server reponse * Add core method for create resqueak. * Add make resqueak method for squeak store * Update db migration to include resqueak hash * Add db method to insert and get resqueak * Return resqueak as squeak entry in db query method * Add RPC method for make resqueak * Got make resqueak modal working in frontend * Show different content for resqueak in frontend * Show resqueaed squeak in display * Show resqueak correctly * Got resqueak display working on squeak card component * Fix display for missing resqueaked squeak. * Fix display for missing resqueaked squeak in squeak card. * Got resqueak from squeak card working * Fix prevent default action on resqueak unlock click * Fix prevent default on make reply squeak in squeak card * Got number of replies to resqueaked squeak working * Fix click on squeak card * Fix too many characters warning message * Fix click on squeak card image and name and pubkey * Rename set reply modal state function * Fix toggle modals for resqueaked squeak * Fix form submit action for squeak modals * Include num resqueaks and num resqueaked resqueaks in get squeak entry db query * Show number of resqueaks in frontend * Fix distinct count in query for num replies and num resqueaks * Update resqueaked squeak in slice state * Update frontend build
This commit is contained in:
parent
ef3d49578f
commit
7658a4d95c
35 changed files with 1301 additions and 319 deletions
|
|
@ -88,6 +88,8 @@ import {
|
||||||
GetSigningProfilesReply,
|
GetSigningProfilesReply,
|
||||||
GetContactProfilesReply,
|
GetContactProfilesReply,
|
||||||
MakeSqueakReply,
|
MakeSqueakReply,
|
||||||
|
MakeResqueakRequest,
|
||||||
|
MakeResqueakReply,
|
||||||
GetSqueakDisplayReply,
|
GetSqueakDisplayReply,
|
||||||
GetAncestorSqueakDisplaysReply,
|
GetAncestorSqueakDisplaysReply,
|
||||||
GetReplySqueakDisplaysReply,
|
GetReplySqueakDisplaysReply,
|
||||||
|
|
@ -340,6 +342,20 @@ export const makeSqueak = (profileId, content, replyTo, hasRecipient, recipientP
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const makeResqueak = (profileId, resqueakedHash, replyTo) => {
|
||||||
|
console.log('Calling makeResqueak');
|
||||||
|
const request = new MakeResqueakRequest();
|
||||||
|
request.setProfileId(profileId);
|
||||||
|
request.setResqueakedHash(resqueakedHash);
|
||||||
|
request.setReplyto(replyTo);
|
||||||
|
const deser = MakeResqueakReply.deserializeBinary;
|
||||||
|
return baseRequest({
|
||||||
|
url: '/makeresqueak',
|
||||||
|
req: request,
|
||||||
|
deser: deser,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
export const getSigningProfiles = () => {
|
export const getSigningProfiles = () => {
|
||||||
console.log('Calling getSigningProfiles');
|
console.log('Calling getSigningProfiles');
|
||||||
const request = new GetSigningProfilesRequest();
|
const request = new GetSigningProfilesRequest();
|
||||||
|
|
|
||||||
|
|
@ -6,23 +6,25 @@ import moment from 'moment'
|
||||||
import { getProfileImageSrcString } from '../../squeakimages/images';
|
import { getProfileImageSrcString } from '../../squeakimages/images';
|
||||||
import { Link, withRouter } from 'react-router-dom'
|
import { Link, withRouter } from 'react-router-dom'
|
||||||
import { ICON_REPLY, ICON_RETWEET,
|
import { ICON_REPLY, ICON_RETWEET,
|
||||||
ICON_HEART, ICON_HEARTFULL, ICON_DELETE, ICON_CLOSE,ICON_IMGUPLOAD, ICON_LOCKFILL} from '../../Icons'
|
ICON_HEART, ICON_HEARTFULL, ICON_DELETE, ICON_CLOSE,ICON_IMGUPLOAD, ICON_LOCKFILL} from '../../Icons'
|
||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
import {API_URL} from '../../config'
|
import {API_URL} from '../../config'
|
||||||
import MakeSqueak from '../../features/squeaks/MakeSqueak'
|
import MakeSqueak from '../../features/squeaks/MakeSqueak'
|
||||||
import DeleteSqueak from '../../features/squeaks/DeleteSqueak'
|
import MakeResqueak from '../../features/squeaks/MakeResqueak'
|
||||||
import BuySqueak from '../../features/squeaks/BuySqueak'
|
import DeleteSqueak from '../../features/squeaks/DeleteSqueak'
|
||||||
import ContentEditable from 'react-contenteditable'
|
import BuySqueak from '../../features/squeaks/BuySqueak'
|
||||||
|
import ContentEditable from 'react-contenteditable'
|
||||||
|
|
||||||
import {
|
import {
|
||||||
setLikeSqueak,
|
setLikeSqueak,
|
||||||
setUnlikeSqueak,
|
setUnlikeSqueak,
|
||||||
fetchSqueakOffers,
|
fetchSqueakOffers,
|
||||||
} from '../../features/squeaks/squeaksSlice'
|
} from '../../features/squeaks/squeaksSlice'
|
||||||
|
|
||||||
|
|
||||||
const SqueakCard = React.memo(function SqueakCard(props) {
|
const SqueakCard = React.memo(function SqueakCard(props) {
|
||||||
const [replyModalOpen, setModalOpen] = useState(false)
|
const [replyModalOpen, setReplyModalOpen] = useState(false)
|
||||||
|
const [resqueakModalOpen, setResqueakModalOpen] = useState(false)
|
||||||
const [deleteModalOpen, setDeleteModalOpen] = useState(false)
|
const [deleteModalOpen, setDeleteModalOpen] = useState(false)
|
||||||
const [buyModalOpen, setBuyModalOpen] = useState(false)
|
const [buyModalOpen, setBuyModalOpen] = useState(false)
|
||||||
const [parent, setParent] = useState(false)
|
const [parent, setParent] = useState(false)
|
||||||
|
|
@ -32,252 +34,308 @@ const SqueakCard = React.memo(function SqueakCard(props) {
|
||||||
|
|
||||||
let info
|
let info
|
||||||
const likeSqueak = (e,id) => {
|
const likeSqueak = (e,id) => {
|
||||||
if(e){ e.stopPropagation() }
|
if(e){
|
||||||
console.log('Clicked like with id', id);
|
e.preventDefault();
|
||||||
dispatch(setLikeSqueak(id));
|
e.stopPropagation();
|
||||||
|
}
|
||||||
|
console.log('Clicked like with id', id);
|
||||||
|
dispatch(setLikeSqueak(id));
|
||||||
}
|
}
|
||||||
const unlikeSqueak = (e,id) => {
|
const unlikeSqueak = (e,id) => {
|
||||||
if(e){ e.stopPropagation() }
|
if(e){
|
||||||
console.log('Clicked unlike with id', id);
|
e.preventDefault();
|
||||||
dispatch(setUnlikeSqueak(id));
|
e.stopPropagation();
|
||||||
|
}
|
||||||
|
console.log('Clicked unlike with id', id);
|
||||||
|
dispatch(setUnlikeSqueak(id));
|
||||||
}
|
}
|
||||||
|
|
||||||
const resqueak = (e,id, resqueakId) => {
|
const resqueak = (e,id, resqueakId) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation()
|
e.stopPropagation()
|
||||||
if(props.history.location.pathname.slice(1,5) === 'prof'){
|
if(props.history.location.pathname.slice(1,5) === 'prof'){
|
||||||
info = { dest: "profile", id, resqueakId }
|
info = { dest: "profile", id, resqueakId }
|
||||||
}else{ info = { id, resqueakId } }
|
}else{ info = { id, resqueakId } }
|
||||||
alert('Re-Squeak not yet implemented!');
|
alert('Re-Squeak not yet implemented!');
|
||||||
}
|
}
|
||||||
|
|
||||||
const toggleReplyModal = (e, type) => {
|
const toggleReplyModal = (e, type) => {
|
||||||
if(e){
|
if(e){
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
}
|
}
|
||||||
setStyleBody(!styleBody)
|
setStyleBody(!styleBody)
|
||||||
if(type === 'parent'){setParent(true)}else{setParent(false)}
|
if(type === 'parent'){setParent(true)}else{setParent(false)}
|
||||||
setTimeout(()=>{ setModalOpen(!replyModalOpen) },20)
|
setTimeout(()=>{ setReplyModalOpen(!replyModalOpen) },20)
|
||||||
|
}
|
||||||
|
|
||||||
|
const toggleResqueakModal = (e, type) => {
|
||||||
|
if(e){
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
}
|
||||||
|
setStyleBody(!styleBody)
|
||||||
|
if(type === 'parent'){setParent(true)}else{setParent(false)}
|
||||||
|
setTimeout(()=>{ setResqueakModalOpen(!resqueakModalOpen) },20)
|
||||||
}
|
}
|
||||||
|
|
||||||
const toggleDeleteModal = (e, type) => {
|
const toggleDeleteModal = (e, type) => {
|
||||||
if(e){
|
if(e){
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
}
|
}
|
||||||
setStyleBody(!styleBody)
|
setStyleBody(!styleBody)
|
||||||
if(type === 'parent'){setParent(true)}else{setParent(false)}
|
if(type === 'parent'){setParent(true)}else{setParent(false)}
|
||||||
setTimeout(()=>{ setDeleteModalOpen(!deleteModalOpen) },20)
|
setTimeout(()=>{ setDeleteModalOpen(!deleteModalOpen) },20)
|
||||||
}
|
}
|
||||||
|
|
||||||
const toggleBuyModal = (e, type) => {
|
const toggleBuyModal = (e, type) => {
|
||||||
if(e){
|
if(e){
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
}
|
}
|
||||||
setStyleBody(!styleBody)
|
setStyleBody(!styleBody)
|
||||||
if(type === 'parent'){setParent(true)}else{setParent(false)}
|
if(type === 'parent'){setParent(true)}else{setParent(false)}
|
||||||
setTimeout(()=>{ setBuyModalOpen(!buyModalOpen) },20)
|
setTimeout(()=>{ setBuyModalOpen(!buyModalOpen) },20)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
const handleModalClick = (e) => {
|
const handleModalClick = (e) => {
|
||||||
e.stopPropagation()
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
}
|
}
|
||||||
|
|
||||||
const isInitialMount = useRef(true);
|
const isInitialMount = useRef(true);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isInitialMount.current){ isInitialMount.current = false }
|
if (isInitialMount.current){ isInitialMount.current = false }
|
||||||
else {
|
else {
|
||||||
document.getElementsByTagName("body")[0].style.cssText = styleBody && "overflow-y: hidden; margin-right: 17px"
|
document.getElementsByTagName("body")[0].style.cssText = styleBody && "overflow-y: hidden; margin-right: 17px"
|
||||||
}
|
}
|
||||||
}, [styleBody])
|
}, [styleBody])
|
||||||
|
|
||||||
useEffect( () => () => document.getElementsByTagName("body")[0].style.cssText = "", [] )
|
useEffect( () => () => document.getElementsByTagName("body")[0].style.cssText = "", [] )
|
||||||
|
|
||||||
useEffect(()=> {
|
useEffect(()=> {
|
||||||
if (isInitialMount.current){ isInitialMount.current = false;}
|
if (isInitialMount.current){ isInitialMount.current = false;}
|
||||||
else if(document.getElementById("replyBox")) {
|
else if(document.getElementById("replyBox")) {
|
||||||
document.getElementById("replyBox").focus(); }
|
document.getElementById("replyBox").focus(); }
|
||||||
}, [replyModalOpen])
|
}, [replyModalOpen])
|
||||||
|
|
||||||
const goToUser = (e,username) => {
|
const getRegularSqueakContent = (squeak) => {
|
||||||
e.stopPropagation()
|
return squeak.getContentStr() ?
|
||||||
props.history.push(`/app/profile/${username}`)
|
<div className="card-content-info">
|
||||||
|
{squeak.getContentStr()}
|
||||||
|
</div> :
|
||||||
|
<div onClick={(e)=> {
|
||||||
|
e.preventDefault();
|
||||||
|
dispatch(fetchSqueakOffers(props.id));
|
||||||
|
toggleBuyModal();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
className="card-content-info card-content-locked-content">
|
||||||
|
<ICON_LOCKFILL styles={{width:'36px', height:"36px", padding: "5px"}} />
|
||||||
|
<div>
|
||||||
|
Locked content
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
const getResqueakContent = (squeak) => {
|
||||||
|
const resqueakedHash = squeak.getResqueakedHash();
|
||||||
|
const resqueakedSqueak = squeak.getResqueakedSqueak();
|
||||||
|
const resqueakedAuthor = resqueakedSqueak && resqueakedSqueak.getAuthor();
|
||||||
|
return <div className="squeak-content">
|
||||||
|
<SqueakCard squeak={resqueakedSqueak} key={resqueakedHash} id={resqueakedHash} user={resqueakedAuthor}
|
||||||
|
replies={[]} hasReply={false} />
|
||||||
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
moment.updateLocale('en', {
|
moment.updateLocale('en', {
|
||||||
relativeTime: { future: 'in %s', past: '%s ago', s: 'few seconds ago', ss: '%ss',
|
relativeTime: { future: 'in %s', past: '%s ago', s: 'few seconds ago', ss: '%ss',
|
||||||
m: '1m', mm: '%dm', h: '1h', hh: '%dh', d: 'a day', dd: '%dd', M: 'a month',
|
m: '1m', mm: '%dm', h: '1h', hh: '%dh', d: 'a day', dd: '%dd', M: 'a month',
|
||||||
MM: '%dM', y: 'a year', yy: '%dY' }
|
MM: '%dM', y: 'a year', yy: '%dY' }
|
||||||
});
|
});
|
||||||
|
|
||||||
const author = props.user;
|
const author = props.user;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
|
|
||||||
<Link onClick={(e)=>e.stopPropagation()} to={`/app/squeak/${props.id}`} key={props.id} className={props.squeak ? "Squeak-card-wrapper" : "Squeak-card-wrapper missing-squeak"} >
|
<Link onClick={(e)=>{
|
||||||
|
e.stopPropagation()
|
||||||
|
}
|
||||||
|
} to={`/app/squeak/${props.id}`} key={props.id} className={props.squeak ? "Squeak-card-wrapper" : "Squeak-card-wrapper missing-squeak"} >
|
||||||
|
|
||||||
{props.squeak ?
|
{props.squeak ?
|
||||||
<>
|
<>
|
||||||
<div className="card-userPic-wrapper">
|
<div className="card-userPic-wrapper">
|
||||||
<Link onClick={(e)=>e.stopPropagation()} to={`/app/profile/${props.squeak.getAuthorPubkey()}`}>
|
<Link onClick={(e)=>{
|
||||||
<img alt="" style={{borderRadius:'50%', minWidth:'49px'}} width="100%" height="49px" src={author ? `${getProfileImageSrcString(author)}` : null}/>
|
e.stopPropagation();
|
||||||
</Link>
|
}} to={`/app/profile/${props.squeak.getAuthorPubkey()}`}>
|
||||||
{props.hasReply? <div className="squeak-reply-thread"></div> : null}
|
<img alt="" style={{borderRadius:'50%', minWidth:'49px'}} width="100%" height="49px" src={author ? `${getProfileImageSrcString(author)}` : null}/>
|
||||||
</div>
|
</Link>
|
||||||
<div className="card-content-wrapper">
|
{props.hasReply? <div className="squeak-reply-thread"></div> : null}
|
||||||
<div className="card-content-header">
|
</div>
|
||||||
<div className="card-header-detail">
|
<div className="card-content-wrapper">
|
||||||
<span className="card-header-user">
|
<div className="card-content-header">
|
||||||
<Link onClick={(e)=>e.stopPropagation()} to={`/app/profile/${props.squeak.getAuthorPubkey()}`}>{author ? author.getProfileName(): 'Unknown Author'}</Link>
|
<div className="card-header-detail">
|
||||||
</span>
|
<span className="card-header-user">
|
||||||
<span className="card-header-username">
|
<Link onClick={(e)=>{
|
||||||
<Link onClick={(e)=>e.stopPropagation()} to={`/app/profile/${props.squeak.getAuthorPubkey()}`}>{'@'+ props.squeak.getAuthorPubkey()}</Link>
|
e.stopPropagation();
|
||||||
</span>
|
}} to={`/app/profile/${props.squeak.getAuthorPubkey()}`}>{author ? author.getProfileName(): 'Unknown Author'}</Link>
|
||||||
<span className="card-header-dot">·</span>
|
</span>
|
||||||
<span className="card-header-date">
|
<span className="card-header-username">
|
||||||
{moment(props.squeak.getBlockTime() * 1000).fromNow(true)}
|
<Link onClick={(e)=>{
|
||||||
</span>
|
e.stopPropagation();
|
||||||
</div>
|
}} to={`/app/profile/${props.squeak.getAuthorPubkey()}`}>{'@'+ props.squeak.getAuthorPubkey()}</Link>
|
||||||
<div className="card-header-more">
|
</span>
|
||||||
|
<span className="card-header-dot">·</span>
|
||||||
|
<span className="card-header-date">
|
||||||
|
{moment(props.squeak.getBlockTime() * 1000).fromNow(true)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="card-header-more">
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{props.squeak.getContentStr() ?
|
{props.squeak.getIsResqueak() ?
|
||||||
<div className="card-content-info">
|
getResqueakContent(props.squeak) :
|
||||||
{props.squeak.getContentStr()}
|
getRegularSqueakContent(props.squeak)
|
||||||
</div> :
|
|
||||||
<div onClick={(e)=> {
|
|
||||||
e.preventDefault();
|
|
||||||
dispatch(fetchSqueakOffers(props.id));
|
|
||||||
toggleBuyModal();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
className="card-content-info card-content-locked-content">
|
|
||||||
<ICON_LOCKFILL styles={{width:'36px', height:"36px", padding: "5px"}} />
|
|
||||||
<div>
|
|
||||||
Locked content
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
}
|
}
|
||||||
|
|
||||||
<div className="card-buttons-wrapper">
|
<div className="card-buttons-wrapper">
|
||||||
<div onClick={(e)=>toggleReplyModal(e)} className="card-button-wrap reply-wrap">
|
<div onClick={(e)=>toggleReplyModal(e)} className="card-button-wrap reply-wrap">
|
||||||
<div className="card-icon reply-icon">
|
<div className="card-icon reply-icon">
|
||||||
<ICON_REPLY styles={{fill:'rgb(101, 119, 134)'}}/>
|
<ICON_REPLY styles={{fill:'rgb(101, 119, 134)'}}/>
|
||||||
</div>
|
|
||||||
<div className="card-icon-value">
|
|
||||||
{props.squeak.getNumReplies()}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div onClick={(e)=>resqueak(e,props.id)} className="card-button-wrap resqueak-wrap">
|
<div className="card-icon-value">
|
||||||
<div className="card-icon resqueak-icon">
|
{props.squeak.getNumReplies()}
|
||||||
<ICON_RETWEET styles={false ? {stroke: 'rgb(23, 191, 99)'} : {fill:'rgb(101, 119, 134)'}}/>
|
|
||||||
</div>
|
|
||||||
<div className="card-icon-value">
|
|
||||||
0
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div onClick={(e)=> {
|
</div>
|
||||||
|
<div onClick={(e)=>toggleResqueakModal(e)} className="card-button-wrap resqueak-wrap">
|
||||||
|
<div className="card-icon resqueak-icon">
|
||||||
|
<ICON_RETWEET styles={false ? {stroke: 'rgb(23, 191, 99)'} : {fill:'rgb(101, 119, 134)'}}/>
|
||||||
|
</div>
|
||||||
|
<div className="card-icon-value">
|
||||||
|
{props.squeak.getNumResqueaks()}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div onClick={(e)=> {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
props.squeak.getLikedTimeMs() ?
|
props.squeak.getLikedTimeMs() ?
|
||||||
unlikeSqueak(e, props.squeak.getSqueakHash()) :
|
unlikeSqueak(e, props.squeak.getSqueakHash()) :
|
||||||
likeSqueak(e, props.squeak.getSqueakHash())
|
likeSqueak(e, props.squeak.getSqueakHash())
|
||||||
}} className="card-button-wrap heart-wrap">
|
}} className="card-button-wrap heart-wrap">
|
||||||
<div className="card-icon heart-icon">
|
<div className="card-icon heart-icon">
|
||||||
{props.squeak.getLikedTimeMs() ?
|
{props.squeak.getLikedTimeMs() ?
|
||||||
<ICON_HEARTFULL styles={{fill:'rgb(224, 36, 94)'}}/> :
|
<ICON_HEARTFULL styles={{fill:'rgb(224, 36, 94)'}}/> :
|
||||||
<ICON_HEART styles={{fill:'rgb(101, 119, 134)'}}/>}
|
<ICON_HEART styles={{fill:'rgb(101, 119, 134)'}}/>}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div onClick={(e)=>toggleDeleteModal(e)} className="card-button-wrap">
|
||||||
<div onClick={(e)=>toggleDeleteModal(e)} className="card-button-wrap">
|
<div className="card-icon share-icon">
|
||||||
<div className="card-icon share-icon">
|
|
||||||
<ICON_DELETE styles={{fill:'rgb(101, 119, 134)'}} />
|
<ICON_DELETE styles={{fill:'rgb(101, 119, 134)'}} />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</> :
|
||||||
</div>
|
<>
|
||||||
</> :
|
<div className="card-userPic-wrapper">
|
||||||
<>
|
<img alt="" style={{borderRadius:'50%', minWidth:'49px'}} width="100%" height="49px" src={null}/>
|
||||||
<div className="card-userPic-wrapper">
|
{props.hasReply? <div className="squeak-reply-thread"></div> : null}
|
||||||
<img alt="" style={{borderRadius:'50%', minWidth:'49px'}} width="100%" height="49px" src={null}/>
|
</div>
|
||||||
{props.hasReply? <div className="squeak-reply-thread"></div> : null}
|
<div className="card-content-wrapper">
|
||||||
</div>
|
<div className="card-content-info">
|
||||||
<div className="card-content-wrapper">
|
Missing Squeak
|
||||||
<div className="card-content-info">
|
|
||||||
Missing Squeak
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
|
|
||||||
</Link>
|
|
||||||
|
|
||||||
{/* reply modal */}
|
|
||||||
{props.squeak ?
|
|
||||||
<div onClick={()=>toggleReplyModal()} style={{display: replyModalOpen ? 'block' : 'none'}} className="modal-edit">
|
|
||||||
{replyModalOpen ?
|
|
||||||
<div style={{minHeight: '350px', height: 'initial'}} onClick={(e)=>handleModalClick(e)} className="modal-content">
|
|
||||||
<div className="modal-header">
|
|
||||||
<div className="modal-closeIcon">
|
|
||||||
<div onClick={()=>toggleReplyModal()} className="modal-closeIcon-wrap">
|
|
||||||
<ICON_CLOSE />
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<p className="modal-title">Reply</p>
|
</>
|
||||||
</div>
|
}
|
||||||
<div style={{marginTop:'5px'}} className="modal-body">
|
|
||||||
<MakeSqueak replyToSqueak={props.squeak} submittedCallback={toggleReplyModal} />
|
|
||||||
</div>
|
|
||||||
</div> : null}
|
|
||||||
</div> : null}
|
|
||||||
|
|
||||||
{/* delete modal */}
|
</Link>
|
||||||
{props.squeak ?
|
|
||||||
<div onClick={()=>toggleDeleteModal()} style={{display: deleteModalOpen ? 'block' : 'none'}} className="modal-edit">
|
{/* reply modal */}
|
||||||
{deleteModalOpen ?
|
{props.squeak ?
|
||||||
<div style={{minHeight: '350px', height: 'initial'}} onClick={(e)=>handleModalClick(e)} className="modal-content">
|
<div onClick={(e)=>toggleReplyModal(e)} style={{display: replyModalOpen ? 'block' : 'none'}} className="modal-edit">
|
||||||
<div className="modal-header">
|
{replyModalOpen ?
|
||||||
<div className="modal-closeIcon">
|
<div style={{minHeight: '350px', height: 'initial'}} onClick={(e)=>handleModalClick(e)} className="modal-content">
|
||||||
<div onClick={()=>toggleDeleteModal()} className="modal-closeIcon-wrap">
|
<div className="modal-header">
|
||||||
<ICON_CLOSE />
|
<div className="modal-closeIcon">
|
||||||
|
<div onClick={(e)=>toggleReplyModal(e)} className="modal-closeIcon-wrap">
|
||||||
|
<ICON_CLOSE />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p className="modal-title">Reply</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div style={{marginTop:'5px'}} className="modal-body">
|
||||||
<p className="modal-title">Delete Squeak</p>
|
<MakeSqueak replyToSqueak={props.squeak} submittedCallback={toggleReplyModal} />
|
||||||
</div>
|
|
||||||
<div style={{marginTop:'5px'}} className="modal-body">
|
|
||||||
<DeleteSqueak squeakHash={props.id} submittedCallback={toggleDeleteModal} />
|
|
||||||
</div>
|
|
||||||
</div> : null}
|
|
||||||
</div> : null}
|
|
||||||
|
|
||||||
{/* buy modal */}
|
|
||||||
{props.squeak ?
|
|
||||||
<div onClick={()=>toggleBuyModal()} style={{display: buyModalOpen ? 'block' : 'none'}} className="modal-edit">
|
|
||||||
{buyModalOpen ?
|
|
||||||
<div style={{minHeight: '350px', height: 'initial'}} onClick={(e)=>handleModalClick(e)} className="modal-content">
|
|
||||||
<div className="modal-header">
|
|
||||||
<div className="modal-closeIcon">
|
|
||||||
<div onClick={()=>toggleBuyModal()} className="modal-closeIcon-wrap">
|
|
||||||
<ICON_CLOSE />
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div> : null}
|
||||||
<p className="modal-title">Buy Squeak</p>
|
</div> : null}
|
||||||
</div>
|
|
||||||
<div style={{marginTop:'5px'}} className="modal-body">
|
{/* resqueak modal */}
|
||||||
<BuySqueak squeak={props.squeak} submittedCallback={toggleBuyModal} />
|
{props.squeak ?
|
||||||
</div>
|
<div onClick={(e)=>toggleResqueakModal(e)} style={{display: resqueakModalOpen ? 'block' : 'none'}} className="modal-edit">
|
||||||
</div> : null}
|
{resqueakModalOpen ?
|
||||||
</div> : null}
|
<div style={{minHeight: '350px', height: 'initial'}} onClick={(e)=>handleModalClick(e)} className="modal-content">
|
||||||
|
<div className="modal-header">
|
||||||
|
<div className="modal-closeIcon">
|
||||||
|
<div onClick={(e)=>toggleResqueakModal(e)} className="modal-closeIcon-wrap">
|
||||||
|
<ICON_CLOSE />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p className="modal-title">Resqueak</p>
|
||||||
|
</div>
|
||||||
|
<div style={{marginTop:'5px'}} className="modal-body">
|
||||||
|
<MakeResqueak resqueakedSqueak={props.squeak} submittedCallback={toggleResqueakModal} />
|
||||||
|
</div>
|
||||||
|
</div> : null}
|
||||||
|
</div> : null}
|
||||||
|
|
||||||
|
{/* delete modal */}
|
||||||
|
{props.squeak ?
|
||||||
|
<div onClick={(e)=>toggleDeleteModal(e)} style={{display: deleteModalOpen ? 'block' : 'none'}} className="modal-edit">
|
||||||
|
{deleteModalOpen ?
|
||||||
|
<div style={{minHeight: '350px', height: 'initial'}} onClick={(e)=>handleModalClick(e)} className="modal-content">
|
||||||
|
<div className="modal-header">
|
||||||
|
<div className="modal-closeIcon">
|
||||||
|
<div onClick={(e)=>toggleDeleteModal(e)} className="modal-closeIcon-wrap">
|
||||||
|
<ICON_CLOSE />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p className="modal-title">Delete Squeak</p>
|
||||||
|
</div>
|
||||||
|
<div style={{marginTop:'5px'}} className="modal-body">
|
||||||
|
<DeleteSqueak squeakHash={props.id} submittedCallback={toggleDeleteModal} />
|
||||||
|
</div>
|
||||||
|
</div> : null}
|
||||||
|
</div> : null}
|
||||||
|
|
||||||
|
{/* buy modal */}
|
||||||
|
{props.squeak ?
|
||||||
|
<div onClick={(e)=>toggleBuyModal(e)} style={{display: buyModalOpen ? 'block' : 'none'}} className="modal-edit">
|
||||||
|
{buyModalOpen ?
|
||||||
|
<div style={{minHeight: '350px', height: 'initial'}} onClick={(e)=>handleModalClick(e)} className="modal-content">
|
||||||
|
<div className="modal-header">
|
||||||
|
<div className="modal-closeIcon">
|
||||||
|
<div onClick={(e)=>toggleBuyModal(e)} className="modal-closeIcon-wrap">
|
||||||
|
<ICON_CLOSE />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p className="modal-title">Buy Squeak</p>
|
||||||
|
</div>
|
||||||
|
<div style={{marginTop:'5px'}} className="modal-body">
|
||||||
|
<BuySqueak squeak={props.squeak} submittedCallback={toggleBuyModal} />
|
||||||
|
</div>
|
||||||
|
</div> : null}
|
||||||
|
</div> : null}
|
||||||
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
});
|
});
|
||||||
|
|
||||||
export default withRouter(SqueakCard)
|
export default withRouter(SqueakCard)
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ import { Link } from 'react-router-dom'
|
||||||
import { getProfileImageSrcString } from '../../squeakimages/images';
|
import { getProfileImageSrcString } from '../../squeakimages/images';
|
||||||
import Loader from '../../components/Loader'
|
import Loader from '../../components/Loader'
|
||||||
|
|
||||||
import { Form, Input, Select, Checkbox, Relevant, Debug, TextArea, Option } from 'informed';
|
import { Form, Input, Select, Checkbox, Relevant, Debug, TextArea, Option, useFormApi } from 'informed';
|
||||||
|
|
||||||
import { useSelector } from 'react-redux'
|
import { useSelector } from 'react-redux'
|
||||||
import { useDispatch } from 'react-redux'
|
import { useDispatch } from 'react-redux'
|
||||||
|
|
@ -36,6 +36,17 @@ const DeleteSqueak = (props) => {
|
||||||
|
|
||||||
const author = props.replyToSqueak && props.replyToSqueak.getAuthor();
|
const author = props.replyToSqueak && props.replyToSqueak.getAuthor();
|
||||||
|
|
||||||
|
const SubmitButton = () => {
|
||||||
|
const formApi = useFormApi();
|
||||||
|
|
||||||
|
return <button
|
||||||
|
type="submit"
|
||||||
|
className={'squeak-btn-side squeak-btn-active'}
|
||||||
|
onClick={formApi.submitForm}>
|
||||||
|
Delete
|
||||||
|
</button>
|
||||||
|
};
|
||||||
|
|
||||||
const DeleteSqueakForm = () => (
|
const DeleteSqueakForm = () => (
|
||||||
<Form onSubmit={deleteSqueak} className="Squeak-input-side">
|
<Form onSubmit={deleteSqueak} className="Squeak-input-side">
|
||||||
<div className="inner-input-links">
|
<div className="inner-input-links">
|
||||||
|
|
@ -44,9 +55,7 @@ const DeleteSqueak = (props) => {
|
||||||
<div className="squeak-btn-holder">
|
<div className="squeak-btn-holder">
|
||||||
<div style={{ fontSize: '13px', color: null }}>
|
<div style={{ fontSize: '13px', color: null }}>
|
||||||
</div>
|
</div>
|
||||||
<button type="submit" className={'squeak-btn-side squeak-btn-active'}>
|
<SubmitButton />
|
||||||
Delete
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Form>
|
</Form>
|
||||||
|
|
|
||||||
165
frontend/src/features/squeaks/MakeResqueak.js
Normal file
165
frontend/src/features/squeaks/MakeResqueak.js
Normal file
|
|
@ -0,0 +1,165 @@
|
||||||
|
import React, { useEffect, useState, useContext, useRef } from 'react'
|
||||||
|
import { withRouter } from 'react-router-dom'
|
||||||
|
import { unwrapResult } from '@reduxjs/toolkit'
|
||||||
|
|
||||||
|
import moment from 'moment'
|
||||||
|
import ContentEditable from 'react-contenteditable'
|
||||||
|
import { Link } from 'react-router-dom'
|
||||||
|
import { getProfileImageSrcString } from '../../squeakimages/images';
|
||||||
|
import Loader from '../../components/Loader'
|
||||||
|
|
||||||
|
import { Form, Input, Select, Checkbox, Relevant, Debug, TextArea, Option, FormStateAccessor, useFormApi } from 'informed';
|
||||||
|
|
||||||
|
import { useSelector } from 'react-redux'
|
||||||
|
import { useDispatch } from 'react-redux'
|
||||||
|
|
||||||
|
|
||||||
|
import {
|
||||||
|
setMakeResqueak,
|
||||||
|
selectMakeResqueakStatus,
|
||||||
|
selectMakeSqueakStatus,
|
||||||
|
} from '../squeaks/squeaksSlice'
|
||||||
|
import {
|
||||||
|
selectSigningProfiles,
|
||||||
|
fetchSigningProfiles,
|
||||||
|
} from '../../features/profiles/profilesSlice'
|
||||||
|
|
||||||
|
const MakeResqueak = (props) => {
|
||||||
|
const signingProfiles = useSelector(selectSigningProfiles);
|
||||||
|
const makeSqueakStatus = useSelector(selectMakeSqueakStatus);
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
dispatch(fetchSigningProfiles());
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
|
||||||
|
const submitSqueak = ({ values }) => {
|
||||||
|
// TODO: toggle modal off here.
|
||||||
|
console.log(values);
|
||||||
|
console.log(values.signingProfileId);
|
||||||
|
|
||||||
|
if (!values.signingProfileId) {
|
||||||
|
alert('Signing Profile must be selected.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!props.resqueakedSqueak) {
|
||||||
|
alert('Resqueaked squeak cannot be empty.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const makeValues = {
|
||||||
|
signingProfile: values.signingProfileId,
|
||||||
|
resqueakedHash: props.resqueakedSqueak.getSqueakHash(),
|
||||||
|
replyTo: props.replyToSqueak ? props.replyToSqueak.getSqueakHash() : null,
|
||||||
|
}
|
||||||
|
console.log('makeResqueak');
|
||||||
|
dispatch(setMakeResqueak(makeValues))
|
||||||
|
.then(unwrapResult)
|
||||||
|
.then((squeakHash) => {
|
||||||
|
props.history.push(`/app/squeak/${squeakHash}`);
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
alert(err.message);
|
||||||
|
});
|
||||||
|
if (props.submittedCallback) {
|
||||||
|
props.submittedCallback();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Show profile image for selected signing profile.
|
||||||
|
// <div className="Squeak-profile-wrapper">
|
||||||
|
// {signingProfile && <img alt="" style={{ borderRadius: '50%', minWidth: '49px' }} width="100%" height="49px" src={`${getProfileImageSrcString(signingProfile)}`} />}
|
||||||
|
// </div>
|
||||||
|
|
||||||
|
const author = props.resqueakedSqueak && props.resqueakedSqueak.getAuthor();
|
||||||
|
|
||||||
|
const validateContent = value => {
|
||||||
|
if (!value || value.length > 280)
|
||||||
|
return 'Content must be less than 280 characters';
|
||||||
|
};
|
||||||
|
|
||||||
|
const SubmitButton = () => {
|
||||||
|
const formApi = useFormApi();
|
||||||
|
|
||||||
|
return <button
|
||||||
|
type="submit"
|
||||||
|
className={'squeak-btn-side squeak-btn-active'}
|
||||||
|
onClick={formApi.submitForm}>
|
||||||
|
Resqueak
|
||||||
|
</button>
|
||||||
|
};
|
||||||
|
|
||||||
|
const MakeResqueakForm = () => (
|
||||||
|
<Form onSubmit={submitSqueak} className="Squeak-input-side">
|
||||||
|
<Select class="informed-select" name="signingProfileId" initialValue="">
|
||||||
|
<Option value="" disabled>
|
||||||
|
Select Signing Profile...
|
||||||
|
</Option>
|
||||||
|
{signingProfiles.map(p => {
|
||||||
|
return <option value={p.getProfileId()}>{p.getProfileName()}</option>
|
||||||
|
})}
|
||||||
|
</Select>
|
||||||
|
<div className="inner-input-links">
|
||||||
|
<div className="input-links-side">
|
||||||
|
</div>
|
||||||
|
<div className="squeak-btn-holder">
|
||||||
|
<SubmitButton />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Form>
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
|
||||||
|
{/* Squeak being resqueaked. */}
|
||||||
|
{props.resqueakedSqueak ?
|
||||||
|
<div className="reply-content-wrapper">
|
||||||
|
<div className="card-userPic-wrapper">
|
||||||
|
<Link onClick={(e)=>e.stopPropagation()} to={`/app/profile/${props.resqueakedSqueak.getAuthorPubkey()}`}>
|
||||||
|
<img alt="" style={{borderRadius:'50%', minWidth:'49px'}} width="100%" height="49px" src={author ? `${getProfileImageSrcString(props.resqueakedSqueak.getAuthor())}`: null}/>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
<div className="card-content-wrapper">
|
||||||
|
<div className="card-content-header">
|
||||||
|
<div className="card-header-detail">
|
||||||
|
<span className="card-header-user">
|
||||||
|
<Link onClick={(e)=>e.stopPropagation()} to={`/app/profile/${props.resqueakedSqueak.getAuthorPubkey()}`}>{author ? author.getProfileName(): 'Unknown Author'}</Link>
|
||||||
|
</span>
|
||||||
|
<span className="card-header-username">
|
||||||
|
<Link onClick={(e)=>e.stopPropagation()} to={`/app/profile/${props.resqueakedSqueak.getAuthorPubkey()}`}>{'@'+props.resqueakedSqueak.getAuthorPubkey()}</Link>
|
||||||
|
</span>
|
||||||
|
<span className="card-header-dot">·</span>
|
||||||
|
<span className="card-header-date">
|
||||||
|
{moment(props.resqueakedSqueak.getBlockTime() * 1000).fromNow()}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="card-content-info">
|
||||||
|
{props.resqueakedSqueak.getContentStr()}
|
||||||
|
</div>
|
||||||
|
<div className="reply-to-user">
|
||||||
|
<span className="reply-squeak-username">
|
||||||
|
Replying to
|
||||||
|
</span>
|
||||||
|
<span className="main-squeak-user">
|
||||||
|
@{props.resqueakedSqueak.getAuthorPubkey()}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div> : null }
|
||||||
|
|
||||||
|
|
||||||
|
{/* New squeak content input. */}
|
||||||
|
<div className="Squeak-input-wrapper">
|
||||||
|
<MakeResqueakForm />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default withRouter(MakeResqueak)
|
||||||
|
|
@ -8,7 +8,7 @@ import { Link } from 'react-router-dom'
|
||||||
import { getProfileImageSrcString } from '../../squeakimages/images';
|
import { getProfileImageSrcString } from '../../squeakimages/images';
|
||||||
import Loader from '../../components/Loader'
|
import Loader from '../../components/Loader'
|
||||||
|
|
||||||
import { Form, Input, Select, Checkbox, Relevant, Debug, TextArea, Option, FormStateAccessor } from 'informed';
|
import { Form, Input, Select, Checkbox, Relevant, Debug, TextArea, Option, FormStateAccessor, useFormApi } from 'informed';
|
||||||
|
|
||||||
import { useSelector } from 'react-redux'
|
import { useSelector } from 'react-redux'
|
||||||
import { useDispatch } from 'react-redux'
|
import { useDispatch } from 'react-redux'
|
||||||
|
|
@ -78,9 +78,20 @@ const MakeSqueak = (props) => {
|
||||||
const author = props.replyToSqueak && props.replyToSqueak.getAuthor();
|
const author = props.replyToSqueak && props.replyToSqueak.getAuthor();
|
||||||
|
|
||||||
const validateContent = value => {
|
const validateContent = value => {
|
||||||
if (!value || value.length > 280)
|
if (value && value.length > 280)
|
||||||
return 'Content must be less than 280 characters';
|
return 'Content must be less than 280 characters';
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const SubmitButton = () => {
|
||||||
|
const formApi = useFormApi();
|
||||||
|
|
||||||
|
return <button
|
||||||
|
type="submit"
|
||||||
|
className={'squeak-btn-side squeak-btn-active'}
|
||||||
|
onClick={formApi.submitForm}>
|
||||||
|
Squeak
|
||||||
|
</button>
|
||||||
|
};
|
||||||
|
|
||||||
const MakeSqueakForm = () => (
|
const MakeSqueakForm = () => (
|
||||||
<Form onSubmit={submitSqueak} className="Squeak-input-side">
|
<Form onSubmit={submitSqueak} className="Squeak-input-side">
|
||||||
|
|
@ -104,9 +115,7 @@ const MakeSqueak = (props) => {
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</FormStateAccessor>
|
</FormStateAccessor>
|
||||||
<button type="submit" className={'squeak-btn-side squeak-btn-active'}>
|
<SubmitButton />
|
||||||
Squeak
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Form>
|
</Form>
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@ import { ICON_ARROWBACK, ICON_HEART, ICON_REPLY, ICON_RETWEET, ICON_HEARTFULL,
|
||||||
import SqueakCard from '../../components/SqueakCard'
|
import SqueakCard from '../../components/SqueakCard'
|
||||||
import Loader from '../../components/Loader'
|
import Loader from '../../components/Loader'
|
||||||
import MakeSqueak from '../squeaks/MakeSqueak'
|
import MakeSqueak from '../squeaks/MakeSqueak'
|
||||||
|
import MakeResqueak from '../squeaks/MakeResqueak'
|
||||||
import DeleteSqueak from '../../features/squeaks/DeleteSqueak'
|
import DeleteSqueak from '../../features/squeaks/DeleteSqueak'
|
||||||
import BuySqueak from '../squeaks/BuySqueak'
|
import BuySqueak from '../squeaks/BuySqueak'
|
||||||
|
|
||||||
|
|
@ -66,6 +67,7 @@ import { ICON_ARROWBACK, ICON_HEART, ICON_REPLY, ICON_RETWEET, ICON_HEARTFULL,
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
const [replyModalOpen, setModalOpen] = useState(false)
|
const [replyModalOpen, setModalOpen] = useState(false)
|
||||||
|
const [resqueakModalOpen, setResqueakModalOpen] = useState(false)
|
||||||
const [deleteModalOpen, setDeleteModalOpen] = useState(false)
|
const [deleteModalOpen, setDeleteModalOpen] = useState(false)
|
||||||
const [buyModalOpen, setBuyModalOpen] = useState(false)
|
const [buyModalOpen, setBuyModalOpen] = useState(false)
|
||||||
const [spendingModalOpen, setSpendingModalOpen] = useState(false)
|
const [spendingModalOpen, setSpendingModalOpen] = useState(false)
|
||||||
|
|
@ -92,6 +94,15 @@ import { ICON_ARROWBACK, ICON_HEART, ICON_REPLY, ICON_RETWEET, ICON_HEARTFULL,
|
||||||
setModalOpen(!replyModalOpen)
|
setModalOpen(!replyModalOpen)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const toggleResqueakModal = (e, type) => {
|
||||||
|
if(e){ e.stopPropagation() }
|
||||||
|
|
||||||
|
console.log('Toggling resqueak modal: ', resqueakModalOpen);
|
||||||
|
// if(param === 'edit'){setSaved(false)}
|
||||||
|
// if(type === 'parent'){setParent(true)}else{setParent(false)}
|
||||||
|
setResqueakModalOpen(!resqueakModalOpen)
|
||||||
|
}
|
||||||
|
|
||||||
const toggleDeleteModal = (e, type) => {
|
const toggleDeleteModal = (e, type) => {
|
||||||
if(e){ e.stopPropagation() }
|
if(e){ e.stopPropagation() }
|
||||||
// if(param === 'edit'){setSaved(false)}
|
// if(param === 'edit'){setSaved(false)}
|
||||||
|
|
@ -170,6 +181,32 @@ import { ICON_ARROWBACK, ICON_HEART, ICON_REPLY, ICON_RETWEET, ICON_HEARTFULL,
|
||||||
// const squeakOffers = [];
|
// const squeakOffers = [];
|
||||||
|
|
||||||
|
|
||||||
|
const getRegularSqueakContent = (squeak) => {
|
||||||
|
return squeak.getContentStr() ?
|
||||||
|
<div className="squeak-content">
|
||||||
|
{squeak.getContentStr()}
|
||||||
|
</div> :
|
||||||
|
<Link>
|
||||||
|
<div onClick={()=>toggleBuyModal(props.match.params.id)}
|
||||||
|
className="squeak-content locked-content">
|
||||||
|
<ICON_LOCKFILL styles={{width:'48px', height:"48px", padding: "5px"}} />
|
||||||
|
<div>
|
||||||
|
Locked content
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Link>
|
||||||
|
}
|
||||||
|
|
||||||
|
const getResqueakContent = (squeak) => {
|
||||||
|
const resqueakedHash = squeak.getResqueakedHash();
|
||||||
|
const resqueakedSqueak = squeak.getResqueakedSqueak();
|
||||||
|
const resqueakedAuthor = resqueakedSqueak && resqueakedSqueak.getAuthor();
|
||||||
|
return <div className="squeak-content">
|
||||||
|
<SqueakCard squeak={resqueakedSqueak} key={resqueakedHash} id={resqueakedHash} user={resqueakedAuthor}
|
||||||
|
replies={[]} hasReply={false} />
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
const squeak = currentSqueak;
|
const squeak = currentSqueak;
|
||||||
const author = currentSqueak && currentSqueak.getAuthor();
|
const author = currentSqueak && currentSqueak.getAuthor();
|
||||||
|
|
@ -199,19 +236,10 @@ import { ICON_ARROWBACK, ICON_HEART, ICON_REPLY, ICON_RETWEET, ICON_HEARTFULL,
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{squeak.getContentStr() ?
|
|
||||||
<div className="squeak-content">
|
{squeak.getIsResqueak() ?
|
||||||
{squeak.getContentStr()}
|
getResqueakContent(squeak) :
|
||||||
</div> :
|
getRegularSqueakContent(squeak)
|
||||||
<Link>
|
|
||||||
<div onClick={()=>toggleBuyModal(props.match.params.id)}
|
|
||||||
className="squeak-content locked-content">
|
|
||||||
<ICON_LOCKFILL styles={{width:'48px', height:"48px", padding: "5px"}} />
|
|
||||||
<div>
|
|
||||||
Locked content
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Link>
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -238,10 +266,11 @@ import { ICON_ARROWBACK, ICON_HEART, ICON_REPLY, ICON_RETWEET, ICON_HEARTFULL,
|
||||||
<div className="card-icon reply-icon"> <ICON_REPLY /> </div>
|
<div className="card-icon reply-icon"> <ICON_REPLY /> </div>
|
||||||
{squeak.getNumReplies()}
|
{squeak.getNumReplies()}
|
||||||
</div>
|
</div>
|
||||||
<div onClick={()=>resqueak(squeak.getSqueakHash())} className="squeak-int-icon">
|
<div onClick={()=>toggleResqueakModal()} className="squeak-int-icon">
|
||||||
<div className="card-icon resqueak-icon">
|
<div className="card-icon resqueak-icon">
|
||||||
<ICON_RETWEET styles={false ? {stroke: 'rgb(23, 191, 99)'} : {fill:'rgb(101, 119, 134)'}}/>
|
<ICON_RETWEET styles={false ? {stroke: 'rgb(23, 191, 99)'} : {fill:'rgb(101, 119, 134)'}}/>
|
||||||
</div>
|
</div>
|
||||||
|
{squeak.getNumResqueaks()}
|
||||||
</div>
|
</div>
|
||||||
<div onClick={()=>{
|
<div onClick={()=>{
|
||||||
squeak.getLikedTimeMs() ?
|
squeak.getLikedTimeMs() ?
|
||||||
|
|
@ -341,88 +370,106 @@ import { ICON_ARROWBACK, ICON_HEART, ICON_REPLY, ICON_RETWEET, ICON_HEARTFULL,
|
||||||
</div>:null}
|
</div>:null}
|
||||||
|
|
||||||
{squeak ?
|
{squeak ?
|
||||||
<div onClick={()=>toggleDeleteModal()} style={{display: deleteModalOpen ? 'block' : 'none'}} className="modal-edit">
|
<div onClick={()=>toggleResqueakModal()} style={{display: resqueakModalOpen ? 'block' : 'none'}} className="modal-edit">
|
||||||
{deleteModalOpen ?
|
{resqueakModalOpen ?
|
||||||
<div style={{minHeight: '379px', height: 'initial'}} onClick={(e)=>handleModalClick(e)} className="modal-content">
|
<div style={{minHeight: '379px', height: 'initial'}} onClick={(e)=>handleModalClick(e)} className="modal-content">
|
||||||
<div className="modal-header">
|
<div className="modal-header">
|
||||||
<div className="modal-closeIcon">
|
<div className="modal-closeIcon">
|
||||||
<div onClick={()=>toggleDeleteModal()} className="modal-closeIcon-wrap">
|
<div onClick={()=>toggleResqueakModal()} className="modal-closeIcon-wrap">
|
||||||
<ICON_CLOSE />
|
<ICON_CLOSE />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<p className="modal-title">Delete Squeak</p>
|
<p className="modal-title">Resqueak</p>
|
||||||
</div>
|
</div>
|
||||||
<div style={{marginTop:'5px'}} className="modal-body">
|
<div style={{marginTop:'5px'}} className="modal-body">
|
||||||
<DeleteSqueak squeakHash={squeak.getSqueakHash()} submittedCallback={toggleDeleteModal} />
|
<MakeResqueak resqueakedSqueak={squeak} submittedCallback={toggleResqueakModal} />
|
||||||
</div>
|
</div>
|
||||||
</div> : null}
|
</div> : null}
|
||||||
</div>:null}
|
</div>:null}
|
||||||
|
|
||||||
{squeak ?
|
{squeak ?
|
||||||
<div onClick={()=>toggleBuyModal()} style={{display: buyModalOpen ? 'block' : 'none'}} className="modal-edit">
|
<div onClick={()=>toggleDeleteModal()} style={{display: deleteModalOpen ? 'block' : 'none'}} className="modal-edit">
|
||||||
{buyModalOpen ?
|
{deleteModalOpen ?
|
||||||
<div style={{minHeight: '379px', height: 'initial'}} onClick={(e)=>handleModalClick(e)} className="modal-content">
|
<div style={{minHeight: '379px', height: 'initial'}} onClick={(e)=>handleModalClick(e)} className="modal-content">
|
||||||
<div className="modal-header">
|
<div className="modal-header">
|
||||||
<div className="modal-closeIcon">
|
<div className="modal-closeIcon">
|
||||||
<div onClick={()=>toggleBuyModal()} className="modal-closeIcon-wrap">
|
<div onClick={()=>toggleDeleteModal()} className="modal-closeIcon-wrap">
|
||||||
<ICON_CLOSE />
|
<ICON_CLOSE />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<p className="modal-title">Buy Squeak</p>
|
<p className="modal-title">Delete Squeak</p>
|
||||||
</div>
|
</div>
|
||||||
<div style={{marginTop:'5px'}} className="modal-body">
|
<div style={{marginTop:'5px'}} className="modal-body">
|
||||||
<BuySqueak squeak={squeak} submittedCallback={toggleBuyModal} />
|
<DeleteSqueak squeakHash={squeak.getSqueakHash()} submittedCallback={toggleDeleteModal} />
|
||||||
</div>
|
</div>
|
||||||
</div> : null}
|
</div> : null}
|
||||||
</div>:null}
|
</div>:null}
|
||||||
|
|
||||||
|
{squeak ?
|
||||||
{/* Modal for sent payments and received payments */}
|
<div onClick={()=>toggleBuyModal()} style={{display: buyModalOpen ? 'block' : 'none'}} className="modal-edit">
|
||||||
{squeak &&
|
{buyModalOpen ?
|
||||||
<div onClick={()=>toggleSpendingModal()} style={{display: spendingModalOpen ? 'block' : 'none'}} className="modal-edit">
|
<div style={{minHeight: '379px', height: 'initial'}} onClick={(e)=>handleModalClick(e)} className="modal-content">
|
||||||
<div onClick={(e)=>handleModalClick(e)} className="modal-content">
|
<div className="modal-header">
|
||||||
<div className="modal-header no-b-border">
|
<div className="modal-closeIcon">
|
||||||
<div className="modal-closeIcon">
|
<div onClick={()=>toggleBuyModal()} className="modal-closeIcon-wrap">
|
||||||
<div onClick={()=>toggleSpendingModal()} className="modal-closeIcon-wrap">
|
<ICON_CLOSE />
|
||||||
<ICON_CLOSE />
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<p className="modal-title">Buy Squeak</p>
|
||||||
</div>
|
</div>
|
||||||
<p className="modal-title">{null}</p>
|
<div style={{marginTop:'5px'}} className="modal-body">
|
||||||
</div>
|
<BuySqueak squeak={squeak} submittedCallback={toggleBuyModal} />
|
||||||
<div className="modal-body">
|
|
||||||
<div className="explore-nav-menu">
|
|
||||||
<div onClick={()=>setTab('Sent Payments')} className={tab =='Sent Payments' ? `explore-nav-item activeTab` : `explore-nav-item`}>
|
|
||||||
Sent Payments
|
|
||||||
</div>
|
|
||||||
<div onClick={()=>setTab('Received Payments')} className={tab =='Received Payments' ? `explore-nav-item activeTab` : `explore-nav-item`}>
|
|
||||||
Received Payments
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="modal-scroll">
|
</div> : null}
|
||||||
{tab === 'Sent Payments' ?
|
</div>:null}
|
||||||
<>
|
|
||||||
<SentPayments squeakHash={props.id} />
|
|
||||||
</>
|
|
||||||
|
|
||||||
:
|
|
||||||
tab === 'Received Payments' ?
|
|
||||||
<>
|
|
||||||
<ReceivedPayments squeakHash={props.id} />
|
|
||||||
</>
|
|
||||||
: <div className="try-searching">
|
|
||||||
Nothing to see here ..
|
|
||||||
<div/>
|
|
||||||
Try searching for people, usernames, or keywords
|
|
||||||
|
|
||||||
|
{/* Modal for sent payments and received payments */}
|
||||||
|
{squeak &&
|
||||||
|
<div onClick={()=>toggleSpendingModal()} style={{display: spendingModalOpen ? 'block' : 'none'}} className="modal-edit">
|
||||||
|
<div onClick={(e)=>handleModalClick(e)} className="modal-content">
|
||||||
|
<div className="modal-header no-b-border">
|
||||||
|
<div className="modal-closeIcon">
|
||||||
|
<div onClick={()=>toggleSpendingModal()} className="modal-closeIcon-wrap">
|
||||||
|
<ICON_CLOSE />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p className="modal-title">{null}</p>
|
||||||
|
</div>
|
||||||
|
<div className="modal-body">
|
||||||
|
<div className="explore-nav-menu">
|
||||||
|
<div onClick={()=>setTab('Sent Payments')} className={tab =='Sent Payments' ? `explore-nav-item activeTab` : `explore-nav-item`}>
|
||||||
|
Sent Payments
|
||||||
|
</div>
|
||||||
|
<div onClick={()=>setTab('Received Payments')} className={tab =='Received Payments' ? `explore-nav-item activeTab` : `explore-nav-item`}>
|
||||||
|
Received Payments
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="modal-scroll">
|
||||||
|
{tab === 'Sent Payments' ?
|
||||||
|
<>
|
||||||
|
<SentPayments squeakHash={props.id} />
|
||||||
|
</>
|
||||||
|
|
||||||
|
:
|
||||||
|
tab === 'Received Payments' ?
|
||||||
|
<>
|
||||||
|
<ReceivedPayments squeakHash={props.id} />
|
||||||
|
</>
|
||||||
|
: <div className="try-searching">
|
||||||
|
Nothing to see here ..
|
||||||
|
<div/>
|
||||||
|
Try searching for people, usernames, or keywords
|
||||||
|
|
||||||
|
</div>
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
}
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>}
|
||||||
</div>
|
|
||||||
</div>}
|
|
||||||
|
|
||||||
|
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
|
|
||||||
export default withRouter(Squeak)
|
export default withRouter(Squeak)
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@ import {
|
||||||
getSearchSqueaks,
|
getSearchSqueaks,
|
||||||
getProfileSqueaks,
|
getProfileSqueaks,
|
||||||
makeSqueak,
|
makeSqueak,
|
||||||
|
makeResqueak,
|
||||||
deleteSqueak,
|
deleteSqueak,
|
||||||
getSqueakOffers,
|
getSqueakOffers,
|
||||||
buySqueak,
|
buySqueak,
|
||||||
|
|
@ -165,6 +166,23 @@ export const setMakeSqueak = createAsyncThunk(
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
export const setMakeResqueak = createAsyncThunk(
|
||||||
|
'squeaks/setMakeResqueak',
|
||||||
|
async (values) => {
|
||||||
|
console.log('Making resqueak');
|
||||||
|
let profileId = values.signingProfile;
|
||||||
|
let resqueakedHash = values.resqueakedHash;
|
||||||
|
let replyTo = values.replyTo;
|
||||||
|
|
||||||
|
const response = await makeResqueak(
|
||||||
|
profileId,
|
||||||
|
resqueakedHash,
|
||||||
|
replyTo,
|
||||||
|
);
|
||||||
|
return response.getSqueakHash();
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
export const fetchSqueakOffers = createAsyncThunk(
|
export const fetchSqueakOffers = createAsyncThunk(
|
||||||
'squeaks/fetchSqueakOffers',
|
'squeaks/fetchSqueakOffers',
|
||||||
async (squeakHash) => {
|
async (squeakHash) => {
|
||||||
|
|
@ -202,10 +220,32 @@ const updatedSqueakInArray = (squeakArr, newSqueak) => {
|
||||||
if (currentIndex != -1) {
|
if (currentIndex != -1) {
|
||||||
squeakArr[currentIndex] = newSqueak;
|
squeakArr[currentIndex] = newSqueak;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Update resqueaked squeaks
|
||||||
|
for (let i = 0; i < squeakArr.length; i++) {
|
||||||
|
const currentSqueak = squeakArr[i];
|
||||||
|
const currentResqueakedSqueak = currentSqueak.getResqueakedSqueak();
|
||||||
|
if (currentResqueakedSqueak && currentResqueakedSqueak.getSqueakHash() === newSqueak.getSqueakHash()) {
|
||||||
|
const modifiedSqueak = currentSqueak.clone();
|
||||||
|
modifiedSqueak.setResqueakedSqueak(newSqueak);
|
||||||
|
squeakArr[i] = modifiedSqueak;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const removeSqueakInArray = (squeakArr, squeakHash) => {
|
const removeSqueakInArray = (squeakArr, squeakHash) => {
|
||||||
return squeakArr.filter(squeak => squeak.getSqueakHash() !== squeakHash);
|
return squeakArr.filter(squeak => squeak.getSqueakHash() !== squeakHash);
|
||||||
|
|
||||||
|
// Remove resqueaked squeaks
|
||||||
|
for (let i = 0; i < squeakArr.length; i++) {
|
||||||
|
const currentSqueak = squeakArr[i];
|
||||||
|
const currentResqueakedSqueak = currentSqueak.getResqueakedSqueak();
|
||||||
|
if (currentResqueakedSqueak && currentResqueakedSqueak.getSqueakHash() === squeakHash) {
|
||||||
|
const modifiedSqueak = currentSqueak.clone();
|
||||||
|
modifiedSqueak.setResqueakedSqueak(null);
|
||||||
|
squeakArr[i] = modifiedSqueak;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -342,6 +382,20 @@ const squeaksSlice = createSlice({
|
||||||
state.makeSqueakStatus = 'idle';
|
state.makeSqueakStatus = 'idle';
|
||||||
console.log('Go to new squeak');
|
console.log('Go to new squeak');
|
||||||
})
|
})
|
||||||
|
.addCase(setMakeResqueak.pending, (state, action) => {
|
||||||
|
console.log('setMakeResqueak pending');
|
||||||
|
state.makeSqueakStatus = 'loading'
|
||||||
|
})
|
||||||
|
.addCase(setMakeResqueak.rejected, (state, action) => {
|
||||||
|
state.makeSqueakStatus = 'idle'
|
||||||
|
})
|
||||||
|
.addCase(setMakeResqueak.fulfilled, (state, action) => {
|
||||||
|
console.log('setMakeResqueak fulfilled');
|
||||||
|
console.log(action);
|
||||||
|
const newSqueakHash = action.payload;
|
||||||
|
state.makeSqueakStatus = 'idle';
|
||||||
|
console.log('Go to new squeak');
|
||||||
|
})
|
||||||
.addCase(fetchSqueakOffers.pending, (state, action) => {
|
.addCase(fetchSqueakOffers.pending, (state, action) => {
|
||||||
state.squeakOffers = [];
|
state.squeakOffers = [];
|
||||||
state.squeakOffersStatus = 'loading'
|
state.squeakOffersStatus = 'loading'
|
||||||
|
|
|
||||||
|
|
@ -128,6 +128,10 @@ service SqueakAdmin {
|
||||||
*/
|
*/
|
||||||
rpc MakeSqueak (MakeSqueakRequest) returns (MakeSqueakReply) {}
|
rpc MakeSqueak (MakeSqueakRequest) returns (MakeSqueakReply) {}
|
||||||
|
|
||||||
|
/** sqkadmin: `makeresqueak`
|
||||||
|
*/
|
||||||
|
rpc MakeResqueak (MakeResqueakRequest) returns (MakeResqueakReply) {}
|
||||||
|
|
||||||
/** sqkadmin: `getsqueakdisplay`
|
/** sqkadmin: `getsqueakdisplay`
|
||||||
*/
|
*/
|
||||||
rpc GetSqueakDisplay (GetSqueakDisplayRequest) returns (GetSqueakDisplayReply) {}
|
rpc GetSqueakDisplay (GetSqueakDisplayRequest) returns (GetSqueakDisplayReply) {}
|
||||||
|
|
@ -588,6 +592,22 @@ message MakeSqueakReply {
|
||||||
string squeak_hash = 1;
|
string squeak_hash = 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
message MakeResqueakRequest {
|
||||||
|
/// The profile id
|
||||||
|
int32 profile_id = 1;
|
||||||
|
|
||||||
|
/// The content
|
||||||
|
string resqueaked_hash = 2;
|
||||||
|
|
||||||
|
/// The replyto hash
|
||||||
|
string replyto = 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
message MakeResqueakReply {
|
||||||
|
/// Hash of the created resqueak.
|
||||||
|
string squeak_hash = 1;
|
||||||
|
}
|
||||||
|
|
||||||
message GetSqueakDisplayRequest {
|
message GetSqueakDisplayRequest {
|
||||||
/// Hash of the squeak.
|
/// Hash of the squeak.
|
||||||
string squeak_hash = 1;
|
string squeak_hash = 1;
|
||||||
|
|
@ -658,6 +678,18 @@ message SqueakDisplayEntry {
|
||||||
|
|
||||||
/// Number of replies
|
/// Number of replies
|
||||||
int32 num_replies = 20;
|
int32 num_replies = 20;
|
||||||
|
|
||||||
|
/// Is resqueak
|
||||||
|
bool is_resqueak = 21;
|
||||||
|
|
||||||
|
/// Resqueaked hash
|
||||||
|
string resqueaked_hash = 22;
|
||||||
|
|
||||||
|
/// Resqueaked squeak
|
||||||
|
SqueakDisplayEntry resqueaked_squeak = 23;
|
||||||
|
|
||||||
|
/// Number of resqueaks
|
||||||
|
int32 num_resqueaks = 24;
|
||||||
}
|
}
|
||||||
|
|
||||||
message GetTimelineSqueakDisplaysRequest {
|
message GetTimelineSqueakDisplaysRequest {
|
||||||
|
|
|
||||||
|
|
@ -3,4 +3,4 @@ grpcio==1.39.0
|
||||||
grpcio-tools==1.39.0
|
grpcio-tools==1.39.0
|
||||||
importlib_resources==1.4.0
|
importlib_resources==1.4.0
|
||||||
pytest==6.2.5
|
pytest==6.2.5
|
||||||
squeaklib==0.12.0
|
squeaklib==0.13.1
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,6 @@ python-bitcoinlib==0.11.0
|
||||||
pyzmq==22.3.0
|
pyzmq==22.3.0
|
||||||
requests[socks]==2.26.0
|
requests[socks]==2.26.0
|
||||||
SQLAlchemy==1.4.25
|
SQLAlchemy==1.4.25
|
||||||
squeaklib==0.12.0
|
squeaklib==0.13.1
|
||||||
typed-config==0.2.5
|
typed-config==0.2.5
|
||||||
Werkzeug==2.0.3
|
Werkzeug==2.0.3
|
||||||
|
|
|
||||||
|
|
@ -67,12 +67,18 @@ def squeak_entry_to_message(squeak_entry: SqueakEntry) -> squeak_admin_pb2.Squea
|
||||||
if squeak_entry.squeak_profile else None),
|
if squeak_entry.squeak_profile else None),
|
||||||
liked_time_ms=squeak_entry.liked_time_ms, # type: ignore
|
liked_time_ms=squeak_entry.liked_time_ms, # type: ignore
|
||||||
num_replies=squeak_entry.num_replies,
|
num_replies=squeak_entry.num_replies,
|
||||||
|
num_resqueaks=squeak_entry.num_resqueaks,
|
||||||
is_private=(squeak_entry.recipient_public_key is not None),
|
is_private=(squeak_entry.recipient_public_key is not None),
|
||||||
recipient_pubkey=(squeak_entry.recipient_public_key.to_bytes(
|
recipient_pubkey=(squeak_entry.recipient_public_key.to_bytes(
|
||||||
).hex() if squeak_entry.recipient_public_key else None),
|
).hex() if squeak_entry.recipient_public_key else None),
|
||||||
is_recipient_known=(squeak_entry.recipient_squeak_profile is not None),
|
is_recipient_known=(squeak_entry.recipient_squeak_profile is not None),
|
||||||
recipient=(squeak_profile_to_message(squeak_entry.recipient_squeak_profile)
|
recipient=(squeak_profile_to_message(squeak_entry.recipient_squeak_profile)
|
||||||
if squeak_entry.recipient_squeak_profile else None),
|
if squeak_entry.recipient_squeak_profile else None),
|
||||||
|
is_resqueak=(squeak_entry.resqueaked_hash is not None),
|
||||||
|
resqueaked_hash=(squeak_entry.resqueaked_hash.hex()
|
||||||
|
if squeak_entry.resqueaked_hash else None), # type: ignore
|
||||||
|
resqueaked_squeak=(squeak_entry_to_message(squeak_entry.resqueaked_squeak)
|
||||||
|
if squeak_entry.resqueaked_squeak else None), # type: ignore
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -194,6 +200,7 @@ def message_to_squeak_entry(msg: squeak_admin_pb2.SqueakDisplayEntry) -> SqueakE
|
||||||
recipient_squeak_profile=None, # TODO: message to squeak profile
|
recipient_squeak_profile=None, # TODO: message to squeak profile
|
||||||
liked_time_ms=(msg.liked_time_ms if msg.liked_time_ms > 0 else None),
|
liked_time_ms=(msg.liked_time_ms if msg.liked_time_ms > 0 else None),
|
||||||
num_replies=0,
|
num_replies=0,
|
||||||
|
num_resqueaks=0,
|
||||||
content=(msg.content_str if len(msg.content_str) > 0 else None),
|
content=(msg.content_str if len(msg.content_str) > 0 else None),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -309,6 +309,30 @@ class SqueakAdminServerHandler(object):
|
||||||
squeak_hash=inserted_squeak_hash_str,
|
squeak_hash=inserted_squeak_hash_str,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def handle_make_resqueak(self, request):
|
||||||
|
profile_id = request.profile_id
|
||||||
|
resqueaked_hash_str = request.resqueaked_hash
|
||||||
|
resqueaked_hash = bytes.fromhex(resqueaked_hash_str)
|
||||||
|
replyto_hash_str = request.replyto
|
||||||
|
replyto_hash = bytes.fromhex(
|
||||||
|
replyto_hash_str) if replyto_hash_str else None
|
||||||
|
logger.info(
|
||||||
|
"Handle make resqueak with author profile id: {}".format(
|
||||||
|
profile_id,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
inserted_resqueak_hash = self.squeak_controller.make_resqueak(
|
||||||
|
profile_id,
|
||||||
|
resqueaked_hash,
|
||||||
|
replyto_hash,
|
||||||
|
)
|
||||||
|
inserted_resqueak_hash_str = optional_squeak_hash_to_hex(
|
||||||
|
inserted_resqueak_hash,
|
||||||
|
)
|
||||||
|
return squeak_admin_pb2.MakeResqueakReply(
|
||||||
|
squeak_hash=inserted_resqueak_hash_str,
|
||||||
|
)
|
||||||
|
|
||||||
def handle_get_squeak_display_entry(self, request):
|
def handle_get_squeak_display_entry(self, request):
|
||||||
squeak_hash_str = request.squeak_hash
|
squeak_hash_str = request.squeak_hash
|
||||||
squeak_hash = bytes.fromhex(squeak_hash_str)
|
squeak_hash = bytes.fromhex(squeak_hash_str)
|
||||||
|
|
|
||||||
|
|
@ -140,6 +140,9 @@ class SqueakAdminServerServicer(squeak_admin_pb2_grpc.SqueakAdminServicer):
|
||||||
def MakeSqueak(self, request, context):
|
def MakeSqueak(self, request, context):
|
||||||
return self.handler.handle_make_squeak(request)
|
return self.handler.handle_make_squeak(request)
|
||||||
|
|
||||||
|
def MakeResqueak(self, request, context):
|
||||||
|
return self.handler.handle_make_resqueak(request)
|
||||||
|
|
||||||
def GetSqueakDisplay(self, request, context):
|
def GetSqueakDisplay(self, request, context):
|
||||||
return self.handler.handle_get_squeak_display_entry(request)
|
return self.handler.handle_get_squeak_display_entry(request)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -313,6 +313,12 @@ def create_app(handler, username, password):
|
||||||
def makesqueakrequest(msg):
|
def makesqueakrequest(msg):
|
||||||
return handler.handle_make_squeak(msg)
|
return handler.handle_make_squeak(msg)
|
||||||
|
|
||||||
|
@app.route("/makeresqueak", methods=["POST"])
|
||||||
|
@login_required
|
||||||
|
@protobuf_serialized(squeak_admin_pb2.MakeResqueakRequest())
|
||||||
|
def makeresqueak(msg):
|
||||||
|
return handler.handle_make_resqueak(msg)
|
||||||
|
|
||||||
@app.route("/getsqueakdisplay", methods=["POST"])
|
@app.route("/getsqueakdisplay", methods=["POST"])
|
||||||
@login_required
|
@login_required
|
||||||
@protobuf_serialized(squeak_admin_pb2.GetSqueakDisplayRequest())
|
@protobuf_serialized(squeak_admin_pb2.GetSqueakDisplayRequest())
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"files": {
|
"files": {
|
||||||
"main.css": "/static/css/main.206389cd.css",
|
"main.css": "/static/css/main.206389cd.css",
|
||||||
"main.js": "/static/js/main.618916f4.js",
|
"main.js": "/static/js/main.bda102e3.js",
|
||||||
"static/css/306.7f7715a0.chunk.css": "/static/css/306.7f7715a0.chunk.css",
|
"static/css/306.7f7715a0.chunk.css": "/static/css/306.7f7715a0.chunk.css",
|
||||||
"static/js/306.90b2a66c.chunk.js": "/static/js/306.90b2a66c.chunk.js",
|
"static/js/306.90b2a66c.chunk.js": "/static/js/306.90b2a66c.chunk.js",
|
||||||
"static/css/900.b66ac5fb.chunk.css": "/static/css/900.b66ac5fb.chunk.css",
|
"static/css/900.b66ac5fb.chunk.css": "/static/css/900.b66ac5fb.chunk.css",
|
||||||
|
|
@ -11,7 +11,7 @@
|
||||||
"static/media/icon.svg": "/static/media/icon.982210ec9275956e4d3cc77fa90167fd.svg",
|
"static/media/icon.svg": "/static/media/icon.982210ec9275956e4d3cc77fa90167fd.svg",
|
||||||
"index.html": "/index.html",
|
"index.html": "/index.html",
|
||||||
"main.206389cd.css.map": "/static/css/main.206389cd.css.map",
|
"main.206389cd.css.map": "/static/css/main.206389cd.css.map",
|
||||||
"main.618916f4.js.map": "/static/js/main.618916f4.js.map",
|
"main.bda102e3.js.map": "/static/js/main.bda102e3.js.map",
|
||||||
"306.7f7715a0.chunk.css.map": "/static/css/306.7f7715a0.chunk.css.map",
|
"306.7f7715a0.chunk.css.map": "/static/css/306.7f7715a0.chunk.css.map",
|
||||||
"306.90b2a66c.chunk.js.map": "/static/js/306.90b2a66c.chunk.js.map",
|
"306.90b2a66c.chunk.js.map": "/static/js/306.90b2a66c.chunk.js.map",
|
||||||
"900.b66ac5fb.chunk.css.map": "/static/css/900.b66ac5fb.chunk.css.map",
|
"900.b66ac5fb.chunk.css.map": "/static/css/900.b66ac5fb.chunk.css.map",
|
||||||
|
|
@ -21,6 +21,6 @@
|
||||||
},
|
},
|
||||||
"entrypoints": [
|
"entrypoints": [
|
||||||
"static/css/main.206389cd.css",
|
"static/css/main.206389cd.css",
|
||||||
"static/js/main.618916f4.js"
|
"static/js/main.bda102e3.js"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
@ -1 +1 @@
|
||||||
<!doctype html><html lang="en"><head><meta charset="utf-8"/><link rel="icon" href="/favicon.ico"/><meta name="viewport" content="width=device-width,initial-scale=1"/><meta name="theme-color" content="#1da1f2"/><meta nam="description" content="Squeaknode!"/><meta name="og:title" content="Squeaknode"/><meta name="og:description" content="check out my the Squeaknode I made"/><link href="https://fonts.googleapis.com/css2?family=Assistant&display=swap" rel="stylesheet"><link rel="apple-touch-icon" href="/logo192.png"/><link rel="manifest" href="/manifest.json"/><title>Squeaknode</title><script defer="defer" src="/static/js/main.618916f4.js"></script><link href="/static/css/main.206389cd.css" rel="stylesheet"></head><body><noscript>You need to enable JavaScript to run this app.</noscript><div id="root"></div></body></html>
|
<!doctype html><html lang="en"><head><meta charset="utf-8"/><link rel="icon" href="/favicon.ico"/><meta name="viewport" content="width=device-width,initial-scale=1"/><meta name="theme-color" content="#1da1f2"/><meta nam="description" content="Squeaknode!"/><meta name="og:title" content="Squeaknode"/><meta name="og:description" content="check out my the Squeaknode I made"/><link href="https://fonts.googleapis.com/css2?family=Assistant&display=swap" rel="stylesheet"><link rel="apple-touch-icon" href="/logo192.png"/><link rel="manifest" href="/manifest.json"/><title>Squeaknode</title><script defer="defer" src="/static/js/main.bda102e3.js"></script><link href="/static/css/main.206389cd.css" rel="stylesheet"></head><body><noscript>You need to enable JavaScript to run this app.</noscript><div id="root"></div></body></html>
|
||||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
|
@ -24,6 +24,7 @@ from typing import List
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
import requests
|
import requests
|
||||||
|
from squeak.core import CResqueak
|
||||||
from squeak.core import CSqueak
|
from squeak.core import CSqueak
|
||||||
from squeak.core.keys import SqueakPublicKey
|
from squeak.core.keys import SqueakPublicKey
|
||||||
|
|
||||||
|
|
@ -97,8 +98,19 @@ class PeerClient:
|
||||||
)
|
)
|
||||||
if r.status_code != requests.codes.ok:
|
if r.status_code != requests.codes.ok:
|
||||||
return None
|
return None
|
||||||
squeak_bytes = r.content
|
logger.info(r)
|
||||||
return CSqueak.deserialize(squeak_bytes)
|
squeak_json = r.json()
|
||||||
|
# squeak_bytes = r.content
|
||||||
|
squeak_type = squeak_json['squeak_type']
|
||||||
|
squeak_bytes_hex = squeak_json['squeak_bytes']
|
||||||
|
squeak_bytes = bytes.fromhex(squeak_bytes_hex)
|
||||||
|
logger.info('Client downloaded: {}'.format(squeak_json))
|
||||||
|
if squeak_type == 'squeak':
|
||||||
|
return CSqueak.deserialize(squeak_bytes)
|
||||||
|
elif squeak_type == 'resqueak':
|
||||||
|
return CResqueak.deserialize(squeak_bytes)
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
|
||||||
def get_secret_key(self, squeak_hash: bytes) -> Optional[bytes]:
|
def get_secret_key(self, squeak_hash: bytes) -> Optional[bytes]:
|
||||||
squeak_hash_str = squeak_hash.hex()
|
squeak_hash_str = squeak_hash.hex()
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,7 @@ from typing import Tuple
|
||||||
|
|
||||||
import grpc
|
import grpc
|
||||||
from bitcoin.core import CBlockHeader
|
from bitcoin.core import CBlockHeader
|
||||||
|
from squeak.core import CBaseSqueak
|
||||||
from squeak.core import CSqueak
|
from squeak.core import CSqueak
|
||||||
|
|
||||||
from squeaknode.bitcoin.bitcoin_client import BitcoinClient
|
from squeaknode.bitcoin.bitcoin_client import BitcoinClient
|
||||||
|
|
@ -46,6 +47,7 @@ from squeaknode.core.squeaks import check_squeak
|
||||||
from squeaknode.core.squeaks import get_decrypted_content
|
from squeaknode.core.squeaks import get_decrypted_content
|
||||||
from squeaknode.core.squeaks import get_hash
|
from squeaknode.core.squeaks import get_hash
|
||||||
from squeaknode.core.squeaks import get_payment_point_of_secret_key
|
from squeaknode.core.squeaks import get_payment_point_of_secret_key
|
||||||
|
from squeaknode.core.squeaks import make_resqueak_with_block
|
||||||
from squeaknode.core.squeaks import make_squeak_with_block
|
from squeaknode.core.squeaks import make_squeak_with_block
|
||||||
from squeaknode.lightning.lightning_client import LightningClient
|
from squeaknode.lightning.lightning_client import LightningClient
|
||||||
|
|
||||||
|
|
@ -71,8 +73,6 @@ class SqueakCore:
|
||||||
) -> Tuple[CSqueak, bytes]:
|
) -> Tuple[CSqueak, bytes]:
|
||||||
"""Create a new squeak.
|
"""Create a new squeak.
|
||||||
|
|
||||||
TODO: Include the block header in the result tuple.
|
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
signing_profile: The profile of the author of the squeak.
|
signing_profile: The profile of the author of the squeak.
|
||||||
content_str: The content of the squeak as a string.
|
content_str: The content of the squeak as a string.
|
||||||
|
|
@ -100,7 +100,40 @@ class SqueakCore:
|
||||||
)
|
)
|
||||||
return squeak, secret_key
|
return squeak, secret_key
|
||||||
|
|
||||||
def check_squeak(self, squeak: CSqueak) -> None:
|
def make_resqueak(
|
||||||
|
self,
|
||||||
|
signing_profile: SqueakProfile,
|
||||||
|
resqueak_hash: bytes,
|
||||||
|
replyto_hash: Optional[bytes] = None,
|
||||||
|
) -> Tuple[CSqueak, bytes]:
|
||||||
|
"""Create a new resqueak.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
signing_profile: The profile of the author of the squeak.
|
||||||
|
resqueak_hash: The hash of the squeak to resqueak.
|
||||||
|
replyto_hash: The hash of the squeak to which this one is replying.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
CResqueak: the resqueak that was created.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
Exception: If the profile does not have a signing key.
|
||||||
|
"""
|
||||||
|
if signing_profile.private_key is None:
|
||||||
|
raise Exception("Can't make squeak with a contact profile.")
|
||||||
|
block_info = self.bitcoin_client.get_best_block_info()
|
||||||
|
block_height = block_info.block_height
|
||||||
|
block_hash = block_info.block_hash
|
||||||
|
squeak = make_resqueak_with_block(
|
||||||
|
signing_profile.private_key,
|
||||||
|
resqueak_hash,
|
||||||
|
block_height,
|
||||||
|
block_hash,
|
||||||
|
replyto_hash=replyto_hash,
|
||||||
|
)
|
||||||
|
return squeak
|
||||||
|
|
||||||
|
def check_squeak(self, base_squeak: CBaseSqueak) -> None:
|
||||||
"""Checks if the squeak is valid and has a valid signature.
|
"""Checks if the squeak is valid and has a valid signature.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
|
|
@ -112,9 +145,9 @@ class SqueakCore:
|
||||||
Raises:
|
Raises:
|
||||||
Exception: If the squeak is not valid.
|
Exception: If the squeak is not valid.
|
||||||
"""
|
"""
|
||||||
check_squeak(squeak)
|
check_squeak(base_squeak)
|
||||||
|
|
||||||
def get_block_header(self, squeak: CSqueak) -> CBlockHeader:
|
def get_block_header(self, base_squeak: CBaseSqueak) -> CBlockHeader:
|
||||||
"""Checks if the embedded block hash in the squeak is valid for its
|
"""Checks if the embedded block hash in the squeak is valid for its
|
||||||
block height and return the associtated block header.
|
block height and return the associtated block header.
|
||||||
|
|
||||||
|
|
@ -128,8 +161,8 @@ class SqueakCore:
|
||||||
Exception: If the block hash is not valid.
|
Exception: If the block hash is not valid.
|
||||||
"""
|
"""
|
||||||
block_info = self.bitcoin_client.get_block_info_by_height(
|
block_info = self.bitcoin_client.get_block_info_by_height(
|
||||||
squeak.nBlockHeight)
|
base_squeak.nBlockHeight)
|
||||||
if squeak.hashBlock != block_info.block_hash:
|
if base_squeak.hashBlock != block_info.block_hash:
|
||||||
raise Exception("Block hash incorrect.")
|
raise Exception("Block hash incorrect.")
|
||||||
return block_info.block_header
|
return block_info.block_header
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -42,5 +42,8 @@ class SqueakEntry(NamedTuple):
|
||||||
squeak_profile: Optional[SqueakProfile]
|
squeak_profile: Optional[SqueakProfile]
|
||||||
recipient_squeak_profile: Optional[SqueakProfile]
|
recipient_squeak_profile: Optional[SqueakProfile]
|
||||||
num_replies: int
|
num_replies: int
|
||||||
|
num_resqueaks: int
|
||||||
liked_time_ms: Optional[int] = None
|
liked_time_ms: Optional[int] = None
|
||||||
content: Optional[str] = None
|
content: Optional[str] = None
|
||||||
|
resqueaked_hash: Optional[bytes] = None
|
||||||
|
resqueaked_squeak: Optional['SqueakEntry'] = None # type: ignore
|
||||||
|
|
|
||||||
|
|
@ -24,7 +24,9 @@ from typing import Optional
|
||||||
from typing import Tuple
|
from typing import Tuple
|
||||||
|
|
||||||
from squeak.core import CheckSqueak
|
from squeak.core import CheckSqueak
|
||||||
|
from squeak.core import CResqueak
|
||||||
from squeak.core import CSqueak
|
from squeak.core import CSqueak
|
||||||
|
from squeak.core import MakeResqueak
|
||||||
from squeak.core import MakeSqueakFromStr
|
from squeak.core import MakeSqueakFromStr
|
||||||
from squeak.core.elliptic import payment_point_bytes_from_scalar_bytes
|
from squeak.core.elliptic import payment_point_bytes_from_scalar_bytes
|
||||||
from squeak.core.keys import SqueakPrivateKey
|
from squeak.core.keys import SqueakPrivateKey
|
||||||
|
|
@ -87,6 +89,37 @@ def make_squeak_with_block(
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def make_resqueak_with_block(
|
||||||
|
private_key: SqueakPrivateKey,
|
||||||
|
resqueak_hash: bytes,
|
||||||
|
block_height: int,
|
||||||
|
block_hash: bytes,
|
||||||
|
replyto_hash: Optional[bytes] = None,
|
||||||
|
) -> CResqueak:
|
||||||
|
"""Create a new resqueak.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
private_key: The private key to sign the squeak.
|
||||||
|
resqueak_hash: The hash of the squeak to resqueak.
|
||||||
|
block_height: The height of the latest block in the bitcoin blockchain.
|
||||||
|
block_hash: The hash of the latest block in the bitcoin blockchain.
|
||||||
|
replyto_hash: The hash of the squeak to which this one is replying.
|
||||||
|
recipient_public_key: The public key of the recipient of a private squeak.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
CResqueak: the resqueak that was created.
|
||||||
|
"""
|
||||||
|
timestamp = int(time.time())
|
||||||
|
return MakeResqueak(
|
||||||
|
private_key,
|
||||||
|
resqueak_hash,
|
||||||
|
block_height,
|
||||||
|
block_hash,
|
||||||
|
timestamp,
|
||||||
|
reply_to=replyto_hash,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def check_squeak(squeak: CSqueak) -> None:
|
def check_squeak(squeak: CSqueak) -> None:
|
||||||
"""Checks if the squeak is valid and has a valid signature.
|
"""Checks if the squeak is valid and has a valid signature.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,48 @@
|
||||||
|
# MIT License
|
||||||
|
#
|
||||||
|
# Copyright (c) 2020 Jonathan Zernik
|
||||||
|
#
|
||||||
|
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
# of this software and associated documentation files (the "Software"), to deal
|
||||||
|
# in the Software without restriction, including without limitation the rights
|
||||||
|
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
# copies of the Software, and to permit persons to whom the Software is
|
||||||
|
# furnished to do so, subject to the following conditions:
|
||||||
|
#
|
||||||
|
# The above copyright notice and this permission notice shall be included in all
|
||||||
|
# copies or substantial portions of the Software.
|
||||||
|
#
|
||||||
|
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
# SOFTWARE.
|
||||||
|
"""Add resqueak hash column in squeak table
|
||||||
|
|
||||||
|
Revision ID: b3d0395263c4
|
||||||
|
Revises: 0e3b79a31b58
|
||||||
|
Create Date: 2022-04-23 21:41:39.063229
|
||||||
|
|
||||||
|
"""
|
||||||
|
import sqlalchemy as sa
|
||||||
|
from alembic import op
|
||||||
|
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = 'b3d0395263c4'
|
||||||
|
down_revision = '0e3b79a31b58'
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade():
|
||||||
|
with op.batch_alter_table('squeak', schema=None) as batch_op:
|
||||||
|
batch_op.add_column(
|
||||||
|
sa.Column('resqueak_hash', sa.LargeBinary(length=32), nullable=True))
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade():
|
||||||
|
with op.batch_alter_table('squeak', schema=None) as batch_op:
|
||||||
|
batch_op.drop_column('resqueak_hash')
|
||||||
|
|
@ -82,6 +82,7 @@ class Models:
|
||||||
Column("block_time_s", Integer, nullable=False),
|
Column("block_time_s", Integer, nullable=False),
|
||||||
Column("liked_time_ms", SLBigInteger, default=None, nullable=True),
|
Column("liked_time_ms", SLBigInteger, default=None, nullable=True),
|
||||||
Column("content", String(280), nullable=True),
|
Column("content", String(280), nullable=True),
|
||||||
|
Column("resqueak_hash", LargeBinary(32), nullable=True),
|
||||||
)
|
)
|
||||||
|
|
||||||
self.profiles = Table(
|
self.profiles = Table(
|
||||||
|
|
|
||||||
|
|
@ -28,12 +28,15 @@ from typing import Optional
|
||||||
|
|
||||||
import sqlalchemy
|
import sqlalchemy
|
||||||
from bitcoin.core import CBlockHeader
|
from bitcoin.core import CBlockHeader
|
||||||
|
from sqlalchemy import distinct
|
||||||
from sqlalchemy import func
|
from sqlalchemy import func
|
||||||
from sqlalchemy import literal
|
from sqlalchemy import literal
|
||||||
from sqlalchemy import not_
|
from sqlalchemy import not_
|
||||||
from sqlalchemy import or_
|
from sqlalchemy import or_
|
||||||
from sqlalchemy.sql import select
|
from sqlalchemy.sql import select
|
||||||
from sqlalchemy.sql import tuple_
|
from sqlalchemy.sql import tuple_
|
||||||
|
from squeak.core import CBaseSqueak
|
||||||
|
from squeak.core import CResqueak
|
||||||
from squeak.core import CSqueak
|
from squeak.core import CSqueak
|
||||||
from squeak.core.keys import SqueakPrivateKey
|
from squeak.core.keys import SqueakPrivateKey
|
||||||
from squeak.core.keys import SqueakPublicKey
|
from squeak.core.keys import SqueakPublicKey
|
||||||
|
|
@ -86,11 +89,16 @@ class SqueakDb:
|
||||||
|
|
||||||
# Create aliases for profiles
|
# Create aliases for profiles
|
||||||
self.author_profiles = self.profiles.alias()
|
self.author_profiles = self.profiles.alias()
|
||||||
|
self.resqueaked_author_profiles = self.profiles.alias()
|
||||||
self.recipient_profiles = self.profiles.alias()
|
self.recipient_profiles = self.profiles.alias()
|
||||||
|
|
||||||
# Create aliases for squeaks
|
# Create aliases for squeaks
|
||||||
self.display_squeaks = self.squeaks.alias()
|
self.display_squeaks = self.squeaks.alias()
|
||||||
self.reply_squeaks = self.squeaks.alias()
|
self.reply_squeaks = self.squeaks.alias()
|
||||||
|
self.resqueaked_squeaks = self.squeaks.alias()
|
||||||
|
self.resqueaked_reply_squeaks = self.squeaks.alias()
|
||||||
|
self.resqueaks = self.squeaks.alias()
|
||||||
|
self.resqueaked_resqueaks = self.squeaks.alias()
|
||||||
|
|
||||||
def init_with_retries(
|
def init_with_retries(
|
||||||
self,
|
self,
|
||||||
|
|
@ -242,7 +250,36 @@ class SqueakDb:
|
||||||
logger.debug("Failed to insert squeak.", exc_info=True)
|
logger.debug("Failed to insert squeak.", exc_info=True)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def get_squeak(self, squeak_hash: bytes) -> Optional[CSqueak]:
|
def insert_resqueak(self, resqueak: CResqueak, block_header: CBlockHeader) -> Optional[bytes]:
|
||||||
|
""" Insert a new resqueak.
|
||||||
|
|
||||||
|
Return the hash (bytes) of the inserted resqueak.
|
||||||
|
Return None if resqueak already exists.
|
||||||
|
"""
|
||||||
|
ins = self.squeaks.insert().values(
|
||||||
|
created_time_ms=self.timestamp_now_ms,
|
||||||
|
hash=get_hash(resqueak),
|
||||||
|
squeak=resqueak.serialize(),
|
||||||
|
reply_hash=(resqueak.hashReplySqk
|
||||||
|
if resqueak.is_reply
|
||||||
|
else None),
|
||||||
|
block_hash=resqueak.hashBlock,
|
||||||
|
block_height=resqueak.nBlockHeight,
|
||||||
|
time_s=resqueak.nTime,
|
||||||
|
author_public_key=resqueak.GetPubKey().to_bytes(),
|
||||||
|
resqueak_hash=resqueak.hashResqueakSqk,
|
||||||
|
block_time_s=block_header.nTime,
|
||||||
|
)
|
||||||
|
with self.get_connection() as connection:
|
||||||
|
try:
|
||||||
|
res = connection.execute(ins)
|
||||||
|
squeak_hash = res.inserted_primary_key[0]
|
||||||
|
return squeak_hash
|
||||||
|
except sqlalchemy.exc.IntegrityError:
|
||||||
|
logger.debug("Failed to insert squeak.", exc_info=True)
|
||||||
|
return None
|
||||||
|
|
||||||
|
def get_squeak(self, squeak_hash: bytes) -> Optional[CBaseSqueak]:
|
||||||
""" Get a squeak. """
|
""" Get a squeak. """
|
||||||
s = select([self.squeaks]).where(
|
s = select([self.squeaks]).where(
|
||||||
self.squeaks.c.hash == squeak_hash)
|
self.squeaks.c.hash == squeak_hash)
|
||||||
|
|
@ -273,8 +310,17 @@ class SqueakDb:
|
||||||
select([
|
select([
|
||||||
self.squeaks,
|
self.squeaks,
|
||||||
self.author_profiles,
|
self.author_profiles,
|
||||||
|
self.resqueaked_author_profiles,
|
||||||
self.recipient_profiles,
|
self.recipient_profiles,
|
||||||
func.count(self.reply_squeaks.c.hash).label("num_replies"),
|
self.resqueaked_squeaks,
|
||||||
|
func.count(distinct(self.reply_squeaks.c.hash)
|
||||||
|
).label("num_replies"),
|
||||||
|
func.count(distinct(self.resqueaked_reply_squeaks.c.hash)).label(
|
||||||
|
"num_resqueak_replies"),
|
||||||
|
func.count(distinct(self.resqueaks.c.hash)
|
||||||
|
).label("num_resqueaks"),
|
||||||
|
func.count(distinct(self.resqueaked_resqueaks.c.hash)).label(
|
||||||
|
"num_resqueak_resqueaks"),
|
||||||
])
|
])
|
||||||
.select_from(
|
.select_from(
|
||||||
self.squeaks
|
self.squeaks
|
||||||
|
|
@ -290,11 +336,33 @@ class SqueakDb:
|
||||||
self.reply_squeaks,
|
self.reply_squeaks,
|
||||||
self.reply_squeaks.c.reply_hash == self.squeaks.c.hash,
|
self.reply_squeaks.c.reply_hash == self.squeaks.c.hash,
|
||||||
)
|
)
|
||||||
|
.outerjoin(
|
||||||
|
self.resqueaks,
|
||||||
|
self.resqueaks.c.resqueak_hash == self.squeaks.c.hash,
|
||||||
|
)
|
||||||
|
.outerjoin(
|
||||||
|
self.resqueaked_squeaks,
|
||||||
|
self.resqueaked_squeaks.c.hash == self.squeaks.c.resqueak_hash,
|
||||||
|
)
|
||||||
|
.outerjoin(
|
||||||
|
self.resqueaked_author_profiles,
|
||||||
|
self.resqueaked_author_profiles.c.public_key == self.resqueaked_squeaks.c.author_public_key,
|
||||||
|
)
|
||||||
|
.outerjoin(
|
||||||
|
self.resqueaked_reply_squeaks,
|
||||||
|
self.resqueaked_reply_squeaks.c.reply_hash == self.resqueaked_squeaks.c.hash,
|
||||||
|
)
|
||||||
|
.outerjoin(
|
||||||
|
self.resqueaked_resqueaks,
|
||||||
|
self.resqueaked_resqueaks.c.resqueak_hash == self.resqueaked_squeaks.c.hash,
|
||||||
|
)
|
||||||
)
|
)
|
||||||
.group_by(
|
.group_by(
|
||||||
self.squeaks,
|
self.squeaks,
|
||||||
self.author_profiles,
|
self.author_profiles,
|
||||||
|
self.resqueaked_author_profiles,
|
||||||
self.recipient_profiles,
|
self.recipient_profiles,
|
||||||
|
self.resqueaked_squeaks,
|
||||||
)
|
)
|
||||||
.where(self.squeaks.c.hash == squeak_hash)
|
.where(self.squeaks.c.hash == squeak_hash)
|
||||||
)
|
)
|
||||||
|
|
@ -334,8 +402,17 @@ class SqueakDb:
|
||||||
select([
|
select([
|
||||||
self.squeaks,
|
self.squeaks,
|
||||||
self.author_profiles,
|
self.author_profiles,
|
||||||
|
self.resqueaked_author_profiles,
|
||||||
self.recipient_profiles,
|
self.recipient_profiles,
|
||||||
func.count(self.reply_squeaks.c.hash).label("num_replies"),
|
self.resqueaked_squeaks,
|
||||||
|
func.count(distinct(self.reply_squeaks.c.hash)
|
||||||
|
).label("num_replies"),
|
||||||
|
func.count(distinct(self.resqueaked_reply_squeaks.c.hash)).label(
|
||||||
|
"num_resqueak_replies"),
|
||||||
|
func.count(distinct(self.resqueaks.c.hash)
|
||||||
|
).label("num_resqueaks"),
|
||||||
|
func.count(distinct(self.resqueaked_resqueaks.c.hash)).label(
|
||||||
|
"num_resqueak_resqueaks"),
|
||||||
])
|
])
|
||||||
.select_from(
|
.select_from(
|
||||||
# self.squeaks.outerjoin(
|
# self.squeaks.outerjoin(
|
||||||
|
|
@ -355,11 +432,33 @@ class SqueakDb:
|
||||||
self.reply_squeaks,
|
self.reply_squeaks,
|
||||||
self.reply_squeaks.c.reply_hash == self.squeaks.c.hash,
|
self.reply_squeaks.c.reply_hash == self.squeaks.c.hash,
|
||||||
)
|
)
|
||||||
|
.outerjoin(
|
||||||
|
self.resqueaks,
|
||||||
|
self.resqueaks.c.resqueak_hash == self.squeaks.c.hash,
|
||||||
|
)
|
||||||
|
.outerjoin(
|
||||||
|
self.resqueaked_squeaks,
|
||||||
|
self.resqueaked_squeaks.c.hash == self.squeaks.c.resqueak_hash,
|
||||||
|
)
|
||||||
|
.outerjoin(
|
||||||
|
self.resqueaked_author_profiles,
|
||||||
|
self.resqueaked_author_profiles.c.public_key == self.resqueaked_squeaks.c.author_public_key,
|
||||||
|
)
|
||||||
|
.outerjoin(
|
||||||
|
self.resqueaked_reply_squeaks,
|
||||||
|
self.resqueaked_reply_squeaks.c.reply_hash == self.resqueaked_squeaks.c.hash,
|
||||||
|
)
|
||||||
|
.outerjoin(
|
||||||
|
self.resqueaked_resqueaks,
|
||||||
|
self.resqueaked_resqueaks.c.resqueak_hash == self.resqueaked_squeaks.c.hash,
|
||||||
|
)
|
||||||
)
|
)
|
||||||
.group_by(
|
.group_by(
|
||||||
self.squeaks,
|
self.squeaks,
|
||||||
self.author_profiles,
|
self.author_profiles,
|
||||||
|
self.resqueaked_author_profiles,
|
||||||
self.recipient_profiles,
|
self.recipient_profiles,
|
||||||
|
self.resqueaked_squeaks,
|
||||||
)
|
)
|
||||||
.where(self.profile_is_following(self.author_profiles))
|
.where(self.profile_is_following(self.author_profiles))
|
||||||
.where(
|
.where(
|
||||||
|
|
@ -407,8 +506,17 @@ class SqueakDb:
|
||||||
select([
|
select([
|
||||||
self.squeaks,
|
self.squeaks,
|
||||||
self.author_profiles,
|
self.author_profiles,
|
||||||
|
self.resqueaked_author_profiles,
|
||||||
self.recipient_profiles,
|
self.recipient_profiles,
|
||||||
func.count(self.reply_squeaks.c.hash).label("num_replies"),
|
self.resqueaked_squeaks,
|
||||||
|
func.count(distinct(self.reply_squeaks.c.hash)
|
||||||
|
).label("num_replies"),
|
||||||
|
func.count(distinct(self.resqueaked_reply_squeaks.c.hash)).label(
|
||||||
|
"num_resqueak_replies"),
|
||||||
|
func.count(distinct(self.resqueaks.c.hash)
|
||||||
|
).label("num_resqueaks"),
|
||||||
|
func.count(distinct(self.resqueaked_resqueaks.c.hash)).label(
|
||||||
|
"num_resqueak_resqueaks"),
|
||||||
])
|
])
|
||||||
.select_from(
|
.select_from(
|
||||||
# self.squeaks.outerjoin(
|
# self.squeaks.outerjoin(
|
||||||
|
|
@ -428,11 +536,33 @@ class SqueakDb:
|
||||||
self.reply_squeaks,
|
self.reply_squeaks,
|
||||||
self.reply_squeaks.c.reply_hash == self.squeaks.c.hash,
|
self.reply_squeaks.c.reply_hash == self.squeaks.c.hash,
|
||||||
)
|
)
|
||||||
|
.outerjoin(
|
||||||
|
self.resqueaks,
|
||||||
|
self.resqueaks.c.resqueak_hash == self.squeaks.c.hash,
|
||||||
|
)
|
||||||
|
.outerjoin(
|
||||||
|
self.resqueaked_squeaks,
|
||||||
|
self.resqueaked_squeaks.c.hash == self.squeaks.c.resqueak_hash,
|
||||||
|
)
|
||||||
|
.outerjoin(
|
||||||
|
self.resqueaked_author_profiles,
|
||||||
|
self.resqueaked_author_profiles.c.public_key == self.resqueaked_squeaks.c.author_public_key,
|
||||||
|
)
|
||||||
|
.outerjoin(
|
||||||
|
self.resqueaked_reply_squeaks,
|
||||||
|
self.resqueaked_reply_squeaks.c.reply_hash == self.resqueaked_squeaks.c.hash,
|
||||||
|
)
|
||||||
|
.outerjoin(
|
||||||
|
self.resqueaked_resqueaks,
|
||||||
|
self.resqueaked_resqueaks.c.resqueak_hash == self.resqueaked_squeaks.c.hash,
|
||||||
|
)
|
||||||
)
|
)
|
||||||
.group_by(
|
.group_by(
|
||||||
self.squeaks,
|
self.squeaks,
|
||||||
self.author_profiles,
|
self.author_profiles,
|
||||||
|
self.resqueaked_author_profiles,
|
||||||
self.recipient_profiles,
|
self.recipient_profiles,
|
||||||
|
self.resqueaked_squeaks,
|
||||||
)
|
)
|
||||||
.where(
|
.where(
|
||||||
self.squeak_is_liked,
|
self.squeak_is_liked,
|
||||||
|
|
@ -485,8 +615,17 @@ class SqueakDb:
|
||||||
select([
|
select([
|
||||||
self.squeaks,
|
self.squeaks,
|
||||||
self.author_profiles,
|
self.author_profiles,
|
||||||
|
self.resqueaked_author_profiles,
|
||||||
self.recipient_profiles,
|
self.recipient_profiles,
|
||||||
func.count(self.reply_squeaks.c.hash).label("num_replies"),
|
self.resqueaked_squeaks,
|
||||||
|
func.count(distinct(self.reply_squeaks.c.hash)
|
||||||
|
).label("num_replies"),
|
||||||
|
func.count(distinct(self.resqueaked_reply_squeaks.c.hash)).label(
|
||||||
|
"num_resqueak_replies"),
|
||||||
|
func.count(distinct(self.resqueaks.c.hash)
|
||||||
|
).label("num_resqueaks"),
|
||||||
|
func.count(distinct(self.resqueaked_resqueaks.c.hash)).label(
|
||||||
|
"num_resqueak_resqueaks"),
|
||||||
])
|
])
|
||||||
.select_from(
|
.select_from(
|
||||||
# self.squeaks.outerjoin(
|
# self.squeaks.outerjoin(
|
||||||
|
|
@ -506,11 +645,33 @@ class SqueakDb:
|
||||||
self.reply_squeaks,
|
self.reply_squeaks,
|
||||||
self.reply_squeaks.c.reply_hash == self.squeaks.c.hash,
|
self.reply_squeaks.c.reply_hash == self.squeaks.c.hash,
|
||||||
)
|
)
|
||||||
|
.outerjoin(
|
||||||
|
self.resqueaks,
|
||||||
|
self.resqueaks.c.resqueak_hash == self.squeaks.c.hash,
|
||||||
|
)
|
||||||
|
.outerjoin(
|
||||||
|
self.resqueaked_squeaks,
|
||||||
|
self.resqueaked_squeaks.c.hash == self.squeaks.c.resqueak_hash,
|
||||||
|
)
|
||||||
|
.outerjoin(
|
||||||
|
self.resqueaked_author_profiles,
|
||||||
|
self.resqueaked_author_profiles.c.public_key == self.resqueaked_squeaks.c.author_public_key,
|
||||||
|
)
|
||||||
|
.outerjoin(
|
||||||
|
self.resqueaked_reply_squeaks,
|
||||||
|
self.resqueaked_reply_squeaks.c.reply_hash == self.resqueaked_squeaks.c.hash,
|
||||||
|
)
|
||||||
|
.outerjoin(
|
||||||
|
self.resqueaked_resqueaks,
|
||||||
|
self.resqueaked_resqueaks.c.resqueak_hash == self.resqueaked_squeaks.c.hash,
|
||||||
|
)
|
||||||
)
|
)
|
||||||
.group_by(
|
.group_by(
|
||||||
self.squeaks,
|
self.squeaks,
|
||||||
self.author_profiles,
|
self.author_profiles,
|
||||||
|
self.resqueaked_author_profiles,
|
||||||
self.recipient_profiles,
|
self.recipient_profiles,
|
||||||
|
self.resqueaked_squeaks,
|
||||||
)
|
)
|
||||||
.where(self.squeaks.c.author_public_key == public_key.to_bytes())
|
.where(self.squeaks.c.author_public_key == public_key.to_bytes())
|
||||||
.where(
|
.where(
|
||||||
|
|
@ -564,8 +725,17 @@ class SqueakDb:
|
||||||
select([
|
select([
|
||||||
self.squeaks,
|
self.squeaks,
|
||||||
self.author_profiles,
|
self.author_profiles,
|
||||||
|
self.resqueaked_author_profiles,
|
||||||
self.recipient_profiles,
|
self.recipient_profiles,
|
||||||
func.count(self.reply_squeaks.c.hash).label("num_replies"),
|
self.resqueaked_squeaks,
|
||||||
|
func.count(distinct(self.reply_squeaks.c.hash)
|
||||||
|
).label("num_replies"),
|
||||||
|
func.count(distinct(self.resqueaked_reply_squeaks.c.hash)).label(
|
||||||
|
"num_resqueak_replies"),
|
||||||
|
func.count(distinct(self.resqueaks.c.hash)
|
||||||
|
).label("num_resqueaks"),
|
||||||
|
func.count(distinct(self.resqueaked_resqueaks.c.hash)).label(
|
||||||
|
"num_resqueak_resqueaks"),
|
||||||
])
|
])
|
||||||
.select_from(
|
.select_from(
|
||||||
# self.squeaks.outerjoin(
|
# self.squeaks.outerjoin(
|
||||||
|
|
@ -585,11 +755,33 @@ class SqueakDb:
|
||||||
self.reply_squeaks,
|
self.reply_squeaks,
|
||||||
self.reply_squeaks.c.reply_hash == self.squeaks.c.hash,
|
self.reply_squeaks.c.reply_hash == self.squeaks.c.hash,
|
||||||
)
|
)
|
||||||
|
.outerjoin(
|
||||||
|
self.resqueaks,
|
||||||
|
self.resqueaks.c.resqueak_hash == self.squeaks.c.hash,
|
||||||
|
)
|
||||||
|
.outerjoin(
|
||||||
|
self.resqueaked_squeaks,
|
||||||
|
self.resqueaked_squeaks.c.hash == self.squeaks.c.resqueak_hash,
|
||||||
|
)
|
||||||
|
.outerjoin(
|
||||||
|
self.resqueaked_author_profiles,
|
||||||
|
self.resqueaked_author_profiles.c.public_key == self.resqueaked_squeaks.c.author_public_key,
|
||||||
|
)
|
||||||
|
.outerjoin(
|
||||||
|
self.resqueaked_reply_squeaks,
|
||||||
|
self.resqueaked_reply_squeaks.c.reply_hash == self.resqueaked_squeaks.c.hash,
|
||||||
|
)
|
||||||
|
.outerjoin(
|
||||||
|
self.resqueaked_resqueaks,
|
||||||
|
self.resqueaked_resqueaks.c.resqueak_hash == self.resqueaked_squeaks.c.hash,
|
||||||
|
)
|
||||||
)
|
)
|
||||||
.group_by(
|
.group_by(
|
||||||
self.squeaks,
|
self.squeaks,
|
||||||
self.author_profiles,
|
self.author_profiles,
|
||||||
|
self.resqueaked_author_profiles,
|
||||||
self.recipient_profiles,
|
self.recipient_profiles,
|
||||||
|
self.resqueaked_squeaks,
|
||||||
)
|
)
|
||||||
.where(self.squeaks.c.content.ilike(f'%{search_text}%'))
|
.where(self.squeaks.c.content.ilike(f'%{search_text}%'))
|
||||||
.where(
|
.where(
|
||||||
|
|
@ -645,8 +837,17 @@ class SqueakDb:
|
||||||
select([
|
select([
|
||||||
self.squeaks,
|
self.squeaks,
|
||||||
self.author_profiles,
|
self.author_profiles,
|
||||||
|
self.resqueaked_author_profiles,
|
||||||
self.recipient_profiles,
|
self.recipient_profiles,
|
||||||
func.count(self.reply_squeaks.c.hash).label("num_replies"),
|
self.resqueaked_squeaks,
|
||||||
|
func.count(distinct(self.reply_squeaks.c.hash)
|
||||||
|
).label("num_replies"),
|
||||||
|
func.count(distinct(self.resqueaked_reply_squeaks.c.hash)).label(
|
||||||
|
"num_resqueak_replies"),
|
||||||
|
func.count(distinct(self.resqueaks.c.hash)
|
||||||
|
).label("num_resqueaks"),
|
||||||
|
func.count(distinct(self.resqueaked_resqueaks.c.hash)).label(
|
||||||
|
"num_resqueak_resqueaks"),
|
||||||
])
|
])
|
||||||
.select_from(
|
.select_from(
|
||||||
self.squeaks.join(
|
self.squeaks.join(
|
||||||
|
|
@ -668,11 +869,33 @@ class SqueakDb:
|
||||||
self.reply_squeaks,
|
self.reply_squeaks,
|
||||||
self.reply_squeaks.c.reply_hash == self.squeaks.c.hash,
|
self.reply_squeaks.c.reply_hash == self.squeaks.c.hash,
|
||||||
)
|
)
|
||||||
|
.outerjoin(
|
||||||
|
self.resqueaks,
|
||||||
|
self.resqueaks.c.resqueak_hash == self.squeaks.c.hash,
|
||||||
|
)
|
||||||
|
.outerjoin(
|
||||||
|
self.resqueaked_squeaks,
|
||||||
|
self.resqueaked_squeaks.c.hash == self.squeaks.c.resqueak_hash,
|
||||||
|
)
|
||||||
|
.outerjoin(
|
||||||
|
self.resqueaked_author_profiles,
|
||||||
|
self.resqueaked_author_profiles.c.public_key == self.resqueaked_squeaks.c.author_public_key,
|
||||||
|
)
|
||||||
|
.outerjoin(
|
||||||
|
self.resqueaked_reply_squeaks,
|
||||||
|
self.resqueaked_reply_squeaks.c.reply_hash == self.resqueaked_squeaks.c.hash,
|
||||||
|
)
|
||||||
|
.outerjoin(
|
||||||
|
self.resqueaked_resqueaks,
|
||||||
|
self.resqueaked_resqueaks.c.resqueak_hash == self.resqueaked_squeaks.c.hash,
|
||||||
|
)
|
||||||
)
|
)
|
||||||
.group_by(
|
.group_by(
|
||||||
self.squeaks,
|
self.squeaks,
|
||||||
self.author_profiles,
|
self.author_profiles,
|
||||||
|
self.resqueaked_author_profiles,
|
||||||
self.recipient_profiles,
|
self.recipient_profiles,
|
||||||
|
self.resqueaked_squeaks,
|
||||||
ancestors.c.depth,
|
ancestors.c.depth,
|
||||||
)
|
)
|
||||||
.order_by(
|
.order_by(
|
||||||
|
|
@ -731,8 +954,17 @@ class SqueakDb:
|
||||||
select([
|
select([
|
||||||
self.squeaks,
|
self.squeaks,
|
||||||
self.author_profiles,
|
self.author_profiles,
|
||||||
|
self.resqueaked_author_profiles,
|
||||||
self.recipient_profiles,
|
self.recipient_profiles,
|
||||||
func.count(self.reply_squeaks.c.hash).label("num_replies"),
|
self.resqueaked_squeaks,
|
||||||
|
func.count(distinct(self.reply_squeaks.c.hash)
|
||||||
|
).label("num_replies"),
|
||||||
|
func.count(distinct(self.resqueaked_reply_squeaks.c.hash)).label(
|
||||||
|
"num_resqueak_replies"),
|
||||||
|
func.count(distinct(self.resqueaks.c.hash)
|
||||||
|
).label("num_resqueaks"),
|
||||||
|
func.count(distinct(self.resqueaked_resqueaks.c.hash)).label(
|
||||||
|
"num_resqueak_resqueaks"),
|
||||||
])
|
])
|
||||||
.select_from(
|
.select_from(
|
||||||
# self.squeaks.outerjoin(
|
# self.squeaks.outerjoin(
|
||||||
|
|
@ -752,11 +984,33 @@ class SqueakDb:
|
||||||
self.reply_squeaks,
|
self.reply_squeaks,
|
||||||
self.reply_squeaks.c.reply_hash == self.squeaks.c.hash,
|
self.reply_squeaks.c.reply_hash == self.squeaks.c.hash,
|
||||||
)
|
)
|
||||||
|
.outerjoin(
|
||||||
|
self.resqueaks,
|
||||||
|
self.resqueaks.c.resqueak_hash == self.squeaks.c.hash,
|
||||||
|
)
|
||||||
|
.outerjoin(
|
||||||
|
self.resqueaked_squeaks,
|
||||||
|
self.resqueaked_squeaks.c.hash == self.squeaks.c.resqueak_hash,
|
||||||
|
)
|
||||||
|
.outerjoin(
|
||||||
|
self.resqueaked_author_profiles,
|
||||||
|
self.resqueaked_author_profiles.c.public_key == self.resqueaked_squeaks.c.author_public_key,
|
||||||
|
)
|
||||||
|
.outerjoin(
|
||||||
|
self.resqueaked_reply_squeaks,
|
||||||
|
self.resqueaked_reply_squeaks.c.reply_hash == self.resqueaked_squeaks.c.hash,
|
||||||
|
)
|
||||||
|
.outerjoin(
|
||||||
|
self.resqueaked_resqueaks,
|
||||||
|
self.resqueaked_resqueaks.c.resqueak_hash == self.resqueaked_squeaks.c.hash,
|
||||||
|
)
|
||||||
)
|
)
|
||||||
.group_by(
|
.group_by(
|
||||||
self.squeaks,
|
self.squeaks,
|
||||||
self.author_profiles,
|
self.author_profiles,
|
||||||
|
self.resqueaked_author_profiles,
|
||||||
self.recipient_profiles,
|
self.recipient_profiles,
|
||||||
|
self.resqueaked_squeaks,
|
||||||
)
|
)
|
||||||
.where(self.squeaks.c.reply_hash == squeak_hash)
|
.where(self.squeaks.c.reply_hash == squeak_hash)
|
||||||
.where(
|
.where(
|
||||||
|
|
@ -2044,8 +2298,11 @@ class SqueakDb:
|
||||||
with self.get_connection() as connection:
|
with self.get_connection() as connection:
|
||||||
connection.execute(delete_twitter_account_stmt)
|
connection.execute(delete_twitter_account_stmt)
|
||||||
|
|
||||||
def _parse_squeak(self, row) -> CSqueak:
|
def _parse_squeak(self, row) -> CBaseSqueak:
|
||||||
return CSqueak.deserialize(row["squeak"])
|
if row["resqueak_hash"]:
|
||||||
|
return CResqueak.deserialize(row["squeak"])
|
||||||
|
else:
|
||||||
|
return CSqueak.deserialize(row["squeak"])
|
||||||
|
|
||||||
# def _parse_squeak_entry(self, row, author_profiles_table=None, recipient_profiles_table=None) -> SqueakEntry:
|
# def _parse_squeak_entry(self, row, author_profiles_table=None, recipient_profiles_table=None) -> SqueakEntry:
|
||||||
def _parse_squeak_entry(self, row) -> SqueakEntry:
|
def _parse_squeak_entry(self, row) -> SqueakEntry:
|
||||||
|
|
@ -2060,6 +2317,8 @@ class SqueakDb:
|
||||||
row, profiles_table=self.author_profiles)
|
row, profiles_table=self.author_profiles)
|
||||||
recipient_profile = self._try_parse_squeak_profile(
|
recipient_profile = self._try_parse_squeak_profile(
|
||||||
row, profiles_table=self.recipient_profiles)
|
row, profiles_table=self.recipient_profiles)
|
||||||
|
resqueaked_squeak = self._parse_resqueaked_squeak_entry(
|
||||||
|
row) if row[self.resqueaked_squeaks.c.author_public_key] else None
|
||||||
return SqueakEntry(
|
return SqueakEntry(
|
||||||
squeak_hash=(row["hash"]),
|
squeak_hash=(row["hash"]),
|
||||||
serialized_squeak=(row["squeak"]),
|
serialized_squeak=(row["squeak"]),
|
||||||
|
|
@ -2075,7 +2334,45 @@ class SqueakDb:
|
||||||
secret_key=(row["secret_key"]),
|
secret_key=(row["secret_key"]),
|
||||||
liked_time_ms=liked_time_ms,
|
liked_time_ms=liked_time_ms,
|
||||||
num_replies=row["num_replies"],
|
num_replies=row["num_replies"],
|
||||||
|
num_resqueaks=row["num_resqueaks"],
|
||||||
content=row["content"],
|
content=row["content"],
|
||||||
|
resqueaked_hash=row["resqueak_hash"],
|
||||||
|
resqueaked_squeak=resqueaked_squeak,
|
||||||
|
squeak_profile=profile,
|
||||||
|
recipient_squeak_profile=recipient_profile,
|
||||||
|
)
|
||||||
|
|
||||||
|
def _parse_resqueaked_squeak_entry(self, row) -> SqueakEntry:
|
||||||
|
public_key_bytes = row[self.resqueaked_squeaks.c.author_public_key]
|
||||||
|
recipient_public_key_bytes = row[self.resqueaked_squeaks.c.recipient_public_key]
|
||||||
|
secret_key_column = row[self.resqueaked_squeaks.c.secret_key]
|
||||||
|
is_locked = bool(secret_key_column)
|
||||||
|
reply_to = (
|
||||||
|
row[self.resqueaked_squeaks.c.reply_hash]) if row[self.resqueaked_squeaks.c.reply_hash] else None
|
||||||
|
liked_time_ms = row[self.resqueaked_squeaks.c.liked_time_ms]
|
||||||
|
profile = self._try_parse_squeak_profile(
|
||||||
|
row, profiles_table=self.resqueaked_author_profiles)
|
||||||
|
recipient_profile = self._try_parse_squeak_profile(
|
||||||
|
row, profiles_table=self.recipient_profiles)
|
||||||
|
return SqueakEntry(
|
||||||
|
squeak_hash=(row[self.resqueaked_squeaks.c.hash]),
|
||||||
|
serialized_squeak=(row[self.resqueaked_squeaks.c.squeak]),
|
||||||
|
public_key=SqueakPublicKey.from_bytes(public_key_bytes),
|
||||||
|
recipient_public_key=SqueakPublicKey.from_bytes(
|
||||||
|
recipient_public_key_bytes) if recipient_public_key_bytes else None,
|
||||||
|
block_height=row[self.resqueaked_squeaks.c.block_height],
|
||||||
|
block_hash=(row[self.resqueaked_squeaks.c.block_hash]),
|
||||||
|
block_time=row[self.resqueaked_squeaks.c.block_time_s],
|
||||||
|
squeak_time=row[self.resqueaked_squeaks.c.time_s],
|
||||||
|
reply_to=reply_to,
|
||||||
|
is_unlocked=is_locked,
|
||||||
|
secret_key=(row[self.resqueaked_squeaks.c.secret_key]),
|
||||||
|
liked_time_ms=liked_time_ms,
|
||||||
|
# TODO: needs another join. row[self.resqueaked_squeaks.c.num_replies],
|
||||||
|
num_replies=row["num_resqueak_replies"],
|
||||||
|
num_resqueaks=row["num_resqueak_resqueaks"],
|
||||||
|
content=row[self.resqueaked_squeaks.c.content],
|
||||||
|
resqueaked_hash=row[self.resqueaked_squeaks.c.resqueak_hash],
|
||||||
squeak_profile=profile,
|
squeak_profile=profile,
|
||||||
recipient_squeak_profile=recipient_profile,
|
recipient_squeak_profile=recipient_profile,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -84,6 +84,18 @@ class SqueakController:
|
||||||
recipient_profile_id,
|
recipient_profile_id,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def make_resqueak(
|
||||||
|
self,
|
||||||
|
profile_id: int,
|
||||||
|
resqueaked_hash: bytes,
|
||||||
|
replyto_hash: Optional[bytes],
|
||||||
|
) -> Optional[bytes]:
|
||||||
|
return self.squeak_store.make_resqueak(
|
||||||
|
profile_id,
|
||||||
|
resqueaked_hash,
|
||||||
|
replyto_hash,
|
||||||
|
)
|
||||||
|
|
||||||
def pay_offer(self, received_offer_id: int) -> int:
|
def pay_offer(self, received_offer_id: int) -> int:
|
||||||
return self.squeak_store.pay_offer(received_offer_id)
|
return self.squeak_store.pay_offer(received_offer_id)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -25,8 +25,10 @@ from typing import Iterator
|
||||||
from typing import List
|
from typing import List
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
|
from squeak.core import CBaseSqueak
|
||||||
from squeak.core import CheckSqueak
|
from squeak.core import CheckSqueak
|
||||||
from squeak.core import CheckSqueakSecretKey
|
from squeak.core import CheckSqueakSecretKey
|
||||||
|
from squeak.core import CResqueak
|
||||||
from squeak.core import CSqueak
|
from squeak.core import CSqueak
|
||||||
from squeak.core.keys import SqueakPrivateKey
|
from squeak.core.keys import SqueakPrivateKey
|
||||||
from squeak.core.keys import SqueakPublicKey
|
from squeak.core.keys import SqueakPublicKey
|
||||||
|
|
@ -119,32 +121,59 @@ class SqueakStore:
|
||||||
)
|
)
|
||||||
return inserted_squeak_hash
|
return inserted_squeak_hash
|
||||||
|
|
||||||
def save_squeak(self, squeak: CSqueak) -> Optional[bytes]:
|
def make_resqueak(
|
||||||
|
self,
|
||||||
|
profile_id: int,
|
||||||
|
resqueaked_hash: bytes,
|
||||||
|
replyto_hash: Optional[bytes],
|
||||||
|
) -> Optional[bytes]:
|
||||||
|
squeak_profile = self.get_squeak_profile(profile_id)
|
||||||
|
if squeak_profile is None:
|
||||||
|
raise Exception("Profile with id {} not found.".format(
|
||||||
|
profile_id,
|
||||||
|
))
|
||||||
|
resqueak = self.squeak_core.make_resqueak(
|
||||||
|
squeak_profile,
|
||||||
|
resqueaked_hash,
|
||||||
|
replyto_hash,
|
||||||
|
)
|
||||||
|
inserted_resqueak_hash = self.save_squeak(resqueak)
|
||||||
|
if inserted_resqueak_hash is None:
|
||||||
|
raise Exception("Failed to save resqueak.")
|
||||||
|
return inserted_resqueak_hash
|
||||||
|
|
||||||
|
def save_squeak(self, base_squeak: CBaseSqueak) -> Optional[bytes]:
|
||||||
# Check if the squeak is valid context free.
|
# Check if the squeak is valid context free.
|
||||||
CheckSqueak(squeak)
|
CheckSqueak(base_squeak)
|
||||||
# Get the block header.
|
# Get the block header.
|
||||||
block_header = self.squeak_core.get_block_header(squeak)
|
block_header = self.squeak_core.get_block_header(base_squeak)
|
||||||
# Check if limit exceeded.
|
# Check if limit exceeded.
|
||||||
if self.squeak_db.get_number_of_squeaks() >= self.max_squeaks:
|
if self.squeak_db.get_number_of_squeaks() >= self.max_squeaks:
|
||||||
raise Exception("Exceeded max number of squeaks.")
|
raise Exception("Exceeded max number of squeaks.")
|
||||||
# TODO: Check if limit per public key per block is exceeded.
|
# TODO: Check if limit per public key per block is exceeded.
|
||||||
if self.squeak_db.number_of_squeaks_with_public_key_with_block_height(
|
if self.squeak_db.number_of_squeaks_with_public_key_with_block_height(
|
||||||
squeak.GetPubKey(),
|
base_squeak.GetPubKey(),
|
||||||
squeak.nBlockHeight,
|
base_squeak.nBlockHeight,
|
||||||
) >= self.max_squeaks_per_public_key_per_block:
|
) >= self.max_squeaks_per_public_key_per_block:
|
||||||
raise Exception(
|
raise Exception(
|
||||||
"Exceeded max number of squeaks per public key per block.")
|
"Exceeded max number of squeaks per public key per block.")
|
||||||
# Insert the squeak in db.
|
# Insert the squeak in db.
|
||||||
inserted_squeak_hash = self.squeak_db.insert_squeak(
|
if isinstance(base_squeak, CSqueak):
|
||||||
squeak,
|
inserted_squeak_hash = self.squeak_db.insert_squeak(
|
||||||
block_header,
|
base_squeak,
|
||||||
)
|
block_header,
|
||||||
|
)
|
||||||
|
elif isinstance(base_squeak, CResqueak):
|
||||||
|
inserted_squeak_hash = self.squeak_db.insert_resqueak(
|
||||||
|
base_squeak,
|
||||||
|
block_header,
|
||||||
|
)
|
||||||
if inserted_squeak_hash is None:
|
if inserted_squeak_hash is None:
|
||||||
return None
|
return None
|
||||||
logger.info("Saved squeak: {}".format(
|
logger.info("Saved squeak: {}".format(
|
||||||
inserted_squeak_hash.hex(),
|
inserted_squeak_hash.hex(),
|
||||||
))
|
))
|
||||||
self.new_squeak_listener.handle_new_item(squeak)
|
self.new_squeak_listener.handle_new_item(base_squeak)
|
||||||
return inserted_squeak_hash
|
return inserted_squeak_hash
|
||||||
|
|
||||||
def save_secret_key(self, squeak_hash: bytes, secret_key: bytes):
|
def save_secret_key(self, squeak_hash: bytes, secret_key: bytes):
|
||||||
|
|
@ -230,7 +259,11 @@ class SqueakStore:
|
||||||
return sent_payment_id
|
return sent_payment_id
|
||||||
|
|
||||||
def get_squeak(self, squeak_hash: bytes) -> Optional[CSqueak]:
|
def get_squeak(self, squeak_hash: bytes) -> Optional[CSqueak]:
|
||||||
return self.squeak_db.get_squeak(squeak_hash)
|
# TODO: remove this after squeak protocol struct stabilizes.
|
||||||
|
try:
|
||||||
|
return self.squeak_db.get_squeak(squeak_hash)
|
||||||
|
except Exception:
|
||||||
|
return None
|
||||||
|
|
||||||
def get_squeak_secret_key(self, squeak_hash: bytes) -> Optional[bytes]:
|
def get_squeak_secret_key(self, squeak_hash: bytes) -> Optional[bytes]:
|
||||||
return self.squeak_db.get_squeak_secret_key(squeak_hash)
|
return self.squeak_db.get_squeak_secret_key(squeak_hash)
|
||||||
|
|
|
||||||
|
|
@ -59,10 +59,13 @@ def create_app(handler):
|
||||||
@app.route('/squeak/<hash>')
|
@app.route('/squeak/<hash>')
|
||||||
def squeak(hash):
|
def squeak(hash):
|
||||||
try:
|
try:
|
||||||
squeak_bytes = handler.handle_get_squeak_bytes(hash)
|
squeak_type, squeak_bytes = handler.handle_get_squeak_bytes(hash)
|
||||||
except NotFoundError:
|
except NotFoundError:
|
||||||
return "Not found", 404
|
return "Not found", 404
|
||||||
return squeak_bytes
|
return jsonify({
|
||||||
|
'squeak_type': squeak_type,
|
||||||
|
'squeak_bytes': squeak_bytes.hex(),
|
||||||
|
})
|
||||||
|
|
||||||
@app.route('/secretkey/<hash>')
|
@app.route('/secretkey/<hash>')
|
||||||
def secret_key(hash):
|
def secret_key(hash):
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,7 @@
|
||||||
import logging
|
import logging
|
||||||
from typing import List
|
from typing import List
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
from typing import Tuple
|
||||||
|
|
||||||
from squeak.core.keys import SqueakPublicKey
|
from squeak.core.keys import SqueakPublicKey
|
||||||
|
|
||||||
|
|
@ -54,12 +55,17 @@ class SqueakPeerServerHandler(object):
|
||||||
self.node_settings = node_settings
|
self.node_settings = node_settings
|
||||||
self.config = config
|
self.config = config
|
||||||
|
|
||||||
def handle_get_squeak_bytes(self, squeak_hash_str) -> bytes:
|
def handle_get_squeak_bytes(self, squeak_hash_str) -> Tuple[str, bytes]:
|
||||||
|
"""Return a tuple with the squeak type and the squeak bytes.
|
||||||
|
|
||||||
|
Returns: (str, bytes)
|
||||||
|
"""
|
||||||
squeak_hash = bytes.fromhex(squeak_hash_str)
|
squeak_hash = bytes.fromhex(squeak_hash_str)
|
||||||
squeak = self.squeak_controller.get_squeak(squeak_hash)
|
squeak = self.squeak_controller.get_squeak(squeak_hash)
|
||||||
if not squeak:
|
if not squeak:
|
||||||
raise NotFoundError()
|
raise NotFoundError()
|
||||||
return squeak.serialize()
|
squeak_type = 'resqueak' if squeak.is_resqueak else 'squeak'
|
||||||
|
return squeak_type, squeak.serialize()
|
||||||
|
|
||||||
def handle_get_secret_key(self, squeak_hash_str) -> bytes:
|
def handle_get_secret_key(self, squeak_hash_str) -> bytes:
|
||||||
squeak_hash = bytes.fromhex(squeak_hash_str)
|
squeak_hash = bytes.fromhex(squeak_hash_str)
|
||||||
|
|
|
||||||
|
|
@ -42,6 +42,7 @@ from squeaknode.core.sent_payment_summary import SentPaymentSummary
|
||||||
from squeaknode.core.squeak_entry import SqueakEntry
|
from squeaknode.core.squeak_entry import SqueakEntry
|
||||||
from squeaknode.core.squeak_peer import SqueakPeer
|
from squeaknode.core.squeak_peer import SqueakPeer
|
||||||
from squeaknode.core.squeaks import get_hash
|
from squeaknode.core.squeaks import get_hash
|
||||||
|
from squeaknode.core.squeaks import make_resqueak_with_block
|
||||||
from squeaknode.core.squeaks import make_squeak_with_block
|
from squeaknode.core.squeaks import make_squeak_with_block
|
||||||
from squeaknode.core.user_config import UserConfig
|
from squeaknode.core.user_config import UserConfig
|
||||||
from tests.utils import gen_contact_profile
|
from tests.utils import gen_contact_profile
|
||||||
|
|
@ -216,6 +217,21 @@ def private_squeak(private_squeak_and_secret_key):
|
||||||
yield squeak
|
yield squeak
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def resqueak(private_key, squeak_hash, block_info):
|
||||||
|
yield make_resqueak_with_block(
|
||||||
|
private_key,
|
||||||
|
squeak_hash,
|
||||||
|
block_info.block_height,
|
||||||
|
block_info.block_hash,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def resqueak_hash(resqueak):
|
||||||
|
yield get_hash(resqueak)
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def peer_address():
|
def peer_address():
|
||||||
yield PeerAddress(
|
yield PeerAddress(
|
||||||
|
|
@ -308,6 +324,7 @@ def squeak_entry_locked(
|
||||||
recipient_squeak_profile=recipient_contact_profile,
|
recipient_squeak_profile=recipient_contact_profile,
|
||||||
liked_time_ms=None,
|
liked_time_ms=None,
|
||||||
num_replies=0,
|
num_replies=0,
|
||||||
|
num_resqueaks=0,
|
||||||
content=None,
|
content=None,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -344,6 +344,21 @@ def test_make_private_squeak(
|
||||||
assert author_decrypted_content == squeak_content
|
assert author_decrypted_content == squeak_content
|
||||||
|
|
||||||
|
|
||||||
|
def test_make_resqueak(
|
||||||
|
squeak_core,
|
||||||
|
signing_profile,
|
||||||
|
squeak_hash,
|
||||||
|
block_header
|
||||||
|
):
|
||||||
|
created_resqueak = squeak_core.make_resqueak(
|
||||||
|
signing_profile,
|
||||||
|
squeak_hash,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert created_resqueak.hashResqueakSqk == squeak_hash
|
||||||
|
squeak_core.check_squeak(created_resqueak)
|
||||||
|
|
||||||
|
|
||||||
def test_get_block_header(
|
def test_get_block_header(
|
||||||
squeak_core,
|
squeak_core,
|
||||||
squeak,
|
squeak,
|
||||||
|
|
|
||||||
|
|
@ -61,6 +61,11 @@ def inserted_reply_squeak_hash(squeak_db, reply_squeak, block_header):
|
||||||
yield squeak_db.insert_squeak(reply_squeak, block_header)
|
yield squeak_db.insert_squeak(reply_squeak, block_header)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def inserted_resqueak_hash(squeak_db, resqueak, block_header):
|
||||||
|
yield squeak_db.insert_resqueak(resqueak, block_header)
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def unlocked_squeak_hash(squeak_db, squeak, inserted_squeak_hash, secret_key, squeak_content):
|
def unlocked_squeak_hash(squeak_db, squeak, inserted_squeak_hash, secret_key, squeak_content):
|
||||||
squeak_db.set_squeak_secret_key(
|
squeak_db.set_squeak_secret_key(
|
||||||
|
|
@ -488,6 +493,12 @@ def test_get_missing_squeak(squeak_db, squeak, squeak_hash):
|
||||||
assert retrieved_squeak is None
|
assert retrieved_squeak is None
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_resqueak(squeak_db, resqueak, inserted_resqueak_hash):
|
||||||
|
retrieved_resqueak = squeak_db.get_squeak(inserted_resqueak_hash)
|
||||||
|
|
||||||
|
assert retrieved_resqueak == resqueak
|
||||||
|
|
||||||
|
|
||||||
def test_get_squeak_entry(
|
def test_get_squeak_entry(
|
||||||
squeak_db,
|
squeak_db,
|
||||||
squeak,
|
squeak,
|
||||||
|
|
@ -584,6 +595,34 @@ def test_get_secret_key_missing_squeak(squeak_db, squeak, squeak_hash):
|
||||||
assert retrieved_secret_key is None
|
assert retrieved_secret_key is None
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_resqueak_entry(
|
||||||
|
squeak_db,
|
||||||
|
resqueak,
|
||||||
|
squeak,
|
||||||
|
block_header,
|
||||||
|
public_key,
|
||||||
|
signing_profile,
|
||||||
|
inserted_resqueak_hash,
|
||||||
|
inserted_squeak_hash,
|
||||||
|
inserted_signing_profile_id,
|
||||||
|
):
|
||||||
|
retrieved_resqueak_entry = squeak_db.get_squeak_entry(
|
||||||
|
inserted_resqueak_hash)
|
||||||
|
retrieved_squeak_entry = squeak_db.get_squeak_entry(inserted_squeak_hash)
|
||||||
|
|
||||||
|
assert retrieved_resqueak_entry.squeak_hash == inserted_resqueak_hash
|
||||||
|
assert retrieved_resqueak_entry.public_key == public_key
|
||||||
|
assert retrieved_resqueak_entry.content is None
|
||||||
|
assert retrieved_resqueak_entry.block_time == block_header.nTime
|
||||||
|
assert retrieved_resqueak_entry.squeak_profile._replace(
|
||||||
|
profile_id=None) == signing_profile
|
||||||
|
assert retrieved_resqueak_entry.resqueaked_hash == inserted_squeak_hash
|
||||||
|
assert retrieved_resqueak_entry.resqueaked_squeak == retrieved_squeak_entry
|
||||||
|
assert retrieved_resqueak_entry.resqueaked_squeak.public_key == public_key
|
||||||
|
assert retrieved_resqueak_entry.num_resqueaks == 0
|
||||||
|
assert retrieved_resqueak_entry.resqueaked_squeak.num_resqueaks == 1
|
||||||
|
|
||||||
|
|
||||||
def test_get_timeline_squeak_entries(squeak_db, followed_squeak_hashes):
|
def test_get_timeline_squeak_entries(squeak_db, followed_squeak_hashes):
|
||||||
timeline_squeak_entries = squeak_db.get_timeline_squeak_entries(
|
timeline_squeak_entries = squeak_db.get_timeline_squeak_entries(
|
||||||
limit=2,
|
limit=2,
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue