mirror of
https://iceshrimp.dev/limepotato/jormungandr-bite.git
synced 2024-11-22 18:07:31 -07:00
Merge branch 'develop' into beta
This commit is contained in:
commit
a57530160a
88 changed files with 1864 additions and 238 deletions
|
@ -72,6 +72,16 @@ redis:
|
||||||
# user:
|
# user:
|
||||||
# pass:
|
# pass:
|
||||||
|
|
||||||
|
# ┌─────────────────────┐
|
||||||
|
#───┘ Sonic configuration └─────────────────────────────────────
|
||||||
|
|
||||||
|
#sonic:
|
||||||
|
# host: localhost
|
||||||
|
# port: 1491
|
||||||
|
# auth: SecretPassword
|
||||||
|
# collection: notes
|
||||||
|
# bucket: default
|
||||||
|
|
||||||
# ┌───────────────┐
|
# ┌───────────────┐
|
||||||
#───┘ ID generation └───────────────────────────────────────────
|
#───┘ ID generation └───────────────────────────────────────────
|
||||||
|
|
||||||
|
|
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -44,6 +44,7 @@ ormconfig.json
|
||||||
packages/backend/assets/instance.css
|
packages/backend/assets/instance.css
|
||||||
packages/backend/assets/sounds/None.mp3
|
packages/backend/assets/sounds/None.mp3
|
||||||
|
|
||||||
|
!packages/backend/src/db
|
||||||
|
|
||||||
# blender backups
|
# blender backups
|
||||||
*.blend1
|
*.blend1
|
||||||
|
|
13
CALCKEY.md
13
CALCKEY.md
|
@ -3,21 +3,17 @@
|
||||||
## Planned
|
## Planned
|
||||||
|
|
||||||
- Stucture
|
- Stucture
|
||||||
- [Sonic](https://crates.io/crates/sonic-server) support as an ElasticSearch alternative
|
|
||||||
- [DragonflyDB](https://dragonflydb.io/) support as a Redis alternative
|
- [DragonflyDB](https://dragonflydb.io/) support as a Redis alternative
|
||||||
- Optionally use [ScyllaDB](https://www.scylladb.com/open-source-nosql-database/) for storing notes
|
- Optionally use [ScyllaDB](https://www.scylladb.com/open-source-nosql-database/) for storing notes
|
||||||
- Rewrite backend in Rust and [Axum](https://github.com/tokio-rs/axum)
|
- Rewrite backend in Rust and [Axum](https://github.com/tokio-rs/axum)
|
||||||
- Function
|
- Function
|
||||||
- Federate with note edits
|
- Federate with note edits
|
||||||
- Admin customizable max note length (100-8000)
|
|
||||||
- User "choices" (recommended users) like Mastodon and Soapbox
|
- User "choices" (recommended users) like Mastodon and Soapbox
|
||||||
- Join Reason system like Mastodon/Pleroma
|
- Join Reason system like Mastodon/Pleroma
|
||||||
- Option to publicize instance blocks
|
- Option to publicize instance blocks
|
||||||
- Backfill remote users
|
|
||||||
- Build flag to remove NSFW/AI stuff
|
- Build flag to remove NSFW/AI stuff
|
||||||
- Timeline filters
|
- Timeline filters
|
||||||
- Filter notifications by user
|
- Filter notifications by user
|
||||||
- Non-nyaify cat mode
|
|
||||||
- Exclude self from antenna
|
- Exclude self from antenna
|
||||||
- Form
|
- Form
|
||||||
- MFM button
|
- MFM button
|
||||||
|
@ -37,6 +33,7 @@
|
||||||
- Admin custom CSS
|
- Admin custom CSS
|
||||||
- Add back time machine (jump to date)
|
- Add back time machine (jump to date)
|
||||||
- Improve accesibility
|
- Improve accesibility
|
||||||
|
- Non-nyaify cat mode
|
||||||
|
|
||||||
## Implemented
|
## Implemented
|
||||||
|
|
||||||
|
@ -108,6 +105,14 @@
|
||||||
- Allows custom emoji
|
- Allows custom emoji
|
||||||
- Fix lint errors
|
- Fix lint errors
|
||||||
- Use Rome instead of ESLint
|
- Use Rome instead of ESLint
|
||||||
|
- Mastodon API support
|
||||||
|
- More antenna options
|
||||||
|
- New dashboard
|
||||||
|
- Backfill follower counts
|
||||||
|
- Improved emoji licensing
|
||||||
|
- Compile time compression
|
||||||
|
- Sonic search
|
||||||
|
- Popular color schemes, including Nord, Gruvbox, and Catppuccin
|
||||||
- MissV: [fix Misskey Forkbomb](https://code.vtopia.live/Vtopia/MissV/commit/40b23c070bd4adbb3188c73546c6c625138fb3c1)
|
- MissV: [fix Misskey Forkbomb](https://code.vtopia.live/Vtopia/MissV/commit/40b23c070bd4adbb3188c73546c6c625138fb3c1)
|
||||||
- [Make showing ads optional](https://github.com/misskey-dev/misskey/pull/8996)
|
- [Make showing ads optional](https://github.com/misskey-dev/misskey/pull/8996)
|
||||||
- [Tapping avatar in mobile opens account modal](https://github.com/misskey-dev/misskey/pull/9056)
|
- [Tapping avatar in mobile opens account modal](https://github.com/misskey-dev/misskey/pull/9056)
|
||||||
|
|
22
README.md
22
README.md
|
@ -34,6 +34,9 @@
|
||||||
- OCR image captioning
|
- OCR image captioning
|
||||||
- New and improved Groups
|
- New and improved Groups
|
||||||
- Better intro tutorial
|
- Better intro tutorial
|
||||||
|
- Compatibility with Mastodon clients/apps
|
||||||
|
- Backfill user information
|
||||||
|
- Sonic search
|
||||||
- Many more user and admin settings
|
- Many more user and admin settings
|
||||||
- [So much more!](./CALCKEY.md)
|
- [So much more!](./CALCKEY.md)
|
||||||
|
|
||||||
|
@ -78,8 +81,9 @@ If you have access to a server that supports one of the sources below, I recomme
|
||||||
### 😗 Optional dependencies
|
### 😗 Optional dependencies
|
||||||
|
|
||||||
- [FFmpeg](https://ffmpeg.org/) for video transcoding
|
- [FFmpeg](https://ffmpeg.org/) for video transcoding
|
||||||
- [ElasticSearch](https://www.elastic.co/elasticsearch/) for full-text search
|
- Full text search (choost one of the following)
|
||||||
- OpenSearch/Sonic are not supported as of right now
|
- 🦔 [Sonic](https://crates.io/crates/sonic-server) (highly recommended!)
|
||||||
|
- [ElasticSearch](https://www.elastic.co/elasticsearch/)
|
||||||
- Management (choose one of the following)
|
- Management (choose one of the following)
|
||||||
- 🛰️ [pm2](https://pm2.io/)
|
- 🛰️ [pm2](https://pm2.io/)
|
||||||
- 🐳 [Docker](https://docker.com)
|
- 🐳 [Docker](https://docker.com)
|
||||||
|
@ -119,6 +123,17 @@ Assuming you set up PostgreSQL correctly, all you have to run is:
|
||||||
psql postgres -c "create database calckey with encoding = 'UTF8';"
|
psql postgres -c "create database calckey with encoding = 'UTF8';"
|
||||||
```
|
```
|
||||||
|
|
||||||
|
In Calckey's directory, fill out the `db` section of `.config/default.yml` with the correct information, where the `db` key is `calckey`.
|
||||||
|
|
||||||
|
## 🦔 Set up search
|
||||||
|
|
||||||
|
Follow sonic's [installation guide](https://github.com/valeriansaliou/sonic#installation)
|
||||||
|
|
||||||
|
If you use IPv4: in Sonic's directory, edit the `config.cfg` file to change `inet` to `"0.0.0.0:1491"`.
|
||||||
|
|
||||||
|
In Calckey's directory, fill out the `sonic` section of `.config/default.yml` with the correct information.
|
||||||
|
|
||||||
|
|
||||||
## 💅 Customize
|
## 💅 Customize
|
||||||
|
|
||||||
- To add custom CSS for all users, edit `./custom/assets/instance.css`.
|
- To add custom CSS for all users, edit `./custom/assets/instance.css`.
|
||||||
|
@ -155,7 +170,8 @@ For migrating from Misskey v13, Misskey v12, and Foundkey, read [this document](
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
# git pull
|
# git pull
|
||||||
NODE_ENV=production pnpm install && pnpm run build && pnpm run migrate
|
pnpm install
|
||||||
|
NODE_ENV=production pnpm run build && pnpm run migrate
|
||||||
pm2 start "NODE_ENV=production pnpm run start" --name Calckey
|
pm2 start "NODE_ENV=production pnpm run start" --name Calckey
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
|
@ -835,7 +835,7 @@ muteThread: "Mute thread"
|
||||||
unmuteThread: "Unmute thread"
|
unmuteThread: "Unmute thread"
|
||||||
ffVisibility: "Follows/Followers Visibility"
|
ffVisibility: "Follows/Followers Visibility"
|
||||||
ffVisibilityDescription: "Allows you to configure who can see who you follow and who follows you."
|
ffVisibilityDescription: "Allows you to configure who can see who you follow and who follows you."
|
||||||
continueThread: "View thread continuation"
|
continueThread: "Continue thread"
|
||||||
deleteAccountConfirm: "This will irreversibly delete your account. Proceed?"
|
deleteAccountConfirm: "This will irreversibly delete your account. Proceed?"
|
||||||
incorrectPassword: "Incorrect password."
|
incorrectPassword: "Incorrect password."
|
||||||
voteConfirm: "Confirm your vote for \"{choice}\"?"
|
voteConfirm: "Confirm your vote for \"{choice}\"?"
|
||||||
|
@ -935,6 +935,7 @@ moveFromLabel: "Account you're moving from:"
|
||||||
moveFromDescription: "This will set an alias of your old account so that you can move from that account to this current one. Do this BEFORE moving from your older account. Please enter the tag of the account formatted like @person@instance.com"
|
moveFromDescription: "This will set an alias of your old account so that you can move from that account to this current one. Do this BEFORE moving from your older account. Please enter the tag of the account formatted like @person@instance.com"
|
||||||
migrationConfirm: "Are you absolutely sure you want to migrate your acccount to {account}? Once you do this, you won't be able to reverse it, and you won't be able to use your account normally again.\nAlso, please ensure that you've set this current account as the account you're moving from."
|
migrationConfirm: "Are you absolutely sure you want to migrate your acccount to {account}? Once you do this, you won't be able to reverse it, and you won't be able to use your account normally again.\nAlso, please ensure that you've set this current account as the account you're moving from."
|
||||||
defaultReaction: "Default emoji reaction for outgoing and incoming posts"
|
defaultReaction: "Default emoji reaction for outgoing and incoming posts"
|
||||||
|
license: "License"
|
||||||
|
|
||||||
_sensitiveMediaDetection:
|
_sensitiveMediaDetection:
|
||||||
description: "Reduces the effort of server moderation through automatically recognizing NSFW media via Machine Learning. This will slightly increase the load on the server."
|
description: "Reduces the effort of server moderation through automatically recognizing NSFW media via Machine Learning. This will slightly increase the load on the server."
|
||||||
|
@ -1400,7 +1401,7 @@ _profile:
|
||||||
metadataContent: "Content"
|
metadataContent: "Content"
|
||||||
changeAvatar: "Change avatar"
|
changeAvatar: "Change avatar"
|
||||||
changeBanner: "Change banner"
|
changeBanner: "Change banner"
|
||||||
locationDescription: "If entered properly, this will display your local time to other users."
|
locationDescription: "If you enter your city, it will display your local time to other users."
|
||||||
_exportOrImport:
|
_exportOrImport:
|
||||||
allNotes: "All posts"
|
allNotes: "All posts"
|
||||||
followingList: "Followed users"
|
followingList: "Followed users"
|
||||||
|
|
|
@ -935,6 +935,7 @@ moveFromLabel: "引っ越し元のアカウント:"
|
||||||
moveFromDescription: "別のアカウントからこのアカウントにフォロワーを引き継いで引っ越したい場合、ここでエイリアスを作成しておく必要があります。必ず引っ越しを実行する前に作成してください!引っ越し元のアカウントをこのように入力してください:@person@instance.com"
|
moveFromDescription: "別のアカウントからこのアカウントにフォロワーを引き継いで引っ越したい場合、ここでエイリアスを作成しておく必要があります。必ず引っ越しを実行する前に作成してください!引っ越し元のアカウントをこのように入力してください:@person@instance.com"
|
||||||
migrationConfirm: "本当にこのアカウントを {account} に引っ越しますか?一度引っ越しを行うと取り消せず、二度とこのアカウントを元の状態で使用することはできません。\nまた、引っ越し先のアカウントでエイリアスを作成したことを確認してください。"
|
migrationConfirm: "本当にこのアカウントを {account} に引っ越しますか?一度引っ越しを行うと取り消せず、二度とこのアカウントを元の状態で使用することはできません。\nまた、引っ越し先のアカウントでエイリアスを作成したことを確認してください。"
|
||||||
defaultReaction: "リモートとローカルの投稿に対するデフォルトの絵文字リアクション"
|
defaultReaction: "リモートとローカルの投稿に対するデフォルトの絵文字リアクション"
|
||||||
|
license: "ライセンス"
|
||||||
|
|
||||||
_sensitiveMediaDetection:
|
_sensitiveMediaDetection:
|
||||||
description: "機械学習を使って自動でセンシティブなメディアを検出し、モデレーションに役立てることができます。サーバーの負荷が少し増えます。"
|
description: "機械学習を使って自動でセンシティブなメディアを検出し、モデレーションに役立てることができます。サーバーの負荷が少し増えます。"
|
||||||
|
|
|
@ -1,12 +1,12 @@
|
||||||
{
|
{
|
||||||
"name": "calckey",
|
"name": "calckey",
|
||||||
"version": "13.2.0-beta",
|
"version": "13.2.0-beta2",
|
||||||
"codename": "aqua",
|
"codename": "aqua",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://codeberg.org/calckey/calckey.git"
|
"url": "https://codeberg.org/calckey/calckey.git"
|
||||||
},
|
},
|
||||||
"packageManager": "pnpm@7.27.1",
|
"packageManager": "pnpm@7.29.3",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"rebuild": "pnpm run clean && pnpm -r run build && pnpm run gulp",
|
"rebuild": "pnpm run clean && pnpm -r run build && pnpm run gulp",
|
||||||
|
|
|
@ -0,0 +1,11 @@
|
||||||
|
export class addPropsForCustomEmoji1678945242650 {
|
||||||
|
name = 'addPropsForCustomEmoji1678945242650'
|
||||||
|
|
||||||
|
async up(queryRunner) {
|
||||||
|
await queryRunner.query(`ALTER TABLE "emoji" ADD "license" character varying(1024)`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async down(queryRunner) {
|
||||||
|
await queryRunner.query(`ALTER TABLE "emoji" DROP COLUMN "license"`);
|
||||||
|
}
|
||||||
|
}
|
13
packages/backend/migration/1679269929000-fix-repo.js
Normal file
13
packages/backend/migration/1679269929000-fix-repo.js
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
export class FixRepo1679269929000 {
|
||||||
|
name = 'FixRepo1679269929000'
|
||||||
|
|
||||||
|
async up(queryRunner) {
|
||||||
|
await queryRunner.query(`UPDATE meta SET "repositoryUrl" = 'https://codeberg.org/calckey/calckey'`);
|
||||||
|
await queryRunner.query(`UPDATE meta SET "feedbackUrl" = 'https://codeberg.org/calckey/calckey/issues'`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async down(queryRunner) {
|
||||||
|
await queryRunner.query(`UPDATE meta SET "repositoryUrl" = 'https://codeberg.org/calckey/calckey'`);
|
||||||
|
await queryRunner.query(`UPDATE meta SET "feedbackUrl" = 'https://codeberg.org/calckey/calckey/issues'`);
|
||||||
|
}
|
||||||
|
}
|
|
@ -81,7 +81,7 @@
|
||||||
"koa-send": "5.0.1",
|
"koa-send": "5.0.1",
|
||||||
"koa-slow": "2.1.0",
|
"koa-slow": "2.1.0",
|
||||||
"koa-views": "7.0.2",
|
"koa-views": "7.0.2",
|
||||||
"@calckey/megalodon": "5.1.2",
|
"@calckey/megalodon": "5.1.21",
|
||||||
"mfm-js": "0.23.2",
|
"mfm-js": "0.23.2",
|
||||||
"mime-types": "2.1.35",
|
"mime-types": "2.1.35",
|
||||||
"multer": "1.4.4-lts.1",
|
"multer": "1.4.4-lts.1",
|
||||||
|
@ -112,6 +112,7 @@
|
||||||
"seedrandom": "^3.0.5",
|
"seedrandom": "^3.0.5",
|
||||||
"semver": "7.3.8",
|
"semver": "7.3.8",
|
||||||
"sharp": "0.31.3",
|
"sharp": "0.31.3",
|
||||||
|
"sonic-channel": "^1.3.1",
|
||||||
"speakeasy": "2.0.0",
|
"speakeasy": "2.0.0",
|
||||||
"stringz": "2.1.0",
|
"stringz": "2.1.0",
|
||||||
"summaly": "2.7.0",
|
"summaly": "2.7.0",
|
||||||
|
|
|
@ -32,6 +32,13 @@ export type Source = {
|
||||||
pass?: string;
|
pass?: string;
|
||||||
index?: string;
|
index?: string;
|
||||||
};
|
};
|
||||||
|
sonic: {
|
||||||
|
host: string;
|
||||||
|
port: number;
|
||||||
|
auth?: string;
|
||||||
|
collection?: string;
|
||||||
|
bucket?: string;
|
||||||
|
};
|
||||||
|
|
||||||
proxy?: string;
|
proxy?: string;
|
||||||
proxySmtp?: string;
|
proxySmtp?: string;
|
||||||
|
|
51
packages/backend/src/db/sonic.ts
Normal file
51
packages/backend/src/db/sonic.ts
Normal file
|
@ -0,0 +1,51 @@
|
||||||
|
import * as SonicChannel from "sonic-channel";
|
||||||
|
import { dbLogger } from "./logger.js";
|
||||||
|
|
||||||
|
import config from "@/config/index.js";
|
||||||
|
|
||||||
|
const logger = dbLogger.createSubLogger("sonic", "gray", false);
|
||||||
|
|
||||||
|
logger.info("Connecting to Sonic");
|
||||||
|
|
||||||
|
const handlers = (type: string): SonicChannel.Handlers => (
|
||||||
|
{
|
||||||
|
connected: () => {
|
||||||
|
logger.succ(`Connected to Sonic ${type}`);
|
||||||
|
},
|
||||||
|
disconnected: (error) => {
|
||||||
|
logger.warn(`Disconnected from Sonic ${type}, error: ${error}`);
|
||||||
|
},
|
||||||
|
error: (error) => {
|
||||||
|
logger.warn(`Sonic ${type} error: ${error}`);
|
||||||
|
},
|
||||||
|
retrying: () => {
|
||||||
|
logger.info(`Sonic ${type} retrying`);
|
||||||
|
},
|
||||||
|
timeout: () => {
|
||||||
|
logger.warn(`Sonic ${type} timeout`);
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
const hasConfig =
|
||||||
|
config.sonic
|
||||||
|
&& ( config.sonic.host
|
||||||
|
|| config.sonic.port
|
||||||
|
|| config.sonic.auth
|
||||||
|
)
|
||||||
|
|
||||||
|
const host = hasConfig ? config.sonic.host ?? "localhost" : "";
|
||||||
|
const port = hasConfig ? config.sonic.port ?? 1491 : 0;
|
||||||
|
const auth = hasConfig ? config.sonic.auth ?? "SecretPassword" : "";
|
||||||
|
const collection = hasConfig ? config.sonic.collection ?? "main" : "";
|
||||||
|
const bucket = hasConfig ? config.sonic.bucket ?? "default" : "";
|
||||||
|
|
||||||
|
export default hasConfig
|
||||||
|
? {
|
||||||
|
search: new SonicChannel.Search({host, port, auth}).connect(handlers("search")),
|
||||||
|
ingest: new SonicChannel.Ingest({host, port, auth}).connect(handlers("ingest")),
|
||||||
|
|
||||||
|
collection,
|
||||||
|
bucket,
|
||||||
|
}
|
||||||
|
: null;
|
|
@ -55,4 +55,9 @@ export class Emoji {
|
||||||
array: true, length: 128, default: '{}',
|
array: true, length: 128, default: '{}',
|
||||||
})
|
})
|
||||||
public aliases: string[];
|
public aliases: string[];
|
||||||
|
|
||||||
|
@Column('varchar', {
|
||||||
|
length: 1024, nullable: true,
|
||||||
|
})
|
||||||
|
public license: string | null;
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,6 +15,7 @@ export const EmojiRepository = db.getRepository(Emoji).extend({
|
||||||
host: emoji.host,
|
host: emoji.host,
|
||||||
// || emoji.originalUrl してるのは後方互換性のため
|
// || emoji.originalUrl してるのは後方互換性のため
|
||||||
url: emoji.publicUrl || emoji.originalUrl,
|
url: emoji.publicUrl || emoji.originalUrl,
|
||||||
|
license: emoji.license,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
|
@ -12,7 +12,6 @@ import {
|
||||||
Channels,
|
Channels,
|
||||||
} from "../index.js";
|
} from "../index.js";
|
||||||
import type { Packed } from "@/misc/schema.js";
|
import type { Packed } from "@/misc/schema.js";
|
||||||
import { nyaize } from "@/misc/nyaize.js";
|
|
||||||
import { awaitAll } from "@/prelude/await-all.js";
|
import { awaitAll } from "@/prelude/await-all.js";
|
||||||
import {
|
import {
|
||||||
convertLegacyReaction,
|
convertLegacyReaction,
|
||||||
|
@ -263,7 +262,7 @@ export const NoteRepository = db.getRepository(Note).extend({
|
||||||
: {}),
|
: {}),
|
||||||
});
|
});
|
||||||
|
|
||||||
if (packed.user.isCat && packed.text) {
|
/* if (packed.user.isCat && packed.text) {
|
||||||
const tokens = packed.text ? mfm.parse(packed.text) : [];
|
const tokens = packed.text ? mfm.parse(packed.text) : [];
|
||||||
mfm.inspect(tokens, (node) => {
|
mfm.inspect(tokens, (node) => {
|
||||||
if (node.type === "text") {
|
if (node.type === "text") {
|
||||||
|
@ -272,7 +271,7 @@ export const NoteRepository = db.getRepository(Note).extend({
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
packed.text = mfm.toString(tokens);
|
packed.text = mfm.toString(tokens);
|
||||||
}
|
} */
|
||||||
|
|
||||||
return packed;
|
return packed;
|
||||||
},
|
},
|
||||||
|
|
|
@ -40,5 +40,10 @@ export const packedEmojiSchema = {
|
||||||
optional: false,
|
optional: false,
|
||||||
nullable: false,
|
nullable: false,
|
||||||
},
|
},
|
||||||
|
license: {
|
||||||
|
type: "string",
|
||||||
|
optional: false,
|
||||||
|
nullable: true,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
} as const;
|
} as const;
|
||||||
|
|
|
@ -13,6 +13,7 @@ import processDb from "./processors/db/index.js";
|
||||||
import processObjectStorage from "./processors/object-storage/index.js";
|
import processObjectStorage from "./processors/object-storage/index.js";
|
||||||
import processSystemQueue from "./processors/system/index.js";
|
import processSystemQueue from "./processors/system/index.js";
|
||||||
import processWebhookDeliver from "./processors/webhook-deliver.js";
|
import processWebhookDeliver from "./processors/webhook-deliver.js";
|
||||||
|
import processBackground from "./processors/background/index.js";
|
||||||
import { endedPollNotification } from "./processors/ended-poll-notification.js";
|
import { endedPollNotification } from "./processors/ended-poll-notification.js";
|
||||||
import { queueLogger } from "./logger.js";
|
import { queueLogger } from "./logger.js";
|
||||||
import { getJobInfo } from "./get-job-info.js";
|
import { getJobInfo } from "./get-job-info.js";
|
||||||
|
@ -24,6 +25,7 @@ import {
|
||||||
objectStorageQueue,
|
objectStorageQueue,
|
||||||
endedPollNotificationQueue,
|
endedPollNotificationQueue,
|
||||||
webhookDeliverQueue,
|
webhookDeliverQueue,
|
||||||
|
backgroundQueue,
|
||||||
} from "./queues.js";
|
} from "./queues.js";
|
||||||
import type { ThinUser } from "./types.js";
|
import type { ThinUser } from "./types.js";
|
||||||
|
|
||||||
|
@ -418,6 +420,17 @@ export function createCleanRemoteFilesJob() {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function createIndexAllNotesJob(data = {}) {
|
||||||
|
return backgroundQueue.add(
|
||||||
|
"indexAllNotes",
|
||||||
|
data,
|
||||||
|
{
|
||||||
|
removeOnComplete: true,
|
||||||
|
removeOnFail: true,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export function webhookDeliver(
|
export function webhookDeliver(
|
||||||
webhook: Webhook,
|
webhook: Webhook,
|
||||||
type: typeof webhookEventTypes[number],
|
type: typeof webhookEventTypes[number],
|
||||||
|
@ -454,6 +467,7 @@ export default function () {
|
||||||
webhookDeliverQueue.process(64, processWebhookDeliver);
|
webhookDeliverQueue.process(64, processWebhookDeliver);
|
||||||
processDb(dbQueue);
|
processDb(dbQueue);
|
||||||
processObjectStorage(objectStorageQueue);
|
processObjectStorage(objectStorageQueue);
|
||||||
|
processBackground(backgroundQueue);
|
||||||
|
|
||||||
systemQueue.add(
|
systemQueue.add(
|
||||||
"tickCharts",
|
"tickCharts",
|
||||||
|
|
|
@ -0,0 +1,76 @@
|
||||||
|
import type Bull from "bull";
|
||||||
|
|
||||||
|
import { queueLogger } from "../../logger.js";
|
||||||
|
import { Notes } from "@/models/index.js";
|
||||||
|
import { MoreThan } from "typeorm";
|
||||||
|
import { index } from "@/services/note/create.js"
|
||||||
|
import { Note } from "@/models/entities/note.js";
|
||||||
|
|
||||||
|
const logger = queueLogger.createSubLogger("index-all-notes");
|
||||||
|
|
||||||
|
export default async function indexAllNotes(
|
||||||
|
job: Bull.Job<Record<string, unknown>>,
|
||||||
|
done: ()=>void,
|
||||||
|
): Promise<void> {
|
||||||
|
logger.info("Indexing all notes...");
|
||||||
|
|
||||||
|
let cursor: string|null = job.data.cursor as string ?? null;
|
||||||
|
let indexedCount: number = job.data.indexedCount as number ?? 0;
|
||||||
|
let total: number = job.data.total as number ?? 0;
|
||||||
|
|
||||||
|
let running = true;
|
||||||
|
const take = 50000;
|
||||||
|
const batch = 100;
|
||||||
|
while (running) {
|
||||||
|
logger.info(`Querying for ${take} notes ${indexedCount}/${total ? total : '?'} at ${cursor}`);
|
||||||
|
|
||||||
|
let notes: Note[] = [];
|
||||||
|
try {
|
||||||
|
notes = await Notes.find({
|
||||||
|
where: {
|
||||||
|
...(cursor ? { id: MoreThan(cursor) } : {}),
|
||||||
|
},
|
||||||
|
take: take,
|
||||||
|
order: {
|
||||||
|
id: 1,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
logger.error(`Failed to query notes ${e}`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (notes.length === 0) {
|
||||||
|
job.progress(100);
|
||||||
|
running = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const count = await Notes.count();
|
||||||
|
total = count;
|
||||||
|
job.update({ indexedCount, cursor, total })
|
||||||
|
} catch (e) {
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i = 0; i < notes.length; i += batch) {
|
||||||
|
const chunk = notes.slice(i, i + batch);
|
||||||
|
await Promise.all(chunk.map(note => index(note)));
|
||||||
|
|
||||||
|
indexedCount += chunk.length;
|
||||||
|
const pct = (indexedCount / total)*100;
|
||||||
|
job.update({ indexedCount, cursor, total })
|
||||||
|
job.progress(+(pct.toFixed(1)));
|
||||||
|
logger.info(`Indexed notes ${indexedCount}/${total ? total : '?'}`);
|
||||||
|
}
|
||||||
|
cursor = notes[notes.length - 1].id;
|
||||||
|
job.update({ indexedCount, cursor, total })
|
||||||
|
|
||||||
|
if (notes.length < take) {
|
||||||
|
running = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
done();
|
||||||
|
logger.info("All notes have been indexed.");
|
||||||
|
}
|
15
packages/backend/src/queue/processors/background/index.ts
Normal file
15
packages/backend/src/queue/processors/background/index.ts
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
import type Bull from "bull";
|
||||||
|
import indexAllNotes from "./index-all-notes.js";
|
||||||
|
|
||||||
|
const jobs = {
|
||||||
|
indexAllNotes,
|
||||||
|
} as Record<
|
||||||
|
string,
|
||||||
|
Bull.ProcessCallbackFunction<Record<string, unknown>>
|
||||||
|
>;
|
||||||
|
|
||||||
|
export default function (q: Bull.Queue) {
|
||||||
|
for (const [k, v] of Object.entries(jobs)) {
|
||||||
|
q.process(k, 16, v);
|
||||||
|
}
|
||||||
|
}
|
|
@ -75,6 +75,7 @@ export async function importCustomEmojis(
|
||||||
originalUrl: driveFile.url,
|
originalUrl: driveFile.url,
|
||||||
publicUrl: driveFile.webpublicUrl ?? driveFile.url,
|
publicUrl: driveFile.webpublicUrl ?? driveFile.url,
|
||||||
type: driveFile.webpublicType ?? driveFile.type,
|
type: driveFile.webpublicType ?? driveFile.type,
|
||||||
|
license: emojiInfo.license,
|
||||||
}).then((x) => Emojis.findOneByOrFail(x.identifiers[0]));
|
}).then((x) => Emojis.findOneByOrFail(x.identifiers[0]));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -27,6 +27,7 @@ export const webhookDeliverQueue = initializeQueue<WebhookDeliverJobData>(
|
||||||
"webhookDeliver",
|
"webhookDeliver",
|
||||||
64,
|
64,
|
||||||
);
|
);
|
||||||
|
export const backgroundQueue = initializeQueue<Record<string, unknown>>("bg");
|
||||||
|
|
||||||
export const queues = [
|
export const queues = [
|
||||||
systemQueue,
|
systemQueue,
|
||||||
|
@ -36,4 +37,5 @@ export const queues = [
|
||||||
dbQueue,
|
dbQueue,
|
||||||
objectStorageQueue,
|
objectStorageQueue,
|
||||||
webhookDeliverQueue,
|
webhookDeliverQueue,
|
||||||
|
backgroundQueue,
|
||||||
];
|
];
|
||||||
|
|
|
@ -198,9 +198,36 @@ export async function createPerson(
|
||||||
const url = getOneApHrefNullable(person.url);
|
const url = getOneApHrefNullable(person.url);
|
||||||
|
|
||||||
if (url && !url.startsWith("https://")) {
|
if (url && !url.startsWith("https://")) {
|
||||||
throw new Error(`unexpected shcema of person url: ${url}`);
|
throw new Error(`unexpected schema of person url: ${url}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let followersCount: number | undefined;
|
||||||
|
|
||||||
|
if (typeof person.followers === "string") {
|
||||||
|
try {
|
||||||
|
let data = await fetch(person.followers, { headers: { "Accept": "application/json" } });
|
||||||
|
let json_data = JSON.parse(await data.text());
|
||||||
|
|
||||||
|
followersCount = json_data.totalItems;
|
||||||
|
} catch {
|
||||||
|
followersCount = undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let followingCount: number | undefined;
|
||||||
|
|
||||||
|
if (typeof person.following === "string") {
|
||||||
|
try {
|
||||||
|
let data = await fetch(person.following, { headers: { "Accept": "application/json" } });
|
||||||
|
let json_data = JSON.parse(await data.text());
|
||||||
|
|
||||||
|
followingCount = json_data.totalItems;
|
||||||
|
} catch (e) {
|
||||||
|
followingCount = undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
// Create user
|
// Create user
|
||||||
let user: IRemoteUser;
|
let user: IRemoteUser;
|
||||||
try {
|
try {
|
||||||
|
@ -228,6 +255,16 @@ export async function createPerson(
|
||||||
followersUri: person.followers
|
followersUri: person.followers
|
||||||
? getApId(person.followers)
|
? getApId(person.followers)
|
||||||
: undefined,
|
: undefined,
|
||||||
|
followersCount: followersCount !== undefined
|
||||||
|
? followersCount
|
||||||
|
: person.followers && typeof person.followers !== "string" && isCollectionOrOrderedCollection(person.followers)
|
||||||
|
? person.followers.totalItems
|
||||||
|
: undefined,
|
||||||
|
followingCount: followingCount !== undefined
|
||||||
|
? followingCount
|
||||||
|
: person.following && typeof person.following !== "string" && isCollectionOrOrderedCollection(person.following)
|
||||||
|
? person.following.totalItems
|
||||||
|
: undefined,
|
||||||
featured: person.featured ? getApId(person.featured) : undefined,
|
featured: person.featured ? getApId(person.featured) : undefined,
|
||||||
uri: person.id,
|
uri: person.id,
|
||||||
tags,
|
tags,
|
||||||
|
@ -396,7 +433,34 @@ export async function updatePerson(
|
||||||
const url = getOneApHrefNullable(person.url);
|
const url = getOneApHrefNullable(person.url);
|
||||||
|
|
||||||
if (url && !url.startsWith("https://")) {
|
if (url && !url.startsWith("https://")) {
|
||||||
throw new Error(`unexpected shcema of person url: ${url}`);
|
throw new Error(`unexpected schema of person url: ${url}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
let followersCount: number | undefined;
|
||||||
|
|
||||||
|
if (typeof person.followers === "string") {
|
||||||
|
try {
|
||||||
|
let data = await fetch(person.followers, { headers: { "Accept": "application/json" } } );
|
||||||
|
let json_data = JSON.parse(await data.text());
|
||||||
|
|
||||||
|
followersCount = json_data.totalItems;
|
||||||
|
} catch {
|
||||||
|
followersCount = undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
let followingCount: number | undefined;
|
||||||
|
|
||||||
|
if (typeof person.following === "string") {
|
||||||
|
try {
|
||||||
|
let data = await fetch(person.following, { headers: { "Accept": "application/json" } } );
|
||||||
|
let json_data = JSON.parse(await data.text());
|
||||||
|
|
||||||
|
followingCount = json_data.totalItems;
|
||||||
|
} catch {
|
||||||
|
followingCount = undefined;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const updates = {
|
const updates = {
|
||||||
|
@ -406,6 +470,16 @@ export async function updatePerson(
|
||||||
person.sharedInbox ||
|
person.sharedInbox ||
|
||||||
(person.endpoints ? person.endpoints.sharedInbox : undefined),
|
(person.endpoints ? person.endpoints.sharedInbox : undefined),
|
||||||
followersUri: person.followers ? getApId(person.followers) : undefined,
|
followersUri: person.followers ? getApId(person.followers) : undefined,
|
||||||
|
followersCount: followersCount !== undefined
|
||||||
|
? followersCount
|
||||||
|
: person.followers && typeof person.followers !== "string" && isCollectionOrOrderedCollection(person.followers)
|
||||||
|
? person.followers.totalItems
|
||||||
|
: undefined,
|
||||||
|
followingCount: followingCount !== undefined
|
||||||
|
? followingCount
|
||||||
|
: person.following && typeof person.following !== "string" && isCollectionOrOrderedCollection(person.following)
|
||||||
|
? person.following.totalItems
|
||||||
|
: undefined,
|
||||||
featured: person.featured,
|
featured: person.featured,
|
||||||
emojis: emojiNames,
|
emojis: emojiNames,
|
||||||
name: truncate(person.name, nameLength),
|
name: truncate(person.name, nameLength),
|
||||||
|
|
|
@ -29,6 +29,7 @@ import * as ep___admin_emoji_list from "./endpoints/admin/emoji/list.js";
|
||||||
import * as ep___admin_emoji_removeAliasesBulk from "./endpoints/admin/emoji/remove-aliases-bulk.js";
|
import * as ep___admin_emoji_removeAliasesBulk from "./endpoints/admin/emoji/remove-aliases-bulk.js";
|
||||||
import * as ep___admin_emoji_setAliasesBulk from "./endpoints/admin/emoji/set-aliases-bulk.js";
|
import * as ep___admin_emoji_setAliasesBulk from "./endpoints/admin/emoji/set-aliases-bulk.js";
|
||||||
import * as ep___admin_emoji_setCategoryBulk from "./endpoints/admin/emoji/set-category-bulk.js";
|
import * as ep___admin_emoji_setCategoryBulk from "./endpoints/admin/emoji/set-category-bulk.js";
|
||||||
|
import * as ep___admin_emoji_setLicenseBulk from "./endpoints/admin/emoji/set-license-bulk.js";
|
||||||
import * as ep___admin_emoji_update from "./endpoints/admin/emoji/update.js";
|
import * as ep___admin_emoji_update from "./endpoints/admin/emoji/update.js";
|
||||||
import * as ep___admin_federation_deleteAllFiles from "./endpoints/admin/federation/delete-all-files.js";
|
import * as ep___admin_federation_deleteAllFiles from "./endpoints/admin/federation/delete-all-files.js";
|
||||||
import * as ep___admin_federation_refreshRemoteInstanceMetadata from "./endpoints/admin/federation/refresh-remote-instance-metadata.js";
|
import * as ep___admin_federation_refreshRemoteInstanceMetadata from "./endpoints/admin/federation/refresh-remote-instance-metadata.js";
|
||||||
|
@ -50,6 +51,7 @@ import * as ep___admin_relays_list from "./endpoints/admin/relays/list.js";
|
||||||
import * as ep___admin_relays_remove from "./endpoints/admin/relays/remove.js";
|
import * as ep___admin_relays_remove from "./endpoints/admin/relays/remove.js";
|
||||||
import * as ep___admin_resetPassword from "./endpoints/admin/reset-password.js";
|
import * as ep___admin_resetPassword from "./endpoints/admin/reset-password.js";
|
||||||
import * as ep___admin_resolveAbuseUserReport from "./endpoints/admin/resolve-abuse-user-report.js";
|
import * as ep___admin_resolveAbuseUserReport from "./endpoints/admin/resolve-abuse-user-report.js";
|
||||||
|
import * as ep___admin_search_indexAll from "./endpoints/admin/search/index-all.js";
|
||||||
import * as ep___admin_sendEmail from "./endpoints/admin/send-email.js";
|
import * as ep___admin_sendEmail from "./endpoints/admin/send-email.js";
|
||||||
import * as ep___admin_serverInfo from "./endpoints/admin/server-info.js";
|
import * as ep___admin_serverInfo from "./endpoints/admin/server-info.js";
|
||||||
import * as ep___admin_showModerationLogs from "./endpoints/admin/show-moderation-logs.js";
|
import * as ep___admin_showModerationLogs from "./endpoints/admin/show-moderation-logs.js";
|
||||||
|
@ -131,6 +133,7 @@ import * as ep___drive_folders_show from "./endpoints/drive/folders/show.js";
|
||||||
import * as ep___drive_folders_update from "./endpoints/drive/folders/update.js";
|
import * as ep___drive_folders_update from "./endpoints/drive/folders/update.js";
|
||||||
import * as ep___drive_stream from "./endpoints/drive/stream.js";
|
import * as ep___drive_stream from "./endpoints/drive/stream.js";
|
||||||
import * as ep___emailAddress_available from "./endpoints/email-address/available.js";
|
import * as ep___emailAddress_available from "./endpoints/email-address/available.js";
|
||||||
|
import * as ep___emoji from "./endpoints/emoji.js";
|
||||||
import * as ep___endpoint from "./endpoints/endpoint.js";
|
import * as ep___endpoint from "./endpoints/endpoint.js";
|
||||||
import * as ep___endpoints from "./endpoints/endpoints.js";
|
import * as ep___endpoints from "./endpoints/endpoints.js";
|
||||||
import * as ep___exportCustomEmojis from "./endpoints/export-custom-emojis.js";
|
import * as ep___exportCustomEmojis from "./endpoints/export-custom-emojis.js";
|
||||||
|
@ -363,6 +366,7 @@ const eps = [
|
||||||
["admin/emoji/remove-aliases-bulk", ep___admin_emoji_removeAliasesBulk],
|
["admin/emoji/remove-aliases-bulk", ep___admin_emoji_removeAliasesBulk],
|
||||||
["admin/emoji/set-aliases-bulk", ep___admin_emoji_setAliasesBulk],
|
["admin/emoji/set-aliases-bulk", ep___admin_emoji_setAliasesBulk],
|
||||||
["admin/emoji/set-category-bulk", ep___admin_emoji_setCategoryBulk],
|
["admin/emoji/set-category-bulk", ep___admin_emoji_setCategoryBulk],
|
||||||
|
["admin/emoji/set-license-bulk", ep___admin_emoji_setLicenseBulk],
|
||||||
["admin/emoji/update", ep___admin_emoji_update],
|
["admin/emoji/update", ep___admin_emoji_update],
|
||||||
["admin/federation/delete-all-files", ep___admin_federation_deleteAllFiles],
|
["admin/federation/delete-all-files", ep___admin_federation_deleteAllFiles],
|
||||||
[
|
[
|
||||||
|
@ -390,6 +394,7 @@ const eps = [
|
||||||
["admin/relays/remove", ep___admin_relays_remove],
|
["admin/relays/remove", ep___admin_relays_remove],
|
||||||
["admin/reset-password", ep___admin_resetPassword],
|
["admin/reset-password", ep___admin_resetPassword],
|
||||||
["admin/resolve-abuse-user-report", ep___admin_resolveAbuseUserReport],
|
["admin/resolve-abuse-user-report", ep___admin_resolveAbuseUserReport],
|
||||||
|
["admin/search/index-all", ep___admin_search_indexAll],
|
||||||
["admin/send-email", ep___admin_sendEmail],
|
["admin/send-email", ep___admin_sendEmail],
|
||||||
["admin/server-info", ep___admin_serverInfo],
|
["admin/server-info", ep___admin_serverInfo],
|
||||||
["admin/show-moderation-logs", ep___admin_showModerationLogs],
|
["admin/show-moderation-logs", ep___admin_showModerationLogs],
|
||||||
|
@ -471,6 +476,7 @@ const eps = [
|
||||||
["drive/folders/update", ep___drive_folders_update],
|
["drive/folders/update", ep___drive_folders_update],
|
||||||
["drive/stream", ep___drive_stream],
|
["drive/stream", ep___drive_stream],
|
||||||
["email-address/available", ep___emailAddress_available],
|
["email-address/available", ep___emailAddress_available],
|
||||||
|
["emoji", ep___emoji],
|
||||||
["endpoint", ep___endpoint],
|
["endpoint", ep___endpoint],
|
||||||
["endpoints", ep___endpoints],
|
["endpoints", ep___endpoints],
|
||||||
["export-custom-emojis", ep___exportCustomEmojis],
|
["export-custom-emojis", ep___exportCustomEmojis],
|
||||||
|
|
|
@ -49,6 +49,7 @@ export default define(meta, paramDef, async (ps, me) => {
|
||||||
originalUrl: file.url,
|
originalUrl: file.url,
|
||||||
publicUrl: file.webpublicUrl ?? file.url,
|
publicUrl: file.webpublicUrl ?? file.url,
|
||||||
type: file.webpublicType ?? file.type,
|
type: file.webpublicType ?? file.type,
|
||||||
|
license: null,
|
||||||
}).then((x) => Emojis.findOneByOrFail(x.identifiers[0]));
|
}).then((x) => Emojis.findOneByOrFail(x.identifiers[0]));
|
||||||
|
|
||||||
await db.queryResultCache!.remove(["meta_emojis"]);
|
await db.queryResultCache!.remove(["meta_emojis"]);
|
||||||
|
|
|
@ -73,6 +73,7 @@ export default define(meta, paramDef, async (ps, me) => {
|
||||||
originalUrl: driveFile.url,
|
originalUrl: driveFile.url,
|
||||||
publicUrl: driveFile.webpublicUrl ?? driveFile.url,
|
publicUrl: driveFile.webpublicUrl ?? driveFile.url,
|
||||||
type: driveFile.webpublicType ?? driveFile.type,
|
type: driveFile.webpublicType ?? driveFile.type,
|
||||||
|
license: emoji.license,
|
||||||
}).then((x) => Emojis.findOneByOrFail(x.identifiers[0]));
|
}).then((x) => Emojis.findOneByOrFail(x.identifiers[0]));
|
||||||
|
|
||||||
await db.queryResultCache!.remove(["meta_emojis"]);
|
await db.queryResultCache!.remove(["meta_emojis"]);
|
||||||
|
|
|
@ -55,6 +55,11 @@ export const meta = {
|
||||||
optional: false,
|
optional: false,
|
||||||
nullable: false,
|
nullable: false,
|
||||||
},
|
},
|
||||||
|
license: {
|
||||||
|
type: "string",
|
||||||
|
optional: false,
|
||||||
|
nullable: true,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
|
@ -55,6 +55,11 @@ export const meta = {
|
||||||
optional: false,
|
optional: false,
|
||||||
nullable: false,
|
nullable: false,
|
||||||
},
|
},
|
||||||
|
license: {
|
||||||
|
type: "string",
|
||||||
|
optional: false,
|
||||||
|
nullable: true,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
|
@ -0,0 +1,45 @@
|
||||||
|
import define from "../../../define.js";
|
||||||
|
import { Emojis } from "@/models/index.js";
|
||||||
|
import { In } from "typeorm";
|
||||||
|
import { ApiError } from "../../../error.js";
|
||||||
|
import { db } from "@/db/postgre.js";
|
||||||
|
|
||||||
|
export const meta = {
|
||||||
|
tags: ["admin"],
|
||||||
|
|
||||||
|
requireCredential: true,
|
||||||
|
requireModerator: true,
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export const paramDef = {
|
||||||
|
type: "object",
|
||||||
|
properties: {
|
||||||
|
ids: {
|
||||||
|
type: "array",
|
||||||
|
items: {
|
||||||
|
type: "string",
|
||||||
|
format: "misskey:id",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
license: {
|
||||||
|
type: "string",
|
||||||
|
nullable: true,
|
||||||
|
description: "Use `null` to reset the license.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
required: ["ids"],
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export default define(meta, paramDef, async (ps) => {
|
||||||
|
await Emojis.update(
|
||||||
|
{
|
||||||
|
id: In(ps.ids),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
updatedAt: new Date(),
|
||||||
|
license: ps.license,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
await db.queryResultCache!.remove(["meta_emojis"]);
|
||||||
|
});
|
|
@ -34,6 +34,10 @@ export const paramDef = {
|
||||||
type: "string",
|
type: "string",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
license: {
|
||||||
|
type: "string",
|
||||||
|
nullable: true,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
required: ["id", "name", "aliases"],
|
required: ["id", "name", "aliases"],
|
||||||
} as const;
|
} as const;
|
||||||
|
@ -48,6 +52,7 @@ export default define(meta, paramDef, async (ps) => {
|
||||||
name: ps.name,
|
name: ps.name,
|
||||||
category: ps.category,
|
category: ps.category,
|
||||||
aliases: ps.aliases,
|
aliases: ps.aliases,
|
||||||
|
license: ps.license,
|
||||||
});
|
});
|
||||||
|
|
||||||
await db.queryResultCache!.remove(["meta_emojis"]);
|
await db.queryResultCache!.remove(["meta_emojis"]);
|
||||||
|
|
|
@ -3,6 +3,7 @@ import {
|
||||||
inboxQueue,
|
inboxQueue,
|
||||||
dbQueue,
|
dbQueue,
|
||||||
objectStorageQueue,
|
objectStorageQueue,
|
||||||
|
backgroundQueue,
|
||||||
} from "@/queue/queues.js";
|
} from "@/queue/queues.js";
|
||||||
import define from "../../../define.js";
|
import define from "../../../define.js";
|
||||||
|
|
||||||
|
@ -37,6 +38,11 @@ export const meta = {
|
||||||
nullable: false,
|
nullable: false,
|
||||||
ref: "QueueCount",
|
ref: "QueueCount",
|
||||||
},
|
},
|
||||||
|
backgroundQueue: {
|
||||||
|
optional: false,
|
||||||
|
nullable: false,
|
||||||
|
ref: "QueueCount",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
} as const;
|
} as const;
|
||||||
|
@ -52,11 +58,13 @@ export default define(meta, paramDef, async (ps) => {
|
||||||
const inboxJobCounts = await inboxQueue.getJobCounts();
|
const inboxJobCounts = await inboxQueue.getJobCounts();
|
||||||
const dbJobCounts = await dbQueue.getJobCounts();
|
const dbJobCounts = await dbQueue.getJobCounts();
|
||||||
const objectStorageJobCounts = await objectStorageQueue.getJobCounts();
|
const objectStorageJobCounts = await objectStorageQueue.getJobCounts();
|
||||||
|
const backgroundJobCounts = await backgroundQueue.getJobCounts();
|
||||||
|
|
||||||
return {
|
return {
|
||||||
deliver: deliverJobCounts,
|
deliver: deliverJobCounts,
|
||||||
inbox: inboxJobCounts,
|
inbox: inboxJobCounts,
|
||||||
db: dbJobCounts,
|
db: dbJobCounts,
|
||||||
objectStorage: objectStorageJobCounts,
|
objectStorage: objectStorageJobCounts,
|
||||||
|
backgroundQueue: backgroundJobCounts,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
|
@ -0,0 +1,28 @@
|
||||||
|
import define from "../../../define.js";
|
||||||
|
import { createIndexAllNotesJob } from "@/queue/index.js";
|
||||||
|
|
||||||
|
export const meta = {
|
||||||
|
tags: ["admin"],
|
||||||
|
|
||||||
|
requireCredential: true,
|
||||||
|
requireModerator: true,
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export const paramDef = {
|
||||||
|
type: "object",
|
||||||
|
properties: {
|
||||||
|
cursor: {
|
||||||
|
type: "string",
|
||||||
|
format: "misskey:id",
|
||||||
|
nullable: true,
|
||||||
|
default: null,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
required: [],
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export default define(meta, paramDef, async (ps, _me) => {
|
||||||
|
createIndexAllNotesJob({
|
||||||
|
cursor: ps.cursor ?? undefined,
|
||||||
|
});
|
||||||
|
});
|
|
@ -54,7 +54,7 @@ export const paramDef = {
|
||||||
folderId: { type: "string", format: "misskey:id", nullable: true },
|
folderId: { type: "string", format: "misskey:id", nullable: true },
|
||||||
name: { type: "string" },
|
name: { type: "string" },
|
||||||
isSensitive: { type: "boolean" },
|
isSensitive: { type: "boolean" },
|
||||||
comment: { type: "string", nullable: true, maxLength: 512 },
|
comment: { type: "string", nullable: true, maxLength: DB_MAX_IMAGE_COMMENT_LENGTH },
|
||||||
},
|
},
|
||||||
required: ["fileId"],
|
required: ["fileId"],
|
||||||
} as const;
|
} as const;
|
||||||
|
|
38
packages/backend/src/server/api/endpoints/emoji.ts
Normal file
38
packages/backend/src/server/api/endpoints/emoji.ts
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
import { IsNull } from "typeorm";
|
||||||
|
import { Emojis } from "@/models/index.js";
|
||||||
|
import define from "../define.js";
|
||||||
|
|
||||||
|
export const meta = {
|
||||||
|
tags: ["meta"],
|
||||||
|
|
||||||
|
requireCredential: false,
|
||||||
|
allowGet: true,
|
||||||
|
cacheSec: 3600,
|
||||||
|
|
||||||
|
res: {
|
||||||
|
type: "object",
|
||||||
|
optional: false, nullable: false,
|
||||||
|
ref: "Emoji",
|
||||||
|
},
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export const paramDef = {
|
||||||
|
type: "object",
|
||||||
|
properties: {
|
||||||
|
name: {
|
||||||
|
type: "string",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
required: ["name"],
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export default define(meta, paramDef, async (ps, me) => {
|
||||||
|
const emoji = await Emojis.findOneOrFail({
|
||||||
|
where: {
|
||||||
|
name: ps.name,
|
||||||
|
host: IsNull(),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return Emojis.pack(emoji);
|
||||||
|
});
|
|
@ -151,7 +151,7 @@ export default define(meta, paramDef, async (ps, user) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
// テキストが無いかつ添付ファイルも無かったらエラー
|
// テキストが無いかつ添付ファイルも無かったらエラー
|
||||||
if (ps.text == null && file == null) {
|
if ((ps.text == null || ps.text.trim() === '') && file == null) {
|
||||||
throw new ApiError(meta.errors.contentRequired);
|
throw new ApiError(meta.errors.contentRequired);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,9 @@
|
||||||
import { In } from "typeorm";
|
import { In } from "typeorm";
|
||||||
import { Notes } from "@/models/index.js";
|
import { Notes } from "@/models/index.js";
|
||||||
|
import { Note } from "@/models/entities/note.js";
|
||||||
import config from "@/config/index.js";
|
import config from "@/config/index.js";
|
||||||
import es from "../../../../db/elasticsearch.js";
|
import es from "../../../../db/elasticsearch.js";
|
||||||
|
import sonic from "../../../../db/sonic.js";
|
||||||
import define from "../../define.js";
|
import define from "../../define.js";
|
||||||
import { makePaginationQuery } from "../../common/make-pagination-query.js";
|
import { makePaginationQuery } from "../../common/make-pagination-query.js";
|
||||||
import { generateVisibilityQuery } from "../../common/generate-visibility-query.js";
|
import { generateVisibilityQuery } from "../../common/generate-visibility-query.js";
|
||||||
|
@ -59,7 +61,7 @@ export const paramDef = {
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export default define(meta, paramDef, async (ps, me) => {
|
export default define(meta, paramDef, async (ps, me) => {
|
||||||
if (es == null) {
|
if (es == null && sonic == null) {
|
||||||
const query = makePaginationQuery(
|
const query = makePaginationQuery(
|
||||||
Notes.createQueryBuilder("note"),
|
Notes.createQueryBuilder("note"),
|
||||||
ps.sinceId,
|
ps.sinceId,
|
||||||
|
@ -92,9 +94,82 @@ export default define(meta, paramDef, async (ps, me) => {
|
||||||
if (me) generateMutedUserQuery(query, me);
|
if (me) generateMutedUserQuery(query, me);
|
||||||
if (me) generateBlockedUserQuery(query, me);
|
if (me) generateBlockedUserQuery(query, me);
|
||||||
|
|
||||||
const notes = await query.take(ps.limit).getMany();
|
const notes: Note[] = await query.take(ps.limit).getMany();
|
||||||
|
|
||||||
return await Notes.packMany(notes, me);
|
return await Notes.packMany(notes, me);
|
||||||
|
} else if (sonic) {
|
||||||
|
let start = 0;
|
||||||
|
const chunkSize = 100;
|
||||||
|
|
||||||
|
// Use sonic to fetch and step through all search results that could match the requirements
|
||||||
|
const ids = [];
|
||||||
|
while (true) {
|
||||||
|
const results = await sonic.search.query(
|
||||||
|
sonic.collection,
|
||||||
|
sonic.bucket,
|
||||||
|
ps.query,
|
||||||
|
{
|
||||||
|
limit: chunkSize,
|
||||||
|
offset: start,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
start += chunkSize;
|
||||||
|
|
||||||
|
if (results.length === 0) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
const res = results
|
||||||
|
.map((k) => JSON.parse(k))
|
||||||
|
.filter((key) => {
|
||||||
|
if (ps.userId && key.userId !== ps.userId) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (ps.channelId && key.channelId !== ps.channelId) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (ps.sinceId && key.id <= ps.sinceId) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (ps.untilId && key.id >= ps.untilId) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
})
|
||||||
|
.map((key) => key.id);
|
||||||
|
|
||||||
|
ids.push(...res);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort all the results by note id DESC (newest first)
|
||||||
|
ids.sort((a, b) => b - a);
|
||||||
|
|
||||||
|
// Fetch the notes from the database until we have enough to satisfy the limit
|
||||||
|
start = 0;
|
||||||
|
const found = [];
|
||||||
|
while (found.length < ps.limit && start < ids.length) {
|
||||||
|
const chunk = ids.slice(start, start + chunkSize);
|
||||||
|
const notes: Note[] = await Notes.find({
|
||||||
|
where: {
|
||||||
|
id: In(chunk),
|
||||||
|
},
|
||||||
|
order: {
|
||||||
|
id: "DESC",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// The notes are checked for visibility and muted/blocked users when packed
|
||||||
|
found.push(...await Notes.packMany(notes, me));
|
||||||
|
start += chunkSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we have more results than the limit, trim them
|
||||||
|
if (found.length > ps.limit) {
|
||||||
|
found.length = ps.limit;
|
||||||
|
}
|
||||||
|
|
||||||
|
return found;
|
||||||
} else {
|
} else {
|
||||||
const userQuery =
|
const userQuery =
|
||||||
ps.userId != null
|
ps.userId != null
|
||||||
|
|
|
@ -33,10 +33,10 @@ export function apiAccountMastodon(router: Router): void {
|
||||||
let acct = data.data;
|
let acct = data.data;
|
||||||
acct.id = convertId(acct.id, IdType.MastodonId);
|
acct.id = convertId(acct.id, IdType.MastodonId);
|
||||||
acct.url = `${BASE_URL}/@${acct.url}`;
|
acct.url = `${BASE_URL}/@${acct.url}`;
|
||||||
acct.note = "";
|
acct.note = acct.note || "";
|
||||||
acct.avatar_static = acct.avatar;
|
acct.avatar_static = acct.avatar;
|
||||||
acct.header = acct.header || "";
|
acct.header = acct.header || "https://http.cat/404";
|
||||||
acct.header_static = acct.header || "";
|
acct.header_static = acct.header || "https://http.cat/404";
|
||||||
acct.source = {
|
acct.source = {
|
||||||
note: acct.note,
|
note: acct.note,
|
||||||
fields: acct.fields,
|
fields: acct.fields,
|
||||||
|
@ -339,7 +339,12 @@ export function apiAccountMastodon(router: Router): void {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const data = await client.getRelationships(ids);
|
let reqIds = [];
|
||||||
|
for (let i = 0; i < ids.length; i++) {
|
||||||
|
reqIds.push(convertId(ids[i], IdType.CalckeyId));
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await client.getRelationships(reqIds);
|
||||||
let resp = data.data;
|
let resp = data.data;
|
||||||
for (let acctIdx = 0; acctIdx < resp.length; acctIdx++) {
|
for (let acctIdx = 0; acctIdx < resp.length; acctIdx++) {
|
||||||
resp[acctIdx].id = convertId(resp[acctIdx].id, IdType.MastodonId);
|
resp[acctIdx].id = convertId(resp[acctIdx].id, IdType.MastodonId);
|
||||||
|
@ -359,7 +364,7 @@ export function apiAccountMastodon(router: Router): void {
|
||||||
const accessTokens = ctx.headers.authorization;
|
const accessTokens = ctx.headers.authorization;
|
||||||
const client = getClient(BASE_URL, accessTokens);
|
const client = getClient(BASE_URL, accessTokens);
|
||||||
try {
|
try {
|
||||||
const data = (await client.getBookmarks(ctx.query as any)) as any;
|
const data = (await client.getBookmarks(limitToInt(ctx.query as any))) as any;
|
||||||
let resp = data.data;
|
let resp = data.data;
|
||||||
for (let statIdx = 0; statIdx < resp.length; statIdx++) {
|
for (let statIdx = 0; statIdx < resp.length; statIdx++) {
|
||||||
resp[statIdx].id = convertId(resp[statIdx].id, IdType.MastodonId);
|
resp[statIdx].id = convertId(resp[statIdx].id, IdType.MastodonId);
|
||||||
|
@ -383,7 +388,7 @@ export function apiAccountMastodon(router: Router): void {
|
||||||
const accessTokens = ctx.headers.authorization;
|
const accessTokens = ctx.headers.authorization;
|
||||||
const client = getClient(BASE_URL, accessTokens);
|
const client = getClient(BASE_URL, accessTokens);
|
||||||
try {
|
try {
|
||||||
const data = await client.getFavourites(ctx.query as any);
|
const data = await client.getFavourites(limitToInt(ctx.query as any));
|
||||||
let resp = data.data;
|
let resp = data.data;
|
||||||
for (let statIdx = 0; statIdx < resp.length; statIdx++) {
|
for (let statIdx = 0; statIdx < resp.length; statIdx++) {
|
||||||
resp[statIdx].id = convertId(resp[statIdx].id, IdType.MastodonId);
|
resp[statIdx].id = convertId(resp[statIdx].id, IdType.MastodonId);
|
||||||
|
@ -407,7 +412,7 @@ export function apiAccountMastodon(router: Router): void {
|
||||||
const accessTokens = ctx.headers.authorization;
|
const accessTokens = ctx.headers.authorization;
|
||||||
const client = getClient(BASE_URL, accessTokens);
|
const client = getClient(BASE_URL, accessTokens);
|
||||||
try {
|
try {
|
||||||
const data = await client.getMutes(ctx.query as any);
|
const data = await client.getMutes(limitToInt(ctx.query as any));
|
||||||
let resp = data.data;
|
let resp = data.data;
|
||||||
for (let acctIdx = 0; acctIdx < resp.length; acctIdx++) {
|
for (let acctIdx = 0; acctIdx < resp.length; acctIdx++) {
|
||||||
resp[acctIdx].id = convertId(resp[acctIdx].id, IdType.MastodonId);
|
resp[acctIdx].id = convertId(resp[acctIdx].id, IdType.MastodonId);
|
||||||
|
@ -425,7 +430,7 @@ export function apiAccountMastodon(router: Router): void {
|
||||||
const accessTokens = ctx.headers.authorization;
|
const accessTokens = ctx.headers.authorization;
|
||||||
const client = getClient(BASE_URL, accessTokens);
|
const client = getClient(BASE_URL, accessTokens);
|
||||||
try {
|
try {
|
||||||
const data = await client.getBlocks(ctx.query as any);
|
const data = await client.getBlocks(limitToInt(ctx.query as any));
|
||||||
let resp = data.data;
|
let resp = data.data;
|
||||||
for (let acctIdx = 0; acctIdx < resp.length; acctIdx++) {
|
for (let acctIdx = 0; acctIdx < resp.length; acctIdx++) {
|
||||||
resp[acctIdx].id = convertId(resp[acctIdx].id, IdType.MastodonId);
|
resp[acctIdx].id = convertId(resp[acctIdx].id, IdType.MastodonId);
|
||||||
|
|
|
@ -4,6 +4,8 @@ import { emojiRegexAtStartToEnd } from "@/misc/emoji-regex.js";
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import querystring from 'node:querystring'
|
import querystring from 'node:querystring'
|
||||||
import qs from 'qs'
|
import qs from 'qs'
|
||||||
|
import { limitToInt } from "./timeline.js";
|
||||||
|
|
||||||
function normalizeQuery(data: any) {
|
function normalizeQuery(data: any) {
|
||||||
const str = querystring.stringify(data);
|
const str = querystring.stringify(data);
|
||||||
return qs.parse(str);
|
return qs.parse(str);
|
||||||
|
@ -101,9 +103,14 @@ export function apiStatusMastodon(router: Router): void {
|
||||||
const client = getClient(BASE_URL, accessTokens);
|
const client = getClient(BASE_URL, accessTokens);
|
||||||
try {
|
try {
|
||||||
const id = ctx.params.id;
|
const id = ctx.params.id;
|
||||||
const data = await client.getStatusContext(id, ctx.query as any);
|
const data = await client.getStatusContext(id, limitToInt(ctx.query as any));
|
||||||
const status = await client.getStatus(id);
|
const status = await client.getStatus(id);
|
||||||
const reactionsAxios = await axios.get(
|
let reqInstance = axios.create({
|
||||||
|
headers: {
|
||||||
|
Authorization : ctx.headers.authorization
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const reactionsAxios = await reqInstance.get(
|
||||||
`${BASE_URL}/api/notes/reactions?noteId=${id}`,
|
`${BASE_URL}/api/notes/reactions?noteId=${id}`,
|
||||||
);
|
);
|
||||||
const reactions: IReaction[] = reactionsAxios.data;
|
const reactions: IReaction[] = reactionsAxios.data;
|
||||||
|
|
|
@ -15,13 +15,16 @@ export function limitToInt(q: ParsedUrlQuery) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function argsToBools(q: ParsedUrlQuery) {
|
export function argsToBools(q: ParsedUrlQuery) {
|
||||||
|
// Values taken from https://docs.joinmastodon.org/client/intro/#boolean
|
||||||
|
const toBoolean = (value: string) => !['0', 'f', 'F', 'false', 'FALSE', 'off', 'OFF'].includes(value);
|
||||||
|
|
||||||
let object: any = q;
|
let object: any = q;
|
||||||
if (q.only_media)
|
if (q.only_media)
|
||||||
if (typeof q.only_media === "string")
|
if (typeof q.only_media === "string")
|
||||||
object.only_media = q.only_media.toLowerCase() === "true";
|
object.only_media = toBoolean(q.only_media);
|
||||||
if (q.exclude_replies)
|
if (q.exclude_replies)
|
||||||
if (typeof q.exclude_replies === "string")
|
if (typeof q.exclude_replies === "string")
|
||||||
object.exclude_replies = q.exclude_replies.toLowerCase() === "true";
|
object.exclude_replies = toBoolean(q.exclude_replies);
|
||||||
return q;
|
return q;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -92,8 +95,8 @@ export function apiTimelineMastodon(router: Router): void {
|
||||||
try {
|
try {
|
||||||
const query: any = ctx.query;
|
const query: any = ctx.query;
|
||||||
const data = query.local
|
const data = query.local
|
||||||
? await client.getLocalTimeline(limitToInt(query))
|
? await client.getLocalTimeline(argsToBools(limitToInt(query)))
|
||||||
: await client.getPublicTimeline(limitToInt(query));
|
: await client.getPublicTimeline(argsToBools(limitToInt(query)));
|
||||||
ctx.body = toTextWithReaction(data.data, ctx.hostname);
|
ctx.body = toTextWithReaction(data.data, ctx.hostname);
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
|
@ -111,7 +114,7 @@ export function apiTimelineMastodon(router: Router): void {
|
||||||
try {
|
try {
|
||||||
const data = await client.getTagTimeline(
|
const data = await client.getTagTimeline(
|
||||||
ctx.params.hashtag,
|
ctx.params.hashtag,
|
||||||
limitToInt(ctx.query),
|
argsToBools(limitToInt(ctx.query)),
|
||||||
);
|
);
|
||||||
ctx.body = toTextWithReaction(data.data, ctx.hostname);
|
ctx.body = toTextWithReaction(data.data, ctx.hostname);
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
|
|
|
@ -6,27 +6,38 @@ main {
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
}
|
}
|
||||||
#tl > div {
|
#tl > div {
|
||||||
padding: 16px;
|
border: 1px solid #908caa;
|
||||||
border-bottom: 1px solid #908caa;
|
border-radius: 10px;
|
||||||
|
margin: 10px;
|
||||||
|
padding: 10px;
|
||||||
|
width: fit-content;
|
||||||
}
|
}
|
||||||
#tl > div > header {
|
#tl > div > header {
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
|
display: inline-flex;
|
||||||
}
|
}
|
||||||
|
|
||||||
* {
|
img {
|
||||||
font-family: BIZ UDGothic, Roboto, HelveticaNeue, Arial, sans-serif;
|
border-radius: 10px;
|
||||||
|
margin-right: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#form {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
#calckey_app {
|
#calckey_app {
|
||||||
display: none !important;
|
display: none !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
body,
|
body,
|
||||||
html {
|
html {
|
||||||
|
font-family: BIZ UDGothic, Roboto, HelveticaNeue, Arial, sans-serif;
|
||||||
background-color: #191724;
|
background-color: #191724;
|
||||||
color: #e0def4;
|
color: #e0def4;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
margin: auto;
|
margin: auto;
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
text-align: center;
|
|
||||||
}
|
}
|
||||||
button {
|
button {
|
||||||
border-radius:999px;
|
border-radius:999px;
|
||||||
|
|
|
@ -45,12 +45,27 @@ window.onload = async () => {
|
||||||
const tl = document.getElementById("tl");
|
const tl = document.getElementById("tl");
|
||||||
for (const note of notes) {
|
for (const note of notes) {
|
||||||
const el = document.createElement("div");
|
const el = document.createElement("div");
|
||||||
const name = document.createElement("header");
|
const header = document.createElement("header");
|
||||||
|
const name = document.createElement("p");
|
||||||
|
const avatar = document.createElement("img")
|
||||||
name.textContent = `${note.user.name} @${note.user.username}`;
|
name.textContent = `${note.user.name} @${note.user.username}`;
|
||||||
|
avatar.src = note.user.avatarUrl;
|
||||||
|
avatar.style = 'height: 40px'
|
||||||
const text = document.createElement("div");
|
const text = document.createElement("div");
|
||||||
text.textContent = `${note.text}`;
|
text.textContent = `${note.text}`;
|
||||||
el.appendChild(name);
|
el.appendChild(header);
|
||||||
|
header.appendChild(avatar);
|
||||||
|
header.appendChild(name);
|
||||||
|
if (note.text) {
|
||||||
el.appendChild(text);
|
el.appendChild(text);
|
||||||
|
}
|
||||||
|
if (note.files) {
|
||||||
|
for (const file of note.files) {
|
||||||
|
const img = document.createElement("img");
|
||||||
|
img.src = file.properties.thumbnailUrl;
|
||||||
|
el.appendChild(img)
|
||||||
|
}
|
||||||
|
}
|
||||||
tl.appendChild(el);
|
tl.appendChild(el);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import * as mfm from "mfm-js";
|
import * as mfm from "mfm-js";
|
||||||
import es from "../../db/elasticsearch.js";
|
import es from "../../db/elasticsearch.js";
|
||||||
|
import sonic from "../../db/sonic.js";
|
||||||
import {
|
import {
|
||||||
publishMainStream,
|
publishMainStream,
|
||||||
publishNotesStream,
|
publishNotesStream,
|
||||||
|
@ -588,7 +589,7 @@ export default async (
|
||||||
}
|
}
|
||||||
|
|
||||||
// Register to search database
|
// Register to search database
|
||||||
index(note);
|
await index(note);
|
||||||
});
|
});
|
||||||
|
|
||||||
async function renderNoteOrRenoteActivity(data: Option, note: Note) {
|
async function renderNoteOrRenoteActivity(data: Option, note: Note) {
|
||||||
|
@ -728,10 +729,11 @@ async function insertNote(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function index(note: Note) {
|
export async function index(note: Note): Promise<void> {
|
||||||
if (note.text == null || config.elasticsearch == null) return;
|
if (!note.text) return;
|
||||||
|
|
||||||
es!.index({
|
if (config.elasticsearch && es) {
|
||||||
|
es.index({
|
||||||
index: config.elasticsearch.index || "misskey_note",
|
index: config.elasticsearch.index || "misskey_note",
|
||||||
id: note.id.toString(),
|
id: note.id.toString(),
|
||||||
body: {
|
body: {
|
||||||
|
@ -742,6 +744,21 @@ function index(note: Note) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (sonic) {
|
||||||
|
await sonic.ingest.push(
|
||||||
|
sonic.collection,
|
||||||
|
sonic.bucket,
|
||||||
|
JSON.stringify({
|
||||||
|
id: note.id,
|
||||||
|
userId: note.userId,
|
||||||
|
userHost: note.userHost,
|
||||||
|
channelId: note.channelId,
|
||||||
|
}),
|
||||||
|
note.text,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function notifyToWatchersOfRenotee(
|
async function notifyToWatchersOfRenotee(
|
||||||
renote: Note,
|
renote: Note,
|
||||||
user: { id: User["id"] },
|
user: { id: User["id"] },
|
||||||
|
|
|
@ -78,6 +78,7 @@
|
||||||
"uuid": "9.0.0",
|
"uuid": "9.0.0",
|
||||||
"vanilla-tilt": "1.8.0",
|
"vanilla-tilt": "1.8.0",
|
||||||
"vite": "^4.1.1",
|
"vite": "^4.1.1",
|
||||||
|
"vite-plugin-compression": "^0.5.1",
|
||||||
"vue": "3.2.45",
|
"vue": "3.2.45",
|
||||||
"vue-isyourpasswordsafe": "^2.0.0",
|
"vue-isyourpasswordsafe": "^2.0.0",
|
||||||
"vue-plyr": "^7.0.0",
|
"vue-plyr": "^7.0.0",
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
<template>
|
<template>
|
||||||
<button class="nrvgflfu _button" @click.stop.prevent="toggle">
|
<button class="nrvgflfu _button" @click.stop="toggle">
|
||||||
<b>{{ modelValue ? i18n.ts._cw.hide : i18n.ts._cw.show }}</b>
|
<b>{{ modelValue ? i18n.ts._cw.hide : i18n.ts._cw.show }}</b>
|
||||||
<span v-if="!modelValue">{{ label }}</span>
|
<span v-if="!modelValue">{{ label }}</span>
|
||||||
</button>
|
</button>
|
||||||
|
@ -36,6 +36,8 @@ const toggle = () => {
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.nrvgflfu {
|
.nrvgflfu {
|
||||||
|
position: relative;
|
||||||
|
z-index: 2;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
padding: 4px 8px;
|
padding: 4px 8px;
|
||||||
font-size: 0.8em;
|
font-size: 0.8em;
|
||||||
|
@ -44,6 +46,7 @@ const toggle = () => {
|
||||||
padding: 6px 10px;
|
padding: 6px 10px;
|
||||||
width: 90%;
|
width: 90%;
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
|
border: 1px solid var(--divider);
|
||||||
margin-top: 10px;
|
margin-top: 10px;
|
||||||
margin-bottom: 10px;
|
margin-bottom: 10px;
|
||||||
transition: background-color 0.25s ease-in-out;
|
transition: background-color 0.25s ease-in-out;
|
||||||
|
|
|
@ -14,9 +14,11 @@
|
||||||
</div>
|
</div>
|
||||||
<header v-if="title" :class="$style.title"><Mfm :text="title"/></header>
|
<header v-if="title" :class="$style.title"><Mfm :text="title"/></header>
|
||||||
<div v-if="text" :class="$style.text"><Mfm :text="text"/></div>
|
<div v-if="text" :class="$style.text"><Mfm :text="text"/></div>
|
||||||
<MkInput v-if="input" v-model="inputValue" autofocus :type="input.type || 'text'" :placeholder="input.placeholder || undefined" @keydown="onInputKeydown">
|
<MkInput v-if="input && input.type !== 'paragraph'" v-model="inputValue" autofocus :type="input.type || 'text'" :placeholder="input.placeholder || undefined" @keydown="onInputKeydown">
|
||||||
<template v-if="input.type === 'password'" #prefix><i class="ph-password ph-bold ph-lg"></i></template>
|
<template v-if="input.type === 'password'" #prefix><i class="ph-password ph-bold ph-lg"></i></template>
|
||||||
</MkInput>
|
</MkInput>
|
||||||
|
<MkTextarea v-if="input && input.type === 'paragraph'" v-model="inputValue" autofocus :type="paragraph" :placeholder="input.placeholder || undefined">
|
||||||
|
</MkTextarea>
|
||||||
<MkSelect v-if="select" v-model="selectedValue" autofocus>
|
<MkSelect v-if="select" v-model="selectedValue" autofocus>
|
||||||
<template v-if="select.items">
|
<template v-if="select.items">
|
||||||
<option v-for="item in select.items" :value="item.value">{{ item.text }}</option>
|
<option v-for="item in select.items" :value="item.value">{{ item.text }}</option>
|
||||||
|
@ -49,6 +51,7 @@ import { onBeforeUnmount, onMounted, ref, shallowRef } from 'vue';
|
||||||
import MkModal from '@/components/MkModal.vue';
|
import MkModal from '@/components/MkModal.vue';
|
||||||
import MkButton from '@/components/MkButton.vue';
|
import MkButton from '@/components/MkButton.vue';
|
||||||
import MkInput from '@/components/form/input.vue';
|
import MkInput from '@/components/form/input.vue';
|
||||||
|
import MkTextarea from '@/components/form/textarea.vue';
|
||||||
import MkSelect from '@/components/form/select.vue';
|
import MkSelect from '@/components/form/select.vue';
|
||||||
import { i18n } from '@/i18n';
|
import { i18n } from '@/i18n';
|
||||||
|
|
||||||
|
|
|
@ -144,13 +144,12 @@ onBeforeUnmount(() => {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
color: var(--accent);
|
color: var(--accent);
|
||||||
background: transparent;
|
|
||||||
border: solid 1px var(--accent);
|
border: solid 1px var(--accent);
|
||||||
padding: 0;
|
padding: 0;
|
||||||
height: 31px;
|
height: 31px;
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
border-radius: 32px;
|
border-radius: 32px;
|
||||||
background: #fff;
|
background: var(--accentedBg);
|
||||||
|
|
||||||
&.full {
|
&.full {
|
||||||
padding: 0 8px 0 12px;
|
padding: 0 8px 0 12px;
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
<template>
|
<template>
|
||||||
<component :is="self ? 'MkA' : 'a'" ref="el" class="xlcxczvw _link" :[attr]="self ? url.substr(local.length) : url" :rel="rel" :target="target"
|
<component :is="self ? 'MkA' : 'a'" ref="el" class="xlcxczvw _link" :[attr]="self ? url.substr(local.length) : url" :rel="rel" :target="target"
|
||||||
:title="url"
|
:title="url" @click.stop
|
||||||
>
|
>
|
||||||
<slot></slot>
|
<slot></slot>
|
||||||
<i v-if="target === '_blank'" class="ph-arrow-square-out ph-bold ph-lg icon"></i>
|
<i v-if="target === '_blank'" class="ph-arrow-square-out ph-bold ph-lg icon"></i>
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
<div class="hoawjimk">
|
<div class="hoawjimk">
|
||||||
<XBanner v-for="media in mediaList.filter(media => !previewable(media))" :key="media.id" :media="media"/>
|
<XBanner v-for="media in mediaList.filter(media => !previewable(media))" :key="media.id" :media="media"/>
|
||||||
<div v-if="mediaList.filter(media => previewable(media)).length > 0" class="gird-container" :class="{ dmWidth: inDm }">
|
<div v-if="mediaList.filter(media => previewable(media)).length > 0" class="gird-container" :class="{ dmWidth: inDm }">
|
||||||
<div ref="gallery" :data-count="mediaList.filter(media => previewable(media)).length" @click.stop.prevent>
|
<div ref="gallery" :data-count="mediaList.filter(media => previewable(media)).length" @click.stop>
|
||||||
<template v-for="media in mediaList.filter(media => previewable(media))">
|
<template v-for="media in mediaList.filter(media => previewable(media))">
|
||||||
<XVideo v-if="media.type.startsWith('video')" :key="media.id" :video="media"/>
|
<XVideo v-if="media.type.startsWith('video')" :key="media.id" :video="media"/>
|
||||||
<XImage v-else-if="media.type.startsWith('image')" :key="media.id" class="image" :data-id="media.id" :image="media" :raw="raw"/>
|
<XImage v-else-if="media.type.startsWith('image')" :key="media.id" class="image" :data-id="media.id" :image="media" :raw="raw"/>
|
||||||
|
|
|
@ -1,12 +1,12 @@
|
||||||
<template>
|
<template>
|
||||||
<MkA v-if="url.startsWith('/')" v-user-preview="canonical" class="akbvjaqn" :class="{ isMe }" :to="url" :style="{ background: bgCss }">
|
<MkA v-if="url.startsWith('/')" v-user-preview="canonical" class="akbvjaqn" :class="{ isMe }" :to="url" :style="{ background: bgCss }" @click.stop>
|
||||||
<img class="icon" :src="`/avatar/@${username}@${host}`" alt="">
|
<img class="icon" :src="`/avatar/@${username}@${host}`" alt="">
|
||||||
<span class="main">
|
<span class="main">
|
||||||
<span class="username">@{{ username }}</span>
|
<span class="username">@{{ username }}</span>
|
||||||
<span v-if="(host != localHost) || $store.state.showFullAcct" class="host">@{{ toUnicode(host) }}</span>
|
<span v-if="(host != localHost) || $store.state.showFullAcct" class="host">@{{ toUnicode(host) }}</span>
|
||||||
</span>
|
</span>
|
||||||
</MkA>
|
</MkA>
|
||||||
<a v-else class="akbvjaqn" :href="url" target="_blank" rel="noopener" :style="{ background: bgCss }">
|
<a v-else class="akbvjaqn" :href="url" target="_blank" rel="noopener" :style="{ background: bgCss }" @click.stop>
|
||||||
<span class="main">
|
<span class="main">
|
||||||
<span class="username">@{{ username }}</span>
|
<span class="username">@{{ username }}</span>
|
||||||
<span class="host">@{{ toUnicode(host) }}</span>
|
<span class="host">@{{ toUnicode(host) }}</span>
|
||||||
|
@ -42,8 +42,13 @@ const bgCss = bg.toRgbString();
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.akbvjaqn {
|
.akbvjaqn {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
padding: 4px 8px 4px 4px;
|
padding: 2px 8px 2px 2px;
|
||||||
|
margin-block: 2px;
|
||||||
border-radius: 999px;
|
border-radius: 999px;
|
||||||
|
max-width: 100%;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: clip;
|
||||||
|
text-overflow: ellipsis;
|
||||||
color: var(--mention);
|
color: var(--mention);
|
||||||
|
|
||||||
&.isMe {
|
&.isMe {
|
||||||
|
|
|
@ -25,7 +25,7 @@
|
||||||
</template>
|
</template>
|
||||||
</I18n>
|
</I18n>
|
||||||
<div class="info">
|
<div class="info">
|
||||||
<button ref="renoteTime" class="_button time" @click="showRenoteMenu()">
|
<button ref="renoteTime" class="_button time" @click.stop="showRenoteMenu()">
|
||||||
<i v-if="isMyRenote" class="ph-dots-three-outline ph-bold ph-lg dropdownIcon"></i>
|
<i v-if="isMyRenote" class="ph-dots-three-outline ph-bold ph-lg dropdownIcon"></i>
|
||||||
<MkTime :time="note.createdAt"/>
|
<MkTime :time="note.createdAt"/>
|
||||||
</button>
|
</button>
|
||||||
|
@ -33,19 +33,20 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<article class="article" @contextmenu.stop="onContextmenu" @click.self="router.push(notePage(appearNote))">
|
<article class="article" @contextmenu.stop="onContextmenu" @click="noteClick">
|
||||||
<div class="main" @click.self="router.push(notePage(appearNote))">
|
<div class="main">
|
||||||
<div class="header-container">
|
<div class="header-container">
|
||||||
<MkAvatar class="avatar" :user="appearNote.user"/>
|
<MkAvatar class="avatar" :user="appearNote.user"/>
|
||||||
<XNoteHeader class="header" :note="appearNote" :mini="true"/>
|
<XNoteHeader class="header" :note="appearNote" :mini="true"/>
|
||||||
</div>
|
</div>
|
||||||
<div class="body">
|
<div class="body">
|
||||||
<p v-if="appearNote.cw != null" class="cw">
|
<p v-if="appearNote.cw != null" class="cw">
|
||||||
<Mfm v-if="appearNote.cw != ''" class="text" :text="appearNote.cw" :author="appearNote.user" :i="$i" :custom-emojis="appearNote.emojis"/>
|
<Mfm v-if="appearNote.cw != ''" class="text" :text="appearNote.cw" :author="appearNote.user" :custom-emojis="appearNote.emojis" :i="$i"/>
|
||||||
|
<br/>
|
||||||
<XCwButton v-model="showContent" :note="appearNote"/>
|
<XCwButton v-model="showContent" :note="appearNote"/>
|
||||||
</p>
|
</p>
|
||||||
<div v-show="appearNote.cw == null || showContent" class="content" :class="{ collapsed, isLong }">
|
<div v-show="appearNote.cw == null || showContent" class="content" :class="{ collapsed, isLong }">
|
||||||
<div class="text" @click.self="router.push(notePage(appearNote))">
|
<div class="text">
|
||||||
<Mfm v-if="appearNote.text" :text="appearNote.text" :author="appearNote.user" :i="$i" :custom-emojis="appearNote.emojis"/>
|
<Mfm v-if="appearNote.text" :text="appearNote.text" :author="appearNote.user" :i="$i" :custom-emojis="appearNote.emojis"/>
|
||||||
<!-- <a v-if="appearNote.renote != null" class="rp">RN:</a> -->
|
<!-- <a v-if="appearNote.renote != null" class="rp">RN:</a> -->
|
||||||
<div v-if="translating || translation" class="translation">
|
<div v-if="translating || translation" class="translation">
|
||||||
|
@ -61,22 +62,23 @@
|
||||||
</div>
|
</div>
|
||||||
<XPoll v-if="appearNote.poll" ref="pollViewer" :note="appearNote" class="poll"/>
|
<XPoll v-if="appearNote.poll" ref="pollViewer" :note="appearNote" class="poll"/>
|
||||||
<MkUrlPreview v-for="url in urls" :key="url" :url="url" :compact="true" :detail="false" class="url-preview"/>
|
<MkUrlPreview v-for="url in urls" :key="url" :url="url" :compact="true" :detail="false" class="url-preview"/>
|
||||||
<div v-if="appearNote.renote" class="renote"><XNoteSimple :note="appearNote.renote"/></div>
|
<div v-if="appearNote.renote" class="renote"><XNoteSimple :note="appearNote.renote" @click.stop="router.push(notePage(appearNote.renote))"/></div>
|
||||||
<button v-if="isLong && collapsed" class="fade _button" @click.stop.prevent="collapsed = false">
|
<button v-if="isLong && collapsed" class="fade _button" @click.stop="collapsed = false">
|
||||||
<span>{{ i18n.ts.showMore }}</span>
|
<span>{{ i18n.ts.showMore }}</span>
|
||||||
</button>
|
</button>
|
||||||
<button v-else-if="isLong && !collapsed" class="showLess _button" @click.stop.prevent="collapsed = true">
|
<button v-else-if="isLong && !collapsed" class="showLess _button" @click.stop="collapsed = true">
|
||||||
<span>{{ i18n.ts.showLess }}</span>
|
<span>{{ i18n.ts.showLess }}</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<MkA v-if="appearNote.channel && !inChannel" class="channel" :to="`/channels/${appearNote.channel.id}`"><i class="ph-television ph-bold ph-lg"></i> {{ appearNote.channel.name }}</MkA>
|
<MkA v-if="appearNote.channel && !inChannel" class="channel" :to="`/channels/${appearNote.channel.id}`" @click.stop><i class="ph-television ph-bold ph-lg"></i> {{ appearNote.channel.name }}</MkA>
|
||||||
</div>
|
</div>
|
||||||
<footer class="footer">
|
<footer ref="el" class="footer" @click.stop>
|
||||||
<XReactionsViewer ref="reactionsViewer" :note="appearNote"/>
|
<XReactionsViewer ref="reactionsViewer" :note="appearNote"/>
|
||||||
<button v-tooltip.noDelay.bottom="i18n.ts.reply" 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 ph-bold ph-lg"></i></template>
|
<i class="ph-arrow-u-up-left ph-bold ph-lg"></i>
|
||||||
<template v-else><i class="ph-arrow-bend-up-left ph-bold ph-lg"></i></template>
|
<template v-if="appearNote.repliesCount > 0">
|
||||||
<p v-if="appearNote.repliesCount > 0" class="count">{{ appearNote.repliesCount }}</p>
|
<p class="count">{{ appearNote.repliesCount }}</p>
|
||||||
|
</template>
|
||||||
</button>
|
</button>
|
||||||
<XRenoteButton ref="renoteButton" class="button" :note="appearNote" :count="appearNote.renoteCount"/>
|
<XRenoteButton ref="renoteButton" class="button" :note="appearNote" :count="appearNote.renoteCount"/>
|
||||||
<XStarButton v-if="appearNote.myReaction == null" ref="starButton" class="button" :note="appearNote"/>
|
<XStarButton v-if="appearNote.myReaction == null" ref="starButton" class="button" :note="appearNote"/>
|
||||||
|
@ -91,6 +93,7 @@
|
||||||
<i class="ph-dots-three-outline ph-bold ph-lg"></i>
|
<i class="ph-dots-three-outline ph-bold ph-lg"></i>
|
||||||
</button>
|
</button>
|
||||||
</footer>
|
</footer>
|
||||||
|
<!-- <MkNoteFooter :note="appearNote"></MkNoteFooter> -->
|
||||||
</div>
|
</div>
|
||||||
</article>
|
</article>
|
||||||
</div>
|
</div>
|
||||||
|
@ -113,15 +116,14 @@ import type * as misskey from 'calckey-js';
|
||||||
import MkNoteSub from '@/components/MkNoteSub.vue';
|
import MkNoteSub from '@/components/MkNoteSub.vue';
|
||||||
import XNoteHeader from '@/components/MkNoteHeader.vue';
|
import XNoteHeader from '@/components/MkNoteHeader.vue';
|
||||||
import XNoteSimple from '@/components/MkNoteSimple.vue';
|
import XNoteSimple from '@/components/MkNoteSimple.vue';
|
||||||
import XReactionsViewer from '@/components/MkReactionsViewer.vue';
|
|
||||||
import XMediaList from '@/components/MkMediaList.vue';
|
import XMediaList from '@/components/MkMediaList.vue';
|
||||||
import XCwButton from '@/components/MkCwButton.vue';
|
import XCwButton from '@/components/MkCwButton.vue';
|
||||||
import XPoll from '@/components/MkPoll.vue';
|
import XPoll from '@/components/MkPoll.vue';
|
||||||
import XStarButton from '@/components/MkStarButton.vue';
|
|
||||||
import XRenoteButton from '@/components/MkRenoteButton.vue';
|
import XRenoteButton from '@/components/MkRenoteButton.vue';
|
||||||
|
import XReactionsViewer from '@/components/MkReactionsViewer.vue';
|
||||||
|
import XStarButton from '@/components/MkStarButton.vue';
|
||||||
import XQuoteButton from '@/components/MkQuoteButton.vue';
|
import XQuoteButton from '@/components/MkQuoteButton.vue';
|
||||||
import MkUrlPreview from '@/components/MkUrlPreview.vue';
|
import MkUrlPreview from '@/components/MkUrlPreview.vue';
|
||||||
import MkInstanceTicker from '@/components/MkInstanceTicker.vue';
|
|
||||||
import MkVisibility from '@/components/MkVisibility.vue';
|
import MkVisibility from '@/components/MkVisibility.vue';
|
||||||
import { pleaseLogin } from '@/scripts/please-login';
|
import { pleaseLogin } from '@/scripts/please-login';
|
||||||
import { focusPrev, focusNext } from '@/scripts/focus';
|
import { focusPrev, focusNext } from '@/scripts/focus';
|
||||||
|
@ -187,7 +189,6 @@ const muted = ref(checkWordMute(appearNote, $i, defaultStore.state.mutedWords));
|
||||||
const translation = ref(null);
|
const translation = ref(null);
|
||||||
const translating = ref(false);
|
const translating = ref(false);
|
||||||
const urls = appearNote.text ? extractUrlFromMfm(mfm.parse(appearNote.text)).slice(0, 5) : null;
|
const urls = appearNote.text ? extractUrlFromMfm(mfm.parse(appearNote.text)).slice(0, 5) : null;
|
||||||
const showTicker = (defaultStore.state.instanceTicker === 'always') || (defaultStore.state.instanceTicker === 'remote' && appearNote.user.instance);
|
|
||||||
|
|
||||||
const keymap = {
|
const keymap = {
|
||||||
'r': () => reply(true),
|
'r': () => reply(true),
|
||||||
|
@ -296,6 +297,14 @@ function focusAfter() {
|
||||||
focusNext(el.value);
|
focusNext(el.value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function noteClick(e) {
|
||||||
|
if (document.getSelection().type === 'Range') {
|
||||||
|
e.stopPropagation();
|
||||||
|
} else {
|
||||||
|
router.push(notePage(appearNote))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function readPromo() {
|
function readPromo() {
|
||||||
os.api('promo/read', {
|
os.api('promo/read', {
|
||||||
noteId: appearNote.id,
|
noteId: appearNote.id,
|
||||||
|
@ -342,9 +351,13 @@ function readPromo() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&:hover > .article > .main > .footer > .button {
|
& > .article > .main {
|
||||||
|
&:hover, &:focus-within {
|
||||||
|
:deep(.footer .button) {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
> .reply-to {
|
> .reply-to {
|
||||||
& + .note-context {
|
& + .note-context {
|
||||||
|
@ -352,9 +365,9 @@ function readPromo() {
|
||||||
content: "";
|
content: "";
|
||||||
display: block;
|
display: block;
|
||||||
margin-bottom: -10px;
|
margin-bottom: -10px;
|
||||||
width: 2px;
|
margin-top: 16px;
|
||||||
background-color: var(--divider);
|
border-left: 2px solid var(--divider);
|
||||||
margin-inline: auto;
|
margin-left: calc((var(--avatarSize) / 2) - 1px);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -477,7 +490,6 @@ function readPromo() {
|
||||||
|
|
||||||
> .body {
|
> .body {
|
||||||
margin-top: .7em;
|
margin-top: .7em;
|
||||||
overflow: hidden;
|
|
||||||
|
|
||||||
> .cw {
|
> .cw {
|
||||||
cursor: default;
|
cursor: default;
|
||||||
|
@ -585,6 +597,10 @@ function readPromo() {
|
||||||
padding: 16px;
|
padding: 16px;
|
||||||
border: solid 1px var(--renote);
|
border: solid 1px var(--renote);
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
|
transition: background .2s;
|
||||||
|
&:hover, &:focus-within {
|
||||||
|
background-color: var(--panelHighlight);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -594,10 +610,13 @@ function readPromo() {
|
||||||
font-size: 80%;
|
font-size: 80%;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
> .footer {
|
> .footer {
|
||||||
|
position: relative;
|
||||||
|
z-index: 2;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
|
pointer-events: none; // Allow clicking anything w/out pointer-events: all; to open post
|
||||||
|
|
||||||
> .button {
|
> .button {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 8px;
|
padding: 8px;
|
||||||
|
@ -606,6 +625,8 @@ function readPromo() {
|
||||||
max-width: 3.5em;
|
max-width: 3.5em;
|
||||||
width: max-content;
|
width: max-content;
|
||||||
min-width: max-content;
|
min-width: max-content;
|
||||||
|
pointer-events: all;
|
||||||
|
transition: opacity .2s;
|
||||||
&:first-of-type {
|
&:first-of-type {
|
||||||
margin-left: -.5em;
|
margin-left: -.5em;
|
||||||
}
|
}
|
||||||
|
@ -627,6 +648,7 @@ function readPromo() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
> .reply {
|
> .reply {
|
||||||
border-top: solid 0.5px var(--divider);
|
border-top: solid 0.5px var(--divider);
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,8 +9,8 @@
|
||||||
:tabindex="!isDeleted ? '-1' : null"
|
:tabindex="!isDeleted ? '-1' : null"
|
||||||
:class="{ renote: isRenote }"
|
:class="{ renote: isRenote }"
|
||||||
>
|
>
|
||||||
<MkNoteSub v-for="note in conversation" :key="note.id" class="reply-to-more" :note="note" @click.self="router.push(notePage(note))"/>
|
<MkNoteSub v-for="note in conversation" :key="note.id" class="reply-to-more" :note="note"/>
|
||||||
<MkNoteSub v-if="appearNote.reply" :note="appearNote.reply" class="reply-to" @click.self="router.push(notePage(appearNote))"/>
|
<MkNoteSub v-if="appearNote.reply" :note="appearNote.reply" class="reply-to"/>
|
||||||
<div v-if="isRenote" class="renote">
|
<div v-if="isRenote" class="renote">
|
||||||
<MkAvatar class="avatar" :user="note.user"/>
|
<MkAvatar class="avatar" :user="note.user"/>
|
||||||
<i class="ph-repeat ph-bold ph-lg"></i>
|
<i class="ph-repeat ph-bold ph-lg"></i>
|
||||||
|
@ -29,7 +29,7 @@
|
||||||
<MkVisibility :note="note"/>
|
<MkVisibility :note="note"/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<article class="article" @contextmenu.stop="onContextmenu">
|
<article ref="noteEl" class="article" @contextmenu.stop="onContextmenu" tabindex="-1">
|
||||||
<header class="header">
|
<header class="header">
|
||||||
<MkAvatar class="avatar" :user="appearNote.user" :show-indicator="true"/>
|
<MkAvatar class="avatar" :user="appearNote.user" :show-indicator="true"/>
|
||||||
<div class="body">
|
<div class="body">
|
||||||
|
@ -48,12 +48,13 @@
|
||||||
</header>
|
</header>
|
||||||
<div class="main">
|
<div class="main">
|
||||||
<div class="body">
|
<div class="body">
|
||||||
<p v-if="appearNote.cw != null" class="cw">
|
<div v-if="appearNote.cw != null" class="cw">
|
||||||
<Mfm v-if="appearNote.cw != ''" class="text" :text="appearNote.cw" :author="appearNote.user" :i="$i" :custom-emojis="appearNote.emojis"/>
|
<Mfm v-if="appearNote.cw != ''" class="text" :text="appearNote.cw" :author="appearNote.user" :i="$i" :custom-emojis="appearNote.emojis"/>
|
||||||
|
<br/>
|
||||||
<XCwButton v-model="showContent" :note="appearNote"/>
|
<XCwButton v-model="showContent" :note="appearNote"/>
|
||||||
</p>
|
</div>
|
||||||
<div v-show="appearNote.cw == null || showContent" class="content">
|
<div v-show="appearNote.cw == null || showContent" class="content">
|
||||||
<div class="text" @click.self="router.push(notePage(appearNote))">
|
<div class="text">
|
||||||
<Mfm v-if="appearNote.text" :text="appearNote.text" :author="appearNote.user" :i="$i" :custom-emojis="appearNote.emojis"/>
|
<Mfm v-if="appearNote.text" :text="appearNote.text" :author="appearNote.user" :i="$i" :custom-emojis="appearNote.emojis"/>
|
||||||
<div v-if="translating || translation" class="translation">
|
<div v-if="translating || translation" class="translation">
|
||||||
<MkLoading v-if="translating" mini/>
|
<MkLoading v-if="translating" mini/>
|
||||||
|
@ -68,7 +69,7 @@
|
||||||
</div>
|
</div>
|
||||||
<XPoll v-if="appearNote.poll" ref="pollViewer" :note="appearNote" class="poll"/>
|
<XPoll v-if="appearNote.poll" ref="pollViewer" :note="appearNote" class="poll"/>
|
||||||
<MkUrlPreview v-for="url in urls" :key="url" :url="url" :compact="true" :detail="true" class="url-preview"/>
|
<MkUrlPreview v-for="url in urls" :key="url" :url="url" :compact="true" :detail="true" class="url-preview"/>
|
||||||
<div v-if="appearNote.renote" class="renote"><XNoteSimple :note="appearNote.renote"/></div>
|
<div v-if="appearNote.renote" class="renote"><XNoteSimple :note="appearNote.renote" @click.stop="router.push(notePage(appearNote.renote))"/></div>
|
||||||
</div>
|
</div>
|
||||||
<MkA v-if="appearNote.channel && !inChannel" class="channel" :to="`/channels/${appearNote.channel.id}`"><i class="ph-television ph-bold ph-lg"></i> {{ appearNote.channel.name }}</MkA>
|
<MkA v-if="appearNote.channel && !inChannel" class="channel" :to="`/channels/${appearNote.channel.id}`"><i class="ph-television ph-bold ph-lg"></i> {{ appearNote.channel.name }}</MkA>
|
||||||
</div>
|
</div>
|
||||||
|
@ -99,7 +100,7 @@
|
||||||
</footer>
|
</footer>
|
||||||
</div>
|
</div>
|
||||||
</article>
|
</article>
|
||||||
<MkNoteSub v-for="note in directReplies" :key="note.id" :note="note" class="reply" :conversation="replies" @click.self="router.push(notePage(note))"/>
|
<MkNoteSub v-for="note in directReplies" :key="note.id" :note="note" class="reply" :conversation="replies"/>
|
||||||
</div>
|
</div>
|
||||||
<div v-else class="_panel muted" @click="muted = false">
|
<div v-else class="_panel muted" @click="muted = false">
|
||||||
<I18n :src="i18n.ts.userSaysSomething" tag="small">
|
<I18n :src="i18n.ts.userSaysSomething" tag="small">
|
||||||
|
@ -113,7 +114,7 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { computed, inject, onMounted, onUnmounted, reactive, ref } from 'vue';
|
import { computed, inject, onMounted, onUnmounted, onUpdated, reactive, ref } from 'vue';
|
||||||
import * as mfm from 'mfm-js';
|
import * as mfm from 'mfm-js';
|
||||||
import type * as misskey from 'calckey-js';
|
import type * as misskey from 'calckey-js';
|
||||||
import MkNoteSub from '@/components/MkNoteSub.vue';
|
import MkNoteSub from '@/components/MkNoteSub.vue';
|
||||||
|
@ -175,6 +176,7 @@ const isRenote = (
|
||||||
);
|
);
|
||||||
|
|
||||||
const el = ref<HTMLElement>();
|
const el = ref<HTMLElement>();
|
||||||
|
const noteEl = $ref();
|
||||||
const menuButton = ref<HTMLElement>();
|
const menuButton = ref<HTMLElement>();
|
||||||
const starButton = ref<InstanceType<typeof XStarButton>>();
|
const starButton = ref<InstanceType<typeof XStarButton>>();
|
||||||
const renoteButton = ref<InstanceType<typeof XRenoteButton>>();
|
const renoteButton = ref<InstanceType<typeof XRenoteButton>>();
|
||||||
|
@ -192,6 +194,8 @@ const showTicker = (defaultStore.state.instanceTicker === 'always') || (defaultS
|
||||||
const conversation = ref<misskey.entities.Note[]>([]);
|
const conversation = ref<misskey.entities.Note[]>([]);
|
||||||
const replies = ref<misskey.entities.Note[]>([]);
|
const replies = ref<misskey.entities.Note[]>([]);
|
||||||
const directReplies = ref<misskey.entities.Note[]>([]);
|
const directReplies = ref<misskey.entities.Note[]>([]);
|
||||||
|
let isScrolling;
|
||||||
|
|
||||||
|
|
||||||
const keymap = {
|
const keymap = {
|
||||||
'r': () => reply(true),
|
'r': () => reply(true),
|
||||||
|
@ -281,20 +285,20 @@ function showRenoteMenu(viaKeyboard = false): void {
|
||||||
}
|
}
|
||||||
|
|
||||||
function focus() {
|
function focus() {
|
||||||
el.value.focus();
|
noteEl.focus();
|
||||||
}
|
}
|
||||||
|
|
||||||
function blur() {
|
function blur() {
|
||||||
el.value.blur();
|
noteEl.blur();
|
||||||
}
|
}
|
||||||
|
|
||||||
os.api('notes/children', {
|
os.api('notes/children', {
|
||||||
noteId: appearNote.id,
|
noteId: appearNote.id,
|
||||||
limit: 30,
|
limit: 30,
|
||||||
depth: 6,
|
depth: 12,
|
||||||
}).then(res => {
|
}).then(res => {
|
||||||
replies.value = res;
|
replies.value = res;
|
||||||
directReplies.value = res.filter(note => note.replyId === appearNote.id || note.renoteId === appearNote.id);
|
directReplies.value = res.filter(note => note.replyId === appearNote.id || note.renoteId === appearNote.id).reverse();
|
||||||
});
|
});
|
||||||
|
|
||||||
if (appearNote.replyId) {
|
if (appearNote.replyId) {
|
||||||
|
@ -302,6 +306,7 @@ if (appearNote.replyId) {
|
||||||
noteId: appearNote.replyId,
|
noteId: appearNote.replyId,
|
||||||
}).then(res => {
|
}).then(res => {
|
||||||
conversation.value = res.reverse();
|
conversation.value = res.reverse();
|
||||||
|
focus();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -322,20 +327,32 @@ function onNoteReplied(noteData: NoteUpdatedEvent): void {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
document.addEventListener("wheel", () => {
|
||||||
|
isScrolling = true;
|
||||||
|
})
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
stream.on("noteUpdated", onNoteReplied);
|
stream.on("noteUpdated", onNoteReplied);
|
||||||
|
isScrolling = false;
|
||||||
|
noteEl.scrollIntoView();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
onUpdated(() => {
|
||||||
|
if (!isScrolling) {
|
||||||
|
noteEl.scrollIntoView()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
stream.off("noteUpdated", onNoteReplied);
|
stream.off("noteUpdated", onNoteReplied);
|
||||||
});
|
});
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.lxwezrsl {
|
.lxwezrsl {
|
||||||
position: relative;
|
position: relative;
|
||||||
transition: box-shadow 0.1s ease;
|
transition: box-shadow 0.1s ease;
|
||||||
overflow: hidden;
|
|
||||||
contain: content;
|
contain: content;
|
||||||
|
|
||||||
&:focus-visible {
|
&:focus-visible {
|
||||||
|
@ -429,8 +446,14 @@ onUnmounted(() => {
|
||||||
|
|
||||||
> .article {
|
> .article {
|
||||||
padding: 32px;
|
padding: 32px;
|
||||||
|
padding-bottom: 6px;
|
||||||
|
&:last-child {
|
||||||
|
padding-bottom: 24px;
|
||||||
|
}
|
||||||
font-size: 1.2em;
|
font-size: 1.2em;
|
||||||
|
overflow: clip;
|
||||||
|
outline: none;
|
||||||
|
scroll-margin-top: calc(var(--stickyTop) + 20vh);
|
||||||
> .header {
|
> .header {
|
||||||
display: flex;
|
display: flex;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
@ -530,6 +553,10 @@ onUnmounted(() => {
|
||||||
padding: 16px;
|
padding: 16px;
|
||||||
border: solid 1px var(--renote);
|
border: solid 1px var(--renote);
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
|
transition: background .2s;
|
||||||
|
&:hover, &:focus-within {
|
||||||
|
background-color: var(--panelHighlight);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -577,19 +604,65 @@ onUnmounted(() => {
|
||||||
> .reply {
|
> .reply {
|
||||||
border-top: solid 0.5px var(--divider);
|
border-top: solid 0.5px var(--divider);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
padding-top: 24px;
|
||||||
|
padding-bottom: 10px;
|
||||||
@media (pointer: coarse) {
|
@media (pointer: coarse) {
|
||||||
cursor: default;
|
cursor: default;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
> .reply, .reply-to, .reply-to-more {
|
// Hover
|
||||||
transition: background-color 0.25s ease-in-out;
|
.reply :deep(.main), .reply-to, .reply-to-more, :deep(.more) {
|
||||||
|
position: relative;
|
||||||
|
&::before {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
inset: -12px -24px;
|
||||||
|
bottom: -0px;
|
||||||
|
background: var(--panelHighlight);
|
||||||
|
border-radius: var(--radius);
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity .2s;
|
||||||
|
z-index: -1;
|
||||||
|
}
|
||||||
|
&.reply-to, &.reply-to-more {
|
||||||
|
&::before {
|
||||||
|
inset: 0px 8px;
|
||||||
|
}
|
||||||
|
&:first-of-type::before {
|
||||||
|
top: 12px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// &::after {
|
||||||
|
// content: "";
|
||||||
|
// position: absolute;
|
||||||
|
// inset: -9999px;
|
||||||
|
// background: var(--modalBg);
|
||||||
|
// opacity: 0;
|
||||||
|
// z-index: -2;
|
||||||
|
// pointer-events: none;
|
||||||
|
// transition: opacity .2s;
|
||||||
|
// }
|
||||||
|
&.more::before {
|
||||||
|
inset: 0 !important;
|
||||||
|
}
|
||||||
|
&:hover, &:focus-within {
|
||||||
|
&::before {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// @media (pointer: coarse) {
|
||||||
|
// &:has(.button:focus-within) {
|
||||||
|
// z-index: 2;
|
||||||
|
// --X13: transparent;
|
||||||
|
// &::after {
|
||||||
|
// opacity: 1;
|
||||||
|
// backdrop-filter: var(--modalBgFilter);
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
|
||||||
&:hover {
|
|
||||||
background-color: var(--panelHighlight);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&.max-width_500px {
|
&.max-width_500px {
|
||||||
font-size: 0.9em;
|
font-size: 0.9em;
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
<header class="kkwtjztg">
|
<header class="kkwtjztg">
|
||||||
<div class="user-info">
|
<div class="user-info">
|
||||||
<div>
|
<div>
|
||||||
<MkA v-user-preview="note.user.id" class="name" :to="userPage(note.user)">
|
<MkA v-user-preview="note.user.id" class="name" :to="userPage(note.user)" @click.stop>
|
||||||
<MkUserName :user="note.user" class="mkusername">
|
<MkUserName :user="note.user" class="mkusername">
|
||||||
<span v-if="note.user.isBot" class="is-bot">bot</span>
|
<span v-if="note.user.isBot" class="is-bot">bot</span>
|
||||||
</MkUserName>
|
</MkUserName>
|
||||||
|
@ -47,6 +47,8 @@ const showTicker = (defaultStore.state.instanceTicker === 'always') || (defaultS
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.kkwtjztg {
|
.kkwtjztg {
|
||||||
|
position: relative;
|
||||||
|
z-index: 2;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
|
|
|
@ -6,6 +6,7 @@
|
||||||
<div class="body">
|
<div class="body">
|
||||||
<p v-if="note.cw != null" class="cw">
|
<p v-if="note.cw != null" class="cw">
|
||||||
<Mfm v-if="note.cw != ''" class="text" :text="note.cw" :author="note.user" :i="$i" :custom-emojis="note.emojis"/>
|
<Mfm v-if="note.cw != ''" class="text" :text="note.cw" :author="note.user" :i="$i" :custom-emojis="note.emojis"/>
|
||||||
|
<br/>
|
||||||
<XCwButton v-model="showContent" :note="note"/>
|
<XCwButton v-model="showContent" :note="note"/>
|
||||||
</p>
|
</p>
|
||||||
<div v-show="note.cw == null || showContent" class="content">
|
<div v-show="note.cw == null || showContent" class="content">
|
||||||
|
|
|
@ -1,28 +1,73 @@
|
||||||
<template>
|
<template>
|
||||||
<div v-size="{ max: [450] }" class="wrpstxzv" :class="{ children: depth > 1 }">
|
<div ref="el"
|
||||||
<div class="main" @click="router.push(notePage(note))">
|
v-size="{ max: [450, 500] }"
|
||||||
|
class="wrpstxzv"
|
||||||
|
:class="{ children: depth > 1, singleStart: replies.length == 1, firstColumn: depth == 1 && conversation }"
|
||||||
|
>
|
||||||
|
<div v-if="conversation && depth > 1" class="line"></div>
|
||||||
|
<div class="main" @click="noteClick">
|
||||||
<div class="avatar-container">
|
<div class="avatar-container">
|
||||||
<MkAvatar class="avatar" :user="note.user"/>
|
<MkAvatar class="avatar" :user="note.user"/>
|
||||||
<div class="line"></div>
|
<div v-if="(!conversation) || replies.length > 0" class="line"></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="body">
|
<div class="body">
|
||||||
<XNoteHeader class="header" :note="note" :mini="true"/>
|
<XNoteHeader class="header" :note="note" :mini="true"/>
|
||||||
<div class="body">
|
<div class="body">
|
||||||
<p v-if="note.cw != null" class="cw">
|
<p v-if="note.cw != null" class="cw">
|
||||||
|
<MkA v-if="note.replyId" :to="`/notes/${note.replyId}`" class="reply-icon" @click.stop>
|
||||||
|
<i class="ph-arrow-bend-left-up ph-bold ph-lg"></i>
|
||||||
|
</MkA>
|
||||||
|
<MkA v-if="conversation && note.renoteId && note.renoteId != parentId" :to="`/notes/${note.renoteId}`" class="reply-icon" @click.stop>
|
||||||
|
<i class="ph-quotes ph-bold ph-lg"></i>
|
||||||
|
</MkA>
|
||||||
<Mfm v-if="note.cw != ''" class="text" :text="note.cw" :author="note.user" :i="$i" :custom-emojis="note.emojis"/>
|
<Mfm v-if="note.cw != ''" class="text" :text="note.cw" :author="note.user" :i="$i" :custom-emojis="note.emojis"/>
|
||||||
|
<br/>
|
||||||
<XCwButton v-model="showContent" :note="note"/>
|
<XCwButton v-model="showContent" :note="note"/>
|
||||||
</p>
|
</p>
|
||||||
<div v-show="note.cw == null || showContent" class="content" @click="router.push(notePage(note))">
|
<div v-show="note.cw == null || showContent" class="content">
|
||||||
<MkSubNoteContent class="text" :note="note"/>
|
<MkSubNoteContent class="text" :note="note" :detailed="true" :parentId="note.parentId" :conversation="conversation"/>
|
||||||
|
</div>
|
||||||
|
<div v-if="translating || translation" class="translation">
|
||||||
|
<MkLoading v-if="translating" mini/>
|
||||||
|
<div v-else class="translated">
|
||||||
|
<b>{{ i18n.t('translatedFrom', { x: translation.sourceLang }) }}: </b>
|
||||||
|
<Mfm :text="translation.text" :author="appearNote.user" :i="$i" :custom-emojis="appearNote.emojis"/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<footer class="footer" @click.stop>
|
||||||
|
<XReactionsViewer ref="reactionsViewer" :note="appearNote"/>
|
||||||
|
<button v-tooltip.noDelay.bottom="i18n.ts.reply" class="button _button" @click="reply()">
|
||||||
|
<i class="ph-arrow-u-up-left ph-bold ph-lg"></i>
|
||||||
|
<template v-if="appearNote.repliesCount > 0">
|
||||||
|
<p class="count">{{ appearNote.repliesCount }}</p>
|
||||||
|
</template>
|
||||||
|
</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" v-tooltip.noDelay.bottom="i18n.ts.reaction" class="button _button" @click="react()">
|
||||||
|
<i class="ph-smiley ph-bold ph-lg"></i>
|
||||||
|
</button>
|
||||||
|
<button v-if="appearNote.myReaction != null" ref="reactButton" class="button _button reacted" @click="undoReact(appearNote)">
|
||||||
|
<i class="ph-minus ph-bold ph-lg"></i>
|
||||||
|
</button>
|
||||||
|
<XQuoteButton class="button" :note="appearNote"/>
|
||||||
|
<button ref="menuButton" v-tooltip.noDelay.bottom="i18n.ts.more" class="button _button" @click="menu()">
|
||||||
|
<i class="ph-dots-three-outline ph-bold ph-lg"></i>
|
||||||
|
</button>
|
||||||
|
</footer>
|
||||||
|
<!-- <MkNoteFooter :note="note" :directReplies="replies.length"></MkNoteFooter> -->
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<template v-if="conversation">
|
<template v-if="conversation">
|
||||||
<template v-if="depth < 5">
|
<template v-if="replies.length == 1">
|
||||||
<MkNoteSub v-for="reply in replies" :key="reply.id" :note="reply" class="reply" :conversation="conversation" :depth="depth + 1"/>
|
<MkNoteSub v-for="reply in replies" :key="reply.id" :note="reply" class="reply single" :conversation="conversation" :depth="depth" :parentId="note.replyId"/>
|
||||||
|
</template>
|
||||||
|
<template v-else-if="depth < 5">
|
||||||
|
<MkNoteSub v-for="reply in replies" :key="reply.id" :note="reply" class="reply" :conversation="conversation" :depth="depth + 1" :parentId="note.replyId"/>
|
||||||
</template>
|
</template>
|
||||||
<div v-else-if="replies.length > 0" class="more">
|
<div v-else-if="replies.length > 0" class="more">
|
||||||
|
<div class="line"></div>
|
||||||
<MkA class="text _link" :to="notePage(note)">{{ i18n.ts.continueThread }} <i class="ph-caret-double-right ph-bold ph-lg"></i></MkA>
|
<MkA class="text _link" :to="notePage(note)">{{ i18n.ts.continueThread }} <i class="ph-caret-double-right ph-bold ph-lg"></i></MkA>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
@ -30,21 +75,33 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { } from 'vue';
|
import { inject, ref } from 'vue';
|
||||||
|
import type { Ref } from 'vue';
|
||||||
import * as misskey from 'calckey-js';
|
import * as misskey from 'calckey-js';
|
||||||
import XNoteHeader from '@/components/MkNoteHeader.vue';
|
import XNoteHeader from '@/components/MkNoteHeader.vue';
|
||||||
import MkSubNoteContent from '@/components/MkSubNoteContent.vue';
|
import MkSubNoteContent from '@/components/MkSubNoteContent.vue';
|
||||||
|
import XReactionsViewer from '@/components/MkReactionsViewer.vue';
|
||||||
|
import XStarButton from '@/components/MkStarButton.vue';
|
||||||
|
import XRenoteButton from '@/components/MkRenoteButton.vue';
|
||||||
|
import XQuoteButton from '@/components/MkQuoteButton.vue';
|
||||||
import XCwButton from '@/components/MkCwButton.vue';
|
import XCwButton from '@/components/MkCwButton.vue';
|
||||||
|
import { pleaseLogin } from '@/scripts/please-login';
|
||||||
|
import { getNoteMenu } from '@/scripts/get-note-menu';
|
||||||
import { notePage } from '@/filters/note';
|
import { notePage } from '@/filters/note';
|
||||||
import { useRouter } from '@/router';
|
import { useRouter } from '@/router';
|
||||||
import * as os from '@/os';
|
import * as os from '@/os';
|
||||||
|
import { reactionPicker } from '@/scripts/reaction-picker';
|
||||||
import { i18n } from '@/i18n';
|
import { i18n } from '@/i18n';
|
||||||
|
import { deepClone } from '@/scripts/clone';
|
||||||
|
import { useNoteCapture } from '@/scripts/use-note-capture';
|
||||||
|
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
const props = withDefaults(defineProps<{
|
const props = withDefaults(defineProps<{
|
||||||
note: misskey.entities.Note;
|
note: misskey.entities.Note;
|
||||||
conversation?: misskey.entities.Note[];
|
conversation?: misskey.entities.Note[];
|
||||||
|
parentId?;
|
||||||
|
|
||||||
// how many notes are in between this one and the note being viewed in detail
|
// how many notes are in between this one and the note being viewed in detail
|
||||||
depth?: number;
|
depth?: number;
|
||||||
|
@ -52,17 +109,96 @@ const props = withDefaults(defineProps<{
|
||||||
depth: 1,
|
depth: 1,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
let note = $ref(deepClone(props.note));
|
||||||
|
|
||||||
|
const isRenote = (
|
||||||
|
note.renote != null &&
|
||||||
|
note.text == null &&
|
||||||
|
note.fileIds.length === 0 &&
|
||||||
|
note.poll == null
|
||||||
|
);
|
||||||
|
|
||||||
|
const el = ref<HTMLElement>();
|
||||||
|
const menuButton = ref<HTMLElement>();
|
||||||
|
const starButton = ref<InstanceType<typeof XStarButton>>();
|
||||||
|
const renoteButton = ref<InstanceType<typeof XRenoteButton>>();
|
||||||
|
const reactButton = ref<HTMLElement>();
|
||||||
|
let appearNote = $computed(() => isRenote ? note.renote as misskey.entities.Note : note);
|
||||||
|
const isDeleted = ref(false);
|
||||||
|
const translation = ref(null);
|
||||||
|
const translating = ref(false);
|
||||||
let showContent = $ref(false);
|
let showContent = $ref(false);
|
||||||
const replies: misskey.entities.Note[] = props.conversation?.filter(item => item.replyId === props.note.id || item.renoteId === props.note.id) ?? [];
|
const replies: misskey.entities.Note[] = props.conversation?.filter(item => item.replyId === props.note.id || item.renoteId === props.note.id).reverse() ?? [];
|
||||||
|
|
||||||
|
useNoteCapture({
|
||||||
|
rootEl: el,
|
||||||
|
note: $$(appearNote)
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
function reply(viaKeyboard = false): void {
|
||||||
|
pleaseLogin();
|
||||||
|
os.post({
|
||||||
|
reply: appearNote,
|
||||||
|
animation: !viaKeyboard,
|
||||||
|
}, () => {
|
||||||
|
focus();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function react(viaKeyboard = false): void {
|
||||||
|
pleaseLogin();
|
||||||
|
blur();
|
||||||
|
reactionPicker.show(reactButton.value, reaction => {
|
||||||
|
os.api('notes/reactions/create', {
|
||||||
|
noteId: appearNote.id,
|
||||||
|
reaction: reaction,
|
||||||
|
});
|
||||||
|
}, () => {
|
||||||
|
focus();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function undoReact(note): void {
|
||||||
|
const oldReaction = note.myReaction;
|
||||||
|
if (!oldReaction) return;
|
||||||
|
os.api('notes/reactions/delete', {
|
||||||
|
noteId: note.id,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentClipPage = inject<Ref<misskey.entities.Clip> | null>('currentClipPage', null);
|
||||||
|
|
||||||
|
|
||||||
|
function menu(viaKeyboard = false): void {
|
||||||
|
os.popupMenu(getNoteMenu({ note: note, translating, translation, menuButton, isDeleted, currentClipPage }), menuButton.value, {
|
||||||
|
viaKeyboard,
|
||||||
|
}).then(focus);
|
||||||
|
}
|
||||||
|
|
||||||
|
function focus() {
|
||||||
|
el.value.focus();
|
||||||
|
}
|
||||||
|
|
||||||
|
function blur() {
|
||||||
|
el.value.blur();
|
||||||
|
}
|
||||||
|
|
||||||
|
function noteClick(e) {
|
||||||
|
if (document.getSelection().type === 'Range') {
|
||||||
|
e.stopPropagation();
|
||||||
|
} else {
|
||||||
|
router.push(notePage(props.note))
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.wrpstxzv {
|
.wrpstxzv {
|
||||||
padding: 16px 32px;
|
padding: 16px 32px;
|
||||||
|
|
||||||
|
|
||||||
&.children {
|
&.children {
|
||||||
padding: 10px 0 0 16px;
|
padding: 10px 0 0 var(--indent);
|
||||||
|
padding-left: var(--indent) !important;
|
||||||
font-size: 1em;
|
font-size: 1em;
|
||||||
cursor: auto;
|
cursor: auto;
|
||||||
|
|
||||||
|
@ -71,6 +207,7 @@ const replies: misskey.entities.Note[] = props.conversation?.filter(item => item
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
> .main {
|
> .main {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
||||||
|
@ -89,6 +226,9 @@ const replies: misskey.entities.Note[] = props.conversation?.filter(item => item
|
||||||
flex: 1;
|
flex: 1;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
margin: 0 -200px;
|
||||||
|
padding: 0 200px;
|
||||||
|
overflow: clip;
|
||||||
@media (pointer: coarse) {
|
@media (pointer: coarse) {
|
||||||
cursor: default;
|
cursor: default;
|
||||||
}
|
}
|
||||||
|
@ -99,6 +239,17 @@ const replies: misskey.entities.Note[] = props.conversation?.filter(item => item
|
||||||
}
|
}
|
||||||
|
|
||||||
> .body {
|
> .body {
|
||||||
|
.reply-icon {
|
||||||
|
display: inline-block;
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: .2em .2em;
|
||||||
|
margin-right: .2em;
|
||||||
|
color: var(--accent);
|
||||||
|
transition: background .2s;
|
||||||
|
&:hover, &:focus {
|
||||||
|
background: var(--buttonHoverBg);
|
||||||
|
}
|
||||||
|
}
|
||||||
> .cw {
|
> .cw {
|
||||||
cursor: default;
|
cursor: default;
|
||||||
display: block;
|
display: block;
|
||||||
|
@ -110,24 +261,113 @@ const replies: misskey.entities.Note[] = props.conversation?.filter(item => item
|
||||||
margin-right: 8px;
|
margin-right: 8px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
> .content {
|
> .content {
|
||||||
> .text {
|
> .text {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
> .translation {
|
||||||
|
border: solid 0.5px var(--divider);
|
||||||
|
border-radius: var(--radius);
|
||||||
|
padding: 12px;
|
||||||
|
margin-top: 8px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
> .footer {
|
||||||
|
position: relative;
|
||||||
|
z-index: 2;
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
pointer-events: none; // Allow clicking anything w/out pointer-events: all; to open post
|
||||||
|
|
||||||
|
> .button {
|
||||||
|
margin: 0;
|
||||||
|
padding: 8px;
|
||||||
|
opacity: 0.7;
|
||||||
|
flex-grow: 1;
|
||||||
|
max-width: 3.5em;
|
||||||
|
width: max-content;
|
||||||
|
min-width: max-content;
|
||||||
|
pointer-events: all;
|
||||||
|
transition: opacity .2s;
|
||||||
|
&:first-of-type {
|
||||||
|
margin-left: -.5em;
|
||||||
|
}
|
||||||
|
&:hover {
|
||||||
|
color: var(--fgHighlighted);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
> .count {
|
||||||
|
display: inline;
|
||||||
|
margin: 0 0 0 8px;
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.reacted {
|
||||||
|
color: var(--accent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
&:first-child > .main > .body {
|
||||||
|
margin-top: -200px;
|
||||||
|
padding-top: 200px;
|
||||||
|
}
|
||||||
|
&.reply {
|
||||||
|
--avatarSize: 38px;
|
||||||
|
.avatar-container {
|
||||||
|
margin-right: 8px !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
> .reply, > .more {
|
> .reply, > .more {
|
||||||
border-left: solid 0.5px var(--divider);
|
|
||||||
margin-top: 10px;
|
margin-top: 10px;
|
||||||
|
&.single {
|
||||||
|
padding: 0 !important;
|
||||||
|
> .line {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
> .more {
|
> .more {
|
||||||
padding: 10px 0 0 16px;
|
display: flex;
|
||||||
|
padding-block: 10px;
|
||||||
|
font-weight: 600;
|
||||||
|
> .line {
|
||||||
|
flex-grow: 0 !important;
|
||||||
|
margin-top: -10px !important;
|
||||||
|
margin-bottom: 10px !important;
|
||||||
|
margin-right: 10px !important;
|
||||||
|
&::before {
|
||||||
|
border-left-style: dashed !important;
|
||||||
|
border-bottom-left-radius: 100px !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
i {
|
||||||
|
font-size: 1em !important;
|
||||||
|
vertical-align: middle !important;
|
||||||
|
}
|
||||||
|
a {
|
||||||
|
position: static;
|
||||||
|
&::before {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
}
|
||||||
|
&::after {
|
||||||
|
content: unset;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.reply, &.reply-to, &.reply-to-more {
|
||||||
|
> .main:hover, > .main:focus-within {
|
||||||
|
:deep(.footer .button) {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&.reply-to, &.reply-to-more {
|
&.reply-to, &.reply-to-more {
|
||||||
|
@ -135,7 +375,16 @@ const replies: misskey.entities.Note[] = props.conversation?.filter(item => item
|
||||||
&:first-child {
|
&:first-child {
|
||||||
padding-top: 30px;
|
padding-top: 30px;
|
||||||
}
|
}
|
||||||
.avatar-container {
|
.line::before {
|
||||||
|
margin-bottom: -16px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reply Lines
|
||||||
|
&.reply, &.reply-to, &.reply-to-more {
|
||||||
|
--indent: calc(var(--avatarSize) - 5px);
|
||||||
|
> .main {
|
||||||
|
> .avatar-container {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
@ -146,30 +395,90 @@ const replies: misskey.entities.Note[] = props.conversation?.filter(item => item
|
||||||
height: var(--avatarSize);
|
height: var(--avatarSize);
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
> .line {
|
}
|
||||||
|
}
|
||||||
|
.line {
|
||||||
|
position: relative;
|
||||||
width: var(--avatarSize);
|
width: var(--avatarSize);
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
|
margin-bottom: -10px;
|
||||||
&::before {
|
&::before {
|
||||||
content: "";
|
content: "";
|
||||||
display: block;
|
position: absolute;
|
||||||
width: 2px;
|
border-left: 2px solid var(--X13);
|
||||||
background-color: var(--divider);
|
margin-left: calc((var(--avatarSize) / 2) - 1px);
|
||||||
margin-inline: auto;
|
width: calc(var(--indent) / 2);
|
||||||
.note > & {
|
inset-block: 0;
|
||||||
margin-bottom: -16px;
|
min-height: 8px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
&.reply-to, &.reply-to-more {
|
||||||
|
> .main > .avatar-container > .line {
|
||||||
|
margin-bottom: 0px !important;
|
||||||
}
|
}
|
||||||
> .main > .body {
|
}
|
||||||
padding-bottom: 16px;
|
&.single, &.singleStart {
|
||||||
|
> .main > .avatar-container > .line {
|
||||||
|
margin-bottom: -10px !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.reply.children:not(:last-child) { // Line that goes through multiple replies
|
||||||
|
position: relative;
|
||||||
|
> .line {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
bottom: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Reply line connectors
|
||||||
|
.reply.children:not(.single) {
|
||||||
|
position: relative;
|
||||||
|
> .line {
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
&::after {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
border-left: 2px solid var(--X13);
|
||||||
|
border-bottom: 2px solid var(--X13);
|
||||||
|
margin-left: calc((var(--avatarSize) / 2) - 1px);
|
||||||
|
width: calc(var(--indent) / 2);
|
||||||
|
height: calc((var(--avatarSize) / 2));
|
||||||
|
border-bottom-left-radius: calc(var(--indent) / 2);
|
||||||
|
top: 8px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
&:not(:last-child) > .line::after {
|
||||||
|
mask: linear-gradient(to right, transparent 2px, black 2px);
|
||||||
|
-webkit-mask: linear-gradient(to right, transparent 2px, black 2px);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.max-width_500px {
|
||||||
|
:not(.reply) > & {
|
||||||
|
.reply {
|
||||||
|
--avatarSize: 24px;
|
||||||
|
--indent: calc(var(--avatarSize) - 4px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
&.firstColumn {
|
||||||
|
> .main, > .line, > .children:not(.single) > .line {
|
||||||
|
--avatarSize: 35px;
|
||||||
|
--indent: 35px;
|
||||||
|
}
|
||||||
|
> .children:not(.single) {
|
||||||
|
padding-left: 28px !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
&.max-width_450px {
|
&.max-width_450px {
|
||||||
padding: 14px 16px;
|
padding: 14px 16px;
|
||||||
&.reply-to, &.reply-to-more {
|
&.reply-to, &.reply-to-more {
|
||||||
|
padding: 14px 16px;
|
||||||
padding-top: 14px !important;
|
padding-top: 14px !important;
|
||||||
padding-bottom: 0 !important;
|
padding-bottom: 0 !important;
|
||||||
margin-bottom: 0 !important;
|
margin-bottom: 0 !important;
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="tivcixzd" :class="{ done: closed || isVoted }">
|
<div class="tivcixzd" :class="{ done: closed || isVoted }">
|
||||||
<ul>
|
<ul>
|
||||||
<li v-for="(choice, i) in note.poll.choices" :key="i" :class="{ voted: choice.voted }" @click="vote(i)">
|
<li v-for="(choice, i) in note.poll.choices" :key="i" :class="{ voted: choice.voted }" @click.stop="vote(i)">
|
||||||
<div class="backdrop" :style="{ 'width': `${showResult ? (choice.votes / total * 100) : 0}%` }"></div>
|
<div class="backdrop" :style="{ 'width': `${showResult ? (choice.votes / total * 100) : 0}%` }"></div>
|
||||||
<span>
|
<span>
|
||||||
<template v-if="choice.isVoted"><i class="ph-check ph-bold ph-lg"></i></template>
|
<template v-if="choice.isVoted"><i class="ph-check ph-bold ph-lg"></i></template>
|
||||||
|
@ -13,7 +13,7 @@
|
||||||
<p v-if="!readOnly">
|
<p v-if="!readOnly">
|
||||||
<span>{{ i18n.t('_poll.totalVotes', { n: total }) }}</span>
|
<span>{{ i18n.t('_poll.totalVotes', { n: total }) }}</span>
|
||||||
<span> · </span>
|
<span> · </span>
|
||||||
<a v-if="!closed && !isVoted" @click="showResult = !showResult">{{ showResult ? i18n.ts._poll.vote : i18n.ts._poll.showResult }}</a>
|
<a v-if="!closed && !isVoted" @click.stop="showResult = !showResult">{{ showResult ? i18n.ts._poll.vote : i18n.ts._poll.showResult }}</a>
|
||||||
<span v-if="isVoted">{{ i18n.ts._poll.voted }}</span>
|
<span v-if="isVoted">{{ i18n.ts._poll.voted }}</span>
|
||||||
<span v-else-if="closed">{{ i18n.ts._poll.closed }}</span>
|
<span v-else-if="closed">{{ i18n.ts._poll.closed }}</span>
|
||||||
<span v-if="remaining > 0"> · {{ timer }}</span>
|
<span v-if="remaining > 0"> · {{ timer }}</span>
|
||||||
|
|
|
@ -22,7 +22,7 @@
|
||||||
<span v-if="visibility === 'specified'"><i class="ph-envelope-simple-open ph-bold ph-lg"></i></span>
|
<span v-if="visibility === 'specified'"><i class="ph-envelope-simple-open ph-bold ph-lg"></i></span>
|
||||||
</button>
|
</button>
|
||||||
<button v-tooltip="i18n.ts.previewNoteText" class="_button preview" :class="{ active: showPreview }" @click="showPreview = !showPreview"><i class="ph-file-code ph-bold ph-lg"></i></button>
|
<button v-tooltip="i18n.ts.previewNoteText" class="_button preview" :class="{ active: showPreview }" @click="showPreview = !showPreview"><i class="ph-file-code ph-bold ph-lg"></i></button>
|
||||||
<button class="submit _buttonGradate" :disabled="!canPost" data-cy-open-post-form-submit @click="post">{{ submitText }}<i :class="reply ? 'ph-arrow-bend-up-left ph-bold ph-lg' : renote ? 'ph-quotes ph-bold ph-lg' : 'ph-paper-plane-tilt ph-bold ph-lg'"></i></button>
|
<button class="submit _buttonGradate" :disabled="!canPost" data-cy-open-post-form-submit @click="post">{{ submitText }}<i :class="reply ? 'ph-arrow-u-up-left ph-bold ph-lg' : renote ? 'ph-quotes ph-bold ph-lg' : 'ph-paper-plane-tilt ph-bold ph-lg'"></i></button>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
<div class="form" :class="{ fixed }">
|
<div class="form" :class="{ fixed }">
|
||||||
|
@ -91,6 +91,7 @@ import { instance } from '@/instance';
|
||||||
import { $i, getAccounts, openAccountMenu as openAccountMenu_ } from '@/account';
|
import { $i, getAccounts, openAccountMenu as openAccountMenu_ } from '@/account';
|
||||||
import { uploadFile } from '@/scripts/upload';
|
import { uploadFile } from '@/scripts/upload';
|
||||||
import { deepClone } from '@/scripts/clone';
|
import { deepClone } from '@/scripts/clone';
|
||||||
|
import { nyaize } from '@/scripts/nyaize';
|
||||||
import XCheatSheet from '@/components/MkCheatSheetDialog.vue';
|
import XCheatSheet from '@/components/MkCheatSheetDialog.vue';
|
||||||
|
|
||||||
const modal = inject('modal');
|
const modal = inject('modal');
|
||||||
|
@ -582,6 +583,10 @@ async function post() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ($i?.isCat) {
|
||||||
|
postData.text = nyaize(`${postData.text}`);
|
||||||
|
}
|
||||||
|
|
||||||
let token = undefined;
|
let token = undefined;
|
||||||
|
|
||||||
if (postAccount) {
|
if (postAccount) {
|
||||||
|
@ -796,6 +801,8 @@ onMounted(() => {
|
||||||
}
|
}
|
||||||
|
|
||||||
> .submit {
|
> .submit {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
margin: 16px 16px 16px 0;
|
margin: 16px 16px 16px 0;
|
||||||
padding: 0 12px;
|
padding: 0 12px;
|
||||||
line-height: 34px;
|
line-height: 34px;
|
||||||
|
|
|
@ -4,7 +4,7 @@
|
||||||
ref="buttonRef"
|
ref="buttonRef"
|
||||||
v-ripple="canToggle"
|
v-ripple="canToggle"
|
||||||
class="hkzvhatu _button"
|
class="hkzvhatu _button"
|
||||||
:class="{ reacted: note.myReaction == reaction, canToggle }"
|
:class="{ reacted: note.myReaction == reaction, canToggle, newlyAdded: !isInitial }"
|
||||||
@click="toggleReaction()"
|
@click="toggleReaction()"
|
||||||
>
|
>
|
||||||
<XReactionIcon class="icon" :reaction="reaction" :custom-emojis="note.emojis"/>
|
<XReactionIcon class="icon" :reaction="reaction" :custom-emojis="note.emojis"/>
|
||||||
|
@ -13,7 +13,7 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { computed, onMounted, ref, watch } from 'vue';
|
import { computed, ref } from 'vue';
|
||||||
import * as misskey from 'calckey-js';
|
import * as misskey from 'calckey-js';
|
||||||
import XDetails from '@/components/MkReactionsViewer.details.vue';
|
import XDetails from '@/components/MkReactionsViewer.details.vue';
|
||||||
import XReactionIcon from '@/components/MkReactionIcon.vue';
|
import XReactionIcon from '@/components/MkReactionIcon.vue';
|
||||||
|
@ -55,20 +55,6 @@ const toggleReaction = () => {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const anime = () => {
|
|
||||||
if (document.hidden) return;
|
|
||||||
|
|
||||||
// TODO: 新しくリアクションが付いたことが視覚的に分かりやすいアニメーション
|
|
||||||
};
|
|
||||||
|
|
||||||
watch(() => props.count, (newCount, oldCount) => {
|
|
||||||
if (oldCount < newCount) anime();
|
|
||||||
});
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
if (!props.isInitial) anime();
|
|
||||||
});
|
|
||||||
|
|
||||||
useTooltip(buttonRef, async (showing) => {
|
useTooltip(buttonRef, async (showing) => {
|
||||||
const reactions = await os.apiGet('notes/reactions', {
|
const reactions = await os.apiGet('notes/reactions', {
|
||||||
noteId: props.note.id,
|
noteId: props.note.id,
|
||||||
|
@ -97,7 +83,25 @@ useTooltip(buttonRef, async (showing) => {
|
||||||
margin: 2px;
|
margin: 2px;
|
||||||
padding: 0 6px;
|
padding: 0 6px;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
|
pointer-events: all;
|
||||||
|
&.newlyAdded {
|
||||||
|
animation: scaleInSmall .3s cubic-bezier(0,0,0,1.2);
|
||||||
|
:deep(.mk-emoji) {
|
||||||
|
animation: scaleIn .4s cubic-bezier(0.7, 0, 0, 1.5);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
:deep(.mk-emoji) {
|
||||||
|
transition: transform .4s cubic-bezier(0,0,0,6);
|
||||||
|
}
|
||||||
|
&.reacted :deep(.mk-emoji) {
|
||||||
|
transition: transform .4s cubic-bezier(0,0,0,1);
|
||||||
|
}
|
||||||
|
&:active {
|
||||||
|
:deep(.mk-emoji) {
|
||||||
|
transition: transform .4s cubic-bezier(0,0,0,1);
|
||||||
|
transform: scale(.85);
|
||||||
|
}
|
||||||
|
}
|
||||||
&.canToggle {
|
&.canToggle {
|
||||||
background: rgba(0, 0, 0, 0.05);
|
background: rgba(0, 0, 0, 0.05);
|
||||||
|
|
||||||
|
@ -119,6 +123,7 @@ useTooltip(buttonRef, async (showing) => {
|
||||||
|
|
||||||
> .count {
|
> .count {
|
||||||
color: var(--fgOnAccent);
|
color: var(--fgOnAccent);
|
||||||
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|
||||||
> .icon {
|
> .icon {
|
||||||
|
|
|
@ -21,7 +21,8 @@ const isMe = computed(() => $i && $i.id === props.note.userId);
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.tdflqwzn {
|
.tdflqwzn {
|
||||||
margin: 4px -2px 0 -2px;
|
margin-inline: -2px;
|
||||||
|
margin-top: .2em;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|
||||||
&:empty {
|
&:empty {
|
||||||
|
|
|
@ -2,22 +2,34 @@
|
||||||
<div class="wrmlmaau" :class="{ collapsed, isLong }">
|
<div class="wrmlmaau" :class="{ collapsed, isLong }">
|
||||||
<div class="body">
|
<div class="body">
|
||||||
<span v-if="note.deletedAt" style="opacity: 0.5">({{ i18n.ts.deleted }})</span>
|
<span v-if="note.deletedAt" style="opacity: 0.5">({{ i18n.ts.deleted }})</span>
|
||||||
<MkA v-if="note.replyId" class="reply" :to="`/notes/${note.replyId}`"><i class="ph-arrow-bend-up-left ph-bold ph-lg"></i></MkA>
|
<template v-if="!note.cw">
|
||||||
|
<MkA v-if="note.replyId" :to="`/notes/${note.replyId}`" class="reply-icon" @click.stop>
|
||||||
|
<i class="ph-arrow-bend-left-up ph-bold ph-lg"></i>
|
||||||
|
</MkA>
|
||||||
|
<MkA v-if="conversation && note.renoteId && note.renoteId != parentId" :to="`/notes/${note.renoteId}`" class="reply-icon" @click.stop>
|
||||||
|
<i class="ph-quotes ph-bold ph-lg"></i>
|
||||||
|
</MkA>
|
||||||
|
</template>
|
||||||
<Mfm v-if="note.text" :text="note.text" :author="note.user" :i="$i" :custom-emojis="note.emojis"/>
|
<Mfm v-if="note.text" :text="note.text" :author="note.user" :i="$i" :custom-emojis="note.emojis"/>
|
||||||
<MkA v-if="note.renoteId" class="rp" :to="`/notes/${note.renoteId}`">{{ i18n.ts.quoteAttached }}: ...</MkA>
|
<MkA v-if="note.renoteId" class="rp" :to="`/notes/${note.renoteId}`">{{ i18n.ts.quoteAttached }}: ...</MkA>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="note.files.length > 0">
|
<div v-if="note.files.length > 0">
|
||||||
<summary>({{ i18n.t('withNFiles', { n: note.files.length }) }})</summary>
|
|
||||||
<XMediaList :media-list="note.files"/>
|
<XMediaList :media-list="note.files"/>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="note.poll">
|
<div v-if="note.poll">
|
||||||
<summary>{{ i18n.ts.poll }}</summary>
|
<summary>{{ i18n.ts.poll }}</summary>
|
||||||
<XPoll :note="note"/>
|
<XPoll :note="note"/>
|
||||||
</div>
|
</div>
|
||||||
<button v-if="isLong && collapsed" class="fade _button" @click.stop.prevent="collapsed = false">
|
<template v-if="detailed">
|
||||||
|
<!-- <div v-if="note.renoteId" class="renote">
|
||||||
|
<XNoteSimple :note="note.renote"/>
|
||||||
|
</div> -->
|
||||||
|
<MkUrlPreview v-for="url in urls" :key="url" :url="url" :compact="true" :detail="false" class="url-preview"/>
|
||||||
|
</template>
|
||||||
|
<button v-if="isLong && collapsed" class="fade _button" @click.stop="collapsed = false">
|
||||||
<span>{{ i18n.ts.showMore }}</span>
|
<span>{{ i18n.ts.showMore }}</span>
|
||||||
</button>
|
</button>
|
||||||
<button v-if="isLong && !collapsed" class="showLess _button" @click.stop.prevent="collapsed = true">
|
<button v-if="isLong && !collapsed" class="showLess _button" @click.stop="collapsed = true">
|
||||||
<span>{{ i18n.ts.showLess }}</span>
|
<span>{{ i18n.ts.showLess }}</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
@ -26,15 +38,21 @@
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { } from 'vue';
|
import { } from 'vue';
|
||||||
import * as misskey from 'calckey-js';
|
import * as misskey from 'calckey-js';
|
||||||
|
import * as mfm from 'mfm-js';
|
||||||
|
import XNoteSimple from '@/components/MkNoteSimple.vue';
|
||||||
import XMediaList from '@/components/MkMediaList.vue';
|
import XMediaList from '@/components/MkMediaList.vue';
|
||||||
import XPoll from '@/components/MkPoll.vue';
|
import XPoll from '@/components/MkPoll.vue';
|
||||||
|
import MkUrlPreview from '@/components/MkUrlPreview.vue';
|
||||||
|
import { extractUrlFromMfm } from '@/scripts/extract-url-from-mfm';
|
||||||
import { i18n } from '@/i18n';
|
import { i18n } from '@/i18n';
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
note: misskey.entities.Note;
|
note: misskey.entities.Note;
|
||||||
|
parentId?;
|
||||||
|
conversation?;
|
||||||
|
detailed?: boolean;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
|
|
||||||
const isLong = (
|
const isLong = (
|
||||||
props.note.cw == null && props.note.text != null && (
|
props.note.cw == null && props.note.text != null && (
|
||||||
(props.note.text.split('\n').length > 9) ||
|
(props.note.text.split('\n').length > 9) ||
|
||||||
|
@ -42,6 +60,8 @@ const isLong = (
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
const collapsed = $ref(props.note.cw == null && isLong);
|
const collapsed = $ref(props.note.cw == null && isLong);
|
||||||
|
const urls = props.note.text ? extractUrlFromMfm(mfm.parse(props.note.text)) : null;
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
@ -49,16 +69,26 @@ const collapsed = $ref(props.note.cw == null && isLong);
|
||||||
overflow-wrap: break-word;
|
overflow-wrap: break-word;
|
||||||
|
|
||||||
> .body {
|
> .body {
|
||||||
> .reply {
|
|
||||||
margin-right: 6px;
|
|
||||||
color: var(--accent);
|
|
||||||
}
|
|
||||||
|
|
||||||
> .rp {
|
> .rp {
|
||||||
margin-left: 4px;
|
margin-left: 4px;
|
||||||
font-style: oblique;
|
font-style: oblique;
|
||||||
color: var(--renote);
|
color: var(--renote);
|
||||||
}
|
}
|
||||||
|
.reply-icon {
|
||||||
|
display: inline-block;
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: .2em .2em;
|
||||||
|
margin-right: .2em;
|
||||||
|
color: var(--accent);
|
||||||
|
transition: background .2s;
|
||||||
|
&:hover, &:focus {
|
||||||
|
background: var(--buttonHoverBg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
> .mk-url-preview {
|
||||||
|
margin-top: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.collapsed {
|
&.collapsed {
|
||||||
|
|
|
@ -1,12 +1,12 @@
|
||||||
<template>
|
<template>
|
||||||
<div v-if="playerEnabled" class="player" :style="`padding: ${(player.height || 0) / (player.width || 1) * 100}% 0 0`">
|
<div v-if="playerEnabled" class="player" :style="`padding: ${(player.height || 0) / (player.width || 1) * 100}% 0 0`" @click.stop>
|
||||||
<button class="disablePlayer" :title="i18n.ts.disablePlayer" @click="playerEnabled = false"><i class="ph-x ph-bold ph-lg"></i></button>
|
<button class="disablePlayer" :title="i18n.ts.disablePlayer" @click="playerEnabled = false"><i class="ph-x ph-bold ph-lg"></i></button>
|
||||||
<iframe :src="player.url + (player.url.match(/\?/) ? '&autoplay=1&auto_play=1' : '?autoplay=1&auto_play=1')" :width="player.width || '100%'" :heigth="player.height || 250" frameborder="0" allow="autoplay; encrypted-media" allowfullscreen/>
|
<iframe :src="player.url + (player.url.match(/\?/) ? '&autoplay=1&auto_play=1' : '?autoplay=1&auto_play=1')" :width="player.width || '100%'" :heigth="player.height || 250" frameborder="0" allow="autoplay; encrypted-media" allowfullscreen/>
|
||||||
</div>
|
</div>
|
||||||
<div v-else-if="tweetId && tweetExpanded" ref="twitter" class="twitter">
|
<div v-else-if="tweetId && tweetExpanded" ref="twitter" class="twitter" @click.stop>
|
||||||
<iframe ref="tweet" scrolling="no" frameborder="no" :style="{ position: 'relative', width: '100%', height: `${tweetHeight}px` }" :src="`https://platform.twitter.com/embed/index.html?embedId=${embedId}&hideCard=false&hideThread=false&lang=en&theme=${$store.state.darkMode ? 'dark' : 'light'}&id=${tweetId}`"></iframe>
|
<iframe ref="tweet" scrolling="no" frameborder="no" :style="{ position: 'relative', width: '100%', height: `${tweetHeight}px` }" :src="`https://platform.twitter.com/embed/index.html?embedId=${embedId}&hideCard=false&hideThread=false&lang=en&theme=${$store.state.darkMode ? 'dark' : 'light'}&id=${tweetId}`"></iframe>
|
||||||
</div>
|
</div>
|
||||||
<div v-else v-size="{ max: [400, 350] }" class="mk-url-preview">
|
<div v-else v-size="{ max: [400, 350] }" class="mk-url-preview" @click.stop>
|
||||||
<transition :name="$store.state.animation ? 'zoom' : ''" mode="out-in">
|
<transition :name="$store.state.animation ? 'zoom' : ''" mode="out-in">
|
||||||
<component :is="self ? 'MkA' : 'a'" v-if="!fetching" class="link" :class="{ compact }" :[attr]="self ? url.substr(local.length) : url" rel="nofollow noopener" :target="target" :title="url">
|
<component :is="self ? 'MkA' : 'a'" v-if="!fetching" class="link" :class="{ compact }" :[attr]="self ? url.substr(local.length) : url" rel="nofollow noopener" :target="target" :title="url">
|
||||||
<div v-if="thumbnail" class="thumbnail" :style="`background-image: url('${thumbnail}')`">
|
<div v-if="thumbnail" class="thumbnail" :style="`background-image: url('${thumbnail}')`">
|
||||||
|
@ -214,9 +214,10 @@ onUnmounted(() => {
|
||||||
border: 1px solid var(--divider);
|
border: 1px solid var(--divider);
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
transition: background .2s;
|
||||||
&:hover {
|
&:hover, &:focus-within {
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
|
background-color: var(--panelHighlight);
|
||||||
> article > header > h1 {
|
> article > header > h1 {
|
||||||
text-decoration: underline;
|
text-decoration: underline;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
<template>
|
<template>
|
||||||
<a :href="to" :class="active ? activeClass : null" @click.prevent="nav" @contextmenu.prevent.stop="onContextmenu">
|
<a :href="to" :class="active ? activeClass : null" @click.prevent="nav" @contextmenu.prevent.stop="onContextmenu" @click.stop>
|
||||||
<slot></slot>
|
<slot></slot>
|
||||||
</a>
|
</a>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
<img class="inner" :src="url" decoding="async"/>
|
<img class="inner" :src="url" decoding="async"/>
|
||||||
<MkUserOnlineIndicator v-if="showIndicator && user.instance == null" class="indicator" :user="user"/>
|
<MkUserOnlineIndicator v-if="showIndicator && user.instance == null" class="indicator" :user="user"/>
|
||||||
</span>
|
</span>
|
||||||
<MkA v-else v-user-preview="disablePreview ? undefined : user.id" class="eiwwqkts _noSelect" :class="{ cat: user.isCat, square: $store.state.squareAvatars }" :style="{ color }" :to="userPage(user)" :title="acct(user)" :target="target">
|
<MkA v-else v-user-preview="disablePreview ? undefined : user.id" class="eiwwqkts _noSelect" :class="{ cat: user.isCat, square: $store.state.squareAvatars }" :style="{ color }" :to="userPage(user)" :title="acct(user)" :target="target" @click.stop>
|
||||||
<img class="inner" :src="url" decoding="async"/>
|
<img class="inner" :src="url" decoding="async"/>
|
||||||
<MkUserOnlineIndicator v-if="showIndicator && user.instance == null" class="indicator" :user="user"/>
|
<MkUserOnlineIndicator v-if="showIndicator && user.instance == null" class="indicator" :user="user"/>
|
||||||
</MkA>
|
</MkA>
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
<template>
|
<template>
|
||||||
<component
|
<component
|
||||||
:is="self ? 'MkA' : 'a'" ref="el" class="ieqqeuvs _link" :[attr]="self ? url.substr(local.length) : url" :rel="rel" :target="target"
|
:is="self ? 'MkA' : 'a'" ref="el" class="ieqqeuvs _link" :[attr]="self ? url.substr(local.length) : url" :rel="rel" :target="target"
|
||||||
@contextmenu.stop="() => {}"
|
@contextmenu.stop="() => {}" @click.stop
|
||||||
>
|
>
|
||||||
<template v-if="!self">
|
<template v-if="!self">
|
||||||
<span class="schema">{{ schema }}//</span>
|
<span class="schema">{{ schema }}//</span>
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
<template>
|
<template>
|
||||||
<Mfm :text="user.name || user.username" :plain="true" :nowrap="nowrap" :custom-emojis="user.emojis"/>
|
<Mfm :class="$style.root" :text="user.name || user.username" :plain="true" :nowrap="nowrap" :custom-emojis="user.emojis"/>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
|
@ -13,3 +13,9 @@ const props = withDefaults(defineProps<{
|
||||||
nowrap: true,
|
nowrap: true,
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" module>
|
||||||
|
.root {
|
||||||
|
unicode-bidi: isolate;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
|
@ -547,7 +547,7 @@
|
||||||
{ "category": "animals_and_nature", "char": "🦤", "name": "dodo", "keywords": ["animal", "nature"] },
|
{ "category": "animals_and_nature", "char": "🦤", "name": "dodo", "keywords": ["animal", "nature"] },
|
||||||
{ "category": "animals_and_nature", "char": "🪶", "name": "feather", "keywords": ["animal", "nature"] },
|
{ "category": "animals_and_nature", "char": "🪶", "name": "feather", "keywords": ["animal", "nature"] },
|
||||||
{ "category": "animals_and_nature", "char": "🦭", "name": "seal", "keywords": ["animal", "nature"] },
|
{ "category": "animals_and_nature", "char": "🦭", "name": "seal", "keywords": ["animal", "nature"] },
|
||||||
{ "category": "animals_and_nature", "char": "🐾", "name": "paw_prints", "keywords": ["animal", "tracking", "footprints", "dog", "cat", "pet", "feet"] },
|
{ "category": "animals_and_nature", "char": "🐾", "name": "paw_prints", "keywords": ["animal", "tracking", "footprints", "dog", "cat", "pet", "feet", "paws", "kitty"] },
|
||||||
{ "category": "animals_and_nature", "char": "🐉", "name": "dragon", "keywords": ["animal", "myth", "nature", "chinese", "green"] },
|
{ "category": "animals_and_nature", "char": "🐉", "name": "dragon", "keywords": ["animal", "myth", "nature", "chinese", "green"] },
|
||||||
{ "category": "animals_and_nature", "char": "🐲", "name": "dragon_face", "keywords": ["animal", "myth", "nature", "chinese", "green"] },
|
{ "category": "animals_and_nature", "char": "🐲", "name": "dragon_face", "keywords": ["animal", "myth", "nature", "chinese", "green"] },
|
||||||
{ "category": "animals_and_nature", "char": "🦧", "name": "orangutan", "keywords": ["animal", "nature"] },
|
{ "category": "animals_and_nature", "char": "🦧", "name": "orangutan", "keywords": ["animal", "nature"] },
|
||||||
|
|
|
@ -359,6 +359,40 @@ export function inputText(props: {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function inputParagraph(props: {
|
||||||
|
title?: string | null;
|
||||||
|
text?: string | null;
|
||||||
|
placeholder?: string | null;
|
||||||
|
default?: string | null;
|
||||||
|
}): Promise<
|
||||||
|
| { canceled: true; result: undefined }
|
||||||
|
| {
|
||||||
|
canceled: false;
|
||||||
|
result: string;
|
||||||
|
}
|
||||||
|
> {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
popup(
|
||||||
|
defineAsyncComponent(() => import("@/components/MkDialog.vue")),
|
||||||
|
{
|
||||||
|
title: props.title,
|
||||||
|
text: props.text,
|
||||||
|
input: {
|
||||||
|
type: "paragraph",
|
||||||
|
placeholder: props.placeholder,
|
||||||
|
default: props.default,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
done: (result) => {
|
||||||
|
resolve(result ? result : { canceled: true });
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"closed",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
export function inputNumber(props: {
|
export function inputNumber(props: {
|
||||||
title?: string | null;
|
title?: string | null;
|
||||||
text?: string | null;
|
text?: string | null;
|
||||||
|
|
|
@ -22,6 +22,9 @@
|
||||||
<template #label>{{ i18n.ts.tags }}</template>
|
<template #label>{{ i18n.ts.tags }}</template>
|
||||||
<template #caption>{{ i18n.ts.setMultipleBySeparatingWithSpace }}</template>
|
<template #caption>{{ i18n.ts.setMultipleBySeparatingWithSpace }}</template>
|
||||||
</MkInput>
|
</MkInput>
|
||||||
|
<MkTextarea v-model="license" class="_formBlock">
|
||||||
|
<template #label>{{ i18n.ts.license }}</template>
|
||||||
|
</MkTextarea>
|
||||||
<MkButton danger @click="del()"><i class="ph-trash ph-bold ph-lg"></i> {{ i18n.ts.delete }}</MkButton>
|
<MkButton danger @click="del()"><i class="ph-trash ph-bold ph-lg"></i> {{ i18n.ts.delete }}</MkButton>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -33,6 +36,7 @@ import { } from 'vue';
|
||||||
import XModalWindow from '@/components/MkModalWindow.vue';
|
import XModalWindow from '@/components/MkModalWindow.vue';
|
||||||
import MkButton from '@/components/MkButton.vue';
|
import MkButton from '@/components/MkButton.vue';
|
||||||
import MkInput from '@/components/form/input.vue';
|
import MkInput from '@/components/form/input.vue';
|
||||||
|
import MkTextarea from '@/components/form/textarea.vue';
|
||||||
import * as os from '@/os';
|
import * as os from '@/os';
|
||||||
import { unique } from '@/scripts/array';
|
import { unique } from '@/scripts/array';
|
||||||
import { i18n } from '@/i18n';
|
import { i18n } from '@/i18n';
|
||||||
|
@ -47,6 +51,7 @@ let name: string = $ref(props.emoji.name);
|
||||||
let category: string = $ref(props.emoji.category);
|
let category: string = $ref(props.emoji.category);
|
||||||
let aliases: string = $ref(props.emoji.aliases.join(' '));
|
let aliases: string = $ref(props.emoji.aliases.join(' '));
|
||||||
let categories: string[] = $ref(emojiCategories);
|
let categories: string[] = $ref(emojiCategories);
|
||||||
|
let license: string = $ref(props.emoji.license ?? '');
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
(ev: 'done', v: { deleted?: boolean, updated?: any }): void,
|
(ev: 'done', v: { deleted?: boolean, updated?: any }): void,
|
||||||
|
@ -63,6 +68,7 @@ async function update() {
|
||||||
name,
|
name,
|
||||||
category,
|
category,
|
||||||
aliases: aliases.split(' '),
|
aliases: aliases.split(' '),
|
||||||
|
license: license === '' ? null : license,
|
||||||
});
|
});
|
||||||
|
|
||||||
emit('done', {
|
emit('done', {
|
||||||
|
@ -71,6 +77,7 @@ async function update() {
|
||||||
name,
|
name,
|
||||||
category,
|
category,
|
||||||
aliases: aliases.split(' '),
|
aliases: aliases.split(' '),
|
||||||
|
license: license === '' ? null : license,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -18,6 +18,7 @@
|
||||||
<MkButton inline @click="addTagBulk">Add tag</MkButton>
|
<MkButton inline @click="addTagBulk">Add tag</MkButton>
|
||||||
<MkButton inline @click="removeTagBulk">Remove tag</MkButton>
|
<MkButton inline @click="removeTagBulk">Remove tag</MkButton>
|
||||||
<MkButton inline @click="setTagBulk">Set tag</MkButton>
|
<MkButton inline @click="setTagBulk">Set tag</MkButton>
|
||||||
|
<MkButton inline @click="setLicenseBulk">Set license</MkButton>
|
||||||
<MkButton inline danger @click="delBulk">Delete</MkButton>
|
<MkButton inline danger @click="delBulk">Delete</MkButton>
|
||||||
</div>
|
</div>
|
||||||
<MkPagination ref="emojisPaginationComponent" :pagination="pagination">
|
<MkPagination ref="emojisPaginationComponent" :pagination="pagination">
|
||||||
|
@ -258,6 +259,18 @@ const setTagBulk = async () => {
|
||||||
emojisPaginationComponent.value.reload();
|
emojisPaginationComponent.value.reload();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const setLicenseBulk = async () => {
|
||||||
|
const { canceled, result } = await os.inputParagraph({
|
||||||
|
title: 'License',
|
||||||
|
});
|
||||||
|
if (canceled) return;
|
||||||
|
await os.apiWithDialog('admin/emoji/set-license-bulk', {
|
||||||
|
ids: selectedEmojis.value,
|
||||||
|
license: result,
|
||||||
|
});
|
||||||
|
emojisPaginationComponent.value.reload();
|
||||||
|
};
|
||||||
|
|
||||||
const delBulk = async () => {
|
const delBulk = async () => {
|
||||||
const { canceled } = await os.confirm({
|
const { canceled } = await os.confirm({
|
||||||
type: 'warning',
|
type: 'warning',
|
||||||
|
|
|
@ -13,7 +13,7 @@
|
||||||
<MkInfo v-if="noEmailServer" warn class="info">{{ i18n.ts.noEmailServerWarning }} <MkA to="/admin/email-settings" class="_link">{{ i18n.ts.configure }}</MkA></MkInfo>
|
<MkInfo v-if="noEmailServer" warn class="info">{{ i18n.ts.noEmailServerWarning }} <MkA to="/admin/email-settings" class="_link">{{ i18n.ts.configure }}</MkA></MkInfo>
|
||||||
<MkInfo v-if="updateAvailable" warn class="info">{{ i18n.ts.updateAvailable }} <a href="https://codeberg.org/calckey/calckey/releases" target="_bank" class="_link">{{ i18n.ts.check }}</a></MkInfo>
|
<MkInfo v-if="updateAvailable" warn class="info">{{ i18n.ts.updateAvailable }} <a href="https://codeberg.org/calckey/calckey/releases" target="_bank" class="_link">{{ i18n.ts.check }}</a></MkInfo>
|
||||||
|
|
||||||
<MkSuperMenu :def="menuDef" :grid="currentPage?.route.name == null"></MkSuperMenu>
|
<MkSuperMenu :def="menuDef" :grid="narrow"></MkSuperMenu>
|
||||||
</div>
|
</div>
|
||||||
</MkSpacer>
|
</MkSpacer>
|
||||||
</div>
|
</div>
|
||||||
|
@ -219,6 +219,12 @@ onUnmounted(() => {
|
||||||
ro.disconnect();
|
ro.disconnect();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
watch(router.currentRef, (to) => {
|
||||||
|
if (to.route.path === "/admin" && to.child?.route.name == null && !narrow) {
|
||||||
|
router.replace('/admin/overview');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
provideMetadataReceiver((info) => {
|
provideMetadataReceiver((info) => {
|
||||||
if (info == null) {
|
if (info == null) {
|
||||||
childInfo = null;
|
childInfo = null;
|
||||||
|
|
|
@ -51,13 +51,6 @@ const props = defineProps<{
|
||||||
|
|
||||||
let channel = $ref(null);
|
let channel = $ref(null);
|
||||||
let showBanner = $ref(true);
|
let showBanner = $ref(true);
|
||||||
const pagination = {
|
|
||||||
endpoint: 'channels/timeline' as const,
|
|
||||||
limit: 10,
|
|
||||||
params: computed(() => ({
|
|
||||||
channelId: props.channelId,
|
|
||||||
})),
|
|
||||||
};
|
|
||||||
|
|
||||||
watch(() => props.channelId, async () => {
|
watch(() => props.channelId, async () => {
|
||||||
channel = await os.api('channels/show', {
|
channel = await os.api('channels/show', {
|
||||||
|
@ -66,14 +59,23 @@ watch(() => props.channelId, async () => {
|
||||||
}, { immediate: true });
|
}, { immediate: true });
|
||||||
|
|
||||||
function edit() {
|
function edit() {
|
||||||
router.push(`/channels/${channel.id}/edit`);
|
router.push(`/channels/${channel?.id}/edit`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const headerActions = $computed(() => channel && channel.userId ? [{
|
const headerActions = $computed(() => [
|
||||||
|
...(
|
||||||
|
channel
|
||||||
|
&& channel?.userId === $i?.id
|
||||||
|
? [
|
||||||
|
{
|
||||||
icon: 'ph-gear-six ph-bold ph-lg',
|
icon: 'ph-gear-six ph-bold ph-lg',
|
||||||
text: i18n.ts.edit,
|
text: i18n.ts.edit,
|
||||||
handler: edit,
|
handler: edit,
|
||||||
}] : null);
|
}
|
||||||
|
]
|
||||||
|
: []
|
||||||
|
),
|
||||||
|
]);
|
||||||
|
|
||||||
const headerTabs = $computed(() => []);
|
const headerTabs = $computed(() => []);
|
||||||
|
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
<img :src="emoji.url" class="img" :alt="emoji.name"/>
|
<img :src="emoji.url" class="img" :alt="emoji.name"/>
|
||||||
<div class="body">
|
<div class="body">
|
||||||
<div class="name _monospace">{{ emoji.name }}</div>
|
<div class="name _monospace">{{ emoji.name }}</div>
|
||||||
<div class="info">{{ emoji.aliases.join(' ') }}</div>
|
<div class="info">{{ emoji.aliases.join(" ") }}</div>
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
</template>
|
</template>
|
||||||
|
@ -20,15 +20,26 @@ const props = defineProps<{
|
||||||
|
|
||||||
function menu(ev) {
|
function menu(ev) {
|
||||||
os.popupMenu([{
|
os.popupMenu([{
|
||||||
type: 'label',
|
type: "label",
|
||||||
text: ':' + props.emoji.name + ':',
|
text: ":" + props.emoji.name + ":",
|
||||||
}, {
|
}, {
|
||||||
text: i18n.ts.copy,
|
text: i18n.ts.copy,
|
||||||
icon: 'ph-clipboard-text ph-bold ph-lg',
|
icon: "ph-clipboard-text ph-bold ph-lg",
|
||||||
action: () => {
|
action: () => {
|
||||||
copyToClipboard(`:${props.emoji.name}:`);
|
copyToClipboard(`:${props.emoji.name}:`);
|
||||||
os.success();
|
os.success();
|
||||||
}
|
},
|
||||||
|
}, {
|
||||||
|
text: i18n.ts.license,
|
||||||
|
icon: "ph-info ph-bold ph-lg",
|
||||||
|
action: () => {
|
||||||
|
os.apiGet("emoji", { name: props.emoji.name }).then(res => {
|
||||||
|
os.alert({
|
||||||
|
type: "info",
|
||||||
|
text: `${res.license || i18n.ts.notSet}`,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
}], ev.currentTarget ?? ev.target);
|
}], ev.currentTarget ?? ev.target);
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -18,7 +18,7 @@
|
||||||
<button class="_button" @click="chooseFile"><i class="ph-upload ph-bold ph-lg"></i></button>
|
<button class="_button" @click="chooseFile"><i class="ph-upload ph-bold ph-lg"></i></button>
|
||||||
<button class="_button" @click="insertEmoji"><i class="ph-smiley ph-bold ph-lg"></i></button>
|
<button class="_button" @click="insertEmoji"><i class="ph-smiley ph-bold ph-lg"></i></button>
|
||||||
<button class="send _button" :disabled="!canSend || sending" :title="i18n.ts.send" @click="send">
|
<button class="send _button" :disabled="!canSend || sending" :title="i18n.ts.send" @click="send">
|
||||||
<template v-if="!sending"><i class="ph-paper-plane-tilt-bold ph-lg"></i></template><template v-if="sending"><i class="ph-circle-notch ph-bold ph-lg fa-pulse ph-fw ph-lg"></i></template>
|
<template v-if="!sending"><i class="ph-paper-plane-tilt ph-bold ph-lg"></i></template><template v-if="sending"><i class="ph-circle-notch ph-bold ph-lg fa-pulse ph-fw ph-lg"></i></template>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
|
@ -57,7 +57,7 @@ const typing = throttle(3000, () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
let draftKey = $computed(() => props.user ? 'user:' + props.user.id : 'group:' + props.group?.id);
|
let draftKey = $computed(() => props.user ? 'user:' + props.user.id : 'group:' + props.group?.id);
|
||||||
let canSend = $computed(() => (text != null && text !== '') || file != null);
|
let canSend = $computed(() => (text != null && text.trim() !== '') || file != null);
|
||||||
|
|
||||||
watch([$$(text), $$(file)], saveDraft);
|
watch([$$(text), $$(file)], saveDraft);
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
<template>
|
<template>
|
||||||
<MkStickyContainer>
|
<MkStickyContainer>
|
||||||
<template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template>
|
<template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template>
|
||||||
<MkSpacer :content-max="800">
|
<MkSpacer :content-max="800" :marginMin="6">
|
||||||
<div class="fcuexfpr">
|
<div class="fcuexfpr">
|
||||||
<transition :name="$store.state.animation ? 'fade' : ''" mode="out-in">
|
<transition :name="$store.state.animation ? 'fade' : ''" mode="out-in">
|
||||||
<div v-if="note" class="note">
|
<div v-if="note" class="note">
|
||||||
|
|
|
@ -7,7 +7,7 @@
|
||||||
<div v-if="!narrow || currentPage?.route.name == null" class="nav">
|
<div v-if="!narrow || currentPage?.route.name == null" class="nav">
|
||||||
<div class="baaadecd">
|
<div class="baaadecd">
|
||||||
<MkInfo v-if="emailNotConfigured" warn class="info">{{ i18n.ts.emailNotConfiguredWarning }} <MkA to="/settings/email" class="_link">{{ i18n.ts.configure }}</MkA></MkInfo>
|
<MkInfo v-if="emailNotConfigured" warn class="info">{{ i18n.ts.emailNotConfiguredWarning }} <MkA to="/settings/email" class="_link">{{ i18n.ts.configure }}</MkA></MkInfo>
|
||||||
<MkSuperMenu :def="menuDef" :grid="currentPage?.route.name == null"></MkSuperMenu>
|
<MkSuperMenu :def="menuDef" :grid="narrow"></MkSuperMenu>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="!(narrow && currentPage?.route.name == null)" class="main">
|
<div v-if="!(narrow && currentPage?.route.name == null)" class="main">
|
||||||
|
@ -230,6 +230,12 @@ onUnmounted(() => {
|
||||||
ro.disconnect();
|
ro.disconnect();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
watch(router.currentRef, (to) => {
|
||||||
|
if (to.route.name === "settings" && to.child?.route.name == null && !narrow) {
|
||||||
|
router.replace('/settings/profile');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
const emailNotConfigured = computed(() => instance.enableEmail && ($i.email == null || !$i.emailVerified));
|
const emailNotConfigured = computed(() => instance.enableEmail && ($i.email == null || !$i.emailVerified));
|
||||||
|
|
||||||
provideMetadataReceiver((info) => {
|
provideMetadataReceiver((info) => {
|
||||||
|
|
|
@ -167,8 +167,8 @@ const timeForThem = $computed(() => {
|
||||||
const tzInfo = cityTimezones.lookupViaCity(props.user.location!.replace(/\s.*/,''));
|
const tzInfo = cityTimezones.lookupViaCity(props.user.location!.replace(/\s.*/,''));
|
||||||
if (tzInfo.length == 0) return "";
|
if (tzInfo.length == 0) return "";
|
||||||
const tz = tzInfo[0].timezone;
|
const tz = tzInfo[0].timezone;
|
||||||
const theirTime = new Date().toLocaleString("en-US", { timeZone: tz, hour12: true })
|
const theirTime = new Date().toLocaleString("en-US", { timeZone: tz, hour12: false });
|
||||||
return ` (${theirTime.split(",")[1].trim().split(":")[0]} ${theirTime.split(" ")[1].slice(-2)})`
|
return ` (${theirTime.split(",")[1].trim().split(":")[0]}:${theirTime.split(" ")[1].slice(-5,-3)})`;
|
||||||
})
|
})
|
||||||
|
|
||||||
function menu(ev) {
|
function menu(ev) {
|
||||||
|
|
|
@ -24,6 +24,8 @@ export const getBuiltinThemes = () =>
|
||||||
[
|
[
|
||||||
"l-rosepinedawn",
|
"l-rosepinedawn",
|
||||||
"l-light",
|
"l-light",
|
||||||
|
"l-nord",
|
||||||
|
"l-gruvbox",
|
||||||
"l-coffee",
|
"l-coffee",
|
||||||
"l-apricot",
|
"l-apricot",
|
||||||
"l-rainy",
|
"l-rainy",
|
||||||
|
@ -35,6 +37,10 @@ export const getBuiltinThemes = () =>
|
||||||
"d-rosepine",
|
"d-rosepine",
|
||||||
"d-rosepinemoon",
|
"d-rosepinemoon",
|
||||||
"d-dark",
|
"d-dark",
|
||||||
|
"d-nord",
|
||||||
|
"d-gruvbox",
|
||||||
|
"d-catppuccin-frappe",
|
||||||
|
"d-catppuccin-mocha",
|
||||||
"d-persimmon",
|
"d-persimmon",
|
||||||
"d-astro",
|
"d-astro",
|
||||||
"d-future",
|
"d-future",
|
||||||
|
|
|
@ -32,7 +32,7 @@ html {
|
||||||
overflow-wrap: break-word;
|
overflow-wrap: break-word;
|
||||||
font-family: "BIZ UDGothic", Roboto, HelveticaNeue, Arial, sans-serif;
|
font-family: "BIZ UDGothic", Roboto, HelveticaNeue, Arial, sans-serif;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
line-height: 1.35;
|
line-height: 1.6;
|
||||||
text-size-adjust: 100%;
|
text-size-adjust: 100%;
|
||||||
tab-size: 2;
|
tab-size: 2;
|
||||||
|
|
||||||
|
@ -88,7 +88,6 @@ html._themeChanging_ {
|
||||||
html, body {
|
html, body {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
scroll-behavior: smooth;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
a {
|
a {
|
||||||
|
@ -155,6 +154,10 @@ hr {
|
||||||
box-shadow: 0px 4px 32px var(--shadow) !important;
|
box-shadow: 0px 4px 32px var(--shadow) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.swiper {
|
||||||
|
overflow: clip !important;
|
||||||
|
}
|
||||||
|
|
||||||
._button {
|
._button {
|
||||||
appearance: none;
|
appearance: none;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
|
@ -479,6 +482,7 @@ hr {
|
||||||
}
|
}
|
||||||
|
|
||||||
._link {
|
._link {
|
||||||
|
position: relative;
|
||||||
color: var(--link);
|
color: var(--link);
|
||||||
|
|
||||||
&:after {
|
&:after {
|
||||||
|
@ -680,3 +684,19 @@ hr {
|
||||||
width: 1.25em;
|
width: 1.25em;
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@media(prefers-reduced-motion: no-preference) {
|
||||||
|
@keyframes scaleIn {
|
||||||
|
from {
|
||||||
|
transform: scale(0);
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@keyframes scaleInSmall {
|
||||||
|
from {
|
||||||
|
transform: scale(.8);
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
94
packages/client/src/themes/d-catppuccin-frappe.json5
Normal file
94
packages/client/src/themes/d-catppuccin-frappe.json5
Normal file
|
@ -0,0 +1,94 @@
|
||||||
|
{
|
||||||
|
id: 'ffcd3328-5c57-4ca3-9dac-4580cbf7742f',
|
||||||
|
base: 'dark',
|
||||||
|
name: 'Catppuccin frappe',
|
||||||
|
props: {
|
||||||
|
X2: ':darken<2<@panel',
|
||||||
|
X3: 'rgba(255, 255, 255, 0.05)',
|
||||||
|
X4: 'rgba(255, 255, 255, 0.1)',
|
||||||
|
X5: 'rgba(255, 255, 255, 0.05)',
|
||||||
|
X6: 'rgba(255, 255, 255, 0.15)',
|
||||||
|
X7: 'rgba(255, 255, 255, 0.05)',
|
||||||
|
X8: ':lighten<5<@accent',
|
||||||
|
X9: ':darken<5<@accent',
|
||||||
|
bg: '#232634',
|
||||||
|
fg: '#c6d0f5',
|
||||||
|
X10: ':alpha<0.4<@accent',
|
||||||
|
X11: 'rgba(0, 0, 0, 0.3)',
|
||||||
|
X12: 'rgba(255, 255, 255, 0.1)',
|
||||||
|
X13: 'rgba(255, 255, 255, 0.15)',
|
||||||
|
X14: ':alpha<0.5<@navBg',
|
||||||
|
X15: ':alpha<0<@panel',
|
||||||
|
X16: ':alpha<0.7<@panel',
|
||||||
|
X17: ':alpha<0.8<@bg',
|
||||||
|
cwBg: '#51576d',
|
||||||
|
cwFg: '#b5bfe2',
|
||||||
|
link: '#8caaee',
|
||||||
|
warn: '#ef9f76',
|
||||||
|
badge: '#8caaee',
|
||||||
|
error: '#e78284',
|
||||||
|
focus: ':alpha<0.3<@accent',
|
||||||
|
navBg: '@panel',
|
||||||
|
navFg: '@fg',
|
||||||
|
panel: ':lighten<3<@bg',
|
||||||
|
popup: ':lighten<3<@panel',
|
||||||
|
accent: '#eebebe',
|
||||||
|
header: ':alpha<0.7<@panel',
|
||||||
|
infoBg: '#414559',
|
||||||
|
infoFg: '#a5adce',
|
||||||
|
renote: '#8caaee',
|
||||||
|
shadow: 'rgba(0, 0, 0, 0.3)',
|
||||||
|
divider: 'rgba(255, 255, 255, 0.1)',
|
||||||
|
hashtag: '#85c1dc',
|
||||||
|
mention: '@accent',
|
||||||
|
modalBg: 'rgba(0, 0, 0, 0.5)',
|
||||||
|
success: '#a6d189',
|
||||||
|
buttonBg: 'rgba(255, 255, 255, 0.05)',
|
||||||
|
switchBg: 'rgba(255, 255, 255, 0.15)',
|
||||||
|
acrylicBg: ':alpha<0.5<@bg',
|
||||||
|
cwHoverBg: '#626880',
|
||||||
|
indicator: '@accent',
|
||||||
|
mentionMe: '@mention',
|
||||||
|
messageBg: '@bg',
|
||||||
|
navActive: '@accent',
|
||||||
|
accentedBg: ':alpha<0.15<@accent',
|
||||||
|
codeNumber: '#a6d189',
|
||||||
|
codeString: '#ef9f76',
|
||||||
|
fgOnAccent: '#303446',
|
||||||
|
infoWarnBg: '#414559',
|
||||||
|
infoWarnFg: '#b5bfe2',
|
||||||
|
navHoverFg: ':lighten<17<@fg',
|
||||||
|
swutchOnBg: '@accentedBg',
|
||||||
|
swutchOnFg: '@accent',
|
||||||
|
codeBoolean: '@accent',
|
||||||
|
dateLabelFg: '@fg',
|
||||||
|
deckDivider: '#737994',
|
||||||
|
inputBorder: 'rgba(255, 255, 255, 0.1)',
|
||||||
|
panelBorder: 'solid 1px var(--divider)',
|
||||||
|
swutchOffBg: 'rgba(255, 255, 255, 0.1)',
|
||||||
|
swutchOffFg: '@fg',
|
||||||
|
accentDarken: ':darken<10<@accent',
|
||||||
|
acrylicPanel: ':alpha<0.5<@panel',
|
||||||
|
navIndicator: '@indicator',
|
||||||
|
windowHeader: ':alpha<0.85<@panel',
|
||||||
|
accentLighten: ':lighten<10<@accent',
|
||||||
|
buttonHoverBg: 'rgba(255, 255, 255, 0.1)',
|
||||||
|
driveFolderBg: ':alpha<0.3<@accent',
|
||||||
|
fgHighlighted: ':lighten<3<@fg',
|
||||||
|
fgTransparent: ':alpha<0.5<@fg',
|
||||||
|
panelHeaderBg: ':lighten<3<@panel',
|
||||||
|
panelHeaderFg: '@fg',
|
||||||
|
buttonGradateA: '@accent',
|
||||||
|
buttonGradateB: ':hue<20<@accent',
|
||||||
|
htmlThemeColor: '@bg',
|
||||||
|
panelHighlight: ':lighten<3<@panel',
|
||||||
|
listItemHoverBg: 'rgba(255, 255, 255, 0.03)',
|
||||||
|
scrollbarHandle: 'rgba(255, 255, 255, 0.2)',
|
||||||
|
inputBorderHover: 'rgba(255, 255, 255, 0.2)',
|
||||||
|
wallpaperOverlay: 'rgba(0, 0, 0, 0.5)',
|
||||||
|
fgTransparentWeak: ':alpha<0.75<@fg',
|
||||||
|
panelHeaderDivider: 'rgba(0, 0, 0, 0)',
|
||||||
|
scrollbarHandleHover: 'rgba(255, 255, 255, 0.4)',
|
||||||
|
},
|
||||||
|
author: 'somebody ¯_(ツ)_/¯',
|
||||||
|
}
|
94
packages/client/src/themes/d-catppuccin-mocha.json5
Normal file
94
packages/client/src/themes/d-catppuccin-mocha.json5
Normal file
|
@ -0,0 +1,94 @@
|
||||||
|
{
|
||||||
|
id: 'd413f41f-a489-48be-9e20-3532ffbb4363',
|
||||||
|
base: 'dark',
|
||||||
|
name: 'Catppuccin mocha',
|
||||||
|
props: {
|
||||||
|
X2: ':darken<2<@panel',
|
||||||
|
X3: 'rgba(255, 255, 255, 0.05)',
|
||||||
|
X4: 'rgba(255, 255, 255, 0.1)',
|
||||||
|
X5: 'rgba(255, 255, 255, 0.05)',
|
||||||
|
X6: 'rgba(255, 255, 255, 0.15)',
|
||||||
|
X7: 'rgba(255, 255, 255, 0.05)',
|
||||||
|
X8: ':lighten<5<@accent',
|
||||||
|
X9: ':darken<5<@accent',
|
||||||
|
bg: '#11111b',
|
||||||
|
fg: '#cdd6f4',
|
||||||
|
X10: ':alpha<0.4<@accent',
|
||||||
|
X11: 'rgba(0, 0, 0, 0.3)',
|
||||||
|
X12: 'rgba(255, 255, 255, 0.1)',
|
||||||
|
X13: 'rgba(255, 255, 255, 0.15)',
|
||||||
|
X14: ':alpha<0.5<@navBg',
|
||||||
|
X15: ':alpha<0<@panel',
|
||||||
|
X16: ':alpha<0.7<@panel',
|
||||||
|
X17: ':alpha<0.8<@bg',
|
||||||
|
cwBg: '#45475a',
|
||||||
|
cwFg: '#bac2de',
|
||||||
|
link: '#89b4fa',
|
||||||
|
warn: '#fab387',
|
||||||
|
badge: '#89b4fa',
|
||||||
|
error: '#f38ba8',
|
||||||
|
focus: ':alpha<0.3<@accent',
|
||||||
|
navBg: '@panel',
|
||||||
|
navFg: '@fg',
|
||||||
|
panel: ':lighten<3<@bg',
|
||||||
|
popup: ':lighten<3<@panel',
|
||||||
|
accent: '#f2cdcd',
|
||||||
|
header: ':alpha<0.7<@panel',
|
||||||
|
infoBg: '#313244',
|
||||||
|
infoFg: '#a6adc8',
|
||||||
|
renote: '#89b4fa',
|
||||||
|
shadow: 'rgba(0, 0, 0, 0.3)',
|
||||||
|
divider: 'rgba(255, 255, 255, 0.1)',
|
||||||
|
hashtag: '#74c7ec',
|
||||||
|
mention: '@accent',
|
||||||
|
modalBg: 'rgba(0, 0, 0, 0.5)',
|
||||||
|
success: '#a6e3a1',
|
||||||
|
buttonBg: 'rgba(255, 255, 255, 0.05)',
|
||||||
|
switchBg: 'rgba(255, 255, 255, 0.15)',
|
||||||
|
acrylicBg: ':alpha<0.5<@bg',
|
||||||
|
cwHoverBg: '#585b70',
|
||||||
|
indicator: '@accent',
|
||||||
|
mentionMe: '@mention',
|
||||||
|
messageBg: '@bg',
|
||||||
|
navActive: '@accent',
|
||||||
|
accentedBg: ':alpha<0.15<@accent',
|
||||||
|
codeNumber: '#a6e3a1',
|
||||||
|
codeString: '#fab387',
|
||||||
|
fgOnAccent: '#1e1e2e',
|
||||||
|
infoWarnBg: '#313244',
|
||||||
|
infoWarnFg: '#bac2de',
|
||||||
|
navHoverFg: ':lighten<17<@fg',
|
||||||
|
swutchOnBg: '@accentedBg',
|
||||||
|
swutchOnFg: '@accent',
|
||||||
|
codeBoolean: '@accent',
|
||||||
|
dateLabelFg: '@fg',
|
||||||
|
deckDivider: '#6c7086',
|
||||||
|
inputBorder: 'rgba(255, 255, 255, 0.1)',
|
||||||
|
panelBorder: 'solid 1px var(--divider)',
|
||||||
|
swutchOffBg: 'rgba(255, 255, 255, 0.1)',
|
||||||
|
swutchOffFg: '@fg',
|
||||||
|
accentDarken: ':darken<10<@accent',
|
||||||
|
acrylicPanel: ':alpha<0.5<@panel',
|
||||||
|
navIndicator: '@indicator',
|
||||||
|
windowHeader: ':alpha<0.85<@panel',
|
||||||
|
accentLighten: ':lighten<10<@accent',
|
||||||
|
buttonHoverBg: 'rgba(255, 255, 255, 0.1)',
|
||||||
|
driveFolderBg: ':alpha<0.3<@accent',
|
||||||
|
fgHighlighted: ':lighten<3<@fg',
|
||||||
|
fgTransparent: ':alpha<0.5<@fg',
|
||||||
|
panelHeaderBg: ':lighten<3<@panel',
|
||||||
|
panelHeaderFg: '@fg',
|
||||||
|
buttonGradateA: '@accent',
|
||||||
|
buttonGradateB: ':hue<20<@accent',
|
||||||
|
htmlThemeColor: '@bg',
|
||||||
|
panelHighlight: ':lighten<3<@panel',
|
||||||
|
listItemHoverBg: 'rgba(255, 255, 255, 0.03)',
|
||||||
|
scrollbarHandle: 'rgba(255, 255, 255, 0.2)',
|
||||||
|
inputBorderHover: 'rgba(255, 255, 255, 0.2)',
|
||||||
|
wallpaperOverlay: 'rgba(0, 0, 0, 0.5)',
|
||||||
|
fgTransparentWeak: ':alpha<0.75<@fg',
|
||||||
|
panelHeaderDivider: 'rgba(0, 0, 0, 0)',
|
||||||
|
scrollbarHandleHover: 'rgba(255, 255, 255, 0.4)',
|
||||||
|
},
|
||||||
|
author: 'somebody ¯_(ツ)_/¯',
|
||||||
|
}
|
30
packages/client/src/themes/d-gruvbox.json5
Normal file
30
packages/client/src/themes/d-gruvbox.json5
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
{
|
||||||
|
id: '256a2e52-440f-4a00-8a76-c93501354dfb',
|
||||||
|
base: 'dark',
|
||||||
|
desc: 'Misskey gruvbox-dark-medium theme. Inspired by https://github.com/morhetz/gruvbox',
|
||||||
|
name: 'Gruvbox Dark Medium',
|
||||||
|
props: {
|
||||||
|
bg: '#282828',
|
||||||
|
fg: '#ebdbb2',
|
||||||
|
link: '#b16286',
|
||||||
|
warn: '#d65d0e',
|
||||||
|
badge: '#458588',
|
||||||
|
error: '#fb4934',
|
||||||
|
navBg: '#32302f',
|
||||||
|
panel: '#32302f',
|
||||||
|
accent: '#98971a',
|
||||||
|
header: ':alpha<0.7<@panel',
|
||||||
|
renote: '@accent',
|
||||||
|
divider: '#7c6f64',
|
||||||
|
hashtag: '#458588',
|
||||||
|
mention: '#98971a',
|
||||||
|
success: '#98971a',
|
||||||
|
mentionMe: '#fb4934',
|
||||||
|
fgHighlighted: '#fbf1c7',
|
||||||
|
panelHeaderBg: '@panel',
|
||||||
|
buttonGradateA: '#98971a',
|
||||||
|
buttonGradateB: '#98971a',
|
||||||
|
panelHeaderDivider: '@divider',
|
||||||
|
},
|
||||||
|
author: '@razzlom@quietplace.xyz',
|
||||||
|
}
|
94
packages/client/src/themes/d-nord.json5
Normal file
94
packages/client/src/themes/d-nord.json5
Normal file
|
@ -0,0 +1,94 @@
|
||||||
|
{
|
||||||
|
id: 'dddbc0c6-af2c-46f8-b8f3-05964adcde0b',
|
||||||
|
base: 'dark',
|
||||||
|
desc: 'Nord: an arctic, north-bluish color palette',
|
||||||
|
name: 'Nord Dark',
|
||||||
|
props: {
|
||||||
|
X2: ':darken<2<@panel',
|
||||||
|
X3: 'rgba(255, 255, 255, 0.05)',
|
||||||
|
X4: 'rgba(255, 255, 255, 0.1)',
|
||||||
|
X5: 'rgba(255, 255, 255, 0.05)',
|
||||||
|
X6: 'rgba(255, 255, 255, 0.15)',
|
||||||
|
X7: 'rgba(255, 255, 255, 0.05)',
|
||||||
|
X8: ':lighten<5<@accent',
|
||||||
|
X9: ':darken<5<@accent',
|
||||||
|
bg: '#2e3440',
|
||||||
|
fg: '#eceff4',
|
||||||
|
X10: ':alpha<0.4<@accent',
|
||||||
|
X11: 'rgba(0, 0, 0, 0.3)',
|
||||||
|
X12: 'rgba(255, 255, 255, 0.1)',
|
||||||
|
X13: 'rgba(255, 255, 255, 0.15)',
|
||||||
|
X14: ':alpha<0.5<@navBg',
|
||||||
|
X15: ':alpha<0<@panel',
|
||||||
|
X16: ':alpha<0.7<@panel',
|
||||||
|
X17: ':alpha<0.8<@bg',
|
||||||
|
cwBg: '#4c566a',
|
||||||
|
cwFg: '#393f4f',
|
||||||
|
link: '#b48ead',
|
||||||
|
warn: '#d08770',
|
||||||
|
badge: '#d08770',
|
||||||
|
error: '#bf616a',
|
||||||
|
focus: ':alpha<0.3<@accent',
|
||||||
|
navBg: '@panel',
|
||||||
|
navFg: '@fg',
|
||||||
|
panel: ':lighten<3<@bg',
|
||||||
|
popup: ':lighten<3<@panel',
|
||||||
|
accent: '#81a1c1',
|
||||||
|
header: ':alpha<0.7<@panel',
|
||||||
|
infoBg: '#4c566a',
|
||||||
|
infoFg: '#d08770',
|
||||||
|
renote: '#ebcb8b',
|
||||||
|
shadow: 'rgba(0, 0, 0, 0.3)',
|
||||||
|
divider: 'rgba(255, 255, 255, 0.1)',
|
||||||
|
hashtag: '#a3be8c',
|
||||||
|
mention: '@accent',
|
||||||
|
modalBg: 'rgba(0, 0, 0, 0.5)',
|
||||||
|
success: '#a3be8c',
|
||||||
|
buttonBg: 'rgba(255, 255, 255, 0.05)',
|
||||||
|
switchBg: 'rgba(255, 255, 255, 0.15)',
|
||||||
|
acrylicBg: ':alpha<0.5<@bg',
|
||||||
|
cwHoverBg: '#707b97',
|
||||||
|
indicator: '@accent',
|
||||||
|
mentionMe: '@mention',
|
||||||
|
messageBg: '@bg',
|
||||||
|
navActive: '@accent',
|
||||||
|
accentedBg: ':alpha<0.15<@accent',
|
||||||
|
codeNumber: '#a3be8c',
|
||||||
|
codeString: '#b48ead',
|
||||||
|
fgOnAccent: '#eceff4',
|
||||||
|
infoWarnBg: '#4c566a',
|
||||||
|
infoWarnFg: '#bf616a',
|
||||||
|
navHoverFg: ':lighten<17<@fg',
|
||||||
|
swutchOnBg: '@accentedBg',
|
||||||
|
swutchOnFg: '@accent',
|
||||||
|
codeBoolean: '#ebcb8b',
|
||||||
|
dateLabelFg: '@fg',
|
||||||
|
inputBorder: 'rgba(255, 255, 255, 0.1)',
|
||||||
|
panelBorder: '" solid 1px var(--divider)',
|
||||||
|
swutchOffBg: 'rgba(255, 255, 255, 0.1)',
|
||||||
|
swutchOffFg: '@fg',
|
||||||
|
accentDarken: '#5e81ac',
|
||||||
|
acrylicPanel: ':alpha<0.5<@panel',
|
||||||
|
navIndicator: '@indicator',
|
||||||
|
windowHeader: ':alpha<0.85<@panel',
|
||||||
|
accentLighten: '#88c0d0',
|
||||||
|
buttonHoverBg: 'rgba(255, 255, 255, 0.1)',
|
||||||
|
driveFolderBg: ':alpha<0.3<@accent',
|
||||||
|
fgHighlighted: ':lighten<3<@fg',
|
||||||
|
fgTransparent: ':alpha<0.5<@fg',
|
||||||
|
panelHeaderBg: ':lighten<3<@panel',
|
||||||
|
panelHeaderFg: '@fg',
|
||||||
|
buttonGradateA: '@accent',
|
||||||
|
buttonGradateB: '#8fbcbb',
|
||||||
|
htmlThemeColor: '@bg',
|
||||||
|
panelHighlight: ':lighten<3<@panel',
|
||||||
|
listItemHoverBg: 'rgba(255, 255, 255, 0.03)',
|
||||||
|
scrollbarHandle: 'rgba(255, 255, 255, 0.2)',
|
||||||
|
inputBorderHover: 'rgba(255, 255, 255, 0.2)',
|
||||||
|
wallpaperOverlay: 'rgba(0, 0, 0, 0.5)',
|
||||||
|
fgTransparentWeak: ':alpha<0.75<@fg',
|
||||||
|
panelHeaderDivider: 'rgba(0, 0, 0, 0)',
|
||||||
|
scrollbarHandleHover: 'rgba(255, 255, 255, 0.4)',
|
||||||
|
},
|
||||||
|
author: '@thatonecalculator@stop.voring.me',
|
||||||
|
}
|
30
packages/client/src/themes/l-gruvbox.json5
Normal file
30
packages/client/src/themes/l-gruvbox.json5
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
{
|
||||||
|
id: '9be7b20e-58b4-4bd2-8b1d-49d41a676685',
|
||||||
|
base: 'light',
|
||||||
|
desc: 'Misskey gruvbox-light-medium theme. Inspired by https://github.com/morhetz/gruvbox',
|
||||||
|
name: 'Gruvbox Light Medium',
|
||||||
|
props: {
|
||||||
|
bg: '#fbf1c7',
|
||||||
|
fg: '#3c3836',
|
||||||
|
link: '#b16286',
|
||||||
|
warn: '#d65d0e',
|
||||||
|
badge: '#458588',
|
||||||
|
error: '#fb4934',
|
||||||
|
navBg: '#f9f5c7',
|
||||||
|
panel: '#f9f5c7',
|
||||||
|
accent: '#98971a',
|
||||||
|
header: ':alpha<0.7<@panel',
|
||||||
|
renote: '@accent',
|
||||||
|
divider: '#7c6f64',
|
||||||
|
hashtag: '#458588',
|
||||||
|
mention: '#98971a',
|
||||||
|
success: '#98971a',
|
||||||
|
mentionMe: '#9d0006',
|
||||||
|
fgHighlighted: '#fbf1c7',
|
||||||
|
panelHeaderBg: '@panel',
|
||||||
|
buttonGradateA: '#98971a',
|
||||||
|
buttonGradateB: '#98971a',
|
||||||
|
panelHeaderDivider: '@divider',
|
||||||
|
},
|
||||||
|
author: '@razzlom@quietplace.xyz',
|
||||||
|
}
|
94
packages/client/src/themes/l-nord.json5
Normal file
94
packages/client/src/themes/l-nord.json5
Normal file
|
@ -0,0 +1,94 @@
|
||||||
|
{
|
||||||
|
id: 'a4b1932e-740c-4ca4-b5d7-06e3322dced4',
|
||||||
|
base: 'light',
|
||||||
|
desc: 'Nord: an arctic, north-bluish color palette',
|
||||||
|
name: 'Nord Light',
|
||||||
|
props: {
|
||||||
|
X2: ':darken<2<@panel',
|
||||||
|
X3: 'rgba(255, 255, 255, 0.05)',
|
||||||
|
X4: 'rgba(255, 255, 255, 0.1)',
|
||||||
|
X5: 'rgba(255, 255, 255, 0.05)',
|
||||||
|
X6: 'rgba(255, 255, 255, 0.15)',
|
||||||
|
X7: 'rgba(255, 255, 255, 0.05)',
|
||||||
|
X8: ':lighten<5<@accent',
|
||||||
|
X9: ':darken<5<@accent',
|
||||||
|
bg: '#d8dee9',
|
||||||
|
fg: '#3b4252',
|
||||||
|
X10: ':alpha<0.4<@accent',
|
||||||
|
X11: 'rgba(0, 0, 0, 0.3)',
|
||||||
|
X12: 'rgba(255, 255, 255, 0.1)',
|
||||||
|
X13: 'rgba(255, 255, 255, 0.15)',
|
||||||
|
X14: ':alpha<0.5<@navBg',
|
||||||
|
X15: ':alpha<0<@panel',
|
||||||
|
X16: ':alpha<0.7<@panel',
|
||||||
|
X17: ':alpha<0.8<@bg',
|
||||||
|
cwBg: '#687390',
|
||||||
|
cwFg: '#393f4f',
|
||||||
|
link: '#44a4c1',
|
||||||
|
warn: '#ecb637',
|
||||||
|
badge: '#31b1ce',
|
||||||
|
error: '#ec4137',
|
||||||
|
focus: ':alpha<0.3<@accent',
|
||||||
|
navBg: '@panel',
|
||||||
|
navFg: '@fg',
|
||||||
|
panel: ':lighten<3<@bg',
|
||||||
|
popup: ':lighten<3<@panel',
|
||||||
|
accent: '#81a1c1',
|
||||||
|
header: ':alpha<0.7<@panel',
|
||||||
|
infoBg: '#253142',
|
||||||
|
infoFg: '#fff',
|
||||||
|
renote: '#229e82',
|
||||||
|
shadow: 'rgba(0, 0, 0, 0.3)',
|
||||||
|
divider: 'rgba(255, 255, 255, 0.1)',
|
||||||
|
hashtag: '#ff9156',
|
||||||
|
mention: '@accent',
|
||||||
|
modalBg: 'rgba(0, 0, 0, 0.5)',
|
||||||
|
success: '#86b300',
|
||||||
|
buttonBg: 'rgba(255, 255, 255, 0.05)',
|
||||||
|
switchBg: 'rgba(255, 255, 255, 0.15)',
|
||||||
|
acrylicBg: ':alpha<0.5<@bg',
|
||||||
|
cwHoverBg: '#707b97',
|
||||||
|
indicator: '@accent',
|
||||||
|
mentionMe: '@mention',
|
||||||
|
messageBg: '@bg',
|
||||||
|
navActive: '@accent',
|
||||||
|
accentedBg: ':alpha<0.15<@accent',
|
||||||
|
codeNumber: '#cfff9e',
|
||||||
|
codeString: '#ffb675',
|
||||||
|
fgOnAccent: '#fff',
|
||||||
|
infoWarnBg: '#42321c',
|
||||||
|
infoWarnFg: '#ffbd3e',
|
||||||
|
navHoverFg: ':lighten<17<@fg',
|
||||||
|
swutchOnBg: '@accentedBg',
|
||||||
|
swutchOnFg: '@accent',
|
||||||
|
codeBoolean: '#c59eff',
|
||||||
|
dateLabelFg: '@fg',
|
||||||
|
inputBorder: 'rgba(255, 255, 255, 0.1)',
|
||||||
|
panelBorder: '" solid 1px var(--divider)',
|
||||||
|
swutchOffBg: 'rgba(255, 255, 255, 0.1)',
|
||||||
|
swutchOffFg: '@fg',
|
||||||
|
accentDarken: ':darken<10<@accent',
|
||||||
|
acrylicPanel: ':alpha<0.5<@panel',
|
||||||
|
navIndicator: '@indicator',
|
||||||
|
windowHeader: ':alpha<0.85<@panel',
|
||||||
|
accentLighten: ':lighten<10<@accent',
|
||||||
|
buttonHoverBg: 'rgba(255, 255, 255, 0.1)',
|
||||||
|
driveFolderBg: ':alpha<0.3<@accent',
|
||||||
|
fgHighlighted: ':lighten<3<@fg',
|
||||||
|
fgTransparent: ':alpha<0.5<@fg',
|
||||||
|
panelHeaderBg: ':lighten<3<@panel',
|
||||||
|
panelHeaderFg: '@fg',
|
||||||
|
buttonGradateA: '@accent',
|
||||||
|
buttonGradateB: ':hue<20<@accent',
|
||||||
|
htmlThemeColor: '@bg',
|
||||||
|
panelHighlight: ':lighten<3<@panel',
|
||||||
|
listItemHoverBg: 'rgba(255, 255, 255, 0.03)',
|
||||||
|
scrollbarHandle: 'rgba(255, 255, 255, 0.2)',
|
||||||
|
inputBorderHover: 'rgba(255, 255, 255, 0.2)',
|
||||||
|
wallpaperOverlay: 'rgba(0, 0, 0, 0.5)',
|
||||||
|
fgTransparentWeak: ':alpha<0.75<@fg',
|
||||||
|
panelHeaderDivider: 'rgba(0, 0, 0, 0)',
|
||||||
|
scrollbarHandleHover: 'rgba(255, 255, 255, 0.4)',
|
||||||
|
},
|
||||||
|
author: '@thatonecalculator@stop.voring.me',
|
||||||
|
}
|
|
@ -376,10 +376,8 @@ const wallpaper = localStorage.getItem('wallpaper') != null;
|
||||||
}
|
}
|
||||||
|
|
||||||
> .button-wrapper {
|
> .button-wrapper {
|
||||||
|
display: inline-flex;
|
||||||
> i {
|
justify-content: center;
|
||||||
transform: translateY(0.05em);
|
|
||||||
}
|
|
||||||
|
|
||||||
&.on {
|
&.on {
|
||||||
background-color: var(--accentedBg);
|
background-color: var(--accentedBg);
|
||||||
|
|
|
@ -67,7 +67,7 @@ defineExpose<WidgetComponentExpose>({
|
||||||
|
|
||||||
> .text {
|
> .text {
|
||||||
::v-deep(b) {
|
::v-deep(b) {
|
||||||
color: #41b781;
|
color: var(--badge);
|
||||||
}
|
}
|
||||||
|
|
||||||
::v-deep(span) {
|
::v-deep(span) {
|
||||||
|
|
|
@ -5,6 +5,7 @@ import { defineConfig } from 'vite';
|
||||||
import locales from '../../locales';
|
import locales from '../../locales';
|
||||||
import meta from '../../package.json';
|
import meta from '../../package.json';
|
||||||
import pluginJson5 from './vite.json5';
|
import pluginJson5 from './vite.json5';
|
||||||
|
import viteCompression from 'vite-plugin-compression';
|
||||||
|
|
||||||
const extensions = ['.ts', '.tsx', '.js', '.jsx', '.mjs', '.json', '.json5', '.svg', '.sass', '.scss', '.css', '.vue'];
|
const extensions = ['.ts', '.tsx', '.js', '.jsx', '.mjs', '.json', '.json5', '.svg', '.sass', '.scss', '.css', '.vue'];
|
||||||
|
|
||||||
|
@ -20,6 +21,9 @@ export default defineConfig(({ command, mode }) => {
|
||||||
reactivityTransform: true,
|
reactivityTransform: true,
|
||||||
}),
|
}),
|
||||||
pluginJson5(),
|
pluginJson5(),
|
||||||
|
viteCompression({
|
||||||
|
algorithm: 'brotliCompress'
|
||||||
|
}),
|
||||||
],
|
],
|
||||||
|
|
||||||
resolve: {
|
resolve: {
|
||||||
|
|
|
@ -55,7 +55,7 @@ importers:
|
||||||
'@bull-board/api': ^4.6.4
|
'@bull-board/api': ^4.6.4
|
||||||
'@bull-board/koa': ^4.6.4
|
'@bull-board/koa': ^4.6.4
|
||||||
'@bull-board/ui': ^4.6.4
|
'@bull-board/ui': ^4.6.4
|
||||||
'@calckey/megalodon': 5.1.2
|
'@calckey/megalodon': 5.1.21
|
||||||
'@discordapp/twemoji': 14.0.2
|
'@discordapp/twemoji': 14.0.2
|
||||||
'@elastic/elasticsearch': 7.17.0
|
'@elastic/elasticsearch': 7.17.0
|
||||||
'@koa/cors': 3.4.3
|
'@koa/cors': 3.4.3
|
||||||
|
@ -196,6 +196,7 @@ importers:
|
||||||
seedrandom: ^3.0.5
|
seedrandom: ^3.0.5
|
||||||
semver: 7.3.8
|
semver: 7.3.8
|
||||||
sharp: 0.31.3
|
sharp: 0.31.3
|
||||||
|
sonic-channel: ^1.3.1
|
||||||
speakeasy: 2.0.0
|
speakeasy: 2.0.0
|
||||||
strict-event-emitter-types: 2.0.0
|
strict-event-emitter-types: 2.0.0
|
||||||
stringz: 2.1.0
|
stringz: 2.1.0
|
||||||
|
@ -224,7 +225,7 @@ importers:
|
||||||
'@bull-board/api': 4.10.2
|
'@bull-board/api': 4.10.2
|
||||||
'@bull-board/koa': 4.10.2_6tybghmia4wsnt33xeid7y4rby
|
'@bull-board/koa': 4.10.2_6tybghmia4wsnt33xeid7y4rby
|
||||||
'@bull-board/ui': 4.10.2
|
'@bull-board/ui': 4.10.2
|
||||||
'@calckey/megalodon': 5.1.2
|
'@calckey/megalodon': 5.1.21
|
||||||
'@discordapp/twemoji': 14.0.2
|
'@discordapp/twemoji': 14.0.2
|
||||||
'@elastic/elasticsearch': 7.17.0
|
'@elastic/elasticsearch': 7.17.0
|
||||||
'@koa/cors': 3.4.3
|
'@koa/cors': 3.4.3
|
||||||
|
@ -310,6 +311,7 @@ importers:
|
||||||
seedrandom: 3.0.5
|
seedrandom: 3.0.5
|
||||||
semver: 7.3.8
|
semver: 7.3.8
|
||||||
sharp: 0.31.3
|
sharp: 0.31.3
|
||||||
|
sonic-channel: 1.3.1
|
||||||
speakeasy: 2.0.0
|
speakeasy: 2.0.0
|
||||||
stringz: 2.1.0
|
stringz: 2.1.0
|
||||||
summaly: 2.7.0
|
summaly: 2.7.0
|
||||||
|
@ -465,6 +467,7 @@ importers:
|
||||||
uuid: 9.0.0
|
uuid: 9.0.0
|
||||||
vanilla-tilt: 1.8.0
|
vanilla-tilt: 1.8.0
|
||||||
vite: ^4.1.1
|
vite: ^4.1.1
|
||||||
|
vite-plugin-compression: ^0.5.1
|
||||||
vue: 3.2.45
|
vue: 3.2.45
|
||||||
vue-isyourpasswordsafe: ^2.0.0
|
vue-isyourpasswordsafe: ^2.0.0
|
||||||
vue-plyr: ^7.0.0
|
vue-plyr: ^7.0.0
|
||||||
|
@ -542,6 +545,7 @@ importers:
|
||||||
uuid: 9.0.0
|
uuid: 9.0.0
|
||||||
vanilla-tilt: 1.8.0
|
vanilla-tilt: 1.8.0
|
||||||
vite: 4.1.1_sass@1.57.1
|
vite: 4.1.1_sass@1.57.1
|
||||||
|
vite-plugin-compression: 0.5.1_vite@4.1.1
|
||||||
vue: 3.2.45
|
vue: 3.2.45
|
||||||
vue-isyourpasswordsafe: 2.0.0
|
vue-isyourpasswordsafe: 2.0.0
|
||||||
vue-plyr: 7.0.0
|
vue-plyr: 7.0.0
|
||||||
|
@ -759,8 +763,8 @@ packages:
|
||||||
'@bull-board/api': 4.10.2
|
'@bull-board/api': 4.10.2
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
/@calckey/megalodon/5.1.2:
|
/@calckey/megalodon/5.1.21:
|
||||||
resolution: {integrity: sha512-bUjPOfASy8X2NxdBvYDOWN9Rw/KdkfbTxy5vMQBcrGXepFbT4M+00blEYNc00Uu/epwH9YoNqpQC8PKQr/WU4w==}
|
resolution: {integrity: sha512-wThdyNb/UofklvYzyeFNQwxJNHqaSLD+z0glQLzV1tmAvSB0EZOL0D15dvpdB0LED0Q2rpJlwaonejkTI9JQhA==}
|
||||||
engines: {node: '>=15.0.0'}
|
engines: {node: '>=15.0.0'}
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/oauth': 0.9.1
|
'@types/oauth': 0.9.1
|
||||||
|
@ -6514,6 +6518,15 @@ packages:
|
||||||
/fs-constants/1.0.0:
|
/fs-constants/1.0.0:
|
||||||
resolution: {integrity: sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==}
|
resolution: {integrity: sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==}
|
||||||
|
|
||||||
|
/fs-extra/10.1.0:
|
||||||
|
resolution: {integrity: sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
dependencies:
|
||||||
|
graceful-fs: 4.2.10
|
||||||
|
jsonfile: 6.1.0
|
||||||
|
universalify: 2.0.0
|
||||||
|
dev: true
|
||||||
|
|
||||||
/fs-extra/8.1.0:
|
/fs-extra/8.1.0:
|
||||||
resolution: {integrity: sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==}
|
resolution: {integrity: sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==}
|
||||||
engines: {node: '>=6 <7 || >=8'}
|
engines: {node: '>=6 <7 || >=8'}
|
||||||
|
@ -11583,6 +11596,11 @@ packages:
|
||||||
smart-buffer: 4.2.0
|
smart-buffer: 4.2.0
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/sonic-channel/1.3.1:
|
||||||
|
resolution: {integrity: sha512-+K4IZVFE7Tf2DB4EFZ23xo7a/+gJaiOHhFzXVZpzkX6Rs/rvf4YbSxnEGdYw8mrTcjtpG+jLVQEhP8sNTtN5VA==}
|
||||||
|
engines: {node: '>= 6.0.0'}
|
||||||
|
dev: false
|
||||||
|
|
||||||
/sort-keys-length/1.0.1:
|
/sort-keys-length/1.0.1:
|
||||||
resolution: {integrity: sha512-GRbEOUqCxemTAk/b32F2xa8wDTs+Z1QHOkbhJDQTvv/6G3ZkbJ+frYWsTcc7cBB3Fu4wy4XlLCuNtJuMn7Gsvw==}
|
resolution: {integrity: sha512-GRbEOUqCxemTAk/b32F2xa8wDTs+Z1QHOkbhJDQTvv/6G3ZkbJ+frYWsTcc7cBB3Fu4wy4XlLCuNtJuMn7Gsvw==}
|
||||||
engines: {node: '>=0.10.0'}
|
engines: {node: '>=0.10.0'}
|
||||||
|
@ -12922,6 +12940,19 @@ packages:
|
||||||
replace-ext: 1.0.1
|
replace-ext: 1.0.1
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/vite-plugin-compression/0.5.1_vite@4.1.1:
|
||||||
|
resolution: {integrity: sha512-5QJKBDc+gNYVqL/skgFAP81Yuzo9R+EAf19d+EtsMF/i8kFUpNi3J/H01QD3Oo8zBQn+NzoCIFkpPLynoOzaJg==}
|
||||||
|
peerDependencies:
|
||||||
|
vite: '>=2.0.0'
|
||||||
|
dependencies:
|
||||||
|
chalk: 4.1.2
|
||||||
|
debug: 4.3.4
|
||||||
|
fs-extra: 10.1.0
|
||||||
|
vite: 4.1.1_sass@1.57.1
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- supports-color
|
||||||
|
dev: true
|
||||||
|
|
||||||
/vite/4.1.1_sass@1.57.1:
|
/vite/4.1.1_sass@1.57.1:
|
||||||
resolution: {integrity: sha512-LM9WWea8vsxhr782r9ntg+bhSFS06FJgCvvB0+8hf8UWtvaiDagKYWXndjfX6kGl74keHJUcpzrQliDXZlF5yg==}
|
resolution: {integrity: sha512-LM9WWea8vsxhr782r9ntg+bhSFS06FJgCvvB0+8hf8UWtvaiDagKYWXndjfX6kGl74keHJUcpzrQliDXZlF5yg==}
|
||||||
engines: {node: ^14.18.0 || >=16.0.0}
|
engines: {node: ^14.18.0 || >=16.0.0}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
{
|
{
|
||||||
"version": "13.1.3-rc",
|
"version": "13.1.3",
|
||||||
"notes": "This release candidate has the following changes:\n• Better blocking/muting\n• Better user refreshing\n• New help menu with app list (More! > Help)\n• Bug fixes and performance improvements",
|
"notes": "This release candidate has the following changes:\n• Better blocking/muting\n• Better user refreshing\n• New help menu with app list (More! > Help)\n• New headerbar style\n• Bug + security fixes and performance improvements",
|
||||||
"screenshots": []
|
"screenshots": []
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue