Skip to content

Commit

Permalink
recovers autocomplete and attach image (#130)
Browse files Browse the repository at this point in the history
  • Loading branch information
elliotBraem authored Feb 8, 2024
1 parent e1d8e9a commit 570fada
Show file tree
Hide file tree
Showing 3 changed files with 333 additions and 17 deletions.
149 changes: 132 additions & 17 deletions apps/builddao/widget/Compose.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,20 @@ if (draft === null) {
return "";
}

const autocompleteEnabled = true;

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");

const [composeKey, setComposeKey] = useState(0);
const memoizedComposeKey = useMemo(() => composeKey, [composeKey]);
Expand All @@ -26,16 +36,6 @@ function generateUID() {

setPostContent(draft || props.template);

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;
Expand Down Expand Up @@ -88,6 +88,12 @@ function checkAndAppendHashtag(input, target) {
}
}

const content = {
type: "md",
image: state.image.cid ? { ipfs_cid: state.image.cid } : undefined,
text: postContent,
};

const postToCustomFeed = ({ feed, text }) => {
const requiredHashtags = props.requiredHashtags || ["build"];
if (feed.hashtag) requiredHashtags.push(feed.hashtag.toLowerCase());
Expand All @@ -111,12 +117,7 @@ const postToCustomFeed = ({ feed, text }) => {
// },
// },
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" } })
Expand Down Expand Up @@ -162,6 +163,43 @@ const postToCustomFeed = ({ feed, text }) => {
});
};

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;
Expand All @@ -173,6 +211,45 @@ 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`
Expand Down Expand Up @@ -371,6 +448,16 @@ const MarkdownPreview = 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 <User accountId={context.accountId} />;
}, [context.accountId]);
Expand All @@ -386,28 +473,56 @@ return (
key={memoizedComposeKey}
>
<Widget
src="mob.near/widget/MarkdownEditorIframe"
src={"buildhub.near/widget/components.MarkdownEditorIframe"}
props={{
initialText: postContent,
data: { handler: handler, content: postContent },
embedCss: props.customCSS || MarkdownEditor,
onChange: (v) => {
setPostContent(v);
textareaInputHandler(content);
Storage.privateSet(draftKey, v || "");
}
}}
/>
{autocompleteEnabled && showAccountAutocomplete && (
<Widget
src="buildhub.near/widget/components.AccountAutocomplete"
props={{
term: mentionInput,
onSelect: autoCompleteAccountId,
onClose: () => setShowAccountAutocomplete(false),
}}
/>
)}
</TextareaWrapper>
) : (
<MarkdownPreview>
<Widget
src="devhub.near/widget/devhub.components.molecule.MarkdownViewer"
props={{ text: postContent }}
/>
{state.image.cid && (
<Widget
src="mob.near/widget/Image"
props={{
image: state.image.cid
? { ipfs_cid: state.image.cid }
: undefined,
}}
/>
)}
</MarkdownPreview>
)}
</div>

<div className="d-flex gap-3 align-self-end">
{view === "editor" && (
<IpfsImageUpload
image={state.image}
className="upload-image-button bi bi-image"
/>
)}
<Button
variant="outline"
onClick={() => setView(view === "editor" ? "preview" : "editor")}
Expand Down
142 changes: 142 additions & 0 deletions apps/builddao/widget/components/AccountAutocomplete.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
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 (
<Wrapper>
<Scroller>
<CloseButton tabIndex={-1} type="button" onClick={props.onClose}>
<i className="bi bi-x-circle" />
</CloseButton>

{results.map((result) => {
return (
<ProfileCardWrapper>
<Widget
key={result.accountId}
src="near/widget/AccountProfile"
props={{
avatarSize: "34px",
accountId: result.accountId,
onClick: onResultClick,
overlayPlacement: "bottom"
}}
/>
</ProfileCardWrapper>
);
})}
</Scroller>
</Wrapper>
);
Loading

0 comments on commit 570fada

Please sign in to comment.