diff --git a/.gitignore b/.gitignore index e86939e..24272d6 100644 --- a/.gitignore +++ b/.gitignore @@ -3,4 +3,5 @@ node_modules pnpm-lock.yaml .todo .env -messages.json \ No newline at end of file +messages.json +api/*.js \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..99a9dfa --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,29 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +## [Unreleased] + +### Added +- dev dependency `cfx` in order to add the `copy-static` script in `package.json` +- a popup file in the chrome extension to permit the user to directly open the react page. Now the link it's hardcoded +

+ +

+ +- add a retry system to connect to the chat in `index.ts` in order to manage anomalies in case the component is loaded faster than the page +- add a `sessionId` generated each time there is a "reload" of the page so the webapp inserts a divider to let the streamer knows where the +chat session begins +- add `ChatDivider` component to let the user know when the chat has been reloaded +- add a footer on the chat that indicates if the autoreload is `on` or `off` +- add a badge on the extension in order to detect if it is connected to a chat + +

+ +

+ +### Changed +- `build` script in order to build the Chrome extension and copy all the relevant files in the ui folder + +### Notes +- the ChatDivider and the footer needs a graphical review but they are both working diff --git a/README.md b/README.md index 6e130fc..67c4b14 100644 --- a/README.md +++ b/README.md @@ -60,6 +60,9 @@ By offering a consistent, user-friendly interface, Chat Fusion makes managing an ```bash pnpm i pnpm build # to build the chrome extension + +cd chat +pnpm i # to install the dependencies for the chat ``` You have to run the client (React) code and the API (fastify) separetly. diff --git a/api/server.ts b/api/server.ts index 665d128..8ab542a 100644 --- a/api/server.ts +++ b/api/server.ts @@ -7,6 +7,7 @@ import cors from "@fastify/cors"; import fs from "fs"; export interface IMessage { + sessionId: string; id: string; platform: string; content: string; @@ -28,12 +29,25 @@ server.post( "/api/messages", async (request: FastifyRequest, reply: FastifyReply) => { const message = request.body as IMessage; + if (messages.length > 0 && messages[messages.length - 1].sessionId !== message.sessionId) { + // new session, add system message + messages.push({ + sessionId: message.sessionId, + id: Math.random().toString(36).substr(2, 9), + platform: message.platform, + content: "Chat reload", + author: "System", + emojis: [], + badge: "", + }); + } messages.push(message); reply.code(201).send(); } ); server.get("/api/messages", async (_: FastifyRequest, reply: FastifyReply) => { + reply.send(messages); }); diff --git a/assets/images/badge.png b/assets/images/badge.png new file mode 100644 index 0000000..8a51ec6 Binary files /dev/null and b/assets/images/badge.png differ diff --git a/assets/images/popup.png b/assets/images/popup.png new file mode 100644 index 0000000..45fe7b5 Binary files /dev/null and b/assets/images/popup.png differ diff --git a/chat/package.json b/chat/package.json index 606adbd..2d15f50 100644 --- a/chat/package.json +++ b/chat/package.json @@ -4,7 +4,7 @@ "version": "0.0.0", "type": "module", "scripts": { - "dev": "VITE_STYLE_MODE=true vite", + "dev": "vite", "build": "tsc && vite build", "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0", "preview": "vite preview" diff --git a/chat/src/Components/Chat/Chat.tsx b/chat/src/Components/Chat/Chat.tsx index 3e144b8..da8484d 100644 --- a/chat/src/Components/Chat/Chat.tsx +++ b/chat/src/Components/Chat/Chat.tsx @@ -1,7 +1,8 @@ -import { useEffect, useRef, FC } from "react"; +import { useEffect, useRef, FC, useState } from "react"; import { IMessage } from "../../types"; import { Message } from "../Message"; import classNames from "classnames"; +import { ChatDivider } from "../ChatDivider"; export const Chat: FC<{ messages: IMessage[]; @@ -9,7 +10,7 @@ export const Chat: FC<{ setFocusedMessage: (message: IMessage | null) => void; }> = ({ messages, focusedMessage, setFocusedMessage }) => { const messagesEndRef = useRef(null); - + const [autoScroll, setAutoScroll] = useState(true); const handleFocusMessage = (message: IMessage) => { setFocusedMessage(message.id === focusedMessage?.id ? null : message); }; @@ -20,28 +21,47 @@ export const Chat: FC<{ } }; - useEffect(() => { - scrollToBottom(); - }, [messages]); - - return ( -
-
- {messages.map((message: IMessage, index: number) => ( + const mapMessages = () => { + return messages.map((message: IMessage, index: number) => { + if (message.author.toLocaleLowerCase() === "system") { + return ( + + ); + } else { + return ( - ))} -
+ ); + } + }); + }; + + useEffect(() => { + if (autoScroll) { + scrollToBottom(); + } + }, [messages, autoScroll]); + + return ( + <> +
+ Autoreload { autoScroll ? (ON) : (OFF)} +
+
+
+ {mapMessages()} +
+
-
+ ); }; diff --git a/chat/src/Components/ChatDivider/ChatDivider.tsx b/chat/src/Components/ChatDivider/ChatDivider.tsx new file mode 100644 index 0000000..a15f04f --- /dev/null +++ b/chat/src/Components/ChatDivider/ChatDivider.tsx @@ -0,0 +1,17 @@ +import { FC } from "react"; +import { IMessage } from "../../types"; + + +export const ChatDivider: FC<{ + message: IMessage; +}> = ({ message}) => { + const { platform } = message; + + return ( +
+ + Chat Reload on: {platform} + +
+ ); +}; diff --git a/chat/src/Components/ChatDivider/index.ts b/chat/src/Components/ChatDivider/index.ts new file mode 100644 index 0000000..f38cd58 --- /dev/null +++ b/chat/src/Components/ChatDivider/index.ts @@ -0,0 +1 @@ +export * from "./ChatDivider"; diff --git a/manifest.json b/manifest.json index 5e51637..f5141dd 100644 --- a/manifest.json +++ b/manifest.json @@ -1,6 +1,11 @@ { "manifest_version": 3, "name": "Chat Fusion", + "description": "Chat Fusion for Streamers", + "action": { + "default_popup": "./dist/popup.html", + "default_title": "Chat Fusion" + }, "version": "1.0", "host_permissions": ["http://*/*", "https://*/*"], "content_scripts": [ @@ -8,5 +13,8 @@ "matches": ["http://*/*", "https://*/*"], "js": ["./dist/index.js"] } - ] + ], + "background": { + "service_worker": "./dist/background.js" + } } diff --git a/package.json b/package.json index 4447671..75fef21 100644 --- a/package.json +++ b/package.json @@ -3,15 +3,17 @@ "version": "1.0.0", "scripts": { "build:content": "tsc -p tsconfig.json", - "build": "npm run build:content", + "copy-static": "cpx src/ui/* dist", + "build": "npm run build:content && npm run copy-static", "build-server": "tsc --target ES2018 --module CommonJS --outDir ./api --strict --esModuleInterop api/server.ts", - "server-dev": "nodemon --watch 'api/server.ts' --exec 'tsc --target ES2018 --module CommonJS --outDir ./api --strict --esModuleInterop api/server.ts && node ./api/server.js'" + "server-dev": "nodemon --watch 'api/server.ts' --exec \"tsc --target ES2018 --module CommonJS --outDir ./api --strict --esModuleInterop api/server.ts && node ./api/server.js\"" }, "devDependencies": { "@types/chrome": "^0.0.248", "@types/node": "^20.8.7", - "typescript": "^5.2.2", - "nodemon": "^2.0.15" + "cpx": "^1.5.0", + "nodemon": "^2.0.15", + "typescript": "^5.2.2" }, "dependencies": { "@fastify/cors": "8.4.0", diff --git a/src/content/background.ts b/src/content/background.ts new file mode 100644 index 0000000..aa5b463 --- /dev/null +++ b/src/content/background.ts @@ -0,0 +1,21 @@ +let active = false; + + +chrome.runtime.onMessage.addListener((request, sender, sendResponse) => { + if (request.type === "badge") { + setBadgeStatus(request.status); + sendResponse({status: active}); + } +}); + +const setBadgeStatus = (status: boolean) => { + if(status){ + chrome.action.setBadgeText({ text: " " }); + chrome.action.setBadgeTextColor({ color: "#FFFFFF" }); + chrome.action.setBadgeBackgroundColor({ color: "#00FF00" }); + }else{ + chrome.action.setBadgeText({ text: " " }); + chrome.action.setBadgeTextColor({ color: "#FFFFFF" }); + chrome.action.setBadgeBackgroundColor({ color: "#FF0000" }); + } +}; diff --git a/src/content/index.ts b/src/content/index.ts index 89fd0f9..5ed6a4d 100644 --- a/src/content/index.ts +++ b/src/content/index.ts @@ -6,6 +6,26 @@ interface ISelectorConfig { chatImageContainer: string; } +/** + * Utility used to generate a sessionId. + * @param {length} length - The length of the ID to generate. + * @returns the generated ID. + */ +function makeId(length: number): string { + let result = ""; + const characters = + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; + const charactersLength = characters.length; + + for (let i = 0; i < length; i++) { + result += characters.charAt( + Math.floor(Math.random() * charactersLength) + ); + } + + return result; +} + /** * Retrieves the CSS selectors based on the hostname for a specific platform. * @param {string} hostname - The hostname of the website. @@ -61,6 +81,7 @@ const getPlatformNameByHostname = ( * Initializes the content script and sets up chat observers. */ const initContentScript = () => { + const sessionId = makeId(10); const hostname = window.location.hostname; const platform = getPlatformNameByHostname(hostname); const config = getSelectorsByPlatform(hostname); @@ -76,7 +97,6 @@ const initContentScript = () => { Array.from(mutation.addedNodes).forEach((chatNode) => { const isHTMLElement = chatNode instanceof Element; - const content = config.chatMessageContentSelector && isHTMLElement ? chatNode.querySelector(config.chatMessageContentSelector) @@ -112,6 +132,7 @@ const initContentScript = () => { : ""; const data = { + sessionId: sessionId, // id to identify the session of the chrome extension id: Math.random().toString(36).substr(2, 9), platform, content, @@ -134,11 +155,40 @@ const initContentScript = () => { }); }); }); + const setBadgeStatus = (status:boolean) => { + chrome.runtime.sendMessage({status: status, type: "badge"}, (response) => { + console.log('⚡[Chat Fusion] badge set'); + }); + + }; + const loadChat = () => { + const chatElement = document.querySelector(config.chatContainerSelector); + if (chatElement) { + chatObserver.observe(chatElement, { childList: true }); + return true; + }else{ + return false; + } + }; - const chatElement = document.querySelector(config.chatContainerSelector); - if (chatElement) { - chatObserver.observe(chatElement, { childList: true }); - } + + let retry = 0; + const interval = setInterval(() => { + if (retry > 3) { + clearInterval(interval); + console.error('⚠️[Chat Fusion] unable to connect to chat after 3 retries'); + setBadgeStatus(false); + + } + if(loadChat()){ + clearInterval(interval); + console.log('⚡[Chat Fusion] connected'); + setBadgeStatus(true); + } + retry++; + }, 1000); + + }; initContentScript(); diff --git a/src/ui/chat-fusion.png b/src/ui/chat-fusion.png new file mode 100644 index 0000000..0df70e6 Binary files /dev/null and b/src/ui/chat-fusion.png differ diff --git a/src/ui/github-mark.svg b/src/ui/github-mark.svg new file mode 100644 index 0000000..37fa923 --- /dev/null +++ b/src/ui/github-mark.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/ui/popup.html b/src/ui/popup.html new file mode 100644 index 0000000..04b5d6e --- /dev/null +++ b/src/ui/popup.html @@ -0,0 +1,42 @@ + + + + + + + Chat Fusion + + + +
+
+
+ Chat Fusion Logo +
+
+
+
+

Chat Fusion

+
+
+
+
+

Your Fusion Companion

+
+
+
+
+ +
+
+
+ +
+ GitHub Logo +
+
+
+ + \ No newline at end of file