From f81459d4c0418b75cb4b2d96708fd909aba04f7f Mon Sep 17 00:00:00 2001 From: Whitewater Date: Mon, 12 Sep 2022 19:17:59 +0800 Subject: [PATCH 01/14] Feat/left menu i18n (#407) * feat(i18n): add left menu i18n * chore(i18n): add missing ComingSoon * chore(i18n): update docs * fix: lint --- .eslintrc.json | 3 ++- .../src/menu/left-menu/LeftMenu.tsx | 22 ++++++++++--------- .../Layout/LayoutSettings.tsx | 4 +++- libs/datasource/i18n/README.md | 14 ++++++++++-- libs/datasource/i18n/project.json | 2 +- libs/datasource/i18n/src/resources/en.json | 6 ++++- 6 files changed, 35 insertions(+), 16 deletions(-) diff --git a/.eslintrc.json b/.eslintrc.json index 549f14aed9..527de2b1b7 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -103,7 +103,8 @@ "datasource:db-services", "components:ui", "components:icons", - "library:feature-flags" + "library:feature-flags", + "datasource:i18n" ] }, { diff --git a/libs/components/editor-plugins/src/menu/left-menu/LeftMenu.tsx b/libs/components/editor-plugins/src/menu/left-menu/LeftMenu.tsx index 043b9a77c6..731b6ea42e 100644 --- a/libs/components/editor-plugins/src/menu/left-menu/LeftMenu.tsx +++ b/libs/components/editor-plugins/src/menu/left-menu/LeftMenu.tsx @@ -1,14 +1,15 @@ -import { useMemo } from 'react'; -import { Virgo, PluginHooks } from '@toeverything/framework/virgo'; -import { Cascader, CascaderItemProps } from '@toeverything/components/ui'; -import { Protocol } from '@toeverything/datasource/db-service'; -import { TurnIntoMenu } from './TurnIntoMenu'; import { AddViewIcon, DeleteCashBinIcon, TurnIntoIcon, UngroupIcon, } from '@toeverything/components/icons'; +import { Cascader, CascaderItemProps } from '@toeverything/components/ui'; +import { Protocol } from '@toeverything/datasource/db-service'; +import { useTranslation } from '@toeverything/datasource/i18n'; +import { PluginHooks, Virgo } from '@toeverything/framework/virgo'; +import { useMemo } from 'react'; +import { TurnIntoMenu } from './TurnIntoMenu'; interface LeftMenuProps { anchorEl?: Element; @@ -21,10 +22,11 @@ interface LeftMenuProps { export function LeftMenu(props: LeftMenuProps) { const { editor, anchorEl, hooks, blockId, onClose } = props; + const { t } = useTranslation(); const menu: CascaderItemProps[] = useMemo( () => [ { - title: 'Delete', + title: t('Delete'), callback: () => { editor.commands.blockCommands.removeBlock(blockId); }, @@ -32,7 +34,7 @@ export function LeftMenu(props: LeftMenuProps) { icon: , }, { - title: 'Turn into', + title: t('Turn into'), subItems: [], children: ( , }, { - title: 'Add A Below Block', + title: t('Add A Below Block'), icon: , callback: async () => { const block = await editor.getBlockById(blockId); @@ -66,14 +68,14 @@ export function LeftMenu(props: LeftMenuProps) { }, }, { - title: 'Divide Here As A New Group', + title: t('Divide Here As A New Group'), icon: , callback: () => { editor.commands.blockCommands.splitGroupFromBlock(blockId); }, }, ], - [editor, hooks, blockId, onClose] + [t, editor, hooks, blockId, onClose] ); // const filterItems = ( diff --git a/libs/components/layout/src/settings-sidebar/Layout/LayoutSettings.tsx b/libs/components/layout/src/settings-sidebar/Layout/LayoutSettings.tsx index 3d6ad7fe41..5019cb951a 100644 --- a/libs/components/layout/src/settings-sidebar/Layout/LayoutSettings.tsx +++ b/libs/components/layout/src/settings-sidebar/Layout/LayoutSettings.tsx @@ -1,9 +1,11 @@ import { styled } from '@toeverything/components/ui'; +import { useTranslation } from '@toeverything/datasource/i18n'; export const LayoutSettings = () => { + const { t } = useTranslation(); return ( -

Layout Settings Coming Soon...

+

{t('ComingSoon')}

); }; diff --git a/libs/datasource/i18n/README.md b/libs/datasource/i18n/README.md index c454be380a..31ab1d66a1 100644 --- a/libs/datasource/i18n/README.md +++ b/libs/datasource/i18n/README.md @@ -2,10 +2,13 @@ ## Usages +- Update missing translations into the base resources, a.k.a the `src/resources/en.json` +- Replace literal text with translation keys + ```tsx import { useTranslation } from '@toeverything/datasource/i18n'; -// base.json +// src/resources/en.json // { // 'Text': 'some text', // 'Switch to language': 'Switch to {{language}}', // <- you can interpolation by curly brackets @@ -33,7 +36,14 @@ const App = () => { }; ``` -## How to sync translations +## How the i18n workflow works? + +- When the `src/resources/en.json`(base language) updated and merged to the develop branch, will trigger the `languages-sync` action. +- The `languages-sync` action will check the base language and add missing translations to the Tolgee platform. +- This way, partners from the community can update the translations. +- Finally, the `languages-download` action will regularly download the latest translation resources from the Tolgee platform. + +## How to sync translations manually - Set token as environment variable diff --git a/libs/datasource/i18n/project.json b/libs/datasource/i18n/project.json index bac263e222..8535bfb237 100644 --- a/libs/datasource/i18n/project.json +++ b/libs/datasource/i18n/project.json @@ -2,7 +2,7 @@ "$schema": "../../../node_modules/nx/schemas/project-schema.json", "sourceRoot": "libs/datasource/i18n/src", "projectType": "library", - "tags": ["datasource"], + "tags": ["datasource:i18n"], "targets": { "build": { "executor": "@nrwl/web:rollup", diff --git a/libs/datasource/i18n/src/resources/en.json b/libs/datasource/i18n/src/resources/en.json index e243d71db1..aae42bd05d 100644 --- a/libs/datasource/i18n/src/resources/en.json +++ b/libs/datasource/i18n/src/resources/en.json @@ -20,5 +20,9 @@ "Import Workspace": "Import Workspace", "Export Workspace": "Export Workspace", "Last edited by": "Last edited by {{name}}", - "Logout": "Logout" + "Logout": "Logout", + "Delete": "Delete", + "Turn into": "Turn into", + "Add A Below Block": "Add A Below Block", + "Divide Here As A New Group": "Divide Here As A New Group" } From 142e0c76458a71db49745bff43f98a0f8e2146a9 Mon Sep 17 00:00:00 2001 From: lawvs <18554747+lawvs@users.noreply.github.com> Date: Mon, 12 Sep 2022 19:51:14 +0800 Subject: [PATCH 02/14] fix: ci permission --- .github/workflows/languages-download.yml | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/.github/workflows/languages-download.yml b/.github/workflows/languages-download.yml index 4e180bccce..ee57fdf5bf 100644 --- a/.github/workflows/languages-download.yml +++ b/.github/workflows/languages-download.yml @@ -13,6 +13,12 @@ concurrency: group: ${{ github.workflow }}-${{ github.event_name == 'pull_request' && github.head_ref || github.sha }} cancel-in-progress: true +# This action need write permission to create pull requests +# See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#permissions +permissions: + contents: write + pull-requests: write + jobs: main: strategy: @@ -41,7 +47,6 @@ jobs: run: pnpm install - name: Sync Languages - if: github.ref == 'refs/heads/develop' || github.ref == 'refs/heads/master' working-directory: ./libs/datasource/i18n run: pnpm run download-resources env: @@ -61,11 +66,11 @@ jobs: fi git config user.name 'github-actions[bot]' git config user.email 'github-actions[bot]@users.noreply.github.com' - git commit --message 'feat(i18n): new translations' + git commit --message 'feat(i18n): new translations' --no-verify git remote set-url origin "https://$GITHUB_ACTOR:$GITHUB_TOKEN@github.com/$GITHUB_REPOSITORY" git push --force origin HEAD:$TARGET_BRANCH env: - GITHUB_TOKEN: ${{ secrets.github_token }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} TARGET_BRANCH: bot/new-translations - name: Get current date From 925673ec1c70775df0deacc25440c9788e5da189 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Mon, 12 Sep 2022 12:21:29 +0000 Subject: [PATCH 03/14] feat(i18n): new translations --- libs/datasource/i18n/src/resources/zh-Hans.json | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/libs/datasource/i18n/src/resources/zh-Hans.json b/libs/datasource/i18n/src/resources/zh-Hans.json index 5122e4b881..0df4daf61b 100644 --- a/libs/datasource/i18n/src/resources/zh-Hans.json +++ b/libs/datasource/i18n/src/resources/zh-Hans.json @@ -21,5 +21,9 @@ "Import Workspace": "导入 Workspace", "Export Workspace": "导出 Workspace", "Last edited by": "最后编辑者为 {{name}}", - "Logout": "退出登录" + "Logout": "退出登录", + "Delete": "删除", + "Turn into": "转换为", + "Add A Below Block": "在下方添加一个新块", + "Divide Here As A New Group": "从这里划分一个新组" } From 255527fa9578a9da51abec086dd577af231dbd4d Mon Sep 17 00:00:00 2001 From: lawvs <18554747+lawvs@users.noreply.github.com> Date: Mon, 12 Sep 2022 21:13:40 +0800 Subject: [PATCH 04/14] fix: #398 --- apps/venus/src/app/common/Header.tsx | 10 ++-- apps/venus/src/app/i18n/index.ts | 70 +++++++++++++++++++++------- 2 files changed, 59 insertions(+), 21 deletions(-) diff --git a/apps/venus/src/app/common/Header.tsx b/apps/venus/src/app/common/Header.tsx index 9a2e8d0173..2dc7c50ff0 100644 --- a/apps/venus/src/app/common/Header.tsx +++ b/apps/venus/src/app/common/Header.tsx @@ -13,7 +13,7 @@ import AFFiNETextLogo from './affine-text-logo.png'; import { HoverMenu } from './HoverMenu'; import { MobileHeader } from './MobileHeader'; -import { options } from '../i18n'; +import { LOCALES } from '../i18n'; export const AFFiNEHeader = () => { const matches = useMediaQuery('(max-width: 1024px)'); @@ -140,15 +140,15 @@ export const AFFiNEHeader = () => { }} /> diff --git a/apps/venus/src/app/i18n/index.ts b/apps/venus/src/app/i18n/index.ts index 9ddaa8330d..f4428a9b09 100644 --- a/apps/venus/src/app/i18n/index.ts +++ b/apps/venus/src/app/i18n/index.ts @@ -1,25 +1,63 @@ -import i18next from 'i18next'; -import { initReactI18next } from 'react-i18next'; -import en_US from './resources/en.json'; -import zh_CN from './resources/zh.json'; +import i18next, { Resource } from 'i18next'; +import { + I18nextProvider, + initReactI18next, + useTranslation, +} from 'react-i18next'; +import { LOCALES } from './resources'; +import type en_US from './resources/en.json'; -const resources = { - en: en_US, - zh: zh_CN, -} as const; +// See https://react.i18next.com/latest/typescript +declare module 'react-i18next' { + interface CustomTypeOptions { + // custom namespace type if you changed it + // defaultNS: 'ns1'; + // custom resources type + resources: { + en: typeof en_US; + }; + } +} + +const STORAGE_KEY = 'i18n_lng'; + +export { i18n, useTranslation, I18nProvider, LOCALES }; + +const resources = LOCALES.reduce( + (acc, { tag, res }) => ({ ...acc, [tag]: { translation: res } }), + {} +); + +const fallbackLng = LOCALES[0].tag; +const standardizeLocale = (language: string) => { + if (LOCALES.find(locale => locale.tag === language)) return language; + if ( + LOCALES.find( + locale => locale.tag === language.slice(0, 2).toLowerCase() + ) + ) + return language; + return fallbackLng; +}; + +const language = standardizeLocale( + localStorage.getItem(STORAGE_KEY) ?? navigator.language +); + +const i18n = i18next.createInstance(); +i18n.use(initReactI18next).init({ + lng: language, + fallbackLng, + debug: process.env['NODE_ENV'] === 'development', -i18next.use(initReactI18next).init({ - lng: 'en', - fallbackLng: 'en', resources, interpolation: { escapeValue: false, // not needed for react as it escapes by default }, }); -export const options = [ - { value: 'en', text: 'English' }, - { value: 'zh', text: '简体中文' }, -]; +i18n.on('languageChanged', lng => { + localStorage.setItem(STORAGE_KEY, lng); +}); -export { i18next }; +const I18nProvider = I18nextProvider; From 19ca0c77efe1c600757a4e9cb015545dbef500f2 Mon Sep 17 00:00:00 2001 From: lawvs <18554747+lawvs@users.noreply.github.com> Date: Mon, 12 Sep 2022 21:14:37 +0800 Subject: [PATCH 05/14] feat: add Spanish --- apps/venus/src/app/i18n/resources/en.json | 75 +++++++++++----------- apps/venus/src/app/i18n/resources/es.json | 32 +++++++++ apps/venus/src/app/i18n/resources/index.ts | 24 +++++++ apps/venus/src/app/i18n/resources/zh.json | 74 +++++++++++---------- 4 files changed, 129 insertions(+), 76 deletions(-) create mode 100644 apps/venus/src/app/i18n/resources/es.json create mode 100644 apps/venus/src/app/i18n/resources/index.ts diff --git a/apps/venus/src/app/i18n/resources/en.json b/apps/venus/src/app/i18n/resources/en.json index 80381df1d7..4dfc81af86 100644 --- a/apps/venus/src/app/i18n/resources/en.json +++ b/apps/venus/src/app/i18n/resources/en.json @@ -1,40 +1,39 @@ { - "translation": { - "Blog": "Blog", - "AboutUs": "About Us", - "Open Source": "Open Source", - "Docs": "Docs", - "Feedback": "Feedback", - "ContactUs": "Contact Us", - "Privacy First": "Privacy First", - "Alternative": "Alternative", - "Check GitHub": "Check GitHub", - "Try it Online": "Try it Online", - "language": "Language", - "description1": { - "part1": "Affine is the next-generation collaborative knowledge base for professionals.", - "part2": "It's not just a collection of Docs, whiteboard, and tables.", - "part3": "Transform any building block as you like.", - "part4": "Say goodbye to redundancy. Store your data once, and keep your data as you like it." - }, - "description2": { - "part1": "Shape Your Page", - "part2": "Docs, Kanbans, and Databases are all fully functional anywhere, anytime. A truly what-you-see-is-what-you-get environment for your data.", - "part3": "All pages come with a document (Paper Mode) and whiteboard (Edgeless Mode) view." - }, - "description3": { - "part1": "Plan Your Task", - "part2": "No more chaos managing multiple views.", - "part3": "Set a TODO with Markdown, and seamlessly edit it within a Kanban.", - "part4": "Managing multi-dimensional tables should be this simple - and now it is." - }, - "description4": { - "part1": "Privacy-first, and collaborative. No compromises whatsoever.", - "part2": "We don't like being locked-in, and neither should you. Privacy is at the foundation of everything we do, but it should not limit us that's why there are no compromises.", - "part3": "Your data is yours;it is always locally stored and secured - available to you always. While still being able to enjoy collaboration features such as real-time editing and sharing with others, without any cloud setup." - }, - "BuildFor": "Build for an open and semantic future", - "KeepUpdated": "Keep Updated on", - "Join": "Join Our Community" - } + "Blog": "Blog", + "AboutUs": "About Us", + "Open Source": "Open Source", + "Docs": "Docs", + "Feedback": "Feedback", + "ContactUs": "Contact Us", + "Privacy First": "Privacy First", + "Alternative": "Alternative", + "Check GitHub": "Check GitHub", + "GitHub": "GitHub", + "Try it Online": "Try it Online", + "language": "Language", + "description1": { + "part1": "Affine is the next-generation collaborative knowledge base for professionals.", + "part2": "It's not just a collection of Docs, whiteboard, and tables.", + "part3": "Transform any building block as you like.", + "part4": "Say goodbye to redundancy. Store your data once, and keep your data as you like it." + }, + "description2": { + "part1": "Shape Your Page", + "part2": "Docs, Kanbans, and Databases are all fully functional anywhere, anytime. A truly what-you-see-is-what-you-get environment for your data.", + "part3": "All pages come with a document (Paper Mode) and whiteboard (Edgeless Mode) view." + }, + "description3": { + "part1": "Plan Your Task", + "part2": "No more chaos managing multiple views.", + "part3": "Set a TODO with Markdown, and seamlessly edit it within a Kanban.", + "part4": "Managing multi-dimensional tables should be this simple - and now it is." + }, + "description4": { + "part1": "Privacy-first, and collaborative. No compromises whatsoever.", + "part2": "We don't like being locked-in, and neither should you. Privacy is at the foundation of everything we do, but it should not limit us that's why there are no compromises.", + "part3": "Your data is yours;it is always locally stored and secured - available to you always. While still being able to enjoy collaboration features such as real-time editing and sharing with others, without any cloud setup." + }, + "BuildFor": "Build for an open and semantic future", + "KeepUpdated": "Keep Updated on", + "Join": "Join Our Community" } diff --git a/apps/venus/src/app/i18n/resources/es.json b/apps/venus/src/app/i18n/resources/es.json new file mode 100644 index 0000000000..034eb54f70 --- /dev/null +++ b/apps/venus/src/app/i18n/resources/es.json @@ -0,0 +1,32 @@ +{ + "Alternative": "Alternativa", + "BuildFor": "Construir por un futuro abierto y semántico", + "Check GitHub": "Revisar en GitHub", + "Join": "Únete a nuestra comunidad", + "KeepUpdated": "Mantente actualizado", + "Open Source": "Código abierto", + "Privacy First": "La privacidad es lo primero", + "Try it Online": "Prueba Online", + "description1": { + "part1": "Affine es un base de conocimiento colaborativa de próxima generación para los profesionales.", + "part2": "Es más que una simple colección de Documentos, Pizarra y Tablas.", + "part3": "Transforma cualquier bloque como tu quieras. ", + "part4": "Dígale adios a la redundancia, Almacene sus datos una vez, y manténgalos como usted quiera." + }, + "description2": { + "part1": "Dele forma a sus páginas.", + "part2": "Los Documentos, Kanbans y Base de datos son completamente funcionales en cualquier lugar, en cualquier momento. Un verdadero entorno de lo que ves y lo que obtienes para tus datos.", + "part3": "Todas las páginas tiene una vista de documento y de pizarra. " + }, + "description3": { + "part1": "Programe sus tareas.", + "part2": "No más caos manejando múltiples vistas.", + "part3": "Establezca un TODO con Markdown, y edítelo sin problemas en un Kanban. ", + "part4": "La gestión de tablas multidimensionales debería ser así de fácil y ahora lo es." + }, + "description4": { + "part1": "Privacidad en primer lugar, y colaboración sin compromiso. ", + "part2": "\nNo nos gusta estar encerrados, y a usted tampoco debería. La privacidad está en la base de todo lo que hacemos, pero no debe limitarnos, por eso no hay compromisos.", + "part3": "Sus datos son suyos; siempre están almacenados y protegidos de forma local, siempre disponibles para usted. Sin dejar de lado el poder disfrutar de funciones de colaboración como la edición en tiempo real y el uso compartido con otros, sin ninguna configuración en la nube." + } +} diff --git a/apps/venus/src/app/i18n/resources/index.ts b/apps/venus/src/app/i18n/resources/index.ts new file mode 100644 index 0000000000..424560a398 --- /dev/null +++ b/apps/venus/src/app/i18n/resources/index.ts @@ -0,0 +1,24 @@ +import en from './en.json'; +import es from './es.json'; +import zh_Hans from './zh.json'; + +export const LOCALES = [ + { + name: 'English', + tag: 'en', + originalName: 'English', + res: en, + }, + { + name: 'Simplified Chinese', + tag: 'zh-Hans', + originalName: '简体中文', + res: zh_Hans, + }, + { + name: 'Spanish', + tag: 'es', + originalName: 'español', + res: es, + }, +] as const; diff --git a/apps/venus/src/app/i18n/resources/zh.json b/apps/venus/src/app/i18n/resources/zh.json index 545a644c28..e630e1c753 100644 --- a/apps/venus/src/app/i18n/resources/zh.json +++ b/apps/venus/src/app/i18n/resources/zh.json @@ -1,40 +1,38 @@ { - "translation": { - "Blog": "博客", - "AboutUs": "关于我们", - "Open Source": "开源", - "Privacy First": "隐私第一", - "Docs": "文档", - "Feedback": "反馈", - "ContactUs": "联系我们", - "Alternative": "的另一种选择", - "Check GitHub": "GitHub中查看", - "Try it Online": "在线试用", - "language": "语言", - "description1": { - "part1": "Affine是面向专业人士的下一代协同知识库", - "part2": "它不仅仅是一个文档、白板和表格的集合。", - "part3": "可以根据需要转换任何构建块。", - "part4": "向冗余说再见吧。将数据存储一次,并保留您喜欢的数据。" - }, - "description2": { - "part1": "塑造您的页面", - "part2": "文档、看板和数据库在任何地方、任何时候都是完全可用的,一个真正的“所见即所得”的数据环境。", - "part3": "所有页面都有一个文档(纸张模式)和白板(无边缘模式)视图。" - }, - "description3": { - "part1": "计划您的任务", - "part2": "不再混乱地管理多个视图。", - "part3": "使用 Markdown 设置 TODO,并在看板中无缝地编辑它。", - "part4": "管理多维表格本应该就这么简单——现在的它就是这么简单。" - }, - "description4": { - "part1": "隐私第一,合作无间,绝不妥协。", - "part2": "我们不喜欢被关起来,您也不应该。隐私是我们做任何事情的基础,但它不应该限制我们,这就是为什么我们决不妥协。", - "part3": "您的数据是您自己的,它总是安全的在本地存储并随时供您使用。您不需要任何云设置就可以享受协作功能,即时编辑和与他人共享。" - }, - "BuildFor": "构建一个开放和语义化的未来", - "KeepUpdated": "持续更新在", - "Join": "加入我们的社区" - } + "Blog": "博客", + "AboutUs": "关于我们", + "Open Source": "开源", + "Privacy First": "隐私第一", + "Docs": "文档", + "Feedback": "反馈", + "ContactUs": "联系我们", + "Alternative": "的另一种选择", + "Check GitHub": "GitHub中查看", + "Try it Online": "在线试用", + "language": "语言", + "description1": { + "part1": "Affine是面向专业人士的下一代协同知识库", + "part2": "它不仅仅是一个文档、白板和表格的集合。", + "part3": "可以根据需要转换任何构建块。", + "part4": "向冗余说再见吧。将数据存储一次,并保留您喜欢的数据。" + }, + "description2": { + "part1": "塑造您的页面", + "part2": "文档、看板和数据库在任何地方、任何时候都是完全可用的,一个真正的“所见即所得”的数据环境。", + "part3": "所有页面都有一个文档(纸张模式)和白板(无边缘模式)视图。" + }, + "description3": { + "part1": "计划您的任务", + "part2": "不再混乱地管理多个视图。", + "part3": "使用 Markdown 设置 TODO,并在看板中无缝地编辑它。", + "part4": "管理多维表格本应该就这么简单——现在的它就是这么简单。" + }, + "description4": { + "part1": "隐私第一,合作无间,绝不妥协。", + "part2": "我们不喜欢被关起来,您也不应该。隐私是我们做任何事情的基础,但它不应该限制我们,这就是为什么我们决不妥协。", + "part3": "您的数据是您自己的,它总是安全的在本地存储并随时供您使用。您不需要任何云设置就可以享受协作功能,即时编辑和与他人共享。" + }, + "BuildFor": "构建一个开放和语义化的未来", + "KeepUpdated": "持续更新在", + "Join": "加入我们的社区" } From 88639ea8bbc80d9ce002745c2754929c9c691ccd Mon Sep 17 00:00:00 2001 From: DiamondThree Date: Tue, 13 Sep 2022 14:50:34 +0800 Subject: [PATCH 06/14] fix: commend-menu create block can leave menu name --- .../editor-plugins/src/menu/command-menu/Menu.tsx | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/libs/components/editor-plugins/src/menu/command-menu/Menu.tsx b/libs/components/editor-plugins/src/menu/command-menu/Menu.tsx index 66b2679220..80afb0726b 100644 --- a/libs/components/editor-plugins/src/menu/command-menu/Menu.tsx +++ b/libs/components/editor-plugins/src/menu/command-menu/Menu.tsx @@ -211,6 +211,8 @@ export const CommandMenu = ({ editor, hooks, style }: CommandMenuProps) => { const handleSelected = async (type: BlockFlavorKeys | string) => { const text = await editor.commands.textCommands.getBlockText(blockId); + const block = await editor.getBlockById(blockId); + let textValue = block.getProperty('text').value; editor.blockHelper.removeSearchSlash(blockId, true); if (type.startsWith('Virgo')) { const handler = @@ -223,14 +225,18 @@ export const CommandMenu = ({ editor, hooks, style }: CommandMenuProps) => { } else { await commonCommandMenuHandler(blockId, type, editor); } - const block = await editor.getBlockById(blockId); const nextBlock = await block.nextSibling(); setTimeout(() => { editor.selectionManager.activeNodeByNodeId(nextBlock.id); + if (textValue.length === 1) { + block.remove(); + } else { + block.setProperty('text', { + //@ts-ignore + value: textValue.filter(text => !text.search), + }); + } }, 100); - if (block.blockProvider.isEmpty()) { - block.remove(); - } } else { if (Protocol.Block.Type[type as BlockFlavorKeys]) { const block = await editor.commands.blockCommands.convertBlock( From f48e414d3dceec4d0be3af54b6a970ac7ac65542 Mon Sep 17 00:00:00 2001 From: lawvs <18554747+lawvs@users.noreply.github.com> Date: Tue, 13 Sep 2022 15:50:06 +0800 Subject: [PATCH 07/14] fix: empty ref page --- .../src/blocks/group/scene-kanban/RefPage.tsx | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/libs/components/editor-blocks/src/blocks/group/scene-kanban/RefPage.tsx b/libs/components/editor-blocks/src/blocks/group/scene-kanban/RefPage.tsx index 34d6dbb583..aa4fbcfacb 100644 --- a/libs/components/editor-blocks/src/blocks/group/scene-kanban/RefPage.tsx +++ b/libs/components/editor-blocks/src/blocks/group/scene-kanban/RefPage.tsx @@ -1,6 +1,13 @@ import { useEditor } from '@toeverything/components/editor-core'; import { MuiBackdrop, styled, useTheme } from '@toeverything/components/ui'; -import { createContext, ReactNode, useContext, useState } from 'react'; +import { + createContext, + ReactNode, + useContext, + useEffect, + useRef, + useState, +} from 'react'; import { createPortal } from 'react-dom'; const Dialog = styled('div')({ @@ -11,7 +18,7 @@ const Dialog = styled('div')({ boxShadow: '0px 1px 10px rgba(152, 172, 189, 0.6)', borderRadius: '10px', padding: '72px 120px', - overflow: 'scroll', + overflowY: 'auto', }); const Modal = ({ open, children }: { open: boolean; children?: ReactNode }) => { @@ -48,13 +55,20 @@ const Modal = ({ open, children }: { open: boolean; children?: ReactNode }) => { const ModalPage = ({ blockId }: { blockId: string | null }) => { const { editor, editorElement } = useEditor(); + const editorRef = useRef(null); const AffineEditor = editorElement as any; + // Active block after modal open + useEffect(() => { + editorRef.current?.selectionManager.activeNodeByNodeId(blockId); + }, [blockId]); + return ( {blockId && ( Date: Tue, 13 Sep 2022 21:00:45 +0800 Subject: [PATCH 08/14] Fix/venus build env (#415) fix: venus build env Co-authored-by: DarkSky <25152247+darkskygit@users.noreply.github.com> --- .github/deployment/Dockerfile-affine | 1 + .github/workflows/livedemo.yml | 6 +++--- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/.github/deployment/Dockerfile-affine b/.github/deployment/Dockerfile-affine index e0bb7e9edf..76c317d6c8 100644 --- a/.github/deployment/Dockerfile-affine +++ b/.github/deployment/Dockerfile-affine @@ -1,4 +1,5 @@ FROM node:16-alpine as builder +ARG HUBSPOT_API_SECRET WORKDIR /app RUN apk add git && npm i -g pnpm@7 COPY . . diff --git a/.github/workflows/livedemo.yml b/.github/workflows/livedemo.yml index d5ed5b9987..432fbaf13d 100644 --- a/.github/workflows/livedemo.yml +++ b/.github/workflows/livedemo.yml @@ -1,4 +1,4 @@ -name: Build AFFiNE-Local +name: Build AFFiNE-Livedemo on: push: @@ -64,14 +64,14 @@ jobs: - name: Build and push Docker image (AFFINE-Local) uses: docker/build-push-action@v3 - env: - HUBSPOT_API_SECRET: ${{ secrets.SuperSecret }} with: context: . push: true file: ./.github/deployment/Dockerfile-affine tags: ${{ env.LOCAL_CACHE }} target: AFFiNE + build-args: | + HUBSPOT_API_SECRET=${{ secrets.HUBSPOT_API_SECRET }} - name: Build and push Docker image (AFFINE-Local) uses: docker/build-push-action@v3 From 42142e4562c2f81c377b7383faa19edd53d797d1 Mon Sep 17 00:00:00 2001 From: tzhangchi Date: Tue, 13 Sep 2022 22:19:05 +0800 Subject: [PATCH 09/14] docs(mysc): add job --- README.md | 2 ++ docs/Jobs.md | 4 ++++ docs/mysc.app.md | 25 +++++++++++++++++++++++++ 3 files changed, 31 insertions(+) create mode 100644 docs/Jobs.md create mode 100644 docs/mysc.app.md diff --git a/README.md b/README.md index 256da653f0..0f43ad56db 100644 --- a/README.md +++ b/README.md @@ -145,6 +145,8 @@ We use the following open source projects to help us build a better development Thanks a lot to the community for providing such powerful and simple libraries, so that we can focus more on the implementation of the product logic, and we hope that in the future our projects will also provide a more easy-to-use knowledge base for everyone. +You enjoy AFFiNE? You are not alone. [Some amazing companies](./docs/Jobs.md) are looking for developers with some AFFiNE and/or AFFiNE technology experience. + # Contributors diff --git a/docs/Jobs.md b/docs/Jobs.md new file mode 100644 index 0000000000..0e5a70fdf7 --- /dev/null +++ b/docs/Jobs.md @@ -0,0 +1,4 @@ +# Jobs + +- [Full Stack Platform Engineer](./mysc.app.md) @[mysc.app](https://mysc.app/) + Rust · GWST · Remote · Shanghai, China diff --git a/docs/mysc.app.md b/docs/mysc.app.md new file mode 100644 index 0000000000..6fc70e2322 --- /dev/null +++ b/docs/mysc.app.md @@ -0,0 +1,25 @@ +## Full Stack Platform Engineer + +### Your responsibilities will include + +- Build APIs in the Data Platform to support new capabilities within mysc. +- Work with backend and client side databases (MongoDB, Redis, SQLite) +- Design and implement algorithms that are highly performant, resilient against failures and race conditions and are easy to use by application developers +- Build up solid knowledge of our product to understand end to end system behaviour and data flow +- Execute performance profiling on existing systems to understand key bottlenecks and improve on their performance characteristics + +### What we're looking for + +- You have strong analytical thinking, planning, and problem-solving skills +- You have 3-5 years experience in building APIs or Platforms +- You have strong computer science fundamentals, including knowledge of data structures, algorithmic complexity, and designing for performance and scalability +- You have experience in NodeJS, Typescript and Go +- You have experience working with JWST +- You have experience with unit / automated testing + +### What we offer + +- A fully remote team based on Gather Town +- A culture that encourages different opinions, respects different values and advocates work life balance +- Real ownership and actual impact +- Learning and career opportunities on the long run From 8e1b260ec37bfcff6946f543201fb194b2cb0822 Mon Sep 17 00:00:00 2001 From: Chi Zhang Date: Tue, 13 Sep 2022 22:20:37 +0800 Subject: [PATCH 10/14] Update Jobs.md --- docs/Jobs.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/Jobs.md b/docs/Jobs.md index 0e5a70fdf7..ccbf350f75 100644 --- a/docs/Jobs.md +++ b/docs/Jobs.md @@ -1,4 +1,5 @@ # Jobs - [Full Stack Platform Engineer](./mysc.app.md) @[mysc.app](https://mysc.app/) + Rust · GWST · Remote · Shanghai, China From d8ee771ea07faee6647a1c4b46c295dc0e30eabe Mon Sep 17 00:00:00 2001 From: Chi Zhang Date: Tue, 13 Sep 2022 22:35:12 +0800 Subject: [PATCH 11/14] Update Jobs.md --- docs/Jobs.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/Jobs.md b/docs/Jobs.md index ccbf350f75..711d8ab5d1 100644 --- a/docs/Jobs.md +++ b/docs/Jobs.md @@ -1,5 +1,5 @@ # Jobs - [Full Stack Platform Engineer](./mysc.app.md) @[mysc.app](https://mysc.app/) - + Rust · GWST · Remote · Shanghai, China From 3ff0dbe781d7dcede315a02671e2d40bad1ad9b5 Mon Sep 17 00:00:00 2001 From: DarkSky Date: Wed, 14 Sep 2022 17:32:08 +0800 Subject: [PATCH 12/14] feat: sync with keck --- apps/keck/src/utils.ts.bak | 139 ++++++++++++++++++ apps/ligo-virgo/proxy.conf.json | 7 +- .../db-service/src/services/database/index.ts | 4 +- libs/datasource/jwt-rpc/src/keckprovider.ts | 115 +++++++++++++++ libs/datasource/jwt-rpc/src/processor.ts | 28 +++- libs/datasource/jwt-rpc/src/websocket.ts | 38 +++-- .../src/{provider.ts => wsprovider.ts} | 4 +- libs/datasource/jwt/src/types/index.ts | 2 +- libs/datasource/jwt/src/yjs/index.ts | 14 +- libs/datasource/jwt/src/yjs/provider.ts | 33 +++++ package.json | 1 + 11 files changed, 350 insertions(+), 35 deletions(-) create mode 100644 apps/keck/src/utils.ts.bak create mode 100644 libs/datasource/jwt-rpc/src/keckprovider.ts rename libs/datasource/jwt-rpc/src/{provider.ts => wsprovider.ts} (96%) diff --git a/apps/keck/src/utils.ts.bak b/apps/keck/src/utils.ts.bak new file mode 100644 index 0000000000..ad8d5840b4 --- /dev/null +++ b/apps/keck/src/utils.ts.bak @@ -0,0 +1,139 @@ +import WebSocket = require('ws'); +import http = require('http'); +import Y = require('yjs'); +import lib0 = require('lib0'); +import syncProtocol = require('y-protocols/sync'); + +const { encoding, decoding, map } = lib0; + +const wsReadyStateConnecting = 0; +const wsReadyStateOpen = 1; + +// disable gc when using snapshots! +const gcEnabled = process.env.GC !== 'false' && process.env.GC !== '0'; + +const docs: Map = new Map(); + +const messageSync = 0; + +const updateHandler = (update: Uint8Array, origin: any, doc: WSSharedDoc) => { + const encoder = encoding.createEncoder(); + encoding.writeVarUint(encoder, messageSync); + syncProtocol.writeUpdate(encoder, update); + const message = encoding.toUint8Array(encoder); + doc.conns.forEach((_, conn) => send(doc, conn, message)); +}; +export class WSSharedDoc extends Y.Doc { + name: string; + conns: Map; + + constructor(name: string) { + super({ gc: gcEnabled }); + this.name = name; + // Maps from conn to set of controlled user ids. Delete all user ids from awareness when this conn is closed + this.conns = new Map(); + + this.on('update', updateHandler); + } +} + +// Gets a Y.Doc by name, whether in memory or on disk +const getYDoc = (docname: string, gc = true): WSSharedDoc => + map.setIfUndefined(docs, docname, () => { + const doc = new WSSharedDoc(docname); + doc.gc = gc; + docs.set(docname, doc); + return doc; + }); + +const closeConn = (doc: WSSharedDoc, conn: any) => { + if (doc.conns.has(conn)) { + doc.conns.delete(conn); + } + conn.close(); +}; + +const send = (doc: WSSharedDoc, conn: any, m: Uint8Array) => { + if ( + conn.readyState !== wsReadyStateConnecting && + conn.readyState !== wsReadyStateOpen + ) { + closeConn(doc, conn); + } + try { + conn.send(m, (/** @param {any} err */ err: any) => { + err != null && closeConn(doc, conn); + }); + } catch (e) { + closeConn(doc, conn); + } +}; + +export const handleConnection = ( + socket: WebSocket.WebSocket, + request: http.IncomingMessage, + docName: string +) => { + const gc = true; + socket.binaryType = 'arraybuffer'; + // get doc, initialize if it does not exist yet + const doc = getYDoc(docName, gc); + doc.conns.set(socket, new Set()); + // listen and reply to events + socket.on('message', (message: ArrayBuffer) => { + try { + const encoder = encoding.createEncoder(); + const decoder = decoding.createDecoder(new Uint8Array(message)); + const messageType = decoding.readVarUint(decoder); + switch (messageType) { + case messageSync: + encoding.writeVarUint(encoder, messageSync); + syncProtocol.readSyncMessage(decoder, encoder, doc, null); + if (encoding.length(encoder) > 1) { + send(doc, socket, encoding.toUint8Array(encoder)); + } + break; + } + } catch (err) { + console.error(err); + doc.emit('error', [err]); + } + }); + + // Check if connection is still alive + let pongReceived = true; + const pingInterval = setInterval(() => { + if (!pongReceived) { + if (doc.conns.has(socket)) { + closeConn(doc, socket); + } + clearInterval(pingInterval); + } else if (doc.conns.has(socket)) { + pongReceived = false; + try { + socket.ping(); + } catch (e) { + closeConn(doc, socket); + clearInterval(pingInterval); + } + } + }, 30 * 1000); + socket.on('close', () => { + closeConn(doc, socket); + clearInterval(pingInterval); + }); + socket.on('pong', () => { + pongReceived = true; + }); + // put the following in a variables in a block so the interval handlers don't keep in in + // scope + { + // send sync step 1 + const encoder = encoding.createEncoder(); + encoding.writeVarUint(encoder, messageSync); + console.log('sync step 0', encoding.toUint8Array(encoder)); + syncProtocol.writeSyncStep1(encoder, doc); + send(doc, socket, encoding.toUint8Array(encoder)); + console.log('sync step 1 sent', encoding.toUint8Array(encoder)); + } +}; diff --git a/apps/ligo-virgo/proxy.conf.json b/apps/ligo-virgo/proxy.conf.json index 8dc733b877..a225b3f59b 100644 --- a/apps/ligo-virgo/proxy.conf.json +++ b/apps/ligo-virgo/proxy.conf.json @@ -1,11 +1,6 @@ { - "/api": { - "target": "https://nightly.affine.pro/", - "secure": false, - "changeOrigin": true - }, "/collaboration": { - "target": "https://canary.affine.pro", + "target": "http://127.0.0.1:3000/", "ws": true, "changeOrigin": true, "secure": false diff --git a/libs/datasource/db-service/src/services/database/index.ts b/libs/datasource/db-service/src/services/database/index.ts index 9586732807..a840b0a02b 100644 --- a/libs/datasource/db-service/src/services/database/index.ts +++ b/libs/datasource/db-service/src/services/database/index.ts @@ -33,13 +33,15 @@ async function _getCurrentToken() { if (user) resolve(user.getIdToken()); }); }); + } else if (process.env['NX_KECK']) { + return 'AFFiNE'; } return undefined; } const _enabled = { demo: [], - AFFiNE: process.env['NX_KECK'] ? ['idb'] : ['idb', 'ws'], + AFFiNE: process.env['NX_KECK'] ? ['idb', 'ws'] : ['idb'], } as any; async function _getBlockDatabase( diff --git a/libs/datasource/jwt-rpc/src/keckprovider.ts b/libs/datasource/jwt-rpc/src/keckprovider.ts new file mode 100644 index 0000000000..6e6654a16a --- /dev/null +++ b/libs/datasource/jwt-rpc/src/keckprovider.ts @@ -0,0 +1,115 @@ +import * as Y from 'yjs'; + +import { Observable } from 'lib0/observable'; +import * as url from 'lib0/url'; + +import { handler } from './handler'; +import { registerKeckUpdateHandler } from './processor'; +import { registerWebsocket } from './websocket'; + +/** + * Websocket Provider for Yjs. Creates a websocket connection to sync the shared document. + * The document name is attached to the provided url. I.e. the following example + * creates a websocket connection to http://localhost:3000/my-document-name + * + * @example + * import * as Y from 'yjs' + * import { KeckProvider } from 'jwt-rpc' + * const doc = new Y.Doc() + * const provider = new KeckProvider('http://localhost:3000', 'my-document-name', doc) + */ +export class KeckProvider extends Observable { + maxBackOffTime: number; + url: string; + roomName: string; + + doc: Y.Doc; + + wsUnsuccessfulReconnects: number; + private _synced: boolean; + + broadcastChannel: string; + private _broadcast?: { + broadcastMessage: (buf: ArrayBuffer) => void; + disconnect: () => void; + }; + + private _websocket?: { + broadcastMessage: (buf: ArrayBuffer) => void; + disconnect: () => void; + }; + + private _updateHandlerDestroy: () => void; + + constructor( + token: string, + serverUrl: string, + roomName: string, + doc: Y.Doc, + { params = {}, resyncInterval = -1, maxBackOffTime = 2500 } = {} + ) { + super(); + + this.roomName = roomName; + // ensure that url is always ends with / + while (serverUrl[serverUrl.length - 1] === '/') { + serverUrl = serverUrl.slice(0, serverUrl.length - 1); + } + this.broadcastChannel = serverUrl + '/' + roomName + '/'; + const encodedParams = url.encodeQueryParams(params); + this.url = + this.broadcastChannel + + (encodedParams.length === 0 ? '' : '?' + encodedParams); + + this.doc = doc; + + this.maxBackOffTime = maxBackOffTime; + this.wsUnsuccessfulReconnects = 0; + + this._synced = false; + + this._websocket = registerWebsocket(this, token, resyncInterval); + + this._updateHandlerDestroy = registerKeckUpdateHandler( + this, + doc, + buf => { + this._websocket?.broadcastMessage(buf); + this._broadcast?.broadcastMessage(buf); + } + ); + } + + get messageHandlers() { + return handler; + } + + get synced() { + return this._synced; + } + + set synced(state) { + if (this._synced !== state) { + this._synced = state; + this.emit('synced', [state]); + this.emit('sync', [state]); + } + } + + override destroy() { + if (this._broadcast) { + const disconnect = this._broadcast.disconnect; + this._broadcast = undefined; + disconnect(); + } + + if (this._websocket) { + const disconnect = this._websocket.disconnect; + this._websocket = undefined; + disconnect(); + } + + this._updateHandlerDestroy?.(); + super.destroy(); + } +} diff --git a/libs/datasource/jwt-rpc/src/processor.ts b/libs/datasource/jwt-rpc/src/processor.ts index c7097156b8..f8283b8c4b 100644 --- a/libs/datasource/jwt-rpc/src/processor.ts +++ b/libs/datasource/jwt-rpc/src/processor.ts @@ -5,10 +5,11 @@ import * as syncProtocol from 'y-protocols/sync'; import * as Y from 'yjs'; import { Message } from './handler'; -import { WebsocketProvider } from './provider'; +import { KeckProvider } from './keckprovider'; +import { WebsocketProvider } from './wsprovider'; export const readMessage = ( - provider: WebsocketProvider, + provider: WebsocketProvider | KeckProvider, buf: Uint8Array, emitSynced: boolean ): encoding.Encoder => { @@ -24,7 +25,7 @@ export const readMessage = ( return encoder; }; -export const registerUpdateHandler = ( +export const registerWsUpdateHandler = ( provider: WebsocketProvider, awareness: awarenessProtocol.Awareness, doc: Y.Doc, @@ -78,3 +79,24 @@ export const registerUpdateHandler = ( doc.off('update', documentUpdateHandler); }; }; + +export const registerKeckUpdateHandler = ( + provider: KeckProvider, + doc: Y.Doc, + broadcastMessage: (buf: ArrayBuffer) => void +) => { + // Listens to Yjs updates and sends them to remote peers (ws and broadcastchannel) + const documentUpdateHandler = (update: Uint8Array, origin: any) => { + if (origin !== provider) { + const encoder = encoding.createEncoder(); + encoding.writeVarUint(encoder, Message.sync); + syncProtocol.writeUpdate(encoder, update); + broadcastMessage(encoding.toUint8Array(encoder)); + } + }; + + doc.on('update', documentUpdateHandler); + return () => { + doc.off('update', documentUpdateHandler); + }; +}; diff --git a/libs/datasource/jwt-rpc/src/websocket.ts b/libs/datasource/jwt-rpc/src/websocket.ts index 4395cf5d53..57cf930856 100644 --- a/libs/datasource/jwt-rpc/src/websocket.ts +++ b/libs/datasource/jwt-rpc/src/websocket.ts @@ -5,8 +5,9 @@ import * as awarenessProtocol from 'y-protocols/awareness'; import * as syncProtocol from 'y-protocols/sync'; import { Message } from './handler'; +import { KeckProvider } from './keckprovider'; import { readMessage } from './processor'; -import { WebsocketProvider } from './provider'; +import { WebsocketProvider } from './wsprovider'; enum WebSocketState { disconnected = 0, @@ -46,14 +47,14 @@ const _getToken = async ( return resp.json(); }; -const _getTimeout = (provider: WebsocketProvider) => +const _getTimeout = (provider: WebsocketProvider | KeckProvider) => math.min( math.pow(2, provider.wsUnsuccessfulReconnects) * 100, provider.maxBackOffTime ); export const registerWebsocket = ( - provider: WebsocketProvider, + provider: WebsocketProvider | KeckProvider, token: string, resync = -1, reconnect = 3, @@ -105,13 +106,19 @@ export const registerWebsocket = ( state = WebSocketState.disconnected; provider.synced = false; // update awareness (all users except local left) - awarenessProtocol.removeAwarenessStates( - provider.awareness, - Array.from( - provider.awareness.getStates().keys() - ).filter(client => client !== provider.doc.clientID), - provider - ); + + const awareness = (provider as any)['awareness']; + if (awareness) { + awarenessProtocol.removeAwarenessStates( + awareness, + Array.from(awareness.getStates().keys()).filter( + (client): client is number => + client !== provider.doc.clientID + ), + provider + ); + } + provider.emit('status', [{ status: 'disconnected' }]); } else { provider.wsUnsuccessfulReconnects++; @@ -139,8 +146,10 @@ export const registerWebsocket = ( encoding.writeVarUint(encoder, Message.sync); syncProtocol.writeSyncStep1(encoder, provider.doc); websocket?.send(encoding.toUint8Array(encoder)); + + const awareness = (provider as any)['awareness']; // broadcast local awareness state - if (provider.awareness.getLocalState() !== null) { + if (awareness && awareness.getLocalState() !== null) { const encoderAwarenessState = encoding.createEncoder(); encoding.writeVarUint( encoderAwarenessState, @@ -148,10 +157,9 @@ export const registerWebsocket = ( ); encoding.writeVarUint8Array( encoderAwarenessState, - awarenessProtocol.encodeAwarenessUpdate( - provider.awareness, - [provider.doc.clientID] - ) + awarenessProtocol.encodeAwarenessUpdate(awareness, [ + provider.doc.clientID, + ]) ); websocket?.send( encoding.toUint8Array(encoderAwarenessState) diff --git a/libs/datasource/jwt-rpc/src/provider.ts b/libs/datasource/jwt-rpc/src/wsprovider.ts similarity index 96% rename from libs/datasource/jwt-rpc/src/provider.ts rename to libs/datasource/jwt-rpc/src/wsprovider.ts index 0ce7f6a1b3..c3d9821515 100644 --- a/libs/datasource/jwt-rpc/src/provider.ts +++ b/libs/datasource/jwt-rpc/src/wsprovider.ts @@ -6,7 +6,7 @@ import * as url from 'lib0/url'; import * as awarenessProtocol from 'y-protocols/awareness'; import { handler } from './handler'; -import { registerUpdateHandler } from './processor'; +import { registerWsUpdateHandler } from './processor'; import { registerWebsocket } from './websocket'; /** @@ -85,7 +85,7 @@ export class WebsocketProvider extends Observable { // this.doc // ); - this._updateHandlerDestroy = registerUpdateHandler( + this._updateHandlerDestroy = registerWsUpdateHandler( this, awareness, doc, diff --git a/libs/datasource/jwt/src/types/index.ts b/libs/datasource/jwt/src/types/index.ts index a205f2a685..7e54afc6bf 100644 --- a/libs/datasource/jwt/src/types/index.ts +++ b/libs/datasource/jwt/src/types/index.ts @@ -17,7 +17,7 @@ function getCollaborationPoint() { const { protocol, host } = getLocation(); const ws = protocol.startsWith('https') ? 'wss' : 'ws'; const isOnline = host.endsWith('affine.pro'); - const site = isOnline ? host : 'localhost:4200'; + const site = isOnline ? host : 'localhost:3000'; return `${ws}://${site}/collaboration/`; } diff --git a/libs/datasource/jwt/src/yjs/index.ts b/libs/datasource/jwt/src/yjs/index.ts index d430d29083..7f270ad2f9 100644 --- a/libs/datasource/jwt/src/yjs/index.ts +++ b/libs/datasource/jwt/src/yjs/index.ts @@ -120,13 +120,13 @@ async function _initYjsDatabase( [name]: p, }; }), - p({ - awareness, - doc: binaries, - token, - workspace: `${workspace}_binaries`, - emitState, - }).then(p => ({ [`${name}_binaries`]: p })), + // p({ + // awareness, + // doc: binaries, + // token, + // workspace: `${workspace}_binaries`, + // emitState, + // }).then(p => ({ [`${name}_binaries`]: p })), ]) ); } diff --git a/libs/datasource/jwt/src/yjs/provider.ts b/libs/datasource/jwt/src/yjs/provider.ts index 0dab4244c0..125655e718 100644 --- a/libs/datasource/jwt/src/yjs/provider.ts +++ b/libs/datasource/jwt/src/yjs/provider.ts @@ -79,6 +79,39 @@ export const getYjsProviders = ( } ) as any; // TODO: type is erased after cascading references + // Wait for ws synchronization to complete, otherwise the data will be modified in reverse, which can be optimized later + return new Promise((resolve, reject) => { + // TODO: synced will also be triggered on reconnection after losing sync + // There needs to be an event mechanism to emit the synchronization state to the upper layer + ws.once('synced', () => resolve()); + ws.once('lost-connection', () => resolve()); + ws.once('connection-error', () => reject()); + ws.on('synced', () => instances.emitState('connected')); + ws.on('lost-connection', () => + instances.emitState('retry') + ); + ws.on('connection-error', () => + instances.emitState('retry') + ); + }); + } else { + return; + } + } + }, + keck: async (instances: YjsDefaultInstances) => { + if (options.enabled.includes('ws')) { + if (instances.token) { + const ws = new WebsocketProvider( + instances.token, + options.backend, + instances.workspace, + instances.doc, + { + params: options.params, + } + ) as any; // TODO: type is erased after cascading references + // Wait for ws synchronization to complete, otherwise the data will be modified in reverse, which can be optimized later return new Promise((resolve, reject) => { // TODO: synced will also be triggered on reconnection after losing sync diff --git a/package.json b/package.json index c845578508..8f939e4b2b 100644 --- a/package.json +++ b/package.json @@ -9,6 +9,7 @@ "start": "env-cmd -f .github/env/.env.local-dev nx serve ligo-virgo", "start:affine": "nx serve ligo-virgo", "start:keck": "nx serve keck", + "start:keck-dev": "env-cmd -f .github/env/.env.local-keck nx serve ligo-virgo", "start:venus": "nx serve venus", "build": "nx build ligo-virgo", "build:local": "env-cmd -f .github/env/.env.local-dev nx build ligo-virgo", From f8544fedd4435197eb1c5fcd6f70eff1552dd1ed Mon Sep 17 00:00:00 2001 From: DarkSky Date: Wed, 14 Sep 2022 17:39:24 +0800 Subject: [PATCH 13/14] feat: sync to keck --- .../db-service/src/services/database/index.ts | 2 +- libs/datasource/jwt-rpc/src/handler.ts | 35 +++++++++++-------- libs/datasource/jwt-rpc/src/index.ts | 3 +- libs/datasource/jwt/src/yjs/index.ts | 14 ++++---- libs/datasource/jwt/src/yjs/provider.ts | 7 ++-- 5 files changed, 34 insertions(+), 27 deletions(-) diff --git a/libs/datasource/db-service/src/services/database/index.ts b/libs/datasource/db-service/src/services/database/index.ts index a840b0a02b..8bee9f6028 100644 --- a/libs/datasource/db-service/src/services/database/index.ts +++ b/libs/datasource/db-service/src/services/database/index.ts @@ -41,7 +41,7 @@ async function _getCurrentToken() { const _enabled = { demo: [], - AFFiNE: process.env['NX_KECK'] ? ['idb', 'ws'] : ['idb'], + AFFiNE: process.env['NX_KECK'] ? ['idb', 'keck'] : ['idb'], } as any; async function _getBlockDatabase( diff --git a/libs/datasource/jwt-rpc/src/handler.ts b/libs/datasource/jwt-rpc/src/handler.ts index e803fb4bab..1f6e9da4af 100644 --- a/libs/datasource/jwt-rpc/src/handler.ts +++ b/libs/datasource/jwt-rpc/src/handler.ts @@ -4,7 +4,8 @@ import * as authProtocol from 'y-protocols/auth'; import * as awarenessProtocol from 'y-protocols/awareness'; import * as syncProtocol from 'y-protocols/sync'; -import { WebsocketProvider } from './provider'; +import { KeckProvider } from './keckprovider'; +import { WebsocketProvider } from './wsprovider'; const permissionDeniedHandler = (provider: WebsocketProvider, reason: string) => console.warn(`Permission denied to access ${provider.url}.\n${reason}`); @@ -19,7 +20,7 @@ export enum Message { export type MessageCallback = ( encoder: encoding.Encoder, decoder: decoding.Decoder, - provider: WebsocketProvider, + provider: WebsocketProvider | KeckProvider, emitSynced: boolean, messageType: number ) => void; @@ -48,14 +49,16 @@ export const handler: Record = { emitSynced, messageType ) => { - encoding.writeVarUint(encoder, Message.queryAwareness); - encoding.writeVarUint8Array( - encoder, - awarenessProtocol.encodeAwarenessUpdate( - provider.awareness, - Array.from(provider.awareness.getStates().keys()) - ) - ); + if (provider instanceof WebsocketProvider) { + encoding.writeVarUint(encoder, Message.queryAwareness); + encoding.writeVarUint8Array( + encoder, + awarenessProtocol.encodeAwarenessUpdate( + provider.awareness, + Array.from(provider.awareness.getStates().keys()) + ) + ); + } }, [Message.awareness]: ( @@ -65,11 +68,13 @@ export const handler: Record = { emitSynced, messageType ) => { - awarenessProtocol.applyAwarenessUpdate( - provider.awareness, - decoding.readVarUint8Array(decoder), - provider - ); + if (provider instanceof WebsocketProvider) { + awarenessProtocol.applyAwarenessUpdate( + provider.awareness, + decoding.readVarUint8Array(decoder), + provider + ); + } }, [Message.auth]: (encoder, decoder, provider, emitSynced, messageType) => { diff --git a/libs/datasource/jwt-rpc/src/index.ts b/libs/datasource/jwt-rpc/src/index.ts index 8fda0de311..89d05b95b7 100644 --- a/libs/datasource/jwt-rpc/src/index.ts +++ b/libs/datasource/jwt-rpc/src/index.ts @@ -1,3 +1,4 @@ export { IndexedDBProvider } from './indexeddb'; -export { WebsocketProvider } from './provider'; +export { KeckProvider } from './keckprovider'; export { SQLiteProvider } from './sqlite'; +export { WebsocketProvider } from './wsprovider'; diff --git a/libs/datasource/jwt/src/yjs/index.ts b/libs/datasource/jwt/src/yjs/index.ts index 7f270ad2f9..d430d29083 100644 --- a/libs/datasource/jwt/src/yjs/index.ts +++ b/libs/datasource/jwt/src/yjs/index.ts @@ -120,13 +120,13 @@ async function _initYjsDatabase( [name]: p, }; }), - // p({ - // awareness, - // doc: binaries, - // token, - // workspace: `${workspace}_binaries`, - // emitState, - // }).then(p => ({ [`${name}_binaries`]: p })), + p({ + awareness, + doc: binaries, + token, + workspace: `${workspace}_binaries`, + emitState, + }).then(p => ({ [`${name}_binaries`]: p })), ]) ); } diff --git a/libs/datasource/jwt/src/yjs/provider.ts b/libs/datasource/jwt/src/yjs/provider.ts index 125655e718..ad7df930e5 100644 --- a/libs/datasource/jwt/src/yjs/provider.ts +++ b/libs/datasource/jwt/src/yjs/provider.ts @@ -3,6 +3,7 @@ import { Doc } from 'yjs'; import { IndexedDBProvider, + KeckProvider, SQLiteProvider, WebsocketProvider, } from '@toeverything/datasource/jwt-rpc'; @@ -22,7 +23,7 @@ export type YjsProvider = ( instances: YjsDefaultInstances ) => Promise; -type ProviderType = 'idb' | 'sqlite' | 'ws'; +type ProviderType = 'idb' | 'sqlite' | 'ws' | 'keck'; export type YjsProviderOptions = { enabled: ProviderType[]; @@ -100,9 +101,9 @@ export const getYjsProviders = ( } }, keck: async (instances: YjsDefaultInstances) => { - if (options.enabled.includes('ws')) { + if (options.enabled.includes('keck')) { if (instances.token) { - const ws = new WebsocketProvider( + const ws = new KeckProvider( instances.token, options.backend, instances.workspace, From bf45591e6c357a06e48448690cbb4810da7ff4a5 Mon Sep 17 00:00:00 2001 From: DarkSky Date: Wed, 14 Sep 2022 17:41:50 +0800 Subject: [PATCH 14/14] fix: ci --- .github/workflows/{affine-keck.yml.bak => affine-keck.yml} | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) rename .github/workflows/{affine-keck.yml.bak => affine-keck.yml} (97%) diff --git a/.github/workflows/affine-keck.yml.bak b/.github/workflows/affine-keck.yml similarity index 97% rename from .github/workflows/affine-keck.yml.bak rename to .github/workflows/affine-keck.yml index 7caf6cadbe..926cd8825f 100644 --- a/.github/workflows/affine-keck.yml.bak +++ b/.github/workflows/affine-keck.yml @@ -1,10 +1,7 @@ name: Build AFFiNE-Local-Keck on: - push: - branches: [master] - # pull_request: - # branches: [master] + workflow_dispatch: # Cancels all previous workflow runs for pull requests that have not completed. # See https://docs.github.com/en/actions/using-jobs/using-concurrency