diff --git a/apps/builddao/widget/Compose.jsx b/apps/builddao/widget/Compose.jsx index 4c388232..92a597da 100644 --- a/apps/builddao/widget/Compose.jsx +++ b/apps/builddao/widget/Compose.jsx @@ -1,19 +1,33 @@ -const { Avatar, Button } = VM.require("buildhub.near/widget/components"); +const { Avatar, Button } = VM.require("buildhub.near/widget/components") || { + Avatar: () => <>, + Button: () => <>, +}; -Avatar = Avatar || (() => <>); -Button = Button || (() => <>); +const { DraftModal } = + VM.require("buildhub.near/widget/components.modals.DraftModal") || + (() => <>); const draftKey = props.feed.name || "draft"; const draft = Storage.privateGet(draftKey); +const autocompleteEnabled = true; + if (draft === null) { return ""; } +State.init({ + image: {}, +}); + const [view, setView] = useState("editor"); const [postContent, setPostContent] = useState(""); const [hideAdvanced, setHideAdvanced] = useState(true); const [labels, setLabels] = useState([]); +const [showAccountAutocomplete, setShowAccountAutocomplete] = useState(false); +const [mentionsArray, setMentionsArray] = useState([]); +const [mentionInput, setMentionInput] = useState(null); +const [handler, setHandler] = useState("update"); setPostContent(draft || props.template); @@ -23,16 +37,6 @@ function generateUID() { return randomNumber.toString(16).padStart(8, "0"); } -function tagsFromLabels(labels) { - return labels.reduce( - (newLabels, label) => ({ - ...newLabels, - [label]: "", - }), - {} - ); -} - const extractMentions = (text) => { const mentionRegex = /@((?:(?:[a-z\d]+[-_])*[a-z\d]+\.)*(?:[a-z\d]+[-_])*[a-z\d]+)/gi; @@ -102,6 +106,12 @@ const postToCustomFeed = ({ feed, text, labels }) => { text = checkAndAppendHashtag(text, hashtag); }); + const content = { + type: "md", + image: state.image.cid ? { ipfs_cid: state.image.cid } : undefined, + text: text, + }; + const data = { // [feed.name]: { // [postId]: { @@ -117,12 +127,7 @@ const postToCustomFeed = ({ feed, text, labels }) => { // }, // }, post: { - main: JSON.stringify({ - type: "md", - text, - // tags: tagsFromLabels(labels), - // postType: feed.name, - }), + main: JSON.stringify(content), }, index: { post: JSON.stringify({ key: "main", value: { type: "md" } }), @@ -165,6 +170,43 @@ const postToCustomFeed = ({ feed, text, labels }) => { }); }; +function textareaInputHandler(value) { + const words = value.split(/\s+/); + const allMentiones = words + .filter((word) => word.startsWith("@")) + .map((mention) => mention.slice(1)); + const newMentiones = allMentiones.filter( + (item) => !mentionsArray.includes(item) + ); + setMentionInput(newMentiones?.[0] ?? ""); + setMentionsArray(allMentiones); + setShowAccountAutocomplete(newMentiones?.length > 0); + setPostContent(value); + setHandler("update"); + Storage.privateSet(draftKey, value || ""); +} + +function autoCompleteAccountId(id) { + let currentIndex = 0; + const updatedDescription = postContent.replace( + /(?:^|\s)(@[^\s]*)/g, + (match) => { + if (currentIndex === mentionsArray.indexOf(mentionInput)) { + currentIndex++; + return ` @${id}`; + } else { + currentIndex++; + return match; + } + } + ); + setPostContent(updatedDescription); + setShowAccountAutocomplete(false); + setMentionInput(null); + setHandler("autocompleteSelected"); + Storage.privateSet(draftKey, updatedDescription || ""); +} + const PostCreator = styled.div` display: flex; flex-direction: column; @@ -175,6 +217,51 @@ const PostCreator = styled.div` border-radius: 12px; margin-bottom: 1rem; + + .upload-image-button { + display: flex; + align-items: center; + justify-content: center; + background: #f1f3f5; + color: #11181c; + border-radius: 40px; + height: 40px; + min-width: 40px; + font-size: 0; + border: none; + cursor: pointer; + transition: background 200ms, opacity 200ms; + + &::before { + font-size: 16px; + } + + &:hover, + &:focus { + background: #d7dbde; + outline: none; + } + + &:disabled { + opacity: 0.5; + pointer-events: none; + } + + span { + margin-left: 12px; + } + } + + .d-inline-block { + display: flex !important; + gap: 12px; + margin: 0 !important; + + .overflow-hidden { + width: 40px !important; + height: 40px !important; + } + } `; const TextareaWrapper = styled.div` @@ -403,6 +490,16 @@ const LabelSelect = styled.div` } `; +// To handle ifram refresh in order to trigger initialText change +const [postUUID, setPostUUID] = useState(generateUID()); +const memoizedPostUUID = useMemo(() => postUUID, [postUUID]); + +useEffect(() => { + if (postContent === "") { + setPostUUID(generateUID()); + } +}, [postContent]); + const avatarComponent = useMemo(() => { return (
@@ -414,63 +511,236 @@ const avatarComponent = useMemo(() => { ); }, [context.accountId]); -return ( - - {avatarComponent} -
- {view === "editor" ? ( - - { - setPostContent(v); - Storage.privateSet(draftKey, v || ""); - }, - }} - /> - - ) : ( - - { + const savedDrafts = Storage.privateGet("savedDrafts") || ""; + const drafts = JSON.parse(savedDrafts); + const newDrafts = drafts.filter((draft, i) => !checkedDrafts.includes(i)); + Storage.privateSet("savedDrafts", JSON.stringify(newDrafts)); + setCheckDrafts([]); +}; + +// handle draft save +const onSaveDraft = () => { + const savedDrafts = Storage.privateGet("savedDrafts") || ""; + const drafts = JSON.parse(savedDrafts) || []; + drafts.push(postContent); + Storage.privateSet("savedDrafts", JSON.stringify(drafts)); +}; + +const DraftLabel = styled.span` + display: inline-flex; + padding: 12px; + align-items: center; + gap: 8px; + + color: #fff; + font-size: 16px; + font-style: normal; + font-weight: 400; + line-height: 170%; /* 27.2px */ +`; + +const DraftItem = ({ draft, checked, isEdit }) => { + return ( +
+ + {isEdit && ( + + )} + {draft} + +
+ ); +}; + +const RenderDrafts = () => { + const savedDrafts = Storage.privateGet("savedDrafts") || ""; + const drafts = JSON.parse(savedDrafts); + + const handleDraftSelect = (draft) => { + textareaInputHandler(draft); + setPostUUID(generateUID()); + setShowDraftsModal(false); + setView("editor"); + }; + + return ( +
+ {drafts.map((draft, i) => ( +
handleDraftSelect(draft)}> + +
+ ))} + {drafts.length === 0 &&

No drafts saved

} +
+ ); +}; + +const EditDrafts = () => { + const savedDrafts = Storage.privateGet("savedDrafts") || ""; + const drafts = JSON.parse(savedDrafts); + + const handleDraftSelect = (draftIndex) => { + if (checkedDrafts.includes(draftIndex)) { + setCheckDrafts(checkedDrafts.filter((draft) => draft !== draftIndex)); + } else { + setCheckDrafts([...checkedDrafts, draftIndex]); + } + }; + + return ( +
+ {drafts.map((draft, i) => ( +
handleDraftSelect(i)}> + - - )} +
+ ))} + {drafts.length === 0 &&

No drafts saved

}
+ ); +}; -
- + setShowDraftsModal(!showDraftsModal)} + editButton={ + draftEditMode ? ( +
+ + +
+ ) : ( + + ) + } + children={
{draftEditMode ? : }
} + /> + +
+ {avatarComponent} + +
+
{view === "editor" ? ( - <> - Preview - + + { + textareaInputHandler(content); + }, + embedCss: MarkdownEditor, + }} + /> + {autocompleteEnabled && showAccountAutocomplete && ( + setShowAccountAutocomplete(false), + }} + /> + )} + ) : ( - <> - Edit - + + + {state.image.cid && ( + + )} + )} - - -
-
+
+ +
+ {view === "editor" && ( + + )} + + +
+ +
); diff --git a/apps/builddao/widget/components.jsx b/apps/builddao/widget/components.jsx index 38499bad..b73c4264 100644 --- a/apps/builddao/widget/components.jsx +++ b/apps/builddao/widget/components.jsx @@ -33,7 +33,11 @@ function Pagination({ function Post(props) { return ( - + } + props={{ ...props }} + /> ); } diff --git a/apps/builddao/widget/components/AccountAutocomplete.jsx b/apps/builddao/widget/components/AccountAutocomplete.jsx new file mode 100644 index 00000000..5823f397 --- /dev/null +++ b/apps/builddao/widget/components/AccountAutocomplete.jsx @@ -0,0 +1,147 @@ +if (!context.accountId || !props.term) return <>; + +let results = []; +const filterAccounts = props.filterAccounts ?? []; // hide certain accounts from the list +const profilesData = Social.get("*/profile/name", "final") || {}; +const followingData = Social.get( + `${context.accountId}/graph/follow/**`, + "final" +); +if (!profilesData) return <>; +const profiles = Object.entries(profilesData); +const term = (props.term || "").replace(/\W/g, "").toLowerCase(); +const limit = 5; + +for (let i = 0; i < profiles.length; i++) { + let score = 0; + const accountId = profiles[i][0]; + const accountIdSearch = profiles[i][0].replace(/\W/g, "").toLowerCase(); + const nameSearch = (profiles[i][1]?.profile?.name || "") + .replace(/\W/g, "") + .toLowerCase(); + const accountIdSearchIndex = accountIdSearch.indexOf(term); + const nameSearchIndex = nameSearch.indexOf(term); + + if (accountIdSearchIndex > -1 || nameSearchIndex > -1) { + score += 10; + + if (accountIdSearchIndex === 0) { + score += 10; + } + if (nameSearchIndex === 0) { + score += 10; + } + if (followingData[accountId] === "") { + score += 30; + } + + results.push({ + accountId, + score + }); + } +} + +results.sort((a, b) => b.score - a.score); +results = results.slice(0, limit); +if (filterAccounts?.length > 0) { + results = results.filter((item) => !filterAccounts?.includes(item.accountId)); +} + +function onResultClick(id) { + props.onSelect && props.onSelect(id); +} + +const Wrapper = styled.div` + position: relative; + + &::before { + content: ""; + display: block; + position: absolute; + right: 0; + width: 6px; + height: 100%; + z-index: 10; + } +`; + +const Scroller = styled.div` + position: relative; + display: flex; + padding: 6px; + gap: 6px; + overflow: auto; + scroll-behavior: smooth; + align-items: center; + scrollbar-width: none; + -ms-overflow-style: none; + &::-webkit-scrollbar { + display: none; + } + + > * { + max-width: 175px; + flex-grow: 0; + flex-shrink: 0; + + button { + border: 1px solid #eceef0; + background: #fff !important; + border-radius: 6px; + padding: 3px 6px; + transition: all 200ms; + + &:focus, + &:hover { + border-color: #687076; + } + } + } +`; + +const CloseButton = styled.button` + background: none; + border: none; + display: block; + padding: 12px; + color white; + transition: all 200ms; + + &:hover { + transform:scale(1.2); + } +`; + +const ProfileCardWrapper = styled.div` + opacity: 0.8; +`; + +if (results.length === 0) return <>; + +return ( + + + + + + + {results.map((result) => { + return ( + + + + ); + })} + + +); diff --git a/apps/builddao/widget/components/Checkbox.jsx b/apps/builddao/widget/components/Checkbox.jsx index 27387079..1f3bed49 100644 --- a/apps/builddao/widget/components/Checkbox.jsx +++ b/apps/builddao/widget/components/Checkbox.jsx @@ -7,6 +7,8 @@ const CheckboxLabel = styled.label` padding: 12px; align-items: center; gap: 8px; + cursor: pointer; + max-width: 100%; color: #fff; font-size: 16px; @@ -15,20 +17,20 @@ const CheckboxLabel = styled.label` line-height: 170%; /* 27.2px */ `; -function Checkbox({ value, onChange, label }) { +function Checkbox({ className, value, onChange, label }) { return ( -
+
{value ? ( - + ) : ( - + )} - {label} + {label}
); } -return { Checkbox }; \ No newline at end of file +return { Checkbox }; diff --git a/apps/builddao/widget/components/MarkdownEditorIframe.jsx b/apps/builddao/widget/components/MarkdownEditorIframe.jsx new file mode 100644 index 00000000..ffb2e26b --- /dev/null +++ b/apps/builddao/widget/components/MarkdownEditorIframe.jsx @@ -0,0 +1,64 @@ +const data = props.data ?? "# Hello World\n\n"; +const embedCss = props.embedCss || ""; + +const code = ` + + + + + + + +
+ + +`; +return ( +