diff --git a/.github/workflows/run_tests.yaml b/.github/workflows/run_tests.yaml new file mode 100644 index 0000000..27c5ba3 --- /dev/null +++ b/.github/workflows/run_tests.yaml @@ -0,0 +1,17 @@ +name: Run Tests + +on: + push: + +jobs: + run_tests: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 20 + - name: Yarn Install + run: yarn install + - name: Run Test Script + run: yarn test diff --git a/README.md b/README.md index bb21d00..788839c 100644 --- a/README.md +++ b/README.md @@ -1 +1,34 @@ # Ome.tv Automator + +Ome.tv Automator automatically sends your messages to thousands of people. + +See it on the [Chrome Web Store](https://chromewebstore.google.com/detail/ometv-automator/kdakicmdgfidhnnfjgomlkoikigebpdf). + +**Production**: + +![Test Status: Production](https://github.com/mstephen19/ome-automator/actions/workflows/run_tests.yaml/badge.svg?branch=main) + +**Development**: + +![Test Status: Development](https://github.com/mstephen19/ome-automator/actions/workflows/run_tests.yaml/badge.svg?branch=develop) + +## Get started + +1. Add a message to the sequence (e.g. "Hello"). Multiple messages also supported. +2. Navigate to and click the Ome.tv Automator "Play" button. + +You'll notice the Ome.tv "Start" button is automatically clicked. Once you're connected to a stranger, messages from the sequence are sent, in order. Afterwards, "Next" is clicked and the sequence is sent to the next connection, and so on. + +## What if a stranger disconnects mid-sequence? + +Ome.tv Automator understands when it's disconnected on, and reacts quickly to restart the sequence, once reconnected with someone new. + +More information & legal disclaimers available under "Help, Info & Terms" in the Ome.tv Automator popup window + +## Screenshots + +![Image](./assets/screenshot-1.png) + +![Image](./assets/screenshot-2.png) + +![Image](./assets/screenshot-3.png) diff --git a/assets/screenshot-1.png b/assets/screenshot-1.png new file mode 100644 index 0000000..afbf26e Binary files /dev/null and b/assets/screenshot-1.png differ diff --git a/assets/screenshot-2.png b/assets/screenshot-2.png new file mode 100644 index 0000000..9b44cb1 Binary files /dev/null and b/assets/screenshot-2.png differ diff --git a/assets/screenshot-3.png b/assets/screenshot-3.png new file mode 100644 index 0000000..1755217 Binary files /dev/null and b/assets/screenshot-3.png differ diff --git a/manifest.json b/manifest.json index 1443f58..37732e1 100644 --- a/manifest.json +++ b/manifest.json @@ -2,7 +2,7 @@ "manifest_version": 3, "name": "Ome.tv Automator", "short_name": "Auto-Ome.tv", - "version": "1.0.0", + "version": "1.0.1", "description": "Reliably deliver message sequences to Ome.tv connections.", "icons": { "16": "public/icon16.png", diff --git a/package.json b/package.json index 93bfe72..6887485 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "ometv", "private": true, - "version": "0.0.0", + "version": "1.0.1", "scripts": { "dev": "vite", "test": "vitest", diff --git a/src/consts.ts b/src/consts.ts index 1202cdb..07408f3 100644 --- a/src/consts.ts +++ b/src/consts.ts @@ -1,5 +1,7 @@ import { AppData, Config, Message, TabData } from './types'; +export const EXTENSION_MANIFEST = chrome.runtime.getManifest(); + export const defaultAppData: AppData = { addMessageText: '', messageSequenceOpen: false, diff --git a/src/content/commands.ts b/src/content/commands.ts index 48f0301..c6d29e4 100644 --- a/src/content/commands.ts +++ b/src/content/commands.ts @@ -1,5 +1,5 @@ import { Command, CommandMessage } from '../types'; -import { TypedEventTarget } from '../utils'; +import { pollPredicate, TypedEventTarget } from '../utils'; import { elements } from './elements'; export const tabCommandRouter = () => { @@ -14,12 +14,16 @@ export const tabCommandRouter = () => { events.dispatchEvent(new CustomEvent(command, { detail: tabId })); }); - // Stops if the user clicks the "Stop" button. - elements.stopButton()?.addEventListener('click', () => { - // Passing invalid tab ID (-1) - // Stopping sets the runningTab to null anyways - events.dispatchEvent(new CustomEvent(Command.Stop, { detail: -1 })); - }); + (async () => { + await pollPredicate(250, () => Boolean(elements.stopButton())); + + // Stops if the user clicks the "Stop" button. + elements.stopButton()?.addEventListener('click', () => { + // Passing invalid tab ID (-1) + // Stopping sets the runningTab to null anyways + events.dispatchEvent(new CustomEvent(Command.Stop, { detail: -1 })); + }); + })(); return { events, diff --git a/src/popup/Accordions/AccordionItem.tsx b/src/popup/Accordions/AccordionItem.tsx index 4d6ce53..f184ed3 100644 --- a/src/popup/Accordions/AccordionItem.tsx +++ b/src/popup/Accordions/AccordionItem.tsx @@ -1,4 +1,4 @@ -import { Accordion, AccordionDetails, AccordionSummary, Avatar, Divider, styled, Typography } from '@mui/material'; +import { Accordion, AccordionDetails, AccordionSummary, Chip, styled, Typography } from '@mui/material'; import ExpandMoreIcon from '@mui/icons-material/ExpandMore'; import { type ReactNode, useContext } from 'react'; import { AppDataContext } from '../context/AppDataProvider'; @@ -13,22 +13,16 @@ const AccordionTitle = styled(AccordionSummary)({ }, }); -const AccordionTitleAvatar = styled(Avatar)({ - width: 20, - height: 20, - fontSize: '1rem', -}); - export const AccordionItem = ({ dataKey, title, - avatar, + chip, maxHeight, children, }: { dataKey: Exclude; title: string; - avatar?: string; + chip?: string; maxHeight?: string; children: ReactNode; }) => { @@ -43,7 +37,7 @@ export const AccordionItem = ({ }> {title} - {avatar && {avatar}} + {chip && } {children} diff --git a/src/popup/Accordions/MessageSequencer/AddMessageBox.tsx b/src/popup/Accordions/MessageSequencer/AddMessageBox.tsx index cb40b08..1914b23 100644 --- a/src/popup/Accordions/MessageSequencer/AddMessageBox.tsx +++ b/src/popup/Accordions/MessageSequencer/AddMessageBox.tsx @@ -1,5 +1,5 @@ import { TextField } from '@mui/material'; -import { useState, useContext } from 'react'; +import { useState, useContext, ChangeEventHandler } from 'react'; import { appDataStore, messageStore } from '../../../storage'; import { sanitize } from '../../../utils'; @@ -11,12 +11,19 @@ export const AddMessageBox = () => { const messages = useContext(MessageSequenceContext); const appData = useContext(AppDataContext); - // const [inputText, setInputText] = useState(''); + // Safe to initialize with async retrieved store value because the provider + // doesn't render children until data is initialized. + const [inputText, setInputText] = useState(appData.addMessageText); const [loading, setLoading] = useState(false); const [validationError, setValidationError] = useState(''); const showError = Boolean(validationError); + const handleMessageChange: ChangeEventHandler = async (e) => { + setInputText(e.target.value); + await appDataStore.write({ ...appData, addMessageText: e.target.value }); + }; + const handleAddMessage = async (unsanitized: string) => { setLoading(true); @@ -39,6 +46,7 @@ export const AddMessageBox = () => { // Add the message to the end of the list // Clear out the addMessageText await Promise.all([messageStore.write([...messages, { id, content }]), appDataStore.write({ ...appData, addMessageText: '' })]); + setInputText(''); setValidationError(''); setLoading(false); @@ -60,9 +68,9 @@ export const AddMessageBox = () => { handleAddMessage(appData.addMessageText); } }} - value={appData.addMessageText} + value={inputText} // ? Store the latest text value in the data store - seamless through popup reloads - onChange={(e) => appDataStore.write({ ...appData, addMessageText: e.target.value })} + onChange={handleMessageChange} helperText={showError ? validationError : 'Press "Enter" to add your message to the sequence.'} error={showError} // sx={{ position: 'sticky', top: 0 }} diff --git a/src/popup/Accordions/index.tsx b/src/popup/Accordions/index.tsx index 2bb7f0f..19a6e61 100644 --- a/src/popup/Accordions/index.tsx +++ b/src/popup/Accordions/index.tsx @@ -7,13 +7,14 @@ import { MessageSequenceContext } from '../context/MessageSequenceProvider'; import { AccordionItem } from './AccordionItem'; import { Divider } from '@mui/material'; import { Help } from './Help'; +import { EXTENSION_MANIFEST } from '../../consts'; export const Accordions = () => { const messages = useContext(MessageSequenceContext); return ( <> - + @@ -27,7 +28,7 @@ export const Accordions = () => { - + diff --git a/src/popup/TopBar/index.tsx b/src/popup/TopBar/index.tsx index ef91f65..4eb59df 100644 --- a/src/popup/TopBar/index.tsx +++ b/src/popup/TopBar/index.tsx @@ -1,4 +1,4 @@ -import { AppBar, Box, Link, Toolbar, Typography } from '@mui/material'; +import { AppBar, Box, Link, Toolbar, Tooltip, Typography } from '@mui/material'; import { AppDataContext } from '../context/AppDataProvider'; import { useContext } from 'react'; import { appDataStore } from '../../storage'; @@ -6,6 +6,7 @@ import { ThemeSwitch } from './ThemeSwitch'; import logoGrey from '../../assets/logo-grey.png'; import logoWhite from '../../assets/logo-white.png'; +import { EXTENSION_MANIFEST } from '../../consts'; const Logo = ({ theme }: { theme: 'dark' | 'light' }) => ( @@ -29,9 +30,11 @@ export const TopBar = () => { - - Ome.tv Automator - + + + Ome.tv Automator + + diff --git a/src/popup/context/StoreProvider.tsx b/src/popup/context/StoreProvider.tsx index e69357a..3b0b37b 100644 --- a/src/popup/context/StoreProvider.tsx +++ b/src/popup/context/StoreProvider.tsx @@ -6,6 +6,8 @@ import { CircularProgress } from '@mui/material'; * Creates a generic provider for use with a {@link chromeStorage} adapter. * * Initializes the store, and updates the context value when changes are detected. + * + * Will not render children until the store has been initialized. Assumes zero store errors. */ export function storeProvider({ context, diff --git a/tests/_setup.ts b/tests/_setup.ts index ab83b1d..db6cba5 100644 --- a/tests/_setup.ts +++ b/tests/_setup.ts @@ -27,6 +27,7 @@ vi.stubGlobal('chrome', { addListener: chromeTabMessages.addListener, removeListener: chromeTabMessages.removeListener, }, + getManifest: () => ({}), }, storage: { local: {