Merge pull request 'develop' (#9178) from develop into account_migration
Reviewed-on: https://codeberg.org/thatonecalculator/calckey/pulls/9178
This commit is contained in:
commit
0e8b155e07
@ -1,4 +1,4 @@
|
||||
# db settings
|
||||
POSTGRES_PASSWORD=example-misskey-pass
|
||||
POSTGRES_USER=example-misskey-user
|
||||
POSTGRES_DB=misskey
|
||||
POSTGRES_PASSWORD=example-calckey-pass
|
||||
POSTGRES_USER=example-calckey-user
|
||||
POSTGRES_DB=calckey
|
||||
|
10
CALCKEY.md
10
CALCKEY.md
@ -9,7 +9,6 @@
|
||||
- User "choices" (recommended users) like Mastodon and Soapbox
|
||||
- Option to publicize instance blocks
|
||||
- Fully revamp non-logged-in screen
|
||||
- Remote follow button
|
||||
- Personal notes for all accounts
|
||||
- Non-nyaify cat mode
|
||||
- Timeline filters
|
||||
@ -21,8 +20,8 @@
|
||||
## Work in progress
|
||||
|
||||
- Better Messaging UI
|
||||
- Videos can be played in DMs
|
||||
- Make your password hasn't been pwned
|
||||
- Better API Documentation
|
||||
- Remote follow button
|
||||
- Admin custom CSS
|
||||
- Add back time machine (jump to date)
|
||||
- Improve accesibility score
|
||||
@ -86,7 +85,12 @@
|
||||
- Link hover effect
|
||||
- Replace all `$ts` with i18n
|
||||
- AVIF support
|
||||
- Page drafts
|
||||
- Patron list
|
||||
- Animations respect reduced motion
|
||||
- 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)
|
||||
- [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)
|
||||
|
54
README.md
54
README.md
@ -1,9 +1,9 @@
|
||||
<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"/>
|
||||
</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>
|
||||
|
||||
@ -20,6 +20,7 @@
|
||||
- Improved UI/UX (especially on mobile)
|
||||
- Improved notifications
|
||||
- Improved instance security
|
||||
- Improved accessibility
|
||||
- Recommended Instances timeline
|
||||
- OCR image captioning
|
||||
- New and improved Groups
|
||||
@ -34,6 +35,9 @@
|
||||
# 🥂 Links
|
||||
|
||||
- 💸 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>
|
||||
- 📜 Instance list: <https://calckey.fediverse.observer/list>
|
||||
- 📖 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
|
||||
|
||||
- At least 🐢 [NodeJS](https://nodejs.org/en/) v16.15.0 (v18.12.1 recommended)
|
||||
|
||||
> ⚠️ NodeJS v19 is not supported as of right now because of [this issue](https://github.com/nodejs/node-gyp/issues/2757).
|
||||
|
||||
- 🐢 At least [NodeJS](https://nodejs.org/en/) v18.12.1 (v19.1.0 recommended)
|
||||
- Install with [nvm](https://github.com/nvm-sh/nvm)
|
||||
- 🐘 At least [PostgreSQL](https://www.postgresql.org/) v12
|
||||
|
||||
- 🍱 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
|
||||
|
||||
@ -70,10 +86,19 @@ cd calckey/
|
||||
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
|
||||
|
||||
- To add custom CSS for all users, edit `./custom/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 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/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`.
|
||||
|
||||
## 🧑🔬 Configuring a new instance
|
||||
@ -93,7 +118,7 @@ cp -r ../misskey/files . # if you don't use object storage
|
||||
|
||||
## 🍀 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
|
||||
- Run `sudo cp ./calckey.nginx.conf ../sites-enabled/`
|
||||
- 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!
|
||||
|
||||
### 🐢 NodeJS
|
||||
### 🐢 NodeJS + pm2
|
||||
|
||||
#### `git pull` and run these steps to update Calckey in the future!
|
||||
|
||||
@ -123,15 +148,16 @@ docker up -d
|
||||
### 🐳 Docker Compose
|
||||
|
||||
```sh
|
||||
docker compose build
|
||||
docker-compose build
|
||||
docker-compose run --rm web yarn run init
|
||||
docker compose up -d
|
||||
docker-compose up -d
|
||||
```
|
||||
|
||||
## 😉 Tips & Tricks
|
||||
|
||||
- When editing the config file, please don't fill out the settings at the bottom. They're designed *only* for managed hosting, not self hosting. Those settings are much better off being set in Calckey's control panel.
|
||||
- Port 3000 (used in the default config) might be already used on your server for something else. To find an open port for Calckey, run `for p in $(seq 3000 4000); do ss -tlnH | tr -s ' ' | cut -d" " -sf4 | grep -q "${p}$" || echo "${p}"; done | head -n 1`
|
||||
- I'd 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.
|
||||
- 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.
|
||||
|
0
custom/locales/.gitkeep
Normal file
0
custom/locales/.gitkeep
Normal file
@ -16,7 +16,7 @@ gulp.task('copy:backend:views', () =>
|
||||
);
|
||||
|
||||
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', () =>
|
||||
@ -24,7 +24,7 @@ gulp.task('copy:client:fonts', () =>
|
||||
);
|
||||
|
||||
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 => {
|
||||
|
@ -32,12 +32,12 @@ uploading: "Uploading..."
|
||||
save: "Save"
|
||||
users: "Users"
|
||||
addUser: "Add a user"
|
||||
favorite: "Add to favorites"
|
||||
favorites: "Favorites"
|
||||
unfavorite: "Remove from favorites"
|
||||
favorited: "Added to favorites."
|
||||
alreadyFavorited: "Already added to favorites."
|
||||
cantFavorite: "Couldn't add to favorites."
|
||||
favorite: "Add to bookmarks"
|
||||
favorites: "Bookmarks"
|
||||
unfavorite: "Remove from bookmarks"
|
||||
favorited: "Added to bookmarks."
|
||||
alreadyFavorited: "Already added to bookmarks."
|
||||
cantFavorite: "Couldn't add to bookmarks."
|
||||
pin: "Pin to profile"
|
||||
unpin: "Unpin from profile"
|
||||
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."
|
||||
host: "Host"
|
||||
selectUser: "Select a user"
|
||||
recipient: "Recipient"
|
||||
recipient: "Recipient(s)"
|
||||
annotation: "Comments"
|
||||
federation: "Federation"
|
||||
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."
|
||||
notSet: "Not set"
|
||||
emailVerified: "Email has been verified"
|
||||
noteFavoritesCount: "Number of favorite notes"
|
||||
noteFavoritesCount: "Number of bookmarked notes"
|
||||
pageLikesCount: "Number of liked Pages"
|
||||
pageLikedCount: "Number of received Page likes"
|
||||
contact: "Contact"
|
||||
@ -771,8 +771,8 @@ noBotProtectionWarning: "Bot protection is not configured."
|
||||
configure: "Configure"
|
||||
postToGallery: "Create new gallery post"
|
||||
gallery: "Gallery"
|
||||
recentPosts: "Recent posts"
|
||||
popularPosts: "Popular posts"
|
||||
recentPosts: "Recent pages"
|
||||
popularPosts: "Popular pages"
|
||||
shareWithNote: "Share with note"
|
||||
ads: "Advertisements"
|
||||
expiration: "Deadline"
|
||||
@ -1002,9 +1002,9 @@ _aboutMisskey:
|
||||
allContributors: "All contributors"
|
||||
source: "Source code"
|
||||
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! 🥰"
|
||||
patrons: "Misskey patrons"
|
||||
patrons: "Calckey patrons"
|
||||
_nsfw:
|
||||
respect: "Hide NSFW media"
|
||||
ignore: "Don't hide NSFW media"
|
||||
@ -1095,7 +1095,7 @@ _channel:
|
||||
usersCount: "{n} Participants"
|
||||
notesCount: "{n} Notes"
|
||||
_messaging:
|
||||
dms: "DMs"
|
||||
dms: "Private"
|
||||
groups: "Groups"
|
||||
_menuDisplay:
|
||||
sideFull: "Side"
|
||||
@ -1228,7 +1228,7 @@ _tutorial:
|
||||
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_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."
|
||||
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\"."
|
||||
@ -1251,8 +1251,8 @@ _permissions:
|
||||
"write:blocks": "Edit your list of blocked users"
|
||||
"read:drive": "Access your Drive files and folders"
|
||||
"write:drive": "Edit or delete your Drive files and folders"
|
||||
"read:favorites": "View your list of favorites"
|
||||
"write:favorites": "Edit your list of favorites"
|
||||
"read:favorites": "View your list of bookmarks"
|
||||
"write:favorites": "Edit your list of bookmarks"
|
||||
"read:following": "View information on who you follow"
|
||||
"write:following": "Follow or unfollow other accounts"
|
||||
"read:messaging": "View your chats"
|
||||
@ -1265,10 +1265,10 @@ _permissions:
|
||||
"read:reactions": "View your reactions"
|
||||
"write:reactions": "Edit your reactions"
|
||||
"write:votes": "Vote on a poll"
|
||||
"read:pages": "View your pages"
|
||||
"write:pages": "Edit or delete your pages"
|
||||
"read:page-likes": "View your likes on pages"
|
||||
"write:page-likes": "Edit your likes on pages"
|
||||
"read:pages": "View your page"
|
||||
"write:pages": "Edit or delete your page"
|
||||
"read:page-likes": "View your likes on page"
|
||||
"write:page-likes": "Edit your likes on page"
|
||||
"read:user-groups": "View your user groups"
|
||||
"write:user-groups": "Edit or delete your user groups"
|
||||
"read:channels": "View your channels"
|
||||
@ -1442,7 +1442,7 @@ _pages:
|
||||
liked: "Liked Pages"
|
||||
featured: "Popular"
|
||||
inspector: "Inspector"
|
||||
contents: "Contents"
|
||||
contents: "Content"
|
||||
content: "Page block"
|
||||
variables: "Variables"
|
||||
title: "Title"
|
||||
|
@ -4,6 +4,8 @@
|
||||
|
||||
const fs = require('fs');
|
||||
const yaml = require('js-yaml');
|
||||
let languages = []
|
||||
let languages_custom = []
|
||||
|
||||
const merge = (...args) => args.reduce((a, c) => ({
|
||||
...a,
|
||||
@ -13,33 +15,20 @@ const merge = (...args) => args.reduce((a, c) => ({
|
||||
.reduce((a, [k, v]) => (a[k] = merge(v, c[k]), a), {})
|
||||
}), {});
|
||||
|
||||
const languages = [
|
||||
'ar-SA',
|
||||
'cs-CZ',
|
||||
'da-DK',
|
||||
'de-DE',
|
||||
'en-US',
|
||||
'es-ES',
|
||||
'fr-FR',
|
||||
'id-ID',
|
||||
'it-IT',
|
||||
'ja-JP',
|
||||
'ja-KS',
|
||||
'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',
|
||||
];
|
||||
|
||||
fs.readdirSync(__dirname).forEach((file) => {
|
||||
if (file.includes('.yml')){
|
||||
file = file.slice(0, file.indexOf('.'))
|
||||
languages.push(file);
|
||||
}
|
||||
})
|
||||
|
||||
fs.readdirSync(__dirname + '/../custom/locales').forEach((file) => {
|
||||
if (file.includes('.yml')){
|
||||
file = file.slice(0, file.indexOf('.'))
|
||||
languages_custom.push(file);
|
||||
}
|
||||
})
|
||||
|
||||
const primaries = {
|
||||
'en': 'US',
|
||||
@ -51,6 +40,8 @@ const primaries = {
|
||||
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_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)
|
||||
.reduce((a, [k ,v]) => (a[k] = (() => {
|
||||
|
@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "calckey",
|
||||
"version": "12.119.0-calc.14.6",
|
||||
"version": "12.119.0-calc.18",
|
||||
"codename": "aqua",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://codeberg.org/thatonecalculator/calckey.git"
|
||||
},
|
||||
"packageManager": "yarn@3.2.4",
|
||||
"packageManager": "yarn@3.3.0",
|
||||
"workspaces": [
|
||||
"packages/client",
|
||||
"packages/backend",
|
||||
@ -42,7 +42,7 @@
|
||||
"@bull-board/api": "^4.6.4",
|
||||
"@bull-board/ui": "^4.6.4",
|
||||
"@tensorflow/tfjs": "^3.21.0",
|
||||
"eslint": "^8.27.0",
|
||||
"eslint": "^8.28.0",
|
||||
"execa": "5.1.1",
|
||||
"gulp": "4.0.2",
|
||||
"gulp-cssnano": "2.1.3",
|
||||
|
8
packages/backend/migration/1668828368510PageDraft.js
Normal file
8
packages/backend/migration/1668828368510PageDraft.js
Normal 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"`);
|
||||
}
|
||||
}
|
11
packages/backend/migration/1668831378728FixCalckeyAgain.js
Normal file
11
packages/backend/migration/1668831378728FixCalckeyAgain.js
Normal 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`);
|
||||
}
|
||||
}
|
@ -4,8 +4,8 @@
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"start": "node --experimental-json-modules ./built/index.js",
|
||||
"start:test": "NODE_ENV=test node --experimental-json-modules ./built/index.js",
|
||||
"start": "node ./built/index.js",
|
||||
"start:test": "NODE_ENV=test node ./built/index.js",
|
||||
"migrate": "typeorm migration:run -d ormconfig.js",
|
||||
"build": "tsc -p tsconfig.json || echo done. && tsc-alias -p tsconfig.json",
|
||||
"watch": "node watch.mjs",
|
||||
@ -36,11 +36,11 @@
|
||||
"archiver": "5.3.1",
|
||||
"autobind-decorator": "2.4.0",
|
||||
"autwh": "0.1.0",
|
||||
"aws-sdk": "2.1255.0",
|
||||
"aws-sdk": "2.1258.0",
|
||||
"bcryptjs": "2.4.3",
|
||||
"blurhash": "1.1.5",
|
||||
"bull": "4.10.1",
|
||||
"cacheable-lookup": "6.1.0",
|
||||
"cacheable-lookup": "7.0.0",
|
||||
"cbor": "8.1.0",
|
||||
"chalk": "5.1.2",
|
||||
"chalk-template": "0.4.0",
|
||||
@ -83,7 +83,7 @@
|
||||
"node-fetch": "3.3.0",
|
||||
"nodemailer": "6.8.0",
|
||||
"nsfwjs": "2.4.2",
|
||||
"oauth": "^0.9.15",
|
||||
"oauth": "^0.10.0",
|
||||
"os-utils": "0.0.14",
|
||||
"parse5": "7.1.1",
|
||||
"pg": "8.8.0",
|
||||
@ -111,7 +111,7 @@
|
||||
"stringz": "2.1.0",
|
||||
"summaly": "2.7.0",
|
||||
"syslog-pro": "1.0.0",
|
||||
"systeminformation": "5.12.14",
|
||||
"systeminformation": "5.13.5",
|
||||
"tesseract.js": "^3.0.3",
|
||||
"tinycolor2": "1.4.2",
|
||||
"tmp": "0.2.1",
|
||||
@ -130,7 +130,7 @@
|
||||
"xev": "3.0.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@redocly/openapi-core": "1.0.0-beta.112",
|
||||
"@redocly/openapi-core": "1.0.0-beta.114",
|
||||
"@types/bcryptjs": "2.4.2",
|
||||
"@types/bull": "3.15.9",
|
||||
"@types/cbor": "6.0.0",
|
||||
@ -165,7 +165,7 @@
|
||||
"@types/rename": "1.0.4",
|
||||
"@types/sanitize-html": "2.6.2",
|
||||
"@types/semver": "7.3.13",
|
||||
"@types/sharp": "0.30.5",
|
||||
"@types/sharp": "0.31.0",
|
||||
"@types/sinonjs__fake-timers": "8.1.2",
|
||||
"@types/speakeasy": "2.0.7",
|
||||
"@types/tinycolor2": "1.4.3",
|
||||
@ -177,7 +177,7 @@
|
||||
"@typescript-eslint/eslint-plugin": "5.43.0",
|
||||
"@typescript-eslint/parser": "5.43.0",
|
||||
"cross-env": "7.0.3",
|
||||
"eslint": "8.27.0",
|
||||
"eslint": "8.28.0",
|
||||
"eslint-plugin-import": "2.26.0",
|
||||
"execa": "6.1.0",
|
||||
"typescript": "4.9.3"
|
||||
|
18
packages/backend/src/misc/clone.ts
Normal file
18
packages/backend/src/misc/clone.ts
Normal 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;
|
||||
}
|
||||
}
|
@ -40,6 +40,9 @@ export class Page {
|
||||
@Column('boolean')
|
||||
public alignCenter: boolean;
|
||||
|
||||
@Column('boolean')
|
||||
public isPublic: boolean;
|
||||
|
||||
@Column('boolean', {
|
||||
default: false,
|
||||
})
|
||||
|
@ -9,6 +9,8 @@ import { query, appendQuery } from '@/prelude/url.js';
|
||||
import { Meta } from '@/models/entities/meta.js';
|
||||
import { fetchMeta } from '@/misc/fetch-meta.js';
|
||||
import { Users, DriveFolders } from '../index.js';
|
||||
import { deepClone } from '@/misc/clone.js';
|
||||
|
||||
|
||||
type PackOptions = {
|
||||
detail?: boolean,
|
||||
@ -29,9 +31,7 @@ export const DriveFileRepository = db.getRepository(DriveFile).extend({
|
||||
|
||||
getPublicProperties(file: DriveFile): DriveFile['properties'] {
|
||||
if (file.properties.orientation != null) {
|
||||
// TODO
|
||||
//const properties = structuredClone(file.properties);
|
||||
const properties = JSON.parse(JSON.stringify(file.properties));
|
||||
const properties = deepClone(file.properties);
|
||||
if (file.properties.orientation >= 5) {
|
||||
[properties.width, properties.height] = [properties.height, properties.width];
|
||||
}
|
||||
|
@ -1,14 +1,14 @@
|
||||
import { In, Repository } from 'typeorm';
|
||||
import { Users, Notes, UserGroupInvitations, AccessTokens, NoteReactions } from '../index.js';
|
||||
import { Notification } from '@/models/entities/notification.js';
|
||||
import { awaitAll } from '@/prelude/await-all.js';
|
||||
import { Packed } from '@/misc/schema.js';
|
||||
import { Note } from '@/models/entities/note.js';
|
||||
import { NoteReaction } from '@/models/entities/note-reaction.js';
|
||||
import { User } from '@/models/entities/user.js';
|
||||
import type { Packed } from '@/misc/schema.js';
|
||||
import type { Note } from '@/models/entities/note.js';
|
||||
import type { NoteReaction } from '@/models/entities/note-reaction.js';
|
||||
import type { User } from '@/models/entities/user.js';
|
||||
import { aggregateNoteEmojis, prefetchEmojis } from '@/misc/populate-emojis.js';
|
||||
import { notificationTypes } from '@/types.js';
|
||||
import { db } from '@/db/postgre.js';
|
||||
import { Users, Notes, UserGroupInvitations, AccessTokens, NoteReactions } from '../index.js';
|
||||
|
||||
export const NotificationRepository = db.getRepository(Notification).extend({
|
||||
async pack(
|
||||
@ -17,7 +17,7 @@ export const NotificationRepository = db.getRepository(Notification).extend({
|
||||
_hintForEachNotes_?: {
|
||||
myReactions: Map<Note['id'], NoteReaction | null>;
|
||||
};
|
||||
}
|
||||
},
|
||||
): Promise<Packed<'Notification'>> {
|
||||
const notification = typeof src === 'object' ? src : await this.findOneByOrFail({ id: src });
|
||||
const token = notification.appAccessTokenId ? await AccessTokens.findOneByOrFail({ id: notification.appAccessTokenId }) : null;
|
||||
@ -86,7 +86,7 @@ export const NotificationRepository = db.getRepository(Notification).extend({
|
||||
|
||||
async packMany(
|
||||
notifications: Notification[],
|
||||
meId: User['id']
|
||||
meId: User['id'],
|
||||
) {
|
||||
if (notifications.length === 0) return [];
|
||||
|
||||
@ -106,10 +106,15 @@ export const NotificationRepository = db.getRepository(Notification).extend({
|
||||
|
||||
await prefetchEmojis(aggregateNoteEmojis(notes));
|
||||
|
||||
return await Promise.all(notifications.map(x => this.pack(x, {
|
||||
_hintForEachNotes_: {
|
||||
myReactions: myReactionsMap,
|
||||
},
|
||||
})));
|
||||
const results = await Promise.all(notifications
|
||||
.map(x =>
|
||||
this.pack(x, {
|
||||
_hintForEachNotes_: {
|
||||
myReactions: myReactionsMap,
|
||||
},
|
||||
}).catch(e => null),
|
||||
),
|
||||
);
|
||||
return results.filter(x => x != null);
|
||||
},
|
||||
});
|
||||
|
@ -65,6 +65,7 @@ export const PageRepository = db.getRepository(Page).extend({
|
||||
content: page.content,
|
||||
variables: page.variables,
|
||||
title: page.title,
|
||||
isPublic: page.isPublic,
|
||||
name: page.name,
|
||||
summary: page.summary,
|
||||
hideTitleWhenPinned: page.hideTitleWhenPinned,
|
||||
|
@ -47,5 +47,9 @@ export const packedPageSchema = {
|
||||
ref: 'UserLite',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
isPublic: {
|
||||
type: 'boolean',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
|
@ -26,7 +26,7 @@ export default async (actor: CacheableRemoteUser, activity: IUpdate): Promise<st
|
||||
await updatePerson(actor.uri!, resolver, object);
|
||||
return `ok: Person updated`;
|
||||
} 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`;
|
||||
} else {
|
||||
return `skip: Unknown type: ${getApType(object)}`;
|
||||
|
@ -272,7 +272,7 @@ export async function createPerson(uri: string, resolver?: Resolver): Promise<Us
|
||||
});
|
||||
//#endregion
|
||||
|
||||
await updateFeatured(user!.id).catch(err => logger.error(err));
|
||||
await updateFeatured(user!.id, resolver).catch(err => logger.error(err));
|
||||
|
||||
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),
|
||||
});
|
||||
|
||||
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 };
|
||||
}
|
||||
|
||||
export async function updateFeatured(userId: User['id']) {
|
||||
export async function updateFeatured(userId: User['id'], resolver?: Resolver) {
|
||||
const user = await Users.findOneByOrFail({ id: userId });
|
||||
if (!Users.isRemoteUser(user)) return;
|
||||
if (!user.featured) return;
|
||||
|
||||
logger.info(`Updating the featured: ${user.uri}`);
|
||||
|
||||
const resolver = new Resolver();
|
||||
if (resolver == null) resolver = new Resolver();
|
||||
|
||||
// Resolve to (Ordered)Collection Object
|
||||
const collection = await resolver.resolveCollection(user.featured);
|
||||
|
@ -40,7 +40,7 @@ export async function extractPollFromQuestion(source: string | IObject, resolver
|
||||
* @param uri URI of AP Question object
|
||||
* @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;
|
||||
|
||||
// URIがこのサーバーを指しているならスキップ
|
||||
@ -55,7 +55,7 @@ export async function updateQuestion(value: any) {
|
||||
//#endregion
|
||||
|
||||
// resolve new Question object
|
||||
const resolver = new Resolver();
|
||||
if (resolver == null) resolver = new Resolver();
|
||||
const question = await resolver.resolve(value) as IQuestion;
|
||||
apLogger.debug(`fetched question: ${JSON.stringify(question, null, 2)}`);
|
||||
|
||||
|
@ -19,9 +19,11 @@ import renderFollow from '@/remote/activitypub/renderer/follow.js';
|
||||
export default class Resolver {
|
||||
private history: Set<string>;
|
||||
private user?: ILocalUser;
|
||||
private recursionLimit?: number;
|
||||
|
||||
constructor() {
|
||||
constructor(recursionLimit = 100) {
|
||||
this.history = new Set();
|
||||
this.recursionLimit = recursionLimit;
|
||||
}
|
||||
|
||||
public getHistory(): string[] {
|
||||
@ -59,7 +61,9 @@ export default class Resolver {
|
||||
if (this.history.has(value)) {
|
||||
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);
|
||||
|
||||
const host = extractDbHost(value);
|
||||
|
@ -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___customSplashIcons from './endpoints/custom-splash-icons.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___requestResetPassword from './endpoints/request-reset-password.js';
|
||||
import * as ep___resetDb from './endpoints/reset-db.js';
|
||||
@ -599,6 +600,7 @@ const eps = [
|
||||
['custom-motd', ep___customMOTD],
|
||||
['custom-splash-icons', ep___customSplashIcons],
|
||||
['latest-version', ep___latestVersion],
|
||||
['patrons', ep___patrons],
|
||||
['promo/read', ep___promo_read],
|
||||
['request-reset-password', ep___requestResetPassword],
|
||||
['reset-db', ep___resetDb],
|
||||
|
@ -53,6 +53,7 @@ export const paramDef = {
|
||||
eyeCatchingImageId: { type: 'string', format: 'misskey:id', nullable: true },
|
||||
font: { type: 'string', enum: ['serif', 'sans-serif'], default: 'sans-serif' },
|
||||
alignCenter: { type: 'boolean', default: false },
|
||||
isPublic: { type: 'boolean', default: true },
|
||||
hideTitleWhenPinned: { type: 'boolean', default: false },
|
||||
},
|
||||
required: ['title', 'name', 'content', 'variables', 'script'],
|
||||
@ -97,6 +98,7 @@ export default define(meta, paramDef, async (ps, user) => {
|
||||
alignCenter: ps.alignCenter,
|
||||
hideTitleWhenPinned: ps.hideTitleWhenPinned,
|
||||
font: ps.font,
|
||||
isPublic: ps.isPublic,
|
||||
})).then(x => Pages.findOneByOrFail(x.identifiers[0]));
|
||||
|
||||
return await Pages.pack(page);
|
||||
|
@ -67,5 +67,9 @@ export default define(meta, paramDef, async (ps, user) => {
|
||||
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);
|
||||
});
|
||||
|
@ -60,6 +60,7 @@ export const paramDef = {
|
||||
font: { type: 'string', enum: ['serif', 'sans-serif'] },
|
||||
alignCenter: { type: 'boolean' },
|
||||
hideTitleWhenPinned: { type: 'boolean' },
|
||||
isPublic: { type: 'boolean' },
|
||||
},
|
||||
required: ['pageId', 'title', 'name', 'content', 'variables', 'script'],
|
||||
} as const;
|
||||
@ -104,6 +105,7 @@ export default define(meta, paramDef, async (ps, user) => {
|
||||
content: ps.content,
|
||||
variables: ps.variables,
|
||||
script: ps.script,
|
||||
isPublic: ps.isPublic,
|
||||
alignCenter: ps.alignCenter === undefined ? page.alignCenter : ps.alignCenter,
|
||||
hideTitleWhenPinned: ps.hideTitleWhenPinned === undefined ? page.hideTitleWhenPinned : ps.hideTitleWhenPinned,
|
||||
font: ps.font === undefined ? page.font : ps.font,
|
||||
|
27
packages/backend/src/server/api/endpoints/patrons.ts
Normal file
27
packages/backend/src/server/api/endpoints/patrons.ts
Normal 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;
|
||||
});
|
@ -34,7 +34,8 @@ export const paramDef = {
|
||||
export default define(meta, paramDef, async (ps, user) => {
|
||||
const query = makePaginationQuery(Pages.createQueryBuilder('page'), ps.sinceId, ps.untilId)
|
||||
.andWhere('page.userId = :userId', { userId: ps.userId })
|
||||
.andWhere('page.visibility = \'public\'');
|
||||
.andWhere('page.visibility = \'public\'')
|
||||
.andWhere('page.isPublic = true');
|
||||
|
||||
const pages = await query
|
||||
.take(ps.limit)
|
||||
|
@ -9,7 +9,7 @@ export function genOpenapiSpec() {
|
||||
|
||||
info: {
|
||||
version: 'v1',
|
||||
title: 'Misskey API',
|
||||
title: 'Calckey API',
|
||||
'x-logo': { url: '/static-assets/api-doc.png' },
|
||||
},
|
||||
|
||||
|
@ -232,8 +232,43 @@ const getFeed = async (acct: string) => {
|
||||
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
|
||||
router.get('/@:user.atom', async ctx => {
|
||||
const atomFeed: Router.Middleware = async ctx => {
|
||||
const feed = await getFeed(ctx.params.user);
|
||||
|
||||
if (feed) {
|
||||
@ -242,10 +277,10 @@ router.get('/@:user.atom', async ctx => {
|
||||
} else {
|
||||
ctx.status = 404;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// RSS
|
||||
router.get('/@:user.rss', async ctx => {
|
||||
const rssFeed: Router.Middleware = async ctx => {
|
||||
const feed = await getFeed(ctx.params.user);
|
||||
|
||||
if (feed) {
|
||||
@ -254,10 +289,10 @@ router.get('/@:user.rss', async ctx => {
|
||||
} else {
|
||||
ctx.status = 404;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// JSON
|
||||
router.get('/@:user.json', async ctx => {
|
||||
const jsonFeed: Router.Middleware = async ctx => {
|
||||
const feed = await getFeed(ctx.params.user);
|
||||
|
||||
if (feed) {
|
||||
@ -266,43 +301,47 @@ router.get('/@:user.json', async ctx => {
|
||||
} else {
|
||||
ctx.status = 404;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
//#region SSR (for crawlers)
|
||||
// User
|
||||
router.get(['/@:user', '/@:user/:sub'], async (ctx, next) => {
|
||||
const { username, host } = Acct.parse(ctx.params.user);
|
||||
const userPage: Router.Middleware = async (ctx, next) => {
|
||||
const userParam = ctx.params.user;
|
||||
const subParam = ctx.params.sub;
|
||||
const { username, host } = Acct.parse(userParam);
|
||||
|
||||
const user = await Users.findOneBy({
|
||||
usernameLower: username.toLowerCase(),
|
||||
host: host ?? IsNull(),
|
||||
isSuspended: false,
|
||||
});
|
||||
|
||||
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にはしない
|
||||
if (user === null) {
|
||||
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 => {
|
||||
const user = await Users.findOneBy({
|
||||
|
@ -42,7 +42,7 @@ html {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
transform: translateY(110px);
|
||||
display: none !important;
|
||||
display: none;
|
||||
color: var(--accent);
|
||||
}
|
||||
#splashSpinner > .spinner {
|
||||
@ -101,6 +101,16 @@ html {
|
||||
}
|
||||
}
|
||||
|
||||
@media(prefers-reduced-motion) {
|
||||
#splashSpinner {
|
||||
display: block;
|
||||
}
|
||||
|
||||
#splashIcon {
|
||||
animation: none;
|
||||
}
|
||||
}
|
||||
|
||||
#splashText {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
|
@ -19,8 +19,8 @@
|
||||
"blurhash": "1.1.5",
|
||||
"broadcast-channel": "4.18.1",
|
||||
"browser-image-resizer": "https://github.com/misskey-dev/browser-image-resizer.git#commit=0380d12c8e736788ea7f4e6e985175521ea7b23c",
|
||||
"chart.js": "3.9.1",
|
||||
"chartjs-adapter-date-fns": "2.0.0",
|
||||
"chart.js": "4.0.1",
|
||||
"chartjs-adapter-date-fns": "2.0.1",
|
||||
"chartjs-plugin-gradient": "0.5.1",
|
||||
"chartjs-plugin-zoom": "1.2.1",
|
||||
"compare-versions": "5.0.1",
|
||||
@ -31,7 +31,7 @@
|
||||
"idb-keyval": "6.2.0",
|
||||
"insert-text-at-cursor": "0.3.0",
|
||||
"json5": "2.2.1",
|
||||
"katex": "0.15.6",
|
||||
"katex": "0.16.3",
|
||||
"matter-js": "0.18.0",
|
||||
"mfm-js": "0.23.0",
|
||||
"misskey-js": "0.0.14",
|
||||
@ -48,7 +48,7 @@
|
||||
"swiper": "^8.4.4",
|
||||
"syuilo-password-strength": "0.0.1",
|
||||
"textarea-caret": "3.1.0",
|
||||
"three": "0.144.0",
|
||||
"three": "0.146.0",
|
||||
"throttle-debounce": "5.0.0",
|
||||
"tinycolor2": "1.4.2",
|
||||
"tsc-alias": "1.7.1",
|
||||
@ -80,7 +80,7 @@
|
||||
"@typescript-eslint/parser": "5.43.0",
|
||||
"cross-env": "7.0.3",
|
||||
"cypress": "10.11.0",
|
||||
"eslint": "8.27.0",
|
||||
"eslint": "8.28.0",
|
||||
"eslint-plugin-import": "2.26.0",
|
||||
"eslint-plugin-vue": "9.7.0",
|
||||
"rollup": "2.79.1",
|
||||
|
@ -81,9 +81,12 @@ const bannerStyle = computed(() => {
|
||||
top: 16px;
|
||||
left: 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;
|
||||
font-size: 1.2em;
|
||||
border-radius: 999px;
|
||||
}
|
||||
|
||||
> .status {
|
||||
@ -93,7 +96,9 @@ const bannerStyle = computed(() => {
|
||||
right: 16px;
|
||||
padding: 8px 12px;
|
||||
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;
|
||||
color: #fff;
|
||||
}
|
||||
|
@ -178,6 +178,7 @@ export default defineComponent({
|
||||
|
||||
> ::v-deep(i) {
|
||||
margin-right: 6px;
|
||||
transform: translateY(0.1em);
|
||||
}
|
||||
|
||||
&:empty {
|
||||
|
@ -1,5 +1,6 @@
|
||||
<template>
|
||||
<button class="kpoogebi _button"
|
||||
<button
|
||||
class="kpoogebi _button"
|
||||
:class="{ wait, active: isFollowing || hasPendingFollowRequestFromYou, full, large }"
|
||||
:disabled="wait"
|
||||
@click="onClick"
|
||||
@ -8,7 +9,8 @@
|
||||
<template v-if="hasPendingFollowRequestFromYou && user.isLocked">
|
||||
<span v-if="full">{{ i18n.ts.followRequestPending }}</span><i class="ph-hourglass-medium-bold ph-lg"></i>
|
||||
</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>
|
||||
</template>
|
||||
<template v-else-if="isFollowing">
|
||||
@ -29,16 +31,16 @@
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { onBeforeUnmount, onMounted } from 'vue';
|
||||
import * as Misskey from 'misskey-js';
|
||||
import type * as Misskey from 'misskey-js';
|
||||
import * as os from '@/os';
|
||||
import { stream } from '@/stream';
|
||||
import { i18n } from '@/i18n';
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
user: Misskey.entities.UserDetailed,
|
||||
full?: boolean,
|
||||
large?: boolean,
|
||||
}>(), {
|
||||
user: Misskey.entities.UserDetailed,
|
||||
full?: boolean,
|
||||
large?: boolean,
|
||||
}>(), {
|
||||
full: false,
|
||||
large: false,
|
||||
});
|
||||
@ -50,9 +52,9 @@ const connection = stream.useChannel('main');
|
||||
|
||||
if (props.user.isFollowing == null) {
|
||||
os.api('users/show', {
|
||||
userId: props.user.id
|
||||
userId: props.user.id,
|
||||
})
|
||||
.then(onFollowChange);
|
||||
.then(onFollowChange);
|
||||
}
|
||||
|
||||
function onFollowChange(user: Misskey.entities.UserDetailed) {
|
||||
@ -75,17 +77,17 @@ async function onClick() {
|
||||
if (canceled) return;
|
||||
|
||||
await os.api('following/delete', {
|
||||
userId: props.user.id
|
||||
userId: props.user.id,
|
||||
});
|
||||
} else {
|
||||
if (hasPendingFollowRequestFromYou) {
|
||||
await os.api('following/requests/cancel', {
|
||||
userId: props.user.id
|
||||
userId: props.user.id,
|
||||
});
|
||||
hasPendingFollowRequestFromYou = false;
|
||||
} else {
|
||||
await os.api('following/create', {
|
||||
userId: props.user.id
|
||||
userId: props.user.id,
|
||||
});
|
||||
hasPendingFollowRequestFromYou = true;
|
||||
}
|
||||
|
@ -64,6 +64,7 @@ const bg = {
|
||||
font-size: 0.9em;
|
||||
vertical-align: top;
|
||||
font-weight: bold;
|
||||
text-overflow: clip;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
@ -2,7 +2,7 @@
|
||||
<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 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-if="pageMetadata?.value" class="title">
|
||||
<i v-if="pageMetadata?.value.icon" class="icon" :class="pageMetadata?.value.icon"></i>
|
||||
|
@ -71,21 +71,21 @@
|
||||
</div>
|
||||
<footer class="footer">
|
||||
<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-else><i class="ph-arrow-bend-up-left-bold ph-lg"></i></template>
|
||||
<p v-if="appearNote.repliesCount > 0" class="count">{{ appearNote.repliesCount }}</p>
|
||||
</button>
|
||||
<XRenoteButton ref="renoteButton" class="button" :note="appearNote" :count="appearNote.renoteCount"/>
|
||||
<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>
|
||||
</button>
|
||||
<button v-if="appearNote.myReaction != null" ref="reactButton" class="button _button reacted" @click="undoReact(appearNote)">
|
||||
<i class="ph-minus-bold ph-lg"></i>
|
||||
</button>
|
||||
<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>
|
||||
</button>
|
||||
</footer>
|
||||
@ -135,6 +135,7 @@ import { i18n } from '@/i18n';
|
||||
import { getNoteMenu } from '@/scripts/get-note-menu';
|
||||
import { useNoteCapture } from '@/scripts/use-note-capture';
|
||||
import { notePage } from '@/filters/note';
|
||||
import { deepClone } from '@/scripts/clone';
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
@ -145,12 +146,12 @@ const props = defineProps<{
|
||||
|
||||
const inChannel = inject('inChannel', null);
|
||||
|
||||
let note = $ref(JSON.parse(JSON.stringify(props.note)));
|
||||
let note = $ref(deepClone(props.note));
|
||||
|
||||
// plugin
|
||||
if (noteViewInterruptors.length > 0) {
|
||||
onMounted(async () => {
|
||||
let result = JSON.parse(JSON.stringify(note));
|
||||
let result = deepClone(note);
|
||||
for (const interruptor of noteViewInterruptors) {
|
||||
result = await interruptor.handler(result);
|
||||
}
|
||||
@ -425,13 +426,18 @@ function readPromo() {
|
||||
> .article {
|
||||
display: flex;
|
||||
padding: 28px 32px 18px;
|
||||
cursor: pointer;
|
||||
|
||||
@media (pointer: coarse) {
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
> .avatar {
|
||||
flex-shrink: 0;
|
||||
display: block;
|
||||
margin: 0 14px 8px 0;
|
||||
width: 58px;
|
||||
height: 58px;
|
||||
width: 52px;
|
||||
height: 52px;
|
||||
position: sticky;
|
||||
/* For some reason this breaks avatar
|
||||
positions on notes, commenting it for now */
|
||||
@ -612,7 +618,7 @@ function readPromo() {
|
||||
margin: 0 10px 8px 0;
|
||||
width: 46px;
|
||||
height: 46px;
|
||||
top: calc(14px + var(--stickyTop, 0px));
|
||||
// top: calc(14px + var(--stickyTop, 0px));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -81,21 +81,21 @@
|
||||
</MkA>
|
||||
</div>
|
||||
<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-else><i class="ph-arrow-bend-up-left-bold ph-lg"></i></template>
|
||||
<p v-if="appearNote.repliesCount > 0" class="count">{{ appearNote.repliesCount }}</p>
|
||||
</button>
|
||||
<XRenoteButton ref="renoteButton" class="button" :note="appearNote" :count="appearNote.renoteCount"/>
|
||||
<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>
|
||||
</button>
|
||||
<button v-if="appearNote.myReaction != null" ref="reactButton" class="button _button reacted" @click="undoReact(appearNote)">
|
||||
<i class="ph-minus-bold ph-lg"></i>
|
||||
</button>
|
||||
<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>
|
||||
</button>
|
||||
</footer>
|
||||
@ -117,7 +117,7 @@
|
||||
<script lang="ts" setup>
|
||||
import { computed, inject, onMounted, onUnmounted, reactive, ref } from 'vue';
|
||||
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 XNoteSimple from '@/components/MkNoteSimple.vue';
|
||||
import XReactionsViewer from '@/components/MkReactionsViewer.vue';
|
||||
@ -143,6 +143,7 @@ import { $i } from '@/account';
|
||||
import { i18n } from '@/i18n';
|
||||
import { getNoteMenu } from '@/scripts/get-note-menu';
|
||||
import { useNoteCapture } from '@/scripts/use-note-capture';
|
||||
import { deepClone } from '@/scripts/clone';
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
@ -153,12 +154,12 @@ const props = defineProps<{
|
||||
|
||||
const inChannel = inject('inChannel', null);
|
||||
|
||||
let note = $ref(JSON.parse(JSON.stringify(props.note)));
|
||||
let note = $ref(deepClone(props.note));
|
||||
|
||||
// plugin
|
||||
if (noteViewInterruptors.length > 0) {
|
||||
onMounted(async () => {
|
||||
let result = JSON.parse(JSON.stringify(note));
|
||||
let result = deepClone(note);
|
||||
for (const interruptor of noteViewInterruptors) {
|
||||
result = await interruptor.handler(result);
|
||||
}
|
||||
@ -345,6 +346,11 @@ if (appearNote.replyId) {
|
||||
|
||||
> .reply-to-more {
|
||||
opacity: 0.7;
|
||||
cursor: pointer;
|
||||
|
||||
@media (pointer: coarse) {
|
||||
cursor: default;
|
||||
}
|
||||
}
|
||||
|
||||
> .renote {
|
||||
@ -410,8 +416,8 @@ if (appearNote.replyId) {
|
||||
> .avatar {
|
||||
display: block;
|
||||
flex-shrink: 0;
|
||||
width: 58px;
|
||||
height: 58px;
|
||||
width: 52px;
|
||||
height: 52px;
|
||||
}
|
||||
|
||||
> .body {
|
||||
@ -542,6 +548,11 @@ if (appearNote.replyId) {
|
||||
|
||||
> .reply {
|
||||
border-top: solid 0.5px var(--divider);
|
||||
cursor: pointer;
|
||||
|
||||
@media (pointer: coarse) {
|
||||
cursor: default;
|
||||
}
|
||||
}
|
||||
|
||||
> .reply, .reply-to, .reply-to-more {
|
||||
|
@ -65,6 +65,7 @@ const replies: misskey.entities.Note[] = props.conversation?.filter(item => item
|
||||
&.children {
|
||||
padding: 10px 0 0 16px;
|
||||
font-size: 1em;
|
||||
cursor: auto;
|
||||
|
||||
&.max-width_450px {
|
||||
padding: 10px 0 0 8px;
|
||||
@ -86,9 +87,15 @@ const replies: misskey.entities.Note[] = props.conversation?.filter(item => item
|
||||
> .body {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
cursor: pointer;
|
||||
|
||||
@media (pointer: coarse) {
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
> .header {
|
||||
margin-bottom: 2px;
|
||||
cursor: auto;
|
||||
}
|
||||
|
||||
> .body {
|
||||
|
@ -5,7 +5,7 @@
|
||||
<MkAvatar v-else-if="notification.user" class="icon" :user="notification.user"/>
|
||||
<img v-else-if="notification.icon" class="icon" :src="notification.icon" alt=""/>
|
||||
<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 === 'followRequestAccepted'" class="ph-check-bold"></i>
|
||||
<i v-else-if="notification.type === 'groupInvited'" class="ph-identification-card-bold"></i>
|
||||
|
@ -57,7 +57,7 @@ const buttonsLeft = $computed(() => {
|
||||
|
||||
if (history.length > 1) {
|
||||
buttons.push({
|
||||
icon: 'ph--left-bold ph-lg',
|
||||
icon: 'ph-caret-left-bold ph-lg',
|
||||
onClick: back,
|
||||
});
|
||||
}
|
||||
|
@ -36,7 +36,7 @@
|
||||
<MkAcct :user="u"/>
|
||||
<button class="_button" @click="removeVisibleUser(u)"><i class="ph-x-bold ph-lg"></i></button>
|
||||
</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>
|
||||
<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 { $i, getAccounts, openAccountMenu as openAccountMenu_ } from '@/account';
|
||||
import { uploadFile } from '@/scripts/upload';
|
||||
import { deepClone } from '@/scripts/clone';
|
||||
|
||||
const modal = inject('modal');
|
||||
|
||||
@ -575,7 +576,7 @@ async function post() {
|
||||
// plugin
|
||||
if (notePostInterruptors.length > 0) {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
> .local-only {
|
||||
margin: 0 0 0 12px;
|
||||
opacity: 0.7;
|
||||
@ -832,7 +833,7 @@ onMounted(() => {
|
||||
padding: 6px 24px;
|
||||
margin-bottom: 8px;
|
||||
overflow: auto;
|
||||
white-space: nowrap;
|
||||
line-height: 2rem;
|
||||
|
||||
> .visibleUsers {
|
||||
display: inline;
|
||||
@ -840,15 +841,19 @@ onMounted(() => {
|
||||
font-size: 14px;
|
||||
|
||||
> button {
|
||||
padding: 4px;
|
||||
padding: 2px;
|
||||
border-radius: 8px;
|
||||
|
||||
> i {
|
||||
transform: translateX(2px);
|
||||
}
|
||||
}
|
||||
|
||||
> span {
|
||||
margin-right: 14px;
|
||||
padding: 8px 0 8px 8px;
|
||||
border-radius: 8px;
|
||||
background: var(--X4);
|
||||
margin: 0.3rem;
|
||||
padding: 4px 0 4px 4px;
|
||||
border-radius: 999px;
|
||||
background: var(--X3);
|
||||
|
||||
> button {
|
||||
padding: 4px 8px;
|
||||
|
@ -1,6 +1,7 @@
|
||||
<template>
|
||||
<button
|
||||
v-if="canRenote && $store.state.seperateRenoteQuote"
|
||||
v-tooltip.noDelay.bottom="i18n.ts.quote"
|
||||
class="eddddedb _button"
|
||||
@click="quote()"
|
||||
>
|
||||
@ -14,6 +15,7 @@ import type { Note } from 'misskey-js/built/entities';
|
||||
import { pleaseLogin } from '@/scripts/please-login';
|
||||
import * as os from '@/os';
|
||||
import { $i } from '@/account';
|
||||
import { i18n } from '@/i18n';
|
||||
|
||||
const props = defineProps<{
|
||||
note: Note;
|
||||
|
@ -2,6 +2,7 @@
|
||||
<button
|
||||
v-if="canRenote"
|
||||
ref="buttonRef"
|
||||
v-tooltip.noDelay.bottom="i18n.ts.renote"
|
||||
class="eddddedb _button canRenote"
|
||||
@click="renote(false, $event)"
|
||||
>
|
||||
@ -15,7 +16,7 @@
|
||||
|
||||
<script lang="ts" setup>
|
||||
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 XDetails from '@/components/MkUsersTooltip.vue';
|
||||
import { pleaseLogin } from '@/scripts/please-login';
|
||||
@ -23,7 +24,7 @@ import * as os from '@/os';
|
||||
import { $i } from '@/account';
|
||||
import { useTooltip } from '@/scripts/use-tooltip';
|
||||
import { i18n } from '@/i18n';
|
||||
import { defaultStore } from "@/store";
|
||||
import { defaultStore } from '@/store';
|
||||
|
||||
const props = defineProps<{
|
||||
note: misskey.entities.Note;
|
||||
@ -52,42 +53,62 @@ useTooltip(buttonRef, async (showing) => {
|
||||
}, {}, 'closed');
|
||||
});
|
||||
|
||||
const renote = (viaKeyboard = false, ev?: MouseEvent) => {
|
||||
const renote = async (viaKeyboard = false, ev?: MouseEvent) => {
|
||||
pleaseLogin();
|
||||
if (defaultStore.state.seperateRenoteQuote) {
|
||||
os.api('notes/create', {
|
||||
renoteId: props.note.id,
|
||||
visibility: props.note.visibility,
|
||||
});
|
||||
const el = ev && (ev.currentTarget ?? ev.target) as HTMLElement | null | undefined;
|
||||
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');
|
||||
}
|
||||
} else {
|
||||
os.popupMenu([{
|
||||
text: i18n.ts.renote,
|
||||
icon: 'ph-repeat-bold ph-lg',
|
||||
action: () => {
|
||||
os.api('notes/create', {
|
||||
renoteId: props.note.id,
|
||||
visibility: props.note.visibility,
|
||||
});
|
||||
},
|
||||
}, {
|
||||
|
||||
const renotes = await os.api('notes/renotes', {
|
||||
noteId: props.note.id,
|
||||
limit: 11,
|
||||
});
|
||||
|
||||
const users = renotes.map(x => x.user.id);
|
||||
const hasRenotedBefore = users.includes($i.id);
|
||||
|
||||
let buttonActions = [{
|
||||
text: i18n.ts.renote,
|
||||
icon: 'ph-repeat-bold ph-lg',
|
||||
danger: false,
|
||||
action: () => {
|
||||
os.api('notes/create', {
|
||||
renoteId: props.note.id,
|
||||
visibility: props.note.visibility,
|
||||
});
|
||||
const el = ev && (ev.currentTarget ?? ev.target) as HTMLElement | null | undefined;
|
||||
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,
|
||||
icon: 'ph-quotes-bold ph-lg',
|
||||
danger: false,
|
||||
action: () => {
|
||||
os.post({
|
||||
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>
|
||||
|
||||
|
@ -65,6 +65,7 @@
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { onMounted, onUnmounted, ref } from 'vue';
|
||||
import { reducedMotion } from '@/scripts/reduced-motion';
|
||||
|
||||
const particles = ref([]);
|
||||
const el = ref<HTMLElement>();
|
||||
@ -75,34 +76,36 @@ let stop = false;
|
||||
let ro: ResizeObserver | undefined;
|
||||
|
||||
onMounted(() => {
|
||||
ro = new ResizeObserver((entries, observer) => {
|
||||
width.value = el.value?.offsetWidth + 64;
|
||||
height.value = el.value?.offsetHeight + 64;
|
||||
});
|
||||
ro.observe(el.value);
|
||||
const add = () => {
|
||||
if (stop) return;
|
||||
const x = (Math.random() * (width.value - 64));
|
||||
const y = (Math.random() * (height.value - 64));
|
||||
const sizeFactor = Math.random();
|
||||
const particle = {
|
||||
id: Math.random().toString(),
|
||||
x,
|
||||
y,
|
||||
size: 0.2 + ((sizeFactor / 10) * 3),
|
||||
dur: 1000 + (sizeFactor * 1000),
|
||||
color: colors[Math.floor(Math.random() * colors.length)],
|
||||
};
|
||||
particles.value.push(particle);
|
||||
window.setTimeout(() => {
|
||||
particles.value = particles.value.filter(x => x.id !== particle.id);
|
||||
}, particle.dur - 100);
|
||||
if (!reducedMotion()) {
|
||||
ro = new ResizeObserver((entries, observer) => {
|
||||
width.value = el.value?.offsetWidth + 64;
|
||||
height.value = el.value?.offsetHeight + 64;
|
||||
});
|
||||
ro.observe(el.value);
|
||||
const add = () => {
|
||||
if (stop) return;
|
||||
const x = (Math.random() * (width.value - 64));
|
||||
const y = (Math.random() * (height.value - 64));
|
||||
const sizeFactor = Math.random();
|
||||
const particle = {
|
||||
id: Math.random().toString(),
|
||||
x,
|
||||
y,
|
||||
size: 0.2 + ((sizeFactor / 10) * 3),
|
||||
dur: 1000 + (sizeFactor * 1000),
|
||||
color: colors[Math.floor(Math.random() * colors.length)],
|
||||
};
|
||||
particles.value.push(particle);
|
||||
window.setTimeout(() => {
|
||||
particles.value = particles.value.filter(x => x.id !== particle.id);
|
||||
}, particle.dur - 100);
|
||||
|
||||
window.setTimeout(() => {
|
||||
add();
|
||||
}, 500 + (Math.random() * 500));
|
||||
};
|
||||
add();
|
||||
window.setTimeout(() => {
|
||||
add();
|
||||
}, 500 + (Math.random() * 500));
|
||||
};
|
||||
add();
|
||||
}
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
|
@ -1,5 +1,5 @@
|
||||
<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>
|
||||
</button>
|
||||
</template>
|
||||
@ -9,6 +9,7 @@ import type { Note } from 'misskey-js/built/entities';
|
||||
import Ripple from '@/components/MkRipple.vue';
|
||||
import { pleaseLogin } from '@/scripts/please-login';
|
||||
import * as os from '@/os';
|
||||
import { i18n } from '@/i18n';
|
||||
|
||||
const props = defineProps<{
|
||||
note: Note;
|
||||
|
@ -1,5 +1,6 @@
|
||||
import { VNode, defineComponent, h } from 'vue';
|
||||
import { defineComponent, h } from 'vue';
|
||||
import * as mfm from 'mfm-js';
|
||||
import type { VNode } from 'vue';
|
||||
import MkUrl from '@/components/global/MkUrl.vue';
|
||||
import MkLink from '@/components/MkLink.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 { host } from '@/config';
|
||||
import { MFM_TAGS } from '@/scripts/mfm-tags';
|
||||
import { reducedMotion } from '@/scripts/reduced-motion';
|
||||
|
||||
export default defineComponent({
|
||||
props: {
|
||||
@ -97,17 +99,17 @@ export default defineComponent({
|
||||
}
|
||||
case 'jelly': {
|
||||
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;
|
||||
}
|
||||
case 'twitch': {
|
||||
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;
|
||||
}
|
||||
case 'shake': {
|
||||
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;
|
||||
}
|
||||
case 'spin': {
|
||||
@ -120,19 +122,30 @@ export default defineComponent({
|
||||
token.props.args.y ? 'mfm-spinY' :
|
||||
'mfm-spin';
|
||||
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;
|
||||
}
|
||||
case 'jump': {
|
||||
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;
|
||||
}
|
||||
case 'bounce': {
|
||||
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;
|
||||
}
|
||||
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': {
|
||||
const transform =
|
||||
(token.props.args.h && token.props.args.v) ? 'scale(-1, -1)' :
|
||||
@ -173,17 +186,6 @@ export default defineComponent({
|
||||
class: '_mfm_blur_',
|
||||
}, 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': {
|
||||
const rotate =
|
||||
token.props.args.x ? 'perspective(128px) rotateX' :
|
||||
|
21027
packages/client/src/icons.scss
Normal file
21027
packages/client/src/icons.scss
Normal file
File diff suppressed because it is too large
Load Diff
@ -3,6 +3,7 @@
|
||||
*/
|
||||
|
||||
import '@/style.scss';
|
||||
import '@/icons.scss';
|
||||
|
||||
//#region account indexedDB migration
|
||||
import { set } from '@/scripts/idb-proxy';
|
||||
@ -295,7 +296,7 @@ import { getAccountFromId } from '@/scripts/get-account-from-id';
|
||||
}, { immediate: true });
|
||||
|
||||
watch(defaultStore.reactiveState.useBlurEffect, v => {
|
||||
if (v) {
|
||||
if (v && deviceKind !== 'smartphone') {
|
||||
document.documentElement.style.removeProperty('--blur');
|
||||
} else {
|
||||
document.documentElement.style.setProperty('--blur', 'none');
|
||||
|
@ -72,7 +72,7 @@ export const navbarItemDef = reactive({
|
||||
},
|
||||
favorites: {
|
||||
title: 'favorites',
|
||||
icon: 'ph-star-bold ph-lg',
|
||||
icon: 'ph-bookmark-simple-bold ph-lg',
|
||||
show: computed(() => $i != null),
|
||||
to: '/my/favorites',
|
||||
},
|
||||
|
@ -24,20 +24,29 @@
|
||||
{{ i18n.ts._aboutMisskey.source }}
|
||||
<template #suffix>Codeberg</template>
|
||||
</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>
|
||||
</FormSection>
|
||||
<FormSection>
|
||||
<template #label>{{ i18n.ts._aboutMisskey.contributors }}</template>
|
||||
<div class="_formLinks">
|
||||
<FormLink to="https://codeberg.org/thatonecalculator" external>ThatOneCalculator (fork developer)</FormLink>
|
||||
<FormLink to="https://github.com/syuilo" external>Syuilo (Misskey developer)</FormLink>
|
||||
<FormLink to="/@t1c@i.calckey.cloud"><Mfm :text="'$[sparkle @t1c@i.calckey.cloud] (Main fork 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>
|
||||
</div>
|
||||
<template #caption><MkLink url="https://codeberg.org/thatonecalculator/calckey/activity">{{ i18n.ts._aboutMisskey.allContributors }}</MkLink></template>
|
||||
</FormSection>
|
||||
<FormSection>
|
||||
<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>
|
||||
</FormSection>
|
||||
</div>
|
||||
@ -53,92 +62,14 @@ import FormLink from '@/components/form/link.vue';
|
||||
import FormSection from '@/components/form/section.vue';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import MkLink from '@/components/MkLink.vue';
|
||||
import MkSparkle from '@/components/MkSparkle.vue';
|
||||
import { physics } from '@/scripts/physics';
|
||||
import { i18n } from '@/i18n';
|
||||
import { defaultStore } from '@/store';
|
||||
import * as os from '@/os';
|
||||
import { definePageMetadata } from '@/scripts/page-metadata';
|
||||
|
||||
const 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',
|
||||
];
|
||||
const patrons = await os.api('patrons');
|
||||
|
||||
let easterEggReady = false;
|
||||
let easterEggEmojis = $ref([]);
|
||||
|
@ -48,7 +48,7 @@ watch(() => props.clipId, async () => {
|
||||
});
|
||||
}, {
|
||||
immediate: true,
|
||||
});
|
||||
});
|
||||
|
||||
provide('currentClipPage', $$(clip));
|
||||
|
||||
|
@ -1,35 +1,45 @@
|
||||
<template>
|
||||
<MkStickyContainer>
|
||||
<template #header><MkPageHeader/></template>
|
||||
<MkSpacer :content-max="800">
|
||||
<MkPagination ref="pagingComponent" :pagination="pagination">
|
||||
<template #empty>
|
||||
<div class="_fullinfo">
|
||||
<img src="/static-assets/badges/info.png" class="_ghost" alt="Info"/>
|
||||
<div>{{ i18n.ts.noNotes }}</div>
|
||||
</div>
|
||||
</template>
|
||||
<MkStickyContainer>
|
||||
<template #header><MkPageHeader /></template>
|
||||
<MkSpacer :content-max="800">
|
||||
<MkPagination ref="pagingComponent" :pagination="pagination">
|
||||
<template #empty>
|
||||
<div class="_fullinfo">
|
||||
<img
|
||||
src="/static-assets/badges/info.png"
|
||||
class="_ghost"
|
||||
alt="Info"
|
||||
/>
|
||||
<div>{{ i18n.ts.noNotes }}</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #default="{ items }">
|
||||
<XList v-slot="{ item }" :items="items" :direction="'down'" :no-gap="false" :ad="false">
|
||||
<XNote :key="item.id" :note="item.note" :class="$style.note"/>
|
||||
</XList>
|
||||
</template>
|
||||
</MkPagination>
|
||||
</MkSpacer>
|
||||
</MkStickyContainer>
|
||||
<template #default="{ items }">
|
||||
<XList
|
||||
v-slot="{ item }"
|
||||
:items="items"
|
||||
:direction="'down'"
|
||||
:no-gap="false"
|
||||
:ad="false"
|
||||
>
|
||||
<XNote :key="item.id" :note="item.note" :class="$style.note" />
|
||||
</XList>
|
||||
</template>
|
||||
</MkPagination>
|
||||
</MkSpacer>
|
||||
</MkStickyContainer>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref } from 'vue';
|
||||
import MkPagination from '@/components/MkPagination.vue';
|
||||
import XNote from '@/components/MkNote.vue';
|
||||
import XList from '@/components/MkDateSeparatedList.vue';
|
||||
import { i18n } from '@/i18n';
|
||||
import { definePageMetadata } from '@/scripts/page-metadata';
|
||||
import { ref } from "vue";
|
||||
import MkPagination from "@/components/MkPagination.vue";
|
||||
import XNote from "@/components/MkNote.vue";
|
||||
import XList from "@/components/MkDateSeparatedList.vue";
|
||||
import { i18n } from "@/i18n";
|
||||
import { definePageMetadata } from "@/scripts/page-metadata";
|
||||
|
||||
const pagination = {
|
||||
endpoint: 'i/favorites' as const,
|
||||
endpoint: "i/favorites" as const,
|
||||
limit: 10,
|
||||
};
|
||||
|
||||
@ -37,7 +47,7 @@ const pagingComponent = ref<InstanceType<typeof MkPagination>>();
|
||||
|
||||
definePageMetadata({
|
||||
title: i18n.ts.favorites,
|
||||
icon: 'ph-star-bold ph-lg',
|
||||
icon: "ph-bookmark-simple-bold ph-lg",
|
||||
});
|
||||
</script>
|
||||
|
||||
|
@ -24,7 +24,7 @@
|
||||
<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-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 class="user">
|
||||
@ -67,6 +67,7 @@ import { url } from '@/config';
|
||||
import { useRouter } from '@/router';
|
||||
import { i18n } from '@/i18n';
|
||||
import { definePageMetadata } from '@/scripts/page-metadata';
|
||||
import { shareAvailable } from '@/scripts/share-available';
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<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="balloon" :class="{ noText: message.text == null }">
|
||||
<button v-if="isMe" class="delete-button" :title="i18n.ts.delete" @click="del">
|
||||
@ -38,7 +38,6 @@
|
||||
<script lang="ts" setup>
|
||||
import { } from 'vue';
|
||||
import * as mfm from 'mfm-js';
|
||||
import VuePlyr from 'vue-plyr';
|
||||
import type * as Misskey from 'misskey-js';
|
||||
import XMediaList from '@/components/MkMediaList.vue';
|
||||
import { extractUrlFromMfm } from '@/scripts/extract-url-from-mfm';
|
||||
@ -73,10 +72,10 @@ function del(): void {
|
||||
|
||||
> .avatar {
|
||||
position: sticky;
|
||||
top: calc(var(--stickyTop, 0px) + 16px);
|
||||
top: calc(var(--stickyTop, 0px) + 20px);
|
||||
display: block;
|
||||
width: 54px;
|
||||
height: 54px;
|
||||
width: 45px;
|
||||
height: 45px;
|
||||
transition: all 0.1s ease;
|
||||
}
|
||||
|
||||
@ -92,14 +91,6 @@ function del(): void {
|
||||
border-radius: 16px;
|
||||
max-width: 100%;
|
||||
|
||||
&:before {
|
||||
content: "";
|
||||
pointer-events: none;
|
||||
display: block;
|
||||
position: absolute;
|
||||
top: 12px;
|
||||
}
|
||||
|
||||
& + * {
|
||||
clear: both;
|
||||
}
|
||||
@ -222,7 +213,7 @@ function del(): void {
|
||||
padding-right: 32px;
|
||||
|
||||
> .balloon {
|
||||
$color: var(--messageBg);
|
||||
$color: var(--X4);
|
||||
background: $color;
|
||||
|
||||
&.noText {
|
||||
|
@ -24,6 +24,7 @@
|
||||
<template #label>{{ i18n.ts._pages.url }}</template>
|
||||
</MkInput>
|
||||
|
||||
<MkSwitch v-model="isPublic" class="_formBlock">{{ i18n.ts.public }}</MkSwitch>
|
||||
<MkSwitch v-model="alignCenter" class="_formBlock">{{ i18n.ts._pages.alignCenter }}</MkSwitch>
|
||||
|
||||
<MkSelect v-model="font" class="_formBlock">
|
||||
@ -47,7 +48,6 @@
|
||||
<div v-else-if="tab === 'contents'">
|
||||
<div>
|
||||
<XBlocks v-model="content" class="content" :hpml="hpml"/>
|
||||
|
||||
<MkButton v-if="!readonly" @click="add()"><i class="ph-plus-bold ph-lg"></i></MkButton>
|
||||
</div>
|
||||
</div>
|
||||
@ -130,6 +130,7 @@ let eyeCatchingImageId = $ref(null);
|
||||
let font = $ref('sans-serif');
|
||||
let content = $ref([]);
|
||||
let alignCenter = $ref(false);
|
||||
let isPublic = $ref(true);
|
||||
let hideTitleWhenPinned = $ref(false);
|
||||
let variables = $ref([]);
|
||||
let hpml = $ref(null);
|
||||
@ -158,6 +159,7 @@ function getSaveOptions() {
|
||||
script: script,
|
||||
hideTitleWhenPinned: hideTitleWhenPinned,
|
||||
alignCenter: alignCenter,
|
||||
isPublic: isPublic,
|
||||
content: content,
|
||||
variables: variables,
|
||||
eyeCatchingImageId: eyeCatchingImageId,
|
||||
@ -393,6 +395,7 @@ async function init() {
|
||||
script = page.script;
|
||||
hideTitleWhenPinned = page.hideTitleWhenPinned;
|
||||
alignCenter = page.alignCenter;
|
||||
isPublic = page.isPublic;
|
||||
content = page.content;
|
||||
variables = page.variables;
|
||||
eyeCatchingImageId = page.eyeCatchingImageId;
|
||||
@ -401,7 +404,7 @@ async function init() {
|
||||
content = [{
|
||||
id,
|
||||
type: 'text',
|
||||
text: 'Hello World!',
|
||||
text: '',
|
||||
}];
|
||||
}
|
||||
}
|
||||
@ -439,7 +442,7 @@ definePageMetadata(computed(() => {
|
||||
return {
|
||||
title: title,
|
||||
icon: 'ph-pencil-bold ph-lg',
|
||||
};
|
||||
};
|
||||
}));
|
||||
</script>
|
||||
|
||||
@ -447,7 +450,7 @@ definePageMetadata(computed(() => {
|
||||
.jqqmcavi {
|
||||
> .button {
|
||||
& + .button {
|
||||
margin-left: 8px;
|
||||
margin: 4px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -4,14 +4,25 @@
|
||||
<MkSpacer :content-max="800">
|
||||
<transition :name="$store.state.animation ? 'fade' : ''" mode="out-in">
|
||||
<div v-if="page" :key="page.id" v-size="{ max: [450] }" class="xcukqgmh">
|
||||
<div class="_block main">
|
||||
<!--
|
||||
<div class="header">
|
||||
<h1>{{ page.title }}</h1>
|
||||
<div class="footer">
|
||||
<div><i class="ph-alarm-bold"/> {{ 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 class="_block main">
|
||||
<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 class="content">
|
||||
<XPage :page="page"/>
|
||||
@ -23,29 +34,25 @@
|
||||
</div>
|
||||
<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.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 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" large class="koudoku"/>
|
||||
</div>
|
||||
<div class="links">
|
||||
<!-- <div class="links">
|
||||
<MkA :to="`/@${username}/pages/${pageName}/view-source`" class="link">{{ i18n.ts._pages.viewSource }}</MkA>
|
||||
<template v-if="$i && $i.id === page.userId">
|
||||
<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-else class="link _textButton" @click="pin(true)">{{ i18n.ts.pin }}</button>
|
||||
</template>
|
||||
</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']"/>
|
||||
<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 { i18n } from '@/i18n';
|
||||
import { definePageMetadata } from '@/scripts/page-metadata';
|
||||
import { shareAvailable } from '@/scripts/share-available';
|
||||
|
||||
const props = defineProps<{
|
||||
pageName: string;
|
||||
@ -81,6 +89,7 @@ const props = defineProps<{
|
||||
}>();
|
||||
|
||||
let page = $ref(null);
|
||||
let bgImg = $ref(null);
|
||||
let error = $ref(null);
|
||||
const otherPostsPagination = {
|
||||
endpoint: 'users/pages' as const,
|
||||
@ -98,11 +107,21 @@ function fetchPage() {
|
||||
username: props.username,
|
||||
}).then(_page => {
|
||||
page = _page;
|
||||
bgImg = getBgImg();
|
||||
}).catch(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() {
|
||||
navigator.share({
|
||||
title: page.title ?? page.name,
|
||||
@ -118,7 +137,7 @@ function shareWithNote() {
|
||||
}
|
||||
|
||||
function like() {
|
||||
os.apiWithDialog('pages/like', {
|
||||
os.api('pages/like', {
|
||||
pageId: page.id,
|
||||
}).then(() => {
|
||||
page.isLiked = true;
|
||||
@ -180,35 +199,65 @@ definePageMetadata(computed(() => page ? {
|
||||
margin: 1rem;
|
||||
}
|
||||
|
||||
> .header {
|
||||
padding: 16px;
|
||||
|
||||
> h1 {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
> .banner {
|
||||
margin: 0rem !important;
|
||||
|
||||
> img {
|
||||
> .banner-image {
|
||||
// TODO: 良い感じのアスペクト比で表示
|
||||
display: block;
|
||||
width: 100%;
|
||||
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 {
|
||||
padding: 16px 0 0 0;
|
||||
padding: 16px 0;
|
||||
}
|
||||
|
||||
> .actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-top: 16px;
|
||||
padding: 16px 0 0 0;
|
||||
padding: 16px 0;
|
||||
border-top: solid 0.5px var(--divider);
|
||||
|
||||
> .like {
|
||||
@ -226,10 +275,8 @@ definePageMetadata(computed(() => page ? {
|
||||
}
|
||||
|
||||
> .other {
|
||||
margin-left: auto;
|
||||
|
||||
> button {
|
||||
padding: 8px;
|
||||
padding: 2px;
|
||||
margin: 0 8px;
|
||||
|
||||
&:hover {
|
||||
@ -237,27 +284,26 @@ definePageMetadata(computed(() => page ? {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
> .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 {
|
||||
> .user {
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -66,8 +66,9 @@ import * as os from '@/os';
|
||||
import { defaultStore } from '@/store';
|
||||
import { i18n } from '@/i18n';
|
||||
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 reactionPickerWidth = $computed(defaultStore.makeGetterSetter('reactionPickerWidth'));
|
||||
@ -101,7 +102,7 @@ async function setDefault() {
|
||||
});
|
||||
if (canceled) return;
|
||||
|
||||
reactions = JSON.parse(JSON.stringify(defaultStore.def.reactions.default));
|
||||
reactions = deepClone(defaultStore.def.reactions.default);
|
||||
}
|
||||
|
||||
function chooseEmoji(ev: MouseEvent) {
|
||||
|
@ -91,13 +91,14 @@ import FormRange from '@/components/form/range.vue';
|
||||
import * as os from '@/os';
|
||||
import { defaultStore } from '@/store';
|
||||
import { i18n } from '@/i18n';
|
||||
import { deepClone } from '@/scripts/clone';
|
||||
|
||||
const props = defineProps<{
|
||||
_id: string;
|
||||
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, () => {
|
||||
if (statusbar.type === 'rss') {
|
||||
@ -128,8 +129,8 @@ watch(statusbar, save);
|
||||
|
||||
async function save() {
|
||||
const i = defaultStore.state.statusbars.findIndex(x => x.id === props._id);
|
||||
const statusbars = JSON.parse(JSON.stringify(defaultStore.state.statusbars));
|
||||
statusbars[i] = JSON.parse(JSON.stringify(statusbar));
|
||||
const statusbars = deepClone(defaultStore.state.statusbars);
|
||||
statusbars[i] = deepClone(statusbar);
|
||||
defaultStore.set('statusbars', statusbars);
|
||||
}
|
||||
|
||||
|
@ -25,9 +25,10 @@
|
||||
</div>
|
||||
</div>
|
||||
<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>
|
||||
<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>
|
||||
<MkAvatar class="avatar" :user="user" :disable-preview="true" :show-indicator="true"/>
|
||||
|
18
packages/client/src/scripts/clone.ts
Normal file
18
packages/client/src/scripts/clone.ts
Normal 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;
|
||||
}
|
||||
}
|
@ -8,6 +8,7 @@ import * as os from '@/os';
|
||||
import copyToClipboard from '@/scripts/copy-to-clipboard';
|
||||
import { url } from '@/config';
|
||||
import { noteActions } from '@/store';
|
||||
import { shareAvailable } from '@/scripts/share-available';
|
||||
|
||||
export function getNoteMenu(props: {
|
||||
note: misskey.entities.Note;
|
||||
@ -220,23 +221,23 @@ export function getNoteMenu(props: {
|
||||
window.open(appearNote.url || appearNote.uri, '_blank');
|
||||
},
|
||||
} : undefined,
|
||||
{
|
||||
shareAvailable() ? {
|
||||
icon: 'ph-share-network-bold ph-lg',
|
||||
text: i18n.ts.share,
|
||||
action: share,
|
||||
},
|
||||
} : undefined,
|
||||
instance.translatorAvailable ? {
|
||||
icon: 'ph-translate-bold ph-lg',
|
||||
text: i18n.ts.translate,
|
||||
action: translate,
|
||||
} : undefined,
|
||||
null,
|
||||
statePromise.then(state => state.isFavorited ? {
|
||||
icon: 'ph-star-bold ph-lg',
|
||||
statePromise.then(state => state?.isFavorited ? {
|
||||
icon: 'ph-bookmark-simple-bold ph-lg',
|
||||
text: i18n.ts.unfavorite,
|
||||
action: () => toggleFavorite(false),
|
||||
} : {
|
||||
icon: 'ph-star-bold ph-lg',
|
||||
icon: 'ph-bookmark-simple-bold ph-lg',
|
||||
text: i18n.ts.favorite,
|
||||
action: () => toggleFavorite(true),
|
||||
}),
|
||||
|
3
packages/client/src/scripts/reduced-motion.ts
Normal file
3
packages/client/src/scripts/reduced-motion.ts
Normal file
@ -0,0 +1,3 @@
|
||||
export function reducedMotion(): boolean {
|
||||
return window.matchMedia('(prefers-reduced-motion: reduce)').matches;
|
||||
}
|
6
packages/client/src/scripts/share-available.ts
Normal file
6
packages/client/src/scripts/share-available.ts
Normal file
@ -0,0 +1,6 @@
|
||||
export function shareAvailable(): boolean {
|
||||
if (navigator.share) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
@ -13,6 +13,7 @@ export type Theme = {
|
||||
|
||||
import lightTheme from '@/themes/_light.json5';
|
||||
import darkTheme from '@/themes/_dark.json5';
|
||||
import { deepClone } from './clone';
|
||||
|
||||
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';
|
||||
|
||||
// Deep copy
|
||||
const _theme = JSON.parse(JSON.stringify(theme));
|
||||
const _theme = deepClone(theme);
|
||||
|
||||
if (_theme.base) {
|
||||
const base = [lightTheme, darkTheme].find(x => x.id === _theme.base);
|
||||
|
@ -98,9 +98,9 @@ a {
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
}
|
||||
|
||||
i {
|
||||
transform: translateY(0.1em);
|
||||
}
|
||||
// i {
|
||||
// transform: translateY(0.1em);
|
||||
// }
|
||||
|
||||
textarea, input {
|
||||
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 {
|
||||
will-change: transform;
|
||||
animation: bounce ease 0.7s;
|
||||
|
@ -11,7 +11,7 @@
|
||||
|
||||
<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>
|
||||
</template>
|
||||
@ -99,8 +99,8 @@ if ($i) {
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: 2147483647;
|
||||
color: #ff0;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
color: #f6c177;
|
||||
background: #6e6a86;
|
||||
padding: 4px 5px;
|
||||
font-size: 14px;
|
||||
pointer-events: none;
|
||||
|
@ -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>
|
||||
</div>
|
||||
<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 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>
|
||||
@ -322,7 +322,7 @@ async function deleteProfile() {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
width: 32px;
|
||||
width: 44px;
|
||||
|
||||
> .top, > .middle, > .bottom {
|
||||
> .button {
|
||||
@ -339,6 +339,11 @@ async function deleteProfile() {
|
||||
> .middle {
|
||||
margin-top: auto;
|
||||
margin-bottom: auto;
|
||||
|
||||
> .new {
|
||||
font-size: 20px;
|
||||
background-color: var(--accentedBg);
|
||||
}
|
||||
}
|
||||
|
||||
> .bottom {
|
||||
|
@ -133,25 +133,25 @@ function getMenu() {
|
||||
text: i18n.ts.move + '...',
|
||||
icon: 'ph-arrows-out-cardinal-bold ph-lg',
|
||||
children: [{
|
||||
icon: 'ph--left-bold ph-lg',
|
||||
icon: 'ph-caret-left-bold ph-lg',
|
||||
text: i18n.ts._deck.swapLeft,
|
||||
action: () => {
|
||||
swapLeftColumn(props.column.id);
|
||||
},
|
||||
}, {
|
||||
icon: 'ph--right-bold ph-lg',
|
||||
icon: 'ph-caret-right-bold ph-lg',
|
||||
text: i18n.ts._deck.swapRight,
|
||||
action: () => {
|
||||
swapRightColumn(props.column.id);
|
||||
},
|
||||
}, props.isStacked ? {
|
||||
icon: 'ph--up-bold ph-lg',
|
||||
icon: 'ph-caret-up-bold ph-lg',
|
||||
text: i18n.ts._deck.swapUp,
|
||||
action: () => {
|
||||
swapUpColumn(props.column.id);
|
||||
},
|
||||
} : undefined, props.isStacked ? {
|
||||
icon: 'ph--down-bold ph-lg',
|
||||
icon: 'ph-caret-down-bold ph-lg',
|
||||
text: i18n.ts._deck.swapDown,
|
||||
action: () => {
|
||||
swapDownColumn(props.column.id);
|
||||
|
@ -4,6 +4,7 @@ import { notificationTypes } from 'misskey-js';
|
||||
import { Storage } from '../../pizzax';
|
||||
import { i18n } from '@/i18n';
|
||||
import { api } from '@/os';
|
||||
import { deepClone } from '@/scripts/clone';
|
||||
|
||||
type ColumnWidget = {
|
||||
name: string;
|
||||
@ -25,10 +26,6 @@ export type Column = {
|
||||
tl?: 'home' | 'local' | 'social' | 'global';
|
||||
};
|
||||
|
||||
function copy<T>(x: T): T {
|
||||
return JSON.parse(JSON.stringify(x));
|
||||
}
|
||||
|
||||
export const deckStore = markRaw(new Storage('deck', {
|
||||
profile: {
|
||||
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 bX = deckStore.state.layout.findIndex(ids => ids.indexOf(b) !== -1);
|
||||
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[bX][bY] = a;
|
||||
deckStore.set('layout', layout);
|
||||
@ -136,7 +133,7 @@ export function swapColumn(a: Column['id'], b: 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) => {
|
||||
if (ids.includes(id)) {
|
||||
const left = deckStore.state.layout[i - 1];
|
||||
@ -152,7 +149,7 @@ export function swapLeftColumn(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) => {
|
||||
if (ids.includes(id)) {
|
||||
const right = deckStore.state.layout[i + 1];
|
||||
@ -168,9 +165,9 @@ export function swapRightColumn(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 ids = copy(deckStore.state.layout[idsIndex]);
|
||||
const ids = deepClone(deckStore.state.layout[idsIndex]);
|
||||
ids.some((x, i) => {
|
||||
if (x === id) {
|
||||
const up = ids[i - 1];
|
||||
@ -188,9 +185,9 @@ export function swapUpColumn(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 ids = copy(deckStore.state.layout[idsIndex]);
|
||||
const ids = deepClone(deckStore.state.layout[idsIndex]);
|
||||
ids.some((x, i) => {
|
||||
if (x === id) {
|
||||
const down = ids[i + 1];
|
||||
@ -208,7 +205,7 @@ export function swapDownColumn(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));
|
||||
layout = layout.map(ids => ids.filter(_id => _id !== id));
|
||||
layout[i - 1].push(id);
|
||||
@ -218,7 +215,7 @@ export function stackLeftColumn(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 affected = layout[i];
|
||||
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);
|
||||
deckStore.set('layout', layout);
|
||||
|
||||
const columns = copy(deckStore.state.columns);
|
||||
const columns = deepClone(deckStore.state.columns);
|
||||
for (const column of columns) {
|
||||
if (affected.includes(column.id)) {
|
||||
column.active = true;
|
||||
@ -238,9 +235,9 @@ export function popRightColumn(id: Column['id']) {
|
||||
}
|
||||
|
||||
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 column = copy(deckStore.state.columns[columnIndex]);
|
||||
const column = deepClone(deckStore.state.columns[columnIndex]);
|
||||
if (column == null) return;
|
||||
if (column.widgets == null) column.widgets = [];
|
||||
column.widgets.unshift(widget);
|
||||
@ -250,9 +247,9 @@ export function addColumnWidget(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 column = copy(deckStore.state.columns[columnIndex]);
|
||||
const column = deepClone(deckStore.state.columns[columnIndex]);
|
||||
if (column == null) return;
|
||||
column.widgets = column.widgets.filter(w => w.id !== widget.id);
|
||||
columns[columnIndex] = column;
|
||||
@ -261,9 +258,9 @@ export function removeColumnWidget(id: Column['id'], widget: 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 column = copy(deckStore.state.columns[columnIndex]);
|
||||
const column = deepClone(deckStore.state.columns[columnIndex]);
|
||||
if (column == null) return;
|
||||
column.widgets = widgets;
|
||||
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) {
|
||||
const columns = copy(deckStore.state.columns);
|
||||
const columns = deepClone(deckStore.state.columns);
|
||||
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;
|
||||
column.widgets = column.widgets.map(w => w.id === widgetId ? {
|
||||
...w,
|
||||
@ -286,9 +283,9 @@ export function updateColumnWidget(id: Column['id'], widgetId: string, widgetDat
|
||||
}
|
||||
|
||||
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 currentColumn = copy(deckStore.state.columns[columnIndex]);
|
||||
const currentColumn = deepClone(deckStore.state.columns[columnIndex]);
|
||||
if (currentColumn == null) return;
|
||||
for (const [k, v] of Object.entries(column)) {
|
||||
currentColumn[k] = v;
|
||||
|
@ -377,6 +377,10 @@ const wallpaper = localStorage.getItem('wallpaper') != null;
|
||||
|
||||
> .button-wrapper {
|
||||
|
||||
> i {
|
||||
transform: translateY(0.05em);
|
||||
}
|
||||
|
||||
&.on {
|
||||
background-color: var(--accentedBg);
|
||||
width: 100%;
|
||||
|
@ -47,12 +47,13 @@
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { onMounted, onUnmounted, reactive, ref } from 'vue';
|
||||
import { GetFormResultType } from '@/scripts/form';
|
||||
import { useWidgetPropsManager, Widget, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget';
|
||||
import { GetFormResultType } from '@/scripts/form';
|
||||
import { stream } from '@/stream';
|
||||
import number from '@/filters/number';
|
||||
import * as sound from '@/scripts/sound';
|
||||
import * as os from '@/os';
|
||||
import { deepClone } from '@/scripts/clone';
|
||||
|
||||
const name = 'jobQueue';
|
||||
|
||||
@ -100,12 +101,12 @@ const prev = reactive({} as typeof current);
|
||||
const jammedSound = sound.setVolume(sound.getAudio('syuilo/queue-jammed'), 1);
|
||||
|
||||
for (const domain of ['inbox', 'deliver']) {
|
||||
prev[domain] = JSON.parse(JSON.stringify(current[domain]));
|
||||
prev[domain] = deepClone(current[domain]);
|
||||
}
|
||||
|
||||
const onStats = (stats) => {
|
||||
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].active = stats[domain].active;
|
||||
current[domain].waiting = stats[domain].waiting;
|
||||
|
@ -2,6 +2,7 @@ import { reactive, watch } from 'vue';
|
||||
import { throttle } from 'throttle-debounce';
|
||||
import { Form, GetFormResultType } from '@/scripts/form';
|
||||
import * as os from '@/os';
|
||||
import { deepClone } from '@/scripts/clone';
|
||||
|
||||
export type Widget<P extends Record<string, unknown>> = {
|
||||
id: string;
|
||||
@ -32,7 +33,7 @@ export const useWidgetPropsManager = <F extends Form & Record<string, { default:
|
||||
save: () => 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 = () => {
|
||||
for (const prop of Object.keys(propsDef)) {
|
||||
@ -43,14 +44,14 @@ export const useWidgetPropsManager = <F extends Form & Record<string, { default:
|
||||
};
|
||||
watch(widgetProps, () => {
|
||||
mergeProps();
|
||||
}, { deep: true, immediate: true, });
|
||||
}, { deep: true, immediate: true });
|
||||
|
||||
const save = throttle(3000, () => {
|
||||
emit('updateProps', widgetProps);
|
||||
});
|
||||
|
||||
const configure = async () => {
|
||||
const form = JSON.parse(JSON.stringify(propsDef));
|
||||
const form = deepClone(propsDef);
|
||||
for (const item of Object.keys(form)) {
|
||||
form[item].default = widgetProps[item];
|
||||
}
|
||||
|
@ -7,11 +7,11 @@
|
||||
"lint": "eslint --quiet src/**/*.{ts}"
|
||||
},
|
||||
"dependencies": {
|
||||
"esbuild": "^0.14.54",
|
||||
"esbuild": "^0.15.14",
|
||||
"idb-keyval": "^6.2.0",
|
||||
"misskey-js": "0.0.14"
|
||||
},
|
||||
"devDependencies": {
|
||||
"eslint": "^8.27.0"
|
||||
"eslint": "^8.28.0"
|
||||
}
|
||||
}
|
||||
|
11
patrons.json
Normal file
11
patrons.json
Normal 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
10
push-docker.sh
Executable 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
|
Loading…
Reference in New Issue
Block a user