mirror of
https://github.com/cheeaun/phanpy.git
synced 2025-02-02 06:06:41 +01:00
Merge remote-tracking branch 'origin/main' into feature/akkoma-local-only
# Conflicts: # src/locales/en.po
This commit is contained in:
commit
7f06187d75
74 changed files with 28158 additions and 14722 deletions
4
.github/release.yml
vendored
Normal file
4
.github/release.yml
vendored
Normal file
|
@ -0,0 +1,4 @@
|
|||
changelog:
|
||||
exclude:
|
||||
labels:
|
||||
- 'i18n'
|
25
.github/workflows/i18n-automerge.yml
vendored
25
.github/workflows/i18n-automerge.yml
vendored
|
@ -3,6 +3,8 @@ name: i18n PR auto-merge
|
|||
on:
|
||||
pull_request:
|
||||
types: [opened, synchronize, reopened, labeled]
|
||||
branches:
|
||||
- main
|
||||
|
||||
jobs:
|
||||
run-and-merge:
|
||||
|
@ -10,14 +12,21 @@ jobs:
|
|||
github.event.pull_request.base.ref == 'main' &&
|
||||
github.event.pull_request.head.ref == 'l10n_main'
|
||||
runs-on: ubuntu-latest
|
||||
# concurrency:
|
||||
# group: ${{ github.workflow }}-${{ github.ref }}
|
||||
# cancel-in-progress: true
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- run: sleep 15
|
||||
|
||||
- name: Check if the branch is dirty
|
||||
run: |
|
||||
git fetch origin ${{ github.event.pull_request.head.ref }}
|
||||
if [ $(git rev-parse HEAD) != $(git rev-parse origin/${{ github.event.pull_request.head.ref }}) ]; then
|
||||
echo "Branch is dirty. Exiting..."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
- name: Check auto-merge conditions
|
||||
run: |
|
||||
BASE_SHA="${{ github.event.pull_request.base.sha }}"
|
||||
|
@ -47,8 +56,16 @@ jobs:
|
|||
exit 0
|
||||
else
|
||||
echo "More than 50 lines have been changed. Merging pull request."
|
||||
|
||||
# List of locales changed
|
||||
LOCALES_CHANGED=$(git diff --name-only $BASE_SHA $HEAD_SHA | grep '\.po$' | awk -F '/' '{print $NF}' | sed 's/\.po$//' | tr '\n' ',' | sed 's/,$//')
|
||||
|
||||
# Better subject
|
||||
# "i18n updates ([LOCALES_CHANGED])"
|
||||
SUBJECT="i18n updates ($LOCALES_CHANGED)"
|
||||
|
||||
PR_NUMBER=$(echo ${{ github.event.pull_request.number }})
|
||||
gh pr merge $PR_NUMBER --auto --squash || true
|
||||
gh pr merge $PR_NUMBER --author "github-actions[bot]@users.noreply.github.com" --squash --subject "$SUBJECT" || true
|
||||
fi
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
|
34
.github/workflows/i18n-update-readme.yml
vendored
Normal file
34
.github/workflows/i18n-update-readme.yml
vendored
Normal file
|
@ -0,0 +1,34 @@
|
|||
name: Update README with list of i18n volunteers
|
||||
|
||||
on:
|
||||
schedule:
|
||||
# Every week
|
||||
- cron: '0 0 * * 0'
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
update-readme:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
- run: npm ci
|
||||
- run: |
|
||||
npm run fetch-i18n-volunteers
|
||||
npm run readme:i18n-volunteers
|
||||
|
||||
# Commit & push if there are changes
|
||||
if git diff --quiet README.md; then
|
||||
echo "No changes to README.md"
|
||||
else
|
||||
echo "Changes to README.md"
|
||||
git config --global user.email "github-actions[bot]@users.noreply.github.com"
|
||||
git config --global user.name "github-actions[bot]"
|
||||
git add README.md
|
||||
git commit -m "Update README.md"
|
||||
git push
|
||||
fi
|
||||
env:
|
||||
CROWDIN_ACCESS_TOKEN: ${{ secrets.CROWDIN_ACCESS_TOKEN }}
|
13
.github/workflows/update-catalogs.yml
vendored
13
.github/workflows/update-catalogs.yml
vendored
|
@ -1,17 +1,18 @@
|
|||
name: Update catalogs
|
||||
name: Update Catalogs
|
||||
|
||||
on:
|
||||
pull_request_target:
|
||||
types:
|
||||
- closed
|
||||
push:
|
||||
branches:
|
||||
- l10n_main
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
update-catalogs:
|
||||
if: github.event.pull_request.merged == true
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
ref: l10n_main
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
|
@ -27,5 +28,5 @@ jobs:
|
|||
git config --global user.name "github-actions[bot]"
|
||||
git add src/data/catalogs.json
|
||||
git commit -m "Update catalogs.json"
|
||||
git push origin HEAD:main
|
||||
git push origin HEAD:l10n_main || true
|
||||
fi
|
||||
|
|
53
README.md
53
README.md
|
@ -292,6 +292,59 @@ Costs involved in running and developing this web app:
|
|||
|
||||
[![Contributors](https://contrib.rocks/image?repo=cheeaun/phanpy)](https://github.com/cheeaun/phanpy/graphs/contributors)
|
||||
|
||||
### Translation volunteers
|
||||
|
||||
<!-- i18n volunteers start -->
|
||||
- <img src="https://crowdin-static.downloads.crowdin.com/avatar/12571163/medium/9f3ea938f4243f5ffe2a43f814ddc9e8_default.png" alt="" width="16" height="16" /> alidsds11 (Arabic)
|
||||
- <img src="https://crowdin-static.downloads.crowdin.com/avatar/13170041/medium/603136896af17fc005fd592ce3f48717_default.png" alt="" width="16" height="16" /> BoFFire (Arabic, French, Kabyle)
|
||||
- <img src="https://crowdin-static.downloads.crowdin.com/avatar/12898464/medium/d3758a76b894bade4bf271c9b32ea69b.png" alt="" width="16" height="16" /> Brawaru (Russian)
|
||||
- <img src="https://crowdin-static.downloads.crowdin.com/avatar/15460040/medium/1cfcfe5f5511b783b5d9f2b968bad819.png" alt="" width="16" height="16" /> cbasje (Dutch)
|
||||
- <img src="https://crowdin-static.downloads.crowdin.com/avatar/15525631/medium/51293156034d0236f1a1020c10f7d539_default.png" alt="" width="16" height="16" /> cbo92 (French)
|
||||
- <img src="https://crowdin-static.downloads.crowdin.com/avatar/15910131/medium/67fab7eeab5551853450e76e2ef19e59.jpeg" alt="" width="16" height="16" /> CDN (Chinese Simplified)
|
||||
- <img src="https://crowdin-static.downloads.crowdin.com/avatar/16556801/medium/ed5e501ca1f3cc6525d2da28db646346.jpeg" alt="" width="16" height="16" /> dannypsnl (Chinese Traditional)
|
||||
- <img src="https://crowdin-static.downloads.crowdin.com/avatar/3711/medium/d95ddd44e8dcb3a039f8a3463aed781d_default.png" alt="" width="16" height="16" /> databio (Catalan)
|
||||
- <img src="https://crowdin-static.downloads.crowdin.com/avatar/12618120/medium/ccb11bd042bbf4c7189033f7af2dbd32_default.png" alt="" width="16" height="16" /> drydenwu (Chinese Traditional)
|
||||
- <img src="https://crowdin-static.downloads.crowdin.com/avatar/13557465/medium/8feebf3677fa80c01e8c54c4fbe097e0_default.png" alt="" width="16" height="16" /> elissarc (French)
|
||||
- <img src="https://crowdin-static.downloads.crowdin.com/avatar/16528627/medium/9036f6eced0257f4e1ea4c5bd499de2d_default.png" alt="" width="16" height="16" /> ElPamplina (Spanish)
|
||||
- <img src="https://crowdin-static.downloads.crowdin.com/avatar/14277386/medium/29b30d2c73a214000e3941c9978f49e4_default.png" alt="" width="16" height="16" /> Fitik (Esperanto, Hebrew)
|
||||
- <img src="https://crowdin-static.downloads.crowdin.com/avatar/14444512/medium/99d0e7a3076deccbdfe0aa0b0612308c.jpeg" alt="" width="16" height="16" /> Freeesia (Japanese)
|
||||
- <img src="https://crowdin-static.downloads.crowdin.com/avatar/12617257/medium/a201650da44fed28890b0e0d8477a663.jpg" alt="" width="16" height="16" /> ghose (Galician)
|
||||
- <img src="https://crowdin-static.downloads.crowdin.com/avatar/15248754/medium/0dac6334ea0f4e8d4194a605c0a5594a.jpeg" alt="" width="16" height="16" /> hongminhee (Korean)
|
||||
- <img src="https://crowdin-static.downloads.crowdin.com/avatar/13454728/medium/1f78b7124b3c962bc4ae55e8d701fc91_default.png" alt="" width="16" height="16" /> isard (Catalan)
|
||||
- <img src="https://crowdin-static.downloads.crowdin.com/avatar/16532403/medium/4cefb19623bcc44d7cdb9e25aebf5250.jpeg" alt="" width="16" height="16" /> karlafej (Czech)
|
||||
- <img src="https://crowdin-static.downloads.crowdin.com/avatar/15791971/medium/88bdda3090339f16f6083390d32bb434_default.png" alt="" width="16" height="16" /> katullo11 (Italian)
|
||||
- <img src="https://crowdin-static.downloads.crowdin.com/avatar/14677260/medium/e53420d200961f48602324e18c091bdc.png" alt="" width="16" height="16" /> Kytta (German)
|
||||
- <img src="https://crowdin-static.downloads.crowdin.com/avatar/16529521/medium/ae6add93a901b0fefa2d9b1077920d73.png" alt="" width="16" height="16" /> llun (Thai)
|
||||
- <img src="https://crowdin-static.downloads.crowdin.com/avatar/16291756/medium/e1c4210f15537394cc764b8bc2dffe37.jpg" alt="" width="16" height="16" /> lucasofchirst (Occitan, Portuguese, Portuguese, Brazilian)
|
||||
- <img src="https://crowdin-static.downloads.crowdin.com/avatar/16537713/medium/825f0bf1a14fc545a76891a52839d86e_default.png" alt="" width="16" height="16" /> marcin.kozinski (Polish)
|
||||
- <img src="https://crowdin-static.downloads.crowdin.com/avatar/12882812/medium/77744d8db46e9a3e09030e1a02b7a572.jpeg" alt="" width="16" height="16" /> mojosoeun (Korean)
|
||||
- <img src="https://crowdin-static.downloads.crowdin.com/avatar/13613969/medium/c7834ddc0ada84a79671697a944bb274.png" alt="" width="16" height="16" /> moreal (Korean)
|
||||
- <img src="https://crowdin-static.downloads.crowdin.com/avatar/14158861/medium/ba1ff31dc5743b067ea6685f735229a5_default.png" alt="" width="16" height="16" /> MrWillCom (Chinese Simplified)
|
||||
- <img src="https://crowdin-static.downloads.crowdin.com/avatar/15652333/medium/7f36f289f9e2fe41d89ad534a1047f0e.png" alt="" width="16" height="16" /> nclm (French)
|
||||
- <img src="https://crowdin-static.downloads.crowdin.com/avatar/16539461/medium/2f41b9f0b802c1d200a6ab62167a7229_default.png" alt="" width="16" height="16" /> pazpi (Italian)
|
||||
- <img src="https://crowdin-static.downloads.crowdin.com/avatar/15106977/medium/54bf93b19af8bbfdee579ea51685bafa.jpeg" alt="" width="16" height="16" /> punkrockgirl (Basque)
|
||||
- <img src="https://crowdin-static.downloads.crowdin.com/avatar/16536247/medium/f010c8e718a36229733a8b58f6bad2a4_default.png" alt="" width="16" height="16" /> radecos (French)
|
||||
- <img src="https://crowdin-static.downloads.crowdin.com/avatar/16538917/medium/092ec03f56f9dd1cbce94379fa4d4d38.png" alt="" width="16" height="16" /> Razem (Czech)
|
||||
- <img src="https://crowdin-static.downloads.crowdin.com/avatar/14345134/medium/89a299239890c79a1d791d08ec3951dc.png" alt="" width="16" height="16" /> realpixelcode (German)
|
||||
- <img src="https://crowdin-static.downloads.crowdin.com/avatar/16527325/medium/37ebb27e7a50f7f85ae93beafc7028a2.jpg" alt="" width="16" height="16" /> rezahosseinzadeh (Persian)
|
||||
- <img src="https://crowdin-static.downloads.crowdin.com/avatar/13422319/medium/66632a98d73d48e36753d94ebcec9d4f.png" alt="" width="16" height="16" /> rwmpelstilzchen (Esperanto, Hebrew)
|
||||
- <img src="https://crowdin-static.downloads.crowdin.com/avatar/16538605/medium/bcdb6e3286b7d6237923f3a9383eed29.png" alt="" width="16" height="16" /> SadmL (Russian)
|
||||
- <img src="https://crowdin-static.downloads.crowdin.com/avatar/14565190/medium/79100599131b7776e9803e4b696915a3_default.png" alt="" width="16" height="16" /> Sky_NiniKo (French)
|
||||
- <img src="https://crowdin-static.downloads.crowdin.com/avatar/16532441/medium/1a47e8d80c95636e02d2260f6e233ca5.png" alt="" width="16" height="16" /> Su5hicz (Czech)
|
||||
- <img src="https://crowdin-static.downloads.crowdin.com/avatar/16533843/medium/7314c15492ef90118c33a80a427e6c87_default.png" alt="" width="16" height="16" /> Talos00 (Italian)
|
||||
- <img src="https://crowdin-static.downloads.crowdin.com/avatar/16530049/medium/683f3581620c6b4a5c753b416ed695a7.jpeg" alt="" width="16" height="16" /> tferrermo (Spanish)
|
||||
- <img src="https://crowdin-static.downloads.crowdin.com/avatar/16527851/medium/649e5a9a8a8cc61ced670d89e9cca082.png" alt="" width="16" height="16" /> tux93 (German)
|
||||
- <img src="https://crowdin-static.downloads.crowdin.com/avatar/16529833/medium/2991a65722acd721849656223014cd49.png" alt="" width="16" height="16" /> Urbestro (Esperanto, Spanish)
|
||||
- <img src="https://crowdin-static.downloads.crowdin.com/avatar/16539171/medium/db6fb87481026c72b895adfb94e17d2c_default.png" alt="" width="16" height="16" /> UsualUsername (Russian)
|
||||
- <img src="https://crowdin-static.downloads.crowdin.com/avatar/14427566/medium/ab733b5044c21867fc5a9d1b22cd2c03.png" alt="" width="16" height="16" /> Vac31. (Lithuanian)
|
||||
- <img src="https://crowdin-static.downloads.crowdin.com/avatar/16026914/medium/e3ca187f354a298ef0c9d02a0ed17be7.jpg" alt="" width="16" height="16" /> valtlai (Finnish)
|
||||
- <img src="https://crowdin-static.downloads.crowdin.com/avatar/15982109/medium/9c03062bdc1d3c6d384dbfead97c26ba.jpeg" alt="" width="16" height="16" /> xabi_itzultzaile (Basque)
|
||||
- <img src="https://crowdin-static.downloads.crowdin.com/avatar/16556017/medium/216e0f7a0c35b079920366939a3aaca7_default.png" alt="" width="16" height="16" /> xen4n (Ukrainian)
|
||||
- <img src="https://crowdin-static.downloads.crowdin.com/avatar/16532657/medium/f309f319266e1ff95f3070eab0c9a9d9_default.png" alt="" width="16" height="16" /> xqueralt (Catalan)
|
||||
- <img src="https://crowdin-static.downloads.crowdin.com/avatar/14041603/medium/6ab77a0467b06aeb49927c6d9c409f89.jpg" alt="" width="16" height="16" /> ZiriSut (Kabyle)
|
||||
- <img src="https://crowdin-static.downloads.crowdin.com/avatar/16530601/medium/e1b6d5c24953b6405405c1ab33c0fa46.jpeg" alt="" width="16" height="16" /> zkreml (Czech)
|
||||
<!-- i18n volunteers end -->
|
||||
|
||||
## Backstory
|
||||
|
||||
I am one of the earliest users of Twitter. Twitter was launched on [15 July 2006](https://en.wikipedia.org/wiki/Twitter). I joined on December 2006 and my [first tweet](https://twitter.com/cheeaun/status/1298723) was posted on 18 December 2006.
|
||||
|
|
345
i18n-volunteers.json
Normal file
345
i18n-volunteers.json
Normal file
|
@ -0,0 +1,345 @@
|
|||
[
|
||||
{
|
||||
"avatarUrl": "https://crowdin-static.downloads.crowdin.com/avatar/12571163/medium/9f3ea938f4243f5ffe2a43f814ddc9e8_default.png",
|
||||
"username": "alidsds11",
|
||||
"languages": [
|
||||
"Arabic"
|
||||
]
|
||||
},
|
||||
{
|
||||
"avatarUrl": "https://crowdin-static.downloads.crowdin.com/avatar/13170041/medium/603136896af17fc005fd592ce3f48717_default.png",
|
||||
"username": "BoFFire",
|
||||
"languages": [
|
||||
"Arabic",
|
||||
"French",
|
||||
"Kabyle"
|
||||
]
|
||||
},
|
||||
{
|
||||
"avatarUrl": "https://crowdin-static.downloads.crowdin.com/avatar/12898464/medium/d3758a76b894bade4bf271c9b32ea69b.png",
|
||||
"username": "Brawaru",
|
||||
"languages": [
|
||||
"Russian"
|
||||
]
|
||||
},
|
||||
{
|
||||
"avatarUrl": "https://crowdin-static.downloads.crowdin.com/avatar/15460040/medium/1cfcfe5f5511b783b5d9f2b968bad819.png",
|
||||
"username": "cbasje",
|
||||
"languages": [
|
||||
"Dutch"
|
||||
]
|
||||
},
|
||||
{
|
||||
"avatarUrl": "https://crowdin-static.downloads.crowdin.com/avatar/15525631/medium/51293156034d0236f1a1020c10f7d539_default.png",
|
||||
"username": "cbo92",
|
||||
"languages": [
|
||||
"French"
|
||||
]
|
||||
},
|
||||
{
|
||||
"avatarUrl": "https://crowdin-static.downloads.crowdin.com/avatar/15910131/medium/67fab7eeab5551853450e76e2ef19e59.jpeg",
|
||||
"username": "CDN",
|
||||
"languages": [
|
||||
"Chinese Simplified"
|
||||
]
|
||||
},
|
||||
{
|
||||
"avatarUrl": "https://crowdin-static.downloads.crowdin.com/avatar/16556801/medium/ed5e501ca1f3cc6525d2da28db646346.jpeg",
|
||||
"username": "dannypsnl",
|
||||
"languages": [
|
||||
"Chinese Traditional"
|
||||
]
|
||||
},
|
||||
{
|
||||
"avatarUrl": "https://crowdin-static.downloads.crowdin.com/avatar/3711/medium/d95ddd44e8dcb3a039f8a3463aed781d_default.png",
|
||||
"username": "databio",
|
||||
"languages": [
|
||||
"Catalan"
|
||||
]
|
||||
},
|
||||
{
|
||||
"avatarUrl": "https://crowdin-static.downloads.crowdin.com/avatar/12618120/medium/ccb11bd042bbf4c7189033f7af2dbd32_default.png",
|
||||
"username": "drydenwu",
|
||||
"languages": [
|
||||
"Chinese Traditional"
|
||||
]
|
||||
},
|
||||
{
|
||||
"avatarUrl": "https://crowdin-static.downloads.crowdin.com/avatar/13557465/medium/8feebf3677fa80c01e8c54c4fbe097e0_default.png",
|
||||
"username": "elissarc",
|
||||
"languages": [
|
||||
"French"
|
||||
]
|
||||
},
|
||||
{
|
||||
"avatarUrl": "https://crowdin-static.downloads.crowdin.com/avatar/16528627/medium/9036f6eced0257f4e1ea4c5bd499de2d_default.png",
|
||||
"username": "ElPamplina",
|
||||
"languages": [
|
||||
"Spanish"
|
||||
]
|
||||
},
|
||||
{
|
||||
"avatarUrl": "https://crowdin-static.downloads.crowdin.com/avatar/14277386/medium/29b30d2c73a214000e3941c9978f49e4_default.png",
|
||||
"username": "Fitik",
|
||||
"languages": [
|
||||
"Esperanto",
|
||||
"Hebrew"
|
||||
]
|
||||
},
|
||||
{
|
||||
"avatarUrl": "https://crowdin-static.downloads.crowdin.com/avatar/14444512/medium/99d0e7a3076deccbdfe0aa0b0612308c.jpeg",
|
||||
"username": "Freeesia",
|
||||
"languages": [
|
||||
"Japanese"
|
||||
]
|
||||
},
|
||||
{
|
||||
"avatarUrl": "https://crowdin-static.downloads.crowdin.com/avatar/12617257/medium/a201650da44fed28890b0e0d8477a663.jpg",
|
||||
"username": "ghose",
|
||||
"languages": [
|
||||
"Galician"
|
||||
]
|
||||
},
|
||||
{
|
||||
"avatarUrl": "https://crowdin-static.downloads.crowdin.com/avatar/15248754/medium/0dac6334ea0f4e8d4194a605c0a5594a.jpeg",
|
||||
"username": "hongminhee",
|
||||
"languages": [
|
||||
"Korean"
|
||||
]
|
||||
},
|
||||
{
|
||||
"avatarUrl": "https://crowdin-static.downloads.crowdin.com/avatar/13454728/medium/1f78b7124b3c962bc4ae55e8d701fc91_default.png",
|
||||
"username": "isard",
|
||||
"languages": [
|
||||
"Catalan"
|
||||
]
|
||||
},
|
||||
{
|
||||
"avatarUrl": "https://crowdin-static.downloads.crowdin.com/avatar/16532403/medium/4cefb19623bcc44d7cdb9e25aebf5250.jpeg",
|
||||
"username": "karlafej",
|
||||
"languages": [
|
||||
"Czech"
|
||||
]
|
||||
},
|
||||
{
|
||||
"avatarUrl": "https://crowdin-static.downloads.crowdin.com/avatar/15791971/medium/88bdda3090339f16f6083390d32bb434_default.png",
|
||||
"username": "katullo11",
|
||||
"languages": [
|
||||
"Italian"
|
||||
]
|
||||
},
|
||||
{
|
||||
"avatarUrl": "https://crowdin-static.downloads.crowdin.com/avatar/14677260/medium/e53420d200961f48602324e18c091bdc.png",
|
||||
"username": "Kytta",
|
||||
"languages": [
|
||||
"German"
|
||||
]
|
||||
},
|
||||
{
|
||||
"avatarUrl": "https://crowdin-static.downloads.crowdin.com/avatar/16529521/medium/ae6add93a901b0fefa2d9b1077920d73.png",
|
||||
"username": "llun",
|
||||
"languages": [
|
||||
"Thai"
|
||||
]
|
||||
},
|
||||
{
|
||||
"avatarUrl": "https://crowdin-static.downloads.crowdin.com/avatar/16291756/medium/e1c4210f15537394cc764b8bc2dffe37.jpg",
|
||||
"username": "lucasofchirst",
|
||||
"languages": [
|
||||
"Occitan",
|
||||
"Portuguese",
|
||||
"Portuguese, Brazilian"
|
||||
]
|
||||
},
|
||||
{
|
||||
"avatarUrl": "https://crowdin-static.downloads.crowdin.com/avatar/16537713/medium/825f0bf1a14fc545a76891a52839d86e_default.png",
|
||||
"username": "marcin.kozinski",
|
||||
"languages": [
|
||||
"Polish"
|
||||
]
|
||||
},
|
||||
{
|
||||
"avatarUrl": "https://crowdin-static.downloads.crowdin.com/avatar/12882812/medium/77744d8db46e9a3e09030e1a02b7a572.jpeg",
|
||||
"username": "mojosoeun",
|
||||
"languages": [
|
||||
"Korean"
|
||||
]
|
||||
},
|
||||
{
|
||||
"avatarUrl": "https://crowdin-static.downloads.crowdin.com/avatar/13613969/medium/c7834ddc0ada84a79671697a944bb274.png",
|
||||
"username": "moreal",
|
||||
"languages": [
|
||||
"Korean"
|
||||
]
|
||||
},
|
||||
{
|
||||
"avatarUrl": "https://crowdin-static.downloads.crowdin.com/avatar/14158861/medium/ba1ff31dc5743b067ea6685f735229a5_default.png",
|
||||
"username": "MrWillCom",
|
||||
"languages": [
|
||||
"Chinese Simplified"
|
||||
]
|
||||
},
|
||||
{
|
||||
"avatarUrl": "https://crowdin-static.downloads.crowdin.com/avatar/15652333/medium/7f36f289f9e2fe41d89ad534a1047f0e.png",
|
||||
"username": "nclm",
|
||||
"languages": [
|
||||
"French"
|
||||
]
|
||||
},
|
||||
{
|
||||
"avatarUrl": "https://crowdin-static.downloads.crowdin.com/avatar/16539461/medium/2f41b9f0b802c1d200a6ab62167a7229_default.png",
|
||||
"username": "pazpi",
|
||||
"languages": [
|
||||
"Italian"
|
||||
]
|
||||
},
|
||||
{
|
||||
"avatarUrl": "https://crowdin-static.downloads.crowdin.com/avatar/15106977/medium/54bf93b19af8bbfdee579ea51685bafa.jpeg",
|
||||
"username": "punkrockgirl",
|
||||
"languages": [
|
||||
"Basque"
|
||||
]
|
||||
},
|
||||
{
|
||||
"avatarUrl": "https://crowdin-static.downloads.crowdin.com/avatar/16536247/medium/f010c8e718a36229733a8b58f6bad2a4_default.png",
|
||||
"username": "radecos",
|
||||
"languages": [
|
||||
"French"
|
||||
]
|
||||
},
|
||||
{
|
||||
"avatarUrl": "https://crowdin-static.downloads.crowdin.com/avatar/16538917/medium/092ec03f56f9dd1cbce94379fa4d4d38.png",
|
||||
"username": "Razem",
|
||||
"languages": [
|
||||
"Czech"
|
||||
]
|
||||
},
|
||||
{
|
||||
"avatarUrl": "https://crowdin-static.downloads.crowdin.com/avatar/14345134/medium/89a299239890c79a1d791d08ec3951dc.png",
|
||||
"username": "realpixelcode",
|
||||
"languages": [
|
||||
"German"
|
||||
]
|
||||
},
|
||||
{
|
||||
"avatarUrl": "https://crowdin-static.downloads.crowdin.com/avatar/16527325/medium/37ebb27e7a50f7f85ae93beafc7028a2.jpg",
|
||||
"username": "rezahosseinzadeh",
|
||||
"languages": [
|
||||
"Persian"
|
||||
]
|
||||
},
|
||||
{
|
||||
"avatarUrl": "https://crowdin-static.downloads.crowdin.com/avatar/13422319/medium/66632a98d73d48e36753d94ebcec9d4f.png",
|
||||
"username": "rwmpelstilzchen",
|
||||
"languages": [
|
||||
"Esperanto",
|
||||
"Hebrew"
|
||||
]
|
||||
},
|
||||
{
|
||||
"avatarUrl": "https://crowdin-static.downloads.crowdin.com/avatar/16538605/medium/bcdb6e3286b7d6237923f3a9383eed29.png",
|
||||
"username": "SadmL",
|
||||
"languages": [
|
||||
"Russian"
|
||||
]
|
||||
},
|
||||
{
|
||||
"avatarUrl": "https://crowdin-static.downloads.crowdin.com/avatar/14565190/medium/79100599131b7776e9803e4b696915a3_default.png",
|
||||
"username": "Sky_NiniKo",
|
||||
"languages": [
|
||||
"French"
|
||||
]
|
||||
},
|
||||
{
|
||||
"avatarUrl": "https://crowdin-static.downloads.crowdin.com/avatar/16532441/medium/1a47e8d80c95636e02d2260f6e233ca5.png",
|
||||
"username": "Su5hicz",
|
||||
"languages": [
|
||||
"Czech"
|
||||
]
|
||||
},
|
||||
{
|
||||
"avatarUrl": "https://crowdin-static.downloads.crowdin.com/avatar/16533843/medium/7314c15492ef90118c33a80a427e6c87_default.png",
|
||||
"username": "Talos00",
|
||||
"languages": [
|
||||
"Italian"
|
||||
]
|
||||
},
|
||||
{
|
||||
"avatarUrl": "https://crowdin-static.downloads.crowdin.com/avatar/16530049/medium/683f3581620c6b4a5c753b416ed695a7.jpeg",
|
||||
"username": "tferrermo",
|
||||
"languages": [
|
||||
"Spanish"
|
||||
]
|
||||
},
|
||||
{
|
||||
"avatarUrl": "https://crowdin-static.downloads.crowdin.com/avatar/16527851/medium/649e5a9a8a8cc61ced670d89e9cca082.png",
|
||||
"username": "tux93",
|
||||
"languages": [
|
||||
"German"
|
||||
]
|
||||
},
|
||||
{
|
||||
"avatarUrl": "https://crowdin-static.downloads.crowdin.com/avatar/16529833/medium/2991a65722acd721849656223014cd49.png",
|
||||
"username": "Urbestro",
|
||||
"languages": [
|
||||
"Esperanto",
|
||||
"Spanish"
|
||||
]
|
||||
},
|
||||
{
|
||||
"avatarUrl": "https://crowdin-static.downloads.crowdin.com/avatar/16539171/medium/db6fb87481026c72b895adfb94e17d2c_default.png",
|
||||
"username": "UsualUsername",
|
||||
"languages": [
|
||||
"Russian"
|
||||
]
|
||||
},
|
||||
{
|
||||
"avatarUrl": "https://crowdin-static.downloads.crowdin.com/avatar/14427566/medium/ab733b5044c21867fc5a9d1b22cd2c03.png",
|
||||
"username": "Vac31.",
|
||||
"languages": [
|
||||
"Lithuanian"
|
||||
]
|
||||
},
|
||||
{
|
||||
"avatarUrl": "https://crowdin-static.downloads.crowdin.com/avatar/16026914/medium/e3ca187f354a298ef0c9d02a0ed17be7.jpg",
|
||||
"username": "valtlai",
|
||||
"languages": [
|
||||
"Finnish"
|
||||
]
|
||||
},
|
||||
{
|
||||
"avatarUrl": "https://crowdin-static.downloads.crowdin.com/avatar/15982109/medium/9c03062bdc1d3c6d384dbfead97c26ba.jpeg",
|
||||
"username": "xabi_itzultzaile",
|
||||
"languages": [
|
||||
"Basque"
|
||||
]
|
||||
},
|
||||
{
|
||||
"avatarUrl": "https://crowdin-static.downloads.crowdin.com/avatar/16556017/medium/216e0f7a0c35b079920366939a3aaca7_default.png",
|
||||
"username": "xen4n",
|
||||
"languages": [
|
||||
"Ukrainian"
|
||||
]
|
||||
},
|
||||
{
|
||||
"avatarUrl": "https://crowdin-static.downloads.crowdin.com/avatar/16532657/medium/f309f319266e1ff95f3070eab0c9a9d9_default.png",
|
||||
"username": "xqueralt",
|
||||
"languages": [
|
||||
"Catalan"
|
||||
]
|
||||
},
|
||||
{
|
||||
"avatarUrl": "https://crowdin-static.downloads.crowdin.com/avatar/14041603/medium/6ab77a0467b06aeb49927c6d9c409f89.jpg",
|
||||
"username": "ZiriSut",
|
||||
"languages": [
|
||||
"Kabyle"
|
||||
]
|
||||
},
|
||||
{
|
||||
"avatarUrl": "https://crowdin-static.downloads.crowdin.com/avatar/16530601/medium/e1b6d5c24953b6405405c1ab33c0fa46.jpeg",
|
||||
"username": "zkreml",
|
||||
"languages": [
|
||||
"Czech"
|
||||
]
|
||||
}
|
||||
]
|
|
@ -1,7 +1,8 @@
|
|||
import { LOCALES } from './src/locales';
|
||||
import { ALL_LOCALES } from './src/locales';
|
||||
|
||||
const config = {
|
||||
locales: LOCALES,
|
||||
locales: ALL_LOCALES,
|
||||
sourceLocale: 'en',
|
||||
pseudoLocale: 'pseudo-LOCALE',
|
||||
fallbackLocales: {
|
||||
default: 'en',
|
||||
|
|
545
package-lock.json
generated
545
package-lock.json
generated
File diff suppressed because it is too large
Load diff
31
package.json
31
package.json
|
@ -11,7 +11,9 @@
|
|||
"bundle-visualizer": "npx vite-bundle-visualizer",
|
||||
"messages:extract": "lingui extract",
|
||||
"messages:extract:clean": "lingui extract --locale en --clean",
|
||||
"messages:compile": "lingui compile"
|
||||
"messages:compile": "lingui compile",
|
||||
"fetch-i18n-volunteers": "env $(cat .env.local | grep -v \"#\" | xargs) node scripts/fetch-i18n-volunteers.js",
|
||||
"readme:i18n-volunteers": "node scripts/update-i18n-volunteers-readme.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"@formatjs/intl-localematcher": "~0.5.4",
|
||||
|
@ -20,9 +22,9 @@
|
|||
"@github/text-expander-element": "~2.7.1",
|
||||
"@iconify-icons/mingcute": "~1.2.9",
|
||||
"@justinribeiro/lite-youtube": "~1.5.0",
|
||||
"@lingui/detect-locale": "~4.11.3",
|
||||
"@lingui/macro": "~4.11.3",
|
||||
"@lingui/react": "~4.11.3",
|
||||
"@lingui/detect-locale": "~4.11.4",
|
||||
"@lingui/macro": "~4.11.4",
|
||||
"@lingui/react": "~4.11.4",
|
||||
"@szhsin/react-menu": "~4.2.2",
|
||||
"compare-versions": "~6.1.1",
|
||||
"fast-blurhash": "~1.1.4",
|
||||
|
@ -31,6 +33,7 @@
|
|||
"html-prettify": "~1.0.7",
|
||||
"idb-keyval": "~6.2.1",
|
||||
"intl-locale-textinfo-polyfill": "~2.1.1",
|
||||
"js-cookie": "~3.0.5",
|
||||
"just-debounce-it": "~3.2.0",
|
||||
"lz-string": "~1.5.0",
|
||||
"masto": "~6.8.0",
|
||||
|
@ -39,7 +42,7 @@
|
|||
"p-throttle": "~6.2.0",
|
||||
"preact": "~10.23.2",
|
||||
"punycode": "~2.3.1",
|
||||
"react-hotkeys-hook": "~4.5.0",
|
||||
"react-hotkeys-hook": "~4.5.1",
|
||||
"react-intersection-observer": "~9.13.0",
|
||||
"react-quick-pinch-zoom": "~5.1.0",
|
||||
"react-router-dom": "6.6.2",
|
||||
|
@ -48,25 +51,25 @@
|
|||
"tinyld": "~1.3.4",
|
||||
"toastify-js": "~1.12.0",
|
||||
"uid": "~2.0.2",
|
||||
"use-debounce": "~10.0.2",
|
||||
"use-debounce": "~10.0.3",
|
||||
"use-long-press": "~3.2.0",
|
||||
"use-resize-observer": "~9.1.0",
|
||||
"valtio": "1.13.2"
|
||||
"valtio": "2.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@ianvs/prettier-plugin-sort-imports": "~4.3.1",
|
||||
"@lingui/cli": "~4.11.3",
|
||||
"@lingui/vite-plugin": "~4.11.3",
|
||||
"@lingui/cli": "~4.11.4",
|
||||
"@lingui/vite-plugin": "~4.11.4",
|
||||
"@preact/preset-vite": "~2.9.0",
|
||||
"babel-plugin-macros": "~3.1.0",
|
||||
"postcss": "~8.4.41",
|
||||
"postcss": "~8.4.45",
|
||||
"postcss-dark-theme-class": "~1.3.0",
|
||||
"postcss-preset-env": "~10.0.1",
|
||||
"postcss-preset-env": "~10.0.2",
|
||||
"twitter-text": "~3.1.0",
|
||||
"vite": "~5.4.1",
|
||||
"vite": "~5.4.3",
|
||||
"vite-plugin-generate-file": "~0.2.0",
|
||||
"vite-plugin-html-config": "~1.0.11",
|
||||
"vite-plugin-pwa": "~0.20.1",
|
||||
"vite-plugin-html-config": "~2.0.2",
|
||||
"vite-plugin-pwa": "~0.20.3",
|
||||
"vite-plugin-remove-console": "~2.2.0",
|
||||
"vite-plugin-run": "~0.5.2",
|
||||
"workbox-cacheable-response": "~7.1.0",
|
||||
|
|
|
@ -73,18 +73,21 @@ function IDN(inputCode, outputCode) {
|
|||
return result;
|
||||
}
|
||||
|
||||
// Sort by percentage
|
||||
const sortedCatalogs = Object.entries(catalogs)
|
||||
.sort((a, b) => b[1] - a[1])
|
||||
const fullCatalogs = Object.entries(catalogs)
|
||||
// sort by key
|
||||
.sort((a, b) => a[0].localeCompare(b[0]))
|
||||
.map(([code, completion]) => {
|
||||
const nativeName = IDN(code, code);
|
||||
const name = IDN('en', code);
|
||||
// let names = {};
|
||||
return { code, nativeName, name, completion };
|
||||
});
|
||||
|
||||
// Sort by completion
|
||||
const sortedCatalogs = [...fullCatalogs].sort(
|
||||
(a, b) => b.completion - a.completion,
|
||||
);
|
||||
console.table(sortedCatalogs);
|
||||
|
||||
const path = 'src/data/catalogs.json';
|
||||
fs.writeFileSync(path, JSON.stringify(sortedCatalogs, null, 2));
|
||||
fs.writeFileSync(path, JSON.stringify(fullCatalogs, null, 2));
|
||||
console.log('File written:', path);
|
||||
|
|
131
scripts/fetch-i18n-volunteers.js
Normal file
131
scripts/fetch-i18n-volunteers.js
Normal file
|
@ -0,0 +1,131 @@
|
|||
import fs from 'fs';
|
||||
|
||||
const { CROWDIN_ACCESS_TOKEN } = process.env;
|
||||
|
||||
const PROJECT_ID = '703337';
|
||||
|
||||
if (!CROWDIN_ACCESS_TOKEN) {
|
||||
throw new Error('CROWDIN_ACCESS_TOKEN is not set');
|
||||
}
|
||||
|
||||
// Generate Report
|
||||
|
||||
let REPORT_ID = null;
|
||||
{
|
||||
const response = await fetch(
|
||||
`https://api.crowdin.com/api/v2/projects/${PROJECT_ID}/reports`,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${CROWDIN_ACCESS_TOKEN}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
name: 'top-members',
|
||||
schema: {
|
||||
format: 'json',
|
||||
},
|
||||
}),
|
||||
},
|
||||
);
|
||||
const json = await response.json();
|
||||
console.log(`Report ID: ${json?.data?.identifier}`);
|
||||
REPORT_ID = json?.data?.identifier;
|
||||
}
|
||||
|
||||
if (!REPORT_ID) {
|
||||
throw new Error('Report ID is not found');
|
||||
}
|
||||
|
||||
// Check Report Generation Status
|
||||
let finished = false;
|
||||
{
|
||||
let maxPolls = 10;
|
||||
do {
|
||||
maxPolls--;
|
||||
if (maxPolls < 0) break;
|
||||
|
||||
// Wait for 1 second
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||
|
||||
const status = await fetch(
|
||||
`https://api.crowdin.com/api/v2/projects/${PROJECT_ID}/reports/${REPORT_ID}`,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${CROWDIN_ACCESS_TOKEN}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
},
|
||||
);
|
||||
const json = await status.json();
|
||||
const progress = json?.data?.progress;
|
||||
console.log(`Progress: ${progress}% (${maxPolls} retries left)`);
|
||||
finished = json?.data?.status === 'finished';
|
||||
} while (!finished);
|
||||
}
|
||||
|
||||
if (!finished) {
|
||||
throw new Error('Failed to generate report');
|
||||
}
|
||||
|
||||
// Download Report
|
||||
let reportURL = null;
|
||||
{
|
||||
const response = await fetch(
|
||||
`https://api.crowdin.com/api/v2/projects/${PROJECT_ID}/reports/${REPORT_ID}/download`,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${CROWDIN_ACCESS_TOKEN}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
},
|
||||
);
|
||||
const json = await response.json();
|
||||
reportURL = json?.data?.url;
|
||||
console.log(`Report URL: ${reportURL}`);
|
||||
}
|
||||
|
||||
if (!reportURL) {
|
||||
throw new Error('Report URL is not found');
|
||||
}
|
||||
|
||||
// Actually download the report
|
||||
let members = null;
|
||||
{
|
||||
const response = await fetch(reportURL);
|
||||
const json = await response.json();
|
||||
|
||||
const { data } = json;
|
||||
|
||||
if (!data?.length) {
|
||||
throw new Error('No data found');
|
||||
}
|
||||
|
||||
// Sort by 'user.fullName'
|
||||
data.sort((a, b) => a.user.username.localeCompare(b.user.username));
|
||||
members = data
|
||||
.filter((item) => {
|
||||
const isMyself = item.user.username === 'cheeaun';
|
||||
const translatedMoreThanZero = item.translated > 0;
|
||||
|
||||
return !isMyself && translatedMoreThanZero;
|
||||
})
|
||||
.map((item) => ({
|
||||
avatarUrl: item.user.avatarUrl,
|
||||
username: item.user.username,
|
||||
languages: item.languages.map((lang) => lang.name),
|
||||
}));
|
||||
|
||||
console.log(members);
|
||||
|
||||
if (members?.length) {
|
||||
fs.writeFileSync(
|
||||
'i18n-volunteers.json',
|
||||
JSON.stringify(members, null, '\t'),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (!members?.length) {
|
||||
throw new Error('No members found');
|
||||
}
|
27
scripts/update-i18n-volunteers-readme.js
Normal file
27
scripts/update-i18n-volunteers-readme.js
Normal file
|
@ -0,0 +1,27 @@
|
|||
// Find for <!-- i18n volunteers start --><!-- i18n volunteers end --> and inject list of i18n volunteers in between
|
||||
|
||||
import fs from 'fs';
|
||||
|
||||
const i18nVolunteers = JSON.parse(fs.readFileSync('i18n-volunteers.json'));
|
||||
|
||||
const readme = fs.readFileSync('README.md', 'utf8');
|
||||
|
||||
const i18nVolunteersStart = '<!-- i18n volunteers start -->';
|
||||
const i18nVolunteersEnd = '<!-- i18n volunteers end -->';
|
||||
|
||||
const i18nVolunteersList = i18nVolunteers
|
||||
.map((member) => {
|
||||
return `- <img src="${member.avatarUrl}" alt="" width="16" height="16" /> ${
|
||||
member.username
|
||||
} (${member.languages.join(', ')})`;
|
||||
})
|
||||
.join('\n');
|
||||
|
||||
const readmeUpdated = readme.replace(
|
||||
new RegExp(`${i18nVolunteersStart}.*${i18nVolunteersEnd}`, 's'),
|
||||
`${i18nVolunteersStart}\n${i18nVolunteersList}\n${i18nVolunteersEnd}`,
|
||||
);
|
||||
|
||||
fs.writeFileSync('README.md', readmeUpdated);
|
||||
|
||||
console.log('Updated README.md');
|
58
src/app.css
58
src/app.css
|
@ -1810,16 +1810,17 @@ body > .szh-menu-container {
|
|||
env(safe-area-inset-bottom) env(safe-area-inset-left);
|
||||
}
|
||||
.szh-menu {
|
||||
padding: 8px 0;
|
||||
padding: 4px 0;
|
||||
margin: 0;
|
||||
font-size: var(--text-size);
|
||||
background-color: var(--bg-color);
|
||||
border: 1px solid var(--outline-color);
|
||||
border: 1px solid var(--outline-stronger-color);
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 3px 16px -3px var(--drop-shadow-color);
|
||||
box-shadow: 0 3px 8px var(--drop-shadow-color),
|
||||
0 6px 32px -6px var(--drop-shadow-color);
|
||||
text-align: start;
|
||||
/* animation: appear-smooth 0.15s ease-in-out; */
|
||||
width: 16em;
|
||||
min-width: 16em;
|
||||
max-width: 90vw;
|
||||
/* overflow: hidden; */
|
||||
}
|
||||
|
@ -1874,13 +1875,16 @@ body > .szh-menu-container {
|
|||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
line-height: 1.1;
|
||||
line-height: 1.3;
|
||||
padding: 8px 16px !important;
|
||||
/* transition: all 0.1s ease-in-out; */
|
||||
text-decoration: none;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
|
||||
--menu-item-bg-inset: 0 4px;
|
||||
--menu-item-bg-color: var(--button-bg-color);
|
||||
}
|
||||
.szh-menu .szh-menu__item--focusable {
|
||||
background-color: transparent;
|
||||
|
@ -1917,9 +1921,30 @@ body > .szh-menu-container {
|
|||
.szh-menu__item:not(.szh-menu__item--disabled, .szh-menu__item--hover) {
|
||||
color: var(--text-color);
|
||||
}
|
||||
.szh-menu .szh-menu__item:not(.menu-field) {
|
||||
position: relative;
|
||||
& > * {
|
||||
/* z-index: 1; */
|
||||
}
|
||||
|
||||
&:before {
|
||||
content: '';
|
||||
background-color: var(--menu-item-bg-color);
|
||||
position: absolute;
|
||||
inset: var(--menu-item-bg-inset);
|
||||
border-radius: 4px;
|
||||
z-index: -1;
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
.szh-menu .szh-menu__item--hover:not(.menu-field) {
|
||||
color: var(--button-text-color);
|
||||
background-color: var(--button-bg-color);
|
||||
/* background-color: var(--button-bg-color); */
|
||||
background-color: transparent;
|
||||
|
||||
&:before {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
.szh-menu__divider {
|
||||
background-color: var(--divider-color);
|
||||
|
@ -1995,10 +2020,12 @@ body > .szh-menu-container {
|
|||
}
|
||||
.szh-menu
|
||||
.szh-menu__item.danger:not(.szh-menu__item--disabled).szh-menu__item--hover {
|
||||
background-color: var(--red-text-color);
|
||||
/* background-color: var(--red-text-color); */
|
||||
--menu-item-bg-color: var(--red-text-color);
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
background-color: var(--red-color);
|
||||
/* background-color: var(--red-color); */
|
||||
--menu-item-bg-color: var(--red-color);
|
||||
}
|
||||
}
|
||||
.szh-menu
|
||||
|
@ -2038,12 +2065,20 @@ body > .szh-menu-container {
|
|||
);
|
||||
}
|
||||
}
|
||||
|
||||
&:before {
|
||||
content: '';
|
||||
}
|
||||
}
|
||||
|
||||
.szh-menu__item--hover {
|
||||
background-color: var(--menu-item-bg-color);
|
||||
}
|
||||
}
|
||||
|
||||
.menu-control-group-horizontal:first-child,
|
||||
li[role='none'] + .menu-control-group-horizontal {
|
||||
margin-top: -8px;
|
||||
margin-top: -4px;
|
||||
margin-bottom: -4px;
|
||||
|
||||
.szh-menu__item {
|
||||
|
@ -2078,6 +2113,8 @@ body > .szh-menu-container {
|
|||
}
|
||||
|
||||
.szh-menu .menu-wrap {
|
||||
min-width: 16em;
|
||||
width: min-content;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
@ -2092,11 +2129,10 @@ body > .szh-menu-container {
|
|||
background-color: var(--bg-blur-color);
|
||||
backdrop-filter: blur(8px) saturate(3);
|
||||
border: var(--hairline-width) solid var(--bg-color);
|
||||
box-shadow: 0 3px 8px -1px var(--drop-shadow-color);
|
||||
text-shadow: 0 var(--hairline-width) var(--bg-color), 0 0 8px var(--bg-color);
|
||||
}
|
||||
.glass-menu .szh-menu__item--hover {
|
||||
background-color: var(--button-bg-blur-color);
|
||||
/* background-color: var(--button-bg-blur-color); */
|
||||
text-shadow: none;
|
||||
}
|
||||
|
||||
|
|
62
src/app.jsx
62
src/app.jsx
|
@ -56,7 +56,11 @@ import { getAccessToken } from './utils/auth';
|
|||
import focusDeck from './utils/focus-deck';
|
||||
import states, { initStates, statusKey } from './utils/states';
|
||||
import store from './utils/store';
|
||||
import { getCurrentAccount, setCurrentAccountID } from './utils/store-utils';
|
||||
import {
|
||||
getAccount,
|
||||
getCurrentAccount,
|
||||
setCurrentAccountID,
|
||||
} from './utils/store-utils';
|
||||
|
||||
import './utils/toast-alert';
|
||||
|
||||
|
@ -317,9 +321,10 @@ function App() {
|
|||
window.location.pathname || '/',
|
||||
);
|
||||
|
||||
const clientID = store.session.get('clientID');
|
||||
const clientSecret = store.session.get('clientSecret');
|
||||
const vapidKey = store.session.get('vapidKey');
|
||||
const clientID = store.sessionCookie.get('clientID');
|
||||
const clientSecret = store.sessionCookie.get('clientSecret');
|
||||
const vapidKey = store.sessionCookie.get('vapidKey');
|
||||
const verifier = store.sessionCookie.get('codeVerifier');
|
||||
|
||||
(async () => {
|
||||
setUIState('loading');
|
||||
|
@ -328,22 +333,46 @@ function App() {
|
|||
client_id: clientID,
|
||||
client_secret: clientSecret,
|
||||
code,
|
||||
code_verifier: verifier || undefined,
|
||||
});
|
||||
|
||||
const client = initClient({ instance: instanceURL, accessToken });
|
||||
await Promise.allSettled([
|
||||
initPreferences(client),
|
||||
initInstance(client, instanceURL),
|
||||
initAccount(client, instanceURL, accessToken, vapidKey),
|
||||
]);
|
||||
initStates();
|
||||
if (accessToken) {
|
||||
const client = initClient({ instance: instanceURL, accessToken });
|
||||
await Promise.allSettled([
|
||||
initPreferences(client),
|
||||
initInstance(client, instanceURL),
|
||||
initAccount(client, instanceURL, accessToken, vapidKey),
|
||||
]);
|
||||
initStates();
|
||||
window.__IGNORE_GET_ACCOUNT_ERROR__ = true;
|
||||
|
||||
setIsLoggedIn(true);
|
||||
setUIState('default');
|
||||
setIsLoggedIn(true);
|
||||
setUIState('default');
|
||||
} else {
|
||||
setUIState('error');
|
||||
}
|
||||
})();
|
||||
} else {
|
||||
window.__IGNORE_GET_ACCOUNT_ERROR__ = true;
|
||||
const account = getCurrentAccount();
|
||||
const searchAccount = decodeURIComponent(
|
||||
(window.location.search.match(/account=([^&]+)/) || [, ''])[1],
|
||||
);
|
||||
let account;
|
||||
if (searchAccount) {
|
||||
account = getAccount(searchAccount);
|
||||
console.log('searchAccount', searchAccount, account);
|
||||
if (account) {
|
||||
setCurrentAccountID(account.info.id);
|
||||
window.history.replaceState(
|
||||
{},
|
||||
document.title,
|
||||
window.location.pathname || '/',
|
||||
);
|
||||
}
|
||||
}
|
||||
if (!account) {
|
||||
account = getCurrentAccount();
|
||||
}
|
||||
if (account) {
|
||||
setCurrentAccountID(account.info.id);
|
||||
const { client } = api({ account });
|
||||
|
@ -365,6 +394,11 @@ function App() {
|
|||
setUIState('default');
|
||||
}
|
||||
}
|
||||
|
||||
// Cleanup
|
||||
store.sessionCookie.del('clientID');
|
||||
store.sessionCookie.del('clientSecret');
|
||||
store.sessionCookie.del('codeVerifier');
|
||||
}, []);
|
||||
|
||||
let location = useLocation();
|
||||
|
|
|
@ -63,7 +63,6 @@ const MUTE_DURATIONS_LABELS = {
|
|||
259_200: i18nDuration(3, 'day'),
|
||||
604_800: i18nDuration(1, 'week'),
|
||||
};
|
||||
console.log({ MUTE_DURATIONS_LABELS });
|
||||
|
||||
const LIMIT = 80;
|
||||
|
||||
|
|
|
@ -16,7 +16,18 @@ export default memo(function BackgroundService({ isLoggedIn }) {
|
|||
// Notifications service
|
||||
// - WebSocket to receive notifications when page is visible
|
||||
const [visible, setVisible] = useState(true);
|
||||
usePageVisibility(setVisible);
|
||||
const visibleTimeout = useRef();
|
||||
usePageVisibility((visible) => {
|
||||
clearTimeout(visibleTimeout.current);
|
||||
if (visible) {
|
||||
setVisible(true);
|
||||
} else {
|
||||
visibleTimeout.current = setTimeout(() => {
|
||||
setVisible(false);
|
||||
}, POLL_INTERVAL);
|
||||
}
|
||||
});
|
||||
|
||||
const checkLatestNotification = async (masto, instance, skipCheckMarkers) => {
|
||||
if (states.notificationsLast) {
|
||||
const notificationsIterator = masto.v1.notifications.list({
|
||||
|
|
|
@ -1154,7 +1154,7 @@ function Compose({
|
|||
class={`toolbar-button ${
|
||||
visibility !== 'public' && !sensitive ? 'show-field' : ''
|
||||
} ${visibility !== 'public' ? 'highlight' : ''}`}
|
||||
title={`Visibility: ${visibility}`}
|
||||
title={visibility}
|
||||
>
|
||||
<Icon icon={visibilityIconsMap[visibility]} alt={visibility} />
|
||||
<select
|
||||
|
@ -1471,7 +1471,14 @@ function Compose({
|
|||
class="large"
|
||||
disabled={uiState === 'loading'}
|
||||
>
|
||||
{replyToStatus ? t`Reply` : editStatus ? t`Update` : t`Post`}
|
||||
{replyToStatus
|
||||
? t`Reply`
|
||||
: editStatus
|
||||
? t`Update`
|
||||
: t({
|
||||
message: 'Post',
|
||||
context: 'Submit button in composer',
|
||||
})}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
|
|
@ -1,13 +1,15 @@
|
|||
import { useLingui } from '@lingui/react';
|
||||
import { useMemo } from 'preact/hooks';
|
||||
|
||||
import { CATALOGS, DEFAULT_LANG, LOCALES } from '../locales';
|
||||
import { CATALOGS, DEFAULT_LANG, DEV_LOCALES, LOCALES } from '../locales';
|
||||
import { activateLang } from '../utils/lang';
|
||||
import localeCode2Text from '../utils/localeCode2Text';
|
||||
import store from '../utils/store';
|
||||
|
||||
const regionMaps = {
|
||||
'zh-CN': 'zh-Hans',
|
||||
'zh-TW': 'zh-Hant',
|
||||
'pt-BR': 'pt-BR',
|
||||
};
|
||||
|
||||
export default function LangSelector() {
|
||||
|
@ -16,10 +18,6 @@ export default function LangSelector() {
|
|||
// Sorted on render, so the order won't suddenly change based on current locale
|
||||
const populatedLocales = useMemo(() => {
|
||||
return LOCALES.map((lang) => {
|
||||
if (lang === 'pseudo-LOCALE') {
|
||||
return { code: lang, native: 'Pseudolocalization (test)' };
|
||||
}
|
||||
|
||||
// Don't need regions for now, it makes text too noisy
|
||||
// Wait till there's too many languages and there are regional clashes
|
||||
const regionlessCode = regionMaps[lang] || lang.replace(/-[a-z]+$/i, '');
|
||||
|
@ -45,9 +43,6 @@ export default function LangSelector() {
|
|||
native,
|
||||
};
|
||||
}).sort((a, b) => {
|
||||
// If pseudo-LOCALE, always put it at the bottom
|
||||
if (a.code === 'pseudo-LOCALE') return 1;
|
||||
if (b.code === 'pseudo-LOCALE') return -1;
|
||||
// Sort by common name
|
||||
const order = a._common.localeCompare(b._common, i18n.locale);
|
||||
if (order !== 0) return order;
|
||||
|
@ -65,21 +60,11 @@ export default function LangSelector() {
|
|||
class="small"
|
||||
value={i18n.locale || DEFAULT_LANG}
|
||||
onChange={(e) => {
|
||||
localStorage.setItem('lang', e.target.value);
|
||||
store.local.set('lang', e.target.value);
|
||||
activateLang(e.target.value);
|
||||
}}
|
||||
>
|
||||
{populatedLocales.map(({ code, regionlessCode, native }) => {
|
||||
if (code === 'pseudo-LOCALE') {
|
||||
return (
|
||||
<>
|
||||
<hr />
|
||||
<option value={code} key={code}>
|
||||
{native}
|
||||
</option>
|
||||
</>
|
||||
);
|
||||
}
|
||||
// Common name changes based on current locale
|
||||
const common = localeCode2Text({
|
||||
code: regionlessCode,
|
||||
|
@ -97,6 +82,33 @@ export default function LangSelector() {
|
|||
</option>
|
||||
);
|
||||
})}
|
||||
{(import.meta.env.DEV || import.meta.env.PHANPY_SHOW_DEV_LOCALES) && (
|
||||
<optgroup label="🚧 Development (<50% translated)">
|
||||
{DEV_LOCALES.map((code) => {
|
||||
if (code === 'pseudo-LOCALE') {
|
||||
return (
|
||||
<>
|
||||
<hr />
|
||||
<option value={code} key={code}>
|
||||
Pseudolocalization (test)
|
||||
</option>
|
||||
</>
|
||||
);
|
||||
}
|
||||
const nativeName = CATALOGS.find(
|
||||
(c) => c.code === code,
|
||||
)?.nativeName;
|
||||
const completion = CATALOGS.find(
|
||||
(c) => c.code === code,
|
||||
)?.completion;
|
||||
return (
|
||||
<option value={code} key={code}>
|
||||
{nativeName || code} ‎[{completion}%]
|
||||
</option>
|
||||
);
|
||||
})}
|
||||
</optgroup>
|
||||
)}
|
||||
</select>
|
||||
</label>
|
||||
);
|
||||
|
|
|
@ -1,7 +1,18 @@
|
|||
.nav-menu section:last-child {
|
||||
background-color: var(--bg-faded-color);
|
||||
margin-bottom: -8px;
|
||||
padding-bottom: 8px;
|
||||
.nav-menu {
|
||||
overflow: hidden;
|
||||
|
||||
section:last-child {
|
||||
background-color: var(--bg-faded-color);
|
||||
margin-bottom: -4px;
|
||||
padding-bottom: 4px;
|
||||
|
||||
.szh-menu__item:before {
|
||||
z-index: 0;
|
||||
}
|
||||
.szh-menu__item > * {
|
||||
z-index: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 23em) {
|
||||
|
@ -13,16 +24,16 @@
|
|||
'top top'
|
||||
'left right';
|
||||
padding: 0;
|
||||
width: 22em;
|
||||
/* min-width: 22em; */
|
||||
max-width: calc(100vw - 16px);
|
||||
}
|
||||
.nav-menu .top-menu {
|
||||
grid-area: top;
|
||||
padding-top: 8px;
|
||||
margin-bottom: -8px;
|
||||
padding-top: 4px;
|
||||
margin-bottom: -4px;
|
||||
}
|
||||
.nav-menu section {
|
||||
padding: 8px 0;
|
||||
padding: 4px 0;
|
||||
/* width: 50%; */
|
||||
}
|
||||
@keyframes phanpying {
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { Plural, t, Trans } from '@lingui/macro';
|
||||
import { Plural, plural, t, Trans } from '@lingui/macro';
|
||||
import { useState } from 'preact/hooks';
|
||||
|
||||
import shortenNumber from '../utils/shorten-number';
|
||||
|
@ -113,9 +113,10 @@ export default function Poll({
|
|||
</div>
|
||||
<div
|
||||
class="poll-option-votes"
|
||||
title={`${optionVotesCount} vote${
|
||||
optionVotesCount === 1 ? '' : 's'
|
||||
}`}
|
||||
title={plural(optionVotesCount, {
|
||||
one: `# vote`,
|
||||
other: `# votes`,
|
||||
})}
|
||||
>
|
||||
{percentage}
|
||||
</div>
|
||||
|
|
|
@ -16,7 +16,8 @@ function isValidDate(value) {
|
|||
|
||||
const resolvedLocale = new Intl.DateTimeFormat().resolvedOptions().locale;
|
||||
const DTF = mem((locale, opts = {}) => {
|
||||
const lang = localeMatch([locale], [resolvedLocale]);
|
||||
const regionlessLocale = locale.replace(/-[a-z]+$/i, '');
|
||||
const lang = localeMatch([regionlessLocale], [resolvedLocale], locale);
|
||||
try {
|
||||
return new Intl.DateTimeFormat(lang, opts);
|
||||
} catch (e) {}
|
||||
|
|
|
@ -121,13 +121,31 @@
|
|||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
}
|
||||
#shortcuts .tab-bar li a {
|
||||
position: relative;
|
||||
&:before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: 4px 0;
|
||||
border-radius: 8px;
|
||||
background-color: var(--bg-color);
|
||||
z-index: -1;
|
||||
transform: scale(0.5);
|
||||
opacity: 0;
|
||||
transition: all 0.1s ease-in-out;
|
||||
}
|
||||
}
|
||||
#shortcuts .tab-bar li a.is-active {
|
||||
color: var(--link-color);
|
||||
background-image: radial-gradient(
|
||||
/* background-image: radial-gradient(
|
||||
closest-side at 50% 50%,
|
||||
var(--bg-color),
|
||||
transparent
|
||||
);
|
||||
); */
|
||||
&:before {
|
||||
transform: scale(1);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
#app:has(#home-page):not(:has(#home-page ~ .deck-container)):has(header[hidden])
|
||||
#shortcuts
|
||||
|
|
|
@ -430,6 +430,7 @@
|
|||
|
||||
> span + span {
|
||||
position: static;
|
||||
width: auto;
|
||||
|
||||
&:empty {
|
||||
display: none;
|
||||
|
@ -1895,6 +1896,7 @@ a:focus-visible .card img {
|
|||
.meta-container {
|
||||
align-self: flex-start;
|
||||
flex-grow: 0;
|
||||
max-width: 100%;
|
||||
}
|
||||
.card .title {
|
||||
line-height: 1.25;
|
||||
|
|
|
@ -1061,7 +1061,14 @@ function Status({
|
|||
)}
|
||||
<MenuItem href={url} target="_blank">
|
||||
<Icon icon="external" />
|
||||
<small class="menu-double-lines">{nicePostURL(url)}</small>
|
||||
<small
|
||||
class="menu-double-lines"
|
||||
style={{
|
||||
maxWidth: '16em',
|
||||
}}
|
||||
>
|
||||
{nicePostURL(url)}
|
||||
</small>
|
||||
</MenuItem>
|
||||
<div class="menu-horizontal">
|
||||
<MenuItem
|
||||
|
@ -2653,7 +2660,12 @@ function Card({ card, selfReferential, instance }) {
|
|||
const imageData = ctx.createImageData(w, h);
|
||||
imageData.data.set(blurhashPixels);
|
||||
ctx.putImageData(imageData, 0, 0);
|
||||
blurhashImage = canvas.toDataURL();
|
||||
if (window.OffscreenCanvas) {
|
||||
const blob = canvas.convertToBlob();
|
||||
blurhashImage = URL.createObjectURL(blob);
|
||||
} else {
|
||||
blurhashImage = canvas.toDataURL();
|
||||
}
|
||||
}
|
||||
|
||||
const isPost = isCardPost(domain);
|
||||
|
|
|
@ -15,6 +15,7 @@ import { useSnapshot } from 'valtio';
|
|||
import FilterContext from '../utils/filter-context';
|
||||
import { filteredItems, isFiltered } from '../utils/filters';
|
||||
import isRTL from '../utils/is-rtl';
|
||||
import showToast from '../utils/show-toast';
|
||||
import states, { statusKey } from '../utils/states';
|
||||
import statusPeek from '../utils/status-peek';
|
||||
import { isMediaFirstInstance } from '../utils/store-utils';
|
||||
|
@ -121,6 +122,9 @@ function Timeline({
|
|||
} catch (e) {
|
||||
console.error(e);
|
||||
setUIState('error');
|
||||
if (firstLoad && !items.length && errorText) {
|
||||
showToast(errorText);
|
||||
}
|
||||
} finally {
|
||||
loadItems.cancel();
|
||||
}
|
||||
|
|
|
@ -9,8 +9,10 @@ import { render } from 'preact';
|
|||
import { useEffect, useState } from 'preact/hooks';
|
||||
|
||||
import ComposeSuspense from './components/compose-suspense';
|
||||
import Loader from './components/loader';
|
||||
import { initActivateLang } from './utils/lang';
|
||||
import { initStates } from './utils/states';
|
||||
import { getCurrentAccount, setCurrentAccountID } from './utils/store-utils';
|
||||
import useTitle from './utils/useTitle';
|
||||
|
||||
initActivateLang();
|
||||
|
@ -21,6 +23,7 @@ if (window.opener) {
|
|||
|
||||
function App() {
|
||||
const [uiState, setUIState] = useState('default');
|
||||
const [isLoggedIn, setIsLoggedIn] = useState(null);
|
||||
|
||||
const { editStatus, replyToStatus, draftStatus } = window.__COMPOSE__ || {};
|
||||
|
||||
|
@ -35,7 +38,11 @@ function App() {
|
|||
);
|
||||
|
||||
useEffect(() => {
|
||||
initStates();
|
||||
const account = getCurrentAccount();
|
||||
setIsLoggedIn(!!account);
|
||||
if (account) {
|
||||
initStates();
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
|
@ -69,24 +76,50 @@ function App() {
|
|||
|
||||
console.debug('OPEN COMPOSE');
|
||||
|
||||
if (isLoggedIn === false) {
|
||||
return (
|
||||
<div class="box">
|
||||
<h1>
|
||||
<Trans>Error</Trans>
|
||||
</h1>
|
||||
<p>
|
||||
<Trans>Login required.</Trans>
|
||||
</p>
|
||||
<p>
|
||||
<a href="/">
|
||||
<Trans>Go home</Trans>
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (isLoggedIn) {
|
||||
return (
|
||||
<ComposeSuspense
|
||||
editStatus={editStatus}
|
||||
replyToStatus={replyToStatus}
|
||||
draftStatus={draftStatus}
|
||||
standalone
|
||||
hasOpener={window.opener}
|
||||
onClose={(results) => {
|
||||
const { newStatus, fn = () => {} } = results || {};
|
||||
try {
|
||||
if (newStatus) {
|
||||
window.opener.__STATES__.reloadStatusPage++;
|
||||
}
|
||||
fn();
|
||||
setUIState('closed');
|
||||
} catch (e) {}
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<ComposeSuspense
|
||||
editStatus={editStatus}
|
||||
replyToStatus={replyToStatus}
|
||||
draftStatus={draftStatus}
|
||||
standalone
|
||||
hasOpener={window.opener}
|
||||
onClose={(results) => {
|
||||
const { newStatus, fn = () => {} } = results || {};
|
||||
try {
|
||||
if (newStatus) {
|
||||
window.opener.__STATES__.reloadStatusPage++;
|
||||
}
|
||||
fn();
|
||||
setUIState('closed');
|
||||
} catch (e) {}
|
||||
}}
|
||||
/>
|
||||
<div class="box">
|
||||
<Loader />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -1,16 +1,52 @@
|
|||
[
|
||||
{
|
||||
"code": "ar-SA",
|
||||
"nativeName": "العربية",
|
||||
"name": "Arabic",
|
||||
"completion": 26
|
||||
},
|
||||
{
|
||||
"code": "ca-ES",
|
||||
"nativeName": "català",
|
||||
"name": "Catalan",
|
||||
"completion": 100
|
||||
},
|
||||
{
|
||||
"code": "cs-CZ",
|
||||
"nativeName": "čeština",
|
||||
"name": "Czech",
|
||||
"completion": 79
|
||||
},
|
||||
{
|
||||
"code": "de-DE",
|
||||
"nativeName": "Deutsch",
|
||||
"name": "German",
|
||||
"completion": 96
|
||||
},
|
||||
{
|
||||
"code": "eo-UY",
|
||||
"nativeName": "Esperanto",
|
||||
"name": "Esperanto",
|
||||
"completion": 30
|
||||
},
|
||||
{
|
||||
"code": "es-ES",
|
||||
"nativeName": "español",
|
||||
"name": "Spanish",
|
||||
"completion": 100
|
||||
},
|
||||
{
|
||||
"code": "eu-ES",
|
||||
"nativeName": "euskara",
|
||||
"name": "Basque",
|
||||
"completion": 100
|
||||
},
|
||||
{
|
||||
"code": "fa-IR",
|
||||
"nativeName": "فارسی",
|
||||
"name": "Persian",
|
||||
"completion": 73
|
||||
},
|
||||
{
|
||||
"code": "fi-FI",
|
||||
"nativeName": "suomi",
|
||||
|
@ -18,58 +54,52 @@
|
|||
"completion": 100
|
||||
},
|
||||
{
|
||||
"code": "zh-CN",
|
||||
"nativeName": "简体中文",
|
||||
"name": "Simplified Chinese",
|
||||
"completion": 100
|
||||
},
|
||||
{
|
||||
"code": "de-DE",
|
||||
"nativeName": "Deutsch",
|
||||
"name": "German",
|
||||
"completion": 98
|
||||
},
|
||||
{
|
||||
"code": "eu-ES",
|
||||
"nativeName": "euskara",
|
||||
"name": "Basque",
|
||||
"completion": 98
|
||||
"code": "fr-FR",
|
||||
"nativeName": "français",
|
||||
"name": "French",
|
||||
"completion": 99
|
||||
},
|
||||
{
|
||||
"code": "gl-ES",
|
||||
"nativeName": "galego",
|
||||
"name": "Galician",
|
||||
"completion": 98
|
||||
"completion": 100
|
||||
},
|
||||
{
|
||||
"code": "fr-FR",
|
||||
"nativeName": "français",
|
||||
"name": "French",
|
||||
"completion": 97
|
||||
"code": "he-IL",
|
||||
"nativeName": "עברית",
|
||||
"name": "Hebrew",
|
||||
"completion": 12
|
||||
},
|
||||
{
|
||||
"code": "ko-KR",
|
||||
"nativeName": "한국어",
|
||||
"name": "Korean",
|
||||
"completion": 75
|
||||
"code": "it-IT",
|
||||
"nativeName": "italiano",
|
||||
"name": "Italian",
|
||||
"completion": 34
|
||||
},
|
||||
{
|
||||
"code": "cs-CZ",
|
||||
"nativeName": "čeština",
|
||||
"name": "Czech",
|
||||
"completion": 72
|
||||
"code": "ja-JP",
|
||||
"nativeName": "日本語",
|
||||
"name": "Japanese",
|
||||
"completion": 31
|
||||
},
|
||||
{
|
||||
"code": "kab",
|
||||
"nativeName": "Taqbaylit",
|
||||
"name": "Kabyle",
|
||||
"completion": 67
|
||||
"completion": 100
|
||||
},
|
||||
{
|
||||
"code": "fa-IR",
|
||||
"nativeName": "فارسی",
|
||||
"name": "Persian",
|
||||
"completion": 62
|
||||
"code": "ko-KR",
|
||||
"nativeName": "한국어",
|
||||
"name": "Korean",
|
||||
"completion": 86
|
||||
},
|
||||
{
|
||||
"code": "lt-LT",
|
||||
"nativeName": "lietuvių",
|
||||
"name": "Lithuanian",
|
||||
"completion": 43
|
||||
},
|
||||
{
|
||||
"code": "nl-NL",
|
||||
|
@ -78,46 +108,28 @@
|
|||
"completion": 48
|
||||
},
|
||||
{
|
||||
"code": "ja-JP",
|
||||
"nativeName": "日本語",
|
||||
"name": "Japanese",
|
||||
"completion": 32
|
||||
"code": "pl-PL",
|
||||
"nativeName": "polski",
|
||||
"name": "Polish",
|
||||
"completion": 1
|
||||
},
|
||||
{
|
||||
"code": "lt-LT",
|
||||
"nativeName": "lietuvių",
|
||||
"name": "Lithuanian",
|
||||
"completion": 28
|
||||
"code": "pt-BR",
|
||||
"nativeName": "português",
|
||||
"name": "Portuguese",
|
||||
"completion": 100
|
||||
},
|
||||
{
|
||||
"code": "pt-PT",
|
||||
"nativeName": "português",
|
||||
"name": "Portuguese",
|
||||
"completion": 100
|
||||
},
|
||||
{
|
||||
"code": "ru-RU",
|
||||
"nativeName": "русский",
|
||||
"name": "Russian",
|
||||
"completion": 23
|
||||
},
|
||||
{
|
||||
"code": "ar-SA",
|
||||
"nativeName": "العربية",
|
||||
"name": "Arabic",
|
||||
"completion": 22
|
||||
},
|
||||
{
|
||||
"code": "it-IT",
|
||||
"nativeName": "italiano",
|
||||
"name": "Italian",
|
||||
"completion": 20
|
||||
},
|
||||
{
|
||||
"code": "eo-UY",
|
||||
"nativeName": "Esperanto",
|
||||
"name": "Esperanto",
|
||||
"completion": 14
|
||||
},
|
||||
{
|
||||
"code": "he-IL",
|
||||
"nativeName": "עברית",
|
||||
"name": "Hebrew",
|
||||
"completion": 11
|
||||
"completion": 100
|
||||
},
|
||||
{
|
||||
"code": "th-TH",
|
||||
|
@ -125,10 +137,22 @@
|
|||
"name": "Thai",
|
||||
"completion": 3
|
||||
},
|
||||
{
|
||||
"code": "uk-UA",
|
||||
"nativeName": "українська",
|
||||
"name": "Ukrainian",
|
||||
"completion": 26
|
||||
},
|
||||
{
|
||||
"code": "zh-CN",
|
||||
"nativeName": "简体中文",
|
||||
"name": "Simplified Chinese",
|
||||
"completion": 100
|
||||
},
|
||||
{
|
||||
"code": "zh-TW",
|
||||
"nativeName": "繁體中文",
|
||||
"name": "Traditional Chinese",
|
||||
"completion": 3
|
||||
"completion": 14
|
||||
}
|
||||
]
|
|
@ -84,6 +84,7 @@
|
|||
var(--text-color) 60%
|
||||
);
|
||||
--outline-color: rgba(128, 128, 128, 0.2);
|
||||
--outline-stronger-color: rgba(128, 128, 128, 0.4);
|
||||
--outline-hover-color: rgba(128, 128, 128, 0.7);
|
||||
--divider-color: rgba(0, 0, 0, 0.1);
|
||||
--backdrop-color: rgba(0, 0, 0, 0.1);
|
||||
|
@ -149,6 +150,11 @@
|
|||
mediumslateblue 70%,
|
||||
var(--text-color) 30%
|
||||
);
|
||||
--button-bg-color: color-mix(
|
||||
in srgb,
|
||||
var(--blue-color) 80%,
|
||||
var(--bg-color) 20%
|
||||
);
|
||||
--reblog-faded-color: #b190f141;
|
||||
--reply-to-text-color: var(--reply-to-color);
|
||||
--reply-to-faded-color: #ffa60017;
|
||||
|
|
|
@ -12,7 +12,15 @@ const locales = [
|
|||
.filter(({ completion }) => completion >= PERCENTAGE_THRESHOLD)
|
||||
.map(({ code }) => code),
|
||||
];
|
||||
if (import.meta.env.DEV) {
|
||||
locales.push('pseudo-LOCALE');
|
||||
}
|
||||
export const LOCALES = locales;
|
||||
|
||||
let devLocales = [];
|
||||
if (import.meta.env?.DEV || import.meta.env?.PHANPY_SHOW_DEV_LOCALES) {
|
||||
devLocales = catalogs
|
||||
.filter(({ completion }) => completion < PERCENTAGE_THRESHOLD)
|
||||
.map(({ code }) => code);
|
||||
devLocales.push('pseudo-LOCALE');
|
||||
}
|
||||
export const DEV_LOCALES = devLocales;
|
||||
|
||||
export const ALL_LOCALES = [...locales, ...devLocales];
|
||||
|
|
1045
src/locales/ar-SA.po
1045
src/locales/ar-SA.po
File diff suppressed because it is too large
Load diff
1018
src/locales/ca-ES.po
1018
src/locales/ca-ES.po
File diff suppressed because it is too large
Load diff
1143
src/locales/cs-CZ.po
1143
src/locales/cs-CZ.po
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
1030
src/locales/en.po
1030
src/locales/en.po
File diff suppressed because it is too large
Load diff
1263
src/locales/eo-UY.po
1263
src/locales/eo-UY.po
File diff suppressed because it is too large
Load diff
1147
src/locales/es-ES.po
1147
src/locales/es-ES.po
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
1069
src/locales/fa-IR.po
1069
src/locales/fa-IR.po
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
1011
src/locales/fr-FR.po
1011
src/locales/fr-FR.po
File diff suppressed because it is too large
Load diff
1027
src/locales/gl-ES.po
1027
src/locales/gl-ES.po
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
1215
src/locales/it-IT.po
1215
src/locales/it-IT.po
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
1357
src/locales/kab.po
1357
src/locales/kab.po
File diff suppressed because it is too large
Load diff
1229
src/locales/ko-KR.po
1229
src/locales/ko-KR.po
File diff suppressed because it is too large
Load diff
1081
src/locales/lt-LT.po
1081
src/locales/lt-LT.po
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
3732
src/locales/pl-PL.po
Normal file
3732
src/locales/pl-PL.po
Normal file
File diff suppressed because it is too large
Load diff
3733
src/locales/pt-BR.po
Normal file
3733
src/locales/pt-BR.po
Normal file
File diff suppressed because it is too large
Load diff
2710
src/locales/pt-PT.po
2710
src/locales/pt-PT.po
File diff suppressed because it is too large
Load diff
2341
src/locales/ru-RU.po
2341
src/locales/ru-RU.po
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
3732
src/locales/uk-UA.po
Normal file
3732
src/locales/uk-UA.po
Normal file
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
1161
src/locales/zh-TW.po
1161
src/locales/zh-TW.po
File diff suppressed because it is too large
Load diff
|
@ -9,6 +9,7 @@ import Avatar from '../components/avatar';
|
|||
import Icon from '../components/icon';
|
||||
import Link from '../components/link';
|
||||
import MenuConfirm from '../components/menu-confirm';
|
||||
import MenuLink from '../components/menu-link';
|
||||
import Menu2 from '../components/menu2';
|
||||
import NameText from '../components/name-text';
|
||||
import { api } from '../utils/api';
|
||||
|
@ -16,6 +17,8 @@ import states from '../utils/states';
|
|||
import store from '../utils/store';
|
||||
import { getCurrentAccountID, setCurrentAccountID } from '../utils/store-utils';
|
||||
|
||||
const isStandalone = window.matchMedia('(display-mode: standalone)').matches;
|
||||
|
||||
function Accounts({ onClose }) {
|
||||
const { masto } = api();
|
||||
// Accounts
|
||||
|
@ -107,6 +110,32 @@ function Accounts({ onClose }) {
|
|||
</button>
|
||||
}
|
||||
>
|
||||
{moreThanOneAccount && (
|
||||
<>
|
||||
<MenuItem
|
||||
disabled={isCurrent}
|
||||
onClick={() => {
|
||||
setCurrentAccountID(account.info.id);
|
||||
location.reload();
|
||||
}}
|
||||
>
|
||||
<Icon icon="transfer" />{' '}
|
||||
<Trans>Switch to this account</Trans>
|
||||
</MenuItem>
|
||||
{!isStandalone && !isCurrent && (
|
||||
<MenuLink
|
||||
href={`./?account=${account.info.id}`}
|
||||
target="_blank"
|
||||
>
|
||||
<Icon icon="external" />
|
||||
<span>
|
||||
<Trans>Switch in new tab/window</Trans>
|
||||
</span>
|
||||
</MenuLink>
|
||||
)}
|
||||
<MenuDivider />
|
||||
</>
|
||||
)}
|
||||
<MenuItem
|
||||
onClick={() => {
|
||||
states.showAccount = `${account.info.username}@${account.instanceURL}`;
|
||||
|
|
|
@ -22,7 +22,7 @@ function Bookmarks() {
|
|||
<Timeline
|
||||
title={t`Bookmarks`}
|
||||
id="bookmarks"
|
||||
emptyText={`No bookmarks yet. Go bookmark something!`}
|
||||
emptyText={t`No bookmarks yet. Go bookmark something!`}
|
||||
errorText={t`Unable to load bookmarks.`}
|
||||
instance={instance}
|
||||
fetchItems={fetchBookmarks}
|
||||
|
|
|
@ -822,6 +822,22 @@ function Catchup() {
|
|||
},
|
||||
);
|
||||
|
||||
const handleArrowKeys = useCallback((e) => {
|
||||
const activeElement = document.activeElement;
|
||||
const isRadio =
|
||||
activeElement?.tagName === 'INPUT' && activeElement.type === 'radio';
|
||||
const isArrowKeys =
|
||||
e.key === 'ArrowDown' ||
|
||||
e.key === 'ArrowUp' ||
|
||||
e.key === 'ArrowLeft' ||
|
||||
e.key === 'ArrowRight';
|
||||
if (isArrowKeys && isRadio) {
|
||||
// Note: page scroll won't trigger on first arrow key press due to this. Subsequent presses will.
|
||||
activeElement.blur();
|
||||
return;
|
||||
}
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={(node) => {
|
||||
|
@ -883,7 +899,7 @@ function Catchup() {
|
|||
</div>
|
||||
</div>
|
||||
</header>
|
||||
<main>
|
||||
<main onKeyDown={handleArrowKeys}>
|
||||
{uiState === 'start' && (
|
||||
<div class="catchup-start">
|
||||
<h1>
|
||||
|
|
|
@ -22,7 +22,7 @@ function Favourites() {
|
|||
<Timeline
|
||||
title={t`Likes`}
|
||||
id="favourites"
|
||||
emptyText={`No likes yet. Go like something!`}
|
||||
emptyText={t`No likes yet. Go like something!`}
|
||||
errorText={t`Unable to load likes.`}
|
||||
instance={instance}
|
||||
fetchItems={fetchFavourites}
|
||||
|
|
|
@ -205,7 +205,7 @@ function Hashtags({ media: mediaView, columnMode, ...props }) {
|
|||
<MenuConfirm
|
||||
subMenu
|
||||
confirm={info.following}
|
||||
confirmLabel={`Unfollow #${hashtag}?`}
|
||||
confirmLabel={t`Unfollow #${hashtag}?`}
|
||||
disabled={followUIState === 'loading' || !authenticated}
|
||||
onClick={() => {
|
||||
setFollowUIState('loading');
|
||||
|
|
|
@ -11,7 +11,12 @@ import LangSelector from '../components/lang-selector';
|
|||
import Link from '../components/link';
|
||||
import Loader from '../components/loader';
|
||||
import instancesListURL from '../data/instances.json?url';
|
||||
import { getAuthorizationURL, registerApplication } from '../utils/auth';
|
||||
import {
|
||||
getAuthorizationURL,
|
||||
getPKCEAuthorizationURL,
|
||||
registerApplication,
|
||||
} from '../utils/auth';
|
||||
import { supportsPKCE } from '../utils/oauth-pkce';
|
||||
import store from '../utils/store';
|
||||
import useTitle from '../utils/useTitle';
|
||||
|
||||
|
@ -63,17 +68,36 @@ function Login() {
|
|||
instanceURL,
|
||||
});
|
||||
|
||||
if (client_id && client_secret) {
|
||||
store.session.set('clientID', client_id);
|
||||
store.session.set('clientSecret', client_secret);
|
||||
store.session.set('vapidKey', vapid_key);
|
||||
const authPKCE = await supportsPKCE({ instanceURL });
|
||||
console.log({ authPKCE });
|
||||
if (authPKCE) {
|
||||
if (client_id && client_secret) {
|
||||
store.sessionCookie.set('clientID', client_id);
|
||||
store.sessionCookie.set('clientSecret', client_secret);
|
||||
store.sessionCookie.set('vapidKey', vapid_key);
|
||||
|
||||
location.href = await getAuthorizationURL({
|
||||
instanceURL,
|
||||
client_id,
|
||||
});
|
||||
const [url, verifier] = await getPKCEAuthorizationURL({
|
||||
instanceURL,
|
||||
client_id,
|
||||
});
|
||||
store.sessionCookie.set('codeVerifier', verifier);
|
||||
location.href = url;
|
||||
} else {
|
||||
alert(t`Failed to register application`);
|
||||
}
|
||||
} else {
|
||||
alert('Failed to register application');
|
||||
if (client_id && client_secret) {
|
||||
store.sessionCookie.set('clientID', client_id);
|
||||
store.sessionCookie.set('clientSecret', client_secret);
|
||||
store.sessionCookie.set('vapidKey', vapid_key);
|
||||
|
||||
location.href = await getAuthorizationURL({
|
||||
instanceURL,
|
||||
client_id,
|
||||
});
|
||||
} else {
|
||||
alert(t`Failed to register application`);
|
||||
}
|
||||
}
|
||||
setUIState('default');
|
||||
} catch (e) {
|
||||
|
@ -158,7 +182,7 @@ function Login() {
|
|||
autocapitalize="off"
|
||||
autocomplete="off"
|
||||
spellCheck={false}
|
||||
placeholder={`instance domain`}
|
||||
placeholder={t`instance domain`}
|
||||
onInput={(e) => {
|
||||
setInstanceText(e.target.value);
|
||||
}}
|
||||
|
|
|
@ -232,9 +232,21 @@ function Settings({ onClose }) {
|
|||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<label>
|
||||
<Trans>Display language</Trans>
|
||||
</label>
|
||||
<span>
|
||||
<label>
|
||||
<Trans>Display language</Trans>
|
||||
</label>
|
||||
<br />
|
||||
<small>
|
||||
<a
|
||||
href="https://crowdin.com/project/phanpy"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<Trans>Volunteer translations</Trans>
|
||||
</a>
|
||||
</small>
|
||||
</span>
|
||||
<LangSelector />
|
||||
</li>
|
||||
</ul>
|
||||
|
|
|
@ -1,12 +1,32 @@
|
|||
const { PHANPY_CLIENT_NAME: CLIENT_NAME, PHANPY_WEBSITE: WEBSITE } = import.meta
|
||||
.env;
|
||||
import { generateCodeChallenge, verifier } from './oauth-pkce';
|
||||
|
||||
const {
|
||||
DEV,
|
||||
PHANPY_CLIENT_NAME: CLIENT_NAME,
|
||||
PHANPY_WEBSITE: WEBSITE,
|
||||
} = import.meta.env;
|
||||
|
||||
const SCOPES = 'read write follow push';
|
||||
|
||||
/*
|
||||
PHANPY_WEBSITE is set to the default official site.
|
||||
It's used in pre-built releases, so there's no way to change it dynamically
|
||||
without rebuilding.
|
||||
Therefore, we can't use it as redirect_uri.
|
||||
We only use PHANPY_WEBSITE if it's "same" as current location URL.
|
||||
|
||||
Very basic check based on location.hostname for now
|
||||
*/
|
||||
const sameSite = WEBSITE
|
||||
? WEBSITE.toLowerCase().includes(location.hostname)
|
||||
: false;
|
||||
const currentLocation = location.origin + location.pathname;
|
||||
const REDIRECT_URI = DEV || !sameSite ? currentLocation : WEBSITE;
|
||||
|
||||
export async function registerApplication({ instanceURL }) {
|
||||
const registrationParams = new URLSearchParams({
|
||||
client_name: CLIENT_NAME,
|
||||
redirect_uris: location.origin + location.pathname,
|
||||
redirect_uris: REDIRECT_URI,
|
||||
scopes: SCOPES,
|
||||
website: WEBSITE,
|
||||
});
|
||||
|
@ -25,11 +45,26 @@ export async function registerApplication({ instanceURL }) {
|
|||
return registrationJSON;
|
||||
}
|
||||
|
||||
export async function getPKCEAuthorizationURL({ instanceURL, client_id }) {
|
||||
const codeVerifier = verifier();
|
||||
const codeChallenge = await generateCodeChallenge(codeVerifier);
|
||||
const params = new URLSearchParams({
|
||||
client_id,
|
||||
code_challenge_method: 'S256',
|
||||
code_challenge: codeChallenge,
|
||||
redirect_uri: REDIRECT_URI,
|
||||
response_type: 'code',
|
||||
scope: SCOPES,
|
||||
});
|
||||
const authorizationURL = `https://${instanceURL}/oauth/authorize?${params.toString()}`;
|
||||
return [authorizationURL, codeVerifier];
|
||||
}
|
||||
|
||||
export async function getAuthorizationURL({ instanceURL, client_id }) {
|
||||
const authorizationParams = new URLSearchParams({
|
||||
client_id,
|
||||
scope: SCOPES,
|
||||
redirect_uri: location.origin + location.pathname,
|
||||
redirect_uri: REDIRECT_URI,
|
||||
// redirect_uri: 'urn:ietf:wg:oauth:2.0:oob',
|
||||
response_type: 'code',
|
||||
});
|
||||
|
@ -42,15 +77,23 @@ export async function getAccessToken({
|
|||
client_id,
|
||||
client_secret,
|
||||
code,
|
||||
code_verifier,
|
||||
}) {
|
||||
const params = new URLSearchParams({
|
||||
client_id,
|
||||
client_secret,
|
||||
redirect_uri: location.origin + location.pathname,
|
||||
redirect_uri: REDIRECT_URI,
|
||||
grant_type: 'authorization_code',
|
||||
code,
|
||||
scope: SCOPES,
|
||||
// client_secret,
|
||||
// code_verifier,
|
||||
});
|
||||
if (client_secret) {
|
||||
params.append('client_secret', client_secret);
|
||||
}
|
||||
if (code_verifier) {
|
||||
params.append('code_verifier', code_verifier);
|
||||
}
|
||||
const tokenResponse = await fetch(`https://${instanceURL}/oauth/token`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
|
|
|
@ -5,6 +5,7 @@ const statusPostRegexes = [
|
|||
/^\/@[^@\/]+\/(?:statuses|posts)\/([^\/]+)/i, // GoToSocial, Takahe
|
||||
/\/notes\/([^\/]+)/i, // Misskey, Firefish
|
||||
/^\/(?:notice|objects)\/([a-z0-9-]+)/i, // Pleroma
|
||||
/\/@[^@\/]+\/post\/([^\/]+)/i, // Threads
|
||||
/\/@[^@\/]+@?[^\/]+?\/([^\/]+)/i, // Mastodon
|
||||
/^\/p\/[^\/]+\/([^\/]+)/i, // Pixelfed
|
||||
];
|
||||
|
|
|
@ -6,7 +6,7 @@ export default function isMastodonLinkMaybe(url) {
|
|||
/^\/(@[^/]+|users\/[^/]+)\/(statuses|posts)\/\w+\/?$/i.test(pathname) || // GoToSocial, Takahe
|
||||
/^\/notes\/[a-z0-9]+$/i.test(pathname) || // Misskey, Firefish
|
||||
/^\/(notice|objects)\/[a-z0-9-]+$/i.test(pathname) || // Pleroma
|
||||
/^\/@[^/]+\/post\/[a-z0-9]+$/i.test(pathname) || // Threads
|
||||
/^\/@[^/]+\/post\/[a-z0-9\-_]+$/i.test(pathname) || // Threads
|
||||
/^\/@[^/]+\/[a-z0-9]+[a-z0-9\-]+[a-z0-9]+$/i.test(pathname) || // Hollo
|
||||
(hostname === 'fed.brid.gy' && pathname.startsWith('/r/http')) || // Bridgy Fed
|
||||
/#\/[^\/]+\.[^\/]+\/s\/.+/i.test(hash) // Phanpy 🫣
|
||||
|
|
|
@ -7,7 +7,7 @@ import {
|
|||
} from '@lingui/detect-locale';
|
||||
import Locale from 'intl-locale-textinfo-polyfill';
|
||||
|
||||
import { DEFAULT_LANG, LOCALES } from '../locales';
|
||||
import { ALL_LOCALES, DEFAULT_LANG } from '../locales';
|
||||
import { messages } from '../locales/en.po';
|
||||
import localeMatch from '../utils/locale-match';
|
||||
|
||||
|
@ -62,7 +62,7 @@ export function initActivateLang() {
|
|||
DEFAULT_LANG,
|
||||
);
|
||||
const matchedLang =
|
||||
LOCALES.find((l) => l === lang) || localeMatch(lang, LOCALES);
|
||||
ALL_LOCALES.find((l) => l === lang) || localeMatch(lang, ALL_LOCALES);
|
||||
activateLang(matchedLang);
|
||||
|
||||
// const yes = confirm(t`Reload to apply language setting?`);
|
||||
|
|
|
@ -1,12 +1,14 @@
|
|||
import { i18n } from '@lingui/core';
|
||||
|
||||
import localeMatch from './locale-match';
|
||||
import mem from './mem';
|
||||
|
||||
const defaultLocale = new Intl.DateTimeFormat().resolvedOptions().locale;
|
||||
|
||||
const _DateTimeFormat = (opts) => {
|
||||
const { locale, dateYear, hideTime, formatOpts } = opts || {};
|
||||
const loc = locale && !/pseudo/i.test(locale) ? locale : defaultLocale;
|
||||
const regionlessLocale = locale.replace(/-[a-z]+$/i, '');
|
||||
const loc = localeMatch([regionlessLocale], [defaultLocale], locale);
|
||||
const currentYear = new Date().getFullYear();
|
||||
const options = {
|
||||
// Show year if not current year
|
||||
|
@ -20,9 +22,11 @@ const _DateTimeFormat = (opts) => {
|
|||
};
|
||||
try {
|
||||
return Intl.DateTimeFormat(loc, options);
|
||||
} catch (e) {
|
||||
return Intl.DateTimeFormat(undefined, options);
|
||||
}
|
||||
} catch (e) {}
|
||||
try {
|
||||
return Intl.DateTimeFormat(locale, options);
|
||||
} catch (e) {}
|
||||
return Intl.DateTimeFormat(undefined, options);
|
||||
};
|
||||
const DateTimeFormat = mem(_DateTimeFormat);
|
||||
|
||||
|
|
46
src/utils/oauth-pkce.js
Normal file
46
src/utils/oauth-pkce.js
Normal file
|
@ -0,0 +1,46 @@
|
|||
function dec2hex(dec) {
|
||||
return ('0' + dec.toString(16)).slice(-2);
|
||||
}
|
||||
export function verifier() {
|
||||
var array = new Uint32Array(56 / 2);
|
||||
window.crypto.getRandomValues(array);
|
||||
return Array.from(array, dec2hex).join('');
|
||||
}
|
||||
function sha256(plain) {
|
||||
// returns promise ArrayBuffer
|
||||
const encoder = new TextEncoder();
|
||||
const data = encoder.encode(plain);
|
||||
return window.crypto.subtle.digest('SHA-256', data);
|
||||
}
|
||||
function base64urlencode(a) {
|
||||
let str = '';
|
||||
const bytes = new Uint8Array(a);
|
||||
const len = bytes.byteLength;
|
||||
for (var i = 0; i < len; i++) {
|
||||
str += String.fromCharCode(bytes[i]);
|
||||
}
|
||||
return btoa(str).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
|
||||
}
|
||||
export async function generateCodeChallenge(v) {
|
||||
const hashed = await sha256(v);
|
||||
return base64urlencode(hashed);
|
||||
}
|
||||
|
||||
// If /.well-known/oauth-authorization-server exists and code_challenge_methods_supported includes "S256", means support PKCE
|
||||
export async function supportsPKCE({ instanceURL }) {
|
||||
if (!instanceURL) return false;
|
||||
try {
|
||||
const res = await fetch(
|
||||
`https://${instanceURL}/.well-known/oauth-authorization-server`,
|
||||
);
|
||||
if (!res.ok || res.status !== 200) return false;
|
||||
const json = await res.json();
|
||||
if (json.code_challenge_methods_supported?.includes('S256')) return true;
|
||||
return false;
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// For debugging
|
||||
window.__generateCodeChallenge = generateCodeChallenge;
|
|
@ -1,6 +1,6 @@
|
|||
// Utils for push notifications
|
||||
import { api } from './api';
|
||||
import { getCurrentAccount } from './store-utils';
|
||||
import { getVapidKey } from './store-utils';
|
||||
|
||||
// Subscription is an object with the following structure:
|
||||
// {
|
||||
|
@ -113,7 +113,7 @@ export async function initSubscription() {
|
|||
// Check if the subscription changed
|
||||
if (backendSubscription && subscription) {
|
||||
const sameEndpoint = backendSubscription.endpoint === subscription.endpoint;
|
||||
const { vapidKey } = getCurrentAccount();
|
||||
const vapidKey = getVapidKey();
|
||||
const sameKey = backendSubscription.serverKey === vapidKey;
|
||||
if (!sameEndpoint) {
|
||||
throw new Error('Backend subscription endpoint changed');
|
||||
|
@ -146,7 +146,7 @@ export async function initSubscription() {
|
|||
|
||||
if (subscription && !backendSubscription) {
|
||||
// check if account's vapidKey is same as subscription's applicationServerKey
|
||||
const { vapidKey } = getCurrentAccount();
|
||||
const vapidKey = getVapidKey();
|
||||
if (vapidKey) {
|
||||
const { applicationServerKey } = subscription.options;
|
||||
const vapidKeyStr = urlBase64ToUint8Array(vapidKey).toString();
|
||||
|
@ -210,7 +210,7 @@ export async function updateSubscription({ data, policy }) {
|
|||
}
|
||||
} else {
|
||||
// User is not subscribed
|
||||
const { vapidKey } = getCurrentAccount();
|
||||
const vapidKey = getVapidKey();
|
||||
if (!vapidKey) throw new Error('No server key found');
|
||||
subscription = await registration.pushManager.subscribe({
|
||||
userVisibleOnly: true,
|
||||
|
|
|
@ -154,6 +154,13 @@ export function getCurrentInstanceConfiguration() {
|
|||
return getInstanceConfiguration(instance);
|
||||
}
|
||||
|
||||
export function getVapidKey() {
|
||||
// Vapid key has moved from account to instance config
|
||||
const config = getCurrentInstanceConfiguration();
|
||||
const vapidKey = config?.vapid?.publicKey || config?.vapid?.public_key;
|
||||
return vapidKey || getCurrentAccount()?.vapidKey;
|
||||
}
|
||||
|
||||
export function isMediaFirstInstance() {
|
||||
const instance = getCurrentInstance();
|
||||
return /pixelfed/i.test(instance?.version);
|
||||
|
|
|
@ -1,5 +1,9 @@
|
|||
import Cookies from 'js-cookie';
|
||||
|
||||
import { getCurrentAccountNS } from './store-utils';
|
||||
|
||||
const cookies = Cookies.withAttributes({ sameSite: 'strict', secure: true });
|
||||
|
||||
const local = {
|
||||
get: (key) => {
|
||||
try {
|
||||
|
@ -86,6 +90,38 @@ const session = {
|
|||
},
|
||||
};
|
||||
|
||||
// Session secure cookie
|
||||
const cookie = {
|
||||
get: (key) => cookies.get(key),
|
||||
set: (key, value) => cookies.set(key, value),
|
||||
del: (key) => cookies.remove(key),
|
||||
};
|
||||
|
||||
// Cookie with sessionStorage fallback
|
||||
const sessionCookie = {
|
||||
get: (key) => {
|
||||
if (navigator.cookieEnabled) {
|
||||
return cookie.get(key);
|
||||
} else {
|
||||
return session.get(key);
|
||||
}
|
||||
},
|
||||
set: (key, value) => {
|
||||
if (navigator.cookieEnabled) {
|
||||
return cookie.set(key, value);
|
||||
} else {
|
||||
return session.set(key, value);
|
||||
}
|
||||
},
|
||||
del: (key) => {
|
||||
if (navigator.cookieEnabled) {
|
||||
return cookie.del(key);
|
||||
} else {
|
||||
return session.del(key);
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
// Store with account namespace (id@domain.tld) <- uses id, not username
|
||||
const account = {
|
||||
get: (key) => {
|
||||
|
@ -118,4 +154,4 @@ const account = {
|
|||
},
|
||||
};
|
||||
|
||||
export default { local, session, account };
|
||||
export default { local, session, sessionCookie, cookie, account };
|
||||
|
|
|
@ -12,9 +12,12 @@ import { VitePWA } from 'vite-plugin-pwa';
|
|||
import removeConsole from 'vite-plugin-remove-console';
|
||||
import { run } from 'vite-plugin-run';
|
||||
|
||||
import { ALL_LOCALES } from './src/locales';
|
||||
|
||||
const allowedEnvPrefixes = ['VITE_', 'PHANPY_'];
|
||||
const { NODE_ENV } = process.env;
|
||||
const {
|
||||
PHANPY_WEBSITE: WEBSITE,
|
||||
PHANPY_CLIENT_NAME: CLIENT_NAME,
|
||||
PHANPY_APP_ERROR_LOGGING: ERROR_LOGGING,
|
||||
} = loadEnv('production', process.cwd(), allowedEnvPrefixes);
|
||||
|
@ -70,6 +73,11 @@ export default defineConfig({
|
|||
run: ['npm', 'run', 'messages:extract:clean'],
|
||||
pattern: 'src/**/*.{js,jsx,ts,tsx}',
|
||||
},
|
||||
// {
|
||||
// name: 'update-catalogs',
|
||||
// run: ['node', 'scripts/catalogs.js'],
|
||||
// pattern: 'src/locales/*.po',
|
||||
// },
|
||||
],
|
||||
}),
|
||||
splitVendorChunkPlugin(),
|
||||
|
@ -78,6 +86,20 @@ export default defineConfig({
|
|||
}),
|
||||
htmlPlugin({
|
||||
headScripts: ERROR_LOGGING ? [rollbarCode] : [],
|
||||
links: [
|
||||
...ALL_LOCALES.map((lang) => ({
|
||||
rel: 'alternate',
|
||||
hreflang: lang,
|
||||
// *Fully-qualified* URLs
|
||||
href: `${WEBSITE}/?lang=${lang}`,
|
||||
})),
|
||||
// https://developers.google.com/search/docs/specialty/international/localized-versions#xdefault
|
||||
{
|
||||
rel: 'alternate',
|
||||
hreflang: 'x-default',
|
||||
href: `${WEBSITE}`,
|
||||
},
|
||||
],
|
||||
}),
|
||||
generateFile([
|
||||
{
|
||||
|
|
Loading…
Reference in a new issue