diff --git a/packages/app/package.json b/packages/app/package.json index 3df0cd70..44916639 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -39,6 +39,7 @@ "react-dom": "^17.0.2", "react-helmet": "^6.1.0", "react-hook-form": "^7.29.0", + "react-p5": "^1.3.33", "react-query": "^3.34.16", "react-router-dom": "^6.2.2", "react-scripts": "4.0.3", diff --git a/packages/app/src/components/commons/Avatar.tsx b/packages/app/src/components/commons/Avatar.tsx new file mode 100644 index 00000000..4bb8a556 --- /dev/null +++ b/packages/app/src/components/commons/Avatar.tsx @@ -0,0 +1,41 @@ +import { Avatar as MaterialAvatar } from "@mui/material" +import React, { Fragment, useState } from "react" +import { useDynamicFavIcon } from "../../hooks/useDynamicFavIco" +import { usePublicationContext } from "../../services/publications/contexts" +import usePublication from "../../services/publications/hooks/usePublication" +import DeterministicAvatar from "./DeterministicAvatar" + +interface AvatarProps { + publicationSlug: string + height: number + width: number + storeImage?: boolean + dynamicFavIcon?: boolean +} + +const Avatar: React.FC = ({ publicationSlug, height, width, storeImage, dynamicFavIcon }) => { + const { imageSrc, data: publication } = usePublication(publicationSlug) + const { setPublicationAvatar } = usePublicationContext() + const [avatar, setAvatar] = useState(imageSrc) + + useDynamicFavIcon(dynamicFavIcon ? (imageSrc ? imageSrc : avatar) : undefined) + const handleImage = (uri: string) => { + if (storeImage && publication && publication.id) { + setAvatar(uri) + setPublicationAvatar({ publicationId: publication.id, uri }) + } + } + return ( + + {imageSrc ? ( + + {" "} + + ) : publication ? ( + + ) : null} + + ) +} + +export default Avatar diff --git a/packages/app/src/components/commons/DeterministicAvatar/index.tsx b/packages/app/src/components/commons/DeterministicAvatar/index.tsx new file mode 100644 index 00000000..6c53ca04 --- /dev/null +++ b/packages/app/src/components/commons/DeterministicAvatar/index.tsx @@ -0,0 +1,253 @@ +import React from "react" +import Sketch from "react-p5" +import p5Types from "p5" +import Random, { genTokenData } from "./utils" +import { Box } from "@mui/material" +interface DeterministicAvatarProps { + hash?: string + width?: number + height?: number + onImageGenerated?: (image: string) => void +} + +const DeterministicAvatar: React.FC = ({ + hash, + width = 160, + height = 160, + onImageGenerated, +}) => { + let x: number = 10 + let y: number = 10 + let markingsLayers: number = 0 + let noiseOpacity: number = 1 + let noiseImg: any + let layers: any[] = [] + let palette: any[] = [] + const divisions = 20 + let tokenData + if (hash) { + const publicationHash = hash?.replace("P-", "").replace(/(\d)-(\d*)/, "$1") + const publicationtokenId = hash?.match(/(\d)-(\d*)/) + tokenData = { hash: publicationHash, tokenId: publicationtokenId && publicationtokenId[2] } + } else { + tokenData = genTokenData(123) + } + const R: any = new Random(tokenData) + + for (let i = 0; i < divisions; i++) { + palette.push([0, 0, 100]) // white + const newFill = [R.random_num(0, 140), R.random_num(30, 65), R.random_num(70, 90)] + palette.push(newFill) // color + } + + const accentHue = R.random_num(0, 360) + const bgColor = [accentHue, R.random_num(10, 30), R.random_num(90, 100)] + const strokeColor = [accentHue, R.random_num(20, 40), R.random_num(30, 40)] + const outerStrokeColor = [accentHue, R.random_num(50, 80), R.random_num(50, 60)] + + const strokeWeight = 0.012 * width + + function Layer(this: any, size: number, pieces: number, showCircles: boolean, theta: number, fill: any) { + this.size = size + this.pieces = pieces + this.showCircles = showCircles + this.theta = theta + this.fill = fill + } + + function Piece( + this: any, + theta: number, + radius: number, + rotation: number, + fill: any, + strokeWeight: number, + strokeColor: any, + bool_seeds: number[], + p5: p5Types, + innerRadius?: number, + outerRadius?: number, + ) { + this.theta = theta + this.initialRadius = radius + this.radius = radius + this.innerRadius = innerRadius + this.outerRadius = radius + this.rotation = rotation + this.initialRotation = rotation + this.fill = fill + this.strokeWeight = strokeWeight + this.strokeColor = strokeColor + this.bool_seeds = bool_seeds + + this.render = function () { + p5.stroke(this.strokeColor) + p5.push() + p5.translate(width / 2, height / 2) + let x1, x2, y1, y2 + p5.rotate(this.rotation) + if (innerRadius) { + const t = this.theta + x1 = this.innerRadius * p5.cos(t - this.theta / 2 + 45 / 2) + y1 = this.innerRadius * p5.sin(t - this.theta / 2 + 45 / 2) + x2 = this.outerRadius * p5.cos(t + 45 / 2) + y2 = this.outerRadius * p5.sin(t + 45 / 2) + let x3 = this.innerRadius * p5.cos(t + this.theta / 2 + 45 / 2) + let y3 = this.innerRadius * p5.sin(t + this.theta / 2 + 45 / 2) + p5.fill(this.fill) + p5.beginShape() + p5.vertex(x1, y1) + p5.vertex(x2, y2) + p5.vertex(x3, y3) + p5.endShape(p5.CLOSE) + } else { + x1 = this.radius + y1 = 0 + x2 = this.radius * p5.cos(this.theta) + y2 = this.radius * p5.sin(this.theta) + + const starPointOffsetRatio = this.bool_seeds[0] ? 0 : 0.1 + p5.fill(this.fill) + p5.beginShape() + p5.vertex(x1, y1) + p5.vertex(0, 0) + p5.vertex(x2, y2) + p5.endShape(p5.CLOSE) + p5.line(x1, y1, x2, y2) + p5.line(x1 - x1 * starPointOffsetRatio, y1 - y1 * starPointOffsetRatio, x2 - x2 * 0.65, y2 - y2 * 0.65) + p5.line(x2 - x2 * starPointOffsetRatio, y2 - y2 * starPointOffsetRatio, x1 - x1 * 0.65, y1 - y1 * 0.65) + } + p5.pop() + } + } + + const setup = (p5: p5Types, canvasParentRef: Element) => { + p5.noLoop() + p5.createCanvas(width, height).parent(canvasParentRef) + p5.colorMode(p5.HSB, 360, 100, 100, 1) + p5.pixelDensity(2) + p5.angleMode(p5.DEGREES) + p5.imageMode(p5.CENTER) + p5.ellipseMode(p5.CENTER) + markingsLayers = p5.round(R.random_num(4, 7)) + p5.strokeJoin(p5.ROUND) + p5.strokeWeight(strokeWeight) + initializeMarkings(p5) + initializeNoiseLayer(p5) + } + + const draw = (p5: p5Types) => { + p5.background(bgColor) + renderMarkings(p5) + renderNoise(p5) + addBorder(p5) + //@ts-ignore + const canvas = p5.canvas.toDataURL("image/png") + if (onImageGenerated) { + onImageGenerated(canvas) + } + } + + const initializeMarkings = (p5: p5Types) => { + const layerWithCircles = R.shuffleArray(Array.from(Array(markingsLayers).keys())).slice(0, 2) + p5.stroke(43, 2, 20) + for (let i = 0; i < markingsLayers; i++) { + const pieces = [] + const fillValue = palette[i] + const bool_seeds = [R.random_bool(0.5), R.random_bool(0.93)] + const radius = p5.map(i, 0, markingsLayers, width * 0.43, height * 0.05) + const isRay = R.random_bool(0.65) + const piecesNum = R.random_choice([!isRay ? 4 : 0, 8, 16]) + const rotOffset = isRay && piecesNum === 4 ? 22.5 : 0 + const innerRadius = isRay ? radius * R.random_num(0.5, 0.8) : null + const theta = 360 / piecesNum + for (let p = 0; p < piecesNum; p++) { + pieces.push( + new (Piece as any)( + theta, // theta, + radius, // radius, + theta * p + rotOffset, // rotation, + fillValue, // fill + strokeWeight, // strokeWeight + strokeColor, // strokeColor + bool_seeds, // bool_seeds + p5, // p5 + innerRadius, // innerRadius + ), + ) + } + layers.push( + new (Layer as any)( + p5.map(i, 0, markingsLayers, width * 0.43, height * 0.05), // size + pieces, // pieces + layerWithCircles.some((layer: number) => layer === i), // showCircles + theta, // theta + fillValue, // fill + ), + ) + } + } + + const initializeNoiseLayer = (p5: p5Types) => { + noiseOpacity = R.random_num(0.1, 0.3) + noiseImg = p5.createGraphics(width, height) + + for (x = 0; x <= width; x++) { + for (y = 0; y <= height; y++) { + noiseImg.noStroke() + noiseImg.fill(p5.map(R.random_dec(), 0, 1, 0, 255)) + noiseImg.circle(x, y, 1) + } + } + } + + const renderMarkings = (p5: p5Types) => { + layers.forEach((layer: any) => { + layer.pieces.forEach((piece: any) => { + piece.render() + }) + if (layer.showCircles) { + const circleSize = R.random_num(0.2, 0.4) + layer.pieces.forEach((piece: any) => { + p5.push() + p5.translate(width / 2, height / 2) + const rotOffset = 22.5 + p5.rotate(piece.rotation + rotOffset) + const cx = layer.size * p5.cos(layer.theta + 45 / 2) + const cy = layer.size * p5.sin(layer.theta + 45 / 2) + const d = 2 * p5.abs(p5.sin(layer.theta / 2)) * layer.size + p5.strokeWeight(strokeWeight) + p5.stroke(strokeColor) + p5.fill(layer.fill) + p5.circle(cx, cy, d * circleSize) + p5.pop() + }) + } + }) + } + + const renderNoise = (p5: p5Types) => { + if (noiseImg) { + p5.translate(p5.width / 2, p5.height / 2) + p5.blendMode(p5.HARD_LIGHT) + p5.tint(255, noiseOpacity) + p5.image(noiseImg, 0, 0, p5.width, p5.height) + p5.blendMode(p5.BLEND) + } + } + + const addBorder = (p5: p5Types) => { + p5.noFill() + p5.stroke(outerStrokeColor) + p5.strokeWeight(strokeWeight * 2) // half of the border gets cropped. + p5.ellipse(0, 0, width - strokeWeight, height - strokeWeight) + } + + return ( + + + + ) +} + +export default DeterministicAvatar diff --git a/packages/app/src/components/commons/DeterministicAvatar/utils.js b/packages/app/src/components/commons/DeterministicAvatar/utils.js new file mode 100644 index 00000000..78654681 --- /dev/null +++ b/packages/app/src/components/commons/DeterministicAvatar/utils.js @@ -0,0 +1,82 @@ +class Random { + constructor(tokenData) { + this.tokenData = tokenData + this.useA = false + let sfc32 = function (uint128Hex) { + let a = parseInt(uint128Hex.substr(0, 8), 16) + let b = parseInt(uint128Hex.substr(8, 8), 16) + let c = parseInt(uint128Hex.substr(16, 8), 16) + let d = parseInt(uint128Hex.substr(24, 8), 16) + return function () { + a |= 0 + b |= 0 + c |= 0 + d |= 0 + let t = (((a + b) | 0) + d) | 0 + d = (d + 1) | 0 + a = b ^ (b >>> 9) + b = (c + (c << 3)) | 0 + c = (c << 21) | (c >>> 11) + c = (c + t) | 0 + return (t >>> 0) / 4294967296 + } + } + // seed prngA with first half of tokenData.hash + this.prngA = new sfc32(this.tokenData.hash.substr(2, 32)) + // seed prngB with second half of tokenData.hash + this.prngB = new sfc32(this.tokenData.hash.substr(34, 32)) + for (let i = 0; i < 1e6; i += 2) { + this.prngA() + this.prngB() + } + } + // random number between 0 (inclusive) and 1 (exclusive) + random_dec() { + this.useA = !this.useA + return this.useA ? this.prngA() : this.prngB() + } + // random number between a (inclusive) and b (exclusive) + random_num(a, b) { + return a + (b - a) * this.random_dec() + } + // random integer between a (inclusive) and b (inclusive) + // requires a < b for proper probability distribution + random_int(a, b) { + return Math.floor(this.random_num(a, b + 1)) + } + // random boolean with p as percent liklihood of true + random_bool(p) { + return this.random_dec() < p + } + // random value in an array of items + random_choice(list) { + return list[this.random_int(0, list.length - 1)] + } + + shuffleArray = (arr) => { + var rand + var tmp + var len = arr.length + var ret = [...arr] + while (len) { + rand = ~~(this.random_dec() * len--) + tmp = ret[len] + ret[len] = ret[rand] + ret[rand] = tmp + } + return ret + } +} + +export const genTokenData = (projectNum) => { + let data = {} + let hash = "0x" + for (var i = 0; i < 64; i++) { + hash += Math.floor(Math.random() * 16).toString(16) + } + data.hash = hash + data.tokenId = (projectNum * 1000000 + Math.floor(Math.random() * 1000)).toString() + return data +} + +export default Random diff --git a/packages/app/src/components/commons/PermissionItem.tsx b/packages/app/src/components/commons/PermissionItem.tsx index b2d0caee..a52f2de2 100644 --- a/packages/app/src/components/commons/PermissionItem.tsx +++ b/packages/app/src/components/commons/PermissionItem.tsx @@ -2,7 +2,7 @@ import React, { useEffect, useState } from "react" import { Box, Grid, Stack } from "@mui/material" import { styled } from "@mui/styles" import { palette } from "../../theme" -import { Permission, Publications } from "../../models/publication" +import { Permission, Publication } from "../../models/publication" import EditIcon from "@mui/icons-material/Edit" import CloseIcon from "@mui/icons-material/Close" import { UserBadge } from "./UserBadge" @@ -33,7 +33,7 @@ const PermissionItemEditContainer = styled(Grid)({ }) type PermissionItemProps = { - publication: Publications | undefined + publication: Publication | undefined permission: Permission canEdit: boolean showRemove: boolean diff --git a/packages/app/src/components/commons/PublicationAvatar.tsx b/packages/app/src/components/commons/PublicationAvatar.tsx index 4358f00f..598fdb2c 100644 --- a/packages/app/src/components/commons/PublicationAvatar.tsx +++ b/packages/app/src/components/commons/PublicationAvatar.tsx @@ -3,8 +3,10 @@ import { styled } from "@mui/styles" import React, { ChangeEvent, useEffect, useRef, useState } from "react" import { palette, typography } from "../../theme" import AddIcon from "@mui/icons-material/Add" -import EditIcon from "@mui/icons-material/Edit" +import ClearIcon from "@mui/icons-material/Clear" import { useIpfs } from "../../hooks/useIpfs" +import { usePublicationContext } from "../../services/publications/contexts" +import { useDynamicFavIcon } from "../../hooks/useDynamicFavIco" const SmallAvatar = styled(Avatar)({ width: 40, @@ -16,16 +18,21 @@ const SmallAvatar = styled(Avatar)({ type PublicationAvatarProps = { defaultImage?: string | null | undefined onFileSelected: (file: File) => void + newPublication?: boolean } -const PublicationAvatar: React.FC = ({ defaultImage, onFileSelected }) => { +const PublicationAvatar: React.FC = ({ defaultImage, onFileSelected, newPublication }) => { const [file, setFile] = useState() const inputFile = useRef(null) const openImagePicker = () => inputFile && inputFile.current?.click() const [uri, setUri] = useState(undefined) const [defaultImageSrc, setDefaultImageSrc] = useState("") + const { publicationAvatar, setRemovePublicationImage, removePublicationImage } = usePublicationContext() + const ipfs = useIpfs() + useDynamicFavIcon(uri) + const handleImage = (event: ChangeEvent) => { if (event.target.files && event.target.files[0]) { let reader = new FileReader() @@ -38,8 +45,11 @@ const PublicationAvatar: React.FC = ({ defaultImage, onF } useEffect(() => { - if (file) onFileSelected(file) - }, [file, onFileSelected]) + if (file) { + onFileSelected(file) + setRemovePublicationImage(false) + } + }, [file, onFileSelected, setRemovePublicationImage]) useEffect(() => { const getDefaultImageSrc = async () => { @@ -48,10 +58,33 @@ const PublicationAvatar: React.FC = ({ defaultImage, onF setDefaultImageSrc(src) } } - if (defaultImage != null && defaultImageSrc === "") { + if (defaultImage != null && defaultImageSrc === "" && !removePublicationImage) { + if (defaultImage.includes("https://")) { + return setDefaultImageSrc(defaultImage) + } getDefaultImageSrc() } - }, [defaultImage, ipfs, defaultImageSrc]) + if (!defaultImage && defaultImageSrc === "" && !removePublicationImage && publicationAvatar && !newPublication) { + setUri(publicationAvatar.uri) + return setDefaultImageSrc(publicationAvatar.uri) + } + if (!defaultImage && !uri && defaultImageSrc) { + return setDefaultImageSrc("") + } + }, [defaultImage, ipfs, defaultImageSrc, removePublicationImage, publicationAvatar, newPublication, uri]) + + const handlerImageAction = () => { + if (!uri && (!defaultImage || removePublicationImage)) { + return openImagePicker() + } + } + + const deleteImage = () => { + setFile(undefined) + setUri(undefined) + setDefaultImageSrc("") + setRemovePublicationImage(true) + } return ( @@ -59,20 +92,37 @@ const PublicationAvatar: React.FC = ({ defaultImage, onF overlap="circular" anchorOrigin={{ vertical: "bottom", horizontal: "right" }} badgeContent={ - - {!uri && !defaultImage && } - {(uri || defaultImage) && } - - + <> + {!uri && (!defaultImage || removePublicationImage) && ( + + + + + )} + {(uri || defaultImage) && !removePublicationImage && ( + + + + )} + } > void } const PublicationItem: React.FC = ({ publication, onClick }) => { const { publicationSlug } = useParams<{ publicationSlug: string }>() const { title, tags } = publication - const { imageSrc } = usePublication(publicationSlug || "") + const slug = publicationSlug || publication.id || "" return ( @@ -41,9 +41,7 @@ const PublicationItem: React.FC = ({ publication, onClick - - {" "} - + {title} diff --git a/packages/app/src/components/layout/PublicationHeader.tsx b/packages/app/src/components/layout/PublicationHeader.tsx index 0e33bbf7..f85f9541 100644 --- a/packages/app/src/components/layout/PublicationHeader.tsx +++ b/packages/app/src/components/layout/PublicationHeader.tsx @@ -1,8 +1,8 @@ import React, { useEffect, useRef, useState } from "react" -import { Avatar, Box, Button, Container, Grid, styled, Typography } from "@mui/material" +import { Box, Button, Container, Grid, styled, Typography } from "@mui/material" import { useWeb3React } from "@web3-react/core" import { WalletBadge } from "../commons/WalletBadge" -import { Publications } from "../../models/publication" +import { Publication } from "../../models/publication" import AddIcon from "@mui/icons-material/Add" import theme, { palette, typography } from "../../theme" import { useLocation, useNavigate, useParams } from "react-router-dom" @@ -12,8 +12,10 @@ import { usePublicationContext } from "../../services/publications/contexts" import { UserOptions } from "../commons/UserOptions" import { useOnClickOutside } from "../../hooks/useOnClickOutside" +import Avatar from "../commons/Avatar" + type Props = { - publication?: Publications + publication?: Publication showCreatePost?: boolean } @@ -34,7 +36,7 @@ const PublicationHeader: React.FC = ({ publication, showCreatePost }) => const { refetch, chainId: publicationChainId } = usePublication(publicationSlug || "") const [show, setShow] = useState(false) const permissions = publication && publication.permissions - const { imageSrc } = usePublication(publicationSlug || "") + const ref = useRef() useOnClickOutside(ref, () => { if (show) { @@ -80,9 +82,7 @@ const PublicationHeader: React.FC = ({ publication, showCreatePost }) => sx={{ cursor: "pointer", transition: "opacity 0.25s ease-in-out", "&:hover": { opacity: 0.6 } }} onClick={handleNavigation} > - - {" "} - + = ({ children, publication, showCreatePost }) => { - const { publicationSlug } = useParams<{ publicationSlug: string }>() - const { imageSrc } = usePublication(publicationSlug || "") - useDynamicFavIcon(imageSrc) return ( <> diff --git a/packages/app/src/components/views/publication/PublicationView.tsx b/packages/app/src/components/views/publication/PublicationView.tsx index 3d2468e9..a4bcd194 100644 --- a/packages/app/src/components/views/publication/PublicationView.tsx +++ b/packages/app/src/components/views/publication/PublicationView.tsx @@ -1,4 +1,4 @@ -import { Avatar, Box, CircularProgress, Grid, Stack, Typography } from "@mui/material" +import { Box, CircularProgress, Grid, Stack, Typography } from "@mui/material" import { useWeb3React } from "@web3-react/core" import React, { useEffect, useState } from "react" import { useParams } from "react-router-dom" @@ -13,6 +13,7 @@ import { PermissionSection } from "./components/PermissionSection" import ArticleSection from "./components/ArticleSection" import PublicationTabs from "./components/PublicationTabs" import { SettingSection } from "./components/SettingSection" +import Avatar from "../../commons/Avatar" interface PublicationViewProps { updateChainId: (chainId: number) => void @@ -79,12 +80,16 @@ export const PublicationView: React.FC = ({ updateChainId > {!editingPublication && ( - - {" "} - + )} {editingPublication && ( - + )} diff --git a/packages/app/src/components/views/publication/PublicationsView.tsx b/packages/app/src/components/views/publication/PublicationsView.tsx index 3a9175af..506794e0 100644 --- a/packages/app/src/components/views/publication/PublicationsView.tsx +++ b/packages/app/src/components/views/publication/PublicationsView.tsx @@ -9,7 +9,7 @@ import usePoster from "../../../services/poster/hooks/usePoster" import { yupResolver } from "@hookform/resolvers/yup" import { useForm, Controller } from "react-hook-form" import * as yup from "yup" -import { Publications } from "../../../models/publication" +import { Publication } from "../../../models/publication" import { useWeb3React } from "@web3-react/core" import { useNavigate } from "react-router-dom" import { accessPublications } from "../../../utils/permission" @@ -87,7 +87,7 @@ export const PublicationsView: React.FC = ({ updateChainI setExecutePollInterval, } = usePublications() const [tags, setTags] = useState([]) - const [publicationsToShow, setPublicationsToShow] = useState([]) + const [publicationsToShow, setPublicationsToShow] = useState([]) const [publicationImg, setPublicationImg] = useState() const ipfs = useIpfs() const { @@ -132,7 +132,7 @@ export const PublicationsView: React.FC = ({ updateChainI handlePublication(data) } - const handlePublicationsToShow = (publications: Publications[], address: string) => { + const handlePublicationsToShow = (publications: Publication[], address: string) => { const show = accessPublications(publications, address) setPublicationsToShow(show) } @@ -179,7 +179,6 @@ export const PublicationsView: React.FC = ({ updateChainI Welcome to Tabula! - {publicationsToShow.length > 0 && ( @@ -211,7 +210,7 @@ export const PublicationsView: React.FC = ({ updateChainI - + diff --git a/packages/app/src/components/views/publication/components/SettingSection.tsx b/packages/app/src/components/views/publication/components/SettingSection.tsx index 0de2bcf4..0765e8c7 100644 --- a/packages/app/src/components/views/publication/components/SettingSection.tsx +++ b/packages/app/src/components/views/publication/components/SettingSection.tsx @@ -46,7 +46,14 @@ export const SettingSection: React.FC = ({ couldDelete, co const [tags, setTags] = useState([]) const [loading, setLoading] = useState(false) const [deleteLoading, setDeleteLoading] = useState(false) - const { publication, saveIsEditing, saveDraftPublicationImage, draftPublicationImage } = usePublicationContext() + const { + publication, + saveIsEditing, + saveDraftPublicationImage, + draftPublicationImage, + removePublicationImage, + setRemovePublicationImage, + } = usePublicationContext() const { executePublication, deletePublication } = usePoster() const { indexing: updateIndexing, @@ -92,8 +99,9 @@ export const SettingSection: React.FC = ({ couldDelete, co useEffect(() => { if (transactionCompleted) { setLoading(false) + setRemovePublicationImage(false) } - }, [transactionCompleted]) + }, [setRemovePublicationImage, transactionCompleted]) const onSubmitHandler = (data: Post) => { handlePublicationUpdate(data) @@ -106,9 +114,13 @@ export const SettingSection: React.FC = ({ couldDelete, co if (draftPublicationImage) { image = await ipfs.uploadContent(draftPublicationImage) } - if (!draftPublicationImage && publication?.image) { + if (!draftPublicationImage && publication?.image && !removePublicationImage) { image = { path: publication.image } } + if (!draftPublicationImage && publication?.image && removePublicationImage) { + image = undefined + } + if (title && publication && publication.id) { await executePublication({ id: publication.id, @@ -127,6 +139,7 @@ export const SettingSection: React.FC = ({ couldDelete, co } else { setLoading(false) } + setRemovePublicationImage(false) } const handlePublicationDelete = async () => { diff --git a/packages/app/src/models/publication.ts b/packages/app/src/models/publication.ts index f445f217..ea5b91b6 100644 --- a/packages/app/src/models/publication.ts +++ b/packages/app/src/models/publication.ts @@ -18,8 +18,9 @@ export interface PermissionAction { "publication/permissions": boolean } -export interface Publications { +export interface Publication { id: string + hash: string description?: string | null image?: string | null tags?: string[] | null @@ -48,6 +49,7 @@ export interface Article { poster?: string publication?: { id: string + hash: string title: string image?: string permissions: Permission[] diff --git a/packages/app/src/services/poster/hooks/usePoster.ts b/packages/app/src/services/poster/hooks/usePoster.ts index 97253896..c189c36e 100644 --- a/packages/app/src/services/poster/hooks/usePoster.ts +++ b/packages/app/src/services/poster/hooks/usePoster.ts @@ -78,6 +78,9 @@ const usePoster = () => { if (fields.image) { content.image = fields.image } + if (!fields.image) { + content.image = "" + } if (isValidChain) { if (signer) { setLoading(true) diff --git a/packages/app/src/services/poster/type.ts b/packages/app/src/services/poster/type.ts index 6b737849..dcd274a4 100644 --- a/packages/app/src/services/poster/type.ts +++ b/packages/app/src/services/poster/type.ts @@ -1,7 +1,7 @@ -import { Article, PermissionAction, Publications } from "../../models/publication" +import { Article, PermissionAction, Publication as Publications } from "../../models/publication" type ArticleAction = "article/create" | "article/update" | "article/delete" | "article/permissions" -export interface Publication extends Omit { +export interface Publication extends Omit { action: "publication/create" | "publication/update" | "publication/delete" | "publication/permissions" id?: string } diff --git a/packages/app/src/services/publications/contexts/publication.context.tsx b/packages/app/src/services/publications/contexts/publication.context.tsx index 5f430fc7..c0517bf2 100644 --- a/packages/app/src/services/publications/contexts/publication.context.tsx +++ b/packages/app/src/services/publications/contexts/publication.context.tsx @@ -1,7 +1,7 @@ import { ethers } from "ethers" import { useState } from "react" import { useIpfs } from "../../../hooks/useIpfs" -import { Article, Permission, Publications } from "../../../models/publication" +import { Article, Permission, Publication } from "../../../models/publication" import { createGenericContext } from "../../../utils/create-generic-context" import { getTextRecordContent } from "../../ens" @@ -11,8 +11,8 @@ const [usePublicationContext, PublicationContextProvider] = createGenericContext const PublicationProvider = ({ children }: PublicationProviderProps) => { const [currentPath, setCurrentPath] = useState(undefined) - const [publications, setPublications] = useState(undefined) - const [publication, setPublication] = useState(undefined) + const [publications, setPublications] = useState(undefined) + const [publication, setPublication] = useState(undefined) const [draftArticle, setDraftArticle] = useState
(undefined) const [article, setArticle] = useState
(undefined) const [permission, setPermission] = useState(undefined) @@ -22,6 +22,10 @@ const PublicationProvider = ({ children }: PublicationProviderProps) => { const [loading, setLoading] = useState(false) const ipfs = useIpfs() const [slugToPublicationId, setSlugToPublicationId] = useState<{ [key: string]: string }>({}) + const [publicationAvatar, setPublicationAvatar] = useState<{ publicationId: string; uri: string } | undefined>( + undefined, + ) + const [removePublicationImage, setRemovePublicationImage] = useState(false) const getPublicationId = async (publicationSlug: string, provider?: ethers.providers.BaseProvider) => { if (slugToPublicationId[publicationSlug]) { @@ -54,8 +58,8 @@ const PublicationProvider = ({ children }: PublicationProviderProps) => { } setLoading(false) } - const savePublication = (publication: Publications | undefined) => setPublication(publication) - const savePublications = (publications: Publications[] | undefined) => setPublications(publications) + const savePublication = (publication: Publication | undefined) => setPublication(publication) + const savePublications = (publications: Publication[] | undefined) => setPublications(publications) const saveDraftArticle = (article: Article | undefined) => setDraftArticle(article) const saveArticle = (article: Article | undefined) => setArticle(article) const savePermission = (permission: Permission) => setPermission(permission) @@ -75,6 +79,10 @@ const PublicationProvider = ({ children }: PublicationProviderProps) => { currentPath, markdownArticle, loading, + publicationAvatar, + removePublicationImage, + setRemovePublicationImage, + setPublicationAvatar, setMarkdownArticle, getIpfsData, getPublicationId, diff --git a/packages/app/src/services/publications/contexts/publication.types.ts b/packages/app/src/services/publications/contexts/publication.types.ts index 3bf51141..81b698e8 100644 --- a/packages/app/src/services/publications/contexts/publication.types.ts +++ b/packages/app/src/services/publications/contexts/publication.types.ts @@ -1,10 +1,10 @@ import { ethers } from "ethers" import { ReactNode } from "react" -import { Article, Permission, Publications } from "../../../models/publication" +import { Article, Permission, Publication } from "../../../models/publication" export type PublicationContextType = { - publication: Publications | undefined - publications: Publications[] | undefined + publication: Publication | undefined + publications: Publication[] | undefined draftArticle: Article | undefined article: Article | undefined permission: Permission | undefined @@ -13,6 +13,10 @@ export type PublicationContextType = { currentPath: string | undefined markdownArticle: string | undefined loading: boolean + publicationAvatar: { publicationId: string; uri: string } | undefined + setPublicationAvatar: (uri: { publicationId: string; uri: string } | undefined) => void + removePublicationImage: boolean + setRemovePublicationImage: (remove: boolean) => void getIpfsData: (hash: string) => void getPublicationId: (publicationSlug: string, provider?: ethers.providers.BaseProvider) => Promise setMarkdownArticle: (markdown: string | undefined) => void @@ -21,8 +25,8 @@ export type PublicationContextType = { setCurrentPath: (path: string | undefined) => void savePermission: (permission: Permission) => void saveDraftArticle: (article: Article | undefined) => void - savePublication: (publication: Publications | undefined) => void - savePublications: (publications: Publications[] | undefined) => void + savePublication: (publication: Publication | undefined) => void + savePublications: (publications: Publication[] | undefined) => void saveArticle: (article: Article | undefined) => void } diff --git a/packages/app/src/services/publications/hooks/usePublication.ts b/packages/app/src/services/publications/hooks/usePublication.ts index 210c5159..a5eae43f 100644 --- a/packages/app/src/services/publications/hooks/usePublication.ts +++ b/packages/app/src/services/publications/hooks/usePublication.ts @@ -5,7 +5,7 @@ import { SupportedChainId } from "../../../constants/chain" import { useIpfs } from "../../../hooks/useIpfs" import { useNotification } from "../../../hooks/useNotification" import { useWallet } from "../../../hooks/useWallet" -import { Permission, Publications } from "../../../models/publication" +import { Permission, Publication } from "../../../models/publication" import { usePosterContext } from "../../poster/context" import { usePublicationContext } from "../contexts" import { GET_PUBLICATION_QUERY } from "../queries" @@ -18,9 +18,10 @@ const usePublication = (publicationSlug: string) => { const [publicationId, setPublicationId] = useState() const openNotification = useNotification() const { transactionUrl } = usePosterContext() - const { publication, permission, savePublication, getPublicationId } = usePublicationContext() + const { publication, permission, savePublication, getPublicationId, publicationAvatar } = usePublicationContext() + const [showToast, setShowToast] = useState(true) - const [data, setData] = useState(undefined) + const [data, setData] = useState(undefined) const [indexing, setIndexing] = useState(false) const [executePollInterval, setExecutePollInterval] = useState(false) const [currentTimestamp, setCurrentTimestamp] = useState(undefined) @@ -63,7 +64,10 @@ const usePublication = (publicationSlug: string) => { if (data?.image != null && imageSrc === "") { getImageSrc() } - }, [data, ipfs, imageSrc]) + if (data?.image === null) { + setImageSrc("") + } + }, [data, ipfs, imageSrc, publicationAvatar, publicationId]) const [{ data: result, fetching: loading }, executeQuery] = useQuery({ query: GET_PUBLICATION_QUERY, @@ -80,7 +84,7 @@ const usePublication = (publicationSlug: string) => { } }, [result]) - //Execute poll interval to know the latest publications indexed + //Execute poll interval to know the latest publication indexed useEffect(() => { if (executePollInterval) { setIndexing(true) @@ -227,6 +231,7 @@ const usePublication = (publicationSlug: string) => { indexing, transactionCompleted, refetch, + setImageSrc, imageSrc, executeQuery, setExecutePollInterval, diff --git a/packages/app/src/services/publications/hooks/usePublications.ts b/packages/app/src/services/publications/hooks/usePublications.ts index eb5209b5..f78e414b 100644 --- a/packages/app/src/services/publications/hooks/usePublications.ts +++ b/packages/app/src/services/publications/hooks/usePublications.ts @@ -2,7 +2,7 @@ import { findIndex, maxBy } from "lodash" import { useCallback, useEffect, useState } from "react" import { useQuery } from "urql" import { useNotification } from "../../../hooks/useNotification" -import { Publications } from "../../../models/publication" +import { Publication } from "../../../models/publication" import { usePosterContext } from "../../poster/context" import { usePublicationContext } from "../contexts" import { GET_PUBLICATIONS_QUERY } from "../queries" @@ -11,7 +11,7 @@ const usePublications = () => { const openNotification = useNotification() const { savePublications } = usePublicationContext() const { transactionUrl } = usePosterContext() - const [data, setData] = useState(undefined) + const [data, setData] = useState(undefined) const [indexing, setIndexing] = useState(false) const [executePollInterval, setExecutePollInterval] = useState(false) const [redirect, setRedirect] = useState(false) diff --git a/packages/app/src/services/publications/queries.ts b/packages/app/src/services/publications/queries.ts index a731675d..1f65930f 100644 --- a/packages/app/src/services/publications/queries.ts +++ b/packages/app/src/services/publications/queries.ts @@ -15,6 +15,7 @@ const PERMISSIONS = ` const PUBLICATION_CONTENT = ` id + hash description image tags diff --git a/packages/app/src/utils/permission.ts b/packages/app/src/utils/permission.ts index bcb07479..71fea99b 100644 --- a/packages/app/src/utils/permission.ts +++ b/packages/app/src/utils/permission.ts @@ -1,5 +1,5 @@ import { filter } from "lodash" -import { Permission, Publications } from "../models/publication" +import { Permission, Publication } from "../models/publication" type Action = | "articleCreate" @@ -9,10 +9,10 @@ type Action = | "publicationPermissions" | "publicationUpdate" -export const accessPublications = (publications: Publications[], address: string): Publications[] => { +export const accessPublications = (publications: Publication[], address: string): Publication[] => { const show = filter(publications, { permissions: [{ address: address.toLowerCase() }] }) if (show.length) { - return show as Publications[] + return show as Publication[] } else { return [] } diff --git a/packages/app/yarn.lock b/packages/app/yarn.lock index 7f8adbe9..dd5a6fbe 100644 --- a/packages/app/yarn.lock +++ b/packages/app/yarn.lock @@ -2824,6 +2824,11 @@ resolved "https://registry.yarnpkg.com/@types/normalize-package-data/-/normalize-package-data-2.4.1.tgz#d3357479a0fdfdd5907fe67e17e0a85c906e1301" integrity sha512-Gj7cI7z+98M282Tqmp2K5EIsoouUEzbBJhQQzDE3jSIRk6r9gsz0oUokqIUR4u1R3dMHo0pDHM7sNOHyhulypw== +"@types/p5@1.3.3": + version "1.3.3" + resolved "https://registry.yarnpkg.com/@types/p5/-/p5-1.3.3.tgz#a6994de51b93bd28f2e2ec3e0f159c209a7278eb" + integrity sha512-PBSFnX6IgV6Pqlx9wocUjSkGlm1I1ymz9tEiTbdNCqig6FOGiWcVUHx13TXRTBfRIhZC9+MqqgztMsgzpueaUg== + "@types/parse-json@^4.0.0": version "4.0.0" resolved "https://registry.yarnpkg.com/@types/parse-json/-/parse-json-4.0.0.tgz#2f8bb441434d163b35fb8ffdccd7138927ffb8c0" @@ -11548,6 +11553,11 @@ open@^7.0.2: is-docker "^2.0.0" is-wsl "^2.1.1" +opencollective-postinstall@2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/opencollective-postinstall/-/opencollective-postinstall-2.0.2.tgz#5657f1bede69b6e33a45939b061eb53d3c6c3a89" + integrity sha512-pVOEP16TrAO2/fjej1IdOyupJY8KDUM1CvsaScRbw6oddvpQoOfGk4ywha0HKKVAD6RkW4x6Q+tNBwhf3Bgpuw== + opn@^5.5.0: version "5.5.0" resolved "https://registry.yarnpkg.com/opn/-/opn-5.5.0.tgz#fc7164fab56d235904c51c3b27da6758ca3b9bfc" @@ -11755,6 +11765,11 @@ p-try@^2.0.0: resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.2.0.tgz#cb2868540e313d61de58fafbe35ce9004d5540e6" integrity sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ== +p5@1.3.1: + version "1.3.1" + resolved "https://registry.yarnpkg.com/p5/-/p5-1.3.1.tgz#7a6021bf2a42974cef71501c56e5b2120f5a2de1" + integrity sha512-g7W2htgEwiAEGcl0WHccAJKbunUJwrUojUSR9+KihphJ33p5VpDdh1K8pDx4ppYjOr/lVEXaZ1XXDj27nwlNOg== + pako@^1.0.2, pako@~1.0.5: version "1.0.11" resolved "https://registry.yarnpkg.com/pako/-/pako-1.0.11.tgz#6c9599d340d54dfd3946380252a35705a6b992bf" @@ -13163,6 +13178,15 @@ react-native-fetch-api@^3.0.0: dependencies: p-defer "^3.0.0" +react-p5@^1.3.33: + version "1.3.33" + resolved "https://registry.yarnpkg.com/react-p5/-/react-p5-1.3.33.tgz#981af17692a9b90ffc4e10be56d9f6e54de41fab" + integrity sha512-CCfkGksk5H9ze6YMAg9vj+pQJUIDyU334M9aeeb9V7nRk6bjUPNy5tp74PKGRgXu2UqBoZYH04b25NWx7Y9t0w== + dependencies: + "@types/p5" "1.3.3" + opencollective-postinstall "2.0.2" + p5 "1.3.1" + react-query@^3.34.16: version "3.34.16" resolved "https://registry.yarnpkg.com/react-query/-/react-query-3.34.16.tgz#279ea180bcaeaec49c7864b29d1711ee9f152594"