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 (
-
-
-
-
-
-
- )
-}
-
-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
-
-
-
-
-
- )
-}
-
-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)}
-
-
- ))}
-
-
- )
-}
-
-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 (
+ <>
+
+
+
+
+
+
+ |
+ My Chats
+
+ |
+
+
+
+ {chats.map((chat) => (
+
+ | {
+ 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 (
+ <>
+
+
+
+ >
+ )
+}
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 (
+
+ )
+}
+
+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 (
+
+
+
+ {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 ..."