/** Bundle generated by Além Library v1.0.0-beta.31 - See more here: https://github.com/wpdas/alem */ /** Project repository: git@github.com:wpdas/create-alem-dapp */ const updateAlemState = (updatedState) => { State.update({ alem: { ...state.alem, ...updatedState, }, });};const alemState = () => state.alem ;const AlemStateInitialBody = { alem: { ready: false, rootProps: props, alemEnvironment: "production", keepRoute: true, previousRoute: null, previousRouteParams: null, alemExternalStylesLoaded: false, alemExternalStylesBody: "", },};State.init(AlemStateInitialBody);State.update({ alem: { ...state.alem, rootProps: props } });const props = { ...props, alem: { ...state.alem, createRoute: (path, component) => ({ path, component }) , useParams: () => { let params = alemState().rootProps; return params; }, loadExternalStyles: (URLs) => { if (!URLs && !alemState().alemExternalStylesLoaded) { return; } let stylesBody = ""; const totalItems = URLs.length; let loadedCounter = 0; const loadStyle = (styleURL) => { asyncFetch(styleURL).then((response) => { Storage.set(styleURL, response.body); stylesBody += response.body; loadedCounter += 1; if (loadedCounter === totalItems) { updateAlemState({ alemExternalStylesLoaded: true, alemExternalStylesBody: stylesBody, }); } }); }; URLs.forEach((styleURL) => { props.alem.promisify( () => Storage.get(styleURL), (response) => { stylesBody += response; loadedCounter += 1; if (loadedCounter === totalItems) { updateAlemState({ alemExternalStylesLoaded: true, alemExternalStylesBody: stylesBody, }); } }, () => { loadStyle(styleURL); }, 100, ); }); return alemState().alemExternalStylesLoaded; }, promisify: ( caller, resolve, reject, _timeout, ) => { const timer = 100; const timeout = _timeout || 10000; let timeoutCheck = 0; const find = () => { const response = caller(); if (response !== undefined && response !== null) { resolve(response); } else { if (timeoutCheck < timeout) { setTimeout(find, timer); timeoutCheck += timer; } else { if (reject) { reject(); } } } }; find(); }, isDevelopment: alemState().alemEnvironment === "development", getAlemEnvironment: () => alemState().alemEnvironment, componentsCode: { Router: ` const props = props; const { routes, type, parameterName, alem, alemRoutes, initialRoute } = props; useEffect(() => { routes.forEach(route => { if (!route.component) { console.error(\`Routes: Invalid component for route "\${route.path}"\`); } if (!route.path) { console.error("Routes: Invalid path:", route.path); } }); }, [routes]); const { routeParameterName, routeType, activeRoute } = alemRoutes; const routeParamName = parameterName || routeParameterName; const checkIfPathIsIncludedToRoutes = routePath => { let pathFound = false; if (routes) { routes.forEach(routeItem => { if (pathFound) return; if (!pathFound) { pathFound = routeItem.path === routePath; } }); } return pathFound; }; useEffect(() => { const bosProps = alem.rootProps; if (routes) { let currentUrlPath = bosProps[routeParamName] && checkIfPathIsIncludedToRoutes(bosProps[routeParamName]) ? bosProps[routeParamName] : routes[0].path; const _routes = routes.map(route => route.path); const _type = type || "URLBased"; let _activeRoute = initialRoute || alemRoutes.activeRoute || currentUrlPath; let _activeRouteParams = null; let _history = null; if (alem.keepRoute && type === "ContentBased") { const storedRouteProps = Storage.privateGet("alem::keep-route"); if (storedRouteProps) { _history = storedRouteProps; const lastHistory = storedRouteProps[storedRouteProps.length - 1]; _activeRoute = lastHistory.route || null; _activeRouteParams = lastHistory.routeParams || null; } } if (!_activeRoute) { _activeRoute = routes[0].path; } if (!alemRoutes.routesInitialized) { alemRoutes.updateRouteParameters({ routes: _routes, routeType: _type, activeRoute: _activeRoute, routeParams: _activeRouteParams, history: _history, routeParameterName: routeParamName }); } } }, [routeType]); const Component = routes.find(route => route.path === activeRoute)?.component || routes[0].component || <></>; return <Component />; `, MessageInput: ` const APP_INDEX_KEY = props.alem.isDevelopment ? "bitbabble-dev-0" : "bitbabble-prod-0";const CHAT_LIST_KEY = props.alem.isDevelopment ? "chatlist-dev-0" : "chatlist-prod-0"; const buildChatId = (from, to) => \`\${APP_INDEX_KEY}--from--\${from}--to--\${to}\`; const registerMessage = ({ from, to, message, onComplete, onCancel}) => { const chatId = buildChatId(from, to); Social.set({ index: { [chatId]: JSON.stringify({ key: "messageData", value: { from, to, message, timestamp: Date.now() } }, undefined, 0) } }, { force: true, onCommit: onComplete, onCancel });}; const useContext = contextKey => { const wasContextInitialized = props[contextKey].initialized; if (!wasContextInitialized) { return {}; } const contextKeys = props[contextKey].keys; const contextItems = {}; contextKeys.forEach(key => { contextItems[key] = props[contextKey][key]; }); return contextItems;}; const useRoutes = () => { const contextData = useContext("alemRoutes"); if (!contextData) { console.error("useRoutes: You need to call \`RouterProvider()\` first."); } const data = { routesInitialized: contextData.routesInitialized, activeRoute: contextData.activeRoute, routeParameterName: contextData.routeParameterName, routes: contextData.routes, routeType: contextData.routeType, routeParams: contextData.routeParams, history: contextData.history }; return data;}; const {onSendMessage: onSendMessage} = props; const [message, setMessage] = useState(""); const { routeParams } = useRoutes(); const sendMessageHandler = () => { if (routeParams.accountId && context.accountId && message) { registerMessage({ from: context.accountId, to: routeParams.accountId, message, onComplete: () => { onSendMessage({ from: context.accountId, to: routeParams.accountId, message, timestamp: Date.now() }); console.log("Message sent!!!"); setMessage(""); }, onCancel: () => { console.warn("Message not sent."); } }); } }; return <div className="message-input"> <input type="text" placeholder="Message" value={message} onChange={event => setMessage(event.target.value)} onKeyDown={e => { if (e.key === "Enter") { sendMessageHandler(); } }} /> <div className="buttons"> {} <button onClick={sendMessageHandler}> <span className="material-symbols-outlined">send</span> </button> </div> </div>; `, Message: ` const en = \` locale: "en", long: { year: { previous: "last year", current: "this year", next: "next year", past: { one: "{0} year ago", other: "{0} years ago", }, future: { one: "in {0} year", other: "in {0} years", }, }, quarter: { previous: "last quarter", current: "this quarter", next: "next quarter", past: { one: "{0} quarter ago", other: "{0} quarters ago", }, future: { one: "in {0} quarter", other: "in {0} quarters", }, }, month: { previous: "last month", current: "this month", next: "next month", past: { one: "{0} month ago", other: "{0} months ago", }, future: { one: "in {0} month", other: "in {0} months", }, }, week: { previous: "last week", current: "this week", next: "next week", past: { one: "{0} week ago", other: "{0} weeks ago", }, future: { one: "in {0} week", other: "in {0} weeks", }, }, day: { previous: "yesterday", current: "today", next: "tomorrow", past: { one: "{0} day ago", other: "{0} days ago", }, future: { one: "in {0} day", other: "in {0} days", }, }, hour: { current: "this hour", past: { one: "{0} hour ago", other: "{0} hours ago", }, future: { one: "in {0} hour", other: "in {0} hours", }, }, minute: { current: "this minute", past: { one: "{0} minute ago", other: "{0} minutes ago", }, future: { one: "in {0} minute", other: "in {0} minutes", }, }, second: { current: "now", past: { one: "{0} second ago", other: "{0} seconds ago", }, future: { one: "in {0} second", other: "in {0} seconds", }, }, }, short: { year: { previous: "last yr.", current: "this yr.", next: "next yr.", past: "{0} yr. ago", future: "in {0} yr.", }, quarter: { previous: "last qtr.", current: "this qtr.", next: "next qtr.", past: { one: "{0} qtr. ago", other: "{0} qtrs. ago", }, future: { one: "in {0} qtr.", other: "in {0} qtrs.", }, }, month: { previous: "last mo.", current: "this mo.", next: "next mo.", past: "{0} mo. ago", future: "in {0} mo.", }, week: { previous: "last wk.", current: "this wk.", next: "next wk.", past: "{0} wk. ago", future: "in {0} wk.", }, day: { previous: "yesterday", current: "today", next: "tomorrow", past: { one: "{0} day ago", other: "{0} days ago", }, future: { one: "in {0} day", other: "in {0} days", }, }, hour: { current: "this hour", past: "{0} hr. ago", future: "in {0} hr.", }, minute: { current: "this minute", past: "{0} min. ago", future: "in {0} min.", }, second: { current: "now", past: "{0} sec. ago", future: "in {0} sec.", }, }, narrow: { year: { previous: "last yr.", current: "this yr.", next: "next yr.", past: "{0}y ago", future: "in {0}y", }, quarter: { previous: "last qtr.", current: "this qtr.", next: "next qtr.", past: "{0}q ago", future: "in {0}q", }, month: { previous: "last mo.", current: "this mo.", next: "next mo.", past: "{0}mo ago", future: "in {0}mo", }, week: { previous: "last wk.", current: "this wk.", next: "next wk.", past: "{0}w ago", future: "in {0}w", }, day: { previous: "yesterday", current: "today", next: "tomorrow", past: "{0}d ago", future: "in {0}d", }, hour: { current: "this hour", past: "{0}h ago", future: "in {0}h", }, minute: { current: "this minute", past: "{0}m ago", future: "in {0}m", }, second: { current: "now", past: "{0}s ago", future: "in {0}s", }, }, now: { now: { current: "now", future: "in a moment", past: "just now", }, }, mini: { year: "{0}yr", month: "{0}mo", week: "{0}wk", day: "{0}d", hour: "{0}h", minute: "{0}m", second: "{0}s", now: "now", }, "short-time": { year: "{0} yr.", month: "{0} mo.", week: "{0} wk.", day: { one: "{0} day", other: "{0} days", }, hour: "{0} hr.", minute: "{0} min.", second: "{0} sec.", }, "long-time": { year: { one: "{0} year", other: "{0} years", }, month: { one: "{0} month", other: "{0} months", }, week: { one: "{0} week", other: "{0} weeks", }, day: { one: "{0} day", other: "{0} days", }, hour: { one: "{0} hour", other: "{0} hours", }, minute: { one: "{0} minute", other: "{0} minutes", }, second: { one: "{0} second", other: "{0} seconds", }, }\`;en; const timeAgo = { moduleSetup: \` TimeAgo.addDefaultLocale({ \${en} }) \`, formatTimestamp: (timestamp, onComplete) => ({ setupCode: timeAgo.moduleSetup, code: \`new TimeAgo('en-US').format(new Date(\${timestamp}))\`, onComplete })}; const useModule = inputs => { const callId = Math.round(Date.now() * Math.random()); props.alemModulesContext.callModule(inputs.setupCode, inputs.code, callId, inputs.onComplete);}; const {messageData: messageData} = props; const itsMe = messageData.from === context.accountId; const [timeText, setTimeText] = useState("..."); useEffect(() => { if (messageData.timestamp) { useModule(timeAgo.formatTimestamp(messageData.timestamp, text => { setTimeText(text); })); } }, [messageData.timestamp]); if (itsMe) { return <div className="message-right"> <div className="text-container"> <p className="its-me">{messageData.message}</p> {timeText && <p className="time">{timeText}</p>} </div> </div>; } return <div className="message-left"> <div className="text-container"> <p className="its-not-me">{messageData.message}</p> {timeText && <p className="time">{timeText}</p>} </div> </div>; `, Chat: ` const getChatList = onComplete => { const fetchChatList = () => Storage.get(CHAT_LIST_KEY); const _onComplete = data => { if (onComplete) { onComplete(data); } }; props.alem.promisify(fetchChatList, _onComplete, () => _onComplete([]), 300); return fetchChatList();}; const updateChatList = friendAccountId => { getChatList(previousChatList => { const updatedChatList = [...(previousChatList || []), friendAccountId]; const filtered = updatedChatList.filter((item, itemIndex) => updatedChatList.indexOf(item) === itemIndex); Storage.set(CHAT_LIST_KEY, filtered); });}; const APP_INDEX_KEY = props.alem.isDevelopment ? "bitbabble-dev-0" : "bitbabble-prod-0";const CHAT_LIST_KEY = props.alem.isDevelopment ? "chatlist-dev-0" : "chatlist-prod-0"; const buildChatId = (from, to) => \`\${APP_INDEX_KEY}--from--\${from}--to--\${to}\`; const getMessages = ({ accountId, friendAccountId, onComplete}) => { const chatId = buildChatId(accountId, friendAccountId); const reverseChatId = buildChatId(friendAccountId, accountId); const fetchMessagesA = () => Social.index(chatId, "messageData", { subscribe: true, limit: 1000 }); const fetchMessagesB = () => Social.index(reverseChatId, "messageData", { subscribe: true, limit: 1000 }); props.alem.promisify(fetchMessagesA, dataA => { props.alem.promisify(fetchMessagesB, dataB => { const mergedData = [...dataA, ...dataB]; const sorted = mergedData.sort((m1, m2) => m1.blockHeight - m2.blockHeight); const mapped = sorted.map(msgData => ({ from: msgData.value.from, to: msgData.value.to, message: msgData.value.message, timestamp: msgData.value.timestamp })); onComplete(mapped); }); });}; const useContext = contextKey => { const wasContextInitialized = props[contextKey].initialized; if (!wasContextInitialized) { return {}; } const contextKeys = props[contextKey].keys; const contextItems = {}; contextKeys.forEach(key => { contextItems[key] = props[contextKey][key]; }); return contextItems;}; const useRoutes = () => { const contextData = useContext("alemRoutes"); if (!contextData) { console.error("useRoutes: You need to call \`RouterProvider()\` first."); } const data = { routesInitialized: contextData.routesInitialized, activeRoute: contextData.activeRoute, routeParameterName: contextData.routeParameterName, routes: contextData.routes, routeType: contextData.routeType, routeParams: contextData.routeParams, history: contextData.history }; return data;}; const { routeParams } = useRoutes(); const [messages, setMessages] = useState([]); useEffect(() => { const fetchMessages = () => { getMessages({ accountId: context.accountId, friendAccountId: routeParams.accountId, onComplete: (data) => { if (data.length >= messages.length) { setMessages(data); } updateChatList(routeParams.accountId); } }); }; const subscription = setInterval(() => { fetchMessages(); }, 5000); fetchMessages(); const kill = () => { clearInterval(subscription); }; return kill; }, [messages]); const onSendMessage = (messageData) => { const updatedMessages = [...messages, messageData]; setMessages(updatedMessages); }; return <div className="chat-container"> <div className="chat-messages"> {messages.map((messageData) => <Widget loading=" " code={props.alem.componentsCode.Message} props={{ ...{ messageData: messageData, ...props } }} />)} </div> <Widget loading=" " code={props.alem.componentsCode.MessageInput} props={{ ...{ onSendMessage: onSendMessage, ...props } }} /> </div>; `, ContactItems: ` const CenterMessage = ({ message}) => <div style={{ margin: "auto", paddingTop: "236px", width: "100%"}}> <div style={{ display: "flex", justifyContent: "center", alignItems: "center" }}> <p className="loading-message">{message}</p> </div> </div>; const Avatar = ({ name, image}) => { const nameParts = name ? name?.split(" ") : [""]; const finalName = \`\${nameParts[0][0]}\${nameParts[1] ? nameParts[1][0] : ""}\`; return <div className="avatar"> {!image ? <p className="avatar-text">{finalName}</p> : <img src={image} alt={\`Image of user \${name}\`} />} </div>;}; const getNftImage = (nft, onComplete) => { if (!nft.contractId || !nft.tokenId) return null; const processImage = (nftMetadata, tokenMetadata) => { let finalImageUrl = null; if (nftMetadata && tokenMetadata) { let tokenMedia = tokenMetadata.media || ""; finalImageUrl = tokenMedia.startsWith("https://") || tokenMedia.startsWith("http://") || tokenMedia.startsWith("data:image") ? tokenMedia : nftMetadata.base_uri ? \`\${nftMetadata.base_uri}/\${tokenMedia}\` : tokenMedia.startsWith("Qm") || tokenMedia.startsWith("ba") ? \`https://ipfs.near.social/ipfs/\${tokenMedia}\` : tokenMedia; if (!tokenMedia && tokenMetadata.reference) { if (nftMetadata.base_uri === "https://arweave.net" && !tokenMetadata.reference.startsWith("https://")) { const res = fetch(\`\${nftMetadata.base_uri}/\${tokenMetadata.reference}\`); finalImageUrl = res.body.media; } else if (tokenMetadata.reference.startsWith("https://") || tokenMetadata.reference.startsWith("http://")) { const res = fetch(tokenMetadata.reference); finalImageUrl = JSON.parse(res.body).media; } else if (tokenMetadata.reference.startsWith("ar://")) { const res = fetch(\`\${"https://arweave.net"}/\${tokenMetadata.reference.split("//")[1]}\`); finalImageUrl = JSON.parse(res.body).media; } } if (!finalImageUrl) { finalImageUrl = false; } } if (onComplete) { onComplete(finalImageUrl); } }; const getTokenMetadata = nftMetadata => { props.alem.promisify(() => Near.view(nft.contractId, "nft_token", { token_id: nft.tokenId }).metadata, tokenMetadata => { processImage(nftMetadata, tokenMetadata); }); }; const getNftMetadata = () => { props.alem.promisify(() => Near.view(nft.contractId, "nft_metadata"), nftMetadata => { getTokenMetadata(nftMetadata); }); }; getNftMetadata();}; const getProfileImage = (imageProp, onComplete) => { if (!onComplete) return; function toUrl(image) { return image.ipfs_cid ? \`https://ipfs.near.social/ipfs/\${image.ipfs_cid}\` : image.url; } if (imageProp.nft.contractId && imageProp.nft.tokenId) { return getNftImage(imageProp.nft, onComplete); } onComplete(toUrl(imageProp));}; const truncate = (text, maxLength) => text.length > maxLength ? text.substring(0, maxLength) + "..." : text; const getProfileInfo = accountId => { const profileInfo = Social.get(\`\${accountId}/profile/**\`); return profileInfo;}; const processContactProfileItems = (accountIds, onComplete) => { if (!accountIds || !onComplete) return; let _items = []; accountIds.forEach(accountId => { const profileInfo = getProfileInfo(accountId); if (profileInfo.name) { let name = truncate(profileInfo.name, 22); props.alem.promisify(() => Storage.privateGet(\`\${accountId}-profile-image\`), image => { if (image) { let profileImage = image; _items.push({ accountId, name, profileImage }); onComplete(_items); } }, () => { getProfileImage(profileInfo.image, image => { Storage.privateSet(\`\${accountId}-profile-image\`, image); let profileImage = image; _items.push({ accountId, name, profileImage }); onComplete(_items); }); }, 300); } });};const getContactProfileItems = (accountIds, onComplete) => { props.alem.promisify(() => processContactProfileItems(accountIds, onComplete), () => {});}; const {accountIds: accountIds, onSelect: onSelect} = props; const [items, setItems] = useState([]); const [ready, setReady] = useState(false); useMemo(() => { getContactProfileItems(accountIds, items => { const alphabeticallySortedItems = items.sort((a, b) => { if (a.name.toUpperCase() < b.name.toUpperCase()) return -1; if (a.name.toUpperCase() > b.name.toUpperCase()) return 1; return 0; }); setItems(alphabeticallySortedItems); setReady(true); }); }, [accountIds]); if (!ready) { <CenterMessage message="Loading..." />; } if (items.length === 0 && ready) { return <CenterMessage message="No contacts found. Your contacts are based on the people you follow." />; } const contentItems = items.map(item => <div className="chat-item" onClick={() => onSelect && onSelect(item)}> <div className="left"> <Avatar name={item.name} image={item.profileImage} /> <p className="text">{item.name}</p> </div> <span className="material-symbols-outlined">add_comment</span> </div>); return <>{contentItems}</>; `, Main: ` const BottomBar = () => { const routes = useRoutes(); const Items = ({ children, name, to, active }) => { const onClick = () => { navigate.to(to, { previous: routes.activeRoute }); }; return <button className={\`item \${active ? "active" : ""}\`} onClick={onClick}> {children} <p className="item-text">{name}</p> </button>; }; return <div className="bottombar"> <Items name="Chats" to={routesPath.CHATS_LIST} active={routes.activeRoute === routesPath.CHATS_LIST}> <span className="material-symbols-outlined">forum</span> </Items> {} <Items name="Contacts" to={routesPath.CONTACTS_LIST} active={routes.activeRoute === routesPath.CONTACTS_LIST}> <span className="material-symbols-outlined">list_alt</span> </Items> </div>;}; const TopBar = () => { const { routeParams, activeRoute } = useRoutes(); const onClickBackHandler = () => { navigate.back(); }; if (routeParams.name && activeRoute === routesPath.CHAT) { return <div className="topbar left"> <button onClick={onClickBackHandler}> <span className="material-symbols-outlined">arrow_back</span> </button> <Avatar image={routeParams.profileImage} /> <h3 className="topbar-title">{routeParams.name}</h3> </div>; } return <div className="topbar"> <h3 className="topbar-title">BitBabble</h3> </div>;}; const getChatList = onComplete => { const fetchChatList = () => Storage.get(CHAT_LIST_KEY); const _onComplete = data => { if (onComplete) { onComplete(data); } }; props.alem.promisify(fetchChatList, _onComplete, () => _onComplete([]), 300); return fetchChatList();}; const ChatsList = () => { const chats = getChatList(); const onSelect = (profileInfo) => { navigate.to(routesPath.CHAT, profileInfo); }; if (!chats) { <CenterMessage message="Loading..." />; } return <div className="contacts-list"> {chats.length === 0 ? <CenterMessage message="No chat has started yet" /> : <Widget loading=" " code={props.alem.componentsCode.ContactItems} props={{ ...{ accountIds: chats, onSelect: onSelect, ...props } }} />} </div>; }; const Avatar = ({ name, image}) => { const nameParts = name ? name?.split(" ") : [""]; const finalName = \`\${nameParts[0][0]}\${nameParts[1] ? nameParts[1][0] : ""}\`; return <div className="avatar"> {!image ? <p className="avatar-text">{finalName}</p> : <img src={image} alt={\`Image of user \${name}\`} />} </div>;}; const ChatItem = ({ name, onSelectChat}) => { return <div className="chat-item"> <div className="left"> <Avatar name={name} /> <p className="text">{name}</p> </div> <span className="material-symbols-outlined">arrow_forward_ios</span> </div>;}; const normalizeRoomName = text => text.split("-").map(str => str[0].toUpperCase() + str.substring(1)).join(" "); const useGroups = () => useContext("groups-context"); const GroupsList = () => { const { groupsList, ready } = useGroups(); if (!ready) { return <CenterMessage message="Loading..." />; } return <> <div className="contacts-list"> {groupsList && groupsList.map(group => <ChatItem name={normalizeRoomName(group.value)} />)} {groupsList.length === 0 && ready && <CenterMessage message="No groups found" />} </div> </>;}; const APP_INDEX_KEY = props.alem.isDevelopment ? "bitbabble-dev-0" : "bitbabble-prod-0";const CHAT_LIST_KEY = props.alem.isDevelopment ? "chatlist-dev-0" : "chatlist-prod-0"; const getGroupsList = () => { const data = Social.index(APP_INDEX_KEY, "room", { limit: 1000, order: "desc" }); if (!data) return null; const sorted = data.sort((m1, m2) => m1.blockHeight - m2.blockHeight); return sorted;}; const createContext = contextKey => { const setDefaultData = defaultStateValue => { if (!state[contextKey] || !state[contextKey].initialized) { const stateKeys = Object.keys(defaultStateValue); let mainKeys = [...stateKeys]; mainKeys = mainKeys.filter((item, index) => mainKeys.indexOf(item) === index); State.update({ ...state, [contextKey]: { initialized: true, keys: mainKeys, ...defaultStateValue } }); } props = { ...props, ...state, [contextKey]: { ...state[contextKey] } }; }; const updateData = updates => { const updatedState = { [contextKey]: { ...state[contextKey], ...updates } }; State.update(updatedState); props = { ...props, ...updatedState }; }; const getSelf = () => props[contextKey]; return { setDefaultData, updateData, getSelf };}; const GroupsContext = () => { const { setDefaultData, updateData, getSelf } = createContext("groups-context"); setDefaultData({ groupsList: [], ready: false, updateGroupsList: updatedGroupsList => { if (!updatedGroupsList) return; updateData({ groupsList: updatedGroupsList, ready: true }); } }); const self = getSelf(); if (!self.ready) { const groupsList = getGroupsList(); if (groupsList) { self.updateGroupsList(groupsList); } }};const GroupsProvider = ({ children}) => { GroupsContext(); return <>{children}</>;}; const GroupsListScreen = () => { return <GroupsProvider> <GroupsList /> </GroupsProvider>;}; const getFollowings = accountId => { const following = Social.keys(\`\${accountId}/graph/follow/*\`, "final", { return_type: "BlockHeight", values_only: true }); if (following) { return Object.keys(following[accountId].graph.follow); } return null;}; const CenterMessage = ({ message}) => <div style={{ margin: "auto", paddingTop: "236px", width: "100%"}}> <div style={{ display: "flex", justifyContent: "center", alignItems: "center" }}> <p className="loading-message">{message}</p> </div> </div>; const navigate = { to: (route, params) => { const routeContext = useContext("alemRoutes"); if (!routeContext) { console.error("navigate is being used without Router on top of it."); } if (props.alem.isDevelopment && routeContext.routeType === "URLBased") { console.warn('The route type is "URLBased", "navigate" should only be used with the "ContentBased" type.'); } if (routeContext.routes.includes(route)) { routeContext.updateRouteParameters({ ...routeContext, activeRoute: route, routeParams: params || {} }); } }, back: () => { const routeContext = useContext("alemRoutes"); if (!routeContext) { console.error("navigate is being used without Router on top of it."); } if (props.alem.isDevelopment && routeContext.routeType === "URLBased") { console.warn('The route type is "URLBased", "navigate" should only be used with the "ContentBased" type.'); } const updatedHistory = routeContext.history; if (updatedHistory) { updatedHistory.pop(); const routeProps = updatedHistory.at(-1); if (routeProps.route) { routeContext.updateRouteParameters({ ...routeContext, history: updatedHistory, activeRoute: routeProps.route, routeParams: routeProps.routeParams }); } } }}; const ContactsList = () => { const contacts = getFollowings(context.accountId); const onSelectHandler = (profileInfo) => { navigate.to(routesPath.CHAT, profileInfo); }; return <div className="contacts-list"> {!contacts && <CenterMessage message="Loading..." />} {contacts && contacts.length === 0 ? <CenterMessage message="No chat has started yet" /> : <Widget loading=" " code={props.alem.componentsCode.ContactItems} props={{ ...{ accountIds: contacts, onSelect: onSelectHandler, ...props } }} />} </div>; }; const routesPath = { CHATS_LIST: "chats-list", CONTACTS_LIST: "contacts-list", GROUPS_LIST: "groups-list", CHAT: "chat"}; const Routes = () => { const routes = [props.alem.createRoute(routesPath.CHATS_LIST, () => <ChatsList />), props.alem.createRoute(routesPath.CONTACTS_LIST, () => <ContactsList />), props.alem.createRoute(routesPath.GROUPS_LIST, () => <GroupsListScreen />), props.alem.createRoute(routesPath.CHAT, () => <Widget loading=" " code={props.alem.componentsCode.Chat} props={{ ...{ ...props } }} />)]; return <Widget loading=" " code={props.alem.componentsCode.Router} props={{ ...{ routes: routes, type: "ContentBased", ...props } }} />; }; const useContext = contextKey => { const wasContextInitialized = props[contextKey].initialized; if (!wasContextInitialized) { return {}; } const contextKeys = props[contextKey].keys; const contextItems = {}; contextKeys.forEach(key => { contextItems[key] = props[contextKey][key]; }); return contextItems;}; const useRoutes = () => { const contextData = useContext("alemRoutes"); if (!contextData) { console.error("useRoutes: You need to call \`RouterProvider()\` first."); } const data = { routesInitialized: contextData.routesInitialized, activeRoute: contextData.activeRoute, routeParameterName: contextData.routeParameterName, routes: contextData.routes, routeType: contextData.routeType, routeParams: contextData.routeParams, history: contextData.history }; return data;}; const { activeRoute } = useRoutes(); const accountId = context.accountId; const Content = useMemo(() => () => accountId ? <Routes /> : <CenterMessage message="You need to Sign in before using this chat." />, [accountId, activeRoute]); return <div className="main-container"> <div className="top-bar-margin" /> <TopBar /> <Content /> {activeRoute !== routesPath.CHAT && accountId && <BottomBar />} <div className="bottom-bar-margin" /> </div>; `, App: ` const APP_INDEX_KEY = props.alem.isDevelopment ? "bitbabble-dev-0" : "bitbabble-prod-0";const CHAT_LIST_KEY = props.alem.isDevelopment ? "chatlist-dev-0" : "chatlist-prod-0"; const cleanUpChatList = () => Storage.set(CHAT_LIST_KEY, []); const Spinner = () => <div style={{ margin: "auto", paddingTop: "236px", width: "100%"}}> <div style={{ display: "flex", justifyContent: "center", alignItems: "center" }}> <span className="spinner"></span> </div> </div>; const ModulesContext = () => { const { setDefaultData, updateData, getSelf } = createContext("alemModulesContext"); setDefaultData({ calls: {}, callModule: (setupCode, code, callId, onComplete) => { if (!code || !callId) return; const codeStructure = \` \${setupCode}; event.source.postMessage({response: \${code.replaceAll(";", "")}, forCallId: \${callId}}, "*"); \`; const updatedCalls = { ...getSelf().calls }; updatedCalls[callId] = { code: codeStructure, handler: onComplete }; updateData({ calls: updatedCalls }); }, removeCall: callId => { const updatedCalls = {}; const currentCalls = getSelf().calls; const calls = Object.keys(currentCalls); calls.forEach(call => { if (call !== callId.toString()) { updatedCalls[call] = currentCalls[call]; } }); updateData({ calls: updatedCalls }); } });}; const ModulesProvider = () => { ModulesContext(); const modulesHandler = \` <script src="https://unpkg.com/javascript-time-ago@2.5.9/bundle/javascript-time-ago.js" crossorigin></script> <script> window.addEventListener("message", (event) => { if (event.data.code) { eval(event.data.code); } }, false); </script> \`; const modules = useContext("alemModulesContext"); const calls = modules.calls; const callsKeys = Object.keys(calls); return <> {callsKeys.map(callKey => <iframe style={{ height: 0, width: 0 }} srcDoc={modulesHandler} message={{ code: calls[callKey].code }} onMessage={message => { if (message) { calls[message.forCallId].handler(message.response); modules.removeCall(message.forCallId); } }} />)} </>;}; const createContext = contextKey => { const setDefaultData = defaultStateValue => { if (!state[contextKey] || !state[contextKey].initialized) { const stateKeys = Object.keys(defaultStateValue); let mainKeys = [...stateKeys]; mainKeys = mainKeys.filter((item, index) => mainKeys.indexOf(item) === index); State.update({ ...state, [contextKey]: { initialized: true, keys: mainKeys, ...defaultStateValue } }); } props = { ...props, ...state, [contextKey]: { ...state[contextKey] } }; }; const updateData = updates => { const updatedState = { [contextKey]: { ...state[contextKey], ...updates } }; State.update(updatedState); props = { ...props, ...updatedState }; }; const getSelf = () => props[contextKey]; return { setDefaultData, updateData, getSelf };}; const useContext = contextKey => { const wasContextInitialized = props[contextKey].initialized; if (!wasContextInitialized) { return {}; } const contextKeys = props[contextKey].keys; const contextItems = {}; contextKeys.forEach(key => { contextItems[key] = props[contextKey][key]; }); return contextItems;}; const ALEM_ROUTES_CONTEXT_KEY = "alemRoutes";const RouterContext = () => { const { setDefaultData, updateData, getSelf } = createContext(ALEM_ROUTES_CONTEXT_KEY); const updateAlemRoutesState = updatedState => { updateData({ ...updatedState }); }; const alemRoutesState = () => useContext(ALEM_ROUTES_CONTEXT_KEY); setDefaultData({ routesInitialized: false, activeRoute: "", routeParams: {}, history: [], routeParameterName: "path", routes: [], routeType: "URLBased", updateRouteParameters: routeProps => { const currentHistory = alemRoutesState().history; const hasPreviousHistory = currentHistory.length === 0 && routeProps.history; const updatedHistory = hasPreviousHistory ? routeProps.history : alemRoutesState().history; if (routeProps.activeRoute) { const newHistory = { route: routeProps.activeRoute, routeParams: routeProps.routeParams }; if (updatedHistory.length > 10) { updatedHistory.shift(); } if (updatedHistory.at(-1).route !== routeProps.activeRoute) { updatedHistory.push(newHistory); } } updateAlemRoutesState({ routes: routeProps.routes || getSelf().routes, routeType: routeProps.routeType || getSelf().routeType, activeRoute: routeProps.activeRoute || getSelf().activeRoute, routeParams: routeProps.routeParams || getSelf().routeParams, routeParameterName: routeProps.routeParameterName || getSelf().routeParameterName, history: updatedHistory, routesInitialized: true }); if (props.alem.keepRoute && routeProps.activeRoute) { Storage.privateSet("alem::keep-route", updatedHistory); } } });}; const ready = props.alem.loadExternalStyles(["https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@24,400,0,0"]); RouterContext(); useEffect(() => { if (!context.accountId) { cleanUpChatList(); } }, [context.accountId]); return <div className="App"> <ModulesProvider /> {ready ? <Widget loading=" " code={props.alem.componentsCode.Main} props={{ ...{ ...props } }} /> : <Spinner />} <div className="App-bg" /> </div>; `, }, },};if (props.alem.keepRoute) { if (!props.alem.ready) { props.alem.promisify( () => Storage.privateGet("alem::keep-route"), (data) => { updateAlemState({ previousRoute: data.route, previousRouteParams: data.routeParams, ready: true, }); }, () => { updateAlemState({ previousRoute: null, ready: true, }); }, 300, ); }} else { updateAlemState({ previousRoute: null, ready: true, });}const alemCssBody = `.App { text-align: center; margin-top: calc(-1 * var(--body-top-padding, 0));}.App-bg { background-color: #F1FFFD; /* background-color: #E6F0F0; */ /* background-image: linear-gradient(180deg, #E6F0F0 0%, #A8CBD6 100%); */ position: fixed; top: 0; left: 0; right: 0; bottom: 0; z-index: -2;}.main-container { display: flex; flex-direction: column; width: 100%;}/* http://meyerweb.com/eric/tools/css/reset/ v2.0 | 20110126 License: none (public domain)*/html, body, div, span, applet, object, iframe,h1, h2, h3, h4, h5, h6, p, blockquote, pre,a, abbr, acronym, address, big, cite, code,del, dfn, em, img, ins, kbd, q, s, samp,small, strike, strong, sub, sup, tt, var,b, u, i, center,dl, dt, dd, ol, ul, li,fieldset, form, label, legend,table, caption, tbody, tfoot, thead, tr, th, td,article, aside, canvas, details, embed, figure, figcaption, footer, header, hgroup, menu, nav, output, ruby, section, summary,time, mark, audio, video { margin: 0; padding: 0; border: 0; font-size: 100%; font: inherit; vertical-align: baseline;}/* HTML5 display-role reset for older browsers */article, aside, details, figcaption, figure, footer, header, hgroup, menu, nav, section { display: block;}body { line-height: 1;}ol, ul { list-style: none;}blockquote, q { quotes: none;}blockquote:before, blockquote:after,q:before, q:after { content: ''; content: none;}table { border-collapse: collapse; border-spacing: 0;}.contacts-list { display: flex; flex-direction: column; /* gap: 0.5rem; */ padding: 0.5rem; /* background-color: #171923; */ width: 100%;}.chat-container { display: flex; .chat-messages { display: flex; flex-direction: column; width: 100%; }}.topbar { display: flex; position: fixed; width: 100%; left: 0px; /* top: 56px; */ top: calc(-1 * var(--body-top-padding, 0) + 80px); background-color: #103144; padding: 1rem; justify-content: center; @media screen and (max-width: 442px) { top: calc(-1 * var(--body-top-padding, 0) + 100px); }}.left { justify-content: flex-start; align-items: center; gap: 1rem; button { background-color: #103144; height: 34px; border: none; :hover { background-color: #436e87; } }}.topbar-title { color: #F1FFFD; font-weight: 600; font-size: 1.3rem;}.top-bar-margin { width: 100%; height: 90px;}/* https://cssloaders.github.io/ */.spinner { width: 38px; height: 38px; border: 3px solid #103144; border-bottom-color: transparent; border-radius: 50%; display: inline-block; box-sizing: border-box; animation: rotation 0.6s linear infinite; } @keyframes rotation { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } } .message-input { bottom: 0px; left: 0px; width: 100%; position: fixed; display: flex; background-color: #F1FFFD; padding: 1rem; justify-content: space-between; align-items: center; gap: 1rem; input { background-color: #436e87; border-radius: 80px; border: none; font-weight: 600; font-size: 0.875rem; box-shadow: none!important; color: #fff; padding: 0.5rem; padding-left: 20px; ::placeholder { color: #e3e3e3; } } .buttons { display: flex; flex-direction: row; gap: 0.5rem; button { border-radius: 9999px; width: 38px; height: 38px; justify-content: center; align-items: center; display: flex; background-color: rgb(23, 25, 35); border: none; :hover { background-color: rgb(66, 70, 99); } } }}.message-right, .message-left { display: flex; justify-content: right; width: 100%; margin-top: 0.75rem; padding-left: 3rem; padding-right: 1rem; .its-me { color: #FFF; padding: 8px 12px; } .its-not-me { color: #2C5067; padding: 8px 12px; } .text-container { display: flex; flex-direction: column; align-items: flex-end; background-color: #2C5067; border-radius: 12px; border-top-right-radius: 0; text-align: right; } .time { color: #ffffffc6; margin-top: -5px; padding-bottom: 8px; padding-right: 12px; font-size: 13px; padding-left: 1rem; }}.message-left { justify-content: left; padding-left: 1rem; padding-right: 3rem; .text-container { align-items: flex-start; background-color: #D9E7E8; border-radius: 12px; border-top-left-radius: 0; text-align: left; } .time { color: #2c5067b7; padding-left: 12px; padding-right: 1rem; }}.chat-item { cursor: pointer; display: flex; padding: 16px 8px; align-items: center; justify-content: space-between; border-bottom: 1px solid #c9dad5; :hover { background-color: #D9F3EB; } :active { background-color: #c2e9de; } .left { display: flex; align-items: flex-start; align-items: center; } .text { color: #103144; font-weight: bold; margin-left: 1rem; }}.chat-item { cursor: pointer; display: flex; padding: 16px 8px; align-items: center; justify-content: space-between; border-bottom: 1px solid #c9dad5; :hover { background-color: #D9F3EB; } :active { background-color: #c2e9de; } .left { display: flex; align-items: flex-start; align-items: center; } .text { color: #103144; font-weight: bold; margin-left: 1rem; }}.loading-message { color: #1031449a; font-weight: 600;}.bottombar { bottom: 0px; left: 0px; width: 100%; position: fixed; display: flex; background-color: #103144; padding: 1rem 2.5rem; justify-content: space-around; .active { p, span { color: #EEFFFB; } span { background-color: #1f5574; border-radius: 0.5rem; } } .item { background-color: transparent; border: none; display: flex; flex-direction: column; align-items: center; } .item-text { margin-top: 0.2rem; font-weight: 600; color: #bbc9c5; } span { padding: 0.15rem 1rem; color: #bbc9c5; }}.bottom-bar-margin { width: 100%; height: 90px;}.avatar { display: flex; /* background-color: #e2e8f0; */ border: 2px solid #103144; border-radius: 9999px; width: 32px; height: 32px; justify-content: center; align-items: center; img { width: 32px; height: 32px; border-radius: 9999px; }}.avatar-text { color: #4a5568; font-size: 0.9rem; font-weight: 600;}`;const AlemTheme = styled.div` ${state.alem.alemExternalStylesBody} ${alemCssBody}`;const AlemApp = useMemo(() => { if (!props.alem.ready) { return ""; } const Container = styled.div` display: flex; margin-top: 48%; justify-content: center; width: 100%; `; const Loading = () => ( <Container> <div className="spinner-border text-secondary" role="status" /> </Container> ); return ( <AlemTheme> <Widget loading={<Loading />} code={props.alem.componentsCode.App} props={{ alem: props.alem }} /> </AlemTheme> );}, [props.alem.ready, props.alem.alemExternalStylesBody, props.alem.rootProps]);return AlemApp;