Skip to content

Commit

Permalink
Merge pull request #77 from Megha-Dev-19/update-compose
Browse files Browse the repository at this point in the history
Added autocomplete and image upload support to compose
  • Loading branch information
elliotBraem authored Jan 23, 2024
2 parents e5d20f7 + 8eedbb0 commit f262229
Show file tree
Hide file tree
Showing 4 changed files with 350 additions and 22 deletions.
155 changes: 134 additions & 21 deletions apps/builddao/widget/Compose.jsx
Original file line number Diff line number Diff line change
@@ -1,19 +1,29 @@
const { Avatar, Button } = VM.require("buildhub.near/widget/components");

Avatar = Avatar || (() => <></>);
Button = Button || (() => <></>);
const { Avatar, Button } = VM.require("buildhub.near/widget/components") || {
Avatar: () => <></>,
Button: () => <></>,
};

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

Expand All @@ -23,16 +33,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;
Expand Down Expand Up @@ -102,6 +102,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]: {
Expand All @@ -118,8 +124,7 @@ const postToCustomFeed = ({ feed, text, labels }) => {
// },
post: {
main: JSON.stringify({
type: "md",
text,
content,
// tags: tagsFromLabels(labels),
// postType: feed.name,
}),
Expand Down Expand Up @@ -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;
Expand All @@ -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`
Expand Down Expand Up @@ -425,28 +512,54 @@ return (
key={props.feed.name}
>
<Widget
src="mob.near/widget/MarkdownEditorIframe"
src={"buildhub.near/widget/components.MarkdownEditorIframe"}
props={{
initialText: postContent,
embedCss: MarkdownEditor,
onChange: (v) => {
setPostContent(v);
Storage.privateSet(draftKey, v || "");
data: { handler: handler, content: postContent },
onChange: (content) => {
textareaInputHandler(content);
},
embedCss: MarkdownEditor,
}}
/>
{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
6 changes: 5 additions & 1 deletion apps/builddao/widget/components.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,11 @@ function Pagination({

function Post(props) {
return (
<Widget src={"buildhub.near/widget/components.Post"} props={{ ...props }} />
<Widget
src={"buildhub.near/widget/components.Post"}
loading={<div className="w-100" style={{ height: "200px" }} />}
props={{ ...props }}
/>
);
}

Expand Down
147 changes: 147 additions & 0 deletions apps/builddao/widget/components/AccountAutocomplete.jsx
Original file line number Diff line number Diff line change
@@ -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 (
<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 f262229

Please sign in to comment.