From a15a670b90522f9bc919dd0449c42960fe2893cb Mon Sep 17 00:00:00 2001
From: Elliot Braem <16282460+elliotBraem@users.noreply.github.com>
Date: Mon, 22 Jan 2024 23:13:38 -0500
Subject: [PATCH] init
---
apps/bos-blocks/bos.config.json | 3 +
apps/bos-blocks/widget/Compose.jsx | 99 +++++++
apps/bos-blocks/widget/Feed.jsx | 89 ++++++
apps/bos-blocks/widget/Library.jsx | 270 ++++++++++++++++++
.../widget/PR/FilteredIndexFeed.jsx | 11 +
apps/bos-blocks/widget/PR/IndexFeed.jsx | 192 +++++++++++++
apps/bos-blocks/widget/PR/MergedIndexFeed.jsx | 259 +++++++++++++++++
apps/bos-blocks/widget/Router.jsx | 72 +++++
apps/builddao/widget/app.jsx | 82 ++++++
apps/builddao/widget/config.jsx | 32 +++
apps/builddao/widget/page/feed.jsx | 49 ++++
apps/builddao/widget/page/home.jsx | 1 +
src/App.js | 31 +-
src/pages/Viewer.js | 74 +++++
14 files changed, 1256 insertions(+), 8 deletions(-)
create mode 100644 apps/bos-blocks/bos.config.json
create mode 100644 apps/bos-blocks/widget/Compose.jsx
create mode 100644 apps/bos-blocks/widget/Feed.jsx
create mode 100644 apps/bos-blocks/widget/Library.jsx
create mode 100644 apps/bos-blocks/widget/PR/FilteredIndexFeed.jsx
create mode 100644 apps/bos-blocks/widget/PR/IndexFeed.jsx
create mode 100644 apps/bos-blocks/widget/PR/MergedIndexFeed.jsx
create mode 100644 apps/bos-blocks/widget/Router.jsx
create mode 100644 apps/builddao/widget/app.jsx
create mode 100644 apps/builddao/widget/config.jsx
create mode 100644 apps/builddao/widget/page/feed.jsx
create mode 100644 apps/builddao/widget/page/home.jsx
create mode 100644 src/pages/Viewer.js
diff --git a/apps/bos-blocks/bos.config.json b/apps/bos-blocks/bos.config.json
new file mode 100644
index 00000000..25f7b186
--- /dev/null
+++ b/apps/bos-blocks/bos.config.json
@@ -0,0 +1,3 @@
+{
+ "appAccount": "devs.near"
+}
\ No newline at end of file
diff --git a/apps/bos-blocks/widget/Compose.jsx b/apps/bos-blocks/widget/Compose.jsx
new file mode 100644
index 00000000..203c52cf
--- /dev/null
+++ b/apps/bos-blocks/widget/Compose.jsx
@@ -0,0 +1,99 @@
+if (!context.accountId) {
+ return "";
+}
+
+const index = props.index || {
+ post: JSON.stringify({
+ key: "main",
+ value: {
+ type: "md",
+ },
+ }),
+};
+
+const composeData = () => {
+ if (props.appendHashtags) {
+ state.content.text = props.appendHashtags(state.content.text);
+ }
+ const data = {
+ post: {
+ main: JSON.stringify(state.content),
+ },
+ index,
+ };
+
+ const item = {
+ type: "social",
+ path: `${context.accountId}/post/main`,
+ };
+
+ const notifications = state.extractMentionNotifications(
+ state.content.text,
+ item
+ );
+
+ if (notifications.length) {
+ data.index.notify = JSON.stringify(
+ notifications.length > 1 ? notifications : notifications[0]
+ );
+ }
+
+ const hashtags = state.extractHashtags(state.content.text);
+
+ if (hashtags.length) {
+ data.index.hashtag = JSON.stringify(
+ hashtags.map((hashtag) => ({
+ key: hashtag,
+ value: item,
+ }))
+ );
+ }
+
+ return data;
+};
+
+State.init({
+ onChange: ({ content }) => {
+ State.update({ content });
+ },
+});
+
+return (
+ <>
+
+ {
+ State.update({ extractMentionNotifications, extractHashtags });
+ },
+ composeButton: (onCompose) => (
+ {
+ onCompose();
+ }}
+ >
+ Post
+
+ ),
+ }}
+ />
+
+ {state.content && (
+
+ )}
+ >
+);
diff --git a/apps/bos-blocks/widget/Feed.jsx b/apps/bos-blocks/widget/Feed.jsx
new file mode 100644
index 00000000..f252659e
--- /dev/null
+++ b/apps/bos-blocks/widget/Feed.jsx
@@ -0,0 +1,89 @@
+const Feed = ({ index, typeWhitelist, Item, Layout, showCompose }) => {
+ Item = Item || ((props) => {JSON.stringify(props)}
);
+ Layout = Layout || (({ children }) => children);
+
+ const renderItem = (a, i) => {
+ if (typeWhitelist && !typeWhitelist.includes(a.value.type)) {
+ return false;
+ }
+ return (
+
+
+
+ );
+ };
+
+ const composeIndex = () => {
+ const arr = Array.isArray(index) ? index : [index];
+
+ const grouped = arr.reduce((acc, i) => {
+ if (i.action !== "repost") {
+ if (!acc[i.action]) {
+ acc[i.action] = [];
+ }
+ acc[i.action].push({ key: i.key, value: { type: "md" } });
+ }
+ return acc;
+ }, {});
+
+ Object.keys(grouped).forEach((action) => {
+ if (grouped[action].length === 1) {
+ grouped[action] = grouped[action][0];
+ }
+ grouped[action] = JSON.stringify(grouped[action]);
+ });
+
+ return grouped;
+ };
+
+ const appendHashtags = (v) => {
+ const arr = Array.isArray(index) ? index : [index];
+ const hashtags = arr
+ .filter((i) => i.action === "hashtag")
+ .map((i) => i.key);
+
+ hashtags.forEach((hashtag) => {
+ if (v.toLowerCase().includes(`#${hashtag.toLowerCase()}`)) return;
+ else v += ` #${hashtag}`;
+ });
+
+ return v;
+ };
+
+ return (
+ <>
+ {showCompose && (
+
+ )}
+ {Array.isArray(index) ? (
+ {children} ,
+ }}
+ />
+ ) : (
+ {children} ,
+ }}
+ />
+ )}
+ >
+ );
+};
+
+return { Feed };
diff --git a/apps/bos-blocks/widget/Library.jsx b/apps/bos-blocks/widget/Library.jsx
new file mode 100644
index 00000000..c164e5b9
--- /dev/null
+++ b/apps/bos-blocks/widget/Library.jsx
@@ -0,0 +1,270 @@
+/**
+ * This is just a direct copy of mob.near/widget/N.Library
+ */
+const accountId = context.accountId || "root.near";
+const authorId = "mob.near";
+
+const itemDescription =
+ 'The identifier item. It will be used as a unique identifier of the entity that receives the action. It\'s also used as a key of the action in the index.\nThe item should be an object with the following keys: `type`, `path` and optional `blockHeight`.\n- `type`: If the data is stored in the social DB, then the type is likely `"social"`, other types can be defined in the standards.\n- `path`: The path to the item. For a `"social"` type, it\'s absolute path within SocialDB, e.g. `alice.near/post/main`.\n- `blockHeight`: An optional paremeter to indicate the block when the data was stored. Since SocialDB data can be overwritten to save storage, the exact data should be referenced by the block height (e.g. for a given post). But if the latest data should be used, then `blockHeight` should be ommited.\n\nExamples of `item`:\n- `{type: "social", path: "mob.near/widget/N.Library"}`\n- `{type: "social", path: "mob.near/post/main", blockHeight: 81101335}`\n';
+
+const components = [
+ {
+ title: "Feed",
+ // category: "Profile",
+ widgetName: "Feed",
+ description:
+ "",
+ // demoProps: { accountId },
+ // requiredProps: {
+ // accountId: "The account ID of the profile",
+ // },
+ // optionalProps: {
+ // profile: "Object that holds profile information to display",
+ // fast: "Render profile picture faster using external cache, default true if the `props.profile` is not provided",
+ // hideDescription: "Don't show description, default false",
+ // },
+ },
+ {
+ title: "Context Menu",
+ // category: "Profile",
+ widgetName: "ContextMenu",
+ description:
+ "",
+ // demoProps: { accountId, tooltip: true },
+ // requiredProps: {
+ // accountId: "The account ID of the profile",
+ // },
+ // optionalProps: {
+ // profile: "Object that holds profile information to display",
+ // fast: "Render profile picture faster using external cache, default true if the `props.profile` is not provided",
+ // tooltip:
+ // "Display overlay tooltip when you hover over the profile, default false",
+ // },
+ },
+ {
+ title: "Router",
+ // category: "Profile",
+ widgetName: "Router",
+ description:
+ "",
+ // demoProps: { accountId, tooltip: true },
+ // requiredProps: {
+ // accountId: "The account ID of the profile",
+ // },
+ // optionalProps: {
+ // link: "Whether to make profile clickable with a link to the profile page, default true.",
+ // hideAccountId: "Don't show account ID, default false",
+ // hideName: "Don't show profile name, default false",
+ // hideImage: "Don't show profile picture, default false",
+ // hideCheckmark: "Don't show premium checkmark, default false",
+ // profile: "Object that holds profile information to display",
+ // fast: "Render profile picture faster using external cache, default true if the `props.profile` is not provided",
+ // title:
+ // 'Optional title when you hover over the profile. Default `"${name} ${accountId}"`',
+ // tooltip:
+ // "Display overlay tooltip or title when you hover over the profile, default false. Will display a custom title if tooltip is given. If tooltip is true, the full tooltip is displayed. Default false",
+ // },
+ },
+];
+
+const renderProps = (props, optional) => {
+ return Object.entries(props || {}).map(([key, desc]) => {
+ return (
+
+
+
+ {key}
+
+
+
+
+
+
+ );
+ });
+};
+
+const renderComponent = (c, i) => {
+ const widgetSrc = `${authorId}/widget/${c.widgetName}`;
+ const embedCode = ` ` ${s}`)
+ .join("\n")}}}\n/>\n`;
+ const id = c.title.toLowerCase().replaceAll(" ", "-");
+ return (
+
+
+
+ {c.title}
+
+
{c.description}
+
Preview
+
+
+
+
Component
+
+
Props
+
+
+
+ Key
+ Description
+
+
+
+ {renderProps(c.requiredProps)}
+ {renderProps(c.optionalProps, true)}
+
+
+
Example
+
+
+ );
+};
+
+const renderMenuItem = (c, i) => {
+ const prev = i ? components[i - 1] : null;
+ const res = [];
+ if (!prev || prev.category !== c.category) {
+ res.push(
+
+ {c.category}
+
+ );
+ }
+ const id = c.title.toLowerCase().replaceAll(" ", "-");
+ res.push(
+
+ );
+ return res;
+};
+
+const Wrapper = styled.div`
+@media(min-width: 992px) {
+ .b-s {
+ border-left: 1px solid #eee;
+ }
+ .b-e {
+ border-right: 1px solid #eee;
+ }
+}
+.category:not(:first-child) {
+ margin-top: 1em;
+}
+.component {
+ padding: 0.5em 12px;
+ padding-bottom: 0;
+ margin-bottom: 3em;
+ margin: 0 -12px 3em;
+ position: relative;
+
+ &:hover {
+ background: rgba(0, 0, 0, 0.03);
+ }
+
+ .anchor {
+ position: absolute;
+ top: -70px;
+ }
+
+ table {
+ background: white;
+ }
+
+ label {
+ font-size: 20px;
+ }
+
+ .code {
+ display: inline-flex;
+ line-height: normal;
+ border-radius: 0.3em;
+ padding: 0 4px;
+ border: 1px solid #ddd;
+ background: rgba(0, 0, 0, 0.03);
+ font-family: var(--bs-font-monospace);
+ }
+ .path {
+
+ }
+ .preview {
+ background-color: white;
+ padding: 12px;
+ border: 1px solid #eee;
+ border-radius: 12px;
+ pre {
+ margin-bottom: 0;
+ }
+ }
+ .props {
+ .prop-key {
+ font-weight: 600;
+ &.optional {
+ font-weight: normal;
+ }
+ }
+ .prop-desc {
+ p {
+ margin-bottom: 0;
+ }
+ }
+ }
+ .embed-code {
+ position: relative;
+
+ .embed-copy {
+ position: absolute;
+ top: 18px;
+ right: 10px;
+ }
+ }
+}
+`;
+
+return (
+
+ Social Components Library
+
+ This library contains common social components used by near.social
+
+
+
{components.map(renderMenuItem)}
+
{components.map(renderComponent)}
+
+
+);
\ No newline at end of file
diff --git a/apps/bos-blocks/widget/PR/FilteredIndexFeed.jsx b/apps/bos-blocks/widget/PR/FilteredIndexFeed.jsx
new file mode 100644
index 00000000..4decaa9f
--- /dev/null
+++ b/apps/bos-blocks/widget/PR/FilteredIndexFeed.jsx
@@ -0,0 +1,11 @@
+const filter = context.accountId && {
+ ignore: Social.getr(`${context.accountId}/graph/hide`),
+};
+
+return (
+
+);
\ No newline at end of file
diff --git a/apps/bos-blocks/widget/PR/IndexFeed.jsx b/apps/bos-blocks/widget/PR/IndexFeed.jsx
new file mode 100644
index 00000000..651bc880
--- /dev/null
+++ b/apps/bos-blocks/widget/PR/IndexFeed.jsx
@@ -0,0 +1,192 @@
+const index = JSON.parse(JSON.stringify(props.index));
+if (!index) {
+ return "props.index is not defined";
+}
+
+const filter = props.filter;
+
+const renderItem =
+ props.renderItem ??
+ ((item, i) => (
+
+ #{item.blockHeight}: {JSON.stringify(item)}
+
+ ));
+const cachedRenderItem = (item, i) => {
+ const key = JSON.stringify(item);
+
+ if (!(key in state.cachedItems)) {
+ state.cachedItems[key] = renderItem(item, i);
+ State.update();
+ }
+ return state.cachedItems[key];
+};
+
+index.options = index.options || {};
+const initialRenderLimit =
+ props.initialRenderLimit ?? index.options.limit ?? 10;
+const addDisplayCount = props.nextLimit ?? initialRenderLimit;
+
+index.options.limit = Math.min(
+ Math.max(initialRenderLimit + addDisplayCount * 2, index.options.limit ?? 0),
+ 100
+);
+const reverse = !!props.reverse;
+
+const initialItems = Social.index(index.action, index.key, index.options);
+if (initialItems === null) {
+ return "";
+}
+
+const computeFetchFrom = (items, limit) => {
+ if (!items || items.length < limit) {
+ return false;
+ }
+ const blockHeight = items[items.length - 1].blockHeight;
+ return index.options.order === "desc" ? blockHeight - 1 : blockHeight + 1;
+};
+
+const mergeItems = (newItems) => {
+ const items = [
+ ...new Set([...newItems, ...state.items].map((i) => JSON.stringify(i))),
+ ].map((i) => JSON.parse(i));
+ items.sort((a, b) => a.blockHeight - b.blockHeight);
+ if (index.options.order === "desc") {
+ items.reverse();
+ }
+ return items;
+};
+
+const jInitialItems = JSON.stringify(initialItems);
+if (state.jInitialItems !== jInitialItems) {
+ const jIndex = JSON.stringify(index);
+ const nextFetchFrom = computeFetchFrom(initialItems, index.options.limit);
+ if (jIndex !== state.jIndex || nextFetchFrom !== state.initialNextFetchFrom) {
+ State.update({
+ jIndex,
+ jInitialItems,
+ items: initialItems,
+ fetchFrom: false,
+ initialNextFetchFrom: nextFetchFrom,
+ nextFetchFrom,
+ displayCount: initialRenderLimit,
+ cachedItems: {},
+ });
+ } else {
+ State.update({
+ jInitialItems,
+ items: mergeItems(initialItems),
+ });
+ }
+}
+
+if (state.fetchFrom) {
+ const limit = addDisplayCount;
+ const newItems = Social.index(
+ index.action,
+ index.key,
+ Object.assign({}, index.options, {
+ from: state.fetchFrom,
+ subscribe: undefined,
+ limit,
+ })
+ );
+ if (newItems !== null) {
+ State.update({
+ items: mergeItems(newItems),
+ fetchFrom: false,
+ nextFetchFrom: computeFetchFrom(newItems, limit),
+ });
+ }
+}
+
+const filteredItems = state.items;
+if (filter) {
+ if (filter.ignore) {
+ filteredItems = filteredItems.filter(
+ (item) => !(item.accountId in filter.ignore)
+ );
+ }
+}
+
+const maybeFetchMore = () => {
+ if (
+ filteredItems.length - state.displayCount < addDisplayCount * 2 &&
+ !state.fetchFrom &&
+ state.nextFetchFrom &&
+ state.nextFetchFrom !== state.fetchFrom
+ ) {
+ State.update({
+ fetchFrom: state.nextFetchFrom,
+ });
+ }
+};
+
+maybeFetchMore();
+
+const makeMoreItems = () => {
+ State.update({
+ displayCount: state.displayCount + addDisplayCount,
+ });
+ maybeFetchMore();
+};
+
+const loader = (
+
+
+ Loading ...
+
+);
+
+const fetchMore =
+ props.manual &&
+ !props.hideFetchMore &&
+ (state.fetchFrom && filteredItems.length < state.displayCount
+ ? loader
+ : state.displayCount < filteredItems.length && (
+
+ ));
+
+const items = filteredItems ? filteredItems.slice(0, state.displayCount) : [];
+if (reverse) {
+ items.reverse();
+}
+
+const renderedItems = items.map(cachedRenderItem);
+const Layout = props.Layout;
+
+return props.manual ? (
+ <>
+ {reverse && fetchMore}
+ {renderedItems}
+ {!reverse && fetchMore}
+ >
+) : (
+
+
+ Loading ...
+
+ }
+ >
+ {props.headerElement}
+ {Layout ? {renderedItems} : <>{renderedItems}>}
+ {props.footerElement}
+
+);
\ No newline at end of file
diff --git a/apps/bos-blocks/widget/PR/MergedIndexFeed.jsx b/apps/bos-blocks/widget/PR/MergedIndexFeed.jsx
new file mode 100644
index 00000000..9a62accb
--- /dev/null
+++ b/apps/bos-blocks/widget/PR/MergedIndexFeed.jsx
@@ -0,0 +1,259 @@
+if (!props.index) {
+ return "props.index is not defined";
+}
+const indices = JSON.parse(
+ JSON.stringify(Array.isArray(props.index) ? props.index : [props.index])
+);
+
+const filter = props.filter;
+
+const renderItem =
+ props.renderItem ??
+ ((item) => (
+
+ #{item.blockHeight}: {JSON.stringify(item)}
+
+ ));
+const cachedRenderItem = (item, i) => {
+ const key = JSON.stringify(item);
+
+ if (!(key in state.cachedItems)) {
+ state.cachedItems[key] = renderItem(item, i);
+ State.update();
+ }
+ return state.cachedItems[key];
+};
+
+const initialRenderLimit = props.initialRenderLimit ?? 10;
+const addDisplayCount = props.nextLimit ?? initialRenderLimit;
+const reverse = !!props.reverse;
+
+const computeFetchFrom = (items, limit, desc) => {
+ if (!items || items.length < limit) {
+ return false;
+ }
+ const blockHeight = items[items.length - 1].blockHeight;
+ return desc ? blockHeight - 1 : blockHeight + 1;
+};
+
+const mergeItems = (iIndex, oldItems, newItems, desc) => {
+ const index = indices[iIndex];
+ const items = [
+ ...new Set(
+ [
+ ...newItems.map((item) => ({
+ ...item,
+ action: index.action,
+ key: index.key,
+ index: iIndex,
+ })),
+ ...oldItems,
+ ].map((i) => JSON.stringify(i))
+ ),
+ ].map((i) => JSON.parse(i));
+ items.sort((a, b) => a.blockHeight - b.blockHeight);
+ if (desc) {
+ items.reverse();
+ }
+ return items;
+};
+
+const jIndices = JSON.stringify(indices);
+if (jIndices !== state.jIndices) {
+ State.update({
+ jIndices,
+ feeds: indices.map(() => ({})),
+ items: [],
+ displayCount: initialRenderLimit,
+ cachedItems: {},
+ });
+}
+
+let stateChanged = false;
+for (let iIndex = 0; iIndex < indices.length; ++iIndex) {
+ const index = indices[iIndex];
+ const feed = state.feeds[iIndex];
+ let feedChanged = false;
+ index.options = index.options || {};
+ index.options.limit = Math.min(
+ Math.max(initialRenderLimit + addDisplayCount * 2, index.options.limit),
+ 100
+ );
+ const desc = index.options.order === "desc";
+
+ const initialItems = Social.index(
+ index.action,
+ index.key,
+ index.options,
+ index.cacheOptions
+ );
+ if (initialItems === null) {
+ continue;
+ }
+
+ const jInitialItems = JSON.stringify(initialItems);
+ const nextFetchFrom = computeFetchFrom(
+ initialItems,
+ index.options.limit,
+ desc
+ );
+ if (feed.jInitialItems !== jInitialItems) {
+ feed.jInitialItems = jInitialItems;
+ feedChanged = true;
+ if (nextFetchFrom !== feed.initialNextFetchFrom) {
+ feed.fetchFrom = false;
+ feed.items = mergeItems(iIndex, [], initialItems, desc);
+ feed.initialNextFetchFrom = nextFetchFrom;
+ feed.nextFetchFrom = nextFetchFrom;
+ } else {
+ feed.items = mergeItems(iIndex, feed.items, initialItems, desc);
+ }
+ }
+
+ feed.usedCount = 0;
+
+ if (feedChanged) {
+ state.feeds[iIndex] = feed;
+ stateChanged = true;
+ }
+}
+
+// Construct merged feed and compute usage per feed.
+
+const filteredItems = [];
+while (filteredItems.length < state.displayCount) {
+ let bestItem = null;
+ for (let iIndex = 0; iIndex < indices.length; ++iIndex) {
+ const index = indices[iIndex];
+ const feed = state.feeds[iIndex];
+ const desc = index.options.order === "desc";
+ if (!feed.items) {
+ continue;
+ }
+ const item = feed.items[feed.usedCount];
+ if (!item) {
+ continue;
+ }
+ if (
+ bestItem === null ||
+ (desc
+ ? item.blockHeight > bestItem.blockHeight
+ : item.blockHeight < bestItem.blockHeight)
+ ) {
+ bestItem = item;
+ }
+ }
+ if (!bestItem) {
+ break;
+ }
+ state.feeds[bestItem.index].usedCount++;
+ if (filter) {
+ if (filter.ignore) {
+ if (bestItem.accountId in filter.ignore) {
+ continue;
+ }
+ }
+ }
+ filteredItems.push(bestItem);
+}
+
+// Fetch new items for feeds that don't have enough items.
+for (let iIndex = 0; iIndex < indices.length; ++iIndex) {
+ const index = indices[iIndex];
+ const feed = state.feeds[iIndex];
+ const desc = index.options.order === "desc";
+ let feedChanged = false;
+
+ if (
+ (feed.items.length || 0) - feed.usedCount < addDisplayCount * 2 &&
+ !feed.fetchFrom &&
+ feed.nextFetchFrom &&
+ feed.nextFetchFrom !== feed.fetchFrom
+ ) {
+ feed.fetchFrom = feed.nextFetchFrom;
+ feedChanged = true;
+ }
+
+ if (feed.fetchFrom) {
+ const limit = addDisplayCount;
+ const newItems = Social.index(
+ index.action,
+ index.key,
+ Object.assign({}, index.options, {
+ from: feed.fetchFrom,
+ subscribe: undefined,
+ limit,
+ })
+ );
+ if (newItems !== null) {
+ feed.items = mergeItems(iIndex, feed.items, newItems, desc);
+ feed.fetchFrom = false;
+ feed.nextFetchFrom = computeFetchFrom(newItems, limit, desc);
+ feedChanged = true;
+ }
+ }
+
+ if (feedChanged) {
+ state.feeds[iIndex] = feed;
+ stateChanged = true;
+ }
+}
+
+if (stateChanged) {
+ State.update();
+}
+
+const makeMoreItems = () => {
+ State.update({
+ displayCount: state.displayCount + addDisplayCount,
+ });
+};
+
+const loader = (
+
+
+ Loading ...
+
+);
+
+const fetchMore =
+ props.manual &&
+ (state.feeds.some((f) => !!f.fetchFrom) &&
+ filteredItems.length < state.displayCount
+ ? loader
+ : state.displayCount < filteredItems.length && (
+
+ ));
+
+const items = filteredItems ? filteredItems.slice(0, state.displayCount) : [];
+if (reverse) {
+ items.reverse();
+}
+
+const renderedItems = items.map(cachedRenderItem);
+const Layout = props.Layout;
+
+return props.manual ? (
+ <>
+ {reverse && fetchMore}
+ {renderedItems}
+ {!reverse && fetchMore}
+ >
+) : (
+
+ {Layout ? {renderedItems} : <>{renderedItems}>}
+
+);
\ No newline at end of file
diff --git a/apps/bos-blocks/widget/Router.jsx b/apps/bos-blocks/widget/Router.jsx
new file mode 100644
index 00000000..c6bea38c
--- /dev/null
+++ b/apps/bos-blocks/widget/Router.jsx
@@ -0,0 +1,72 @@
+// We eventually want to merge this with what we have in buildhub.near/widget/app
+
+const routes = props.routes;
+if (!routes) {
+ routes = [];
+}
+const Navigator = props.Navigator;
+
+State.init({
+ CurrentWidget: null,
+});
+
+function init() {
+ if (!state.CurrentWidget) {
+ // TODO: check from local storage or props
+ const initialSrc = Object.values(props.routes)[0].src;
+ State.update({ CurrentWidget: initialSrc });
+ // () =>
+ }
+}
+
+init();
+
+// Function to handle navigation
+function handleNavigate(newRoute, passProps) {
+ const currentSrc = props.routes[newRoute]?.src;
+ State.update({ CurrentWidget: currentSrc, passProps });
+}
+
+// const activePage = pages.find((p) => p.active);
+
+// const navigate = (v, params) => {
+// State.update({ page: v, project: params?.project });
+// const url = Url.construct("#//*__@appAccount__*//widget/home", params);
+// Storage.set("url", url);
+// };
+
+function RouterLink({ to, children, passProps }) {
+ return (
+ handleNavigate(to, passProps)}
+ key={"link-to-" + to}
+ style={{ cursor: "pointer" }}
+ >
+ {children}
+
+ );
+}
+
+// Render the current widget or a default message if the route is not found
+return (
+
+ {/* Navigation buttons -- this should be passed to a Navigator widget */}
+
+
+
+ {/** This could already render all of the children, but just put them as display none (lazy loading) */}
+ {state.CurrentWidget ? (
+
+ ) : (
+
{JSON.stringify(state.CurrentWidget)}
+ )}
+
+);
\ No newline at end of file
diff --git a/apps/builddao/widget/app.jsx b/apps/builddao/widget/app.jsx
new file mode 100644
index 00000000..5c2f5b60
--- /dev/null
+++ b/apps/builddao/widget/app.jsx
@@ -0,0 +1,82 @@
+const { page, layout, loading, ...passProps } = props;
+
+const { routes, theme } = VM.require("buildhub.near/widget/config") ?? {
+ routes: {},
+ theme: "background-color: red;"
+};
+
+const { AppLayout } = VM.require("every.near/widget/layout") || {
+ AppLayout: () => <>Layout loading...>,
+};
+
+if (!page) page = Object.keys(routes)[0] || "home";
+
+const Root = styled.div`
+ a {
+ color: inherit;
+ }
+
+ ${theme}
+
+ // can come from config
+`;
+
+const [activeRoute, setActiveRoute] = useState(page);
+
+useEffect(() => {
+ setActiveRoute(page);
+}, [page]);
+
+function Router({ active, routes }) { // this may be converted to a module at devs.near/widget/Router
+ const routeParts = active.split(".");
+
+ let currentRoute = routes;
+ let src = "";
+ let defaultProps = {};
+
+ for (let part of routeParts) {
+ if (currentRoute[part]) {
+ currentRoute = currentRoute[part];
+ src = currentRoute.path;
+
+ if (currentRoute.init) {
+ defaultProps = { ...defaultProps, ...currentRoute.init };
+ }
+ } else {
+ // Handle 404 or default case for unknown routes
+ return 404 Not Found
;
+ }
+ }
+
+ return (
+
+
+
+ );
+}
+
+const Container = styled.div`
+ display: flex;
+ height: 100vh;
+`;
+
+const Content = styled.div`
+ width: 100%;
+ height: 100%;
+ overflow: scroll;
+`;
+
+return (
+
+
+
+
+
+
+
+
+
+);
diff --git a/apps/builddao/widget/config.jsx b/apps/builddao/widget/config.jsx
new file mode 100644
index 00000000..2d71b26e
--- /dev/null
+++ b/apps/builddao/widget/config.jsx
@@ -0,0 +1,32 @@
+return {
+ "type": "app",
+ "routes": {
+ "home": {
+ "path": "buildhub.near/widget/page.home",
+ "blockHeight": "final",
+ "init": {
+ "name": "Home",
+ "icon": "bi bi-house"
+ }
+ },
+ "feed": {
+ "path": "buildhub.near/widget/page.feed",
+ "blockHeight": "final",
+ "init": {
+ "icon": "bi bi-globe"
+ }
+ },
+ "inspect": {
+ "path": "mob.near/widget/WidgetSource",
+ "blockHeight": "final",
+ "hide": true
+ },
+ "notifications": {
+ "path": "mob.near/widget/NotificationFeed",
+ "blockHeight": "final",
+ "hide": true
+ }
+ },
+ "theme": "background-color: white;"
+}
+;
\ No newline at end of file
diff --git a/apps/builddao/widget/page/feed.jsx b/apps/builddao/widget/page/feed.jsx
new file mode 100644
index 00000000..53cc3566
--- /dev/null
+++ b/apps/builddao/widget/page/feed.jsx
@@ -0,0 +1,49 @@
+const { Feed } = VM.require("devs.near/widget/Feed") || {
+ // this is being pulled from local apps/bos-blocks/widget/Feed
+ Feed: () => Feed loading...
,
+};
+
+// would a provider pattern be helpful here?
+// const { items } = props;
+
+const [activeItem, setActiveItem] = useState(null);
+
+function Sidebar() {
+ return ( // minimal styling, classnames from Theme
+ setActiveItem}>
+
sidebar
+
+ );
+}
+
+// can we take influence from the pattern in buildhub.near/widget/app?
+
+return (
+ <>
+
+ {JSON.stringify(p)}
}
+ />
+ >
+);
diff --git a/apps/builddao/widget/page/home.jsx b/apps/builddao/widget/page/home.jsx
new file mode 100644
index 00000000..c61d8eb7
--- /dev/null
+++ b/apps/builddao/widget/page/home.jsx
@@ -0,0 +1 @@
+return home
;
diff --git a/src/App.js b/src/App.js
index 2f8395ff..b0f84a8a 100644
--- a/src/App.js
+++ b/src/App.js
@@ -41,6 +41,7 @@ import ProposePage from "./pages/ProposePage";
import ViewPage from "./pages/ViewPage";
import ResourcesPage from "./pages/ResourcesPage";
import LibraryPage from "./pages/LibraryPage";
+import Viewer from "./pages/Viewer";
export const refreshAllowanceObj = {};
const documentationHref = "https://docs.near.org/bos";
@@ -80,17 +81,24 @@ function App() {
bundle: false,
}),
setupNightly(),
- setupKeypom({
+ setupKeypom({
networkId: NetworkId,
- signInContractId: NetworkId == "testnet" ? "v1.social08.testnet" : "social.near",
+ signInContractId:
+ NetworkId == "testnet" ? "v1.social08.testnet" : "social.near",
trialAccountSpecs: {
- url: NetworkId == "testnet" ? "https://test.nearbuilders.org/#trial-url/ACCOUNT_ID/SECRET_KEY" : "https://nearbuilders.org/#trial-url/ACCOUNT_ID/SECRET_KEY",
- modalOptions: KEYPOM_OPTIONS(NetworkId)
+ url:
+ NetworkId == "testnet"
+ ? "https://test.nearbuilders.org/#trial-url/ACCOUNT_ID/SECRET_KEY"
+ : "https://nearbuilders.org/#trial-url/ACCOUNT_ID/SECRET_KEY",
+ modalOptions: KEYPOM_OPTIONS(NetworkId),
},
instantSignInSpecs: {
- url: NetworkId == 'testnet' ? 'https://test.nearbuilders.org/#instant-url/ACCOUNT_ID/SECRET_KEY/MODULE_ID' : 'https://nearbuilders.org/#instant-url/ACCOUNT_ID/SECRET_KEY/MODULE_ID',
+ url:
+ NetworkId == "testnet"
+ ? "https://test.nearbuilders.org/#instant-url/ACCOUNT_ID/SECRET_KEY/MODULE_ID"
+ : "https://nearbuilders.org/#instant-url/ACCOUNT_ID/SECRET_KEY/MODULE_ID",
},
- }),
+ }),
],
}),
customElements: {
@@ -102,7 +110,7 @@ function App() {
if (props.to) {
props.to =
typeof props.to === "string" &&
- isValidAttribute("a", "href", props.to)
+ isValidAttribute("a", "href", props.to)
? props.to
: "about:blank";
}
@@ -195,8 +203,15 @@ function App() {
+
+ {/* I've added the below as the isolated route for rendering the app */}
+
+
+
+
+ {/* Legacy: */}
@@ -231,4 +246,4 @@ function App() {
);
}
-export default App;
\ No newline at end of file
+export default App;
diff --git a/src/pages/Viewer.js b/src/pages/Viewer.js
new file mode 100644
index 00000000..5310c7bd
--- /dev/null
+++ b/src/pages/Viewer.js
@@ -0,0 +1,74 @@
+import { Widget } from "near-social-vm";
+import React, { useEffect, useMemo, useState } from "react";
+import { useLocation, useParams } from "react-router-dom";
+
+const SESSION_STORAGE_REDIRECT_MAP_KEY = "nearSocialVMredirectMap";
+
+function Viewer({ code }) {
+ const { path } = useParams(); // get path from url, could be socialdb path or relative to "core"
+ const location = useLocation(); // get query params from url
+ const searchParams = new URLSearchParams(location.search);
+
+ // create props from params
+ const passProps = useMemo(() => {
+ return Array.from(searchParams.entries()).reduce((props, [key, value]) => {
+ props[key] = value;
+ return props;
+ }, {});
+ }, [location]);
+
+ const src = useMemo(() => {
+ const defaultSrc = "buildhub.near/widget/app"; // default widget to load
+ const pathSrc = path || defaultSrc; // if no path, load default widget
+ return pathSrc;
+ // const lastSlashIndex = pathSrc.lastIndexOf("/", pathSrc.indexOf(".near"));
+ // return lastSlashIndex !== -1
+ // ? pathSrc.substring(lastSlashIndex + 1)
+ // : defaultSrc;
+ }, [path]);
+
+ const [redirectMap, setRedirectMap] = useState(null);
+
+ useEffect(() => {
+ const fetchRedirectMap = async () => {
+ try {
+ const localStorageFlags = JSON.parse(
+ localStorage.getItem("flags") || "{}"
+ );
+ let redirectMapData;
+
+ if (localStorageFlags.bosLoaderUrl) {
+ const response = await fetch(localStorageFlags.bosLoaderUrl);
+ const data = await response.json();
+ redirectMapData = data.components;
+ } else {
+ redirectMapData = JSON.parse(
+ sessionStorage.getItem(SESSION_STORAGE_REDIRECT_MAP_KEY) || "{}"
+ );
+ }
+ setRedirectMap(redirectMapData);
+ } catch (error) {
+ console.error("Error fetching redirect map:", error);
+ }
+ };
+ fetchRedirectMap();
+ }, []);
+
+ console.log(
+ `gateway rendering: ${src} with props: ${JSON.stringify(passProps)}`
+ );
+
+ return (
+
+ );
+}
+
+export default Viewer;