Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,5 @@ node_modules
pnpm-lock.yaml
.todo
.env
messages.json
messages.json
api/*.js
29 changes: 29 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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
<p align="center">
<img src="./assets/images/popup.png" style="width: 400px;">
</p>

- 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

<p align="center">
<img src="./assets/images/badge.png">
</p>

### 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
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
14 changes: 14 additions & 0 deletions api/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import cors from "@fastify/cors";
import fs from "fs";

export interface IMessage {
sessionId: string;
id: string;
platform: string;
content: string;
Expand All @@ -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);
});

Expand Down
Binary file added assets/images/badge.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added assets/images/popup.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion chat/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
56 changes: 38 additions & 18 deletions chat/src/Components/Chat/Chat.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
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[];
focusedMessage: IMessage | null;
setFocusedMessage: (message: IMessage | null) => void;
}> = ({ messages, focusedMessage, setFocusedMessage }) => {
const messagesEndRef = useRef<HTMLDivElement | null>(null);

const [autoScroll, setAutoScroll] = useState<boolean>(true);
const handleFocusMessage = (message: IMessage) => {
setFocusedMessage(message.id === focusedMessage?.id ? null : message);
};
Expand All @@ -20,28 +21,47 @@ export const Chat: FC<{
}
};

useEffect(() => {
scrollToBottom();
}, [messages]);

return (
<div className="w-full p-12">
<div
className={classNames("flex flex-col gap-2", {
"items-center justify-center h-screen w-screen overflow-hidden":
focusedMessage,
})}
>
{messages.map((message: IMessage, index: number) => (
const mapMessages = () => {
return messages.map((message: IMessage, index: number) => {
if (message.author.toLocaleLowerCase() === "system") {
return (
<ChatDivider message={message} />
);
} else {
return (
<Message
key={index}
message={message}
focusedMessage={focusedMessage}
onMessageClick={handleFocusMessage}
/>
))}
<div ref={messagesEndRef}></div>
);
}
});
};

useEffect(() => {
if (autoScroll) {
scrollToBottom();
}
}, [messages, autoScroll]);

return (
<>
<div className="fixed bottom-0 left-0 right-0 h-10 w-full text-center text-white bg-slate-700">
Autoreload { autoScroll ? (<span className="text-green-700">ON</span>) : (<span className="text-red-700">OFF</span>)} <button className="bg-slate-700 hover:bg-slate-600 text-white font-bold py-2 px-4 rounded" onClick={() => setAutoScroll(!autoScroll)}>Toggle</button>
</div>
<div className="w-full p-12 flex-grow">
<div
className={classNames("flex flex-col gap-2", {
"items-center justify-center h-screen w-screen overflow-hidden":
focusedMessage,
})}
>
{mapMessages()}
<div ref={messagesEndRef}></div>
</div>
</div>
</div>
</>
);
};
17 changes: 17 additions & 0 deletions chat/src/Components/ChatDivider/ChatDivider.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { FC } from "react";
import { IMessage } from "../../types";


export const ChatDivider: FC<{
message: IMessage;
}> = ({ message}) => {
const { platform } = message;

return (
<div className="w-100 border-b border-b-red-700 text-center h-5 my-2">
<span className=" text-white text-xl mb-2 bg-red-700">
Chat Reload on: <b>{platform}</b>
</span>
</div>
);
};
1 change: 1 addition & 0 deletions chat/src/Components/ChatDivider/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from "./ChatDivider";
10 changes: 9 additions & 1 deletion manifest.json
Original file line number Diff line number Diff line change
@@ -1,12 +1,20 @@
{
"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": [
{
"matches": ["http://*/*", "https://*/*"],
"js": ["./dist/index.js"]
}
]
],
"background": {
"service_worker": "./dist/background.js"
}
}
10 changes: 6 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
21 changes: 21 additions & 0 deletions src/content/background.ts
Original file line number Diff line number Diff line change
@@ -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" });
}
};
60 changes: 55 additions & 5 deletions src/content/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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);
Expand All @@ -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)
Expand Down Expand Up @@ -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,
Expand All @@ -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();
Binary file added src/ui/chat-fusion.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions src/ui/github-mark.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading