├── .dockerignore ├── .env.example ├── .env.mock ├── .gitattributes ├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── bug_report.md │ ├── config.yml │ └── feature_request.md ├── renovate.json5 └── workflows │ ├── ci.yml │ ├── release.yml │ └── semantic-pull-request.yml ├── .gitignore ├── .npmrc ├── .nvmrc ├── .stackblitz └── codeflow.json ├── .stackblitzrc ├── .vscode ├── extensions.json └── settings.json ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── Dockerfile ├── LICENSE ├── README.md ├── app.vue ├── components ├── account │ ├── AccountAvatar.vue │ ├── AccountBigAvatar.vue │ ├── AccountBigCard.vue │ ├── AccountBigCardSkeleton.vue │ ├── AccountBotIndicator.vue │ ├── AccountCard.vue │ ├── AccountDisplayName.vue │ ├── AccountFollowButton.vue │ ├── AccountHandle.vue │ ├── AccountHeader.vue │ ├── AccountHoverCard.vue │ ├── AccountHoverWrapper.vue │ ├── AccountInfo.vue │ ├── AccountInlineInfo.vue │ ├── AccountLockIndicator.vue │ ├── AccountMoreButton.vue │ ├── AccountMoved.vue │ ├── AccountPaginator.vue │ ├── AccountPostsFollowers.vue │ ├── AccountRolesIndicator.vue │ ├── AccountTabs.vue │ └── TagHoverWrapper.vue ├── aria │ ├── AriaAnnouncer.vue │ ├── AriaLog.vue │ └── AriaStatus.vue ├── command │ ├── CommandItem.vue │ ├── CommandKey.vue │ └── CommandPanel.vue ├── common │ ├── AnimateNumber.vue │ ├── CommonAlert.vue │ ├── CommonBlurhash.vue │ ├── CommonCheckbox.vue │ ├── CommonCropImage.vue │ ├── CommonErrorMessage.vue │ ├── CommonInputImage.vue │ ├── CommonMask.vue │ ├── CommonNotFound.vue │ ├── CommonPaginator.vue │ ├── CommonPreviewPrompt.vue │ ├── CommonRadio.vue │ ├── CommonRouteTabs.vue │ ├── CommonScrollIntoView.vue │ ├── CommonTabs.vue │ ├── CommonTooltip.vue │ ├── CommonTrending.vue │ ├── CommonTrendingCharts.vue │ ├── LocalizedNumber.vue │ ├── OfflineChecker.vue │ └── dropdown │ │ ├── Dropdown.vue │ │ └── DropdownItem.vue ├── content │ ├── ContentCode.vue │ ├── ContentMentionGroup.vue │ └── ContentRich.setup.ts ├── conversation │ ├── ConversationCard.vue │ └── ConversationPaginator.vue ├── emoji │ └── Emoji.vue ├── help │ └── HelpPreview.vue ├── list │ ├── Account.vue │ ├── ListEntry.vue │ └── Lists.vue ├── magickeys │ └── MagickeysKeyboardShortcuts.vue ├── main │ └── MainContent.vue ├── modal │ ├── DurationPicker.vue │ ├── ModalConfirm.vue │ ├── ModalContainer.vue │ ├── ModalDialog.vue │ ├── ModalError.vue │ ├── ModalMediaPreview.vue │ └── ModalMediaPreviewCarousel.vue ├── nav │ ├── NavBottom.vue │ ├── NavBottomMoreMenu.vue │ ├── NavFooter.vue │ ├── NavLogo.vue │ ├── NavSide.vue │ ├── NavSideItem.vue │ ├── NavTitle.vue │ ├── NavUser.vue │ ├── NavUserSkeleton.vue │ └── button │ │ ├── Compose.vue │ │ ├── Explore.vue │ │ ├── Favorite.vue │ │ ├── Hashtag.vue │ │ ├── Home.vue │ │ ├── List.vue │ │ ├── Local.vue │ │ ├── Mention.vue │ │ ├── MoreMenu.vue │ │ ├── Notification.vue │ │ └── Search.vue ├── notification │ ├── NotificationCard.vue │ ├── NotificationEnablePushNotification.client.vue │ ├── NotificationGroupedFollow.vue │ ├── NotificationGroupedLikes.vue │ ├── NotificationPaginator.vue │ ├── NotificationPreferences.client.vue │ └── NotificationSubscribePushNotificationError.vue ├── publish │ ├── PublishAttachment.vue │ ├── PublishCharacterCounter.vue │ ├── PublishEditorTools.vue │ ├── PublishEmojiPicker.client.vue │ ├── PublishLanguagePicker.vue │ ├── PublishThreadTools.vue │ ├── PublishWidget.vue │ ├── PublishWidgetFull.client.vue │ └── PublishWidgetList.vue ├── pwa │ ├── PwaBadge.client.vue │ ├── PwaInstallPrompt.client.vue │ └── PwaPrompt.client.vue ├── report │ └── ReportModal.vue ├── search │ ├── SearchAccountInfo.vue │ ├── SearchEmojiInfo.vue │ ├── SearchHashtagInfo.vue │ ├── SearchResult.vue │ ├── SearchResultSkeleton.vue │ └── SearchWidget.vue ├── settings │ ├── SettingsBottomNav.vue │ ├── SettingsColorMode.vue │ ├── SettingsFontSize.vue │ ├── SettingsItem.vue │ ├── SettingsLanguage.vue │ ├── SettingsProfileMetadata.vue │ ├── SettingsSponsorsList.vue │ ├── SettingsThemeColors.vue │ ├── SettingsToggleItem.vue │ └── SettingsTranslations.vue ├── status │ ├── StatusAccountDetails.vue │ ├── StatusActionButton.vue │ ├── StatusActions.vue │ ├── StatusActionsMore.vue │ ├── StatusAttachment.vue │ ├── StatusBody.vue │ ├── StatusCard.vue │ ├── StatusCardSkeleton.vue │ ├── StatusContent.vue │ ├── StatusDetails.vue │ ├── StatusEmbeddedMedia.vue │ ├── StatusFavouritedBoostedBy.vue │ ├── StatusLink.vue │ ├── StatusMedia.vue │ ├── StatusNotFound.vue │ ├── StatusPreviewCard.vue │ ├── StatusPreviewCardInfo.vue │ ├── StatusPreviewCardMoreFromAuthor.vue │ ├── StatusPreviewCardNormal.vue │ ├── StatusPreviewCardSkeleton.vue │ ├── StatusPreviewGitHub.vue │ ├── StatusPreviewStackBlitz.vue │ ├── StatusReplyingTo.vue │ ├── StatusSpoiler.vue │ └── StatusTranslation.vue ├── tag │ ├── TagActionButton.vue │ ├── TagCard.vue │ ├── TagCardPaginator.vue │ └── TagCardSkeleton.vue ├── timeline │ ├── TimelineBlocks.vue │ ├── TimelineConversations.vue │ ├── TimelineDomainBlocks.vue │ ├── TimelineFavourites.vue │ ├── TimelineHome.vue │ ├── TimelineMutes.vue │ ├── TimelineNotifications.vue │ ├── TimelinePaginator.vue │ ├── TimelinePinned.vue │ ├── TimelinePublic.vue │ ├── TimelinePublicLocal.vue │ └── TimelineSkeleton.vue ├── tiptap │ ├── TiptapCodeBlock.vue │ ├── TiptapEmojiList.vue │ ├── TiptapHashtagList.vue │ └── TiptapMentionList.vue └── user │ ├── UserDropdown.vue │ ├── UserPicker.vue │ ├── UserSignIn.vue │ ├── UserSignInEntry.vue │ └── UserSwitcher.vue ├── composables ├── about.ts ├── aria.ts ├── cache.ts ├── command.ts ├── content-parse.ts ├── content-render.ts ├── dialog.ts ├── i18n.ts ├── idb │ └── index.ts ├── injections.ts ├── langugage.ts ├── magickeys.ts ├── mask.ts ├── masto │ ├── account.ts │ ├── icons.ts │ ├── masto.ts │ ├── notification.ts │ ├── publish.ts │ ├── relationship.ts │ ├── routes.ts │ ├── search.ts │ ├── status.ts │ ├── statusDrafts.ts │ └── translate.ts ├── misc.ts ├── notification.ts ├── paginator.ts ├── push-notifications │ ├── createPushSubscription.ts │ ├── types.ts │ └── usePushManager.ts ├── screen.ts ├── settings │ ├── definition.ts │ ├── index.ts │ ├── metadata.ts │ └── storage.ts ├── setups.ts ├── shiki.ts ├── sign-in.ts ├── thread.ts ├── timeline.ts ├── tiptap.ts ├── tiptap │ ├── custom-emoji.ts │ ├── emoji.ts │ ├── shiki-parser.ts │ ├── shiki.ts │ └── suggestion.ts ├── users.ts ├── vue.ts └── web-share-target.ts ├── config ├── emojis.ts ├── env.ts ├── i18n.config.ts ├── i18n.ts └── pwa.ts ├── constants ├── index.ts ├── options.ts ├── symbols.ts └── themes.json ├── docker-compose.yaml ├── docs ├── .env.example ├── .gitignore ├── README.md ├── app.config.ts ├── app.vue ├── components │ └── global │ │ ├── ClipboardIcon.vue │ │ ├── IconBluesky.vue │ │ ├── Logo.vue │ │ ├── ToggleIcon.vue │ │ └── TranslationState.vue ├── content │ ├── 0.index.md │ ├── 1.guide │ │ ├── 1.index.md │ │ ├── 2.features.md │ │ ├── 3.contributing.md │ │ └── 4.sponsoring.md │ ├── 2.deployment │ │ └── 1.netlify.md │ ├── 80.pwa.md │ └── 99.privacy.md ├── netlify.toml ├── nuxt.config.ts ├── package.json ├── public │ ├── apple-touch-icon.png │ ├── elk-screenshot.png │ ├── favicon.ico │ ├── fonts │ │ └── DM-sans-v11.ttf │ ├── images │ │ ├── nuxtlabs.svg │ │ ├── selfhosting-guide │ │ │ ├── cf-api-token-settings.png │ │ │ └── github-fork.png │ │ └── stackblitz.svg │ ├── logo.svg │ ├── pwa-192x192.png │ ├── pwa-512x512.png │ ├── screenshot.png │ └── site.webmanifest ├── tokens.config.ts ├── tsconfig.json └── types.ts ├── elk.svg ├── emoji-mart-traslation.d.ts ├── error.vue ├── eslint.config.js ├── https-dev-config ├── local-https-server.mjs ├── localhost.crt └── localhost.key ├── images ├── nuxtlabs.svg └── stackblitz.svg ├── layouts ├── default.vue └── none.vue ├── locales ├── ar-EG.json ├── ar.json ├── ca-ES.json ├── ca-valencia.json ├── ca.json ├── ckb.json ├── cs-CZ.json ├── cy.json ├── de-DE.json ├── el-GR.json ├── en-CA.json ├── en-GB.json ├── en-US.json ├── en.json ├── es-419.json ├── es-ES.json ├── es.json ├── eu-ES.json ├── fa-IR.json ├── fi.json ├── fr-FR.json ├── gl-ES.json ├── hu-HU.json ├── id-ID.json ├── it-IT.json ├── ja-JP.json ├── ko-KR.json ├── nl-NL.json ├── pl-PL.json ├── pt-BR.json ├── pt-PT.json ├── pt.json ├── ru-RU.json ├── th-TH.json ├── tl-PH.json ├── tr-TR.json ├── uk-UA.json ├── vi-VN.json ├── zh-CN.json └── zh-TW.json ├── middleware ├── 1.permalink.global.ts ├── 2.single-instance.global.ts └── auth.ts ├── mocks ├── class.ts ├── prosemirror.ts ├── semver.ts └── tiptap.ts ├── modules ├── build-env.ts ├── emoji-mart-translation.ts ├── purge-comments.ts ├── pwa │ ├── config.ts │ ├── i18n.ts │ ├── index.ts │ ├── runtime │ │ ├── pwa-plugin.client.ts │ │ └── types.d.ts │ └── types.ts └── tauri │ ├── index.ts │ └── runtime │ ├── build-info.ts │ ├── logging.client.ts │ ├── nitro.client.ts │ ├── storage-config.ts │ └── storage.ts ├── netlify.toml ├── nuxt.config.ts ├── package.json ├── page-lifecycle.d.ts ├── pages ├── [...permalink].vue ├── [[server]] │ ├── @[account] │ │ ├── [status].vue │ │ ├── index.vue │ │ └── index │ │ │ ├── followers.vue │ │ │ ├── following.vue │ │ │ ├── index.vue │ │ │ ├── media.vue │ │ │ └── with_replies.vue │ ├── explore.vue │ ├── explore │ │ ├── index.vue │ │ ├── links.vue │ │ ├── tags.vue │ │ └── users.vue │ ├── list │ │ └── [list] │ │ │ ├── index.vue │ │ │ └── index │ │ │ ├── accounts.vue │ │ │ └── index.vue │ ├── lists.vue │ ├── lists │ │ └── index.vue │ ├── public │ │ ├── index.vue │ │ └── local.vue │ ├── search.vue │ ├── status │ │ └── [status].vue │ └── tags │ │ └── [tag].vue ├── blocks.vue ├── compose.vue ├── conversations.vue ├── domain_blocks.vue ├── favourites.vue ├── hashtags.vue ├── hashtags │ └── index.vue ├── home.vue ├── index.vue ├── intent │ └── post.vue ├── mutes.vue ├── notifications.vue ├── notifications │ ├── [filter].vue │ └── index.vue ├── pinned.vue ├── settings.vue ├── settings │ ├── about │ │ └── index.vue │ ├── index.vue │ ├── interface │ │ └── index.vue │ ├── language │ │ └── index.vue │ ├── notifications │ │ ├── index.vue │ │ ├── notifications.vue │ │ └── push-notifications.vue │ ├── preferences │ │ └── index.vue │ ├── profile │ │ ├── appearance.vue │ │ ├── featured-tags.vue │ │ └── index.vue │ └── users │ │ └── index.vue └── share-target.vue ├── patches ├── .gitkeep └── pinceau.patch ├── plugins ├── 0.setup-users.ts ├── 1.scroll-to-top.ts ├── color-mode.ts ├── floating-vue.ts ├── hydration.client.ts ├── magic-keys.client.ts ├── page-lifecycle.client.ts ├── path.ts ├── setup-global-effects.client.ts ├── setup-head-script.server.ts ├── setup-i18n.ts └── social.server.ts ├── pnpm-lock.yaml ├── pnpm-workspace.yaml ├── public-dev ├── apple-touch-icon.png ├── favicon.ico ├── logo.svg ├── maskable-icon.png ├── pwa-192x192.png ├── pwa-512x512.png └── pwa-64x64.png ├── public-staging ├── apple-touch-icon.png ├── favicon.ico ├── logo.svg ├── maskable-icon.png ├── pwa-192x192.png ├── pwa-512x512.png ├── pwa-64x64.png └── robots.txt ├── public ├── _redirects ├── apple-touch-icon.png ├── avatars │ ├── antfu-100x100.png │ ├── antfu-60x60.png │ ├── danielroe-100x100.png │ ├── danielroe-60x60.png │ ├── patak-dev-100x100.png │ ├── patak-dev-60x60.png │ ├── shuuji3-100x100.png │ ├── shuuji3-60x60.png │ ├── sxzz-100x100.png │ ├── sxzz-60x60.png │ ├── userquin-100x100.png │ └── userquin-60x60.png ├── elk-og.png ├── favicon.ico ├── fonts │ ├── DM-mono-v10.ttf │ ├── DM-sans-v11.ttf │ ├── DM-serif-display-v10.ttf │ └── homemade-apple-v18.ttf ├── logo.svg ├── maskable-icon.png ├── nimbus.svg ├── pwa-192x192.png ├── pwa-512x512.png ├── pwa-64x64.png ├── robots.txt ├── screenshots │ ├── dark-1.webp │ └── light-1.webp └── shortcuts │ ├── compose-96x96.png │ ├── compose.png │ ├── home-96x96.png │ ├── home.png │ ├── local-96x96.png │ ├── local.png │ ├── notifications-96x96.png │ ├── notifications.png │ ├── settings-96x96.png │ └── settings.png ├── scripts ├── avatars.ts ├── cleanup-translations.ts ├── generate-pwa-icons.ts ├── generate-themes.ts ├── prepare-translation-status.ts ├── prepare.ts └── release.ts ├── server ├── api │ ├── [server] │ │ ├── clear.ts │ │ ├── login.ts │ │ └── oauth │ │ │ └── [origin].ts │ └── list-servers.ts ├── cache-driver.ts └── utils │ └── shared.ts ├── service-worker ├── nimbus-sw.ts ├── notification.ts ├── share-target.ts ├── tsconfig.json ├── types.ts └── web-push-notifications.ts ├── shims.d.ts ├── styles ├── default-theme.css ├── dropdown.css ├── global.css ├── scrollbars.css ├── tiptap.css └── vars.css ├── tests ├── nuxt │ ├── __snapshots__ │ │ ├── content-rich.test.ts.snap │ │ └── html-parse.test.ts.snap │ ├── content-rich.test.ts │ ├── html-parse.test.ts │ └── html-to-text.test.ts ├── setup.ts └── unit │ ├── language.test.ts │ ├── permalinks.test.ts │ └── reorder-timeline.test.ts ├── tsconfig.json ├── types ├── index.ts ├── translation-status.ts └── utils.ts ├── unocss.config.ts ├── utils ├── elk-idb.ts ├── i18n.ts └── language.ts ├── vitest.config.ts └── vue-compiler-options.d.ts /.dockerignore: -------------------------------------------------------------------------------- 1 | # Modified from .gitignore 2 | node_modules 3 | *.log 4 | dist 5 | .output 6 | .nuxt 7 | #.env # Not ignoring this file because it can contain build-related settings. 8 | .DS_Store 9 | .idea/ 10 | .vite-inspect 11 | .netlify/ 12 | .eslintcache 13 | 14 | public/emojis 15 | 16 | *~ 17 | *swp 18 | *swo 19 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | NUXT_PUBLIC_TRANSLATE_API= 2 | NUXT_PUBLIC_DEFAULT_SERVER= 3 | NUXT_PUBLIC_SINGLE_INSTANCE= 4 | NUXT_PUBLIC_PRIVACY_POLICY_URL= 5 | 6 | # Production only 7 | NUXT_CLOUDFLARE_ACCOUNT_ID= 8 | NUXT_CLOUDFLARE_NAMESPACE_ID= 9 | NUXT_CLOUDFLARE_API_TOKEN= 10 | 11 | # 'cloudflare' | 'vercel' | 'fs' 12 | NUXT_STORAGE_DRIVER= 13 | NUXT_STORAGE_FS_BASE= 14 | 15 | NUXT_ADMIN_KEY= 16 | 17 | NUXT_PUBLIC_DISABLE_VERSION_CHECK= 18 | 19 | NUXT_GITHUB_CLIENT_ID= 20 | NUXT_GITHUB_CLIENT_SECRET= 21 | NUXT_GITHUB_INVITE_TOKEN= 22 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto eol=lf 2 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: [] 2 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: 🐞 Bug report 3 | about: Report an issue 4 | labels: ['s: pending triage', 'c: bug'] 5 | --- 6 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | contact_links: 3 | - name: Discord Chat 4 | url: https://discord.gg/JuFDMrTRSD # TODO update with chat.nimbus.town later 5 | about: Ask questions and discuss with other users in real time. 6 | - name: Questions & Discussions 7 | url: https://github.com/nimbus-town/nimbus/discussions 8 | about: Use GitHub discussions for message-board style questions and discussions. 9 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: 🚀 New feature proposal 3 | about: Propose a new feature 4 | labels: 's: pending triage' 5 | --- 6 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: ci 2 | 3 | permissions: {} 4 | 5 | on: 6 | push: 7 | branches: 8 | - main 9 | pull_request: 10 | branches: 11 | - main 12 | workflow_dispatch: {} 13 | merge_group: {} 14 | 15 | jobs: 16 | ci: 17 | runs-on: ubuntu-latest 18 | 19 | steps: 20 | - uses: actions/checkout@v4 21 | # workaround for npm registry key change 22 | # ref. `pnpm@10.1.0` / `pnpm@9.15.4` cannot be installed due to key id mismatch · Issue #612 · nodejs/corepack 23 | # - https://github.com/nodejs/corepack/issues/612#issuecomment-2629496091 24 | - run: npm i -g corepack@latest && corepack enable 25 | - uses: actions/setup-node@v4.2.0 26 | 27 | - name: 📦 Install dependencies 28 | run: pnpm install --frozen-lockfile 29 | 30 | - name: 🚧 Set up project 31 | run: pnpm nuxi prepare 32 | 33 | - name: 🧪 Test project 34 | run: pnpm test:ci 35 | timeout-minutes: 10 36 | 37 | - name: 📝 Lint 38 | run: pnpm lint 39 | 40 | - name: 💪 Type check 41 | run: pnpm test:typecheck 42 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | permissions: 4 | contents: write 5 | 6 | on: 7 | push: 8 | tags: 9 | - 'v*' 10 | 11 | jobs: 12 | release: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/checkout@v4 16 | with: 17 | fetch-depth: 0 18 | 19 | - name: Set node 20 | uses: actions/setup-node@v4 21 | with: 22 | node-version: 18 23 | 24 | - run: npx changelogithub 25 | env: 26 | GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} 27 | -------------------------------------------------------------------------------- /.github/workflows/semantic-pull-request.yml: -------------------------------------------------------------------------------- 1 | name: Semantic Pull Request 2 | 3 | on: 4 | pull_request_target: 5 | types: 6 | - opened 7 | - edited 8 | - synchronize 9 | 10 | permissions: {} 11 | 12 | jobs: 13 | main: 14 | permissions: 15 | pull-requests: read # to analyze PRs (amannn/action-semantic-pull-request) 16 | statuses: write # to mark status of analyzed PR (amannn/action-semantic-pull-request) 17 | 18 | runs-on: ubuntu-latest 19 | name: Semantic Pull Request 20 | steps: 21 | - name: Validate PR title 22 | uses: amannn/action-semantic-pull-request@v5.5.3 23 | env: 24 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 25 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | *.log 3 | dist 4 | .output 5 | .pnpm-store 6 | .nuxt 7 | .env 8 | .DS_Store 9 | .idea/ 10 | .vite-inspect 11 | .netlify/ 12 | .eslintcache 13 | nimbus-translation-status.json 14 | 15 | public/emojis 16 | 17 | *~ 18 | *swp 19 | *swo 20 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | shamefully-hoist=true 2 | shell-emulator=true 3 | ignore-workspace-root-check=true 4 | package-manager-strict=false 5 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | 20 -------------------------------------------------------------------------------- /.stackblitz/codeflow.json: -------------------------------------------------------------------------------- 1 | { 2 | "bot": { 3 | "issues": { 4 | "trigger": "all-issues" 5 | } 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /.stackblitzrc: -------------------------------------------------------------------------------- 1 | { 2 | "installDependencies": true, 3 | "startCommand": "npm run dev:mocked" 4 | } 5 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "antfu.iconify", 4 | "antfu.unocss", 5 | "antfu.goto-alias", 6 | "csstools.postcss", 7 | "dbaeumer.vscode-eslint", 8 | "vue.volar", 9 | "lokalise.i18n-ally" 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022-present, Elk contributors, 2024-present Nimbus contributors 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /app.vue: -------------------------------------------------------------------------------- 1 | 20 | 21 | 37 | -------------------------------------------------------------------------------- /components/account/AccountAvatar.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 35 | -------------------------------------------------------------------------------- /components/account/AccountBigAvatar.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 18 | -------------------------------------------------------------------------------- /components/account/AccountBigCardSkeleton.vue: -------------------------------------------------------------------------------- 1 | 31 | -------------------------------------------------------------------------------- /components/account/AccountBotIndicator.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 22 | -------------------------------------------------------------------------------- /components/account/AccountCard.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 29 | -------------------------------------------------------------------------------- /components/account/AccountDisplayName.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 18 | -------------------------------------------------------------------------------- /components/account/AccountHandle.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 18 | -------------------------------------------------------------------------------- /components/account/AccountHoverCard.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 25 | -------------------------------------------------------------------------------- /components/account/AccountInlineInfo.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 18 | 19 | 32 | -------------------------------------------------------------------------------- /components/account/AccountLockIndicator.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 24 | -------------------------------------------------------------------------------- /components/account/AccountMoved.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 29 | -------------------------------------------------------------------------------- /components/account/AccountRolesIndicator.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 32 | -------------------------------------------------------------------------------- /components/account/AccountTabs.vue: -------------------------------------------------------------------------------- 1 | 40 | 41 | 44 | -------------------------------------------------------------------------------- /components/account/TagHoverWrapper.vue: -------------------------------------------------------------------------------- 1 | 27 | 28 | 47 | -------------------------------------------------------------------------------- /components/aria/AriaLog.vue: -------------------------------------------------------------------------------- 1 | 23 | 24 | 39 | -------------------------------------------------------------------------------- /components/aria/AriaStatus.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 24 | -------------------------------------------------------------------------------- /components/command/CommandKey.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 40 | -------------------------------------------------------------------------------- /components/common/AnimateNumber.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 15 | -------------------------------------------------------------------------------- /components/common/CommonAlert.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 26 | -------------------------------------------------------------------------------- /components/common/CommonBlurhash.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 17 | -------------------------------------------------------------------------------- /components/common/CommonCheckbox.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 37 | 38 | 44 | -------------------------------------------------------------------------------- /components/common/CommonErrorMessage.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 20 | -------------------------------------------------------------------------------- /components/common/CommonMask.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 14 | -------------------------------------------------------------------------------- /components/common/CommonNotFound.vue: -------------------------------------------------------------------------------- 1 | 9 | -------------------------------------------------------------------------------- /components/common/CommonPreviewPrompt.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 28 | -------------------------------------------------------------------------------- /components/common/CommonRadio.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 29 | 30 | 36 | -------------------------------------------------------------------------------- /components/common/CommonScrollIntoView.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 22 | -------------------------------------------------------------------------------- /components/common/CommonTooltip.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 28 | -------------------------------------------------------------------------------- /components/common/CommonTrending.vue: -------------------------------------------------------------------------------- 1 | 18 | 19 | 24 | -------------------------------------------------------------------------------- /components/common/CommonTrendingCharts.vue: -------------------------------------------------------------------------------- 1 | 30 | 31 | 34 | -------------------------------------------------------------------------------- /components/common/LocalizedNumber.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 27 | -------------------------------------------------------------------------------- /components/common/OfflineChecker.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 15 | -------------------------------------------------------------------------------- /components/common/dropdown/Dropdown.vue: -------------------------------------------------------------------------------- 1 | 23 | 24 | 32 | -------------------------------------------------------------------------------- /components/content/ContentCode.vue: -------------------------------------------------------------------------------- 1 | 19 | 20 | 24 | -------------------------------------------------------------------------------- /components/content/ContentMentionGroup.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 12 | -------------------------------------------------------------------------------- /components/content/ContentRich.setup.ts: -------------------------------------------------------------------------------- 1 | defineOptions({ 2 | name: 'ContentRich', 3 | }) 4 | 5 | const { 6 | content, 7 | hideEmojis = false, 8 | markdown = true, 9 | } = defineProps<{ 10 | content: string 11 | hideEmojis?: boolean 12 | markdown?: boolean 13 | }>() 14 | 15 | export default () => h( 16 | 'span', 17 | { class: 'content-rich', dir: 'auto' }, 18 | contentToVNode(content, { 19 | hideEmojis, 20 | markdown, 21 | }), 22 | ) 23 | -------------------------------------------------------------------------------- /components/conversation/ConversationCard.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 27 | -------------------------------------------------------------------------------- /components/conversation/ConversationPaginator.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 26 | -------------------------------------------------------------------------------- /components/emoji/Emoji.vue: -------------------------------------------------------------------------------- 1 | 25 | 26 | 31 | -------------------------------------------------------------------------------- /components/list/Account.vue: -------------------------------------------------------------------------------- 1 | 29 | 30 | 56 | -------------------------------------------------------------------------------- /components/modal/DurationPicker.vue: -------------------------------------------------------------------------------- 1 | 29 | 30 | 46 | -------------------------------------------------------------------------------- /components/modal/ModalError.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 32 | -------------------------------------------------------------------------------- /components/nav/NavUser.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 48 | -------------------------------------------------------------------------------- /components/nav/NavUserSkeleton.vue: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /components/nav/button/Compose.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 12 | -------------------------------------------------------------------------------- /components/nav/button/Explore.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 16 | -------------------------------------------------------------------------------- /components/nav/button/Favorite.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 12 | -------------------------------------------------------------------------------- /components/nav/button/Hashtag.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 12 | -------------------------------------------------------------------------------- /components/nav/button/Home.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 12 | -------------------------------------------------------------------------------- /components/nav/button/List.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 18 | -------------------------------------------------------------------------------- /components/nav/button/Local.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 12 | -------------------------------------------------------------------------------- /components/nav/button/Mention.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 16 | -------------------------------------------------------------------------------- /components/nav/button/MoreMenu.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 20 | -------------------------------------------------------------------------------- /components/nav/button/Notification.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 22 | -------------------------------------------------------------------------------- /components/nav/button/Search.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 12 | -------------------------------------------------------------------------------- /components/publish/PublishCharacterCounter.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 13 | -------------------------------------------------------------------------------- /components/publish/PublishThreadTools.vue: -------------------------------------------------------------------------------- 1 | 28 | 29 | 46 | -------------------------------------------------------------------------------- /components/publish/PublishWidgetList.vue: -------------------------------------------------------------------------------- 1 | 28 | 29 | 44 | -------------------------------------------------------------------------------- /components/pwa/PwaBadge.client.vue: -------------------------------------------------------------------------------- 1 | 14 | -------------------------------------------------------------------------------- /components/pwa/PwaInstallPrompt.client.vue: -------------------------------------------------------------------------------- 1 | 23 | -------------------------------------------------------------------------------- /components/pwa/PwaPrompt.client.vue: -------------------------------------------------------------------------------- 1 | 22 | -------------------------------------------------------------------------------- /components/search/SearchAccountInfo.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 10 | 11 | 24 | -------------------------------------------------------------------------------- /components/search/SearchEmojiInfo.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 23 | -------------------------------------------------------------------------------- /components/search/SearchHashtagInfo.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 32 | -------------------------------------------------------------------------------- /components/search/SearchResult.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 32 | -------------------------------------------------------------------------------- /components/search/SearchResultSkeleton.vue: -------------------------------------------------------------------------------- 1 | 14 | -------------------------------------------------------------------------------- /components/settings/SettingsColorMode.vue: -------------------------------------------------------------------------------- 1 | 28 | 29 | 50 | -------------------------------------------------------------------------------- /components/settings/SettingsLanguage.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 17 | -------------------------------------------------------------------------------- /components/status/StatusAccountDetails.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 22 | -------------------------------------------------------------------------------- /components/status/StatusCardSkeleton.vue: -------------------------------------------------------------------------------- 1 | 16 | -------------------------------------------------------------------------------- /components/status/StatusLink.vue: -------------------------------------------------------------------------------- 1 | 31 | 32 | 49 | -------------------------------------------------------------------------------- /components/status/StatusMedia.vue: -------------------------------------------------------------------------------- 1 | 20 | 21 | 35 | 36 | 47 | -------------------------------------------------------------------------------- /components/status/StatusNotFound.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 31 | -------------------------------------------------------------------------------- /components/status/StatusPreviewCard.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 22 | -------------------------------------------------------------------------------- /components/status/StatusPreviewCardInfo.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 37 | -------------------------------------------------------------------------------- /components/status/StatusPreviewCardMoreFromAuthor.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 21 | -------------------------------------------------------------------------------- /components/status/StatusPreviewCardSkeleton.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 47 | -------------------------------------------------------------------------------- /components/status/StatusTranslation.vue: -------------------------------------------------------------------------------- 1 | 32 | 33 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /components/tag/TagCardPaginator.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 23 | -------------------------------------------------------------------------------- /components/tag/TagCardSkeleton.vue: -------------------------------------------------------------------------------- 1 | 12 | -------------------------------------------------------------------------------- /components/timeline/TimelineBlocks.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 8 | -------------------------------------------------------------------------------- /components/timeline/TimelineConversations.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 8 | -------------------------------------------------------------------------------- /components/timeline/TimelineDomainBlocks.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 22 | -------------------------------------------------------------------------------- /components/timeline/TimelineFavourites.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 8 | -------------------------------------------------------------------------------- /components/timeline/TimelineHome.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 22 | -------------------------------------------------------------------------------- /components/timeline/TimelineMutes.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 8 | -------------------------------------------------------------------------------- /components/timeline/TimelineNotifications.vue: -------------------------------------------------------------------------------- 1 | 26 | 27 | 30 | -------------------------------------------------------------------------------- /components/timeline/TimelinePinned.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 8 | -------------------------------------------------------------------------------- /components/timeline/TimelinePublic.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 16 | -------------------------------------------------------------------------------- /components/timeline/TimelinePublicLocal.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 16 | -------------------------------------------------------------------------------- /components/timeline/TimelineSkeleton.vue: -------------------------------------------------------------------------------- 1 | 9 | -------------------------------------------------------------------------------- /components/tiptap/TiptapCodeBlock.vue: -------------------------------------------------------------------------------- 1 | 36 | 37 | 57 | -------------------------------------------------------------------------------- /components/user/UserDropdown.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 16 | -------------------------------------------------------------------------------- /components/user/UserPicker.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 47 | -------------------------------------------------------------------------------- /components/user/UserSignInEntry.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 32 | -------------------------------------------------------------------------------- /composables/aria.ts: -------------------------------------------------------------------------------- 1 | export type AriaLive = 'off' | 'polite' | 'assertive' 2 | export type AriaAnnounceType = 'announce' | 'mute' | 'unmute' 3 | 4 | const ariaAnnouncer = useEventBus(Symbol('aria-announcer')) 5 | 6 | export function useAriaAnnouncer() { 7 | const announce = (message: string) => { 8 | ariaAnnouncer.emit('announce', message) 9 | } 10 | 11 | const mute = () => { 12 | ariaAnnouncer.emit('mute') 13 | } 14 | 15 | const unmute = () => { 16 | ariaAnnouncer.emit('unmute') 17 | } 18 | 19 | return { announce, ariaAnnouncer, mute, unmute } 20 | } 21 | 22 | export function useAriaLog() { 23 | const logs = ref([]) 24 | 25 | const announceLogs = (messages: any[]) => { 26 | logs.value = messages 27 | } 28 | 29 | const appendLogs = (messages: any[]) => { 30 | logs.value = logs.value.concat(messages) 31 | } 32 | 33 | const clearLogs = () => { 34 | logs.value = [] 35 | } 36 | 37 | return { 38 | announceLogs, 39 | appendLogs, 40 | clearLogs, 41 | logs, 42 | } 43 | } 44 | 45 | export function useAriaStatus() { 46 | const status = ref('') 47 | 48 | const announceStatus = (message: any) => { 49 | status.value = message 50 | } 51 | 52 | const clearStatus = () => { 53 | status.value = '' 54 | } 55 | 56 | return { 57 | announceStatus, 58 | clearStatus, 59 | status, 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /composables/injections.ts: -------------------------------------------------------------------------------- 1 | import { InjectionKeyDropdownContext } from '~/constants/symbols' 2 | 3 | export function useDropdownContext() { 4 | return inject(InjectionKeyDropdownContext, undefined) 5 | } 6 | -------------------------------------------------------------------------------- /composables/langugage.ts: -------------------------------------------------------------------------------- 1 | import ISO6391 from 'iso-639-1' 2 | 3 | export const languagesNameList: { 4 | code: string 5 | nativeName: string 6 | name: string 7 | }[] = ISO6391.getAllCodes().map(code => ({ 8 | code, 9 | nativeName: ISO6391.getNativeName(code), 10 | name: ISO6391.getName(code), 11 | })) 12 | -------------------------------------------------------------------------------- /composables/magickeys.ts: -------------------------------------------------------------------------------- 1 | import type { ComputedRef } from 'vue' 2 | 3 | // TODO: consider to allow combinations similar to useMagicKeys using proxy? 4 | // e.g. `const magicSequence = useMagicSequence()` 5 | // `magicSequence['Shift+Ctrl+A']` 6 | // `const { Ctrl_A_B } = useMagicSequence()` 7 | 8 | /** 9 | * source: inspired by https://github.com/vueuse/vueuse/issues/427#issuecomment-815619446 10 | * @param keys ordered list of keys making up the sequence 11 | */ 12 | export function useMagicSequence(keys: string[]): ComputedRef { 13 | const magicKeys = useMagicKeys() 14 | 15 | const success = ref(false) 16 | const i = ref(0) 17 | let down = false 18 | 19 | watch( 20 | () => magicKeys.current, 21 | () => { 22 | if (magicKeys[keys[i.value]].value && !down) { 23 | down = true 24 | i.value += 1 25 | } 26 | else if (i.value > 0 && !magicKeys[keys[i.value - 1]].value && down) { 27 | down = false 28 | } 29 | else { 30 | i.value = 0 31 | down = false 32 | success.value = false 33 | } 34 | if (i.value >= keys.length && !down) { 35 | i.value = 0 36 | down = false 37 | success.value = true 38 | } 39 | }, 40 | { 41 | deep: true, 42 | }, 43 | ) 44 | 45 | return computed(() => success.value) 46 | } 47 | -------------------------------------------------------------------------------- /composables/mask.ts: -------------------------------------------------------------------------------- 1 | import { h, render } from 'vue' 2 | import CommonMask from '~/components/common/CommonMask.vue' 3 | 4 | export interface UseMaskOptions { 5 | getContainer?: () => HTMLElement 6 | background?: string 7 | zIndex?: number 8 | } 9 | 10 | export function useMask(options: UseMaskOptions = {}) { 11 | const { 12 | background = 'transparent', 13 | getContainer = () => document.body, 14 | zIndex = 100, 15 | } = options 16 | const wrapperEl = (import.meta.server ? null : document.createElement('div')) as HTMLDivElement 17 | 18 | function show() { 19 | const container = getContainer() 20 | container?.appendChild(wrapperEl) 21 | const MaskComp = h(CommonMask, { background, zIndex }) 22 | render(MaskComp, wrapperEl) 23 | } 24 | 25 | function hide() { 26 | render(null, wrapperEl) 27 | wrapperEl.parentNode?.removeChild(wrapperEl) 28 | } 29 | 30 | return { 31 | show, 32 | hide, 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /composables/misc.ts: -------------------------------------------------------------------------------- 1 | export const UserLinkRE = /^(?:https:\/)?\/([^/]+)\/@([^/]+)$/ 2 | export const TagLinkRE = /^https?:\/\/([^/]+)\/tags\/([^/]+)\/?$/ 3 | export const HTMLTagRE = /<[^>]+>/g 4 | 5 | export function getDataUrlFromArr(arr: Uint8ClampedArray, w: number, h: number) { 6 | if (typeof w === 'undefined' || typeof h === 'undefined') 7 | w = h = Math.sqrt(arr.length / 4) 8 | 9 | const canvas = document.createElement('canvas') 10 | const ctx = canvas.getContext('2d')! 11 | 12 | canvas.width = w 13 | canvas.height = h 14 | 15 | const imgData = ctx.createImageData(w, h) 16 | imgData.data.set(arr) 17 | ctx.putImageData(imgData, 0, 0) 18 | 19 | return canvas.toDataURL() 20 | } 21 | 22 | export function noop() {} 23 | 24 | export function useIsMac() { 25 | const headers = useRequestHeaders(['user-agent']) 26 | return computed(() => headers['user-agent']?.includes('Macintosh') 27 | ?? navigator?.platform?.includes('Mac') ?? false) 28 | } 29 | 30 | export function isEmptyObject(object: object) { 31 | return Object.keys(object).length === 0 32 | } 33 | 34 | export function removeHTMLTags(str: string) { 35 | return str.replaceAll(HTMLTagRE, '') 36 | } 37 | -------------------------------------------------------------------------------- /composables/notification.ts: -------------------------------------------------------------------------------- 1 | import type { mastodon } from 'masto' 2 | import { NOTIFICATION_FILTER_TYPES } from '~/constants' 3 | 4 | /** 5 | * Typeguard to check if an object is a valid notification filter 6 | * @param obj the object to be checked 7 | * @returns boolean and assigns type to object if true 8 | */ 9 | export function isNotificationFilter(obj: unknown): obj is mastodon.v1.NotificationType { 10 | return !!obj && NOTIFICATION_FILTER_TYPES.includes(obj as unknown as mastodon.v1.NotificationType) 11 | } 12 | 13 | /** 14 | * Typeguard to check if an object is a valid notification 15 | * @param obj the object to be checked 16 | * @returns boolean and assigns type to object if true 17 | */ 18 | export function isNotification(obj: unknown): obj is mastodon.v1.NotificationType { 19 | return !!obj && ['mention', ...NOTIFICATION_FILTER_TYPES].includes(obj as unknown as mastodon.v1.NotificationType) 20 | } 21 | -------------------------------------------------------------------------------- /composables/push-notifications/types.ts: -------------------------------------------------------------------------------- 1 | import type { mastodon } from 'masto' 2 | 3 | import type { UserLogin } from '~/types' 4 | 5 | export type SubscriptionResult = 'subscribed' | 'notification-denied' | 'not-supported' | 'invalid-vapid-key' | 'no-user' 6 | export interface PushManagerSubscriptionInfo { 7 | registration: ServiceWorkerRegistration 8 | subscription: PushSubscription | null 9 | } 10 | 11 | export interface RequiredUserLogin extends Required> { 12 | pushSubscription?: mastodon.v1.WebPushSubscription 13 | } 14 | 15 | export interface CreatePushNotification { 16 | alerts?: Partial | null 17 | policy?: mastodon.v1.WebPushSubscriptionPolicy 18 | } 19 | 20 | export type PushNotificationRequest = Record 21 | export type PushNotificationPolicy = Record 22 | 23 | export type PushSubscriptionErrorCode = 'too_many_registrations' | 'vapid_not_supported' | 'invalid_vapid_key' 24 | 25 | export class PushSubscriptionError extends Error { 26 | code: PushSubscriptionErrorCode 27 | constructor(code: PushSubscriptionErrorCode, message?: string) { 28 | super(message) 29 | this.code = code 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /composables/screen.ts: -------------------------------------------------------------------------------- 1 | import { breakpointsTailwind } from '@vueuse/core' 2 | 3 | export const breakpoints = useBreakpoints(breakpointsTailwind) 4 | 5 | export const isSmallScreen = breakpoints.smallerOrEqual('sm') 6 | export const isMediumOrLargeScreen = breakpoints.between('sm', 'xl') 7 | export const isExtraLargeScreen = breakpoints.smallerOrEqual('xl') 8 | -------------------------------------------------------------------------------- /composables/settings/index.ts: -------------------------------------------------------------------------------- 1 | export * from './definition' 2 | export * from './storage' 3 | -------------------------------------------------------------------------------- /composables/settings/metadata.ts: -------------------------------------------------------------------------------- 1 | import type { Node } from 'ultrahtml' 2 | import { decode } from 'tiny-decode' 3 | import { parse, TEXT_NODE } from 'ultrahtml' 4 | 5 | export const maxAccountFieldCount = computed(() => isGlitchEdition.value ? 16 : 4) 6 | 7 | export function convertMetadata(metadata: string) { 8 | try { 9 | const tree = parse(metadata) 10 | return (tree.children as Node[]).map(n => convertToText(n)).join('').trim() 11 | } 12 | catch (err) { 13 | console.error(err) 14 | return '' 15 | } 16 | } 17 | 18 | function convertToText(input: Node): string { 19 | let text = '' 20 | 21 | if (input.type === TEXT_NODE) 22 | return decode(input.value) 23 | 24 | if ('children' in input) 25 | text = (input.children as Node[]).map(n => convertToText(n)).join('') 26 | 27 | return text 28 | } 29 | -------------------------------------------------------------------------------- /composables/tiptap/custom-emoji.ts: -------------------------------------------------------------------------------- 1 | export interface EmojiOptions { 2 | inline: boolean 3 | allowBase64: boolean 4 | HTMLAttributes: Record 5 | } 6 | 7 | declare module '@tiptap/core' { 8 | interface Commands { 9 | emoji: { 10 | /** 11 | * Insert a emoji. 12 | */ 13 | insertEmoji: (native: string) => ReturnType 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /composables/tiptap/shiki-parser.ts: -------------------------------------------------------------------------------- 1 | import type { Parser } from 'prosemirror-highlight/shiki' 2 | import type { BuiltinLanguage } from 'shiki' 3 | import { createParser } from 'prosemirror-highlight/shiki' 4 | 5 | let parser: Parser | undefined 6 | 7 | export const shikiParser: Parser = (options) => { 8 | const lang = options.language ?? 'text' 9 | 10 | // Register the language if it's not yet registered 11 | const { highlighter, promise } = useHighlighter(lang as BuiltinLanguage) 12 | 13 | // If the highlighter or the language is not available, return a promise that 14 | // will resolve when it's ready. When the promise resolves, the editor will 15 | // re-parse the code block. 16 | if (!highlighter) 17 | return promise ?? [] 18 | 19 | if (!parser) 20 | parser = createParser(highlighter) 21 | 22 | return parser(options) 23 | } 24 | -------------------------------------------------------------------------------- /composables/tiptap/shiki.ts: -------------------------------------------------------------------------------- 1 | import CodeBlock from '@tiptap/extension-code-block' 2 | import { VueNodeViewRenderer } from '@tiptap/vue-3' 3 | 4 | import { createHighlightPlugin } from 'prosemirror-highlight' 5 | import TiptapCodeBlock from '~/components/tiptap/TiptapCodeBlock.vue' 6 | import { shikiParser } from './shiki-parser' 7 | 8 | export const TiptapPluginCodeBlockShiki = CodeBlock.extend({ 9 | addOptions() { 10 | return { 11 | ...this.parent?.(), 12 | defaultLanguage: null, 13 | } 14 | }, 15 | 16 | addProseMirrorPlugins() { 17 | return [ 18 | createHighlightPlugin({ parser: shikiParser, nodeTypes: ['codeBlock'] }), 19 | ] 20 | }, 21 | 22 | addNodeView() { 23 | return VueNodeViewRenderer(TiptapCodeBlock) 24 | }, 25 | }) 26 | -------------------------------------------------------------------------------- /composables/web-share-target.ts: -------------------------------------------------------------------------------- 1 | export function useWebShareTarget(listener?: (message: MessageEvent) => void) { 2 | if (import.meta.server) 3 | return 4 | 5 | onBeforeMount(() => { 6 | // PWA must be installed to use share target 7 | if (useNuxtApp().$pwa?.isInstalled && 'serviceWorker' in navigator) { 8 | if (listener) 9 | navigator.serviceWorker.addEventListener('message', listener) 10 | 11 | navigator.serviceWorker.getRegistration() 12 | .then((registration) => { 13 | if (registration && registration.active) { 14 | // we need to signal the service worker that we are ready to receive data 15 | registration.active.postMessage({ action: 'ready-to-receive' }) 16 | } 17 | }) 18 | .catch(err => console.error('Could not get registration', err)) 19 | 20 | if (listener) 21 | onBeforeUnmount(() => navigator.serviceWorker.removeEventListener('message', listener)) 22 | } 23 | }) 24 | } 25 | -------------------------------------------------------------------------------- /config/emojis.ts: -------------------------------------------------------------------------------- 1 | import type { EmojiRegexMatch } from '@iconify/utils/lib/emoji/replace/find' 2 | // @unimport-disabled 3 | import { emojiFilename, emojiPrefix, emojiRegEx } from '@iconify-emoji/twemoji' 4 | import { getEmojiMatchesInText } from '@iconify/utils/lib/emoji/replace/find' 5 | 6 | // Re-export everything from package 7 | export * from '@iconify-emoji/twemoji' 8 | 9 | // Package name 10 | export const iconifyEmojiPackage = '@iconify-emoji/twemoji' 11 | 12 | export function getEmojiAttributes(input: EmojiRegexMatch | string) { 13 | const match = typeof input === 'string' 14 | ? getEmojiMatchesInText(emojiRegEx, input)?.[0] 15 | : input 16 | const file = emojiFilename(match) 17 | const className = `iconify-emoji iconify-emoji--${emojiPrefix}${file.padding ? ' iconify-emoji-padded' : ''}` 18 | return { 19 | class: className, 20 | src: `/emojis/${emojiPrefix}/${file.filename}`, 21 | alt: match.match, 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /config/i18n.config.ts: -------------------------------------------------------------------------------- 1 | import { 2 | currentLocales, 3 | datetimeFormats, 4 | numberFormats, 5 | pluralRules, 6 | } from './i18n' 7 | 8 | export default defineI18nConfig(() => { 9 | return { 10 | legacy: false, 11 | availableLocales: currentLocales.map(l => l.code), 12 | fallbackLocale: 'en-US', 13 | fallbackWarn: true, 14 | missingWarn: true, 15 | datetimeFormats, 16 | numberFormats, 17 | pluralRules, 18 | } 19 | }) 20 | -------------------------------------------------------------------------------- /config/pwa.ts: -------------------------------------------------------------------------------- 1 | import type { VitePWANuxtOptions } from '../modules/pwa/types' 2 | import { isCI, isDevelopment } from 'std-env' 3 | 4 | export const pwa: VitePWANuxtOptions = { 5 | mode: isCI ? 'production' : 'development', 6 | // disable PWA only when in preview mode 7 | disable: /* temporarily test in CI isPreview || */ (isDevelopment && process.env.VITE_DEV_PWA !== 'true'), 8 | scope: '/', 9 | srcDir: './service-worker', 10 | filename: 'nimbus-sw.ts', 11 | strategies: 'injectManifest', 12 | injectRegister: false, 13 | includeManifestIcons: false, 14 | manifest: false, 15 | injectManifest: { 16 | globPatterns: ['**/*.{js,json,css,html,txt,svg,png,ico,webp,woff,woff2,ttf,eot,otf,wasm}'], 17 | globIgnores: ['emojis/**', 'manifest**.webmanifest'], 18 | }, 19 | devOptions: { 20 | enabled: process.env.VITE_DEV_PWA === 'true', 21 | type: 'module', 22 | }, 23 | } 24 | -------------------------------------------------------------------------------- /constants/options.ts: -------------------------------------------------------------------------------- 1 | export const oldFontSizeMap = { 2 | xs: '13px', 3 | sm: '14px', 4 | md: '15px', 5 | lg: '16px', 6 | xl: '17px', 7 | } 8 | -------------------------------------------------------------------------------- /constants/symbols.ts: -------------------------------------------------------------------------------- 1 | import type { InjectionKey } from 'vue' 2 | 3 | export const InjectionKeyDropdownContext: InjectionKey<{ 4 | hide: () => void 5 | }> = Symbol('dropdown-context') 6 | -------------------------------------------------------------------------------- /docker-compose.yaml: -------------------------------------------------------------------------------- 1 | services: 2 | nimbus: 3 | build: 4 | context: . 5 | dockerfile: Dockerfile 6 | volumes: 7 | # make sure this directory has the same ownership as the nimbus user from the Dockerfile 8 | # otherwise Nimbus will not be able to store configs for accounts 9 | # e.q. mkdir ./nimbus-storage; sudo chown 911:911 ./nimbus-storage 10 | - './nimbus-storage:/nimbus/data' 11 | ports: 12 | - 5314:5314 13 | -------------------------------------------------------------------------------- /docs/.env.example: -------------------------------------------------------------------------------- 1 | # Create one with no scope selected on https://github.com/settings/tokens/new 2 | # This token is used for fetching the repository releases. 3 | GITHUB_TOKEN= -------------------------------------------------------------------------------- /docs/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | *.iml 3 | .idea 4 | *.log* 5 | .nuxt 6 | .vscode 7 | .DS_Store 8 | coverage 9 | dist 10 | sw.* 11 | .env 12 | .output 13 | translation-status.json 14 | -------------------------------------------------------------------------------- /docs/app.config.ts: -------------------------------------------------------------------------------- 1 | export default defineAppConfig({ 2 | docus: { 3 | title: 'Nimbus', 4 | description: 'A nimble Bluesky web client.', 5 | image: 'https://docs.nimbus.town/nimbus-screenshot.png', 6 | socials: { 7 | // twitter: 'elk_zone', 8 | github: 'nimbus-town/nimbus', 9 | bluesky: { 10 | label: 'Bluesky', 11 | icon: 'IconBluesky', 12 | href: 'https://bsky.app/profile/nimbus.town', 13 | }, 14 | }, 15 | aside: { 16 | level: 0, 17 | exclude: [], 18 | }, 19 | header: { 20 | logo: true, 21 | showLinkIcon: true, 22 | exclude: [], 23 | }, 24 | footer: { 25 | iconLinks: [ 26 | { 27 | href: 'https://nuxt.com', 28 | icon: 'IconNuxtLabs', 29 | }, 30 | { 31 | href: 'https://bsky.app/profile/nimbus.town', 32 | icon: 'IconBluesky', 33 | }, 34 | ], 35 | }, 36 | }, 37 | }) 38 | -------------------------------------------------------------------------------- /docs/app.vue: -------------------------------------------------------------------------------- 1 | 6 | -------------------------------------------------------------------------------- /docs/components/global/ClipboardIcon.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 15 | -------------------------------------------------------------------------------- /docs/components/global/IconBluesky.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | -------------------------------------------------------------------------------- /docs/components/global/Logo.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 24 | -------------------------------------------------------------------------------- /docs/components/global/ToggleIcon.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 15 | -------------------------------------------------------------------------------- /docs/content/0.index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Nimbus 3 | navigation: false 4 | layout: page 5 | --- 6 | 7 | ::block-hero 8 | --- 9 | cta: 10 | - Read more 11 | - /guide 12 | secondary: 13 | - Try it out → 14 | - https://nimbus.town 15 | --- 16 | 17 | #title 18 | Nimbus 19 | 20 | #description 21 | An in-progress, nimble Bluesky web client 22 | 23 | #support 24 | ![Screenshot of Nimbus](/screenshot.png) 25 | 26 | #extra 27 | ::list 28 | - markdown support 29 | - code blocks 30 | - reordering and connecting posts in timelines 31 | - multi account 32 | - GitHub HTML cards 33 | - and more... 34 | :: 35 | 36 | :: 37 | -------------------------------------------------------------------------------- /docs/content/1.guide/4.sponsoring.md: -------------------------------------------------------------------------------- 1 | # Sponsoring 2 | 3 | If you're enjoying the app, consider sponsoring our team: 4 | 5 | - [Anthony Fu](https://github.com/sponsors/antfu) 6 | - [Daniel Roe](https://github.com/sponsors/danielroe) 7 | - [三咲智子 Kevin Deng](https://github.com/sponsors/sxzz) 8 | - [Patak](https://github.com/sponsors/patak-dev) 9 | 10 | We would also appreciate sponsoring other contributors to the Nimbus project. If someone helps you solve an issue or implement a feature you wanted, supporting them would help make this project and OS more sustainable. 11 | -------------------------------------------------------------------------------- /docs/netlify.toml: -------------------------------------------------------------------------------- 1 | [build] 2 | publish = "dist" 3 | command = "pnpm generate" 4 | base = "/docs/" 5 | 6 | # Allow previewing docs 7 | [[redirects]] 8 | from = "/docs/*" 9 | to = "/:splat" 10 | status = 200 11 | force = true 12 | -------------------------------------------------------------------------------- /docs/nuxt.config.ts: -------------------------------------------------------------------------------- 1 | export default defineNuxtConfig({ 2 | extends: '@nuxt-themes/docus', 3 | 4 | vite: { 5 | optimizeDeps: { 6 | include: ['scule'], 7 | }, 8 | }, 9 | 10 | compatibilityDate: '2024-11-07', 11 | }) 12 | -------------------------------------------------------------------------------- /docs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nimbus-docs", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "nuxi dev", 7 | "build": "nuxi build", 8 | "generate": "nuxi generate", 9 | "preview": "nuxi preview" 10 | }, 11 | "dependencies": { 12 | "theme-colors": "^0.1.0" 13 | }, 14 | "devDependencies": { 15 | "@nuxt-themes/docus": "^1.15.1", 16 | "nuxt": "^3.15.2" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /docs/public/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nimbus-town/nimbus/568f0beccfc4fb208442e9456f720da87d4bb1b9/docs/public/apple-touch-icon.png -------------------------------------------------------------------------------- /docs/public/elk-screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nimbus-town/nimbus/568f0beccfc4fb208442e9456f720da87d4bb1b9/docs/public/elk-screenshot.png -------------------------------------------------------------------------------- /docs/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nimbus-town/nimbus/568f0beccfc4fb208442e9456f720da87d4bb1b9/docs/public/favicon.ico -------------------------------------------------------------------------------- /docs/public/fonts/DM-sans-v11.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nimbus-town/nimbus/568f0beccfc4fb208442e9456f720da87d4bb1b9/docs/public/fonts/DM-sans-v11.ttf -------------------------------------------------------------------------------- /docs/public/images/selfhosting-guide/cf-api-token-settings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nimbus-town/nimbus/568f0beccfc4fb208442e9456f720da87d4bb1b9/docs/public/images/selfhosting-guide/cf-api-token-settings.png -------------------------------------------------------------------------------- /docs/public/images/selfhosting-guide/github-fork.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nimbus-town/nimbus/568f0beccfc4fb208442e9456f720da87d4bb1b9/docs/public/images/selfhosting-guide/github-fork.png -------------------------------------------------------------------------------- /docs/public/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /docs/public/pwa-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nimbus-town/nimbus/568f0beccfc4fb208442e9456f720da87d4bb1b9/docs/public/pwa-192x192.png -------------------------------------------------------------------------------- /docs/public/pwa-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nimbus-town/nimbus/568f0beccfc4fb208442e9456f720da87d4bb1b9/docs/public/pwa-512x512.png -------------------------------------------------------------------------------- /docs/public/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nimbus-town/nimbus/568f0beccfc4fb208442e9456f720da87d4bb1b9/docs/public/screenshot.png -------------------------------------------------------------------------------- /docs/public/site.webmanifest: -------------------------------------------------------------------------------- 1 | { 2 | "name": "", 3 | "short_name": "", 4 | "icons": [ 5 | { 6 | "src": "/pwa-192x192.png", 7 | "sizes": "192x192", 8 | "type": "image/png" 9 | }, 10 | { 11 | "src": "/pwa-512x512.png", 12 | "sizes": "512x512", 13 | "type": "image/png" 14 | } 15 | ], 16 | "theme_color": "#ffffff", 17 | "background_color": "#ffffff", 18 | "display": "standalone" 19 | } 20 | -------------------------------------------------------------------------------- /docs/tokens.config.ts: -------------------------------------------------------------------------------- 1 | import { defineTheme } from 'pinceau' 2 | import { getColors } from 'theme-colors' 3 | 4 | const light = getColors('#007ca7') 5 | const primary = Object 6 | .entries(getColors('#0097fd')) 7 | .reduce((acc, [key, value]) => { 8 | acc[key] = { 9 | initial: light[key]!, 10 | dark: value, 11 | } 12 | return acc 13 | }, {} as Record) 14 | 15 | export default defineTheme({ 16 | color: { primary }, 17 | }) 18 | -------------------------------------------------------------------------------- /docs/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./.nuxt/tsconfig.json" 3 | } 4 | -------------------------------------------------------------------------------- /docs/types.ts: -------------------------------------------------------------------------------- 1 | export interface LocaleEntry { 2 | title: string 3 | file: string 4 | useFile: string 5 | translated: string[] 6 | missing: string[] 7 | outdated: string[] 8 | total: number 9 | isSource?: boolean 10 | } 11 | 12 | export type TranslationStatus = Record 13 | -------------------------------------------------------------------------------- /emoji-mart-traslation.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'virtual:emoji-mart-lang-importer' { 2 | export default function (lang: string): Promise 3 | } 4 | -------------------------------------------------------------------------------- /eslint.config.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | import antfu from '@antfu/eslint-config' 3 | 4 | export default await antfu( 5 | { 6 | unocss: false, 7 | vue: { 8 | overrides: { 9 | 'vue/no-restricted-syntax': ['error', { 10 | selector: 'VElement[name=\'a\']', 11 | message: 'Use NuxtLink instead.', 12 | }], 13 | }, 14 | }, 15 | ignores: [ 16 | 'public/**', 17 | 'public-dev/**', 18 | 'public-staging/**', 19 | 'https-dev-config/**', 20 | 'nimbus-translation-status.json', 21 | 'docs/translation-status.json', 22 | ], 23 | }, 24 | { 25 | rules: { 26 | // TODO: migrate all process reference to `import.meta.env` and remove this rule 27 | 'node/prefer-global/process': 'off', 28 | }, 29 | }, 30 | // Sort local files 31 | { 32 | files: ['locales/**.json'], 33 | rules: { 34 | 'jsonc/sort-keys': 'error', 35 | }, 36 | }, 37 | ) 38 | -------------------------------------------------------------------------------- /https-dev-config/local-https-server.mjs: -------------------------------------------------------------------------------- 1 | import { readFileSync } from 'node:fs' 2 | import { fileURLToPath } from 'node:url' 3 | 4 | process.env.NITRO_SSL_CERT = readFileSync(fileURLToPath(new URL('./localhost.crt', import.meta.url)), 'utf8') 5 | process.env.NITRO_SSL_KEY = readFileSync(fileURLToPath(new URL('./localhost.key', import.meta.url)), 'utf8') 6 | 7 | async function run() { 8 | await import('../.output/server/index.mjs') 9 | } 10 | 11 | run() 12 | -------------------------------------------------------------------------------- /layouts/none.vue: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /locales/ar-EG.json: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /locales/ca-ES.json: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /locales/en-US.json: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /locales/es-ES.json: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /locales/pt-PT.json: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /middleware/2.single-instance.global.ts: -------------------------------------------------------------------------------- 1 | export default defineNuxtRouteMiddleware(async (to) => { 2 | if (import.meta.server || !useRuntimeConfig().public.singleInstance) 3 | return 4 | 5 | if (to.params.server) { 6 | const newTo = { ...to } 7 | delete newTo.params.server 8 | return newTo 9 | } 10 | }) 11 | -------------------------------------------------------------------------------- /middleware/auth.ts: -------------------------------------------------------------------------------- 1 | import type { RouteLocationNormalized } from 'vue-router' 2 | 3 | export default defineNuxtRouteMiddleware((to) => { 4 | if (import.meta.server) 5 | return 6 | 7 | if (to.path === '/signin/callback') 8 | return 9 | 10 | if (isHydrated.value) 11 | return handleAuth(to) 12 | 13 | onHydrated(() => handleAuth(to)) 14 | }) 15 | 16 | function handleAuth(to: RouteLocationNormalized) { 17 | if (to.path === '/') { 18 | // Installed PWA shortcut to notifications 19 | if (to.query['notifications-pwa-shortcut'] !== undefined) { 20 | if (currentUser.value) 21 | return navigateTo('/notifications') 22 | else 23 | return navigateTo(`/${currentServer.value}/public/local`) 24 | } 25 | 26 | // Installed PWA shortcut to local 27 | if (to.query['local-pwa-shortcut'] !== undefined) 28 | return navigateTo(`/${currentServer.value}/public/local`) 29 | } 30 | 31 | if (!currentUser.value) { 32 | if (to.path === '/home' && to.query['share-target'] !== undefined) 33 | return navigateTo('/share-target') 34 | else 35 | return navigateTo(`/${currentServer.value}/public/local`) 36 | } 37 | 38 | if (to.path === '/') 39 | return navigateTo('/home') 40 | } 41 | -------------------------------------------------------------------------------- /mocks/class.ts: -------------------------------------------------------------------------------- 1 | export default class SomeClass { 2 | 3 | } 4 | -------------------------------------------------------------------------------- /mocks/prosemirror.ts: -------------------------------------------------------------------------------- 1 | import proxy from 'unenv/runtime/mock/proxy' 2 | 3 | export const Plugin = proxy 4 | export const PluginKey = proxy 5 | export const createParser = proxy 6 | export const createHighlightPlugin = proxy 7 | 8 | export { proxy as default } 9 | -------------------------------------------------------------------------------- /mocks/semver.ts: -------------------------------------------------------------------------------- 1 | import proxy from 'unenv/runtime/mock/proxy' 2 | 3 | export const lt = proxy 4 | export const gt = proxy 5 | export const gte = proxy 6 | export const satisfies = proxy 7 | export class SemVer {} 8 | -------------------------------------------------------------------------------- /mocks/tiptap.ts: -------------------------------------------------------------------------------- 1 | import proxy from 'unenv/runtime/mock/proxy' 2 | 3 | export const Extension = proxy 4 | export const useEditor = proxy 5 | export const EditorContent = proxy 6 | export const NodeViewContent = proxy 7 | export const NodeViewWrapper = proxy 8 | export const nodeViewProps = proxy 9 | export const Node = proxy 10 | export const mergeAttributes = proxy 11 | export const nodeInputRule = proxy 12 | export const nodePasteRule = proxy 13 | export const VueNodeViewRenderer = proxy 14 | export const findChildren = proxy 15 | export const VueRenderer = proxy 16 | export const callOrReturn = proxy 17 | export const InputRule = proxy 18 | 19 | export { proxy as default } 20 | -------------------------------------------------------------------------------- /modules/build-env.ts: -------------------------------------------------------------------------------- 1 | import type { BuildInfo } from '~/types' 2 | import { createResolver, defineNuxtModule } from '@nuxt/kit' 3 | import { isCI } from 'std-env' 4 | import { getEnv, version } from '../config/env' 5 | 6 | const { resolve } = createResolver(import.meta.url) 7 | 8 | export default defineNuxtModule({ 9 | meta: { 10 | name: 'nimbus:build-env', 11 | }, 12 | async setup(_options, nuxt) { 13 | const { env, commit, shortCommit, branch } = await getEnv() 14 | const buildInfo: BuildInfo = { 15 | version, 16 | time: +Date.now(), 17 | commit, 18 | shortCommit, 19 | branch, 20 | env, 21 | } 22 | 23 | nuxt.options.appConfig = nuxt.options.appConfig || {} 24 | nuxt.options.appConfig.env = env 25 | nuxt.options.appConfig.buildInfo = buildInfo 26 | 27 | nuxt.options.nitro.virtual = nuxt.options.nitro.virtual || {} 28 | nuxt.options.nitro.virtual['#build-info'] = `export const env = ${JSON.stringify(env)}` 29 | 30 | nuxt.options.nitro.publicAssets = nuxt.options.nitro.publicAssets || [] 31 | if (env === 'dev') 32 | nuxt.options.nitro.publicAssets.unshift({ dir: resolve('../public-dev') }) 33 | else if (env === 'canary' || env === 'preview' || !isCI) 34 | nuxt.options.nitro.publicAssets.unshift({ dir: resolve('../public-staging') }) 35 | }, 36 | }) 37 | -------------------------------------------------------------------------------- /modules/purge-comments.ts: -------------------------------------------------------------------------------- 1 | import { addVitePlugin, defineNuxtModule } from '@nuxt/kit' 2 | import MagicString from 'magic-string' 3 | 4 | export default defineNuxtModule({ 5 | meta: { 6 | name: 'purge-comments', 7 | }, 8 | setup() { 9 | addVitePlugin({ 10 | name: 'purge-comments', 11 | enforce: 'pre', 12 | transform: (code, id) => { 13 | if (!id.endsWith('.vue') || !code.includes('/gs, '') 18 | 19 | if (s.hasChanged()) { 20 | return { 21 | code: s.toString(), 22 | map: s.generateMap({ source: id, includeContent: true }), 23 | } 24 | } 25 | }, 26 | }) 27 | }, 28 | }) 29 | -------------------------------------------------------------------------------- /modules/pwa/runtime/types.d.ts: -------------------------------------------------------------------------------- 1 | import type { Ref } from 'vue' 2 | import type { UnwrapNestedRefs } from 'vue' 3 | 4 | export interface PwaInjection { 5 | isInstalled: boolean 6 | showInstallPrompt: Ref 7 | cancelInstall: () => void 8 | install: () => Promise 9 | swActivated: Ref 10 | registrationError: Ref 11 | needRefresh: Ref 12 | updateServiceWorker: (reloadPage?: boolean | undefined) => Promise 13 | close: () => Promise 14 | } 15 | 16 | declare module '#app' { 17 | interface NuxtApp { 18 | $pwa?: UnwrapNestedRefs 19 | } 20 | } 21 | 22 | declare module 'vue' { 23 | interface ComponentCustomProperties { 24 | $pwa?: UnwrapNestedRefs 25 | } 26 | } 27 | 28 | export {} 29 | -------------------------------------------------------------------------------- /modules/pwa/types.ts: -------------------------------------------------------------------------------- 1 | import type { VitePWAOptions } from 'vite-plugin-pwa' 2 | 3 | export interface VitePWANuxtOptions extends Partial {} 4 | 5 | declare module '@nuxt/schema' { 6 | interface NuxtConfig { 7 | pwa?: { [K in keyof VitePWANuxtOptions]?: Partial } 8 | } 9 | interface NuxtOptions { 10 | pwa: VitePWANuxtOptions 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /modules/tauri/runtime/build-info.ts: -------------------------------------------------------------------------------- 1 | export const env = useAppConfig().env 2 | -------------------------------------------------------------------------------- /modules/tauri/runtime/logging.client.ts: -------------------------------------------------------------------------------- 1 | import * as log from 'tauri-plugin-log-api' 2 | 3 | // When running inside Tauri, catch all logs from 3rd party packages and direct them to the unified logging stream 4 | export default defineNuxtPlugin(() => { 5 | // eslint-disable-next-line no-global-assign 6 | console = { 7 | ...console, 8 | trace: log.trace, 9 | debug: log.debug, 10 | log: log.info, 11 | warn: log.warn, 12 | error: log.error, 13 | } 14 | 15 | window.addEventListener('unhandledrejection', err => 16 | log.error(err.reason)) 17 | window.addEventListener('error', err => log.error(err.error), true) 18 | }) 19 | -------------------------------------------------------------------------------- /modules/tauri/runtime/storage-config.ts: -------------------------------------------------------------------------------- 1 | export const driver = undefined 2 | export const fsBase = '' 3 | -------------------------------------------------------------------------------- /modules/tauri/runtime/storage.ts: -------------------------------------------------------------------------------- 1 | import { Store } from 'tauri-plugin-store-api' 2 | import { createStorage } from 'unstorage' 3 | 4 | const store = new Store('.servers.dat') 5 | const storage = createStorage() 6 | storage.mount('servers', { 7 | getKeys() { 8 | return store.keys() 9 | }, 10 | async removeItem(key: string) { 11 | await store.delete(key) 12 | }, 13 | clear() { 14 | return store.clear() 15 | }, 16 | hasItem(key: string) { 17 | return store.has(key) 18 | }, 19 | setItem(key: string, value: any) { 20 | return store.set(key, value) 21 | }, 22 | getItem(key: string) { 23 | return store.get(key) 24 | }, 25 | }) 26 | 27 | export function useStorage() { 28 | return storage 29 | } 30 | -------------------------------------------------------------------------------- /netlify.toml: -------------------------------------------------------------------------------- 1 | [build] 2 | publish = "dist" 3 | command = "pnpm run build" 4 | 5 | [build.environment] 6 | NODE_OPTIONS = '--max-old-space-size=4096' 7 | 8 | # Redirect to Discord server 9 | [[redirects]] 10 | from = "https://chat.nimbus.town" 11 | to = "https://discord.gg/JuFDMrTRSD" 12 | status = 301 13 | force = true 14 | 15 | # Redirect to Discord server 16 | [[redirects]] 17 | from = "https://code.elk.zone" 18 | to = "https://github.com/elk-zone/elk" 19 | status = 301 20 | force = true 21 | -------------------------------------------------------------------------------- /page-lifecycle.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'page-lifecycle/dist/lifecycle.mjs' { 2 | type PageLifecycleState = 'active' | 'passive' | 'hidden' | 'frozen' | 'terminated' 3 | 4 | interface PageLifecycleEvent extends Event { 5 | newState: PageLifecycleState 6 | oldState: PageLifecycleState 7 | } 8 | interface PageLifecycle extends EventTarget { 9 | get state(): PageLifecycleState 10 | get pageWasDiscarded(): boolean 11 | addUnsavedChanges: (id: symbol | any) => void 12 | removeUnsavedChanges: (id: symbol | any) => void 13 | addEventListener: (type: string, listener: (evt: PageLifecycleEvent) => void) => void 14 | } 15 | const lifecycle: PageLifecycle 16 | export default lifecycle 17 | } 18 | -------------------------------------------------------------------------------- /pages/[...permalink].vue: -------------------------------------------------------------------------------- 1 | 22 | 23 | 26 | -------------------------------------------------------------------------------- /pages/[[server]]/@[account]/index/followers.vue: -------------------------------------------------------------------------------- 1 | 19 | 20 | 25 | -------------------------------------------------------------------------------- /pages/[[server]]/@[account]/index/following.vue: -------------------------------------------------------------------------------- 1 | 19 | 20 | 25 | -------------------------------------------------------------------------------- /pages/[[server]]/@[account]/index/index.vue: -------------------------------------------------------------------------------- 1 | 35 | 36 | 45 | -------------------------------------------------------------------------------- /pages/[[server]]/@[account]/index/media.vue: -------------------------------------------------------------------------------- 1 | 18 | 19 | 25 | -------------------------------------------------------------------------------- /pages/[[server]]/@[account]/index/with_replies.vue: -------------------------------------------------------------------------------- 1 | 18 | 19 | 25 | -------------------------------------------------------------------------------- /pages/[[server]]/explore/index.vue: -------------------------------------------------------------------------------- 1 | 22 | 23 | 30 | -------------------------------------------------------------------------------- /pages/[[server]]/explore/links.vue: -------------------------------------------------------------------------------- 1 | 22 | 23 | 39 | -------------------------------------------------------------------------------- /pages/[[server]]/explore/tags.vue: -------------------------------------------------------------------------------- 1 | 25 | 26 | 33 | -------------------------------------------------------------------------------- /pages/[[server]]/explore/users.vue: -------------------------------------------------------------------------------- 1 | 21 | 22 | 39 | -------------------------------------------------------------------------------- /pages/[[server]]/list/[list]/index/accounts.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 24 | -------------------------------------------------------------------------------- /pages/[[server]]/list/[list]/index/index.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 18 | -------------------------------------------------------------------------------- /pages/[[server]]/lists.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 20 | -------------------------------------------------------------------------------- /pages/[[server]]/public/index.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 21 | -------------------------------------------------------------------------------- /pages/[[server]]/public/local.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 21 | -------------------------------------------------------------------------------- /pages/[[server]]/search.vue: -------------------------------------------------------------------------------- 1 | 24 | 25 | 39 | -------------------------------------------------------------------------------- /pages/[[server]]/status/[status].vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 16 | -------------------------------------------------------------------------------- /pages/[[server]]/tags/[tag].vue: -------------------------------------------------------------------------------- 1 | 27 | 28 | 45 | -------------------------------------------------------------------------------- /pages/blocks.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 22 | -------------------------------------------------------------------------------- /pages/compose.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 24 | -------------------------------------------------------------------------------- /pages/conversations.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 25 | -------------------------------------------------------------------------------- /pages/domain_blocks.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 22 | -------------------------------------------------------------------------------- /pages/favourites.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 26 | -------------------------------------------------------------------------------- /pages/hashtags.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 21 | -------------------------------------------------------------------------------- /pages/hashtags/index.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 21 | -------------------------------------------------------------------------------- /pages/home.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 30 | -------------------------------------------------------------------------------- /pages/index.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 10 | -------------------------------------------------------------------------------- /pages/intent/post.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 23 | -------------------------------------------------------------------------------- /pages/mutes.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 22 | -------------------------------------------------------------------------------- /pages/notifications/[filter].vue: -------------------------------------------------------------------------------- 1 | 23 | 24 | 27 | -------------------------------------------------------------------------------- /pages/notifications/index.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 12 | -------------------------------------------------------------------------------- /pages/pinned.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 25 | -------------------------------------------------------------------------------- /pages/settings/index.vue: -------------------------------------------------------------------------------- 1 | 9 | -------------------------------------------------------------------------------- /pages/settings/interface/index.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 24 | -------------------------------------------------------------------------------- /pages/settings/notifications/index.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 36 | -------------------------------------------------------------------------------- /pages/settings/notifications/notifications.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 32 | -------------------------------------------------------------------------------- /pages/settings/notifications/push-notifications.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 26 | -------------------------------------------------------------------------------- /pages/settings/profile/featured-tags.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 32 | -------------------------------------------------------------------------------- /pages/settings/profile/index.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 46 | -------------------------------------------------------------------------------- /pages/share-target.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 39 | -------------------------------------------------------------------------------- /patches/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nimbus-town/nimbus/568f0beccfc4fb208442e9456f720da87d4bb1b9/patches/.gitkeep -------------------------------------------------------------------------------- /patches/pinceau.patch: -------------------------------------------------------------------------------- 1 | diff --git a/dist/index.d.ts b/dist/index.d.ts 2 | index 612f1c7908c2e973870be08c6fba1515e6e2b9ca..445a3b0574c5388b624d537fc17cbf4a08973ded 100644 3 | --- a/dist/index.d.ts 4 | +++ b/dist/index.d.ts 5 | @@ -115,7 +115,7 @@ interface ModuleHooks { 6 | interface ModuleOptions extends PinceauOptions { 7 | } 8 | 9 | -declare module '@vue/runtime-core' { 10 | +declare module 'vue' { 11 | interface ComponentCustomProperties { 12 | $dt: DtFunction; 13 | $pinceau: ComputedRef; 14 | -------------------------------------------------------------------------------- /plugins/1.scroll-to-top.ts: -------------------------------------------------------------------------------- 1 | export default defineNuxtPlugin(() => { 2 | return { 3 | provide: { 4 | scrollToTop: () => { 5 | window.scrollTo({ top: 0, left: 0, behavior: 'smooth' }) 6 | }, 7 | }, 8 | } 9 | }) 10 | -------------------------------------------------------------------------------- /plugins/color-mode.ts: -------------------------------------------------------------------------------- 1 | import { THEME_COLORS } from '~/constants' 2 | 3 | export default defineNuxtPlugin(() => { 4 | const colorMode = useColorMode() 5 | useHead({ 6 | meta: [{ 7 | id: 'theme-color', 8 | name: 'theme-color', 9 | content: () => colorMode.value === 'dark' ? THEME_COLORS.themeDark : THEME_COLORS.themeLight, 10 | }], 11 | }) 12 | }) 13 | -------------------------------------------------------------------------------- /plugins/floating-vue.ts: -------------------------------------------------------------------------------- 1 | import { defineNuxtPlugin } from '#imports' 2 | import FloatingVue from 'floating-vue' 3 | 4 | export default defineNuxtPlugin((nuxtApp) => { 5 | nuxtApp.vueApp.use(FloatingVue) 6 | }) 7 | -------------------------------------------------------------------------------- /plugins/hydration.client.ts: -------------------------------------------------------------------------------- 1 | export default defineNuxtPlugin((nuxtApp) => { 2 | nuxtApp.hooks.hookOnce('app:suspense:resolve', () => { 3 | isHydrated.value = true 4 | }) 5 | }) 6 | -------------------------------------------------------------------------------- /plugins/page-lifecycle.client.ts: -------------------------------------------------------------------------------- 1 | import lifecycle from 'page-lifecycle/dist/lifecycle.mjs' 2 | import { NIMBUS_PAGE_LIFECYCLE_FROZEN } from '~/constants' 3 | import { closeDatabases } from '~/utils/elk-idb' 4 | 5 | export default defineNuxtPlugin(() => { 6 | const state = ref(lifecycle.state) 7 | const frozenListeners: (() => void)[] = [] 8 | const frozenState = useLocalStorage(NIMBUS_PAGE_LIFECYCLE_FROZEN, false) 9 | 10 | lifecycle.addEventListener('statechange', (evt) => { 11 | if (evt.newState === 'hidden' && evt.oldState === 'frozen') { 12 | frozenState.value = false 13 | nextTick().then(() => window.location.reload()) 14 | return 15 | } 16 | 17 | if (evt.newState === 'frozen') { 18 | frozenState.value = true 19 | frozenListeners.forEach(listener => listener()) 20 | } 21 | else { 22 | state.value = evt.newState 23 | } 24 | }) 25 | 26 | const addFrozenListener = (listener: () => void) => { 27 | frozenListeners.push(listener) 28 | } 29 | 30 | addFrozenListener(() => { 31 | if (useAppConfig().pwaEnabled && navigator.serviceWorker.controller) 32 | navigator.serviceWorker.controller.postMessage(NIMBUS_PAGE_LIFECYCLE_FROZEN) 33 | 34 | closeDatabases() 35 | }) 36 | 37 | return { 38 | provide: { 39 | pageLifecycle: reactive({ 40 | state, 41 | addFrozenListener, 42 | }), 43 | }, 44 | } 45 | }) 46 | -------------------------------------------------------------------------------- /plugins/path.ts: -------------------------------------------------------------------------------- 1 | export default defineNuxtPlugin({ 2 | order: -40, 3 | setup: (nuxtApp) => { 4 | delete nuxtApp.payload.path 5 | }, 6 | }) 7 | -------------------------------------------------------------------------------- /plugins/setup-global-effects.client.ts: -------------------------------------------------------------------------------- 1 | import type { OldFontSize } from '~/composables/settings' 2 | import { DEFAULT_FONT_SIZE } from '~/constants' 3 | import { oldFontSizeMap } from '~/constants/options' 4 | 5 | export default defineNuxtPlugin(() => { 6 | const userSettings = useUserSettings() 7 | const html = document.documentElement 8 | watchEffect(() => { 9 | const { fontSize } = userSettings.value 10 | html.style.setProperty('--font-size', fontSize ? (oldFontSizeMap[fontSize as OldFontSize] ?? fontSize) : DEFAULT_FONT_SIZE) 11 | }) 12 | watchEffect(() => { 13 | html.classList.toggle('zen', getPreferences(userSettings.value, 'zenMode')) 14 | }) 15 | watchEffect(() => { 16 | Object.entries(userSettings.value.themeColors || {}).forEach(([k, v]) => html.style.setProperty(k, v)) 17 | }) 18 | }) 19 | -------------------------------------------------------------------------------- /plugins/setup-head-script.server.ts: -------------------------------------------------------------------------------- 1 | import { STORAGE_KEY_CURRENT_USER_HANDLE, STORAGE_KEY_SETTINGS } from '~/constants' 2 | import { oldFontSizeMap } from '~/constants/options' 3 | 4 | /** 5 | * Injecting scripts before renders 6 | */ 7 | export default defineNuxtPlugin(() => { 8 | useHead({ 9 | script: [ 10 | { 11 | innerHTML: ` 12 | ;(function() { 13 | const handle = localStorage.getItem('${STORAGE_KEY_CURRENT_USER_HANDLE}') || '[anonymous]' 14 | const allSettings = JSON.parse(localStorage.getItem('${STORAGE_KEY_SETTINGS}') || '{}') 15 | const settings = allSettings[handle] 16 | if (!settings) { return } 17 | 18 | const html = document.documentElement 19 | ${import.meta.dev ? 'console.log({ settings })' : ''} 20 | 21 | if (settings.fontSize) { 22 | const oldFontSizeMap = ${JSON.stringify(oldFontSizeMap)} 23 | html.style.setProperty('--font-size', oldFontSizeMap[settings.fontSize] || settings.fontSize) 24 | } 25 | if (settings.language) { 26 | html.setAttribute('lang', settings.language) 27 | } 28 | if (settings.preferences.zenMode) { 29 | html.classList.add('zen') 30 | } 31 | if (settings.themeColors) { 32 | Object.entries(settings.themeColors).map(i => html.style.setProperty(i[0], i[1])) 33 | } 34 | })()`.trim().replace(/\s*\n\s*/g, ';'), 35 | }, 36 | ], 37 | }) 38 | }) 39 | -------------------------------------------------------------------------------- /plugins/setup-i18n.ts: -------------------------------------------------------------------------------- 1 | import type { Locale } from '#i18n' 2 | 3 | export default defineNuxtPlugin(async (nuxt) => { 4 | const t = nuxt.vueApp.config.globalProperties.$t 5 | const d = nuxt.vueApp.config.globalProperties.$d 6 | const n = nuxt.vueApp.config.globalProperties.$n 7 | 8 | nuxt.vueApp.config.globalProperties.$t = wrapI18n(t) 9 | nuxt.vueApp.config.globalProperties.$d = wrapI18n(d) 10 | nuxt.vueApp.config.globalProperties.$n = wrapI18n(n) 11 | 12 | if (import.meta.client) { 13 | const i18n = useNuxtApp().$i18n 14 | const { setLocale, locales } = i18n 15 | const userSettings = useUserSettings() 16 | const lang = computed(() => userSettings.value.language as Locale) 17 | 18 | const supportLanguages = unref(locales).map(locale => locale.code) 19 | if (!supportLanguages.includes(lang.value)) 20 | userSettings.value.language = getDefaultLanguage(supportLanguages) 21 | 22 | if (lang.value !== i18n.locale) 23 | await setLocale(userSettings.value.language as Locale) 24 | 25 | watch([lang, isHydrated], () => { 26 | if (isHydrated.value && lang.value !== i18n.locale) 27 | setLocale(lang.value) 28 | }, { immediate: true }) 29 | } 30 | }) 31 | -------------------------------------------------------------------------------- /plugins/social.server.ts: -------------------------------------------------------------------------------- 1 | import { sendRedirect } from 'h3' 2 | 3 | const BOT_RE = /bot\b|index|spider|facebookexternalhit|crawl|wget|slurp|mediapartners-google|whatsapp/i 4 | 5 | export default defineNuxtPlugin(async (nuxtApp) => { 6 | const route = useRoute() 7 | if (!('server' in route.params)) 8 | return 9 | 10 | const userAgent = useRequestHeaders()['user-agent'] 11 | if (!userAgent) 12 | return 13 | 14 | const isOpenGraphCrawler = BOT_RE.test(userAgent) 15 | if (isOpenGraphCrawler) { 16 | // Redirect bots to the original instance to respect their social sharing settings 17 | await sendRedirect(nuxtApp.ssrContext!.event, `https:/${route.path}`, 301) 18 | } 19 | }) 20 | -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - docs 3 | -------------------------------------------------------------------------------- /public-dev/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nimbus-town/nimbus/568f0beccfc4fb208442e9456f720da87d4bb1b9/public-dev/apple-touch-icon.png -------------------------------------------------------------------------------- /public-dev/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nimbus-town/nimbus/568f0beccfc4fb208442e9456f720da87d4bb1b9/public-dev/favicon.ico -------------------------------------------------------------------------------- /public-dev/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | -------------------------------------------------------------------------------- /public-dev/maskable-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nimbus-town/nimbus/568f0beccfc4fb208442e9456f720da87d4bb1b9/public-dev/maskable-icon.png -------------------------------------------------------------------------------- /public-dev/pwa-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nimbus-town/nimbus/568f0beccfc4fb208442e9456f720da87d4bb1b9/public-dev/pwa-192x192.png -------------------------------------------------------------------------------- /public-dev/pwa-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nimbus-town/nimbus/568f0beccfc4fb208442e9456f720da87d4bb1b9/public-dev/pwa-512x512.png -------------------------------------------------------------------------------- /public-dev/pwa-64x64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nimbus-town/nimbus/568f0beccfc4fb208442e9456f720da87d4bb1b9/public-dev/pwa-64x64.png -------------------------------------------------------------------------------- /public-staging/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nimbus-town/nimbus/568f0beccfc4fb208442e9456f720da87d4bb1b9/public-staging/apple-touch-icon.png -------------------------------------------------------------------------------- /public-staging/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nimbus-town/nimbus/568f0beccfc4fb208442e9456f720da87d4bb1b9/public-staging/favicon.ico -------------------------------------------------------------------------------- /public-staging/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | -------------------------------------------------------------------------------- /public-staging/maskable-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nimbus-town/nimbus/568f0beccfc4fb208442e9456f720da87d4bb1b9/public-staging/maskable-icon.png -------------------------------------------------------------------------------- /public-staging/pwa-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nimbus-town/nimbus/568f0beccfc4fb208442e9456f720da87d4bb1b9/public-staging/pwa-192x192.png -------------------------------------------------------------------------------- /public-staging/pwa-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nimbus-town/nimbus/568f0beccfc4fb208442e9456f720da87d4bb1b9/public-staging/pwa-512x512.png -------------------------------------------------------------------------------- /public-staging/pwa-64x64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nimbus-town/nimbus/568f0beccfc4fb208442e9456f720da87d4bb1b9/public-staging/pwa-64x64.png -------------------------------------------------------------------------------- /public-staging/robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: * 2 | Disallow: / 3 | -------------------------------------------------------------------------------- /public/_redirects: -------------------------------------------------------------------------------- 1 | /docs/* https://docs.nimbus.town/:splat 200 2 | /settings/* /index.html 200 3 | -------------------------------------------------------------------------------- /public/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nimbus-town/nimbus/568f0beccfc4fb208442e9456f720da87d4bb1b9/public/apple-touch-icon.png -------------------------------------------------------------------------------- /public/avatars/antfu-100x100.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nimbus-town/nimbus/568f0beccfc4fb208442e9456f720da87d4bb1b9/public/avatars/antfu-100x100.png -------------------------------------------------------------------------------- /public/avatars/antfu-60x60.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nimbus-town/nimbus/568f0beccfc4fb208442e9456f720da87d4bb1b9/public/avatars/antfu-60x60.png -------------------------------------------------------------------------------- /public/avatars/danielroe-100x100.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nimbus-town/nimbus/568f0beccfc4fb208442e9456f720da87d4bb1b9/public/avatars/danielroe-100x100.png -------------------------------------------------------------------------------- /public/avatars/danielroe-60x60.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nimbus-town/nimbus/568f0beccfc4fb208442e9456f720da87d4bb1b9/public/avatars/danielroe-60x60.png -------------------------------------------------------------------------------- /public/avatars/patak-dev-100x100.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nimbus-town/nimbus/568f0beccfc4fb208442e9456f720da87d4bb1b9/public/avatars/patak-dev-100x100.png -------------------------------------------------------------------------------- /public/avatars/patak-dev-60x60.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nimbus-town/nimbus/568f0beccfc4fb208442e9456f720da87d4bb1b9/public/avatars/patak-dev-60x60.png -------------------------------------------------------------------------------- /public/avatars/shuuji3-100x100.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nimbus-town/nimbus/568f0beccfc4fb208442e9456f720da87d4bb1b9/public/avatars/shuuji3-100x100.png -------------------------------------------------------------------------------- /public/avatars/shuuji3-60x60.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nimbus-town/nimbus/568f0beccfc4fb208442e9456f720da87d4bb1b9/public/avatars/shuuji3-60x60.png -------------------------------------------------------------------------------- /public/avatars/sxzz-100x100.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nimbus-town/nimbus/568f0beccfc4fb208442e9456f720da87d4bb1b9/public/avatars/sxzz-100x100.png -------------------------------------------------------------------------------- /public/avatars/sxzz-60x60.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nimbus-town/nimbus/568f0beccfc4fb208442e9456f720da87d4bb1b9/public/avatars/sxzz-60x60.png -------------------------------------------------------------------------------- /public/avatars/userquin-100x100.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nimbus-town/nimbus/568f0beccfc4fb208442e9456f720da87d4bb1b9/public/avatars/userquin-100x100.png -------------------------------------------------------------------------------- /public/avatars/userquin-60x60.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nimbus-town/nimbus/568f0beccfc4fb208442e9456f720da87d4bb1b9/public/avatars/userquin-60x60.png -------------------------------------------------------------------------------- /public/elk-og.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nimbus-town/nimbus/568f0beccfc4fb208442e9456f720da87d4bb1b9/public/elk-og.png -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nimbus-town/nimbus/568f0beccfc4fb208442e9456f720da87d4bb1b9/public/favicon.ico -------------------------------------------------------------------------------- /public/fonts/DM-mono-v10.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nimbus-town/nimbus/568f0beccfc4fb208442e9456f720da87d4bb1b9/public/fonts/DM-mono-v10.ttf -------------------------------------------------------------------------------- /public/fonts/DM-sans-v11.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nimbus-town/nimbus/568f0beccfc4fb208442e9456f720da87d4bb1b9/public/fonts/DM-sans-v11.ttf -------------------------------------------------------------------------------- /public/fonts/DM-serif-display-v10.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nimbus-town/nimbus/568f0beccfc4fb208442e9456f720da87d4bb1b9/public/fonts/DM-serif-display-v10.ttf -------------------------------------------------------------------------------- /public/fonts/homemade-apple-v18.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nimbus-town/nimbus/568f0beccfc4fb208442e9456f720da87d4bb1b9/public/fonts/homemade-apple-v18.ttf -------------------------------------------------------------------------------- /public/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | -------------------------------------------------------------------------------- /public/maskable-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nimbus-town/nimbus/568f0beccfc4fb208442e9456f720da87d4bb1b9/public/maskable-icon.png -------------------------------------------------------------------------------- /public/nimbus.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /public/pwa-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nimbus-town/nimbus/568f0beccfc4fb208442e9456f720da87d4bb1b9/public/pwa-192x192.png -------------------------------------------------------------------------------- /public/pwa-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nimbus-town/nimbus/568f0beccfc4fb208442e9456f720da87d4bb1b9/public/pwa-512x512.png -------------------------------------------------------------------------------- /public/pwa-64x64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nimbus-town/nimbus/568f0beccfc4fb208442e9456f720da87d4bb1b9/public/pwa-64x64.png -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: * 2 | Allow: / 3 | 4 | # Disallow authenticated pages 5 | 6 | Disallow: /intent 7 | Disallow: /settings 8 | Disallow: /blocks 9 | Disallow: /compose 10 | Disallow: /conversations 11 | Disallow: /domain_blocks 12 | Disallow: /favourites 13 | Disallow: /home 14 | Disallow: /mutes 15 | Disallow: /notifications 16 | Disallow: /pinned 17 | Disallow: /search 18 | Disallow: /settings 19 | Disallow: /share-target 20 | 21 | # Wait 1 second between successive requests. 22 | Crawl-delay: 1 23 | -------------------------------------------------------------------------------- /public/screenshots/dark-1.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nimbus-town/nimbus/568f0beccfc4fb208442e9456f720da87d4bb1b9/public/screenshots/dark-1.webp -------------------------------------------------------------------------------- /public/screenshots/light-1.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nimbus-town/nimbus/568f0beccfc4fb208442e9456f720da87d4bb1b9/public/screenshots/light-1.webp -------------------------------------------------------------------------------- /public/shortcuts/compose-96x96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nimbus-town/nimbus/568f0beccfc4fb208442e9456f720da87d4bb1b9/public/shortcuts/compose-96x96.png -------------------------------------------------------------------------------- /public/shortcuts/compose.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nimbus-town/nimbus/568f0beccfc4fb208442e9456f720da87d4bb1b9/public/shortcuts/compose.png -------------------------------------------------------------------------------- /public/shortcuts/home-96x96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nimbus-town/nimbus/568f0beccfc4fb208442e9456f720da87d4bb1b9/public/shortcuts/home-96x96.png -------------------------------------------------------------------------------- /public/shortcuts/home.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nimbus-town/nimbus/568f0beccfc4fb208442e9456f720da87d4bb1b9/public/shortcuts/home.png -------------------------------------------------------------------------------- /public/shortcuts/local-96x96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nimbus-town/nimbus/568f0beccfc4fb208442e9456f720da87d4bb1b9/public/shortcuts/local-96x96.png -------------------------------------------------------------------------------- /public/shortcuts/local.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nimbus-town/nimbus/568f0beccfc4fb208442e9456f720da87d4bb1b9/public/shortcuts/local.png -------------------------------------------------------------------------------- /public/shortcuts/notifications-96x96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nimbus-town/nimbus/568f0beccfc4fb208442e9456f720da87d4bb1b9/public/shortcuts/notifications-96x96.png -------------------------------------------------------------------------------- /public/shortcuts/notifications.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nimbus-town/nimbus/568f0beccfc4fb208442e9456f720da87d4bb1b9/public/shortcuts/notifications.png -------------------------------------------------------------------------------- /public/shortcuts/settings-96x96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nimbus-town/nimbus/568f0beccfc4fb208442e9456f720da87d4bb1b9/public/shortcuts/settings-96x96.png -------------------------------------------------------------------------------- /public/shortcuts/settings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nimbus-town/nimbus/568f0beccfc4fb208442e9456f720da87d4bb1b9/public/shortcuts/settings.png -------------------------------------------------------------------------------- /scripts/avatars.ts: -------------------------------------------------------------------------------- 1 | import { writeFile } from 'node:fs/promises' 2 | import fs from 'fs-extra' 3 | import { ofetch } from 'ofetch' 4 | import { join, resolve } from 'pathe' 5 | import { nimbusTeamMembers } from '../composables/about' 6 | 7 | const avatarsDir = resolve('./public/avatars/') 8 | 9 | const sizes = [60, 100] 10 | 11 | async function download(url: string, fileName: string) { 12 | console.log('downloading', fileName) 13 | try { 14 | const image = await ofetch(url, { responseType: 'arrayBuffer' }) 15 | await writeFile(fileName, new Uint8Array(image)) 16 | } 17 | catch (err) { 18 | console.error(err) 19 | } 20 | } 21 | 22 | async function fetchAvatars() { 23 | await fs.ensureDir(avatarsDir) 24 | 25 | await Promise.all(nimbusTeamMembers.reduce((acc, { github }) => { 26 | acc.push(...sizes.map(s => download(`https://github.com/${github}.png?size=${s}`, join(avatarsDir, `${github}-${s}x${s}.png`)))) 27 | return acc 28 | }, [] as Promise[])) 29 | } 30 | 31 | fetchAvatars() 32 | -------------------------------------------------------------------------------- /scripts/generate-themes.ts: -------------------------------------------------------------------------------- 1 | import type { ThemeColors } from '~/composables/settings' 2 | import chroma from 'chroma-js' 3 | 4 | // #cc7d24 -> hcl(67.14,62.19,59.56) 5 | export const themesColor = Array.from( 6 | { length: 9 }, 7 | (_, i) => chroma.hcl((67.14 + i * 40) % 360, 62.19, 59.56).hex(), 8 | ) 9 | 10 | export function getThemeColors(primary: string): ThemeColors { 11 | const c = chroma(primary) 12 | const dc = c.brighten(0.1) 13 | 14 | return { 15 | '--theme-color-name': primary, 16 | 17 | '--c-primary': 'rgb(var(--rgb-primary))', 18 | '--c-primary-active': c.darken(0.5).hex(), 19 | '--c-primary-light': c.alpha(0.5).hex(), 20 | '--c-primary-fade': c.darken(0.1).alpha(0.1).hex(), 21 | '--rgb-primary': c.rgb().join(', '), 22 | 23 | '--c-dark-primary': 'rgb(var(--rgb-dark-primary))', 24 | '--c-dark-primary-active': dc.darken(0.5).hex(), 25 | '--c-dark-primary-light': dc.alpha(0.5).hex(), 26 | '--c-dark-primary-fade': dc.darken(0.1).alpha(0.1).hex(), 27 | '--rgb-dark-primary': c.rgb().join(', '), 28 | } 29 | } 30 | 31 | export const colorsMap = themesColor.map(color => [color, getThemeColors(color)]) 32 | -------------------------------------------------------------------------------- /scripts/prepare.ts: -------------------------------------------------------------------------------- 1 | import process from 'node:process' 2 | import fs from 'fs-extra' 3 | import { emojiPrefix, iconifyEmojiPackage } from '../config/emojis' 4 | import { colorsMap } from './generate-themes' 5 | 6 | const dereference = process.platform === 'win32' ? true : undefined 7 | 8 | await fs.copy(`node_modules/${iconifyEmojiPackage}/icons`, `public/emojis/${emojiPrefix}`, { overwrite: true, dereference }) 9 | 10 | await fs.writeJSON('constants/themes.json', colorsMap, { spaces: 2, EOL: '\n' }) 11 | await fs.writeFile('styles/default-theme.css', `:root {\n${Object.entries(colorsMap[0][1]).map(([k, v]) => ` ${k}: ${v};`).join('\n')}\n}\n`, { encoding: 'utf-8' }) 12 | -------------------------------------------------------------------------------- /scripts/release.ts: -------------------------------------------------------------------------------- 1 | import Git from 'simple-git' 2 | 3 | const git = Git() 4 | 5 | const hash = await git.revparse(['main']) 6 | 7 | console.log('Checkout release branch') 8 | await git.checkout('release') 9 | 10 | console.log(`Reset to main branch (${hash})`) 11 | await git.reset(['--hard', hash]) 12 | 13 | console.log('Push to release branch') 14 | await git.push(['--force']) 15 | 16 | console.log('Checkout main branch') 17 | await git.checkout('main') 18 | -------------------------------------------------------------------------------- /server/api/[server]/clear.ts: -------------------------------------------------------------------------------- 1 | export default defineEventHandler(async (event) => { 2 | const { server } = getRouterParams(event) 3 | const { key } = getQuery(event) 4 | 5 | if (key !== String(useRuntimeConfig().adminKey)) 6 | return { status: false, error: 'incorrect key' } 7 | 8 | await deleteApp(server) 9 | 10 | return { status: true } 11 | }) 12 | -------------------------------------------------------------------------------- /server/api/[server]/login.ts: -------------------------------------------------------------------------------- 1 | import { stringifyQuery } from 'ufo' 2 | 3 | export default defineEventHandler(async (event) => { 4 | let { server } = getRouterParams(event) 5 | const { origin, force_login, lang } = await readBody(event) 6 | server = server.toLocaleLowerCase().trim() 7 | const app = await getApp(origin, server) 8 | 9 | if (!app) { 10 | throw createError({ 11 | statusCode: 400, 12 | statusMessage: `App not registered for server: ${server}`, 13 | }) 14 | } 15 | 16 | const query = stringifyQuery({ 17 | client_id: app.client_id, 18 | force_login: force_login === true ? 'true' : 'false', 19 | scope: 'read write follow push', 20 | response_type: 'code', 21 | lang, 22 | redirect_uri: getRedirectURI(origin, server), 23 | }) 24 | 25 | return `https://${server}/oauth/authorize?${query}` 26 | }) 27 | -------------------------------------------------------------------------------- /server/api/[server]/oauth/[origin].ts: -------------------------------------------------------------------------------- 1 | import { stringifyQuery } from 'ufo' 2 | 3 | import { defaultUserAgent } from '~/server/utils/shared' 4 | 5 | export default defineEventHandler(async (event) => { 6 | let { server, origin } = getRouterParams(event) 7 | server = server.toLocaleLowerCase().trim() 8 | origin = decodeURIComponent(origin) 9 | const app = await getApp(origin, server) 10 | 11 | if (!app) { 12 | throw createError({ 13 | statusCode: 400, 14 | statusMessage: `App not registered for server: ${server}`, 15 | }) 16 | } 17 | 18 | const { code } = getQuery(event) 19 | if (!code) { 20 | throw createError({ 21 | statusCode: 422, 22 | statusMessage: 'Missing authentication code.', 23 | }) 24 | } 25 | 26 | try { 27 | const result: any = await $fetch(`https://${server}/oauth/token`, { 28 | method: 'POST', 29 | headers: { 30 | 'user-agent': defaultUserAgent, 31 | }, 32 | body: { 33 | client_id: app.client_id, 34 | client_secret: app.client_secret, 35 | redirect_uri: getRedirectURI(origin, server), 36 | grant_type: 'authorization_code', 37 | code, 38 | scope: 'read write follow push', 39 | }, 40 | retry: 3, 41 | }) 42 | 43 | const url = `/signin/callback?${stringifyQuery({ server, token: result.access_token, vapid_key: app.vapid_key })}` 44 | await sendRedirect(event, url, 302) 45 | } 46 | catch { 47 | throw createError({ 48 | statusCode: 400, 49 | statusMessage: 'Could not complete log in.', 50 | }) 51 | } 52 | }) 53 | -------------------------------------------------------------------------------- /server/api/list-servers.ts: -------------------------------------------------------------------------------- 1 | let servers: string[] 2 | 3 | export default defineEventHandler(async () => { 4 | if (!servers) 5 | servers = await listServers() 6 | return servers 7 | }) 8 | -------------------------------------------------------------------------------- /server/cache-driver.ts: -------------------------------------------------------------------------------- 1 | import type { Driver } from 'unstorage' 2 | import { defineDriver } from 'unstorage' 3 | import memory from 'unstorage/drivers/memory' 4 | 5 | export interface CacheDriverOptions { 6 | driver: Driver 7 | } 8 | 9 | export default defineDriver((driver: Driver = memory()) => { 10 | const memoryDriver = memory() 11 | return { 12 | ...driver, 13 | async hasItem(key: string) { 14 | if (await memoryDriver.hasItem(key, {})) 15 | return true 16 | 17 | return driver.hasItem(key, {}) 18 | }, 19 | async setItem(key: string, value: any, opts: any = {}) { 20 | await Promise.all([ 21 | memoryDriver.setItem?.(key, value, {}), 22 | driver.setItem?.(key, value, opts), 23 | ]) 24 | }, 25 | async getItem(key: string) { 26 | let value = await memoryDriver.getItem(key) 27 | 28 | if (value !== null) 29 | return value 30 | 31 | value = await driver.getItem(key) 32 | memoryDriver.setItem?.(key, value as string, {}) 33 | 34 | return value 35 | }, 36 | } 37 | }) 38 | -------------------------------------------------------------------------------- /service-worker/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "lib": ["ESNext", "WebWorker", "DOM.Iterable"], 5 | "types": ["vite/client"] 6 | }, 7 | "include": ["./"], 8 | "exclude": [] 9 | } 10 | -------------------------------------------------------------------------------- /shims.d.ts: -------------------------------------------------------------------------------- 1 | declare global { 2 | namespace NodeJS { 3 | interface Process { 4 | test?: boolean 5 | } 6 | } 7 | } 8 | 9 | export {} 10 | -------------------------------------------------------------------------------- /styles/default-theme.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --theme-color-name: #cc7d24; 3 | --c-primary: rgb(var(--rgb-primary)); 4 | --c-primary-active: #b16605; 5 | --c-primary-light: #cc7d2480; 6 | --c-primary-fade: #c7781f1a; 7 | --rgb-primary: 204, 125, 36; 8 | --c-dark-primary: rgb(var(--rgb-dark-primary)); 9 | --c-dark-primary-active: #b66b0d; 10 | --c-dark-primary-light: #d1822980; 11 | --c-dark-primary-fade: #cc7d241a; 12 | --rgb-dark-primary: 204, 125, 36; 13 | } 14 | -------------------------------------------------------------------------------- /styles/dropdown.css: -------------------------------------------------------------------------------- 1 | .v-popper--theme-dropdown .v-popper__inner { 2 | --at-apply: bg-base text-base rounded border border-base shadow; 3 | box-shadow: 0 6px 30px #0000001a; 4 | } 5 | .v-popper--theme-dropdown .v-popper__arrow-inner { 6 | visibility: visible; 7 | --at-apply: border-$c-bg-base; 8 | } 9 | .v-popper--theme-dropdown .v-popper__arrow-outer { 10 | --at-apply: border-base; 11 | } 12 | .v-popper--theme-tooltip .v-popper__inner { 13 | --at-apply: bg-base text-base rounded border border-base shadow-sm; 14 | padding: 7px 12px 6px; 15 | } 16 | .v-popper--theme-tooltip .v-popper__arrow-inner { 17 | visibility: visible; 18 | --at-apply: border-bg-base; 19 | } 20 | .v-popper--theme-tooltip .v-popper__arrow-outer { 21 | --at-apply: border-base; 22 | } 23 | -------------------------------------------------------------------------------- /styles/scrollbars.css: -------------------------------------------------------------------------------- 1 | * { 2 | scrollbar-color: #8885 var(--c-border); 3 | } 4 | 5 | ::-webkit-scrollbar { 6 | width: 10px; 7 | } 8 | 9 | ::-webkit-scrollbar:horizontal { 10 | height: 10px; 11 | } 12 | 13 | ::-webkit-scrollbar-track { 14 | background: var(--c-border); 15 | border-radius: 1px; 16 | } 17 | 18 | ::-webkit-scrollbar-thumb { 19 | background: #8885; 20 | border-radius: 1px; 21 | } 22 | 23 | ::-webkit-scrollbar-thumb:hover { 24 | background: #8886; 25 | } -------------------------------------------------------------------------------- /styles/tiptap.css: -------------------------------------------------------------------------------- 1 | .ProseMirror { 2 | p.is-editor-empty:first-child::before { 3 | content: attr(data-placeholder); 4 | float: left; 5 | pointer-events: none; 6 | height: 0; 7 | opacity: 0.4; 8 | } 9 | span[data-type='mention'], 10 | span[data-type='hashtag'] { 11 | --at-apply: text-primary; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /styles/vars.css: -------------------------------------------------------------------------------- 1 | :root, :root::selection { 2 | --c-border: #eee; 3 | --c-border-dark: #dccfcf; 4 | --c-border-code: #ddd; 5 | --c-danger: #FF3C1B; 6 | --c-danger-active: #B50900; 7 | 8 | --rgb-bg-base: 250, 250, 250; 9 | 10 | --c-bg-base: rgb(var(--rgb-bg-base)); 11 | 12 | --c-bg-active: #f2f2f2; 13 | --c-bg-card: #00000006; 14 | --c-bg-code: #00000006; 15 | --c-bg-selection: #8885; 16 | --c-bg-dm: #f1e8e6; 17 | 18 | --c-text-base: #232323; 19 | --c-text-code: #63470c; 20 | --c-text-secondary: #686868; 21 | --c-text-secondary-light: #919191; 22 | 23 | --c-bg-btn-disabled: #a1a1a1; 24 | --c-text-btn-disabled: #fff; 25 | --c-text-btn-disabled-deeper: #a1a1a1; 26 | 27 | --c-success: #67C23A; 28 | --c-warning: #E6A23C; 29 | --c-error: #F56C6C; 30 | } 31 | 32 | .dark { 33 | --c-primary: var(--c-dark-primary); 34 | --c-primary-active: var(--c-dark-primary-active); 35 | --c-primary-light: var(--c-dark-primary-light); 36 | --c-primary-fade: var(--c-dark-primary-fade); 37 | --c-danger: #FF2810; 38 | --c-danger-active: #E02F00; 39 | 40 | --c-border: #222; 41 | --c-border-code: #333; 42 | --c-border-dark: #545251; 43 | 44 | --rgb-bg-base: 17, 17, 17; 45 | 46 | --c-bg-active: #191919; 47 | --c-bg-card: #ffffff06; 48 | --c-bg-code: #ffffff06; 49 | --c-bg-dm: #0a2f35; 50 | 51 | --c-text-base: #f3f3f3; 52 | --c-text-code: #ecd88e; 53 | --c-text-secondary: #888; 54 | --c-text-secondary-light: #686868; 55 | 56 | --c-bg-btn-disabled: #2a2a2a; 57 | --c-text-btn-disabled: #919191; 58 | } 59 | -------------------------------------------------------------------------------- /tests/nuxt/html-to-text.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from 'vitest' 2 | 3 | describe('html-to-text', () => { 4 | it('inline code', () => { 5 | expect(htmlToText('

text code inline

')) 6 | .toMatchInlineSnapshot('"text `code` inline"') 7 | }) 8 | 9 | it('code block', () => { 10 | expect(htmlToText('

text

code
')) 11 | .toMatchInlineSnapshot(` 12 | "text 13 | \`\`\`js 14 | code 15 | \`\`\`" 16 | `) 17 | }) 18 | 19 | it('bold & italic', () => { 20 | expect(htmlToText('

text bold italic

')) 21 | .toMatchInlineSnapshot('"text **bold** *italic*"') 22 | }) 23 | }) 24 | -------------------------------------------------------------------------------- /tests/setup.ts: -------------------------------------------------------------------------------- 1 | // We have TypeError: AbortSignal.timeout is not a function when running tests against masto.js v6 2 | if (!AbortSignal.timeout) { 3 | AbortSignal.timeout = (ms) => { 4 | const controller = new AbortController() 5 | setTimeout(() => controller.abort(new DOMException('TimeoutError')), ms) 6 | return controller.signal 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /tests/unit/language.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from 'vitest' 2 | import { matchLanguages } from '../../utils/language' 3 | 4 | describe('language', () => { 5 | it('match language', () => { 6 | expect(matchLanguages(['zh-CN', 'zh-TW'], ['zh'])).toMatchInlineSnapshot('"zh-CN"') 7 | expect(matchLanguages(['zh-CN', 'zh-TW'], ['en'])).toMatchInlineSnapshot('null') 8 | 9 | expect(matchLanguages(['zh-CN', 'zh-TW', 'en-US'], ['zh', 'en'])).toMatchInlineSnapshot('"zh-CN"') 10 | expect(matchLanguages(['zh-CN', 'zh-TW', 'en-US'], ['en', 'zh-CN'])).toMatchInlineSnapshot('"en-US"') 11 | expect(matchLanguages(['zh-CN', 'zh-TW', 'en-US'], ['zh-TW', 'en'])).toMatchInlineSnapshot('"zh-TW"') 12 | 13 | expect(matchLanguages(['zh-TW', 'en-US'], ['zh-CN', 'en-GB'])).toMatchInlineSnapshot('"zh-TW"') 14 | expect(matchLanguages(['zh-TW', 'en-GB'], ['ja-JP', 'zh-CN'])).toMatchInlineSnapshot('"zh-TW"') 15 | 16 | expect(matchLanguages(['zh-TW'], ['zh-tw'])).toMatchInlineSnapshot('"zh-TW"') 17 | }) 18 | }) 19 | -------------------------------------------------------------------------------- /tests/unit/permalinks.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from 'vitest' 2 | import { HANDLED_MASTO_URLS } from '~/constants' 3 | 4 | const validPermalinks = [ 5 | 'https://m1as-social34.to.social/@elk', 6 | 'https://m1as-social34.to.social/@elk22/123', 7 | 'https://m1as-social34.to.social/@elk22/objects/123', 8 | 'webtoo.ls/@elk', 9 | ] 10 | 11 | const invalidPermalinks = [ 12 | 'https://webtoo.ls', 13 | 'https://webtoo.ls/elk/123', 14 | ] 15 | 16 | describe('permalinks', () => { 17 | it.each(validPermalinks)('should recognise %s', (url) => { 18 | expect(HANDLED_MASTO_URLS.test(url)).toBe(true) 19 | }) 20 | it.each(invalidPermalinks)('should not recognise %s', (url) => { 21 | expect(HANDLED_MASTO_URLS.test(url)).toBe(false) 22 | }) 23 | }) 24 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./.nuxt/tsconfig.json" 3 | } 4 | -------------------------------------------------------------------------------- /types/translation-status.ts: -------------------------------------------------------------------------------- 1 | export interface NimbusTranslationStatus { 2 | total: number 3 | locales: Record 7 | } 8 | -------------------------------------------------------------------------------- /types/utils.ts: -------------------------------------------------------------------------------- 1 | export type Mutable = { 2 | -readonly[P in keyof T]: T[P] 3 | } 4 | 5 | export type Overwrite = Omit & O 6 | export type MarkNonNullable = Overwrite 8 | }> 9 | -------------------------------------------------------------------------------- /utils/elk-idb.ts: -------------------------------------------------------------------------------- 1 | import type { UseStore } from 'idb-keyval' 2 | import { 3 | del as delIdb, 4 | get as getIdb, 5 | promisifyRequest, 6 | set as setIdb, 7 | update as updateIdb, 8 | 9 | } from 'idb-keyval' 10 | 11 | const databases: IDBOpenDBRequest[] = [] 12 | 13 | function createStore(): UseStore { 14 | const storeName = 'keyval' 15 | const request = indexedDB.open('keyval-store') 16 | databases.push(request) 17 | request.onupgradeneeded = () => request.result.createObjectStore(storeName) 18 | const dbp = promisifyRequest(request) 19 | return (txMode, callback) => dbp.then(db => callback(db.transaction(storeName, txMode).objectStore(storeName))) 20 | } 21 | 22 | let defaultGetStoreFunc: UseStore | undefined 23 | function defaultGetStore() { 24 | if (!defaultGetStoreFunc) 25 | defaultGetStoreFunc = createStore() 26 | 27 | return defaultGetStoreFunc 28 | } 29 | 30 | export function get(key: IDBValidKey) { 31 | return getIdb(key, defaultGetStore()) 32 | } 33 | 34 | export function set(key: IDBValidKey, value: any) { 35 | return setIdb(key, value, defaultGetStore()) 36 | } 37 | 38 | export function update(key: IDBValidKey, updater: (oldValue: T | undefined) => T) { 39 | return updateIdb(key, updater, defaultGetStore()) 40 | } 41 | 42 | export function del(key: IDBValidKey) { 43 | return delIdb(key, defaultGetStore()) 44 | } 45 | 46 | export function closeDatabases() { 47 | databases.forEach((db) => { 48 | if (db.result) 49 | db.result.close() 50 | }) 51 | defaultGetStoreFunc = undefined 52 | } 53 | -------------------------------------------------------------------------------- /utils/i18n.ts: -------------------------------------------------------------------------------- 1 | import { useI18n as useOriginalI18n } from 'vue-i18n' 2 | 3 | export function useI18n() { 4 | const { 5 | t, 6 | d, 7 | n, 8 | ...rest 9 | } = useOriginalI18n() 10 | 11 | return { 12 | ...rest, 13 | t: wrapI18n(t), 14 | d: wrapI18n(d), 15 | n: wrapI18n(n), 16 | } satisfies ReturnType 17 | } 18 | 19 | export function wrapI18n any>(t: T): T { 20 | return ((...args: any[]) => { 21 | return isHydrated.value ? t(...args) : '' 22 | }) 23 | } 24 | -------------------------------------------------------------------------------- /utils/language.ts: -------------------------------------------------------------------------------- 1 | export function matchLanguages(languages: string[], acceptLanguages: readonly string[]): string | null { 2 | { 3 | // const lang = acceptLanguages.map(userLang => languages.find(lang => lang.startsWith(userLang))).filter(v => !!v)[0] 4 | // TODO: Support es-419, remove this code if we include spanish country variants 5 | const lang = acceptLanguages.map(userLang => languages.find((currentLang) => { 6 | if (currentLang === userLang) 7 | return currentLang 8 | 9 | // Edge browser: case for ca-valencia 10 | if (currentLang === 'ca-valencia' && userLang === 'ca-Es-VALENCIA') 11 | return currentLang 12 | 13 | if (userLang.startsWith('es-') && userLang !== 'es-ES' && currentLang === 'es-419') 14 | return currentLang 15 | 16 | return currentLang.startsWith(userLang) ? currentLang : undefined 17 | })).filter(v => !!v)?.[0] 18 | if (lang) 19 | return lang 20 | } 21 | 22 | const lang = acceptLanguages.map((userLang) => { 23 | userLang = userLang.split('-')[0]! 24 | return languages.find(lang => lang.startsWith(userLang)) 25 | }).filter(v => !!v)[0] 26 | if (lang) 27 | return lang 28 | 29 | return null 30 | } 31 | -------------------------------------------------------------------------------- /vitest.config.ts: -------------------------------------------------------------------------------- 1 | import { defineVitestConfig } from '@nuxt/test-utils/config' 2 | import { isCI } from 'std-env' 3 | 4 | export default defineVitestConfig({ 5 | define: { 6 | 'process.test': 'true', 7 | }, 8 | test: { 9 | reporters: isCI ? ['default', 'hanging-process'] : ['default'], 10 | setupFiles: [ 11 | '/tests/setup.ts', 12 | ], 13 | environmentOptions: { 14 | nuxt: { 15 | mock: { 16 | indexedDb: true, 17 | intersectionObserver: true, 18 | }, 19 | }, 20 | }, 21 | }, 22 | }) 23 | -------------------------------------------------------------------------------- /vue-compiler-options.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'pkg-types' { 2 | interface TSConfig { 3 | // TODO: augment in nuxt 4 | vueCompilerOptions: any 5 | } 6 | } 7 | 8 | export {} 9 | --------------------------------------------------------------------------------