diff --git a/.github/workflows/build-and-lint.yml b/.github/workflows/build-and-lint.yml new file mode 100644 index 000000000..d92c4001a --- /dev/null +++ b/.github/workflows/build-and-lint.yml @@ -0,0 +1,46 @@ +name: Build and Lint + +on: + push: + branches: [main, develop] + pull_request: + branches: [main, develop] + +jobs: + build-and-lint: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v3 + + - name: Set up Node.js + uses: actions/setup-node@v3 + with: + node-version: 16.19.0 + + - name: Set up Yarn + uses: actions/setup-node@v3 + with: + node-version: 16.19.0 + cache: 'yarn' + + - name: Cache dependencies + uses: actions/cache@v2 + with: + path: ~/.yarn + key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }} + restore-keys: | + ${{ runner.os }}-yarn- + + - name: Install dependencies + run: yarn + + - name: Format check + run: yarn format:check + + - name: Lint check + run: yarn lint + + - name: Build + run: yarn build && yarn build:storybook diff --git a/.github/workflows/playwright.yml b/.github/workflows/playwright.yml new file mode 100644 index 000000000..088f92dcd --- /dev/null +++ b/.github/workflows/playwright.yml @@ -0,0 +1,60 @@ +name: Playwright Tests +on: + push: + branches: [main, develop] + pull_request: + branches: [main, develop] +jobs: + test: + timeout-minutes: 60 + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v3 + + - name: Setup Node.js environment + uses: actions/setup-node@v3 + with: + node-version: 16.19.0 + + - name: Cache dependencies + uses: actions/cache@v2 + with: + path: ~/.yarn + key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }} + restore-keys: | + ${{ runner.os }}-yarn- + + - name: Install dependencies + run: yarn install + + - name: Build the project + run: | + yarn build + + - name: Get installed Playwright version + id: playwright-version + run: echo "::set-output name=version::$(yarn why --json @playwright/test | grep -h 'workspace:.' | jq --raw-output '.children[].locator' | sed -e 's/@playwright\/test@.*://')" + + - uses: actions/cache@v3 + id: playwright-cache + with: + path: '~/.cache/ms-playwright' + key: '${{ runner.os }}-playwright-${{ steps.playwright-version.outputs.version }}' + restore-keys: | + ${{ runner.os }}-playwright- + + - name: Install Playwright's dependencies + if: steps.playwright-cache.outputs.cache-hit != 'true' + run: npx playwright install --with-deps + + - name: Run Playwright tests + run: cd packages/e2e-react && npx playwright test + + - name: Upload Playwright report + uses: actions/upload-artifact@v3 + if: always() + with: + name: playwright-report + path: packages/e2e-react/playwright-report/ + retention-days: 30 diff --git a/README.md b/README.md index 81b81cd01..e5abf5c7f 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,7 @@ -# EmbeddedChat +

Embedded chat: A staple in excellent customer service

+ +![image](https://github.com/coderboy-yash/EmbeddedChat/assets/109899959/b2961a35-4300-48df-b674-8a128c73e838) + An easy to use full-stack component (ReactJS + backend behaviors) embedding Rocket.Chat into your webapp. @@ -13,7 +16,7 @@ _EmbeddedChat is a full-stack React component node module of the RocketChat appl ## Installation and Usage -Installtion and usage documentation could be found here [EmbeddedChat installation and usage](packages/react/README.md) +Installation and usage documentation could be found here [EmbeddedChat installation and usage](packages/react/README.md) ## Development @@ -42,6 +45,7 @@ To develop and test `EmbeddedChat`, a local instance of Rocket.Chat server is ne Install all necessary dependencies and build the packages (`auth`, `api`, and `react`) with: + ```bash yarn ``` diff --git a/package.json b/package.json index f19dbb472..086c7ff1d 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,11 @@ "scripts": { "preinstall": "node scripts/node-check.js", "postinstall": "yarn build", - "build": "lerna run build" + "build": "lerna run build", + "lint": "lerna run lint", + "build:storybook": "lerna run build-storybook", + "format": "lerna run format", + "format:check": "lerna run format:check" }, "devDependencies": { "@changesets/cli": "^2.26.2", diff --git a/packages/api/package.json b/packages/api/package.json index 1481af2e8..f6c304e94 100644 --- a/packages/api/package.json +++ b/packages/api/package.json @@ -9,7 +9,9 @@ "scripts": { "test": "echo \"Error: no test specified\" && exit 1", "build": "rollup -c", - "dev": "yarn parcel playground/index.html" + "dev": "yarn parcel playground/index.html", + "format": "prettier --write 'src/'", + "format:check": "prettier --check 'src/'" }, "author": "", "license": "ISC", diff --git a/packages/api/src/EmbeddedChatApi.ts b/packages/api/src/EmbeddedChatApi.ts index ce81a5427..6d3cc6987 100644 --- a/packages/api/src/EmbeddedChatApi.ts +++ b/packages/api/src/EmbeddedChatApi.ts @@ -1,6 +1,10 @@ -import { Rocketchat } from '@rocket.chat/sdk'; -import cloneArray from './cloneArray'; -import { IRocketChatAuthOptions, RocketChatAuth, ApiError } from '@embeddedchat/auth'; +import { Rocketchat } from "@rocket.chat/sdk"; +import cloneArray from "./cloneArray"; +import { + IRocketChatAuthOptions, + RocketChatAuth, + ApiError, +} from "@embeddedchat/auth"; // mutliple typing status can come at the same time they should be processed in order. let typingHandlerLock = 0; @@ -8,19 +12,23 @@ export default class EmbeddedChatApi { host: string; rid: string; rcClient: Rocketchat; - onMessageCallbacks: ((message: any) => void)[] - onMessageDeleteCallbacks: ((messageId: string) => void)[] + onMessageCallbacks: ((message: any) => void)[]; + onMessageDeleteCallbacks: ((messageId: string) => void)[]; onTypingStatusCallbacks: ((users: string[]) => void)[]; onActionTriggeredCallbacks: ((data: any) => void)[]; onUiInteractionCallbacks: ((data: any) => void)[]; typingUsers: string[]; auth: RocketChatAuth; - constructor(host: string, rid: string, { getToken, saveToken, deleteToken, autoLogin }: IRocketChatAuthOptions ) { + constructor( + host: string, + rid: string, + { getToken, saveToken, deleteToken, autoLogin }: IRocketChatAuthOptions + ) { this.host = host; this.rid = rid; this.rcClient = new Rocketchat({ - protocol: 'ddp', + protocol: "ddp", host: this.host, useSsl: !/http:\/\//.test(host), reopen: 20000, @@ -59,13 +67,13 @@ export default class EmbeddedChatApi { const tokens = await signIn(); let acsPayload = null; - if (typeof acsCode === 'string') { + if (typeof acsCode === "string") { acsPayload = acsCode; } const payload = acsCode ? JSON.stringify({ - serviceName: 'google', + serviceName: "google", accessToken: tokens.access_token, idToken: tokens.id_token, expiresIn: 3600, @@ -74,24 +82,24 @@ export default class EmbeddedChatApi { }, }) : JSON.stringify({ - serviceName: 'google', + serviceName: "google", accessToken: tokens.access_token, idToken: tokens.id_token, expiresIn: 3600, - scope: 'profile', + scope: "profile", }); try { const req = await fetch(`${this.host}/api/v1/login`, { - method: 'POST', + method: "POST", headers: { - 'Content-Type': 'application/json', + "Content-Type": "application/json", }, body: payload, }); const response = await req.json(); - if (response.status === 'success') { + if (response.status === "success") { if (!response.data.me.username) { await this.updateUserUsername( response.data.userId, @@ -101,7 +109,7 @@ export default class EmbeddedChatApi { return { status: response.status, me: response.data.me }; } - if (response.error === 'totp-required') { + if (response.error === "totp-required") { return response; } } catch (err) { @@ -126,12 +134,9 @@ export default class EmbeddedChatApi { try { const data = await this.auth.loginWithPassword(credentials); if (!data.me.username) { - await this.updateUserUsername( - data.userId, - data.me.name - ); + await this.updateUserUsername(data.userId, data.me.name); } - return { status: 'success', me: data.me }; + return { status: "success", me: data.me }; } catch (error) { if (error instanceof ApiError && error.response?.status === 401) { const authErrorRes = await error.response.json(); @@ -165,7 +170,7 @@ export default class EmbeddedChatApi { return; } const message = JSON.parse(JSON.stringify(data)); - if( message.ts?.$date) { + if (message.ts?.$date) { console.log(message.ts?.$date); message.ts = message.ts.$date; } @@ -175,58 +180,68 @@ export default class EmbeddedChatApi { this.onMessageCallbacks.map((callback) => callback(message)); }); await this.rcClient.subscribe( - 'stream-notify-room', + "stream-notify-room", `${this.rid}/user-activity` ); - await this.rcClient.onStreamData('stream-notify-room', (ddpMessage: any) => { - const [roomId, event] = ddpMessage.fields.eventName.split('/'); + await this.rcClient.onStreamData( + "stream-notify-room", + (ddpMessage: any) => { + const [roomId, event] = ddpMessage.fields.eventName.split("/"); - if (roomId !== this.rid) { - return; - } + if (roomId !== this.rid) { + return; + } - if (event === 'user-activity') { - const typingUser = ddpMessage.fields.args[0]; - const isTyping = ddpMessage.fields.args[1]?.includes('user-typing'); - this.handleTypingEvent({ typingUser, isTyping }); - } + if (event === "user-activity") { + const typingUser = ddpMessage.fields.args[0]; + const isTyping = ddpMessage.fields.args[1]?.includes("user-typing"); + this.handleTypingEvent({ typingUser, isTyping }); + } - if (event === 'typing') { - const typingUser = ddpMessage.fields.args[0]; - const isTyping = ddpMessage.fields.args[1]; - this.handleTypingEvent({ typingUser, isTyping }); - } - if (event === 'deleteMessage') { - const messageId = ddpMessage.fields.args[0]?._id; - this.onMessageDeleteCallbacks.map((callback) => callback(messageId)); - } - }); - await this.rcClient.subscribeNotifyUser(); - await this.rcClient.onStreamData('stream-notify-user', (ddpMessage: any) => { - const [, event] = ddpMessage.fields.eventName.split('/'); - const args: any[] = ddpMessage.fields.args - ? Array.isArray(ddpMessage.fields.args) - ? ddpMessage.fields.args - : [ddpMessage.fields.args] - : []; - if (event === 'message') { - const data = args[0]; - if (!data || data?.rid !== this.rid) { - return; + if (event === "typing") { + const typingUser = ddpMessage.fields.args[0]; + const isTyping = ddpMessage.fields.args[1]; + this.handleTypingEvent({ typingUser, isTyping }); } - const message = JSON.parse(JSON.stringify(data)); - if( message.ts?.$date) { - message.ts = message.ts.$date; + if (event === "deleteMessage") { + const messageId = ddpMessage.fields.args[0]?._id; + this.onMessageDeleteCallbacks.map((callback) => + callback(messageId) + ); } - if (!message.ts) { - message.ts = new Date().toISOString(); + } + ); + await this.rcClient.subscribeNotifyUser(); + await this.rcClient.onStreamData( + "stream-notify-user", + (ddpMessage: any) => { + const [, event] = ddpMessage.fields.eventName.split("/"); + const args: any[] = ddpMessage.fields.args + ? Array.isArray(ddpMessage.fields.args) + ? ddpMessage.fields.args + : [ddpMessage.fields.args] + : []; + if (event === "message") { + const data = args[0]; + if (!data || data?.rid !== this.rid) { + return; + } + const message = JSON.parse(JSON.stringify(data)); + if (message.ts?.$date) { + message.ts = message.ts.$date; + } + if (!message.ts) { + message.ts = new Date().toISOString(); + } + message.renderType = "blocks"; + this.onMessageCallbacks.map((callback) => callback(message)); + } else if (event === "uiInteraction") { + this.onUiInteractionCallbacks.forEach((callback) => + callback(args[0]) + ); } - message.renderType = 'blocks'; - this.onMessageCallbacks.map((callback) => callback(message)); - } else if (event === 'uiInteraction') { - this.onUiInteractionCallbacks.forEach((callback) => callback(args[0])); } - }); + ); } catch (err) { await this.close(); } @@ -295,9 +310,7 @@ export default class EmbeddedChatApi { } async addUiInteractionListener(callback: (data: any) => void) { - const idx = this.onUiInteractionCallbacks.findIndex( - (c) => c === callback - ); + const idx = this.onUiInteractionCallbacks.findIndex((c) => c === callback); if (idx !== -1) { this.onUiInteractionCallbacks[idx] = callback; } else { @@ -311,7 +324,10 @@ export default class EmbeddedChatApi { ); } - handleTypingEvent({ typingUser, isTyping }: { + handleTypingEvent({ + typingUser, + isTyping, + }: { typingUser: string; isTyping: boolean; }) { @@ -339,16 +355,16 @@ export default class EmbeddedChatApi { async updateUserNameThroughSuggestion(userid: string) { try { - const { userId, authToken } = await this.auth.getCurrentUser() || {}; + const { userId, authToken } = (await this.auth.getCurrentUser()) || {}; const response = await fetch( `${this.host}/api/v1/users.getUsernameSuggestion`, { headers: { - 'Content-Type': 'application/json', - 'X-Auth-Token': authToken, - 'X-User-Id': userId, + "Content-Type": "application/json", + "X-Auth-Token": authToken, + "X-User-Id": userId, }, - method: 'GET', + method: "GET", } ); @@ -358,11 +374,11 @@ export default class EmbeddedChatApi { const response2 = await fetch(`${this.host}/api/v1/users.update`, { body: `{"userId": "${userid}", "data": { "username": "${suggestedUsername.result}" }}`, headers: { - 'Content-Type': 'application/json', - 'X-Auth-Token': authToken, - 'X-User-Id': userId, + "Content-Type": "application/json", + "X-Auth-Token": authToken, + "X-User-Id": userId, }, - method: 'POST', + method: "POST", }); return await response2.json(); @@ -373,28 +389,28 @@ export default class EmbeddedChatApi { } async updateUserUsername(userid: string, username: string) { - const newUserName = username.replace(/\s/g, '.').toLowerCase(); + const newUserName = username.replace(/\s/g, ".").toLowerCase(); const usernameRegExp = /[0-9a-zA-Z-_.]+/; if (usernameRegExp.test(newUserName)) { try { - const { userId, authToken } = await this.auth.getCurrentUser() || {}; + const { userId, authToken } = (await this.auth.getCurrentUser()) || {}; const response = await fetch(`${this.host}/api/v1/users.update`, { body: `{"userId": "${userid}", "data": { "username": "${newUserName}" }}`, headers: { - 'Content-Type': 'application/json', - 'X-Auth-Token': authToken, - 'X-User-Id': userId, + "Content-Type": "application/json", + "X-Auth-Token": authToken, + "X-User-Id": userId, }, - method: 'POST', + method: "POST", }); const result = await response.json(); if ( !result.success && - result.errorType === 'error-could-not-save-identity' + result.errorType === "error-could-not-save-identity" ) { return await this.updateUserNameThroughSuggestion(userid); } @@ -409,16 +425,16 @@ export default class EmbeddedChatApi { async channelInfo() { try { - const { userId, authToken } = await this.auth.getCurrentUser() || {}; + const { userId, authToken } = (await this.auth.getCurrentUser()) || {}; const response = await fetch( `${this.host}/api/v1/rooms.info?roomId=${this.rid}`, { headers: { - 'Content-Type': 'application/json', - 'X-Auth-Token': authToken, - 'X-User-Id': userId, + "Content-Type": "application/json", + "X-Auth-Token": authToken, + "X-User-Id": userId, }, - method: 'GET', + method: "GET", } ); return await response.json(); @@ -427,6 +443,23 @@ export default class EmbeddedChatApi { } } + async permissionInfo() { + try { + const { userId, authToken } = (await this.auth.getCurrentUser()) || {}; + const response = await fetch(`${this.host}/api/v1/permissions.listAll`, { + headers: { + "Content-Type": "application/json", + "X-Auth-Token": authToken, + "X-User-Id": userId, + }, + method: "GET", + }); + return await response.json(); + } catch (err) { + console.error(err); + } + } + async close() { await this.rcClient.unsubscribeAll(); await this.rcClient.disconnect(); @@ -439,32 +472,36 @@ export default class EmbeddedChatApi { * fields - json object with properties that have either 1 or 0 to include them or exclude them * @returns messages */ - async getMessages(anonymousMode = false, options: { - query?: object | undefined; - field?: object | undefined; + async getMessages( + anonymousMode = false, + options: { + query?: object | undefined; + field?: object | undefined; } = { query: undefined, - field: undefined - }, isChannelPrivate = false) { - const roomType = isChannelPrivate ? 'groups' : 'channels' ; - const endp = anonymousMode ? 'anonymousread' : 'messages'; + field: undefined, + }, + isChannelPrivate = false + ) { + const roomType = isChannelPrivate ? "groups" : "channels"; + const endp = anonymousMode ? "anonymousread" : "messages"; const query = options?.query ? `&query=${JSON.stringify(options.query)}` - : ''; + : ""; const field = options?.field ? `&field=${JSON.stringify(options.field)}` - : ''; + : ""; try { - const { userId, authToken } = await this.auth.getCurrentUser() || {}; + const { userId, authToken } = (await this.auth.getCurrentUser()) || {}; const messages = await fetch( `${this.host}/api/v1/${roomType}.${endp}?roomId=${this.rid}${query}${field}`, { headers: { - 'Content-Type': 'application/json', - 'X-Auth-Token': authToken, - 'X-User-Id': userId, + "Content-Type": "application/json", + "X-Auth-Token": authToken, + "X-User-Id": userId, }, - method: 'GET', + method: "GET", } ); return await messages.json(); @@ -474,26 +511,30 @@ export default class EmbeddedChatApi { } async getThreadMessages(tmid: string, isChannelPrivate = false) { - return this.getMessages(false, { - query: { - tmid, + return this.getMessages( + false, + { + query: { + tmid, + }, }, - }, isChannelPrivate); + isChannelPrivate + ); } async getChannelRoles(isChannelPrivate = false) { - const roomType = isChannelPrivate ? 'groups' : 'channels'; + const roomType = isChannelPrivate ? "groups" : "channels"; try { - const { userId, authToken } = await this.auth.getCurrentUser() || {}; + const { userId, authToken } = (await this.auth.getCurrentUser()) || {}; const roles = await fetch( `${this.host}/api/v1/${roomType}.roles?roomId=${this.rid}`, { headers: { - 'Content-Type': 'application/json', - 'X-Auth-Token': authToken, - 'X-User-Id': userId, + "Content-Type": "application/json", + "X-Auth-Token": authToken, + "X-User-Id": userId, }, - method: 'GET', + method: "GET", } ); return await roles.json(); @@ -505,10 +546,10 @@ export default class EmbeddedChatApi { async sendTypingStatus(username: string, typing: boolean) { try { this.rcClient.methodCall( - 'stream-notify-room', + "stream-notify-room", `${this.rid}/user-activity`, username, - typing ? ['user-typing'] : [] + typing ? ["user-typing"] : [] ); } catch (err) { console.error(err); @@ -521,7 +562,7 @@ export default class EmbeddedChatApi { */ async sendMessage(message: any, threadId: string) { const messageObj = - typeof message === 'string' + typeof message === "string" ? { rid: this.rid, msg: message, @@ -534,15 +575,15 @@ export default class EmbeddedChatApi { messageObj.tmid = threadId; } try { - const { userId, authToken } = await this.auth.getCurrentUser() || {}; + const { userId, authToken } = (await this.auth.getCurrentUser()) || {}; const response = await fetch(`${this.host}/api/v1/chat.sendMessage`, { body: JSON.stringify({ message: messageObj }), headers: { - 'Content-Type': 'application/json', - 'X-Auth-Token': authToken, - 'X-User-Id': userId, + "Content-Type": "application/json", + "X-Auth-Token": authToken, + "X-User-Id": userId, }, - method: 'POST', + method: "POST", }); return await response.json(); } catch (err) { @@ -552,15 +593,15 @@ export default class EmbeddedChatApi { async deleteMessage(msgId: string) { try { - const { userId, authToken } = await this.auth.getCurrentUser() || {}; + const { userId, authToken } = (await this.auth.getCurrentUser()) || {}; const response = await fetch(`${this.host}/api/v1/chat.delete`, { body: `{"roomId": "${this.rid}", "msgId": "${msgId}","asUser" : true }`, headers: { - 'Content-Type': 'application/json', - 'X-Auth-Token': authToken, - 'X-User-Id': userId, + "Content-Type": "application/json", + "X-Auth-Token": authToken, + "X-User-Id": userId, }, - method: 'POST', + method: "POST", }); return await response.json(); } catch (err) { @@ -570,15 +611,15 @@ export default class EmbeddedChatApi { async updateMessage(msgId: string, text: string) { try { - const { userId, authToken } = await this.auth.getCurrentUser() || {}; + const { userId, authToken } = (await this.auth.getCurrentUser()) || {}; const response = await fetch(`${this.host}/api/v1/chat.update`, { body: `{"roomId": "${this.rid}", "msgId": "${msgId}","text" : "${text}" }`, headers: { - 'Content-Type': 'application/json', - 'X-Auth-Token': authToken, - 'X-User-Id': userId, + "Content-Type": "application/json", + "X-Auth-Token": authToken, + "X-User-Id": userId, }, - method: 'POST', + method: "POST", }); return await response.json(); } catch (err) { @@ -588,15 +629,15 @@ export default class EmbeddedChatApi { async starMessage(mid: string) { try { - const { userId, authToken } = await this.auth.getCurrentUser() || {}; + const { userId, authToken } = (await this.auth.getCurrentUser()) || {}; const response = await fetch(`${this.host}/api/v1/chat.starMessage`, { body: `{"messageId": "${mid}"}`, headers: { - 'Content-Type': 'application/json', - 'X-Auth-Token': authToken, - 'X-User-Id': userId, + "Content-Type": "application/json", + "X-Auth-Token": authToken, + "X-User-Id": userId, }, - method: 'POST', + method: "POST", }); return await response.json(); } catch (err) { @@ -606,15 +647,15 @@ export default class EmbeddedChatApi { async unstarMessage(mid: string) { try { - const { userId, authToken } = await this.auth.getCurrentUser() || {}; + const { userId, authToken } = (await this.auth.getCurrentUser()) || {}; const response = await fetch(`${this.host}/api/v1/chat.unStarMessage`, { body: `{"messageId": "${mid}"}`, headers: { - 'Content-Type': 'application/json', - 'X-Auth-Token': authToken, - 'X-User-Id': userId, + "Content-Type": "application/json", + "X-Auth-Token": authToken, + "X-User-Id": userId, }, - method: 'POST', + method: "POST", }); return await response.json(); } catch (err) { @@ -624,16 +665,16 @@ export default class EmbeddedChatApi { async getStarredMessages() { try { - const { userId, authToken } = await this.auth.getCurrentUser() || {}; + const { userId, authToken } = (await this.auth.getCurrentUser()) || {}; const response = await fetch( `${this.host}/api/v1/chat.getStarredMessages?roomId=${this.rid}`, { headers: { - 'Content-Type': 'application/json', - 'X-Auth-Token': authToken, - 'X-User-Id': userId, + "Content-Type": "application/json", + "X-Auth-Token": authToken, + "X-User-Id": userId, }, - method: 'GET', + method: "GET", } ); return await response.json(); @@ -644,16 +685,16 @@ export default class EmbeddedChatApi { async getPinnedMessages() { try { - const { userId, authToken } = await this.auth.getCurrentUser() || {}; + const { userId, authToken } = (await this.auth.getCurrentUser()) || {}; const response = await fetch( `${this.host}/api/v1/chat.getPinnedMessages?roomId=${this.rid}`, { headers: { - 'Content-Type': 'application/json', - 'X-Auth-Token': authToken, - 'X-User-Id': userId, + "Content-Type": "application/json", + "X-Auth-Token": authToken, + "X-User-Id": userId, }, - method: 'GET', + method: "GET", } ); return await response.json(); @@ -664,15 +705,15 @@ export default class EmbeddedChatApi { async pinMessage(mid: string) { try { - const { userId, authToken } = await this.auth.getCurrentUser() || {}; + const { userId, authToken } = (await this.auth.getCurrentUser()) || {}; const response = await fetch(`${this.host}/api/v1/chat.pinMessage`, { body: `{"messageId": "${mid}"}`, headers: { - 'Content-Type': 'application/json', - 'X-Auth-Token': authToken, - 'X-User-Id': userId, + "Content-Type": "application/json", + "X-Auth-Token": authToken, + "X-User-Id": userId, }, - method: 'POST', + method: "POST", }); return await response.json(); } catch (err) { @@ -684,15 +725,15 @@ export default class EmbeddedChatApi { async unpinMessage(mid: string) { try { - const { userId, authToken } = await this.auth.getCurrentUser() || {}; + const { userId, authToken } = (await this.auth.getCurrentUser()) || {}; const response = await fetch(`${this.host}/api/v1/chat.unPinMessage`, { body: `{"messageId": "${mid}"}`, headers: { - 'Content-Type': 'application/json', - 'X-Auth-Token': authToken, - 'X-User-Id': userId, + "Content-Type": "application/json", + "X-Auth-Token": authToken, + "X-User-Id": userId, }, - method: 'POST', + method: "POST", }); return await response.json(); } catch (err) { @@ -702,15 +743,15 @@ export default class EmbeddedChatApi { async reactToMessage(emoji: string, messageId: string, shouldReact: string) { try { - const { userId, authToken } = await this.auth.getCurrentUser() || {}; + const { userId, authToken } = (await this.auth.getCurrentUser()) || {}; const response = await fetch(`${this.host}/api/v1/chat.react`, { body: `{"messageId": "${messageId}", "emoji": "${emoji}", "shouldReact": ${shouldReact}}`, headers: { - 'Content-Type': 'application/json', - 'X-Auth-Token': authToken, - 'X-User-Id': userId, + "Content-Type": "application/json", + "X-Auth-Token": authToken, + "X-User-Id": userId, }, - method: 'POST', + method: "POST", }); return await response.json(); } catch (err) { @@ -720,15 +761,15 @@ export default class EmbeddedChatApi { async reportMessage(messageId: string, description: string) { try { - const { userId, authToken } = await this.auth.getCurrentUser() || {}; + const { userId, authToken } = (await this.auth.getCurrentUser()) || {}; const response = await fetch(`${this.host}/api/v1/chat.reportMessage`, { body: `{"messageId": "${messageId}", "description": "${description}"}`, headers: { - 'Content-Type': 'application/json', - 'X-Auth-Token': authToken, - 'X-User-Id': userId, + "Content-Type": "application/json", + "X-Auth-Token": authToken, + "X-User-Id": userId, }, - method: 'POST', + method: "POST", }); return await response.json(); } catch (err) { @@ -738,14 +779,14 @@ export default class EmbeddedChatApi { async findOrCreateInvite() { try { - const { userId, authToken } = await this.auth.getCurrentUser() || {}; + const { userId, authToken } = (await this.auth.getCurrentUser()) || {}; const response = await fetch(`${this.host}/api/v1/findOrCreateInvite`, { - method: 'POST', + method: "POST", body: JSON.stringify({ rid: this.rid, days: 1, maxUses: 10 }), headers: { - 'Content-Type': 'application/json', - 'X-Auth-Token': authToken, - 'X-User-Id': userId, + "Content-Type": "application/json", + "X-Auth-Token": authToken, + "X-User-Id": userId, }, }); return await response.json(); @@ -757,26 +798,26 @@ export default class EmbeddedChatApi { async sendAttachment( file: File, fileName: string, - fileDescription = '', + fileDescription = "", threadId = undefined ) { try { - const { userId, authToken } = await this.auth.getCurrentUser() || {}; + const { userId, authToken } = (await this.auth.getCurrentUser()) || {}; const form = new FormData(); if (threadId) { - form.append('tmid', threadId); + form.append("tmid", threadId); } - form.append('file', file, fileName); + form.append("file", file, fileName); form.append( - 'description', - fileDescription.length !== 0 ? fileDescription : '' + "description", + fileDescription.length !== 0 ? fileDescription : "" ); const response = fetch(`${this.host}/api/v1/rooms.upload/${this.rid}`, { - method: 'POST', + method: "POST", body: form, headers: { - 'X-Auth-Token': authToken, - 'X-User-Id': userId, + "X-Auth-Token": authToken, + "X-User-Id": userId, }, }).then((r) => r.json()); return response; @@ -787,14 +828,14 @@ export default class EmbeddedChatApi { async me() { try { - const { userId, authToken } = await this.auth.getCurrentUser() || {}; + const { userId, authToken } = (await this.auth.getCurrentUser()) || {}; const response = await fetch(`${this.host}/api/v1/me`, { headers: { - 'Content-Type': 'application/json', - 'X-Auth-Token': authToken, - 'X-User-Id': userId, + "Content-Type": "application/json", + "X-Auth-Token": authToken, + "X-User-Id": userId, }, - method: 'GET', + method: "GET", }); return await response.json(); } catch (err) { @@ -803,18 +844,18 @@ export default class EmbeddedChatApi { } async getChannelMembers(isChannelPrivate = false) { - const roomType = isChannelPrivate ? 'groups' : 'channels'; + const roomType = isChannelPrivate ? "groups" : "channels"; try { - const { userId, authToken } = await this.auth.getCurrentUser() || {}; + const { userId, authToken } = (await this.auth.getCurrentUser()) || {}; const response = await fetch( `${this.host}/api/v1/${roomType}.members?roomId=${this.rid}`, { headers: { - 'Content-Type': 'application/json', - 'X-Auth-Token': authToken, - 'X-User-Id': userId, + "Content-Type": "application/json", + "X-Auth-Token": authToken, + "X-User-Id": userId, }, - method: 'GET', + method: "GET", } ); return await response.json(); @@ -825,16 +866,16 @@ export default class EmbeddedChatApi { async getSearchMessages(text: string) { try { - const { userId, authToken } = await this.auth.getCurrentUser() || {}; + const { userId, authToken } = (await this.auth.getCurrentUser()) || {}; const response = await fetch( `${this.host}/api/v1/chat.search?roomId=${this.rid}&searchText=${text}`, { headers: { - 'Content-Type': 'application/json', - 'X-Auth-Token': authToken, - 'X-User-Id': userId, + "Content-Type": "application/json", + "X-Auth-Token": authToken, + "X-User-Id": userId, }, - method: 'GET', + method: "GET", } ); return await response.json(); @@ -853,7 +894,7 @@ export default class EmbeddedChatApi { ...rest }: any) { try { - const { userId, authToken } = await this.auth.getCurrentUser() || {}; + const { userId, authToken } = (await this.auth.getCurrentUser()) || {}; const triggerId = Math.random().toString(32).slice(2, 16); @@ -863,13 +904,13 @@ export default class EmbeddedChatApi { `${this.host}/api/apps/ui.interaction/${appId}`, { headers: { - 'Content-Type': 'application/json', - 'X-Auth-Token': authToken, - 'X-User-Id': userId, + "Content-Type": "application/json", + "X-Auth-Token": authToken, + "X-User-Id": userId, }, - method: 'POST', + method: "POST", body: JSON.stringify({ - type: 'blockAction', + type: "blockAction", actionId, payload, container, @@ -889,28 +930,28 @@ export default class EmbeddedChatApi { } async getCommandsList() { - const { userId, authToken } = await this.auth.getCurrentUser() || {}; + const { userId, authToken } = (await this.auth.getCurrentUser()) || {}; const response = await fetch(`${this.host}/api/v1/commands.list`, { headers: { - 'Content-Type': 'application/json', - 'X-Auth-Token': authToken, - 'X-User-Id': userId, + "Content-Type": "application/json", + "X-Auth-Token": authToken, + "X-User-Id": userId, }, - method: 'GET', + method: "GET", }); const data = await response.json(); return data; } - async execCommand({ command, params }: { command: string, params: string; }) { - const { userId, authToken } = await this.auth.getCurrentUser() || {}; + async execCommand({ command, params }: { command: string; params: string }) { + const { userId, authToken } = (await this.auth.getCurrentUser()) || {}; const response = await fetch(`${this.host}/api/v1/commands.run`, { headers: { - 'Content-Type': 'application/json', - 'X-Auth-Token': authToken, - 'X-User-Id': userId, + "Content-Type": "application/json", + "X-Auth-Token": authToken, + "X-User-Id": userId, }, - method: 'POST', + method: "POST", body: JSON.stringify({ command, params, diff --git a/packages/api/src/cloneArray.ts b/packages/api/src/cloneArray.ts index cbdaa185d..ceb43ad43 100644 --- a/packages/api/src/cloneArray.ts +++ b/packages/api/src/cloneArray.ts @@ -5,7 +5,7 @@ */ const cloneArray = (array: any[]) => { const newArray = [...array].map((item) => - typeof item === 'object' ? { ...item } : item + typeof item === "object" ? { ...item } : item ); return newArray; }; diff --git a/packages/api/src/index.ts b/packages/api/src/index.ts index aa9ac1d7d..b4ff83a1c 100644 --- a/packages/api/src/index.ts +++ b/packages/api/src/index.ts @@ -1 +1 @@ -export { default as EmbeddedChatApi } from './EmbeddedChatApi' +export { default as EmbeddedChatApi } from "./EmbeddedChatApi"; diff --git a/packages/auth/package.json b/packages/auth/package.json index 6e49486b5..0eb2f1a77 100644 --- a/packages/auth/package.json +++ b/packages/auth/package.json @@ -9,7 +9,9 @@ "scripts": { "test": "echo \"Error: no test specified\" && exit 1", "build": "rollup -c", - "dev": "yarn parcel playground/index.html" + "dev": "yarn parcel playground/index.html", + "format": "prettier --write 'src/' ", + "format:check": "prettier --check 'src/' " }, "author": "", "license": "ISC", diff --git a/packages/auth/src/Api.ts b/packages/auth/src/Api.ts index d124efe0a..78d6c82c7 100644 --- a/packages/auth/src/Api.ts +++ b/packages/auth/src/Api.ts @@ -1,58 +1,63 @@ export class ApiError extends Error { - response: Response; - constructor( - response: Response, - message?: string | undefined, - options?: ErrorOptions | undefined, - ...other: any[] - ) { - super(message, options, ...other as []); - this.response = response; - } + response: Response; + constructor( + response: Response, + message?: string | undefined, + options?: ErrorOptions | undefined, + ...other: any[] + ) { + super(message, options, ...(other as [])); + this.response = response; + } } export class Api { - baseUrl: string; - constructor(baseUrl: string) { - this.baseUrl = baseUrl; - } - getFetchConfig = (config: RequestInit) => { - const headers = { - 'Content-Type': 'application/json', - ...(config?.headers || {}) - } - const requestInit: RequestInit = { - ...config, - headers, - } - return requestInit; - } - async request(method: string = 'GET', endpoint: string, data: any, config: RequestInit) { - const url = new URL(endpoint, this.baseUrl).toString(); - const response = await fetch(url, { - body: data ? JSON.stringify(data) : undefined, - method, - headers: { - 'Content-Type': 'application/json' - } - }); - if (!response.ok) { - throw new ApiError(response, "Failed Api Request for "+endpoint); - } - const jsonData = await response.json(); - return { data: jsonData }; - } + baseUrl: string; + constructor(baseUrl: string) { + this.baseUrl = baseUrl; + } + getFetchConfig = (config: RequestInit) => { + const headers = { + "Content-Type": "application/json", + ...(config?.headers || {}), + }; + const requestInit: RequestInit = { + ...config, + headers, + }; + return requestInit; + }; + async request( + method: string = "GET", + endpoint: string, + data: any, + config: RequestInit + ) { + const url = new URL(endpoint, this.baseUrl).toString(); + const response = await fetch(url, { + body: data ? JSON.stringify(data) : undefined, + method, + headers: { + ...config.headers, + }, + }); + if (!response.ok) { + throw new ApiError(response, "Failed Api Request for " + endpoint); + } + const jsonData = await response.json(); + return { data: jsonData }; + } - async post(endpoint: string, data: any, config: RequestInit = {}) { - return this.request('POST', endpoint, data, this.getFetchConfig(config)); - } - async get(endpoint: string, config: RequestInit = {}) { - return this.request('GET', endpoint, null, this.getFetchConfig(config)); - } - async put(endpoint: string, data: any, config: RequestInit = {}) { - return this.request('PUT', endpoint, data, this.getFetchConfig(config)); - } - async delete(endpoint: string, config: RequestInit = {}) { - return this.request('DELETE', endpoint, null, this.getFetchConfig(config)); - } + async post(endpoint: string, data: any, config: RequestInit = {}) { + return this.request("POST", endpoint, data, this.getFetchConfig(config)); + } + async get(endpoint: string, config: RequestInit = {}) { + return this.request("GET", endpoint, null, this.getFetchConfig(config)); + } + async put(endpoint: string, data: any, config: RequestInit = {}) { + return this.request("PUT", endpoint, data, this.getFetchConfig(config)); + } + async delete(endpoint: string, config: RequestInit = {}) { + return this.request("DELETE", endpoint, null, this.getFetchConfig(config)); + } } diff --git a/packages/auth/src/IRocketChatAuthOptions.ts b/packages/auth/src/IRocketChatAuthOptions.ts index 06181cdb2..1c452fa4a 100644 --- a/packages/auth/src/IRocketChatAuthOptions.ts +++ b/packages/auth/src/IRocketChatAuthOptions.ts @@ -1,7 +1,7 @@ export interface IRocketChatAuthOptions { - host: string; - saveToken: (token: string) => Promise; - getToken: () => Promise; - deleteToken: () => Promise; - autoLogin?: boolean; + host: string; + saveToken: (token: string) => Promise; + getToken: () => Promise; + deleteToken: () => Promise; + autoLogin?: boolean; } diff --git a/packages/auth/src/auth.ts b/packages/auth/src/auth.ts index e8b8620a6..5d06758da 100644 --- a/packages/auth/src/auth.ts +++ b/packages/auth/src/auth.ts @@ -1,22 +1,20 @@ -import { IRocketChatAuthOptions } from './IRocketChatAuthOptions'; -import RocketChatAuth from './RocketChatAuth'; +import { IRocketChatAuthOptions } from "./IRocketChatAuthOptions"; +import RocketChatAuth from "./RocketChatAuth"; const rocketChatAuth = ({ - host, - saveToken, - getToken, - deleteToken, - autoLogin, + host, + saveToken, + getToken, + deleteToken, + autoLogin, }: IRocketChatAuthOptions) => { - return new RocketChatAuth({ - host, - saveToken, - getToken, - deleteToken, - autoLogin, - }); -} + return new RocketChatAuth({ + host, + saveToken, + getToken, + deleteToken, + autoLogin, + }); +}; -export { - rocketChatAuth -} +export { rocketChatAuth }; diff --git a/packages/auth/src/getAuthorizationUrl.ts b/packages/auth/src/getAuthorizationUrl.ts index 1c9346706..32e9a5e15 100644 --- a/packages/auth/src/getAuthorizationUrl.ts +++ b/packages/auth/src/getAuthorizationUrl.ts @@ -1,12 +1,15 @@ const getAuthorizationUrl = (oauthService: any) => { - if (oauthService.authorizePath?.startsWith('http')){ - return oauthService.authorizePath; - } - if (oauthService.serverURL) { - return new URL(oauthService.authorizePath, oauthService.serverURL).toString(); - } else { - return oauthService.authorizePath; - } -} + if (oauthService.authorizePath?.startsWith("http")) { + return oauthService.authorizePath; + } + if (oauthService.serverURL) { + return new URL( + oauthService.authorizePath, + oauthService.serverURL + ).toString(); + } else { + return oauthService.authorizePath; + } +}; export default getAuthorizationUrl; diff --git a/packages/auth/src/index.ts b/packages/auth/src/index.ts index 91b9edfc0..ff36dfb6a 100644 --- a/packages/auth/src/index.ts +++ b/packages/auth/src/index.ts @@ -1,4 +1,4 @@ -export * from './auth'; -export { default as RocketChatAuth } from './RocketChatAuth'; -export * from './IRocketChatAuthOptions'; -export {ApiError} from './Api'; \ No newline at end of file +export * from "./auth"; +export { default as RocketChatAuth } from "./RocketChatAuth"; +export * from "./IRocketChatAuthOptions"; +export { ApiError } from "./Api"; diff --git a/packages/auth/src/loginWithOAuthServiceToken.ts b/packages/auth/src/loginWithOAuthServiceToken.ts index bd823505b..64518ee44 100644 --- a/packages/auth/src/loginWithOAuthServiceToken.ts +++ b/packages/auth/src/loginWithOAuthServiceToken.ts @@ -1,17 +1,17 @@ import { Api } from "./Api"; const loginWithOAuthServiceToken = async ( - config: { - api: Api - }, - credentials: { - service: string; - access_token: string; - [key:string]: string; - } + config: { + api: Api; + }, + credentials: { + service: string; + access_token: string; + [key: string]: string; + } ) => { - const response = await config.api.post('/api/v1/login', credentials) - return response.data; -} + const response = await config.api.post("/api/v1/login", credentials); + return response.data; +}; export default loginWithOAuthServiceToken; diff --git a/packages/auth/src/loginWithPassword.ts b/packages/auth/src/loginWithPassword.ts index c85a2ee1f..7843efb38 100644 --- a/packages/auth/src/loginWithPassword.ts +++ b/packages/auth/src/loginWithPassword.ts @@ -1,23 +1,25 @@ import { Api } from "./Api"; const loginWithPassword = async ( - config : { - api: Api; - }, { - user, - password, - code, - }: { - user: string; - password: string; - code?: string | number; -}) => { - const response = await config.api.post('/api/v1/login', { - user, - password, - code - }) - return response.data; -} + config: { + api: Api; + }, + { + user, + password, + code, + }: { + user: string; + password: string; + code?: string | number; + } +) => { + const response = await config.api.post("/api/v1/login", { + user, + password, + code, + }); + return response.data; +}; export default loginWithPassword; diff --git a/packages/auth/src/loginWithResumeToken.ts b/packages/auth/src/loginWithResumeToken.ts index e85407dbd..b9b1f2b6e 100644 --- a/packages/auth/src/loginWithResumeToken.ts +++ b/packages/auth/src/loginWithResumeToken.ts @@ -1,15 +1,15 @@ import { Api } from "./Api"; const loginWithResumeToken = async ( - config: { - api: Api - }, - credentials: { - resume: string - } + config: { + api: Api; + }, + credentials: { + resume: string; + } ) => { - const response = await config.api.post('/api/v1/login', credentials) - return response.data; -} + const response = await config.api.post("/api/v1/login", credentials); + return response.data; +}; export default loginWithResumeToken; diff --git a/packages/auth/src/loginWithRocketChatOAuth.ts b/packages/auth/src/loginWithRocketChatOAuth.ts index 7eea59823..de01ae295 100644 --- a/packages/auth/src/loginWithRocketChatOAuth.ts +++ b/packages/auth/src/loginWithRocketChatOAuth.ts @@ -52,17 +52,17 @@ width=800,height=600,left=-1000,top=-1000,rel=opener`; expiresIn, serviceName, }); - popup.close(); + popup.close(); resolve(response.data); } }; window.addEventListener("message", onMessage); const checkInterval = setInterval(() => { - if (popup.closed) { - clearInterval(checkInterval); - window.removeEventListener("message", onMessage); - } - }, 1000); + if (popup.closed) { + clearInterval(checkInterval); + window.removeEventListener("message", onMessage); + } + }, 1000); } else { throw new Error("Popup blocked"); } diff --git a/packages/auth/src/utils/constants.ts b/packages/auth/src/utils/constants.ts index 25b9dd64f..846757b54 100644 --- a/packages/auth/src/utils/constants.ts +++ b/packages/auth/src/utils/constants.ts @@ -1 +1 @@ -export const ROCKETCHAT_APP_ID = '4c977b2e-eda2-4627-8bfe-2d0358304a79'; +export const ROCKETCHAT_APP_ID = "4c977b2e-eda2-4627-8bfe-2d0358304a79"; diff --git a/packages/auth/src/utils/getRCAppBaseURL.ts b/packages/auth/src/utils/getRCAppBaseURL.ts index 4850de224..6416e06a6 100644 --- a/packages/auth/src/utils/getRCAppBaseURL.ts +++ b/packages/auth/src/utils/getRCAppBaseURL.ts @@ -1,6 +1,6 @@ -import { ROCKETCHAT_APP_ID } from "./constants" +import { ROCKETCHAT_APP_ID } from "./constants"; export const getRCAppBaseURL = (host: string) => { - const url = new URL(`api/apps/public/${ROCKETCHAT_APP_ID}`, host); - return url.toString(); -} + const url = new URL(`api/apps/public/${ROCKETCHAT_APP_ID}`, host); + return url.toString(); +}; diff --git a/packages/auth/src/utils/getRCAppInfo.ts b/packages/auth/src/utils/getRCAppInfo.ts index ea4e0d617..191cac93d 100644 --- a/packages/auth/src/utils/getRCAppInfo.ts +++ b/packages/auth/src/utils/getRCAppInfo.ts @@ -1,12 +1,12 @@ import { getRCAppBaseURL } from "./getRCAppBaseURL"; export const getRCAppInfo = async (host: string) => { - const rcAppBaseUrl = getRCAppBaseURL(host); - const infoUrl = rcAppBaseUrl + '/info'; - const response = await fetch(infoUrl.toString()); - if (!response.ok) { - return null; - } - const info = await response.json(); - return info; -} + const rcAppBaseUrl = getRCAppBaseURL(host); + const infoUrl = rcAppBaseUrl + "/info"; + const response = await fetch(infoUrl.toString()); + if (!response.ok) { + return null; + } + const info = await response.json(); + return info; +}; diff --git a/packages/auth/src/utils/getRCAuthorizeURL.ts b/packages/auth/src/utils/getRCAuthorizeURL.ts index a8c0a71c2..6ddec5606 100644 --- a/packages/auth/src/utils/getRCAuthorizeURL.ts +++ b/packages/auth/src/utils/getRCAuthorizeURL.ts @@ -1,8 +1,12 @@ -export const getRCAuthorizeURL = (host: string, redirectUri: string, clientId: string) => { - const url = new URL(`oauth/authorize`, host); - url.searchParams.set('response_type', 'code'); - url.searchParams.set('client_id', clientId); - url.searchParams.set('redirect_uri', redirectUri); - url.searchParams.set('state', encodeURIComponent(window.location.origin)); - return url.toString(); -} +export const getRCAuthorizeURL = ( + host: string, + redirectUri: string, + clientId: string +) => { + const url = new URL(`oauth/authorize`, host); + url.searchParams.set("response_type", "code"); + url.searchParams.set("client_id", clientId); + url.searchParams.set("redirect_uri", redirectUri); + url.searchParams.set("state", encodeURIComponent(window.location.origin)); + return url.toString(); +}; diff --git a/packages/e2e-react/.eslintrc.cjs b/packages/e2e-react/.eslintrc.cjs new file mode 100644 index 000000000..d6c953795 --- /dev/null +++ b/packages/e2e-react/.eslintrc.cjs @@ -0,0 +1,18 @@ +module.exports = { + root: true, + env: { browser: true, es2020: true }, + extends: [ + 'eslint:recommended', + 'plugin:@typescript-eslint/recommended', + 'plugin:react-hooks/recommended', + ], + ignorePatterns: ['dist', '.eslintrc.cjs'], + parser: '@typescript-eslint/parser', + plugins: ['react-refresh'], + rules: { + 'react-refresh/only-export-components': [ + 'warn', + { allowConstantExport: true }, + ], + }, +} diff --git a/packages/e2e-react/.gitignore b/packages/e2e-react/.gitignore new file mode 100644 index 000000000..b88c8135a --- /dev/null +++ b/packages/e2e-react/.gitignore @@ -0,0 +1,28 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? +/test-results/ +/playwright-report/ +/blob-report/ +/playwright/.cache/ diff --git a/packages/e2e-react/README.md b/packages/e2e-react/README.md new file mode 100644 index 000000000..181d94ad9 --- /dev/null +++ b/packages/e2e-react/README.md @@ -0,0 +1 @@ +# E2E EmbeddedChat setup diff --git a/packages/e2e-react/index.html b/packages/e2e-react/index.html new file mode 100644 index 000000000..e4b78eae1 --- /dev/null +++ b/packages/e2e-react/index.html @@ -0,0 +1,13 @@ + + + + + + + Vite + React + TS + + +
+ + + diff --git a/packages/e2e-react/package.json b/packages/e2e-react/package.json new file mode 100644 index 000000000..40a454323 --- /dev/null +++ b/packages/e2e-react/package.json @@ -0,0 +1,34 @@ +{ + "name": "e2e-react", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc && vite build", + "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0", + "preview": "vite preview", + "test": "playwright test", + "format": "prettier --write 'src/' ", + "format:check": "prettier --check 'src/' " + }, + "dependencies": { + "@embeddedchat/react": "workspace:*", + "react": "^18.2.0", + "react-dom": "^18.2.0" + }, + "devDependencies": { + "@playwright/test": "^1.41.2", + "@types/node": "^20.11.19", + "@types/react": "^18.2.55", + "@types/react-dom": "^18.2.19", + "@typescript-eslint/eslint-plugin": "^6.21.0", + "@typescript-eslint/parser": "^6.21.0", + "@vitejs/plugin-react": "^4.2.1", + "eslint": "^8.56.0", + "eslint-plugin-react-hooks": "^4.6.0", + "eslint-plugin-react-refresh": "^0.4.5", + "typescript": "^5.2.2", + "vite": "^5.1.0" + } +} diff --git a/packages/e2e-react/playwright.config.ts b/packages/e2e-react/playwright.config.ts new file mode 100644 index 000000000..a91deee96 --- /dev/null +++ b/packages/e2e-react/playwright.config.ts @@ -0,0 +1,77 @@ +import { defineConfig, devices } from '@playwright/test'; + +/** + * Read environment variables from file. + * https://github.com/motdotla/dotenv + */ +// require('dotenv').config(); + +/** + * See https://playwright.dev/docs/test-configuration. + */ +export default defineConfig({ + testDir: './tests', + /* Run tests in files in parallel */ + fullyParallel: true, + /* Fail the build on CI if you accidentally left test.only in the source code. */ + forbidOnly: !!process.env.CI, + /* Retry on CI only */ + retries: process.env.CI ? 2 : 0, + /* Opt out of parallel tests on CI. */ + workers: process.env.CI ? 1 : undefined, + /* Reporter to use. See https://playwright.dev/docs/test-reporters */ + reporter: 'html', + /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ + use: { + /* Base URL to use in actions like `await page.goto('/')`. */ + baseURL: 'http://127.0.0.1:5173', + + /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ + trace: 'on-first-retry', + }, + + /* Configure projects for major browsers */ + projects: [ + { + name: 'chromium', + use: { ...devices['Desktop Chrome'] }, + }, + + // { + // name: 'firefox', + // use: { ...devices['Desktop Firefox'] }, + // }, + + // { + // name: 'webkit', + // use: { ...devices['Desktop Safari'] }, + // }, + + /* Test against mobile viewports. */ + // { + // name: 'Mobile Chrome', + // use: { ...devices['Pixel 5'] }, + // }, + // { + // name: 'Mobile Safari', + // use: { ...devices['iPhone 12'] }, + // }, + + /* Test against branded browsers. */ + // { + // name: 'Microsoft Edge', + // use: { ...devices['Desktop Edge'], channel: 'msedge' }, + // }, + // { + // name: 'Google Chrome', + // use: { ...devices['Desktop Chrome'], channel: 'chrome' }, + // }, + ], + + /* Run your local dev server before starting the tests */ + webServer: { + command: 'yarn dev', + url: 'http://127.0.0.1:5173', + reuseExistingServer: !process.env.CI, + }, +}); diff --git a/packages/e2e-react/public/vite.svg b/packages/e2e-react/public/vite.svg new file mode 100644 index 000000000..e7b8dfb1b --- /dev/null +++ b/packages/e2e-react/public/vite.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/e2e-react/src/App.tsx b/packages/e2e-react/src/App.tsx new file mode 100644 index 000000000..5be050dd0 --- /dev/null +++ b/packages/e2e-react/src/App.tsx @@ -0,0 +1,14 @@ +// @ts-expect-error no types served yet +import { EmbeddedChat } from "@embeddedchat/react"; + +function App() { + return ( + + ); +} + +export default App; diff --git a/packages/e2e-react/src/main.tsx b/packages/e2e-react/src/main.tsx new file mode 100644 index 000000000..95e2bdc2c --- /dev/null +++ b/packages/e2e-react/src/main.tsx @@ -0,0 +1,9 @@ +import React from "react"; +import ReactDOM from "react-dom/client"; +import App from "./App.tsx"; + +ReactDOM.createRoot(document.getElementById("root")!).render( + + + +); diff --git a/packages/e2e-react/src/vite-env.d.ts b/packages/e2e-react/src/vite-env.d.ts new file mode 100644 index 000000000..11f02fe2a --- /dev/null +++ b/packages/e2e-react/src/vite-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/packages/e2e-react/tests/example.spec.ts b/packages/e2e-react/tests/example.spec.ts new file mode 100644 index 000000000..4e9ba0589 --- /dev/null +++ b/packages/e2e-react/tests/example.spec.ts @@ -0,0 +1,18 @@ +import { test, expect } from '@playwright/test'; + +test('EmbeddedChat should render', async ({ page }) => { + await page.goto('/'); + await expect(page.locator('.ec-embedded-chat')).toBeVisible(); +}); + +test('EmbeddedChat has a title', async ({ page }) => { + await page.goto('/'); + await expect(page.locator('.ec-chat-header--channelDescription')).toHaveText('Login to chat'); +}); + +test('EmbeddedChat has messages', async ({ page }) => { + await page.goto('/'); + + await page.waitForSelector('.ec-message'); + expect(await page.locator('.ec-message').count()).toBeGreaterThan(0); +}); diff --git a/packages/e2e-react/tsconfig.json b/packages/e2e-react/tsconfig.json new file mode 100644 index 000000000..a7fc6fbf2 --- /dev/null +++ b/packages/e2e-react/tsconfig.json @@ -0,0 +1,25 @@ +{ + "compilerOptions": { + "target": "ES2020", + "useDefineForClassFields": true, + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "module": "ESNext", + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "react-jsx", + + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true + }, + "include": ["src"], + "references": [{ "path": "./tsconfig.node.json" }] +} diff --git a/packages/e2e-react/tsconfig.node.json b/packages/e2e-react/tsconfig.node.json new file mode 100644 index 000000000..97ede7ee6 --- /dev/null +++ b/packages/e2e-react/tsconfig.node.json @@ -0,0 +1,11 @@ +{ + "compilerOptions": { + "composite": true, + "skipLibCheck": true, + "module": "ESNext", + "moduleResolution": "bundler", + "allowSyntheticDefaultImports": true, + "strict": true + }, + "include": ["vite.config.ts"] +} diff --git a/packages/e2e-react/vite.config.ts b/packages/e2e-react/vite.config.ts new file mode 100644 index 000000000..028287cd5 --- /dev/null +++ b/packages/e2e-react/vite.config.ts @@ -0,0 +1,10 @@ +import { defineConfig } from 'vite' +import react from '@vitejs/plugin-react' + +// https://vitejs.dev/config/ +export default defineConfig({ + plugins: [react()], + define: { + 'process.env': {} + } +}) diff --git a/packages/htmlembed/package.json b/packages/htmlembed/package.json index ce5b68820..33a837024 100644 --- a/packages/htmlembed/package.json +++ b/packages/htmlembed/package.json @@ -8,7 +8,9 @@ "private": true, "scripts": { "build": "vite build && node postbuild.cjs", - "preview": "npm run build && vite preview --port=4001" + "preview": "npm run build && vite preview --port=4001", + "format": "prettier --write 'src/' ", + "format:check": "prettier --check 'src/' " }, "dependencies": { "@embeddedchat/react": "0.1.12", diff --git a/packages/htmlembed/src/EmbeddedChat.jsx b/packages/htmlembed/src/EmbeddedChat.jsx index a771bfd1b..71fae93f4 100644 --- a/packages/htmlembed/src/EmbeddedChat.jsx +++ b/packages/htmlembed/src/EmbeddedChat.jsx @@ -1,37 +1,39 @@ -import React from 'react' -import ReactDOM from 'react-dom/client' -import { EmbeddedChat as EmbeddedChatComponent } from '@embeddedchat/react'; +import React from "react"; +import ReactDOM from "react-dom/client"; +import { EmbeddedChat as EmbeddedChatComponent } from "@embeddedchat/react"; const EmbeddedChat = { - renderInElementWithId(config, id) { - if (!id) { - throw new Error("Please provide a valid id of the element to render embeddedchat"); - } - ReactDOM.createRoot(document.getElementById(id)).render( - - - - ) - }, - renderInElementWithSelector(config, selector) { - if(!selector) { - throw new Error("Please provide a valid selector to render embeddedchat"); - } - ReactDOM.createRoot(document.querySelector(selector)).render( - - - - ) - }, - renderInElement(config, element) { - if (!element) { - throw new Error("Please provide a valid element to render embeddedchat"); - } - ReactDOM.createRoot(element).render( - - - - ) - } -} + renderInElementWithId(config, id) { + if (!id) { + throw new Error( + "Please provide a valid id of the element to render embeddedchat" + ); + } + ReactDOM.createRoot(document.getElementById(id)).render( + + + + ); + }, + renderInElementWithSelector(config, selector) { + if (!selector) { + throw new Error("Please provide a valid selector to render embeddedchat"); + } + ReactDOM.createRoot(document.querySelector(selector)).render( + + + + ); + }, + renderInElement(config, element) { + if (!element) { + throw new Error("Please provide a valid element to render embeddedchat"); + } + ReactDOM.createRoot(element).render( + + + + ); + }, +}; export default EmbeddedChat; diff --git a/packages/rc-app/endpoints/CallbackEndpoint.ts b/packages/rc-app/endpoints/CallbackEndpoint.ts index c230a716e..1a5e6560e 100644 --- a/packages/rc-app/endpoints/CallbackEndpoint.ts +++ b/packages/rc-app/endpoints/CallbackEndpoint.ts @@ -55,30 +55,42 @@ export class CallbackEndpoint extends ApiEndpoint { const response = await http.post(tokenUrl, { content: formData.toString(), headers: { - 'Content-Type': 'application/x-www-form-urlencoded', + "Content-Type": "application/x-www-form-urlencoded", }, - }) + }); if (response.statusCode !== 200) { - return { - status: response.statusCode, - content: await getCallbackContent(read, null, origin, response.data.error_description || 'Unknown'), - headers: { - 'Content-Security-Policy': "default-src 'self'; script-src 'self' 'unsafe-inline'; connect-src 'self'; img-src 'self' data:; style-src 'self' 'unsafe-inline'; frame-src 'self'; font-src 'self'; object-src 'none'", - } - } + return { + status: response.statusCode, + content: await getCallbackContent( + read, + null, + origin, + response.data.error_description || "Unknown" + ), + headers: { + "Content-Security-Policy": + "default-src 'self'; script-src 'self' 'unsafe-inline'; connect-src 'self'; img-src 'self' data:; style-src 'self' 'unsafe-inline'; frame-src 'self'; font-src 'self'; object-src 'none'", + }, + }; } return { status: 200, - content: await getCallbackContent(read, { - accessToken: response.data?.access_token, - expiresIn: response.data?.expires_in, - serviceName: customOAuthName, - }, origin, false), + content: await getCallbackContent( + read, + { + accessToken: response.data?.access_token, + expiresIn: response.data?.expires_in, + serviceName: customOAuthName, + }, + origin, + false + ), headers: { - 'Content-Security-Policy': "default-src 'self'; script-src 'self' 'unsafe-inline'; connect-src 'self'; img-src 'self' data:; style-src 'self' 'unsafe-inline'; frame-src 'self'; font-src 'self'; object-src 'none'", - } + "Content-Security-Policy": + "default-src 'self'; script-src 'self' 'unsafe-inline'; connect-src 'self'; img-src 'self' data:; style-src 'self' 'unsafe-inline'; frame-src 'self'; font-src 'self'; object-src 'none'", + }, }; } } diff --git a/packages/rc-app/endpoints/InfoEndpoint.ts b/packages/rc-app/endpoints/InfoEndpoint.ts index 8548bc8a1..ab0a69dd9 100644 --- a/packages/rc-app/endpoints/InfoEndpoint.ts +++ b/packages/rc-app/endpoints/InfoEndpoint.ts @@ -52,8 +52,8 @@ export class InfoEndpoint extends ApiEndpoint { client_id: !!client_id, client_secret: !!client_secret, custom_oauth_name: !!serviceName, - } - } - } + }, + }, + }; } } diff --git a/packages/rc-app/lib/getAllowedOrigins.ts b/packages/rc-app/lib/getAllowedOrigins.ts index ea30e038c..aa94c4d7f 100644 --- a/packages/rc-app/lib/getAllowedOrigins.ts +++ b/packages/rc-app/lib/getAllowedOrigins.ts @@ -1,8 +1,13 @@ export const getAllowedOrigins = async (read) => { - const allowedOrigins = await read.getEnvironmentReader().getSettings().getValueById("allowed-origins"); + const allowedOrigins = await read + .getEnvironmentReader() + .getSettings() + .getValueById("allowed-origins"); if (!allowedOrigins) { return []; } - const origins = allowedOrigins.split(',').filter((domain) => !!domain.trim()); + const origins = allowedOrigins + .split(",") + .filter((domain) => !!domain.trim()); return origins; -} +}; diff --git a/packages/rc-app/lib/getCallbackContent.ts b/packages/rc-app/lib/getCallbackContent.ts index c8c1a2898..165aa1c1b 100644 --- a/packages/rc-app/lib/getCallbackContent.ts +++ b/packages/rc-app/lib/getCallbackContent.ts @@ -5,22 +5,27 @@ interface ICredentials { accessToken: string; expiresIn: number; serviceName: string; -}; +} -export const getCallbackContent = async (read: IRead, credentials: ICredentials | null, origin: string, error) => { +export const getCallbackContent = async ( + read: IRead, + credentials: ICredentials | null, + origin: string, + error +) => { const { accessToken, expiresIn = 3600, serviceName } = credentials || {}; - const isAllowed = await isAllowedOrigin(read, origin);; + const isAllowed = await isAllowedOrigin(read, origin); let config: any = {}; if (error) { config = { success: false, error, - } + }; } else if (!isAllowed) { config = { success: false, - error: 'origin not allowed', - } + error: "origin not allowed", + }; } else { config = { success: true, @@ -29,8 +34,8 @@ export const getCallbackContent = async (read: IRead, credentials: ICredentials accessToken, expiresIn, serviceName, - } - } + }, + }; } const closeLinkHtml = `

${ config.success ? "Login Successful" : "Login Failed: " + config.error @@ -66,4 +71,4 @@ export const getCallbackContent = async (read: IRead, credentials: ICredentials `; -} +}; diff --git a/packages/rc-app/lib/getCallbackUrl.ts b/packages/rc-app/lib/getCallbackUrl.ts index 2d129497b..3174e7bbe 100644 --- a/packages/rc-app/lib/getCallbackUrl.ts +++ b/packages/rc-app/lib/getCallbackUrl.ts @@ -8,11 +8,12 @@ export const getCallbackUrl = async (app: IApp) => { .getValueById("Site_Url"); const callbackEndPoint = app .getAccessors() - .providedApiEndpoints.find( - (endpoint) => endpoint.path === "callback" - ); + .providedApiEndpoints.find((endpoint) => endpoint.path === "callback"); if (callbackEndPoint) { - const webhookURL = new URL(callbackEndPoint.computedPath || "", serverURL) + const webhookURL = new URL( + callbackEndPoint.computedPath || "", + serverURL + ); return webhookURL.toString(); } return ""; diff --git a/packages/rc-app/lib/getTokenUrl.ts b/packages/rc-app/lib/getTokenUrl.ts index b0a71a84d..c7128c5ac 100644 --- a/packages/rc-app/lib/getTokenUrl.ts +++ b/packages/rc-app/lib/getTokenUrl.ts @@ -2,7 +2,10 @@ import { IRead } from "@rocket.chat/apps-engine/definition/accessors"; import { URL } from "url"; export const getTokenUrl = async (read: IRead) => { - const serverURL = await read.getEnvironmentReader().getServerSettings().getValueById("Site_Url"); - const url = new URL('/oauth/token', serverURL); + const serverURL = await read + .getEnvironmentReader() + .getServerSettings() + .getValueById("Site_Url"); + const url = new URL("/oauth/token", serverURL); return url.toString(); -} +}; diff --git a/packages/rc-app/lib/isAllowedOrigin.ts b/packages/rc-app/lib/isAllowedOrigin.ts index ff6a26d58..1bf7e3019 100644 --- a/packages/rc-app/lib/isAllowedOrigin.ts +++ b/packages/rc-app/lib/isAllowedOrigin.ts @@ -8,4 +8,4 @@ export const isAllowedOrigin = async (read: IRead, origin: string) => { return true; } return allowedOrigins.includes(origin); -} +}; diff --git a/packages/rc-app/package.json b/packages/rc-app/package.json index a31494bc7..a55446ec3 100644 --- a/packages/rc-app/package.json +++ b/packages/rc-app/package.json @@ -7,5 +7,9 @@ "@types/node": "14.14.6", "tslint": "^5.10.0", "typescript": "^4.0.5" + }, + "scripts": { + "format": "prettier --write '*/**.ts'", + "format:check": "prettier --check '*/**.ts'" } } diff --git a/packages/rc-app/settings/settings.ts b/packages/rc-app/settings/settings.ts index fb925485b..268bb91b9 100644 --- a/packages/rc-app/settings/settings.ts +++ b/packages/rc-app/settings/settings.ts @@ -1,41 +1,46 @@ -import { ISetting, SettingType } from '@rocket.chat/apps-engine/definition/settings'; +import { + ISetting, + SettingType, +} from "@rocket.chat/apps-engine/definition/settings"; -export const settings:ISetting[] = [ - { - id: 'client-id', - i18nLabel: 'Client ID', - i18nDescription: 'The client id of the third party login app', - type: SettingType.STRING, - required: true, - public: false, - packageValue: '', - }, - { - id: 'client-secret', - i18nLabel: 'Client Secret', - i18nDescription: 'The client secret of the third party login app', - type: SettingType.PASSWORD, - required: true, - public: false, - packageValue: '', - }, +export const settings: ISetting[] = [ { - id: 'allowed-origins', - i18nLabel: 'Allowed origins', - i18nDescription: 'The allowed origins for third party login for EmbeddedChat (Comma separated). Leave blank to allow all origins.', - type: SettingType.STRING, - required: false, - public: false, - packageValue: '', + id: "client-id", + i18nLabel: "Client ID", + i18nDescription: "The client id of the third party login app", + type: SettingType.STRING, + required: true, + public: false, + packageValue: "", + }, + { + id: "client-secret", + i18nLabel: "Client Secret", + i18nDescription: "The client secret of the third party login app", + type: SettingType.PASSWORD, + required: true, + public: false, + packageValue: "", + }, + { + id: "allowed-origins", + i18nLabel: "Allowed origins", + i18nDescription: + "The allowed origins for third party login for EmbeddedChat (Comma separated). Leave blank to allow all origins.", + type: SettingType.STRING, + required: false, + public: false, + packageValue: "", multiline: true, - }, + }, { - id: 'custom-oauth-name', - i18nLabel: 'Custom OAuth Name', - i18nDescription: 'Provide name with which the custom oauth is saved. The custom oauth should be configured with the above third party login app.', - type: SettingType.STRING, - required: false, - public: false, - packageValue: '', - } -] + id: "custom-oauth-name", + i18nLabel: "Custom OAuth Name", + i18nDescription: + "Provide name with which the custom oauth is saved. The custom oauth should be configured with the above third party login app.", + type: SettingType.STRING, + required: false, + public: false, + packageValue: "", + }, +]; diff --git a/packages/react/.eslintignore b/packages/react/.eslintignore index 2f5d60c64..3d9e4d531 100644 --- a/packages/react/.eslintignore +++ b/packages/react/.eslintignore @@ -1 +1,2 @@ -rollup.config.js \ No newline at end of file +rollup.config.js +*.test.js diff --git a/packages/react/.gitignore b/packages/react/.gitignore index efccce6e7..ac494021e 100644 --- a/packages/react/.gitignore +++ b/packages/react/.gitignore @@ -105,3 +105,5 @@ dist # pnpm-lock pnpm-lock.yaml + +storybook-static/ diff --git a/packages/react/package.json b/packages/react/package.json index 971b1649d..5e9df2fe9 100644 --- a/packages/react/package.json +++ b/packages/react/package.json @@ -21,7 +21,9 @@ "test:unit": "cross-env CI=1 react-scripts test --env=jsdom", "test:watch": "react-scripts test --env=jsdom --coverage --collectCoverageFrom=src/components/**/*.js", "test:build": "run-s build", - "format": "prettier --write 'src/' --loglevel=silent", + "format": "prettier --write 'src/' ", + "format:check": "prettier --check 'src/' ", + "lint": "eslint 'src/**/*.js'", "lint:fix": "eslint 'src/**/*.js' --fix", "storybook": "storybook dev -p 6006", "build-storybook": "storybook build", diff --git a/packages/react/src/components/AllThreads/AllThreads.js b/packages/react/src/components/AllThreads/AllThreads.js index 24a56dc02..389b5034b 100644 --- a/packages/react/src/components/AllThreads/AllThreads.js +++ b/packages/react/src/components/AllThreads/AllThreads.js @@ -4,14 +4,17 @@ import classes from './AllThreads.module.css'; import { Icon } from '../Icon'; import { Box } from '../Box'; import { ActionButton } from '../ActionButton'; -import { useMessageStore, useUserStore, useThreadsMessageStore } from '../../store'; +import { + useMessageStore, + useUserStore, + useThreadsMessageStore, +} from '../../store'; import { MessageBody } from '../Message/MessageBody'; import { MessageMetrics } from '../Message/MessageMetrics'; import MessageAvatarContainer from '../Message/MessageAvatarContainer'; import MessageBodyContainer from '../Message/MessageBodyContainer'; import MessageHeader from '../Message/MessageHeader'; - const MessageCss = css` display: flex; flex-direction: row; @@ -32,118 +35,146 @@ const MessageCss = css` `; const AllThreads = () => { - const showAvatar = useUserStore((state) => state.showAvatar); - const messages = useMessageStore((state) => state.messages); - const setShowAllThreads = useThreadsMessageStore((state) => state.setShowAllThreads); - const openThread = useMessageStore((state) => state.openThread); - const [text, setText] = useState(''); + const showAvatar = useUserStore((state) => state.showAvatar); + const messages = useMessageStore((state) => state.messages); + const setShowAllThreads = useThreadsMessageStore( + (state) => state.setShowAllThreads + ); + const openThread = useMessageStore((state) => state.openThread); + const [text, setText] = useState(''); - const toggleShowAllThreads = () => { - setShowAllThreads(false); - }; + const toggleShowAllThreads = () => { + setShowAllThreads(false); + }; - const handleOpenThread = (msg) => () => { - openThread(msg); - toggleShowAllThreads(false); - }; + const handleOpenThread = (msg) => () => { + openThread(msg); + toggleShowAllThreads(false); + }; - const handleInputChange = (e) => { - setText(e.target.value); - }; + const handleInputChange = (e) => { + setText(e.target.value); + }; - const filteredThreads = useMemo(() => { - return messages.filter((message) => - message.msg.toLowerCase().includes(text.toLowerCase()) - ); - }, [messages, text]); + const filteredThreads = useMemo( + () => + messages.filter((message) => + message.msg.toLowerCase().includes(text.toLowerCase()) + ), + [messages, text] + ); - return ( - - + return ( + + + + +

+ + + Threads + + + + +

+ - - -

- - - Threads - - - - -

-
+ + - - + + + - -
- + + {filteredThreads.length === 0 ? ( + + + + No threads found + + + ) : ( + filteredThreads.map( + (message) => + !message.t && + message.tcount && ( + + {showAvatar && ( + + )} + + - - {filteredThreads.length === 0 ? ( - - - No threads found - - ) : (filteredThreads - .map((message) => ( - !message.t && message.tcount && ( - - {showAvatar && ( - - )} - - {} - - {message.attachments && message.attachments.length > 0 ? ( - message.file.name - ) : ( - message.msg - )} - + + {message.attachments && message.attachments.length > 0 + ? message.file.name + : message.msg} + - - - - ) - )))} - - + + + + ) + ) + )} - ); + + + ); }; export default AllThreads; diff --git a/packages/react/src/components/Attachments/AttachmentWindow.js b/packages/react/src/components/Attachments/AttachmentWindow.js index 9536ace88..3fce7ddfd 100644 --- a/packages/react/src/components/Attachments/AttachmentWindow.js +++ b/packages/react/src/components/Attachments/AttachmentWindow.js @@ -1,4 +1,5 @@ import React, { useContext, useState } from 'react'; +import { css } from '@emotion/react'; import useAttachmentWindowStore from '../../store/attachmentwindow'; import ValidateComponent from './AttachmentWindow/validateComponent'; import Backdrop from './AttachmentWindow/Backdrop'; @@ -7,7 +8,6 @@ import styles from './AttachmentWindow.module.css'; import { useMessageStore } from '../../store'; import { Box } from '../Box'; import { Icon } from '../Icon'; -import { css } from '@emotion/react'; function AttachmentWindow() { const { RCInstance, ECOptions } = useContext(RCContext); @@ -61,29 +61,30 @@ function AttachmentWindow() { - + diff --git a/packages/react/src/components/Attachments/AttachmentWindow.module.css b/packages/react/src/components/Attachments/AttachmentWindow.module.css index 8a5a6b1c3..16a27d2a3 100644 --- a/packages/react/src/components/Attachments/AttachmentWindow.module.css +++ b/packages/react/src/components/Attachments/AttachmentWindow.module.css @@ -54,7 +54,7 @@ } .attachment_window_close:active { - background-color: #8f9194; + background-color: #8f9194; } .attachment_window_input_container { @@ -109,8 +109,8 @@ outline: none; border: none; border-radius: 4px; - cursor: pointer; - background: #e4e7ea; + cursor: pointer; + background: #e4e7ea; margin-right: 5px; } diff --git a/packages/react/src/components/Attachments/AttachmentWindow/Backdrop.js b/packages/react/src/components/Attachments/AttachmentWindow/Backdrop.js index 038673b22..7cb4e2fc4 100644 --- a/packages/react/src/components/Attachments/AttachmentWindow/Backdrop.js +++ b/packages/react/src/components/Attachments/AttachmentWindow/Backdrop.js @@ -2,22 +2,21 @@ import React from 'react'; import { Box } from '../../Box'; const Backdrop = ({ children, onClick }) => ( - - {children} - + + {children} + ); export default Backdrop; diff --git a/packages/react/src/components/Attachments/AttachmentWindow/preview/image.js b/packages/react/src/components/Attachments/AttachmentWindow/preview/image.js index a14f45793..4905104a6 100644 --- a/packages/react/src/components/Attachments/AttachmentWindow/preview/image.js +++ b/packages/react/src/components/Attachments/AttachmentWindow/preview/image.js @@ -5,10 +5,7 @@ import { Box } from '../../../Box'; function PreviewImage({ previewURL }) { return ( - + ); } diff --git a/packages/react/src/components/Attachments/AttachmentWindow/validateComponent.js b/packages/react/src/components/Attachments/AttachmentWindow/validateComponent.js index 74083e4e7..67f77847b 100644 --- a/packages/react/src/components/Attachments/AttachmentWindow/validateComponent.js +++ b/packages/react/src/components/Attachments/AttachmentWindow/validateComponent.js @@ -11,12 +11,11 @@ const ValidateComponent = ({ data }) => { const [previewURL, setPreviewURL] = useState(''); const dispatchToastMessage = useToastBarDispatch(); - useEffect(() => { if (!data) { dispatchToastMessage({ type: 'error', - message: 'Media Type Not Accepted' + message: 'Media Type Not Accepted', }); } }, [data, dispatchToastMessage]); diff --git a/packages/react/src/components/Attachments/PinnedAttachment.js b/packages/react/src/components/Attachments/PinnedAttachment.js index 4e0588096..ae694b2a6 100644 --- a/packages/react/src/components/Attachments/PinnedAttachment.js +++ b/packages/react/src/components/Attachments/PinnedAttachment.js @@ -6,7 +6,7 @@ const PinnedAttachment = ({ attachment }) => ( {attachment?.author_name} diff --git a/packages/react/src/components/ChatBody/ChatBody.js b/packages/react/src/components/ChatBody/ChatBody.js index 002f8890a..a13a472f2 100644 --- a/packages/react/src/components/ChatBody/ChatBody.js +++ b/packages/react/src/components/ChatBody/ChatBody.js @@ -13,8 +13,15 @@ import ThreadMessageList from '../Thread/ThreadMessageList'; import ModalBlock from '../uiKit/blocks/ModalBlock'; import useComponentOverrides from '../../theme/useComponentOverrides'; import RecentMessageButton from './RecentMessageButton'; - -const ChatBody = ({ height, anonymousMode, showRoles, scrollToBottom, messageListRef }) => { +import useFetchChatData from '../../hooks/useFetchChatData'; + +const ChatBody = ({ + height, + anonymousMode, + showRoles, + scrollToBottom, + messageListRef, +}) => { const { classNames, styleOverrides } = useComponentOverrides('ChatBody'); const ChatBodyCss = css` word-break: break-all; @@ -42,16 +49,18 @@ const ChatBody = ({ height, anonymousMode, showRoles, scrollToBottom, messageLis } `; + const [scrollPosition, setScrollPosition] = useState(0); + const [popupVisible, setPopupVisible] = useState(false); + const [, setIsUserScrolledUp] = useState(false); + const [otherUserMessage, setOtherUserMessage] = useState(false); + const { RCInstance, ECOptions } = useContext(RCContext); const messages = useMessageStore((state) => state.messages); const threadMessages = useMessageStore((state) => state.threadMessages); - const setMessages = useMessageStore((state) => state.setMessages); const setThreadMessages = useMessageStore((state) => state.setThreadMessages); const upsertMessage = useMessageStore((state) => state.upsertMessage); const removeMessage = useMessageStore((state) => state.removeMessage); - const setFilter = useMessageStore((state) => state.setFilter); - const setRoles = useUserStore((state) => state.setRoles); const isChannelPrivate = useChannelStore((state) => state.isChannelPrivate); const [isThreadOpen, threadMainMessage] = useMessageStore((state) => [ @@ -65,66 +74,9 @@ const ChatBody = ({ height, anonymousMode, showRoles, scrollToBottom, messageLis (state) => state.isUserAuthenticated ); - const username = useUserStore( - (state) => state.username - ); + const username = useUserStore((state) => state.username); - const getMessagesAndRoles = useCallback( - async (anonymousMode) => { - try { - if (!isUserAuthenticated && !anonymousMode) { - return; - } - const { messages } = await RCInstance.getMessages( - anonymousMode, - ECOptions?.enableThreads - ? { - query: { - tmid: { - $exists: false, - }, - }, - } - : undefined, anonymousMode ? false : isChannelPrivate - ); - if (messages) { - setMessages(messages.filter((message) => message._hidden !== true)); - } - if (!isUserAuthenticated) { - // fetch roles only when user is authenticated - return; - } - if (showRoles) { - const { roles } = await RCInstance.getChannelRoles(isChannelPrivate); - // convert roles array from api into object for better search - const rolesObj = roles?.length > 0 - ? roles.reduce((obj, item) => ({ ...obj, [item.u.username]: item }), {}) - : {}; - setRoles(rolesObj); - } - } catch (e) { - console.error(e); - } - }, - [ - isUserAuthenticated, - RCInstance, - ECOptions?.enableThreads, - showRoles, - setMessages, - setRoles, - isChannelPrivate - ] - ); - - const handleGoBack = async () => { - if (isUserAuthenticated) { - getMessagesAndRoles(); - } else { - getMessagesAndRoles(anonymousMode); - } - setFilter(false); - }; + const getMessagesAndRoles = useFetchChatData(showRoles); const getThreadMessages = useCallback(async () => { if (isUserAuthenticated && threadMainMessage?._id) { @@ -147,7 +99,7 @@ const ChatBody = ({ height, anonymousMode, showRoles, scrollToBottom, messageLis RCInstance, threadMainMessage?._id, setThreadMessages, - isChannelPrivate + isChannelPrivate, ]); useEffect(() => { @@ -226,12 +178,6 @@ const ChatBody = ({ height, anonymousMode, showRoles, scrollToBottom, messageLis anonymousMode, ]); - - const [scrollPosition, setScrollPosition] = useState(0); - const [popupVisible, setPopupVisible] = useState(false); - const [isUserScrolledUp, setIsUserScrolledUp] = useState(false); - const [otherUserMessage, setOtherUserMessage] = useState(false); - const handlePopupClick = () => { scrollToBottom(); setIsUserScrolledUp(false); @@ -239,13 +185,12 @@ const ChatBody = ({ height, anonymousMode, showRoles, scrollToBottom, messageLis setPopupVisible(false); }; - const handleScroll = () => { setScrollPosition(messageListRef.current.scrollTop); setIsUserScrolledUp( messageListRef.current.scrollTop + messageListRef.current.clientHeight < - messageListRef.current.scrollHeight + messageListRef.current.scrollHeight ); const isAtBottom = messageListRef.current.scrollTop === 0; @@ -260,7 +205,6 @@ const ChatBody = ({ height, anonymousMode, showRoles, scrollToBottom, messageLis setPopupVisible(true); }; - useEffect(() => { messageListRef.current.addEventListener('scroll', handleScroll); @@ -269,7 +213,6 @@ const ChatBody = ({ height, anonymousMode, showRoles, scrollToBottom, messageLis }; }, [messageListRef]); - useEffect(() => { const isScrolledUp = scrollPosition + messageListRef.current.clientHeight < @@ -300,7 +243,7 @@ const ChatBody = ({ height, anonymousMode, showRoles, scrollToBottom, messageLis threadMessages={threadMessages} /> ) : ( - + )} @@ -314,9 +257,9 @@ const ChatBody = ({ height, anonymousMode, showRoles, scrollToBottom, messageLis /> )} - {(popupVisible && otherUserMessage) && ( + {popupVisible && otherUserMessage && ( diff --git a/packages/react/src/components/ChatBody/RecentMessageButton.js b/packages/react/src/components/ChatBody/RecentMessageButton.js index b02c46ac6..c843affd5 100644 --- a/packages/react/src/components/ChatBody/RecentMessageButton.js +++ b/packages/react/src/components/ChatBody/RecentMessageButton.js @@ -1,8 +1,7 @@ import React, { useState } from 'react'; +import { css } from '@emotion/react'; import { Button } from '../Button'; import { Icon } from '../Icon'; -import { css } from '@emotion/react'; - const buttonStyle = css` position: relative; @@ -55,11 +54,7 @@ const RecentMessageButton = ({ visible, onClick, text }) => { return (