diff --git a/package.json b/package.json index c291389251b..711c36e6295 100644 --- a/package.json +++ b/package.json @@ -16,6 +16,7 @@ "@ecency/render-helper": "^2.2.26", "@ecency/render-helper-amp": "^1.1.0", "@emoji-mart/data": "^1.1.2", + "@emoji-mart/react": "^1.1.1", "@firebase/analytics": "^0.8.0", "@firebase/app": "^0.7.28", "@firebase/messaging": "^0.9.16", diff --git a/src/common/api/private-api.ts b/src/common/api/private-api.ts index ea8e912be6d..b109a4a354a 100644 --- a/src/common/api/private-api.ts +++ b/src/common/api/private-api.ts @@ -217,12 +217,7 @@ export const addImage = (username: string, url: string): Promise => { export interface DraftMetadata extends MetaData { beneficiaries: BeneficiaryRoute[]; rewardType: RewardType; - isThreespeak?: boolean; - speakAuthor?: string; - speakPermlink?: string; - videoId?: string; - isNsfw?: boolean; - videoMetadata?: ThreeSpeakVideo; + videos?: Record; } export interface Draft { diff --git a/src/common/components/beneficiary-editor/index.tsx b/src/common/components/beneficiary-editor/index.tsx index d1f39fe6dc2..94dce4cf62b 100644 --- a/src/common/components/beneficiary-editor/index.tsx +++ b/src/common/components/beneficiary-editor/index.tsx @@ -1,21 +1,13 @@ -import React, { Component } from "react"; - -import { Button, Form, FormControl, InputGroup, Modal } from "react-bootstrap"; - -import BaseComponent from "../base"; +import React, { useMemo, useRef, useState } from "react"; +import { Button, Form, InputGroup, Modal } from "react-bootstrap"; import { error } from "../feedback"; - import { BeneficiaryRoute } from "../../api/operations"; - import { getAccount } from "../../api/hive"; - import { _t } from "../../i18n"; - import { accountMultipleSvg, deleteForeverSvg, plusSvg } from "../../img/svg"; import { handleInvalid, handleOnInput } from "../../util/input-util"; import "./_index.scss"; - -const THREE_SPEAK_VIDEO_PATTERN = /\[!\[]\(https:\/\/ipfs-3speak.*\)\]\(https:\/\/3speak\.tv.*\)/g; +import { useThreeSpeakManager } from "../../pages/submit/hooks"; interface Props { body: string; @@ -25,225 +17,173 @@ interface Props { onDelete: (username: string) => void; } -interface DialogBodyState { - username: string; - percentage: string; - inProgress: boolean; -} - -export class DialogBody extends BaseComponent { - state: DialogBodyState = { - username: "", - percentage: "", - inProgress: false - }; - - form = React.createRef(); - - usernameChanged = (e: React.ChangeEvent): void => { - const username = e.target.value.trim().toLowerCase(); - this.stateSet({ username }); - }; - - percentageChanged = (e: React.ChangeEvent): void => { - this.stateSet({ percentage: e.target.value }); - }; - - render() { - const { list, author } = this.props; - const { username, percentage, inProgress } = this.state; - - const used = list.reduce((a, b) => a + b.weight / 100, 0); - const available = 100 - used; - - return ( -
{ - e.preventDefault(); - e.stopPropagation(); - - if (!this.form.current?.checkValidity()) { - return; - } - - const { onAdd, list } = this.props; - const { username, percentage } = this.state; - - if (list.find((x) => x.account === username) !== undefined) { - error(_t("beneficiary-editor.user-exists-error", { n: username })); - return; - } - - this.stateSet({ inProgress: true }); - getAccount(username) - .then((r) => { - if (!r) { - error(_t("beneficiary-editor.user-error", { n: username })); - return; - } - - onAdd({ - account: username, - weight: Number(percentage) * 100 - }); - - this.stateSet({ username: "", percentage: "" }); - }) - .finally(() => this.stateSet({ inProgress: false })); - }} - > -
- - - - - - - - - {author && available > 0 && ( - - - - - )} - - - - - - {list.map((x) => { - return ( - - - - + + {list.map((x) => { + return ( + + + + + + ); + })} + +
{_t("beneficiary-editor.username")}{_t("beneficiary-editor.reward")} -
{`@${author}`}{`${available}%`} -
- - - @ - - - handleInvalid(e, "beneficiary-editor.", "validation-username") - } - onInput={handleOnInput} - onChange={this.usernameChanged} - /> - - - - - handleInvalid(e, "beneficiary-editor.", "validation-percentage") - } - onInput={handleOnInput} - /> - - % - - - - -
{`@${x.account}`}{`${x.weight / 100}%`} - {!!this.props.body.match(THREE_SPEAK_VIDEO_PATTERN) && - x.src === "ENCODER_PAY" ? ( - <> - ) : ( - + + {visible && ( + setVisible(!visible)} + show={true} + centered={true} + animation={false} + className="beneficiary-editor-dialog" + > + + {_t("beneficiary-editor.title")} + + + { + e.preventDefault(); + e.stopPropagation(); + + if (!formRef.current?.checkValidity()) { + return; + } + + if (list.find((x) => x.account === username) !== undefined) { + error(_t("beneficiary-editor.user-exists-error", { n: username })); + return; + } + + setInProgress(true); + getAccount(username) + .then((r) => { + if (!r) { + error(_t("beneficiary-editor.user-error", { n: username })); + return; + } + + onAdd({ + account: username, + weight: Number(percentage) * 100 + }); + + setUsername(""); + setPercentage(""); + }) + .finally(() => setInProgress(false)); + }} + > +
+ + + + + + + + + {author && 100 - used > 0 && ( + + + + + )} + + + + - - ); - })} - -
{_t("beneficiary-editor.username")}{_t("beneficiary-editor.reward")} +
{`@${author}`}{`${100 - used}%`} +
+ + + @ + + + handleInvalid(e, "beneficiary-editor.", "validation-username") + } + onInput={handleOnInput} + onChange={(e) => setUsername(e.target.value.trim().toLowerCase())} + /> + + + + setPercentage(e.target.value)} + onInvalid={(e: any) => + handleInvalid(e, "beneficiary-editor.", "validation-percentage") + } + onInput={handleOnInput} + /> + + % + + + + - )} -
-
- - ); - } -} - -interface State { - visible: boolean; -} - -export default class BeneficiaryEditorDialog extends Component { - state: State = { - visible: false - }; - - toggle = () => { - const { visible } = this.state; - this.setState({ visible: !visible }); - }; - - render() { - const { list } = this.props; - const { visible } = this.state; - - const btnLabel = - list.length > 0 - ? _t("beneficiary-editor.btn-label-n", { n: list.length }) - : _t("beneficiary-editor.btn-label"); - - return ( - <> - - - {visible && ( - - - {_t("beneficiary-editor.title")} - - - - - - - - - )} - - ); - } +
{`@${x.account}`}{`${x.weight / 100}%`} + {Object.values(videos).length > 0 && x.src === "ENCODER_PAY" ? ( + <> + ) : ( + + )} +
+
+ + + + + + + )} + + ); } diff --git a/src/common/components/emoji-picker/index.tsx b/src/common/components/emoji-picker/index.tsx index 370196f7496..546d0e83cc7 100644 --- a/src/common/components/emoji-picker/index.tsx +++ b/src/common/components/emoji-picker/index.tsx @@ -1,22 +1,11 @@ -import React, { useEffect, useRef, useState } from "react"; -import { useQuery } from "@tanstack/react-query"; -import { QueryIdentifiers } from "../../core"; -import { Picker } from "emoji-mart"; +import React, { useEffect, useMemo, useRef, useState } from "react"; import { createPortal } from "react-dom"; -import useClickAway from "react-use/lib/useClickAway"; import useMountedState from "react-use/lib/useMountedState"; import "./_index.scss"; +import { v4 } from "uuid"; +import Picker from "@emoji-mart/react"; import { useMappedStore } from "../../store/use-mapped-store"; - -export const DEFAULT_EMOJI_DATA = { - categories: [], - emojis: {}, - aliases: {}, - sheet: { - cols: 0, - rows: 0 - } -}; +import useClickAway from "react-use/lib/useClickAway"; interface Props { anchor: Element | null; @@ -37,46 +26,15 @@ export function EmojiPicker({ anchor, onSelect }: Props) { const [show, setShow] = useState(false); const [position, setPosition] = useState({ x: 0, y: 0 }); - const [pickerInstance, setPickerInstance] = useState(); + // Due to ability to hold multiple dialogs we have to identify them + const dialogId = useMemo(() => v4(), []); useClickAway(ref, () => { setShow(false); }); - const { data } = useQuery( - [QueryIdentifiers.EMOJI_PICKER], - async () => { - try { - const data = await import(/* webpackChunkName: "emojis" */ "@emoji-mart/data"); - return data.default as typeof DEFAULT_EMOJI_DATA; - } catch (e) { - console.error("Failed to load emoji data"); - } - - return DEFAULT_EMOJI_DATA; - }, - { - initialData: DEFAULT_EMOJI_DATA - } - ); - const isMounted = useMountedState(); - useEffect(() => { - if (data.categories.length > 0) { - setPickerInstance( - new Picker({ - dynamicWidth: true, - onEmojiSelect: (e: { native: string }) => onSelect(e.native), - previewPosition: "none", - ref, - set: "apple", - theme: global.theme === "day" ? "light" : "dark" - }) - ); - } - }, [data, global.theme]); - useEffect(() => { if (anchor) { anchor.addEventListener("click", () => { @@ -90,14 +48,24 @@ export function EmojiPicker({ anchor, onSelect }: Props) { return isMounted() ? ( createPortal(
, + > + onSelect(e.native)} + previewPosition="none" + set="apple" + theme={global.theme === "day" ? "light" : "dark"} + /> +
, document.querySelector("#root")!! ) ) : ( diff --git a/src/common/components/video-gallery/index.scss b/src/common/components/video-gallery/index.scss index 7c2fc534d80..75b16e6b18f 100644 --- a/src/common/components/video-gallery/index.scss +++ b/src/common/components/video-gallery/index.scss @@ -37,12 +37,44 @@ } .list-details-wrapper { - display: flex; - flex-direction: column; + display: grid; + grid-template-columns: 1fr min-content min-content; + gap: 0.5rem; + grid-template-areas: + "status info copy" + "title title title" + "actions actions actions"; width: 100%; padding: 0.75rem; - .details-actions { + &-title { + grid-area: title; + } + + &-info, &-copy { + grid-area: info; + opacity: 0.5; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + + &:hover { + opacity: 0.8; + } + + svg { + width: 1rem; + height: 1rem; + } + } + + &-copy { + grid-area: copy; + } + + &-actions { + grid-area: actions; display: flex; align-items: center; justify-content: flex-end; @@ -50,80 +82,54 @@ margin-top: 1.5rem; } - .list-title { - .info-status { - display: flex; - align-items: center; - justify-content: space-between; - margin-bottom: 0.5rem; + &-status { + display: flex; + align-items: center; + gap: 0.5rem; + font-size: 0.75rem; + color: $gray-600; + + .status-icon-wrapper { + + .status-icon-encoded { + width: 0.5rem; + height: 0.5rem; + border-radius: 50%; + cursor: pointer; + background-color: $green; + } + + .status-icon-published { + width: 0.5rem; + height: 0.5rem; + border-radius: 50%; + cursor: pointer; + background-color: $dark-sky-blue; + } - .status { - display: flex; - align-items: center; - gap: 0.5rem; - font-size: 0.75rem; - color: $gray-600; + .status-icon-failed { + width: 0.5rem; + height: 0.5rem; + border-radius: 50%; + cursor: pointer; + background-color: rgb(199, 94, 94); } - .status-icon-wrapper { - - .status-icon-encoded { - width: 0.5rem; - height: 0.5rem; - border-radius: 50%; - cursor: pointer; - background-color: $green; - } - - .status-icon-published { - width: 0.5rem; - height: 0.5rem; - border-radius: 50%; - cursor: pointer; - background-color: $dark-sky-blue; - } - - .status-icon-failed { - width: 0.5rem; - height: 0.5rem; - border-radius: 50%; - cursor: pointer; - background-color: rgb(199, 94, 94); - } - - .status-icon-encoding { - width: 0.5rem; - height: 0.5rem; - border-radius: 50%; - cursor: pointer; - background-color: rgb(228, 144, 42); - } - - .status-icon-deleted { - width: 0.5rem; - height: 0.5rem; - border-radius: 50%; - cursor: pointer; - background-color: $gray-400; - } + .status-icon-encoding { + width: 0.5rem; + height: 0.5rem; + border-radius: 50%; + cursor: pointer; + background-color: rgb(228, 144, 42); } - .info-icon-wrapper { - opacity: 0.5; + .status-icon-deleted { + width: 0.5rem; + height: 0.5rem; + border-radius: 50%; cursor: pointer; - display: flex; - align-items: center; - justify-items: center; - - &:hover { - opacity: 0.8; - } - - svg { - width: 1rem; - height: 1rem; - } + background-color: $gray-400; } } } @@ -157,12 +163,14 @@ .custom-dropdown .dropdown-btn .label { font-size: 0.875rem; } + .hint { border-radius: 1.15rem; line-height: 1.25; padding: 0.4rem .575rem; margin-left: auto; } + .refresh-gallery svg { width: 1.25rem; height: 1.25rem; diff --git a/src/common/components/video-gallery/index.tsx b/src/common/components/video-gallery/index.tsx index d857eaec775..a777ad9d5fd 100644 --- a/src/common/components/video-gallery/index.tsx +++ b/src/common/components/video-gallery/index.tsx @@ -16,7 +16,6 @@ interface Props { insertText: (before: string, after?: string) => any; setVideoEncoderBeneficiary?: (video: any) => void; toggleNsfwC?: () => void; - preFilter?: string; setVideoMetadata?: (v: ThreeSpeakVideo) => void; } @@ -26,7 +25,6 @@ const VideoGallery = ({ insertText, setVideoEncoderBeneficiary, toggleNsfwC, - preFilter, setVideoMetadata }: Props) => { const { activeUser } = useMappedStore(); @@ -34,19 +32,17 @@ const VideoGallery = ({ const [label, setLabel] = useState("All"); const [filterStatus, setFilterStatus] = useState( - preFilter ?? "all" + isEditing ? "published" : "all" ); const { data: items, isFetching, refresh } = useThreeSpeakVideo(filterStatus, showGallery); useEffect(() => { - if (isEditing) { - setFilterStatus("published"); - } + setFilterStatus(isEditing ? "published" : "all"); }, [isEditing]); useEffect(() => { - setFilterStatus(preFilter ?? "all"); + setFilterStatus(isEditing ? "published" : "all"); }, [activeUser]); return ( @@ -63,7 +59,7 @@ const VideoGallery = ({
- {!preFilter && !isEditing ? ( + {!isEditing ? ( { - setFilterStatus(isEditing ? filterStatus : preFilter ?? "all"); + setFilterStatus(isEditing ? filterStatus : "all"); setLabel(_t("video-gallery.all")); refresh(); }} diff --git a/src/common/components/video-gallery/video-gallery-item.tsx b/src/common/components/video-gallery/video-gallery-item.tsx index c4d8324689f..3936579cec6 100644 --- a/src/common/components/video-gallery/video-gallery-item.tsx +++ b/src/common/components/video-gallery/video-gallery-item.tsx @@ -1,4 +1,4 @@ -import { informationSvg } from "../../img/svg"; +import { copyOutlinSvg, informationSvg } from "../../img/svg"; import { _t } from "../../i18n"; import { dateToFullRelative } from "../../helper/parse-date"; import React, { useEffect, useState } from "react"; @@ -6,6 +6,7 @@ import { ThreeSpeakVideo, useThreeSpeakVideo } from "../../api/threespeak"; import { Button } from "react-bootstrap"; import { proxifyImageSrc } from "@ecency/render-helper"; import { useMappedStore } from "../../store/use-mapped-store"; +import useCopyToClipboard from "react-use/lib/useCopyToClipboard"; interface videoProps { status: string; @@ -34,6 +35,7 @@ export function VideoGalleryItem({ }: Props) { const { global } = useMappedStore(); const { data } = useThreeSpeakVideo("all"); + const [_, copyToClipboard] = useCopyToClipboard(); const [showMoreInfo, setShowMoreInfo] = useState(false); const [hoveredItem, setHoveredItem] = useState(null); @@ -52,32 +54,10 @@ export function VideoGalleryItem({ setHoveredItem(item); }; - const embeddVideo = (video: videoProps) => { - const speakFile = `[![](${video.thumbUrl})](${speakUrl}${video.owner}/${video.permlink})`; - - const element = `
${speakFile}[Source](${video.filename.replace( - "ipfs://", - "https://ipfs-3speak.b-cdn.net/ipfs/" - )})
`; - const body = insertText("").innerHTML; - const hup = manualPublishSpeakVideos - .map((i) => `[![](${i.thumbUrl})](${speakUrl}${i.owner}/${i.permlink})`) - .some((i) => body.includes(i)); - - if (!hup || video.status == "published") { - setVideoMetadata?.( - manualPublishSpeakVideos.find( - (v) => v.permlink === video.permlink && v.owner === video.owner - )!! - ); - insertText(element); - } - }; - const insert = async (isNsfw = false) => { let nextItem = item; - embeddVideo(nextItem); + setVideoMetadata?.(nextItem); const body = insertText("").innerHTML; const hup = manualPublishSpeakVideos .map((i) => `[![](${i.thumbUrl})](${speakUrl}${i.owner}/${i.permlink})`) @@ -134,40 +114,51 @@ export function VideoGalleryItem({ />
-
-
-
- {statusIcons(item.status)} - {toolTipContent(item.status)}{" "} - {item.status == "encoding_ipfs" || item.status == "encoding_preparing" - ? `${item.encodingProgress.toFixed(2)}%` - : ""} -
-
{ - getHoveredItem(item); - setShowMoreInfo(true); - }} - onMouseOut={() => setShowMoreInfo(false)} - className="info-icon-wrapper" - > - {informationSvg} -
-
-
{item.title}
- {["publish_manual", "published"].includes(item.status) && ( -
- - {item.status != "published" && ( - - )} -
- )} +
+ {statusIcons(item.status)} + {toolTipContent(item.status)}{" "} + {item.status == "encoding_ipfs" || item.status == "encoding_preparing" + ? `${item.encodingProgress.toFixed(2)}%` + : ""} +
+ +
{ + getHoveredItem(item); + setShowMoreInfo(true); + }} + onMouseOut={() => setShowMoreInfo(false)} + className="list-details-wrapper-info" + > + {informationSvg}
+ + + +
{item.title}
+ + {["publish_manual", "published"].includes(item.status) && ( +
+ + {item.status != "published" && ( + + )} +
+ )}
{showMoreInfo && hoveredItem._id === item._id && (
diff --git a/src/common/core/react-query.ts b/src/common/core/react-query.ts index de5ac6e4422..f3db0fc44c1 100644 --- a/src/common/core/react-query.ts +++ b/src/common/core/react-query.ts @@ -25,7 +25,5 @@ export enum QueryIdentifiers { THREE_SPEAK_VIDEO_LIST = "three-speak-video-list", THREE_SPEAK_VIDEO_LIST_FILTERED = "three-speak-video-list-filtered", DRAFTS = "drafts", - BY_DRAFT_ID = "by-draft-id", - - EMOJI_PICKER = "emoji-picker" + BY_DRAFT_ID = "by-draft-id" } diff --git a/src/common/i18n/locales/en-US.json b/src/common/i18n/locales/en-US.json index e66eda91b0a..21aae09d039 100644 --- a/src/common/i18n/locales/en-US.json +++ b/src/common/i18n/locales/en-US.json @@ -73,7 +73,8 @@ "confirm": "Confirm", "username": "Username", "past-few-days": "the past few days", - "you": "You" + "you": "You", + "copy-clipboard": "Copy to clipboard" }, "confirm": { "title": "Are you sure?", @@ -1439,7 +1440,8 @@ "empty-title-alert": "Enter post title", "empty-tags-alert": "Enter post tags", "empty-body-alert": "Enter post body", - "description": "Short description" + "description": "Short description", + "should-be-only-one-unpublished": "Post should contain only one unpublished video. Please, remove other ones." }, "video-upload": { "choose-video": "Select a video", diff --git a/src/common/img/svg.tsx b/src/common/img/svg.tsx index e326d428ebb..75a73171e92 100644 --- a/src/common/img/svg.tsx +++ b/src/common/img/svg.tsx @@ -1907,7 +1907,7 @@ export const copyOutlinSvg = ( height="30" viewBox="0 0 24 24" fill="none" - stroke="#357ce6" + stroke="currentColor" strokeWidth="2" strokeLinecap="square" strokeLinejoin="round" diff --git a/src/common/pages/submit-old.tsx b/src/common/pages/submit-old.tsx deleted file mode 100644 index 03c262c2077..00000000000 --- a/src/common/pages/submit-old.tsx +++ /dev/null @@ -1,1581 +0,0 @@ -import React, { Component } from "react"; - -import { connect } from "react-redux"; - -import { match } from "react-router"; - -import queryString from "query-string"; - -import isEqual from "react-fast-compare"; - -import { History } from "history"; - -import { Button, Col, Form, FormControl, Row, Spinner } from "react-bootstrap"; - -import moment, { Moment } from "moment"; - -import defaults from "../constants/defaults.json"; -import { postBodySummary, proxifyImageSrc, setProxyBase } from "@ecency/render-helper"; -import { Entry } from "../store/entries/types"; -import { Global } from "../store/global/types"; -import { FullAccount } from "../store/accounts/types"; - -import BaseComponent from "../components/base"; -import Meta from "../components/meta"; -import Theme from "../components/theme"; -import Feedback, { error, success } from "../components/feedback"; -import NavBar from "../components/navbar"; -import NavBarElectron from "../../desktop/app/components/navbar"; -import FullHeight from "../components/full-height"; -import EditorToolbar, { detectEvent, toolbarEventListener } from "../components/editor-toolbar"; -import TagSelector from "../components/tag-selector"; -import CommunitySelector from "../components/community-selector"; -import Tag from "../components/tag"; -import LoginRequired from "../components/login-required"; -import WordCount from "../components/word-counter"; -import { makePath as makePathEntry } from "../components/entry-link"; -import MdHandler from "../components/md-handler"; -import BeneficiaryEditor from "../components/beneficiary-editor"; -import PostScheduler from "../components/post-scheduler"; -import ClickAwayListener from "../components/clickaway-listener"; -import "./submit.scss"; - -import { - addDraft, - addSchedule, - Draft, - DraftMetadata, - getDrafts, - updateDraft -} from "../api/private-api"; - -import { - createPatch, - createPermlink, - extractMetaData, - makeCommentOptions, - makeJsonMetaData -} from "../helper/posting"; - -import tempEntry, { correctIsoDate } from "../helper/temp-entry"; -import isCommunity from "../helper/is-community"; - -import { BeneficiaryRoute, comment, formatError, reblog, RewardType } from "../api/operations"; - -import * as bridgeApi from "../api/bridge"; -import * as hiveApi from "../api/hive"; - -import { _t } from "../i18n"; - -import _c from "../util/fix-class-names"; - -import * as ls from "../util/local-storage"; - -import { version } from "../../../package.json"; - -import { checkSvg, contentLoadSvg, contentSaveSvg, helpIconSvg } from "../img/svg"; - -import { pageMapDispatchToProps, pageMapStateToProps, PageProps } from "./common"; -import ModalConfirm from "../components/modal-confirm"; -import TextareaAutocomplete from "../components/textarea-autocomplete"; -import Drafts from "../components/drafts"; -import { AvailableCredits } from "../components/available-credits"; -import { handleFloatingContainer } from "../components/floating-faq"; - -import { markAsPublished, ThreeSpeakVideo, updateSpeakVideoInfo } from "../api/threespeak"; -// import { ConfirmNsfwContent } from "../components/video-nsfw"; -import { PostBodyLazyRenderer } from "../components/post-body-lazy-renderer"; - -setProxyBase(defaults.imageServer); - -interface PostBase { - title: string; - tags: string[]; - body: string; - description: string | null; -} - -interface Advanced { - reward: RewardType; - beneficiaries: BeneficiaryRoute[]; - schedule: string | null; - reblogSwitch: boolean; - description: string | null; - // Speak Advanced - isThreespeak: boolean; - videoId: string; - speakPermlink: string; - speakAuthor: string; - isNsfw: boolean; - videoMetadata: any; -} - -interface PreviewProps extends PostBase { - history: History; - global: Global; -} - -interface videoProps { - beneficiaries: string; - _id: string; - owner: string; - permlink: string; -} - -class PreviewContent extends Component { - shouldComponentUpdate(nextProps: Readonly): boolean { - return ( - !isEqual(this.props.title, nextProps.title) || - !isEqual(this.props.tags, nextProps.tags) || - !isEqual(this.props.body, nextProps.body) - ); - } - - render() { - const { title, tags, body, global } = this.props; - return ( - <> -
{title}
- -
- {tags.map((x) => { - return ( - - {Tag({ - ...this.props, - tag: x, - children: {x}, - type: "span" - })} - - ); - })} -
- - - - ); - } -} - -interface MatchParams { - permlink?: string; - username?: string; - draftId?: string; -} - -interface Props extends PageProps { - match: match; -} - -interface State extends PostBase, Advanced { - preview: PostBase; - posting: boolean; - description: string | null; - editingEntry: Entry | null; - saving: boolean; - editingDraft: Draft | null; - advanced: boolean; - clearModal: boolean; - disabled: boolean; - thumbnails: string[]; - selectedThumbnail: string; - selectionTouched: boolean; - isDraftEmpty: boolean; - drafts: boolean; - showHelp: boolean; - videoMetadata: ThreeSpeakVideo | null; -} - -class SubmitPage extends BaseComponent { - postBodyRef: React.RefObject; - constructor(props: Props) { - super(props); - this.postBodyRef = React.createRef(); - } - state: State = { - title: "", - tags: [], - body: "", - description: null, - reward: "default", - posting: false, - editingEntry: null, - saving: false, - editingDraft: null, - selectionTouched: false, - advanced: false, - beneficiaries: [], - thumbnails: [], - selectedThumbnail: "", - schedule: null, - reblogSwitch: false, - clearModal: false, - preview: { - title: "", - tags: [], - body: "", - description: "" - }, - disabled: true, - isDraftEmpty: true, - drafts: false, - showHelp: false, - // Speakstates - isThreespeak: false, - videoId: "", - speakPermlink: "", - speakAuthor: "", - isNsfw: false, - videoMetadata: null - }; - - _updateTimer: any = null; - - componentDidMount = (): void => { - this.loadLocalDraft(); - - this.loadAdvanced(); - - this.detectCommunity(); - - this.detectEntry().then(); - - this.detectDraft().then(); - - let selectedThumbnail = ls.get("draft_selected_image"); - if (selectedThumbnail?.length > 0) { - this.selectThumbnails(selectedThumbnail); - } - - this.addToolbarEventListners(); - if (typeof window !== "undefined") { - window.addEventListener("resize", this.handleResize); - } - }; - - componentDidUpdate(prevProps: Readonly) { - const { activeUser, location } = this.props; - - // active user changed - if (activeUser?.username !== prevProps.activeUser?.username) { - // delete active user from beneficiaries list - if (activeUser) { - const { beneficiaries } = this.state; - if (beneficiaries.find((x: { account: string }) => x.account === activeUser.username)) { - const b = [ - ...beneficiaries.filter((x: { account: string }) => x.account !== activeUser.username) - ]; - this.stateSet({ beneficiaries: b }); - } - } - } - - // location change. only occurs once a draft picked on drafts dialog - if (location.pathname !== prevProps.location.pathname) { - this.detectDraft().then(); - } - } - - componentWillUnmount(): void { - this.removeToolbarEventListners(); - if (typeof window !== "undefined") { - window.removeEventListener("resize", this.handleResize); - } - } - - addToolbarEventListners = () => { - if (this.postBodyRef) { - const el = this.postBodyRef?.current; - - if (el) { - el.addEventListener("paste", this.handlePaste); - el.addEventListener("dragover", this.handleDragover); - el.addEventListener("drop", this.handleDrop); - } - } - }; - - removeToolbarEventListners = () => { - if (this.postBodyRef) { - const el = this.postBodyRef?.current; - - if (el) { - el.removeEventListener("paste", this.handlePaste); - el.removeEventListener("dragover", this.handleDragover); - el.removeEventListener("drop", this.handleDrop); - } - } - }; - - handleResize = () => { - if (typeof window !== "undefined" && window.innerWidth < 992) { - this.setState({ showHelp: false }); - handleFloatingContainer(false, "submit"); - } - }; - - handlePaste = (event: Event): void => { - toolbarEventListener(event, "paste"); - }; - - handleDragover = (event: Event): void => { - toolbarEventListener(event, "dragover"); - }; - - handleDrop = (event: Event): void => { - toolbarEventListener(event, "drop"); - }; - - handleValidForm = (value: boolean) => { - this.setState({ disabled: value }); - }; - - isEntry = (): boolean => { - const { match, activeUser } = this.props; - const { path, params } = match; - - return !!(activeUser && path.endsWith("/edit") && params.username && params.permlink); - }; - - isDraft = (): boolean => { - const { match, activeUser } = this.props; - const { path, params } = match; - - return !!(activeUser && path.startsWith("/draft") && params.draftId); - }; - - detectEntry = async () => { - const { match, history } = this.props; - const { params } = match; - - if (this.isEntry()) { - let entry; - try { - entry = await bridgeApi.normalizePost( - await hiveApi.getPost(params.username!.replace("@", ""), params.permlink!) - ); - } catch (e) { - error(...formatError(e)); - return; - } - - if (!entry) { - error("Could not fetch post data."); - history.push("/submit"); - return; - } - - const { title, body } = entry; - - let description = entry.json_metadata?.description || postBodySummary(body, 200); - let tags = entry.json_metadata?.tags || []; - tags = [...new Set(tags)]; - - this.stateSet({ title, tags, body, description, editingEntry: entry }, this.updatePreview); - } else { - if (this.state.editingEntry) { - this.stateSet({ editingEntry: null }); - } - } - }; - - detectDraft = async () => { - const { match, activeUser, history } = this.props; - const { params } = match; - - if (this.isDraft()) { - let drafts: Draft[]; - - try { - drafts = await getDrafts(activeUser?.username!); - } catch (err) { - drafts = []; - } - - drafts = drafts.filter((x) => x._id === params.draftId); - if (drafts?.length === 1) { - const [draft] = drafts; - const { title, body } = draft; - - let tags: string[]; - - try { - tags = draft.tags.trim() ? draft.tags.split(/[ ,]+/) : []; - } catch (e) { - tags = []; - } - - const image = draft.meta?.image && draft.meta?.image.length > 0 ? draft.meta.image[0] : ""; - this.stateSet( - { - title, - tags, - body, - editingDraft: draft, - beneficiaries: draft.meta?.beneficiaries || [], - reward: draft.meta?.rewardType || "default", - selectedThumbnail: image, - description: draft.meta?.description || "" - }, - this.updatePreview - ); - } else { - error("Could not fetch draft data."); - history.push("/submit"); - return; - } - } else { - if (this.state.editingDraft) { - this.stateSet({ editingDraft: null }); - } - } - }; - - detectCommunity = () => { - const { location } = this.props; - const qs = queryString.parse(location.search); - if (qs.com) { - const com = qs.com as string; - - this.stateSet({ tags: [com] }); - } - }; - - loadLocalDraft = (): void => { - if (this.isEntry() || this.isDraft()) { - return; - } - - const localDraft = ls.get("local_draft") as PostBase; - if (!localDraft || JSON.stringify(localDraft) === "{}") { - this.stateSet({ isDraftEmpty: true }); - return; - } - - const { title, tags, body } = localDraft; - this.stateSet({ title, tags, body }, this.updatePreview); - - for (const key in localDraft) { - if (localDraft && localDraft[key] && localDraft[key].length > 0) { - this.stateSet({ isDraftEmpty: false }); - } - } - }; - - saveLocalDraft = (): void => { - const { title, tags, body, description } = this.state; - const localDraft: PostBase = { title, tags, body, description }; - ls.set("local_draft", localDraft); - }; - - loadAdvanced = (): void => { - const advanced = ls.get("local_advanced") as Advanced; - if (!advanced) { - return; - } - - this.stateSet({ ...advanced }); - }; - - saveAdvanced = (): void => { - const { - reward, - beneficiaries, - schedule, - reblogSwitch, - description, - isThreespeak, - videoId, - speakPermlink, - speakAuthor, - isNsfw, - videoMetadata - } = this.state; - - const advanced: Advanced = { - reward, - beneficiaries, - schedule, - reblogSwitch, - description, - // Speak Advanced - isThreespeak, - videoId, - speakPermlink, - speakAuthor, - isNsfw, - videoMetadata - }; - - ls.set("local_advanced", advanced); - }; - - hasAdvanced = (): boolean => { - const { reward, beneficiaries, schedule, reblogSwitch, description } = this.state; - - return ( - reward !== "default" || - beneficiaries?.length > 0 || - schedule !== null || - reblogSwitch || - description !== "" - ); - }; - - titleChanged = (e: React.ChangeEvent): void => { - const { value: title } = e.target; - this.stateSet({ title }, () => { - this.updatePreview(); - }); - }; - - tagsChanged = (tags: string[]): void => { - if (isEqual(this.state.tags, tags)) { - // tag selector calls onchange event 2 times on each change. - // one for add event one for sort event. - // important to check if tags really changed. - return; - } - - this.stateSet({ tags }, () => { - this.updatePreview(); - }); - - // Toggle off reblog switch if it is true and the first tag is not community tag. - const { reblogSwitch } = this.state; - if (reblogSwitch) { - const isCommunityTag = tags?.length > 0 && isCommunity(tags[0]); - - if (!isCommunityTag) { - this.stateSet({ reblogSwitch: false }, this.saveAdvanced); - } - } - }; - - bodyChanged = (e: React.ChangeEvent): void => { - const { value: body } = e.target; - this.stateSet({ body }, () => { - this.updatePreview(); - }); - }; - - descriptionChanged = (e: React.ChangeEvent): void => { - const { value: description } = e.target; - this.stateSet({ description }, this.saveAdvanced); - }; - - rewardChanged = (e: React.ChangeEvent): void => { - const reward = e.target.value as RewardType; - this.stateSet({ reward }, this.saveAdvanced); - }; - - beneficiaryAdded = (item: BeneficiaryRoute) => { - const { beneficiaries } = this.state; - const b = [...beneficiaries, item].sort((a, b) => (a.account < b.account ? -1 : 1)); - this.stateSet({ beneficiaries: b }, this.saveAdvanced); - }; - - beneficiaryDeleted = (username: string) => { - const { beneficiaries } = this.state; - const b = [...beneficiaries.filter((x: { account: string }) => x.account !== username)]; - this.stateSet({ beneficiaries: b }, this.saveAdvanced); - }; - - scheduleChanged = (d: Moment | null) => { - this.stateSet({ schedule: d ? d.toISOString(true) : null }, this.saveAdvanced); - }; - - reblogSwitchChanged = (e: React.ChangeEvent): void => { - this.stateSet({ reblogSwitch: e.target.checked }, this.saveAdvanced); - }; - - clear = (): void => { - this.stateSet( - { - title: "", - tags: [], - body: "", - advanced: false, - reward: "default", - beneficiaries: [], - schedule: null, - reblogSwitch: false, - clearModal: false, - isDraftEmpty: true - }, - () => { - this.clearAdvanced(); - this.updatePreview(); - ls.remove("draft_selected_image"); - } - ); - - const { editingDraft } = this.state; - if (editingDraft) { - const { history } = this.props; - history.push("/submit"); - } - }; - - clearAdvanced = (): void => { - this.stateSet( - { - advanced: false, - reward: "default", - beneficiaries: [], - schedule: null, - reblogSwitch: false, - description: "", - // Speak Advanced - isThreespeak: false, - videoId: "", - speakPermlink: "", - speakAuthor: "", - isNsfw: false, - videoMetadata: null - }, - () => { - this.saveAdvanced(); - } - ); - }; - - toggleAdvanced = (): void => { - const { advanced } = this.state; - this.stateSet({ advanced: !advanced }); - }; - - updatePreview = (): void => { - if (this._updateTimer) { - clearTimeout(this._updateTimer); - this._updateTimer = null; - } - - // Not sure why we are using setTimeOut(), but it causes some odd behavior and sets input value to preview.body when you try to delete/cancel text - this._updateTimer = setTimeout(() => { - const { title, tags, body, editingEntry, description } = this.state; - const { thumbnails } = extractMetaData(body); - this.stateSet({ preview: { title, tags, body, description }, thumbnails: thumbnails || [] }); - if (editingEntry === null) { - this.saveLocalDraft(); - } - if (title?.length || tags?.length || body?.length) { - this.stateSet({ isDraftEmpty: false }); - } else { - this.stateSet({ isDraftEmpty: true }); - } - }, 50); - }; - - focusInput = (parentSelector: string): void => { - const el = document.querySelector(`${parentSelector} .form-control`) as HTMLInputElement; - if (el) { - el.focus(); - } - }; - - validate = (): boolean => { - const { title, tags, body } = this.state; - - if (title.trim() === "") { - this.focusInput(".title-input"); - error(_t("submit.empty-title-alert")); - return false; - } - - if (tags?.length === 0) { - this.focusInput(".tag-input"); - error(_t("submit.empty-tags-alert")); - return false; - } - - if (body.trim() === "") { - this.focusInput(".body-input"); - error(_t("submit.empty-body-alert")); - return false; - } - - return true; - }; - - markVideo = async (videoId: string) => { - const { activeUser } = this.props; - await markAsPublished(activeUser!.username, videoId); - }; - - publish = async (): Promise => { - if (!this.validate()) { - return; - } - - const { activeUser, history, addEntry } = this.props; - const { - title, - tags, - body, - description, - reward, - reblogSwitch, - beneficiaries, - videoId, - isThreespeak, - speakPermlink, - speakAuthor, - isNsfw - } = this.state; - - // clean body - const cbody = body.replace(/[\x00-\x09\x0B-\x0C\x0E-\x1F\x7F-\x9F]/g, ""); - - // make sure active user fully loaded - if (!activeUser || !activeUser.data.__loaded) { - return; - } - - this.stateSet({ posting: true }); - - let author = activeUser.username; - let authorData = activeUser.data as FullAccount; - - let permlink = createPermlink(title); - - // permlink duplication check - let c; - try { - c = await bridgeApi.getPostHeader(author, permlink); - } catch (e) { - /*error(_t("g.server-error")); - this.stateSet({posting: false}); - return;*/ - } - - if (c && c.author && !isThreespeak && speakPermlink === "") { - // create permlink with random suffix - permlink = createPermlink(title, true); - } - - const [parentPermlink] = tags; - let jsonMeta = this.buildMetadata(); - if (jsonMeta && jsonMeta.image && jsonMeta.image.length > 0) { - jsonMeta.image_ratios = await Promise.all( - jsonMeta.image - .slice(0, 5) - .map((element: string) => this.getHeightAndWidthFromDataUrl(proxifyImageSrc(element))) - ); - } - - if (isThreespeak && speakPermlink !== "") { - permlink = speakPermlink; - // update speak video with title, body and tags - updateSpeakVideoInfo(activeUser.username, body, videoId, title, tags, isNsfw); - } - - const options = makeCommentOptions(author, permlink, reward, beneficiaries); - this.stateSet({ posting: true }); - comment(author, "", parentPermlink, permlink, title, cbody, jsonMeta, options, true) - .then(() => { - this.clearAdvanced(); - - // Create entry object in store - const entry = { - ...tempEntry({ - author: authorData!, - permlink, - parentAuthor: "", - parentPermlink, - title, - body, - tags, - description - }), - max_accepted_payout: options.max_accepted_payout, - percent_hbd: options.percent_hbd - }; - addEntry(entry); - - success(_t("submit.published")); - this.clear(); - const newLoc = makePathEntry(parentPermlink, author, permlink); - history.push(newLoc); - - //Mark speak video as published - if (isThreespeak && activeUser.username === speakAuthor) { - success(_t("vidoe-upload.publishing")); - setTimeout(() => { - this.markVideo(videoId); - }, 10000); - } - }) - .then(() => { - if (isCommunity(tags[0]) && reblogSwitch) { - reblog(author, author, permlink); - } - }) - .catch((e) => { - error(...formatError(e)); - }) - .finally(() => { - this.stateSet({ posting: false }); - }); - }; - - update = async (): Promise => { - if (!this.validate()) { - return; - } - - const { activeUser, updateEntry, history } = this.props; - const { title, tags, body, description, editingEntry } = this.state; - if (!editingEntry) { - return; - } - - const { body: oldBody, author, permlink, category, json_metadata } = editingEntry; - // clean and copy body - let newBody = body.replace(/[\x00-\x09\x0B-\x0C\x0E-\x1F\x7F-\x9F]/g, ""); - const patch = createPatch(oldBody, newBody.trim()); - if (patch && patch.length < Buffer.from(editingEntry.body, "utf-8").length) { - newBody = patch; - } - - let jsonMeta = Object.assign( - {}, - json_metadata, - this.buildMetadata(), - { tags }, - { description } - ); - - if (jsonMeta && jsonMeta.image && jsonMeta.image.length > 0) { - jsonMeta.image_ratios = await Promise.all( - jsonMeta.image - .slice(0, 5) - .map((element: string) => this.getHeightAndWidthFromDataUrl(proxifyImageSrc(element))) - ); - } - - this.stateSet({ posting: true }); - - comment(activeUser?.username!, "", category, permlink, title, newBody, jsonMeta, null) - .then(() => { - this.stateSet({ posting: false }); - - // Update the entry object in store - const entry: Entry = { - ...editingEntry, - title, - body, - category: tags[0], - json_metadata: jsonMeta, - updated: correctIsoDate(moment().toISOString()) - }; - updateEntry(entry); - - success(_t("submit.updated")); - const newLoc = makePathEntry(category, author, permlink); - history.push(newLoc); - }) - .catch((e) => { - this.stateSet({ posting: false }); - error(...formatError(e)); - }); - }; - - cancelUpdate = () => { - const { history } = this.props; - const { editingEntry } = this.state; - if (!editingEntry) { - return; - } - - const newLoc = makePathEntry( - editingEntry?.category!, - editingEntry.author, - editingEntry.permlink - ); - history.push(newLoc); - }; - - saveDraft = () => { - if (!this.validate()) { - return; - } - - const { activeUser, history } = this.props; - const { title, body, tags, editingDraft, beneficiaries, reward, schedule } = this.state; - const tagJ = tags.join(" "); - - const meta = this.buildMetadata(); - const draftMeta: DraftMetadata = { - ...meta, - beneficiaries, - rewardType: reward - }; - - let promise: Promise; - - this.stateSet({ saving: true }); - - if (editingDraft) { - promise = updateDraft( - activeUser?.username!, - editingDraft._id, - title, - body, - tagJ, - draftMeta - ).then(() => { - success(_t("submit.draft-updated")); - }); - } else { - promise = addDraft(activeUser?.username!, title, body, tagJ, draftMeta).then((resp) => { - success(_t("submit.draft-saved")); - - const { drafts } = resp; - const draft = drafts[drafts?.length - 1]; - - history.push(`/draft/${draft._id}`); - }); - } - - promise - .catch(() => error(_t("g.server-error"))) - .finally(() => this.stateSet({ saving: false })); - }; - - schedule = async () => { - if (!this.validate()) { - return; - } - - const { activeUser } = this.props; - const { title, tags, body, reward, reblogSwitch, beneficiaries, schedule, description } = - this.state; - - // make sure active user and schedule date has set - if (!activeUser || !schedule) { - return; - } - - this.stateSet({ posting: true }); - - let author = activeUser.username; - - let permlink = createPermlink(title); - - // permlink duplication check - let c; - try { - c = await bridgeApi.getPostHeader(author, permlink); - } catch (e) {} - - if (c && c.author) { - // create permlink with random suffix - permlink = createPermlink(title, true); - } - - const meta = extractMetaData(body); - const jsonMeta = makeJsonMetaData(meta, tags, description, version); - const options = makeCommentOptions(author, permlink, reward, beneficiaries); - - const reblog = isCommunity(tags[0]) && reblogSwitch; - - this.stateSet({ posting: true }); - addSchedule(author, permlink, title, body, jsonMeta, options, schedule, reblog) - .then((resp) => { - success(_t("submit.scheduled")); - this.clear(); - }) - .catch((e) => { - if (e.response?.data?.message) { - error(e.response?.data?.message); - } else { - error(_t("g.server-error")); - } - }) - .finally(() => this.stateSet({ posting: false })); - }; - - selectThumbnails = (selectedThumbnail: string) => { - this.setState({ selectedThumbnail }); - ls.set("draft_selected_image", selectedThumbnail); - }; - - buildMetadata = () => { - const { tags, title, body, description, selectedThumbnail, selectionTouched, videoMetadata } = - this.state; - const { thumbnails, ...meta } = extractMetaData(body); - let localThumbnail = ls.get("draft_selected_image"); - - if (meta.image) { - if (selectionTouched) { - meta.image = [selectedThumbnail, ...meta.image!.splice(0, 9)]; - } else { - meta.image = [...meta.image!.splice(0, 9)]; - } - } else if (selectedThumbnail === localThumbnail) { - ls.remove("draft_selected_image"); - } else { - meta.image = selectedThumbnail ? [selectedThumbnail] : []; - } - if (meta.image) { - meta.image = [...new Set(meta.image)]; - } - if (videoMetadata) { - meta.video = { - info: { - platform: "3speak", - title: title || videoMetadata.title, - author: videoMetadata.owner, - permlink: videoMetadata.permlink, - duration: videoMetadata.duration, - filesize: videoMetadata.size, - file: videoMetadata.filename, - lang: videoMetadata.language, - firstUpload: videoMetadata.firstUpload, - ipfs: null, - ipfsThumbnail: null, - video_v2: videoMetadata.video_v2, - sourceMap: [ - { - type: "video", - url: videoMetadata.video_v2, - format: "m3u8" - }, - { - type: "thumbnail", - url: videoMetadata.thumbUrl - } - ] - }, - content: { - description: description || videoMetadata.description, - tags: videoMetadata.tags_v2 - } - }; - } - - const summary = description === null ? postBodySummary(this.state.body, 200) : description; - - return makeJsonMetaData(meta, tags, summary, version); - }; - - getHeightAndWidthFromDataUrl = (dataURL: string) => - new Promise((resolve) => { - const img = new Image(); - img.onload = () => { - resolve((img.width / img.height).toFixed(4)); - }; - img.onerror = function () { - resolve(0); - }; - img.src = dataURL; - }); - - handleShortcuts = (e: React.KeyboardEvent) => { - if (e.altKey && e.key === "b") { - detectEvent("bold"); - } - if (e.altKey && e.key === "i") { - detectEvent("italic"); - } - if (e.altKey && e.key === "t") { - detectEvent("table"); - } - if (e.altKey && e.key === "k") { - detectEvent("link"); - } - if (e.altKey && e.key === "c") { - detectEvent("codeBlock"); - } - if (e.altKey && e.key === "d") { - detectEvent("image"); - } - if (e.altKey && e.key === "m") { - detectEvent("blockquote"); - } - }; - - handleFloatingFaq = () => { - const { showHelp } = this.state; - this.setState({ showHelp: !showHelp }, () => - handleFloatingContainer(this.state.showHelp, "submit") - ); - }; - - setVideoEncoderBeneficiary = async (video: videoProps) => { - const videoBeneficiary = JSON.parse(video.beneficiaries); - const videoEncoders = [ - { - account: "spk.beneficiary", - src: "ENCODER_PAY", - weight: 900 - }, - { - account: "threespeakleader", - src: "ENCODER_PAY", - weight: 100 - } - ]; - const joinedBeneficiary = [...videoBeneficiary, ...videoEncoders]; - this.stateSet( - { - beneficiaries: joinedBeneficiary, - videoId: await video._id, - isThreespeak: true, - speakPermlink: video.permlink, - speakAuthor: video.owner - }, - this.saveAdvanced - ); - }; - - toggleNsfwC = () => { - this.setState({ isNsfw: true }, this.saveAdvanced); - }; - - render() { - const { - title, - tags, - body, - reward, - preview, - posting, - editingEntry, - saving, - editingDraft, - advanced, - beneficiaries, - schedule, - reblogSwitch, - clearModal, - selectedThumbnail, - thumbnails, - disabled, - drafts - } = this.state; - - // Meta config - const ncount = - this.props.notifications.unread > 0 ? `(${this.props.notifications.unread}) ` : ""; - const metaProps = { - title: ncount + _t("submit.page-title"), - description: _t("submit.page-description") - }; - - const { global, activeUser } = this.props; - - const spinner = ( - - ); - // const isMobile = typeof window !== 'undefined' && window.innerWidth < 570; - let containerClasses = global.isElectron ? " mt-0 pt-6" : ""; - return ( - <> - - - - - {clearModal && ( - this.setState({ clearModal: false })} - /> - )} - {global.isElectron && } - {global.isElectron ? ( - NavBarElectron({ - ...this.props - }) - ) : ( - - )} - -
-
- {editingEntry === null && activeUser && ( -
- {CommunitySelector({ - ...this.props, - activeUser, - tags, - onSelect: (prev, next) => { - const { tags } = this.state; - - const newTags = [ - ...[next ? next : ""], - ...tags.filter((x) => x !== prev) - ].filter((x) => x); - - this.tagsChanged(newTags); - } - })} -
- )} - {EditorToolbar({ - ...this.props, - setVideoEncoderBeneficiary: this.setVideoEncoderBeneficiary, - toggleNsfwC: this.toggleNsfwC, - comment: false, - setVideoMetadata: (v: ThreeSpeakVideo) => - this.setState({ videoMetadata: v }, this.saveAdvanced) - })} -
- -
-
- {TagSelector({ - ...this.props, - tags, - maxItem: 10, - onChange: this.tagsChanged, - onValid: this.handleValidForm - })} -
-
- 0 ? body : preview.body} - onChange={this.bodyChanged} - disableRows={true} - maxrows={100} - spellCheck={true} - activeUser={(activeUser && activeUser.username) || ""} - /> -
- {/* { this.state.showConfirmNsfw && - - } */} - {activeUser ? ( - - ) : ( - <> - )} -
- {editingEntry === null && ( - - )} - -
- -
-
-
-
- {(() => { - const toolBar = schedule ? ( -
- - {LoginRequired({ - ...this.props, - children: ( - - ) - })} -
- ) : ( -
- {editingEntry === null && ( - <> - -
- this.setState({ showHelp: false })}> - - - {global.usePrivate && this.state.isDraftEmpty ? ( - <> - {LoginRequired({ - ...this.props, - children: ( - - ) - })} - - ) : ( - <> - {LoginRequired({ - ...this.props, - children: ( - - ) - })} - - )} - {LoginRequired({ - ...this.props, - children: ( - - ) - })} -
- - )} - {drafts && activeUser && ( - this.setState({ drafts: !drafts })} /> - )} - - {editingEntry !== null && ( - <> - - {LoginRequired({ - ...this.props, - children: ( - - ) - })} - - )} -
- ); - - if (advanced) { - return ( -
-
-

{_t("submit.advanced")}

-
-
-
- {editingEntry === null && ( - <> - - - {_t("submit.reward")} - - - - - - - - {_t("submit.reward-hint")} - - - - - {_t("submit.beneficiaries")} - - - - {_t("submit.beneficiaries-hint")} - - - - )} - - - {_t("submit.description")} - - - - - {this.state.description !== "" - ? this.state.description - : postBodySummary(body, 200)} - - - - {editingEntry === null && ( - <> - {global.usePrivate && ( - - - {_t("submit.schedule")} - - - - {_t("submit.schedule-hint")} - - - )} - - )} - {editingEntry === null && tags?.length > 0 && isCommunity(tags[0]) && ( - - - - - {_t("submit.reblog-hint")} - - - )} - {thumbnails?.length > 0 && ( - - - {_t("submit.thumbnail")} - -
- {[...new Set(thumbnails)]!.map((item, i) => { - let selectedItem = selectedThumbnail; - switch (selectedItem) { - case "": - selectedItem = thumbnails[0]; - break; - } - if (!thumbnails.includes(selectedThumbnail)) { - selectedItem = thumbnails[0]; - } - return ( -
-
{ - this.selectThumbnails(item); - this.setState({ selectionTouched: true }); - }} - key={item} - /> - {selectedItem === item && ( -
- {checkSvg} -
- )} -
- ); - })} -
- - )} -
-
- {toolBar} -
- ); - } - - return ( -
-
-

{_t("submit.preview")}

- -
- - {toolBar} -
- ); - })()} -
- - ); - } -} - -export default connect(pageMapStateToProps, pageMapDispatchToProps)(SubmitPage as any); diff --git a/src/common/pages/submit/_index.scss b/src/common/pages/submit/_index.scss index ff0a537113e..b519f2b602c 100644 --- a/src/common/pages/submit/_index.scss +++ b/src/common/pages/submit/_index.scss @@ -213,6 +213,75 @@ } } } + + .submit-video-attachments { + + .alert { + font-size: 0.875rem; + } + + .submit-video-attachments-list { + display: flex; + gap: 1rem; + justify-content: flex-start; + overflow-x: auto; + + .attachment-item { + min-width: 12rem; + max-width: 12rem; + height: 9rem; + border-radius: 1rem; + border: 1px solid var(--border-color); + background-size: cover; + overflow: hidden; + display: flex; + flex-direction: column; + justify-content: flex-end; + padding: 0.5rem; + + .type { + position: absolute; + background-color: $dark-sky-blue; + color: $white; + rotate: -45deg; + padding: 0.125rem; + text-align: center; + width: 100px; + top: 0.75rem; + left: -1.75rem; + font-size: 0.675rem; + text-transform: uppercase; + } + + .title { + padding: 0.5rem; + background-color: $white; + border-radius: 0.75rem; + border: 1px solid var(--border-color); + } + + .remove { + position: absolute; + top: 0.5rem; + right: 0.5rem; + background-color: $white; + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; + + @include button-variant(rgba($primary, 0.35), rgba($primary, 0.35)); + @include padding(0.5rem); + @include border-radius(0.75rem); + + svg { + width: 1rem; + height: 1rem; + } + } + } + } + } } .selection-item { @@ -244,6 +313,7 @@ font-size: 14px; top: 9px; right: 25px; + svg { width: 25px; } diff --git a/src/common/pages/submit/api/publish.ts b/src/common/pages/submit/api/publish.ts index 9b6169634a5..4a15c4a85fd 100644 --- a/src/common/pages/submit/api/publish.ts +++ b/src/common/pages/submit/api/publish.ts @@ -25,14 +25,7 @@ import { EntriesCacheContext } from "../../../core"; export function usePublishApi(history: History, onClear: () => void) { const { activeUser } = useMappedStore(); - const { - videoId, - is3Speak: isThreespeak, - speakPermlink, - speakAuthor, - isNsfw, - videoMetadata - } = useThreeSpeakManager(); + const { videos, isNsfw, buildBody } = useThreeSpeakManager(); const { updateCache } = useContext(EntriesCacheContext); return useMutation( @@ -58,6 +51,9 @@ export function usePublishApi(history: History, onClear: () => void) { selectedThumbnail?: string; selectionTouched: boolean; }) => { + const unpublished3SpeakVideo = Object.values(videos).find( + (v) => v.status === "publish_manual" + ); // clean body const cbody = body.replace(/[\x00-\x09\x0B-\x0C\x0E-\x1F\x7F-\x9F]/g, ""); @@ -88,7 +84,7 @@ export function usePublishApi(history: History, onClear: () => void) { body, tags, description, - videoMetadata, + videoMetadata: unpublished3SpeakVideo, selectionTouched, selectedThumbnail }); @@ -101,11 +97,20 @@ export function usePublishApi(history: History, onClear: () => void) { ); } - if (isThreespeak && speakPermlink !== "") { - permlink = speakPermlink; + // If post have one unpublished video need to modify + // json metadata which matches to 3Speak + if (unpublished3SpeakVideo) { + // Permlink should be got from 3speak video metadata + permlink = unpublished3SpeakVideo.permlink; // update speak video with title, body and tags - await updateSpeakVideoInfo(activeUser.username, body, videoId, title, tags, isNsfw); - + await updateSpeakVideoInfo( + activeUser.username, + buildBody(body), + unpublished3SpeakVideo._id, + title, + tags, + isNsfw + ); // set specific metadata for 3speak jsonMeta.app = "3speak/0.3.0"; jsonMeta.type = "video"; @@ -114,9 +119,19 @@ export function usePublishApi(history: History, onClear: () => void) { const options = makeCommentOptions(author, permlink, reward, beneficiaries); try { - await comment(author, "", parentPermlink, permlink, title, cbody, jsonMeta, options, true); + await comment( + author, + "", + parentPermlink, + permlink, + title, + buildBody(cbody), + jsonMeta, + options, + true + ); - // Create entry object in store + // Create entry object in store and cache const entry = { ...tempEntry({ author: authorData!, @@ -124,7 +139,7 @@ export function usePublishApi(history: History, onClear: () => void) { parentAuthor: "", parentPermlink, title, - body, + body: buildBody(body), tags, description }), @@ -139,10 +154,10 @@ export function usePublishApi(history: History, onClear: () => void) { history.push(newLoc); //Mark speak video as published - if (isThreespeak && activeUser.username === speakAuthor) { - success(_t("vidoe-upload.publishing")); + if (!!unpublished3SpeakVideo && activeUser.username === unpublished3SpeakVideo.owner) { + success(_t("video-upload.publishing")); setTimeout(() => { - markAsPublished(activeUser!.username, videoId); + markAsPublished(activeUser!.username, unpublished3SpeakVideo._id); }, 10000); } if (isCommunity(tags[0]) && reblogSwitch) { diff --git a/src/common/pages/submit/api/save-draft.ts b/src/common/pages/submit/api/save-draft.ts index 1749d437e5f..da2e1e1d849 100644 --- a/src/common/pages/submit/api/save-draft.ts +++ b/src/common/pages/submit/api/save-draft.ts @@ -12,13 +12,7 @@ import { QueryIdentifiers } from "../../../core"; export function useSaveDraftApi(history: History) { const { activeUser } = useMappedStore(); - const { - isNsfw, - videoId, - speakAuthor, - is3Speak: isThreespeak, - speakPermlink - } = useThreeSpeakManager(); + const { videos } = useThreeSpeakManager(); const queryClient = useQueryClient(); @@ -62,11 +56,7 @@ export function useSaveDraftApi(history: History) { ...meta, beneficiaries, rewardType: reward, - isNsfw, - videoId, - speakAuthor, - isThreespeak, - speakPermlink + videos }; try { diff --git a/src/common/pages/submit/api/schedule.ts b/src/common/pages/submit/api/schedule.ts index 7a5ab6a353d..3fc47fcf26f 100644 --- a/src/common/pages/submit/api/schedule.ts +++ b/src/common/pages/submit/api/schedule.ts @@ -12,9 +12,11 @@ import { error } from "../../../components/feedback"; import { _t } from "../../../i18n"; import { useMappedStore } from "../../../store/use-mapped-store"; import { version } from "../../../../../package.json"; +import { useThreeSpeakManager } from "../hooks"; export function useScheduleApi(onClear: () => void) { const { activeUser } = useMappedStore(); + const { buildBody } = useThreeSpeakManager(); return useMutation( ["schedule"], @@ -55,7 +57,16 @@ export function useScheduleApi(onClear: () => void) { const reblog = isCommunity(tags[0]) && reblogSwitch; try { - await addSchedule(author, permlink, title, body, jsonMeta, options, schedule, reblog); + await addSchedule( + author, + permlink, + title, + buildBody(body), + jsonMeta, + options, + schedule, + reblog + ); onClear(); } catch (e) { if (e.response?.data?.message) { diff --git a/src/common/pages/submit/api/update.ts b/src/common/pages/submit/api/update.ts index 4ea9a100bab..41bd71db406 100644 --- a/src/common/pages/submit/api/update.ts +++ b/src/common/pages/submit/api/update.ts @@ -11,16 +11,14 @@ import { makePath as makePathEntry } from "../../../components/entry-link"; import { useMappedStore } from "../../../store/use-mapped-store"; import { History } from "history"; import { buildMetadata, getDimensionsFromDataUrl } from "../functions"; -import { useThreeSpeakManager } from "../hooks"; import { useContext } from "react"; import { EntriesCacheContext } from "../../../core"; +import { useThreeSpeakManager } from "../hooks"; export function useUpdateApi(history: History, onClear: () => void) { const { activeUser } = useMappedStore(); - - const { videoMetadata } = useThreeSpeakManager(); - const { updateCache } = useContext(EntriesCacheContext); + const { buildBody } = useThreeSpeakManager(); return useMutation( ["update"], @@ -61,7 +59,6 @@ export function useUpdateApi(history: History, onClear: () => void) { body, tags, description, - videoMetadata, selectionTouched, selectedThumbnail }), @@ -84,7 +81,7 @@ export function useUpdateApi(history: History, onClear: () => void) { category, permlink, title, - newBody, + buildBody(newBody), jsonMeta, null ); @@ -93,7 +90,7 @@ export function useUpdateApi(history: History, onClear: () => void) { const entry: Entry = { ...editingEntry, title, - body, + body: buildBody(body), category: tags[0], json_metadata: jsonMeta, updated: correctIsoDate(moment().toISOString()) diff --git a/src/common/pages/submit/hooks/advanced-manager.ts b/src/common/pages/submit/hooks/advanced-manager.ts index 8f444f28568..4bc91d1c1ef 100644 --- a/src/common/pages/submit/hooks/advanced-manager.ts +++ b/src/common/pages/submit/hooks/advanced-manager.ts @@ -31,12 +31,7 @@ export function useAdvancedManager() { setSchedule(localAdvanced.schedule); setReblogSwitch(localAdvanced.reblogSwitch); setDescription(localAdvanced.description); - threeSpeakManager.setIs3Speak(localAdvanced.isThreespeak); - threeSpeakManager.setSpeakAuthor(localAdvanced.speakAuthor); - threeSpeakManager.setSpeakPermlink(localAdvanced.speakPermlink); - threeSpeakManager.setVideoId(localAdvanced.videoId); threeSpeakManager.setIsNsfw(localAdvanced.isNsfw); - threeSpeakManager.setVideoMetadata(localAdvanced.videoMetadata); removeLocalAdvanced(); } diff --git a/src/common/pages/submit/hooks/three-speak-manager-context.ts b/src/common/pages/submit/hooks/three-speak-manager-context.ts new file mode 100644 index 00000000000..a504ec8d795 --- /dev/null +++ b/src/common/pages/submit/hooks/three-speak-manager-context.ts @@ -0,0 +1,34 @@ +import { ThreeSpeakVideo } from "../../../api/threespeak"; +import { createContext } from "react"; + +export interface ThreeSpeakManagerContext { + videos: Record; + isNsfw: boolean; + setIsNsfw: (v: boolean) => void; + isEditing: boolean; + setIsEditing: (v: boolean) => void; + clear: () => void; + attach: (item: ThreeSpeakVideo) => void; + remove: (itemId: string) => void; + hasMultipleUnpublishedVideo: boolean; + buildBody: (v: string) => string; + checkBodyForVideos: (v: string) => void; + + // getters + hasUnpublishedVideo: boolean; +} + +export const ThreeSpeakVideoContext = createContext({ + videos: {}, + isNsfw: false, + setIsNsfw: () => {}, + clear: () => {}, + isEditing: false, + setIsEditing: () => {}, + hasUnpublishedVideo: false, + attach: () => {}, + remove: () => {}, + hasMultipleUnpublishedVideo: false, + buildBody: () => "", + checkBodyForVideos: () => {} +}); diff --git a/src/common/pages/submit/hooks/three-speak-manager.tsx b/src/common/pages/submit/hooks/three-speak-manager.tsx index c11eb585edf..6dcecfa9e11 100644 --- a/src/common/pages/submit/hooks/three-speak-manager.tsx +++ b/src/common/pages/submit/hooks/three-speak-manager.tsx @@ -1,177 +1,89 @@ -import React, { createContext, ReactNode, useContext, useEffect, useMemo, useState } from "react"; +import React, { ReactNode, useContext, useMemo, useState } from "react"; import { ThreeSpeakVideo } from "../../../api/threespeak"; import useLocalStorage from "react-use/lib/useLocalStorage"; import { PREFIX } from "../../../util/local-storage"; -import { useBodyVersioningManager } from "./body-versioning-manager"; - -export interface ThreeSpeakManagerContext { - clear: () => void; - is3Speak: boolean; - setIs3Speak: (is3Speak: boolean) => void; - videoId: string; - setVideoId: (videoId: string) => void; - speakPermlink: string; - setSpeakPermlink: (speakPermlink: string) => void; - speakAuthor: string; - setSpeakAuthor: (speakAuthor: string) => void; - isNsfw: boolean; - setIsNsfw: (isNsfw: boolean) => void; - videoMetadata?: ThreeSpeakVideo; - setVideoMetadata: (videoMetadata?: ThreeSpeakVideo) => void; - isEditing: boolean; - setIsEditing: (v: boolean) => void; - - // getters - hasUnpublishedVideo: boolean; - - // funcs - has3SpeakVideo: (body: string) => boolean; - findAndClearUnpublished3SpeakVideo: (body: string) => void; -} - -export const ThreeSpeakVideoContext = createContext({ - is3Speak: false, - setIs3Speak: () => {}, - videoId: "", - setVideoId: () => {}, - speakPermlink: "", - setSpeakPermlink: () => {}, - speakAuthor: "", - setSpeakAuthor: () => {}, - isNsfw: false, - setIsNsfw: () => {}, - videoMetadata: undefined, - setVideoMetadata: () => {}, - clear: () => {}, - isEditing: false, - setIsEditing: () => {}, - hasUnpublishedVideo: false, - has3SpeakVideo: () => false, - findAndClearUnpublished3SpeakVideo: () => {} -}); +import { ThreeSpeakVideoContext } from "./three-speak-manager-context"; export function useThreeSpeakManager() { return useContext(ThreeSpeakVideoContext); } -const THREE_SPEAK_VIDEO_PATTERN = - /\[!\[\]\(https:\/\/ipfs-3speak\.b-cdn\.net\/ipfs.*\)\]\(https:\/\/3speak\.tv\/watch\?.*\)\[Source\]\(https:\/\/ipfs-3speak\.b-cdn\.net\/ipfs\/(.*)\)/g; - export function ThreeSpeakManager(props: { children: ReactNode }) { - const [is3Speak, setIs3Speak, clearIs3Speak] = useLocalStorage(PREFIX + "_sa_3s", false); - const [videoId, setVideoId, clearVideoId] = useLocalStorage(PREFIX + "_sa_3s_vid", ""); - const [speakPermlink, setSpeakPermlink, clearSpeakPermlink] = useLocalStorage( - PREFIX + "_sa_3s_p", - "" + // Each post could contain multiple published and only one unpublished video itself + const [videos, setVideos] = useLocalStorage>( + PREFIX + "_sa_3s_v", + {} ); - const [speakAuthor, setSpeakAuthor, clearSpeakAuthor] = useLocalStorage(PREFIX + "_sa_3s_a", ""); + const [isNsfw, setIsNsfw, clearIsNsfw] = useLocalStorage(PREFIX + "_sa_3s_n", false); - const [videoMetadata, setVideoMetadata, clearVideoMetadata] = useLocalStorage( - PREFIX + "_sa_3s_vm" - ); const [isEditing, setIsEditing] = useState(false); const hasUnpublishedVideo = useMemo( - () => (videoMetadata ? videoMetadata.status !== "published" : false), - [videoMetadata] + () => [...Object.values(videos!!)].some((item) => item.status === "publish_manual"), + [videos] ); - const bodyManager = useBodyVersioningManager(({ metadata }) => { - const { videoMetadata } = metadata as { - videoMetadata: { - videoMetadata: ThreeSpeakVideo; - is3Speak: boolean; - isNsfw: boolean; - videoId: string; - speakPermlink: string; - speakAuthor: string; - }; - }; - - if (videoMetadata) { - setVideoMetadata(videoMetadata.videoMetadata); - setIs3Speak(videoMetadata.is3Speak); - setVideoId(videoMetadata.videoId); - setSpeakPermlink(videoMetadata.speakPermlink); - setSpeakAuthor(videoMetadata.speakAuthor); - setIsNsfw(videoMetadata.isNsfw); - } - }); - - useEffect(() => { - bodyManager.updateMetadata({ - videoMetadata: { - is3Speak, - videoId, - speakPermlink, - speakAuthor, - isNsfw, - videoMetadata - } - }); - }, [speakAuthor]); - - const has3SpeakVideo = (body: string) => { - const groups = body.matchAll(THREE_SPEAK_VIDEO_PATTERN); - let has = false; - for (const group of groups) { - const match = group[1]; - - has = `ipfs://${match}` === videoMetadata?.filename; - } - - return has; - }; - - const findAndClearUnpublished3SpeakVideo = (body: string) => { - const groups = body.matchAll(THREE_SPEAK_VIDEO_PATTERN); - for (const group of groups) { - const match = group[1]; - - ///
[![](https://ipfs-3speak.b-cdn.net/ipfs/bafkreic3bnsxobtm2va6j3i6v44gc2p2wazi4iuiqqci23tdt7eba5ad5e)](https://3speak.tv/watch?v=demo.com/meohyozpiu)[Source](https://ipfs-3speak.b-cdn.net/ipfs/QmUu28DUQ6wQpH8sFt5pVb3ZDfnvib67BUdtyE8uD4p64j)
- // Has unpublished video - if (`ipfs://${match}` === videoMetadata?.filename && videoMetadata?.status !== "published") { - body.replace( - new RegExp( - `\[!\[\]\(https:\/\/ipfs-3speak\.b-cdn\.net\/ipfs\/${videoMetadata?.thumbnail.replace( - "ipfs://", - "" - )}\)\]\(https:\/\/3speak\.tv\/watch\?.*\)\[Source\]\(https:\/\/ipfs-3speak\.b-cdn\.net\/ipfs\/${match}\)` - ), - "" - ); - } - } - }; + const hasMultipleUnpublishedVideo = useMemo( + () => + [...Object.values(videos!!)].filter((item) => item.status === "publish_manual").length > 1, + [videos] + ); return ( { + setVideos({ ...videos, [item._id]: item }); + }, + remove: (itemId: string) => { + const temp = { ...videos }; + delete temp[itemId]; + setVideos(temp); + }, + hasMultipleUnpublishedVideo, clear: () => { - clearIs3Speak(); - clearIsNsfw(); - clearVideoId(); - clearVideoMetadata(); - clearSpeakPermlink(); - clearSpeakAuthor(); + setVideos({}); + }, + + // Building body based on tokens + buildBody: (body: string) => { + let nextBody = `${body}`; + // Build the preview with 3Speak videos + const existingVideos = nextBody.match(/\[3speak]\(.*\)/gm); + existingVideos + ?.map((group) => ({ group, id: group.replace("[3speak](", "").replace(")", "") })) + ?.filter(({ id }) => !!videos!![id]) + ?.forEach(({ group, id }) => { + const video = videos!![id]; + nextBody = nextBody.replace( + group, + `
[![](https://ipfs-3speak.b-cdn.net/ipfs/${video.thumbUrl})](https://3speak.tv/watch?v=${video.owner}/${video.permlink})
` + ); + }); + return nextBody; + }, + + checkBodyForVideos: (body: string) => { + if (body) { + const nextVideos = {}; + const existingVideos = body.match(/\[3speak]\(.*\)/gm); + existingVideos + ?.map((group) => ({ id: group.replace("[3speak](", "").replace(")", "") })) + ?.filter(({ id }) => !!videos!![id]) + ?.forEach(({ id }) => { + nextVideos[id] = videos!![id]; + }); + + setVideos(nextVideos); + } } }} > diff --git a/src/common/pages/submit/index.tsx b/src/common/pages/submit/index.tsx index 7572b341641..64ea747d3af 100644 --- a/src/common/pages/submit/index.tsx +++ b/src/common/pages/submit/index.tsx @@ -40,7 +40,7 @@ import TextareaAutocomplete from "../../components/textarea-autocomplete"; import { AvailableCredits } from "../../components/available-credits"; import ClickAwayListener from "../../components/clickaway-listener"; import { checkSvg, contentLoadSvg, contentSaveSvg, helpIconSvg } from "../../img/svg"; -import BeneficiaryEditor from "../../components/beneficiary-editor"; +import { BeneficiaryEditorDialog } from "../../components/beneficiary-editor"; import PostScheduler from "../../components/post-scheduler"; import moment from "moment/moment"; import isCommunity from "../../helper/is-community"; @@ -57,6 +57,7 @@ import { RewardType } from "../../api/operations"; import { SubmitPreviewContent } from "./submit-preview-content"; import { useUpdateApi } from "./api/update"; import "./_index.scss"; +import { SubmitVideoAttachments } from "./submit-video-attachments"; interface MatchProps { match: MatchType; @@ -136,6 +137,9 @@ export function Submit(props: PageProps & MatchProps) { threeSpeakManager.setIsEditing(true); } else if (editingEntry) { setEditingEntry(null); + threeSpeakManager.setIsEditing(false); + } else { + threeSpeakManager.setIsEditing(false); } }); @@ -157,14 +161,9 @@ export function Submit(props: PageProps & MatchProps) { setSelectedThumbnail(draft.meta?.image?.[0]); setDescription(draft.meta?.description ?? ""); - if (draft.meta?.isThreespeak) { - threeSpeakManager.setIs3Speak(draft.meta?.isThreespeak ?? false); - threeSpeakManager.setSpeakAuthor(draft.meta?.speakAuthor ?? ""); - threeSpeakManager.setSpeakPermlink(draft.meta?.speakPermlink ?? ""); - threeSpeakManager.setVideoId(draft.meta?.videoId ?? ""); - threeSpeakManager.setIsNsfw(draft.meta?.isNsfw ?? false); - threeSpeakManager.setVideoMetadata(draft.meta?.videoMetadata); - } + [...Object.values(draft.meta?.videos ?? {})].forEach((item) => + threeSpeakManager.attach(item) + ); setTimeout(() => setIsDraftEmpty(false), 100); }, @@ -204,12 +203,12 @@ export function Submit(props: PageProps & MatchProps) { }, [postBodyRef]); useEffect(() => { - if (activeUser?.username !== previousActiveUser?.username && activeUser) { + if (activeUser?.username !== previousActiveUser?.username && activeUser && previousActiveUser) { // delete active user from beneficiaries list setBeneficiaries(beneficiaries.filter((x) => x.account !== activeUser.username)); // clear not current user videos - threeSpeakManager.findAndClearUnpublished3SpeakVideo(body); + threeSpeakManager.clear(); } }, [activeUser]); @@ -226,10 +225,7 @@ export function Submit(props: PageProps & MatchProps) { }, [title, body, tags]); useEffect(() => { - if (!threeSpeakManager.has3SpeakVideo(body) && !!previousBody) { - threeSpeakManager.clear(); - console.log("clearing 3speak"); - } + threeSpeakManager.checkBodyForVideos(body); }, [body]); const updatePreview = (): void => { @@ -313,10 +309,6 @@ export function Submit(props: PageProps & MatchProps) { ]; const joinedBeneficiary = [...videoBeneficiary, ...videoEncoders]; setBeneficiaries(joinedBeneficiary); - threeSpeakManager.setVideoId(video._id); - threeSpeakManager.setIs3Speak(true); - threeSpeakManager.setSpeakPermlink(video.permlink); - threeSpeakManager.setSpeakAuthor(video.owner); }; const cancelUpdate = () => { @@ -358,6 +350,11 @@ export function Submit(props: PageProps & MatchProps) { return false; } + if (threeSpeakManager.hasMultipleUnpublishedVideo) { + error(_t("submit.should-be-only-one-unpublished")); + return false; + } + return true; }; @@ -405,7 +402,9 @@ export function Submit(props: PageProps & MatchProps) { }} comment={false} setVideoMetadata={(v) => { - threeSpeakManager.setVideoMetadata(v); + threeSpeakManager.attach(v); + // Attach videos as special token in a body and render it in a preview + setBody(`${body}\n[3speak](${v._id})`); }} />
@@ -445,6 +444,7 @@ export function Submit(props: PageProps & MatchProps) { activeUser={(activeUser && activeUser.username) || ""} />
+ {activeUser ? ( - buildBody(body), [body]); return ( <> @@ -30,7 +34,7 @@ export function SubmitPreviewContent({ title, tags, body, history }: Props) { })}
- + ); } diff --git a/src/common/pages/submit/submit-video-attachments.tsx b/src/common/pages/submit/submit-video-attachments.tsx new file mode 100644 index 00000000000..6411a03bead --- /dev/null +++ b/src/common/pages/submit/submit-video-attachments.tsx @@ -0,0 +1,45 @@ +import { useThreeSpeakManager } from "./hooks"; +import React, { useMemo } from "react"; +import { proxifyImageSrc } from "@ecency/render-helper"; +import { useMappedStore } from "../../store/use-mapped-store"; +import { closeSvg } from "../../img/svg"; +import { Alert } from "react-bootstrap"; +import { _t } from "../../i18n"; + +export function SubmitVideoAttachments() { + const { global } = useMappedStore(); + const { videos, remove, hasMultipleUnpublishedVideo } = useThreeSpeakManager(); + + const videoList = useMemo(() => [...Object.values(videos)], [videos]); + + return ( +
+ {videoList.length > 0 ?

Attached 3Speak videos

: <>} + {hasMultipleUnpublishedVideo && ( + {_t("submit.should-be-only-one-unpublished")} + )} +
+ {videoList.map((item) => ( +
+
3speak
+
remove(item._id)}> + {closeSvg} +
+
{item.title}
+
+ ))} +
+
+ ); +} diff --git a/yarn.lock b/yarn.lock index 14e04cfeeac..91d68a28847 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1349,6 +1349,11 @@ resolved "https://registry.yarnpkg.com/@emoji-mart/data/-/data-1.1.2.tgz#777c976f8f143df47cbb23a7077c9ca9fe5fc513" integrity sha512-1HP8BxD2azjqWJvxIaWAMyTySeZY0Osr83ukYjltPVkNXeJvTz7yDrPLBtnrD5uqJ3tg4CcLuuBW09wahqL/fg== +"@emoji-mart/react@^1.1.1": + version "1.1.1" + resolved "https://registry.yarnpkg.com/@emoji-mart/react/-/react-1.1.1.tgz#ddad52f93a25baf31c5383c3e7e4c6e05554312a" + integrity sha512-NMlFNeWgv1//uPsvLxvGQoIerPuVdXwK/EUek8OOkJ6wVOWPUizRBJU0hDqWZCOROVpfBgCemaC3m6jDOXi03g== + "@firebase/analytics@^0.8.0": version "0.8.0" resolved "https://registry.yarnpkg.com/@firebase/analytics/-/analytics-0.8.0.tgz#b5d595082f57d33842b1fd9025d88f83065e87fe"