Merge pull request 'develop' (#9125) from develop into main

Reviewed-on: https://codeberg.org/thatonecalculator/calckey/pulls/9125
This commit is contained in:
Kainoa Kanter 2022-11-19 04:20:09 +00:00
commit c124d2cb25
64 changed files with 450 additions and 345 deletions

View File

@ -1,5 +1,5 @@
#━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ #━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
# Misskey configuration # Calckey configuration
#━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ #━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
# ┌─────┐ # ┌─────┐
@ -38,11 +38,11 @@ db:
port: 5432 port: 5432
# Database name # Database name
db: misskey db: calckey
# Auth # Auth
user: example-misskey-user user: example-calckey-user
pass: example-misskey-pass pass: example-calckey-pass
# Whether disable Caching queries # Whether disable Caching queries
#disableCache: true #disableCache: true
@ -147,7 +147,8 @@ id: 'aid'
# Managed hosting settings # Managed hosting settings
# !!!!!!!!!! # !!!!!!!!!!
# >>>>>> NORMAL SELF-HOSTERS, STAY AWAY! YOU DON'T NEED THIS! <<<<<< # >>>>>> NORMAL SELF-HOSTERS, STAY AWAY! <<<<<<
# >>>>>> YOU DON'T NEED THIS! <<<<<<
# !!!!!!!!!! # !!!!!!!!!!
# Each category is optional, but if each item in each category is mandatory! # Each category is optional, but if each item in each category is mandatory!
# If you mess this up, that's on you, you've been warned... # If you mess this up, that's on you, you've been warned...
@ -181,4 +182,11 @@ id: 'aid'
# connnectOverProxy: false # connnectOverProxy: false
# setPublicReadOnUpload: true # setPublicReadOnUpload: true
# s3ForcePathStyle: true # s3ForcePathStyle: true
#summalyProxyUrl: 'https://summaly.arkjp.net'
# !!!!!!!!!!
# >>>>>> AGAIN, NORMAL SELF-HOSTERS, STAY AWAY! <<<<<<
# >>>>>> YOU DON'T NEED THIS, ABOVE SETTINGS ARE FOR MANAGED HOSTING ONLY! <<<<<<
# !!!!!!!!!!
# Seriously. Do NOT fill out the above settings if you're self-hosting.
# They're much better off being set from the control panel.

View File

@ -9,7 +9,6 @@
- User "choices" (recommended users) like Mastodon and Soapbox - User "choices" (recommended users) like Mastodon and Soapbox
- Option to publicize instance blocks - Option to publicize instance blocks
- Fully revamp non-logged-in screen - Fully revamp non-logged-in screen
- Remote follow button
- Personal notes for all accounts - Personal notes for all accounts
- Non-nyaify cat mode - Non-nyaify cat mode
- Timeline filters - Timeline filters
@ -21,8 +20,8 @@
## Work in progress ## Work in progress
- Better Messaging UI - Better Messaging UI
- Videos can be played in DMs
- Make your password hasn't been pwned - Make your password hasn't been pwned
- Remote follow button
- Admin custom CSS - Admin custom CSS
- Add back time machine (jump to date) - Add back time machine (jump to date)
- Improve accesibility score - Improve accesibility score
@ -86,6 +85,7 @@
- Link hover effect - Link hover effect
- Replace all `$ts` with i18n - Replace all `$ts` with i18n
- AVIF support - AVIF support
- Page drafts
- Obliteration of Ai-chan - Obliteration of Ai-chan
- [Make showing ads optional](https://github.com/misskey-dev/misskey/pull/8996) - [Make showing ads optional](https://github.com/misskey-dev/misskey/pull/8996)
- [Tapping avatar in mobile opens account modal](https://github.com/misskey-dev/misskey/pull/9056) - [Tapping avatar in mobile opens account modal](https://github.com/misskey-dev/misskey/pull/9056)

100
README.md
View File

@ -1,15 +1,15 @@
<div align="center"> <div align="center">
<a href="https://stop.voring.me/"> <a href="https://i.calckey.cloud/">
<img src="./.github/title_float.svg" alt="Calckey logo" style="border-radius:50%" width="400"/> <img src="./.github/title_float.svg" alt="Calckey logo" style="border-radius:50%" width="400"/>
</a> </a>
**🌎 **[Calckey](https://stop.voring.me/)** is an open source, decentralized social media platform that's free forever! 🚀** **🌎 **[Calckey](https://i.calckey.cloud/)** is an open source, decentralized social media platform that's free forever! 🚀**
</div> </div>
<div> <div>
<img src="https://pool.jortage.com/voringme/misskey/e7cd2a17-8b23-4e1e-b5cf-709480c623e2.png" align="right" height="320px"/> <img src="https://pool.jortage.com/voringme/misskey/e7cd2a17-8b23-4e1e-b5cf-709480c623e2.png" align="right" height="320px" alt="Calc (the Calckey mascot) smoking a fat dart"/>
# ✨ About Calckey # ✨ About Calckey
@ -33,6 +33,8 @@
# 🥂 Links # 🥂 Links
- 🚢 Flagship instance: <https://i.calckey.cloud>
- 📣 Official account: <https://i.calckey.cloud/@calckey>
- 💸 Liberapay: <https://liberapay.com/ThatOneCalculator> - 💸 Liberapay: <https://liberapay.com/ThatOneCalculator>
- 💁 Matrix support room: <https://matrix.to/#/#calckey:matrix.fedibird.com> - 💁 Matrix support room: <https://matrix.to/#/#calckey:matrix.fedibird.com>
- 📜 Instance list: <https://calckey.fediverse.observer/list> - 📜 Instance list: <https://calckey.fediverse.observer/list>
@ -93,89 +95,17 @@ cp -r ../misskey/files . # if you don't use object storage
## 🍀 NGINX ## 🍀 NGINX
<details> - Run `sudo cp ./calckey.nginx.conf /etc/nginx/sites-available/ && cd /etc/nginx/sites-available/`
<summary>Click to see an example NGINX config:</summary> - Edit `calckey.nginx.conf` to reflect your instance properly
- Run `sudo cp ./calckey.nginx.conf ../sites-enabled/`
```nginx - Run `sudo nginx -t` to validate that the config is valid, then restart the NGINX service.
# Replace example.tld with your domain
# For WebSocket
map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}
proxy_cache_path /tmp/nginx_cache levels=1:2 keys_zone=cache1:16m max_size=1g inactive=720m use_temp_path=off;
server {
listen 80;
listen [::]:80;
server_name example.tld;
# For SSL domain validation
root /var/www/html;
location /.well-known/acme-challenge/ { allow all; }
location /.well-known/pki-validation/ { allow all; }
location / { return 301 https://$server_name$request_uri; }
}
server {
listen 443 ssl http2;
listen [::]:443 ssl http2;
server_name example.tld;
ssl_session_timeout 1d;
ssl_session_cache shared:ssl_session_cache:10m;
ssl_session_tickets off;
# To use Let's Encrypt certificate
ssl_certificate /etc/letsencrypt/live/example.tld/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/example.tld/privkey.pem;
# To use Debian/Ubuntu's self-signed certificate (For testing or before issuing a certificate)
#ssl_certificate /etc/ssl/certs/ssl-cert-snakeoil.pem;
#ssl_certificate_key /etc/ssl/private/ssl-cert-snakeoil.key;
# SSL protocol settings
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384;
ssl_prefer_server_ciphers off;
ssl_stapling on;
ssl_stapling_verify on;
# Change to your upload limit
client_max_body_size 80m;
# Proxy to Node
location / {
proxy_pass http://127.0.0.1:3000;
proxy_set_header Host $host;
proxy_http_version 1.1;
proxy_redirect off;
# If it's behind another reverse proxy or CDN, remove the following.
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto https;
# For WebSocket
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
# Cache settings
proxy_cache cache1;
proxy_cache_lock on;
proxy_cache_use_stale updating;
add_header X-Cache $upstream_cache_status;
}
}
```
</details> </details>
## 🚀 Build and launch! ## 🚀 Build and launch!
### 🐢 NodeJS ### 🐢 NodeJS
#### `git pull` and run these steps to update Calckey in the future! #### `git pull` and run these steps to update Calckey in the future!
```sh ```sh
@ -195,13 +125,15 @@ docker up -d
### 🐳 Docker Compose ### 🐳 Docker Compose
```sh ```sh
sudo docker compose build docker-compose build
sudo docker-compose run --rm web yarn run init docker-compose run --rm web yarn run init
sudo docker compose up -d docker-compose up -d
``` ```
## 😉 Tips & Tricks ## 😉 Tips & Tricks
- When editing the config file, please don't fill out the settings at the bottom. They're designed *only* for managed hosting, not self hosting. Those settings are much better off being set in Calckey's control panel.
- Port 3000 (used in the default config) might be already used on your server for something else. To find an open port for Calckey, run `for p in $(seq 3000 4000); do ss -tlnH | tr -s ' ' | cut -d" " -sf4 | grep -q "${p}$" || echo "${p}"; done | head -n 1`
- I'd ***strongly*** recommend against using CloudFlare, but if you do, make sure to turn code minification off. - I'd ***strongly*** recommend against using CloudFlare, but if you do, make sure to turn code minification off.
- For push notifications, run `npx web-push generate-vapid-keys`, the put the public and private keys into Control Panel > General > ServiceWorker. - For push notifications, run `npx web-push generate-vapid-keys`, the put the public and private keys into Control Panel > General > ServiceWorker.
- For translations, make a [DeepL](https://deepl.com) account and generate an API key, then put it into Control Panel > General > DeepL Translation. - For translations, make a [DeepL](https://deepl.com) account and generate an API key, then put it into Control Panel > General > DeepL Translation.
@ -209,5 +141,5 @@ sudo docker compose up -d
- Go to the user's page > 3 Dots > About > Moderation > turn on "Moderator" - Go to the user's page > 3 Dots > About > Moderation > turn on "Moderator"
- Go back to Overview > click the clipboard icon next to the ID - Go back to Overview > click the clipboard icon next to the ID
- Run `psql -d calckey` (or whatever the database name is) - Run `psql -d calckey` (or whatever the database name is)
- Run `UPDATE "user" SET "isAdmin" = true WHERE id='999999';` (replace 999999 with the copied ID) - Run `UPDATE "user" SET "isAdmin" = true WHERE id='999999';` (replace `999999` with the copied ID)
- Have the new admin log out and log back in - Have the new admin log out and log back in

72
calckey.nginx.conf Normal file
View File

@ -0,0 +1,72 @@
# Replace example.tld with your domain
# For WebSocket
map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}
proxy_cache_path /tmp/nginx_cache levels=1:2 keys_zone=cache1:16m max_size=1g inactive=720m use_temp_path=off;
server {
listen 80;
listen [::]:80;
server_name example.tld;
# For SSL domain validation
root /var/www/html;
location /.well-known/acme-challenge/ { allow all; }
location /.well-known/pki-validation/ { allow all; }
location / { return 301 https://$server_name$request_uri; }
}
server {
listen 443 ssl http2;
listen [::]:443 ssl http2;
server_name example.tld;
ssl_session_timeout 1d;
ssl_session_cache shared:ssl_session_cache:10m;
ssl_session_tickets off;
# To use Let's Encrypt certificate
ssl_certificate /etc/letsencrypt/live/example.tld/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/example.tld/privkey.pem;
# To use Debian/Ubuntu's self-signed certificate (For testing or before issuing a certificate)
#ssl_certificate /etc/ssl/certs/ssl-cert-snakeoil.pem;
#ssl_certificate_key /etc/ssl/private/ssl-cert-snakeoil.key;
# SSL protocol settings
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384;
ssl_prefer_server_ciphers off;
ssl_stapling on;
ssl_stapling_verify on;
# Change to your upload limit
client_max_body_size 80m;
# Proxy to Node
location / {
proxy_pass http://127.0.0.1:3000;
proxy_set_header Host $host;
proxy_http_version 1.1;
proxy_redirect off;
# If it's behind another reverse proxy or CDN, remove the following.
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto https;
# For WebSocket
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
# Cache settings
proxy_cache cache1;
proxy_cache_lock on;
proxy_cache_use_stale updating;
add_header X-Cache $upstream_cache_status;
}
}

View File

@ -159,7 +159,7 @@ proxyAccount: "Proxy account"
proxyAccountDescription: "A proxy account is an account that acts as a remote follower for users under certain conditions. For example, when a user adds a remote user to the list, the remote user's activity will not be delivered to the instance if no local user is following that user, so the proxy account will follow instead." proxyAccountDescription: "A proxy account is an account that acts as a remote follower for users under certain conditions. For example, when a user adds a remote user to the list, the remote user's activity will not be delivered to the instance if no local user is following that user, so the proxy account will follow instead."
host: "Host" host: "Host"
selectUser: "Select a user" selectUser: "Select a user"
recipient: "Recipient" recipient: "Recipient(s)"
annotation: "Comments" annotation: "Comments"
federation: "Federation" federation: "Federation"
instances: "Instances" instances: "Instances"
@ -770,8 +770,8 @@ noBotProtectionWarning: "Bot protection is not configured."
configure: "Configure" configure: "Configure"
postToGallery: "Create new gallery post" postToGallery: "Create new gallery post"
gallery: "Gallery" gallery: "Gallery"
recentPosts: "Recent posts" recentPosts: "Recent pages"
popularPosts: "Popular posts" popularPosts: "Popular pages"
shareWithNote: "Share with note" shareWithNote: "Share with note"
ads: "Advertisements" ads: "Advertisements"
expiration: "Deadline" expiration: "Deadline"
@ -1094,7 +1094,7 @@ _channel:
usersCount: "{n} Participants" usersCount: "{n} Participants"
notesCount: "{n} Notes" notesCount: "{n} Notes"
_messaging: _messaging:
dms: "DMs" dms: "Private"
groups: "Groups" groups: "Groups"
_menuDisplay: _menuDisplay:
sideFull: "Side" sideFull: "Side"
@ -1264,10 +1264,10 @@ _permissions:
"read:reactions": "View your reactions" "read:reactions": "View your reactions"
"write:reactions": "Edit your reactions" "write:reactions": "Edit your reactions"
"write:votes": "Vote on a poll" "write:votes": "Vote on a poll"
"read:pages": "View your pages" "read:pages": "View your page"
"write:pages": "Edit or delete your pages" "write:pages": "Edit or delete your page"
"read:page-likes": "View your likes on pages" "read:page-likes": "View your likes on page"
"write:page-likes": "Edit your likes on pages" "write:page-likes": "Edit your likes on page"
"read:user-groups": "View your user groups" "read:user-groups": "View your user groups"
"write:user-groups": "Edit or delete your user groups" "write:user-groups": "Edit or delete your user groups"
"read:channels": "View your channels" "read:channels": "View your channels"
@ -1441,7 +1441,7 @@ _pages:
liked: "Liked Pages" liked: "Liked Pages"
featured: "Popular" featured: "Popular"
inspector: "Inspector" inspector: "Inspector"
contents: "Contents" contents: "Content"
content: "Page block" content: "Page block"
variables: "Variables" variables: "Variables"
title: "Title" title: "Title"

View File

@ -1,12 +1,12 @@
{ {
"name": "calckey", "name": "calckey",
"version": "12.119.0-calc.14", "version": "12.119.0-calc.15",
"codename": "aqua", "codename": "aqua",
"repository": { "repository": {
"type": "git", "type": "git",
"url": "https://codeberg.org/thatonecalculator/calckey.git" "url": "https://codeberg.org/thatonecalculator/calckey.git"
}, },
"packageManager": "yarn@3.2.4", "packageManager": "yarn@3.3.0",
"workspaces": [ "workspaces": [
"packages/client", "packages/client",
"packages/backend", "packages/backend",
@ -39,10 +39,10 @@
"lodash": "^4.17.21" "lodash": "^4.17.21"
}, },
"dependencies": { "dependencies": {
"@bull-board/api": "^4.6.3", "@bull-board/api": "^4.6.4",
"@bull-board/ui": "^4.6.3", "@bull-board/ui": "^4.6.4",
"@tensorflow/tfjs": "^3.21.0", "@tensorflow/tfjs": "^3.21.0",
"eslint": "^8.27.0", "eslint": "^8.28.0",
"execa": "5.1.1", "execa": "5.1.1",
"gulp": "4.0.2", "gulp": "4.0.2",
"gulp-cssnano": "2.1.3", "gulp-cssnano": "2.1.3",
@ -55,13 +55,13 @@
"seedrandom": "^3.0.5" "seedrandom": "^3.0.5"
}, },
"devDependencies": { "devDependencies": {
"@types/gulp": "4.0.9", "@types/gulp": "4.0.10",
"@types/gulp-rename": "2.0.1", "@types/gulp-rename": "2.0.1",
"@typescript-eslint/parser": "5.42.1", "@typescript-eslint/parser": "5.43.0",
"cross-env": "7.0.3", "cross-env": "7.0.3",
"cypress": "10.11.0", "cypress": "10.11.0",
"start-server-and-test": "1.14.0", "start-server-and-test": "1.14.0",
"typescript": "4.8.4", "typescript": "4.9.3",
"vue-eslint-parser": "^9.1.0" "vue-eslint-parser": "^9.1.0"
} }
} }

View File

@ -0,0 +1,8 @@
export class Page1668828368510 {
async up(queryRunner) {
await queryRunner.query(`ALTER TABLE "page" ADD "isPublic" boolean NOT NULL DEFAULT true`);
}
async down(queryRunner) {
await queryRunner.query(`ALTER TABLE "page" DROP COLUMN "isPublic"`);
}
}

View File

@ -0,0 +1,11 @@
export class FixCalckeyAgain1668831378728 {
name = 'FixCalckeyAgain1668831378728'
async up(queryRunner) {
await queryRunner.query(`UPDATE "meta" SET "useStarForReactionFallback" = TRUE`);
}
async down(queryRunner) {
await queryRunner.query(`UPDATE "meta" SET "useStarForReactionFallback" = FALSE`);
}
}

View File

@ -21,9 +21,9 @@
"@tensorflow/tfjs-node": "3.21.1" "@tensorflow/tfjs-node": "3.21.1"
}, },
"dependencies": { "dependencies": {
"@bull-board/api": "^4.6.3", "@bull-board/api": "^4.6.4",
"@bull-board/koa": "^4.6.3", "@bull-board/koa": "^4.6.4",
"@bull-board/ui": "^4.6.3", "@bull-board/ui": "^4.6.4",
"@discordapp/twemoji": "14.0.2", "@discordapp/twemoji": "14.0.2",
"@elastic/elasticsearch": "7.17.0", "@elastic/elasticsearch": "7.17.0",
"@koa/cors": "3.4.3", "@koa/cors": "3.4.3",
@ -32,15 +32,15 @@
"@peertube/http-signature": "1.7.0", "@peertube/http-signature": "1.7.0",
"@sinonjs/fake-timers": "9.1.2", "@sinonjs/fake-timers": "9.1.2",
"@syuilo/aiscript": "0.11.1", "@syuilo/aiscript": "0.11.1",
"ajv": "8.11.0", "ajv": "8.11.2",
"archiver": "5.3.1", "archiver": "5.3.1",
"autobind-decorator": "2.4.0", "autobind-decorator": "2.4.0",
"autwh": "0.1.0", "autwh": "0.1.0",
"aws-sdk": "2.1253.0", "aws-sdk": "2.1258.0",
"bcryptjs": "2.4.3", "bcryptjs": "2.4.3",
"blurhash": "1.1.5", "blurhash": "1.1.5",
"bull": "4.10.1", "bull": "4.10.1",
"cacheable-lookup": "6.1.0", "cacheable-lookup": "7.0.0",
"cbor": "8.1.0", "cbor": "8.1.0",
"chalk": "5.1.2", "chalk": "5.1.2",
"chalk-template": "0.4.0", "chalk-template": "0.4.0",
@ -54,10 +54,10 @@
"feed": "4.2.2", "feed": "4.2.2",
"file-type": "17.1.6", "file-type": "17.1.6",
"fluent-ffmpeg": "2.1.2", "fluent-ffmpeg": "2.1.2",
"got": "12.5.2", "got": "12.5.3",
"hpagent": "0.1.2", "hpagent": "0.1.2",
"ioredis": "4.28.5", "ioredis": "4.28.5",
"ip-cidr": "3.0.10", "ip-cidr": "3.0.11",
"is-svg": "4.3.2", "is-svg": "4.3.2",
"js-yaml": "4.1.0", "js-yaml": "4.1.0",
"jsdom": "20.0.2", "jsdom": "20.0.2",
@ -83,7 +83,7 @@
"node-fetch": "3.3.0", "node-fetch": "3.3.0",
"nodemailer": "6.8.0", "nodemailer": "6.8.0",
"nsfwjs": "2.4.2", "nsfwjs": "2.4.2",
"oauth": "^0.9.15", "oauth": "^0.10.0",
"os-utils": "0.0.14", "os-utils": "0.0.14",
"parse5": "7.1.1", "parse5": "7.1.1",
"pg": "8.8.0", "pg": "8.8.0",
@ -111,7 +111,7 @@
"stringz": "2.1.0", "stringz": "2.1.0",
"summaly": "2.7.0", "summaly": "2.7.0",
"syslog-pro": "1.0.0", "syslog-pro": "1.0.0",
"systeminformation": "5.12.14", "systeminformation": "5.13.5",
"tesseract.js": "^3.0.3", "tesseract.js": "^3.0.3",
"tinycolor2": "1.4.2", "tinycolor2": "1.4.2",
"tmp": "0.2.1", "tmp": "0.2.1",
@ -130,7 +130,7 @@
"xev": "3.0.2" "xev": "3.0.2"
}, },
"devDependencies": { "devDependencies": {
"@redocly/openapi-core": "1.0.0-beta.112", "@redocly/openapi-core": "1.0.0-beta.114",
"@types/bcryptjs": "2.4.2", "@types/bcryptjs": "2.4.2",
"@types/bull": "3.15.9", "@types/bull": "3.15.9",
"@types/cbor": "6.0.0", "@types/cbor": "6.0.0",
@ -165,7 +165,7 @@
"@types/rename": "1.0.4", "@types/rename": "1.0.4",
"@types/sanitize-html": "2.6.2", "@types/sanitize-html": "2.6.2",
"@types/semver": "7.3.13", "@types/semver": "7.3.13",
"@types/sharp": "0.30.5", "@types/sharp": "0.31.0",
"@types/sinonjs__fake-timers": "8.1.2", "@types/sinonjs__fake-timers": "8.1.2",
"@types/speakeasy": "2.0.7", "@types/speakeasy": "2.0.7",
"@types/tinycolor2": "1.4.3", "@types/tinycolor2": "1.4.3",
@ -174,12 +174,12 @@
"@types/web-push": "3.3.2", "@types/web-push": "3.3.2",
"@types/websocket": "1.0.5", "@types/websocket": "1.0.5",
"@types/ws": "8.5.3", "@types/ws": "8.5.3",
"@typescript-eslint/eslint-plugin": "5.42.1", "@typescript-eslint/eslint-plugin": "5.43.0",
"@typescript-eslint/parser": "5.42.1", "@typescript-eslint/parser": "5.43.0",
"cross-env": "7.0.3", "cross-env": "7.0.3",
"eslint": "8.27.0", "eslint": "8.28.0",
"eslint-plugin-import": "2.26.0", "eslint-plugin-import": "2.26.0",
"execa": "6.1.0", "execa": "6.1.0",
"typescript": "4.8.4" "typescript": "4.9.3"
} }
} }

View File

@ -0,0 +1,18 @@
// structredCloneが遅いため
// SEE: http://var.blog.jp/archives/86038606.html
type Cloneable = string | number | boolean | null | { [key: string]: Cloneable } | Cloneable[];
export function deepClone<T extends Cloneable>(x: T): T {
if (typeof x === 'object') {
if (x === null) return x;
if (Array.isArray(x)) return x.map(deepClone) as T;
const obj = {} as Record<string, Cloneable>;
for (const [k, v] of Object.entries(x)) {
obj[k] = deepClone(v);
}
return obj as T;
} else {
return x;
}
}

View File

@ -40,6 +40,9 @@ export class Page {
@Column('boolean') @Column('boolean')
public alignCenter: boolean; public alignCenter: boolean;
@Column('boolean')
public isPublic: boolean;
@Column('boolean', { @Column('boolean', {
default: false, default: false,
}) })

View File

@ -9,6 +9,8 @@ import { query, appendQuery } from '@/prelude/url.js';
import { Meta } from '@/models/entities/meta.js'; import { Meta } from '@/models/entities/meta.js';
import { fetchMeta } from '@/misc/fetch-meta.js'; import { fetchMeta } from '@/misc/fetch-meta.js';
import { Users, DriveFolders } from '../index.js'; import { Users, DriveFolders } from '../index.js';
import { deepClone } from '@/misc/clone.js';
type PackOptions = { type PackOptions = {
detail?: boolean, detail?: boolean,
@ -29,9 +31,7 @@ export const DriveFileRepository = db.getRepository(DriveFile).extend({
getPublicProperties(file: DriveFile): DriveFile['properties'] { getPublicProperties(file: DriveFile): DriveFile['properties'] {
if (file.properties.orientation != null) { if (file.properties.orientation != null) {
// TODO const properties = deepClone(file.properties);
//const properties = structuredClone(file.properties);
const properties = JSON.parse(JSON.stringify(file.properties));
if (file.properties.orientation >= 5) { if (file.properties.orientation >= 5) {
[properties.width, properties.height] = [properties.height, properties.width]; [properties.width, properties.height] = [properties.height, properties.width];
} }

View File

@ -65,6 +65,7 @@ export const PageRepository = db.getRepository(Page).extend({
content: page.content, content: page.content,
variables: page.variables, variables: page.variables,
title: page.title, title: page.title,
isPublic: page.isPublic,
name: page.name, name: page.name,
summary: page.summary, summary: page.summary,
hideTitleWhenPinned: page.hideTitleWhenPinned, hideTitleWhenPinned: page.hideTitleWhenPinned,

View File

@ -47,5 +47,9 @@ export const packedPageSchema = {
ref: 'UserLite', ref: 'UserLite',
optional: false, nullable: false, optional: false, nullable: false,
}, },
isPublic: {
type: 'boolean',
optional: false, nullable: false,
},
}, },
} as const; } as const;

View File

@ -53,6 +53,7 @@ export const paramDef = {
eyeCatchingImageId: { type: 'string', format: 'misskey:id', nullable: true }, eyeCatchingImageId: { type: 'string', format: 'misskey:id', nullable: true },
font: { type: 'string', enum: ['serif', 'sans-serif'], default: 'sans-serif' }, font: { type: 'string', enum: ['serif', 'sans-serif'], default: 'sans-serif' },
alignCenter: { type: 'boolean', default: false }, alignCenter: { type: 'boolean', default: false },
isPublic: { type: 'boolean', default: true },
hideTitleWhenPinned: { type: 'boolean', default: false }, hideTitleWhenPinned: { type: 'boolean', default: false },
}, },
required: ['title', 'name', 'content', 'variables', 'script'], required: ['title', 'name', 'content', 'variables', 'script'],
@ -97,6 +98,7 @@ export default define(meta, paramDef, async (ps, user) => {
alignCenter: ps.alignCenter, alignCenter: ps.alignCenter,
hideTitleWhenPinned: ps.hideTitleWhenPinned, hideTitleWhenPinned: ps.hideTitleWhenPinned,
font: ps.font, font: ps.font,
isPublic: ps.isPublic,
})).then(x => Pages.findOneByOrFail(x.identifiers[0])); })).then(x => Pages.findOneByOrFail(x.identifiers[0]));
return await Pages.pack(page); return await Pages.pack(page);

View File

@ -67,5 +67,9 @@ export default define(meta, paramDef, async (ps, user) => {
throw new ApiError(meta.errors.noSuchPage); throw new ApiError(meta.errors.noSuchPage);
} }
if (!page.isPublic && (user == null || (page.userId !== user.id))) {
throw new ApiError(meta.errors.noSuchPage);
}
return await Pages.pack(page, user); return await Pages.pack(page, user);
}); });

View File

@ -60,6 +60,7 @@ export const paramDef = {
font: { type: 'string', enum: ['serif', 'sans-serif'] }, font: { type: 'string', enum: ['serif', 'sans-serif'] },
alignCenter: { type: 'boolean' }, alignCenter: { type: 'boolean' },
hideTitleWhenPinned: { type: 'boolean' }, hideTitleWhenPinned: { type: 'boolean' },
isPublic: { type: 'boolean' },
}, },
required: ['pageId', 'title', 'name', 'content', 'variables', 'script'], required: ['pageId', 'title', 'name', 'content', 'variables', 'script'],
} as const; } as const;
@ -104,6 +105,7 @@ export default define(meta, paramDef, async (ps, user) => {
content: ps.content, content: ps.content,
variables: ps.variables, variables: ps.variables,
script: ps.script, script: ps.script,
isPublic: ps.isPublic,
alignCenter: ps.alignCenter === undefined ? page.alignCenter : ps.alignCenter, alignCenter: ps.alignCenter === undefined ? page.alignCenter : ps.alignCenter,
hideTitleWhenPinned: ps.hideTitleWhenPinned === undefined ? page.hideTitleWhenPinned : ps.hideTitleWhenPinned, hideTitleWhenPinned: ps.hideTitleWhenPinned === undefined ? page.hideTitleWhenPinned : ps.hideTitleWhenPinned,
font: ps.font === undefined ? page.font : ps.font, font: ps.font === undefined ? page.font : ps.font,

View File

@ -34,7 +34,8 @@ export const paramDef = {
export default define(meta, paramDef, async (ps, user) => { export default define(meta, paramDef, async (ps, user) => {
const query = makePaginationQuery(Pages.createQueryBuilder('page'), ps.sinceId, ps.untilId) const query = makePaginationQuery(Pages.createQueryBuilder('page'), ps.sinceId, ps.untilId)
.andWhere('page.userId = :userId', { userId: ps.userId }) .andWhere('page.userId = :userId', { userId: ps.userId })
.andWhere('page.visibility = \'public\''); .andWhere('page.visibility = \'public\'')
.andWhere('page.isPublic = true');
const pages = await query const pages = await query
.take(ps.limit) .take(ps.limit)

View File

@ -19,8 +19,8 @@
"blurhash": "1.1.5", "blurhash": "1.1.5",
"broadcast-channel": "4.18.1", "broadcast-channel": "4.18.1",
"browser-image-resizer": "https://github.com/misskey-dev/browser-image-resizer.git#commit=0380d12c8e736788ea7f4e6e985175521ea7b23c", "browser-image-resizer": "https://github.com/misskey-dev/browser-image-resizer.git#commit=0380d12c8e736788ea7f4e6e985175521ea7b23c",
"chart.js": "3.9.1", "chart.js": "4.0.1",
"chartjs-adapter-date-fns": "2.0.0", "chartjs-adapter-date-fns": "2.0.1",
"chartjs-plugin-gradient": "0.5.1", "chartjs-plugin-gradient": "0.5.1",
"chartjs-plugin-zoom": "1.2.1", "chartjs-plugin-zoom": "1.2.1",
"compare-versions": "5.0.1", "compare-versions": "5.0.1",
@ -31,7 +31,7 @@
"idb-keyval": "6.2.0", "idb-keyval": "6.2.0",
"insert-text-at-cursor": "0.3.0", "insert-text-at-cursor": "0.3.0",
"json5": "2.2.1", "json5": "2.2.1",
"katex": "0.15.6", "katex": "0.16.3",
"matter-js": "0.18.0", "matter-js": "0.18.0",
"mfm-js": "0.23.0", "mfm-js": "0.23.0",
"misskey-js": "0.0.14", "misskey-js": "0.0.14",
@ -48,16 +48,16 @@
"swiper": "^8.4.4", "swiper": "^8.4.4",
"syuilo-password-strength": "0.0.1", "syuilo-password-strength": "0.0.1",
"textarea-caret": "3.1.0", "textarea-caret": "3.1.0",
"three": "0.144.0", "three": "0.146.0",
"throttle-debounce": "5.0.0", "throttle-debounce": "5.0.0",
"tinycolor2": "1.4.2", "tinycolor2": "1.4.2",
"tsc-alias": "1.7.1", "tsc-alias": "1.7.1",
"tsconfig-paths": "4.1.0", "tsconfig-paths": "4.1.0",
"twemoji-parser": "14.0.0", "twemoji-parser": "14.0.0",
"typescript": "4.8.4", "typescript": "4.9.3",
"uuid": "9.0.0", "uuid": "9.0.0",
"vanilla-tilt": "1.7.3", "vanilla-tilt": "1.7.3",
"vite": "^3.2.3", "vite": "^3.2.4",
"vue": "3.2.45", "vue": "3.2.45",
"vue-isyourpasswordsafe": "^2.0.0", "vue-isyourpasswordsafe": "^2.0.0",
"vue-plyr": "^7.0.0", "vue-plyr": "^7.0.0",
@ -67,7 +67,7 @@
"devDependencies": { "devDependencies": {
"@types/escape-regexp": "0.0.1", "@types/escape-regexp": "0.0.1",
"@types/glob": "8.0.0", "@types/glob": "8.0.0",
"@types/gulp": "4.0.9", "@types/gulp": "4.0.10",
"@types/gulp-rename": "2.0.1", "@types/gulp-rename": "2.0.1",
"@types/katex": "0.14.0", "@types/katex": "0.14.0",
"@types/matter-js": "0.18.2", "@types/matter-js": "0.18.2",
@ -76,11 +76,11 @@
"@types/throttle-debounce": "5.0.0", "@types/throttle-debounce": "5.0.0",
"@types/tinycolor2": "1.4.3", "@types/tinycolor2": "1.4.3",
"@types/uuid": "8.3.4", "@types/uuid": "8.3.4",
"@typescript-eslint/eslint-plugin": "5.42.1", "@typescript-eslint/eslint-plugin": "5.43.0",
"@typescript-eslint/parser": "5.42.1", "@typescript-eslint/parser": "5.43.0",
"cross-env": "7.0.3", "cross-env": "7.0.3",
"cypress": "10.11.0", "cypress": "10.11.0",
"eslint": "8.27.0", "eslint": "8.28.0",
"eslint-plugin-import": "2.26.0", "eslint-plugin-import": "2.26.0",
"eslint-plugin-vue": "9.7.0", "eslint-plugin-vue": "9.7.0",
"rollup": "2.79.1", "rollup": "2.79.1",

View File

@ -33,7 +33,7 @@
<MkButton v-if="showCancelButton || input || select" inline @click="cancel">{{ i18n.ts.cancel }}</MkButton> <MkButton v-if="showCancelButton || input || select" inline @click="cancel">{{ i18n.ts.cancel }}</MkButton>
</div> </div>
<div v-else> <div v-else>
<MkButton v-if="showOkButton" inline primary :autofocus="!input && !select" @click="ok">{{ (showCancelButton || input || select) ? i18n.ts.ok : i18n.ts.yes }}</MkButton> <MkButton v-if="showOkButton" inline primary :autofocus="!input && !select" @click="ok">{{ i18n.ts.yes }}</MkButton>
<MkButton v-if="showCancelButton || input || select" inline @click="cancel">{{ i18n.ts.no }}</MkButton> <MkButton v-if="showCancelButton || input || select" inline @click="cancel">{{ i18n.ts.no }}</MkButton>
</div> </div>
</div> </div>

View File

@ -1,5 +1,6 @@
<template> <template>
<button class="kpoogebi _button" <button
class="kpoogebi _button"
:class="{ wait, active: isFollowing || hasPendingFollowRequestFromYou, full, large }" :class="{ wait, active: isFollowing || hasPendingFollowRequestFromYou, full, large }"
:disabled="wait" :disabled="wait"
@click="onClick" @click="onClick"
@ -8,7 +9,8 @@
<template v-if="hasPendingFollowRequestFromYou && user.isLocked"> <template v-if="hasPendingFollowRequestFromYou && user.isLocked">
<span v-if="full">{{ i18n.ts.followRequestPending }}</span><i class="ph-hourglass-medium-bold ph-lg"></i> <span v-if="full">{{ i18n.ts.followRequestPending }}</span><i class="ph-hourglass-medium-bold ph-lg"></i>
</template> </template>
<template v-else-if="hasPendingFollowRequestFromYou && !user.isLocked"> <!-- つまりリモートフォローの場合 --> <template v-else-if="hasPendingFollowRequestFromYou && !user.isLocked">
<!-- つまりリモートフォローの場合 -->
<span v-if="full">{{ i18n.ts.processing }}</span><i class="ph-circle-notch-bold ph-lg fa-pulse"></i> <span v-if="full">{{ i18n.ts.processing }}</span><i class="ph-circle-notch-bold ph-lg fa-pulse"></i>
</template> </template>
<template v-else-if="isFollowing"> <template v-else-if="isFollowing">
@ -29,7 +31,7 @@
<script lang="ts" setup> <script lang="ts" setup>
import { onBeforeUnmount, onMounted } from 'vue'; import { onBeforeUnmount, onMounted } from 'vue';
import * as Misskey from 'misskey-js'; import type * as Misskey from 'misskey-js';
import * as os from '@/os'; import * as os from '@/os';
import { stream } from '@/stream'; import { stream } from '@/stream';
import { i18n } from '@/i18n'; import { i18n } from '@/i18n';
@ -38,7 +40,7 @@ const props = withDefaults(defineProps<{
user: Misskey.entities.UserDetailed, user: Misskey.entities.UserDetailed,
full?: boolean, full?: boolean,
large?: boolean, large?: boolean,
}>(), { }>(), {
full: false, full: false,
large: false, large: false,
}); });
@ -50,7 +52,7 @@ const connection = stream.useChannel('main');
if (props.user.isFollowing == null) { if (props.user.isFollowing == null) {
os.api('users/show', { os.api('users/show', {
userId: props.user.id userId: props.user.id,
}) })
.then(onFollowChange); .then(onFollowChange);
} }
@ -75,17 +77,17 @@ async function onClick() {
if (canceled) return; if (canceled) return;
await os.api('following/delete', { await os.api('following/delete', {
userId: props.user.id userId: props.user.id,
}); });
} else { } else {
if (hasPendingFollowRequestFromYou) { if (hasPendingFollowRequestFromYou) {
await os.api('following/requests/cancel', { await os.api('following/requests/cancel', {
userId: props.user.id userId: props.user.id,
}); });
hasPendingFollowRequestFromYou = false; hasPendingFollowRequestFromYou = false;
} else { } else {
await os.api('following/create', { await os.api('following/create', {
userId: props.user.id userId: props.user.id,
}); });
hasPendingFollowRequestFromYou = true; hasPendingFollowRequestFromYou = true;
} }

View File

@ -49,7 +49,7 @@
<div v-if="translating || translation" class="translation"> <div v-if="translating || translation" class="translation">
<MkLoading v-if="translating" mini/> <MkLoading v-if="translating" mini/>
<div v-else class="translated"> <div v-else class="translated">
<b>{{ $t('translatedFrom', { x: translation.sourceLang }) }}: </b> <b>{{ i18n.t('translatedFrom', { x: translation.sourceLang }) }}: </b>
<Mfm :text="translation.text" :author="appearNote.user" :i="$i" :custom-emojis="appearNote.emojis"/> <Mfm :text="translation.text" :author="appearNote.user" :i="$i" :custom-emojis="appearNote.emojis"/>
</div> </div>
</div> </div>
@ -104,9 +104,10 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { computed, inject, onMounted, onUnmounted, reactive, ref, Ref } from 'vue'; import { computed, inject, onMounted, onUnmounted, reactive, ref } from 'vue';
import * as mfm from 'mfm-js'; import * as mfm from 'mfm-js';
import * as misskey from 'misskey-js'; import type { Ref } from 'vue';
import type * as misskey from 'misskey-js';
import MkNoteSub from '@/components/MkNoteSub.vue'; import MkNoteSub from '@/components/MkNoteSub.vue';
import XNoteHeader from '@/components/MkNoteHeader.vue'; import XNoteHeader from '@/components/MkNoteHeader.vue';
import XNoteSimple from '@/components/MkNoteSimple.vue'; import XNoteSimple from '@/components/MkNoteSimple.vue';
@ -134,6 +135,7 @@ import { i18n } from '@/i18n';
import { getNoteMenu } from '@/scripts/get-note-menu'; import { getNoteMenu } from '@/scripts/get-note-menu';
import { useNoteCapture } from '@/scripts/use-note-capture'; import { useNoteCapture } from '@/scripts/use-note-capture';
import { notePage } from '@/filters/note'; import { notePage } from '@/filters/note';
import { deepClone } from '@/scripts/clone';
const router = useRouter(); const router = useRouter();
@ -144,12 +146,12 @@ const props = defineProps<{
const inChannel = inject('inChannel', null); const inChannel = inject('inChannel', null);
let note = $ref(JSON.parse(JSON.stringify(props.note))); let note = $ref(deepClone(props.note));
// plugin // plugin
if (noteViewInterruptors.length > 0) { if (noteViewInterruptors.length > 0) {
onMounted(async () => { onMounted(async () => {
let result = JSON.parse(JSON.stringify(note)); let result = deepClone(note);
for (const interruptor of noteViewInterruptors) { for (const interruptor of noteViewInterruptors) {
result = await interruptor.handler(result); result = await interruptor.handler(result);
} }
@ -432,7 +434,9 @@ function readPromo() {
width: 58px; width: 58px;
height: 58px; height: 58px;
position: sticky; position: sticky;
top: var(--stickyTop, 0px); /* For some reason this breaks avatar
positions on notes, commenting it for now */
/* top: var(--stickyTop, 0px); */
left: 0; left: 0;
} }

View File

@ -60,7 +60,7 @@
<div v-if="translating || translation" class="translation"> <div v-if="translating || translation" class="translation">
<MkLoading v-if="translating" mini/> <MkLoading v-if="translating" mini/>
<div v-else class="translated"> <div v-else class="translated">
<b>{{ $t('translatedFrom', { x: translation.sourceLang }) }}: </b> <b>{{ i18n.t('translatedFrom', { x: translation.sourceLang }) }}: </b>
<Mfm :text="translation.text" :author="appearNote.user" :i="$i" :custom-emojis="appearNote.emojis"/> <Mfm :text="translation.text" :author="appearNote.user" :i="$i" :custom-emojis="appearNote.emojis"/>
</div> </div>
</div> </div>
@ -143,6 +143,7 @@ import { $i } from '@/account';
import { i18n } from '@/i18n'; import { i18n } from '@/i18n';
import { getNoteMenu } from '@/scripts/get-note-menu'; import { getNoteMenu } from '@/scripts/get-note-menu';
import { useNoteCapture } from '@/scripts/use-note-capture'; import { useNoteCapture } from '@/scripts/use-note-capture';
import { deepClone } from '@/scripts/clone';
const router = useRouter(); const router = useRouter();
@ -153,12 +154,12 @@ const props = defineProps<{
const inChannel = inject('inChannel', null); const inChannel = inject('inChannel', null);
let note = $ref(JSON.parse(JSON.stringify(props.note))); let note = $ref(deepClone(props.note));
// plugin // plugin
if (noteViewInterruptors.length > 0) { if (noteViewInterruptors.length > 0) {
onMounted(async () => { onMounted(async () => {
let result = JSON.parse(JSON.stringify(note)); let result = deepClone(note);
for (const interruptor of noteViewInterruptors) { for (const interruptor of noteViewInterruptors) {
result = await interruptor.handler(result); result = await interruptor.handler(result);
} }

View File

@ -5,7 +5,7 @@
<MkAvatar v-else-if="notification.user" class="icon" :user="notification.user"/> <MkAvatar v-else-if="notification.user" class="icon" :user="notification.user"/>
<img v-else-if="notification.icon" class="icon" :src="notification.icon" alt=""/> <img v-else-if="notification.icon" class="icon" :src="notification.icon" alt=""/>
<div class="sub-icon" :class="notification.type"> <div class="sub-icon" :class="notification.type">
<i v-if="notification.type === 'follow'" class="ph-plus-bold"></i> <i v-if="notification.type === 'follow'" class="ph-hand-waving-bold"></i>
<i v-else-if="notification.type === 'receiveFollowRequest'" class="ph-clock-bold"></i> <i v-else-if="notification.type === 'receiveFollowRequest'" class="ph-clock-bold"></i>
<i v-else-if="notification.type === 'followRequestAccepted'" class="ph-check-bold"></i> <i v-else-if="notification.type === 'followRequestAccepted'" class="ph-check-bold"></i>
<i v-else-if="notification.type === 'groupInvited'" class="ph-identification-card-bold"></i> <i v-else-if="notification.type === 'groupInvited'" class="ph-identification-card-bold"></i>

View File

@ -6,12 +6,12 @@
<span> <span>
<template v-if="choice.isVoted"><i class="ph-check-bold ph-lg"></i></template> <template v-if="choice.isVoted"><i class="ph-check-bold ph-lg"></i></template>
<Mfm :text="choice.text" :plain="true" :custom-emojis="note.emojis"/> <Mfm :text="choice.text" :plain="true" :custom-emojis="note.emojis"/>
<span v-if="showResult" class="votes">({{ $t('_poll.votesCount', { n: choice.votes }) }})</span> <span v-if="showResult" class="votes">({{ i18n.t('_poll.votesCount', { n: choice.votes }) }})</span>
</span> </span>
</li> </li>
</ul> </ul>
<p v-if="!readOnly"> <p v-if="!readOnly">
<span>{{ $t('_poll.totalVotes', { n: total }) }}</span> <span>{{ i18n.t('_poll.totalVotes', { n: total }) }}</span>
<span> · </span> <span> · </span>
<a v-if="!closed && !isVoted" @click="showResult = !showResult">{{ showResult ? i18n.ts._poll.vote : i18n.ts._poll.showResult }}</a> <a v-if="!closed && !isVoted" @click="showResult = !showResult">{{ showResult ? i18n.ts._poll.vote : i18n.ts._poll.showResult }}</a>
<span v-if="isVoted">{{ i18n.ts._poll.voted }}</span> <span v-if="isVoted">{{ i18n.ts._poll.voted }}</span>

View File

@ -5,7 +5,7 @@
</p> </p>
<ul> <ul>
<li v-for="(choice, i) in choices" :key="i"> <li v-for="(choice, i) in choices" :key="i">
<MkInput class="input" small :model-value="choice" :placeholder="$t('_poll.choiceN', { n: i + 1 })" @update:modelValue="onInput(i, $event)"> <MkInput class="input" small :model-value="choice" :placeholder="i18n.t('_poll.choiceN', { n: i + 1 })" @update:modelValue="onInput(i, $event)">
</MkInput> </MkInput>
<button class="_button" @click="remove(i)"> <button class="_button" @click="remove(i)">
<i class="ph-x-bold ph-lg"></i> <i class="ph-x-bold ph-lg"></i>

View File

@ -36,7 +36,7 @@
<MkAcct :user="u"/> <MkAcct :user="u"/>
<button class="_button" @click="removeVisibleUser(u)"><i class="ph-x-bold ph-lg"></i></button> <button class="_button" @click="removeVisibleUser(u)"><i class="ph-x-bold ph-lg"></i></button>
</span> </span>
<button class="_buttonPrimary" @click="addVisibleUser"><i class="ph-plus-bold ph-lg ph-fw ph-lg"></i></button> <button class="_button" @click="addVisibleUser"><i class="ph-plus-bold ph-md ph-fw ph-lg"></i></button>
</div> </div>
</div> </div>
<MkInfo v-if="hasNotSpecifiedMentions" warn class="hasNotSpecifiedMentions">{{ i18n.ts.notSpecifiedMentionWarning }} - <button class="_textButton" @click="addMissingMention()">{{ i18n.ts.add }}</button></MkInfo> <MkInfo v-if="hasNotSpecifiedMentions" warn class="hasNotSpecifiedMentions">{{ i18n.ts.notSpecifiedMentionWarning }} - <button class="_textButton" @click="addMissingMention()">{{ i18n.ts.add }}</button></MkInfo>
@ -89,6 +89,7 @@ import { i18n } from '@/i18n';
import { instance } from '@/instance'; import { instance } from '@/instance';
import { $i, getAccounts, openAccountMenu as openAccountMenu_ } from '@/account'; import { $i, getAccounts, openAccountMenu as openAccountMenu_ } from '@/account';
import { uploadFile } from '@/scripts/upload'; import { uploadFile } from '@/scripts/upload';
import { deepClone } from '@/scripts/clone';
const modal = inject('modal'); const modal = inject('modal');
@ -458,7 +459,7 @@ async function onPaste(ev: ClipboardEvent) {
if (!props.renote && !quoteId && paste.startsWith(url + '/notes/')) { if (!props.renote && !quoteId && paste.startsWith(url + '/notes/')) {
ev.preventDefault(); ev.preventDefault();
os.confirm({ os.yesno({
type: 'info', type: 'info',
text: i18n.ts.quoteQuestion, text: i18n.ts.quoteQuestion,
}).then(({ canceled }) => { }).then(({ canceled }) => {
@ -575,7 +576,7 @@ async function post() {
// plugin // plugin
if (notePostInterruptors.length > 0) { if (notePostInterruptors.length > 0) {
for (const interruptor of notePostInterruptors) { for (const interruptor of notePostInterruptors) {
postData = await interruptor.handler(JSON.parse(JSON.stringify(postData))); postData = await interruptor.handler(deepClone(postData));
} }
} }
@ -832,7 +833,7 @@ onMounted(() => {
padding: 6px 24px; padding: 6px 24px;
margin-bottom: 8px; margin-bottom: 8px;
overflow: auto; overflow: auto;
white-space: nowrap; line-height: 2rem;
> .visibleUsers { > .visibleUsers {
display: inline; display: inline;
@ -840,15 +841,19 @@ onMounted(() => {
font-size: 14px; font-size: 14px;
> button { > button {
padding: 4px; padding: 2px;
border-radius: 8px; border-radius: 8px;
> i {
transform: translateX(2px);
}
} }
> span { > span {
margin-right: 14px; margin: 0.3rem;
padding: 8px 0 8px 8px; padding: 4px 0 4px 4px;
border-radius: 8px; border-radius: 999px;
background: var(--X4); background: var(--X3);
> button { > button {
padding: 4px 8px; padding: 4px 8px;

View File

@ -41,9 +41,9 @@
</div> </div>
</div> </div>
<div class="social _section"> <div class="social _section">
<a v-if="meta && meta.enableTwitterIntegration" class="_borderButton _gap" :href="`${apiUrl}/signin/twitter`"><i class="fab fa-twitter" style="margin-right: 4px;"></i>{{ $t('signinWith', { x: 'Twitter' }) }}</a> <a v-if="meta && meta.enableTwitterIntegration" class="_borderButton _gap" :href="`${apiUrl}/signin/twitter`"><i class="fab fa-twitter" style="margin-right: 4px;"></i>{{ i18n.t('signinWith', { x: 'Twitter' }) }}</a>
<a v-if="meta && meta.enableGithubIntegration" class="_borderButton _gap" :href="`${apiUrl}/signin/github`"><i class="fab fa-github" style="margin-right: 4px;"></i>{{ $t('signinWith', { x: 'GitHub' }) }}</a> <a v-if="meta && meta.enableGithubIntegration" class="_borderButton _gap" :href="`${apiUrl}/signin/github`"><i class="fab fa-github" style="margin-right: 4px;"></i>{{ i18n.t('signinWith', { x: 'GitHub' }) }}</a>
<a v-if="meta && meta.enableDiscordIntegration" class="_borderButton _gap" :href="`${apiUrl}/signin/discord`"><i class="fab fa-discord" style="margin-right: 4px;"></i>{{ $t('signinWith', { x: 'Discord' }) }}</a> <a v-if="meta && meta.enableDiscordIntegration" class="_borderButton _gap" :href="`${apiUrl}/signin/discord`"><i class="fab fa-discord" style="margin-right: 4px;"></i>{{ i18n.t('signinWith', { x: 'Discord' }) }}</a>
</div> </div>
</form> </form>
</template> </template>
@ -51,6 +51,7 @@
<script lang="ts" setup> <script lang="ts" setup>
import { defineAsyncComponent } from 'vue'; import { defineAsyncComponent } from 'vue';
import { toUnicode } from 'punycode/'; import { toUnicode } from 'punycode/';
import { showSuspendedDialog } from '../scripts/show-suspended-dialog';
import MkButton from '@/components/MkButton.vue'; import MkButton from '@/components/MkButton.vue';
import MkInput from '@/components/form/input.vue'; import MkInput from '@/components/form/input.vue';
import MkInfo from '@/components/MkInfo.vue'; import MkInfo from '@/components/MkInfo.vue';
@ -58,7 +59,6 @@ import { apiUrl, host as configHost } from '@/config';
import { byteify, hexify } from '@/scripts/2fa'; import { byteify, hexify } from '@/scripts/2fa';
import * as os from '@/os'; import * as os from '@/os';
import { login } from '@/account'; import { login } from '@/account';
import { showSuspendedDialog } from '../scripts/show-suspended-dialog';
import { instance } from '@/instance'; import { instance } from '@/instance';
import { i18n } from '@/i18n'; import { i18n } from '@/i18n';
@ -85,7 +85,7 @@ const props = defineProps({
withAvatar: { withAvatar: {
type: Boolean, type: Boolean,
required: false, required: false,
default: true default: true,
}, },
autoSet: { autoSet: {
type: Boolean, type: Boolean,
@ -95,13 +95,13 @@ const props = defineProps({
message: { message: {
type: String, type: String,
required: false, required: false,
default: '' default: '',
} },
}); });
function onUsernameChange() { function onUsernameChange() {
os.api('users/show', { os.api('users/show', {
username: username username: username,
}).then(userResponse => { }).then(userResponse => {
user = userResponse; user = userResponse;
}, () => { }, () => {
@ -123,10 +123,10 @@ function queryKey() {
allowCredentials: challengeData.securityKeys.map(key => ({ allowCredentials: challengeData.securityKeys.map(key => ({
id: byteify(key.id, 'hex'), id: byteify(key.id, 'hex'),
type: 'public-key', type: 'public-key',
transports: ['usb', 'nfc', 'ble', 'internal'] transports: ['usb', 'nfc', 'ble', 'internal'],
})), })),
timeout: 60 * 1000 timeout: 60 * 1000,
} },
}).catch(() => { }).catch(() => {
queryingKey = false; queryingKey = false;
return Promise.reject(null); return Promise.reject(null);
@ -151,7 +151,7 @@ function queryKey() {
if (err === null) return; if (err === null) return;
os.alert({ os.alert({
type: 'error', type: 'error',
text: i18n.ts.signinFailed text: i18n.ts.signinFailed,
}); });
signing = false; signing = false;
}); });
@ -183,7 +183,7 @@ function onSubmit() {
password, password,
'hcaptcha-response': hCaptchaResponse, 'hcaptcha-response': hCaptchaResponse,
'g-recaptcha-response': reCaptchaResponse, 'g-recaptcha-response': reCaptchaResponse,
token: user && user.twoFactorEnabled ? token : undefined token: user && user.twoFactorEnabled ? token : undefined,
}).then(res => { }).then(res => {
emit('login', res); emit('login', res);
onLogin(res); onLogin(res);
@ -197,7 +197,7 @@ function loginFailed(err) {
os.alert({ os.alert({
type: 'error', type: 'error',
title: i18n.ts.loginFailed, title: i18n.ts.loginFailed,
text: i18n.ts.noSuchUser text: i18n.ts.noSuchUser,
}); });
break; break;
} }
@ -226,7 +226,7 @@ function loginFailed(err) {
os.alert({ os.alert({
type: 'error', type: 'error',
title: i18n.ts.loginFailed, title: i18n.ts.loginFailed,
text: JSON.stringify(err) text: JSON.stringify(err),
}); });
} }
} }

View File

@ -7,7 +7,7 @@
<MkA v-if="note.renoteId" class="rp" :to="`/notes/${note.renoteId}`">RN: ...</MkA> <MkA v-if="note.renoteId" class="rp" :to="`/notes/${note.renoteId}`">RN: ...</MkA>
</div> </div>
<div v-if="note.files.length > 0"> <div v-if="note.files.length > 0">
<summary>({{ $t('withNFiles', { n: note.files.length }) }})</summary> <summary>({{ i18n.t('withNFiles', { n: note.files.length }) }})</summary>
<XMediaList :media-list="note.files"/> <XMediaList :media-list="note.files"/>
</div> </div>
<div v-if="note.poll"> <div v-if="note.poll">

View File

@ -23,7 +23,7 @@
<div style="margin-bottom: 16px;"><b>{{ i18n.ts.permission }}</b></div> <div style="margin-bottom: 16px;"><b>{{ i18n.ts.permission }}</b></div>
<MkButton inline @click="disableAll">{{ i18n.ts.disableAll }}</MkButton> <MkButton inline @click="disableAll">{{ i18n.ts.disableAll }}</MkButton>
<MkButton inline @click="enableAll">{{ i18n.ts.enableAll }}</MkButton> <MkButton inline @click="enableAll">{{ i18n.ts.enableAll }}</MkButton>
<MkSwitch v-for="kind in (initialPermissions || kinds)" :key="kind" v-model="permissions[kind]">{{ $t(`_permissions.${kind}`) }}</MkSwitch> <MkSwitch v-for="kind in (initialPermissions || kinds)" :key="kind" v-model="permissions[kind]">{{ i18n.t(`_permissions.${kind}`) }}</MkSwitch>
</div> </div>
</XModalWindow> </XModalWindow>
</template> </template>

View File

@ -22,7 +22,7 @@ import { i18n } from '@/i18n';
export default defineComponent({ export default defineComponent({
components: { components: {
MkButton MkButton,
}, },
props: { props: {
@ -60,7 +60,7 @@ export default defineComponent({
watch(() => props.p, () => { watch(() => props.p, () => {
process(); process();
}, { }, {
immediate: true immediate: true,
}); });
const retry = () => { const retry = () => {
@ -73,6 +73,7 @@ export default defineComponent({
rejected, rejected,
result, result,
retry, retry,
i18n,
}; };
} }
}); });

View File

@ -8,7 +8,7 @@
<i v-if="relay.status === 'accepted'" class="ph-check-bold ph-lg icon accepted"></i> <i v-if="relay.status === 'accepted'" class="ph-check-bold ph-lg icon accepted"></i>
<i v-else-if="relay.status === 'rejected'" class="ph-prohibit-bold ph-lg icon rejected"></i> <i v-else-if="relay.status === 'rejected'" class="ph-prohibit-bold ph-lg icon rejected"></i>
<i v-else class="ph-clock-bold ph-lg icon requesting"></i> <i v-else class="ph-clock-bold ph-lg icon requesting"></i>
<span>{{ $t(`_relayStatus.${relay.status}`) }}</span> <span>{{ i18n.t(`_relayStatus.${relay.status}`) }}</span>
</div> </div>
<MkButton class="button" inline danger @click="remove(relay.inbox)"><i class="ph-trash-bold ph-lg"></i> {{ i18n.ts.remove }}</MkButton> <MkButton class="button" inline danger @click="remove(relay.inbox)"><i class="ph-trash-bold ph-lg"></i> {{ i18n.ts.remove }}</MkButton>
</div> </div>

View File

@ -1,6 +1,6 @@
<template> <template>
<section class="_section"> <section class="_section">
<div class="_title">{{ $t('_auth.shareAccess', { name: app.name }) }}</div> <div class="_title">{{ i18n.t('_auth.shareAccess', { name: app.name }) }}</div>
<div class="_content"> <div class="_content">
<h2>{{ app.name }}</h2> <h2>{{ app.name }}</h2>
<p class="id">{{ app.id }}</p> <p class="id">{{ app.id }}</p>
@ -9,7 +9,7 @@
<div class="_content"> <div class="_content">
<h2>{{ i18n.ts._auth.permissionAsk }}</h2> <h2>{{ i18n.ts._auth.permissionAsk }}</h2>
<ul> <ul>
<li v-for="p in app.permission" :key="p">{{ $t(`_permissions.${p}`) }}</li> <li v-for="p in app.permission" :key="p">{{ i18n.t(`_permissions.${p}`) }}</li>
</ul> </ul>
</div> </div>
<div class="_footer"> <div class="_footer">

View File

@ -15,7 +15,7 @@
<h1>{{ i18n.ts._auth.denied }}</h1> <h1>{{ i18n.ts._auth.denied }}</h1>
</div> </div>
<div v-if="state == 'accepted'" class="accepted"> <div v-if="state == 'accepted'" class="accepted">
<h1>{{ session.app.isAuthorized ? $t('already-authorized') : i18n.ts.allowed }}</h1> <h1>{{ session.app.isAuthorized ? i18n.t('already-authorized') : i18n.ts.allowed }}</h1>
<p v-if="session.app.callbackUrl">{{ i18n.ts._auth.callback }}<MkEllipsis/></p> <p v-if="session.app.callbackUrl">{{ i18n.ts._auth.callback }}<MkEllipsis/></p>
<p v-if="!session.app.callbackUrl">{{ i18n.ts._auth.pleaseGoBack }}</p> <p v-if="!session.app.callbackUrl">{{ i18n.ts._auth.pleaseGoBack }}</p>
</div> </div>
@ -47,6 +47,7 @@ export default defineComponent({
state: null, state: null,
session: null, session: null,
fetching: true, fetching: true,
i18n,
}; };
}, },
mounted() { mounted() {

View File

@ -202,11 +202,11 @@ definePageMetadata(computed(() => post ? {
> .like { > .like {
> .button { > .button {
--accent: rgb(241 97 132); --accent: #eb6f92;
--X8: rgb(241 92 128); --X8: #eb6f92;
--buttonBg: rgb(216 71 106 / 5%); --buttonBg: rgb(216 71 106 / 5%);
--buttonHoverBg: rgb(216 71 106 / 10%); --buttonHoverBg: rgb(216 71 106 / 10%);
color: #ff002f; color: #eb6f92;
::v-deep(.count) { ::v-deep(.count) {
margin-left: 0.5em; margin-left: 0.5em;

View File

@ -1,6 +1,6 @@
<template> <template>
<div v-size="{ max: [400, 500] }" class="thvuemwp" :class="{ isMe }"> <div v-size="{ max: [400, 500] }" class="thvuemwp" :class="{ isMe }">
<MkAvatar class="avatar" :user="message.user" :show-indicator="true"/> <MkAvatar v-if="!isMe" class="avatar" :user="message.user" :show-indicator="true"/>
<div class="content"> <div class="content">
<div class="balloon" :class="{ noText: message.text == null }"> <div class="balloon" :class="{ noText: message.text == null }">
<button v-if="isMe" class="delete-button" :title="i18n.ts.delete" @click="del"> <button v-if="isMe" class="delete-button" :title="i18n.ts.delete" @click="del">
@ -38,7 +38,6 @@
<script lang="ts" setup> <script lang="ts" setup>
import { } from 'vue'; import { } from 'vue';
import * as mfm from 'mfm-js'; import * as mfm from 'mfm-js';
import VuePlyr from 'vue-plyr';
import type * as Misskey from 'misskey-js'; import type * as Misskey from 'misskey-js';
import XMediaList from '@/components/MkMediaList.vue'; import XMediaList from '@/components/MkMediaList.vue';
import { extractUrlFromMfm } from '@/scripts/extract-url-from-mfm'; import { extractUrlFromMfm } from '@/scripts/extract-url-from-mfm';
@ -73,10 +72,10 @@ function del(): void {
> .avatar { > .avatar {
position: sticky; position: sticky;
top: calc(var(--stickyTop, 0px) + 16px); top: calc(var(--stickyTop, 0px) + 20px);
display: block; display: block;
width: 54px; width: 45px;
height: 54px; height: 45px;
transition: all 0.1s ease; transition: all 0.1s ease;
} }
@ -91,14 +90,7 @@ function del(): void {
min-height: 38px; min-height: 38px;
border-radius: 16px; border-radius: 16px;
max-width: 100%; max-width: 100%;
margin-left: 10rem;
&:before {
content: "";
pointer-events: none;
display: block;
position: absolute;
top: 12px;
}
& + * { & + * {
clear: both; clear: both;
@ -222,7 +214,9 @@ function del(): void {
padding-right: 32px; padding-right: 32px;
> .balloon { > .balloon {
$color: var(--messageBg); $color: var(--X4);
margin-right: 10rem;
margin-left: 0rem !important;
background: $color; background: $color;
&.noText { &.noText {

View File

@ -18,12 +18,12 @@
</div> </div>
</div> </div>
<div v-else class="_section"> <div v-else class="_section">
<div v-if="name" class="_title">{{ $t('_auth.shareAccess', { name: name }) }}</div> <div v-if="name" class="_title">{{ i18n.t('_auth.shareAccess', { name: name }) }}</div>
<div v-else class="_title">{{ i18n.ts._auth.shareAccessAsk }}</div> <div v-else class="_title">{{ i18n.ts._auth.shareAccessAsk }}</div>
<div class="_content"> <div class="_content">
<p>{{ i18n.ts._auth.permissionAsk }}</p> <p>{{ i18n.ts._auth.permissionAsk }}</p>
<ul> <ul>
<li v-for="p in _permissions" :key="p">{{ $t(`_permissions.${p}`) }}</li> <li v-for="p in _permissions" :key="p">{{ i18n.t(`_permissions.${p}`) }}</li>
</ul> </ul>
</div> </div>
<div class="_footer"> <div class="_footer">

View File

@ -21,7 +21,7 @@
<MkInput v-model="value.message"><template #label>{{ i18n.ts._pages.blocks._button._action._pushEvent.message }}</template></MkInput> <MkInput v-model="value.message"><template #label>{{ i18n.ts._pages.blocks._button._action._pushEvent.message }}</template></MkInput>
<MkSelect v-model="value.var"> <MkSelect v-model="value.var">
<template #label>{{ i18n.ts._pages.blocks._button._action._pushEvent.variable }}</template> <template #label>{{ i18n.ts._pages.blocks._button._action._pushEvent.variable }}</template>
<option :value="null">{{ $t('_pages.blocks._button._action._pushEvent.no-variable') }}</option> <option :value="null">{{ i18n.t('_pages.blocks._button._action._pushEvent.no-variable') }}</option>
<option v-for="v in hpml.getVarsByType()" :value="v.name">{{ v.name }}</option> <option v-for="v in hpml.getVarsByType()" :value="v.name">{{ v.name }}</option>
<optgroup :label="i18n.ts._pages.script.pageVariables"> <optgroup :label="i18n.ts._pages.script.pageVariables">
<option v-for="v in hpml.getPageVarsByType()" :value="v">{{ v }}</option> <option v-for="v in hpml.getPageVarsByType()" :value="v">{{ v }}</option>

View File

@ -16,8 +16,8 @@
</button> </button>
</div> </div>
</header> </header>
<p v-show="showBody" v-if="error != null" class="error">{{ $t('_pages.script.typeError', { slot: error.arg + 1, expect: $t(`script.types.${error.expect}`), actual: $t(`script.types.${error.actual}`) }) }}</p> <p v-show="showBody" v-if="error != null" class="error">{{ i18n.t('_pages.script.typeError', { slot: error.arg + 1, expect: i18n.t(`script.types.${error.expect}`), actual: i18n.t(`script.types.${error.actual}`) }) }}</p>
<p v-show="showBody" v-if="warn != null" class="warn">{{ $t('_pages.script.thereIsEmptySlot', { slot: warn.slot + 1 }) }}</p> <p v-show="showBody" v-if="warn != null" class="warn">{{ i18n.t('_pages.script.thereIsEmptySlot', { slot: warn.slot + 1 }) }}</p>
<div v-show="showBody" class="body"> <div v-show="showBody" class="body">
<slot></slot> <slot></slot>
</div> </div>
@ -26,34 +26,36 @@
<script lang="ts"> <script lang="ts">
import { defineComponent } from 'vue'; import { defineComponent } from 'vue';
import { i18n } from '@/i18n';
export default defineComponent({ export default defineComponent({
props: { props: {
expanded: { expanded: {
type: Boolean, type: Boolean,
default: true default: true,
}, },
removable: { removable: {
type: Boolean, type: Boolean,
default: true default: true,
}, },
draggable: { draggable: {
type: Boolean, type: Boolean,
default: false default: false,
}, },
error: { error: {
required: false, required: false,
default: null default: null,
}, },
warn: { warn: {
required: false, required: false,
default: null default: null,
} },
}, },
emits: ['toggle', 'remove'], emits: ['toggle', 'remove'],
data() { data() {
return { return {
showBody: this.expanded, showBody: this.expanded,
i18n,
}; };
}, },
methods: { methods: {
@ -63,8 +65,8 @@ export default defineComponent({
}, },
remove() { remove() {
this.$emit('remove'); this.$emit('remove');
} },
} },
}); });
</script> </script>

View File

@ -43,15 +43,15 @@
<section v-else-if="modelValue.type === 'fn'" class="" style="padding:0 16px 16px 16px;"> <section v-else-if="modelValue.type === 'fn'" class="" style="padding:0 16px 16px 16px;">
<MkTextarea v-model="slots"> <MkTextarea v-model="slots">
<template #label>{{ i18n.ts._pages.script.blocks._fn.slots }}</template> <template #label>{{ i18n.ts._pages.script.blocks._fn.slots }}</template>
<template #caption>{{ $t('_pages.script.blocks._fn.slots-info') }}</template> <template #caption>{{ i18n.t('_pages.script.blocks._fn.slots-info') }}</template>
</MkTextarea> </MkTextarea>
<XV v-if="modelValue.value.expression" v-model="modelValue.value.expression" :title="$t(`_pages.script.blocks._fn.arg1`)" :get-expected-type="() => null" :hpml="hpml" :fn-slots="modelValue.value.slots" :name="name"/> <XV v-if="modelValue.value.expression" v-model="modelValue.value.expression" :title="i18n.t(`_pages.script.blocks._fn.arg1`)" :get-expected-type="() => null" :hpml="hpml" :fn-slots="modelValue.value.slots" :name="name"/>
</section> </section>
<section v-else-if="modelValue.type.startsWith('fn:')" class="" style="padding:16px;"> <section v-else-if="modelValue.type.startsWith('fn:')" class="" style="padding:16px;">
<XV v-for="(x, i) in modelValue.args" :key="i" v-model="modelValue.args[i]" :title="hpml.getVarByName(modelValue.type.split(':')[1]).value.slots[i].name" :get-expected-type="() => null" :hpml="hpml" :name="name"/> <XV v-for="(x, i) in modelValue.args" :key="i" v-model="modelValue.args[i]" :title="hpml.getVarByName(modelValue.type.split(':')[1]).value.slots[i].name" :get-expected-type="() => null" :hpml="hpml" :name="name"/>
</section> </section>
<section v-else class="" style="padding:16px;"> <section v-else class="" style="padding:16px;">
<XV v-for="(x, i) in modelValue.args" :key="i" v-model="modelValue.args[i]" :title="$t(`_pages.script.blocks._${modelValue.type}.arg${i + 1}`)" :get-expected-type="() => _getExpectedType(i)" :hpml="hpml" :name="name" :fn-slots="fnSlots"/> <XV v-for="(x, i) in modelValue.args" :key="i" v-model="modelValue.args[i]" :title="i18n.t(`_pages.script.blocks._${modelValue.type}.arg${i + 1}`)" :get-expected-type="() => _getExpectedType(i)" :hpml="hpml" :name="name" :fn-slots="fnSlots"/>
</section> </section>
</XContainer> </XContainer>
</template> </template>
@ -124,7 +124,7 @@ export default defineComponent({
typeText(): any { typeText(): any {
if (this.modelValue.type === null) return null; if (this.modelValue.type === null) return null;
if (this.modelValue.type.startsWith('fn:')) return this.modelValue.type.split(':')[1]; if (this.modelValue.type.startsWith('fn:')) return this.modelValue.type.split(':')[1];
return this.$t(`_pages.script.blocks.${this.modelValue.type}`); return i18n.t(`_pages.script.blocks.${this.modelValue.type}`);
}, },
}, },

View File

@ -24,6 +24,7 @@
<template #label>{{ i18n.ts._pages.url }}</template> <template #label>{{ i18n.ts._pages.url }}</template>
</MkInput> </MkInput>
<MkSwitch v-model="isPublic" class="_formBlock">{{ i18n.ts.public }}</MkSwitch>
<MkSwitch v-model="alignCenter" class="_formBlock">{{ i18n.ts._pages.alignCenter }}</MkSwitch> <MkSwitch v-model="alignCenter" class="_formBlock">{{ i18n.ts._pages.alignCenter }}</MkSwitch>
<MkSelect v-model="font" class="_formBlock"> <MkSelect v-model="font" class="_formBlock">
@ -47,7 +48,6 @@
<div v-else-if="tab === 'contents'"> <div v-else-if="tab === 'contents'">
<div> <div>
<XBlocks v-model="content" class="content" :hpml="hpml"/> <XBlocks v-model="content" class="content" :hpml="hpml"/>
<MkButton v-if="!readonly" @click="add()"><i class="ph-plus-bold ph-lg"></i></MkButton> <MkButton v-if="!readonly" @click="add()"><i class="ph-plus-bold ph-lg"></i></MkButton>
</div> </div>
</div> </div>
@ -130,6 +130,7 @@ let eyeCatchingImageId = $ref(null);
let font = $ref('sans-serif'); let font = $ref('sans-serif');
let content = $ref([]); let content = $ref([]);
let alignCenter = $ref(false); let alignCenter = $ref(false);
let isPublic = $ref(true);
let hideTitleWhenPinned = $ref(false); let hideTitleWhenPinned = $ref(false);
let variables = $ref([]); let variables = $ref([]);
let hpml = $ref(null); let hpml = $ref(null);
@ -158,6 +159,7 @@ function getSaveOptions() {
script: script, script: script,
hideTitleWhenPinned: hideTitleWhenPinned, hideTitleWhenPinned: hideTitleWhenPinned,
alignCenter: alignCenter, alignCenter: alignCenter,
isPublic: isPublic,
content: content, content: content,
variables: variables, variables: variables,
eyeCatchingImageId: eyeCatchingImageId, eyeCatchingImageId: eyeCatchingImageId,
@ -393,6 +395,7 @@ async function init() {
script = page.script; script = page.script;
hideTitleWhenPinned = page.hideTitleWhenPinned; hideTitleWhenPinned = page.hideTitleWhenPinned;
alignCenter = page.alignCenter; alignCenter = page.alignCenter;
isPublic = page.isPublic;
content = page.content; content = page.content;
variables = page.variables; variables = page.variables;
eyeCatchingImageId = page.eyeCatchingImageId; eyeCatchingImageId = page.eyeCatchingImageId;
@ -401,7 +404,7 @@ async function init() {
content = [{ content = [{
id, id,
type: 'text', type: 'text',
text: 'Hello World!', text: '',
}]; }];
} }
} }
@ -447,7 +450,7 @@ definePageMetadata(computed(() => {
.jqqmcavi { .jqqmcavi {
> .button { > .button {
& + .button { & + .button {
margin-left: 8px; margin: 4px;
} }
} }
} }

View File

@ -1,7 +1,7 @@
<template> <template>
<MkStickyContainer> <MkStickyContainer>
<template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template> <template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template>
<MkSpacer :content-max="700"> <MkSpacer :content-max="800">
<transition :name="$store.state.animation ? 'fade' : ''" mode="out-in"> <transition :name="$store.state.animation ? 'fade' : ''" mode="out-in">
<div v-if="page" :key="page.id" v-size="{ max: [450] }" class="xcukqgmh"> <div v-if="page" :key="page.id" v-size="{ max: [450] }" class="xcukqgmh">
<div class="_block main"> <div class="_block main">
@ -25,14 +25,14 @@
<button v-tooltip="i18n.ts.shareWithNote" v-click-anime class="_button" @click="shareWithNote"><i class="ph-repeat-bold ph-lg ph-fw ph-lg"></i></button> <button v-tooltip="i18n.ts.shareWithNote" v-click-anime class="_button" @click="shareWithNote"><i class="ph-repeat-bold ph-lg ph-fw ph-lg"></i></button>
<button v-tooltip="i18n.ts.share" v-click-anime class="_button" @click="share"><i class="ph-share-network-bold ph-lg ph-fw ph-lg"></i></button> <button v-tooltip="i18n.ts.share" v-click-anime class="_button" @click="share"><i class="ph-share-network-bold ph-lg ph-fw ph-lg"></i></button>
</div> </div>
</div>
<div class="user"> <div class="user">
<MkAvatar :user="page.user" class="avatar"/> <MkAvatar :user="page.user" class="avatar"/>
<div class="name"> <div class="name">
<MkUserName :user="page.user" style="display: block;"/> <MkUserName :user="page.user" style="display: block;"/>
<MkAcct :user="page.user"/> <MkAcct :user="page.user"/>
</div> </div>
<MkFollowButton v-if="!$i || $i.id != page.user.id" :user="page.user" :inline="true" :transparent="false" :full="true" large class="koudoku"/> <MkFollowButton v-if="!$i || $i.id != page.user.id" :user="page.user" :inline="true" :transparent="false" :full="true" class="koudoku"/>
</div>
</div> </div>
<div class="links"> <div class="links">
<MkA :to="`/@${username}/pages/${pageName}/view-source`" class="link">{{ i18n.ts._pages.viewSource }}</MkA> <MkA :to="`/@${username}/pages/${pageName}/view-source`" class="link">{{ i18n.ts._pages.viewSource }}</MkA>
@ -176,6 +176,10 @@ definePageMetadata(computed(() => page ? {
.xcukqgmh { .xcukqgmh {
> .main { > .main {
> * {
margin: 1rem;
}
> .header { > .header {
padding: 16px; padding: 16px;
@ -185,6 +189,8 @@ definePageMetadata(computed(() => page ? {
} }
> .banner { > .banner {
margin: 0rem !important;
> img { > img {
// TODO: // TODO:
display: block; display: block;
@ -195,7 +201,6 @@ definePageMetadata(computed(() => page ? {
} }
> .content { > .content {
margin: 1rem;
padding: 16px 0 0 0; padding: 16px 0 0 0;
} }
@ -208,11 +213,11 @@ definePageMetadata(computed(() => page ? {
> .like { > .like {
> .button { > .button {
--accent: rgb(241 97 132); --accent: #eb6f92;
--X8: rgb(241 92 128); --X8: #eb6f92;
--buttonBg: rgb(216 71 106 / 5%); --buttonBg: rgb(216 71 106 / 5%);
--buttonHoverBg: rgb(216 71 106 / 10%); --buttonHoverBg: rgb(216 71 106 / 10%);
color: #ff002f; color: #eb6f92;
::v-deep(.count) { ::v-deep(.count) {
margin-left: 0.5em; margin-left: 0.5em;
@ -221,8 +226,6 @@ definePageMetadata(computed(() => page ? {
} }
> .other { > .other {
margin-left: auto;
> button { > button {
padding: 8px; padding: 8px;
margin: 0 8px; margin: 0 8px;
@ -232,12 +235,9 @@ definePageMetadata(computed(() => page ? {
} }
} }
} }
}
> .user { > .user {
margin-top: 16px; margin-left: auto;
padding: 16px 0 0 0;
border-top: solid 0.5px var(--divider);
display: flex; display: flex;
align-items: center; align-items: center;
@ -253,16 +253,18 @@ definePageMetadata(computed(() => page ? {
> .koudoku { > .koudoku {
margin-left: auto; margin-left: auto;
margin: 1rem;
}
} }
} }
> .links { > .links {
margin-top: 16px; margin-top: 16px;
padding: 24px 0 0 0; padding: 14px 0;
border-top: solid 0.5px var(--divider); border-top: solid 0.5px var(--divider);
> .link { > .link {
margin-right: 0.75em; margin-right: 2em;
} }
} }
} }

View File

@ -27,7 +27,7 @@
<details> <details>
<summary>{{ i18n.ts.details }}</summary> <summary>{{ i18n.ts.details }}</summary>
<ul> <ul>
<li v-for="p in token.permission" :key="p">{{ $t(`_permissions.${p}`) }}</li> <li v-for="p in token.permission" :key="p">{{ i18n.t(`_permissions.${p}`) }}</li>
</ul> </ul>
</details> </details>
</div> </div>

View File

@ -66,8 +66,9 @@ import * as os from '@/os';
import { defaultStore } from '@/store'; import { defaultStore } from '@/store';
import { i18n } from '@/i18n'; import { i18n } from '@/i18n';
import { definePageMetadata } from '@/scripts/page-metadata'; import { definePageMetadata } from '@/scripts/page-metadata';
import { deepClone } from '@/scripts/clone';
let reactions = $ref(JSON.parse(JSON.stringify(defaultStore.state.reactions))); let reactions = $ref(deepClone(defaultStore.state.reactions));
const reactionPickerSize = $computed(defaultStore.makeGetterSetter('reactionPickerSize')); const reactionPickerSize = $computed(defaultStore.makeGetterSetter('reactionPickerSize'));
const reactionPickerWidth = $computed(defaultStore.makeGetterSetter('reactionPickerWidth')); const reactionPickerWidth = $computed(defaultStore.makeGetterSetter('reactionPickerWidth'));
@ -101,7 +102,7 @@ async function setDefault() {
}); });
if (canceled) return; if (canceled) return;
reactions = JSON.parse(JSON.stringify(defaultStore.def.reactions.default)); reactions = deepClone(defaultStore.def.reactions.default);
} }
function chooseEmoji(ev: MouseEvent) { function chooseEmoji(ev: MouseEvent) {

View File

@ -7,7 +7,7 @@
<FormSection> <FormSection>
<template #label>{{ i18n.ts.sounds }}</template> <template #label>{{ i18n.ts.sounds }}</template>
<FormLink v-for="type in Object.keys(sounds)" :key="type" style="margin-bottom: 8px;" @click="edit(type)"> <FormLink v-for="type in Object.keys(sounds)" :key="type" style="margin-bottom: 8px;" @click="edit(type)">
{{ $t('_sfx.' + type) }} {{ i18n.t('_sfx.' + type) }}
<template #suffix>{{ sounds[type].type || i18n.ts.none }}</template> <template #suffix>{{ sounds[type].type || i18n.ts.none }}</template>
<template #suffixIcon><i class="ph-caret-down-bold ph-lg"></i></template> <template #suffixIcon><i class="ph-caret-down-bold ph-lg"></i></template>
</FormLink> </FormLink>

View File

@ -91,13 +91,14 @@ import FormRange from '@/components/form/range.vue';
import * as os from '@/os'; import * as os from '@/os';
import { defaultStore } from '@/store'; import { defaultStore } from '@/store';
import { i18n } from '@/i18n'; import { i18n } from '@/i18n';
import { deepClone } from '@/scripts/clone';
const props = defineProps<{ const props = defineProps<{
_id: string; _id: string;
userLists: any[] | null; userLists: any[] | null;
}>(); }>();
const statusbar = reactive(JSON.parse(JSON.stringify(defaultStore.state.statusbars.find(x => x.id === props._id)))); const statusbar = reactive(deepClone(defaultStore.state.statusbars.find(x => x.id === props._id)));
watch(() => statusbar.type, () => { watch(() => statusbar.type, () => {
if (statusbar.type === 'rss') { if (statusbar.type === 'rss') {
@ -128,8 +129,8 @@ watch(statusbar, save);
async function save() { async function save() {
const i = defaultStore.state.statusbars.findIndex(x => x.id === props._id); const i = defaultStore.state.statusbars.findIndex(x => x.id === props._id);
const statusbars = JSON.parse(JSON.stringify(defaultStore.state.statusbars)); const statusbars = deepClone(defaultStore.state.statusbars);
statusbars[i] = JSON.parse(JSON.stringify(statusbar)); statusbars[i] = deepClone(statusbar);
defaultStore.set('statusbars', statusbars); defaultStore.set('statusbars', statusbars);
} }

View File

@ -86,9 +86,7 @@ if (defaultStore.reactiveState.tutorial.value !== -1) {
const isLocalTimelineAvailable = const isLocalTimelineAvailable =
!instance.disableLocalTimeline || !instance.disableLocalTimeline ||
($i != null && ($i.isModerator || $i.isAdmin)); ($i != null && ($i.isModerator || $i.isAdmin));
const isRecommendedTimelineAvailable = const isRecommendedTimelineAvailable = !instance.disableRecommendedTimeline;
!instance.disableRecommendedTimeline ||
($i != null && ($i.isModerator || $i.isAdmin));
const isGlobalTimelineAvailable = const isGlobalTimelineAvailable =
!instance.disableGlobalTimeline || !instance.disableGlobalTimeline ||
($i != null && ($i.isModerator || $i.isAdmin)); ($i != null && ($i.isModerator || $i.isAdmin));

View File

@ -24,9 +24,10 @@
</div> </div>
</div> </div>
<span v-if="$i && $i.id != user.id && user.isFollowed" class="followed">{{ i18n.ts.followsYou }}</span> <span v-if="$i && $i.id != user.id && user.isFollowed" class="followed">{{ i18n.ts.followsYou }}</span>
<div v-if="$i" class="actions"> <div class="actions">
<button class="menu _button" @click="menu"><i class="ph-dots-three-outline-bold ph-lg"></i></button> <button class="menu _button" @click="menu"><i class="ph-dots-three-outline-bold ph-lg"></i></button>
<MkFollowButton v-if="$i.id != user.id" :user="user" :inline="true" :transparent="false" :full="true" class="koudoku"/> <MkFollowButton v-if="$i != null && $i.id != user.id" :user="user" :inline="true" :transparent="false" :full="true" class="koudoku"/>
<MkFollowButton v-else :user="user" :remote="true" :inline="true" :transparent="false" :full="true" class="koudoku"/>
</div> </div>
</div> </div>
<MkAvatar class="avatar" :user="user" :disable-preview="true" :show-indicator="true"/> <MkAvatar class="avatar" :user="user" :disable-preview="true" :show-indicator="true"/>
@ -51,7 +52,7 @@
</dl> </dl>
<dl v-if="user.birthday" class="field"> <dl v-if="user.birthday" class="field">
<dt class="name"><i class="ph-cake-bold ph-lg ph-fw ph-lg"></i> {{ i18n.ts.birthday }}</dt> <dt class="name"><i class="ph-cake-bold ph-lg ph-fw ph-lg"></i> {{ i18n.ts.birthday }}</dt>
<dd class="value">{{ user.birthday.replace('-', '/').replace('-', '/') }} ({{ $t('yearsOld', { age }) }})</dd> <dd class="value">{{ user.birthday.replace('-', '/').replace('-', '/') }} ({{ i18n.t('yearsOld', { age }) }})</dd>
</dl> </dl>
<dl class="field"> <dl class="field">
<dt class="name"><i class="ph-calendar-blank-bold ph-lg ph-fw ph-lg"></i> {{ i18n.ts.registeredDate }}</dt> <dt class="name"><i class="ph-calendar-blank-bold ph-lg ph-fw ph-lg"></i> {{ i18n.ts.registeredDate }}</dt>

View File

@ -83,26 +83,26 @@ const headerTabs = $computed(() =>
{ {
key: 'home', key: 'home',
title: i18n.ts.overview, title: i18n.ts.overview,
icon: 'ph-user-bold ph-large', icon: 'ph-user-bold ph-lg',
}, },
...(($i && $i.id === user.id) || user.publicReactions ...(($i && $i.id === user.id) || user.publicReactions
? [{ ? [{
key: 'reactions', key: 'reactions',
title: i18n.ts.reaction, title: i18n.ts.reaction,
icon: 'ph-smiley-bold ph-large', icon: 'ph-smiley-bold ph-lg',
}] : []), }] : []),
...(user.instance == null ? [{ ...(user.instance == null ? [{
key: 'clips', key: 'clips',
title: i18n.ts.clips, title: i18n.ts.clips,
icon: 'ph-paperclip-bold ph-large', icon: 'ph-paperclip-bold ph-lg',
}, { }, {
key: 'pages', key: 'pages',
title: i18n.ts.pages, title: i18n.ts.pages,
icon: 'ph-file-text-bold ph-large', icon: 'ph-file-text-bold ph-lg',
}, { }, {
key: 'gallery', key: 'gallery',
title: i18n.ts.gallery, title: i18n.ts.gallery,
icon: 'ph-image-square-bold ph-large', icon: 'ph-image-square-bold ph-lg',
}] : []), }] : []),
] ]
: null, : null,

View File

@ -105,7 +105,7 @@ export default defineComponent({
showMenu(ev) { showMenu(ev) {
os.popupMenu([{ os.popupMenu([{
text: this.$t('aboutX', { x: instanceName }), text: i18n.t('aboutX', { x: instanceName }),
icon: 'ph-info-bold ph-lg', icon: 'ph-info-bold ph-lg',
action: () => { action: () => {
os.pageWindow('/about'); os.pageWindow('/about');

View File

@ -125,7 +125,7 @@ export default defineComponent({
showMenu(ev) { showMenu(ev) {
os.popupMenu([{ os.popupMenu([{
text: this.$t('aboutX', { x: instanceName }), text: i18n.t('aboutX', { x: instanceName }),
icon: 'ph-info-bold ph-lg', icon: 'ph-info-bold ph-lg',
action: () => { action: () => {
os.pageWindow('/about'); os.pageWindow('/about');

View File

@ -0,0 +1,18 @@
// structredCloneが遅いため
// SEE: http://var.blog.jp/archives/86038606.html
type Cloneable = string | number | boolean | null | { [key: string]: Cloneable } | Cloneable[];
export function deepClone<T extends Cloneable>(x: T): T {
if (typeof x === 'object') {
if (x === null) return x;
if (Array.isArray(x)) return x.map(deepClone) as T;
const obj = {} as Record<string, Cloneable>;
for (const [k, v] of Object.entries(x)) {
obj[k] = deepClone(v);
}
return obj as T;
} else {
return x;
}
}

View File

@ -13,6 +13,7 @@ export type Theme = {
import lightTheme from '@/themes/_light.json5'; import lightTheme from '@/themes/_light.json5';
import darkTheme from '@/themes/_dark.json5'; import darkTheme from '@/themes/_dark.json5';
import { deepClone } from './clone';
export const themeProps = Object.keys(lightTheme.props).filter(key => !key.startsWith('X')); export const themeProps = Object.keys(lightTheme.props).filter(key => !key.startsWith('X'));
@ -63,7 +64,7 @@ export function applyTheme(theme: Theme, persist = true) {
const colorSchema = theme.base === 'dark' ? 'dark' : 'light'; const colorSchema = theme.base === 'dark' ? 'dark' : 'light';
// Deep copy // Deep copy
const _theme = JSON.parse(JSON.stringify(theme)); const _theme = deepClone(theme);
if (_theme.base) { if (_theme.base) {
const base = [lightTheme, darkTheme].find(x => x.id === _theme.base); const base = [lightTheme, darkTheme].find(x => x.id === _theme.base);

View File

@ -38,7 +38,7 @@
<button v-tooltip.noDelay.left="i18n.ts._deck.deleteProfile" class="_button button" @click="deleteProfile"><i class="ph-trash-bold ph-lg"></i></button> <button v-tooltip.noDelay.left="i18n.ts._deck.deleteProfile" class="_button button" @click="deleteProfile"><i class="ph-trash-bold ph-lg"></i></button>
</div> </div>
<div class="middle"> <div class="middle">
<button v-tooltip.noDelay.left="i18n.ts._deck.addColumn" class="_button button" @click="addColumn"><i class="ph-plus-bold ph-lg"></i></button> <button v-tooltip.noDelay.left="i18n.ts._deck.addColumn" class="_button button new" @click="addColumn"><i class="ph-plus-bold ph-lg"></i></button>
</div> </div>
<div class="bottom"> <div class="bottom">
<button v-tooltip.noDelay.left="i18n.ts.settings" class="_button button settings" @click="showSettings"><i class="ph-gear-six-bold ph-lg"></i></button> <button v-tooltip.noDelay.left="i18n.ts.settings" class="_button button settings" @click="showSettings"><i class="ph-gear-six-bold ph-lg"></i></button>
@ -322,7 +322,7 @@ async function deleteProfile() {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
justify-content: center; justify-content: center;
width: 32px; width: 44px;
> .top, > .middle, > .bottom { > .top, > .middle, > .bottom {
> .button { > .button {
@ -339,6 +339,11 @@ async function deleteProfile() {
> .middle { > .middle {
margin-top: auto; margin-top: auto;
margin-bottom: auto; margin-bottom: auto;
> .new {
font-size: 20px;
background-color: var(--accentedBg);
}
} }
> .bottom { > .bottom {

View File

@ -4,6 +4,7 @@ import { notificationTypes } from 'misskey-js';
import { Storage } from '../../pizzax'; import { Storage } from '../../pizzax';
import { i18n } from '@/i18n'; import { i18n } from '@/i18n';
import { api } from '@/os'; import { api } from '@/os';
import { deepClone } from '@/scripts/clone';
type ColumnWidget = { type ColumnWidget = {
name: string; name: string;
@ -25,10 +26,6 @@ export type Column = {
tl?: 'home' | 'local' | 'social' | 'global'; tl?: 'home' | 'local' | 'social' | 'global';
}; };
function copy<T>(x: T): T {
return JSON.parse(JSON.stringify(x));
}
export const deckStore = markRaw(new Storage('deck', { export const deckStore = markRaw(new Storage('deck', {
profile: { profile: {
where: 'deviceAccount', where: 'deviceAccount',
@ -128,7 +125,7 @@ export function swapColumn(a: Column['id'], b: Column['id']) {
const aY = deckStore.state.layout[aX].findIndex(id => id === a); const aY = deckStore.state.layout[aX].findIndex(id => id === a);
const bX = deckStore.state.layout.findIndex(ids => ids.indexOf(b) !== -1); const bX = deckStore.state.layout.findIndex(ids => ids.indexOf(b) !== -1);
const bY = deckStore.state.layout[bX].findIndex(id => id === b); const bY = deckStore.state.layout[bX].findIndex(id => id === b);
const layout = copy(deckStore.state.layout); const layout = deepClone(deckStore.state.layout);
layout[aX][aY] = b; layout[aX][aY] = b;
layout[bX][bY] = a; layout[bX][bY] = a;
deckStore.set('layout', layout); deckStore.set('layout', layout);
@ -136,7 +133,7 @@ export function swapColumn(a: Column['id'], b: Column['id']) {
} }
export function swapLeftColumn(id: Column['id']) { export function swapLeftColumn(id: Column['id']) {
const layout = copy(deckStore.state.layout); const layout = deepClone(deckStore.state.layout);
deckStore.state.layout.some((ids, i) => { deckStore.state.layout.some((ids, i) => {
if (ids.includes(id)) { if (ids.includes(id)) {
const left = deckStore.state.layout[i - 1]; const left = deckStore.state.layout[i - 1];
@ -152,7 +149,7 @@ export function swapLeftColumn(id: Column['id']) {
} }
export function swapRightColumn(id: Column['id']) { export function swapRightColumn(id: Column['id']) {
const layout = copy(deckStore.state.layout); const layout = deepClone(deckStore.state.layout);
deckStore.state.layout.some((ids, i) => { deckStore.state.layout.some((ids, i) => {
if (ids.includes(id)) { if (ids.includes(id)) {
const right = deckStore.state.layout[i + 1]; const right = deckStore.state.layout[i + 1];
@ -168,9 +165,9 @@ export function swapRightColumn(id: Column['id']) {
} }
export function swapUpColumn(id: Column['id']) { export function swapUpColumn(id: Column['id']) {
const layout = copy(deckStore.state.layout); const layout = deepClone(deckStore.state.layout);
const idsIndex = deckStore.state.layout.findIndex(ids => ids.includes(id)); const idsIndex = deckStore.state.layout.findIndex(ids => ids.includes(id));
const ids = copy(deckStore.state.layout[idsIndex]); const ids = deepClone(deckStore.state.layout[idsIndex]);
ids.some((x, i) => { ids.some((x, i) => {
if (x === id) { if (x === id) {
const up = ids[i - 1]; const up = ids[i - 1];
@ -188,9 +185,9 @@ export function swapUpColumn(id: Column['id']) {
} }
export function swapDownColumn(id: Column['id']) { export function swapDownColumn(id: Column['id']) {
const layout = copy(deckStore.state.layout); const layout = deepClone(deckStore.state.layout);
const idsIndex = deckStore.state.layout.findIndex(ids => ids.includes(id)); const idsIndex = deckStore.state.layout.findIndex(ids => ids.includes(id));
const ids = copy(deckStore.state.layout[idsIndex]); const ids = deepClone(deckStore.state.layout[idsIndex]);
ids.some((x, i) => { ids.some((x, i) => {
if (x === id) { if (x === id) {
const down = ids[i + 1]; const down = ids[i + 1];
@ -208,7 +205,7 @@ export function swapDownColumn(id: Column['id']) {
} }
export function stackLeftColumn(id: Column['id']) { export function stackLeftColumn(id: Column['id']) {
let layout = copy(deckStore.state.layout); let layout = deepClone(deckStore.state.layout);
const i = deckStore.state.layout.findIndex(ids => ids.includes(id)); const i = deckStore.state.layout.findIndex(ids => ids.includes(id));
layout = layout.map(ids => ids.filter(_id => _id !== id)); layout = layout.map(ids => ids.filter(_id => _id !== id));
layout[i - 1].push(id); layout[i - 1].push(id);
@ -218,7 +215,7 @@ export function stackLeftColumn(id: Column['id']) {
} }
export function popRightColumn(id: Column['id']) { export function popRightColumn(id: Column['id']) {
let layout = copy(deckStore.state.layout); let layout = deepClone(deckStore.state.layout);
const i = deckStore.state.layout.findIndex(ids => ids.includes(id)); const i = deckStore.state.layout.findIndex(ids => ids.includes(id));
const affected = layout[i]; const affected = layout[i];
layout = layout.map(ids => ids.filter(_id => _id !== id)); layout = layout.map(ids => ids.filter(_id => _id !== id));
@ -226,7 +223,7 @@ export function popRightColumn(id: Column['id']) {
layout = layout.filter(ids => ids.length > 0); layout = layout.filter(ids => ids.length > 0);
deckStore.set('layout', layout); deckStore.set('layout', layout);
const columns = copy(deckStore.state.columns); const columns = deepClone(deckStore.state.columns);
for (const column of columns) { for (const column of columns) {
if (affected.includes(column.id)) { if (affected.includes(column.id)) {
column.active = true; column.active = true;
@ -238,9 +235,9 @@ export function popRightColumn(id: Column['id']) {
} }
export function addColumnWidget(id: Column['id'], widget: ColumnWidget) { export function addColumnWidget(id: Column['id'], widget: ColumnWidget) {
const columns = copy(deckStore.state.columns); const columns = deepClone(deckStore.state.columns);
const columnIndex = deckStore.state.columns.findIndex(c => c.id === id); const columnIndex = deckStore.state.columns.findIndex(c => c.id === id);
const column = copy(deckStore.state.columns[columnIndex]); const column = deepClone(deckStore.state.columns[columnIndex]);
if (column == null) return; if (column == null) return;
if (column.widgets == null) column.widgets = []; if (column.widgets == null) column.widgets = [];
column.widgets.unshift(widget); column.widgets.unshift(widget);
@ -250,9 +247,9 @@ export function addColumnWidget(id: Column['id'], widget: ColumnWidget) {
} }
export function removeColumnWidget(id: Column['id'], widget: ColumnWidget) { export function removeColumnWidget(id: Column['id'], widget: ColumnWidget) {
const columns = copy(deckStore.state.columns); const columns = deepClone(deckStore.state.columns);
const columnIndex = deckStore.state.columns.findIndex(c => c.id === id); const columnIndex = deckStore.state.columns.findIndex(c => c.id === id);
const column = copy(deckStore.state.columns[columnIndex]); const column = deepClone(deckStore.state.columns[columnIndex]);
if (column == null) return; if (column == null) return;
column.widgets = column.widgets.filter(w => w.id !== widget.id); column.widgets = column.widgets.filter(w => w.id !== widget.id);
columns[columnIndex] = column; columns[columnIndex] = column;
@ -261,9 +258,9 @@ export function removeColumnWidget(id: Column['id'], widget: ColumnWidget) {
} }
export function setColumnWidgets(id: Column['id'], widgets: ColumnWidget[]) { export function setColumnWidgets(id: Column['id'], widgets: ColumnWidget[]) {
const columns = copy(deckStore.state.columns); const columns = deepClone(deckStore.state.columns);
const columnIndex = deckStore.state.columns.findIndex(c => c.id === id); const columnIndex = deckStore.state.columns.findIndex(c => c.id === id);
const column = copy(deckStore.state.columns[columnIndex]); const column = deepClone(deckStore.state.columns[columnIndex]);
if (column == null) return; if (column == null) return;
column.widgets = widgets; column.widgets = widgets;
columns[columnIndex] = column; columns[columnIndex] = column;
@ -272,9 +269,9 @@ export function setColumnWidgets(id: Column['id'], widgets: ColumnWidget[]) {
} }
export function updateColumnWidget(id: Column['id'], widgetId: string, widgetData: any) { export function updateColumnWidget(id: Column['id'], widgetId: string, widgetData: any) {
const columns = copy(deckStore.state.columns); const columns = deepClone(deckStore.state.columns);
const columnIndex = deckStore.state.columns.findIndex(c => c.id === id); const columnIndex = deckStore.state.columns.findIndex(c => c.id === id);
const column = copy(deckStore.state.columns[columnIndex]); const column = deepClone(deckStore.state.columns[columnIndex]);
if (column == null) return; if (column == null) return;
column.widgets = column.widgets.map(w => w.id === widgetId ? { column.widgets = column.widgets.map(w => w.id === widgetId ? {
...w, ...w,
@ -286,9 +283,9 @@ export function updateColumnWidget(id: Column['id'], widgetId: string, widgetDat
} }
export function updateColumn(id: Column['id'], column: Partial<Column>) { export function updateColumn(id: Column['id'], column: Partial<Column>) {
const columns = copy(deckStore.state.columns); const columns = deepClone(deckStore.state.columns);
const columnIndex = deckStore.state.columns.findIndex(c => c.id === id); const columnIndex = deckStore.state.columns.findIndex(c => c.id === id);
const currentColumn = copy(deckStore.state.columns[columnIndex]); const currentColumn = deepClone(deckStore.state.columns[columnIndex]);
if (currentColumn == null) return; if (currentColumn == null) return;
for (const [k, v] of Object.entries(column)) { for (const [k, v] of Object.entries(column)) {
currentColumn[k] = v; currentColumn[k] = v;

View File

@ -11,9 +11,9 @@
<div v-if="disabled" class="iwaalbte"> <div v-if="disabled" class="iwaalbte">
<p> <p>
<i class="ph-minus-circle-bold ph-lg"></i> <i class="ph-minus-circle-bold ph-lg"></i>
{{ $t('disabled-timeline.title') }} {{ i18n.t('disabled-timeline.title') }}
</p> </p>
<p class="desc">{{ $t('disabled-timeline.description') }}</p> <p class="desc">{{ i18n.t('disabled-timeline.description') }}</p>
</div> </div>
<XTimeline v-else-if="column.tl" ref="timeline" :key="column.tl" :src="column.tl" @after="() => emit('loaded')" @queue="queueUpdated" @note="onNote"/> <XTimeline v-else-if="column.tl" ref="timeline" :key="column.tl" :src="column.tl" @after="() => emit('loaded')" @queue="queueUpdated" @note="onNote"/>
</XColumn> </XColumn>

View File

@ -2,11 +2,11 @@
<div class="mkw-calendar" :class="{ _panel: !widgetProps.transparent }"> <div class="mkw-calendar" :class="{ _panel: !widgetProps.transparent }">
<div class="calendar" :class="{ isHoliday }"> <div class="calendar" :class="{ isHoliday }">
<p class="month-and-year"> <p class="month-and-year">
<span class="year">{{ $t('yearX', { year }) }}</span> <span class="year">{{ i18n.t('yearX', { year }) }}</span>
<span class="month">{{ $t('monthX', { month }) }}</span> <span class="month">{{ i18n.t('monthX', { month }) }}</span>
</p> </p>
<p v-if="month === 1 && day === 1" class="day">🎉{{ $t('dayX', { day }) }}<span style="display: inline-block; transform: scaleX(-1);">🎉</span></p> <p v-if="month === 1 && day === 1" class="day">🎉{{ i18n.t('dayX', { day }) }}<span style="display: inline-block; transform: scaleX(-1);">🎉</span></p>
<p v-else class="day">{{ $t('dayX', { day }) }}</p> <p v-else class="day">{{ i18n.t('dayX', { day }) }}</p>
<p class="week-day">{{ weekDay }}</p> <p class="week-day">{{ weekDay }}</p>
</div> </div>
<div class="info"> <div class="info">

View File

@ -47,12 +47,13 @@
<script lang="ts" setup> <script lang="ts" setup>
import { onMounted, onUnmounted, reactive, ref } from 'vue'; import { onMounted, onUnmounted, reactive, ref } from 'vue';
import { GetFormResultType } from '@/scripts/form';
import { useWidgetPropsManager, Widget, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget'; import { useWidgetPropsManager, Widget, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget';
import { GetFormResultType } from '@/scripts/form';
import { stream } from '@/stream'; import { stream } from '@/stream';
import number from '@/filters/number'; import number from '@/filters/number';
import * as sound from '@/scripts/sound'; import * as sound from '@/scripts/sound';
import * as os from '@/os'; import * as os from '@/os';
import { deepClone } from '@/scripts/clone';
const name = 'jobQueue'; const name = 'jobQueue';
@ -100,12 +101,12 @@ const prev = reactive({} as typeof current);
const jammedSound = sound.setVolume(sound.getAudio('syuilo/queue-jammed'), 1); const jammedSound = sound.setVolume(sound.getAudio('syuilo/queue-jammed'), 1);
for (const domain of ['inbox', 'deliver']) { for (const domain of ['inbox', 'deliver']) {
prev[domain] = JSON.parse(JSON.stringify(current[domain])); prev[domain] = deepClone(current[domain]);
} }
const onStats = (stats) => { const onStats = (stats) => {
for (const domain of ['inbox', 'deliver']) { for (const domain of ['inbox', 'deliver']) {
prev[domain] = JSON.parse(JSON.stringify(current[domain])); prev[domain] = deepClone(current[domain]);
current[domain].activeSincePrevTick = stats[domain].activeSincePrevTick; current[domain].activeSincePrevTick = stats[domain].activeSincePrevTick;
current[domain].active = stats[domain].active; current[domain].active = stats[domain].active;
current[domain].waiting = stats[domain].waiting; current[domain].waiting = stats[domain].waiting;

View File

@ -4,7 +4,7 @@
<p v-if="widgetProps.folderId == null"> <p v-if="widgetProps.folderId == null">
{{ i18n.ts.folder }} {{ i18n.ts.folder }}
</p> </p>
<p v-if="widgetProps.folderId != null && images.length === 0 && !fetching">{{ $t('no-image') }}</p> <p v-if="widgetProps.folderId != null && images.length === 0 && !fetching">{{ i18n.t('no-image') }}</p>
<div ref="slideA" class="slide a"></div> <div ref="slideA" class="slide a"></div>
<div ref="slideB" class="slide b"></div> <div ref="slideB" class="slide b"></div>
</div> </div>

View File

@ -8,7 +8,7 @@
<i v-else-if="widgetProps.src === 'global'" class="ph-planet-bold ph-lg"></i> <i v-else-if="widgetProps.src === 'global'" class="ph-planet-bold ph-lg"></i>
<i v-else-if="widgetProps.src === 'list'" class="ph-list-bullets-bold ph-lg"></i> <i v-else-if="widgetProps.src === 'list'" class="ph-list-bullets-bold ph-lg"></i>
<i v-else-if="widgetProps.src === 'antenna'" class="ph-television-bold ph-lg"></i> <i v-else-if="widgetProps.src === 'antenna'" class="ph-television-bold ph-lg"></i>
<span style="margin-left: 8px;">{{ widgetProps.src === 'list' ? widgetProps.list.name : widgetProps.src === 'antenna' ? widgetProps.antenna.name : $t('_timelines.' + widgetProps.src) }}</span> <span style="margin-left: 8px;">{{ widgetProps.src === 'list' ? widgetProps.list.name : widgetProps.src === 'antenna' ? widgetProps.antenna.name : i18n.t('_timelines.' + widgetProps.src) }}</span>
<i :class="menuOpened ? 'ph-caret-up-bold ph-lg' : 'ph-caret-down-bold ph-lg'" style="margin-left: 8px;"></i> <i :class="menuOpened ? 'ph-caret-up-bold ph-lg' : 'ph-caret-down-bold ph-lg'" style="margin-left: 8px;"></i>
</button> </button>
</template> </template>

View File

@ -8,7 +8,7 @@
<div v-for="stat in stats" :key="stat.tag"> <div v-for="stat in stats" :key="stat.tag">
<div class="tag"> <div class="tag">
<MkA class="a" :to="`/tags/${ encodeURIComponent(stat.tag) }`" :title="stat.tag">#{{ stat.tag }}</MkA> <MkA class="a" :to="`/tags/${ encodeURIComponent(stat.tag) }`" :title="stat.tag">#{{ stat.tag }}</MkA>
<p>{{ $t('nUsersMentioned', { n: stat.usersCount }) }}</p> <p>{{ i18n.t('nUsersMentioned', { n: stat.usersCount }) }}</p>
</div> </div>
<MkMiniChart class="chart" :src="stat.chart"/> <MkMiniChart class="chart" :src="stat.chart"/>
</div> </div>

View File

@ -2,6 +2,7 @@ import { reactive, watch } from 'vue';
import { throttle } from 'throttle-debounce'; import { throttle } from 'throttle-debounce';
import { Form, GetFormResultType } from '@/scripts/form'; import { Form, GetFormResultType } from '@/scripts/form';
import * as os from '@/os'; import * as os from '@/os';
import { deepClone } from '@/scripts/clone';
export type Widget<P extends Record<string, unknown>> = { export type Widget<P extends Record<string, unknown>> = {
id: string; id: string;
@ -32,7 +33,7 @@ export const useWidgetPropsManager = <F extends Form & Record<string, { default:
save: () => void; save: () => void;
configure: () => void; configure: () => void;
} => { } => {
const widgetProps = reactive(props.widget ? JSON.parse(JSON.stringify(props.widget.data)) : {}); const widgetProps = reactive(props.widget ? deepClone(props.widget.data) : {});
const mergeProps = () => { const mergeProps = () => {
for (const prop of Object.keys(propsDef)) { for (const prop of Object.keys(propsDef)) {
@ -43,14 +44,14 @@ export const useWidgetPropsManager = <F extends Form & Record<string, { default:
}; };
watch(widgetProps, () => { watch(widgetProps, () => {
mergeProps(); mergeProps();
}, { deep: true, immediate: true, }); }, { deep: true, immediate: true });
const save = throttle(3000, () => { const save = throttle(3000, () => {
emit('updateProps', widgetProps); emit('updateProps', widgetProps);
}); });
const configure = async () => { const configure = async () => {
const form = JSON.parse(JSON.stringify(propsDef)); const form = deepClone(propsDef);
for (const item of Object.keys(form)) { for (const item of Object.keys(form)) {
form[item].default = widgetProps[item]; form[item].default = widgetProps[item];
} }

View File

@ -7,11 +7,11 @@
"lint": "eslint --quiet src/**/*.{ts}" "lint": "eslint --quiet src/**/*.{ts}"
}, },
"dependencies": { "dependencies": {
"esbuild": "^0.14.54", "esbuild": "^0.15.14",
"idb-keyval": "^6.2.0", "idb-keyval": "^6.2.0",
"misskey-js": "0.0.14" "misskey-js": "0.0.14"
}, },
"devDependencies": { "devDependencies": {
"eslint": "^8.27.0" "eslint": "^8.28.0"
} }
} }