diff --git a/packages/chat/.env.example b/packages/chat/.env.example new file mode 100644 index 000000000..049c517f5 --- /dev/null +++ b/packages/chat/.env.example @@ -0,0 +1,11 @@ +# Application configuration +VITE_CHAIN=LTC +VITE_NETWORK=regtest +VITE_URL=http://127.0.0.1:1031 + +# Application Port +VITE_PORT=1032 + +# Smart Contract Locations +# Run 'npm run deploy' and copy the output here +VITE_CHAT_MOD_SPEC=b7fa7873fb6fcf7f13555c6cf18cc7361ba7ff9f146f50b502e2bc5abbe2b28a:0 diff --git a/packages/chat/.eslintrc b/packages/chat/.eslintrc deleted file mode 100644 index 669d4a8f8..000000000 --- a/packages/chat/.eslintrc +++ /dev/null @@ -1,25 +0,0 @@ -{ - "parser": "@typescript-eslint/parser", - "extends": ["airbnb-base", "prettier"], - "plugins": [], - "env": { - "jest": true - }, - "globals": { - "document": true, - "window": true - }, - "rules": { - "semi": ["error", "never"], - "import/extensions": "off", - "lines-between-class-members": "off", - "import/prefer-default-export": "off", - "no-underscore-dangle": [ - "error", - { - "allowAfterThis": true, - "allow": ["_readers", "_owners", "_amount", "_id", "_rev", "_root"] - } - ] - } -} diff --git a/packages/chat/.gitignore b/packages/chat/.gitignore index 274ff2da6..9aad0ad64 100644 --- a/packages/chat/.gitignore +++ b/packages/chat/.gitignore @@ -10,6 +10,7 @@ # production /build +/dist # misc .DS_Store diff --git a/packages/chat/.prettierrc b/packages/chat/.prettierrc index a65b64ade..b7a412b57 100644 --- a/packages/chat/.prettierrc +++ b/packages/chat/.prettierrc @@ -1,6 +1,6 @@ { "printWidth": 100, "semi": false, - "singleQuote": true, + "singleQuote": false, "trailingComma": "none" } diff --git a/packages/chat/README.md b/packages/chat/README.md index 04ec1d2e2..e9da1ab96 100644 --- a/packages/chat/README.md +++ b/packages/chat/README.md @@ -57,6 +57,7 @@ Have a look at the [docs](https://docs.bitcoincomputer.io/) for the Bitcoin Comp If you have any questions, please let us know on Telegram, Twitter, or by email clemens@bitcoincomputer.io. ## Development Status + See [here](https://github.com/bitcoin-computer/monorepo/tree/main/packages/lib#development-status). ## Price diff --git a/packages/chat/eslint.config.js b/packages/chat/eslint.config.js new file mode 100644 index 000000000..092408a9f --- /dev/null +++ b/packages/chat/eslint.config.js @@ -0,0 +1,28 @@ +import js from '@eslint/js' +import globals from 'globals' +import reactHooks from 'eslint-plugin-react-hooks' +import reactRefresh from 'eslint-plugin-react-refresh' +import tseslint from 'typescript-eslint' + +export default tseslint.config( + { ignores: ['dist'] }, + { + extends: [js.configs.recommended, ...tseslint.configs.recommended], + files: ['**/*.{ts,tsx}'], + languageOptions: { + ecmaVersion: 2020, + globals: globals.browser, + }, + plugins: { + 'react-hooks': reactHooks, + 'react-refresh': reactRefresh, + }, + rules: { + ...reactHooks.configs.recommended.rules, + 'react-refresh/only-export-components': [ + 'warn', + { allowConstantExport: true }, + ], + }, + }, +) diff --git a/packages/chat/imgs/chat-screen.png b/packages/chat/imgs/chat-screen.png deleted file mode 100644 index d0cc04bde..000000000 Binary files a/packages/chat/imgs/chat-screen.png and /dev/null differ diff --git a/packages/chat/index.html b/packages/chat/index.html new file mode 100644 index 000000000..3ed11b3ea --- /dev/null +++ b/packages/chat/index.html @@ -0,0 +1,13 @@ + + + + + + + Bitcoin Computer Chat App + + +
+ + + diff --git a/packages/chat/package.json b/packages/chat/package.json index 51dd52114..972339fd2 100644 --- a/packages/chat/package.json +++ b/packages/chat/package.json @@ -1,41 +1,42 @@ { - "name": "@bitcoin-computer/chat", - "version": "0.22.0-beta.0", + "name": "chat", "private": true, - "dependencies": { - "@bitcoin-computer/lib": "^0.22.0-beta.0", - "@testing-library/jest-dom": "^4.2.4", - "@testing-library/react": "^9.3.2", - "@testing-library/user-event": "^7.1.2", - "react": "^18.2.0", - "react-dom": "^18.2.0", - "react-router-dom": "^6.3.0", - "react-scripts": "5.0.1", - "web-vitals": "^2.1.4" - }, + "version": "0.22.0-beta.0", + "type": "module", "scripts": { - "start": "react-scripts start", - "build": "react-scripts build", - "eject": "react-scripts eject", - "lint": "eslint src", - "lint-fix": "eslint src --fix" - }, - "eslintConfig": { - "extends": "react-app" + "dev": "vite", + "build": "tsc -b && vite build", + "lint": "eslint .", + "preview": "vite preview", + "test": "vitest", + "deploy": "node --loader ts-node/esm scripts/deploy.ts" }, - "browserslist": { - "production": [ - ">0.2%", - "not dead", - "not op_mini all" - ], - "development": [ - "last 1 chrome version", - "last 1 firefox version", - "last 1 safari version" - ] + "dependencies": { + "@bitcoin-computer/components": "^0.22.0-beta.0", + "@bitcoin-computer/lib": "^0.22.0-beta.0", + "flowbite": "^2.3.0", + "react": "^18.3.1", + "react-dom": "^18.3.1" }, "devDependencies": { - "@babel/plugin-proposal-private-property-in-object": "^7.21.11" + "@eslint/js": "^9.9.0", + "@testing-library/jest-dom": "^6.5.0", + "@testing-library/react": "^16.0.1", + "@testing-library/user-event": "^14.5.2", + "@types/react": "^18.3.3", + "@types/react-dom": "^18.3.0", + "@vitejs/plugin-react": "^4.3.1", + "autoprefixer": "^10.4.20", + "eslint": "^9.9.0", + "eslint-plugin-react-hooks": "^5.1.0-rc.0", + "eslint-plugin-react-refresh": "^0.4.9", + "globals": "^15.9.0", + "jsdom": "^25.0.0", + "postcss": "^8.4.44", + "tailwindcss": "^3.4.10", + "typescript": "^5.5.3", + "typescript-eslint": "^8.0.1", + "vite": "^5.4.1", + "vitest": "^2.0.5" } } diff --git a/packages/chat/postcss.config.js b/packages/chat/postcss.config.js new file mode 100644 index 000000000..2e7af2b7f --- /dev/null +++ b/packages/chat/postcss.config.js @@ -0,0 +1,6 @@ +export default { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +} diff --git a/packages/chat/public/BitcoinComputer-Logo.png b/packages/chat/public/BitcoinComputer-Logo.png new file mode 100644 index 000000000..539aeba1a Binary files /dev/null and b/packages/chat/public/BitcoinComputer-Logo.png differ diff --git a/packages/chat/public/favicon.ico b/packages/chat/public/favicon.ico deleted file mode 100644 index e6e12e617..000000000 Binary files a/packages/chat/public/favicon.ico and /dev/null differ diff --git a/packages/chat/public/index.html b/packages/chat/public/index.html deleted file mode 100644 index 606576c7a..000000000 --- a/packages/chat/public/index.html +++ /dev/null @@ -1,43 +0,0 @@ - - - - - - - - - - - - - React App - - - -
- - - diff --git a/packages/chat/public/logo.png b/packages/chat/public/logo.png new file mode 100644 index 000000000..64678214a Binary files /dev/null and b/packages/chat/public/logo.png differ diff --git a/packages/chat/public/logo192.png b/packages/chat/public/logo192.png deleted file mode 100644 index fc44b0a37..000000000 Binary files a/packages/chat/public/logo192.png and /dev/null differ diff --git a/packages/chat/public/logo512.png b/packages/chat/public/logo512.png deleted file mode 100644 index a4e47a654..000000000 Binary files a/packages/chat/public/logo512.png and /dev/null differ diff --git a/packages/chat/public/manifest.json b/packages/chat/public/manifest.json deleted file mode 100644 index 080d6c77a..000000000 --- a/packages/chat/public/manifest.json +++ /dev/null @@ -1,25 +0,0 @@ -{ - "short_name": "React App", - "name": "Create React App Sample", - "icons": [ - { - "src": "favicon.ico", - "sizes": "64x64 32x32 24x24 16x16", - "type": "image/x-icon" - }, - { - "src": "logo192.png", - "type": "image/png", - "sizes": "192x192" - }, - { - "src": "logo512.png", - "type": "image/png", - "sizes": "512x512" - } - ], - "start_url": ".", - "display": "standalone", - "theme_color": "#000000", - "background_color": "#ffffff" -} diff --git a/packages/chat/public/robots.txt b/packages/chat/public/robots.txt deleted file mode 100644 index e9e57dc4d..000000000 --- a/packages/chat/public/robots.txt +++ /dev/null @@ -1,3 +0,0 @@ -# https://www.robotstxt.org/robotstxt.html -User-agent: * -Disallow: diff --git a/packages/chat/public/vite.svg b/packages/chat/public/vite.svg new file mode 100644 index 000000000..e7b8dfb1b --- /dev/null +++ b/packages/chat/public/vite.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/chat/scripts/deploy.ts b/packages/chat/scripts/deploy.ts new file mode 100644 index 000000000..1089d5551 --- /dev/null +++ b/packages/chat/scripts/deploy.ts @@ -0,0 +1,53 @@ +import { config } from "dotenv" +import * as readline from "node:readline/promises" +import { stdin as input, stdout as output } from "node:process" +import { Computer } from "@bitcoin-computer/lib" +import { ChatSc } from "../src/contracts/chat.js" + +config() + +const rl = readline.createInterface({ input, output }) + +const { VITE_CHAIN: chain, VITE_NETWORK: network, VITE_URL: url, MNEMONIC: mnemonic } = process.env + +if (network !== "regtest") { + if (!mnemonic) throw new Error("Please set MNEMONIC in the .env file") +} + +const computer = new Computer({ chain, network, mnemonic, url }) +if (network === "regtest") { + await computer.faucet(2e8) +} + +const balance = await computer.wallet.getBalance() + +// Summary +console.log(`Chain \x1b[2m${chain}\x1b[0m +Network \x1b[2m${network}\x1b[0m +Node Url \x1b[2m${url}\x1b[0m +Address \x1b[2m${computer.wallet.address}\x1b[0m +Mnemonic \x1b[2m${mnemonic}\x1b[0m +Balance \x1b[2m${balance.balance / 1e8}\x1b[0m`) + +const answer = await rl.question("\nDo you want to deploy the contract? \x1b[2m(y/n)\x1b[0m") +if (answer === "n") { + console.log("\n Aborting...\n") +} else { + console.log("\n * Deploying ChatSc contract...") + const chatModSpec = await computer.deploy(`export ${ChatSc}`) + + console.log(` +Successfully deployed chat smart contract. + +----------------- + ACTION REQUIRED +----------------- + +(1) Update the following rows in your .env file. + +VITE_CHAT_MOD_SPEC\x1b[2m=${chatModSpec}\x1b[0m +(2) Run 'npm start' to start the application. +`) +} + +rl.close() diff --git a/packages/chat/src/App.css b/packages/chat/src/App.css new file mode 100644 index 000000000..74b5e0534 --- /dev/null +++ b/packages/chat/src/App.css @@ -0,0 +1,38 @@ +.App { + text-align: center; +} + +.App-logo { + height: 40vmin; + pointer-events: none; +} + +@media (prefers-reduced-motion: no-preference) { + .App-logo { + animation: App-logo-spin infinite 20s linear; + } +} + +.App-header { + background-color: #282c34; + min-height: 100vh; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + font-size: calc(10px + 2vmin); + color: white; +} + +.App-link { + color: #61dafb; +} + +@keyframes App-logo-spin { + from { + transform: rotate(0deg); + } + to { + transform: rotate(360deg); + } +} diff --git a/packages/chat/src/App.js b/packages/chat/src/App.js deleted file mode 100644 index 097805ede..000000000 --- a/packages/chat/src/App.js +++ /dev/null @@ -1,67 +0,0 @@ -import React, { useState } from 'react' -import { BrowserRouter as Router, Routes, Route } from 'react-router-dom' -import { Computer } from '@bitcoin-computer/lib' -import Wallet from './Wallet' -import Chat from './Chat' -import SideBar from './SideBar' -import useInterval from './useInterval' - -/** - * This is a simple chat app that demonstrates how to use the @bitcoin-computer/lib. - */ -function App() { - const getConf = () => ({ - chain: window.localStorage.getItem('CHAIN'), - // the BIP_39_KEY is set on login and we fetch it from local storage - mnemonic: window.localStorage.getItem('BIP_39_KEY') - }) - - // To connect the app to a local Bitcoin Computer node set "network" to "regtest" - const [config] = useState(getConf()) - const [computer, setComputer] = useState(null) - const [chats, setChats] = useState([]) - - useInterval(() => { - const isLoggedIn = config.mnemonic && config.chain - - // if you are currently logging in - if (isLoggedIn && !computer) { - setComputer(new Computer(config)) - // eslint-disable-next-line no-console - console.log(`Bitcoin Computer created on chain ${ config.chain}`) - // if you are currently logging out - } else if (!isLoggedIn && computer) { - // eslint-disable-next-line no-console - console.log('You have been logged out') - setComputer(null) - } - }, 5000) - - useInterval(() => { - const refresh = async () => { - if (computer) { - const revs = await computer.query({ publicKey: computer.getPublicKey() }) - setChats(await Promise.all(revs.map(async (rev) => computer.sync(rev)))) - } - } - refresh() - }, 7000) - - return ( - -
- {/* bind the value of chain stored in the state to the child component */} - - - -
- - } /> - -
-
-
- ) -} - -export default App diff --git a/packages/chat/src/App.test.js b/packages/chat/src/App.test.js deleted file mode 100644 index 55d18b05a..000000000 --- a/packages/chat/src/App.test.js +++ /dev/null @@ -1,9 +0,0 @@ -import React from 'react' -import { render } from '@testing-library/react' -import App from './App' - -test('renders learn react link', () => { - const { getByText } = render() - const linkElement = getByText(/Public Key/i) - expect(linkElement).toBeInTheDocument() -}) diff --git a/packages/chat/src/App.test.tsx b/packages/chat/src/App.test.tsx new file mode 100644 index 000000000..c53ac5c74 --- /dev/null +++ b/packages/chat/src/App.test.tsx @@ -0,0 +1,10 @@ +import { screen, render } from "@testing-library/react" +import App from "./App" + +describe("App", () => { + it("renders the App component", () => { + render() + const linkElement = screen.getByText(/All Counters/i) + expect(linkElement).toBeInTheDocument() + }) +}) diff --git a/packages/chat/src/App.tsx b/packages/chat/src/App.tsx new file mode 100644 index 000000000..350a432d5 --- /dev/null +++ b/packages/chat/src/App.tsx @@ -0,0 +1,49 @@ +import "./App.css" +import { useEffect, useState } from "react" +import { BrowserRouter, Route, Routes, Navigate } from "react-router-dom" +import { initFlowbite } from "flowbite" +import { + Auth, + Error404, + UtilsContext, + Wallet, + SmartObject, + Transaction, + ComputerContext +} from "@bitcoin-computer/components" +import Mint from "./components/Mint" +import { Chats } from "./components/Chats" +import { Navbar } from "./components/Navbar" + +export default function App() { + const [computer] = useState(Auth.getComputer()) + + useEffect(() => { + initFlowbite() + }, []) + + return ( + + + + + + + +
+ + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + +
+
+
+
+ ) +} diff --git a/packages/chat/src/Chat.js b/packages/chat/src/Chat.js deleted file mode 100644 index c01465c93..000000000 --- a/packages/chat/src/Chat.js +++ /dev/null @@ -1,56 +0,0 @@ -import React, { useState, useEffect } from 'react' -import { useParams } from 'react-router-dom' -import InviteUser from './InviteUser' - -function Chat({ computer }) { - const [message, setMessage] = useState('') - const [chat, setChat] = useState({ messages: [] }) - const [refresh, setRefresh] = useState(null) - - const { rev } = useParams() - - useEffect(() => { - const refreshChat = async () => { - if (computer) { - const [latestRev] = await computer.query({ ids: [rev] }) - setChat(await computer.sync(latestRev)) - } - } - refreshChat() - }, [rev, computer, refresh]) - - useEffect(() => { - setTimeout(() => setRefresh(refresh + 1), 5000) - }, [refresh]) - - const send = async (e) => { - e.preventDefault() - const username = window.localStorage.getItem('USER_NAME') - const line = `${username}: ${message}` - try { - await chat.post(line) - // eslint-disable-next-line no-console - console.log(`Sent message ${line}\n chat id ${chat._id}\n chat rev ${chat._rev}`) - } catch (error) { - if (error.message.startsWith('Insufficient balance in address')) { - // eslint-disable-next-line no-alert, no-undef - alert('You have to fund your wallet') - } - } - setMessage('') - } - - return ( -
- -
- -
- setMessage(e.target.value)} /> - -
-
- ) -} - -export default Chat diff --git a/packages/chat/src/InviteUser.js b/packages/chat/src/InviteUser.js deleted file mode 100644 index 497c30234..000000000 --- a/packages/chat/src/InviteUser.js +++ /dev/null @@ -1,22 +0,0 @@ -import React from 'react' - -function InviteUser({ chat }) { - const inviteUser = async (e) => { - try { - e.preventDefault() - // eslint-disable-next-line no-alert, no-undef - const publicKey = prompt('Enter the public key of a friend and send them the url.') - await chat.invite(publicKey) - } catch (err) { - // eslint-disable-next-line no-console - console.log(err) - } - } - return ( -
- -
- ) -} - -export default InviteUser diff --git a/packages/chat/src/Login.js b/packages/chat/src/Login.js deleted file mode 100644 index 9313a0bec..000000000 --- a/packages/chat/src/Login.js +++ /dev/null @@ -1,82 +0,0 @@ -import React, { useState } from 'react' -import useInterval from './useInterval' - -function Login() { - const [password, setPassword] = useState('') - const [username, setUsername] = useState('') - const [loggedIn, setLoggedIn] = useState(false) - const [chain, setChain] = useState('LTC') - - useInterval(() => { - setLoggedIn(!!window.localStorage.getItem('BIP_39_KEY')) - }, 500) - - const login = (e) => { - e.preventDefault() - window.localStorage.setItem('BIP_39_KEY', password) - window.localStorage.setItem('USER_NAME', username) - window.localStorage.setItem('CHAIN', chain) - } - - const logout = (e) => { - e.preventDefault() - window.localStorage.removeItem('BIP_39_KEY') - window.localStorage.removeItem('USER_NAME') - window.localStorage.removeItem('CHAIN') - } - - return loggedIn ? ( - <> - -
- - ) : ( -
-
-
-

Chat - By Bitcoin Computer

-
- -
- setUsername(e.target.value)} - /> -
- setPassword(e.target.value)} - /> -
- -
-
- {' '} - Need A Seed (Password?){' '} - - Click Here - -
-
-
-
- ) -} - -export default Login diff --git a/packages/chat/src/SideBar.js b/packages/chat/src/SideBar.js deleted file mode 100644 index 4b9dfc293..000000000 --- a/packages/chat/src/SideBar.js +++ /dev/null @@ -1,43 +0,0 @@ -import React from 'react' -import { Link } from 'react-router-dom' -import StartChat from './StartChat' - -function SideBar({ chats, computer }) { - return ( -
- -
- {chats.map((object) => ( - - {object._id.substr(0, 16)} -
-
- ))} -
- This chat runs on the -
- - - Bitcoin Computer - - -
- - - View on Github - - -
-
- ) -} - -export default SideBar diff --git a/packages/chat/src/StartChat.js b/packages/chat/src/StartChat.js deleted file mode 100644 index 84594043f..000000000 --- a/packages/chat/src/StartChat.js +++ /dev/null @@ -1,40 +0,0 @@ -import React from 'react' -import { useNavigate } from 'react-router-dom' -import ChatSc from './chat-sc' - -function StartChat({ computer }) { - const navigate = useNavigate() - - const createChat = async (e) => { - try { - e.preventDefault() - const publicKey = computer.getPublicKey() - // eslint-disable-next-line no-console - console.log('creating chat') - let chat - try { - if ((await computer.getBalance()).balance < 100) { - await computer.faucet(1e7) - } - chat = await computer.new(ChatSc, [publicKey]) - } catch (err) { - if (err.message.startsWith('Insufficient balance in address')) - // eslint-disable-next-line no-alert, no-undef - alert('You have to fund your wallet') - } - // eslint-disable-next-line no-console - console.log('created chat', chat) - navigate(`/chat/${chat._id}`) - } catch (err) { - // eslint-disable-next-line no-console - console.log('error creating chat', err) - } - } - return ( -
- -
- ) -} - -export default StartChat diff --git a/packages/chat/src/Wallet.js b/packages/chat/src/Wallet.js deleted file mode 100644 index 6e6caafc8..000000000 --- a/packages/chat/src/Wallet.js +++ /dev/null @@ -1,35 +0,0 @@ -import React, { useState } from 'react' -import Login from './Login' -import useInterval from './useInterval' - -function Wallet({ computer, chain }) { - const [balance, setBalance] = useState(0) - - useInterval(() => { - const getBalance = async () => { - if (computer) setBalance((await computer.getBalance()).balance) - } - getBalance() - }, 3000) - - return ( -
- - Public Key {computer ? computer.getPublicKey() : ''} -
-
- - Balance {balance / 1e8} {chain} - - - Address {computer ? computer.getAddress() : ''} -
-
- - - -
- ) -} - -export default Wallet diff --git a/packages/chat/src/assets/react.svg b/packages/chat/src/assets/react.svg new file mode 100644 index 000000000..6c87de9bb --- /dev/null +++ b/packages/chat/src/assets/react.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/chat/src/chat-sc.js b/packages/chat/src/chat-sc.js deleted file mode 100644 index c0c0c075f..000000000 --- a/packages/chat/src/chat-sc.js +++ /dev/null @@ -1,17 +0,0 @@ -// eslint-disable-next-line no-undef -export default class ChatSc extends Contract { - constructor(publicKey) { - super({ - messages: [], - _owners: [publicKey], - }) - } - - post(message) { - this.messages.push(message) - } - - invite(publicKey) { - this._owners.push(publicKey) - } -} diff --git a/packages/chat/src/components/Assets.tsx b/packages/chat/src/components/Assets.tsx new file mode 100644 index 000000000..abc9e1e16 --- /dev/null +++ b/packages/chat/src/components/Assets.tsx @@ -0,0 +1,14 @@ +import { Auth, Gallery } from "@bitcoin-computer/components" +import { VITE_CHAT_MOD_SPEC } from "../constants/modSpecs" + +const publicKey = Auth.getComputer().getPublicKey() + +// How to prevent users from accessing other chats +export function MyAssets() { + return ( + <> +

My Chats

+ + + ) +} diff --git a/packages/chat/src/components/Chat.tsx b/packages/chat/src/components/Chat.tsx new file mode 100644 index 000000000..6e147d4ee --- /dev/null +++ b/packages/chat/src/components/Chat.tsx @@ -0,0 +1,350 @@ +import { ComputerContext, Modal, sleep, UtilsContext } from "@bitcoin-computer/components" +import { useContext, useEffect, useState } from "react" +import { useNavigate } from "react-router-dom" +import { HiRefresh, HiUserAdd } from "react-icons/hi" + +import { ChatSc } from "../contracts/chat" +const addUserModal = "add-user-modal" + +interface messageI { + text: string + publicKey: string + time: string +} +const getInitials = (name: string | undefined) => { + if (!name) { + return "" + } + const names = name.trim().split(" ") + if (names.length === 1) return names[0].charAt(0).toUpperCase() + return (names[0].charAt(0) + names[1].charAt(0)).toUpperCase() +} + +const getColor = (publicKey: string) => { + return `#${publicKey.slice(0, 6)}` +} + +const getInitialsFromPublicKey = (publicKey: string) => { + return (publicKey[0].charAt(0) + publicKey[3].charAt(0)).toUpperCase() +} + +const formatTime = (str: string) => { + const date = new Date(parseInt(str)) + let hours = date.getHours() + let minutes = date.getMinutes() + + // Format time + const formattedTime = `${hours < 10 ? "0" + hours : hours}:${minutes < 10 ? "0" + minutes : minutes}` + + // Format date as dd mmm yy + const day = date.getDate() + const monthNames = [ + "Jan", + "Feb", + "Mar", + "Apr", + "May", + "Jun", + "Jul", + "Aug", + "Sep", + "Oct", + "Nov", + "Dec" + ] + const month = monthNames[date.getMonth()] + const year = date.getFullYear().toString().slice(-2) + + const formattedDate = `${day < 10 ? "0" + day : day} ${month}'${year}` + + return `${formattedDate} ${formattedTime}` +} + +const ReceivedMessage = ({ message }: { message: messageI }) => { + return ( +
+
+ {getInitialsFromPublicKey(message.publicKey)} +
+
+

{message.text}

+ + {formatTime(message.time)} + +
+
+ ) +} + +const SentMessage = ({ message }: { message: messageI }) => { + return ( +
+
+

{message.text}

+ + {formatTime(message.time)} + +
+
+ {getInitialsFromPublicKey(message.publicKey)} +
+
+ ) +} + +function AddUserToChat(chatObj: ChatSc) { + const [publicKey, setPublicKey] = useState("") + const [creating, setCreating] = useState(false) + const { showSnackBar } = UtilsContext.useUtilsComponents() + + const inviteUser = async (e: React.SyntheticEvent) => { + e.preventDefault() + try { + setCreating(true) + console.log(chatObj) + await chatObj.invite(publicKey) + setPublicKey("") + showSnackBar("User added to the chat", true) + Modal.hideModal(addUserModal) + } catch (err) { + if (err instanceof Error) { + showSnackBar(err.message, false) + } + } finally { + setCreating(false) + } + } + + return ( + <> +
+
+
+ + setPublicKey(e.target.value)} + className="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" + placeholder="User Public Key" + required + /> +
+ +
+
+ + ) +} + +const ChatHeader = ({ + channelName, + refreshChat, + chatObj +}: { + channelName?: string + refreshChat: () => Promise + chatObj: ChatSc +}) => { + const [addUserToChat, setAddUserToChat] = useState() + + const addUser = (chat: ChatSc) => { + setAddUserToChat(chat) + Modal.showModal(addUserModal) + } + + return ( +
+
+
+ {getInitials(channelName)} +
+
+
{channelName}
+ Online +
+
+ + {/* Icon Group */} +
+ + addUser(chatObj)} + className="w-6 h-6 cursor-pointer hover:opacity-80 dark:hover:opacity-80" + style={{ color: "#999999" }} + /> +
+ +
+ ) +} + +const ChatInput = ({ + disabled, + refreshChat, + chatId +}: { + chatId: string + disabled: boolean + refreshChat: () => Promise +}) => { + const computer = useContext(ComputerContext) + const [message, setMessage] = useState("") + const [sending, setSending] = useState(false) + const { showSnackBar, showLoader } = UtilsContext.useUtilsComponents() + + const sendMessage = async () => { + try { + setSending(true) + showLoader(true) + const messageData: messageI = { + text: message, + publicKey: computer.getPublicKey(), + time: Date.now().toString() + } + const latesRev = await computer.getLatestRev(chatId) + const chatObj = (await computer.sync(latesRev)) as ChatSc + await chatObj.post(JSON.stringify(messageData)) + await sleep(2000) + await refreshChat() + setMessage("") + } catch (error) { + if (error instanceof Error) showSnackBar(error.message, false) + } finally { + showLoader(false) + setSending(false) + } + } + + const handleKeyDown = (e: React.KeyboardEvent) => { + if (e.key === "Enter" && !e.shiftKey) { + e.preventDefault() + sendMessage() + } + } + + return ( +
+
+ setMessage(e.target.value)} + onKeyDown={handleKeyDown} + disabled={disabled} + /> + +
+
+ ) +} + +export function Chat({ chatId }: { chatId: string }) { + const computer = useContext(ComputerContext) + const { showSnackBar, showLoader } = UtilsContext.useUtilsComponents() + const navigate = useNavigate() + const [id] = useState(chatId || "") + const [chatObj, setChatObj] = useState(null) + const [messages, setMessages] = useState([]) + + const refreshChat = async () => { + try { + showLoader(true) + const latesRev = await computer.getLatestRev(id) + const synced = (await computer.sync(latesRev)) as ChatSc + setChatObj(synced) + const messagesData: messageI[] = [] + synced.messages.forEach((message) => { + messagesData.push(JSON.parse(message)) + }) + setMessages(messagesData) + showLoader(false) + } catch (error) { + showLoader(false) + showSnackBar("Not a valid Chat", false) + } + } + + useEffect(() => { + const fetch = async () => { + await refreshChat() + } + fetch() + }, [computer, id, chatId, location, navigate]) + + return ( + <> +
+
+ {chatObj && ( + <> + + +
+ {messages.map((data, index) => + data.publicKey === computer.getPublicKey() ? ( + + ) : ( + + ) + )} +
+ + + + )} +
+
+ + ) +} diff --git a/packages/chat/src/components/Chats.tsx b/packages/chat/src/components/Chats.tsx new file mode 100644 index 000000000..6aaa4898a --- /dev/null +++ b/packages/chat/src/components/Chats.tsx @@ -0,0 +1,177 @@ +import { Auth, ComputerContext, Modal, UtilsContext } from "@bitcoin-computer/components" +import { useContext, useEffect, useState } from "react" +import { useNavigate, useParams } from "react-router-dom" +import { HiPlusCircle } from "react-icons/hi" + +import { VITE_CHAT_MOD_SPEC } from "../constants/modSpecs" +import { ChatSc } from "../contracts/chat" +import { Chat } from "./Chat" + +const newChatModal = "new-chat-modal" + +function CreateNewChat() { + const computer = useContext(ComputerContext) + const [name, setName] = useState("") + const [creating, setCreating] = useState(false) + const { showSnackBar, showLoader } = UtilsContext.useUtilsComponents() + const navigate = useNavigate() + + const onSubmit = async (e: React.SyntheticEvent) => { + e.preventDefault() + try { + showLoader(true) + setCreating(true) + const { tx, effect } = await computer.encode({ + exp: `new ChatSc("${name}", "${computer.getPublicKey()}")`, + mod: VITE_CHAT_MOD_SPEC + }) + await computer.broadcast(tx) + setName("") + if (typeof effect.res === "object" && !Array.isArray(effect.res)) { + showLoader(false) + showSnackBar("You created a new chat", true) + navigate(`/chats/${effect.res?._id as string}`) + window.location.reload() + } + } catch (err) { + if (err instanceof Error) { + showSnackBar(err.message, false) + } + } finally { + setCreating(false) + showLoader(false) + } + } + + return ( + <> +
+
+
+ + setName(e.target.value)} + className="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" + placeholder="Channel Name" + required + /> +
+ +
+
+ + ) +} + +export function Chats() { + const computer = useContext(ComputerContext) + const publicKey = Auth.getComputer().getPublicKey() + const params = useParams() + const navigate = useNavigate() + const [chatId] = useState(params.id || "") + const [chats, setChats] = useState([]) + + useEffect(() => { + const fetch = async () => { + const result = await computer.query({ mod: VITE_CHAT_MOD_SPEC, publicKey }) + const chatsPromise: Promise[] = [] + result.forEach((rev: string) => { + chatsPromise.push(computer.sync(rev) as Promise) + }) + + Promise.allSettled(chatsPromise).then((results) => { + const successfulChats = results + .filter( + (result): result is PromiseFulfilledResult => result.status === "fulfilled" + ) + .map((result) => result.value) + + setChats(successfulChats) + }) + } + fetch() + }, [computer, location, navigate]) + + const newChat = () => { + Modal.showModal(newChatModal) + } + + return ( + <> +
+
+
+ + + + + + + + {chats.map((chat) => ( + + + + ))} + +
+ My Chats + +
{ + navigate(`/chats/${chat._id}`) + window.location.reload() + }} + > + + {chat.channelName} + +
+
+
+
+ {chatId ? ( + + ) : ( +
+

+ Create new chat or select a existing existing one{" "} +

+
+ )} +
+
+ + + ) +} diff --git a/packages/chat/src/components/Mint.tsx b/packages/chat/src/components/Mint.tsx new file mode 100644 index 000000000..29be3c89a --- /dev/null +++ b/packages/chat/src/components/Mint.tsx @@ -0,0 +1,126 @@ +import { useContext, useState } from "react" +import { ComputerContext, Modal } from "@bitcoin-computer/components" +import { Link } from "react-router-dom" +import { VITE_CHAT_MOD_SPEC } from "../constants/modSpecs" + +function SuccessContent(rev: string) { + return ( + <> +
+
+ You created a{" "} + { + Modal.hideModal("success-modal") + }} + > + chat + +
+
+
+ +
+ + ) +} + +function ErrorContent(msg: string) { + return ( + <> +
+
+ Something went wrong. +
+
+ {msg} +
+
+
+ +
+ + ) +} + +export default function Mint() { + const computer = useContext(ComputerContext) + const [successRev, setSuccessRev] = useState("") + const [errorMsg, setErrorMsg] = useState("") + const [name, setName] = useState("") + + const onSubmit = async (e: React.SyntheticEvent) => { + e.preventDefault() + try { + const { tx, effect } = await computer.encode({ + exp: `new ChatSc("${name}", "${computer.getPublicKey()}")`, + mod: VITE_CHAT_MOD_SPEC + }) + await computer.broadcast(tx) + if (typeof effect.res === "object" && !Array.isArray(effect.res)) { + setSuccessRev(effect.res?._id as string) + Modal.showModal("success-modal") + } else { + setErrorMsg("Error occurred while creating chat") + Modal.showModal("error-modal") + } + } catch (err) { + if (err instanceof Error) { + setErrorMsg(err.message) + Modal.showModal("error-modal") + } + } + } + + return ( + <> +
+

Create a new Chat

+
+ + setName(e.target.value)} + className="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" + /> +
+ +
+ + + + ) +} diff --git a/packages/chat/src/components/Navbar.tsx b/packages/chat/src/components/Navbar.tsx new file mode 100644 index 000000000..405a5378a --- /dev/null +++ b/packages/chat/src/components/Navbar.tsx @@ -0,0 +1,262 @@ +import { Link } from "react-router-dom" +import { Modal, Auth, UtilsContext, Drawer } from "@bitcoin-computer/components" +import { useEffect, useState } from "react" +import { initFlowbite } from "flowbite" +import { Chain, Network } from "../types/common" + +const modalTitle = "Connect to Node" +const modalId = "unsupported-config-modal" + +function formatChainAndNetwork(chain: Chain, network: Network) { + const map = { + mainnet: "", + testnet: "t", + regtest: "r" + } + const prefix = map[network] + return `${prefix}${chain}` +} + +function ModalContent() { + const [url, setUrl] = useState("") + function setNetwork(e: React.SyntheticEvent) { + e.preventDefault() + localStorage.setItem("URL", url) + } + + function closeModal() { + Modal.get(modalId).hide() + } + + return ( +
+
+
+ + + setUrl(e.target.value)} + value={url} + type="text" + name="url" + id="url" + className="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-600 dark:border-gray-500 dark:placeholder-gray-400 dark:text-white" + placeholder="http://127.0.0.1:1031" + required + /> + + +
+
+ +
+ + +
+
+ ) +} + +function SignInItem() { + return ( +
  • + +
  • + ) +} + +export function NotLoggedMenu() { + const [dropDownLabel, setDropDownLabel] = useState("LTC") + const { showSnackBar } = UtilsContext.useUtilsComponents() + + useEffect(() => { + initFlowbite() + + const { chain, network } = Auth.defaultConfiguration() + setDropDownLabel(formatChainAndNetwork(chain, network)) + }, []) + + const setChainAndNetwork = (chain: Chain, network: Network) => { + try { + localStorage.setItem("CHAIN", chain) + localStorage.setItem("NETWORK", network) + setDropDownLabel(formatChainAndNetwork(chain, network)) + window.location.href = "/" + } catch (err) { + showSnackBar("Error setting chain and network", false) + Modal.get(modalId).show() + } + } + + function CoinSelectionItem({ chain, network }: { chain: Chain; network: Network }) { + return ( +
  • +
    setChainAndNetwork(chain, network)} + className="block px-4 py-2 hover:bg-gray-100 dark:hover:bg-gray-600 dark:hover:text-white" + > + {chain} {network} +
    +
  • + ) + } + + return ( + <> + +
      +
    • + + +
    • + + +
    + + ) +} + +function WalletItem() { + return ( +
  • + +
  • + ) +} + +export function LoggedInMenu() { + return ( +
      + +
    + ) +} + +function NavbarDropdownButton() { + return ( + + ) +} + +export function Logo({ name = "Bitcoin Computer Chat" }) { + return ( + + Bitcoin Computer Logo + + {name} + + + ) +} + +export function Navbar() { + useEffect(() => { + initFlowbite() + }, []) + + return ( + <> + + + ) +} diff --git a/packages/chat/src/constants/modSpecs.ts b/packages/chat/src/constants/modSpecs.ts new file mode 100644 index 000000000..09a5f62d1 --- /dev/null +++ b/packages/chat/src/constants/modSpecs.ts @@ -0,0 +1,2 @@ +const { VITE_CHAT_MOD_SPEC } = import.meta.env +export { VITE_CHAT_MOD_SPEC } diff --git a/packages/chat/src/contracts/chat.ts b/packages/chat/src/contracts/chat.ts new file mode 100644 index 000000000..9ae7a2c46 --- /dev/null +++ b/packages/chat/src/contracts/chat.ts @@ -0,0 +1,19 @@ +export class ChatSc extends Contract { + messages!: string[] + channelName!: string + constructor(channelName: string, publicKey: string) { + super({ + messages: [], + channelName, + _owners: [publicKey] + }) + } + + post(message: string) { + this.messages.push(message) + } + + invite(publicKey: string) { + this._owners.push(publicKey) + } +} diff --git a/packages/chat/src/contracts/counter.ts b/packages/chat/src/contracts/counter.ts new file mode 100644 index 000000000..ab3f09338 --- /dev/null +++ b/packages/chat/src/contracts/counter.ts @@ -0,0 +1,10 @@ +export class Counter extends Contract { + count!: number + constructor() { + super({ count: 0 }) + } + + inc() { + this.count += 1 + } +} diff --git a/packages/chat/src/index.css b/packages/chat/src/index.css index abf5213eb..e2ac8d522 100644 --- a/packages/chat/src/index.css +++ b/packages/chat/src/index.css @@ -1,73 +1,14 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; body { - font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', - 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', - sans-serif; + margin: 0; + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", "Ubuntu", + "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; - line-height: 1.4; } code { - font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', - monospace; -} - -.sidebar { - margin: 68px 8px 8px 8px; - height: calc(100vh - 80px); - width: 165px; - position: fixed; - z-index: 1; - top: 0; - left: 0; - background-color: #fff; - overflow-x: hidden; - line-height: 1.5 -} - -.main { - margin: 40px 0px 5px 180px; -} - -.flex { - display: flex; - justify-content: space-between; -} - -textarea { - width: calc(100vw - 200px); - height: calc(100vh - 160px); -} - -input { - width: calc(100vw - 257px); - margin-right: 5px; -} - -.login-screen { - height: 100%; - width: 100%; - position: fixed; - z-index: 2; - top: 0; - left: 0; - background-color: #fff; - display: flex; - justify-content: center; - align-items: center; -} - -.login-screen input { - width: 400px; - margin: 5px 0; -} - -.login-screen button { - width: 100px; - margin: 5px 0; -} - -.branding { - position: absolute; - bottom: 0; + font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New", monospace; } diff --git a/packages/chat/src/index.js b/packages/chat/src/index.js deleted file mode 100644 index a74bc0c80..000000000 --- a/packages/chat/src/index.js +++ /dev/null @@ -1,18 +0,0 @@ -import React from 'react' -import ReactDOM from 'react-dom/client' -import './index.css' -import App from './App' -import * as serviceWorker from './serviceWorker' - -const root = ReactDOM.createRoot(document.getElementById('root')) - -root.render( - - - , -) - -// If you want your app to work offline and load faster, you can change -// unregister() to register() below. Note this comes with some pitfalls. -// Learn more about service workers: https://bit.ly/CRA-PWA -serviceWorker.unregister() diff --git a/packages/chat/src/logo.svg b/packages/chat/src/logo.svg deleted file mode 100644 index 6b60c1042..000000000 --- a/packages/chat/src/logo.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - diff --git a/packages/chat/src/main.tsx b/packages/chat/src/main.tsx new file mode 100644 index 000000000..6f4ac9bcc --- /dev/null +++ b/packages/chat/src/main.tsx @@ -0,0 +1,10 @@ +import { StrictMode } from 'react' +import { createRoot } from 'react-dom/client' +import App from './App.tsx' +import './index.css' + +createRoot(document.getElementById('root')!).render( + + + , +) diff --git a/packages/chat/src/serviceWorker.js b/packages/chat/src/serviceWorker.js deleted file mode 100644 index f3141a123..000000000 --- a/packages/chat/src/serviceWorker.js +++ /dev/null @@ -1,141 +0,0 @@ -/* eslint-disable no-use-before-define */ -/* eslint-disable no-param-reassign */ -/* eslint-disable no-console */ -/* eslint-disable no-undef */ -// This optional code is used to register a service worker. -// register() is not called by default. - -// This lets the app load faster on subsequent visits in production, and gives -// it offline capabilities. However, it also means that developers (and users) -// will only see deployed updates on subsequent visits to a page, after all the -// existing tabs open on the page have been closed, since previously cached -// resources are updated in the background. - -// To learn more about the benefits of this model and instructions on how to -// opt-in, read https://bit.ly/CRA-PWA - -const isLocalhost = Boolean( - window.location.hostname === 'localhost' || - // [::1] is the IPv6 localhost address. - window.location.hostname === '[::1]' || - // 127.0.0.0/8 are considered localhost for IPv4. - window.location.hostname.match(/^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/), -) - -export function register(config) { - if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) { - // The URL constructor is available in all browsers that support SW. - const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href) - if (publicUrl.origin !== window.location.origin) { - // Our service worker won't work if PUBLIC_URL is on a different origin - // from what our page is served on. This might happen if a CDN is used to - // serve assets; see https://github.com/facebook/create-react-app/issues/2374 - return - } - - window.addEventListener('load', () => { - const swUrl = `${process.env.PUBLIC_URL}/service-worker.js` - - if (isLocalhost) { - // This is running on localhost. Let's check if a service worker still exists or not. - checkValidServiceWorker(swUrl, config) - - // Add some additional logging to localhost, pointing developers to the - // service worker/PWA documentation. - navigator.serviceWorker.ready.then(() => { - console.log( - 'This web app is being served cache-first by a service ' + - 'worker. To learn more, visit https://bit.ly/CRA-PWA', - ) - }) - } else { - // Is not localhost. Just register service worker - registerValidSW(swUrl, config) - } - }) - } -} - -function registerValidSW(swUrl, config) { - navigator.serviceWorker - .register(swUrl) - .then((registration) => { - registration.onupdatefound = () => { - const installingWorker = registration.installing - if (installingWorker == null) { - return - } - installingWorker.onstatechange = () => { - if (installingWorker.state === 'installed') { - if (navigator.serviceWorker.controller) { - // At this point, the updated precached content has been fetched, - // but the previous service worker will still serve the older - // content until all client tabs are closed. - console.log( - 'New content is available and will be used when all ' + - 'tabs for this page are closed. See https://bit.ly/CRA-PWA.', - ) - - // Execute callback - if (config && config.onUpdate) { - config.onUpdate(registration) - } - } else { - // At this point, everything has been precached. - // It's the perfect time to display a - // "Content is cached for offline use." message. - console.log('Content is cached for offline use.') - - // Execute callback - if (config && config.onSuccess) { - config.onSuccess(registration) - } - } - } - } - } - }) - .catch((error) => { - console.error('Error during service worker registration:', error) - }) -} - -function checkValidServiceWorker(swUrl, config) { - // Check if the service worker can be found. If it can't reload the page. - fetch(swUrl, { - headers: { 'Service-Worker': 'script' }, - }) - .then((response) => { - // Ensure service worker exists, and that we really are getting a JS file. - const contentType = response.headers.get('content-type') - if ( - response.status === 404 || - (contentType != null && contentType.indexOf('javascript') === -1) - ) { - // No service worker found. Probably a different app. Reload the page. - navigator.serviceWorker.ready.then((registration) => { - registration.unregister().then(() => { - window.location.reload() - }) - }) - } else { - // Service worker found. Proceed as normal. - registerValidSW(swUrl, config) - } - }) - .catch(() => { - console.log('No internet connection found. App is running in offline mode.') - }) -} - -export function unregister() { - if ('serviceWorker' in navigator) { - navigator.serviceWorker.ready - .then((registration) => { - registration.unregister() - }) - .catch((error) => { - console.error(error.message) - }) - } -} diff --git a/packages/chat/src/setupTests.js b/packages/chat/src/setupTests.js deleted file mode 100644 index 2eb59b05d..000000000 --- a/packages/chat/src/setupTests.js +++ /dev/null @@ -1,5 +0,0 @@ -// jest-dom adds custom jest matchers for asserting on DOM nodes. -// allows you to do things like: -// expect(element).toHaveTextContent(/react/i) -// learn more: https://github.com/testing-library/jest-dom -import '@testing-library/jest-dom/extend-expect' diff --git a/packages/chat/src/setupTests.ts b/packages/chat/src/setupTests.ts new file mode 100644 index 000000000..3f925457f --- /dev/null +++ b/packages/chat/src/setupTests.ts @@ -0,0 +1,3 @@ +import * as matchers from "@testing-library/jest-dom/matchers"; + +expect.extend(matchers); diff --git a/packages/chat/src/types/common.ts b/packages/chat/src/types/common.ts new file mode 100644 index 000000000..58276575f --- /dev/null +++ b/packages/chat/src/types/common.ts @@ -0,0 +1,2 @@ +export type Chain = "LTC" | "BTC" | "DOGE" | "PEPE" +export type Network = "testnet" | "mainnet" | "regtest" diff --git a/packages/chat/src/useInterval.js b/packages/chat/src/useInterval.js deleted file mode 100644 index 1ddf497ec..000000000 --- a/packages/chat/src/useInterval.js +++ /dev/null @@ -1,22 +0,0 @@ -import { useEffect, useRef } from 'react' - -export default function useInterval(callback, delay) { - const savedCallback = useRef() - - // Remember the latest callback. - useEffect(() => { - savedCallback.current = callback - }, [callback]) - - // Set up the interval. - // eslint-disable-next-line consistent-return - useEffect(() => { - function tick() { - savedCallback.current() - } - if (delay !== null) { - const id = setInterval(tick, delay) - return () => clearInterval(id) - } - }, [delay]) -} diff --git a/packages/chat/src/utils.js b/packages/chat/src/utils.js deleted file mode 100644 index c128fd0e3..000000000 --- a/packages/chat/src/utils.js +++ /dev/null @@ -1,6 +0,0 @@ -export default class Utils { - static async importFromPublic(fileName) { - const response = await fetch(process.env.PUBLIC_URL + fileName) - return response.text() - } -} diff --git a/packages/chat/src/vite-env.d.ts b/packages/chat/src/vite-env.d.ts new file mode 100644 index 000000000..11f02fe2a --- /dev/null +++ b/packages/chat/src/vite-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/packages/chat/tailwind.config.js b/packages/chat/tailwind.config.js new file mode 100644 index 000000000..40183bda5 --- /dev/null +++ b/packages/chat/tailwind.config.js @@ -0,0 +1,19 @@ +/** @type {import('tailwindcss').Config} */ +module.exports = { + content: ["./src/**/*.{js,jsx,ts,tsx}", "../components/built/**/*.{js,jsx,ts,tsx}"], + darkMode: "media", + theme: { + extend: { + colors: { + "blue-1": "#000F38", + "blue-2": "#002A99", + "blue-3": "#0046FF", + "blue-4": "#A7BFFF" + }, + height: { + 120: "36rem" + } + } + }, + plugins: [] +} diff --git a/packages/chat/tsconfig.json b/packages/chat/tsconfig.json new file mode 100644 index 000000000..21d8316bc --- /dev/null +++ b/packages/chat/tsconfig.json @@ -0,0 +1,25 @@ +{ + "compilerOptions": { + "target": "ES2020", + "useDefineForClassFields": false, + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "module": "ESNext", + "skipLibCheck": true, + "types": ["vitest/globals", "@testing-library/jest-dom"], + + /* Bundler mode */ + "moduleResolution": "node", + "allowImportingTsExtensions": true, + "isolatedModules": true, + "moduleDetection": "force", + "noEmit": true, + "jsx": "react-jsx", + + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true + }, + "include": ["src"] +} diff --git a/packages/chat/vite.config.ts b/packages/chat/vite.config.ts new file mode 100644 index 000000000..fa48d77f8 --- /dev/null +++ b/packages/chat/vite.config.ts @@ -0,0 +1,28 @@ +/// +/// + +import { defineConfig, loadEnv } from "vite" +import react from "@vitejs/plugin-react" +import path from "path" + +// https://vitejs.dev/config/ +export default defineConfig(({ mode }) => { + const env = loadEnv(mode, process.cwd(), "") + return { + plugins: [react()], + resolve: { + alias: { + // Define the alias pointing to the specific entry point in node_modules + "@bitcoin-computer/lib": path.resolve(__dirname, "../lib/dist/bc-lib.browser.min.mjs") + } + }, + server: { + port: parseInt(env.VITE_PORT) + }, + test: { + globals: true, + environment: "jsdom", + setupFiles: ["./src/setupTests.ts"] + } + } +}) diff --git a/scripts/check-obfuscation.sh b/scripts/check-obfuscation.sh index 225c9ad92..4e971cdec 100755 --- a/scripts/check-obfuscation.sh +++ b/scripts/check-obfuscation.sh @@ -1,7 +1,7 @@ #!/bin/bash # List of folders to skip -skip_folders=("vite-template" "nft-vite" "explorer-vite" "wallet-vite") +skip_folders=("vite-template" "nft-vite" "explorer-vite" "wallet-vite" "chat-vite") # Check if the obfuscation was successful on all dist folders msg="Checking obfuscation ..."