From 6a33d2cfbe9f09e5f6b87d485f3e02a6363accba Mon Sep 17 00:00:00 2001 From: Megha-Dev-19 Date: Thu, 28 Mar 2024 23:06:41 +0530 Subject: [PATCH 1/2] added discussion feed page --- .../widget/Common/Components/Compose.jsx | 389 ++++++++++++++++++ .../widget/Common/Components/Feed.jsx | 30 ++ .../widget/Common/Components/Post.jsx | 260 ++++++++++++ apps/astraplusplus/widget/DAO/Discussion.jsx | 16 - apps/astraplusplus/widget/DAO/Feed.jsx | 127 ++++++ apps/astraplusplus/widget/DAO/index.jsx | 4 +- 6 files changed, 808 insertions(+), 18 deletions(-) create mode 100644 apps/astraplusplus/widget/Common/Components/Compose.jsx create mode 100644 apps/astraplusplus/widget/Common/Components/Feed.jsx create mode 100644 apps/astraplusplus/widget/Common/Components/Post.jsx delete mode 100644 apps/astraplusplus/widget/DAO/Discussion.jsx create mode 100644 apps/astraplusplus/widget/DAO/Feed.jsx diff --git a/apps/astraplusplus/widget/Common/Components/Compose.jsx b/apps/astraplusplus/widget/Common/Components/Compose.jsx new file mode 100644 index 0000000..94388dd --- /dev/null +++ b/apps/astraplusplus/widget/Common/Components/Compose.jsx @@ -0,0 +1,389 @@ +const autocompleteEnabled = props.autocompleteEnabled ?? true; +const onPreview = props.onPreview; +const accountId = props.accountId; +const onPostClick = props.onPostClick ?? (() => {}); + +const [editorKey, setEditorKey] = useState(0); +const memoizedEditorKey = useMemo(() => editorKey, [editorKey]); + +const MemoizedAvatar = useMemo( + () => ( + + ), + [accountId] +); + +const Wrapper = styled.div` + line-height: normal; + display: flex; + flex-direction: column; + gap: 1rem; + padding: 1.5rem; + + .right { + flex-grow: 1; + min-width: 0; + } + + .up-buttons { + margin-top: 12px; + + @media screen and (max-width: 768px) { + display: flex; + align-items: center; + gap: 1rem; + } + } + + div[data-component="near/widget/AccountProfile"] { + border-radius: 0px !important; + } +`; + +const embedCss = ` +.rc-md-editor { + border-radius: 10px; + overflow: auto; +} +`; + +const TextareaWrapper = styled.div` + display: grid; + vertical-align: top; + align-items: center; + position: relative; + align-items: stretch; + + textarea { + display: flex; + align-items: center; + transition: all 0.3s ease; + } + + textarea::placeholder { + padding-top: 4px; + font-size: 20px; + } + + textarea:focus::placeholder { + font-size: inherit; + padding-top: 0px; + } + + &::after, + textarea, + iframe { + width: 100%; + padding: 8px 0; + min-width: 1em; + height: unset; + min-height: 3em; + font: inherit; + margin: 0; + resize: none; + background: none; + appearance: none; + border: 0px solid #eee; + grid-area: 1 / 1; + overflow: hidden; + outline: none; + } + + iframe { + padding: 0; + } + + textarea:focus, + textarea:not(:empty) { + border-bottom: 1px solid #eee; + min-height: 5em; + } + + &::after { + content: attr(data-value) " "; + visibility: hidden; + white-space: pre-wrap; + } + &.markdown-editor::after { + padding-top: 66px; + font-family: monospace; + font-size: 14px; + } +`; + +const Actions = styled.div` + display: flex; + gap: 12px; + justify-content: end; + + .commit-post-button, + .preview-post-button { + background: #59e692; + color: #09342e; + border-radius: 40px; + height: 40px; + padding: 0 35px; + font-weight: 600; + font-size: 14px; + border: none; + cursor: pointer; + transition: + background 200ms, + opacity 200ms; + + &:hover, + &:focus { + background: rgb(112 242 164); + outline: none; + } + + &:disabled { + opacity: 0.5; + pointer-events: none; + } + } + + .preview-post-button { + color: #11181c; + background: #f1f3f5; + padding: 0; + width: 40px; + + &:hover, + &:focus { + background: #d7dbde; + outline: none; + } + } + + .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 PreviewWrapper = styled.div` + position: relative; + padding: var(--padding); + padding-bottom: calc(40px + (var(--padding) * 2)); + + a { + padding-inline: 5px; + padding-block: 2px; + border-radius: 8px; + border: 1px solid #e1e3fd; + background: #f5f6fe; + color: #626ad1 !important; + font-size: 12px; + font-style: normal; + font-weight: 500; + line-height: 20px; + } + + li a { + margin-right: 2px; + } + + a[data-component="near/widget/AccountProfile"] { + border: none; + } + + ul { + line-height: 30px; + } +`; + +if (state.image === undefined) { + State.init({ + image: {}, + text: props.initialText || "", + mentionsArray: [], + mentionInput: "" + }); +} + +function autoCompleteAccountId(id) { + // to make sure we update the @ at correct index + let currentIndex = 0; + const updatedDescription = state.text.replace( + /(?:^|\s)(@[^\s]*)/g, + (match) => { + if (currentIndex === state.mentionsArray.indexOf(state.mentionInput)) { + currentIndex++; + return ` @${id}`; + } else { + currentIndex++; + return match; + } + } + ); + + State.update((lastKnownState) => ({ + ...lastKnownState, + text: updatedDescription, + showAccountAutocomplete: false + })); + setEditorKey((prev) => prev + 1); +} + +function onChange(value) { + // since this fn gets called twice on every text update + if (value === state.text) { + return; + } + const words = value.split(/\s+/); + const allMentiones = words + .filter((word) => word.startsWith("@")) + .map((mention) => mention.slice(1)); + const newMentiones = allMentiones.filter( + (item) => !state.mentionsArray.includes(item) + ); + State.update((lastKnownState) => ({ + ...lastKnownState, + text: value, + showAccountAutocomplete: newMentiones?.length > 0, + mentionsArray: allMentiones, + mentionInput: newMentiones?.[0] ?? "" + })); +} + +const content = (state.text || state.image.cid || state.image.url) && { + type: "md", + text: state.text, + image: state.image.url + ? { url: state.image.url } + : state.image.cid + ? { ipfs_cid: state.image.cid } + : undefined +}; + +const clearCompose = () => { + State.update({ + image: {}, + text: "" + }); +}; + +return ( + + {state.showPreview ? ( + + + + ) : ( +
+
{MemoizedAvatar}
+
+ + + {autocompleteEnabled && state.showAccountAutocomplete && ( +
+ + State.update({ showAccountAutocomplete: false }) + }} + /> +
+ )} +
+
+
+ )} + + {!state.showPreview && ( + + )} + + + + + +
+); diff --git a/apps/astraplusplus/widget/Common/Components/Feed.jsx b/apps/astraplusplus/widget/Common/Components/Feed.jsx new file mode 100644 index 0000000..c74f1a6 --- /dev/null +++ b/apps/astraplusplus/widget/Common/Components/Feed.jsx @@ -0,0 +1,30 @@ +const { Feed } = VM.require("devs.near/widget/Module.Feed") || { + Feed: () => <> +}; + +Feed = Feed || (() => <>); + +const accounts = props.accounts ?? []; +return ( + { + return ( + } + props={{ accountId: p.accountId, blockHeight: p.blockHeight }} + /> + ); + }} + /> +); diff --git a/apps/astraplusplus/widget/Common/Components/Post.jsx b/apps/astraplusplus/widget/Common/Components/Post.jsx new file mode 100644 index 0000000..f3c6b12 --- /dev/null +++ b/apps/astraplusplus/widget/Common/Components/Post.jsx @@ -0,0 +1,260 @@ +const accountId = props.accountId; +const blockHeight = + props.blockHeight === "now" ? "now" : parseInt(props.blockHeight); +const subscribe = !!props.subscribe; +const notifyAccountId = accountId; +const postUrl = `https://near.org/s/p?a=${accountId}&b=${blockHeight}`; +const showFlagAccountFeature = props.showFlagAccountFeature; + +State.init({ hasBeenFlagged: false }); + +const edits = []; // Social.index('edit', { accountId, blockHeight }, { limit: 1, order: "desc", accountId }) + +const content = + props.content ?? + JSON.parse( + edits.length + ? Social.get(`${accountId}/edit/main`, edits.blockHeight) + : Social.get(`${accountId}/post/main`, blockHeight) + ); + +const item = { + type: "social", + path: `${accountId}/post/main`, + blockHeight +}; + +const Post = styled.div` + position: relative; + + &::before { + content: ""; + display: block; + position: absolute; + left: 19px; + top: 52px; + bottom: 12px; + width: 2px; + background: #eceef0; + } + + [data-component="near/widget/AccountProfile"] { + border-radius: 0px !important; + } +`; + +const Header = styled.div` + margin-bottom: 0; + display: inline-flex; + + p[data-component="near/widget/AccountProfile"] { + display: none !important; + } +`; + +const Body = styled.div` + padding-left: 52px; + padding-bottom: 1px; + + a { + padding-inline: 5px; + padding-block: 2px; + border-radius: 8px; + border: 1px solid #e1e3fd; + background: #f5f6fe; + color: #626ad1 !important; + font-size: 12px; + font-style: normal; + font-weight: 500; + line-height: 20px; + } + + li a { + margin-right: 2px; + } + + a[data-component="near/widget/AccountProfile"] { + border: none; + } + + ul { + line-height: 30px; + } +`; + +const Content = styled.div` + img { + display: block; + max-width: 100%; + max-height: 80vh; + margin: 0 0 12px; + } +`; + +const Text = styled.p` + display: block; + margin: 0; + font-size: 14px; + line-height: 20px; + font-weight: 400; + color: #687076; + white-space: nowrap; +`; + +const Actions = styled.div` + display: flex; + align-items: center; + gap: 12px; + margin: -6px -6px 6px; +`; + +const Comments = styled.div` + > div > div:first-child { + padding-top: 12px; + } +`; + +if (state.hasBeenFlagged) { + return ( +
+ This content has been flagged for moderation +
+ ); +} + +const profile = Social.get(`${accountId}/profile/**`, "final"); + +function readableDate(timestamp) { + var a = new Date(timestamp); + var options = { + month: "short", + day: "2-digit", + year: "numeric", + hour: "2-digit", + minute: "2-digit" + }; + return a.toLocaleString("en-US", options); +} + +const res = fetch(`https://api.near.social/time?blockHeight=${blockHeight}`); +if (!res) { + return "Loading"; +} + +const timeMs = parseFloat(res.body); + +return ( + +
+
+
+ +
+
+
+ {profile.name || accountId.split(".near")[0]} +
+

+ Updated at {readableDate(timeMs)} +

+
+
+
+ + + + {content.text && ( + + )} + + {content.image && ( + + )} + + + {blockHeight !== "now" && ( + + + State.update({ showReply: !state.showReply }) + }} + /> + + + { + State.update({ hasBeenFlagged: true }); + } + }} + /> + + )} + + {state.showReply && ( +
+
+ State.update({ showReply: false }) + }} + /> +
+ + + +
+ )} + +
+); diff --git a/apps/astraplusplus/widget/DAO/Discussion.jsx b/apps/astraplusplus/widget/DAO/Discussion.jsx deleted file mode 100644 index 4cde79b..0000000 --- a/apps/astraplusplus/widget/DAO/Discussion.jsx +++ /dev/null @@ -1,16 +0,0 @@ -return ( - <> -

Curated Posts

- - Update Feed - -
- - -); diff --git a/apps/astraplusplus/widget/DAO/Feed.jsx b/apps/astraplusplus/widget/DAO/Feed.jsx new file mode 100644 index 0000000..c855984 --- /dev/null +++ b/apps/astraplusplus/widget/DAO/Feed.jsx @@ -0,0 +1,127 @@ +const daoId = props.daoId; +const [showComposeModal, setShowComposeModal] = useState(false); +const [postContent, setPostContent] = useState(null); + +const policy = Near.view(daoId, "get_policy"); + +function extractMentions(text) { + const mentionRegex = + /@((?:(?:[a-z\d]+[-_])*[a-z\d]+\.)*(?:[a-z\d]+[-_])*[a-z\d]+)/gi; + mentionRegex.lastIndex = 0; + const accountIds = new Set(); + for (const match of text.matchAll(mentionRegex)) { + if ( + !/[\w`]/.test(match.input.charAt(match.index - 1)) && + !/[/\w`]/.test(match.input.charAt(match.index + match[0].length)) && + match[1].length >= 2 && + match[1].length <= 64 + ) { + accountIds.add(match[1].toLowerCase()); + } + } + return [...accountIds]; +} + +function extractTagNotifications(text, item) { + return extractMentions(text || "") + .filter((accountId) => accountId !== daoId) + .map((accountId) => ({ + key: accountId, + value: { + type: "mention", + item + } + })); +} + +function composeData(content) { + const data = { + post: { + main: JSON.stringify(content) + }, + index: { + post: JSON.stringify({ + key: "main", + value: { + type: "md" + } + }) + } + }; + + const notifications = extractTagNotifications(content.text, { + type: "social", + path: `${daoId}/post/main` + }); + + if (notifications.length) { + data.index.notify = JSON.stringify( + notifications.length > 1 ? notifications : notifications[0] + ); + } + return data; +} + +function addProposal(content) { + const base64 = Buffer.from( + JSON.stringify({ + data: { + [daoId]: composeData(content) + }, + options: { refund_unused_deposit: true } + }), + "utf-8" + ).toString("base64"); + Near.call({ + contractName: daoId, + methodName: "add_proposal", + args: { + proposal: { + description: "Social Feed post", + kind: { + FunctionCall: { + receiver_id: "social.near", + actions: [ + { + method_name: "set", + args: base64, + deposit: "100000000000000000000000", + gas: "200000000000000" + } + ] + } + } + } + }, + deposit: policy?.proposal_bond || 100000000000000000000000, + gas: 200000000000000 + }); +} + +const Wrapper = styled.div` + .border-vertical { + border-top: 1px solid rgb(236, 238, 240); + border-bottom: 1px solid rgb(236, 238, 240); + margin-bottom: 2rem; + } +`; + +return ( + +
+

DAO Feed

+
+ +
+
+ +
+); diff --git a/apps/astraplusplus/widget/DAO/index.jsx b/apps/astraplusplus/widget/DAO/index.jsx index 5e9a487..04b0f94 100644 --- a/apps/astraplusplus/widget/DAO/index.jsx +++ b/apps/astraplusplus/widget/DAO/index.jsx @@ -49,8 +49,8 @@ const tabs = { }) }, home: { - name: "Discussion", - widget: "DAO.Discussion", + name: "Feed", + widget: "DAO.Feed", href: constructURL({ tab: "home", daoId: props.daoId, From b4a0e56af27e9964e41aa1f7d22874ccfc25ce94 Mon Sep 17 00:00:00 2001 From: Megha-Dev-19 Date: Mon, 1 Apr 2024 16:03:23 +0530 Subject: [PATCH 2/2] update description --- apps/astraplusplus/widget/DAO/Feed.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/astraplusplus/widget/DAO/Feed.jsx b/apps/astraplusplus/widget/DAO/Feed.jsx index c855984..8cb32ac 100644 --- a/apps/astraplusplus/widget/DAO/Feed.jsx +++ b/apps/astraplusplus/widget/DAO/Feed.jsx @@ -77,7 +77,7 @@ function addProposal(content) { methodName: "add_proposal", args: { proposal: { - description: "Social Feed post", + description: `Social Feed post created by ${context.accountId}`, kind: { FunctionCall: { receiver_id: "social.near",