├── .env ├── .env.production ├── .gitattributes ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.yml │ ├── config.yml │ └── feature_request.yml ├── release.yml └── workflows │ ├── i18n-automerge.yml │ ├── i18n-update-readme.yml │ ├── main2prod.yml │ ├── playwright.yml │ ├── prettier-pr.yml │ ├── prodtag.yml │ └── update-catalogs.yml ├── .gitignore ├── .prettierrc ├── CHANGELOG.md ├── LICENSE ├── PRIVACY.MD ├── README.md ├── SECURITY.md ├── compose └── index.html ├── crowdin.yml ├── design ├── logo-2-avatar.png ├── logo-2.png ├── logo-2.svg ├── logo-3.png ├── logo-3.svg ├── logo-4.png ├── logo-4.svg ├── logo-bg-4.png ├── logo-bg-4.svg ├── logo-bw-4.png ├── logo-bw-4.svg ├── logo-favicon-4.png ├── logo-text.svg ├── logo-wb-4.png ├── logo-wb-4.svg ├── logo.afdesign ├── logo.png ├── logo.svg └── profile-banner.png ├── i18n-volunteers.json ├── index.html ├── lingui.config.js ├── package-lock.json ├── package.json ├── playwright.config.js ├── public ├── 404.html ├── apple-touch-icon.png ├── favicon.ico ├── logo-192.png ├── logo-512.png ├── logo-badge-72.png ├── logo-maskable-512.png ├── og-image-2.jpg ├── og-image.png ├── robots.txt └── sw.js ├── readme-assets ├── boosts-carousel.jpg ├── fancy-screenshot.jpg ├── hashtag-stuffing-collapsing.jpg ├── thread-number-badge.jpg └── user-name-display.jpg ├── rollbar.js ├── scripts ├── catalogs.js ├── extract-url.js ├── fetch-i18n-volunteers.js ├── fetch-instances-list.js ├── fetch-lingva-languages.js ├── fetch-supported-languages.js ├── fetch-translang-languages.js └── update-i18n-volunteers-readme.js ├── src ├── app.css ├── app.jsx ├── assets │ ├── features │ │ ├── boosts-carousel.jpg │ │ ├── catch-up.png │ │ ├── grouped-notifications.jpg │ │ ├── multi-column.jpg │ │ ├── multi-hashtag-timeline.jpg │ │ └── nested-comments-thread.jpg │ ├── floating-button.svg │ ├── logo-text.svg │ ├── logo.svg │ ├── multi-column.svg │ ├── phanpy-bg.svg │ ├── powered-by-giphy.svg │ ├── sandbox │ │ ├── big-buck-bunny-muted.webm │ │ ├── big-buck-bunny-preview.png │ │ ├── big-buck-bunny.mp3 │ │ └── big-buck-bunny.webm │ └── tab-menu-bar.svg ├── cloak-mode.css ├── components │ ├── AsyncText.jsx │ ├── ICONS.jsx │ ├── ScheduledAtField.jsx │ ├── account-block.css │ ├── account-block.jsx │ ├── account-info.css │ ├── account-info.jsx │ ├── account-sheet.jsx │ ├── avatar.css │ ├── avatar.jsx │ ├── background-service.jsx │ ├── columns.jsx │ ├── compose-button.jsx │ ├── compose-suspense.jsx │ ├── compose.css │ ├── compose.jsx │ ├── custom-emoji.jsx │ ├── drafts.css │ ├── drafts.jsx │ ├── embed-modal.css │ ├── embed-modal.jsx │ ├── emoji-text.jsx │ ├── follow-request-buttons.jsx │ ├── generic-accounts.css │ ├── generic-accounts.jsx │ ├── icon.jsx │ ├── intersection-view.jsx │ ├── intl-segmenter-suspense.jsx │ ├── keyboard-shortcuts-help.css │ ├── keyboard-shortcuts-help.jsx │ ├── lang-selector.jsx │ ├── lazy-shazam.jsx │ ├── link.jsx │ ├── links-bar.css │ ├── list-add-edit.jsx │ ├── list-exclusive-badge.css │ ├── list-exclusive-badge.jsx │ ├── loader.css │ ├── loader.jsx │ ├── media-alt-modal.jsx │ ├── media-modal.jsx │ ├── media-post.css │ ├── media-post.jsx │ ├── media.jsx │ ├── menu-confirm.jsx │ ├── menu-link.jsx │ ├── menu2.jsx │ ├── modal.css │ ├── modal.jsx │ ├── modals.jsx │ ├── name-text.css │ ├── name-text.jsx │ ├── nav-menu.css │ ├── nav-menu.jsx │ ├── notification-service.jsx │ ├── notification.jsx │ ├── poll.jsx │ ├── relative-time.jsx │ ├── report-modal.css │ ├── report-modal.jsx │ ├── search-command.css │ ├── search-command.jsx │ ├── search-form.jsx │ ├── shortcuts-settings.css │ ├── shortcuts-settings.jsx │ ├── shortcuts.css │ ├── shortcuts.jsx │ ├── status.css │ ├── status.jsx │ ├── submenu2.jsx │ ├── timeline.jsx │ ├── translation-block.css │ └── translation-block.jsx ├── compose.jsx ├── data │ ├── catalogs.json │ ├── features.json │ ├── instances.json │ ├── lingva-source-languages.json │ ├── lingva-target-languages.json │ ├── listed-locales.json │ ├── listed-locales.json.orig │ ├── status-supported-languages.json │ ├── translang-languages-native.json │ ├── translang-languages.json │ └── url-regex.json ├── index.css ├── locales.js ├── locales │ ├── ar-SA.po │ ├── ca-ES.po │ ├── cs-CZ.po │ ├── de-DE.po │ ├── en.po │ ├── eo-UY.po │ ├── es-ES.po │ ├── eu-ES.po │ ├── fa-IR.po │ ├── fi-FI.po │ ├── fr-FR.po │ ├── gl-ES.po │ ├── he-IL.po │ ├── it-IT.po │ ├── ja-JP.po │ ├── kab.po │ ├── ko-KR.po │ ├── lt-LT.po │ ├── nb-NO.po │ ├── nl-NL.po │ ├── oc-FR.po │ ├── pl-PL.po │ ├── pseudo-LOCALE.po │ ├── pt-BR.po │ ├── pt-PT.po │ ├── ru-RU.po │ ├── th-TH.po │ ├── tr-TR.po │ ├── uk-UA.po │ ├── zh-CN.po │ └── zh-TW.po ├── main.jsx ├── pages │ ├── 404.jsx │ ├── account-statuses.jsx │ ├── accounts.css │ ├── accounts.jsx │ ├── annual-report.css │ ├── annual-report.jsx │ ├── bookmarks.jsx │ ├── catchup.css │ ├── catchup.jsx │ ├── favourites.jsx │ ├── filters.css │ ├── filters.jsx │ ├── followed-hashtags.jsx │ ├── following.jsx │ ├── hashtag.jsx │ ├── home.jsx │ ├── http-route.jsx │ ├── list.jsx │ ├── lists.css │ ├── lists.jsx │ ├── login.css │ ├── login.jsx │ ├── mentions.jsx │ ├── notifications-menu.css │ ├── notifications.css │ ├── notifications.jsx │ ├── public.jsx │ ├── sandbox.css │ ├── sandbox.jsx │ ├── scheduled-posts.css │ ├── scheduled-posts.jsx │ ├── search.css │ ├── search.jsx │ ├── settings.css │ ├── settings.jsx │ ├── status-route.jsx │ ├── status.css │ ├── status.jsx │ ├── trending.css │ ├── trending.jsx │ ├── welcome.css │ └── welcome.jsx ├── polyfills.js └── utils │ ├── api.js │ ├── auth.js │ ├── browser-translator.js │ ├── color-utils.js │ ├── db.js │ ├── emojify-text.js │ ├── enhance-content.js │ ├── filter-context.js │ ├── filters.js │ ├── focus-deck.js │ ├── followed-tags.js │ ├── format-duration.js │ ├── get-domain.js │ ├── get-instance-status-url.js │ ├── get-translate-target-language.js │ ├── getHTMLText.js │ ├── group-notifications.js │ ├── handle-content-links.js │ ├── html-content-length.js │ ├── i18n-duration.js │ ├── is-rtl.js │ ├── isMastodonLinkMaybe.js │ ├── lang.js │ ├── lists.js │ ├── locale-match.js │ ├── localeCode2Text.js │ ├── mem.js │ ├── nice-date-time.js │ ├── oauth-pkce.js │ ├── open-compose.js │ ├── open-osk.js │ ├── pmem.js │ ├── pretty-bytes.js │ ├── push-notifications.js │ ├── ratelimit.js │ ├── relationships.js │ ├── safe-bounding-box-padding.js │ ├── shorten-number.js │ ├── show-compose.js │ ├── show-toast.js │ ├── speech.js │ ├── states.js │ ├── status-peek.js │ ├── store-utils.js │ ├── store.js │ ├── supports.js │ ├── timeline-utils.js │ ├── toast-alert.js │ ├── unfurl-link.js │ ├── useCloseWatcher.js │ ├── useInterval.js │ ├── useLocationChange.js │ ├── usePageVisibility.js │ ├── useScroll.js │ ├── useScrollFn.js │ ├── useTitle.js │ ├── useTruncated.js │ ├── useWindowSize.js │ └── visibility-icons-map.js ├── tests └── logged-out-view.spec.js └── vite.config.js /.env: -------------------------------------------------------------------------------- 1 | PHANPY_CLIENT_NAME=Phanpy 2 | PHANPY_WEBSITE=https://phanpy.social 3 | PHANPY_LINGVA_INSTANCES="lingva.phanpy.social lingva.lunar.icu lingva.garudalinux.org translate.plausibility.cloud" 4 | PHANPY_PRIVACY_POLICY_URL="https://github.com/cheeaun/phanpy/blob/main/PRIVACY.MD" 5 | PHANPY_TRANSLANG_INSTANCES="translang.phanpy.social" -------------------------------------------------------------------------------- /.env.production: -------------------------------------------------------------------------------- 1 | VITE_APP_ENV=production -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | *.po linguist-generated 2 | readme-assets/** linguist-documentation 3 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.yml: -------------------------------------------------------------------------------- 1 | name: "Bug report" 2 | description: "Create a report to help us improve" 3 | 4 | labels: 5 | - "bug" 6 | 7 | body: 8 | - type: input 9 | id: "site" 10 | attributes: 11 | label: "Site" 12 | description: |- 13 | What site(s) did you encounter this bug on? 14 | placeholder: |- 15 | phanpy.social 16 | 17 | - type: input 18 | id: "version" 19 | attributes: 20 | label: "Version" 21 | description: |- 22 | Which Phanpy version(s) did you encounter this bug on? 23 | You can see and copy your current version by opening the Settings menu and scrolling down to the About section. 24 | placeholder: |- 25 | 2024.10.08.0a176e2 26 | 27 | - type: input 28 | id: "instance" 29 | attributes: 30 | label: "Instance" 31 | description: |- 32 | Which instance(s) did you encounter this bug on? 33 | placeholder: |- 34 | mastodon.social 35 | 36 | - type: textarea 37 | id: "Browser" 38 | attributes: 39 | label: "Browser" 40 | description: |- 41 | Which browser(s) did you encounter this bug on? 42 | placeholder: |- 43 | - Firefox 132.0b5 on Windows 11 44 | - Safari 18 on iOS 18 on iPhone 16 Pro Max 45 | 46 | - type: textarea 47 | id: "description" 48 | attributes: 49 | label: "Bug description" 50 | description: |- 51 | A concise description of what the bug is. 52 | If applicable, add screenshots to help explain your problem. 53 | You can paste screenshots here and GitHub will convert them to Markdown for you. 54 | 55 | - type: textarea 56 | id: "steps" 57 | attributes: 58 | label: "To reproduce" 59 | description: |- 60 | A list of steps that can be performed to make the bug happen again. 61 | If possible, add screenshots to help demonstrate the steps. 62 | You can paste screenshots here and GitHub will convert them to Markdown for you. 63 | placeholder: |- 64 | 1. Go to '...' 65 | 2. Click on '...' 66 | 3. Scroll down to '...' 67 | 4. See error 68 | 69 | - type: textarea 70 | id: "behavior" 71 | attributes: 72 | label: "Expected behavior" 73 | description: |- 74 | A concise description of what you expected to happen. 75 | 76 | - type: textarea 77 | id: "other" 78 | attributes: 79 | label: "Other" 80 | description: |- 81 | Anything you want to add? 82 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: true 2 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.yml: -------------------------------------------------------------------------------- 1 | name: "Feature request" 2 | description: "Suggest an idea for this project" 3 | 4 | labels: 5 | - "enhancement" 6 | 7 | 8 | body: 9 | - type: textarea 10 | id: "problem" 11 | attributes: 12 | label: "Problem I have" 13 | description: |- 14 | If your request is related to a problem, please provide a clear and concise description of what the problem is. 15 | placeholder: |- 16 | I'm always frustrated when [...] 17 | 18 | - type: textarea 19 | id: "solution" 20 | attributes: 21 | label: "Solution I'd like" 22 | description: |- 23 | A clear and concise description of what you want to happen. 24 | 25 | - type: textarea 26 | id: "alternatives" 27 | attributes: 28 | label: "Alternatives considered" 29 | description: |- 30 | A clear and concise description of any alternative solutions or features you've considered. 31 | 32 | - type: textarea 33 | id: "other" 34 | attributes: 35 | label: "Other" 36 | description: |- 37 | Anything you want to add? 38 | -------------------------------------------------------------------------------- /.github/release.yml: -------------------------------------------------------------------------------- 1 | changelog: 2 | exclude: 3 | labels: 4 | - 'i18n' 5 | -------------------------------------------------------------------------------- /.github/workflows/i18n-automerge.yml: -------------------------------------------------------------------------------- 1 | name: i18n PR auto-merge 2 | 3 | on: 4 | pull_request: 5 | types: [opened, synchronize, reopened, labeled] 6 | branches: 7 | - main 8 | 9 | jobs: 10 | run-and-merge: 11 | if: contains(github.event.pull_request.labels.*.name, 'i18n') && 12 | github.event.pull_request.base.ref == 'main' && 13 | github.event.pull_request.head.ref == 'l10n_main' 14 | runs-on: ubuntu-latest 15 | 16 | steps: 17 | - uses: actions/checkout@v4 18 | with: 19 | fetch-depth: 0 20 | - run: sleep 15 21 | 22 | - name: Check if the branch is dirty 23 | run: | 24 | git fetch origin ${{ github.event.pull_request.head.ref }} 25 | if [ $(git rev-parse HEAD) != $(git rev-parse origin/${{ github.event.pull_request.head.ref }}) ]; then 26 | echo "Branch is dirty. Exiting..." 27 | exit 0 28 | fi 29 | 30 | - name: Check auto-merge conditions 31 | run: | 32 | BASE_SHA="${{ github.event.pull_request.base.sha }}" 33 | HEAD_SHA="${{ github.event.pull_request.head.sha }}" 34 | 35 | # Debug: Show the base and head SHA 36 | echo "Base SHA: $BASE_SHA" 37 | echo "Head SHA: $HEAD_SHA" 38 | 39 | # Check if the commits exist 40 | if ! git cat-file -e $BASE_SHA || ! git cat-file -e $HEAD_SHA; then 41 | echo "ERROR: One or both of the commits are not available." 42 | exit 1 43 | fi 44 | 45 | # Calculate the total number of lines changed (added, removed, or modified) 46 | LINES_CHANGED=$(git diff --shortstat $BASE_SHA $HEAD_SHA | awk '{print $4 + $6 + $8}') 47 | 48 | if [ -z "$LINES_CHANGED" ]; then 49 | LINES_CHANGED=0 50 | fi 51 | 52 | echo "Total lines changed: $LINES_CHANGED" 53 | 54 | # Check if the number of lines changed is more than 50 55 | if [ "$LINES_CHANGED" -le 50 ]; then 56 | exit 0 57 | else 58 | echo "More than 50 lines have been changed. Merging pull request." 59 | 60 | # List of locales changed 61 | LOCALES_CHANGED=$(git diff --name-only $BASE_SHA $HEAD_SHA | grep '\.po$' | awk -F '/' '{print $NF}' | sed 's/\.po$//' | tr '\n' ',' | sed 's/,$//') 62 | 63 | # Better subject 64 | # "i18n updates ([LOCALES_CHANGED])" 65 | PR_NUMBER=$(echo ${{ github.event.pull_request.number }}) 66 | SUBJECT="i18n updates ($LOCALES_CHANGED) (#$PR_NUMBER)" 67 | 68 | gh pr merge $PR_NUMBER --squash --subject "$SUBJECT" || true 69 | fi 70 | env: 71 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 72 | -------------------------------------------------------------------------------- /.github/workflows/i18n-update-readme.yml: -------------------------------------------------------------------------------- 1 | name: Update README with list of i18n volunteers 2 | 3 | on: 4 | schedule: 5 | # Every week 6 | - cron: '0 0 * * 0' 7 | workflow_dispatch: 8 | 9 | jobs: 10 | update-readme: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v4 14 | - uses: actions/setup-node@v4 15 | with: 16 | node-version: 20 17 | - run: npm ci 18 | - run: | 19 | npm run fetch-i18n-volunteers 20 | npm run readme:i18n-volunteers 21 | 22 | # Commit & push if there are changes 23 | if git diff --quiet README.md; then 24 | echo "No changes to README.md" 25 | else 26 | echo "Changes to README.md" 27 | git config --global user.email "github-actions[bot]@users.noreply.github.com" 28 | git config --global user.name "github-actions[bot]" 29 | git add README.md 30 | git commit -m "Update README.md" 31 | git push 32 | fi 33 | env: 34 | CROWDIN_ACCESS_TOKEN: ${{ secrets.CROWDIN_ACCESS_TOKEN }} 35 | -------------------------------------------------------------------------------- /.github/workflows/main2prod.yml: -------------------------------------------------------------------------------- 1 | name: Pull Request into `production` from `main` 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | types: [closed] 9 | branches: 10 | - main 11 | 12 | jobs: 13 | auto-pull-request: 14 | if: github.repository == 'cheeaun/phanpy' 15 | runs-on: ubuntu-latest 16 | steps: 17 | - uses: vsoch/pull-request-action@master 18 | env: 19 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 20 | PULL_REQUEST_FROM_BRANCH: 'main' 21 | PULL_REQUEST_BRANCH: 'production' 22 | -------------------------------------------------------------------------------- /.github/workflows/playwright.yml: -------------------------------------------------------------------------------- 1 | name: Playwright Tests 2 | permissions: 3 | contents: read 4 | on: 5 | push: 6 | branches: 7 | - main 8 | pull_request: 9 | branches: 10 | - main 11 | jobs: 12 | test: 13 | timeout-minutes: 60 14 | runs-on: macos-latest 15 | steps: 16 | - uses: actions/checkout@v4 17 | - uses: actions/setup-node@v4 18 | with: 19 | node-version: lts/* 20 | - name: Install dependencies 21 | run: npm ci 22 | - name: Install Playwright Browsers 23 | run: npx playwright install --with-deps webkit --only-shell 24 | - name: Run Playwright tests 25 | run: npx playwright test 26 | - uses: actions/upload-artifact@v4 27 | if: ${{ !cancelled() }} 28 | with: 29 | name: playwright-report 30 | path: playwright-report/ 31 | retention-days: 30 32 | -------------------------------------------------------------------------------- /.github/workflows/prettier-pr.yml: -------------------------------------------------------------------------------- 1 | name: Prettier on pull requests 2 | 3 | on: 4 | pull_request: 5 | workflow_dispatch: 6 | 7 | jobs: 8 | prettier: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v4 12 | # Need node to install prettier plugin(s) 13 | - uses: actions/setup-node@v4 14 | with: 15 | node-version: 20 16 | - run: npm ci 17 | - run: | 18 | echo "Prettier-ing files" 19 | npx prettier "src/**/*.{js,jsx}" --check 20 | -------------------------------------------------------------------------------- /.github/workflows/prodtag.yml: -------------------------------------------------------------------------------- 1 | name: Auto-create tag/release on every push to `production` 2 | 3 | on: 4 | push: 5 | branches: 6 | - production 7 | 8 | jobs: 9 | tag: 10 | runs-on: ubuntu-latest 11 | permissions: 12 | contents: write 13 | steps: 14 | - uses: actions/checkout@v4 15 | with: 16 | ref: production 17 | # - run: git tag "`date +%Y.%m.%d`.`git rev-parse --short HEAD`" $(git rev-parse HEAD) 18 | # - run: git push --tags 19 | - uses: actions/setup-node@v3 20 | with: 21 | node-version: 18 22 | - run: npm ci && npm run build 23 | - run: cd dist && zip -r ../phanpy-dist.zip . && tar -czf ../phanpy-dist.tar.gz . && cd .. 24 | - id: tag_name 25 | run: echo ::set-output name=tag_name::$(date +%Y.%m.%d).$(git rev-parse --short HEAD) 26 | - uses: softprops/action-gh-release@v1 27 | with: 28 | tag_name: ${{ steps.tag_name.outputs.tag_name }} 29 | generate_release_notes: true 30 | files: | 31 | phanpy-dist.zip 32 | phanpy-dist.tar.gz 33 | -------------------------------------------------------------------------------- /.github/workflows/update-catalogs.yml: -------------------------------------------------------------------------------- 1 | name: Update Catalogs 2 | 3 | on: 4 | push: 5 | branches: 6 | - l10n_main 7 | workflow_dispatch: 8 | 9 | jobs: 10 | update-catalogs: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v4 14 | with: 15 | ref: l10n_main 16 | - uses: actions/setup-node@v4 17 | with: 18 | node-version: 20 19 | - run: npm ci 20 | - name: Update catalogs.json 21 | run: | 22 | node scripts/catalogs.js 23 | if git diff --quiet src/data/catalogs.json; then 24 | echo "No changes to catalogs.json" 25 | else 26 | echo "Changes to catalogs.json" 27 | git config --global user.email "github-actions[bot]@users.noreply.github.com" 28 | git config --global user.name "github-actions[bot]" 29 | git add src/data/catalogs.json 30 | git commit -m "Update catalogs.json" 31 | git push origin HEAD:l10n_main || true 32 | fi 33 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | 26 | # Custom 27 | .env.dev 28 | phanpy-dist.zip 29 | phanpy-dist.tar.gz 30 | sonda-report.html 31 | 32 | # Compiled locale files 33 | src/locales/*.js 34 | 35 | # Playwright 36 | /test-results/ 37 | /playwright-report/ 38 | /blob-report/ 39 | /playwright/.cache/ 40 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "tabWidth": 2, 3 | "useTabs": false, 4 | "singleQuote": true, 5 | "trailingComma": "all", 6 | "plugins": ["@ianvs/prettier-plugin-sort-imports"], 7 | "importOrder": [ 8 | "^[^.].*.css$", 9 | "index.css$", 10 | ".css$", 11 | "", 12 | "./polyfills", 13 | "", 14 | "", 15 | "", 16 | "/assets/", 17 | "", 18 | "^../", 19 | "", 20 | "^[./]" 21 | ] 22 | } 23 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Lim Chee Aun 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 | -------------------------------------------------------------------------------- /PRIVACY.MD: -------------------------------------------------------------------------------- 1 | # Privacy Policy 2 | 3 | Phanpy does not collect or process any personal information from its users. The website is used to connect to third-party Mastodon servers that may or may not collect personal information and are not covered by this privacy policy. Each third-party Mastodon server comes equipped with its own privacy policy that can be viewed through that server's website. 4 | 5 | ## Hosting 6 | 7 | Phanpy is hosted on [Cloudflare Pages](https://pages.cloudflare.com/) as a static website. Read more about [Cloudflare's privacy policy](https://www.cloudflare.com/privacypolicy/). 8 | 9 | ## Translations 10 | 11 | Phanpy uses [TransLang API](https://github.com/cheeaun/translang-api) for translating post content, profile bio and media description. Read more about [TransLang API's privacy policy](https://github.com/cheeaun/translang-api/blob/main/PRIVACY.md). 12 | 13 | ## Error logging 14 | 15 | Phanpy dev site (*dev.phanpy.social*) uses [Rollbar](https://rollbar.com/) to log errors for debugging purposes. Read more about [Rollbar's privacy policy](https://rollbar.com/privacy/). The production site (*phanpy.social*) does not use error logging. 16 | 17 | ## Analytics 18 | 19 | Phanpy uses [Cloudflare Web Analytics](https://www.cloudflare.com/web-analytics/) to collect anonymous usage statistics. Read more about [Cloudflare's privacy policy](https://www.cloudflare.com/privacypolicy/). 20 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Supported Versions 4 | Only the **latest production release** of Phanpy receives security updates. Always update to the newest production version for the best protection. 5 | 6 | ## Reporting a Vulnerability 7 | 8 | **Please don’t discuss security issues in public GitHub issues.** Instead: 9 | 10 | 1. **GitHub Private Reporting** (preferred): 11 | - Click ["Report a vulnerability"](https://github.com/cheeaun/phanpy/security/advisories/new) under the **Security** tab. 12 | 2. **Email**: 13 | - Reach out to me directly at cheeaun@gmail.com 14 | 15 | **Include**: 16 | - Steps to reproduce the issue 17 | - Which parts of Phanpy are affected 18 | - How severe you think the impact could be 19 | 20 | ## Disclosure Policy 21 | 22 | **Heads up:** I’m a solo maintainer working on Phanpy in my free time. While I take security seriously, I can’t promise enterprise-grade response times. Here’s how I’ll handle reports: 23 | 24 | 1. **Confirmation**: I’ll acknowledge reports when possible, but this might take weeks due to limited availability. 25 | 2. **Fixing**: Critical bugs will be prioritized, but fixes may take significant time. If it’s urgent, feel free to follow up. 26 | 3. **Public Disclosure**: Patched vulnerabilities will be disclosed once the fix is confirmed stable and most users have updated. 27 | 28 | ## Security Practices 29 | 30 | ### For Users 31 | 32 | - Use Phanpy with a Mastodon instance that enforces **HTTPS**. 33 | - Treat OAuth tokens like passwords – don’t share them! 34 | 35 | ### For Developers 36 | 37 | - **Dependencies**: GitHub Dependabot alerts are enabled for vulnerability monitoring. 38 | - **Code**: 39 | - Basic input sanitization to prevent XSS. 40 | - *Planned*: Improvements to client-side storage security (contributions welcome!). -------------------------------------------------------------------------------- /compose/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Compose / %PHANPY_CLIENT_NAME% 8 | 9 | 10 | 11 | 12 |
13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /crowdin.yml: -------------------------------------------------------------------------------- 1 | pull_request_labels: 2 | - i18n 3 | commit_message: New translations (%language%) 4 | append_commit_message: false 5 | files: 6 | - source: /src/locales/en.po 7 | translation: /src/locales/%locale%.po 8 | -------------------------------------------------------------------------------- /design/logo-2-avatar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cheeaun/phanpy/70224b1b4e978034679f0ca886d4c9dfeae0e945/design/logo-2-avatar.png -------------------------------------------------------------------------------- /design/logo-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cheeaun/phanpy/70224b1b4e978034679f0ca886d4c9dfeae0e945/design/logo-2.png -------------------------------------------------------------------------------- /design/logo-3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cheeaun/phanpy/70224b1b4e978034679f0ca886d4c9dfeae0e945/design/logo-3.png -------------------------------------------------------------------------------- /design/logo-3.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /design/logo-4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cheeaun/phanpy/70224b1b4e978034679f0ca886d4c9dfeae0e945/design/logo-4.png -------------------------------------------------------------------------------- /design/logo-4.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /design/logo-bg-4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cheeaun/phanpy/70224b1b4e978034679f0ca886d4c9dfeae0e945/design/logo-bg-4.png -------------------------------------------------------------------------------- /design/logo-bg-4.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /design/logo-bw-4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cheeaun/phanpy/70224b1b4e978034679f0ca886d4c9dfeae0e945/design/logo-bw-4.png -------------------------------------------------------------------------------- /design/logo-bw-4.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /design/logo-favicon-4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cheeaun/phanpy/70224b1b4e978034679f0ca886d4c9dfeae0e945/design/logo-favicon-4.png -------------------------------------------------------------------------------- /design/logo-text.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /design/logo-wb-4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cheeaun/phanpy/70224b1b4e978034679f0ca886d4c9dfeae0e945/design/logo-wb-4.png -------------------------------------------------------------------------------- /design/logo-wb-4.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /design/logo.afdesign: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cheeaun/phanpy/70224b1b4e978034679f0ca886d4c9dfeae0e945/design/logo.afdesign -------------------------------------------------------------------------------- /design/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cheeaun/phanpy/70224b1b4e978034679f0ca886d4c9dfeae0e945/design/logo.png -------------------------------------------------------------------------------- /design/profile-banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cheeaun/phanpy/70224b1b4e978034679f0ca886d4c9dfeae0e945/design/profile-banner.png -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 9 | %PHANPY_CLIENT_NAME% 10 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 30 | 38 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 57 | 58 | 59 | 60 |
61 | 62 | 63 | 64 | 65 | -------------------------------------------------------------------------------- /lingui.config.js: -------------------------------------------------------------------------------- 1 | import { ALL_LOCALES } from './src/locales'; 2 | 3 | const config = { 4 | locales: ALL_LOCALES, 5 | sourceLocale: 'en', 6 | pseudoLocale: 'pseudo-LOCALE', 7 | fallbackLocales: { 8 | default: 'en', 9 | }, 10 | catalogs: [ 11 | { 12 | path: '/src/locales/{locale}', 13 | include: ['src'], 14 | }, 15 | ], 16 | // compileNamespace: 'es', 17 | orderBy: 'origin', 18 | }; 19 | 20 | export default config; 21 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "phanpy", 3 | "version": "0.1.0", 4 | "type": "module", 5 | "scripts": { 6 | "dev": "vite", 7 | "build": "vite build", 8 | "preview": "vite preview", 9 | "fetch-instances": "node scripts/fetch-instances-list.js", 10 | "sourcemap": "npx source-map-explorer dist/assets/*.js", 11 | "bundle-visualizer": "npx vite-bundle-visualizer", 12 | "messages:extract": "lingui extract", 13 | "messages:extract:clean": "lingui extract --locale en --clean", 14 | "messages:compile": "lingui compile", 15 | "fetch-i18n-volunteers": "env $(cat .env.local | grep -v \"#\" | xargs) node scripts/fetch-i18n-volunteers.js", 16 | "readme:i18n-volunteers": "node scripts/update-i18n-volunteers-readme.js" 17 | }, 18 | "dependencies": { 19 | "@formatjs/intl-localematcher": "~0.6.1", 20 | "@formatjs/intl-segmenter": "~11.7.10", 21 | "@formkit/auto-animate": "~0.8.2", 22 | "@github/text-expander-element": "~2.9.2", 23 | "@iconify-icons/mingcute": "~1.2.9", 24 | "@justinribeiro/lite-youtube": "~1.8.2", 25 | "@lingui/detect-locale": "~5.3.2", 26 | "@lingui/macro": "~5.3.2", 27 | "@lingui/react": "~5.3.2", 28 | "@szhsin/react-menu": "~4.4.1", 29 | "chroma-js": "~3.1.2", 30 | "compare-versions": "~6.1.1", 31 | "fast-blurhash": "~1.1.4", 32 | "fast-equals": "~5.2.2", 33 | "fuse.js": "~7.1.0", 34 | "html-prettify": "~1.0.7", 35 | "idb-keyval": "~6.2.2", 36 | "intl-locale-textinfo-polyfill": "~2.1.1", 37 | "js-cookie": "~3.0.5", 38 | "just-debounce-it": "~3.2.0", 39 | "lz-string": "~1.5.0", 40 | "masto": "~7.1.0", 41 | "moize": "~6.1.6", 42 | "p-retry": "~6.2.1", 43 | "p-throttle": "~7.0.0", 44 | "preact": "10.26.8", 45 | "punycode": "~2.3.1", 46 | "react-hotkeys-hook": "~5.1.0", 47 | "react-intersection-observer": "~9.16.0", 48 | "react-quick-pinch-zoom": "~5.1.0", 49 | "react-router-dom": "6.6.2", 50 | "string-length": "6.0.0", 51 | "swiped-events": "~1.2.0", 52 | "tinyld": "~1.3.4", 53 | "toastify-js": "~1.12.0", 54 | "uid": "~2.0.2", 55 | "use-debounce": "~10.0.4", 56 | "use-long-press": "~3.3.0", 57 | "use-resize-observer": "~9.1.0", 58 | "valtio": "2.1.5" 59 | }, 60 | "devDependencies": { 61 | "@ianvs/prettier-plugin-sort-imports": "~4.4.2", 62 | "@lingui/babel-plugin-lingui-macro": "~5.3.2", 63 | "@lingui/cli": "~5.3.2", 64 | "@lingui/vite-plugin": "~5.3.2", 65 | "@playwright/test": "~1.52.0", 66 | "@preact/preset-vite": "~2.10.1", 67 | "@types/node": "~22.15.30", 68 | "postcss": "~8.5.4", 69 | "postcss-dark-theme-class": "~1.3.0", 70 | "postcss-preset-env": "~10.2.0", 71 | "prettier": "3.5.3", 72 | "sonda": "~0.7.1", 73 | "twitter-text": "~3.1.0", 74 | "vite": "~6.3.5", 75 | "vite-plugin-generate-file": "~0.3.1", 76 | "vite-plugin-html-config": "~2.0.2", 77 | "vite-plugin-pwa": "~1.0.0", 78 | "vite-plugin-remove-console": "~2.2.0", 79 | "vite-plugin-run": "~0.6.1", 80 | "workbox-cacheable-response": "~7.3.0", 81 | "workbox-expiration": "~7.3.0", 82 | "workbox-navigation-preload": "~7.3.0", 83 | "workbox-routing": "~7.3.0", 84 | "workbox-strategies": "~7.3.0" 85 | }, 86 | "postcss": { 87 | "plugins": { 88 | "postcss-dark-theme-class": {}, 89 | "postcss-preset-env": { 90 | "features": { 91 | "logical-properties-and-values": false 92 | } 93 | } 94 | } 95 | }, 96 | "overrides": { 97 | "vite": { 98 | "rollup": ">=4.5.1" 99 | } 100 | }, 101 | "browserslist": [ 102 | "defaults", 103 | "android >= 4" 104 | ] 105 | } 106 | -------------------------------------------------------------------------------- /playwright.config.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | import { defineConfig, devices } from '@playwright/test'; 3 | 4 | /** 5 | * Read environment variables from file. 6 | * https://github.com/motdotla/dotenv 7 | */ 8 | // import dotenv from 'dotenv'; 9 | // import path from 'path'; 10 | // dotenv.config({ path: path.resolve(__dirname, '.env') }); 11 | 12 | /** 13 | * @see https://playwright.dev/docs/test-configuration 14 | */ 15 | export default defineConfig({ 16 | testDir: './tests', 17 | /* Run tests in files in parallel */ 18 | fullyParallel: true, 19 | /* Fail the build on CI if you accidentally left test.only in the source code. */ 20 | forbidOnly: !!process.env.CI, 21 | /* Retry on CI only */ 22 | retries: process.env.CI ? 2 : 0, 23 | /* Opt out of parallel tests on CI. */ 24 | workers: process.env.CI ? 1 : undefined, 25 | /* Reporter to use. See https://playwright.dev/docs/test-reporters */ 26 | reporter: 'html', 27 | /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ 28 | use: { 29 | /* Base URL to use in actions like `await page.goto('/')`. */ 30 | baseURL: 'http://localhost:5173', 31 | 32 | /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ 33 | trace: 'on-first-retry', 34 | }, 35 | 36 | /* Configure projects for major browsers */ 37 | projects: [ 38 | { 39 | name: 'Mobile Safari', 40 | use: { ...devices['iPhone 13 Mini'] }, 41 | }, 42 | ], 43 | 44 | /* Run your local dev server before starting the tests */ 45 | webServer: { 46 | command: 'npm run dev', 47 | url: 'http://localhost:5173', 48 | reuseExistingServer: !process.env.CI, 49 | }, 50 | }); 51 | -------------------------------------------------------------------------------- /public/404.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 9 | Page not found 10 | 11 | 27 | 28 | 29 |

Page not found

30 |

Go home

31 | 32 | 33 | -------------------------------------------------------------------------------- /public/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cheeaun/phanpy/70224b1b4e978034679f0ca886d4c9dfeae0e945/public/apple-touch-icon.png -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cheeaun/phanpy/70224b1b4e978034679f0ca886d4c9dfeae0e945/public/favicon.ico -------------------------------------------------------------------------------- /public/logo-192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cheeaun/phanpy/70224b1b4e978034679f0ca886d4c9dfeae0e945/public/logo-192.png -------------------------------------------------------------------------------- /public/logo-512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cheeaun/phanpy/70224b1b4e978034679f0ca886d4c9dfeae0e945/public/logo-512.png -------------------------------------------------------------------------------- /public/logo-badge-72.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cheeaun/phanpy/70224b1b4e978034679f0ca886d4c9dfeae0e945/public/logo-badge-72.png -------------------------------------------------------------------------------- /public/logo-maskable-512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cheeaun/phanpy/70224b1b4e978034679f0ca886d4c9dfeae0e945/public/logo-maskable-512.png -------------------------------------------------------------------------------- /public/og-image-2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cheeaun/phanpy/70224b1b4e978034679f0ca886d4c9dfeae0e945/public/og-image-2.jpg -------------------------------------------------------------------------------- /public/og-image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cheeaun/phanpy/70224b1b4e978034679f0ca886d4c9dfeae0e945/public/og-image.png -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: * 2 | Allow: / -------------------------------------------------------------------------------- /readme-assets/boosts-carousel.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cheeaun/phanpy/70224b1b4e978034679f0ca886d4c9dfeae0e945/readme-assets/boosts-carousel.jpg -------------------------------------------------------------------------------- /readme-assets/fancy-screenshot.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cheeaun/phanpy/70224b1b4e978034679f0ca886d4c9dfeae0e945/readme-assets/fancy-screenshot.jpg -------------------------------------------------------------------------------- /readme-assets/hashtag-stuffing-collapsing.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cheeaun/phanpy/70224b1b4e978034679f0ca886d4c9dfeae0e945/readme-assets/hashtag-stuffing-collapsing.jpg -------------------------------------------------------------------------------- /readme-assets/thread-number-badge.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cheeaun/phanpy/70224b1b4e978034679f0ca886d4c9dfeae0e945/readme-assets/thread-number-badge.jpg -------------------------------------------------------------------------------- /readme-assets/user-name-display.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cheeaun/phanpy/70224b1b4e978034679f0ca886d4c9dfeae0e945/readme-assets/user-name-display.jpg -------------------------------------------------------------------------------- /scripts/extract-url.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import regexSupplant from 'twitter-text/dist/lib/regexSupplant.js'; 3 | import validDomain from 'twitter-text/dist/regexp/validDomain.js'; 4 | import validPortNumber from 'twitter-text/dist/regexp/validPortNumber.js'; 5 | import validUrlPath from 'twitter-text/dist/regexp/validUrlPath.js'; 6 | import validUrlPrecedingChars from 'twitter-text/dist/regexp/validUrlPrecedingChars.js'; 7 | import validUrlQueryChars from 'twitter-text/dist/regexp/validUrlQueryChars.js'; 8 | import validUrlQueryEndingChars from 'twitter-text/dist/regexp/validUrlQueryEndingChars.js'; 9 | 10 | // The difference with twitter-text's extractURL is that the protocol isn't 11 | // optional. 12 | 13 | const urlRegex = regexSupplant( 14 | '(' + // $1 total match 15 | '(#{validUrlPrecedingChars})' + // $2 Preceeding chracter 16 | '(' + // $3 URL 17 | '(https?:\\/\\/)' + // $4 Protocol (optional) <-- THIS IS THE DIFFERENCE, MISSING '?' AFTER PROTOCOL 18 | '(#{validDomain})' + // $5 Domain(s) 19 | '(?::(#{validPortNumber}))?' + // $6 Port number (optional) 20 | '(\\/#{validUrlPath}*)?' + // $7 URL Path 21 | '(\\?#{validUrlQueryChars}*#{validUrlQueryEndingChars})?' + // $8 Query String 22 | ')' + 23 | ')', 24 | { 25 | validUrlPrecedingChars, 26 | validDomain, 27 | validPortNumber, 28 | validUrlPath, 29 | validUrlQueryChars, 30 | validUrlQueryEndingChars, 31 | }, 32 | 'gi', 33 | ); 34 | 35 | const filePath = 'src/data/url-regex.json'; 36 | fs.writeFile( 37 | filePath, 38 | JSON.stringify({ 39 | source: urlRegex.source, 40 | flags: urlRegex.flags, 41 | }), 42 | (err) => { 43 | if (err) { 44 | console.error(err); 45 | } 46 | console.log(`Wrote ${filePath}`); 47 | }, 48 | ); 49 | -------------------------------------------------------------------------------- /scripts/fetch-i18n-volunteers.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | 3 | const { CROWDIN_ACCESS_TOKEN } = process.env; 4 | 5 | const PROJECT_ID = '703337'; 6 | 7 | if (!CROWDIN_ACCESS_TOKEN) { 8 | throw new Error('CROWDIN_ACCESS_TOKEN is not set'); 9 | } 10 | 11 | // Generate Report 12 | 13 | let REPORT_ID = null; 14 | { 15 | const response = await fetch( 16 | `https://api.crowdin.com/api/v2/projects/${PROJECT_ID}/reports`, 17 | { 18 | headers: { 19 | Authorization: `Bearer ${CROWDIN_ACCESS_TOKEN}`, 20 | 'Content-Type': 'application/json', 21 | }, 22 | method: 'POST', 23 | body: JSON.stringify({ 24 | name: 'top-members', 25 | schema: { 26 | format: 'json', 27 | }, 28 | }), 29 | }, 30 | ); 31 | const json = await response.json(); 32 | console.log(`Report ID: ${json?.data?.identifier}`); 33 | REPORT_ID = json?.data?.identifier; 34 | } 35 | 36 | if (!REPORT_ID) { 37 | throw new Error('Report ID is not found'); 38 | } 39 | 40 | // Check Report Generation Status 41 | let finished = false; 42 | { 43 | let maxPolls = 10; 44 | do { 45 | maxPolls--; 46 | if (maxPolls < 0) break; 47 | 48 | // Wait for 1 second 49 | await new Promise((resolve) => setTimeout(resolve, 1000)); 50 | 51 | const status = await fetch( 52 | `https://api.crowdin.com/api/v2/projects/${PROJECT_ID}/reports/${REPORT_ID}`, 53 | { 54 | headers: { 55 | Authorization: `Bearer ${CROWDIN_ACCESS_TOKEN}`, 56 | 'Content-Type': 'application/json', 57 | }, 58 | }, 59 | ); 60 | const json = await status.json(); 61 | const progress = json?.data?.progress; 62 | console.log(`Progress: ${progress}% (${maxPolls} retries left)`); 63 | finished = json?.data?.status === 'finished'; 64 | } while (!finished); 65 | } 66 | 67 | if (!finished) { 68 | throw new Error('Failed to generate report'); 69 | } 70 | 71 | // Download Report 72 | let reportURL = null; 73 | { 74 | const response = await fetch( 75 | `https://api.crowdin.com/api/v2/projects/${PROJECT_ID}/reports/${REPORT_ID}/download`, 76 | { 77 | headers: { 78 | Authorization: `Bearer ${CROWDIN_ACCESS_TOKEN}`, 79 | 'Content-Type': 'application/json', 80 | }, 81 | }, 82 | ); 83 | const json = await response.json(); 84 | reportURL = json?.data?.url; 85 | console.log(`Report URL: ${reportURL}`); 86 | } 87 | 88 | if (!reportURL) { 89 | throw new Error('Report URL is not found'); 90 | } 91 | 92 | // Actually download the report 93 | let members = null; 94 | { 95 | const response = await fetch(reportURL); 96 | const json = await response.json(); 97 | 98 | const { data } = json; 99 | 100 | if (!data?.length) { 101 | throw new Error('No data found'); 102 | } 103 | 104 | // Sort by 'user.fullName' 105 | data.sort((a, b) => a.user.username.localeCompare(b.user.username)); 106 | members = data 107 | .filter((item) => { 108 | const isMyself = item.user.username === 'cheeaun'; 109 | const translatedMoreThanZero = item.translated > 0; 110 | 111 | return !isMyself && translatedMoreThanZero; 112 | }) 113 | .map((item) => ({ 114 | avatarUrl: item.user.avatarUrl, 115 | username: item.user.username, 116 | languages: item.languages.map((lang) => lang.name), 117 | })); 118 | 119 | console.log(members); 120 | 121 | if (members?.length) { 122 | fs.writeFileSync( 123 | 'i18n-volunteers.json', 124 | JSON.stringify(members, null, '\t'), 125 | ); 126 | } 127 | } 128 | 129 | if (!members?.length) { 130 | throw new Error('No members found'); 131 | } 132 | -------------------------------------------------------------------------------- /scripts/fetch-instances-list.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | 3 | const url = 'https://api.joinmastodon.org/servers'; 4 | const results = await fetch(url); 5 | 6 | const json = await results.json(); 7 | 8 | const domains = json.map((instance) => instance.domain); 9 | 10 | // Write to file 11 | const path = './src/data/instances.json'; 12 | fs.writeFileSync(path, JSON.stringify(domains, null, '\t'), 'utf8'); 13 | -------------------------------------------------------------------------------- /scripts/fetch-lingva-languages.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | 3 | fetch('https://lingva.phanpy.social/api/v1/languages/source') 4 | .then((response) => response.json()) 5 | .then((json) => { 6 | const file = './src/data/lingva-source-languages.json'; 7 | console.log(`Writing ${file}...`); 8 | fs.writeFileSync(file, JSON.stringify(json.languages, null, '\t'), 'utf8'); 9 | }); 10 | 11 | fetch('https://lingva.phanpy.social/api/v1/languages/target') 12 | .then((response) => response.json()) 13 | .then((json) => { 14 | const file = './src/data/lingva-target-languages.json'; 15 | console.log(`Writing ${file}...`); 16 | fs.writeFileSync(file, JSON.stringify(json.languages, null, '\t'), 'utf8'); 17 | }); 18 | -------------------------------------------------------------------------------- /scripts/fetch-supported-languages.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | 3 | const url = 'https://mastodon.social/'; 4 | 5 | const html = await fetch(url).then((res) => res.text()); 6 | 7 | // Extract the JSON between 8 | const json = html.match( 9 | /