From 6d5674996a097d42eefeb2a7183e85f71212428c Mon Sep 17 00:00:00 2001 From: Elliot Braem <16282460+elliotBraem@users.noreply.github.com> Date: Wed, 14 Feb 2024 18:14:24 -0800 Subject: [PATCH] common compose --- .../widget/Common/AccountAutocomplete.jsx | 144 +++++++ apps/devs.near/widget/Common/Compose.jsx | 374 ++++++++++++++++++ 2 files changed, 518 insertions(+) create mode 100644 apps/devs.near/widget/Common/AccountAutocomplete.jsx create mode 100644 apps/devs.near/widget/Common/Compose.jsx diff --git a/apps/devs.near/widget/Common/AccountAutocomplete.jsx b/apps/devs.near/widget/Common/AccountAutocomplete.jsx new file mode 100644 index 0000000..060fe0e --- /dev/null +++ b/apps/devs.near/widget/Common/AccountAutocomplete.jsx @@ -0,0 +1,144 @@ +if (!context.accountId || !props.term) return <>>; + +let results = []; +const profilesData = Social.get("*/profile/name", "final") || {}; +const followingData = Social.get( + `${context.accountId}/graph/follow/**`, + "final" +); + +if (!profilesData || !followingData) 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); + +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%; + background: linear-gradient(to left, rgb(55, 55, 55), rgba(55, 55, 55, 0)); + 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: 200px; + text-align: left; + 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: #687076; + transition: all 200ms; + + &:hover { + color: #000; + } +`; + +if (results.length === 0) return <>>; + +return ( + + + + + + + {results.map((result) => { + return ( + onResultClick(result.accountId)} + > + + + ); + })} + + +); diff --git a/apps/devs.near/widget/Common/Compose.jsx b/apps/devs.near/widget/Common/Compose.jsx new file mode 100644 index 0000000..294448d --- /dev/null +++ b/apps/devs.near/widget/Common/Compose.jsx @@ -0,0 +1,374 @@ +const { User } = VM.require("buildhub.near/widget/components") || { + User: () => <>>, +}; + +const autocompleteEnabled = props.autocompleteEnabled ?? true; +const onPreview = props.onPreview; + +const [editorKey, setEditorKey] = useState(0); +const memoizedEditorKey = useMemo(() => editorKey, [editorKey]); + +if (state.image === undefined) { + State.init({ + image: {}, + text: props.initialText || "", + }); + + if (props.onHelper) { + const 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]; + }; + + const extractHashtags = (text) => { + const hashtagRegex = /#(\w+)/gi; + hashtagRegex.lastIndex = 0; + const hashtags = new Set(); + for (const match of text.matchAll(hashtagRegex)) { + if ( + !/[\w`]/.test(match.input.charAt(match.index - 1)) && + !/[/\w`]/.test(match.input.charAt(match.index + match[0].length)) + ) { + hashtags.add(match[1].toLowerCase()); + } + } + return [...hashtags]; + }; + + const extractMentionNotifications = (text, item) => + extractMentions(text || "") + .filter((accountId) => accountId !== context.accountId) + .map((accountId) => ({ + key: accountId, + value: { + type: "mention", + item, + }, + })); + + props.onHelper({ + extractHashtags, + extractMentions, + extractTagNotifications: extractMentionNotifications, + extractMentionNotifications, + }); + } +} + +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, +}; + +if (content && props.extraContent) { + Object.assign(content, props.extraContent); +} + +function autoCompleteAccountId(id) { + let text = state.text.replace(/[\s]{0,1}@[^\s]*$/, ""); + text = `${text} @${id}`.trim() + " "; + State.update({ text, showAccountAutocomplete: false }); + setEditorKey((prev) => prev + 1); +} + +const onChange = (text) => { + const showAccountAutocomplete = /@[\w][^\s]*$/.test(text); + State.update({ text, showAccountAutocomplete }); +}; + +const jContent = JSON.stringify(content); +if (props.onChange && jContent !== state.jContent) { + State.update({ + jContent, + }); + props.onChange({ content }); +} + +const onCompose = () => { + State.update({ + image: {}, + text: "", + }); +}; + +const [gifSearch, setGifSearch] = useState(false); + +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 Wrapper = styled.div` + line-height: normal; + display: flex; + flex-direction: column; + gap: 1.5rem; + padding: 1.5rem; + + .right { + flex-grow: 1; + min-width: 0; + } + + .up-buttons { + margin-top: 12px; + } +`; + +const embedCss = ` +.rc-md-editor { + border: 0; +} +.rc-md-editor .editor-container>.section { + border: 0; +} +.rc-md-editor .editor-container .sec-md .input { + overflow-y: auto; + padding: 8px 0 !important; + line-height: normal; +} + +html { + background: #23242b; +} + +* { + border: none !important; +} + +.rc-md-editor { + background: #4f5055; + border-top: 1px solid #4f5055 !important; + border-radius: 8px; +} + +.editor-container { + background: #4f5055; +} + +.drop-wrap { + + border-radius: 0.5rem !important; +} + +.header-list { + display: flex; + align-items: center; +} + +textarea { + background: #23242b !important; + color: #fff !important; + + font-family: sans-serif !important; + font-size: 1rem; + + border: 1px solid #4f5055 !important; + border-top: 0 !important; + border-radius: 0 0 8px 8px; +} + +.rc-md-navigation { + background: #23242b !important; + border: 1px solid #4f5055 !important; + border-top: 0 !important; + border-bottom: 0 !important; + border-radius: 8px 8px 0 0; + + i { + color: #cdd0d5; + } +} + +.editor-container { + border-radius: 0 0 8px 8px; +} + +.rc-md-editor .editor-container .sec-md .input { + overflow-y: auto; + padding: 8px !important; + line-height: normal; + border-radius: 0 0 8px 8px; +} +`; + +const gifSvg = ( + + + + GIF + + +); + +const gifSearchWidget = useMemo( + () => + gifSearch ? ( + setGifSearch(false), + onSelect: (gif) => { + State.update({ + image: { url: gif.url }, + }); + setGifSearch(false); + }, + }} + /> + ) : undefined, + [gifSearch] +); + +const MemoizedAvatar = useMemo( + () => , + [context.accountId] +); + +useEffect(() => { + if (state.text === "") { + setEditorKey((prev) => prev + 1); + } +}, [state.text]); + +return ( + + {MemoizedAvatar} + + + + {autocompleteEnabled && state.showAccountAutocomplete && ( + + State.update({ showAccountAutocomplete: false }), + }} + /> + + )} + + + + + setGifSearch(!gifSearch)} + > + {gifSvg} + + + + {props.previewButton && props.previewButton()} + {props.composeButton && props.composeButton(onCompose)} + + + + {gifSearchWidget} + +);