diff --git a/.github/workflows/backend.yml b/.github/workflows/backend.yml index ca43515e8f..e3e556f942 100644 --- a/.github/workflows/backend.yml +++ b/.github/workflows/backend.yml @@ -12,6 +12,8 @@ jobs: runs-on: ubuntu-20.04 steps: - uses: actions/checkout@v2 + - name: build-test + run: cd twake && docker-compose -f docker-compose.tests.yml run -e NODE_OPTIONS=--unhandled-rejections=warn node npm run build - name: unit-test run: cd twake && docker-compose -f docker-compose.tests.yml run -e NODE_OPTIONS=--unhandled-rejections=warn node npm run test:unit - name: e2e-mongo-test diff --git a/.github/workflows/saas-update-backend.yml b/.github/workflows/saas-update-backend.yml index f943b10469..307824e60d 100644 --- a/.github/workflows/saas-update-backend.yml +++ b/.github/workflows/saas-update-backend.yml @@ -13,13 +13,13 @@ jobs: deploy-php: runs-on: ubuntu-20.04 steps: - - run: 'echo "DOCKERTAGVERSION=2021.Q4.846" >> $GITHUB_ENV' + - run: 'echo "DOCKERTAGVERSION=2021.Q4.860" >> $GITHUB_ENV' - name: Set env to develop if: endsWith(github.ref, '/develop') - run: 'echo "DOCKERTAG=develop" >> $GITHUB_ENV; echo "DOCKERTAGVERSION=2021.Q4.846" >> $GITHUB_ENV' + run: 'echo "DOCKERTAG=develop" >> $GITHUB_ENV; echo "DOCKERTAGVERSION=2021.Q4.860" >> $GITHUB_ENV' - name: Set env to develop if: endsWith(github.ref, '/qa') - run: 'echo "DOCKERTAG=qa" >> $GITHUB_ENV; echo "DOCKERTAGVERSION=2021.Q4.846" >> $GITHUB_ENV' + run: 'echo "DOCKERTAG=qa" >> $GITHUB_ENV; echo "DOCKERTAGVERSION=2021.Q4.860" >> $GITHUB_ENV' - name: Set env to develop if: endsWith(github.ref, '/canary') run: 'echo "DOCKERTAG=canary" >> $GITHUB_ENV; echo "DOCKERTAGVERSION=${{ env.DOCKERTAGVERSION }}-canary" >> $GITHUB_ENV' @@ -40,13 +40,13 @@ jobs: deploy-node: runs-on: ubuntu-20.04 steps: - - run: 'echo "DOCKERTAGVERSION=2021.Q4.846" >> $GITHUB_ENV' + - run: 'echo "DOCKERTAGVERSION=2021.Q4.860" >> $GITHUB_ENV' - name: Set env to develop if: endsWith(github.ref, '/develop') - run: 'echo "DOCKERTAG=develop" >> $GITHUB_ENV; echo "DOCKERTAGVERSION=2021.Q4.846" >> $GITHUB_ENV' + run: 'echo "DOCKERTAG=develop" >> $GITHUB_ENV; echo "DOCKERTAGVERSION=2021.Q4.860" >> $GITHUB_ENV' - name: Set env to develop if: endsWith(github.ref, '/qa') - run: 'echo "DOCKERTAG=qa" >> $GITHUB_ENV; echo "DOCKERTAGVERSION=2021.Q4.846" >> $GITHUB_ENV' + run: 'echo "DOCKERTAG=qa" >> $GITHUB_ENV; echo "DOCKERTAGVERSION=2021.Q4.860" >> $GITHUB_ENV' - name: Set env to develop if: endsWith(github.ref, '/canary') run: 'echo "DOCKERTAG=canary" >> $GITHUB_ENV; echo "DOCKERTAGVERSION=${{ env.DOCKERTAGVERSION }}-canary" >> $GITHUB_ENV' diff --git a/.github/workflows/saas-update-front.yml b/.github/workflows/saas-update-front.yml index bd287bb520..88649b43b3 100644 --- a/.github/workflows/saas-update-front.yml +++ b/.github/workflows/saas-update-front.yml @@ -48,13 +48,13 @@ jobs: runs-on: ubuntu-20.04 if: github.ref == 'refs/heads/main' || github.ref == 'refs/heads/develop' || github.ref == 'refs/heads/qa' || github.ref == 'refs/heads/canary' steps: - - run: 'echo "DOCKERTAGVERSION=2021.Q4.846" >> $GITHUB_ENV' + - run: 'echo "DOCKERTAGVERSION=2021.Q4.860" >> $GITHUB_ENV' - name: Set env to develop if: endsWith(github.ref, '/develop') - run: 'echo "DOCKERTAG=develop" >> $GITHUB_ENV; echo "DOCKERTAGVERSION=2021.Q4.846" >> $GITHUB_ENV' + run: 'echo "DOCKERTAG=develop" >> $GITHUB_ENV; echo "DOCKERTAGVERSION=2021.Q4.860" >> $GITHUB_ENV' - name: Set env to develop if: endsWith(github.ref, '/qa') - run: 'echo "DOCKERTAG=qa" >> $GITHUB_ENV; echo "DOCKERTAGVERSION=2021.Q4.846" >> $GITHUB_ENV' + run: 'echo "DOCKERTAG=qa" >> $GITHUB_ENV; echo "DOCKERTAGVERSION=2021.Q4.860" >> $GITHUB_ENV' - name: Set env to develop if: endsWith(github.ref, '/canary') run: 'echo "DOCKERTAG=canary" >> $GITHUB_ENV; echo "DOCKERTAGVERSION=${{ env.DOCKERTAGVERSION }}-canary" >> $GITHUB_ENV' diff --git a/README.md b/README.md index bcb21e9097..6d3d5223d7 100644 --- a/README.md +++ b/README.md @@ -34,6 +34,7 @@ Or run your own local Twake instance with : cd twake sudo ./start.sh ``` + Twake will be running on http//localhost and by default redirect to https and uses a self-signed certificate. If you want to run http only then set SSL_CERTS=none at docker-compose.yml ## Documentation @@ -61,9 +62,9 @@ Everyone can contribute at their own level, even if they only give a few minutes Install Twake on your machine with docker-compose using the install documentation here : [doc.twake.app/installation](https://doc.twake.app/installation) -### Migration to 2021.Q1.385 +`cd twake; docker-compose -f docker-compose.onpremise.mongo.yml up -d` -If you migrate to the 2021.Q1 version for a 2020.Q4 version or earlier, please follow the documentation at https://github.com/Twake/Twake/tree/main/migration/2020Q3_to_2020Q4 +Twake will be available on port 3000. ## License diff --git a/changelog.md b/changelog.md index 29e46a333f..b25fa6726a 100644 --- a/changelog.md +++ b/changelog.md @@ -1,3 +1,26 @@ +# Twake 2021.Q4.860 + +### Messages + +- Improved message top right menu +- Fixed message sending / edition / reaction / pinned etc, now instant ⚡️ +- User availability indicator 🟢 +- User is writing indicator + +### Workspace parameters + +- Improved integrations management page 🧹 +- Improved workspace users management page 🧹 +- Improved workspace preferences management page 🧹 + +### General + +- A lot of bug fixed, more to come on the 22 of January 🐞 +- Improved Twake loading time (still in progress) ⏰ +- New state management ( recoiljs.org ) on everything except channels and channels members +- Everything except Drive Tasks and Calendar was migrated to node backend 🧹 +- Twake is now working with a simplified docker-compose with only node + mongodb. Note that this works only with messages (no Drive, Calendar or Tasks yet) 🚀 + # Twake 2021.Q3.640 ### Messages diff --git a/twake/backend/core/src/Twake/Core/Controller/Version.php b/twake/backend/core/src/Twake/Core/Controller/Version.php index c544366535..c49ba8a603 100755 --- a/twake/backend/core/src/Twake/Core/Controller/Version.php +++ b/twake/backend/core/src/Twake/Core/Controller/Version.php @@ -49,10 +49,10 @@ function getVersion(Request $request) "auth_mode" => array_keys($auth), "auth" => $auth, "version" => [ - "current" => /* @VERSION_DETAIL */ "2021.Q4.846", + "current" => /* @VERSION_DETAIL */ "2021.Q4.860", "minimal" => [ - "web" => /* @MIN_VERSION_WEB */ "2021.Q4.846", - "mobile" => /* @MIN_VERSION_MOBILE */ "2021.Q4.846", + "web" => /* @MIN_VERSION_WEB */ "2021.Q4.860", + "mobile" => /* @MIN_VERSION_MOBILE */ "2021.Q4.860", ] ], "elastic_search_available" => !!$this->container->getParameter("es.host"), diff --git a/twake/backend/node/src/services/console/clients/remote.ts b/twake/backend/node/src/services/console/clients/remote.ts index bab92a765d..1730b66a1f 100644 --- a/twake/backend/node/src/services/console/clients/remote.ts +++ b/twake/backend/node/src/services/console/clients/remote.ts @@ -249,7 +249,7 @@ export class ConsoleRemoteClient implements ConsoleServiceClient { } user = getInstance({}); - user.username_canonical = username; + user.username_canonical = (username || "").toLocaleLowerCase(); user.email_canonical = userDTO.email; user.deleted = false; } diff --git a/twake/backend/node/src/services/console/web/controller.ts b/twake/backend/node/src/services/console/web/controller.ts index d6c8dc0b78..bcb3122735 100644 --- a/twake/backend/node/src/services/console/web/controller.ts +++ b/twake/backend/node/src/services/console/web/controller.ts @@ -72,7 +72,7 @@ export class ConsoleController { first_name: request.body.first_name, last_name: request.body.last_name, email_canonical: email, - username_canonical: email.replace("@", "."), + username_canonical: (email.replace("@", ".") || "").toLocaleLowerCase(), }); const user = await this.userService.users.create(newUser); await this.userService.users.setPassword({ id: user.entity.id }, request.body.password); diff --git a/twake/backend/node/src/services/messages/services/engine/processors/message-to-notifications/index.ts b/twake/backend/node/src/services/messages/services/engine/processors/message-to-notifications/index.ts index 0d5adc3d72..13f6795fae 100644 --- a/twake/backend/node/src/services/messages/services/engine/processors/message-to-notifications/index.ts +++ b/twake/backend/node/src/services/messages/services/engine/processors/message-to-notifications/index.ts @@ -83,7 +83,9 @@ export class MessageToNotificationsProcessor { text = `${senderName}: ${text}`; } - const mentions = getMentions(messageResource); + const mentions = await getMentions(messageResource, async (username: string) => { + return await this.user.users.getByUsername(username); + }); const messageEvent: MessageNotification = { company_id: participant.company_id, diff --git a/twake/backend/node/src/services/messages/services/messages/service.ts b/twake/backend/node/src/services/messages/services/messages/service.ts index a71076edab..ae40b96e37 100644 --- a/twake/backend/node/src/services/messages/services/messages/service.ts +++ b/twake/backend/node/src/services/messages/services/messages/service.ts @@ -449,7 +449,9 @@ export class ThreadMessagesService implements MessageThreadMessagesServiceAPI { let ids: string[] = []; if (message.user_id) ids.push(message.user_id); if (message.pinned_info?.pinned_by) ids.push(message.pinned_info?.pinned_by); - const mentions = getMentions(message); + const mentions = await getMentions(message, async (username: string) => { + return await this.user.users.getByUsername(username); + }); for (const mentionedUser of mentions.users) { ids.push(mentionedUser); } diff --git a/twake/backend/node/src/services/messages/services/utils.ts b/twake/backend/node/src/services/messages/services/utils.ts index a3620bdfb0..99c001025f 100644 --- a/twake/backend/node/src/services/messages/services/utils.ts +++ b/twake/backend/node/src/services/messages/services/utils.ts @@ -2,6 +2,7 @@ import { FindOptions } from "../../../core/platform/services/database/services/o import { Pagination } from "../../../core/platform/framework/api/crud-service"; import { Message } from "../entities/messages"; import { specialMention } from "../types"; +import User from "../../../services/user/entities/user"; export const buildMessageListPagination = ( pagination: Pagination, @@ -16,14 +17,34 @@ export const buildMessageListPagination = ( }; }; -export const getMentions = (messageResource: Message) => { +export const getMentions = async ( + messageResource: Message, + findByUsername: (username: string) => Promise, +) => { + let idsFromUsernames = []; + try { + const usersNoIdOutput = (messageResource.text || "").match(/( |^)@[a-zA-Z0-9-_.]+/gm); + const usernames = (usersNoIdOutput || []).map(u => (u || "").trim().split("@").pop()); + for (const username of usernames) { + if (!"all|here|channel|everyone".split("|").includes(username)) { + const user = await findByUsername(username); + if (user) idsFromUsernames.push(user.id); + } + } + } catch (err) { + console.log(err); + } + const usersOutput = (messageResource.text || "").match(/@[^: ]+:([0-f-]{36})/gm); const globalOutput = (messageResource.text || "").match( /(^| )@(all|here|channel|everyone)([^a-z]|$)/gm, ); return { - users: (usersOutput || []).map(u => (u || "").trim().split(":").pop()), + users: [ + ...(usersOutput || []).map(u => (u || "").trim().split(":").pop()), + ...idsFromUsernames, + ], specials: (globalOutput || []).map(g => (g || "").trim().split("@").pop()) as specialMention[], }; }; diff --git a/twake/backend/node/src/services/user/api.ts b/twake/backend/node/src/services/user/api.ts index 3ef2bf9b70..2997e4f01c 100644 --- a/twake/backend/node/src/services/user/api.ts +++ b/twake/backend/node/src/services/user/api.ts @@ -52,6 +52,7 @@ export interface UsersServiceAPI context?: ExecutionContext, ): Promise>; + getByUsername(username: string): Promise; getByEmail(email: string): Promise; getByEmails(email: string[]): Promise; getByConsoleId(consoleUserId: string): Promise; diff --git a/twake/backend/node/src/services/user/entities/user.ts b/twake/backend/node/src/services/user/entities/user.ts index 34de074a24..d9ae913b1d 100644 --- a/twake/backend/node/src/services/user/entities/user.ts +++ b/twake/backend/node/src/services/user/entities/user.ts @@ -7,7 +7,7 @@ export const TYPE = "user"; @Entity(TYPE, { primaryKey: [["id"]], - globalIndexes: [["email_canonical"]], + globalIndexes: [["email_canonical"], ["username_canonical"]], type: TYPE, search, }) diff --git a/twake/backend/node/src/services/user/services/users/service.ts b/twake/backend/node/src/services/user/services/users/service.ts index dc0823b01d..49eda75a67 100644 --- a/twake/backend/node/src/services/user/services/users/service.ts +++ b/twake/backend/node/src/services/user/services/users/service.ts @@ -77,7 +77,9 @@ export class UserService implements UsersServiceAPI { if (user.identity_provider_id && !user.identity_provider) user.identity_provider = "console"; if (user.email_canonical) user.email_canonical = user.email_canonical.toLocaleLowerCase(); if (user.username_canonical) - user.username_canonical = user.username_canonical.toLocaleLowerCase(); + user.username_canonical = (user.username_canonical || "") + .toLocaleLowerCase() + .replace(/[^a-z0-9_-]/, ""); } async create(user: User): Promise> { @@ -204,6 +206,12 @@ export class UserService implements UsersServiceAPI { return await this.repository.findOne(pk); } + async getByUsername(username: string): Promise { + return await this.repository.findOne({ + username_canonical: (username || "").toLocaleLowerCase(), + }); + } + async getByConsoleId(id: string, service_id: string = "console"): Promise { const extUser = await this.extUserRepository.findOne({ service_id, external_id: id }); if (!extUser) { @@ -220,9 +228,9 @@ export class UserService implements UsersServiceAPI { return this.repository.findOne({ email_canonical: email }).then(user => Boolean(user)); } async getAvailableUsername(username: string): Promise { - const users = await this.repository.find({}).then(a => a.getEntities()); + const user = await this.getByUsername(username); - if (!users.find(user => user.username_canonical == username.toLocaleLowerCase())) { + if (!user) { return username; } @@ -230,7 +238,7 @@ export class UserService implements UsersServiceAPI { for (let i = 1; i < 1000; i++) { const dynamicUsername = username + i; - if (!users.find(user => user.username_canonical == dynamicUsername.toLocaleLowerCase())) { + if (!(await this.getByUsername(dynamicUsername.toLocaleLowerCase()))) { suitableUsername = dynamicUsername; break; } diff --git a/twake/backend/node/src/version.ts b/twake/backend/node/src/version.ts index 9e0dbdff2d..8f9fbf688f 100644 --- a/twake/backend/node/src/version.ts +++ b/twake/backend/node/src/version.ts @@ -1,7 +1,7 @@ export default { - current: /* @VERSION_DETAIL */ "2021.Q4.846", + current: /* @VERSION_DETAIL */ "2021.Q4.860", minimal: { - web: /* @MIN_VERSION_WEB */ "2021.Q4.846", - mobile: /* @MIN_VERSION_MOBILE */ "2021.Q4.846", + web: /* @MIN_VERSION_WEB */ "2021.Q4.860", + mobile: /* @MIN_VERSION_MOBILE */ "2021.Q4.860", }, }; diff --git a/twake/docker-compose.onpremise.mongo.yml b/twake/docker-compose.onpremise.mongo.yml new file mode 100644 index 0000000000..a4bf588285 --- /dev/null +++ b/twake/docker-compose.onpremise.mongo.yml @@ -0,0 +1,28 @@ +version: "3.4" + +services: + mongo: + container_name: mongo + image: mongo + volumes: + - ./docker-data/mongo:/data/db + + node: + image: twaketech/twake-node + ports: + - 3000:3000 + environment: + - DEV=production + - SEARCH_DRIVER=mongodb + - DB_DRIVER=mongodb + - PUBSUB_TYPE=local + build: + context: . + dockerfile: ./docker/twake-node/Dockerfile + target: development + volumes: + - ./docker-data/documents/:/storage/ + depends_on: + - mongo + links: + - mongo \ No newline at end of file diff --git a/twake/frontend/src/app/environment/version.ts b/twake/frontend/src/app/environment/version.ts index d36b4dd311..ec1bc99a61 100644 --- a/twake/frontend/src/app/environment/version.ts +++ b/twake/frontend/src/app/environment/version.ts @@ -1,5 +1,5 @@ export default { version: /* @VERSION */ '2021.Q4', - version_detail: /* @VERSION_DETAIL */ '2021.Q4.846', + version_detail: /* @VERSION_DETAIL */ '2021.Q4.860', version_name: /* @VERSION_NAME */ 'Albatros', }; diff --git a/twake/frontend/src/app/scenes/Apps/Messages/Message/Parts/Blocks.tsx b/twake/frontend/src/app/scenes/Apps/Messages/Message/Parts/Blocks.tsx index 927f01cfa7..c30bcdb627 100644 --- a/twake/frontend/src/app/scenes/Apps/Messages/Message/Parts/Blocks.tsx +++ b/twake/frontend/src/app/scenes/Apps/Messages/Message/Parts/Blocks.tsx @@ -48,7 +48,9 @@ export default React.memo((props: Props) => { }, }} > - {props.fallback} + {(props.fallback || '') + //Fix markdown simple line break + .replace(/\n/g, '  \n')} ) : ( diff --git a/twake/frontend/src/app/state/recoil/hooks/messages/useEphemeralMessages.ts b/twake/frontend/src/app/state/recoil/hooks/messages/useEphemeralMessages.ts index b32dd5bdc7..9cecfd1f65 100644 --- a/twake/frontend/src/app/state/recoil/hooks/messages/useEphemeralMessages.ts +++ b/twake/frontend/src/app/state/recoil/hooks/messages/useEphemeralMessages.ts @@ -4,11 +4,20 @@ import { useRealtimeRoom } from 'app/services/Realtime/useRealtime'; import CurrentUser from 'app/services/user/CurrentUser'; import _ from 'lodash'; import { useState } from 'react'; +import { atomFamily, useRecoilCallback, useRecoilState } from 'recoil'; import { AtomChannelKey } from '../../atoms/Messages'; import { useSetMessage } from './useMessage'; +const EphemeralMessageState = atomFamily({ + key: 'EphemeralMessageState', + default: key => null, +}); + export const useEphemeralMessages = (key: AtomChannelKey) => { - const [lastEphemeral, setLastEphemeral] = useState(null); + const [lastEphemeral, setLastEphemeral] = useRecoilState(EphemeralMessageState(key)); + const getLastEphemeral = useRecoilCallback(({ snapshot }) => (key: AtomChannelKey) => { + return snapshot.getLoadable(EphemeralMessageState(key)).valueMaybe(); + }); const setMessage = useSetMessage(key.companyId); useRealtimeRoom( @@ -17,6 +26,7 @@ export const useEphemeralMessages = (key: AtomChannelKey) => { async (action: string, event: any) => { if (action === 'created' || action === 'updated') { const message = event as NodeMessage; + const lastEphemeral = getLastEphemeral(key); if (message.ephemeral) { if ( message.subtype === 'deleted' && diff --git a/twake/update_version.js b/twake/update_version.js index eb15c580a6..35ab54e6ed 100755 --- a/twake/update_version.js +++ b/twake/update_version.js @@ -5,9 +5,9 @@ const versions = { VERSION_NAME: process.env.TWAKE_VERSION_NAME || "Albatros", VERSION: process.env.TWAKE_VERSION || "2021.Q4", - VERSION_DETAIL: process.env.TWAKE_VERSION_DETAIL || "2021.Q4.846", - MIN_VERSION_WEB: process.env.TWAKE_MIN_VERSION_WEB || "2021.Q4.846", - MIN_VERSION_MOBILE: process.env.TWAKE_MIN_VERSION_MOBILE || "2021.Q4.846", + VERSION_DETAIL: process.env.TWAKE_VERSION_DETAIL || "2021.Q4.860", + MIN_VERSION_WEB: process.env.TWAKE_MIN_VERSION_WEB || "2021.Q4.860", + MIN_VERSION_MOBILE: process.env.TWAKE_MIN_VERSION_MOBILE || "2021.Q4.860", }; const files = [