Merge pull request 'develop' (#9178) from develop into account_migration

Reviewed-on: https://codeberg.org/thatonecalculator/calckey/pulls/9178
This commit is contained in:
Kainoa Kanter 2022-12-04 06:26:26 +00:00
commit 0e8b155e07
77 changed files with 21821 additions and 505 deletions

View file

@ -1,4 +1,4 @@
# db settings # db settings
POSTGRES_PASSWORD=example-misskey-pass POSTGRES_PASSWORD=example-calckey-pass
POSTGRES_USER=example-misskey-user POSTGRES_USER=example-calckey-user
POSTGRES_DB=misskey POSTGRES_DB=calckey

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 - Better API Documentation
- 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,7 +85,12 @@
- Link hover effect - Link hover effect
- Replace all `$ts` with i18n - Replace all `$ts` with i18n
- AVIF support - AVIF support
- Page drafts
- Patron list
- Animations respect reduced motion
- Obliteration of Ai-chan - Obliteration of Ai-chan
- Undo renote button inside original note
- MissV: [fix Misskey Forkbomb](https://code.vtopia.live/Vtopia/MissV/commit/40b23c070bd4adbb3188c73546c6c625138fb3c1)
- [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)
- [OAuth bearer token authentication](https://github.com/misskey-dev/misskey/pull/9021) - [OAuth bearer token authentication](https://github.com/misskey-dev/misskey/pull/9021)

View file

@ -1,9 +1,9 @@
<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>
@ -20,6 +20,7 @@
- Improved UI/UX (especially on mobile) - Improved UI/UX (especially on mobile)
- Improved notifications - Improved notifications
- Improved instance security - Improved instance security
- Improved accessibility
- Recommended Instances timeline - Recommended Instances timeline
- OCR image captioning - OCR image captioning
- New and improved Groups - New and improved Groups
@ -34,6 +35,9 @@
# 🥂 Links # 🥂 Links
- 💸 Liberapay: <https://liberapay.com/ThatOneCalculator> - 💸 Liberapay: <https://liberapay.com/ThatOneCalculator>
- Donate publicly to get your name on the Patron list!
- 🚢 Flagship instance: <https://i.calckey.cloud>
- 📣 Official account: <https://i.calckey.cloud/@calckey>
- 💁 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>
- 📖 JoinFediverse Wiki: <https://joinfediverse.wiki/What_is_Calckey%3F> - 📖 JoinFediverse Wiki: <https://joinfediverse.wiki/What_is_Calckey%3F>
@ -45,15 +49,27 @@ This guide will work for both **starting from scratch** and **migrating from Mis
## 📦 Dependencies ## 📦 Dependencies
- At least 🐢 [NodeJS](https://nodejs.org/en/) v16.15.0 (v18.12.1 recommended) - 🐢 At least [NodeJS](https://nodejs.org/en/) v18.12.1 (v19.1.0 recommended)
- Install with [nvm](https://github.com/nvm-sh/nvm)
> ⚠️ NodeJS v19 is not supported as of right now because of [this issue](https://github.com/nodejs/node-gyp/issues/2757).
- 🐘 At least [PostgreSQL](https://www.postgresql.org/) v12 - 🐘 At least [PostgreSQL](https://www.postgresql.org/) v12
- 🍱 At least [Redis](https://redis.io/) v6 (v7 recommended) - 🍱 At least [Redis](https://redis.io/) v6 (v7 recommended)
- 🛰️ (Optional, for non-Docker) [pm2](https://pm2.io/) ### 😗 Optional dependencies
- 📗 [FFmpeg](https://ffmpeg.org/) for video transcoding
- 🔍 [ElasticSearch](https://www.elastic.co/elasticsearch/) for full-text search
- OpenSearch/Sonic are not supported as of right now
- 🥡 Management (choose one of the following)
- 🛰️ [pm2](https://pm2.io/)
- 🐳 [Docker](https://docker.com)
- 📐 Service manager (systemd, openrc, etc)
### 🏗️ Build dependencies
- 🦬 C/C++ compiler & build tools
- `build-essential` on Debian/Ubuntu Linux
- `base-devel` on Arch Linux
- 🐍 [Python 3](https://www.python.org/)
## 👀 Get folder ready ## 👀 Get folder ready
@ -70,10 +86,19 @@ cd calckey/
corepack enable corepack enable
``` ```
## 🐘 Create database
Assuming you set up PostgreSQL correctly, all you have to run is:
```sh
psql postgres -c "create database calckey with encoding = 'UTF8';"
```
## 💅 Customize ## 💅 Customize
- To add custom CSS for all users, edit `./custom/instance.css`. - To add custom CSS for all users, edit `./custom/assets/instance.css`.
- To add static assets (such as images for the splash screen), place them in the `./custom/` directory. They'll then be avaliable on `https://yourinstance.tld/static-assets/filename.ext`. - To add static assets (such as images for the splash screen), place them in the `./custom/assets/` directory. They'll then be avaliable on `https://yourinstance.tld/static-assets/filename.ext`.
- To add custom locales, place them in the `./custom/locales/` directory. If you name your custom locale the same as an existing locale, it will overwrite it. If you give it a unique name, it will be added to the list. Also make sure that the first part of the filename matches the locale you're basing it on. (Example: `en-FOO.yml`)
- To update custom assets without rebuilding, just run `yarn run gulp`. - To update custom assets without rebuilding, just run `yarn run gulp`.
## 🧑‍🔬 Configuring a new instance ## 🧑‍🔬 Configuring a new instance
@ -93,7 +118,7 @@ cp -r ../misskey/files . # if you don't use object storage
## 🍀 NGINX ## 🍀 NGINX
- Run `sudo cp ./calckey.nginx.conf /etc/nginx/sites-avaliable/ && cd /etc/nginx/sites-avaliable/` - Run `sudo cp ./calckey.nginx.conf /etc/nginx/sites-available/ && cd /etc/nginx/sites-available/`
- Edit `calckey.nginx.conf` to reflect your instance properly - Edit `calckey.nginx.conf` to reflect your instance properly
- Run `sudo cp ./calckey.nginx.conf ../sites-enabled/` - Run `sudo cp ./calckey.nginx.conf ../sites-enabled/`
- Run `sudo nginx -t` to validate that the config is valid, then restart the NGINX service. - Run `sudo nginx -t` to validate that the config is valid, then restart the NGINX service.
@ -102,7 +127,7 @@ cp -r ../misskey/files . # if you don't use object storage
## 🚀 Build and launch! ## 🚀 Build and launch!
### 🐢 NodeJS ### 🐢 NodeJS + pm2
#### `git pull` and run these steps to update Calckey in the future! #### `git pull` and run these steps to update Calckey in the future!
@ -123,15 +148,16 @@ docker up -d
### 🐳 Docker Compose ### 🐳 Docker Compose
```sh ```sh
docker compose build docker-compose build
docker-compose run --rm web yarn run init docker-compose run --rm web yarn run init
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. - 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` - 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 recommend you use a S3 Bucket/CDN for Object Storage, especially if you use Docker.
- 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.

0
custom/locales/.gitkeep Normal file
View file

View file

@ -16,7 +16,7 @@ gulp.task('copy:backend:views', () =>
); );
gulp.task('copy:backend:custom', () => gulp.task('copy:backend:custom', () =>
gulp.src('./custom/*').pipe(gulp.dest('./packages/backend/assets/')) gulp.src('./custom/assets/*').pipe(gulp.dest('./packages/backend/assets/'))
); );
gulp.task('copy:client:fonts', () => gulp.task('copy:client:fonts', () =>
@ -24,7 +24,7 @@ gulp.task('copy:client:fonts', () =>
); );
gulp.task('copy:client:phosphor', () => gulp.task('copy:client:phosphor', () =>
gulp.src('./node_modules/phosphor-icons/src/css/*').pipe(gulp.dest('./built/_client_dist_/phosphor/')) gulp.src('./node_modules/phosphor-icons/src/fonts/*').pipe(gulp.dest('./built/_client_dist_/phosphor/'))
); );
gulp.task('copy:client:locales', cb => { gulp.task('copy:client:locales', cb => {

View file

@ -32,12 +32,12 @@ uploading: "Uploading..."
save: "Save" save: "Save"
users: "Users" users: "Users"
addUser: "Add a user" addUser: "Add a user"
favorite: "Add to favorites" favorite: "Add to bookmarks"
favorites: "Favorites" favorites: "Bookmarks"
unfavorite: "Remove from favorites" unfavorite: "Remove from bookmarks"
favorited: "Added to favorites." favorited: "Added to bookmarks."
alreadyFavorited: "Already added to favorites." alreadyFavorited: "Already added to bookmarks."
cantFavorite: "Couldn't add to favorites." cantFavorite: "Couldn't add to bookmarks."
pin: "Pin to profile" pin: "Pin to profile"
unpin: "Unpin from profile" unpin: "Unpin from profile"
copyContent: "Copy contents" copyContent: "Copy contents"
@ -160,7 +160,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"
@ -680,7 +680,7 @@ disableShowingAnimatedImages: "Don't play animated images"
verificationEmailSent: "A verification email has been sent. Please follow the included link to complete verification." verificationEmailSent: "A verification email has been sent. Please follow the included link to complete verification."
notSet: "Not set" notSet: "Not set"
emailVerified: "Email has been verified" emailVerified: "Email has been verified"
noteFavoritesCount: "Number of favorite notes" noteFavoritesCount: "Number of bookmarked notes"
pageLikesCount: "Number of liked Pages" pageLikesCount: "Number of liked Pages"
pageLikedCount: "Number of received Page likes" pageLikedCount: "Number of received Page likes"
contact: "Contact" contact: "Contact"
@ -771,8 +771,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"
@ -1002,9 +1002,9 @@ _aboutMisskey:
allContributors: "All contributors" allContributors: "All contributors"
source: "Source code" source: "Source code"
translation: "Translate Misskey" translation: "Translate Misskey"
donate: "Donate to Misskey" donate: "Donate to Calckey"
morePatrons: "We also appreciate the support of many other helpers not listed here. Thank you! 🥰" morePatrons: "We also appreciate the support of many other helpers not listed here. Thank you! 🥰"
patrons: "Misskey patrons" patrons: "Calckey patrons"
_nsfw: _nsfw:
respect: "Hide NSFW media" respect: "Hide NSFW media"
ignore: "Don't hide NSFW media" ignore: "Don't hide NSFW media"
@ -1095,7 +1095,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"
@ -1228,7 +1228,7 @@ _tutorial:
step5_3: "The Home {icon} timeline is where you can see posts from your followers." step5_3: "The Home {icon} timeline is where you can see posts from your followers."
step5_4: "The Local {icon} timeline is where you can see posts from everyone else on this instance." step5_4: "The Local {icon} timeline is where you can see posts from everyone else on this instance."
step5_5: "The Recommended {icon} timeline is where you can see posts from instances the admins recommend." step5_5: "The Recommended {icon} timeline is where you can see posts from instances the admins recommend."
step5_6: "The Social {icon} timeline is where you can see posts from friends of your followers." step5_6: "The Social {icon} timeline is your home + local."
step5_7: "The Global {icon} timeline is where you can see posts from every other connected instance." step5_7: "The Global {icon} timeline is where you can see posts from every other connected instance."
step6_1: "So, what is this place?" step6_1: "So, what is this place?"
step6_2: "Well, you didn't just join Calckey. You joined a portal to the Fediverse, an interconnected network of thousands of servers, called \"instances\"." step6_2: "Well, you didn't just join Calckey. You joined a portal to the Fediverse, an interconnected network of thousands of servers, called \"instances\"."
@ -1251,8 +1251,8 @@ _permissions:
"write:blocks": "Edit your list of blocked users" "write:blocks": "Edit your list of blocked users"
"read:drive": "Access your Drive files and folders" "read:drive": "Access your Drive files and folders"
"write:drive": "Edit or delete your Drive files and folders" "write:drive": "Edit or delete your Drive files and folders"
"read:favorites": "View your list of favorites" "read:favorites": "View your list of bookmarks"
"write:favorites": "Edit your list of favorites" "write:favorites": "Edit your list of bookmarks"
"read:following": "View information on who you follow" "read:following": "View information on who you follow"
"write:following": "Follow or unfollow other accounts" "write:following": "Follow or unfollow other accounts"
"read:messaging": "View your chats" "read:messaging": "View your chats"
@ -1265,10 +1265,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"
@ -1442,7 +1442,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

@ -4,6 +4,8 @@
const fs = require('fs'); const fs = require('fs');
const yaml = require('js-yaml'); const yaml = require('js-yaml');
let languages = []
let languages_custom = []
const merge = (...args) => args.reduce((a, c) => ({ const merge = (...args) => args.reduce((a, c) => ({
...a, ...a,
@ -13,33 +15,20 @@ const merge = (...args) => args.reduce((a, c) => ({
.reduce((a, [k, v]) => (a[k] = merge(v, c[k]), a), {}) .reduce((a, [k, v]) => (a[k] = merge(v, c[k]), a), {})
}), {}); }), {});
const languages = [
'ar-SA', fs.readdirSync(__dirname).forEach((file) => {
'cs-CZ', if (file.includes('.yml')){
'da-DK', file = file.slice(0, file.indexOf('.'))
'de-DE', languages.push(file);
'en-US', }
'es-ES', })
'fr-FR',
'id-ID', fs.readdirSync(__dirname + '/../custom/locales').forEach((file) => {
'it-IT', if (file.includes('.yml')){
'ja-JP', file = file.slice(0, file.indexOf('.'))
'ja-KS', languages_custom.push(file);
'kab-KAB', }
'kn-IN', })
'ko-KR',
'nl-NL',
'no-NO',
'pl-PL',
'pt-PT',
'ru-RU',
'sk-SK',
'ug-CN',
'uk-UA',
'vi-VN',
'zh-CN',
'zh-TW',
];
const primaries = { const primaries = {
'en': 'US', 'en': 'US',
@ -51,6 +40,8 @@ const primaries = {
const clean = (text) => text.replace(new RegExp(String.fromCodePoint(0x08), 'g'), ''); const clean = (text) => text.replace(new RegExp(String.fromCodePoint(0x08), 'g'), '');
const locales = languages.reduce((a, c) => (a[c] = yaml.load(clean(fs.readFileSync(`${__dirname}/${c}.yml`, 'utf-8'))) || {}, a), {}); const locales = languages.reduce((a, c) => (a[c] = yaml.load(clean(fs.readFileSync(`${__dirname}/${c}.yml`, 'utf-8'))) || {}, a), {});
const locales_custom = languages_custom.reduce((a, c) => (a[c] = yaml.load(clean(fs.readFileSync(`${__dirname}/../custom/locales/${c}.yml`, 'utf-8'))) || {}, a), {});
Object.assign(locales, locales_custom)
module.exports = Object.entries(locales) module.exports = Object.entries(locales)
.reduce((a, [k ,v]) => (a[k] = (() => { .reduce((a, [k ,v]) => (a[k] = (() => {

View file

@ -1,12 +1,12 @@
{ {
"name": "calckey", "name": "calckey",
"version": "12.119.0-calc.14.6", "version": "12.119.0-calc.18",
"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",
@ -42,7 +42,7 @@
"@bull-board/api": "^4.6.4", "@bull-board/api": "^4.6.4",
"@bull-board/ui": "^4.6.4", "@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",

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

@ -4,8 +4,8 @@
"private": true, "private": true,
"type": "module", "type": "module",
"scripts": { "scripts": {
"start": "node --experimental-json-modules ./built/index.js", "start": "node ./built/index.js",
"start:test": "NODE_ENV=test node --experimental-json-modules ./built/index.js", "start:test": "NODE_ENV=test node ./built/index.js",
"migrate": "typeorm migration:run -d ormconfig.js", "migrate": "typeorm migration:run -d ormconfig.js",
"build": "tsc -p tsconfig.json || echo done. && tsc-alias -p tsconfig.json", "build": "tsc -p tsconfig.json || echo done. && tsc-alias -p tsconfig.json",
"watch": "node watch.mjs", "watch": "node watch.mjs",
@ -36,11 +36,11 @@
"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.1255.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",
@ -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",
@ -177,7 +177,7 @@
"@typescript-eslint/eslint-plugin": "5.43.0", "@typescript-eslint/eslint-plugin": "5.43.0",
"@typescript-eslint/parser": "5.43.0", "@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.9.3" "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

@ -1,14 +1,14 @@
import { In, Repository } from 'typeorm'; import { In, Repository } from 'typeorm';
import { Users, Notes, UserGroupInvitations, AccessTokens, NoteReactions } from '../index.js';
import { Notification } from '@/models/entities/notification.js'; import { Notification } from '@/models/entities/notification.js';
import { awaitAll } from '@/prelude/await-all.js'; import { awaitAll } from '@/prelude/await-all.js';
import { Packed } from '@/misc/schema.js'; import type { Packed } from '@/misc/schema.js';
import { Note } from '@/models/entities/note.js'; import type { Note } from '@/models/entities/note.js';
import { NoteReaction } from '@/models/entities/note-reaction.js'; import type { NoteReaction } from '@/models/entities/note-reaction.js';
import { User } from '@/models/entities/user.js'; import type { User } from '@/models/entities/user.js';
import { aggregateNoteEmojis, prefetchEmojis } from '@/misc/populate-emojis.js'; import { aggregateNoteEmojis, prefetchEmojis } from '@/misc/populate-emojis.js';
import { notificationTypes } from '@/types.js'; import { notificationTypes } from '@/types.js';
import { db } from '@/db/postgre.js'; import { db } from '@/db/postgre.js';
import { Users, Notes, UserGroupInvitations, AccessTokens, NoteReactions } from '../index.js';
export const NotificationRepository = db.getRepository(Notification).extend({ export const NotificationRepository = db.getRepository(Notification).extend({
async pack( async pack(
@ -17,7 +17,7 @@ export const NotificationRepository = db.getRepository(Notification).extend({
_hintForEachNotes_?: { _hintForEachNotes_?: {
myReactions: Map<Note['id'], NoteReaction | null>; myReactions: Map<Note['id'], NoteReaction | null>;
}; };
} },
): Promise<Packed<'Notification'>> { ): Promise<Packed<'Notification'>> {
const notification = typeof src === 'object' ? src : await this.findOneByOrFail({ id: src }); const notification = typeof src === 'object' ? src : await this.findOneByOrFail({ id: src });
const token = notification.appAccessTokenId ? await AccessTokens.findOneByOrFail({ id: notification.appAccessTokenId }) : null; const token = notification.appAccessTokenId ? await AccessTokens.findOneByOrFail({ id: notification.appAccessTokenId }) : null;
@ -86,7 +86,7 @@ export const NotificationRepository = db.getRepository(Notification).extend({
async packMany( async packMany(
notifications: Notification[], notifications: Notification[],
meId: User['id'] meId: User['id'],
) { ) {
if (notifications.length === 0) return []; if (notifications.length === 0) return [];
@ -106,10 +106,15 @@ export const NotificationRepository = db.getRepository(Notification).extend({
await prefetchEmojis(aggregateNoteEmojis(notes)); await prefetchEmojis(aggregateNoteEmojis(notes));
return await Promise.all(notifications.map(x => this.pack(x, { const results = await Promise.all(notifications
_hintForEachNotes_: { .map(x =>
myReactions: myReactionsMap, this.pack(x, {
}, _hintForEachNotes_: {
}))); myReactions: myReactionsMap,
},
}).catch(e => null),
),
);
return results.filter(x => x != null);
}, },
}); });

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

@ -26,7 +26,7 @@ export default async (actor: CacheableRemoteUser, activity: IUpdate): Promise<st
await updatePerson(actor.uri!, resolver, object); await updatePerson(actor.uri!, resolver, object);
return `ok: Person updated`; return `ok: Person updated`;
} else if (getApType(object) === 'Question') { } else if (getApType(object) === 'Question') {
await updateQuestion(object).catch(e => console.log(e)); await updateQuestion(object, resolver).catch(e => console.log(e));
return `ok: Question updated`; return `ok: Question updated`;
} else { } else {
return `skip: Unknown type: ${getApType(object)}`; return `skip: Unknown type: ${getApType(object)}`;

View file

@ -272,7 +272,7 @@ export async function createPerson(uri: string, resolver?: Resolver): Promise<Us
}); });
//#endregion //#endregion
await updateFeatured(user!.id).catch(err => logger.error(err)); await updateFeatured(user!.id, resolver).catch(err => logger.error(err));
return user!; return user!;
} }
@ -386,7 +386,7 @@ export async function updatePerson(uri: string, resolver?: Resolver | null, hint
followerSharedInbox: person.sharedInbox || (person.endpoints ? person.endpoints.sharedInbox : undefined), followerSharedInbox: person.sharedInbox || (person.endpoints ? person.endpoints.sharedInbox : undefined),
}); });
await updateFeatured(exist.id).catch(err => logger.error(err)); await updateFeatured(exist.id, resolver).catch(err => logger.error(err));
} }
/** /**
@ -464,14 +464,14 @@ export function analyzeAttachments(attachments: IObject | IObject[] | undefined)
return { fields, services }; return { fields, services };
} }
export async function updateFeatured(userId: User['id']) { export async function updateFeatured(userId: User['id'], resolver?: Resolver) {
const user = await Users.findOneByOrFail({ id: userId }); const user = await Users.findOneByOrFail({ id: userId });
if (!Users.isRemoteUser(user)) return; if (!Users.isRemoteUser(user)) return;
if (!user.featured) return; if (!user.featured) return;
logger.info(`Updating the featured: ${user.uri}`); logger.info(`Updating the featured: ${user.uri}`);
const resolver = new Resolver(); if (resolver == null) resolver = new Resolver();
// Resolve to (Ordered)Collection Object // Resolve to (Ordered)Collection Object
const collection = await resolver.resolveCollection(user.featured); const collection = await resolver.resolveCollection(user.featured);

View file

@ -40,7 +40,7 @@ export async function extractPollFromQuestion(source: string | IObject, resolver
* @param uri URI of AP Question object * @param uri URI of AP Question object
* @returns true if updated * @returns true if updated
*/ */
export async function updateQuestion(value: any) { export async function updateQuestion(value: any, resolver?: Resolver) {
const uri = typeof value === 'string' ? value : value.id; const uri = typeof value === 'string' ? value : value.id;
// URIがこのサーバーを指しているならスキップ // URIがこのサーバーを指しているならスキップ
@ -55,7 +55,7 @@ export async function updateQuestion(value: any) {
//#endregion //#endregion
// resolve new Question object // resolve new Question object
const resolver = new Resolver(); if (resolver == null) resolver = new Resolver();
const question = await resolver.resolve(value) as IQuestion; const question = await resolver.resolve(value) as IQuestion;
apLogger.debug(`fetched question: ${JSON.stringify(question, null, 2)}`); apLogger.debug(`fetched question: ${JSON.stringify(question, null, 2)}`);

View file

@ -19,9 +19,11 @@ import renderFollow from '@/remote/activitypub/renderer/follow.js';
export default class Resolver { export default class Resolver {
private history: Set<string>; private history: Set<string>;
private user?: ILocalUser; private user?: ILocalUser;
private recursionLimit?: number;
constructor() { constructor(recursionLimit = 100) {
this.history = new Set(); this.history = new Set();
this.recursionLimit = recursionLimit;
} }
public getHistory(): string[] { public getHistory(): string[] {
@ -59,7 +61,9 @@ export default class Resolver {
if (this.history.has(value)) { if (this.history.has(value)) {
throw new Error('cannot resolve already resolved one'); throw new Error('cannot resolve already resolved one');
} }
if (this.recursionLimit && this.history.size > this.recursionLimit) {
throw new Error('hit recursion limit');
}
this.history.add(value); this.history.add(value);
const host = extractDbHost(value); const host = extractDbHost(value);

View file

@ -275,6 +275,7 @@ import * as ep___pinnedUsers from './endpoints/pinned-users.js';
import * as ep___customMOTD from './endpoints/custom-motd.js'; import * as ep___customMOTD from './endpoints/custom-motd.js';
import * as ep___customSplashIcons from './endpoints/custom-splash-icons.js'; import * as ep___customSplashIcons from './endpoints/custom-splash-icons.js';
import * as ep___latestVersion from './endpoints/latest-version.js'; import * as ep___latestVersion from './endpoints/latest-version.js';
import * as ep___patrons from './endpoints/patrons.js';
import * as ep___promo_read from './endpoints/promo/read.js'; import * as ep___promo_read from './endpoints/promo/read.js';
import * as ep___requestResetPassword from './endpoints/request-reset-password.js'; import * as ep___requestResetPassword from './endpoints/request-reset-password.js';
import * as ep___resetDb from './endpoints/reset-db.js'; import * as ep___resetDb from './endpoints/reset-db.js';
@ -599,6 +600,7 @@ const eps = [
['custom-motd', ep___customMOTD], ['custom-motd', ep___customMOTD],
['custom-splash-icons', ep___customSplashIcons], ['custom-splash-icons', ep___customSplashIcons],
['latest-version', ep___latestVersion], ['latest-version', ep___latestVersion],
['patrons', ep___patrons],
['promo/read', ep___promo_read], ['promo/read', ep___promo_read],
['request-reset-password', ep___requestResetPassword], ['request-reset-password', ep___requestResetPassword],
['reset-db', ep___resetDb], ['reset-db', ep___resetDb],

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

@ -0,0 +1,27 @@
import define from '../define.js';
export const meta = {
tags: ['meta'],
description: 'Get list of Calckey patrons from Codeberg',
requireCredential: false,
requireCredentialPrivateMode: false,
} as const;
export const paramDef = {
type: 'object',
properties: {},
required: [],
} as const;
// eslint-disable-next-line import/no-default-export
export default define(meta, paramDef, async () => {
let patrons;
await fetch('https://codeberg.org/thatonecalculator/calckey/raw/branch/develop/patrons.json')
.then((response) => response.json())
.then((data) => {
patrons = data['patrons'];
});
return patrons;
});

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

@ -9,7 +9,7 @@ export function genOpenapiSpec() {
info: { info: {
version: 'v1', version: 'v1',
title: 'Misskey API', title: 'Calckey API',
'x-logo': { url: '/static-assets/api-doc.png' }, 'x-logo': { url: '/static-assets/api-doc.png' },
}, },

View file

@ -232,8 +232,43 @@ const getFeed = async (acct: string) => {
return user && await packFeed(user); return user && await packFeed(user);
}; };
// As the /@user[.json|.rss|.atom]/sub endpoint is complicated, we will use a regex to switch between them.
const reUser = new RegExp(`^/@(?<user>[^/]+?)(?:\.(?<feed>json|rss|atom))?(?:/(?<sub>[^/]+))?$`);
router.get(reUser, async (ctx, next) => {
const groups = reUser.exec(ctx.originalUrl)?.groups;
if (!groups) {
await next();
return;
}
ctx.params = groups;
console.log(ctx, ctx.params)
if (groups.feed) {
if (groups.sub) {
await next();
return;
}
switch (groups.feed) {
case 'json':
await jsonFeed(ctx, next);
break;
case 'rss':
await rssFeed(ctx, next);
break;
case 'atom':
await atomFeed(ctx, next);
break;
}
return;
}
await userPage(ctx, next);
});
// Atom // Atom
router.get('/@:user.atom', async ctx => { const atomFeed: Router.Middleware = async ctx => {
const feed = await getFeed(ctx.params.user); const feed = await getFeed(ctx.params.user);
if (feed) { if (feed) {
@ -242,10 +277,10 @@ router.get('/@:user.atom', async ctx => {
} else { } else {
ctx.status = 404; ctx.status = 404;
} }
}); };
// RSS // RSS
router.get('/@:user.rss', async ctx => { const rssFeed: Router.Middleware = async ctx => {
const feed = await getFeed(ctx.params.user); const feed = await getFeed(ctx.params.user);
if (feed) { if (feed) {
@ -254,10 +289,10 @@ router.get('/@:user.rss', async ctx => {
} else { } else {
ctx.status = 404; ctx.status = 404;
} }
}); };
// JSON // JSON
router.get('/@:user.json', async ctx => { const jsonFeed: Router.Middleware = async ctx => {
const feed = await getFeed(ctx.params.user); const feed = await getFeed(ctx.params.user);
if (feed) { if (feed) {
@ -266,43 +301,47 @@ router.get('/@:user.json', async ctx => {
} else { } else {
ctx.status = 404; ctx.status = 404;
} }
}); };
//#region SSR (for crawlers) //#region SSR (for crawlers)
// User // User
router.get(['/@:user', '/@:user/:sub'], async (ctx, next) => { const userPage: Router.Middleware = async (ctx, next) => {
const { username, host } = Acct.parse(ctx.params.user); const userParam = ctx.params.user;
const subParam = ctx.params.sub;
const { username, host } = Acct.parse(userParam);
const user = await Users.findOneBy({ const user = await Users.findOneBy({
usernameLower: username.toLowerCase(), usernameLower: username.toLowerCase(),
host: host ?? IsNull(), host: host ?? IsNull(),
isSuspended: false, isSuspended: false,
}); });
if (user != null) { if (user === null) {
const profile = await UserProfiles.findOneByOrFail({ userId: user.id });
const meta = await fetchMeta();
const me = profile.fields
? profile.fields
.filter(filed => filed.value != null && filed.value.match(/^https?:/))
.map(field => field.value)
: [];
await ctx.render('user', {
user, profile, me,
avatarUrl: await Users.getAvatarUrl(user),
sub: ctx.params.sub,
instanceName: meta.name || 'Calckey',
icon: meta.iconUrl,
themeColor: meta.themeColor,
privateMode: meta.privateMode,
});
ctx.set('Cache-Control', 'public, max-age=15');
} else {
// リモートユーザーなので
// モデレータがAPI経由で参照可能にするために404にはしない
await next(); await next();
return;
} }
});
const profile = await UserProfiles.findOneByOrFail({ userId: user.id });
const meta = await fetchMeta();
const me = profile.fields
? profile.fields
.filter(filed => filed.value != null && filed.value.match(/^https?:/))
.map(field => field.value)
: [];
const userDetail = {
user, profile, me,
avatarUrl: await Users.getAvatarUrl(user),
sub: subParam,
instanceName: meta.name || 'Calckey',
icon: meta.iconUrl,
themeColor: meta.themeColor,
privateMode: meta.privateMode,
};
await ctx.render('user', userDetail);
ctx.set('Cache-Control', 'public, max-age=15');
};
router.get('/users/:user', async ctx => { router.get('/users/:user', async ctx => {
const user = await Users.findOneBy({ const user = await Users.findOneBy({

View file

@ -42,7 +42,7 @@ html {
width: 28px; width: 28px;
height: 28px; height: 28px;
transform: translateY(110px); transform: translateY(110px);
display: none !important; display: none;
color: var(--accent); color: var(--accent);
} }
#splashSpinner > .spinner { #splashSpinner > .spinner {
@ -101,6 +101,16 @@ html {
} }
} }
@media(prefers-reduced-motion) {
#splashSpinner {
display: block;
}
#splashIcon {
animation: none;
}
}
#splashText { #splashText {
position: absolute; position: absolute;
top: 0; top: 0;

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,7 +48,7 @@
"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",
@ -80,7 +80,7 @@
"@typescript-eslint/parser": "5.43.0", "@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

@ -81,9 +81,12 @@ const bannerStyle = computed(() => {
top: 16px; top: 16px;
left: 16px; left: 16px;
padding: 12px 16px; padding: 12px 16px;
background: rgba(0, 0, 0, 0.7); -webkit-backdrop-filter: var(--blur, blur(8px));
backdrop-filter: var(--blur, blur(8px));
background: rgba(0, 0, 0, 0.2);
color: #fff; color: #fff;
font-size: 1.2em; font-size: 1.2em;
border-radius: 999px;
} }
> .status { > .status {
@ -93,7 +96,9 @@ const bannerStyle = computed(() => {
right: 16px; right: 16px;
padding: 8px 12px; padding: 8px 12px;
font-size: 80%; font-size: 80%;
background: rgba(0, 0, 0, 0.7); -webkit-backdrop-filter: var(--blur, blur(8px));
backdrop-filter: var(--blur, blur(8px));
background: rgba(0, 0, 0, 0.2);
border-radius: 6px; border-radius: 6px;
color: #fff; color: #fff;
} }

View file

@ -178,6 +178,7 @@ export default defineComponent({
> ::v-deep(i) { > ::v-deep(i) {
margin-right: 6px; margin-right: 6px;
transform: translateY(0.1em);
} }
&:empty { &:empty {

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,16 +31,16 @@
<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';
const props = withDefaults(defineProps<{ 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,9 +52,9 @@ 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);
} }
function onFollowChange(user: Misskey.entities.UserDetailed) { function onFollowChange(user: Misskey.entities.UserDetailed) {
@ -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

@ -64,6 +64,7 @@ const bg = {
font-size: 0.9em; font-size: 0.9em;
vertical-align: top; vertical-align: top;
font-weight: bold; font-weight: bold;
text-overflow: clip;
} }
} }
</style> </style>

View file

@ -2,7 +2,7 @@
<MkModal ref="modal" @click="$emit('click')" @closed="$emit('closed')"> <MkModal ref="modal" @click="$emit('click')" @closed="$emit('closed')">
<div ref="rootEl" class="hrmcaedk _narrow_" :style="{ width: `${width}px`, height: (height ? `min(${height}px, 100%)` : '100%') }"> <div ref="rootEl" class="hrmcaedk _narrow_" :style="{ width: `${width}px`, height: (height ? `min(${height}px, 100%)` : '100%') }">
<div class="header" @contextmenu="onContextmenu"> <div class="header" @contextmenu="onContextmenu">
<button v-if="history.length > 0" v-tooltip="i18n.ts.goBack" class="_button" @click="back()"><i class="ph--left-bold ph-lg"></i></button> <button v-if="history.length > 0" v-tooltip="i18n.ts.goBack" class="_button" @click="back()"><i class="ph-caret-left-bold ph-lg"></i></button>
<span v-else style="display: inline-block; width: 20px"></span> <span v-else style="display: inline-block; width: 20px"></span>
<span v-if="pageMetadata?.value" class="title"> <span v-if="pageMetadata?.value" class="title">
<i v-if="pageMetadata?.value.icon" class="icon" :class="pageMetadata?.value.icon"></i> <i v-if="pageMetadata?.value.icon" class="icon" :class="pageMetadata?.value.icon"></i>

View file

@ -71,21 +71,21 @@
</div> </div>
<footer class="footer"> <footer class="footer">
<XReactionsViewer ref="reactionsViewer" :note="appearNote"/> <XReactionsViewer ref="reactionsViewer" :note="appearNote"/>
<button class="button _button" @click="reply()"> <button v-tooltip.noDelay.bottom="i18n.ts.reply" class="button _button" @click="reply()">
<template v-if="appearNote.reply"><i class="ph-arrow-u-up-left-bold ph-lg"></i></template> <template v-if="appearNote.reply"><i class="ph-arrow-u-up-left-bold ph-lg"></i></template>
<template v-else><i class="ph-arrow-bend-up-left-bold ph-lg"></i></template> <template v-else><i class="ph-arrow-bend-up-left-bold ph-lg"></i></template>
<p v-if="appearNote.repliesCount > 0" class="count">{{ appearNote.repliesCount }}</p> <p v-if="appearNote.repliesCount > 0" class="count">{{ appearNote.repliesCount }}</p>
</button> </button>
<XRenoteButton ref="renoteButton" class="button" :note="appearNote" :count="appearNote.renoteCount"/> <XRenoteButton ref="renoteButton" class="button" :note="appearNote" :count="appearNote.renoteCount"/>
<XStarButton v-if="appearNote.myReaction == null" ref="starButton" class="button" :note="appearNote"/> <XStarButton v-if="appearNote.myReaction == null" ref="starButton" class="button" :note="appearNote"/>
<button v-if="appearNote.myReaction == null" ref="reactButton" class="button _button" @click="react()"> <button v-if="appearNote.myReaction == null" ref="reactButton" v-tooltip.noDelay.bottom="i18n.ts.reaction" class="button _button" @click="react()">
<i class="ph-smiley-bold ph-lg"></i> <i class="ph-smiley-bold ph-lg"></i>
</button> </button>
<button v-if="appearNote.myReaction != null" ref="reactButton" class="button _button reacted" @click="undoReact(appearNote)"> <button v-if="appearNote.myReaction != null" ref="reactButton" class="button _button reacted" @click="undoReact(appearNote)">
<i class="ph-minus-bold ph-lg"></i> <i class="ph-minus-bold ph-lg"></i>
</button> </button>
<XQuoteButton class="button" :note="appearNote"/> <XQuoteButton class="button" :note="appearNote"/>
<button ref="menuButton" class="button _button" @click="menu()"> <button ref="menuButton" v-tooltip.noDelay.bottom="i18n.ts.more" class="button _button" @click="menu()">
<i class="ph-dots-three-outline-bold ph-lg"></i> <i class="ph-dots-three-outline-bold ph-lg"></i>
</button> </button>
</footer> </footer>
@ -135,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();
@ -145,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);
} }
@ -425,13 +426,18 @@ function readPromo() {
> .article { > .article {
display: flex; display: flex;
padding: 28px 32px 18px; padding: 28px 32px 18px;
cursor: pointer;
@media (pointer: coarse) {
cursor: default;
}
> .avatar { > .avatar {
flex-shrink: 0; flex-shrink: 0;
display: block; display: block;
margin: 0 14px 8px 0; margin: 0 14px 8px 0;
width: 58px; width: 52px;
height: 58px; height: 52px;
position: sticky; position: sticky;
/* For some reason this breaks avatar /* For some reason this breaks avatar
positions on notes, commenting it for now */ positions on notes, commenting it for now */
@ -612,7 +618,7 @@ function readPromo() {
margin: 0 10px 8px 0; margin: 0 10px 8px 0;
width: 46px; width: 46px;
height: 46px; height: 46px;
top: calc(14px + var(--stickyTop, 0px)); // top: calc(14px + var(--stickyTop, 0px));
} }
} }
} }

View file

@ -81,21 +81,21 @@
</MkA> </MkA>
</div> </div>
<XReactionsViewer ref="reactionsViewer" :note="appearNote"/> <XReactionsViewer ref="reactionsViewer" :note="appearNote"/>
<button class="button _button" @click="reply()"> <button v-tooltip.noDelay.bottom="i18n.ts.reply" class="button _button" @click="reply()">
<template v-if="appearNote.reply"><i class="ph-arrow-u-up-left-bold ph-lg"></i></template> <template v-if="appearNote.reply"><i class="ph-arrow-u-up-left-bold ph-lg"></i></template>
<template v-else><i class="ph-arrow-bend-up-left-bold ph-lg"></i></template> <template v-else><i class="ph-arrow-bend-up-left-bold ph-lg"></i></template>
<p v-if="appearNote.repliesCount > 0" class="count">{{ appearNote.repliesCount }}</p> <p v-if="appearNote.repliesCount > 0" class="count">{{ appearNote.repliesCount }}</p>
</button> </button>
<XRenoteButton ref="renoteButton" class="button" :note="appearNote" :count="appearNote.renoteCount"/> <XRenoteButton ref="renoteButton" class="button" :note="appearNote" :count="appearNote.renoteCount"/>
<XStarButton v-if="appearNote.myReaction == null" ref="starButton" class="button" :note="appearNote"/> <XStarButton v-if="appearNote.myReaction == null" ref="starButton" class="button" :note="appearNote"/>
<button v-if="appearNote.myReaction == null" ref="reactButton" class="button _button" @click="react()"> <button v-if="appearNote.myReaction == null" ref="reactButton" v-tooltip.noDelay.bottom="i18n.ts.reaction" class="button _button" @click="react()">
<i class="ph-smiley-bold ph-lg"></i> <i class="ph-smiley-bold ph-lg"></i>
</button> </button>
<button v-if="appearNote.myReaction != null" ref="reactButton" class="button _button reacted" @click="undoReact(appearNote)"> <button v-if="appearNote.myReaction != null" ref="reactButton" class="button _button reacted" @click="undoReact(appearNote)">
<i class="ph-minus-bold ph-lg"></i> <i class="ph-minus-bold ph-lg"></i>
</button> </button>
<XQuoteButton class="button" :note="appearNote"/> <XQuoteButton class="button" :note="appearNote"/>
<button ref="menuButton" class="button _button" @click="menu()"> <button ref="menuButton" v-tooltip.noDelay.bottom="i18n.ts.more" class="button _button" @click="menu()">
<i class="ph-dots-three-outline-bold ph-lg"></i> <i class="ph-dots-three-outline-bold ph-lg"></i>
</button> </button>
</footer> </footer>
@ -117,7 +117,7 @@
<script lang="ts" setup> <script lang="ts" setup>
import { computed, inject, onMounted, onUnmounted, reactive, 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 * as misskey from 'misskey-js';
import MkNoteSub from '@/components/MkNoteSub.vue'; import MkNoteSub from '@/components/MkNoteSub.vue';
import XNoteSimple from '@/components/MkNoteSimple.vue'; import XNoteSimple from '@/components/MkNoteSimple.vue';
import XReactionsViewer from '@/components/MkReactionsViewer.vue'; import XReactionsViewer from '@/components/MkReactionsViewer.vue';
@ -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);
} }
@ -345,6 +346,11 @@ if (appearNote.replyId) {
> .reply-to-more { > .reply-to-more {
opacity: 0.7; opacity: 0.7;
cursor: pointer;
@media (pointer: coarse) {
cursor: default;
}
} }
> .renote { > .renote {
@ -410,8 +416,8 @@ if (appearNote.replyId) {
> .avatar { > .avatar {
display: block; display: block;
flex-shrink: 0; flex-shrink: 0;
width: 58px; width: 52px;
height: 58px; height: 52px;
} }
> .body { > .body {
@ -542,6 +548,11 @@ if (appearNote.replyId) {
> .reply { > .reply {
border-top: solid 0.5px var(--divider); border-top: solid 0.5px var(--divider);
cursor: pointer;
@media (pointer: coarse) {
cursor: default;
}
} }
> .reply, .reply-to, .reply-to-more { > .reply, .reply-to, .reply-to-more {

View file

@ -65,6 +65,7 @@ const replies: misskey.entities.Note[] = props.conversation?.filter(item => item
&.children { &.children {
padding: 10px 0 0 16px; padding: 10px 0 0 16px;
font-size: 1em; font-size: 1em;
cursor: auto;
&.max-width_450px { &.max-width_450px {
padding: 10px 0 0 8px; padding: 10px 0 0 8px;
@ -86,9 +87,15 @@ const replies: misskey.entities.Note[] = props.conversation?.filter(item => item
> .body { > .body {
flex: 1; flex: 1;
min-width: 0; min-width: 0;
cursor: pointer;
@media (pointer: coarse) {
cursor: default;
}
> .header { > .header {
margin-bottom: 2px; margin-bottom: 2px;
cursor: auto;
} }
> .body { > .body {

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

@ -57,7 +57,7 @@ const buttonsLeft = $computed(() => {
if (history.length > 1) { if (history.length > 1) {
buttons.push({ buttons.push({
icon: 'ph--left-bold ph-lg', icon: 'ph-caret-left-bold ph-lg',
onClick: back, onClick: back,
}); });
} }

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');
@ -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));
} }
} }
@ -761,7 +762,7 @@ onMounted(() => {
margin-left: 0 !important; margin-left: 0 !important;
} }
} }
> .local-only { > .local-only {
margin: 0 0 0 12px; margin: 0 0 0 12px;
opacity: 0.7; opacity: 0.7;
@ -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

@ -1,6 +1,7 @@
<template> <template>
<button <button
v-if="canRenote && $store.state.seperateRenoteQuote" v-if="canRenote && $store.state.seperateRenoteQuote"
v-tooltip.noDelay.bottom="i18n.ts.quote"
class="eddddedb _button" class="eddddedb _button"
@click="quote()" @click="quote()"
> >
@ -14,6 +15,7 @@ import type { Note } from 'misskey-js/built/entities';
import { pleaseLogin } from '@/scripts/please-login'; import { pleaseLogin } from '@/scripts/please-login';
import * as os from '@/os'; import * as os from '@/os';
import { $i } from '@/account'; import { $i } from '@/account';
import { i18n } from '@/i18n';
const props = defineProps<{ const props = defineProps<{
note: Note; note: Note;

View file

@ -2,6 +2,7 @@
<button <button
v-if="canRenote" v-if="canRenote"
ref="buttonRef" ref="buttonRef"
v-tooltip.noDelay.bottom="i18n.ts.renote"
class="eddddedb _button canRenote" class="eddddedb _button canRenote"
@click="renote(false, $event)" @click="renote(false, $event)"
> >
@ -15,7 +16,7 @@
<script lang="ts" setup> <script lang="ts" setup>
import { computed, ref } from 'vue'; import { computed, ref } from 'vue';
import * as misskey from 'misskey-js'; import type * as misskey from 'misskey-js';
import Ripple from '@/components/MkRipple.vue'; import Ripple from '@/components/MkRipple.vue';
import XDetails from '@/components/MkUsersTooltip.vue'; import XDetails from '@/components/MkUsersTooltip.vue';
import { pleaseLogin } from '@/scripts/please-login'; import { pleaseLogin } from '@/scripts/please-login';
@ -23,7 +24,7 @@ import * as os from '@/os';
import { $i } from '@/account'; import { $i } from '@/account';
import { useTooltip } from '@/scripts/use-tooltip'; import { useTooltip } from '@/scripts/use-tooltip';
import { i18n } from '@/i18n'; import { i18n } from '@/i18n';
import { defaultStore } from "@/store"; import { defaultStore } from '@/store';
const props = defineProps<{ const props = defineProps<{
note: misskey.entities.Note; note: misskey.entities.Note;
@ -52,42 +53,62 @@ useTooltip(buttonRef, async (showing) => {
}, {}, 'closed'); }, {}, 'closed');
}); });
const renote = (viaKeyboard = false, ev?: MouseEvent) => { const renote = async (viaKeyboard = false, ev?: MouseEvent) => {
pleaseLogin(); pleaseLogin();
if (defaultStore.state.seperateRenoteQuote) {
os.api('notes/create', { const renotes = await os.api('notes/renotes', {
renoteId: props.note.id, noteId: props.note.id,
visibility: props.note.visibility, limit: 11,
}); });
const el = ev && (ev.currentTarget ?? ev.target) as HTMLElement | null | undefined;
if (el) { const users = renotes.map(x => x.user.id);
const rect = el.getBoundingClientRect(); const hasRenotedBefore = users.includes($i.id);
const x = rect.left + (el.offsetWidth / 2);
const y = rect.top + (el.offsetHeight / 2); let buttonActions = [{
os.popup(Ripple, { x, y }, {}, 'end'); text: i18n.ts.renote,
} icon: 'ph-repeat-bold ph-lg',
} else { danger: false,
os.popupMenu([{ action: () => {
text: i18n.ts.renote, os.api('notes/create', {
icon: 'ph-repeat-bold ph-lg', renoteId: props.note.id,
action: () => { visibility: props.note.visibility,
os.api('notes/create', { });
renoteId: props.note.id, const el = ev && (ev.currentTarget ?? ev.target) as HTMLElement | null | undefined;
visibility: props.note.visibility, if (el) {
}); const rect = el.getBoundingClientRect();
}, const x = rect.left + (el.offsetWidth / 2);
}, { const y = rect.top + (el.offsetHeight / 2);
os.popup(Ripple, { x, y }, {}, 'end');
}
},
}];
if (!defaultStore.state.seperateRenoteQuote) {
buttonActions.push({
text: i18n.ts.quote, text: i18n.ts.quote,
icon: 'ph-quotes-bold ph-lg', icon: 'ph-quotes-bold ph-lg',
danger: false,
action: () => { action: () => {
os.post({ os.post({
renote: props.note, renote: props.note,
}); });
}, },
}], buttonRef.value, {
viaKeyboard,
}); });
} }
if (hasRenotedBefore) {
buttonActions.push({
text: i18n.ts.unrenote,
icon: 'ph-trash-bold ph-lg',
danger: true,
action: () => {
os.api('notes/unrenote', {
noteId: props.note.id,
});
},
});
}
os.popupMenu(buttonActions, buttonRef.value, { viaKeyboard });
}; };
</script> </script>

View file

@ -65,6 +65,7 @@
<script lang="ts" setup> <script lang="ts" setup>
import { onMounted, onUnmounted, ref } from 'vue'; import { onMounted, onUnmounted, ref } from 'vue';
import { reducedMotion } from '@/scripts/reduced-motion';
const particles = ref([]); const particles = ref([]);
const el = ref<HTMLElement>(); const el = ref<HTMLElement>();
@ -75,34 +76,36 @@ let stop = false;
let ro: ResizeObserver | undefined; let ro: ResizeObserver | undefined;
onMounted(() => { onMounted(() => {
ro = new ResizeObserver((entries, observer) => { if (!reducedMotion()) {
width.value = el.value?.offsetWidth + 64; ro = new ResizeObserver((entries, observer) => {
height.value = el.value?.offsetHeight + 64; width.value = el.value?.offsetWidth + 64;
}); height.value = el.value?.offsetHeight + 64;
ro.observe(el.value); });
const add = () => { ro.observe(el.value);
if (stop) return; const add = () => {
const x = (Math.random() * (width.value - 64)); if (stop) return;
const y = (Math.random() * (height.value - 64)); const x = (Math.random() * (width.value - 64));
const sizeFactor = Math.random(); const y = (Math.random() * (height.value - 64));
const particle = { const sizeFactor = Math.random();
id: Math.random().toString(), const particle = {
x, id: Math.random().toString(),
y, x,
size: 0.2 + ((sizeFactor / 10) * 3), y,
dur: 1000 + (sizeFactor * 1000), size: 0.2 + ((sizeFactor / 10) * 3),
color: colors[Math.floor(Math.random() * colors.length)], dur: 1000 + (sizeFactor * 1000),
}; color: colors[Math.floor(Math.random() * colors.length)],
particles.value.push(particle); };
window.setTimeout(() => { particles.value.push(particle);
particles.value = particles.value.filter(x => x.id !== particle.id); window.setTimeout(() => {
}, particle.dur - 100); particles.value = particles.value.filter(x => x.id !== particle.id);
}, particle.dur - 100);
window.setTimeout(() => { window.setTimeout(() => {
add(); add();
}, 500 + (Math.random() * 500)); }, 500 + (Math.random() * 500));
}; };
add(); add();
}
}); });
onUnmounted(() => { onUnmounted(() => {

View file

@ -1,5 +1,5 @@
<template> <template>
<button class="skdfgljsdkf _button" @click="star($event)"> <button v-tooltip.noDelay.bottom="i18n.ts._gallery.like" class="skdfgljsdkf _button" @click="star($event)">
<i class="ph-star-bold ph-lg"></i> <i class="ph-star-bold ph-lg"></i>
</button> </button>
</template> </template>
@ -9,6 +9,7 @@ import type { Note } from 'misskey-js/built/entities';
import Ripple from '@/components/MkRipple.vue'; import Ripple from '@/components/MkRipple.vue';
import { pleaseLogin } from '@/scripts/please-login'; import { pleaseLogin } from '@/scripts/please-login';
import * as os from '@/os'; import * as os from '@/os';
import { i18n } from '@/i18n';
const props = defineProps<{ const props = defineProps<{
note: Note; note: Note;

View file

@ -1,5 +1,6 @@
import { VNode, defineComponent, h } from 'vue'; import { defineComponent, h } from 'vue';
import * as mfm from 'mfm-js'; import * as mfm from 'mfm-js';
import type { VNode } from 'vue';
import MkUrl from '@/components/global/MkUrl.vue'; import MkUrl from '@/components/global/MkUrl.vue';
import MkLink from '@/components/MkLink.vue'; import MkLink from '@/components/MkLink.vue';
import MkMention from '@/components/MkMention.vue'; import MkMention from '@/components/MkMention.vue';
@ -12,6 +13,7 @@ import MkSparkle from '@/components/MkSparkle.vue';
import MkA from '@/components/global/MkA.vue'; import MkA from '@/components/global/MkA.vue';
import { host } from '@/config'; import { host } from '@/config';
import { MFM_TAGS } from '@/scripts/mfm-tags'; import { MFM_TAGS } from '@/scripts/mfm-tags';
import { reducedMotion } from '@/scripts/reduced-motion';
export default defineComponent({ export default defineComponent({
props: { props: {
@ -97,17 +99,17 @@ export default defineComponent({
} }
case 'jelly': { case 'jelly': {
const speed = validTime(token.props.args.speed) || '1s'; const speed = validTime(token.props.args.speed) || '1s';
style = (this.$store.state.animatedMfm ? `animation: mfm-rubberBand ${speed} linear infinite both;` : ''); style = (this.$store.state.animatedMfm && !reducedMotion() ? `animation: mfm-rubberBand ${speed} linear infinite both;` : '');
break; break;
} }
case 'twitch': { case 'twitch': {
const speed = validTime(token.props.args.speed) || '0.5s'; const speed = validTime(token.props.args.speed) || '0.5s';
style = this.$store.state.animatedMfm ? `animation: mfm-twitch ${speed} ease infinite;` : ''; style = this.$store.state.animatedMfm && !reducedMotion() ? `animation: mfm-twitch ${speed} ease infinite;` : '';
break; break;
} }
case 'shake': { case 'shake': {
const speed = validTime(token.props.args.speed) || '0.5s'; const speed = validTime(token.props.args.speed) || '0.5s';
style = this.$store.state.animatedMfm ? `animation: mfm-shake ${speed} ease infinite;` : ''; style = this.$store.state.animatedMfm && !reducedMotion() ? `animation: mfm-shake ${speed} ease infinite;` : '';
break; break;
} }
case 'spin': { case 'spin': {
@ -120,19 +122,30 @@ export default defineComponent({
token.props.args.y ? 'mfm-spinY' : token.props.args.y ? 'mfm-spinY' :
'mfm-spin'; 'mfm-spin';
const speed = validTime(token.props.args.speed) || '1.5s'; const speed = validTime(token.props.args.speed) || '1.5s';
style = this.$store.state.animatedMfm ? `animation: ${anime} ${speed} linear infinite; animation-direction: ${direction};` : ''; style = this.$store.state.animatedMfm && !reducedMotion() ? `animation: ${anime} ${speed} linear infinite; animation-direction: ${direction};` : '';
break; break;
} }
case 'jump': { case 'jump': {
const speed = validTime(token.props.args.speed) || '0.75s'; const speed = validTime(token.props.args.speed) || '0.75s';
style = this.$store.state.animatedMfm ? `animation: mfm-jump ${speed} linear infinite;` : ''; style = this.$store.state.animatedMfm && !reducedMotion() ? `animation: mfm-jump ${speed} linear infinite;` : '';
break; break;
} }
case 'bounce': { case 'bounce': {
const speed = validTime(token.props.args.speed) || '0.75s'; const speed = validTime(token.props.args.speed) || '0.75s';
style = this.$store.state.animatedMfm ? `animation: mfm-bounce ${speed} linear infinite; transform-origin: center bottom;` : ''; style = this.$store.state.animatedMfm && !reducedMotion() ? `animation: mfm-bounce ${speed} linear infinite; transform-origin: center bottom;` : '';
break; break;
} }
case 'rainbow': {
const speed = validTime(token.props.args.speed) || '1s';
style = this.$store.state.animatedMfm && !reducedMotion() ? `animation: mfm-rainbow ${speed} linear infinite;` : '';
break;
}
case 'sparkle': {
if (!this.$store.state.animatedMfm && !reducedMotion()) {
return genEl(token.children);
}
return h(MkSparkle, {}, genEl(token.children));
}
case 'flip': { case 'flip': {
const transform = const transform =
(token.props.args.h && token.props.args.v) ? 'scale(-1, -1)' : (token.props.args.h && token.props.args.v) ? 'scale(-1, -1)' :
@ -173,17 +186,6 @@ export default defineComponent({
class: '_mfm_blur_', class: '_mfm_blur_',
}, genEl(token.children)); }, genEl(token.children));
} }
case 'rainbow': {
const speed = validTime(token.props.args.speed) || '1s';
style = this.$store.state.animatedMfm ? `animation: mfm-rainbow ${speed} linear infinite;` : '';
break;
}
case 'sparkle': {
if (!this.$store.state.animatedMfm) {
return genEl(token.children);
}
return h(MkSparkle, {}, genEl(token.children));
}
case 'rotate': { case 'rotate': {
const rotate = const rotate =
token.props.args.x ? 'perspective(128px) rotateX' : token.props.args.x ? 'perspective(128px) rotateX' :

21027
packages/client/src/icons.scss Normal file

File diff suppressed because it is too large Load diff

View file

@ -3,6 +3,7 @@
*/ */
import '@/style.scss'; import '@/style.scss';
import '@/icons.scss';
//#region account indexedDB migration //#region account indexedDB migration
import { set } from '@/scripts/idb-proxy'; import { set } from '@/scripts/idb-proxy';
@ -295,7 +296,7 @@ import { getAccountFromId } from '@/scripts/get-account-from-id';
}, { immediate: true }); }, { immediate: true });
watch(defaultStore.reactiveState.useBlurEffect, v => { watch(defaultStore.reactiveState.useBlurEffect, v => {
if (v) { if (v && deviceKind !== 'smartphone') {
document.documentElement.style.removeProperty('--blur'); document.documentElement.style.removeProperty('--blur');
} else { } else {
document.documentElement.style.setProperty('--blur', 'none'); document.documentElement.style.setProperty('--blur', 'none');

View file

@ -72,7 +72,7 @@ export const navbarItemDef = reactive({
}, },
favorites: { favorites: {
title: 'favorites', title: 'favorites',
icon: 'ph-star-bold ph-lg', icon: 'ph-bookmark-simple-bold ph-lg',
show: computed(() => $i != null), show: computed(() => $i != null),
to: '/my/favorites', to: '/my/favorites',
}, },

View file

@ -24,20 +24,29 @@
{{ i18n.ts._aboutMisskey.source }} {{ i18n.ts._aboutMisskey.source }}
<template #suffix>Codeberg</template> <template #suffix>Codeberg</template>
</FormLink> </FormLink>
<FormLink to="https://liberapay.com/ThatOneCalculator" external>
<template #icon><i class="ph-money-bold ph-lg"></i></template>
{{ i18n.ts._aboutMisskey.donate }}
<template #suffix>Donate</template>
</FormLink>
</div> </div>
</FormSection> </FormSection>
<FormSection> <FormSection>
<template #label>{{ i18n.ts._aboutMisskey.contributors }}</template> <template #label>{{ i18n.ts._aboutMisskey.contributors }}</template>
<div class="_formLinks"> <div class="_formLinks">
<FormLink to="https://codeberg.org/thatonecalculator" external>ThatOneCalculator (fork developer)</FormLink> <FormLink to="/@t1c@i.calckey.cloud"><Mfm :text="'$[sparkle @t1c@i.calckey.cloud] (Main fork developer)'"/></FormLink>
<FormLink to="https://github.com/syuilo" external>Syuilo (Misskey developer)</FormLink> <FormLink to="/@syuilo@misskey.io"><Mfm :text="'@syuilo@misskey.io (Misskey developer)'"/></FormLink>
<FormLink to="https://www.youtube.com/c/Henkiwashere" external>Henki (error images artist)</FormLink> <FormLink to="https://www.youtube.com/c/Henkiwashere" external>Henki (error images artist)</FormLink>
</div> </div>
<template #caption><MkLink url="https://codeberg.org/thatonecalculator/calckey/activity">{{ i18n.ts._aboutMisskey.allContributors }}</MkLink></template> <template #caption><MkLink url="https://codeberg.org/thatonecalculator/calckey/activity">{{ i18n.ts._aboutMisskey.allContributors }}</MkLink></template>
</FormSection> </FormSection>
<FormSection> <FormSection>
<template #label><Mfm text="$[jelly ❤]"/> {{ i18n.ts._aboutMisskey.patrons }}</template> <template #label><Mfm text="$[jelly ❤]"/> {{ i18n.ts._aboutMisskey.patrons }}</template>
<div v-for="patron in patrons" :key="patron">{{ patron }}</div> <MkSparkle>
<div v-for="patron in patrons" :key="patron" style="margin-bottom: 0.5rem">
<Mfm :text="`${patron}`"/>
</div>
</MkSparkle>
<template #caption>{{ i18n.ts._aboutMisskey.morePatrons }}</template> <template #caption>{{ i18n.ts._aboutMisskey.morePatrons }}</template>
</FormSection> </FormSection>
</div> </div>
@ -53,92 +62,14 @@ import FormLink from '@/components/form/link.vue';
import FormSection from '@/components/form/section.vue'; import FormSection from '@/components/form/section.vue';
import MkButton from '@/components/MkButton.vue'; import MkButton from '@/components/MkButton.vue';
import MkLink from '@/components/MkLink.vue'; import MkLink from '@/components/MkLink.vue';
import MkSparkle from '@/components/MkSparkle.vue';
import { physics } from '@/scripts/physics'; import { physics } from '@/scripts/physics';
import { i18n } from '@/i18n'; import { i18n } from '@/i18n';
import { defaultStore } from '@/store'; import { defaultStore } from '@/store';
import * as os from '@/os'; import * as os from '@/os';
import { definePageMetadata } from '@/scripts/page-metadata'; import { definePageMetadata } from '@/scripts/page-metadata';
const patrons = [ const patrons = await os.api('patrons');
'まっちゃとーにゅ',
'mametsuko',
'noellabo',
'AureoleArk',
'Gargron',
'Nokotaro Takeda',
'Suji Yan',
'oi_yekssim',
'regtan',
'Hekovic',
'nenohi',
'Gitmo Life Services',
'naga_rus',
'Efertone',
'Melilot',
'motcha',
'nanami kan',
'sevvie Rose',
'Hayato Ishikawa',
'Puniko',
'skehmatics',
'Quinton Macejkovic',
'YUKIMOCHI',
'dansup',
'mewl hayabusa',
'Emilis',
'Fristi',
'makokunsan',
'chidori ninokura',
'Peter G.',
'見当かなみ',
'natalie',
'Maronu',
'Steffen K9',
'takimura',
'sikyosyounin',
'Nesakko',
'YuzuRyo61',
'blackskye',
'sheeta.s',
'osapon',
'public_yusuke',
'CG',
'吴浥',
't_w',
'Jerry',
'nafuchoco',
'Takumi Sugita',
'GLaTAN',
'mkatze',
'kabo2468y',
'mydarkstar',
'Roujo',
'DignifiedSilence',
'uroco @99',
'totokoro',
'うし',
'kiritan',
'weepjp',
'Liaizon Wakest',
'Duponin',
'Blue',
'Naoki Hirayama',
'wara',
'Wataru Manji (manji0)',
'みなしま',
'kanoy',
'xianon',
'Denshi',
'Osushimaru',
'にょんへら',
'おのだい',
'Leni',
'oss',
'Weeble',
'蝉暮せせせ',
'ThatOneCalculator',
'pixeldesu',
];
let easterEggReady = false; let easterEggReady = false;
let easterEggEmojis = $ref([]); let easterEggEmojis = $ref([]);

View file

@ -48,7 +48,7 @@ watch(() => props.clipId, async () => {
}); });
}, { }, {
immediate: true, immediate: true,
}); });
provide('currentClipPage', $$(clip)); provide('currentClipPage', $$(clip));

View file

@ -1,35 +1,45 @@
<template> <template>
<MkStickyContainer> <MkStickyContainer>
<template #header><MkPageHeader/></template> <template #header><MkPageHeader /></template>
<MkSpacer :content-max="800"> <MkSpacer :content-max="800">
<MkPagination ref="pagingComponent" :pagination="pagination"> <MkPagination ref="pagingComponent" :pagination="pagination">
<template #empty> <template #empty>
<div class="_fullinfo"> <div class="_fullinfo">
<img src="/static-assets/badges/info.png" class="_ghost" alt="Info"/> <img
<div>{{ i18n.ts.noNotes }}</div> src="/static-assets/badges/info.png"
</div> class="_ghost"
</template> alt="Info"
/>
<div>{{ i18n.ts.noNotes }}</div>
</div>
</template>
<template #default="{ items }"> <template #default="{ items }">
<XList v-slot="{ item }" :items="items" :direction="'down'" :no-gap="false" :ad="false"> <XList
<XNote :key="item.id" :note="item.note" :class="$style.note"/> v-slot="{ item }"
</XList> :items="items"
</template> :direction="'down'"
</MkPagination> :no-gap="false"
</MkSpacer> :ad="false"
</MkStickyContainer> >
<XNote :key="item.id" :note="item.note" :class="$style.note" />
</XList>
</template>
</MkPagination>
</MkSpacer>
</MkStickyContainer>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { ref } from 'vue'; import { ref } from "vue";
import MkPagination from '@/components/MkPagination.vue'; import MkPagination from "@/components/MkPagination.vue";
import XNote from '@/components/MkNote.vue'; import XNote from "@/components/MkNote.vue";
import XList from '@/components/MkDateSeparatedList.vue'; import XList from "@/components/MkDateSeparatedList.vue";
import { i18n } from '@/i18n'; import { i18n } from "@/i18n";
import { definePageMetadata } from '@/scripts/page-metadata'; import { definePageMetadata } from "@/scripts/page-metadata";
const pagination = { const pagination = {
endpoint: 'i/favorites' as const, endpoint: "i/favorites" as const,
limit: 10, limit: 10,
}; };
@ -37,7 +47,7 @@ const pagingComponent = ref<InstanceType<typeof MkPagination>>();
definePageMetadata({ definePageMetadata({
title: i18n.ts.favorites, title: i18n.ts.favorites,
icon: 'ph-star-bold ph-lg', icon: "ph-bookmark-simple-bold ph-lg",
}); });
</script> </script>

View file

@ -24,7 +24,7 @@
<div class="other"> <div class="other">
<button v-if="$i && $i.id === post.user.id" v-tooltip="i18n.ts.edit" v-click-anime class="_button" @click="edit"><i class="ph-pencil-bold ph-lg ph-fw ph-lg"></i></button> <button v-if="$i && $i.id === post.user.id" v-tooltip="i18n.ts.edit" v-click-anime class="_button" @click="edit"><i class="ph-pencil-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.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-if="shareAvailable()" 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>
<div class="user"> <div class="user">
@ -67,6 +67,7 @@ import { url } from '@/config';
import { useRouter } from '@/router'; import { useRouter } from '@/router';
import { i18n } from '@/i18n'; import { i18n } from '@/i18n';
import { definePageMetadata } from '@/scripts/page-metadata'; import { definePageMetadata } from '@/scripts/page-metadata';
import { shareAvailable } from '@/scripts/share-available';
const router = useRouter(); const router = useRouter();

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;
} }
@ -92,14 +91,6 @@ function del(): void {
border-radius: 16px; border-radius: 16px;
max-width: 100%; max-width: 100%;
&:before {
content: "";
pointer-events: none;
display: block;
position: absolute;
top: 12px;
}
& + * { & + * {
clear: both; clear: both;
} }
@ -222,7 +213,7 @@ function del(): void {
padding-right: 32px; padding-right: 32px;
> .balloon { > .balloon {
$color: var(--messageBg); $color: var(--X4);
background: $color; background: $color;
&.noText { &.noText {

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: '',
}]; }];
} }
} }
@ -439,7 +442,7 @@ definePageMetadata(computed(() => {
return { return {
title: title, title: title,
icon: 'ph-pencil-bold ph-lg', icon: 'ph-pencil-bold ph-lg',
}; };
})); }));
</script> </script>
@ -447,7 +450,7 @@ definePageMetadata(computed(() => {
.jqqmcavi { .jqqmcavi {
> .button { > .button {
& + .button { & + .button {
margin-left: 8px; margin: 4px;
} }
} }
} }

View file

@ -4,14 +4,25 @@
<MkSpacer :content-max="800"> <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="footer">
<!-- <div><i class="ph-alarm-bold"/> {{ i18n.ts.createdAt }}: <MkTime :time="page.createdAt" mode="detail"/></div>
<div class="header"> <div v-if="page.createdAt != page.updatedAt"><i class="ph-alarm-bold"></i> {{ i18n.ts.updatedAt }}: <MkTime :time="page.updatedAt" mode="detail"/></div>
<h1>{{ page.title }}</h1>
</div> </div>
--> <div class="_block main">
<div class="banner"> <div class="banner">
<img v-if="page.eyeCatchingImageId" :src="page.eyeCatchingImage.url"/> <div class="banner-image">
<div class="header">
<h1>{{ page.title }}</h1>
</div>
<div class="menu-actions">
<MkA v-tooltip="i18n.ts._pages.viewSource" :to="`/@${username}/pages/${pageName}/view-source`" class="menu _button"><i class="ph-code-bold ph-lg"/></MkA>
<template v-if="$i && $i.id === page.userId">
<MkA v-tooltip="i18n.ts._pages.editPage" class="menu _button" :to="`/pages/edit/${page.id}`"><i class="ph-pencil-bold ph-lg"/></MkA>
<button v-if="$i.pinnedPageId === page.id" v-tooltip="i18n.ts.unpin" class="menu _button" @click="pin(false)"><i class="ph-push-pin-slash-bold ph-lg"/></button>
<button v-else v-tooltip="i18n.ts.pin" class="menu _button" @click="pin(true)"><i class="ph-push-pin-bold ph-lg"/></button>
</template>
</div>
</div>
</div> </div>
<div class="content"> <div class="content">
<XPage :page="page"/> <XPage :page="page"/>
@ -23,29 +34,25 @@
</div> </div>
<div class="other"> <div class="other">
<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-if="shareAvailable()" 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 class="user">
<MkAvatar :user="page.user" class="avatar"/>
<div class="name">
<MkUserName :user="page.user" style="display: block;"/>
<MkAcct :user="page.user"/>
</div>
<MkFollowButton v-if="!$i || $i.id != page.user.id" :user="page.user" :inline="true" :transparent="false" :full="true" class="koudoku"/>
</div> </div>
</div> </div>
<div class="user"> <!-- <div class="links">
<MkAvatar :user="page.user" class="avatar"/>
<div class="name">
<MkUserName :user="page.user" style="display: block;"/>
<MkAcct :user="page.user"/>
</div>
<MkFollowButton v-if="!$i || $i.id != page.user.id" :user="page.user" :inline="true" :transparent="false" :full="true" large class="koudoku"/>
</div>
<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>
<template v-if="$i && $i.id === page.userId"> <template v-if="$i && $i.id === page.userId">
<MkA :to="`/pages/edit/${page.id}`" class="link">{{ i18n.ts._pages.editThisPage }}</MkA> <MkA :to="`/pages/edit/${page.id}`" class="link">{{ i18n.ts._pages.editThisPage }}</MkA>
<button v-if="$i.pinnedPageId === page.id" class="link _textButton" @click="pin(false)">{{ i18n.ts.unpin }}</button> <button v-if="$i.pinnedPageId === page.id" class="link _textButton" @click="pin(false)">{{ i18n.ts.unpin }}</button>
<button v-else class="link _textButton" @click="pin(true)">{{ i18n.ts.pin }}</button> <button v-else class="link _textButton" @click="pin(true)">{{ i18n.ts.pin }}</button>
</template> </template>
</div> </div> -->
</div>
<div class="footer">
<div><i class="ph-alarm-bold"></i> {{ i18n.ts.createdAt }}: <MkTime :time="page.createdAt" mode="detail"/></div>
<div v-if="page.createdAt != page.updatedAt"><i class="ph-alarm-bold"></i> {{ i18n.ts.updatedAt }}: <MkTime :time="page.updatedAt" mode="detail"/></div>
</div> </div>
<MkAd :prefer="['horizontal', 'horizontal-big']"/> <MkAd :prefer="['horizontal', 'horizontal-big']"/>
<MkContainer :max-height="300" :foldable="true" class="other"> <MkContainer :max-height="300" :foldable="true" class="other">
@ -74,6 +81,7 @@ import MkPagination from '@/components/MkPagination.vue';
import MkPagePreview from '@/components/MkPagePreview.vue'; import MkPagePreview from '@/components/MkPagePreview.vue';
import { i18n } from '@/i18n'; import { i18n } from '@/i18n';
import { definePageMetadata } from '@/scripts/page-metadata'; import { definePageMetadata } from '@/scripts/page-metadata';
import { shareAvailable } from '@/scripts/share-available';
const props = defineProps<{ const props = defineProps<{
pageName: string; pageName: string;
@ -81,6 +89,7 @@ const props = defineProps<{
}>(); }>();
let page = $ref(null); let page = $ref(null);
let bgImg = $ref(null);
let error = $ref(null); let error = $ref(null);
const otherPostsPagination = { const otherPostsPagination = {
endpoint: 'users/pages' as const, endpoint: 'users/pages' as const,
@ -98,11 +107,21 @@ function fetchPage() {
username: props.username, username: props.username,
}).then(_page => { }).then(_page => {
page = _page; page = _page;
bgImg = getBgImg();
}).catch(err => { }).catch(err => {
error = err; error = err;
}); });
} }
function getBgImg(): string {
if (page.eyeCatchingImage != null) {
return `url(${page.eyeCatchingImage.url})`;
}
else {
return 'linear-gradient(to bottom right, #31748f, #9ccfd8)'
}
}
function share() { function share() {
navigator.share({ navigator.share({
title: page.title ?? page.name, title: page.title ?? page.name,
@ -118,7 +137,7 @@ function shareWithNote() {
} }
function like() { function like() {
os.apiWithDialog('pages/like', { os.api('pages/like', {
pageId: page.id, pageId: page.id,
}).then(() => { }).then(() => {
page.isLiked = true; page.isLiked = true;
@ -180,35 +199,65 @@ definePageMetadata(computed(() => page ? {
margin: 1rem; margin: 1rem;
} }
> .header {
padding: 16px;
> h1 {
margin: 0;
}
}
> .banner { > .banner {
margin: 0rem !important; margin: 0rem !important;
> img { > .banner-image {
// TODO: // TODO:
display: block; display: block;
width: 100%; width: 100%;
height: 150px; height: 150px;
object-fit: cover; background-position: center;
background-size: cover;
background-image: v-bind('bgImg');
> .header {
padding: 16px;
> h1 {
margin: 0;
color: white;
text-shadow: 0 0 8px #000;
}
}
> .menu-actions {
-webkit-backdrop-filter: var(--blur, blur(8px));
backdrop-filter: var(--blur, blur(8px));
background: rgba(0, 0, 0, 0.2);
padding: 8px;
border-radius: 24px;
width: fit-content;
position: relative;
top: -10px;
left: 1rem;
> .menu {
vertical-align: bottom;
height: 31px;
width: 31px;
color: #fff;
text-shadow: 0 0 8px #000;
font-size: 16px;
}
> .koudoku {
margin-left: 4px;
vertical-align: bottom;
}
}
} }
} }
> .content { > .content {
padding: 16px 0 0 0; padding: 16px 0;
} }
> .actions { > .actions {
display: flex; display: flex;
align-items: center; align-items: center;
margin-top: 16px; margin-top: 16px;
padding: 16px 0 0 0; padding: 16px 0;
border-top: solid 0.5px var(--divider); border-top: solid 0.5px var(--divider);
> .like { > .like {
@ -226,10 +275,8 @@ definePageMetadata(computed(() => page ? {
} }
> .other { > .other {
margin-left: auto;
> button { > button {
padding: 8px; padding: 2px;
margin: 0 8px; margin: 0 8px;
&:hover { &:hover {
@ -237,27 +284,26 @@ definePageMetadata(computed(() => page ? {
} }
} }
} }
}
> .user { > .user {
margin-top: 16px;
padding: 16px 0 0 0;
border-top: solid 0.5px var(--divider);
display: flex;
align-items: center;
> .avatar {
width: 52px;
height: 52px;
}
> .name {
margin: 0 0 0 12px;
font-size: 90%;
}
> .koudoku {
margin-left: auto; margin-left: auto;
display: flex;
align-items: center;
> .avatar {
width: 40px;
height: 40px;
}
> .name {
margin: 0 0 0 12px;
font-size: 90%;
}
> .koudoku {
margin-left: auto;
margin: 1rem;
}
} }
} }

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

@ -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

@ -25,9 +25,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-if="$i == null" :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"/>

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

@ -8,6 +8,7 @@ import * as os from '@/os';
import copyToClipboard from '@/scripts/copy-to-clipboard'; import copyToClipboard from '@/scripts/copy-to-clipboard';
import { url } from '@/config'; import { url } from '@/config';
import { noteActions } from '@/store'; import { noteActions } from '@/store';
import { shareAvailable } from '@/scripts/share-available';
export function getNoteMenu(props: { export function getNoteMenu(props: {
note: misskey.entities.Note; note: misskey.entities.Note;
@ -220,23 +221,23 @@ export function getNoteMenu(props: {
window.open(appearNote.url || appearNote.uri, '_blank'); window.open(appearNote.url || appearNote.uri, '_blank');
}, },
} : undefined, } : undefined,
{ shareAvailable() ? {
icon: 'ph-share-network-bold ph-lg', icon: 'ph-share-network-bold ph-lg',
text: i18n.ts.share, text: i18n.ts.share,
action: share, action: share,
}, } : undefined,
instance.translatorAvailable ? { instance.translatorAvailable ? {
icon: 'ph-translate-bold ph-lg', icon: 'ph-translate-bold ph-lg',
text: i18n.ts.translate, text: i18n.ts.translate,
action: translate, action: translate,
} : undefined, } : undefined,
null, null,
statePromise.then(state => state.isFavorited ? { statePromise.then(state => state?.isFavorited ? {
icon: 'ph-star-bold ph-lg', icon: 'ph-bookmark-simple-bold ph-lg',
text: i18n.ts.unfavorite, text: i18n.ts.unfavorite,
action: () => toggleFavorite(false), action: () => toggleFavorite(false),
} : { } : {
icon: 'ph-star-bold ph-lg', icon: 'ph-bookmark-simple-bold ph-lg',
text: i18n.ts.favorite, text: i18n.ts.favorite,
action: () => toggleFavorite(true), action: () => toggleFavorite(true),
}), }),

View file

@ -0,0 +1,3 @@
export function reducedMotion(): boolean {
return window.matchMedia('(prefers-reduced-motion: reduce)').matches;
}

View file

@ -0,0 +1,6 @@
export function shareAvailable(): boolean {
if (navigator.share) {
return true;
}
return false;
}

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

@ -98,9 +98,9 @@ a {
-webkit-tap-highlight-color: transparent; -webkit-tap-highlight-color: transparent;
} }
i { // i {
transform: translateY(0.1em); // transform: translateY(0.1em);
} // }
textarea, input { textarea, input {
tap-highlight-color: transparent; tap-highlight-color: transparent;
@ -568,6 +568,22 @@ hr {
} }
} }
@media(prefers-reduced-motion) {
@keyframes tada {
from {
transform: scale3d(1, 1, 1);
}
50% {
transform: scale3d(1.1, 1.1, 1.1);
}
to {
transform: scale3d(1, 1, 1);
}
}
}
._anime_bounce { ._anime_bounce {
will-change: transform; will-change: transform;
animation: bounce ease 0.7s; animation: bounce ease 0.7s;

View file

@ -11,7 +11,7 @@
<XStreamIndicator/> <XStreamIndicator/>
<div v-if="pendingApiRequestsCount > 0" id="wait"></div> <!-- <div v-if="pendingApiRequestsCount > 0" id="wait"></div> -->
<div v-if="dev" id="devTicker"><span>DEV BUILD</span></div> <div v-if="dev" id="devTicker"><span>DEV BUILD</span></div>
</template> </template>
@ -99,8 +99,8 @@ if ($i) {
top: 0; top: 0;
left: 0; left: 0;
z-index: 2147483647; z-index: 2147483647;
color: #ff0; color: #f6c177;
background: rgba(0, 0, 0, 0.5); background: #6e6a86;
padding: 4px 5px; padding: 4px 5px;
font-size: 14px; font-size: 14px;
pointer-events: none; pointer-events: none;

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

@ -133,25 +133,25 @@ function getMenu() {
text: i18n.ts.move + '...', text: i18n.ts.move + '...',
icon: 'ph-arrows-out-cardinal-bold ph-lg', icon: 'ph-arrows-out-cardinal-bold ph-lg',
children: [{ children: [{
icon: 'ph--left-bold ph-lg', icon: 'ph-caret-left-bold ph-lg',
text: i18n.ts._deck.swapLeft, text: i18n.ts._deck.swapLeft,
action: () => { action: () => {
swapLeftColumn(props.column.id); swapLeftColumn(props.column.id);
}, },
}, { }, {
icon: 'ph--right-bold ph-lg', icon: 'ph-caret-right-bold ph-lg',
text: i18n.ts._deck.swapRight, text: i18n.ts._deck.swapRight,
action: () => { action: () => {
swapRightColumn(props.column.id); swapRightColumn(props.column.id);
}, },
}, props.isStacked ? { }, props.isStacked ? {
icon: 'ph--up-bold ph-lg', icon: 'ph-caret-up-bold ph-lg',
text: i18n.ts._deck.swapUp, text: i18n.ts._deck.swapUp,
action: () => { action: () => {
swapUpColumn(props.column.id); swapUpColumn(props.column.id);
}, },
} : undefined, props.isStacked ? { } : undefined, props.isStacked ? {
icon: 'ph--down-bold ph-lg', icon: 'ph-caret-down-bold ph-lg',
text: i18n.ts._deck.swapDown, text: i18n.ts._deck.swapDown,
action: () => { action: () => {
swapDownColumn(props.column.id); swapDownColumn(props.column.id);

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

@ -377,6 +377,10 @@ const wallpaper = localStorage.getItem('wallpaper') != null;
> .button-wrapper { > .button-wrapper {
> i {
transform: translateY(0.05em);
}
&.on { &.on {
background-color: var(--accentedBg); background-color: var(--accentedBg);
width: 100%; width: 100%;

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

@ -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"
} }
} }

11
patrons.json Normal file
View file

@ -0,0 +1,11 @@
{
"patrons": [
"@atomicpoet@vancity.social",
"@shoq@newsroom.social",
"@pikadude@erisly.social",
"@sage@stop.voring.me",
"@sky@therian.club",
"@panos@electricrequiem.com",
"@redhunt07@www.foxyhole.io"
]
}

10
push-docker.sh Executable file
View file

@ -0,0 +1,10 @@
sudo systemctl start docker.service
sudo docker rmi $(docker images -q)
sudo docker compose build
sudo docker tag thatonecalculator/calckey:latest thatonecalculator/calckey:$(git describe --tags --exact-match)
sudo docker images
echo "\nPress any key to continue\n"
read
sudo docker push thatonecalculator/calckey:$(git describe --tags --exact-match)
sudo docker push thatonecalculator/calckey:latest
sudo systemctl stop docker.service