diff --git a/.eslintrc.js b/.eslintrc.js index 24a2c01b4c..c46726dd12 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -6,6 +6,7 @@ module.exports = { "@typescript-eslint/no-unused-vars": "error", "react-hooks/rules-of-hooks": "error", // Checks rules of Hooks "react-hooks/exhaustive-deps": "error", // Checks effect dependencies + "prettier/prettier": "error", }, overrides: [ { diff --git a/assets/icons/lock.svg b/assets/icons/lock.svg new file mode 100644 index 0000000000..bfce8aadd3 --- /dev/null +++ b/assets/icons/lock.svg @@ -0,0 +1,3 @@ + + + diff --git a/packages/candymachine/pinata-upload.ts b/packages/candymachine/pinata-upload.ts index 3b63118379..5c97ce635f 100644 --- a/packages/candymachine/pinata-upload.ts +++ b/packages/candymachine/pinata-upload.ts @@ -1,6 +1,6 @@ import axios from "axios"; -import { LocalFileData } from "../utils/types/feed"; +import { LocalFileData } from "../utils/types/files"; interface PinataFileProps { file: LocalFileData; diff --git a/packages/components/FilePreview/AudioView.tsx b/packages/components/FilePreview/AudioView.tsx index b101c3b02c..60fd6e6472 100644 --- a/packages/components/FilePreview/AudioView.tsx +++ b/packages/components/FilePreview/AudioView.tsx @@ -4,11 +4,11 @@ import { View, Image, TouchableOpacity } from "react-native"; import { ActivityIndicator } from "react-native-paper"; import { AudioWaveform } from "./AudioWaveform"; -import { ipfsURLToHTTPURL } from "./ipfs"; import pauseSVG from "../../../assets/icons/pause.svg"; import playSVG from "../../../assets/icons/play.svg"; import { useMaxResolution } from "../../hooks/useMaxResolution"; import { getAudioDuration } from "../../utils/audio"; +import { ipfsURLToHTTPURL } from "../../utils/ipfs"; import { errorColor, neutral00, @@ -17,7 +17,7 @@ import { } from "../../utils/style/colors"; import { fontSemibold13, fontSemibold14 } from "../../utils/style/fonts"; import { layout, screenContentMaxWidth } from "../../utils/style/layout"; -import { RemoteFileData } from "../../utils/types/feed"; +import { RemoteFileData } from "../../utils/types/files"; import { BrandText } from "../BrandText"; import { SVG } from "../SVG"; import { THUMBNAIL_WIDTH } from "../socialFeed/SocialThread/SocialMessageContent"; diff --git a/packages/components/FilePreview/EditableAudioPreview.tsx b/packages/components/FilePreview/EditableAudioPreview.tsx index 2ba285ae3c..20194d7461 100644 --- a/packages/components/FilePreview/EditableAudioPreview.tsx +++ b/packages/components/FilePreview/EditableAudioPreview.tsx @@ -15,7 +15,7 @@ import { } from "../../utils/style/colors"; import { fontMedium32, fontSemibold12 } from "../../utils/style/fonts"; import { layout } from "../../utils/style/layout"; -import { LocalFileData } from "../../utils/types/feed"; +import { LocalFileData } from "../../utils/types/files"; import { BrandText } from "../BrandText"; import { SVG } from "../SVG"; import { FileUploader } from "../fileUploader"; diff --git a/packages/components/FilePreview/FilesPreviewsContainer.tsx b/packages/components/FilePreview/FilesPreviewsContainer.tsx index 392af54e59..0aa172f034 100644 --- a/packages/components/FilePreview/FilesPreviewsContainer.tsx +++ b/packages/components/FilePreview/FilesPreviewsContainer.tsx @@ -8,7 +8,7 @@ import { VideoView } from "./VideoView"; import { GIF_MIME_TYPE } from "../../utils/mime"; import { convertGIFToLocalFileType } from "../../utils/social-feed"; import { layout } from "../../utils/style/layout"; -import { LocalFileData, RemoteFileData } from "../../utils/types/feed"; +import { LocalFileData, RemoteFileData } from "../../utils/types/files"; interface FilePreviewContainerProps { files?: LocalFileData[]; diff --git a/packages/components/FilePreview/ImagesFullViewModal.tsx b/packages/components/FilePreview/ImagesFullViewModal.tsx index 3314a058af..8e1efc94a4 100644 --- a/packages/components/FilePreview/ImagesFullViewModal.tsx +++ b/packages/components/FilePreview/ImagesFullViewModal.tsx @@ -7,9 +7,9 @@ import { } from "react-native"; import { SvgProps } from "react-native-svg"; -import { ipfsURLToHTTPURL } from "./ipfs"; import chevronLeft from "../../../assets/icons/chevron-left.svg"; import chevronRight from "../../../assets/icons/chevron-right.svg"; +import { ipfsURLToHTTPURL } from "../../utils/ipfs"; import { neutral22, neutral33 } from "../../utils/style/colors"; import { SVG } from "../SVG"; import ModalBase from "../modals/ModalBase"; diff --git a/packages/components/FilePreview/ImagesViews.tsx b/packages/components/FilePreview/ImagesViews.tsx index 34fa33f385..f28d648f17 100644 --- a/packages/components/FilePreview/ImagesViews.tsx +++ b/packages/components/FilePreview/ImagesViews.tsx @@ -3,11 +3,11 @@ import { Image, TouchableOpacity, View } from "react-native"; import { DeleteButton } from "./DeleteButton"; import { ImagesFullViewModal } from "./ImagesFullViewModal"; -import { ipfsURLToHTTPURL } from "./ipfs"; +import { ipfsURLToHTTPURL } from "../../utils/ipfs"; import { errorColor } from "../../utils/style/colors"; import { fontSemibold13 } from "../../utils/style/fonts"; import { layout } from "../../utils/style/layout"; -import { LocalFileData, RemoteFileData } from "../../utils/types/feed"; +import { LocalFileData, RemoteFileData } from "../../utils/types/files"; import { BrandText } from "../BrandText"; interface ImagePreviewProps { diff --git a/packages/components/FilePreview/VideoView.tsx b/packages/components/FilePreview/VideoView.tsx index b552186393..0b091b72de 100644 --- a/packages/components/FilePreview/VideoView.tsx +++ b/packages/components/FilePreview/VideoView.tsx @@ -3,11 +3,11 @@ import React from "react"; import { View } from "react-native"; import { DeleteButton } from "./DeleteButton"; -import { ipfsURLToHTTPURL } from "./ipfs"; +import { ipfsURLToHTTPURL } from "../../utils/ipfs"; import { errorColor } from "../../utils/style/colors"; import { fontSemibold13 } from "../../utils/style/fonts"; import { layout } from "../../utils/style/layout"; -import { LocalFileData, RemoteFileData } from "../../utils/types/feed"; +import { LocalFileData, RemoteFileData } from "../../utils/types/files"; import { BrandText } from "../BrandText"; interface VideoPreviewProps { diff --git a/packages/components/FilePreview/ipfs.ts b/packages/components/FilePreview/ipfs.ts deleted file mode 100644 index 8469975bd8..0000000000 --- a/packages/components/FilePreview/ipfs.ts +++ /dev/null @@ -1,15 +0,0 @@ -// temporary hotfix -// pinata-pinned files are weirdly handled by nft.storage gateway - -export const ipfsURLToHTTPURL = (ipfsURL: string | undefined) => { - if (!ipfsURL) { - return ""; - } - if (ipfsURL.startsWith("https://") || ipfsURL.startsWith("blob:")) { - return ipfsURL; - } - if (ipfsURL.startsWith("ipfs://")) { - return ipfsURL.replace("ipfs://", "https://cloudflare-ipfs.com/ipfs/"); - } - return "https://cloudflare-ipfs.com/ipfs/" + ipfsURL; -}; diff --git a/packages/components/fileUploader/FileUploader.type.ts b/packages/components/fileUploader/FileUploader.type.ts index d2a8e83dcd..6f028c0fef 100644 --- a/packages/components/fileUploader/FileUploader.type.ts +++ b/packages/components/fileUploader/FileUploader.type.ts @@ -1,7 +1,7 @@ import React from "react"; import { StyleProp, ViewStyle } from "react-native"; -import { LocalFileData } from "../../utils/types/feed"; +import { LocalFileData } from "../../utils/types/files"; export interface FileUploaderProps { onUpload: (files: LocalFileData[]) => void; diff --git a/packages/components/fileUploader/formatFile.ts b/packages/components/fileUploader/formatFile.ts index cc555761f6..cdddc3dd07 100644 --- a/packages/components/fileUploader/formatFile.ts +++ b/packages/components/fileUploader/formatFile.ts @@ -3,7 +3,7 @@ import { IMAGE_MIME_TYPES, VIDEO_MIME_TYPES, } from "./../../utils/mime"; -import { FileType, LocalFileData } from "../../utils/types/feed"; +import { FileType, LocalFileData } from "../../utils/types/files"; import { getAudioData } from "../../utils/waveform"; export const formatFile = async (file: File): Promise => { diff --git a/packages/components/inputs/SelectInput.tsx b/packages/components/inputs/SelectInput.tsx new file mode 100644 index 0000000000..8356088657 --- /dev/null +++ b/packages/components/inputs/SelectInput.tsx @@ -0,0 +1,232 @@ +import React, { ReactElement, useState } from "react"; +import { + View, + ScrollView, + StyleSheet, + StyleProp, + ViewStyle, +} from "react-native"; + +import { Label } from "./TextInputCustom"; +import chevronDownSVG from "../../../assets/icons/chevron-down.svg"; +import chevronUpSVG from "../../../assets/icons/chevron-up.svg"; +import lockSVG from "../../../assets/icons/lock.svg"; +import { + neutral00, + neutral33, + neutral77, + neutralA3, + secondaryColor, +} from "../../utils/style/colors"; +import { fontMedium13, fontSemibold14 } from "../../utils/style/fonts"; +import { layout } from "../../utils/style/layout"; +import { BrandText } from "../BrandText"; +import { SVG } from "../SVG"; +import { CustomPressable } from "../buttons/CustomPressable"; +import { SpacerColumn, SpacerRow } from "../spacer"; + +export type SelectInputDataValue = string | number; + +export type SelectInputData = { + label: string; + value: SelectInputDataValue; + iconComponent?: ReactElement; +}; + +type Props = { + data: SelectInputData[]; + placeHolder?: string; + selectedData: SelectInputData; + setData: (data: SelectInputData) => void; + disabled?: boolean; + style?: StyleProp; + boxStyle?: StyleProp; + label?: string; + isRequired?: boolean; +}; + +export const SelectInput: React.FC = ({ + data, + placeHolder, + selectedData, + setData, + disabled, + style, + boxStyle, + label, + isRequired, +}) => { + const [openMenu, setOpenMenu] = useState(false); + const [hoveredIndex, setHoveredIndex] = useState(0); + const [hovered, setHovered] = useState(false); + + const getScrollViewStyle = () => { + if (data.length > 5) { + return [styles.dropdownMenu, { height: 200 }]; + } + return styles.dropdownMenu; + }; + + return ( + setHovered(true)} + onHoverOut={() => setHovered(false)} + onPress={() => { + if (!disabled || data.length) setOpenMenu((value) => !value); + }} + disabled={disabled || !data.length} + > + {label && ( + <> + + + + )} + + + + {selectedData.iconComponent && ( + <> + {selectedData.iconComponent} + + + )} + + + {selectedData?.label ? selectedData.label : placeHolder} + + + + + + + {/*TODO: If the opened menu appears under other elements, you'll may need to set zIndex:-1 or something to these elements*/} + {openMenu && ( + + {data.map((item, index) => ( + { + setHoveredIndex(index + 1); + setHovered(true); + }} + onHoverOut={() => { + setHoveredIndex(0); + setHovered(false); + }} + onPress={() => { + setData(item); + setOpenMenu(false); + }} + key={index} + style={styles.dropdownMenuRow} + > + + {item.iconComponent && ( + <> + {item.iconComponent} + + + )} + + + {item.label} + + + + ))} + + )} + + + ); +}; + +const styles = StyleSheet.create({ + selectInputLabel: StyleSheet.flatten([fontSemibold14, { color: neutralA3 }]), + selectInput: { + backgroundColor: neutral00, + fontSize: 14, + fontWeight: 600, + color: secondaryColor, + borderColor: neutral33, + borderWidth: 1, + borderRadius: 12, + padding: layout.padding_x1_5, + flexDirection: "row", + justifyContent: "space-between", + alignItems: "center", + }, + inputContainer: { + backgroundColor: neutral00, + borderWidth: 1, + borderColor: neutral33, + borderRadius: 12, + paddingHorizontal: layout.padding_x1_5, + }, + inputItemStyle: { + backgroundColor: "#292929", + color: neutralA3, + paddingVertical: layout.padding_x1_5, + paddingHorizontal: layout.padding_x1, + }, + iconLabel: { + flexDirection: "row", + alignItems: "center", + }, + + dropdownMenu: { + backgroundColor: "#292929", + borderWidth: 1, + borderColor: neutral33, + borderRadius: 12, + padding: layout.padding_x1, + position: "absolute", + top: 52, + width: "100%", + zIndex: 10, + }, + dropdownMenuText: StyleSheet.flatten([fontMedium13]), + dropdownMenuRow: { + borderRadius: 6, + padding: layout.padding_x1, + }, + // dropdownMenuRow: { + // backgroundColor: neutral00, + // borderRadius: 6, + // padding: layout.padding_x1, + // }, +}); diff --git a/packages/components/inputs/TextInputCustom.tsx b/packages/components/inputs/TextInputCustom.tsx index 84a9681655..9f91a19b8f 100644 --- a/packages/components/inputs/TextInputCustom.tsx +++ b/packages/components/inputs/TextInputCustom.tsx @@ -6,6 +6,7 @@ import React, { useEffect, useMemo, useRef, + useState, } from "react"; import { RegisterOptions, @@ -16,7 +17,7 @@ import { FieldValues, } from "react-hook-form"; import { - Pressable, + ActivityIndicator, StyleProp, StyleSheet, TextInput, @@ -27,7 +28,6 @@ import { } from "react-native"; import { SvgProps } from "react-native-svg"; -// import { TextInputLabelProps } from "./TextInputOutsideLabel"; import { DEFAULT_FORM_ERRORS } from "../../utils/errors"; import { handleKeyPress } from "../../utils/keyboard"; import { @@ -39,22 +39,19 @@ import { neutralA3, secondaryColor, } from "../../utils/style/colors"; -import { - fontMedium10, - fontSemibold14, - fontSemibold20, -} from "../../utils/style/fonts"; +import { fontMedium10, fontSemibold14 } from "../../utils/style/fonts"; import { layout } from "../../utils/style/layout"; import { BrandText } from "../BrandText"; import { ErrorText } from "../ErrorText"; import { SVG } from "../SVG"; import { TertiaryBox } from "../boxes/TertiaryBox"; +import { CustomPressable } from "../buttons/CustomPressable"; import { SpacerColumn, SpacerRow } from "../spacer"; export interface TextInputCustomProps extends Omit { label: string; - variant?: "regular" | "labelOutside" | "noCropBorder" | "noStyle"; + variant?: "regular" | "labelOutside" | "noStyle"; iconSVG?: React.FC; placeHolder?: string; squaresBackgroundColor?: string; @@ -72,7 +69,9 @@ export interface TextInputCustomProps defaultValue?: PathValue>; subtitle?: React.ReactElement; hideLabel?: boolean; + errorStyle?: ViewStyle; valueModifier?: (value: string) => string; + isLoading?: boolean; labelStyle?: TextStyle; containerStyle?: ViewStyle; boxMainContainerStyle?: ViewStyle; @@ -84,21 +83,30 @@ export interface TextInputCustomProps export const Label: React.FC<{ children: string; - style?: TextStyle; + style?: StyleProp; isRequired?: boolean; -}> = ({ children, style, isRequired }) => ( + hovered?: boolean; +}> = ({ children, style, isRequired, hovered }) => ( - + {children} {!!isRequired && children && ( @@ -123,6 +131,7 @@ export const TextInputCustom = ({ width, height, variant = "regular", + noBrokenCorners, name, control, defaultValue, @@ -130,10 +139,10 @@ export const TextInputCustom = ({ subtitle, labelStyle, iconSVG, - noBrokenCorners, - // isAsterickSign, hideLabel, valueModifier, + errorStyle, + isLoading, containerStyle, boxMainContainerStyle, error, @@ -141,14 +150,13 @@ export const TextInputCustom = ({ setRef, ...restProps }: TextInputCustomProps) => { - // variables const { field, fieldState } = useController({ name, control, rules, - defaultValue, }); const inputRef = useRef(null); + const [hovered, setHovered] = useState(false); // Passing ref to parent since I didn't find a pattern to handle generic argument AND forwardRef useEffect(() => { if (inputRef.current && setRef) { @@ -158,7 +166,7 @@ export const TextInputCustom = ({ useEffect(() => { if (defaultValue) { - handleChangeText(defaultValue); + handleChangeText(defaultValue || ""); } // handleChangeText changes on every render and we want to call handleChangeText only when default value changes so we disable exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps @@ -218,25 +226,35 @@ export const TextInputCustom = ({ ); return ( - - {variant === "labelOutside" && ( + setHovered(true)} + onHoverOut={() => setHovered(false)} + onPress={() => inputRef?.current?.focus()} + disabled={disabled} + > + {variant === "labelOutside" && !hideLabel && ( <> - - + )} - ({ )} {!variant || - (!["labelOutside", "noCropBorder"].includes(variant) && - !hideLabel && ( - inputRef.current?.focus()}> - - {label} - - - - ))} + (variant !== "labelOutside" && !hideLabel && ( + <> + + {label} + + + + ))} handleKeyPress({ event, onPressEnter })} - placeholderTextColor={neutralA3} + placeholderTextColor={neutral77} value={field.value} style={[styles.textInput, textInputStyle]} {...restProps} /> - <>{children} + {isLoading ? ( + + ) : ( + <>{children} + )} {error || fieldError} - + ); }; const styles = StyleSheet.create({ - rowEnd: { - flexDirection: "row", - justifyContent: "space-between", - alignItems: "flex-end", - }, mainContainer: { alignItems: "flex-start", paddingHorizontal: 12, @@ -304,7 +320,7 @@ const styles = StyleSheet.create({ paddingVertical: layout.padding_x1_5, }, labelText: { - color: neutral77, + color: neutralA3, }, textInput: { fontSize: 14, @@ -317,4 +333,9 @@ const styles = StyleSheet.create({ alignItems: "center", width: "100%", }, + rowEnd: { + flexDirection: "row", + justifyContent: "space-between", + alignItems: "flex-end", + }, }); diff --git a/packages/components/socialFeed/NewsFeed/NewsFeed.type.ts b/packages/components/socialFeed/NewsFeed/NewsFeed.type.ts index 11e839ecd9..12d9baf14b 100644 --- a/packages/components/socialFeed/NewsFeed/NewsFeed.type.ts +++ b/packages/components/socialFeed/NewsFeed/NewsFeed.type.ts @@ -1,6 +1,6 @@ import { Post } from "../../../api/feed/v1/feed"; import { PostResult } from "../../../contracts-clients/teritori-social-feed/TeritoriSocialFeed.types"; -import { LocalFileData, RemoteFileData } from "../../../utils/types/feed"; +import { LocalFileData, RemoteFileData } from "../../../utils/types/files"; export enum PostCategory { Reaction, diff --git a/packages/components/socialFeed/NewsFeed/NewsFeedInput.tsx b/packages/components/socialFeed/NewsFeed/NewsFeedInput.tsx index cdc90b4cd7..9ba9a8b4b3 100644 --- a/packages/components/socialFeed/NewsFeed/NewsFeedInput.tsx +++ b/packages/components/socialFeed/NewsFeed/NewsFeedInput.tsx @@ -10,6 +10,7 @@ import { useWindowDimensions, } from "react-native"; import Animated, { useSharedValue } from "react-native-reanimated"; +import { useSelector } from "react-redux"; import { v4 as uuidv4 } from "uuid"; import { @@ -18,11 +19,7 @@ import { ReplyToType, SocialFeedMetadata, } from "./NewsFeed.type"; -import { - generatePostMetadata, - getPostCategory, - uploadPostFilesToPinata, -} from "./NewsFeedQueries"; +import { generatePostMetadata, getPostCategory } from "./NewsFeedQueries"; import { NotEnoughFundModal } from "./NotEnoughFundModal"; import audioSVG from "../../../../assets/icons/audio.svg"; import cameraSVG from "../../../../assets/icons/camera.svg"; @@ -46,9 +43,11 @@ import { getUserId, mustGetCosmosNetwork, } from "../../../networks"; +import { selectNFTStorageAPI } from "../../../store/slices/settings"; import { prettyPrice } from "../../../utils/coins"; import { defaultSocialFeedFee } from "../../../utils/fee"; -import { adenaVMCall } from "../../../utils/gno"; +import { adenaDoContract } from "../../../utils/gno"; +import { generateIpfsKey, uploadFilesToPinata } from "../../../utils/ipfs"; import { AUDIO_MIME_TYPES, IMAGE_MIME_TYPES, @@ -58,7 +57,6 @@ import { SOCIAL_FEED_ARTICLE_MIN_CHARS_LIMIT, hashtagMatch, mentionMatch, - generateIpfsKey, replaceFileInArray, removeFileFromArray, } from "../../../utils/social-feed"; @@ -83,7 +81,7 @@ import { SOCIAL_FEED_BREAKPOINT_M, } from "../../../utils/style/layout"; import { replaceBetweenString } from "../../../utils/text"; -import { LocalFileData, RemoteFileData } from "../../../utils/types/feed"; +import { LocalFileData, RemoteFileData } from "../../../utils/types/files"; import { BrandText } from "../../BrandText"; import { FilesPreviewsContainer } from "../../FilePreview/FilesPreviewsContainer"; import FlexRow from "../../FlexRow"; @@ -194,7 +192,7 @@ export const NewsFeedInput = React.forwardRef< } ); const formValues = watch(); - + const userIPFSKey = useSelector(selectNFTStorageAPI); const { postFee } = useUpdatePostFee( selectedNetworkId, getPostCategory(formValues) @@ -247,9 +245,10 @@ export const NewsFeedInput = React.forwardRef< let files: RemoteFileData[] = []; if (formValues.files?.length) { - const pinataJWTKey = await generateIpfsKey(selectedNetworkId, userId); + const pinataJWTKey = + userIPFSKey || (await generateIpfsKey(selectedNetworkId, userId)); if (pinataJWTKey) { - files = await uploadPostFilesToPinata({ + files = await uploadFilesToPinata({ files: formValues.files, pinataJWTKey, }); @@ -332,12 +331,17 @@ export const NewsFeedInput = React.forwardRef< msg.metadata, ], }; - const tx = await adenaVMCall(vmCall, { - gasWanted: 2_000_000, - }); + + const txHash = await adenaDoContract( + selectedNetworkId, + [{ type: "/vm.m_call", value: vmCall }], + { + gasWanted: 1_000_000, + } + ); const provider = new GnoJSONRPCProvider(selectedNetwork.endpoint); - await provider.waitForTransaction(tx.data.hash); + await provider.waitForTransaction(txHash); onPostCreationSuccess(); } else { const client = await signingSocialFeedClient({ @@ -591,7 +595,7 @@ export const NewsFeedInput = React.forwardRef< : `The cost for this ${type} is ${prettyPrice( selectedNetworkId, postFee.toString(), - "utori" + selectedNetwork?.currencies?.[0].denom || "utori" )}`} diff --git a/packages/components/socialFeed/NewsFeed/NewsFeedQueries.ts b/packages/components/socialFeed/NewsFeed/NewsFeedQueries.ts index a698cbe135..ba4a759f6e 100644 --- a/packages/components/socialFeed/NewsFeed/NewsFeedQueries.ts +++ b/packages/components/socialFeed/NewsFeed/NewsFeedQueries.ts @@ -1,21 +1,24 @@ import { coin } from "@cosmjs/amino"; -import { omit } from "lodash"; import { v4 as uuidv4 } from "uuid"; import { - PostCategory, NewPostFormValues, + PostCategory, SocialFeedMetadata, } from "./NewsFeed.type"; -import { pinataPinFileToIPFS } from "../../../candymachine/pinata-upload"; import { nonSigningSocialFeedClient, signingSocialFeedClient, } from "../../../client-creators/socialFeedClient"; import { Wallet } from "../../../context/WalletsProvider"; +import { mustGetNetwork, NetworkKind } from "../../../networks"; import { defaultSocialFeedFee } from "../../../utils/fee"; -import { ipfsURLToHTTPURL } from "../../../utils/ipfs"; -import { LocalFileData, RemoteFileData } from "../../../utils/types/feed"; +import { ipfsURLToHTTPURL, uploadFilesToPinata } from "../../../utils/ipfs"; +import { RemoteFileData } from "../../../utils/types/files"; +import { GNO_SOCIAL_FEEDS_PKG_PATH, TERITORI_FEED_ID } from "../const"; +import { adenaDoContract } from "../../../utils/gno"; +import { GnoJSONRPCProvider } from "@gnolang/gno-js-client"; + interface GetAvailableFreePostParams { networkId: string; wallet?: Wallet; @@ -124,15 +127,10 @@ export const createPost = async ({ return; } - const client = await signingSocialFeedClient({ - networkId, - walletAddress: wallet.address, - }); - let files: RemoteFileData[] = []; if (formValues.files?.length && pinataJWTKey) { - files = await uploadPostFilesToPinata({ + files = await uploadFilesToPinata({ files: formValues.files, pinataJWTKey, }); @@ -163,62 +161,59 @@ export const createPost = async ({ mentions: formValues.mentions || [], }); - await client.createPost( - { + const network = mustGetNetwork(networkId); + + if (network.kind === NetworkKind.Gno) { + const msg = { category, - identifier: identifier || uuidv4(), + identifier, metadata: JSON.stringify(metadata), parentPostIdentifier: parentId, - }, - defaultSocialFeedFee, - "", - freePostCount ? undefined : [coin(fee, "utori")] - ); - return true; -}; - -interface UploadPostFilesToPinataParams { - files: LocalFileData[]; - pinataJWTKey: string; -} + }; + + const vmCall = { + caller: wallet.address, + send: "", + pkg_path: GNO_SOCIAL_FEEDS_PKG_PATH, + func: "CreatePost", + args: [ + TERITORI_FEED_ID, + msg.parentPostIdentifier || "0", + msg.category.toString(), + msg.metadata, + ], + }; + + const txHash = await adenaDoContract( + networkId, + [{ type: "/vm.m_call", value: vmCall }], + { + gasWanted: 2_000_000, + } + ); -export const uploadPostFilesToPinata = async ({ - files, - pinataJWTKey, -}: UploadPostFilesToPinataParams): Promise => { - const storedFile = async (file: LocalFileData): Promise => { - const fileData = await pinataPinFileToIPFS({ - file, - pinataJWTKey, + const provider = new GnoJSONRPCProvider(network.endpoint); + await provider.waitForTransaction(txHash); + } else { + const client = await signingSocialFeedClient({ + networkId, + walletAddress: wallet.address, }); - if (file.thumbnailFileData) { - const thumbnailData = await pinataPinFileToIPFS({ - file: file.thumbnailFileData, - pinataJWTKey, - }); - - return { - ...omit(file, "file"), - url: fileData?.IpfsHash || "", - thumbnailFileData: { - ...omit(file.thumbnailFileData, "file"), - url: thumbnailData?.IpfsHash || "", - }, - }; - } else { - return { - ...omit(file, "file"), - url: fileData?.IpfsHash || "", - }; - } - }; - const queries = []; - for (const file of files) { - const storedFileQuery = storedFile(file); - queries.push(storedFileQuery); + await client.createPost( + { + category, + identifier: identifier || uuidv4(), + metadata: JSON.stringify(metadata), + parentPostIdentifier: parentId, + }, + defaultSocialFeedFee, + "", + freePostCount ? undefined : [coin(fee, "utori")] + ); } - return await Promise.all(queries); + + return true; }; interface GeneratePostMetadataParams { diff --git a/packages/components/socialFeed/RichText/RichText.type.ts b/packages/components/socialFeed/RichText/RichText.type.ts index 9fd0949420..527b8ee727 100644 --- a/packages/components/socialFeed/RichText/RichText.type.ts +++ b/packages/components/socialFeed/RichText/RichText.type.ts @@ -1,6 +1,6 @@ import { EntityInstance } from "draft-js"; -import { LocalFileData, RemoteFileData } from "../../../utils/types/feed"; +import { LocalFileData, RemoteFileData } from "../../../utils/types/files"; export type PublishValues = { hashtags: string[]; diff --git a/packages/components/socialFeed/RichText/RichText.web.tsx b/packages/components/socialFeed/RichText/RichText.web.tsx index 63d5fb2d58..ac940817a5 100644 --- a/packages/components/socialFeed/RichText/RichText.web.tsx +++ b/packages/components/socialFeed/RichText/RichText.web.tsx @@ -81,7 +81,7 @@ import { import { neutral77 } from "../../../utils/style/colors"; import { fontSemibold14 } from "../../../utils/style/fonts"; import { layout, SOCIAL_FEED_BREAKPOINT_M } from "../../../utils/style/layout"; -import { LocalFileData } from "../../../utils/types/feed"; +import { LocalFileData } from "../../../utils/types/files"; import { BrandText } from "../../BrandText"; import { AudioView } from "../../FilePreview/AudioView"; import { EditableAudioPreview } from "../../FilePreview/EditableAudioPreview"; diff --git a/packages/components/socialFeed/SocialThread/ArticleRenderer.tsx b/packages/components/socialFeed/SocialThread/ArticleRenderer.tsx index ce37069805..5deee2f0df 100644 --- a/packages/components/socialFeed/SocialThread/ArticleRenderer.tsx +++ b/packages/components/socialFeed/SocialThread/ArticleRenderer.tsx @@ -1,11 +1,11 @@ import React from "react"; import { Image } from "react-native"; +import { ipfsURLToHTTPURL } from "../../../utils/ipfs"; import { ARTICLE_COVER_IMAGE_HEIGHT } from "../../../utils/social-feed"; import { layout } from "../../../utils/style/layout"; -import { RemoteFileData } from "../../../utils/types/feed"; +import { RemoteFileData } from "../../../utils/types/files"; import { BrandText } from "../../BrandText"; -import { ipfsURLToHTTPURL } from "../../FilePreview/ipfs"; import { SocialFeedMetadata } from "../NewsFeed/NewsFeed.type"; import { RichText } from "../RichText"; diff --git a/packages/screens/FeedNewArticle/FeedNewArticleScreen.tsx b/packages/screens/FeedNewArticle/FeedNewArticleScreen.tsx index e1f5a4e846..f39e3cb337 100644 --- a/packages/screens/FeedNewArticle/FeedNewArticleScreen.tsx +++ b/packages/screens/FeedNewArticle/FeedNewArticleScreen.tsx @@ -1,6 +1,7 @@ import React, { useState } from "react"; import { Controller, useForm } from "react-hook-form"; import { View } from "react-native"; +import { useSelector } from "react-redux"; import priceSVG from "../../../assets/icons/price.svg"; import { BrandText } from "../../components/BrandText"; @@ -27,16 +28,18 @@ import { useUpdateAvailableFreePost } from "../../hooks/feed/useUpdateAvailableF import { useUpdatePostFee } from "../../hooks/feed/useUpdatePostFee"; import { useBalances } from "../../hooks/useBalances"; import { useIsMobile } from "../../hooks/useIsMobile"; -import { useSelectedNetworkId } from "../../hooks/useSelectedNetwork"; +import { + useSelectedNetworkId, + useSelectedNetworkInfo, +} from "../../hooks/useSelectedNetwork"; import useSelectedWallet from "../../hooks/useSelectedWallet"; -import { getUserId, NetworkKind } from "../../networks"; +import { getUserId, NetworkFeature, NetworkKind } from "../../networks"; +import { selectNFTStorageAPI } from "../../store/slices/settings"; import { prettyPrice } from "../../utils/coins"; +import { generateIpfsKey } from "../../utils/ipfs"; import { IMAGE_MIME_TYPES } from "../../utils/mime"; import { ScreenFC, useAppNavigation } from "../../utils/navigation"; -import { - ARTICLE_COVER_IMAGE_HEIGHT, - generateIpfsKey, -} from "../../utils/social-feed"; +import { ARTICLE_COVER_IMAGE_HEIGHT } from "../../utils/social-feed"; import { neutral00, neutral11, @@ -51,7 +54,8 @@ import { pluralOrNot } from "../../utils/text"; export const FeedNewArticleScreen: ScreenFC<"FeedNewArticle"> = () => { const isMobile = useIsMobile(); - const selectedNetworkId = useSelectedNetworkId(); + const selectNetworkInfo = useSelectedNetworkInfo(); + const selectedNetworkId = selectNetworkInfo?.id || ""; const wallet = useSelectedWallet(); const { postFee } = useUpdatePostFee(selectedNetworkId, PostCategory.Article); const { freePostCount } = useUpdateAvailableFreePost( @@ -61,6 +65,7 @@ export const FeedNewArticleScreen: ScreenFC<"FeedNewArticle"> = () => { ); const [isNotEnoughFundModal, setNotEnoughFundModal] = useState(false); const [loading, setLoading] = useState(false); + const userIPFSKey = useSelector(selectNFTStorageAPI); const { setToastSuccess, setToastError } = useFeedbacks(); const navigation = useAppNavigation(); @@ -113,7 +118,8 @@ export const FeedNewArticleScreen: ScreenFC<"FeedNewArticle"> = () => { } let pinataJWTKey = undefined; if (files?.length) { - pinataJWTKey = await generateIpfsKey(selectedNetworkId, userId); + pinataJWTKey = + userIPFSKey || (await generateIpfsKey(selectedNetworkId, userId)); } const result = await createPost({ @@ -178,7 +184,7 @@ export const FeedNewArticleScreen: ScreenFC<"FeedNewArticle"> = () => { return ( = () => { : `The cost for this Article is ${prettyPrice( selectedNetworkId, postFee.toString(), - "utori" + selectNetworkInfo?.currencies?.[0].denom || "utori" )}`} @@ -268,31 +274,32 @@ export const FeedNewArticleScreen: ScreenFC<"FeedNewArticle"> = () => { borderRadius: 12, }} /> - - - {/**@ts-ignore error:TS2589: Type instantiation is excessively deep and possibly infinite. */} - ( - - )} - /> + + + + ( + + )} + /> + ); diff --git a/packages/screens/Organizations/components/ConfigureVotingSection.tsx b/packages/screens/Organizations/components/ConfigureVotingSection.tsx index 1ab0124e5d..57f8caa5f9 100644 --- a/packages/screens/Organizations/components/ConfigureVotingSection.tsx +++ b/packages/screens/Organizations/components/ConfigureVotingSection.tsx @@ -76,7 +76,7 @@ export const ConfigureVotingSection: React.FC = ({ name="days" - variant="noCropBorder" + noBrokenCorners hideLabel control={control} label="" @@ -89,7 +89,7 @@ export const ConfigureVotingSection: React.FC = ({ name="hours" - variant="noCropBorder" + noBrokenCorners hideLabel control={control} label="" @@ -103,7 +103,7 @@ export const ConfigureVotingSection: React.FC = ({ name="minutes" - variant="noCropBorder" + noBrokenCorners hideLabel control={control} label="" diff --git a/packages/screens/Organizations/components/CreateDAOSection.tsx b/packages/screens/Organizations/components/CreateDAOSection.tsx index 6871ce06fb..7e48beecf2 100644 --- a/packages/screens/Organizations/components/CreateDAOSection.tsx +++ b/packages/screens/Organizations/components/CreateDAOSection.tsx @@ -63,43 +63,44 @@ export const CreateDAOSection: React.FC = ({ + noBrokenCorners + variant="labelOutside" control={control} - variant="noCropBorder" label="Organization's name" placeHolder="Type organization's name here" name="organizationName" rules={{ required: true }} - // isAsterickSign /> + noBrokenCorners + variant="labelOutside" control={control} - variant="noCropBorder" label="Associated Teritori Name Service" placeHolder="your-organization.tori" name="associatedTeritoriNameService" rules={{ required: true }} - // isAsterickSign /> + noBrokenCorners control={control} - variant="noCropBorder" + variant="labelOutside" label="Organization's image url" placeHolder="https://example.com/preview.png" name="imageUrl" rules={{ required: true }} - // isAsterickSign /> + noBrokenCorners + variant="labelOutside" control={control} - variant="noCropBorder" label="Organization's description" placeHolder="Type organization's description here" name="organizationDescription" diff --git a/packages/screens/Organizations/components/MemberSettingsSection.tsx b/packages/screens/Organizations/components/MemberSettingsSection.tsx index c514cc3513..d61fbb698f 100644 --- a/packages/screens/Organizations/components/MemberSettingsSection.tsx +++ b/packages/screens/Organizations/components/MemberSettingsSection.tsx @@ -54,11 +54,10 @@ export const MemberSettingsSection: React.FC = ({ name={`members.${index}.addr`} - variant="noCropBorder" + noBrokenCorners label="Member Address" hideLabel={index > 0} control={control} - // isAsterickSign rules={{ required: true, validate: validateAddress }} placeHolder="Account address" iconSVG={walletInputSVG} @@ -75,11 +74,10 @@ export const MemberSettingsSection: React.FC = ({ name={`members.${index}.weight`} - variant="noCropBorder" + noBrokenCorners label="Weight" hideLabel={index > 0} control={control} - // isAsterickSign rules={{ required: true, pattern: patternOnlyNumbers }} placeHolder="1" /> diff --git a/packages/screens/Organizations/components/TokenSettingsSection.tsx b/packages/screens/Organizations/components/TokenSettingsSection.tsx index 0dfe358e59..970ae4581d 100644 --- a/packages/screens/Organizations/components/TokenSettingsSection.tsx +++ b/packages/screens/Organizations/components/TokenSettingsSection.tsx @@ -56,10 +56,9 @@ export const TokenSettingsSection: React.FC = ({ name="tokenName" - variant="noCropBorder" + noBrokenCorners label="Token name" control={control} - // isAsterickSign rules={{ required: true }} placeHolder="My Organization Token" /> @@ -68,10 +67,9 @@ export const TokenSettingsSection: React.FC = ({ name="tokenSymbol" - variant="noCropBorder" + noBrokenCorners label="Token Symbol" control={control} - // isAsterickSign valueModifier={(value) => value.toUpperCase()} rules={{ required: true, pattern: patternOnlyLetters }} placeHolder="ABC" @@ -85,11 +83,10 @@ export const TokenSettingsSection: React.FC = ({ name={`tokenHolders.${index}.address`} - variant="noCropBorder" + noBrokenCorners label="Token Holders" hideLabel={index > 0} control={control} - // isAsterickSign rules={{ required: true, validate: validateAddress }} placeHolder="Account address" iconSVG={walletInputSVG} @@ -106,11 +103,10 @@ export const TokenSettingsSection: React.FC = ({ name={`tokenHolders.${index}.balance`} - variant="noCropBorder" + noBrokenCorners label="Balances" hideLabel={index > 0} control={control} - // isAsterickSign rules={{ required: true, pattern: patternOnlyNumbers }} placeHolder="0" /> diff --git a/packages/screens/Settings/SettingsScreen.tsx b/packages/screens/Settings/SettingsScreen.tsx index c9986b7d7a..012bf6e64c 100644 --- a/packages/screens/Settings/SettingsScreen.tsx +++ b/packages/screens/Settings/SettingsScreen.tsx @@ -25,7 +25,7 @@ import { neutralA3, primaryColor } from "../../utils/style/colors"; import { fontSemibold14 } from "../../utils/style/fonts"; const NFTAPIKeyInput: React.FC = () => { - const NFTApiKey = useSelector(selectNFTStorageAPI); + const userIPFSKey = useSelector(selectNFTStorageAPI); const dispatch = useAppDispatch(); const commonStyles = useCommonStyles(); @@ -40,11 +40,12 @@ const NFTAPIKeyInput: React.FC = () => { }, ]} > - NFT.Storage/Pinata.cloud API key (for Social Feed) + app.pinata.cloud JWT key (For file upload) - dispatch(setNFTStorageAPI(process.env.NFT_STORAGE_API || "")) + // We ask key at each upload for now (Don't have Teritori's key for now) + dispatch(setNFTStorageAPI("")) } > @@ -55,7 +56,7 @@ const NFTAPIKeyInput: React.FC = () => { dispatch(setNFTStorageAPI(value))} /> diff --git a/packages/utils/ipfs.ts b/packages/utils/ipfs.ts index 837241c733..f792d182fb 100644 --- a/packages/utils/ipfs.ts +++ b/packages/utils/ipfs.ts @@ -1,3 +1,87 @@ +import { omit } from "lodash"; + +import { mustGetFeedClient } from "./backend"; +import { LocalFileData, RemoteFileData } from "./types/files"; +import { pinataPinFileToIPFS } from "../candymachine/pinata-upload"; + +interface UploadPostFilesToPinataParams { + files: LocalFileData[]; + pinataJWTKey: string; +} + +export const uploadFilesToPinata = async ({ + files, + pinataJWTKey, +}: UploadPostFilesToPinataParams): Promise => { + const storedFile = async (file: LocalFileData): Promise => { + const fileData = await pinataPinFileToIPFS({ + file, + pinataJWTKey, + }); + if (file.thumbnailFileData) { + const thumbnailData = await pinataPinFileToIPFS({ + file: file.thumbnailFileData, + pinataJWTKey, + }); + + return { + ...omit(file, "file"), + url: fileData?.IpfsHash || "", + thumbnailFileData: { + ...omit(file.thumbnailFileData, "file"), + url: thumbnailData?.IpfsHash || "", + }, + }; + } else { + return { + ...omit(file, "file"), + url: fileData?.IpfsHash || "", + }; + } + }; + + const queries = []; + for (const file of files) { + const storedFileQuery = storedFile(file); + queries.push(storedFileQuery); + } + return await Promise.all(queries); +}; + +export const generateIpfsKey = async (networkId: string, userId: string) => { + try { + const backendClient = mustGetFeedClient(networkId); + const response = await backendClient.IPFSKey({ userId }); + return response.jwt; + } catch (e) { + console.error("ERROR WHILE GENERATING IPFSKey : ", e); + return undefined; + } +}; + +// Get IPFS Key and upload files. +// But you can do separately generateIpfsKey then uploadFilesToPinata (Ex in NewsFeedInput.tsx) +export const uploadFileToIPFS = async ( + file: LocalFileData, + networkId: string, + userId: string, + userKey?: string +) => { + let uploadedFiles: RemoteFileData[] = []; + const pinataJWTKey = userKey || (await generateIpfsKey(networkId, userId)); + + if (pinataJWTKey) { + uploadedFiles = await uploadFilesToPinata({ + files: [file], + pinataJWTKey, + }); + } + if (!uploadedFiles.find((file) => file.url)) { + console.error("upload file err : Fail to pin to IPFS"); + } else return uploadedFiles[0]; +}; + +// Used to get a correct image URL for displaying or storing export const ipfsURLToHTTPURL = (ipfsURL: string | undefined) => { if (!ipfsURL) { return ""; diff --git a/packages/utils/social-feed.ts b/packages/utils/social-feed.ts index 78b7c60b47..ff3238a4ca 100644 --- a/packages/utils/social-feed.ts +++ b/packages/utils/social-feed.ts @@ -1,7 +1,6 @@ -import { mustGetFeedClient } from "./backend"; import { GIF_MIME_TYPE } from "./mime"; import { HASHTAG_REGEX, MENTION_REGEX, URL_REGEX } from "./regex"; -import { LocalFileData } from "./types/feed"; +import { LocalFileData } from "./types/files"; import { Post, Reaction } from "../api/feed/v1/feed"; import { PostCategory, @@ -100,17 +99,6 @@ export const postResultToPost = ( return post; }; -export const generateIpfsKey = async (networkId: string, userId: string) => { - try { - const backendClient = mustGetFeedClient(networkId); - const response = await backendClient.IPFSKey({ userId }); - return response.jwt; - } catch (e) { - console.error("ERROR WHILE GENERATING IPFSKey : ", e); - return undefined; - } -}; - export const replaceFileInArray = ( files: LocalFileData[], newFile: LocalFileData diff --git a/packages/utils/types/feed.ts b/packages/utils/types/files.ts similarity index 100% rename from packages/utils/types/feed.ts rename to packages/utils/types/files.ts diff --git a/packages/utils/waveform/waveform.web.ts b/packages/utils/waveform/waveform.web.ts index 696067f606..8b8fa12531 100644 --- a/packages/utils/waveform/waveform.web.ts +++ b/packages/utils/waveform/waveform.web.ts @@ -1,7 +1,7 @@ import WaveformData from "waveform-data"; import { BAR_LENGTH } from "./constants"; -import { AudioFileMetadata } from "../types/feed"; +import { AudioFileMetadata } from "../types/files"; //@ts-ignore window.AudioContext = window.AudioContext || window?.webkitAudioContext;