diff --git a/.eslintrc.yml b/.eslintrc.yml deleted file mode 100644 index 4f4b4fbd..00000000 --- a/.eslintrc.yml +++ /dev/null @@ -1,29 +0,0 @@ -env: - browser: true - es2021: true -extends: - - plugin:react/recommended - - standard-with-typescript - - prettier -overrides: [] -parserOptions: - ecmaVersion: latest - project: - - tsconfig.json - sourceType: module -plugins: - - react -rules: - '@typescript-eslint/no-dynamic-delete': ['off'] - '@typescript-eslint/no-floating-promises': ['off'] - '@typescript-eslint/no-misused-promises': ['off'] - '@typescript-eslint/no-non-null-assertion': ['off'] - '@typescript-eslint/no-unused-vars': ['warn'] - '@typescript-eslint/prefer-nullish-coalescing': ['off'] - '@typescript-eslint/promise-function-async': ['off'] - '@typescript-eslint/restrict-plus-operands': ['off'] - '@typescript-eslint/restrict-template-expressions': ['off'] - '@typescript-eslint/strict-boolean-expressions': ['off'] - '@typescript-eslint/triple-slash-reference': ['off'] - "@typescript-eslint/consistent-type-assertions": ["off"] - 'react/react-in-jsx-scope': ['off'] diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index a72430b7..edb57d14 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -15,6 +15,7 @@ jobs: - name: Install dependencies run: | + npm i corepack@latest -g corepack enable pnpm pnpm i diff --git a/.github/workflows/playwright.yml b/.github/workflows/playwright.yml index 42fa72c2..69356aae 100644 --- a/.github/workflows/playwright.yml +++ b/.github/workflows/playwright.yml @@ -32,18 +32,24 @@ jobs: - name: Install dependencies run: | + npm i corepack@latest -g corepack enable pnpm pnpm i + - name: build project + run: pnpm build + - name: Install Playwright Browsers + working-directory: app run: pnpm exec playwright install --with-deps ${{ matrix.browser }} - name: Run Playwright tests + working-directory: app run: pnpm exec playwright test --project=${{ matrix.project }} - uses: actions/upload-artifact@v4 if: always() with: name: playwright-report-${{ matrix.project }} - path: playwright-report/ + path: app/playwright-report/ retention-days: 30 diff --git a/.gitignore b/.gitignore index 1a26e0a5..3730ee3f 100644 --- a/.gitignore +++ b/.gitignore @@ -29,5 +29,6 @@ package-lock.yaml .wrangler test-results playwright-report +storybook-static dev-dist diff --git a/.storybook/preview.tsx b/.storybook/preview.tsx deleted file mode 100644 index 4a0aee95..00000000 --- a/.storybook/preview.tsx +++ /dev/null @@ -1,70 +0,0 @@ -import React from 'react' -import { Preview } from '@storybook/react' -import { Themes, loadConcurrentTheme } from '../src/themes' -import { Box, CssBaseline, Paper, ThemeProvider, Typography } from '@mui/material'; - -const themeNames = Object.keys(Themes) - -const preview: Preview = { - parameters: { - layout: 'fullscreen', - }, - globalTypes: { - theme: { - description: 'Global theme for components', - defaultValue: 'All', - toolbar: { - title: 'Theme', - icon: 'circlehollow', - dynamicTitle: true, - items: ['All', ...themeNames], - } - } - }, - decorators: [ - (StoryFn, context) => { - const themeName = context.globals.theme; - const previewTarget = Array() - if (themeName === 'All') { - themeNames.forEach((themeName) => { - previewTarget.push(loadConcurrentTheme(themeName, {})) - }) - } else { - previewTarget.push(loadConcurrentTheme(themeName, {})) - } - return ( - - {previewTarget.map((theme) => ( - - - - - {theme.meta.name} - - - - - ))} - - ); - } - ] -} - -export default preview diff --git a/.storybook/main.ts b/app/.storybook/main.ts similarity index 69% rename from .storybook/main.ts rename to app/.storybook/main.ts index 0752d2c6..9413e554 100644 --- a/.storybook/main.ts +++ b/app/.storybook/main.ts @@ -1,13 +1,11 @@ import type { StorybookConfig } from '@storybook/react-vite' const config: StorybookConfig = { - stories: ['../src/**/*.stories.ts', "../src/**/*.stories.tsx"], + stories: ['../src/**/*.stories.ts', '../src/**/*.stories.tsx'], addons: ['@storybook/addon-essentials'], framework: { name: '@storybook/react-vite', options: {} }, - docs: { - autodocs: 'tag' - } + docs: {} } export default config diff --git a/.storybook/preview-head.html b/app/.storybook/preview-head.html similarity index 100% rename from .storybook/preview-head.html rename to app/.storybook/preview-head.html diff --git a/app/.storybook/preview.tsx b/app/.storybook/preview.tsx new file mode 100644 index 00000000..1d8cafa8 --- /dev/null +++ b/app/.storybook/preview.tsx @@ -0,0 +1,70 @@ +import React from 'react' +import { Preview } from '@storybook/react' +import { Themes, loadConcurrentTheme } from '../src/themes' +import { Box, CssBaseline, Paper, ThemeProvider, Typography } from '@mui/material' + +const themeNames = Object.keys(Themes) + +const preview: Preview = { + parameters: { + layout: 'fullscreen' + }, + globalTypes: { + theme: { + description: 'Global theme for components', + defaultValue: 'All', + toolbar: { + title: 'Theme', + icon: 'circlehollow', + dynamicTitle: true, + items: ['All', ...themeNames] + } + } + }, + decorators: [ + (StoryFn, context) => { + const themeName = context.globals.theme + const previewTarget = Array() + if (themeName === 'All') { + themeNames.forEach((themeName) => { + previewTarget.push(loadConcurrentTheme(themeName, {})) + }) + } else { + previewTarget.push(loadConcurrentTheme(themeName, {})) + } + return ( + + {previewTarget.map((theme) => ( + + + + + {theme.meta.name} + + + + + ))} + + ) + } + ] +} + +export default preview diff --git a/devpublic/192.png b/app/devpublic/192.png similarity index 100% rename from devpublic/192.png rename to app/devpublic/192.png diff --git a/devpublic/512.png b/app/devpublic/512.png similarity index 100% rename from devpublic/512.png rename to app/devpublic/512.png diff --git a/devpublic/apple-touch-icon.png b/app/devpublic/apple-touch-icon.png similarity index 100% rename from devpublic/apple-touch-icon.png rename to app/devpublic/apple-touch-icon.png diff --git a/devpublic/favicon.ico b/app/devpublic/favicon.ico similarity index 100% rename from devpublic/favicon.ico rename to app/devpublic/favicon.ico diff --git a/devpublic/robots.txt b/app/devpublic/robots.txt similarity index 100% rename from devpublic/robots.txt rename to app/devpublic/robots.txt diff --git a/devpublic/splash.png b/app/devpublic/splash.png similarity index 100% rename from devpublic/splash.png rename to app/devpublic/splash.png diff --git a/app/dist b/app/dist new file mode 120000 index 00000000..85d8c32f --- /dev/null +++ b/app/dist @@ -0,0 +1 @@ +../dist \ No newline at end of file diff --git a/index.html b/app/index.html similarity index 55% rename from index.html rename to app/index.html index 7a0c2cbb..1fdf6656 100644 --- a/index.html +++ b/app/index.html @@ -1,4 +1,4 @@ - + diff --git a/app/package.json b/app/package.json new file mode 100644 index 00000000..099ae619 --- /dev/null +++ b/app/package.json @@ -0,0 +1,108 @@ +{ + "name": "concrnt-world", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite dev", + "build": "pnpm prebuild && tsc && vite build && pnpm postbuild", + "postbuild": "touch dist/.gitkeep", + "prebuild": "./prebuild.sh", + "storybook": "storybook dev -p 6006", + "build-storybook": "storybook build", + "test": "playwright test --project=chromium", + "test-all": "playwright test" + }, + "dependencies": { + "@aws-sdk/client-s3": "^3.592.0", + "@aws-sdk/s3-request-presigner": "^3.592.0", + "@concrnt/worldlib": "workspace:*", + "@cosmjs/encoding": "^0.32.4", + "@cosmjs/proto-signing": "^0.32.4", + "@cosmjs/stargate": "^0.32.4", + "@emotion/react": "^11.14.0", + "@emotion/styled": "^11.14.0", + "@fortawesome/free-brands-svg-icons": "^6.7.2", + "@fortawesome/react-fontawesome": "^0.2.2", + "@google/model-viewer": "^4.0.0", + "@mui/icons-material": "^6.4.3", + "@mui/material": "^6.4.3", + "@rjsf/core": "^5.24.3", + "@rjsf/mui": "^5.24.3", + "@rjsf/utils": "^5.24.3", + "@rjsf/validator-ajv8": "^5.24.3", + "@zxing/browser": "^0.1.4", + "@zxing/library": "^0.20.0", + "blurhash": "^2.0.5", + "boring-avatars": "github:totegamma/boring-avatars#49ed345027665e1d6135ae6b0b6f2a442ff52915", + "cosmjs-types": "^0.9.0", + "elliptic": "^6.5.4", + "ethers": "^6.3.0", + "fuzzysort": "^2.0.4", + "html2canvas": "^1.4.1", + "i18next": "^23.5.1", + "i18next-browser-languagedetector": "^7.1.0", + "i18next-http-backend": "^2.2.2", + "json-schema-to-typescript": "^13.0.1", + "jspdf": "^2.5.1", + "notistack": "^3.0.1", + "protobufjs": "^7.3.2", + "react": "^18.2.0", + "react-blurhash": "^0.3.0", + "react-colorful": "^5.6.1", + "react-dom": "^18.2.0", + "react-error-boundary": "^4.0.10", + "react-helmet-async": "^2.0.5", + "react-i18next": "^13.2.2", + "react-icons": "^5.2.1", + "react-markdown": "^8.0.7", + "react-parallax-tilt": "^1.7.146", + "react-qr-code": "^2.0.12", + "react-router-dom": "^6.11.0", + "react-syntax-highlighter": "^15.5.0", + "react-use-websocket": "^4.3.1", + "react-zoom-pan-pinch": "^3.4.2", + "rehype-raw": "^6.1.1", + "rehype-sanitize": "^5.0.1", + "remark-breaks": "^3.0.2", + "textarea-caret": "^3.1.0", + "unist-util-inspect": "^8.0.0", + "unist-util-visit": "^5.0.0", + "unplugin-info": "^1.2.1", + "use-sound": "^4.0.1", + "uuid": "^9.0.0", + "virtua": "^0.33.5", + "vite": "^6.0.11", + "vite-plugin-pwa": "^0.21.1" + }, + "devDependencies": { + "@keplr-wallet/types": "^0.12.114", + "@playwright/test": "^1.46.1", + "@storybook/addon-essentials": "^8.5.3", + "@storybook/react": "^8.5.3", + "@storybook/react-vite": "^8.5.3", + "@types/elliptic": "^6.4.14", + "@types/node-forge": "^1.3.1", + "@types/react": "^18.0.26", + "@types/react-dom": "^18.0.9", + "@types/react-syntax-highlighter": "^15.5.6", + "@types/textarea-caret": "^3.0.1", + "@types/uuid": "^9.0.1", + "@types/ws": "^8.5.4", + "@vitejs/plugin-react-swc": "^3.7.2", + "preset": "link:@storybook/react-vite/preset", + "prop-types": "15.8.1", + "rollup-plugin-visualizer": "^5.14.0", + "storybook": "^8.5.3", + "workbox-precaching": "^7.3.0" + }, + "packageManager": "pnpm@8.6.1", + "volta": { + "node": "22.12.0" + }, + "eslintConfig": { + "extends": [ + "plugin:storybook/recommended" + ] + } +} diff --git a/app/playwright.config.ts b/app/playwright.config.ts new file mode 100644 index 00000000..87c0ff83 --- /dev/null +++ b/app/playwright.config.ts @@ -0,0 +1,78 @@ +import { defineConfig, devices } from '@playwright/test' + +/** + * Read environment variables from file. + * https://github.com/motdotla/dotenv + */ +// import dotenv from 'dotenv'; +// dotenv.config({ path: path.resolve(__dirname, '.env') }); + +/** + * 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:3000', + + /* 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: 'pnpm dev', + url: 'http://127.0.0.1:5173', + reuseExistingServer: !process.env.CI + } +}) diff --git a/prebuild.sh b/app/prebuild.sh similarity index 100% rename from prebuild.sh rename to app/prebuild.sh diff --git a/public/192.png b/app/public/192.png similarity index 100% rename from public/192.png rename to app/public/192.png diff --git a/public/512.png b/app/public/512.png similarity index 100% rename from public/512.png rename to app/public/512.png diff --git a/public/_routes.json b/app/public/_routes.json similarity index 100% rename from public/_routes.json rename to app/public/_routes.json diff --git a/public/apple-touch-icon.png b/app/public/apple-touch-icon.png similarity index 100% rename from public/apple-touch-icon.png rename to app/public/apple-touch-icon.png diff --git a/public/concrnt.png b/app/public/concrnt.png similarity index 100% rename from public/concrnt.png rename to app/public/concrnt.png diff --git a/public/concrnt.svg b/app/public/concrnt.svg similarity index 100% rename from public/concrnt.svg rename to app/public/concrnt.svg diff --git a/public/favicon.ico b/app/public/favicon.ico similarity index 100% rename from public/favicon.ico rename to app/public/favicon.ico diff --git a/public/locales/en/translation.json b/app/public/locales/en/translation.json similarity index 99% rename from public/locales/en/translation.json rename to app/public/locales/en/translation.json index 9c9f9973..43879f08 100644 --- a/public/locales/en/translation.json +++ b/app/public/locales/en/translation.json @@ -157,7 +157,7 @@ "basic": "Basic", "showEditorOnTop": "Show editor on top", "showEditorOnTopMobile": "Show editor on top (mobile)", - "autoSwitchMediaType": "Automatically switch the post type when an image is attached", + "autoSwitchMediaPostType": "Automatically switch the post type when an image is attached", "developerMode": "Developer mode", "generateInviteCode": "Generate invite code", "inviteCode": "Invite code (valid for 24 hours)", @@ -516,5 +516,4 @@ "otherTips": "## Additional Tips\n\n### For Password Manager Users & Frequent Logins\n\nIf you're using a password manager to log in to , it's recommended to use a Sub Key instead of the Master Key.\nThis is because:\n1. Some password managers struggle with handling Japanese characters as passwords.\n2. Logging in with the Master Key creates a new session.\n\nYou can copy your Sub Key from Settings > ID Management while logged in under normal mode. Sub Key login works via the same login screen as the Master Key, with a dedicated input field.\n\n## Other Features\n\n### Sub-Profiles\nConcrnt lets you create multiple profiles. You can choose which profile to use when posting.\n\n### ActivityPub Integration\nBy enabling ActivityPub integration, you can interact with other ActivityPub-compatible platforms (Mastodon, Misskey, Pleroma, etc.).\n\n### Data Management & Migration\nConcrnt is a decentralized SNS, allowing you to manage your own data.\nTo safeguard against server shutdowns, regularly export your data from Settings > Data Management and keep it securely.\nYou can also migrate to a different server if your current one shuts down." } } - } diff --git a/public/locales/ja/translation.json b/app/public/locales/ja/translation.json similarity index 99% rename from public/locales/ja/translation.json rename to app/public/locales/ja/translation.json index 84746961..9dd7306e 100644 --- a/public/locales/ja/translation.json +++ b/app/public/locales/ja/translation.json @@ -358,7 +358,6 @@ "auto1": "おまかせでサーバーを選んで", "auto2": "はじめる", "custom": "自分でサーバーを選ぶ" - }, "chooseDomain": { "title": "ドメインの選択", @@ -502,7 +501,7 @@ "list": { "desc": "# リスト\n\n複数コミュニティをウォッチしていると、タイムラインが雑多になりすぎてしまうことがあります。\nそういう時は、リストを増やして整理しましょう。 メニューの「リスト」からリストの管理画面を開くことができます。\n\n## リストのピン止め\n\nリスト設定から特定のリストをピン止めすることができます。ピン止めをすると、画面上部のタブからいつでもそのリストにアクセスすることができます。\n\n## プリセットとしてのリスト\n\nコンカレントのリストはただまとめるだけではなく、プリセットとして強力に機能します。\nたとえば、複数のリストにそれぞれ別のデフォルト投稿先を設定しておくことで、素早く投稿先を切り替えることができます。\n" }, - + "customize": { "desc": "# 便利な使い方\n\n## アプリ化\nコンカレントはPWAという技術を使って、このwebページをスマートフォンのアプリのように使うことができます。\n\n<説明の画像>\n\n### 通知設定\n\nアプリ化することで、通知を受け取る設定を行うことができます。デフォルトではすべての通知がオフになっていますが、設定>一般から「通知を有効にする」ボタンを押すことで通知を有効にすることができます。また有効にした後、どういうときに通知を受け取るかの詳細を設定することもできます。\n\nまた、通知を有効にできる端末は1つのみです。別の端末で通知を有効にすると、前の端末の通知設定は解除されます。\n\n## カスタマイズ\n\nコンカレントでは、様々なカスタマイズ機能があります。\n### カラーテーマ\nコンカレントのカラーテーマは、設定画面のテーマから変更することができます。また、自分で新しいテーマを作成することも、そしてそのテーマを共有することもできます。\n共有されたテーマはタイムライン上でこのように表示されます。\n```theme\n{\"meta\":{\"name\":\"おりーぶ\",\"author\":\"con1t0tey8uxhkqkd4wcp4hd4jedt7f0vfhk29xdd2\"},\"palette\":{\"primary\":{\"main\":\"#292e24\",\"contrastText\":\"#ffffff\"},\"secondary\":{\"main\":\"#265E2C\"},\"background\":{\"default\":\"#12a129\",\"paper\":\"#fffcfa\",\"contrastText\":\"#292e24\"},\"text\":{\"primary\":\"#292e24\",\"secondary\":\"#265E2C\",\"hint\":\"rgba(41, 46, 36, 0.5)\",\"disabled\":\"rgba(41, 46, 36, 0.5)\"},\"divider\":\"rgba(41, 46, 36, 0.12)\"},\"components\":{\"MuiButton\":{\"defaultProps\":{\"variant\":\"contained\",\"disableElevation\":false}},\"MuiPaper\":{\"defaultProps\":{\"variant\":\"elevation\"}},\"MuiAppBar\":{\"defaultProps\":{\"color\":\"primary\"}}}}\n```\n\nテーマのインストールボタンを押すことで、すぐにそのテーマを適用することができます。\n\n### 絵文字パック\n\nコンカレントでは投稿の本文やリアクションとして絵文字を使うことができますが、この絵文字は絵文字パックをインストールすることで追加することができます。\n\n共有された絵文字パックはタイムライン上でこのように表示されます。実際にクリックして、絵文字パックをインストールしてみましょう。\n\n\n\nまた、少し手間が必要ですが、自分で絵文字パックを作成することもできます。詳細は[こちら](https://square.concrnt.net/general/world/emojipack/)を参照してください。\n" }, @@ -514,9 +513,6 @@ "showTutorial": "メニューにチュートリアルを表示する", "hideTutorial": "メニューからチュートリアルを非表示にする", "otherTips": "## その他Tips\n\n### パスワードマネージャをお使いの方・頻繁にログインする方へ\n\nパスワードマネージャーを使ってコンカレントにログインする場合は、マスターキーではなくサブキーを使う方がオススメです。\nこれは、パスワードマネージャーが日本語をパスワードとして取り扱う際に難があることがある為・マスターキーによるログインは新しいセッションを作成してしまう為です。\n\nサブキーは、通常モードログイン時に、設定/ID管理からコピーして控えることができます。ログインは、マスターキーによるログインと同じ画面にサブキー向けの入力欄あがるので、そこからログイン可能です。\n\n\n## その他の機能\n\n### サブプロフィール\nコンカレントでは、自分のプロフィールを複数作ることができます。投稿する際に、どのプロフィールで投稿するか選ぶことができます。\n\n### Activitypubとの連携\nコンカレントでは、Activitypub連携を有効化することで、ほかのActivitypubに対応したSNS(Mastodon, Misskey, Pleromaなど)と連携することができます。\n\n### データ管理と引っ越し\nコンカレントは分散型SNSです。自分のデータを自分で管理することができます。\nサーバーがいつ消滅してもいいように、定期的に「設定>データ管理」からデータをエクスポートして、自分の手元に保存しておくと安心です。\nまた、自分がいるサーバーがサービス終了するときは、ほかのサーバーに引っ越すことができます。\n\n" - } - - } } diff --git a/public/locales/ko/translation.json b/app/public/locales/ko/translation.json similarity index 100% rename from public/locales/ko/translation.json rename to app/public/locales/ko/translation.json diff --git a/public/locales/th/translation.json b/app/public/locales/th/translation.json similarity index 100% rename from public/locales/th/translation.json rename to app/public/locales/th/translation.json diff --git a/public/locales/zh-Hans/translation.json b/app/public/locales/zh-Hans/translation.json similarity index 98% rename from public/locales/zh-Hans/translation.json rename to app/public/locales/zh-Hans/translation.json index eb215031..749ebca6 100644 --- a/public/locales/zh-Hans/translation.json +++ b/app/public/locales/zh-Hans/translation.json @@ -37,13 +37,18 @@ "explore": { "title": "探索", "domains": "域", - "community": "社区", + "communities": "社区", + "peoples": "人们", "createNew": "创建新的", "createNewCommunity": { "title": "创建新的社区", - "desc1": "创建一个新的信息流在 ", + "desc1": "创建一个新的社区在 ", "desc2": " 上,使用以下设置。" - } + }, + "reroll": "重新抽选", + "searchForPeoples": "输入想要搜索的人", + "searchForCommunities": "输入想要搜索的社区", + "quicklook": "快速浏览" }, "devtool": { "title": "开发者工具" @@ -127,6 +132,7 @@ "community": "社区", "user": "用户", "pin": "固定", + "isIconTab": "使用图标选项卡", "delete": "删除", "reallyDelete": "确定要删除这个列表吗?", "confirmDelete": "确定" @@ -151,7 +157,7 @@ "basic": "基本", "showEditorOnTop": "在顶部显示编辑器", "showEditorOnTopMobile": "在顶部显示编辑器(移动端)", - "autoSwitchMediaType": "附加图片时自动切换帖子类型", + "autoSwitchMediaPostType": "附加图片时自动切换帖子类型", "developerMode": "开发者模式", "generateInviteCode": "生成邀请码", "inviteCode": "邀请码(24 小时内有效)", diff --git a/public/locales/zh-Hant/translation.json b/app/public/locales/zh-Hant/translation.json similarity index 98% rename from public/locales/zh-Hant/translation.json rename to app/public/locales/zh-Hant/translation.json index d6e5d3e0..c421e91e 100644 --- a/public/locales/zh-Hant/translation.json +++ b/app/public/locales/zh-Hant/translation.json @@ -36,14 +36,19 @@ }, "explore": { "title": "探索", - "domains": "網域", + "communities": "網域", + "peoples": "人們", "community": "社群", "createNew": "建立新的", "createNewCommunity": { "title": "建立新社群", - "desc1": "建立一個新的資訊流在 ", + "desc1": "建立一個新的社群在 ", "desc2": " 上,使用以下設定。" - } + }, + "reroll": "重新抽選", + "searchForPeoples": "輸入想要搜尋的人", + "searchForCommunities": "輸入想要搜尋的社群", + "quicklook": "快速瀏覽" }, "devtool": { "title": "開發者工具" @@ -127,6 +132,7 @@ "community": "社群", "user": "使用者", "pin": "釘選", + "isIconTab": "使用圖示選項卡", "delete": "刪除", "reallyDelete": "確定要刪除這個清單嗎?", "confirmDelete": "確定" @@ -151,7 +157,7 @@ "basic": "基本", "showEditorOnTop": "在頂部顯示編輯器", "showEditorOnTopMobile": "在頂部顯示編輯器(行動裝置)", - "autoSwitchMediaType": "附加圖片時自動切換貼文類型", + "autoSwitchMediaPostType": "附加圖片時自動切換貼文類型", "developerMode": "開發者模式", "generateInviteCode": "產生邀請碼", "inviteCode": "邀請碼(24 小時內有效)", diff --git a/public/robots.txt b/app/public/robots.txt similarity index 100% rename from public/robots.txt rename to app/public/robots.txt diff --git a/public/screenshot_narrow.jpg b/app/public/screenshot_narrow.jpg similarity index 100% rename from public/screenshot_narrow.jpg rename to app/public/screenshot_narrow.jpg diff --git a/public/screenshot_wide.png b/app/public/screenshot_wide.png similarity index 100% rename from public/screenshot_wide.png rename to app/public/screenshot_wide.png diff --git a/public/splash.png b/app/public/splash.png similarity index 100% rename from public/splash.png rename to app/public/splash.png diff --git a/app/public/sw.js b/app/public/sw.js new file mode 100644 index 00000000..3cee8938 --- /dev/null +++ b/app/public/sw.js @@ -0,0 +1,173 @@ +import { precacheAndRoute } from 'workbox-precaching' + +const seed = 'ariake.concrnt.net' // TODO: find a way to sync with the main app + +const resolveHost = async (ccid) => { + const response = await fetch(`https://${seed}/api/v1/entity/${ccid}`) + const data = await response.json() + const content = data.content + return content.domain +} + +const getProfile = async (ccid) => { + const host = await resolveHost(ccid) + const response = await fetch(`https://${host}/api/v1/profile/${ccid}/world.concrnt.p`) + const data = await response.json() + const document = JSON.parse(data.content.document) + return document +} + +const getMessage = async (id, owner) => { + const host = await resolveHost(owner) + const response = await fetch(`https://${host}/api/v1/message/${id}`) + const data = await response.json() + const document = JSON.parse(data.content.document) + return document +} + +self.addEventListener('message', (event) => { + console.log('SW Received Message: ', event.data) + if (event.data && event.data.type === 'SKIP_WAITING') { + self.skipWaiting() + } +}) + +self.addEventListener('push', (event) => { + console.log(`[Service Worker] Push had this data: "${event.data.text()}"`) + const notify = async () => { + const document = event.data.json() + + let title = '' + let body = '' + let icon = '/192.png' + let badge = undefined + + let signer = undefined + let username = document.body.profileOverride?.username + if (!username) { + try { + signer = await getProfile(document.signer) + username = signer.body.username + } catch (error) { + console.log(error) + } + } + + if (!username) { + username = 'Anonymouse' + } + + switch (document.schema) { + case Schemas.likeAssociation: { + // Like + title = `${username} さんがあなたの投稿にいいねしました` + try { + const message = await getMessage(document.target, document.owner) + body = message.body.body + } catch (error) { + console.log(error) + } + + break + } + case Schemas.reactionAssociation: { + // Reaction + title = `${username} さんがあなたの投稿にリアクションしました :${document.body.shortcode}:` + try { + const message = await getMessage(document.target, document.owner) + body = message.body.body + if (document.body.imageUrl) { + badge = `https://ariake.concrnt.net/image/96x/${document.body.imageUrl}` + } + } catch (error) { + console.log(error) + } + + break + } + case Schemas.rerouteAssociation: { + // Reroute + title = `${username} さんがあなたの投稿をリルートしました` + try { + const message = await getMessage(document.target, document.owner) + body = message.body.body + } catch (error) { + console.log(error) + } + + break + } + case Schemas.replyAssociation: { + // Reply + title = `${username} さんがあなたの投稿にリプライしました` + try { + const message = await getMessage(document.body.messageId, document.body.messageAuthor) + body = message.body.body + if (signer?.body?.avatar) { + icon = `https://ariake.concrnt.net/image/128x/${signer.body.avatar}` + } + } catch (error) { + console.log(error) + } + + break + } + case Schemas.mentionAssociation: { + // Mention + title = `${username} さんがあなたをメンションしました` + try { + const message = await getMessage(document.target, document.owner) + body = message.body.body + if (signer?.body?.avatar) { + icon = `https://ariake.concrnt.net/image/128x/${signer.body.avatar}` + } + } catch (error) { + console.log(error) + } + + break + } + case Schemas.readAccessRequestAssociation: { + // Read Access Request + title = `${username} さんが閲覧リクエストを送信しています` + break + } + + default: + break + } + return self.registration.showNotification(title, { + body, + icon, + badge + }) + } + + event.waitUntil(notify()) +}) + +const Schemas = { + markdownMessage: 'https://schema.concrnt.world/m/markdown.json', + replyMessage: 'https://schema.concrnt.world/m/reply.json', + rerouteMessage: 'https://schema.concrnt.world/m/reroute.json', + plaintextMessage: 'https://schema.concrnt.world/m/plaintext.json', + mediaMessage: 'https://schema.concrnt.world/m/media.json', + + likeAssociation: 'https://schema.concrnt.world/a/like.json', + mentionAssociation: 'https://schema.concrnt.world/a/mention.json', + replyAssociation: 'https://schema.concrnt.world/a/reply.json', + rerouteAssociation: 'https://schema.concrnt.world/a/reroute.json', + reactionAssociation: 'https://schema.concrnt.world/a/reaction.json', + upgradeAssociation: 'https://schema.concrnt.world/a/upgrade.json', + readAccessRequestAssociation: 'https://schema.concrnt.world/a/readaccessrequest.json', + + profile: 'https://schema.concrnt.world/p/main.json', + + communityTimeline: 'https://schema.concrnt.world/t/community.json', + emptyTimeline: 'https://schema.concrnt.world/t/empty.json', + subprofileTimeline: 'https://schema.concrnt.world/t/subprofile.json', + + listSubscription: 'https://schema.concrnt.world/s/list.json' +} + +precacheAndRoute(self.__WB_MANIFEST) diff --git a/src/App.tsx b/app/src/App.tsx similarity index 88% rename from src/App.tsx rename to app/src/App.tsx index 6485bd74..8ee15e6e 100644 --- a/src/App.tsx +++ b/app/src/App.tsx @@ -17,14 +17,7 @@ import { ThinMenu } from './components/Menu/ThinMenu' import { usePreference } from './context/PreferenceContext' import TickerProvider from './context/Ticker' import { ContactsPage } from './pages/Contacts' -import { - Schemas, - type Subscription, - type ProfileSchema, - type ReplyAssociationSchema, - type TimelineEvent, - type CCDocument -} from '@concurrent-world/client' +import { type TimelineEvent, type CCDocument, type SocketListener } from '@concrnt/client' import { UrlSummaryProvider } from './context/urlSummaryContext' import { StorageProvider } from './context/StorageContext' import { MarkdownRendererLite } from './components/ui/MarkdownRendererLite' @@ -43,6 +36,8 @@ import { ConfirmProvider } from './context/Confirm' import { type ConcurrentTheme } from './model' import { TimelineDrawerProvider } from './context/TimelineDrawer' import { UserDrawerProvider } from './context/UserDrawer' +import { Schemas } from '@concrnt/worldlib' +import type { MarkdownMessageSchema, ProfileSchema, ReplyAssociationSchema } from '@concrnt/worldlib' const SwitchMasterToSub = lazy(() => import('./components/SwitchMasterToSub')) @@ -54,7 +49,7 @@ function App(): JSX.Element { const theme = useTheme() - const subscription = useRef() + const listener = useRef() const identity = JSON.parse(localStorage.getItem('Identity') || 'null') const [progress] = usePreference('tutorialProgress') @@ -129,42 +124,43 @@ function App(): JSX.Element { }) }) - setInterval(() => { - registration.update() - }, 1000 * 60 * 10) // 10 minutes + setInterval( + () => { + registration.update() + }, + 1000 * 60 * 10 + ) // 10 minutes }) }, []) useEffect(() => { if (!client) return - client.newSubscription().then((sub) => { - subscription.current = sub - subscription.current.listen([ - ...(client?.user?.notificationTimeline ? [client?.user?.notificationTimeline] : []) - ]) - sub.on('AssociationCreated', (event: TimelineEvent) => { - const a = event.document as CCDocument.Association + client.newSocketListener().then((l) => { + listener.current = l + l.listen([...(client?.user?.notificationTimeline ? [client?.user?.notificationTimeline] : [])]) + l.on('AssociationCreated', (event: TimelineEvent) => { + const a = event.parsedDoc as CCDocument.Association if (!a) return if (a.schema === Schemas.replyAssociation) { const replyassociation = a as CCDocument.Association client?.api - .getMessageWithAuthor(replyassociation.body.messageId, replyassociation.body.messageAuthor) + .getMessageWithAuthor(replyassociation.body.messageId, replyassociation.body.messageAuthor) .then((m) => { m && client?.api .getProfileBySemanticID('world.concrnt.p', a.signer) .then((c) => { playNotificationRef.current() - const profile = c?.document.body + const profile = c?.parsedDoc.body enqueueSnackbar( {profile?.username ?? 'anonymous'} replied to your message:{' '} @@ -175,19 +171,20 @@ function App(): JSX.Element { } if (a.schema === Schemas.rerouteAssociation) { - client?.api.getMessageWithAuthor(a.target, event.item.owner).then((m) => { + if (!event.item) return + client?.api.getMessageWithAuthor(a.target, event.item.owner).then((m) => { m && client?.api.getProfileBySemanticID('world.concrnt.p', a.signer).then((c) => { playNotificationRef.current() - const profile = c?.document.body + const profile = c?.parsedDoc.body enqueueSnackbar( {profile?.username ?? 'anonymous'} rerouted to your message:{' '} @@ -198,7 +195,8 @@ function App(): JSX.Element { } if (a.schema === Schemas.likeAssociation) { - client?.api.getMessageWithAuthor(a.target, event.item.owner).then(async (m) => { + if (!event.item) return + client?.api.getMessageWithAuthor(a.target, event.item.owner).then(async (m) => { if (!m) return let username = a.body.profileOverride?.username if (!username) { @@ -206,7 +204,7 @@ function App(): JSX.Element { 'world.concrnt.p', a.signer ) - username = profile?.document.body.username + username = profile?.parsedDoc.body.username } playNotificationRef.current() @@ -214,8 +212,8 @@ function App(): JSX.Element { {username ?? 'anonymous'} liked your message: @@ -225,7 +223,8 @@ function App(): JSX.Element { } if (a.schema === Schemas.reactionAssociation) { - client.api.getMessageWithAuthor(a.target, event.item.owner).then(async (m) => { + if (!event.item) return + client.api.getMessageWithAuthor(a.target, event.item.owner).then(async (m) => { if (!m) return let username = a.body.profileOverride?.username if (!username) { @@ -233,7 +232,7 @@ function App(): JSX.Element { 'world.concrnt.p', a.signer ) - username = profile?.document.body.username + username = profile?.parsedDoc.body.username } playNotificationRef.current() @@ -244,8 +243,8 @@ function App(): JSX.Element { @@ -254,17 +253,18 @@ function App(): JSX.Element { } if (a.schema === Schemas.mentionAssociation) { - client?.api.getMessageWithAuthor(a.target, event.item.owner).then((m) => { + if (!event.item) return + client?.api.getMessageWithAuthor(a.target, event.item.owner).then((m) => { m && client.api.getProfileBySemanticID('world.concrnt.p', a.signer).then((c) => { playNotificationRef.current() - const profile = c?.document.body + const profile = c?.parsedDoc.body enqueueSnackbar( {profile?.username ?? 'anonymous'} mentioned you:{' '} @@ -324,9 +324,9 @@ function App(): JSX.Element { alignItems: 'center', background: `${theme.palette.background.default}, linear-gradient(${theme.palette.background.default}, ${darken( - theme.palette.background.default, - 0.1 - )})`, + theme.palette.background.default, + 0.1 + )})`, width: '100vw', height: '100dvh', overflow: 'hidden', @@ -383,7 +383,7 @@ function App(): JSX.Element { padding: '10px' }} > - あなたのドメイン{client.api.host} + あなたのドメイン{client.host} は現在オフラインの為、読み込み専用モードです。復旧までしばらくお待ちください。 )} diff --git a/src/components/AckButton.tsx b/app/src/components/AckButton.tsx similarity index 96% rename from src/components/AckButton.tsx rename to app/src/components/AckButton.tsx index a0ff4c55..02cc6f5b 100644 --- a/src/components/AckButton.tsx +++ b/app/src/components/AckButton.tsx @@ -1,6 +1,6 @@ import { Button } from '@mui/material' import { useMemo, useState } from 'react' -import { type User } from '@concurrent-world/client' +import { type User } from '@concrnt/worldlib' import { useTranslation } from 'react-i18next' import { useClient } from '../context/ClientContext' diff --git a/src/components/AckList.tsx b/app/src/components/AckList.tsx similarity index 98% rename from src/components/AckList.tsx rename to app/src/components/AckList.tsx index 75a040d6..f48ef207 100644 --- a/src/components/AckList.tsx +++ b/app/src/components/AckList.tsx @@ -1,4 +1,4 @@ -import { type User } from '@concurrent-world/client' +import { type User } from '@concrnt/worldlib' import { useEffect, useState } from 'react' import { Box, Chip, Tab, Tabs, Typography } from '@mui/material' import { CCAvatar } from './ui/CCAvatar' diff --git a/src/components/Activitypub/FollowManager.tsx b/app/src/components/Activitypub/FollowManager.tsx similarity index 90% rename from src/components/Activitypub/FollowManager.tsx rename to app/src/components/Activitypub/FollowManager.tsx index 36278638..a068ef7c 100644 --- a/src/components/Activitypub/FollowManager.tsx +++ b/app/src/components/Activitypub/FollowManager.tsx @@ -19,14 +19,13 @@ export const ApFollowManager = (): JSX.Element => { const follow = (target: string): void => { client.api - .fetchWithCredential(client.api.host, `/ap/api/follow/${target}`, { + .fetchWithCredential(client.host, `/ap/api/follow/${target}`, { method: 'POST', headers: { 'content-type': 'application/json' } }) - .then(async (res) => await res.json()) - .then((data) => { + .then((_data) => { getStats() setDrawerOpen(false) }) @@ -34,25 +33,21 @@ export const ApFollowManager = (): JSX.Element => { const unFollow = (target: string): void => { client.api - .fetchWithCredential(client.api.host, `/ap/api/follow/${target}`, { + .fetchWithCredential(client.host, `/ap/api/follow/${target}`, { method: 'DELETE', headers: { 'content-type': 'application/json' } }) - .then(async (res) => await res.json()) - .then((data) => { + .then((_data) => { getStats() }) } const getStats = (): void => { - client.api - .fetchWithCredential(client.api.host, `/ap/api/stats`, {}) - .then(async (res) => await res.json()) - .then((data) => { - setStats(data.content) - }) + client.api.fetchWithCredential(client.host, `/ap/api/stats`, {}).then((data) => { + setStats(data.content) + }) } useEffect(() => { @@ -106,9 +101,7 @@ export const ApFollowManager = (): JSX.Element => { {stats?.followers.length}フォロワー - {stats?.followers.map((x) => ( - - ))} + {stats?.followers.map((x) => )} { client.api - .fetchWithCredential(client.api.host, `/ap/api/resolve/${encodeURIComponent(props.url)}`, { + .fetchWithCredential(client.host, `/ap/api/resolve/${encodeURIComponent(props.url)}`, { method: 'GET', headers: { accept: 'application/ld+json' } }) - .then(async (res) => await res.json()) .then((data) => { setPerson(data.content) }) diff --git a/src/components/Activitypub/Setup.tsx b/app/src/components/Activitypub/Setup.tsx similarity index 92% rename from src/components/Activitypub/Setup.tsx rename to app/src/components/Activitypub/Setup.tsx index 8ea66620..05e26cdf 100644 --- a/src/components/Activitypub/Setup.tsx +++ b/app/src/components/Activitypub/Setup.tsx @@ -2,7 +2,7 @@ import { Alert, AlertTitle, Box, Button, TextField, Typography } from '@mui/mate import { useEffect, useState } from 'react' import { useClient } from '../../context/ClientContext' import { useSnackbar } from 'notistack' -import { Schemas } from '@concurrent-world/client' +import { Schemas } from '@concrnt/worldlib' import { useTranslation } from 'react-i18next' import { useGlobalState } from '../../context/GlobalState' @@ -31,9 +31,8 @@ export const ApSetup = (): JSX.Element => { } } client.api - .fetchWithCredential(client.api.host, `/ap/api/entity?id=${userID}`, requestOptions) - .then(async (res) => await res.json()) - .then((profile) => { + .fetchWithCredential(client.host, `/ap/api/entity?id=${userID}`, requestOptions) + .then((_profile) => { setEntityFound(true) }) .catch((_e) => { @@ -82,7 +81,7 @@ export const ApSetup = (): JSX.Element => { policyParams: JSON.stringify({ isWritePublic: false, isReadPublic: true, - writer: [proxyCCID], + writer: [client.ccid, proxyCCID], reader: [] }) } @@ -91,7 +90,7 @@ export const ApSetup = (): JSX.Element => { client.api.subscribe('world.concrnt.t-ap@' + client.ccid, Object.keys(listedSubscriptions)[0]) await client.api - .fetchWithCredential(client.api.host, `/ap/api/entity`, { + .fetchWithCredential(client.host, `/ap/api/entity`, { method: 'POST', headers: { 'content-type': 'application/json' @@ -103,7 +102,6 @@ export const ApSetup = (): JSX.Element => { followstream: followstream.id }) }) - .then(async (res) => await res.json()) .catch((e) => { enqueueSnackbar(`register entity failed: ${e}`, { variant: 'error' diff --git a/src/components/Association/AssociationFrame.tsx b/app/src/components/Association/AssociationFrame.tsx similarity index 96% rename from src/components/Association/AssociationFrame.tsx rename to app/src/components/Association/AssociationFrame.tsx index 0b0b4732..deb5e37f 100644 --- a/src/components/Association/AssociationFrame.tsx +++ b/app/src/components/Association/AssociationFrame.tsx @@ -7,7 +7,7 @@ import { type RerouteAssociationSchema, Schemas, type ReadAccessRequestAssociationSchema -} from '@concurrent-world/client' +} from '@concrnt/worldlib' import { useClient } from '../../context/ClientContext' import { Box, ListItem, type SxProps, Typography, alpha, useTheme } from '@mui/material' import { MessageSkeleton } from '../MessageSkeleton' @@ -64,7 +64,17 @@ export const AssociationFrame = memo((props: AssociationFr } }, [props.sx]) - if (fetching) return + if (fetching) { + return ( + <> + + + + {props.after} + + ) + } + if (!association) { if (isDevMode) { return ( diff --git a/src/components/Association/FavoriteAssociation.tsx b/app/src/components/Association/FavoriteAssociation.tsx similarity index 99% rename from src/components/Association/FavoriteAssociation.tsx rename to app/src/components/Association/FavoriteAssociation.tsx index 198d8136..535340ca 100644 --- a/src/components/Association/FavoriteAssociation.tsx +++ b/app/src/components/Association/FavoriteAssociation.tsx @@ -5,7 +5,7 @@ import { type ReplyMessageSchema, type MarkdownMessageSchema, type User -} from '@concurrent-world/client' +} from '@concrnt/worldlib' import { ContentWithCCAvatar } from '../ContentWithCCAvatar' import { Box, IconButton, Link, ListItemIcon, ListItemText, Menu, MenuItem, Typography } from '@mui/material' import { TimeDiff } from '../ui/TimeDiff' diff --git a/src/components/Association/MentionAssociation.tsx b/app/src/components/Association/MentionAssociation.tsx similarity index 95% rename from src/components/Association/MentionAssociation.tsx rename to app/src/components/Association/MentionAssociation.tsx index cf1760fa..97f45052 100644 --- a/src/components/Association/MentionAssociation.tsx +++ b/app/src/components/Association/MentionAssociation.tsx @@ -5,11 +5,10 @@ import { type ReplyMessageSchema, type MarkdownMessageSchema, type User -} from '@concurrent-world/client' +} from '@concrnt/worldlib' import { ContentWithCCAvatar } from '../ContentWithCCAvatar' -import { Box, Link, Typography } from '@mui/material' +import { Box, Typography } from '@mui/material' import { TimeDiff } from '../ui/TimeDiff' -import { Link as RouterLink } from 'react-router-dom' import { type ReactElement, useEffect, useState } from 'react' import { CCLink } from '../ui/CCLink' diff --git a/src/components/Association/ReactionAssociation.tsx b/app/src/components/Association/ReactionAssociation.tsx similarity index 99% rename from src/components/Association/ReactionAssociation.tsx rename to app/src/components/Association/ReactionAssociation.tsx index 67a296f8..ea45b677 100644 --- a/src/components/Association/ReactionAssociation.tsx +++ b/app/src/components/Association/ReactionAssociation.tsx @@ -5,7 +5,7 @@ import { type ReplyMessageSchema, type MarkdownMessageSchema, type User -} from '@concurrent-world/client' +} from '@concrnt/worldlib' import { ContentWithCCAvatar } from '../ContentWithCCAvatar' import { Box, IconButton, ListItemIcon, ListItemText, Menu, MenuItem, Typography } from '@mui/material' import { TimeDiff } from '../ui/TimeDiff' diff --git a/src/components/Association/ReadAccessAssociation.tsx b/app/src/components/Association/ReadAccessAssociation.tsx similarity index 92% rename from src/components/Association/ReadAccessAssociation.tsx rename to app/src/components/Association/ReadAccessAssociation.tsx index 3d879659..61a9fb3c 100644 --- a/src/components/Association/ReadAccessAssociation.tsx +++ b/app/src/components/Association/ReadAccessAssociation.tsx @@ -1,4 +1,4 @@ -import { type ReadAccessRequestAssociationSchema, type Association, type Timeline } from '@concurrent-world/client' +import { type ReadAccessRequestAssociationSchema, type Association, type Timeline } from '@concrnt/worldlib' import { ContentWithCCAvatar } from '../ContentWithCCAvatar' import { Box, Typography } from '@mui/material' import { TimeDiff } from '../ui/TimeDiff' @@ -28,7 +28,7 @@ export const ReadAccessAssociation = (props: ReadAccessAssociationProps): ReactE {props.association.authorUser?.profile?.username} {' '} さんが - + への読み取りアクセスを希望しています diff --git a/src/components/Concord/Assets.tsx b/app/src/components/Concord/Assets.tsx similarity index 100% rename from src/components/Concord/Assets.tsx rename to app/src/components/Concord/Assets.tsx diff --git a/src/components/Concord/BadgeSeries.tsx b/app/src/components/Concord/BadgeSeries.tsx similarity index 100% rename from src/components/Concord/BadgeSeries.tsx rename to app/src/components/Concord/BadgeSeries.tsx diff --git a/src/components/Concord/Explorer.tsx b/app/src/components/Concord/Explorer.tsx similarity index 100% rename from src/components/Concord/Explorer.tsx rename to app/src/components/Concord/Explorer.tsx diff --git a/src/components/ContentWithCCAvatar.tsx b/app/src/components/ContentWithCCAvatar.tsx similarity index 97% rename from src/components/ContentWithCCAvatar.tsx rename to app/src/components/ContentWithCCAvatar.tsx index 6d980ba8..508930ed 100644 --- a/src/components/ContentWithCCAvatar.tsx +++ b/app/src/components/ContentWithCCAvatar.tsx @@ -3,13 +3,13 @@ import { type Message, type ReplyMessageSchema, type ProfileSchema, - type User -} from '@concurrent-world/client' + type User, + type ProfileOverride +} from '@concrnt/worldlib' import { Box, IconButton, ListItem, Paper, type SxProps, Tooltip } from '@mui/material' import { UserProfileCard } from './UserProfileCard' import { Link as routerLink, useNavigate, useLocation } from 'react-router-dom' import { CCAvatar } from './ui/CCAvatar' -import { type ProfileOverride } from '@concurrent-world/client/dist/types/model/core' export interface ContentWithCCAvatarProps { message?: Message diff --git a/src/components/ContentWithUserFetch.tsx b/app/src/components/ContentWithUserFetch.tsx similarity index 98% rename from src/components/ContentWithUserFetch.tsx rename to app/src/components/ContentWithUserFetch.tsx index de30a0b2..8985cc65 100644 --- a/src/components/ContentWithUserFetch.tsx +++ b/app/src/components/ContentWithUserFetch.tsx @@ -4,7 +4,7 @@ import { Link as routerLink } from 'react-router-dom' import { CCAvatar } from './ui/CCAvatar' import { useClient } from '../context/ClientContext' import { useEffect, useState } from 'react' -import { type User } from '@concurrent-world/client' +import { type User } from '@concrnt/worldlib' export interface ContentWithUserFetchProps { ccid: string diff --git a/src/components/Devtool/CCComposer.tsx b/app/src/components/Devtool/CCComposer.tsx similarity index 98% rename from src/components/Devtool/CCComposer.tsx rename to app/src/components/Devtool/CCComposer.tsx index 7f8ecc0f..253e7b35 100644 --- a/src/components/Devtool/CCComposer.tsx +++ b/app/src/components/Devtool/CCComposer.tsx @@ -1,7 +1,7 @@ import { Box, Button, Divider, MenuItem, Select, TextField, Typography } from '@mui/material' import { forwardRef, useState } from 'react' import { useClient } from '../../context/ClientContext' -import { type Schema } from '@concurrent-world/client' +import { type Schema } from '@concrnt/worldlib' import { CCEditor } from '../ui/cceditor' export const CCComposer = forwardRef((props, ref): JSX.Element => { diff --git a/src/components/Devtool/Debugger.tsx b/app/src/components/Devtool/Debugger.tsx similarity index 76% rename from src/components/Devtool/Debugger.tsx rename to app/src/components/Devtool/Debugger.tsx index 6792d4c4..f77aeab8 100644 --- a/src/components/Devtool/Debugger.tsx +++ b/app/src/components/Devtool/Debugger.tsx @@ -1,11 +1,9 @@ import { Box, Button, Typography } from '@mui/material' import { forwardRef } from 'react' -import { useClient } from '../../context/ClientContext' import { useSnackbar } from 'notistack' import { usePreference } from '../../context/PreferenceContext' export const Debugger = forwardRef((props, ref): JSX.Element => { - const { client } = useClient() const { enqueueSnackbar } = useSnackbar() const [_progress, setProgress] = usePreference('tutorialProgress') @@ -47,14 +45,6 @@ export const Debugger = forwardRef((props, ref): JSX.Element => チュートリアルをリセット - ConnectedDomains - {client.api.domainCache && - Object.keys(client.api.domainCache).map((domain, _) => ( - - {domain} -
{JSON.stringify(client.api.domainCache[domain], null, 2)}
-
- ))} ) diff --git a/src/components/Devtool/IdentityGenerator.tsx b/app/src/components/Devtool/IdentityGenerator.tsx similarity index 97% rename from src/components/Devtool/IdentityGenerator.tsx rename to app/src/components/Devtool/IdentityGenerator.tsx index 2e901d39..23ce70f5 100644 --- a/src/components/Devtool/IdentityGenerator.tsx +++ b/app/src/components/Devtool/IdentityGenerator.tsx @@ -1,4 +1,4 @@ -import { GenerateIdentity } from '@concurrent-world/client' +import { GenerateIdentity } from '@concrnt/client' import { Box, Button, Typography } from '@mui/material' import { forwardRef, useState } from 'react' diff --git a/src/components/Devtool/UserJWT.tsx b/app/src/components/Devtool/UserJWT.tsx similarity index 94% rename from src/components/Devtool/UserJWT.tsx rename to app/src/components/Devtool/UserJWT.tsx index 73622c86..532d90ec 100644 --- a/src/components/Devtool/UserJWT.tsx +++ b/app/src/components/Devtool/UserJWT.tsx @@ -1,13 +1,13 @@ import { forwardRef, useState } from 'react' import { useClient } from '../../context/ClientContext' import { Box, Button, TextField, Typography } from '@mui/material' -import { IssueJWT } from '@concurrent-world/client' +import { IssueJWT } from '@concrnt/client' export const UserJWT = forwardRef((props, ref): JSX.Element => { const { client } = useClient() const [issuedJwt, setIssuedJwt] = useState('') - const [audience, setAudience] = useState(client?.api.host ?? '') + const [audience, setAudience] = useState(client.host) const [subject, setSubject] = useState('') const issueJwt = (): void => { diff --git a/src/components/Editor/CCPostEditor.tsx b/app/src/components/Editor/CCPostEditor.tsx similarity index 99% rename from src/components/Editor/CCPostEditor.tsx rename to app/src/components/Editor/CCPostEditor.tsx index d615760f..0616aae1 100644 --- a/src/components/Editor/CCPostEditor.tsx +++ b/app/src/components/Editor/CCPostEditor.tsx @@ -24,7 +24,7 @@ import { StreamPicker } from '../ui/StreamPicker' import { useSnackbar } from 'notistack' import { usePersistent } from '../../hooks/usePersistent' import HomeIcon from '@mui/icons-material/Home' -import { type CommunityTimelineSchema, type Timeline, type Message, type User } from '@concurrent-world/client' +import { type CommunityTimelineSchema, type Timeline, type Message, type User } from '@concrnt/worldlib' import { useClient } from '../../context/ClientContext' import { type WorldMedia, type Emoji, type EmojiLite } from '../../model' import { useTranslation } from 'react-i18next' @@ -219,7 +219,8 @@ export const CCPostEditor = memo((props: CCPostEditorProps): enqueueSnackbar('set destination required', { variant: 'error' }) return } - const destTimelineIDs = destTimelines.map((s) => s.id) + + const destTimelineIDs = destTimelines.map((t) => t.fqid).filter((e) => e) const homeTimeline = selectedSubprofile ? 'world.concrnt.t-subhome.' + selectedSubprofile + '@' + client.user.ccid @@ -289,10 +290,11 @@ export const CCPostEditor = memo((props: CCPostEditorProps): enqueueSnackbar('Invalid mode', { variant: 'error' }) } - req?.then(() => { - reset() - props.onPost?.() - }) + req + ?.then(() => { + reset() + props.onPost?.() + }) .catch((error) => { enqueueSnackbar(`Failed to post message: ${error.message}`, { variant: 'error' }) }) diff --git a/src/components/Editor/EditorActions.tsx b/app/src/components/Editor/EditorActions.tsx similarity index 99% rename from src/components/Editor/EditorActions.tsx rename to app/src/components/Editor/EditorActions.tsx index ed4ef43d..5a2b2a84 100644 --- a/src/components/Editor/EditorActions.tsx +++ b/app/src/components/Editor/EditorActions.tsx @@ -28,7 +28,7 @@ import { type Dispatch, type SetStateAction, useRef, useCallback, useState } fro import { useTranslation } from 'react-i18next' import { type WorldMedia, type Emoji, type EmojiLite } from '../../model' import { UserPicker } from '../ui/UserPicker' -import { type User } from '@concurrent-world/client' +import { type User } from '@concrnt/worldlib' import { usePersistent } from '../../hooks/usePersistent' import { genBlurHash } from '../../util' import { useGlobalState } from '../../context/GlobalState' diff --git a/src/components/Editor/EditorPreview.tsx b/app/src/components/Editor/EditorPreview.tsx similarity index 94% rename from src/components/Editor/EditorPreview.tsx rename to app/src/components/Editor/EditorPreview.tsx index 7066d6a0..4167e7ae 100644 --- a/src/components/Editor/EditorPreview.tsx +++ b/app/src/components/Editor/EditorPreview.tsx @@ -101,7 +101,11 @@ export const EditorPreview = (props: EditorPreviewProps): JSX.Element => { }} > - + ) diff --git a/src/components/Editor/EmojiSuggestion.tsx b/app/src/components/Editor/EmojiSuggestion.tsx similarity index 100% rename from src/components/Editor/EmojiSuggestion.tsx rename to app/src/components/Editor/EmojiSuggestion.tsx diff --git a/src/components/Editor/UserSuggestion.tsx b/app/src/components/Editor/UserSuggestion.tsx similarity index 99% rename from src/components/Editor/UserSuggestion.tsx rename to app/src/components/Editor/UserSuggestion.tsx index 9fb9db21..7039ffa0 100644 --- a/src/components/Editor/UserSuggestion.tsx +++ b/app/src/components/Editor/UserSuggestion.tsx @@ -1,7 +1,7 @@ import { useCallback, useEffect, useState } from 'react' import { Box, Collapse, List, ListItemButton, ListItemIcon, ListItemText, Paper, Popper } from '@mui/material' import caretPosition from 'textarea-caret' -import { type User } from '@concurrent-world/client' +import { type User } from '@concrnt/worldlib' import { useClient } from '../../context/ClientContext' export interface UserSuggestionProps { diff --git a/src/components/EditorModal.tsx b/app/src/components/EditorModal.tsx similarity index 90% rename from src/components/EditorModal.tsx rename to app/src/components/EditorModal.tsx index 799b8341..c5fee354 100644 --- a/src/components/EditorModal.tsx +++ b/app/src/components/EditorModal.tsx @@ -3,8 +3,9 @@ import { CCPostEditor, type CCPostEditorProps, type EditorMode } from './Editor/ import { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react' import { useGlobalState } from '../context/GlobalState' import { usePreference } from '../context/PreferenceContext' -import { type CommunityTimelineSchema, type Message, type Timeline } from '@concurrent-world/client' +import { type CommunityTimelineSchema, type Message, type Timeline } from '@concrnt/worldlib' import { MessageContainer } from './Message/MessageContainer' +import { useClient } from '../context/ClientContext' export interface EditorModalState { open: (opts?: OpenOptions) => void @@ -40,6 +41,7 @@ export interface EditorModalProps { } export const EditorModalProvider = (props: EditorModalProps): JSX.Element => { + const { client } = useClient() const [viewportHeight, setViewportHeight] = useState(visualViewport?.height ?? 0) useEffect(() => { function handleResize(): void { @@ -72,12 +74,30 @@ export const EditorModalProvider = (props: EditorModalProps): JSX.Element => { const [lists] = usePreference('lists') const home = Object.keys(lists).length > 0 ? lists[Object.keys(lists)[0]] : null - const homePostTimelines = useMemo(() => { - if (!home) return [] - return home.defaultPostStreams - .map((timelineID) => allKnownTimelines.find((e) => (e.cacheKey ?? e.id) === timelineID)) - .filter((e) => e) as Array> - }, [lists, allKnownTimelines]) + + const [homePostTimelines, setHomePostTimelines] = useState>>([]) + + useEffect(() => { + if (!home) return + let isMounted = true + const exec = async (): Promise => { + const requests = await Promise.allSettled( + home.defaultPostStreams.map((timelineID) => { + return client.getTimeline(timelineID) + }) + ) + const fulfilled = requests.filter((e) => e.status === 'fulfilled') as Array< + PromiseFulfilledResult> + > + const homePostTimelines = fulfilled.map((e) => e.value) + if (!isMounted) return + setHomePostTimelines(homePostTimelines) + } + exec() + return () => { + isMounted = false + } + }, [lists, allKnownTimelines, home]) const open = useCallback( (openOpts?: OpenOptions): void => { diff --git a/src/components/EmergencyKit.tsx b/app/src/components/EmergencyKit.tsx similarity index 84% rename from src/components/EmergencyKit.tsx rename to app/src/components/EmergencyKit.tsx index 9bcc9710..96e21d17 100644 --- a/src/components/EmergencyKit.tsx +++ b/app/src/components/EmergencyKit.tsx @@ -4,7 +4,8 @@ import { type Preference, defaultPreference } from '../context/PreferenceContext // @ts-expect-error vite dynamic import import buildTime from '~build/time' // @ts-expect-error vite dynamic import -import { branch, sha } from '~build/info' +import { branch, sha } from '~build/git' +import { useEffect } from 'react' const buttonStyle = { backgroundColor: '#0476d9', @@ -29,6 +30,25 @@ const messages = [ ] export function EmergencyKit({ error }: FallbackProps): JSX.Element { + useEffect(() => { + // do not refresh in 5 minutes + const lastQuickFix = localStorage.getItem('lastQuickFix') + if (lastQuickFix) { + const diff = new Date().getTime() - new Date(lastQuickFix).getTime() + if (diff < 5 * 60 * 1000) { + return + } + } + + if ( + error?.message.includes('Failed to fetch dynamically imported module') || + error?.message.includes("'text/html' is not a valid JavaScript MIME type") + ) { + localStorage.setItem('lastQuickFix', new Date().toISOString()) + window.location.reload() + } + }, []) + const gracefulResetLocalStorage = (): void => { for (const key in localStorage) { if (['Domain', 'PrivateKey', 'SubKey'].includes(key)) continue @@ -117,24 +137,26 @@ buildTime: ${buildTime.toLocaleString()}` maxWidth: 'unset', padding: '20px' }} - onClick={(): void => { + onClick={async (): Promise => { + localStorage.removeItem('lastQuickFix') + // delete all caches if (window.caches) { - window.caches - .keys() - .then((keys) => { - return Promise.all( - keys.map((key) => { - return caches.delete(key) - }) - ) + const keys = await window.caches.keys() + await Promise.all( + keys.map((key) => { + return window.caches.delete(key) }) - .then(() => { - window.location.replace('/') - }) - } else { - window.location.replace('/') + ) + } + if (window.indexedDB) { + await new Promise((resolve) => { + const req = window.indexedDB.deleteDatabase('concrnt-client') + req.onsuccess = resolve + req.onerror = resolve + }) } + window.location.replace('/') }} > リロード diff --git a/src/components/EmojipackCard.tsx b/app/src/components/EmojipackCard.tsx similarity index 100% rename from src/components/EmojipackCard.tsx rename to app/src/components/EmojipackCard.tsx diff --git a/app/src/components/EntityMetaEditor.tsx b/app/src/components/EntityMetaEditor.tsx new file mode 100644 index 00000000..15e8a727 --- /dev/null +++ b/app/src/components/EntityMetaEditor.tsx @@ -0,0 +1,46 @@ +import { useEffect, useState } from 'react' +import { useClient } from '../context/ClientContext' +import { CCEditor } from './ui/cceditor' +import { Button } from '@mui/material' +import { useSnackbar } from 'notistack' + +export const EntityMetaEditor = () => { + const { client } = useClient() + const { enqueueSnackbar } = useSnackbar() + + const [meta, setMeta] = useState(null) + + useEffect(() => { + client.api.fetchWithCredential(client.host, '/api/v1/entity/meta').then((res) => { + setMeta(JSON.parse(res.content.info)) + }) + }, []) + + return meta ? ( + <> + + + + ) : ( + <>loading... + ) +} diff --git a/src/components/GuestBase.tsx b/app/src/components/GuestBase.tsx similarity index 100% rename from src/components/GuestBase.tsx rename to app/src/components/GuestBase.tsx diff --git a/src/components/Importer/ImportMasterkey.tsx b/app/src/components/Importer/ImportMasterkey.tsx similarity index 84% rename from src/components/Importer/ImportMasterkey.tsx rename to app/src/components/Importer/ImportMasterkey.tsx index 7ee3fff4..67ff8143 100644 --- a/src/components/Importer/ImportMasterkey.tsx +++ b/app/src/components/Importer/ImportMasterkey.tsx @@ -2,8 +2,8 @@ import Button from '@mui/material/Button' import TextField from '@mui/material/TextField' import Typography from '@mui/material/Typography' import { useEffect, useMemo, useState } from 'react' +import { Client } from '@concrnt/worldlib' import { - Client, LoadKey, ComputeCCID, IsValid256k1PrivateKey, @@ -12,7 +12,8 @@ import { LoadIdentity, GenerateIdentity, ComputeCKID -} from '@concurrent-world/client' +} from '@concrnt/client' + import { Box, IconButton, InputAdornment } from '@mui/material' import Visibility from '@mui/icons-material/Visibility' import VisibilityOff from '@mui/icons-material/VisibilityOff' @@ -50,21 +51,22 @@ export function ImportMasterKey(): JSX.Element { const timer = setTimeout(() => { try { - const client = new Client(searchTarget, keypair, ccid) - client.api - .getEntity(ccid, searchTarget) - .then((entity) => { - if (entity?.domain) { - setDomainInput(entity.domain) - setErrorMessage('') - } else { - setDomainAutoDetectionFailed(true) + Client.createAsGuest(searchTarget).then((client) => { + client.api + .getEntity(ccid, searchTarget) + .then((entity) => { + if (entity?.domain) { + setDomainInput(entity.domain) + setErrorMessage('') + } else { + setDomainAutoDetectionFailed(true) + setErrorMessage(t('notFound')) + } + }) + .catch((_) => { setErrorMessage(t('notFound')) - } - }) - .catch((_) => { - setErrorMessage(t('notFound')) - }) + }) + }) } catch (e) { console.error(e) } @@ -79,14 +81,16 @@ export function ImportMasterKey(): JSX.Element { if (!domainInput || !keypair || !ccid) return const timer = setTimeout(() => { try { - const client = new Client(domainInput, keypair, ccid) - client.api.fetchWithCredential(domainInput, '/api/v1/entity', {}).then((res) => { - if (res.ok) { - setErrorMessage('') - setRegistrationOK(true) - } else { - setRegistrationOK(false) - } + Client.create(keypair.privatekey, domainInput).then((client) => { + client.api + .fetchWithCredential(domainInput, '/api/v1/entity', {}) + .then((_) => { + setErrorMessage('') + setRegistrationOK(true) + }) + .catch((_) => { + setRegistrationOK(false) + }) }) } catch (e) { console.error(e) diff --git a/src/components/Importer/ImportSubkey.tsx b/app/src/components/Importer/ImportSubkey.tsx similarity index 98% rename from src/components/Importer/ImportSubkey.tsx rename to app/src/components/Importer/ImportSubkey.tsx index 317bf13a..aa2b7499 100644 --- a/src/components/Importer/ImportSubkey.tsx +++ b/app/src/components/Importer/ImportSubkey.tsx @@ -5,7 +5,7 @@ import { useTranslation } from 'react-i18next' import Visibility from '@mui/icons-material/Visibility' import VisibilityOff from '@mui/icons-material/VisibilityOff' import CheckCircleIcon from '@mui/icons-material/CheckCircle' -import { Client } from '@concurrent-world/client' +import { Client } from '@concrnt/worldlib' export const ImportSubkey = (): JSX.Element => { const { t } = useTranslation('', { keyPrefix: 'import' }) diff --git a/src/components/ListSettings.tsx b/app/src/components/ListSettings.tsx similarity index 96% rename from src/components/ListSettings.tsx rename to app/src/components/ListSettings.tsx index 4d3e18fb..20a68d06 100644 --- a/src/components/ListSettings.tsx +++ b/app/src/components/ListSettings.tsx @@ -10,20 +10,13 @@ import { Tab, Tabs, TextField, - Typography, - alpha, - useTheme + Typography } from '@mui/material' import { StreamPicker } from './ui/StreamPicker' import { useEffect, useState } from 'react' import { usePreference } from '../context/PreferenceContext' -import { - type CoreSubscription, - type Timeline, - type CommunityTimelineSchema, - type ListSubscriptionSchema, - Schemas -} from '@concurrent-world/client' +import { type Timeline, type CommunityTimelineSchema, type ListSubscriptionSchema, Schemas } from '@concrnt/worldlib' +import { type Subscription } from '@concrnt/client' import { useClient } from '../context/ClientContext' import { ListItemTimeline } from './ui/ListItemTimeline' import PlaylistRemoveIcon from '@mui/icons-material/PlaylistRemove' @@ -36,10 +29,9 @@ import DeleteIcon from '@mui/icons-material/Delete' import { useEmojiPicker } from '../context/EmojiPickerContext' import { CCIconButton } from './ui/CCIconButton' import EmojiEmotions from '@mui/icons-material/EmojiEmotions' -import { CheckBox } from '@mui/icons-material' export interface ListSettingsProps { - subscription: CoreSubscription + subscription: Subscription onModified?: () => void } @@ -65,8 +57,8 @@ export function ListSettings(props: ListSettingsProps): JSX.Element { const emojiPicker = useEmojiPicker() useEffect(() => { - setListName(props.subscription.document.body.name) - setIconURL(props.subscription.document.body.iconURL ?? '') + setListName(props.subscription.parsedDoc.body.name) + setIconURL(props.subscription.parsedDoc.body.iconURL ?? '') if (!list) return Promise.all(list.defaultPostStreams.map((streamID) => client.getTimeline(streamID))).then((streams) => { @@ -199,7 +191,7 @@ export function ListSettings(props: ListSettingsProps): JSX.Element { setSelected={(value) => { updateList(props.subscription.id, { ...list, - defaultPostStreams: value.map((e) => e.cacheKey ?? e.id) + defaultPostStreams: value.map((e) => e.fqid).filter((e) => e !== null) }) setPostStreams(value) }} @@ -281,7 +273,7 @@ export function ListSettings(props: ListSettingsProps): JSX.Element { }) }, { - description: props.subscription.document.body.name, + description: props.subscription.parsedDoc.body.name, confirmText: t('confirmDelete') } ) diff --git a/src/components/ListsMenu/main.tsx b/app/src/components/ListsMenu/main.tsx similarity index 100% rename from src/components/ListsMenu/main.tsx rename to app/src/components/ListsMenu/main.tsx diff --git a/src/components/ListsMenu/minimal.tsx b/app/src/components/ListsMenu/minimal.tsx similarity index 87% rename from src/components/ListsMenu/minimal.tsx rename to app/src/components/ListsMenu/minimal.tsx index 74d045d7..d290a0f3 100644 --- a/src/components/ListsMenu/minimal.tsx +++ b/app/src/components/ListsMenu/minimal.tsx @@ -19,8 +19,8 @@ export const MinimalListsMenu = (): JSX.Element => { }} > {Object.keys(lists).map((key) => { - const iconURL = listedSubscriptions[key]?.document?.body?.iconURL - const name = listedSubscriptions[key]?.document?.body?.name + const iconURL = listedSubscriptions[key]?.parsedDoc?.body?.iconURL + const name = listedSubscriptions[key]?.parsedDoc?.body?.name return ( ((props: MenuProps): JSX.Element => { {client?.user?.profile?.username} - {client.api.host} + {client.host} diff --git a/src/components/Menu/MobileMenu.tsx b/app/src/components/Menu/MobileMenu.tsx similarity index 100% rename from src/components/Menu/MobileMenu.tsx rename to app/src/components/Menu/MobileMenu.tsx diff --git a/src/components/Menu/ThinMenu.tsx b/app/src/components/Menu/ThinMenu.tsx similarity index 100% rename from src/components/Menu/ThinMenu.tsx rename to app/src/components/Menu/ThinMenu.tsx diff --git a/src/components/Message/DummyMessageView.tsx b/app/src/components/Message/DummyMessageView.tsx similarity index 99% rename from src/components/Message/DummyMessageView.tsx rename to app/src/components/Message/DummyMessageView.tsx index a6d25133..d0d296be 100644 --- a/src/components/Message/DummyMessageView.tsx +++ b/app/src/components/Message/DummyMessageView.tsx @@ -1,6 +1,6 @@ import { Box, IconButton, ListItem, type SxProps, Typography } from '@mui/material' import { CCAvatar } from '../ui/CCAvatar' -import { type ProfileSchema, type ReplyMessageSchema, type MarkdownMessageSchema } from '@concurrent-world/client' +import { type ProfileSchema, type ReplyMessageSchema, type MarkdownMessageSchema } from '@concrnt/worldlib' import { MarkdownRenderer } from '../ui/MarkdownRenderer' import { IconButtonWithNumber } from '../ui/IconButtonWithNumber' diff --git a/app/src/components/Message/MarkdownMessageView.tsx b/app/src/components/Message/MarkdownMessageView.tsx new file mode 100644 index 00000000..445f9494 --- /dev/null +++ b/app/src/components/Message/MarkdownMessageView.tsx @@ -0,0 +1,26 @@ +import { type Message, type MarkdownMessageSchema, type RerouteMessageSchema } from '@concrnt/worldlib' +import { MessageViewBase } from './MessageViewBase' +import { MarkdownRenderer } from '../ui/MarkdownRenderer' + +export interface MarkdownMessageViewProps { + message: Message + rerouted?: Message + userCCID?: string + beforeMessage?: JSX.Element + lastUpdated?: number + forceExpanded?: boolean + clipHeight?: number + simple?: boolean + additionalMenuItems?: JSX.Element | JSX.Element[] +} + +export const MarkdownMessageView = (props: MarkdownMessageViewProps): JSX.Element => { + return ( + + + + ) +} diff --git a/app/src/components/Message/MediaMessageView.tsx b/app/src/components/Message/MediaMessageView.tsx new file mode 100644 index 00000000..4632a285 --- /dev/null +++ b/app/src/components/Message/MediaMessageView.tsx @@ -0,0 +1,36 @@ +import { type RerouteMessageSchema, type Message, type MediaMessageSchema } from '@concrnt/worldlib' +import { EmbeddedGallery } from '../ui/EmbeddedGallery' +import { MessageViewBase } from './MessageViewBase' +import { MarkdownRenderer } from '../ui/MarkdownRenderer' + +export interface MediaMessageViewProps { + message: Message + rerouted?: Message + userCCID?: string + beforeMessage?: JSX.Element + lastUpdated?: number + forceExpanded?: boolean + clipHeight?: number + simple?: boolean + additionalMenuItems?: JSX.Element | JSX.Element[] +} + +export const MediaMessageView = (props: MediaMessageViewProps): JSX.Element => { + return ( + + {props.message.document.body.medias && ( + + )} + + } + > + + + ) +} diff --git a/src/components/Message/MessageActions.tsx b/app/src/components/Message/MessageActions.tsx similarity index 98% rename from src/components/Message/MessageActions.tsx rename to app/src/components/Message/MessageActions.tsx index fb546ea8..4a5667e9 100644 --- a/src/components/Message/MessageActions.tsx +++ b/app/src/components/Message/MessageActions.tsx @@ -18,7 +18,7 @@ import { type ReplyMessageSchema, Schemas, type MarkdownMessageSchema -} from '@concurrent-world/client' +} from '@concrnt/worldlib' import { useState } from 'react' import ContentPasteIcon from '@mui/icons-material/ContentPaste' import ManageSearchIcon from '@mui/icons-material/ManageSearch' @@ -33,10 +33,10 @@ import { usePreference } from '../../context/PreferenceContext' import { useConcord } from '../../context/ConcordContext' import { useEditorModal } from '../EditorModal' import { useConfirm } from '../../context/Confirm' -import { MessageView } from './MessageView' import { useTranslation } from 'react-i18next' import { convertToGoogleTranslateCode } from '../../util' import { useGlobalState } from '../../context/GlobalState' +import { MarkdownMessageView } from './MarkdownMessageView' export interface MessageActionsProps { message: Message @@ -94,7 +94,7 @@ export const MessageActions = (props: MessageActionsProps): JSX.Element => { editorModal.open({ mode: 'reply', target: props.message, - streamPickerInitial: props.message.postedStreams?.filter( + streamPickerInitial: props.message.postedTimelines?.filter( (t) => t.schema === Schemas.communityTimeline ) }) @@ -339,7 +339,7 @@ export const MessageActions = (props: MessageActionsProps): JSX.Element => { }, { confirmText: t('confirmDelete'), - description: + description: } ) }} diff --git a/src/components/Message/MessageContainer.tsx b/app/src/components/Message/MessageContainer.tsx similarity index 59% rename from src/components/Message/MessageContainer.tsx rename to app/src/components/Message/MessageContainer.tsx index 58f286e5..f0af2978 100644 --- a/src/components/Message/MessageContainer.tsx +++ b/app/src/components/Message/MessageContainer.tsx @@ -6,14 +6,13 @@ import { type MarkdownMessageSchema, type MediaMessageSchema, type PlaintextMessageSchema -} from '@concurrent-world/client' +} from '@concrnt/worldlib' import { useClient } from '../../context/ClientContext' import { memo, useEffect, useMemo, useState } from 'react' import { ReplyMessageFrame } from './ReplyMessageFrame' import { RerouteMessageFrame } from './RerouteMessageFrame' import { MessageSkeleton } from '../MessageSkeleton' import { Box, type SxProps, Typography, Button, useTheme, alpha } from '@mui/material' -import { MessageView } from './MessageView' import { usePreference } from '../../context/PreferenceContext' import { ContentWithUserFetch } from '../ContentWithUserFetch' @@ -22,6 +21,11 @@ import TerminalIcon from '@mui/icons-material/Terminal' import { CopyChip } from '../ui/CopyChip' import { PlainMessageView } from './PlainMessageView' import { MediaMessageView } from './MediaMessageView' +import { NotFoundError } from '@concrnt/client' +import { MarkdownMessageView } from './MarkdownMessageView' +import { Link as routerLink } from 'react-router-dom' +import { CCAvatarWithResolver } from '../ui/CCAvatarWithResolver' +import InfoIcon from '@mui/icons-material/Info' interface MessageContainerProps { messageID: string @@ -46,7 +50,7 @@ export const MessageContainer = memo((props: MessageConta const [devMode] = usePreference('devMode') const [forceUpdateCount, setForceUpdateCount] = useState(0) const [_, setStaticUpdateCount] = useState(0) - const [error, setError] = useState(null) + const [error, setError] = useState(null) if (message) { message.onUpdate = () => { @@ -61,7 +65,7 @@ export const MessageContainer = memo((props: MessageConta setMessage(msg) }) .catch((e) => { - setError(e.toString()) + setError(e) }) .finally(() => { setIsFetching(false) @@ -90,89 +94,106 @@ export const MessageContainer = memo((props: MessageConta ) } - if (!message && devMode) { - if (!error) { + if (error) { + if (error instanceof NotFoundError) { // 404 return ( - - - - 404 - - - {props.resolveHint && ( - - )} - - - - 開発者ビュー + <> + + + + + + このカレントは削除されました + {props.after} - + ) } else { - return ( - - - - - - {error} - - - - 開発者ビュー - - - - - - - {props.resolveHint && ( - - )} - - - - + + + {error.message} + + + + 開発者ビュー + + + + + + + {props.resolveHint && ( + + )} + + + + + + - - {props.after} - - ) + {props.after} + + ) + } } - } else if (!message && !devMode) { + } + + if (!message) { return null } @@ -184,7 +205,7 @@ export const MessageContainer = memo((props: MessageConta - } lastUpdated={props.lastUpdated} diff --git a/src/components/Message/MessageHeader.tsx b/app/src/components/Message/MessageHeader.tsx similarity index 98% rename from src/components/Message/MessageHeader.tsx rename to app/src/components/Message/MessageHeader.tsx index 5eb69ff4..e1e2217f 100644 --- a/src/components/Message/MessageHeader.tsx +++ b/app/src/components/Message/MessageHeader.tsx @@ -2,7 +2,7 @@ import { Box, Typography, Tooltip, Menu, IconButton } from '@mui/material' import { TimeDiff } from '../ui/TimeDiff' import { useMemo, useState } from 'react' import CheckCircleIcon from '@mui/icons-material/CheckCircle' -import { type Message, type ReplyMessageSchema, type MarkdownMessageSchema } from '@concurrent-world/client' +import { type Message, type ReplyMessageSchema, type MarkdownMessageSchema } from '@concrnt/worldlib' import MoreHorizIcon from '@mui/icons-material/MoreHoriz' import { useClient } from '../../context/ClientContext' @@ -28,7 +28,7 @@ export const MessageHeader = (props: MessageHeaderProps): JSX.Element => { }, [props.message, client]) const isWhisper = props.message?.policy === 'https://policy.concrnt.world/m/whisper.json' - const participants: string[] = isWhisper ? props.message.policyParams?.participants : [] + const participants: string[] = isWhisper ? props.message.policyParams.participants : [] return ( { }) .then((associations) => { for (const association of associations) { - const txhash = association.document.body.txhash + const txhash = association.parsedDoc.body.txhash concord.getRawTx(txhash).then(async (tx) => { if (!tx) return const memo = tx.body.memo @@ -92,7 +92,7 @@ export const MessageReactions = (props: MessageReactionsProps): JSX.Element => { const ownReactions = Object.fromEntries( props.message?.ownAssociations .filter((association) => association.schema === Schemas.reactionAssociation) - .map((association) => [association.document.body.imageUrl, association]) + .map((association) => [association.parsedDoc.body.imageUrl, association]) ) const loadReactionMembers = (reaction: string): void => { diff --git a/src/components/Message/MessageView.tsx b/app/src/components/Message/MessageViewBase.tsx similarity index 82% rename from src/components/Message/MessageView.tsx rename to app/src/components/Message/MessageViewBase.tsx index d188b525..55d94e9b 100644 --- a/src/components/Message/MessageView.tsx +++ b/app/src/components/Message/MessageViewBase.tsx @@ -1,16 +1,9 @@ import { Box, Button, alpha, useTheme } from '@mui/material' -import { SimpleNote } from './SimpleNote' import { MessageHeader } from './MessageHeader' import { MessageActions } from './MessageActions' import { MessageReactions } from './MessageReactions' -import { - type RerouteMessageSchema, - type Message, - type ReplyMessageSchema, - type MarkdownMessageSchema, - Schemas, - type CoreProfile -} from '@concurrent-world/client' +import { type RerouteMessageSchema, type Message, Schemas } from '@concrnt/worldlib' +import { Profile } from '@concrnt/client' import { PostedStreams } from './PostedStreams' import { ContentWithCCAvatar } from '../ContentWithCCAvatar' import ArrowForwardIcon from '@mui/icons-material/ArrowForward' @@ -20,7 +13,7 @@ import { useClient } from '../../context/ClientContext' import { AutoSummaryProvider } from '../../context/AutoSummaryContext' export interface MessageViewProps { - message: Message + message: Message rerouted?: Message userCCID?: string beforeMessage?: JSX.Element @@ -29,23 +22,25 @@ export interface MessageViewProps { clipHeight?: number simple?: boolean additionalMenuItems?: JSX.Element | JSX.Element[] + children?: JSX.Element + afterMessage?: JSX.Element } const gradationHeight = 80 -export const MessageView = (props: MessageViewProps): JSX.Element => { +export const MessageViewBase = (props: MessageViewProps): JSX.Element => { const theme = useTheme() const clipHeight = props.clipHeight ?? 450 const [expanded, setExpanded] = useState(props.forceExpanded ?? false) const { client } = useClient() - const [characterOverride, setProfileOverride] = useState | undefined>(undefined) + const [characterOverride, setProfileOverride] = useState | undefined>(undefined) useEffect(() => { if (!(client && props.message.document.body.profileOverride?.profileID)) return client.api - .getProfileByID(props.message.document.body.profileOverride?.profileID, props.message.author) + .getProfile(props.message.document.body.profileOverride?.profileID, props.message.author) .then((profile) => { setProfileOverride(profile ?? undefined) }) @@ -54,12 +49,12 @@ export const MessageView = (props: MessageViewProps): JSX.Element => { const reroutedsame = useMemo(() => { if (!props.rerouted) return false const A = - props.rerouted.postedStreams?.filter( - (stream) => stream.schema === Schemas.communityTimeline || stream.schema === Schemas.emptyTimeline + props.rerouted.postedTimelines?.filter( + (timeline) => timeline.schema === Schemas.communityTimeline || timeline.schema === Schemas.emptyTimeline ) ?? [] const B = - props.message.postedStreams?.filter( - (stream) => stream.schema === Schemas.communityTimeline || stream.schema === Schemas.emptyTimeline + props.message.postedTimelines?.filter( + (timeline) => timeline.schema === Schemas.communityTimeline || timeline.schema === Schemas.emptyTimeline ) ?? [] if (A.length !== B.length) return false const Aids = A.map((e) => e.id).sort() @@ -72,11 +67,11 @@ export const MessageView = (props: MessageViewProps): JSX.Element => { message={props.message} author={props.message.authorUser} profileOverride={props.message.document.body.profileOverride} - avatarOverride={characterOverride?.document.body.avatar} - characterOverride={characterOverride?.document.body} + avatarOverride={characterOverride?.parsedDoc.body.avatar} + characterOverride={characterOverride?.parsedDoc.body} > { Show more - - - + {props.children} + {props.afterMessage} {(!props.simple && ( <> diff --git a/src/components/Message/OneLineMessageView.tsx b/app/src/components/Message/OneLineMessageView.tsx similarity index 89% rename from src/components/Message/OneLineMessageView.tsx rename to app/src/components/Message/OneLineMessageView.tsx index a17b286e..d5f15406 100644 --- a/src/components/Message/OneLineMessageView.tsx +++ b/app/src/components/Message/OneLineMessageView.tsx @@ -2,17 +2,13 @@ import { Box, IconButton, Tooltip } from '@mui/material' import { Link as routerLink } from 'react-router-dom' import { CCAvatar } from '../ui/CCAvatar' import { TimeDiff } from '../ui/TimeDiff' -import { - type Message, - type ReplyMessageSchema, - type MarkdownMessageSchema, - type CoreProfile -} from '@concurrent-world/client' +import { type Message, type ReplyMessageSchema, type MarkdownMessageSchema } from '@concrnt/worldlib' import { MarkdownRendererLite } from '../ui/MarkdownRendererLite' import { MarkdownRenderer } from '../ui/MarkdownRenderer' import { useEffect, useState } from 'react' import { useClient } from '../../context/ClientContext' import { CCLink } from '../ui/CCLink' +import { Profile } from '@concrnt/client' export interface OneLineMessageViewProps { message: Message @@ -21,14 +17,14 @@ export interface OneLineMessageViewProps { export const OneLineMessageView = (props: OneLineMessageViewProps): JSX.Element => { const { client } = useClient() - const [characterOverride, setProfileOverride] = useState | undefined>(undefined) + const [characterOverride, setProfileOverride] = useState | undefined>(undefined) const externalLink = props.message.document.meta?.apObjectRef // Link to external message useEffect(() => { if (!(client && props.message.document.body.profileOverride?.profileID)) return client.api - .getProfileByID(props.message.document.body.profileOverride?.profileID, props.message.author) + .getProfile(props.message.document.body.profileOverride?.profileID, props.message.author) .then((profile) => { setProfileOverride(profile ?? undefined) }) @@ -58,7 +54,7 @@ export const OneLineMessageView = (props: OneLineMessageViewProps): JSX.Element avatarURL={props.message.authorUser?.profile?.avatar} identiconSource={props.message.author} avatarOverride={ - characterOverride?.document.body.avatar ?? props.message.document.body.profileOverride?.avatar + characterOverride?.parsedDoc.body.avatar ?? props.message.document.body.profileOverride?.avatar } sx={{ width: { xs: '38px', sm: '48px' }, diff --git a/app/src/components/Message/PlainMessageView.tsx b/app/src/components/Message/PlainMessageView.tsx new file mode 100644 index 00000000..27f1060d --- /dev/null +++ b/app/src/components/Message/PlainMessageView.tsx @@ -0,0 +1,29 @@ +import { Typography } from '@mui/material' +import { type Message, type PlaintextMessageSchema, type RerouteMessageSchema } from '@concrnt/worldlib' +import { MessageViewBase } from './MessageViewBase' + +export interface PlainMessageViewProps { + message: Message + rerouted?: Message + userCCID?: string + beforeMessage?: JSX.Element + lastUpdated?: number + forceExpanded?: boolean + clipHeight?: number + simple?: boolean + additionalMenuItems?: JSX.Element | JSX.Element[] +} + +export const PlainMessageView = (props: PlainMessageViewProps): JSX.Element => { + return ( + + + {props.message.document.body.body} + + + ) +} diff --git a/src/components/Message/PostedStreams.tsx b/app/src/components/Message/PostedStreams.tsx similarity index 91% rename from src/components/Message/PostedStreams.tsx rename to app/src/components/Message/PostedStreams.tsx index 32ae23c1..9bf3c20a 100644 --- a/src/components/Message/PostedStreams.tsx +++ b/app/src/components/Message/PostedStreams.tsx @@ -4,7 +4,7 @@ import { Schemas, type MarkdownMessageSchema, type RerouteMessageSchema -} from '@concurrent-world/client' +} from '@concrnt/worldlib' import { Box, Tooltip } from '@mui/material' import { useCallback, useMemo, useRef } from 'react' @@ -32,17 +32,17 @@ export const PostedStreams = (props: PostedStreamsProps): JSX.Element => { const timelineDrawer = useTimelineDrawer() - const postedStreams = useMemo(() => { - const streams = - props.message.postedStreams?.filter( - (stream) => - (stream.schema === Schemas.communityTimeline && - (stream.author === client.ccid || stream.indexable)) || - stream.schema === Schemas.emptyTimeline || - stream.schema === Schemas.subprofileTimeline || - allKnownTimelines.map((t) => t.id).includes(stream.id) + const postedTimelines = useMemo(() => { + const timelines = + props.message.postedTimelines?.filter( + (timeline) => + (timeline.schema === Schemas.communityTimeline && + (timeline.author === client.ccid || timeline.indexable)) || + timeline.schema === Schemas.emptyTimeline || + timeline.schema === Schemas.subprofileTimeline || + allKnownTimelines.map((t) => t.id).includes(timeline.id) ) ?? [] - const uniq = [...new Set(streams)] + const uniq = [...new Set(timelines)] return uniq }, [props.message]) @@ -77,7 +77,7 @@ export const PostedStreams = (props: PostedStreamsProps): JSX.Element => { ml: 'auto' }} > - {postedStreams.length === 0 && ( + {postedTimelines.length === 0 && ( { }} /> )} - {postedStreams.map((e) => { + {postedTimelines.map((e) => { const isPrivate = isPrivateTimeline(e) switch (e.schema) { @@ -94,7 +94,7 @@ export const PostedStreams = (props: PostedStreamsProps): JSX.Element => { return ( @@ -38,7 +38,7 @@ export const ReplyMessageFrame = (props: ReplyMessageFrameProp): JSX.Element => <> {replyTo && } - (null) - const [character, setProfile] = useState | null>(null) + const [character, setProfile] = useState | null>(null) const confirm = useConfirm() const profileOverride = props.message.document.body.profileOverride const avatarURL = - character?.document.body.avatar ?? profileOverride?.avatar ?? props.message.authorUser?.profile?.avatar + character?.parsedDoc.body.avatar ?? profileOverride?.avatar ?? props.message.authorUser?.profile?.avatar const username = - character?.document.body.username ?? + character?.parsedDoc.body.username ?? profileOverride?.username ?? props.message.authorUser?.profile?.username ?? 'Anonymous' @@ -45,7 +46,7 @@ export const RerouteMessageFrame = (props: RerouteMessageFrameProp): JSX.Element useEffect(() => { if (profileOverride?.profileID) { - client.api.getProfileByID(profileOverride.profileID, props.message.author).then((character) => { + client.api.getProfile(profileOverride.profileID, props.message.author).then((character) => { setProfile(character ?? null) }) } diff --git a/src/components/MessageSkeleton.tsx b/app/src/components/MessageSkeleton.tsx similarity index 100% rename from src/components/MessageSkeleton.tsx rename to app/src/components/MessageSkeleton.tsx diff --git a/src/components/Migrator.tsx b/app/src/components/Migrator.tsx similarity index 96% rename from src/components/Migrator.tsx rename to app/src/components/Migrator.tsx index 2865d4ed..4c98bc96 100644 --- a/src/components/Migrator.tsx +++ b/app/src/components/Migrator.tsx @@ -1,4 +1,5 @@ -import { Client, type CoreDomain } from '@concurrent-world/client' +import { Client } from '@concrnt/worldlib' +import { Domain } from '@concrnt/client' import { useEffect, useState } from 'react' import { useClient } from '../context/ClientContext' import { Box, Typography, Avatar, TextField, Stepper, Step, StepLabel, StepContent, Button } from '@mui/material' @@ -12,9 +13,9 @@ import { type JobRequest } from '../model' export function Migrator(): JSX.Element { const { client } = useClient() - const [currentDomain, setCurrentDomain] = useState(null) + const [currentDomain, setCurrentDomain] = useState(null) const [destFqdn, setDestFqdn] = usePersistent('migrator-dest-fqdn', '') - const [destinationDomain, setDestinationDomain] = useState(null) + const [destinationDomain, setDestinationDomain] = useState(null) const activeStep = parseInt(location.hash.replace('#', '')) || 0 const setActiveStep = (step: number): void => { window.location.hash = step.toString() @@ -88,6 +89,7 @@ export function Migrator(): JSX.Element { fullWidth color="primary" onClick={() => { + console.log(client.ccid, client.keyPair) jumpToDomainRegistration( client.ccid!, client.keyPair!.privatekey, @@ -152,6 +154,7 @@ export function Migrator(): JSX.Element { <> 実際にすべてのデータが読み込まれるまで時間がかかる場合があります。 { setImported(err === '') @@ -241,6 +244,7 @@ export function Migrator(): JSX.Element { - - ) -} - -export function V0RepositoryImportButton(): JSX.Element { - const { client } = useClient() - - const fileInputRef = useRef(null) - const [importStatus, setImportStatus] = useState<'idle' | 'loading' | 'success' | 'error'>('idle') - - const importFromVersion0 = (backup: v0data): void => { - if (importStatus !== 'idle') return - if (backup.content.length === 0) return - if (!client.keyPair) return - if (!client.ccid) return - if (!client.user?.homeTimeline) return - - setImportStatus('loading') - - const logs = [] - for (const message of backup.content) { - const payload = JSON.parse(message.payload) - - if ( - payload.schema !== - 'https://raw.githubusercontent.com/totegamma/concurrent-schemas/master/messages/note/0.0.1.json' - ) { - console.error('unsupported schema', payload.schema) - continue - } - - const doc: CCDocument.Message = { - signer: client.ccid, - type: 'message', - schema: Schemas.markdownMessage, - body: payload.body, - meta: payload.meta, - timelines: [client.user.homeTimeline], - signedAt: payload.signedAt - } - - const document = JSON.stringify(doc) - const signature = Sign(client.keyPair.privatekey, document) - - const entries = [message.id, client.ccid, signature, document] - const entry = entries.join(' ') - logs.push(entry) - } - - const log = logs.join('\n') - client.api - .fetchWithCredential( - client.api.host, - '/api/v1/repository', - { - method: 'POST', - headers: { - 'Content-Type': 'text/plain' - }, - body: log - }, - 1000 * 60 * 10 // 10 minutes - ) - .then((res) => { - if (res.ok) { - console.log('imported') - res.json().then((data) => { - console.log(data) - }) - setImportStatus('success') - } else { - console.error('failed to import') - setImportStatus('error') - } - }) - } - - return ( - <> - { - const file = e.target.files?.[0] - if (file) { - const reader = new FileReader() - reader.onload = (e) => { - const contents = e.target?.result - if (contents) { - const data = JSON.parse(contents as string) as v0data - importFromVersion0(data) - } - } - reader.readAsText(file) - } - }} - /> - + {importLog && ( + + インポートログ + {importLog} + + )} ) } @@ -249,13 +134,9 @@ export function RepositoryExportButton(): JSX.Element { useEffect(() => { if (!client.user) return - client.api - .fetchWithCredential(client.host, '/api/v1/repositories/sync', {}) - .then((res) => res.json()) - .then((data) => { - console.log(data.content) - setSyncStatus(data.content) - }) + client.api.fetchWithCredential(client.host, '/api/v1/repositories/sync', {}).then((data) => { + setSyncStatus(data.content) + }) }, []) const isDateValid = syncStatus && new Date(syncStatus?.latestOnFile).getTime() > 0 @@ -277,7 +158,6 @@ export function RepositoryExportButton(): JSX.Element { onClick={() => client.api .fetchWithCredential(client.host, '/api/v1/repositories/sync', {}) - .then((res) => res.json()) .then((data) => { setSyncStatus(data.content) }) @@ -304,10 +184,9 @@ export function RepositoryExportButton(): JSX.Element { .fetchWithCredential(client.host, '/api/v1/repositories/sync', { method: 'POST' }) - .then((res) => res.json()) .then((data) => { console.log(data) - setSyncStatus(data.content) + setSyncStatus(data) enqueueSnackbar('更新をリクエストしました。しばらくお待ちください。', { variant: 'info' }) @@ -333,32 +212,29 @@ export function RepositoryExportButton(): JSX.Element { ) diff --git a/src/components/Settings/APSettings.tsx b/app/src/components/Settings/APSettings.tsx similarity index 63% rename from src/components/Settings/APSettings.tsx rename to app/src/components/Settings/APSettings.tsx index 126ab056..357235aa 100644 --- a/src/components/Settings/APSettings.tsx +++ b/app/src/components/Settings/APSettings.tsx @@ -1,4 +1,4 @@ -import { Box, Button, Divider, IconButton, TextField, Typography } from '@mui/material' +import { Alert, AlertTitle, Box, Button, Divider, IconButton, TextField, Typography } from '@mui/material' import { useEffect, useState } from 'react' import { useClient } from '../../context/ClientContext' import { type ApEntity } from '../../model' @@ -12,7 +12,7 @@ import { useSnackbar } from 'notistack' import SettingsIcon from '@mui/icons-material/Settings' import { StreamPicker } from '../ui/StreamPicker' import { useGlobalState } from '../../context/GlobalState' -import { type Timeline } from '@concurrent-world/client' +import { CommunityTimelineSchema, Schemas, type Timeline } from '@concrnt/worldlib' export const APSettings = (): JSX.Element => { const { client } = useClient() @@ -26,8 +26,45 @@ export const APSettings = (): JSX.Element => { const [newAlias, setNewAlias] = useState('') const [listenTimelines, setListenTimelines] = useState>>([]) const { allKnownTimelines } = useGlobalState() + const [apTimeline, setApTimeline] = useState | null>(null) + const [meta, setMeta] = useState({}) + + console.log('apTimeline', apTimeline) + + const timelineNGReason = (() => { + if (!client.ccid) return null + if (!apTimeline) return 'Activitypub受信用タイムラインが見つかりません' + if (apTimeline.policy === 'https://policy.concrnt.world/t/inline-read-write.json') { + if (!apTimeline.policyParams.writer.includes(client.ccid)) { + return '自身の書き込み権限が設定されていません' + } + if (!apTimeline.policyParams.writer.includes(meta.metadata?.proxyCCID)) { + return '受信用botアカウントの書き込み権限が設定されていません' + } + } + + return null + })() useEffect(() => { + client.api + .fetchWithCredential(client.host, `/ap/api/settings`, {}) + .then(async (data: any) => { + const requests = await Promise.allSettled( + data.listen_timelines.map((id: string) => { + return client.getTimeline(id) + }) + ) + + const fulfilled = requests.filter((r) => r.status === 'fulfilled') as Array< + PromiseFulfilledResult> + > + setListenTimelines(fulfilled.map((r) => r.value)) + }) + .catch((e) => { + console.error(e) + }) + const requestOptions = { method: 'GET', headers: { @@ -36,38 +73,29 @@ export const APSettings = (): JSX.Element => { } client.api - .fetchWithCredential(client.api.host, `/ap/api/entity/${client.ccid}`, requestOptions) - .then(async (res) => await res.json()) + .fetchWithCredential(client.host, `/ap/api/entity/${client.ccid}`, requestOptions) .then((data) => { setEntity(data.content) - setAliases(data.content.aliases ?? []) + if (data) setAliases(data.content.aliases ?? []) }) - .catch((e) => { + .catch((_) => { setEntity(null) }) - }, []) - useEffect(() => { - client.api - .fetchWithCredential(client.api.host, `/ap/api/settings`, {}) - .then(async (res) => await res.json()) - .then((data) => { - setListenTimelines( - allKnownTimelines.filter( - (t) => - (t.cacheKey && data.content.listen_timelines.includes(t.cacheKey)) || - data.content.listen_timelines.includes(t.id) - ) - ) - }) - .catch((e) => { - console.error(e) + client.getTimeline('world.concrnt.t-ap@' + client.ccid).then((t) => { + setApTimeline(t) + }) + + fetch(`https://${client.host}/ap/nodeinfo/2.0`) + .then((res) => res.json()) + .then((res) => { + setMeta(res) }) - }, [allKnownTimelines]) + }, []) const updateSettings = (): void => { client.api - .fetchWithCredential(client.api.host, `/ap/api/settings`, { + .fetchWithCredential(client.host, `/ap/api/settings`, { method: 'POST', headers: { 'content-type': 'application/json' @@ -85,15 +113,14 @@ export const APSettings = (): JSX.Element => { const inquery = (url: string): void => { client.api - .fetchWithCredential(client.api.host, `/ap/api/import?note=${encodeURIComponent(url)}`, { + .fetchWithCredential(client.host, `/ap/api/import?note=${encodeURIComponent(url)}`, { method: 'GET', headers: { 'content-type': 'application/json' } }) - .then(async (res) => await res.json()) - .then((data) => { - navigate(`/${data.content.author}/${data.content.id}`) + .then((data: any) => { + navigate(`/${data.author}/${data.id}`) }) } @@ -122,7 +149,7 @@ export const APSettings = (): JSX.Element => { width="100%" alignItems="center" > - + @{entity.id}@{client.host} @@ -135,7 +162,7 @@ export const APSettings = (): JSX.Element => { }} /> - + { @@ -145,6 +172,62 @@ export const APSettings = (): JSX.Element => { + {timelineNGReason && ( + { + if (meta.metadata?.proxyCCID === undefined) + alert('サーバー設定が不正です。管理者に問い合わせてください') + + const base = apTimeline?.policyParams.writer ?? [] + const writers = [...new Set([...base, client.ccid, meta.metadata?.proxyCCID])] + + client.api + .upsertTimeline( + Schemas.communityTimeline, + { + name: apTimeline?.document?.body?.name ?? 'ActivityPub', + shortname: apTimeline?.document?.body?.shortname ?? 'activitypub', + description: + apTimeline?.document?.body?.description ?? + 'ActivityPub home stream' + }, + { + semanticID: 'world.concrnt.t-ap', + indexable: false, + policy: 'https://policy.concrnt.world/t/inline-read-write.json', + policyParams: JSON.stringify({ + isWritePublic: false, + isReadPublic: true, + writer: writers, + reader: [] + }) + } + ) + .then((_) => { + window.location.reload() + }) + .catch((e) => { + alert(e) + }) + }} + > + 修復 + + } + > + タイムラインの設定に問題があります + {timelineNGReason} + + )} + )} @@ -223,7 +306,7 @@ export const APSettings = (): JSX.Element => { onClick={() => { client.api .fetchWithCredential( - client.api.host, + client.host, `/ap/api/resolve/${encodeURIComponent(newAlias)}`, { method: 'GET', @@ -232,13 +315,12 @@ export const APSettings = (): JSX.Element => { } } ) - .then(async (res) => await res.json()) - .then((data) => { - if (data.content.id) { - const newAliases = [...new Set([...aliases, data.content.id])] + .then((data: any) => { + if (data.id) { + const newAliases = [...new Set([...aliases, data.id])] client.api - .fetchWithCredential(client.api.host, `/ap/api/entities/aliases`, { + .fetchWithCredential(client.host, `/ap/api/entities/aliases`, { method: 'POST', headers: { 'content-type': 'application/json' @@ -247,8 +329,7 @@ export const APSettings = (): JSX.Element => { aliases: newAliases }) }) - .then(async (res) => await res.json()) - .then((data) => { + .then((_) => { enqueueSnackbar('更新しました', { variant: 'success' }) @@ -277,7 +358,7 @@ export const APSettings = (): JSX.Element => { remove={(body) => { const newAliases = aliases.filter((a) => a !== body.URL) client.api - .fetchWithCredential(client.api.host, `/ap/api/entities/aliases`, { + .fetchWithCredential(client.host, `/ap/api/entities/aliases`, { method: 'POST', headers: { 'content-type': 'application/json' @@ -286,8 +367,7 @@ export const APSettings = (): JSX.Element => { aliases: newAliases }) }) - .then(async (res) => await res.json()) - .then((data) => { + .then((_) => { enqueueSnackbar('更新しました', { variant: 'success' }) diff --git a/src/components/Settings/Emoji.tsx b/app/src/components/Settings/Emoji.tsx similarity index 100% rename from src/components/Settings/Emoji.tsx rename to app/src/components/Settings/Emoji.tsx diff --git a/src/components/Settings/General.tsx b/app/src/components/Settings/General.tsx similarity index 89% rename from src/components/Settings/General.tsx rename to app/src/components/Settings/General.tsx index dcd5a34a..734636f7 100644 --- a/src/components/Settings/General.tsx +++ b/app/src/components/Settings/General.tsx @@ -18,13 +18,14 @@ import { usePreference } from '../../context/PreferenceContext' import { useClient } from '../../context/ClientContext' import { useEffect, useState } from 'react' import { useSnackbar } from 'notistack' -import { IssueJWT, Schemas } from '@concurrent-world/client' +import { IssueJWT } from '@concrnt/client' +import { Schemas } from '@concrnt/worldlib' import { useTranslation } from 'react-i18next' -import { type JobRequest, type NotificationSubscription } from '../../model' +import { type NotificationSubscription } from '../../model' +import ExpandMoreIcon from '@mui/icons-material/ExpandMore' import TextIncreaseIcon from '@mui/icons-material/TextIncrease' import TextDecreaseIcon from '@mui/icons-material/TextDecrease' -import { useConfirm } from '../../context/Confirm' export const GeneralSettings = (): JSX.Element => { const { client } = useClient() @@ -53,11 +54,9 @@ export const GeneralSettings = (): JSX.Element => { const [reload, setReload] = useState(0) - const confirm = useConfirm() - useEffect(() => { setCurrentLanguage(i18n.resolvedLanguage || 'en') - fetch(`https://${client.api.host}/api/v1/domain`, { + fetch(`https://${client.host}/api/v1/domain`, { cache: 'no-cache' }).then((res) => { res.json().then((data) => { @@ -65,12 +64,10 @@ export const GeneralSettings = (): JSX.Element => { }) }) client.api - .fetchWithCredential(client.host, `/api/v1/notification/${client.ccid}/concrnt.world`, {}) - .then((res) => { - res.json().then((data) => { - setNotification(data.content) - setSchemas(data.content.schemas) - }) + .fetchWithCredential(client.host, `/api/v1/notification/${client.ccid}/concrnt.world`, {}) + .then((data) => { + setNotification(data.content) + setSchemas(data.content.schemas ?? []) }) }, [reload]) @@ -491,8 +488,8 @@ export const GeneralSettings = (): JSX.Element => { )} {!enableConcord && ( - - + + }> Concord Network(プレビュー) @@ -527,12 +524,12 @@ export const GeneralSettings = (): JSX.Element => { {invitationCode === '' ? ( - - - ) } diff --git a/src/components/Settings/Identity.tsx b/app/src/components/Settings/Identity.tsx similarity index 78% rename from src/components/Settings/Identity.tsx rename to app/src/components/Settings/Identity.tsx index 212e6c79..49b5909a 100644 --- a/src/components/Settings/Identity.tsx +++ b/app/src/components/Settings/Identity.tsx @@ -23,20 +23,21 @@ import Tilt from 'react-parallax-tilt' import { Passport } from '../theming/Passport' import { useEffect, useMemo, useState } from 'react' import { useClient } from '../../context/ClientContext' -import { type Key } from '@concurrent-world/client/dist/types/model/core' import { usePreference } from '../../context/PreferenceContext' import { useTranslation } from 'react-i18next' import { Codeblock } from '../ui/Codeblock' import { KeyCard } from '../ui/KeyCard' -import { Sign, type Identity } from '@concurrent-world/client' +import { Sign, type Identity, Key, MasterKeyAuthProvider } from '@concrnt/client' import { enqueueSnackbar } from 'notistack' import { useGlobalState } from '../../context/GlobalState' import ExpandMoreIcon from '@mui/icons-material/ExpandMore' import { Node, type NodeProps } from '../ui/TreeGraph' -import { type ConcurrentTheme } from '../../model' +import { JobRequest, type ConcurrentTheme } from '../../model' import { CCIconButton } from '../ui/CCIconButton' import ContentPasteIcon from '@mui/icons-material/ContentPaste' +import { EntityMetaEditor } from '../EntityMetaEditor' +import { useConfirm } from '../../context/Confirm' interface CertChain { id: string @@ -100,7 +101,7 @@ export const KeyTree = (props: KeyTreeProps): JSX.Element => { const theme = useTheme() const { t } = useTranslation('', { keyPrefix: 'settings.identity' }) - const key: Key = props.certChain.key ?? { + const keyBase: Omit = props.certChain.key ?? { id: props.certChain.id, root: props.certChain.id, parent: 'cck1null', @@ -110,7 +111,9 @@ export const KeyTree = (props: KeyTreeProps): JSX.Element => { validUntil: 'null' } - const currentKey = client.api.ckid ?? client.api.ccid + const key = Object.setPrototypeOf(keyBase, Key.prototype) + + const currentKey = client.ckid ?? client.ccid const [target, setTarget] = useState(null) const [anchorEl, setAnchorEl] = useState(null) @@ -212,12 +215,14 @@ export const IdentitySettings = (): JSX.Element => { const [certChain, setCertChain] = useState(null) const [devMode] = usePreference('devMode') - const subkey = client.api.ckid + const subkey = client.ckid const [forceUpdate, setForceUpdate] = useState(0) const forceUpdateCallback = (): void => { setForceUpdate(forceUpdate + 1) } + const confirm = useConfirm() + const { t } = useTranslation('', { keyPrefix: 'settings.identity' }) const signature = useMemo(() => { @@ -299,16 +304,8 @@ export const IdentitySettings = (): JSX.Element => { - - - } - > + + }> {client.user?.alias ? ( アカウントにはエイリアス{client.user.alias}が設定されています。 @@ -382,7 +379,8 @@ _concrnt.${aliasDraft} TXT "hint=${client.host}"`} action={ { - navigator.clipboard.writeText(client.api.privatekey ?? '') + const authProvider = client.api.authProvider as MasterKeyAuthProvider + navigator.clipboard.writeText(authProvider.privatekey) enqueueSnackbar('Copied', { variant: 'info' }) }} > @@ -429,20 +427,60 @@ _concrnt.${aliasDraft} TXT "hint=${client.host}"`} {!subkey && !identity && {t('loginType.secret')}} - - - - } - label={t('hideSubKey')} - /> - - + + }> + ドメイン{client.host}に届けて出ている情報 + + + + + + + + }> + + Danger Zone + + + + + + }} > セッション情報 + + + + } + label={t('hideSubKey')} + /> + + {certChain && } diff --git a/app/src/components/Settings/ImportExport.tsx b/app/src/components/Settings/ImportExport.tsx new file mode 100644 index 00000000..8a7ea9ca --- /dev/null +++ b/app/src/components/Settings/ImportExport.tsx @@ -0,0 +1,44 @@ +import { Divider, Tab, Tabs, Typography } from '@mui/material' +import { useTranslation } from 'react-i18next' +import { RepositoryExportButton, RepositoryImportButton } from '../RepositoryManageButtons' +import { Migrator } from '../Migrator' +import { useParams, useNavigate } from 'react-router-dom' + +export function ImportExport(): JSX.Element { + const { t } = useTranslation('', { keyPrefix: 'settings.importexport' }) + const { tab } = useParams() + const navigate = useNavigate() + + return ( + <> + { + navigate(`/settings/importexport/${v}`) + }} + > + + + + + + {tab === 'manage' && ( + <> + {t('export')} + + + + + {t('import')} + + + )} + + {tab === 'migrate' && } + + ) +} diff --git a/src/components/Settings/Index.tsx b/app/src/components/Settings/Index.tsx similarity index 93% rename from src/components/Settings/Index.tsx rename to app/src/components/Settings/Index.tsx index 0e35e96f..ac996d90 100644 --- a/src/components/Settings/Index.tsx +++ b/app/src/components/Settings/Index.tsx @@ -20,9 +20,9 @@ import { useMemo } from 'react' // @ts-expect-error vite dynamic import import buildTime from '~build/time' // @ts-expect-error vite dynamic import -import { branch, sha } from '~build/info' +import { branch, sha } from '~build/git' import { useGlobalState } from '../../context/GlobalState' -import { type Identity } from '@concurrent-world/client' +import { type Identity } from '@concrnt/client' const branchName = branch || window.location.host.split('.')[0] @@ -42,6 +42,19 @@ export function SettingsIndex(): JSX.Element { caches.delete(name) }) }) + if (window.indexedDB) { + const req = window.indexedDB.deleteDatabase('concrnt-client') + console.log(req) + req.onsuccess = () => { + // reload + console.log('deleted') + window.location.reload() + } + req.onerror = () => { + console.log('failed to delete') + } + } + enqueueSnackbar('Cache deleted', { variant: 'success' }) } else { enqueueSnackbar('No cache to delete', { variant: 'info' }) diff --git a/src/components/Settings/Jobs.tsx b/app/src/components/Settings/Jobs.tsx similarity index 97% rename from src/components/Settings/Jobs.tsx rename to app/src/components/Settings/Jobs.tsx index df51b023..1cf8bbc1 100644 --- a/src/components/Settings/Jobs.tsx +++ b/app/src/components/Settings/Jobs.tsx @@ -83,13 +83,13 @@ export function Jobs(): JSX.Element { const [jobScheduled, setJobScheduled] = useState(1) const loadJobs = async (): Promise => { - const res = await client?.api.fetchWithCredential(client.host, '/api/v1/jobs', { - method: 'GET' - }) - if (res.ok) { - const json = await res.json() - setJobs(json.content) - } + client?.api + .fetchWithCredential(client.host, '/api/v1/jobs', { + method: 'GET' + }) + .then(async (data) => { + setJobs(data.content ?? []) + }) } useEffect(() => { diff --git a/src/components/Settings/LoginQR.tsx b/app/src/components/Settings/LoginQR.tsx similarity index 97% rename from src/components/Settings/LoginQR.tsx rename to app/src/components/Settings/LoginQR.tsx index 7317f478..09662aea 100644 --- a/src/components/Settings/LoginQR.tsx +++ b/app/src/components/Settings/LoginQR.tsx @@ -1,7 +1,7 @@ import { Alert, Button, Tab, Tabs, Typography } from '@mui/material' import SubkeyInfo from '../SubkeyInfo' import { useState } from 'react' -import { ComputeCKID, GenerateIdentity } from '@concurrent-world/client' +import { ComputeCKID, GenerateIdentity } from '@concrnt/client' import { useClient } from '../../context/ClientContext' import { useTranslation } from 'react-i18next' diff --git a/src/components/Settings/LogoutButton/index.tsx b/app/src/components/Settings/LogoutButton/index.tsx similarity index 95% rename from src/components/Settings/LogoutButton/index.tsx rename to app/src/components/Settings/LogoutButton/index.tsx index d990b2b3..f0814be2 100644 --- a/src/components/Settings/LogoutButton/index.tsx +++ b/app/src/components/Settings/LogoutButton/index.tsx @@ -2,7 +2,7 @@ import { useCallback, useEffect, useState } from 'react' import { Box, Button, Modal, Typography, useTheme } from '@mui/material' import { useNavigate } from 'react-router-dom' import { useTranslation } from 'react-i18next' -import { type Identity } from '@concurrent-world/client' +import { type Identity } from '@concrnt/client' import { TestMasterkey } from '../../SwitchMasterToSub' export const LogoutButton = (): JSX.Element => { @@ -19,6 +19,9 @@ export const LogoutButton = (): JSX.Element => { for (const key in localStorage) { localStorage.removeItem(key) } + if (window.indexedDB) { + window.indexedDB.deleteDatabase('concrnt-client') + } setOpenLogoutModal(false) navigate('/welcome') } diff --git a/src/components/Settings/Media.tsx b/app/src/components/Settings/Media.tsx similarity index 96% rename from src/components/Settings/Media.tsx rename to app/src/components/Settings/Media.tsx index f2de712a..0c62dd27 100644 --- a/src/components/Settings/Media.tsx +++ b/app/src/components/Settings/Media.tsx @@ -42,8 +42,8 @@ interface File { interface FileResponse { content: File[] - next: string | undefined - prev: string | undefined + next?: string + prev?: string } export const MediaSettings = (): JSX.Element => { @@ -79,12 +79,8 @@ export const MediaSettings = (): JSX.Element => { useEffect(() => { if (storageProvider !== 'domain') return const url = itr.cursor ? `/storage/files?limit=9&${itr.mode}=${itr.cursor}` : '/storage/files?limit=9' - client.api.fetchWithCredential(client.host, url, {}).then((res) => { - if (res.ok) { - res.json().then((resp) => { - setFileResponse(resp) - }) - } + client.api.fetchWithCredential(client.host, url, {}).then((data) => { + if (data) setFileResponse(data) }) }, [storageProvider, itr]) @@ -93,14 +89,12 @@ export const MediaSettings = (): JSX.Element => { .fetchWithCredential(client.host, `/storage/file/${id}`, { method: 'DELETE' }) - .then((res) => { - if (res.ok) { - setFileResponse({ - content: fileResponse?.content.filter((e) => e.id !== id) ?? [], - next: fileResponse?.next, - prev: fileResponse?.prev - }) - } + .then((_) => { + setFileResponse({ + content: fileResponse?.content.filter((e) => e.id !== id) ?? [], + next: fileResponse?.next, + prev: fileResponse?.prev + }) }) } diff --git a/src/components/Settings/Profile.tsx b/app/src/components/Settings/Profile.tsx similarity index 97% rename from src/components/Settings/Profile.tsx rename to app/src/components/Settings/Profile.tsx index ff047e4e..1931b08d 100644 --- a/src/components/Settings/Profile.tsx +++ b/app/src/components/Settings/Profile.tsx @@ -18,7 +18,6 @@ import { useSnackbar } from 'notistack' import { useTranslation } from 'react-i18next' import { type ProfileSchema, - type CoreProfile, type Schema, type BadgeRef, Schemas, @@ -28,7 +27,8 @@ import { type User, type Association, type ReadAccessRequestAssociationSchema -} from '@concurrent-world/client' +} from '@concrnt/worldlib' +import { Profile } from '@concrnt/client' import { useEffect, useState } from 'react' import { CCDrawer } from '../ui/CCDrawer' import { CCEditor } from '../ui/cceditor' @@ -60,14 +60,14 @@ export const ProfileSettings = (): JSX.Element => { const path = useLocation() const hash = path.hash.replace('#', '') - const [allProfiles, setAllProfiles] = useState>>([]) + const [allProfiles, setAllProfiles] = useState>>([]) const [openProfileEditor, setOpenProfileEditor] = useState(false) const [openReaderEditor, setOpenReaderEditor] = useState<((_: User[]) => void) | null>(null) const [schemaURLDraft, setSchemaURLDraft] = useState('https://schema.concrnt.world/p/basic.json') const [schemaURL, setSchemaURL] = useState(null) - const [latestProfile, setLatestProfile] = useState(client.user?.profile) + const [latestProfile, setLatestProfile] = useState(client.user?.profile) const [subprofileDraft, setSubprofileDraft] = useState(null) const [badges, setBadges] = useState([]) @@ -89,15 +89,17 @@ export const ProfileSettings = (): JSX.Element => { if (!client?.ccid) return if (!client.user?.profile) return - client.api.getProfileBySemanticID('world.concrnt.p', client.ccid).then((profile) => { - setLatestProfile(profile?.document.body) - client.api.getProfiles({ author: client.ccid }).then((profiles) => { - const subprofiles = (profiles ?? []).filter((p) => p.id !== profile?.id) - setAllProfiles(subprofiles) + client.api + .getProfileBySemanticID('world.concrnt.p', client.ccid, { cache: 'no-cache' }) + .then((profile) => { + setLatestProfile(profile?.parsedDoc.body) + client.api.getProfiles({ author: client.ccid }).then((profiles) => { + const subprofiles = (profiles ?? []).filter((p) => p.id !== profile?.id) + setAllProfiles(subprofiles) + }) }) - }) - client.getTimeline(client.user.homeTimeline).then((timeline) => { + client.getTimeline(client.user.homeTimeline, { cache: 'no-cache' }).then((timeline) => { setHomeTimeline(timeline) if (!timeline) return timeline.getAssociations().then((assocs) => { @@ -153,7 +155,7 @@ export const ProfileSettings = (): JSX.Element => { > { enqueueSnackbar(t('updated'), { variant: 'success' }) }} diff --git a/src/components/Settings/Sound.tsx b/app/src/components/Settings/Sound.tsx similarity index 100% rename from src/components/Settings/Sound.tsx rename to app/src/components/Settings/Sound.tsx diff --git a/src/components/Settings/Theme.tsx b/app/src/components/Settings/Theme.tsx similarity index 99% rename from src/components/Settings/Theme.tsx rename to app/src/components/Settings/Theme.tsx index bf7f8049..1b92bcd0 100644 --- a/src/components/Settings/Theme.tsx +++ b/app/src/components/Settings/Theme.tsx @@ -13,7 +13,7 @@ import { import { ThemeCreator } from '../ThemeCreator' import { useEffect, useMemo, useState } from 'react' import { useTranslation } from 'react-i18next' -import { type User } from '@concurrent-world/client' +import { type User } from '@concrnt/worldlib' import { useClient } from '../../context/ClientContext' import { usePreference } from '../../context/PreferenceContext' import { type ConcurrentTheme } from '../../model' diff --git a/src/components/Stream/Card.tsx b/app/src/components/Stream/Card.tsx similarity index 95% rename from src/components/Stream/Card.tsx rename to app/src/components/Stream/Card.tsx index c23bfdc2..eee431f0 100644 --- a/src/components/Stream/Card.tsx +++ b/app/src/components/Stream/Card.tsx @@ -5,7 +5,7 @@ import { CCWallpaper } from '../ui/CCWallpaper' import { WatchButton } from '../WatchButton' interface StreamCardProps { - streamID: string + timelineFQID: string name: string description: string banner: string @@ -46,7 +46,7 @@ export function StreamCard(props: StreamCardProps): JSX.Element { ) : ( <> - + - + )} diff --git a/src/components/StreamInfo.tsx b/app/src/components/StreamInfo.tsx similarity index 90% rename from src/components/StreamInfo.tsx rename to app/src/components/StreamInfo.tsx index ec675350..f671ff8b 100644 --- a/src/components/StreamInfo.tsx +++ b/app/src/components/StreamInfo.tsx @@ -19,7 +19,7 @@ import { type Association, type ReadAccessRequestAssociationSchema, Schemas -} from '@concurrent-world/client' +} from '@concrnt/worldlib' import IosShareIcon from '@mui/icons-material/IosShare' import { CCEditor, type CCEditorError } from './ui/cceditor' import { useSnackbar } from 'notistack' @@ -31,6 +31,7 @@ import { CCIconButton } from './ui/CCIconButton' import { CCComboBox } from './ui/CCComboBox' import { useConfirm } from '../context/Confirm' import { WatchRequestAcceptButton } from './WatchRequestAccpetButton' +import { IsCCID, IsCSID } from '@concrnt/client' export interface StreamInfoProps { id: string @@ -43,14 +44,14 @@ export function StreamInfo(props: StreamInfoProps): JSX.Element { const { client } = useClient() const confirm = useConfirm() const { enqueueSnackbar } = useSnackbar() - const [stream, setStream] = useState>() - const isAuthor = stream?.author === client.ccid + const [timeline, setTimeline] = useState>() + const isAuthor = timeline?.author === client.ccid const [visible, setVisible] = useState(false) const [schemaDraft, setSchemaDraft] = useState('') const [policyDraft, setPolicyDraft] = useState(undefined) - const [documentBody, setDocumentBody] = useState(stream?.document.body) + const [documentBody, setDocumentBody] = useState(timeline?.document.body) const [policyParams, setPolicyParams] = useState() const [policyErrors, setPolicyErrors] = useState() @@ -58,11 +59,13 @@ export function StreamInfo(props: StreamInfoProps): JSX.Element { const [tab, setTab] = useState<'info' | 'edit'>('info') + const [update, setUpdate] = useState(0) + useEffect(() => { if (!props.id) return - client.getTimeline(props.id).then((e) => { + client.getTimeline(props.id, { cache: 'no-cache' }).then((e) => { if (!e) return - setStream(e) + setTimeline(e) setDocumentBody(e.document.body) setPolicyParams(JSON.stringify(e.policyParams)) setVisible(e.indexable) @@ -73,26 +76,36 @@ export function StreamInfo(props: StreamInfoProps): JSX.Element { setRequests(assocs.filter((e) => e.schema === Schemas.readAccessRequestAssociation)) }) }) - }, [props.id]) + }, [props.id, update]) const updateStream = useCallback(() => { - if (!stream) return + if (!timeline) return + + const opts: any = { + indexable: visible, + policy: policyDraft, + policyParams + } + + const split = props.id.split('@') + if (split.length === 2 && (IsCCID(split[1]) || IsCSID(split[1]))) { + opts.semanticID = split[0] + } else { + opts.id = timeline.id + } + client.api - .upsertTimeline(schemaDraft, documentBody, { - id: props.id, - indexable: visible, - policy: policyDraft, - policyParams - }) + .upsertTimeline(schemaDraft, documentBody, opts) .then((_) => { + setUpdate((e) => e + 1) enqueueSnackbar('更新しました', { variant: 'success' }) }) .catch((_) => { enqueueSnackbar('更新に失敗しました', { variant: 'error' }) }) - }, [client.api, stream, schemaDraft, props.id, visible, enqueueSnackbar, documentBody, policyDraft, policyParams]) + }, [client.api, timeline, schemaDraft, props.id, visible, enqueueSnackbar, documentBody, policyDraft, policyParams]) - if (!stream) { + if (!timeline) { return <>stream information not found } @@ -102,7 +115,7 @@ export function StreamInfo(props: StreamInfoProps): JSX.Element { return ( <> - {stream.document.body.name} - + {timeline.document.body.name} + { navigator.clipboard.writeText(`https://concrnt.world/timeline/${props.id}`) @@ -154,7 +167,7 @@ export function StreamInfo(props: StreamInfoProps): JSX.Element { {props.id} - {stream.document.body.description || 'まだ説明はありません'} + {timeline.document.body.description || 'まだ説明はありません'} {props.detailed && ( @@ -187,7 +200,7 @@ export function StreamInfo(props: StreamInfoProps): JSX.Element { { setRequests(requests.filter((e) => e.id !== request.id)) }} @@ -205,7 +218,7 @@ export function StreamInfo(props: StreamInfoProps): JSX.Element { flexWrap: 'wrap' }} > - + {props.writers && props.writers.length > 0 && ( <> diff --git a/src/components/StreamUserList.tsx b/app/src/components/StreamUserList.tsx similarity index 97% rename from src/components/StreamUserList.tsx rename to app/src/components/StreamUserList.tsx index 391b20d6..7fbf9f66 100644 --- a/src/components/StreamUserList.tsx +++ b/app/src/components/StreamUserList.tsx @@ -1,4 +1,4 @@ -import { type User } from '@concurrent-world/client' +import { type User } from '@concrnt/worldlib' import { useState } from 'react' import { Box, Link, Tab, Tabs } from '@mui/material' import { CCAvatar } from './ui/CCAvatar' diff --git a/src/components/SubProfileCard.tsx b/app/src/components/SubProfileCard.tsx similarity index 90% rename from src/components/SubProfileCard.tsx rename to app/src/components/SubProfileCard.tsx index 5ccc18dc..e736418a 100644 --- a/src/components/SubProfileCard.tsx +++ b/app/src/components/SubProfileCard.tsx @@ -1,4 +1,5 @@ -import { Schemas, type CoreProfile } from '@concurrent-world/client' +import { Schemas } from '@concrnt/worldlib' +import { Profile } from '@concrnt/client' import { Box, Chip, IconButton, Menu, Paper, Typography } from '@mui/material' import { CCWallpaper } from './ui/CCWallpaper' import { CCAvatar } from './ui/CCAvatar' @@ -13,14 +14,14 @@ import { ProfileProperties } from './ui/ProfileProperties' export interface SubProfileCardProps { showccid?: boolean - character: CoreProfile + character: Profile additionalMenuItems?: JSX.Element | JSX.Element[] children?: JSX.Element | JSX.Element[] resolveHint?: string } export const SubProfileCard = (props: SubProfileCardProps): JSX.Element => { - const isProfile = 'username' in props.character.document.body && 'avatar' in props.character.document.body + const isProfile = 'username' in props.character.parsedDoc.body && 'avatar' in props.character.parsedDoc.body const { enqueueSnackbar } = useSnackbar() const [menuAnchor, setMenuAnchor] = useState(null) @@ -39,7 +40,7 @@ export const SubProfileCard = (props: SubProfileCardProps): JSX.Element => { height: '80px', position: 'relative' }} - override={props.character.document.body.banner} + override={props.character.parsedDoc.body.banner} > {props.additionalMenuItems && ( { }} > { px={1} mb={1} > - {props.character.document.body.username} + {props.character.parsedDoc.body.username} {props.showccid ? ( { }} > diff --git a/src/components/SubkeyInfo.tsx b/app/src/components/SubkeyInfo.tsx similarity index 100% rename from src/components/SubkeyInfo.tsx rename to app/src/components/SubkeyInfo.tsx diff --git a/src/components/SubprofileCardWithEdit.tsx b/app/src/components/SubprofileCardWithEdit.tsx similarity index 95% rename from src/components/SubprofileCardWithEdit.tsx rename to app/src/components/SubprofileCardWithEdit.tsx index 5456ea70..8553d654 100644 --- a/src/components/SubprofileCardWithEdit.tsx +++ b/app/src/components/SubprofileCardWithEdit.tsx @@ -1,5 +1,4 @@ import { - type CoreProfile, type ProfileSchema, Schemas, type SubprofileTimelineSchema, @@ -7,7 +6,9 @@ import { type User, type Association, type ReadAccessRequestAssociationSchema -} from '@concurrent-world/client' +} from '@concrnt/worldlib' + +import { Profile } from '@concrnt/client' import { useClient } from '../context/ClientContext' import { useEffect, useState } from 'react' @@ -32,7 +33,7 @@ import { WatchRequestAcceptButton } from './WatchRequestAccpetButton' interface SubprofileCardWithEditProps { mainProfile: ProfileSchema - subProfile: CoreProfile + subProfile: Profile onModified?: () => void } @@ -48,7 +49,7 @@ export const SubprofileCardWithEdit = (props: SubprofileCardWithEditProps): JSX. const [schemaURLDraft, setSchemaURLDraft] = useState('https://schema.concrnt.world/p/basic.json') const [schemaURL, setSchemaURL] = useState(null) - const [editingProfile, setEditingProfile] = useState | null>(null) + const [editingProfile, setEditingProfile] = useState | null>(null) const [subprofileDraft, setSubprofileDraft] = useState(null) const [openReaderEditor, setOpenReaderEditor] = useState(false) @@ -67,13 +68,15 @@ export const SubprofileCardWithEdit = (props: SubprofileCardWithEditProps): JSX. useEffect(() => { client.api.invalidateTimeline('world.concrnt.t-subhome.' + props.subProfile.id + '@' + client.ccid!) - client.getTimeline('world.concrnt.t-subhome.' + props.subProfile.id + '@' + client.ccid!).then((timeline) => { - if (!timeline) return - setTimeline(timeline) - timeline.getAssociations().then((assocs) => { - setRequests(assocs.filter((e) => e.schema === Schemas.readAccessRequestAssociation)) + client + .getTimeline('world.concrnt.t-subhome.' + props.subProfile.id + '@' + client.ccid!, { cache: 'no-cache' }) + .then((timeline) => { + if (!timeline) return + setTimeline(timeline) + timeline.getAssociations().then((assocs) => { + setRequests(assocs.filter((e) => e.schema === Schemas.readAccessRequestAssociation)) + }) }) - }) }, [props.subProfile.id, update]) const menuItems = [ @@ -145,7 +148,7 @@ export const SubprofileCardWithEdit = (props: SubprofileCardWithEditProps): JSX. { - setSubprofileDraft(props.subProfile.document.body) + setSubprofileDraft(props.subProfile.parsedDoc.body) setEditingProfile(props.subProfile) setSchemaURL(props.subProfile.schema) setSchemaURLDraft(props.subProfile.schema) diff --git a/src/components/SubprofileSelector.tsx b/app/src/components/SubprofileSelector.tsx similarity index 91% rename from src/components/SubprofileSelector.tsx rename to app/src/components/SubprofileSelector.tsx index eeb2a0bf..01943142 100644 --- a/src/components/SubprofileSelector.tsx +++ b/app/src/components/SubprofileSelector.tsx @@ -68,7 +68,11 @@ export const SubprofileSelector = (props: SubprofileSelectorProps): JSX.Element }} > - + ) diff --git a/src/components/SwitchMasterToSub.tsx b/app/src/components/SwitchMasterToSub.tsx similarity index 99% rename from src/components/SwitchMasterToSub.tsx rename to app/src/components/SwitchMasterToSub.tsx index 154ce7fd..588ce4dc 100644 --- a/src/components/SwitchMasterToSub.tsx +++ b/app/src/components/SwitchMasterToSub.tsx @@ -1,7 +1,7 @@ import { Box, Button, Grid, MenuItem, Select, TextField, Typography } from '@mui/material' import { useClient } from '../context/ClientContext' import { useEffect, useMemo, useRef, useState } from 'react' -import { ComputeCKID, type Identity, GenerateIdentity } from '@concurrent-world/client' +import { ComputeCKID, type Identity, GenerateIdentity } from '@concrnt/client' import { Trans, useTranslation } from 'react-i18next' import EmailIcon from '@mui/icons-material/Email' import FileDownloadIcon from '@mui/icons-material/FileDownload' diff --git a/src/components/ThemeCard.tsx b/app/src/components/ThemeCard.tsx similarity index 100% rename from src/components/ThemeCard.tsx rename to app/src/components/ThemeCard.tsx diff --git a/src/components/ThemeCreator.tsx b/app/src/components/ThemeCreator.tsx similarity index 100% rename from src/components/ThemeCreator.tsx rename to app/src/components/ThemeCreator.tsx diff --git a/src/components/Timeline/index.ts b/app/src/components/Timeline/index.ts similarity index 100% rename from src/components/Timeline/index.ts rename to app/src/components/Timeline/index.ts diff --git a/src/components/Timeline/main.tsx b/app/src/components/Timeline/main.tsx similarity index 85% rename from src/components/Timeline/main.tsx rename to app/src/components/Timeline/main.tsx index 5628883b..aa46f976 100644 --- a/src/components/Timeline/main.tsx +++ b/app/src/components/Timeline/main.tsx @@ -7,16 +7,17 @@ import { ErrorBoundary, type FallbackProps } from 'react-error-boundary' import HeartBrokenIcon from '@mui/icons-material/HeartBroken' import ArrowUpwardIcon from '@mui/icons-material/ArrowUpward' import SyncIcon from '@mui/icons-material/Sync' -import { type TimelineReader } from '@concurrent-world/client' +import { type TimelineReader } from '@concrnt/client' import { useRefWithForceUpdate } from '../../hooks/useRefWithForceUpdate' import useSound from 'use-sound' import { usePreference } from '../../context/PreferenceContext' import { VList, type VListHandle } from 'virtua' import { useClient } from '../../context/ClientContext' import { UseSoundFormats } from '../../constants' +import { useGlobalState } from '../../context/GlobalState' export interface TimelineProps { - streams: string[] + timelineFQIDs: string[] perspective?: string header?: JSX.Element onScroll?: (top: number) => void @@ -40,6 +41,7 @@ const timelineElemSx: SxProps = { const timeline = forwardRef((props: TimelineProps, ref: ForwardedRef): JSX.Element => { const { client } = useClient() + const { isDomainOffline } = useGlobalState() const theme = useTheme() const [sound] = usePreference('sound') @@ -68,40 +70,54 @@ const timeline = forwardRef((props: TimelineProps, ref: ForwardedRef { let isCancelled = false - if (props.streams.length === 0) return - setTimelineLoading(true) - const mt = client - .newTimelineReader({ - withoutSocket: props.noRealtime ?? false - }) - .then((t) => { - if (isCancelled) return - timeline.current = t - t.onUpdate = () => { - timelineChanged() + const request = async () => { + if (props.timelineFQIDs.length === 0) return + setTimelineLoading(true) + + let hostOverride = undefined + if (isDomainOffline && props.timelineFQIDs.length === 1) { + const leaderTimeline = await client.getTimeline(props.timelineFQIDs[0]) + if (leaderTimeline) { + hostOverride = leaderTimeline.host + console.warn('timeline host override:', hostOverride) } - t.onRealtimeEvent = (event) => { - if (event.document?.type === 'message') { - playBubbleRef.current() + } + + return client + .newTimelineReader({ + withoutSocket: props.noRealtime ?? false, + hostOverride: hostOverride + }) + .then((t) => { + if (isCancelled) return + timeline.current = t + t.onUpdate = () => { + timelineChanged() } - } - timeline.current - .listen(props.streams) - .then((hasMore) => { - setHasMoreData(hasMore) - }) - .finally(() => { - setTimelineLoading(false) - }) - return t - }) + t.onRealtimeEvent = (event) => { + if (event.parsedDoc?.type === 'message') { + playBubbleRef.current() + } + } + timeline.current + .listen(props.timelineFQIDs) + .then((hasMore) => { + setHasMoreData(hasMore) + }) + .finally(() => { + setTimelineLoading(false) + }) + return t + }) + } + const mt = request() return () => { isCancelled = true mt.then((t) => { t?.dispose() }) } - }, [props.streams]) + }, [props.timelineFQIDs, isDomainOffline]) const positionRef = useRef(0) const scrollParentRef = useRef(null) @@ -141,7 +157,7 @@ const timeline = forwardRef((props: TimelineProps, ref: ForwardedRef { if (!scrollParentRef.current) return @@ -267,7 +283,7 @@ const timeline = forwardRef((props: TimelineProps, ref: ForwardedRef ) - : (timelineLoading ? [] : timeline.current?.body ?? []).map((e) => { + : (timelineLoading ? [] : (timeline.current?.body ?? [])).map((e) => { let element const type = e.resourceID[0] switch (type) { @@ -281,7 +297,7 @@ const timeline = forwardRef((props: TimelineProps, ref: ForwardedRef ) break diff --git a/src/components/TimelineFilter.tsx b/app/src/components/TimelineFilter.tsx similarity index 98% rename from src/components/TimelineFilter.tsx rename to app/src/components/TimelineFilter.tsx index d7662f3f..5205cbe0 100644 --- a/src/components/TimelineFilter.tsx +++ b/app/src/components/TimelineFilter.tsx @@ -1,4 +1,4 @@ -import { Schemas } from '@concurrent-world/client' +import { Schemas } from '@concrnt/worldlib' import { Box, type SxProps } from '@mui/material' import { useTranslation } from 'react-i18next' import { CCChip } from './ui/CCChip' diff --git a/src/components/TimelineHeader.tsx b/app/src/components/TimelineHeader.tsx similarity index 100% rename from src/components/TimelineHeader.tsx rename to app/src/components/TimelineHeader.tsx diff --git a/src/components/UserProfileCard.tsx b/app/src/components/UserProfileCard.tsx similarity index 96% rename from src/components/UserProfileCard.tsx rename to app/src/components/UserProfileCard.tsx index 23c0b048..f5f4d402 100644 --- a/src/components/UserProfileCard.tsx +++ b/app/src/components/UserProfileCard.tsx @@ -1,4 +1,4 @@ -import { type ProfileSchema, type User } from '@concurrent-world/client' +import { type ProfileSchema, type User } from '@concrnt/worldlib' import { Badge, Box, Chip, Typography } from '@mui/material' import { CCAvatar } from './ui/CCAvatar' import { useClient } from '../context/ClientContext' @@ -69,7 +69,7 @@ export const UserProfileCard = (props: UserProfileCardProps): JSX.Element => { avatarURL={ props.profileOverride ? profile.avatar - : props.user?.profile?.avatar ?? props.profile?.avatar + : (props.user?.profile?.avatar ?? props.profile?.avatar) } identiconSource={ccid} sx={{ diff --git a/src/components/WatchButton.tsx b/app/src/components/WatchButton.tsx similarity index 84% rename from src/components/WatchButton.tsx rename to app/src/components/WatchButton.tsx index ed849caf..a461d770 100644 --- a/src/components/WatchButton.tsx +++ b/app/src/components/WatchButton.tsx @@ -1,14 +1,15 @@ import { Box, Button, ButtonGroup, Checkbox, IconButton, Menu, MenuItem, Tooltip, useTheme } from '@mui/material' import ArrowDropDownIcon from '@mui/icons-material/ArrowDropDown' -import { useState } from 'react' +import { useEffect, useState } from 'react' import { useTranslation } from 'react-i18next' import { useClient } from '../context/ClientContext' import PlaylistAddIcon from '@mui/icons-material/PlaylistAdd' import { useGlobalState } from '../context/GlobalState' +import { type Timeline } from '@concrnt/worldlib' export interface WatchButtonProps { - timelineID: string + timelineFQID: string minimal?: boolean small?: boolean } @@ -23,7 +24,15 @@ export const WatchButton = (props: WatchButtonProps): JSX.Element => { const { t } = useTranslation('', { keyPrefix: 'common' }) - const watching = allKnownTimelines.find((e) => (e.cacheKey ?? e.id) === props.timelineID) !== undefined + const [timeline, setTimeline] = useState>(null) + + useEffect(() => { + client?.getTimeline(props.timelineFQID).then((timeline) => { + setTimeline(timeline) + }) + }, [props.timelineFQID]) + + const watching = allKnownTimelines.find((e) => e.fqid === timeline?.fqid) !== undefined return ( @@ -51,7 +60,7 @@ export const WatchButton = (props: WatchButtonProps): JSX.Element => { setMenuAnchor(e.currentTarget) } else { client.api - .subscribe(props.timelineID, Object.keys(listedSubscriptions)[0]) + .subscribe(props.timelineFQID, Object.keys(listedSubscriptions)[0]) .then((subscription) => { reloadList() }) @@ -92,18 +101,18 @@ export const WatchButton = (props: WatchButtonProps): JSX.Element => { > {Object.keys(listedSubscriptions).map((key) => ( {}}> - {listedSubscriptions[key].document.body.name} + {listedSubscriptions[key].parsedDoc.body.name} e.id === props.timelineID) !== undefined + listedSubscriptions[key].items.find((e) => e.id === props.timelineFQID) !== undefined } onChange={(check) => { if (check.target.checked) { - client.api.subscribe(props.timelineID, key).then((_) => { + client.api.subscribe(props.timelineFQID, key).then((_) => { reloadList() }) } else { - client.api.unsubscribe(props.timelineID, key).then((_) => { + client.api.unsubscribe(props.timelineFQID, key).then((_) => { reloadList() }) } diff --git a/src/components/WatchRequestAccpetButton.tsx b/app/src/components/WatchRequestAccpetButton.tsx similarity index 98% rename from src/components/WatchRequestAccpetButton.tsx rename to app/src/components/WatchRequestAccpetButton.tsx index f091b5e8..0896b852 100644 --- a/src/components/WatchRequestAccpetButton.tsx +++ b/app/src/components/WatchRequestAccpetButton.tsx @@ -1,4 +1,4 @@ -import { type Association, type ReadAccessRequestAssociationSchema, type Timeline } from '@concurrent-world/client' +import { type Association, type ReadAccessRequestAssociationSchema, type Timeline } from '@concrnt/worldlib' import { CCAvatar } from './ui/CCAvatar' import { Box, Button } from '@mui/material' import { useClient } from '../context/ClientContext' diff --git a/src/components/theming/ConcrntLogo.tsx b/app/src/components/theming/ConcrntLogo.tsx similarity index 100% rename from src/components/theming/ConcrntLogo.tsx rename to app/src/components/theming/ConcrntLogo.tsx diff --git a/src/components/theming/ConcrntLogo_Splitted.tsx b/app/src/components/theming/ConcrntLogo_Splitted.tsx similarity index 100% rename from src/components/theming/ConcrntLogo_Splitted.tsx rename to app/src/components/theming/ConcrntLogo_Splitted.tsx diff --git a/src/components/theming/ConcurrentWordmark.tsx b/app/src/components/theming/ConcurrentWordmark.tsx similarity index 100% rename from src/components/theming/ConcurrentWordmark.tsx rename to app/src/components/theming/ConcurrentWordmark.tsx diff --git a/src/components/theming/Passport.tsx b/app/src/components/theming/Passport.tsx similarity index 98% rename from src/components/theming/Passport.tsx rename to app/src/components/theming/Passport.tsx index b7a47772..c799ab77 100644 --- a/src/components/theming/Passport.tsx +++ b/app/src/components/theming/Passport.tsx @@ -4,7 +4,7 @@ import { useEffect, useId, useMemo, useRef, useState } from 'react' import { useClient } from '../../context/ClientContext' import BoringAvatar from 'boring-avatars' import html2canvas from 'html2canvas' -import { type CCDocument } from '@concurrent-world/client' +import { type CCDocument } from '@concrnt/client' export function Passport(): JSX.Element { const { client } = useClient() @@ -26,7 +26,7 @@ export function Passport(): JSX.Element { ccid={client.ccid || ''} name={client?.user?.profile?.username || ''} avatar={client?.user?.profile?.avatar || ''} - host={client.api.host || ''} + host={client.host || ''} cdate={affiliationDate?.toLocaleDateString() || 'N/A'} trust={100} /> diff --git a/src/components/ui/Badge.tsx b/app/src/components/ui/Badge.tsx similarity index 95% rename from src/components/ui/Badge.tsx rename to app/src/components/ui/Badge.tsx index 5402b375..dd45a51d 100644 --- a/src/components/ui/Badge.tsx +++ b/app/src/components/ui/Badge.tsx @@ -1,7 +1,7 @@ import { useEffect, useState } from 'react' import { type Badge } from '../../model' import { Box, type SxProps, Tooltip } from '@mui/material' -import { type BadgeRef } from '@concurrent-world/client' +import { type BadgeRef } from '@concrnt/worldlib' import { useConcord } from '../../context/ConcordContext' export interface ConcordBadgeProps { diff --git a/src/components/ui/BreadcrumbList.tsx b/app/src/components/ui/BreadcrumbList.tsx similarity index 100% rename from src/components/ui/BreadcrumbList.tsx rename to app/src/components/ui/BreadcrumbList.tsx diff --git a/src/components/ui/CCAvatar.stories.tsx b/app/src/components/ui/CCAvatar.stories.tsx similarity index 100% rename from src/components/ui/CCAvatar.stories.tsx rename to app/src/components/ui/CCAvatar.stories.tsx diff --git a/src/components/ui/CCAvatar.tsx b/app/src/components/ui/CCAvatar.tsx similarity index 100% rename from src/components/ui/CCAvatar.tsx rename to app/src/components/ui/CCAvatar.tsx diff --git a/app/src/components/ui/CCAvatarWithResolver.tsx b/app/src/components/ui/CCAvatarWithResolver.tsx new file mode 100644 index 00000000..10e4bf92 --- /dev/null +++ b/app/src/components/ui/CCAvatarWithResolver.tsx @@ -0,0 +1,33 @@ +import { useEffect, useState } from 'react' +import { useClient } from '../../context/ClientContext' +import { User } from '@concrnt/worldlib' +import { CCAvatar } from './CCAvatar' +import { Skeleton, SxProps } from '@mui/material' + +export interface CCAvatarWithResolverProps { + ccid: string + sx?: SxProps +} + +export const CCAvatarWithResolver = (props: CCAvatarWithResolverProps): JSX.Element => { + const { client } = useClient() + + const [user, setUser] = useState(null) + + useEffect(() => { + client.getUser(props.ccid).then(setUser) + }, [props.ccid]) + + if (!user) { + return + } + + return ( + + ) +} diff --git a/src/components/ui/CCButton.stories.tsx b/app/src/components/ui/CCButton.stories.tsx similarity index 100% rename from src/components/ui/CCButton.stories.tsx rename to app/src/components/ui/CCButton.stories.tsx diff --git a/src/components/ui/CCButton.tsx b/app/src/components/ui/CCButton.tsx similarity index 100% rename from src/components/ui/CCButton.tsx rename to app/src/components/ui/CCButton.tsx diff --git a/src/components/ui/CCChip.stories.tsx b/app/src/components/ui/CCChip.stories.tsx similarity index 100% rename from src/components/ui/CCChip.stories.tsx rename to app/src/components/ui/CCChip.stories.tsx diff --git a/src/components/ui/CCChip.tsx b/app/src/components/ui/CCChip.tsx similarity index 100% rename from src/components/ui/CCChip.tsx rename to app/src/components/ui/CCChip.tsx diff --git a/src/components/ui/CCComboBox.stories.tsx b/app/src/components/ui/CCComboBox.stories.tsx similarity index 100% rename from src/components/ui/CCComboBox.stories.tsx rename to app/src/components/ui/CCComboBox.stories.tsx diff --git a/src/components/ui/CCComboBox.tsx b/app/src/components/ui/CCComboBox.tsx similarity index 100% rename from src/components/ui/CCComboBox.tsx rename to app/src/components/ui/CCComboBox.tsx diff --git a/src/components/ui/CCDrawer.tsx b/app/src/components/ui/CCDrawer.tsx similarity index 100% rename from src/components/ui/CCDrawer.tsx rename to app/src/components/ui/CCDrawer.tsx diff --git a/src/components/ui/CCIconButton.stories.tsx b/app/src/components/ui/CCIconButton.stories.tsx similarity index 100% rename from src/components/ui/CCIconButton.stories.tsx rename to app/src/components/ui/CCIconButton.stories.tsx diff --git a/src/components/ui/CCIconButton.tsx b/app/src/components/ui/CCIconButton.tsx similarity index 100% rename from src/components/ui/CCIconButton.tsx rename to app/src/components/ui/CCIconButton.tsx diff --git a/src/components/ui/CCLink.tsx b/app/src/components/ui/CCLink.tsx similarity index 100% rename from src/components/ui/CCLink.tsx rename to app/src/components/ui/CCLink.tsx diff --git a/src/components/ui/CCTextField.stories.tsx b/app/src/components/ui/CCTextField.stories.tsx similarity index 100% rename from src/components/ui/CCTextField.stories.tsx rename to app/src/components/ui/CCTextField.stories.tsx diff --git a/src/components/ui/CCTextField.tsx b/app/src/components/ui/CCTextField.tsx similarity index 100% rename from src/components/ui/CCTextField.tsx rename to app/src/components/ui/CCTextField.tsx diff --git a/src/components/ui/CCUserChip.tsx b/app/src/components/ui/CCUserChip.tsx similarity index 79% rename from src/components/ui/CCUserChip.tsx rename to app/src/components/ui/CCUserChip.tsx index f9c8a989..15e05244 100644 --- a/src/components/ui/CCUserChip.tsx +++ b/app/src/components/ui/CCUserChip.tsx @@ -1,11 +1,10 @@ import { Tooltip, Paper } from '@mui/material' import { UserProfileCard } from '../UserProfileCard' -import { type User } from '@concurrent-world/client' +import { ProfileOverride, type User } from '@concrnt/worldlib' import AlternateEmailIcon from '@mui/icons-material/AlternateEmail' import { useClient } from '../../context/ClientContext' import { useEffect, useState } from 'react' import { CCAvatar } from './CCAvatar' -import { type ProfileOverride } from '@concurrent-world/client/dist/types/model/core' import { CCChip } from './CCChip' export interface CCUserChipProps { @@ -34,19 +33,19 @@ export const CCUserChip = (props: CCUserChipProps): JSX.Element => { } }, [props.ccid]) - const icon = props.avatar - ? ( - - ) ?? - : props.iconOverride ?? + const icon = props.avatar ? ( + + ) : ( + (props.iconOverride ?? ) + ) return ( { const { client } = useClient() const theme = useTheme() - const [domain, setDomain] = useState(null) + const [domain, setDomain] = useState(null) useEffect(() => { client.api.getDomain(props.domainFQDN).then((e) => { - setDomain(e ?? null) + setDomain(e) }) }, [props.domainFQDN]) diff --git a/src/components/ui/EmbeddedGallery.tsx b/app/src/components/ui/EmbeddedGallery.tsx similarity index 100% rename from src/components/ui/EmbeddedGallery.tsx rename to app/src/components/ui/EmbeddedGallery.tsx diff --git a/src/components/ui/EventCard.tsx b/app/src/components/ui/EventCard.tsx similarity index 100% rename from src/components/ui/EventCard.tsx rename to app/src/components/ui/EventCard.tsx diff --git a/src/components/ui/FullScreenLoading.tsx b/app/src/components/ui/FullScreenLoading.tsx similarity index 100% rename from src/components/ui/FullScreenLoading.tsx rename to app/src/components/ui/FullScreenLoading.tsx diff --git a/src/components/ui/IconButtonWithLabel.tsx b/app/src/components/ui/IconButtonWithLabel.tsx similarity index 100% rename from src/components/ui/IconButtonWithLabel.tsx rename to app/src/components/ui/IconButtonWithLabel.tsx diff --git a/src/components/ui/IconButtonWithNumber.tsx b/app/src/components/ui/IconButtonWithNumber.tsx similarity index 100% rename from src/components/ui/IconButtonWithNumber.tsx rename to app/src/components/ui/IconButtonWithNumber.tsx diff --git a/src/components/ui/KeyCard.stories.tsx b/app/src/components/ui/KeyCard.stories.tsx similarity index 90% rename from src/components/ui/KeyCard.stories.tsx rename to app/src/components/ui/KeyCard.stories.tsx index 0d473a33..e1e65c51 100644 --- a/src/components/ui/KeyCard.stories.tsx +++ b/app/src/components/ui/KeyCard.stories.tsx @@ -1,6 +1,6 @@ import type { Meta } from '@storybook/react' import { KeyCard } from './KeyCard' -import { type Key } from '@concurrent-world/client/dist/types/model/core' +import { Key } from '@concrnt/client' interface Props extends Meta { id: string @@ -12,7 +12,7 @@ interface Props extends Meta { } export const Default = (props: Props): JSX.Element => { - const key: Key = { + const keyBase: Omit = { id: props.id, root: props.root, parent: props.parent, @@ -24,6 +24,8 @@ export const Default = (props: Props): JSX.Element => { validUntil: 'null' } + const key = Object.setPrototypeOf(keyBase, Key.prototype) + return } diff --git a/src/components/ui/KeyCard.tsx b/app/src/components/ui/KeyCard.tsx similarity index 98% rename from src/components/ui/KeyCard.tsx rename to app/src/components/ui/KeyCard.tsx index a1666dfb..ad76e9d1 100644 --- a/src/components/ui/KeyCard.tsx +++ b/app/src/components/ui/KeyCard.tsx @@ -1,4 +1,4 @@ -import { type Key } from '@concurrent-world/client/dist/types/model/core' +import { type Key } from '@concrnt/client' import { alpha, useTheme, IconButton } from '@mui/material' import { type ConcurrentTheme } from '../../model' import MoreHorizIcon from '@mui/icons-material/MoreHoriz' diff --git a/src/components/ui/LinkChip.tsx b/app/src/components/ui/LinkChip.tsx similarity index 95% rename from src/components/ui/LinkChip.tsx rename to app/src/components/ui/LinkChip.tsx index bd98afdf..3aa6b282 100644 --- a/src/components/ui/LinkChip.tsx +++ b/app/src/components/ui/LinkChip.tsx @@ -50,7 +50,7 @@ export const LinkChip = ({ href, service, icon, children }: LinkChipProps): JSX. } + icon={useAvatar ? undefined : (iconMap[service] ?? )} avatar={useAvatar ? : undefined} to={href} label={children} diff --git a/src/components/ui/ListItemDomain.tsx b/app/src/components/ui/ListItemDomain.tsx similarity index 91% rename from src/components/ui/ListItemDomain.tsx rename to app/src/components/ui/ListItemDomain.tsx index 05130ee7..0d711461 100644 --- a/src/components/ui/ListItemDomain.tsx +++ b/app/src/components/ui/ListItemDomain.tsx @@ -8,7 +8,7 @@ import { type SxProps } from '@mui/material' import { useEffect, useState } from 'react' -import { type CoreDomain } from '@concurrent-world/client' +import { type Domain } from '@concrnt/client' import { useClient } from '../../context/ClientContext' import OpenInNewIcon from '@mui/icons-material/OpenInNew' @@ -20,7 +20,7 @@ export interface ListItemDomainProps { export const ListItemDomain = (props: ListItemDomainProps): JSX.Element | null => { const { client } = useClient() - const [domain, setDomain] = useState(null) + const [domain, setDomain] = useState(null) useEffect(() => { client.api.getDomain(props.domainFQDN).then((e) => { diff --git a/src/components/ui/ListItemSubscription.tsx b/app/src/components/ui/ListItemSubscription.tsx similarity index 79% rename from src/components/ui/ListItemSubscription.tsx rename to app/src/components/ui/ListItemSubscription.tsx index 246f15c3..36ad6594 100644 --- a/src/components/ui/ListItemSubscription.tsx +++ b/app/src/components/ui/ListItemSubscription.tsx @@ -1,5 +1,6 @@ import { ListItem, ListItemButton, ListItemText } from '@mui/material' -import { type ListSubscriptionSchema, type CoreSubscription } from '@concurrent-world/client' +import { type ListSubscriptionSchema } from '@concrnt/worldlib' +import { type Subscription } from '@concrnt/client' import { useGlobalState } from '../../context/GlobalState' export interface ListItemSubscriptionProps { @@ -11,7 +12,7 @@ export interface ListItemSubscriptionProps { export const ListItemSubscription = (props: ListItemSubscriptionProps): JSX.Element => { const { allKnownSubscriptions } = useGlobalState() const subscription = allKnownSubscriptions.find((sub) => sub.id === props.id) as - | CoreSubscription + | Subscription | undefined return ( @@ -25,7 +26,7 @@ export const ListItemSubscription = (props: ListItemSubscriptionProps): JSX.Elem > {subscription ? ( - + ) : ( )} diff --git a/src/components/ui/ListItemSubscriptionTree.tsx b/app/src/components/ui/ListItemSubscriptionTree.tsx similarity index 82% rename from src/components/ui/ListItemSubscriptionTree.tsx rename to app/src/components/ui/ListItemSubscriptionTree.tsx index 2b9bc181..4b5031d8 100644 --- a/src/components/ui/ListItemSubscriptionTree.tsx +++ b/app/src/components/ui/ListItemSubscriptionTree.tsx @@ -5,7 +5,10 @@ import { Link as RouterLink } from 'react-router-dom' import ExpandMore from '@mui/icons-material/ExpandMore' import { ListItemTimeline } from './ListItemTimeline' import { usePreference } from '../../context/PreferenceContext' -import { type ListSubscriptionSchema, type CoreSubscription } from '@concurrent-world/client' +import { type ListSubscriptionSchema } from '@concrnt/worldlib' +import { type Subscription } from '@concrnt/client' +import { useClient } from '../../context/ClientContext' +import { useEffect, useState } from 'react' import { useGlobalState } from '../../context/GlobalState' export interface ListItemSubscriptionTreeProps { @@ -15,6 +18,8 @@ export interface ListItemSubscriptionTreeProps { } export const ListItemSubscriptionTree = (props: ListItemSubscriptionTreeProps): JSX.Element => { + const { client } = useClient() + const { allKnownSubscriptions } = useGlobalState() // for event handling const [lists, updateLists] = usePreference('lists') const open = props.body.expanded const setOpen = (newOpen: boolean): void => { @@ -26,12 +31,21 @@ export const ListItemSubscriptionTree = (props: ListItemSubscriptionTreeProps): updateLists(old) } - const { allKnownSubscriptions } = useGlobalState() - const subscription = allKnownSubscriptions.find((sub) => sub.id === props.id) as - | CoreSubscription - | undefined + const [subscription, setSubscription] = useState | undefined>(undefined) - const iconURL = subscription?.document.body.iconURL + useEffect(() => { + client.api + .getSubscription(props.id, { + expressGetter: (sub) => { + setSubscription(sub) + } + }) + .catch((e) => { + console.log(e) + }) + }, [props.id, allKnownSubscriptions]) + + const iconURL = subscription?.parsedDoc.body.iconURL return ( <> @@ -106,7 +120,7 @@ export const ListItemSubscriptionTree = (props: ListItemSubscriptionTreeProps): to={`/#${props.id}`} > {subscription ? ( - + ) : ( )} diff --git a/src/components/ui/ListItemTimeline.tsx b/app/src/components/ui/ListItemTimeline.tsx similarity index 72% rename from src/components/ui/ListItemTimeline.tsx rename to app/src/components/ui/ListItemTimeline.tsx index 3c427db3..543415ed 100644 --- a/src/components/ui/ListItemTimeline.tsx +++ b/app/src/components/ui/ListItemTimeline.tsx @@ -1,20 +1,20 @@ import { ListItemButton, type SxProps } from '@mui/material' import { useEffect, useState } from 'react' import { Link as RouterLink } from 'react-router-dom' +import { IsCSID } from '@concrnt/client' import { - type Timeline, - type CommunityTimelineSchema, - IsCSID, Schemas, + Timeline, + type CommunityTimelineSchema, type ProfileSchema, - type CoreTimeline, type SubprofileTimelineSchema -} from '@concurrent-world/client' +} from '@concrnt/worldlib' import { useClient } from '../../context/ClientContext' import TagIcon from '@mui/icons-material/Tag' import CloudOffIcon from '@mui/icons-material/CloudOff' import AlternateEmailIcon from '@mui/icons-material/AlternateEmail' import { FaTheaterMasks } from 'react-icons/fa' +import HelpOutlineIcon from '@mui/icons-material/HelpOutline' export interface ListItemTimelineProps { timelineID: string @@ -27,33 +27,47 @@ export const ListItemTimeline = (props: ListItemTimelineProps): JSX.Element | nu const [timeline, setTimeline] = useState | null | undefined>(null) const [profile, setProfile] = useState(null) + const [isOnline, setIsOnline] = useState(false) + useEffect(() => { client.getTimeline(props.timelineID).then((e) => { if (!e) return setTimeline(e) + client.api.getDomainOnlineStatus(e.host).then((online) => { + setIsOnline(online) + }) if (e.schema === Schemas.emptyTimeline) { - const timeline: CoreTimeline = e + const timeline: Timeline = e client.getUser(timeline.author).then((user) => { setProfile(user?.profile) }) } else if (e.schema === Schemas.subprofileTimeline) { - const timeline: CoreTimeline = e + const timeline: Timeline = e client.api - .getProfileByID(timeline.document.body.subprofile, timeline.author) + .getProfile(timeline.document.body.subprofile, timeline.author) .then((profile) => { if (!profile) return - setProfile(profile.document.body) + setProfile(profile.parsedDoc.body) }) } }) }, [props.timelineID]) if (!timeline) { + return ( + + + Not found + + ) + } + + if (!isOnline) { return ( - offline + {timeline?.document.body.name || profile?.username || 'Unknown'} ) } diff --git a/src/components/ui/Loading.tsx b/app/src/components/ui/Loading.tsx similarity index 100% rename from src/components/ui/Loading.tsx rename to app/src/components/ui/Loading.tsx diff --git a/src/components/ui/MarkdownRenderer.tsx b/app/src/components/ui/MarkdownRenderer.tsx similarity index 99% rename from src/components/ui/MarkdownRenderer.tsx rename to app/src/components/ui/MarkdownRenderer.tsx index 8208a7ba..75eef032 100644 --- a/src/components/ui/MarkdownRenderer.tsx +++ b/app/src/components/ui/MarkdownRenderer.tsx @@ -170,7 +170,7 @@ export const MarkdownRenderer = memo((props: MarkdownRend return }, streamlink: ({ streamId }) => { - return + return }, social: ({ href, icon, service, children }) => { return ( diff --git a/src/components/ui/MarkdownRendererLite.tsx b/app/src/components/ui/MarkdownRendererLite.tsx similarity index 99% rename from src/components/ui/MarkdownRendererLite.tsx rename to app/src/components/ui/MarkdownRendererLite.tsx index eb087971..a887cfea 100644 --- a/src/components/ui/MarkdownRendererLite.tsx +++ b/app/src/components/ui/MarkdownRendererLite.tsx @@ -117,7 +117,7 @@ export function MarkdownRendererLite(props: MarkdownRendererProps): JSX.Element return }, streamlink: ({ streamId }) => { - return + return }, social: ({ href, icon, service, children }) => { return ( diff --git a/src/components/ui/MediaInput.tsx b/app/src/components/ui/MediaInput.tsx similarity index 100% rename from src/components/ui/MediaInput.tsx rename to app/src/components/ui/MediaInput.tsx diff --git a/src/components/ui/PolicyEditor.tsx b/app/src/components/ui/PolicyEditor.tsx similarity index 100% rename from src/components/ui/PolicyEditor.tsx rename to app/src/components/ui/PolicyEditor.tsx diff --git a/src/components/ui/ProfilePicker.tsx b/app/src/components/ui/ProfilePicker.tsx similarity index 84% rename from src/components/ui/ProfilePicker.tsx rename to app/src/components/ui/ProfilePicker.tsx index c861dfdd..d6339d0f 100644 --- a/src/components/ui/ProfilePicker.tsx +++ b/app/src/components/ui/ProfilePicker.tsx @@ -3,11 +3,11 @@ import { useClient } from '../../context/ClientContext' import { Avatar, IconButton, ListItemIcon, Menu, MenuItem } from '@mui/material' import { CCAvatar } from './CCAvatar' import { useGlobalState } from '../../context/GlobalState' -import { type CoreProfile } from '@concurrent-world/client' +import { type Profile } from '@concrnt/client' export interface ProfilePickerProps { - selected?: CoreProfile - setSelected: (subprofile?: CoreProfile) => void + selected?: Profile + setSelected: (subprofile?: Profile) => void } export const ProfilePicker = (props: ProfilePickerProps): JSX.Element => { @@ -29,7 +29,7 @@ export const ProfilePicker = (props: ProfilePickerProps): JSX.Element => { }} > { }} > - + ) diff --git a/src/components/ui/ProfileProperties.tsx b/app/src/components/ui/ProfileProperties.tsx similarity index 93% rename from src/components/ui/ProfileProperties.tsx rename to app/src/components/ui/ProfileProperties.tsx index 3e09a838..2471cb41 100644 --- a/src/components/ui/ProfileProperties.tsx +++ b/app/src/components/ui/ProfileProperties.tsx @@ -1,4 +1,4 @@ -import { type CoreProfile } from '@concurrent-world/client' +import { type Profile } from '@concrnt/client' import { Box, Button, Typography } from '@mui/material' import { useEffect, useMemo, useState } from 'react' import { Link as RouterLink } from 'react-router-dom' @@ -6,7 +6,7 @@ import { CCDrawer } from './CCDrawer' import { CCEditor } from './cceditor' export interface ProfilePropertiesProps { - character: CoreProfile + character: Profile showCreateLink?: boolean } @@ -55,10 +55,10 @@ export const ProfileProperties = (props: ProfilePropertiesProps): JSX.Element => {properties.map( (property, index) => - property.key in props.character.document.body && ( + property.key in props.character.parsedDoc.body && ( - {property.title}: {props.character.document.body[property.key]} + {property.title}: {props.character.parsedDoc.body[property.key]} ) @@ -105,7 +105,7 @@ export const ProfileProperties = (props: ProfilePropertiesProps): JSX.Element => {schema && ( - {}} /> + {}} /> )}
diff --git a/src/components/ui/QRCodeReader.tsx b/app/src/components/ui/QRCodeReader.tsx similarity index 100% rename from src/components/ui/QRCodeReader.tsx rename to app/src/components/ui/QRCodeReader.tsx diff --git a/src/components/ui/SecretCode.tsx b/app/src/components/ui/SecretCode.tsx similarity index 100% rename from src/components/ui/SecretCode.tsx rename to app/src/components/ui/SecretCode.tsx diff --git a/src/components/ui/StreamPicker.tsx b/app/src/components/ui/StreamPicker.tsx similarity index 98% rename from src/components/ui/StreamPicker.tsx rename to app/src/components/ui/StreamPicker.tsx index 1afe7b5a..b6872c36 100644 --- a/src/components/ui/StreamPicker.tsx +++ b/app/src/components/ui/StreamPicker.tsx @@ -1,5 +1,5 @@ import { Autocomplete, Box, InputBase, type SxProps } from '@mui/material' -import { type CommunityTimelineSchema, type Timeline } from '@concurrent-world/client' +import { type CommunityTimelineSchema, type Timeline } from '@concrnt/worldlib' import { CCChip } from './CCChip' import TagIcon from '@mui/icons-material/Tag' @@ -93,7 +93,6 @@ export const StreamPicker = (props: StreamPickerProps): JSX.Element => { } renderOption={(props, option, _state, _ownerState) => ( | null>(null) + const [character, setProfile] = useState | null>(null) useEffect(() => { - client.api.getProfileByID(props.characterID, props.authorCCID).then((character) => { + client.api.getProfile(props.characterID, props.authorCCID).then((character) => { setProfile(character ?? null) }) }, [props.characterID]) @@ -27,10 +27,10 @@ export function SubprofileBadge(props: SubprofileBadgeProps): JSX.Element { if (!character) return return ( - + { if (props.enablePreview) { - mediaViewer.openSingle(character?.document.body.avatar) + mediaViewer.openSingle(character?.parsedDoc.body.avatar) } else { props.onClick?.(props.characterID) } }} > - + ) diff --git a/src/components/ui/TimeDiff.tsx b/app/src/components/ui/TimeDiff.tsx similarity index 100% rename from src/components/ui/TimeDiff.tsx rename to app/src/components/ui/TimeDiff.tsx diff --git a/src/components/ui/TimelineChip.tsx b/app/src/components/ui/TimelineChip.tsx similarity index 80% rename from src/components/ui/TimelineChip.tsx rename to app/src/components/ui/TimelineChip.tsx index 6c34a917..1cf48013 100644 --- a/src/components/ui/TimelineChip.tsx +++ b/app/src/components/ui/TimelineChip.tsx @@ -1,12 +1,11 @@ import { Tooltip, Paper } from '@mui/material' import { + Schemas, type CommunityTimelineSchema, - type CoreTimeline, type ProfileSchema, - Schemas, type SubprofileTimelineSchema, type Timeline -} from '@concurrent-world/client' +} from '@concrnt/worldlib' import TagIcon from '@mui/icons-material/Tag' import { useClient } from '../../context/ClientContext' import { useEffect, useState } from 'react' @@ -16,7 +15,7 @@ import { UserProfileCard } from '../UserProfileCard' import HomeIcon from '@mui/icons-material/Home' export interface TimelineChipProps { - timelineID?: string + timelineFQID?: string } export const TimelineChip = (props: TimelineChipProps): JSX.Element => { @@ -24,43 +23,43 @@ export const TimelineChip = (props: TimelineChipProps): JSX.Element => { const [timeline, setTimeline] = useState | null | undefined>(undefined) const [profile, setProfile] = useState(null) - const domain = props.timelineID?.split('@')?.[1] + const domain = props.timelineFQID?.split('@')?.[1] useEffect(() => { if (timeline !== undefined) return - if (!props.timelineID) return - client.getTimeline(props.timelineID).then((t) => { + if (!props.timelineFQID) return + client.getTimeline(props.timelineFQID).then((t) => { setTimeline(t) if (!t) return if (t.schema === Schemas.emptyTimeline) { - const timeline: CoreTimeline = t + const timeline: Timeline = t client.getUser(timeline.author).then((user) => { setProfile(user?.profile) }) } else if (t.schema === Schemas.subprofileTimeline) { - const timeline: CoreTimeline = t + const timeline: Timeline = t client.api - .getProfileByID(timeline.document.body.subprofile, timeline.author) + .getProfile(timeline.document.body.subprofile, timeline.author) .then((profile) => { if (!profile) return - setProfile(profile.document.body) + setProfile(profile.parsedDoc.body) }) } }) }, []) - if (!props.timelineID) { + if (!props.timelineFQID) { return } /> } if (!timeline) { - return } /> + return } /> } - let link = `/timeline/${props.timelineID}` + let link = `/timeline/${props.timelineFQID}` - const split = props.timelineID.split('@') + const split = props.timelineFQID.split('@') if (split[0] === 'world.concrnt.t-home') { link = `/${split[1]}` } else if (timeline.schema === Schemas.subprofileTimeline) { @@ -101,7 +100,7 @@ export const TimelineChip = (props: TimelineChipProps): JSX.Element => { <> {domain && ( { : } /> diff --git a/src/components/ui/TreeGraph.tsx b/app/src/components/ui/TreeGraph.tsx similarity index 94% rename from src/components/ui/TreeGraph.tsx rename to app/src/components/ui/TreeGraph.tsx index dae20b05..102483a4 100644 --- a/src/components/ui/TreeGraph.tsx +++ b/app/src/components/ui/TreeGraph.tsx @@ -82,8 +82,8 @@ export function Node(props: NodeProps): JSX.Element { props.nodeposition === 'start' ? 'flex-end' : props.nodeposition === 'end' - ? 'flex-start' - : 'center' + ? 'flex-start' + : 'center' }} >
diff --git a/src/components/ui/UrlSummaryCard.tsx b/app/src/components/ui/UrlSummaryCard.tsx similarity index 100% rename from src/components/ui/UrlSummaryCard.tsx rename to app/src/components/ui/UrlSummaryCard.tsx diff --git a/src/components/ui/UserPicker.tsx b/app/src/components/ui/UserPicker.tsx similarity index 85% rename from src/components/ui/UserPicker.tsx rename to app/src/components/ui/UserPicker.tsx index 8d4a1c7e..6bed5714 100644 --- a/src/components/ui/UserPicker.tsx +++ b/app/src/components/ui/UserPicker.tsx @@ -1,9 +1,8 @@ import { Autocomplete, Box, InputBase, ListItem, ListItemIcon, ListItemText, type SxProps } from '@mui/material' -import { type User } from '@concurrent-world/client' +import { type User } from '@concrnt/worldlib' import { useClient } from '../../context/ClientContext' import { CCUserChip } from './CCUserChip' import { CCAvatar } from './CCAvatar' -import { useMemo } from 'react' export interface UserPickerProps { selected: User[] @@ -14,19 +13,6 @@ export interface UserPickerProps { export const UserPicker = (props: UserPickerProps): JSX.Element => { const { client } = useClient() - const selected = useMemo( - () => - JSON.parse( - JSON.stringify(props.selected ?? [], (key, value) => { - if (key === 'client' || key === 'api') { - return undefined - } - return value - }) - ), - [props.selected] - ) - return ( { filterSelectedOptions sx={{ width: 1 }} multiple - value={selected} + value={props.selected} options={[...(client.ackings ?? []), ...(client.user ? [client.user] : [])]} getOptionKey={(option: User) => option.ccid} getOptionLabel={(option: User) => option.profile?.username ?? ''} diff --git a/src/components/ui/cceditor.tsx b/app/src/components/ui/cceditor.tsx similarity index 98% rename from src/components/ui/cceditor.tsx rename to app/src/components/ui/cceditor.tsx index 107d17c8..838387b5 100644 --- a/src/components/ui/cceditor.tsx +++ b/app/src/components/ui/cceditor.tsx @@ -4,7 +4,7 @@ import validator from '@rjsf/validator-ajv8' import { memo, useEffect, useState } from 'react' import { fetchWithTimeout } from '../../util' import { useClient } from '../../context/ClientContext' -import { type User } from '@concurrent-world/client' +import { type User } from '@concrnt/worldlib' import { UserPicker } from '../ui/UserPicker' import { type RegistryWidgetsType, type UiSchema, type WidgetProps } from '@rjsf/utils' import { MediaInput } from './MediaInput' diff --git a/src/components/ui/type.d.ts b/app/src/components/ui/type.d.ts similarity index 100% rename from src/components/ui/type.d.ts rename to app/src/components/ui/type.d.ts diff --git a/src/components/welcome/AppMock.tsx b/app/src/components/welcome/AppMock.tsx similarity index 100% rename from src/components/welcome/AppMock.tsx rename to app/src/components/welcome/AppMock.tsx diff --git a/src/components/welcome/Contributors.tsx b/app/src/components/welcome/Contributors.tsx similarity index 100% rename from src/components/welcome/Contributors.tsx rename to app/src/components/welcome/Contributors.tsx diff --git a/src/constants.ts b/app/src/constants.ts similarity index 100% rename from src/constants.ts rename to app/src/constants.ts diff --git a/src/context/AutoSummaryContext.tsx b/app/src/context/AutoSummaryContext.tsx similarity index 100% rename from src/context/AutoSummaryContext.tsx rename to app/src/context/AutoSummaryContext.tsx diff --git a/src/context/ClientContext.tsx b/app/src/context/ClientContext.tsx similarity index 94% rename from src/context/ClientContext.tsx rename to app/src/context/ClientContext.tsx index 2d11d57a..808e8a9d 100644 --- a/src/context/ClientContext.tsx +++ b/app/src/context/ClientContext.tsx @@ -1,10 +1,10 @@ import { createContext, useCallback, useContext, useEffect, useMemo, useState } from 'react' -import type { Client } from '@concurrent-world/client' import { usePersistent } from '../hooks/usePersistent' // @ts-expect-error vite dynamic import -import { branch, sha } from '~build/info' +import { branch, sha } from '~build/git' import { FullScreenLoading } from '../components/ui/FullScreenLoading' +import { type Client } from '@concrnt/worldlib' const branchName = branch || window.location.host.split('.')[0] const versionString = `${location.hostname}-${branchName as string}-${sha.slice(0, 7) as string}` @@ -38,7 +38,7 @@ export const ClientProvider = (props: ClientProviderProps): JSX.Element => { if (props.client) return const loader = async (): Promise => { - const { Client } = await import('@concurrent-world/client') + const { Client } = await import('@concrnt/worldlib') if (prvkey !== '') { Client.create(prvkey, domain, { diff --git a/src/context/ConcordContext.tsx b/app/src/context/ConcordContext.tsx similarity index 99% rename from src/context/ConcordContext.tsx rename to app/src/context/ConcordContext.tsx index b36d9fc8..fdbfb4be 100644 --- a/src/context/ConcordContext.tsx +++ b/app/src/context/ConcordContext.tsx @@ -1,6 +1,6 @@ import { createContext, useCallback, useContext, useEffect, useState } from 'react' import { CCDrawer } from '../components/ui/CCDrawer' -import { type BadgeRef, type Message } from '@concurrent-world/client' +import { type BadgeRef, type Message } from '@concrnt/worldlib' import { type Emoji, type Badge } from '../model' import { Alert, AlertTitle, Box, Button, Divider, Paper, TextField, Typography } from '@mui/material' import { CCUserChip } from '../components/ui/CCUserChip' diff --git a/src/context/Confirm.tsx b/app/src/context/Confirm.tsx similarity index 100% rename from src/context/Confirm.tsx rename to app/src/context/Confirm.tsx diff --git a/src/context/EmojiPickerContext.tsx b/app/src/context/EmojiPickerContext.tsx similarity index 100% rename from src/context/EmojiPickerContext.tsx rename to app/src/context/EmojiPickerContext.tsx diff --git a/src/context/GA4.tsx b/app/src/context/GA4.tsx similarity index 100% rename from src/context/GA4.tsx rename to app/src/context/GA4.tsx diff --git a/src/context/GlobalActions.tsx b/app/src/context/GlobalActions.tsx similarity index 98% rename from src/context/GlobalActions.tsx rename to app/src/context/GlobalActions.tsx index 1689f955..6a450038 100644 --- a/src/context/GlobalActions.tsx +++ b/app/src/context/GlobalActions.tsx @@ -2,7 +2,7 @@ import { Box, Paper, Modal, Typography, Divider, Button, Drawer, useTheme, Toolt import { InspectorProvider } from '../context/Inspector' import { createContext, useCallback, useContext, useEffect, useMemo, useState } from 'react' import { useClient } from './ClientContext' -import { type CommunityTimelineSchema, Schemas, type CoreTimeline } from '@concurrent-world/client' +import { type CommunityTimelineSchema, Schemas, type Timeline } from '@concrnt/worldlib' import { usePreference } from './PreferenceContext' import { ProfileEditor } from '../components/ProfileEditor' import { Menu } from '../components/Menu/Menu' @@ -58,7 +58,7 @@ export const GlobalActionsProvider = (props: GlobalActionsProps): JSX.Element => const setupAccountRequired = client?.user !== null && client?.user.profile === undefined const noListDetected = Object.keys(lists).length === 0 - const [timelines, setTimelines] = useState>>([]) + const [timelines, setTimelines] = useState>>([]) const [selectedTieline, setSelectedTimeline] = useState(undefined) const setupList = useCallback( @@ -226,7 +226,7 @@ export const GlobalActionsProvider = (props: GlobalActionsProps): JSX.Element => {timelines.map((timeline) => ( > - allKnownSubscriptions: Array> - listedSubscriptions: Record> - allProfiles: Array> + allKnownSubscriptions: Array> + listedSubscriptions: Record> + allProfiles: Array> reloadList: () => void getImageURL: (url?: string, params?: { maxWidth?: number; maxHeight?: number; format?: string }) => string setSwitchToSub: (state: boolean) => void @@ -38,7 +33,7 @@ export const GlobalStateProvider = ({ children }: GlobalStateProps): JSX.Element const [lists] = usePreference('lists') const [isDomainOffline, setDomainIsOffline] = useState(false) - const [entity, setEntity] = useState(null) + const [entity, setEntity] = useState(null) const isCanonicalUser = entity ? entity.domain === client?.host : true const [isRegistered, setIsRegistered] = useState(true) const identity = JSON.parse(localStorage.getItem('Identity') || 'null') @@ -46,10 +41,10 @@ export const GlobalStateProvider = ({ children }: GlobalStateProps): JSX.Element const theme = useTheme() const isMobileSize = useMediaQuery(theme.breakpoints.down('sm')) - const [allProfiles, setAllProfiles] = useState>>([]) + const [allProfiles, setAllProfiles] = useState>>([]) const [allKnownTimelines, setAllKnownTimelines] = useState>>([]) - const [allKnownSubscriptions, setAllKnownSubscriptions] = useState>>([]) - const [listedSubscriptions, setListedSubscriptions] = useState>>({}) + const [allKnownSubscriptions, setAllKnownSubscriptions] = useState>>([]) + const [listedSubscriptions, setListedSubscriptions] = useState>>({}) const [switchToSubOpen, setKeyModalOpen] = useState(false) @@ -94,7 +89,7 @@ export const GlobalStateProvider = ({ children }: GlobalStateProps): JSX.Element ) ).then((subs) => { if (unmounted) return - const validsubsarr = subs.filter((e) => e[1]) as Array<[string, CoreSubscription]> + const validsubsarr = subs.filter((e) => e[1]) as Array<[string, Subscription]> const listedSubs = Object.fromEntries(validsubsarr) setListedSubscriptions(listedSubs) @@ -130,7 +125,7 @@ export const GlobalStateProvider = ({ children }: GlobalStateProps): JSX.Element }) ) ).then((subs) => { - const validsubsarr = subs.filter((e) => e[1]) as Array<[string, CoreSubscription]> + const validsubsarr = subs.filter((e) => e[1]) as Array<[string, Subscription]> const listedSubs = Object.fromEntries(validsubsarr) setListedSubscriptions(listedSubs) @@ -153,20 +148,18 @@ export const GlobalStateProvider = ({ children }: GlobalStateProps): JSX.Element useEffect(() => { client.api - .fetchWithCredential(client.host, '/api/v1/entity', { + .fetchWithCredential(client.host, '/api/v1/entity', { method: 'GET' }) - .then((res) => { - if (res.status === 403) { - setIsRegistered(false) - } - res.json().then((json) => { - setEntity(json.content) - }) + .then((data) => { + setEntity(data.content) }) .catch((e) => { - console.error(e) - setDomainIsOffline(true) + if (e instanceof PermissionError) { + setIsRegistered(false) + } else { + setDomainIsOffline(true) + } }) }, [client]) diff --git a/src/context/Inspector.tsx b/app/src/context/Inspector.tsx similarity index 91% rename from src/context/Inspector.tsx rename to app/src/context/Inspector.tsx index 7ca27f34..64639d5a 100644 --- a/src/context/Inspector.tsx +++ b/app/src/context/Inspector.tsx @@ -2,11 +2,10 @@ import { Alert, Box, IconButton, List, ListItem, Paper, Typography } from '@mui/ import { createContext, useContext, useEffect, useMemo, useState } from 'react' import { useClient } from './ClientContext' -import { ValidateSignature, type CoreMessage, type CoreAssociation } from '@concurrent-world/client' +import { ValidateSignature, type Message, type Association, type Key } from '@concrnt/client' import { Codeblock } from '../components/ui/Codeblock' import { MessageContainer } from '../components/Message/MessageContainer' import { CCDrawer } from '../components/ui/CCDrawer' -import { type Key } from '@concurrent-world/client/dist/types/model/core' import { KeyCard } from '../components/ui/KeyCard' import { ListItemTimeline } from '../components/ui/ListItemTimeline' import PlaylistRemoveIcon from '@mui/icons-material/PlaylistRemove' @@ -27,25 +26,27 @@ export const InspectorProvider = (props: InspectorProps): JSX.Element => { const { client } = useClient() const { enqueueSnackbar } = useSnackbar() const [inspectingItem, inspectItem] = useState<{ messageId: string; author: string } | null>(null) - const [message, setMessage] = useState | undefined>() - const [associations, setAssociations] = useState>>([]) + const [message, setMessage] = useState | undefined>() + const [associations, setAssociations] = useState>>([]) const [keyResolution, setKeyResolution] = useState([]) useEffect(() => { if (!inspectingItem) return let isMounted = true - client.api.getMessageWithAuthor(inspectingItem.messageId, inspectingItem.author).then((msg) => { - if (!msg) return - if (!isMounted) return - setMessage(msg) - if (msg.document.keyID && msg.document.keyID !== '') { - client.api.getKeyResolution(msg.document.keyID, inspectingItem.author).then((keys) => { - if (!isMounted) return - setKeyResolution(keys) - }) - } - }) + client.api + .getMessageWithAuthor(inspectingItem.messageId, inspectingItem.author, undefined, { cache: 'no-cache' }) + .then((msg) => { + if (!msg) return + if (!isMounted) return + setMessage(msg) + if (msg.parsedDoc.keyID && msg.parsedDoc.keyID !== '') { + client.api.getKeyResolution(msg.parsedDoc.keyID, inspectingItem.author).then((keys) => { + if (!isMounted) return + setKeyResolution(keys) + }) + } + }) client.api.getMessageAssociationsByTarget(inspectingItem.messageId, inspectingItem.author).then((assocs) => { if (!isMounted) return @@ -60,9 +61,9 @@ export const InspectorProvider = (props: InspectorProps): JSX.Element => { const signatureIsValid = useMemo(() => { if (message) { return ValidateSignature( - message._document, + message.document, message.signature, - message.document.keyID ?? message.document.signer + message.parsedDoc.keyID ?? message.parsedDoc.signer ) } return false @@ -79,7 +80,7 @@ export const InspectorProvider = (props: InspectorProps): JSX.Element => { const isSignedBySubkey = useMemo(() => { if (message) { - return message.document.keyID && message.document.keyID !== '' + return message.parsedDoc.keyID && message.parsedDoc.keyID !== '' } return false }, [message]) @@ -142,10 +143,10 @@ export const InspectorProvider = (props: InspectorProps): JSX.Element => { } if (valid === undefined) { - if (since && message && since > new Date(message.document.signedAt)) { + if (since && message && since > new Date(message.parsedDoc.signedAt)) { valid = false reason = 'keychain is not valid at the time of signing' - } else if (until && message && until < new Date(message.document.signedAt)) { + } else if (until && message && until < new Date(message.parsedDoc.signedAt)) { valid = false reason = 'keychain is not valid at the time of signing' } else { @@ -201,7 +202,7 @@ export const InspectorProvider = (props: InspectorProps): JSX.Element => {
With subkey:
- {message.document.keyID} + {message.parsedDoc.keyID} ) : ( Signature is valid! diff --git a/src/context/MediaViewer.tsx b/app/src/context/MediaViewer.tsx similarity index 100% rename from src/context/MediaViewer.tsx rename to app/src/context/MediaViewer.tsx diff --git a/src/context/PreferenceContext.tsx b/app/src/context/PreferenceContext.tsx similarity index 100% rename from src/context/PreferenceContext.tsx rename to app/src/context/PreferenceContext.tsx diff --git a/src/context/StorageContext.tsx b/app/src/context/StorageContext.tsx similarity index 99% rename from src/context/StorageContext.tsx rename to app/src/context/StorageContext.tsx index dd5e2c30..40740134 100644 --- a/src/context/StorageContext.tsx +++ b/app/src/context/StorageContext.tsx @@ -122,7 +122,7 @@ export const StorageProvider = ({ children }: { children: JSX.Element | JSX.Elem const xhr = new XMLHttpRequest() xhr.open('POST', `https://${client.host}/storage/files`, true) xhr.setRequestHeader('Content-Type', file.type) - xhr.setRequestHeader('Authorization', `Bearer ${client.api.generateApiToken(client.host)}`) + xhr.setRequestHeader('Authorization', `Bearer ${client.api.authProvider.getAuthToken(client.host)}`) xhr.upload.onprogress = (e) => { if (e.lengthComputable) { diff --git a/src/context/Theme.tsx b/app/src/context/Theme.tsx similarity index 96% rename from src/context/Theme.tsx rename to app/src/context/Theme.tsx index a00fbfd3..66c6264d 100644 --- a/src/context/Theme.tsx +++ b/app/src/context/Theme.tsx @@ -17,7 +17,6 @@ export const ConcrntThemeProvider = (props: ConcrntThemeProps): JSX.Element => { useEffect(() => { const newtheme = loadConcurrentTheme(themeName, customThemes, { fontSize: baseFontSize }) newtheme.typography.fontSize = baseFontSize - console.log('fontsize changed:', baseFontSize) localStorage.setItem('theme', JSON.stringify(newtheme)) setTheme(newtheme) let themeColorMetaTag: HTMLMetaElement = document.querySelector('meta[name="theme-color"]') as HTMLMetaElement diff --git a/src/context/Ticker.tsx b/app/src/context/Ticker.tsx similarity index 100% rename from src/context/Ticker.tsx rename to app/src/context/Ticker.tsx diff --git a/src/context/TimelineDrawer.tsx b/app/src/context/TimelineDrawer.tsx similarity index 95% rename from src/context/TimelineDrawer.tsx rename to app/src/context/TimelineDrawer.tsx index 31e5cb93..f197508e 100644 --- a/src/context/TimelineDrawer.tsx +++ b/app/src/context/TimelineDrawer.tsx @@ -1,7 +1,7 @@ import { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react' import { useClient } from './ClientContext' import { CCDrawer } from '../components/ui/CCDrawer' -import { type CommunityTimelineSchema, type Timeline as typeTimeline } from '@concurrent-world/client' +import { type CommunityTimelineSchema, type Timeline as typeTimeline } from '@concrnt/worldlib' import { TimelineHeader } from '../components/TimelineHeader' import { type VListHandle } from 'virtua' @@ -36,10 +36,8 @@ export const TimelineDrawerProvider = (props: TimelineDrawerProps): JSX.Element const navigate = useNavigate() useEffect(() => { - console.log('TimelineDrawerProvider', timelineID) if (!timelineID) return client.getTimeline(timelineID).then((timeline) => { - console.log('TimelineDrawerProvider', timeline) setTimeline(timeline) }) }, [timelineID]) @@ -100,7 +98,7 @@ export const TimelineDrawerProvider = (props: TimelineDrawerProps): JSX.Element > {readable ? ( : <>} /> diff --git a/app/src/context/UserDrawer.tsx b/app/src/context/UserDrawer.tsx new file mode 100644 index 00000000..806d74cd --- /dev/null +++ b/app/src/context/UserDrawer.tsx @@ -0,0 +1,170 @@ +import { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react' +import { useClient } from './ClientContext' +import { CCDrawer } from '../components/ui/CCDrawer' +import { Schemas, type User } from '@concrnt/worldlib' +import { TimelineHeader } from '../components/TimelineHeader' +import { type VListHandle } from 'virtua' + +import { Box, Collapse, Divider, Tab, Tabs } from '@mui/material' + +import { useLocation } from 'react-router-dom' +import AlternateEmailIcon from '@mui/icons-material/AlternateEmail' +import { Profile } from '../components/Profile' +import { QueryTimelineReader } from '../components/QueryTimeline' +import { TimelineFilter } from '../components/TimelineFilter' +import { useTranslation } from 'react-i18next' + +export interface UserDrawerState { + open: (id: string) => void +} + +const UserDrawerContext = createContext(undefined) + +interface UserDrawerProps { + children: JSX.Element | JSX.Element[] +} + +export const UserDrawerProvider = (props: UserDrawerProps): JSX.Element => { + const { client } = useClient() + + const [CCID, setCCID] = useState(null) + const [user, setUser] = useState(null) + + useEffect(() => { + if (!CCID) return + client.getUser(CCID).then((user) => { + setUser(user) + }) + }, [CCID]) + + const { t } = useTranslation('', { keyPrefix: 'common' }) + + const timelineRef = useRef(null) + + const [showHeader, setShowHeader] = useState(false) + + const path = useLocation() + const subProfileID = path.hash.replace('#', '') + + const [filter, setFilter] = useState(undefined) + + const [tab, setTab] = useState<'' | 'media' | 'activity'>('') + + const targetTimeline = useMemo(() => { + switch (tab ?? '') { + case '': + case 'media': + if (subProfileID) return 'world.concrnt.t-subhome.' + subProfileID + '@' + user?.ccid + return user?.homeTimeline + case 'activity': + return user?.associationTimeline + } + }, [user, tab, subProfileID]) + + const query = useMemo(() => { + switch (tab) { + case 'media': + return { schema: Schemas.mediaMessage } + case 'activity': + return { schema: filter } + default: + return {} + } + }, [tab, filter]) + + const open = useCallback((id: string) => { + setCCID(id) + }, []) + + return ( + { + return { + open + } + }, [])} + > + {props.children} + { + setCCID(null) + // hash がある場合は削除 + if (subProfileID) { + window.location.hash = '' + } + setUser(null) + }} + > + + + + } + onTitleClick={() => { + timelineRef.current?.scrollToIndex(0, { align: 'start', smooth: true }) + }} + /> + + + + {targetTimeline && CCID && ( + { + setShowHeader(top > 180) + }} + header={ + <> + { + window.location.hash = id + }} + /> + { + setTab(value) + }} + textColor="secondary" + indicatorColor="secondary" + > + + + + + + {tab === 'activity' && ( + <> + + + + )} + + } + /> + )} + + + + ) +} + +export function useUserDrawer(): UserDrawerState { + return useContext(UserDrawerContext) as UserDrawerState +} diff --git a/src/context/WatchingStreamContext.tsx b/app/src/context/WatchingStreamContext.tsx similarity index 100% rename from src/context/WatchingStreamContext.tsx rename to app/src/context/WatchingStreamContext.tsx diff --git a/src/context/urlSummaryContext.tsx b/app/src/context/urlSummaryContext.tsx similarity index 89% rename from src/context/urlSummaryContext.tsx rename to app/src/context/urlSummaryContext.tsx index b87ccfc5..c9a7991a 100644 --- a/src/context/urlSummaryContext.tsx +++ b/app/src/context/urlSummaryContext.tsx @@ -1,4 +1,4 @@ -import { fetchWithTimeout } from '@concurrent-world/client' +import { fetchWithTimeout } from '@concrnt/client' import { createContext, useCallback, useContext, useRef } from 'react' export interface Summary { @@ -27,7 +27,7 @@ export const UrlSummaryProvider = (props: UrlSummaryProviderProps): JSX.Element const getSummary = useCallback( async (url: string): Promise => { if (url in cache.current) return await cache.current[url] - const response = await fetchWithTimeout(props.host, `/summary?url=${url}`, {}).catch(() => { + const response = await fetchWithTimeout(`https://${props.host}/summary?url=${url}`, {}).catch(() => { cache.current[url] = Promise.resolve(undefined) }) if (!response) return undefined diff --git a/src/declare/window.d.ts b/app/src/declare/window.d.ts similarity index 100% rename from src/declare/window.d.ts rename to app/src/declare/window.d.ts diff --git a/src/hooks/useObjectList.ts b/app/src/hooks/useObjectList.ts similarity index 100% rename from src/hooks/useObjectList.ts rename to app/src/hooks/useObjectList.ts diff --git a/src/hooks/usePersistent.ts b/app/src/hooks/usePersistent.ts similarity index 100% rename from src/hooks/usePersistent.ts rename to app/src/hooks/usePersistent.ts diff --git a/src/hooks/useRefWithForceUpdate.ts b/app/src/hooks/useRefWithForceUpdate.ts similarity index 100% rename from src/hooks/useRefWithForceUpdate.ts rename to app/src/hooks/useRefWithForceUpdate.ts diff --git a/src/hooks/useResourceManager.ts b/app/src/hooks/useResourceManager.ts similarity index 92% rename from src/hooks/useResourceManager.ts rename to app/src/hooks/useResourceManager.ts index 5396b3b0..3d796d57 100644 --- a/src/hooks/useResourceManager.ts +++ b/app/src/hooks/useResourceManager.ts @@ -7,9 +7,7 @@ export interface IuseResourceManager { invalidate: (key: string) => void } -export function useResourceManager( - resolver: (key: string) => Promise -): IuseResourceManager { +export function useResourceManager(resolver: (key: string) => Promise): IuseResourceManager { const body = useRef>({}) const get = useCallback(async (key: string): Promise => { diff --git a/src/i18n.ts b/app/src/i18n.ts similarity index 94% rename from src/i18n.ts rename to app/src/i18n.ts index 1bb7f5a8..b8b0562b 100644 --- a/src/i18n.ts +++ b/app/src/i18n.ts @@ -12,7 +12,7 @@ i18n.use(Backend) 'zh-CN': ['zh-Hans', 'en'], 'zh-HK': ['zh-Hant', 'en'], 'zh-TW': ['zh-Hant', 'en'], - 'default': ['en'] + default: ['en'] }, interpolation: { escapeValue: false // not needed for react as it escapes by default diff --git a/src/main.tsx b/app/src/main.tsx similarity index 100% rename from src/main.tsx rename to app/src/main.tsx diff --git a/src/model.ts b/app/src/model.ts similarity index 95% rename from src/model.ts rename to app/src/model.ts index 16c0af0f..67c4d3b8 100644 --- a/src/model.ts +++ b/app/src/model.ts @@ -9,9 +9,9 @@ import type { TypeText } from '@mui/material/styles/createPalette' -import type { CommunityTimelineSchema, CoreTimelineItem, Timeline } from '@concurrent-world/client' +import type { TimelineItem } from '@concrnt/client' -export interface StreamItemDated extends CoreTimelineItem { +export interface StreamItemDated extends TimelineItem { LastUpdated: number } diff --git a/src/pages/AccountImport.tsx b/app/src/pages/AccountImport.tsx similarity index 98% rename from src/pages/AccountImport.tsx rename to app/src/pages/AccountImport.tsx index 17fca91a..38258080 100644 --- a/src/pages/AccountImport.tsx +++ b/app/src/pages/AccountImport.tsx @@ -9,7 +9,7 @@ import PasswordIcon from '@mui/icons-material/Password' import { IconButtonWithLabel } from '../components/ui/IconButtonWithLabel' import { useTranslation } from 'react-i18next' import { Suspense, lazy, useState } from 'react' -import { Client } from '@concurrent-world/client' +import { Client } from '@concrnt/worldlib' import { Helmet } from 'react-helmet-async' const QRCodeReader = lazy(() => import('../components/ui/QRCodeReader')) diff --git a/src/pages/Associations.tsx b/app/src/pages/Associations.tsx similarity index 91% rename from src/pages/Associations.tsx rename to app/src/pages/Associations.tsx index 44608a50..ab5380ed 100644 --- a/src/pages/Associations.tsx +++ b/app/src/pages/Associations.tsx @@ -6,7 +6,7 @@ import { useClient } from '../context/ClientContext' export function Associations(): JSX.Element { const { client } = useClient() - const streams = useMemo(() => { + const timelines = useMemo(() => { const target = client.user?.notificationTimeline return target ? [target] : [] }, [client]) @@ -31,7 +31,7 @@ export function Associations(): JSX.Element { - + ) } diff --git a/src/pages/Concord.tsx b/app/src/pages/Concord.tsx similarity index 100% rename from src/pages/Concord.tsx rename to app/src/pages/Concord.tsx diff --git a/src/pages/Contacts.tsx b/app/src/pages/Contacts.tsx similarity index 100% rename from src/pages/Contacts.tsx rename to app/src/pages/Contacts.tsx diff --git a/src/pages/Devtool.tsx b/app/src/pages/Devtool.tsx similarity index 100% rename from src/pages/Devtool.tsx rename to app/src/pages/Devtool.tsx diff --git a/src/pages/Entity.tsx b/app/src/pages/Entity.tsx similarity index 98% rename from src/pages/Entity.tsx rename to app/src/pages/Entity.tsx index 924da4bd..7f0b7678 100644 --- a/src/pages/Entity.tsx +++ b/app/src/pages/Entity.tsx @@ -2,7 +2,7 @@ import { Box, Collapse, Divider, Tab, Tabs } from '@mui/material' import { useEffect, useMemo, useRef, useState } from 'react' import { useLocation, useNavigate, useParams } from 'react-router-dom' import { useClient } from '../context/ClientContext' -import { type EmptyTimelineSchema, Schemas, type Timeline, type User } from '@concurrent-world/client' +import { type EmptyTimelineSchema, Schemas, type Timeline, type User } from '@concrnt/worldlib' import { type VListHandle } from 'virtua' import { TimelineHeader } from '../components/TimelineHeader' import AlternateEmailIcon from '@mui/icons-material/AlternateEmail' @@ -46,7 +46,7 @@ export function EntityPage(): JSX.Element { useEffect(() => { if (!id) return - client.getUser(id, hint).then((user) => { + client.getUser(id, hint, { TTL: 0 }).then((user) => { setUser(user) }) }, [id]) diff --git a/src/pages/Explorer.tsx b/app/src/pages/Explorer.tsx similarity index 96% rename from src/pages/Explorer.tsx rename to app/src/pages/Explorer.tsx index d24db8b4..aad15bb7 100644 --- a/src/pages/Explorer.tsx +++ b/app/src/pages/Explorer.tsx @@ -1,5 +1,6 @@ import { Box, Button, Divider, Tab, Tabs, TextField, Typography, useTheme } from '@mui/material' -import { type CommunityTimelineSchema, Schemas, type CoreProfile, type Timeline } from '@concurrent-world/client' +import { type CommunityTimelineSchema, Schemas, type Timeline } from '@concrnt/worldlib' +import { type Profile } from '@concrnt/client' import { useClient } from '../context/ClientContext' import { NavLink, useLocation, useNavigate, useParams } from 'react-router-dom' import { useEffect, useMemo, useState } from 'react' @@ -22,7 +23,7 @@ interface StreamWithDomain { interface ProfileWithDomain { domain: string - profile: CoreProfile + profile: Profile } export function Explorer(): JSX.Element { @@ -65,8 +66,8 @@ export function Explorer(): JSX.Element { const { enqueueSnackbar } = useSnackbar() const selectedDomains = useMemo(() => { - return hashQuery.domains?.split(',') ?? [client.api.host] - }, [hashQuery, client.api.host]) + return hashQuery.domains?.split(',') ?? [client.host] + }, [hashQuery, client.host]) const updateHash = (key: string, value: string): void => { hashQuery[key] = value @@ -78,7 +79,6 @@ export function Explorer(): JSX.Element { const load = (): void => { client.api.getDomains().then((e) => { - if (!client.api.host) return const domains = [client.host, ...e.filter((e) => e.fqdn !== client.host).map((e) => e.fqdn)] setDomains(domains) }) @@ -139,7 +139,7 @@ export function Explorer(): JSX.Element { .flat() .reverse() .filter((e) => { - return 'username' in e.profile.document.body && 'avatar' in e.profile.document.body + return 'username' in e.profile.parsedDoc.body && 'avatar' in e.profile.parsedDoc.body }) .sort((a, b) => { if (a.profile.cdate < b.profile.cdate) return 1 @@ -186,8 +186,6 @@ export function Explorer(): JSX.Element { ) }, [search]) - if (!client.api.host) return <>loading... - return ( <> @@ -294,7 +292,7 @@ export function Explorer(): JSX.Element { return ( {t('createNewCommunity.desc1')} - {client.api.host} + {client.host} {t('createNewCommunity.desc2')} diff --git a/src/pages/ExplorerPlus.tsx b/app/src/pages/ExplorerPlus.tsx similarity index 80% rename from src/pages/ExplorerPlus.tsx rename to app/src/pages/ExplorerPlus.tsx index 7264cdd9..6910e20f 100644 --- a/src/pages/ExplorerPlus.tsx +++ b/app/src/pages/ExplorerPlus.tsx @@ -34,7 +34,7 @@ import ForumIcon from '@mui/icons-material/Forum' import EmojiPeopleIcon from '@mui/icons-material/EmojiPeople' import { useClient } from '../context/ClientContext' import { useSnackbar } from 'notistack' -import { type CommunityTimelineSchema, Schemas } from '@concurrent-world/client' +import { type CommunityTimelineSchema, Schemas } from '@concrnt/worldlib' import { CCEditor } from '../components/ui/cceditor' import { CCDrawer } from '../components/ui/CCDrawer' @@ -382,73 +382,104 @@ export function ExplorerPlusPage(): JSX.Element { > {timelines.map((tl) => { return ( - - - - - + - - - {tl._parsedDocument.body.name} - - + - {tl._parsedDocument.body.description} - - + override={tl._parsedDocument.body.banner} + /> + - - - {getDomainFromFQDN(tl.domainFQDN)?.meta?.nickname} - - - - - { - open(tl.id) - }} - > - - - + + + {tl._parsedDocument.body.name} + + + {tl._parsedDocument.body.description} + + + + + + {getDomainFromFQDN(tl.domainFQDN)?.meta?.nickname} + + { + e.stopPropagation() + e.preventDefault() + }} + > + + + { + open(tl.id) + }} + > + + + + - + ) })} @@ -465,7 +496,7 @@ export function ExplorerPlusPage(): JSX.Element { {t('createNewCommunity.desc1')} - {client.api.host} + {client.host} {t('createNewCommunity.desc2')} @@ -581,7 +612,7 @@ export function ExplorerPlusPage(): JSX.Element { () useEffect(() => { if (!authorID || !messageID) return - const client = new Client('ariake.concrnt.net') - initializeClient(client) let isMounted = true - client - .getMessage(messageID, authorID) - .then((msg) => { - if (!isMounted || !msg) return - setMessage(msg) + Client.createAsGuest('ariake.concrnt.net').then((client) => { + initializeClient(client) + client + .getMessage(messageID, authorID) + .then((msg) => { + if (!isMounted || !msg) return + setMessage(msg) - msg.getReplyMessages().then((replies) => { - if (!isMounted) return - setReplies(replies) - }) - - if (msg.schema === Schemas.replyMessage) { - msg.getReplyTo().then((replyTo) => { + msg.getReplyMessages().then((replies) => { if (!isMounted) return - setReplyTo(replyTo) + setReplies(replies) }) - } - }) - .finally(() => { - if (!isMounted) return - setIsFetching(false) - }) + + if (msg.schema === Schemas.replyMessage) { + msg.getReplyTo().then((replyTo) => { + if (!isMounted) return + setReplyTo(replyTo) + }) + } + }) + .finally(() => { + if (!isMounted) return + setIsFetching(false) + }) + }) return () => { isMounted = false @@ -191,7 +192,7 @@ export default function GuestMessagePage(): JSX.Element { itemProp="datePublished" content={new Date(message.cdate).toISOString()} /> -