Compare commits
93 Commits
85ed47ff62
...
f8bb504e9f
Author | SHA1 | Date |
---|---|---|
|
f8bb504e9f | |
|
7ea4cad12e | |
|
d864e9a269 | |
|
4e0434c275 | |
|
e2f939080a | |
|
6956f44d1f | |
|
a393d5a87e | |
|
b59cd38190 | |
|
bbd87bbd01 | |
|
b9552c253e | |
|
6c634de482 | |
|
fc02e0d34d | |
|
cb1a90ddad | |
|
f76c3dd078 | |
|
a98b1349a6 | |
|
6f1bbad3d8 | |
|
4953ed7c10 | |
|
c587ba8094 | |
|
5e123049d6 | |
|
f0fb3a56a8 | |
|
eb67e0552c | |
|
56ef212147 | |
|
c22844c367 | |
|
1f9659802b | |
|
b599b1bfa0 | |
|
e9fb4a7625 | |
|
7b7512f798 | |
|
2dea0989bc | |
|
f82c1f1092 | |
|
298be1a093 | |
|
2fb58a6394 | |
|
b8ae7edcec | |
|
e24233c1c7 | |
|
1fd7387f14 | |
|
225154d76d | |
|
396bcbdb0f | |
|
9664408b4c | |
|
3de7a0bf4e | |
|
f2e3154319 | |
|
c5f9c0ce5c | |
|
cce302ae4f | |
|
e0d210e15b | |
|
0b7634b126 | |
|
d1446d195a | |
|
94ee22e9a3 | |
|
a0b95a48d0 | |
|
ff7e669e70 | |
|
218070eb13 | |
|
fa97ab984c | |
|
742c452716 | |
|
0f8c068e84 | |
|
69d66b89f2 | |
|
211365de64 | |
|
966127c63e | |
|
54800971eb | |
|
13d5c6d2b2 | |
|
2cff00eedd | |
|
3fc2261041 | |
|
050b67bb02 | |
|
56fe87e560 | |
|
beaf1882c9 | |
|
5791b3b03d | |
|
cd29664664 | |
|
13018434b8 | |
|
3c10839770 | |
|
693b9f5374 | |
|
1fd2c652d4 | |
|
9a1be19bf0 | |
|
4d9c8c870c | |
|
425ffe97a4 | |
|
0727929ef7 | |
|
1f42b9d0aa | |
|
18d66c0233 | |
|
2f52c20150 | |
|
9d70c9ad78 | |
|
42b2aea533 | |
|
97adf6f2cc | |
|
93ff209c51 | |
|
5fe08d0bbb | |
|
8c413d01e6 | |
|
b231da7c7c | |
|
df3e44f62e | |
|
e504560477 | |
|
bcb2073715 | |
|
6a80c23a50 | |
|
2621f468ff | |
|
d4654dd7bd | |
|
b7da6cad87 | |
|
5b4115e21a | |
|
c174c5c144 | |
|
aebc3f781e | |
|
f60b6291d7 | |
|
7673874675 |
|
@ -105,6 +105,16 @@ port: 3000
|
|||
# socket: /path/to/misskey.sock
|
||||
# chmodSocket: '777'
|
||||
|
||||
# Proxy trust settings
|
||||
#
|
||||
# Changes how the server interpret the origin IP of the request.
|
||||
#
|
||||
# Any format supported by Fastify is accepted.
|
||||
# Default: trust all proxies (i.e. trustProxy: true)
|
||||
# See: https://fastify.dev/docs/latest/reference/server/#trustproxy
|
||||
#
|
||||
# trustProxy: 1
|
||||
|
||||
# ┌──────────────────────────┐
|
||||
#───┘ PostgreSQL configuration └────────────────────────────────
|
||||
|
||||
|
|
|
@ -81,7 +81,7 @@ jobs:
|
|||
cache: 'pnpm'
|
||||
- run: pnpm i --frozen-lockfile
|
||||
- name: Restore eslint cache
|
||||
uses: actions/cache@v4.2.4
|
||||
uses: actions/cache@v4.3.0
|
||||
with:
|
||||
path: ${{ env.eslint-cache-path }}
|
||||
key: eslint-${{ env.eslint-cache-version }}-${{ matrix.workspace }}-${{ hashFiles('**/pnpm-lock.yaml') }}-${{ github.ref_name }}-${{ github.sha }}
|
||||
|
|
|
@ -16,7 +16,7 @@ jobs:
|
|||
# api-artifact
|
||||
steps:
|
||||
- name: Download artifact
|
||||
uses: actions/github-script@v7.0.1
|
||||
uses: actions/github-script@v7.1.0
|
||||
with:
|
||||
script: |
|
||||
const fs = require('fs');
|
||||
|
|
|
@ -90,7 +90,7 @@ jobs:
|
|||
env:
|
||||
CHROMATIC_PROJECT_TOKEN: ${{ secrets.CHROMATIC_PROJECT_TOKEN }}
|
||||
- name: Notify that Chromatic detects changes
|
||||
uses: actions/github-script@v7.0.1
|
||||
uses: actions/github-script@v7.1.0
|
||||
if: github.event_name != 'pull_request_target' && steps.chromatic_push.outputs.success == 'false'
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
|
26
CHANGELOG.md
26
CHANGELOG.md
|
@ -1,14 +1,30 @@
|
|||
## Unreleased
|
||||
## 2025.10.0
|
||||
|
||||
### NOTE
|
||||
- pnpm 10.16.0 が必要です
|
||||
|
||||
### General
|
||||
-
|
||||
- Feat: 予約投稿ができるようになりました
|
||||
- デフォルトで作成可能数は1になっています。適宜ロールのポリシーで設定を行ってください。
|
||||
- Enhance: 広告ごとにセンシティブフラグを設定できるようになりました
|
||||
- Enhance: 依存関係の更新
|
||||
- Enhance: 翻訳の更新
|
||||
|
||||
### Client
|
||||
-
|
||||
- Feat: アカウントのQRコードを表示・読み取りできるようになりました
|
||||
- Feat: 動画を圧縮してアップロードできるようになりました
|
||||
- Enhance: チャットの日本語名称がダイレクトメッセージに戻るとともに、ベータ版機能ではなくなりました
|
||||
- Enhance: 画像編集にマスクエフェクト(塗りつぶし、ぼかし、モザイク)を追加
|
||||
- Enhance: 画像編集の集中線エフェクトを強化
|
||||
- Enhance: ウォーターマークにアカウントのQRコードを追加できるように
|
||||
- Enhance: テーマをドラッグ&ドロップできるように
|
||||
- Enhance: 絵文字ピッカーのサイズをより大きくできるように
|
||||
- Enhance: 時刻計算のための基準値を一か所で管理するようにし、パフォーマンスを向上
|
||||
- Fix: iOSで、デバイスがダークモードだと初回読み込み時にエラーになる問題を修正
|
||||
- Fix: アクティビティウィジェットのグラフモードが動作しない問題を修正
|
||||
|
||||
### Server
|
||||
-
|
||||
|
||||
- Enhance: ユーザーIPを確実に取得できるために設定ファイルにFastifyOptions.trustProxyを追加しました
|
||||
|
||||
## 2025.9.0
|
||||
|
||||
|
|
|
@ -0,0 +1,232 @@
|
|||
<!--
|
||||
SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
SPDX-License-Identifier: AGPL-3.0-only
|
||||
-->
|
||||
|
||||
<template>
|
||||
<canvas ref="canvasEl" style="display: block; width: 100%; height: 100%; pointer-events: none;"></canvas>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { onMounted, onUnmounted, useTemplateRef } from 'vue';
|
||||
import isChromatic from 'chromatic/isChromatic';
|
||||
import { initShaderProgram } from '@/utility/webgl.js';
|
||||
|
||||
const VERTEX_SHADER = `#version 300 es
|
||||
in vec2 position;
|
||||
out vec2 in_uv;
|
||||
|
||||
void main() {
|
||||
in_uv = (position + 1.0) / 2.0;
|
||||
gl_Position = vec4(position, 0.0, 1.0);
|
||||
}
|
||||
`;
|
||||
|
||||
const FRAGMENT_SHADER = `#version 300 es
|
||||
precision mediump float;
|
||||
|
||||
const float PI = 3.141592653589793;
|
||||
const float TWO_PI = 6.283185307179586;
|
||||
const float HALF_PI = 1.5707963267948966;
|
||||
|
||||
in vec2 in_uv;
|
||||
uniform vec2 in_resolution;
|
||||
uniform float u_scale;
|
||||
uniform float u_time;
|
||||
uniform float u_seed;
|
||||
uniform float u_angle;
|
||||
uniform float u_radius;
|
||||
uniform vec3 u_color;
|
||||
uniform vec2 u_ripplePositions[16];
|
||||
uniform float u_rippleRadiuses[16];
|
||||
out vec4 out_color;
|
||||
|
||||
float getRipple(vec2 uv) {
|
||||
float strength = 0.0;
|
||||
float thickness = 0.05;
|
||||
for (int i = 0; i < 16; i++) {
|
||||
if (u_rippleRadiuses[i] <= 0.0) continue;
|
||||
|
||||
float d = distance(uv, u_ripplePositions[i]);
|
||||
|
||||
// フチ
|
||||
if (d < u_rippleRadiuses[i] + thickness && d > u_rippleRadiuses[i] - thickness) {
|
||||
float gradate = abs(d - u_rippleRadiuses[i] + thickness) / thickness;
|
||||
strength += (1.0 - u_rippleRadiuses[i]) * gradate;
|
||||
}
|
||||
|
||||
// 内側
|
||||
if (d < u_rippleRadiuses[i] + thickness) {
|
||||
strength += 0.25 * (1.0 - u_rippleRadiuses[i]);
|
||||
}
|
||||
}
|
||||
return strength;
|
||||
}
|
||||
|
||||
void main() {
|
||||
float x_ratio = min(in_resolution.x / in_resolution.y, 1.0);
|
||||
float y_ratio = min(in_resolution.y / in_resolution.x, 1.0);
|
||||
|
||||
float angle = -(u_angle * PI);
|
||||
vec2 centeredUv = (in_uv - vec2(0.5, 0.5)) * vec2(x_ratio, y_ratio);
|
||||
vec2 rotatedUV = vec2(
|
||||
centeredUv.x * cos(angle) - centeredUv.y * sin(angle),
|
||||
centeredUv.x * sin(angle) + centeredUv.y * cos(angle)
|
||||
);
|
||||
vec2 uv = rotatedUV;
|
||||
|
||||
float time = u_time * 0.00025;
|
||||
|
||||
float size = 1.0 / u_scale;
|
||||
float size_half = size / 2.0;
|
||||
float modX = mod(uv.x, size);
|
||||
float modY = mod(uv.y, size);
|
||||
|
||||
vec2 pixelated_uv = vec2(
|
||||
(size * (floor((uv.x - 0.5 - size) / size) + 0.5)),
|
||||
(size * (floor((uv.y - 0.5 - size) / size) + 0.5))
|
||||
) + vec2(0.5 + size, 0.5 + size);
|
||||
|
||||
float strength = getRipple(pixelated_uv);
|
||||
|
||||
float opacity = min(max(strength, 0.0), 1.0);
|
||||
|
||||
float threshold = ((u_radius / 2.0) / u_scale);
|
||||
if (length(vec2(modX - size_half, modY - size_half)) < threshold) {
|
||||
out_color = vec4(u_color.r, u_color.g, u_color.b, opacity);
|
||||
//out_color = vec4(1.0);
|
||||
return;
|
||||
}
|
||||
|
||||
// debug
|
||||
//float a = min(max(getRipple(uv), 0.0), 1.0);
|
||||
//out_color = vec4(u_color.r, u_color.g, u_color.b, (opacity + a) / 2.0);
|
||||
|
||||
out_color = vec4(0.0, 0.0, 0.0, 0.0);
|
||||
}
|
||||
`;
|
||||
|
||||
const canvasEl = useTemplateRef('canvasEl');
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
scale?: number;
|
||||
}>(), {
|
||||
scale: 48,
|
||||
});
|
||||
|
||||
let handle: ReturnType<typeof window['requestAnimationFrame']> | null = null;
|
||||
|
||||
onMounted(() => {
|
||||
const canvas = canvasEl.value!;
|
||||
let width = canvas.offsetWidth;
|
||||
let height = canvas.offsetHeight;
|
||||
canvas.width = width;
|
||||
canvas.height = height;
|
||||
|
||||
const maybeGl = canvas.getContext('webgl2', { preserveDrawingBuffer: false, alpha: true, premultipliedAlpha: false, antialias: true });
|
||||
if (maybeGl == null) return;
|
||||
|
||||
const gl = maybeGl;
|
||||
|
||||
const VERTICES = new Float32Array([-1, -1, -1, 1, 1, 1, -1, -1, 1, 1, 1, -1]);
|
||||
const vertexBuffer = gl.createBuffer();
|
||||
gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer);
|
||||
gl.bufferData(gl.ARRAY_BUFFER, VERTICES, gl.STATIC_DRAW);
|
||||
|
||||
//gl.clearColor(0.0, 0.0, 0.0, 0.0);
|
||||
//gl.clear(gl.COLOR_BUFFER_BIT);
|
||||
|
||||
const shaderProgram = initShaderProgram(gl, VERTEX_SHADER, FRAGMENT_SHADER);
|
||||
|
||||
gl.useProgram(shaderProgram);
|
||||
|
||||
const positionLocation = gl.getAttribLocation(shaderProgram, 'position');
|
||||
gl.vertexAttribPointer(positionLocation, 2, gl.FLOAT, false, 0, 0);
|
||||
gl.enableVertexAttribArray(positionLocation);
|
||||
|
||||
const in_resolution = gl.getUniformLocation(shaderProgram, 'in_resolution');
|
||||
gl.uniform2fv(in_resolution, [canvas.width, canvas.height]);
|
||||
|
||||
const u_time = gl.getUniformLocation(shaderProgram, 'u_time');
|
||||
const u_seed = gl.getUniformLocation(shaderProgram, 'u_seed');
|
||||
const u_scale = gl.getUniformLocation(shaderProgram, 'u_scale');
|
||||
const u_angle = gl.getUniformLocation(shaderProgram, 'u_angle');
|
||||
const u_radius = gl.getUniformLocation(shaderProgram, 'u_radius');
|
||||
const u_color = gl.getUniformLocation(shaderProgram, 'u_color');
|
||||
gl.uniform1f(u_seed, Math.random() * 1000);
|
||||
gl.uniform1f(u_scale, props.scale);
|
||||
gl.uniform1f(u_angle, 0.0);
|
||||
gl.uniform1f(u_radius, 0.15);
|
||||
gl.uniform3fv(u_color, [0.5, 1.0, 0]);
|
||||
|
||||
if (isChromatic()) {
|
||||
gl.uniform1f(u_time, 0);
|
||||
gl.drawArrays(gl.TRIANGLES, 0, 6);
|
||||
} else {
|
||||
let ripples = [] as { position: [number, number]; startTime: number; }[];
|
||||
const LIFE_TIME = 1000 * 4;
|
||||
|
||||
function render(timeStamp: number) {
|
||||
let sizeChanged = false;
|
||||
if (Math.abs(height - canvas.offsetHeight) > 2) {
|
||||
height = canvas.offsetHeight;
|
||||
canvas.height = height;
|
||||
sizeChanged = true;
|
||||
}
|
||||
if (Math.abs(width - canvas.offsetWidth) > 2) {
|
||||
width = canvas.offsetWidth;
|
||||
canvas.width = width;
|
||||
sizeChanged = true;
|
||||
}
|
||||
if (sizeChanged && gl) {
|
||||
gl.uniform2fv(in_resolution, [width, height]);
|
||||
gl.viewport(0, 0, width, height);
|
||||
}
|
||||
|
||||
gl.uniform1f(u_time, timeStamp);
|
||||
|
||||
if (Math.random() < 0.01 && ripples.length < 16) {
|
||||
ripples.push({ position: [(Math.random() * 2) - 1, (Math.random() * 2) - 1], startTime: timeStamp });
|
||||
}
|
||||
|
||||
for (let i = 0; i < 16; i++) {
|
||||
const o = gl.getUniformLocation(shaderProgram, `u_ripplePositions[${i.toString()}]`);
|
||||
const r = gl.getUniformLocation(shaderProgram, `u_rippleRadiuses[${i.toString()}]`);
|
||||
const ripple = ripples[i];
|
||||
if (ripple == null) {
|
||||
gl.uniform2f(o, 0, 0);
|
||||
gl.uniform1f(r, 0.0);
|
||||
continue;
|
||||
}
|
||||
|
||||
const delta = timeStamp - ripple.startTime;
|
||||
|
||||
gl.uniform2f(o, ripple.position[0], ripple.position[1]);
|
||||
gl.uniform1f(r, delta / LIFE_TIME);
|
||||
}
|
||||
|
||||
ripples = ripples.filter(r => (timeStamp - r.startTime) < LIFE_TIME);
|
||||
if (ripples.length === 0) {
|
||||
ripples.push({ position: [(Math.random() * 2) - 1, (Math.random() * 2) - 1], startTime: timeStamp });
|
||||
}
|
||||
|
||||
gl.drawArrays(gl.TRIANGLES, 0, 6);
|
||||
|
||||
handle = window.requestAnimationFrame(render);
|
||||
}
|
||||
|
||||
handle = window.requestAnimationFrame(render);
|
||||
}
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
if (handle) {
|
||||
window.cancelAnimationFrame(handle);
|
||||
}
|
||||
|
||||
// TODO: WebGLリソースの解放
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
</style>
|
|
@ -0,0 +1,190 @@
|
|||
<!--
|
||||
SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
SPDX-License-Identifier: AGPL-3.0-only
|
||||
-->
|
||||
|
||||
<template>
|
||||
<canvas ref="canvasEl" style="display: block; width: 100%; height: 100%; pointer-events: none;"></canvas>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { onMounted, onUnmounted, useTemplateRef } from 'vue';
|
||||
import isChromatic from 'chromatic/isChromatic';
|
||||
import { GLSL_LIB_SNOISE, initShaderProgram } from '@/utility/webgl.js';
|
||||
|
||||
const VERTEX_SHADER = `#version 300 es
|
||||
in vec2 position;
|
||||
out vec2 in_uv;
|
||||
|
||||
void main() {
|
||||
in_uv = (position + 1.0) / 2.0;
|
||||
gl_Position = vec4(position, 0.0, 1.0);
|
||||
}
|
||||
`;
|
||||
|
||||
const FRAGMENT_SHADER = `#version 300 es
|
||||
precision mediump float;
|
||||
|
||||
const float PI = 3.141592653589793;
|
||||
const float TWO_PI = 6.283185307179586;
|
||||
const float HALF_PI = 1.5707963267948966;
|
||||
|
||||
${GLSL_LIB_SNOISE}
|
||||
|
||||
in vec2 in_uv;
|
||||
uniform vec2 in_resolution;
|
||||
uniform float u_scale;
|
||||
uniform float u_time;
|
||||
uniform float u_seed;
|
||||
uniform float u_angle;
|
||||
uniform float u_radius;
|
||||
uniform vec3 u_color;
|
||||
out vec4 out_color;
|
||||
|
||||
void main() {
|
||||
float x_ratio = min(in_resolution.x / in_resolution.y, 1.0);
|
||||
float y_ratio = min(in_resolution.y / in_resolution.x, 1.0);
|
||||
|
||||
float size = 1.0 / u_scale;
|
||||
float size_half = size / 2.0;
|
||||
|
||||
float angle = -(u_angle * PI);
|
||||
vec2 centeredUv = (in_uv - vec2(0.5, 0.5)) * vec2(x_ratio, y_ratio);
|
||||
vec2 rotatedUV = vec2(
|
||||
centeredUv.x * cos(angle) - centeredUv.y * sin(angle),
|
||||
centeredUv.x * sin(angle) + centeredUv.y * cos(angle)
|
||||
);
|
||||
vec2 uv = rotatedUV;
|
||||
|
||||
float modX = mod(uv.x, size);
|
||||
float modY = mod(uv.y, size);
|
||||
|
||||
vec2 pixelated_uv = vec2(
|
||||
(size * (floor((uv.x - 0.5 - size) / size) + 0.5)),
|
||||
(size * (floor((uv.y - 0.5 - size) / size) + 0.5))
|
||||
) + vec2(0.5 + size, 0.5 + size);
|
||||
|
||||
float time = u_time * 0.00025;
|
||||
|
||||
float noiseAScale = 1.0;
|
||||
float noiseAX = (pixelated_uv.x + u_seed) * (u_scale / noiseAScale);
|
||||
float noiseAY = (pixelated_uv.y + u_seed) * (u_scale / noiseAScale);
|
||||
float noiseA = snoise(vec3(noiseAX, noiseAY, time * 2.0));
|
||||
|
||||
float noiseBScale = 32.0;
|
||||
float noiseBX = (pixelated_uv.x + u_seed) * (u_scale / noiseBScale);
|
||||
float noiseBY = (pixelated_uv.y + u_seed) * (u_scale / noiseBScale);
|
||||
float noiseB = snoise(vec3(noiseBX, noiseBY, time));
|
||||
|
||||
float strength = 0.0;
|
||||
strength += noiseA * 0.2;
|
||||
strength += noiseB * 0.8;
|
||||
|
||||
float opacity = min(max(strength, 0.0), 1.0);
|
||||
|
||||
float threshold = ((u_radius / 2.0) / u_scale);
|
||||
if (length(vec2(modX - size_half, modY - size_half)) < threshold) {
|
||||
out_color = vec4(u_color.r, u_color.g, u_color.b, opacity);
|
||||
return;
|
||||
}
|
||||
|
||||
out_color = vec4(0.0, 0.0, 0.0, 0.0);
|
||||
}
|
||||
`;
|
||||
|
||||
const canvasEl = useTemplateRef('canvasEl');
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
scale?: number;
|
||||
}>(), {
|
||||
scale: 48,
|
||||
});
|
||||
|
||||
let handle: ReturnType<typeof window['requestAnimationFrame']> | null = null;
|
||||
|
||||
onMounted(() => {
|
||||
const canvas = canvasEl.value!;
|
||||
let width = canvas.offsetWidth;
|
||||
let height = canvas.offsetHeight;
|
||||
canvas.width = width;
|
||||
canvas.height = height;
|
||||
|
||||
const maybeGl = canvas.getContext('webgl2', { preserveDrawingBuffer: false, alpha: true, premultipliedAlpha: false, antialias: true });
|
||||
if (maybeGl == null) return;
|
||||
|
||||
const gl = maybeGl;
|
||||
|
||||
const VERTICES = new Float32Array([-1, -1, -1, 1, 1, 1, -1, -1, 1, 1, 1, -1]);
|
||||
const vertexBuffer = gl.createBuffer();
|
||||
gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer);
|
||||
gl.bufferData(gl.ARRAY_BUFFER, VERTICES, gl.STATIC_DRAW);
|
||||
|
||||
//gl.clearColor(0.0, 0.0, 0.0, 0.0);
|
||||
//gl.clear(gl.COLOR_BUFFER_BIT);
|
||||
|
||||
const shaderProgram = initShaderProgram(gl, VERTEX_SHADER, FRAGMENT_SHADER);
|
||||
|
||||
gl.useProgram(shaderProgram);
|
||||
|
||||
const positionLocation = gl.getAttribLocation(shaderProgram, 'position');
|
||||
gl.vertexAttribPointer(positionLocation, 2, gl.FLOAT, false, 0, 0);
|
||||
gl.enableVertexAttribArray(positionLocation);
|
||||
|
||||
const in_resolution = gl.getUniformLocation(shaderProgram, 'in_resolution');
|
||||
gl.uniform2fv(in_resolution, [canvas.width, canvas.height]);
|
||||
|
||||
const u_time = gl.getUniformLocation(shaderProgram, 'u_time');
|
||||
const u_seed = gl.getUniformLocation(shaderProgram, 'u_seed');
|
||||
const u_scale = gl.getUniformLocation(shaderProgram, 'u_scale');
|
||||
const u_angle = gl.getUniformLocation(shaderProgram, 'u_angle');
|
||||
const u_radius = gl.getUniformLocation(shaderProgram, 'u_radius');
|
||||
const u_color = gl.getUniformLocation(shaderProgram, 'u_color');
|
||||
gl.uniform1f(u_seed, Math.random() * 1000);
|
||||
gl.uniform1f(u_scale, props.scale);
|
||||
gl.uniform1f(u_angle, 0.0);
|
||||
gl.uniform1f(u_radius, 0.15);
|
||||
gl.uniform3fv(u_color, [0.5, 1.0, 0]);
|
||||
|
||||
if (isChromatic()) {
|
||||
gl.uniform1f(u_time, 0);
|
||||
gl.drawArrays(gl.TRIANGLES, 0, 6);
|
||||
} else {
|
||||
function render(timeStamp: number) {
|
||||
let sizeChanged = false;
|
||||
if (Math.abs(height - canvas.offsetHeight) > 2) {
|
||||
height = canvas.offsetHeight;
|
||||
canvas.height = height;
|
||||
sizeChanged = true;
|
||||
}
|
||||
if (Math.abs(width - canvas.offsetWidth) > 2) {
|
||||
width = canvas.offsetWidth;
|
||||
canvas.width = width;
|
||||
sizeChanged = true;
|
||||
}
|
||||
if (sizeChanged && gl) {
|
||||
gl.uniform2fv(in_resolution, [width, height]);
|
||||
gl.viewport(0, 0, width, height);
|
||||
}
|
||||
|
||||
gl.uniform1f(u_time, timeStamp);
|
||||
|
||||
gl.drawArrays(gl.TRIANGLES, 0, 6);
|
||||
|
||||
handle = window.requestAnimationFrame(render);
|
||||
}
|
||||
|
||||
handle = window.requestAnimationFrame(render);
|
||||
}
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
if (handle) {
|
||||
window.cancelAnimationFrame(handle);
|
||||
}
|
||||
|
||||
// TODO: WebGLリソースの解放
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
</style>
|
|
@ -253,6 +253,7 @@ noteDeleteConfirm: "Segur que voleu eliminar aquesta publicació?"
|
|||
pinLimitExceeded: "No podeu fixar més publicacions"
|
||||
done: "Fet"
|
||||
processing: "S'està processant..."
|
||||
preprocessing: "Preparant"
|
||||
preview: "Vista prèvia"
|
||||
default: "Per defecte"
|
||||
defaultValueIs: "Per defecte: {value}"
|
||||
|
@ -1316,6 +1317,7 @@ acknowledgeNotesAndEnable: "Activa'l després de comprendre els possibles perill
|
|||
federationSpecified: "Aquest servidor treballa amb una federació de llistes blanques. No pot interactuar amb altres servidors que no siguin els especificats per l'administrador."
|
||||
federationDisabled: "La unió es troba deshabilitada en aquest servidor. No es pot interactuar amb usuaris d'altres servidors."
|
||||
draft: "Esborrany "
|
||||
draftsAndScheduledNotes: "Esborranys i publicacions programades"
|
||||
confirmOnReact: "Confirmar en reaccionar"
|
||||
reactAreYouSure: "Vols reaccionar amb \"{emoji}\"?"
|
||||
markAsSensitiveConfirm: "Vols marcar aquest contingut com a sensible?"
|
||||
|
@ -1372,6 +1374,8 @@ redisplayAllTips: "Torna ha mostrat tots els trucs i consells"
|
|||
hideAllTips: "Amagar tots els trucs i consells"
|
||||
defaultImageCompressionLevel: "Nivell de comprensió de la imatge per defecte"
|
||||
defaultImageCompressionLevel_description: "Baixa, conserva la qualitat de la imatge però la mida de l'arxiu és més gran. <br>Alta, redueix la mida de l'arxiu però també la qualitat de la imatge."
|
||||
defaultCompressionLevel: "Nivell de compressió predeterminat"
|
||||
defaultCompressionLevel_description: "Si el redueixes augmentaràs la qualitat de la imatge, però la mida de l'arxiu serà més gran. <br>Si augmentes l'opció redueixes la mida de l'arxiu i la qualitat de la imatge és pitjor."
|
||||
inMinutes: "Minut(s)"
|
||||
inDays: "Di(a)(es)"
|
||||
safeModeEnabled: "Mode segur activat"
|
||||
|
@ -1380,6 +1384,20 @@ customCssIsDisabledBecauseSafeMode: "El CSS personalitzat no s'aplica perquè el
|
|||
themeIsDefaultBecauseSafeMode: "El tema predeterminat es farà servir mentre el mode segur estigui activat. Una vegada es desactivi el mode segur es restablirà el tema escollit."
|
||||
thankYouForTestingBeta: "Gràcies per ajudar-nos a provar la versió beta!"
|
||||
createUserSpecifiedNote: "Crear notes especificades per l'usuari "
|
||||
schedulePost: "Programar una nota"
|
||||
scheduleToPostOnX: "Programar una nota per {x}"
|
||||
scheduledToPostOnX: "S'ha programat la nota per {x}"
|
||||
schedule: "Programa"
|
||||
scheduled: "Programat"
|
||||
_compression:
|
||||
_quality:
|
||||
high: "Qualitat alta"
|
||||
medium: "Qualitat mitjana"
|
||||
low: "Qualitat baixa"
|
||||
_size:
|
||||
large: "Mida gran"
|
||||
medium: "Mida mitjana"
|
||||
small: "Mida petita"
|
||||
_order:
|
||||
newest: "Més recent"
|
||||
oldest: "Antigues primer"
|
||||
|
@ -2022,6 +2040,7 @@ _role:
|
|||
uploadableFileTypes_caption: "Especifica el tipus MIME. Es poden especificar diferents tipus MIME separats amb una nova línia, i es poden especificar comodins amb asteriscs (*). (Per exemple: image/*)"
|
||||
uploadableFileTypes_caption2: "Pot que no sigui possible determinar el tipus MIME d'alguns arxius. Per permetre aquests tipus d'arxius afegeix {x} a les especificacions."
|
||||
noteDraftLimit: "Nombre possible d'esborranys de notes al servidor"
|
||||
scheduledNoteLimit: "Màxim nombre de notes programades que es poden crear simultàniament"
|
||||
watermarkAvailable: "Pots fer servir la marca d'aigua"
|
||||
_condition:
|
||||
roleAssignedTo: "Assignat a rols manuals"
|
||||
|
@ -2647,6 +2666,8 @@ _notification:
|
|||
youReceivedFollowRequest: "Has rebut una petició de seguiment"
|
||||
yourFollowRequestAccepted: "La teva petició de seguiment ha sigut acceptada"
|
||||
pollEnded: "Ja pots veure els resultats de l'enquesta "
|
||||
scheduledNotePosted: "Una nota programada ha sigut publicada"
|
||||
scheduledNotePostFailed: "Ha fallat la publicació d'una nota programada"
|
||||
newNote: "Nota nova"
|
||||
unreadAntennaNote: "Antena {name}"
|
||||
roleAssigned: "Rol assignat "
|
||||
|
@ -3201,6 +3222,8 @@ _imageEffector:
|
|||
mirror: "Mirall"
|
||||
invert: "Inversió cromàtica "
|
||||
grayscale: "Monocrom "
|
||||
blur: "Desenfocament"
|
||||
pixelate: "Mosaic"
|
||||
colorAdjust: "Correcció de color"
|
||||
colorClamp: "Compressió cromàtica "
|
||||
colorClampAdvanced: "Compressió de cromàtica avançada "
|
||||
|
@ -3212,11 +3235,13 @@ _imageEffector:
|
|||
checker: "Escacs"
|
||||
blockNoise: "Bloqueig de soroll"
|
||||
tearing: "Trencament d'imatge "
|
||||
fillSquare: "Omplir quadrat"
|
||||
fill: "Omplir"
|
||||
_fxProps:
|
||||
angle: "Angle"
|
||||
scale: "Mida"
|
||||
size: "Mida"
|
||||
radius: "Radi"
|
||||
samples: "Mida de la mostra"
|
||||
offset: "Posició "
|
||||
color: "Color"
|
||||
opacity: "Opacitat"
|
||||
|
@ -3246,6 +3271,7 @@ _imageEffector:
|
|||
zoomLinesThreshold: "Amplada de línia a l'augmentar "
|
||||
zoomLinesMaskSize: "Diàmetre del centre"
|
||||
zoomLinesBlack: "Obscurir"
|
||||
circle: "Cercle"
|
||||
drafts: "Esborrany "
|
||||
_drafts:
|
||||
select: "Seleccionar esborrany"
|
||||
|
@ -3261,6 +3287,9 @@ _drafts:
|
|||
restoreFromDraft: "Restaurar des dels esborranys"
|
||||
restore: "Restaurar esborrany"
|
||||
listDrafts: "Llistat d'esborranys"
|
||||
schedule: "Programació esborranys"
|
||||
listScheduledNotes: "Llista de notes programades"
|
||||
cancelSchedule: "Cancel·lar la programació"
|
||||
qr: "Codi QR"
|
||||
_qr:
|
||||
showTabTitle: "Veure"
|
||||
|
|
|
@ -253,6 +253,7 @@ noteDeleteConfirm: "Are you sure you want to delete this note?"
|
|||
pinLimitExceeded: "You cannot pin any more notes"
|
||||
done: "Done"
|
||||
processing: "Processing..."
|
||||
preprocessing: "Preparing..."
|
||||
preview: "Preview"
|
||||
default: "Default"
|
||||
defaultValueIs: "Default: {value}"
|
||||
|
@ -1316,6 +1317,7 @@ acknowledgeNotesAndEnable: "Turn on after understanding the precautions."
|
|||
federationSpecified: "This server is operated in a whitelist federation. Interacting with servers other than those designated by the administrator is not allowed."
|
||||
federationDisabled: "Federation is disabled on this server. You cannot interact with users on other servers."
|
||||
draft: "Drafts"
|
||||
draftsAndScheduledNotes: "Drafts and scheduled notes"
|
||||
confirmOnReact: "Confirm when reacting"
|
||||
reactAreYouSure: "Would you like to add a \"{emoji}\" reaction?"
|
||||
markAsSensitiveConfirm: "Do you want to set this media as sensitive?"
|
||||
|
@ -1372,6 +1374,8 @@ redisplayAllTips: "Show all “Tips & Tricks” again"
|
|||
hideAllTips: "Hide all \"Tips & Tricks\""
|
||||
defaultImageCompressionLevel: "Default image compression level"
|
||||
defaultImageCompressionLevel_description: "Lower level preserves image quality but increases file size.<br>Higher level reduce file size, but reduce image quality."
|
||||
defaultCompressionLevel: "Default compression level"
|
||||
defaultCompressionLevel_description: "Lower compression preserves quality but increases file size.<br>Higher compression reduces file size but lowers quality."
|
||||
inMinutes: "Minute(s)"
|
||||
inDays: "Day(s)"
|
||||
safeModeEnabled: "Safe mode is enabled"
|
||||
|
@ -1380,6 +1384,20 @@ customCssIsDisabledBecauseSafeMode: "Custom CSS is not applied because safe mode
|
|||
themeIsDefaultBecauseSafeMode: "While safe mode is active, the default theme is used. Disabling safe mode will revert these changes."
|
||||
thankYouForTestingBeta: "Thank you for helping us test the beta version!"
|
||||
createUserSpecifiedNote: "Create a direct note"
|
||||
schedulePost: "Schedule note"
|
||||
scheduleToPostOnX: "Scheduled to note on {x}"
|
||||
scheduledToPostOnX: "Note is scheduled for {x}"
|
||||
schedule: "Schedule"
|
||||
scheduled: "Scheduled"
|
||||
_compression:
|
||||
_quality:
|
||||
high: "High quality"
|
||||
medium: "Medium quality"
|
||||
low: "Low quality"
|
||||
_size:
|
||||
large: "Large size"
|
||||
medium: "Medium size"
|
||||
small: "Small size"
|
||||
_order:
|
||||
newest: "Newest First"
|
||||
oldest: "Oldest First"
|
||||
|
@ -2022,6 +2040,7 @@ _role:
|
|||
uploadableFileTypes_caption: "Specifies the allowed MIME/file types. Multiple MIME types can be specified by separating them with a new line, and wildcards can be specified with an asterisk (*). (e.g., image/*)"
|
||||
uploadableFileTypes_caption2: "Some files types might fail to be detected. To allow such files, add {x} to the specification."
|
||||
noteDraftLimit: "Number of possible drafts of server notes"
|
||||
scheduledNoteLimit: "Maximum number of simultaneous scheduled notes"
|
||||
watermarkAvailable: "Watermark function"
|
||||
_condition:
|
||||
roleAssignedTo: "Assigned to manual roles"
|
||||
|
@ -2647,6 +2666,8 @@ _notification:
|
|||
youReceivedFollowRequest: "You've received a follow request"
|
||||
yourFollowRequestAccepted: "Your follow request was accepted"
|
||||
pollEnded: "Poll results have become available"
|
||||
scheduledNotePosted: "Scheduled note has been posted"
|
||||
scheduledNotePostFailed: "Failed to post scheduled note"
|
||||
newNote: "New note"
|
||||
unreadAntennaNote: "Antenna {name}"
|
||||
roleAssigned: "Role given"
|
||||
|
@ -3201,6 +3222,8 @@ _imageEffector:
|
|||
mirror: "Mirror"
|
||||
invert: "Invert Colors"
|
||||
grayscale: "Grayscale"
|
||||
blur: "Blur"
|
||||
pixelate: "Pixelate"
|
||||
colorAdjust: "Color Correction"
|
||||
colorClamp: "Color Compression"
|
||||
colorClampAdvanced: "Color Compression (Advanced)"
|
||||
|
@ -3212,11 +3235,13 @@ _imageEffector:
|
|||
checker: "Checker"
|
||||
blockNoise: "Block Noise"
|
||||
tearing: "Tearing"
|
||||
fillSquare: "Fill (Square)"
|
||||
fill: "Fill"
|
||||
_fxProps:
|
||||
angle: "Angle"
|
||||
scale: "Size"
|
||||
size: "Size"
|
||||
radius: "Radius"
|
||||
samples: "Sample count"
|
||||
offset: "Position"
|
||||
color: "Color"
|
||||
opacity: "Opacity"
|
||||
|
@ -3246,6 +3271,7 @@ _imageEffector:
|
|||
zoomLinesThreshold: "Zoom line width"
|
||||
zoomLinesMaskSize: "Center diameter"
|
||||
zoomLinesBlack: "Make black"
|
||||
circle: "Circular"
|
||||
drafts: "Drafts"
|
||||
_drafts:
|
||||
select: "Select Draft"
|
||||
|
@ -3261,6 +3287,9 @@ _drafts:
|
|||
restoreFromDraft: "Restore from Draft"
|
||||
restore: "Restore"
|
||||
listDrafts: "List of Drafts"
|
||||
schedule: "Schedule note"
|
||||
listScheduledNotes: "Scheduled notes list"
|
||||
cancelSchedule: "Cancel schedule"
|
||||
qr: "QR Code"
|
||||
_qr:
|
||||
showTabTitle: "Display"
|
||||
|
|
|
@ -253,6 +253,7 @@ noteDeleteConfirm: "¿Quieres borrar esta nota?"
|
|||
pinLimitExceeded: "Ya no se pueden fijar más notas"
|
||||
done: "Terminado"
|
||||
processing: "Procesando..."
|
||||
preprocessing: "Preparando"
|
||||
preview: "Vista previa"
|
||||
default: "Predeterminado"
|
||||
defaultValueIs: "Por defecto: {value}"
|
||||
|
@ -1316,6 +1317,7 @@ acknowledgeNotesAndEnable: "Activar después de comprender las precauciones"
|
|||
federationSpecified: "Este servidor opera en una federación de listas blancas. No puede interactuar con otros servidores que no sean los especificados por el administrador."
|
||||
federationDisabled: "La federación está desactivada en este servidor. No puede interactuar con usuarios de otros servidores"
|
||||
draft: "Borrador"
|
||||
draftsAndScheduledNotes: "Borradores y notas programadas"
|
||||
confirmOnReact: "Confirmar la reacción"
|
||||
reactAreYouSure: "¿Quieres añadir una reacción «{emoji}»?"
|
||||
markAsSensitiveConfirm: "¿Desea establecer este medio multimedia(Imagen,vídeo...) como sensible?"
|
||||
|
@ -1372,6 +1374,8 @@ redisplayAllTips: "Volver a mostrar todos \"Trucos y consejos\""
|
|||
hideAllTips: "Ocultar todos los \"Trucos y consejos\""
|
||||
defaultImageCompressionLevel: "Nivel de compresión de la imagen por defecto"
|
||||
defaultImageCompressionLevel_description: "Baja, conserva la calidad de la imagen pero la medida del archivo es más grande. <br>Alta, reduce la medida del archivo pero también la calidad de la imagen."
|
||||
defaultCompressionLevel: "Nivel de compresión predeterminado"
|
||||
defaultCompressionLevel_description: "Al reducir el ajuste se conserva la calidad, pero aumenta el tamaño del archivo.<br>Al aumentar el ajuste se reduce el tamaño del archivo, pero disminuye la calidad."
|
||||
inMinutes: "Minutos"
|
||||
inDays: "Días"
|
||||
safeModeEnabled: "El modo seguro está activado"
|
||||
|
@ -1380,6 +1384,20 @@ customCssIsDisabledBecauseSafeMode: "El modo seguro está activado, por lo que n
|
|||
themeIsDefaultBecauseSafeMode: "Mientras el modo seguro esté activado, se utilizará el tema predeterminado. Cuando se desactive el modo seguro, se volverá al tema original."
|
||||
thankYouForTestingBeta: "¡Gracias por tu colaboración en la prueba de la versión beta!"
|
||||
createUserSpecifiedNote: "Crear notas especificadas por el usuario"
|
||||
schedulePost: "Programar una nota"
|
||||
scheduleToPostOnX: "Programar una nota para {x}"
|
||||
scheduledToPostOnX: "La nota está programada para {x}."
|
||||
schedule: "Programado"
|
||||
scheduled: "Programado"
|
||||
_compression:
|
||||
_quality:
|
||||
high: "Calidad alta"
|
||||
medium: "Calidad media"
|
||||
low: "Calidad baja"
|
||||
_size:
|
||||
large: "Tamaño grande"
|
||||
medium: "Tamaño mediano"
|
||||
small: "Tamaño pequeño"
|
||||
_order:
|
||||
newest: "Los más recientes primero"
|
||||
oldest: "Los más antiguos primero"
|
||||
|
@ -2022,6 +2040,7 @@ _role:
|
|||
uploadableFileTypes_caption: "Especifica los tipos MIME/archivos permitidos. Se pueden especificar varios tipos MIME separándolos con una nueva línea, y se pueden especificar comodines con un asterisco (*). (por ejemplo, image/*)"
|
||||
uploadableFileTypes_caption2: "Es posible que no se detecten algunos tipos de archivos. Para permitir estos archivos, añade {x} a la especificación."
|
||||
noteDraftLimit: "Número de posibles borradores de notas del servidor"
|
||||
scheduledNoteLimit: "Máximo número de notas programadas que se pueden crear simultáneamente."
|
||||
watermarkAvailable: "Disponibilidad de la función de marca de agua"
|
||||
_condition:
|
||||
roleAssignedTo: "Asignado a roles manuales"
|
||||
|
@ -2647,6 +2666,8 @@ _notification:
|
|||
youReceivedFollowRequest: "Has mandado una solicitud de seguimiento"
|
||||
yourFollowRequestAccepted: "Tu solicitud de seguimiento fue aceptada"
|
||||
pollEnded: "Estan disponibles los resultados de la encuesta"
|
||||
scheduledNotePosted: "Una nota programada ha sido publicada"
|
||||
scheduledNotePostFailed: "Ha fallado la publicación de una nota programada"
|
||||
newNote: "Nueva nota"
|
||||
unreadAntennaNote: "Antena {name}"
|
||||
roleAssigned: "Rol asignado"
|
||||
|
@ -3172,7 +3193,9 @@ _watermarkEditor:
|
|||
opacity: "Opacidad"
|
||||
scale: "Tamaño"
|
||||
text: "Texto"
|
||||
qr: "Código QR"
|
||||
position: "Posición"
|
||||
margin: "Margen"
|
||||
type: "Tipo"
|
||||
image: "Imágenes"
|
||||
advanced: "Avanzado"
|
||||
|
@ -3187,6 +3210,7 @@ _watermarkEditor:
|
|||
polkadotSubDotOpacity: "Opacidad del círculo secundario"
|
||||
polkadotSubDotRadius: "Tamaño del círculo secundario."
|
||||
polkadotSubDotDivisions: "Número de subpuntos."
|
||||
leaveBlankToAccountUrl: "Si dejas este campo en blanco, se utilizará la URL de tu cuenta."
|
||||
_imageEffector:
|
||||
title: "Efecto"
|
||||
addEffect: "Añadir Efecto"
|
||||
|
@ -3198,6 +3222,8 @@ _imageEffector:
|
|||
mirror: "Espejo"
|
||||
invert: "Invertir colores"
|
||||
grayscale: "Blanco y negro"
|
||||
blur: "Difuminar"
|
||||
pixelate: "Pixelar"
|
||||
colorAdjust: "Corrección de Color"
|
||||
colorClamp: "Compresión cromática"
|
||||
colorClampAdvanced: "Compresión cromática avanzada"
|
||||
|
@ -3209,10 +3235,13 @@ _imageEffector:
|
|||
checker: "Corrector"
|
||||
blockNoise: "Bloquear Ruido"
|
||||
tearing: "Rasgado de Imagen (Tearing)"
|
||||
fill: "Relleno de color"
|
||||
_fxProps:
|
||||
angle: "Ángulo"
|
||||
scale: "Tamaño"
|
||||
size: "Tamaño"
|
||||
radius: "Radio"
|
||||
samples: "Tamaño de muestra"
|
||||
offset: "Posición"
|
||||
color: "Color"
|
||||
opacity: "Opacidad"
|
||||
|
@ -3242,6 +3271,7 @@ _imageEffector:
|
|||
zoomLinesThreshold: "Ancho de línea del zoom"
|
||||
zoomLinesMaskSize: "Diámetro del centro"
|
||||
zoomLinesBlack: "Hacer oscuro"
|
||||
circle: "Círculo"
|
||||
drafts: "Borrador"
|
||||
_drafts:
|
||||
select: "Seleccionar borradores"
|
||||
|
@ -3257,6 +3287,22 @@ _drafts:
|
|||
restoreFromDraft: "Restaurar desde los borradores"
|
||||
restore: "Restaurar"
|
||||
listDrafts: "Listar los borradores"
|
||||
schedule: "Programar Nota"
|
||||
listScheduledNotes: "Lista de notas programadas"
|
||||
cancelSchedule: "Cancelar programación"
|
||||
qr: "Código QR"
|
||||
_qr:
|
||||
showTabTitle: "Apariencia"
|
||||
readTabTitle: "Escanear"
|
||||
shareTitle: "{name} {acct}"
|
||||
shareText: "¡Sígueme en el Fediverso!"
|
||||
chooseCamera: "Seleccione cámara"
|
||||
cannotToggleFlash: "No se puede activar el flash"
|
||||
turnOnFlash: "Encender el flash"
|
||||
turnOffFlash: "Apagar el flash"
|
||||
startQr: "Reiniciar el lector de códigos QR"
|
||||
stopQr: "Detener el lector de códigos QR"
|
||||
noQrCodeFound: "No se encontró el código QR"
|
||||
scanFile: "Escanear imagen desde un dispositivo"
|
||||
raw: "Texto"
|
||||
mfm: "MFM"
|
||||
|
|
|
@ -1030,6 +1030,10 @@ export interface Locale extends ILocale {
|
|||
* 処理中
|
||||
*/
|
||||
"processing": string;
|
||||
/**
|
||||
* 準備中
|
||||
*/
|
||||
"preprocessing": string;
|
||||
/**
|
||||
* プレビュー
|
||||
*/
|
||||
|
@ -1227,7 +1231,7 @@ export interface Locale extends ILocale {
|
|||
*/
|
||||
"noMoreHistory": string;
|
||||
/**
|
||||
* チャットを始める
|
||||
* メッセージを送る
|
||||
*/
|
||||
"startChat": string;
|
||||
/**
|
||||
|
@ -1927,7 +1931,7 @@ export interface Locale extends ILocale {
|
|||
*/
|
||||
"markAsReadAllUnreadNotes": string;
|
||||
/**
|
||||
* すべてのチャットを既読にする
|
||||
* すべてのダイレクトメッセージを既読にする
|
||||
*/
|
||||
"markAsReadAllTalkMessages": string;
|
||||
/**
|
||||
|
@ -5282,6 +5286,10 @@ export interface Locale extends ILocale {
|
|||
* 下書き
|
||||
*/
|
||||
"draft": string;
|
||||
/**
|
||||
* 下書きと予約投稿
|
||||
*/
|
||||
"draftsAndScheduledNotes": string;
|
||||
/**
|
||||
* リアクションする際に確認する
|
||||
*/
|
||||
|
@ -5390,6 +5398,14 @@ export interface Locale extends ILocale {
|
|||
* チャット
|
||||
*/
|
||||
"chat": string;
|
||||
/**
|
||||
* ダイレクトメッセージ
|
||||
*/
|
||||
"directMessage": string;
|
||||
/**
|
||||
* メッセージ
|
||||
*/
|
||||
"directMessage_short": string;
|
||||
/**
|
||||
* 旧設定情報を移行
|
||||
*/
|
||||
|
@ -5501,6 +5517,14 @@ export interface Locale extends ILocale {
|
|||
* 低くすると画質を保てますが、ファイルサイズは増加します。<br>高くするとファイルサイズを減らせますが、画質は低下します。
|
||||
*/
|
||||
"defaultImageCompressionLevel_description": string;
|
||||
/**
|
||||
* デフォルトの圧縮度
|
||||
*/
|
||||
"defaultCompressionLevel": string;
|
||||
/**
|
||||
* 低くすると品質を保てますが、ファイルサイズは増加します。<br>高くするとファイルサイズを減らせますが、品質は低下します。
|
||||
*/
|
||||
"defaultCompressionLevel_description": string;
|
||||
/**
|
||||
* 分
|
||||
*/
|
||||
|
@ -5529,6 +5553,60 @@ export interface Locale extends ILocale {
|
|||
* ベータ版の検証にご協力いただきありがとうございます!
|
||||
*/
|
||||
"thankYouForTestingBeta": string;
|
||||
/**
|
||||
* ユーザー指定ノートを作成
|
||||
*/
|
||||
"createUserSpecifiedNote": string;
|
||||
/**
|
||||
* 投稿を予約
|
||||
*/
|
||||
"schedulePost": string;
|
||||
/**
|
||||
* {x}に投稿を予約します
|
||||
*/
|
||||
"scheduleToPostOnX": ParameterizedString<"x">;
|
||||
/**
|
||||
* {x}に投稿が予約されています
|
||||
*/
|
||||
"scheduledToPostOnX": ParameterizedString<"x">;
|
||||
/**
|
||||
* 予約
|
||||
*/
|
||||
"schedule": string;
|
||||
/**
|
||||
* 予約
|
||||
*/
|
||||
"scheduled": string;
|
||||
"_compression": {
|
||||
"_quality": {
|
||||
/**
|
||||
* 高品質
|
||||
*/
|
||||
"high": string;
|
||||
/**
|
||||
* 中品質
|
||||
*/
|
||||
"medium": string;
|
||||
/**
|
||||
* 低品質
|
||||
*/
|
||||
"low": string;
|
||||
};
|
||||
"_size": {
|
||||
/**
|
||||
* サイズ大
|
||||
*/
|
||||
"large": string;
|
||||
/**
|
||||
* サイズ中
|
||||
*/
|
||||
"medium": string;
|
||||
/**
|
||||
* サイズ小
|
||||
*/
|
||||
"small": string;
|
||||
};
|
||||
};
|
||||
"_order": {
|
||||
/**
|
||||
* 新しい順
|
||||
|
@ -5540,6 +5618,10 @@ export interface Locale extends ILocale {
|
|||
"oldest": string;
|
||||
};
|
||||
"_chat": {
|
||||
/**
|
||||
* メッセージ
|
||||
*/
|
||||
"messages": string;
|
||||
/**
|
||||
* まだメッセージはありません
|
||||
*/
|
||||
|
@ -5549,36 +5631,36 @@ export interface Locale extends ILocale {
|
|||
*/
|
||||
"newMessage": string;
|
||||
/**
|
||||
* 個人チャット
|
||||
* 個別
|
||||
*/
|
||||
"individualChat": string;
|
||||
/**
|
||||
* 特定ユーザーとの一対一のチャットができます。
|
||||
* 特定ユーザーと個別にメッセージのやりとりができます。
|
||||
*/
|
||||
"individualChat_description": string;
|
||||
/**
|
||||
* ルームチャット
|
||||
* グループ
|
||||
*/
|
||||
"roomChat": string;
|
||||
/**
|
||||
* 複数人でのチャットができます。
|
||||
* また、個人チャットを許可していないユーザーとでも、相手が受け入れればチャットができます。
|
||||
* 複数人でメッセージのやりとりができます。
|
||||
* また、個別のメッセージを許可していないユーザーとでも、相手が受け入れればやりとりできます。
|
||||
*/
|
||||
"roomChat_description": string;
|
||||
/**
|
||||
* ルームを作成
|
||||
* グループを作成
|
||||
*/
|
||||
"createRoom": string;
|
||||
/**
|
||||
* ユーザーを招待してチャットを始めましょう
|
||||
* ユーザーを招待してメッセージを送信しましょう
|
||||
*/
|
||||
"inviteUserToChat": string;
|
||||
/**
|
||||
* 作成したルーム
|
||||
* 作成したグループ
|
||||
*/
|
||||
"yourRooms": string;
|
||||
/**
|
||||
* 参加中のルーム
|
||||
* 参加中のグループ
|
||||
*/
|
||||
"joiningRooms": string;
|
||||
/**
|
||||
|
@ -5598,7 +5680,7 @@ export interface Locale extends ILocale {
|
|||
*/
|
||||
"noHistory": string;
|
||||
/**
|
||||
* ルームはありません
|
||||
* グループはありません
|
||||
*/
|
||||
"noRooms": string;
|
||||
/**
|
||||
|
@ -5618,7 +5700,7 @@ export interface Locale extends ILocale {
|
|||
*/
|
||||
"ignore": string;
|
||||
/**
|
||||
* ルームから退出
|
||||
* グループから退出
|
||||
*/
|
||||
"leave": string;
|
||||
/**
|
||||
|
@ -5642,35 +5724,35 @@ export interface Locale extends ILocale {
|
|||
*/
|
||||
"newline": string;
|
||||
/**
|
||||
* このルームをミュート
|
||||
* このグループをミュート
|
||||
*/
|
||||
"muteThisRoom": string;
|
||||
/**
|
||||
* ルームを削除
|
||||
* グループを削除
|
||||
*/
|
||||
"deleteRoom": string;
|
||||
/**
|
||||
* このサーバー、またはこのアカウントでチャットは有効化されていません。
|
||||
* このサーバー、またはこのアカウントでダイレクトメッセージは有効化されていません。
|
||||
*/
|
||||
"chatNotAvailableForThisAccountOrServer": string;
|
||||
/**
|
||||
* このサーバー、またはこのアカウントでチャットは読み取り専用となっています。新たに書き込んだり、チャットルームを作成・参加したりすることはできません。
|
||||
* このサーバー、またはこのアカウントでダイレクトメッセージは読み取り専用となっています。新たに書き込んだり、グループを作成・参加したりすることはできません。
|
||||
*/
|
||||
"chatIsReadOnlyForThisAccountOrServer": string;
|
||||
/**
|
||||
* 相手のアカウントでチャット機能が使えない状態になっています。
|
||||
* 相手のアカウントでダイレクトメッセージが使えない状態になっています。
|
||||
*/
|
||||
"chatNotAvailableInOtherAccount": string;
|
||||
/**
|
||||
* このユーザーとのチャットを開始できません
|
||||
* このユーザーとのダイレクトメッセージを開始できません
|
||||
*/
|
||||
"cannotChatWithTheUser": string;
|
||||
/**
|
||||
* チャットが使えない状態になっているか、相手がチャットを開放していません。
|
||||
* ダイレクトメッセージが使えない状態になっているか、相手がダイレクトメッセージを開放していません。
|
||||
*/
|
||||
"cannotChatWithTheUser_description": string;
|
||||
/**
|
||||
* あなたはこのルームの参加者ではありませんが、招待が届いています。参加するには、招待を承認してください。
|
||||
* あなたはこのグループの参加者ではありませんが、招待が届いています。参加するには、招待を承認してください。
|
||||
*/
|
||||
"youAreNotAMemberOfThisRoomButInvited": string;
|
||||
/**
|
||||
|
@ -5678,31 +5760,31 @@ export interface Locale extends ILocale {
|
|||
*/
|
||||
"doYouAcceptInvitation": string;
|
||||
/**
|
||||
* チャットする
|
||||
* ダイレクトメッセージ
|
||||
*/
|
||||
"chatWithThisUser": string;
|
||||
/**
|
||||
* このユーザーはフォロワーからのみチャットを受け付けています。
|
||||
* このユーザーはフォロワーからのみメッセージを受け付けています。
|
||||
*/
|
||||
"thisUserAllowsChatOnlyFromFollowers": string;
|
||||
/**
|
||||
* このユーザーは、このユーザーがフォローしているユーザーからのみチャットを受け付けています。
|
||||
* このユーザーは、このユーザーがフォローしているユーザーからのみメッセージを受け付けています。
|
||||
*/
|
||||
"thisUserAllowsChatOnlyFromFollowing": string;
|
||||
/**
|
||||
* このユーザーは相互フォローのユーザーからのみチャットを受け付けています。
|
||||
* このユーザーは相互フォローのユーザーからのみメッセージを受け付けています。
|
||||
*/
|
||||
"thisUserAllowsChatOnlyFromMutualFollowing": string;
|
||||
/**
|
||||
* このユーザーは誰からもチャットを受け付けていません。
|
||||
* このユーザーは誰からもメッセージを受け付けていません。
|
||||
*/
|
||||
"thisUserNotAllowedChatAnyone": string;
|
||||
/**
|
||||
* チャットを許可する相手
|
||||
* メッセージを許可する相手
|
||||
*/
|
||||
"chatAllowedUsers": string;
|
||||
/**
|
||||
* 自分からチャットメッセージを送った相手とはこの設定に関わらずチャットが可能です。
|
||||
* 自分からメッセージを送った相手とはこの設定に関わらずメッセージの送受信が可能です。
|
||||
*/
|
||||
"chatAllowedUsers_note": string;
|
||||
"_chatAllowedUsers": {
|
||||
|
@ -7856,7 +7938,7 @@ export interface Locale extends ILocale {
|
|||
*/
|
||||
"canImportUserLists": string;
|
||||
/**
|
||||
* チャットを許可
|
||||
* ダイレクトメッセージを許可
|
||||
*/
|
||||
"chatAvailability": string;
|
||||
/**
|
||||
|
@ -7875,6 +7957,10 @@ export interface Locale extends ILocale {
|
|||
* サーバーサイドのノートの下書きの作成可能数
|
||||
*/
|
||||
"noteDraftLimit": string;
|
||||
/**
|
||||
* 予約投稿の同時作成可能数
|
||||
*/
|
||||
"scheduledNoteLimit": string;
|
||||
/**
|
||||
* ウォーターマーク機能の使用可否
|
||||
*/
|
||||
|
@ -8706,7 +8792,7 @@ export interface Locale extends ILocale {
|
|||
*/
|
||||
"badge": string;
|
||||
/**
|
||||
* チャットの背景
|
||||
* メッセージの背景
|
||||
*/
|
||||
"messageBg": string;
|
||||
/**
|
||||
|
@ -8733,7 +8819,7 @@ export interface Locale extends ILocale {
|
|||
*/
|
||||
"reaction": string;
|
||||
/**
|
||||
* チャットのメッセージ
|
||||
* ダイレクトメッセージ
|
||||
*/
|
||||
"chatMessage": string;
|
||||
};
|
||||
|
@ -9017,11 +9103,11 @@ export interface Locale extends ILocale {
|
|||
*/
|
||||
"write:following": string;
|
||||
/**
|
||||
* チャットを見る
|
||||
* ダイレクトメッセージを見る
|
||||
*/
|
||||
"read:messaging": string;
|
||||
/**
|
||||
* チャットを操作する
|
||||
* ダイレクトメッセージを操作する
|
||||
*/
|
||||
"write:messaging": string;
|
||||
/**
|
||||
|
@ -9313,11 +9399,11 @@ export interface Locale extends ILocale {
|
|||
*/
|
||||
"write:report-abuse": string;
|
||||
/**
|
||||
* チャットを操作する
|
||||
* ダイレクトメッセージを操作する
|
||||
*/
|
||||
"write:chat": string;
|
||||
/**
|
||||
* チャットを閲覧する
|
||||
* ダイレクトメッセージを閲覧する
|
||||
*/
|
||||
"read:chat": string;
|
||||
};
|
||||
|
@ -9543,7 +9629,7 @@ export interface Locale extends ILocale {
|
|||
*/
|
||||
"birthdayFollowings": string;
|
||||
/**
|
||||
* チャット
|
||||
* ダイレクトメッセージ
|
||||
*/
|
||||
"chat": string;
|
||||
};
|
||||
|
@ -10270,6 +10356,14 @@ export interface Locale extends ILocale {
|
|||
* アンケートの結果が出ました
|
||||
*/
|
||||
"pollEnded": string;
|
||||
/**
|
||||
* 予約ノートが投稿されました
|
||||
*/
|
||||
"scheduledNotePosted": string;
|
||||
/**
|
||||
* 予約ノートの投稿に失敗しました
|
||||
*/
|
||||
"scheduledNotePostFailed": string;
|
||||
/**
|
||||
* 新しい投稿
|
||||
*/
|
||||
|
@ -10283,7 +10377,7 @@ export interface Locale extends ILocale {
|
|||
*/
|
||||
"roleAssigned": string;
|
||||
/**
|
||||
* チャットルームへ招待されました
|
||||
* ダイレクトメッセージのグループへ招待されました
|
||||
*/
|
||||
"chatRoomInvitationReceived": string;
|
||||
/**
|
||||
|
@ -10396,7 +10490,7 @@ export interface Locale extends ILocale {
|
|||
*/
|
||||
"roleAssigned": string;
|
||||
/**
|
||||
* チャットルームへ招待された
|
||||
* ダイレクトメッセージのグループへ招待された
|
||||
*/
|
||||
"chatRoomInvitationReceived": string;
|
||||
/**
|
||||
|
@ -10578,7 +10672,7 @@ export interface Locale extends ILocale {
|
|||
*/
|
||||
"roleTimeline": string;
|
||||
/**
|
||||
* チャット
|
||||
* ダイレクトメッセージ
|
||||
*/
|
||||
"chat": string;
|
||||
};
|
||||
|
@ -10945,7 +11039,7 @@ export interface Locale extends ILocale {
|
|||
*/
|
||||
"deleteGalleryPost": string;
|
||||
/**
|
||||
* チャットルームを削除
|
||||
* ダイレクトメッセージのグループを削除
|
||||
*/
|
||||
"deleteChatRoom": string;
|
||||
/**
|
||||
|
@ -12219,10 +12313,18 @@ export interface Locale extends ILocale {
|
|||
* テキスト
|
||||
*/
|
||||
"text": string;
|
||||
/**
|
||||
* 二次元コード
|
||||
*/
|
||||
"qr": string;
|
||||
/**
|
||||
* 位置
|
||||
*/
|
||||
"position": string;
|
||||
/**
|
||||
* マージン
|
||||
*/
|
||||
"margin": string;
|
||||
/**
|
||||
* タイプ
|
||||
*/
|
||||
|
@ -12279,6 +12381,10 @@ export interface Locale extends ILocale {
|
|||
* サブドットの数
|
||||
*/
|
||||
"polkadotSubDotDivisions": string;
|
||||
/**
|
||||
* 空欄にするとアカウントのURLになります
|
||||
*/
|
||||
"leaveBlankToAccountUrl": string;
|
||||
};
|
||||
"_imageEffector": {
|
||||
/**
|
||||
|
@ -12318,6 +12424,14 @@ export interface Locale extends ILocale {
|
|||
* 白黒
|
||||
*/
|
||||
"grayscale": string;
|
||||
/**
|
||||
* ぼかし
|
||||
*/
|
||||
"blur": string;
|
||||
/**
|
||||
* モザイク
|
||||
*/
|
||||
"pixelate": string;
|
||||
/**
|
||||
* 色調補正
|
||||
*/
|
||||
|
@ -12362,6 +12476,10 @@ export interface Locale extends ILocale {
|
|||
* ティアリング
|
||||
*/
|
||||
"tearing": string;
|
||||
/**
|
||||
* 塗りつぶし
|
||||
*/
|
||||
"fill": string;
|
||||
};
|
||||
"_fxProps": {
|
||||
/**
|
||||
|
@ -12376,6 +12494,18 @@ export interface Locale extends ILocale {
|
|||
* サイズ
|
||||
*/
|
||||
"size": string;
|
||||
/**
|
||||
* 半径
|
||||
*/
|
||||
"radius": string;
|
||||
/**
|
||||
* サンプル数
|
||||
*/
|
||||
"samples": string;
|
||||
/**
|
||||
* 位置
|
||||
*/
|
||||
"offset": string;
|
||||
/**
|
||||
* 色
|
||||
*/
|
||||
|
@ -12488,6 +12618,10 @@ export interface Locale extends ILocale {
|
|||
* 黒色にする
|
||||
*/
|
||||
"zoomLinesBlack": string;
|
||||
/**
|
||||
* 円形
|
||||
*/
|
||||
"circle": string;
|
||||
};
|
||||
};
|
||||
/**
|
||||
|
@ -12547,6 +12681,80 @@ export interface Locale extends ILocale {
|
|||
* 下書き一覧
|
||||
*/
|
||||
"listDrafts": string;
|
||||
/**
|
||||
* 投稿予約
|
||||
*/
|
||||
"schedule": string;
|
||||
/**
|
||||
* 予約投稿一覧
|
||||
*/
|
||||
"listScheduledNotes": string;
|
||||
/**
|
||||
* 予約解除
|
||||
*/
|
||||
"cancelSchedule": string;
|
||||
};
|
||||
/**
|
||||
* 二次元コード
|
||||
*/
|
||||
"qr": string;
|
||||
"_qr": {
|
||||
/**
|
||||
* 表示
|
||||
*/
|
||||
"showTabTitle": string;
|
||||
/**
|
||||
* 読み取る
|
||||
*/
|
||||
"readTabTitle": string;
|
||||
/**
|
||||
* {name} {acct}
|
||||
*/
|
||||
"shareTitle": ParameterizedString<"name" | "acct">;
|
||||
/**
|
||||
* Fediverseで私をフォローしてください!
|
||||
*/
|
||||
"shareText": string;
|
||||
/**
|
||||
* カメラを選択
|
||||
*/
|
||||
"chooseCamera": string;
|
||||
/**
|
||||
* ライト選択不可
|
||||
*/
|
||||
"cannotToggleFlash": string;
|
||||
/**
|
||||
* ライトをオンにする
|
||||
*/
|
||||
"turnOnFlash": string;
|
||||
/**
|
||||
* ライトをオフにする
|
||||
*/
|
||||
"turnOffFlash": string;
|
||||
/**
|
||||
* コードリーダーを再開
|
||||
*/
|
||||
"startQr": string;
|
||||
/**
|
||||
* コードリーダーを停止
|
||||
*/
|
||||
"stopQr": string;
|
||||
/**
|
||||
* QRコードが見つかりません
|
||||
*/
|
||||
"noQrCodeFound": string;
|
||||
/**
|
||||
* 端末の画像をスキャン
|
||||
*/
|
||||
"scanFile": string;
|
||||
/**
|
||||
* テキスト
|
||||
*/
|
||||
"raw": string;
|
||||
/**
|
||||
* MFM
|
||||
*/
|
||||
"mfm": string;
|
||||
};
|
||||
}
|
||||
declare const locales: {
|
||||
|
|
|
@ -253,6 +253,7 @@ noteDeleteConfirm: "Vuoi davvero eliminare questa Nota?"
|
|||
pinLimitExceeded: "Non puoi fissare altre note "
|
||||
done: "Fine"
|
||||
processing: "In elaborazione"
|
||||
preprocessing: "In preparazione"
|
||||
preview: "Anteprima"
|
||||
default: "Predefinito"
|
||||
defaultValueIs: "Predefinito: {value}"
|
||||
|
@ -1316,6 +1317,7 @@ acknowledgeNotesAndEnable: "Attivare dopo averne compreso il comportamento."
|
|||
federationSpecified: "Questo server è federato solo con istanze specifiche del Fediverso. Puoi interagire solo con quelle scelte dall'amministrazione."
|
||||
federationDisabled: "Questo server ha la federazione disabilitata. Non puoi interagire con profili provenienti da altri server."
|
||||
draft: "Bozza"
|
||||
draftsAndScheduledNotes: "Bozze e Note pianificate"
|
||||
confirmOnReact: "Confermare le reazioni"
|
||||
reactAreYouSure: "Vuoi davvero reagire con {emoji} ?"
|
||||
markAsSensitiveConfirm: "Vuoi davvero indicare questo contenuto multimediale come esplicito?"
|
||||
|
@ -1372,6 +1374,8 @@ redisplayAllTips: "Mostra tutti i suggerimenti"
|
|||
hideAllTips: "Nascondi tutti i suggerimenti"
|
||||
defaultImageCompressionLevel: "Livello predefinito di compressione immagini"
|
||||
defaultImageCompressionLevel_description: "La compressione diminuisce la qualità dell'immagine, poca compressione mantiene alta qualità delle immagini. Aumentandola, si riducono le dimensioni del file, a discapito della qualità dell'immagine."
|
||||
defaultCompressionLevel: "Compressione predefinita"
|
||||
defaultCompressionLevel_description: "Diminuisci per mantenere la qualità aumentando le dimensioni del file.<br> Aumenta per ridurre le dimensioni del file e anche la qualità."
|
||||
inMinutes: "min"
|
||||
inDays: "giorni"
|
||||
safeModeEnabled: "La modalità sicura è attiva"
|
||||
|
@ -1380,6 +1384,20 @@ customCssIsDisabledBecauseSafeMode: "Il CSS personalizzato non è stato applicat
|
|||
themeIsDefaultBecauseSafeMode: "Quando la modalità sicura è attiva, viene utilizzato il tema predefinito. Quando la modalità sicura viene disattivata, il tema torna a essere quello precedente."
|
||||
thankYouForTestingBeta: "Grazie per la tua collaborazione nella verifica delle versioni beta!"
|
||||
createUserSpecifiedNote: "Creare Nota personalizzata"
|
||||
schedulePost: "Pianificare la pubblicazione"
|
||||
scheduleToPostOnX: "Pianificare la pubblicazione {x}"
|
||||
scheduledToPostOnX: "Pubblicazione pianificata {x}"
|
||||
schedule: "Pianificare"
|
||||
scheduled: "Pianificata"
|
||||
_compression:
|
||||
_quality:
|
||||
high: "Alta qualità"
|
||||
medium: "Media qualità"
|
||||
low: "Bassa qualità"
|
||||
_size:
|
||||
large: "Taglia grande"
|
||||
medium: "Taglia media"
|
||||
small: "Taglia piccola"
|
||||
_order:
|
||||
newest: "Prima i più recenti"
|
||||
oldest: "Meno recenti prima"
|
||||
|
@ -2022,6 +2040,7 @@ _role:
|
|||
uploadableFileTypes_caption: "Specifica il tipo MIME. Puoi specificare più valori separandoli andando a capo, oppure indicare caratteri jolly con un asterisco (*). Ad esempio: image/*"
|
||||
uploadableFileTypes_caption2: "A seconda del file, il tipo potrebbe non essere determinato. Se si desidera consentire tali file, aggiungere {x} alla specifica."
|
||||
noteDraftLimit: "Numero massimo di Note in bozza, lato server"
|
||||
scheduledNoteLimit: "Quantità di Note pianificabili contemporaneamente"
|
||||
watermarkAvailable: "Disponibilità della funzione filigrana"
|
||||
_condition:
|
||||
roleAssignedTo: "Assegnato a ruoli manualmente"
|
||||
|
@ -2647,6 +2666,8 @@ _notification:
|
|||
youReceivedFollowRequest: "Hai ricevuto una richiesta di follow"
|
||||
yourFollowRequestAccepted: "La tua richiesta di follow è stata accettata"
|
||||
pollEnded: "Risultati del sondaggio."
|
||||
scheduledNotePosted: "Pubblicazione Nota pianificata"
|
||||
scheduledNotePostFailed: "Impossibile pubblicare la Nota pianificata"
|
||||
newNote: "Nuove Note"
|
||||
unreadAntennaNote: "Antenna {name}"
|
||||
roleAssigned: "Ruolo assegnato"
|
||||
|
@ -3172,7 +3193,9 @@ _watermarkEditor:
|
|||
opacity: "Opacità"
|
||||
scale: "Dimensioni"
|
||||
text: "Testo"
|
||||
qr: "QR Code"
|
||||
position: "Posizione"
|
||||
margin: "Margine"
|
||||
type: "Tipo"
|
||||
image: "Immagini"
|
||||
advanced: "Avanzato"
|
||||
|
@ -3187,6 +3210,7 @@ _watermarkEditor:
|
|||
polkadotSubDotOpacity: "Opacità del punto secondario"
|
||||
polkadotSubDotRadius: "Dimensione del punto secondario"
|
||||
polkadotSubDotDivisions: "Quantità di punti secondari"
|
||||
leaveBlankToAccountUrl: "Il valore vuoto indica la URL dell'account"
|
||||
_imageEffector:
|
||||
title: "Effetto"
|
||||
addEffect: "Aggiungi effetto"
|
||||
|
@ -3198,6 +3222,8 @@ _imageEffector:
|
|||
mirror: "Specchio"
|
||||
invert: "Inversione colore"
|
||||
grayscale: "Bianco e nero"
|
||||
blur: "Sfocatura"
|
||||
pixelate: "Mosaico"
|
||||
colorAdjust: "Correzione Colore"
|
||||
colorClamp: "Compressione del colore"
|
||||
colorClampAdvanced: "Compressione del colore (avanzata)"
|
||||
|
@ -3209,11 +3235,13 @@ _imageEffector:
|
|||
checker: "revisore"
|
||||
blockNoise: "Attenua rumore"
|
||||
tearing: "Strappa immagine"
|
||||
fillSquare: "Riempi (quadrato)"
|
||||
fill: "Riempimento"
|
||||
_fxProps:
|
||||
angle: "Angolo"
|
||||
scale: "Dimensioni"
|
||||
size: "Dimensioni"
|
||||
radius: "Raggio"
|
||||
samples: "Quantità di campioni"
|
||||
offset: "Posizione"
|
||||
color: "Colore"
|
||||
opacity: "Opacità"
|
||||
|
@ -3243,6 +3271,7 @@ _imageEffector:
|
|||
zoomLinesThreshold: "Limite delle linee zoom"
|
||||
zoomLinesMaskSize: "Ampiezza del diametro"
|
||||
zoomLinesBlack: "Bande nere"
|
||||
circle: "Circolare"
|
||||
drafts: "Bozze"
|
||||
_drafts:
|
||||
select: "Selezionare bozza"
|
||||
|
@ -3258,6 +3287,22 @@ _drafts:
|
|||
restoreFromDraft: "Recuperare dalle bozze"
|
||||
restore: "Ripristina"
|
||||
listDrafts: "Elenco bozze"
|
||||
schedule: "Pianifica pubblicazione"
|
||||
listScheduledNotes: "Elenca Note pianificate"
|
||||
cancelSchedule: "Annulla pianificazione"
|
||||
qr: "QR Code"
|
||||
_qr:
|
||||
showTabTitle: "Visualizza"
|
||||
readTabTitle: "Leggere"
|
||||
shareTitle: "{name} {acct}"
|
||||
shareText: "Seguimi nel Fediverso!"
|
||||
chooseCamera: "Seleziona fotocamera"
|
||||
cannotToggleFlash: "Flash non controllabile"
|
||||
turnOnFlash: "Accendi il flash"
|
||||
turnOffFlash: "Spegni il flash"
|
||||
startQr: "Inizia lettura QR Code"
|
||||
stopQr: "Interrompi lettura QR Code"
|
||||
noQrCodeFound: "Non trovo alcun QR Code"
|
||||
scanFile: "Scansiona immagine nel dispositivo"
|
||||
raw: "Testo"
|
||||
mfm: "MFM"
|
||||
|
|
|
@ -253,6 +253,7 @@ noteDeleteConfirm: "このノートを削除しますか?"
|
|||
pinLimitExceeded: "これ以上ピン留めできません"
|
||||
done: "完了"
|
||||
processing: "処理中"
|
||||
preprocessing: "準備中"
|
||||
preview: "プレビュー"
|
||||
default: "デフォルト"
|
||||
defaultValueIs: "デフォルト: {value}"
|
||||
|
@ -302,7 +303,7 @@ uploadNFiles: "{n}個のファイルをアップロード"
|
|||
explore: "みつける"
|
||||
messageRead: "既読"
|
||||
noMoreHistory: "これより過去の履歴はありません"
|
||||
startChat: "チャットを始める"
|
||||
startChat: "メッセージを送る"
|
||||
nUsersRead: "{n}人が読みました"
|
||||
agreeTo: "{0}に同意"
|
||||
agree: "同意する"
|
||||
|
@ -477,7 +478,7 @@ notFoundDescription: "指定されたURLに該当するページはありませ
|
|||
uploadFolder: "既定アップロード先"
|
||||
markAsReadAllNotifications: "すべての通知を既読にする"
|
||||
markAsReadAllUnreadNotes: "すべての投稿を既読にする"
|
||||
markAsReadAllTalkMessages: "すべてのチャットを既読にする"
|
||||
markAsReadAllTalkMessages: "すべてのダイレクトメッセージを既読にする"
|
||||
help: "ヘルプ"
|
||||
inputMessageHere: "ここにメッセージを入力"
|
||||
close: "閉じる"
|
||||
|
@ -1316,6 +1317,7 @@ acknowledgeNotesAndEnable: "注意事項を理解した上でオンにします
|
|||
federationSpecified: "このサーバーはホワイトリスト連合で運用されています。管理者が指定したサーバー以外とやり取りすることはできません。"
|
||||
federationDisabled: "このサーバーは連合が無効化されています。他のサーバーのユーザーとやり取りすることはできません。"
|
||||
draft: "下書き"
|
||||
draftsAndScheduledNotes: "下書きと予約投稿"
|
||||
confirmOnReact: "リアクションする際に確認する"
|
||||
reactAreYouSure: "\" {emoji} \" をリアクションしますか?"
|
||||
markAsSensitiveConfirm: "このメディアをセンシティブとして設定しますか?"
|
||||
|
@ -1343,6 +1345,8 @@ postForm: "投稿フォーム"
|
|||
textCount: "文字数"
|
||||
information: "情報"
|
||||
chat: "チャット"
|
||||
directMessage: "ダイレクトメッセージ"
|
||||
directMessage_short: "メッセージ"
|
||||
migrateOldSettings: "旧設定情報を移行"
|
||||
migrateOldSettings_description: "通常これは自動で行われていますが、何らかの理由により上手く移行されなかった場合は手動で移行処理をトリガーできます。現在の設定情報は上書きされます。"
|
||||
compress: "圧縮"
|
||||
|
@ -1370,6 +1374,8 @@ redisplayAllTips: "全ての「ヒントとコツ」を再表示"
|
|||
hideAllTips: "全ての「ヒントとコツ」を非表示"
|
||||
defaultImageCompressionLevel: "デフォルトの画像圧縮度"
|
||||
defaultImageCompressionLevel_description: "低くすると画質を保てますが、ファイルサイズは増加します。<br>高くするとファイルサイズを減らせますが、画質は低下します。"
|
||||
defaultCompressionLevel: "デフォルトの圧縮度"
|
||||
defaultCompressionLevel_description: "低くすると品質を保てますが、ファイルサイズは増加します。<br>高くするとファイルサイズを減らせますが、品質は低下します。"
|
||||
inMinutes: "分"
|
||||
inDays: "日"
|
||||
safeModeEnabled: "セーフモードが有効です"
|
||||
|
@ -1377,53 +1383,70 @@ pluginsAreDisabledBecauseSafeMode: "セーフモードが有効なため、プ
|
|||
customCssIsDisabledBecauseSafeMode: "セーフモードが有効なため、カスタムCSSは適用されていません。"
|
||||
themeIsDefaultBecauseSafeMode: "セーフモードが有効な間はデフォルトのテーマが使用されます。セーフモードをオフにすると元に戻ります。"
|
||||
thankYouForTestingBeta: "ベータ版の検証にご協力いただきありがとうございます!"
|
||||
createUserSpecifiedNote: "ユーザー指定ノートを作成"
|
||||
schedulePost: "投稿を予約"
|
||||
scheduleToPostOnX: "{x}に投稿を予約します"
|
||||
scheduledToPostOnX: "{x}に投稿が予約されています"
|
||||
schedule: "予約"
|
||||
scheduled: "予約"
|
||||
|
||||
_compression:
|
||||
_quality:
|
||||
high: "高品質"
|
||||
medium: "中品質"
|
||||
low: "低品質"
|
||||
_size:
|
||||
large: "サイズ大"
|
||||
medium: "サイズ中"
|
||||
small: "サイズ小"
|
||||
|
||||
_order:
|
||||
newest: "新しい順"
|
||||
oldest: "古い順"
|
||||
|
||||
_chat:
|
||||
messages: "メッセージ"
|
||||
noMessagesYet: "まだメッセージはありません"
|
||||
newMessage: "新しいメッセージ"
|
||||
individualChat: "個人チャット"
|
||||
individualChat_description: "特定ユーザーとの一対一のチャットができます。"
|
||||
roomChat: "ルームチャット"
|
||||
roomChat_description: "複数人でのチャットができます。\nまた、個人チャットを許可していないユーザーとでも、相手が受け入れればチャットができます。"
|
||||
createRoom: "ルームを作成"
|
||||
inviteUserToChat: "ユーザーを招待してチャットを始めましょう"
|
||||
yourRooms: "作成したルーム"
|
||||
joiningRooms: "参加中のルーム"
|
||||
individualChat: "個別"
|
||||
individualChat_description: "特定ユーザーと個別にメッセージのやりとりができます。"
|
||||
roomChat: "グループ"
|
||||
roomChat_description: "複数人でメッセージのやりとりができます。\nまた、個別のメッセージを許可していないユーザーとでも、相手が受け入れればやりとりできます。"
|
||||
createRoom: "グループを作成"
|
||||
inviteUserToChat: "ユーザーを招待してメッセージを送信しましょう"
|
||||
yourRooms: "作成したグループ"
|
||||
joiningRooms: "参加中のグループ"
|
||||
invitations: "招待"
|
||||
noInvitations: "招待はありません"
|
||||
history: "履歴"
|
||||
noHistory: "履歴はありません"
|
||||
noRooms: "ルームはありません"
|
||||
noRooms: "グループはありません"
|
||||
inviteUser: "ユーザーを招待"
|
||||
sentInvitations: "送信した招待"
|
||||
join: "参加"
|
||||
ignore: "無視"
|
||||
leave: "ルームから退出"
|
||||
leave: "グループから退出"
|
||||
members: "メンバー"
|
||||
searchMessages: "メッセージを検索"
|
||||
home: "ホーム"
|
||||
send: "送信"
|
||||
newline: "改行"
|
||||
muteThisRoom: "このルームをミュート"
|
||||
deleteRoom: "ルームを削除"
|
||||
chatNotAvailableForThisAccountOrServer: "このサーバー、またはこのアカウントでチャットは有効化されていません。"
|
||||
chatIsReadOnlyForThisAccountOrServer: "このサーバー、またはこのアカウントでチャットは読み取り専用となっています。新たに書き込んだり、チャットルームを作成・参加したりすることはできません。"
|
||||
chatNotAvailableInOtherAccount: "相手のアカウントでチャット機能が使えない状態になっています。"
|
||||
cannotChatWithTheUser: "このユーザーとのチャットを開始できません"
|
||||
cannotChatWithTheUser_description: "チャットが使えない状態になっているか、相手がチャットを開放していません。"
|
||||
youAreNotAMemberOfThisRoomButInvited: "あなたはこのルームの参加者ではありませんが、招待が届いています。参加するには、招待を承認してください。"
|
||||
muteThisRoom: "このグループをミュート"
|
||||
deleteRoom: "グループを削除"
|
||||
chatNotAvailableForThisAccountOrServer: "このサーバー、またはこのアカウントでダイレクトメッセージは有効化されていません。"
|
||||
chatIsReadOnlyForThisAccountOrServer: "このサーバー、またはこのアカウントでダイレクトメッセージは読み取り専用となっています。新たに書き込んだり、グループを作成・参加したりすることはできません。"
|
||||
chatNotAvailableInOtherAccount: "相手のアカウントでダイレクトメッセージが使えない状態になっています。"
|
||||
cannotChatWithTheUser: "このユーザーとのダイレクトメッセージを開始できません"
|
||||
cannotChatWithTheUser_description: "ダイレクトメッセージが使えない状態になっているか、相手がダイレクトメッセージを開放していません。"
|
||||
youAreNotAMemberOfThisRoomButInvited: "あなたはこのグループの参加者ではありませんが、招待が届いています。参加するには、招待を承認してください。"
|
||||
doYouAcceptInvitation: "招待を承認しますか?"
|
||||
chatWithThisUser: "チャットする"
|
||||
thisUserAllowsChatOnlyFromFollowers: "このユーザーはフォロワーからのみチャットを受け付けています。"
|
||||
thisUserAllowsChatOnlyFromFollowing: "このユーザーは、このユーザーがフォローしているユーザーからのみチャットを受け付けています。"
|
||||
thisUserAllowsChatOnlyFromMutualFollowing: "このユーザーは相互フォローのユーザーからのみチャットを受け付けています。"
|
||||
thisUserNotAllowedChatAnyone: "このユーザーは誰からもチャットを受け付けていません。"
|
||||
chatAllowedUsers: "チャットを許可する相手"
|
||||
chatAllowedUsers_note: "自分からチャットメッセージを送った相手とはこの設定に関わらずチャットが可能です。"
|
||||
chatWithThisUser: "ダイレクトメッセージ"
|
||||
thisUserAllowsChatOnlyFromFollowers: "このユーザーはフォロワーからのみメッセージを受け付けています。"
|
||||
thisUserAllowsChatOnlyFromFollowing: "このユーザーは、このユーザーがフォローしているユーザーからのみメッセージを受け付けています。"
|
||||
thisUserAllowsChatOnlyFromMutualFollowing: "このユーザーは相互フォローのユーザーからのみメッセージを受け付けています。"
|
||||
thisUserNotAllowedChatAnyone: "このユーザーは誰からもメッセージを受け付けていません。"
|
||||
chatAllowedUsers: "メッセージを許可する相手"
|
||||
chatAllowedUsers_note: "自分からメッセージを送った相手とはこの設定に関わらずメッセージの送受信が可能です。"
|
||||
_chatAllowedUsers:
|
||||
everyone: "誰でも"
|
||||
followers: "自分のフォロワーのみ"
|
||||
|
@ -2034,11 +2057,12 @@ _role:
|
|||
canImportFollowing: "フォローのインポートを許可"
|
||||
canImportMuting: "ミュートのインポートを許可"
|
||||
canImportUserLists: "リストのインポートを許可"
|
||||
chatAvailability: "チャットを許可"
|
||||
chatAvailability: "ダイレクトメッセージを許可"
|
||||
uploadableFileTypes: "アップロード可能なファイル種別"
|
||||
uploadableFileTypes_caption: "MIMEタイプを指定します。改行で区切って複数指定できるほか、アスタリスク(*)でワイルドカード指定できます。(例: image/*)"
|
||||
uploadableFileTypes_caption2: "ファイルによっては種別を判定できないことがあります。そのようなファイルを許可する場合は {x} を指定に追加してください。"
|
||||
noteDraftLimit: "サーバーサイドのノートの下書きの作成可能数"
|
||||
scheduledNoteLimit: "予約投稿の同時作成可能数"
|
||||
watermarkAvailable: "ウォーターマーク機能の使用可否"
|
||||
_condition:
|
||||
roleAssignedTo: "マニュアルロールにアサイン済み"
|
||||
|
@ -2281,7 +2305,7 @@ _theme:
|
|||
buttonHoverBg: "ボタンの背景 (ホバー)"
|
||||
inputBorder: "入力ボックスの縁取り"
|
||||
badge: "バッジ"
|
||||
messageBg: "チャットの背景"
|
||||
messageBg: "メッセージの背景"
|
||||
fgHighlighted: "強調された文字"
|
||||
|
||||
_sfx:
|
||||
|
@ -2289,7 +2313,7 @@ _sfx:
|
|||
noteMy: "ノート(自分)"
|
||||
notification: "通知"
|
||||
reaction: "リアクション選択時"
|
||||
chatMessage: "チャットのメッセージ"
|
||||
chatMessage: "ダイレクトメッセージ"
|
||||
|
||||
_soundSettings:
|
||||
driveFile: "ドライブの音声を使用"
|
||||
|
@ -2369,8 +2393,8 @@ _permissions:
|
|||
"write:favorites": "お気に入りを操作する"
|
||||
"read:following": "フォローの情報を見る"
|
||||
"write:following": "フォロー・フォロー解除する"
|
||||
"read:messaging": "チャットを見る"
|
||||
"write:messaging": "チャットを操作する"
|
||||
"read:messaging": "ダイレクトメッセージを見る"
|
||||
"write:messaging": "ダイレクトメッセージを操作する"
|
||||
"read:mutes": "ミュートを見る"
|
||||
"write:mutes": "ミュートを操作する"
|
||||
"write:notes": "ノートを作成・削除する"
|
||||
|
@ -2443,8 +2467,8 @@ _permissions:
|
|||
"read:clip-favorite": "クリップのいいねを見る"
|
||||
"read:federation": "連合に関する情報を取得する"
|
||||
"write:report-abuse": "違反を報告する"
|
||||
"write:chat": "チャットを操作する"
|
||||
"read:chat": "チャットを閲覧する"
|
||||
"write:chat": "ダイレクトメッセージを操作する"
|
||||
"read:chat": "ダイレクトメッセージを閲覧する"
|
||||
|
||||
_auth:
|
||||
shareAccessTitle: "アプリへのアクセス許可"
|
||||
|
@ -2507,7 +2531,7 @@ _widgets:
|
|||
chooseList: "リストを選択"
|
||||
clicker: "クリッカー"
|
||||
birthdayFollowings: "今日誕生日のユーザー"
|
||||
chat: "チャット"
|
||||
chat: "ダイレクトメッセージ"
|
||||
|
||||
_cw:
|
||||
hide: "隠す"
|
||||
|
@ -2711,10 +2735,12 @@ _notification:
|
|||
youReceivedFollowRequest: "フォローリクエストが来ました"
|
||||
yourFollowRequestAccepted: "フォローリクエストが承認されました"
|
||||
pollEnded: "アンケートの結果が出ました"
|
||||
scheduledNotePosted: "予約ノートが投稿されました"
|
||||
scheduledNotePostFailed: "予約ノートの投稿に失敗しました"
|
||||
newNote: "新しい投稿"
|
||||
unreadAntennaNote: "アンテナ {name}"
|
||||
roleAssigned: "ロールが付与されました"
|
||||
chatRoomInvitationReceived: "チャットルームへ招待されました"
|
||||
chatRoomInvitationReceived: "ダイレクトメッセージのグループへ招待されました"
|
||||
emptyPushNotificationMessage: "プッシュ通知の更新をしました"
|
||||
achievementEarned: "実績を獲得"
|
||||
testNotification: "通知テスト"
|
||||
|
@ -2744,7 +2770,7 @@ _notification:
|
|||
receiveFollowRequest: "フォロー申請を受け取った"
|
||||
followRequestAccepted: "フォローが受理された"
|
||||
roleAssigned: "ロールが付与された"
|
||||
chatRoomInvitationReceived: "チャットルームへ招待された"
|
||||
chatRoomInvitationReceived: "ダイレクトメッセージのグループへ招待された"
|
||||
achievementEarned: "実績の獲得"
|
||||
exportCompleted: "エクスポートが完了した"
|
||||
login: "ログイン"
|
||||
|
@ -2794,7 +2820,7 @@ _deck:
|
|||
mentions: "メンション"
|
||||
direct: "指名"
|
||||
roleTimeline: "ロールタイムライン"
|
||||
chat: "チャット"
|
||||
chat: "ダイレクトメッセージ"
|
||||
|
||||
_dialog:
|
||||
charactersExceeded: "最大文字数を超えています! 現在 {current} / 制限 {max}"
|
||||
|
@ -2897,7 +2923,7 @@ _moderationLogTypes:
|
|||
deletePage: "ページを削除"
|
||||
deleteFlash: "Playを削除"
|
||||
deleteGalleryPost: "ギャラリーの投稿を削除"
|
||||
deleteChatRoom: "チャットルームを削除"
|
||||
deleteChatRoom: "ダイレクトメッセージのグループを削除"
|
||||
updateProxyAccountDescription: "プロキシアカウントの説明を更新"
|
||||
|
||||
_fileViewer:
|
||||
|
@ -3271,7 +3297,9 @@ _watermarkEditor:
|
|||
opacity: "不透明度"
|
||||
scale: "サイズ"
|
||||
text: "テキスト"
|
||||
qr: "二次元コード"
|
||||
position: "位置"
|
||||
margin: "マージン"
|
||||
type: "タイプ"
|
||||
image: "画像"
|
||||
advanced: "高度"
|
||||
|
@ -3286,6 +3314,7 @@ _watermarkEditor:
|
|||
polkadotSubDotOpacity: "サブドットの不透明度"
|
||||
polkadotSubDotRadius: "サブドットの大きさ"
|
||||
polkadotSubDotDivisions: "サブドットの数"
|
||||
leaveBlankToAccountUrl: "空欄にするとアカウントのURLになります"
|
||||
|
||||
_imageEffector:
|
||||
title: "エフェクト"
|
||||
|
@ -3299,6 +3328,8 @@ _imageEffector:
|
|||
mirror: "ミラー"
|
||||
invert: "色の反転"
|
||||
grayscale: "白黒"
|
||||
blur: "ぼかし"
|
||||
pixelate: "モザイク"
|
||||
colorAdjust: "色調補正"
|
||||
colorClamp: "色の圧縮"
|
||||
colorClampAdvanced: "色の圧縮(高度)"
|
||||
|
@ -3310,11 +3341,15 @@ _imageEffector:
|
|||
checker: "チェッカー"
|
||||
blockNoise: "ブロックノイズ"
|
||||
tearing: "ティアリング"
|
||||
fill: "塗りつぶし"
|
||||
|
||||
_fxProps:
|
||||
angle: "角度"
|
||||
scale: "サイズ"
|
||||
size: "サイズ"
|
||||
radius: "半径"
|
||||
samples: "サンプル数"
|
||||
offset: "位置"
|
||||
color: "色"
|
||||
opacity: "不透明度"
|
||||
normalize: "正規化"
|
||||
|
@ -3343,6 +3378,7 @@ _imageEffector:
|
|||
zoomLinesThreshold: "集中線の幅"
|
||||
zoomLinesMaskSize: "中心径"
|
||||
zoomLinesBlack: "黒色にする"
|
||||
circle: "円形"
|
||||
|
||||
drafts: "下書き"
|
||||
_drafts:
|
||||
|
@ -3359,3 +3395,23 @@ _drafts:
|
|||
restoreFromDraft: "下書きから復元"
|
||||
restore: "復元"
|
||||
listDrafts: "下書き一覧"
|
||||
schedule: "投稿予約"
|
||||
listScheduledNotes: "予約投稿一覧"
|
||||
cancelSchedule: "予約解除"
|
||||
|
||||
qr: "二次元コード"
|
||||
_qr:
|
||||
showTabTitle: "表示"
|
||||
readTabTitle: "読み取る"
|
||||
shareTitle: "{name} {acct}"
|
||||
shareText: "Fediverseで私をフォローしてください!"
|
||||
chooseCamera: "カメラを選択"
|
||||
cannotToggleFlash: "ライト選択不可"
|
||||
turnOnFlash: "ライトをオンにする"
|
||||
turnOffFlash: "ライトをオフにする"
|
||||
startQr: "コードリーダーを再開"
|
||||
stopQr: "コードリーダーを停止"
|
||||
noQrCodeFound: "QRコードが見つかりません"
|
||||
scanFile: "端末の画像をスキャン"
|
||||
raw: "テキスト"
|
||||
mfm: "MFM"
|
||||
|
|
|
@ -1410,7 +1410,7 @@ _accountSettings:
|
|||
makeNotesFollowersOnlyBefore: "昔のノートをフォロワーだけに見てもらう"
|
||||
makeNotesFollowersOnlyBeforeDescription: "この機能が有効になってる間は、設定された日時より前、それか設定された時間が経ったノートがフォロワーのみ見れるようになるで。無効に戻すと、ノートの公開状態も戻るで。"
|
||||
makeNotesHiddenBefore: "昔のノートを見れんようにする"
|
||||
makeNotesHiddenBeforeDescription: "この機能が有効になってる間は、設定された日時より前、それか設定された時間が経ったノートがフォロワーのみ見れるようになるで。無効に戻すと、ノートの公開状態も戻るで。"
|
||||
makeNotesHiddenBeforeDescription: "この機能が有効になってる間は、設定された日時より前、それか設定された時間が経ったノートがあんただけ見れるようになるで。無効に戻すと、ノートの公開状態も戻るで。"
|
||||
mayNotEffectForFederatedNotes: "リモートサーバーに連合されたノートには効果が及ばんかもしれん。"
|
||||
mayNotEffectSomeSituations: "これらの制限は簡易的なものやで。リモートサーバーでの閲覧とかモデレーション時とか、一部のシチュエーションでは適用されへんかもしれん。"
|
||||
notesHavePassedSpecifiedPeriod: "決めた時間が経ったノート"
|
||||
|
|
|
@ -253,6 +253,7 @@ noteDeleteConfirm: "이 노트를 삭제하시겠습니까?"
|
|||
pinLimitExceeded: "더 이상 고정할 수 없습니다."
|
||||
done: "완료"
|
||||
processing: "처리중"
|
||||
preprocessing: "준비중"
|
||||
preview: "미리보기"
|
||||
default: "기본값"
|
||||
defaultValueIs: "기본값: {value}"
|
||||
|
@ -1316,6 +1317,7 @@ acknowledgeNotesAndEnable: "활성화 하기 전에 주의 사항을 확인했
|
|||
federationSpecified: "이 서버는 화이트 리스트 제도로 운영 중 입니다. 정해진 리모트 서버가 아닌 경우 연합되지 않습니다."
|
||||
federationDisabled: "이 서버는 연합을 하지 않고 있습니다. 리모트 서버 유저와 통신을 할 수 없습니다."
|
||||
draft: "초안"
|
||||
draftsAndScheduledNotes: "초안과 예약 게시물"
|
||||
confirmOnReact: "리액션할 때 확인"
|
||||
reactAreYouSure: "\" {emoji} \"로 리액션하시겠습니까?"
|
||||
markAsSensitiveConfirm: "이 미디어를 민감한 미디어로 설정하시겠습니까?"
|
||||
|
@ -1372,6 +1374,8 @@ redisplayAllTips: "모든 '팁과 유용한 정보'를 재표시"
|
|||
hideAllTips: "모든 '팁과 유용한 정보'를 비표시"
|
||||
defaultImageCompressionLevel: "기본 이미지 압축 정도"
|
||||
defaultImageCompressionLevel_description: "낮추면 화질을 유지합니다만 파일 크기는 증가합니다. <br>높이면 파일 크기를 줄일 수 있습니다만 화질은 저하됩니다."
|
||||
defaultCompressionLevel: "기본 압축 정도 "
|
||||
defaultCompressionLevel_description: "낮추면 품질을 유지합니다만 파일 크기는 증가합니다. <br>높이면 파일 크기를 줄일 수 있습니다만 품질은 저하됩니다."
|
||||
inMinutes: "분"
|
||||
inDays: "일"
|
||||
safeModeEnabled: "세이프 모드가 활성화돼있습니다"
|
||||
|
@ -1380,6 +1384,20 @@ customCssIsDisabledBecauseSafeMode: "세이프 모드가 활성화돼있기에
|
|||
themeIsDefaultBecauseSafeMode: "세이프 모드가 활성화돼있는 동안에는 기본 테마가 사용됩니다. 세이프 모드를 끄면 원래대로 돌아옵니다."
|
||||
thankYouForTestingBeta: "베타 버전의 검증에 협력해 주셔서 감사합니다!"
|
||||
createUserSpecifiedNote: "사용자 지정 노트를 작성"
|
||||
schedulePost: "게시 예약"
|
||||
scheduleToPostOnX: "{x}에 게시를 예약합니다."
|
||||
scheduledToPostOnX: "{x}에 게시가 예약돼있습니다."
|
||||
schedule: "예약"
|
||||
scheduled: "예약"
|
||||
_compression:
|
||||
_quality:
|
||||
high: "고품질"
|
||||
medium: "중간 품질"
|
||||
low: "저품질"
|
||||
_size:
|
||||
large: "대형"
|
||||
medium: "중형"
|
||||
small: "소형"
|
||||
_order:
|
||||
newest: "최신 순"
|
||||
oldest: "오래된 순"
|
||||
|
@ -2022,6 +2040,7 @@ _role:
|
|||
uploadableFileTypes_caption: "MIME 유형을 "
|
||||
uploadableFileTypes_caption2: "파일에 따라서는 유형을 검사하지 못하는 경우가 있습니다. 그러한 파일을 허가하는 경우에는 {x}를 지정으로 추가해주십시오."
|
||||
noteDraftLimit: "서버측 노트 초안 작성 가능 수"
|
||||
scheduledNoteLimit: "예약 게시물의 동시 작성 가능 수"
|
||||
watermarkAvailable: "워터마크 기능의 사용 여부"
|
||||
_condition:
|
||||
roleAssignedTo: "수동 역할에 이미 할당됨"
|
||||
|
@ -2647,6 +2666,8 @@ _notification:
|
|||
youReceivedFollowRequest: "새로운 팔로우 요청이 있습니다"
|
||||
yourFollowRequestAccepted: "팔로우 요청이 수락되었습니다"
|
||||
pollEnded: "투표 결과가 발표되었습니다"
|
||||
scheduledNotePosted: "예약 노트가 게시됐습니다."
|
||||
scheduledNotePostFailed: "예약 노트의 게시에 실패했습니다."
|
||||
newNote: "새 게시물"
|
||||
unreadAntennaNote: "안테나 {name}"
|
||||
roleAssigned: "역할이 부여 되었습니다."
|
||||
|
@ -3201,6 +3222,8 @@ _imageEffector:
|
|||
mirror: "미러"
|
||||
invert: "색 반전"
|
||||
grayscale: "흑백"
|
||||
blur: "흐림 효과"
|
||||
pixelate: "모자이크"
|
||||
colorAdjust: "색조 보정"
|
||||
colorClamp: "색 압축"
|
||||
colorClampAdvanced: "색 압축(고급)"
|
||||
|
@ -3212,11 +3235,13 @@ _imageEffector:
|
|||
checker: "체크 무늬"
|
||||
blockNoise: "노이즈 방지"
|
||||
tearing: "티어링"
|
||||
fillSquare: "칠하기(사각형)"
|
||||
fill: "채우기"
|
||||
_fxProps:
|
||||
angle: "각도"
|
||||
scale: "크기"
|
||||
size: "크기"
|
||||
radius: "반지름"
|
||||
samples: "샘플 수"
|
||||
offset: "위치"
|
||||
color: "색"
|
||||
opacity: "불투명도"
|
||||
|
@ -3246,6 +3271,7 @@ _imageEffector:
|
|||
zoomLinesThreshold: "집중선 폭"
|
||||
zoomLinesMaskSize: "중앙 값"
|
||||
zoomLinesBlack: "검은색으로 하기"
|
||||
circle: "원형"
|
||||
drafts: "초안"
|
||||
_drafts:
|
||||
select: "초안 선택"
|
||||
|
@ -3261,6 +3287,9 @@ _drafts:
|
|||
restoreFromDraft: "초안에서 복원\n"
|
||||
restore: "복원"
|
||||
listDrafts: "초안 목록"
|
||||
schedule: "게시 예약"
|
||||
listScheduledNotes: "예약 게시물 목록"
|
||||
cancelSchedule: "예약 해제"
|
||||
qr: "QR 코드"
|
||||
_qr:
|
||||
showTabTitle: "보기"
|
||||
|
|
|
@ -253,6 +253,7 @@ noteDeleteConfirm: "Deseja excluir esta nota?"
|
|||
pinLimitExceeded: "Não é possível fixar novas notas"
|
||||
done: "Concluído"
|
||||
processing: "Em Progresso"
|
||||
preprocessing: "Preparando..."
|
||||
preview: "Pré-visualizar"
|
||||
default: "Predefinição"
|
||||
defaultValueIs: "Predefinição: {value}"
|
||||
|
@ -1054,6 +1055,7 @@ permissionDeniedError: "Operação recusada"
|
|||
permissionDeniedErrorDescription: "Esta conta não tem permissão para executar esta ação."
|
||||
preset: "Predefinições"
|
||||
selectFromPresets: "Escolher de predefinições"
|
||||
custom: "Personalizado"
|
||||
achievements: "Conquistas"
|
||||
gotInvalidResponseError: "Resposta do servidor inválida"
|
||||
gotInvalidResponseErrorDescription: "Servidor fora do ar ou em manutenção. Favor tentar mais tarde."
|
||||
|
@ -1092,6 +1094,7 @@ prohibitedWordsDescription2: "Utilizar espaços irá criar expressões aditivas
|
|||
hiddenTags: "Hashtags escondidas"
|
||||
hiddenTagsDescription: "Selecione tags que não serão exibidas na lista de destaques. Várias tags podem ser escolhidas, separadas por linha."
|
||||
notesSearchNotAvailable: "A pesquisa de notas está indisponível."
|
||||
usersSearchNotAvailable: "Pesquisa de usuário está indisponível."
|
||||
license: "Licença"
|
||||
unfavoriteConfirm: "Deseja realmente remover dos favoritos?"
|
||||
myClips: "Meus clipes"
|
||||
|
@ -1243,6 +1246,7 @@ releaseToRefresh: "Solte para atualizar"
|
|||
refreshing: "Atualizando..."
|
||||
pullDownToRefresh: "Puxe para baixo para atualizar"
|
||||
useGroupedNotifications: "Agrupar notificações"
|
||||
emailVerificationFailedError: "Houve um problema ao verificar seu endereço de email. O link pode ter expirado."
|
||||
cwNotationRequired: "Se \"Esconder conteúdo\" está habilitado, uma descrição deve ser adicionada."
|
||||
doReaction: "Adicionar reação"
|
||||
code: "Código"
|
||||
|
@ -1313,6 +1317,7 @@ acknowledgeNotesAndEnable: "Ative após compreender as precauções."
|
|||
federationSpecified: "Esse servidor opera com uma lista branca de federação. Interagir com servidores diferentes daqueles designados pela administração não é permitido."
|
||||
federationDisabled: "Federação está desabilitada nesse servidor. Você não pode interagir com usuários de outros servidores."
|
||||
draft: "Rascunhos"
|
||||
draftsAndScheduledNotes: "Rascunhos e notas agendadas."
|
||||
confirmOnReact: "Confirmar ao reagir"
|
||||
reactAreYouSure: "Você deseja adicionar uma reação \"{emoji}\"?"
|
||||
markAsSensitiveConfirm: "Você deseja definir essa mídia como sensível?"
|
||||
|
@ -1341,6 +1346,7 @@ textCount: "Contagem de caracteres"
|
|||
information: "Sobre"
|
||||
chat: "Conversas"
|
||||
directMessage: "Conversar com usuário"
|
||||
directMessage_short: "Mensagem"
|
||||
migrateOldSettings: "Migrar configurações antigas de cliente"
|
||||
migrateOldSettings_description: "Isso deve ser feito automaticamente. Caso o processo de migração tenha falhado, você pode acioná-lo manualmente. As informações atuais de migração serão substituídas."
|
||||
compress: "Comprimir"
|
||||
|
@ -1368,12 +1374,35 @@ redisplayAllTips: "Mostrar todas as \"Dicas e Truques\" novamente"
|
|||
hideAllTips: "Ocultas todas as \"Dicas e Truques\""
|
||||
defaultImageCompressionLevel: "Nível de compressão de imagem padrão"
|
||||
defaultImageCompressionLevel_description: "Alto, reduz o tamanho do arquivo mas, também, a qualidade da imagem.<br>Alto, reduz o tamanho do arquivo mas, também, a qualidade da imagem."
|
||||
defaultCompressionLevel: "Nível padrão de compressão"
|
||||
defaultCompressionLevel_description: "Menor compressão preserva a qualidade mas aumenta o tamanho do arquivo.<br>Maior compressão reduz o tamanho do arquivo mas diminui a qualidade."
|
||||
inMinutes: "Minuto(s)"
|
||||
inDays: "Dia(s)"
|
||||
safeModeEnabled: "Modo seguro está habilitado"
|
||||
pluginsAreDisabledBecauseSafeMode: "Todos os plugins estão desabilitados porque o modo seguro está habilitado."
|
||||
customCssIsDisabledBecauseSafeMode: "CSS personalizado não está aplicado porque o modo seguro está habilitado."
|
||||
themeIsDefaultBecauseSafeMode: "Enquanto o modo seguro estiver ativo, o tema padrão é utilizado. Desabilitar o modo seguro reverterá essas mudanças."
|
||||
thankYouForTestingBeta: "Obrigado por nos ajudar a testar a versão beta!"
|
||||
createUserSpecifiedNote: "Criar uma nota direta"
|
||||
schedulePost: "Agendar publicação"
|
||||
scheduleToPostOnX: "Agendar nota para {x}"
|
||||
scheduledToPostOnX: "A nota está agendada para {x}"
|
||||
schedule: "Agendar"
|
||||
scheduled: "Agendado"
|
||||
_compression:
|
||||
_quality:
|
||||
high: "Qualidade alta"
|
||||
medium: "Qualidade média"
|
||||
low: "Qualidade baixa"
|
||||
_size:
|
||||
large: "Tamanho grande"
|
||||
medium: "Tamanho médio"
|
||||
small: "Tamanho pequeno"
|
||||
_order:
|
||||
newest: "Priorizar Mais Novos"
|
||||
oldest: "Priorizar Mais Antigos"
|
||||
_chat:
|
||||
messages: "Mensagem"
|
||||
noMessagesYet: "Ainda não há mensagens"
|
||||
newMessage: "Nova mensagem"
|
||||
individualChat: "Conversa Particular"
|
||||
|
@ -1461,6 +1490,7 @@ _settings:
|
|||
contentsUpdateFrequency_description2: "Quando o modo tempo-real está ativado, o conteúdo é atualizado em tempo real, ignorando essa opção."
|
||||
showUrlPreview: "Exibir prévia de URL"
|
||||
showAvailableReactionsFirstInNote: "Exibir reações disponíveis no topo."
|
||||
showPageTabBarBottom: "Mostrar barra de aba da página inferiormente"
|
||||
_chat:
|
||||
showSenderName: "Exibir nome de usuário do remetente"
|
||||
sendOnEnter: "Pressionar Enter para enviar"
|
||||
|
@ -1634,6 +1664,10 @@ _serverSettings:
|
|||
fanoutTimelineDbFallback: "\"Fallback\" ao banco de dados"
|
||||
fanoutTimelineDbFallbackDescription: "Quando habilitado, a linha do tempo irá recuar ao banco de dados caso consultas adicionais sejam feitas e ela não estiver em cache. Quando desabilitado, o impacto no servidor será reduzido ao eliminar o recuo, mas limita a quantidade de linhas do tempo que podem ser recebidas."
|
||||
reactionsBufferingDescription: "Quando ativado, o desempenho durante a criação de uma reação será melhorado substancialmente, reduzindo a carga do banco de dados. Porém, a o uso de memória do Redis irá aumentar."
|
||||
remoteNotesCleaning: "Limpeza automática de notas remotas"
|
||||
remoteNotesCleaning_description: "Quando habilitado, notas remotas obsoletas e não utilizadas serão periodicamente limpadas para previnir sobrecarga no banco de dados."
|
||||
remoteNotesCleaningMaxProcessingDuration: "Maximizar tempo de processamento da limpeza"
|
||||
remoteNotesCleaningExpiryDaysForEachNotes: "Mínimo de dias para retenção de notas"
|
||||
inquiryUrl: "URL de inquérito"
|
||||
inquiryUrlDescription: "Especifique um URL para um formulário de inquérito para a administração ou uma página web com informações de contato."
|
||||
openRegistration: "Abrir a criação de contas"
|
||||
|
@ -1652,6 +1686,11 @@ _serverSettings:
|
|||
userGeneratedContentsVisibilityForVisitor: "Visibilidade de conteúdo dos usuários para visitantes"
|
||||
userGeneratedContentsVisibilityForVisitor_description: "Isso é útil para prevenir problemas causados por conteúdo inapropriado de usuários remotos de servidores com pouca ou nenhuma moderação, que pode ser hospedado na internet a partir desse servidor."
|
||||
userGeneratedContentsVisibilityForVisitor_description2: "Publicar todo o conteúdo do servidor para a internet pode ser arriscado. Isso é especialmente importante para visitantes que desconhecem a natureza distribuída do conteúdo na internet, pois eles podem acreditar que o conteúdo remoto é criado por usuários desse servidor."
|
||||
restartServerSetupWizardConfirm_title: "Reiniciar o assistente de configuração?"
|
||||
restartServerSetupWizardConfirm_text: "Algumas configurações atuais serão reiniciadas."
|
||||
entrancePageStyle: "Estilo da página de entrada"
|
||||
showTimelineForVisitor: "Mostrar linha do tempo"
|
||||
showActivitiesForVisitor: "Mostrar atividades"
|
||||
_userGeneratedContentsVisibilityForVisitor:
|
||||
all: "Tudo é público"
|
||||
localOnly: "Conteúdo local é publicado, conteúdo remoto é privado"
|
||||
|
@ -1988,6 +2027,7 @@ _role:
|
|||
descriptionOfRateLimitFactor: "Valores menores são menos restritivos, valores maiores são mais restritivos."
|
||||
canHideAds: "Permitir ocultar anúncios"
|
||||
canSearchNotes: "Permitir a busca de notas"
|
||||
canSearchUsers: "Busca de usuário"
|
||||
canUseTranslator: "Uso do tradutor"
|
||||
avatarDecorationLimit: "Número máximo de decorações de avatar que podem ser aplicadas"
|
||||
canImportAntennas: "Permitir importação de antenas"
|
||||
|
@ -2000,6 +2040,7 @@ _role:
|
|||
uploadableFileTypes_caption: "Especifica tipos MIME permitidos. Múltiplos tipos MIME podem ser especificados separando-os por linha. Curingas podem ser especificados com um asterisco (*). (exemplo, image/*)"
|
||||
uploadableFileTypes_caption2: "Alguns tipos de arquivos podem não ser detectados. Para permiti-los, adicione {x} à especificação."
|
||||
noteDraftLimit: "Limite de rascunhos possíveis"
|
||||
scheduledNoteLimit: "Número máximo de notas agendadas simultâneas"
|
||||
watermarkAvailable: "Disponibilidade da função de marca d'água"
|
||||
_condition:
|
||||
roleAssignedTo: "Atribuído a cargos manuais"
|
||||
|
@ -2260,6 +2301,7 @@ _time:
|
|||
minute: "Minuto(s)"
|
||||
hour: "Hora(s)"
|
||||
day: "Dia(s)"
|
||||
month: "Mês(es)"
|
||||
_2fa:
|
||||
alreadyRegistered: "Você já cadastrou um dispositivo de autenticação de dois fatores."
|
||||
registerTOTP: "Cadastrar aplicativo autenticador"
|
||||
|
@ -2624,6 +2666,8 @@ _notification:
|
|||
youReceivedFollowRequest: "Você recebeu um pedido de seguidor"
|
||||
yourFollowRequestAccepted: "Seu pedido de seguidor foi aceito"
|
||||
pollEnded: "Os resultados da enquete agora estão disponíveis"
|
||||
scheduledNotePosted: "Nota agendada foi publicada"
|
||||
scheduledNotePostFailed: "Não foi possível publicar nota agendada"
|
||||
newNote: "Nova nota"
|
||||
unreadAntennaNote: "Antena {name}"
|
||||
roleAssigned: "Cargo dado"
|
||||
|
@ -3062,6 +3106,7 @@ _bootErrors:
|
|||
otherOption1: "Excluir ajustes de cliente e cache"
|
||||
otherOption2: "Iniciar o cliente simples"
|
||||
otherOption3: "Iniciar ferramenta de reparo"
|
||||
otherOption4: "Abrir Misskey no modo seguro"
|
||||
_search:
|
||||
searchScopeAll: "Todos"
|
||||
searchScopeLocal: "Local"
|
||||
|
@ -3098,6 +3143,8 @@ _serverSetupWizard:
|
|||
doYouConnectToFediverse_description1: "Quando conectado com uma rede distribuída de servidores (Fediverso), o conteúdo pode ser trocado com outros servidores."
|
||||
doYouConnectToFediverse_description2: "Conectar com o Fediverso também é chamado de \"federação\""
|
||||
youCanConfigureMoreFederationSettingsLater: "Configurações adicionais como especificar servidores para conectar-se com podem ser feitas posteriormente"
|
||||
remoteContentsCleaning: "Limpeza automática de conteúdos recebidos"
|
||||
remoteContentsCleaning_description: "A federação pode resultar em uma entrada contínua de conteúdo. Habilitar a limpeza automática removerá conteúdo obsoleto e não referenciado do servidor para economizar armazenamento."
|
||||
adminInfo: "Informações da administração"
|
||||
adminInfo_description: "Define as informações do administrador usadas para receber consultas."
|
||||
adminInfo_mustBeFilled: "Deve ser preenchido se o servidor é público ou se a federação está ativa."
|
||||
|
@ -3146,7 +3193,9 @@ _watermarkEditor:
|
|||
opacity: "Opacidade"
|
||||
scale: "Tamanho"
|
||||
text: "Texto"
|
||||
qr: "Código QR"
|
||||
position: "Posição"
|
||||
margin: "Margem"
|
||||
type: "Tipo"
|
||||
image: "imagem"
|
||||
advanced: "Avançado"
|
||||
|
@ -3161,16 +3210,20 @@ _watermarkEditor:
|
|||
polkadotSubDotOpacity: "Opacidade da bolinha secundária"
|
||||
polkadotSubDotRadius: "Raio das bolinhas adicionais"
|
||||
polkadotSubDotDivisions: "Número de bolinhas adicionais"
|
||||
leaveBlankToAccountUrl: "Deixe em branco para utilizar URL da conta"
|
||||
_imageEffector:
|
||||
title: "Efeitos"
|
||||
addEffect: "Adicionar efeitos"
|
||||
discardChangesConfirm: "Tem certeza que deseja sair? Há mudanças não salvas."
|
||||
nothingToConfigure: "Não há nada para configurar"
|
||||
_fxs:
|
||||
chromaticAberration: "Aberração cromática"
|
||||
glitch: "Glitch"
|
||||
mirror: "Espelho"
|
||||
invert: "Inverter Cores"
|
||||
grayscale: "Tons de Cinza"
|
||||
blur: "Desfoque"
|
||||
pixelate: "Pixelizar"
|
||||
colorAdjust: "Correção de Cores"
|
||||
colorClamp: "Compressão de Cores"
|
||||
colorClampAdvanced: "Compressão Avançada de Cores"
|
||||
|
@ -3182,14 +3235,43 @@ _imageEffector:
|
|||
checker: "Xadrez"
|
||||
blockNoise: "Bloquear Ruído"
|
||||
tearing: "Descontinuidade"
|
||||
fill: "Preencher"
|
||||
_fxProps:
|
||||
angle: "Ângulo"
|
||||
scale: "Tamanho"
|
||||
size: "Tamanho"
|
||||
radius: "Raio"
|
||||
samples: "Número de amostras"
|
||||
offset: "Posição"
|
||||
color: "Cor"
|
||||
opacity: "Opacidade"
|
||||
normalize: "Normalizar"
|
||||
amount: "Quantidade"
|
||||
lightness: "Esclarecer"
|
||||
contrast: "Contraste"
|
||||
hue: "Matiz"
|
||||
brightness: "Brilho"
|
||||
saturation: "Saturação"
|
||||
max: "Máximo"
|
||||
min: "Mínimo"
|
||||
direction: "Direção"
|
||||
phase: "Fase"
|
||||
frequency: "Frequência"
|
||||
strength: "Força"
|
||||
glitchChannelShift: "Mudança de canal"
|
||||
seed: "Valor da semente"
|
||||
redComponent: "Componente vermelho"
|
||||
greenComponent: "Componente verde"
|
||||
blueComponent: "Componente azul"
|
||||
threshold: "Limiar"
|
||||
centerX: "Centralizar X"
|
||||
centerY: "Centralizar Y"
|
||||
zoomLinesSmoothing: "Suavização"
|
||||
zoomLinesSmoothingDescription: "Suavização e largura das linhas de zoom não podem ser utilizados simultaneamente."
|
||||
zoomLinesThreshold: "Largura das linhas de zoom"
|
||||
zoomLinesMaskSize: "Diâmetro do centro"
|
||||
zoomLinesBlack: "Linhas pretas"
|
||||
circle: "Circular"
|
||||
drafts: "Rascunhos"
|
||||
_drafts:
|
||||
select: "Selecionar Rascunho"
|
||||
|
@ -3205,6 +3287,22 @@ _drafts:
|
|||
restoreFromDraft: "Restaurar de Rascunho"
|
||||
restore: "Redefinir"
|
||||
listDrafts: "Lista de Rascunhos"
|
||||
schedule: "Agendar nota"
|
||||
listScheduledNotes: "Lista de notas agendadas"
|
||||
cancelSchedule: "Cancelar agendamento"
|
||||
qr: "Código QR"
|
||||
_qr:
|
||||
showTabTitle: "Visualizar"
|
||||
readTabTitle: "Escanear"
|
||||
shareTitle: "{name} {acct}"
|
||||
shareText: "Siga-me no Fediverso!"
|
||||
chooseCamera: "Escolher câmera"
|
||||
cannotToggleFlash: "Não foi possível ligar a lanterna"
|
||||
turnOnFlash: "Ligar a lanterna"
|
||||
turnOffFlash: "Desligar a lanterna"
|
||||
startQr: "Retornar ao leitor de códigos QR"
|
||||
stopQr: "Deixar o leitor de códigos QR"
|
||||
noQrCodeFound: "Nenhum código QR encontrado"
|
||||
scanFile: "Escanear imagem de dispositivo"
|
||||
raw: "Texto"
|
||||
mfm: "MFM"
|
||||
|
|
|
@ -253,6 +253,7 @@ noteDeleteConfirm: "ต้องการลบโน้ตนี้ใช่ไ
|
|||
pinLimitExceeded: "คุณไม่สามารถปักหมุดโน้ตเพิ่มเติมใดๆได้อีก"
|
||||
done: "เสร็จสิ้น"
|
||||
processing: "กำลังประมวลผล..."
|
||||
preprocessing: "กำลังจัดเตรียม..."
|
||||
preview: "แสดงตัวอย่าง"
|
||||
default: "ค่าเริ่มต้น"
|
||||
defaultValueIs: "ค่าเริ่มต้น: {value}"
|
||||
|
@ -1245,6 +1246,7 @@ releaseToRefresh: "ปล่อยเพื่อรีเฟรช"
|
|||
refreshing: "กำลังรีเฟรช..."
|
||||
pullDownToRefresh: "ดึงลงเพื่อรีเฟรช"
|
||||
useGroupedNotifications: "แสดงผลการแจ้งเตือนแบบกลุ่มแล้ว"
|
||||
emailVerificationFailedError: "เกิดปัญหาในขณะตรวจสอบอีเมล อาจเป็นไปได้ว่าลิงก์หมดอายุแล้ว"
|
||||
cwNotationRequired: "หากเปิดใช้งาน “ซ่อนเนื้อหา” จะต้องระบุคำอธิบาย"
|
||||
doReaction: "เพิ่มรีแอคชั่น"
|
||||
code: "โค้ด"
|
||||
|
@ -1315,6 +1317,7 @@ acknowledgeNotesAndEnable: "เปิดใช้งานหลังจาก
|
|||
federationSpecified: "เซิร์ฟเวอร์นี้ดำเนินงานในระบบกลุ่มไวท์ลิสต์ ไม่สามารถติดต่อกับเซิร์ฟเวอร์อื่นที่ไม่ได้รับอนุญาตจากผู้ดูแลระบบได้"
|
||||
federationDisabled: "เซิร์ฟเวอร์นี้ปิดใช้งานสหพันธ์ ไม่สามารถติดต่อหรือแลกเปลี่ยนข้อมูลกับผู้ใช้จากเซิร์ฟเวอร์อื่นได้"
|
||||
draft: "ร่าง"
|
||||
draftsAndScheduledNotes: "ร่างและกำหนดเวลาโพสต์"
|
||||
confirmOnReact: "ยืนยันเมื่อทำการรีแอคชั่น"
|
||||
reactAreYouSure: "ต้องการใส่รีแอคชั่นด้วย \"{emoji}\" หรือไม่?"
|
||||
markAsSensitiveConfirm: "ต้องการตั้งค่าสื่อนี้ว่าเป็นเนื้อหาละเอียดอ่อนหรือไม่?"
|
||||
|
@ -1343,6 +1346,7 @@ textCount: "จำนวนอักขระ"
|
|||
information: "เกี่ยวกับ"
|
||||
chat: "แชต"
|
||||
directMessage: "แชตเลย"
|
||||
directMessage_short: "ข้อความ"
|
||||
migrateOldSettings: "ย้ายข้อมูลการตั้งค่าเก่า"
|
||||
migrateOldSettings_description: "โดยปกติจะทำโดยอัตโนมัติ แต่หากด้วยเหตุผลบางประการที่ไม่สามารถย้ายได้สำเร็จ สามารถสั่งย้ายด้วยตนเองได้ การตั้งค่าปัจจุบันจะถูกเขียนทับ"
|
||||
compress: "บีบอัด"
|
||||
|
@ -1368,8 +1372,10 @@ abort: "หยุดและยกเลิก"
|
|||
tip: "คำแนะนำและเคล็ดลับ"
|
||||
redisplayAllTips: "แสดงคำแนะนำและเคล็ดลับทั้งหมดอีกครั้ง"
|
||||
hideAllTips: "ซ่อนคำแนะนำและเคล็ดลับทั้งหมด"
|
||||
defaultImageCompressionLevel: "ความละเอียดเริ่มต้นสำหรับการบีบอัดภาพ"
|
||||
defaultImageCompressionLevel: "ค่าการบีบอัดภาพเริ่มต้น"
|
||||
defaultImageCompressionLevel_description: "หากตั้งค่าต่ำ จะรักษาคุณภาพภาพได้ดีขึ้นแต่ขนาดไฟล์จะเพิ่มขึ้น<br>หากตั้งค่าสูง จะลดขนาดไฟล์ได้ แต่คุณภาพภาพจะลดลง"
|
||||
defaultCompressionLevel: "ค่าการบีบอัดเริ่มต้น"
|
||||
defaultCompressionLevel_description: "ถ้าต่ำ จะรักษาคุณภาพได้ แต่ขนาดไฟล์จะเพิ่มขึ้น<br>ถ้าสูง จะลดขนาดไฟล์ได้ แต่คุณภาพจะลดลง"
|
||||
inMinutes: "นาที"
|
||||
inDays: "วัน"
|
||||
safeModeEnabled: "โหมดปลอดภัยถูกเปิดใช้งาน"
|
||||
|
@ -1377,10 +1383,26 @@ pluginsAreDisabledBecauseSafeMode: "เนื่องจากโหมดป
|
|||
customCssIsDisabledBecauseSafeMode: "เนื่องจากโหมดปลอดภัยถูกเปิดใช้งาน CSS แบบกำหนดเองจึงไม่ได้ถูกนำมาใช้"
|
||||
themeIsDefaultBecauseSafeMode: "ในระหว่างที่โหมดปลอดภัยถูกเปิดใช้งาน จะใช้ธีมเริ่มต้น เมื่อปิดโหมดปลอดภัยจะกลับคืนดังเดิม"
|
||||
thankYouForTestingBeta: "ขอบคุณที่ให้ความร่วมมือในการทดสอบเวอร์ชันเบต้า!"
|
||||
createUserSpecifiedNote: "สร้างโน้ตแบบไดเร็กต์"
|
||||
schedulePost: "กำหนดเวลาให้โพสต์"
|
||||
scheduleToPostOnX: "กำหนดเวลาให้โพสต์ไว้ที่ {x}"
|
||||
scheduledToPostOnX: "มีการกำหนดเวลาให้โพสต์ไว้ที่ {x}"
|
||||
schedule: "กำหนดเวลา"
|
||||
scheduled: "กำหนดเวลา"
|
||||
_compression:
|
||||
_quality:
|
||||
high: "คุณภาพสูง"
|
||||
medium: "คุณภาพปานกลาง"
|
||||
low: "คุณภาพต่ำ"
|
||||
_size:
|
||||
large: "ขนาดใหญ่"
|
||||
medium: "ขนาดปานกลาง"
|
||||
small: "ขนาดเล็ก"
|
||||
_order:
|
||||
newest: "เรียงจากใหม่ไปเก่า"
|
||||
oldest: "เรียงจากเก่าไปใหม่"
|
||||
_chat:
|
||||
messages: "ข้อความ"
|
||||
noMessagesYet: "ยังไม่มีข้อความ"
|
||||
newMessage: "ข้อความใหม่"
|
||||
individualChat: "แชตส่วนตัว"
|
||||
|
@ -1666,6 +1688,9 @@ _serverSettings:
|
|||
userGeneratedContentsVisibilityForVisitor_description2: "การเปิดเผยเนื้อหาทั้งหมดในเซิร์ฟเวอร์รวมทั้งเนื้อหาที่รับมาจากระยะไกลสู่สาธารณะบนอินเทอร์เน็ตโดยไม่มีข้อจำกัดใดๆ มีความเสี่ยงโดยเฉพาะอย่างยิ่งสำหรับผู้ชมที่ไม่เข้าใจลักษณะของระบบแบบกระจาย อาจทำให้เกิดความเข้าใจผิดคิดว่าเนื้อหาที่มาจากระยะไกลนั้นเป็นเนื้อหาที่สร้างขึ้นภายในเซิร์ฟเวอร์นี้ จึงควรใช้ความระมัดระวังอย่างมาก"
|
||||
restartServerSetupWizardConfirm_title: "ต้องการเริ่มวิซาร์ดการตั้งค่าเซิร์ฟเวอร์ใหม่หรือไม่?"
|
||||
restartServerSetupWizardConfirm_text: "การตั้งค่าบางส่วนในปัจจุบันจะถูกรีเซ็ต"
|
||||
entrancePageStyle: "สไตล์ของหน้าเพจทางเข้า"
|
||||
showTimelineForVisitor: "แสดงไทม์ไลน์"
|
||||
showActivitiesForVisitor: "แสดงกิจกรรม"
|
||||
_userGeneratedContentsVisibilityForVisitor:
|
||||
all: "ทั้งหมดสาธารณะ"
|
||||
localOnly: "เผยแพร่เป็นสาธารณะเฉพาะเนื้อหาท้องถิ่น เนื้อหาระยะไกลให้เป็นส่วนตัว"
|
||||
|
@ -2015,6 +2040,7 @@ _role:
|
|||
uploadableFileTypes_caption: "สามารถระบุ MIME type ได้ โดยใช้การขึ้นบรรทัดใหม่เพื่อแยกหลายรายการ และสามารถใช้ดอกจัน (*) เพื่อระบุแบบไวลด์การ์ดได้ (เช่น: image/*)"
|
||||
uploadableFileTypes_caption2: "ไฟล์บางประเภทอาจไม่สามารถระบุชนิดได้ หากต้องการอนุญาตไฟล์ลักษณะนั้น กรุณาเพิ่ม {x} ลงในรายการที่อนุญาต"
|
||||
noteDraftLimit: "จำนวนโน้ตฉบับร่างที่สามารถสร้างได้บนฝั่งเซิร์ฟเวอร์"
|
||||
scheduledNoteLimit: "จำนวนโพสต์กำหนดเวลาที่สร้างพร้อมกันได้"
|
||||
watermarkAvailable: "มีฟังก์ชั่นลายน้ำให้เลือกใช้"
|
||||
_condition:
|
||||
roleAssignedTo: "มอบหมายให้มีบทบาทแบบทำมือ"
|
||||
|
@ -2640,6 +2666,8 @@ _notification:
|
|||
youReceivedFollowRequest: "ได้รับคำขอติดตาม"
|
||||
yourFollowRequestAccepted: "คำขอติดตามได้รับการอนุมัติแล้ว"
|
||||
pollEnded: "ผลโพลออกมาแล้ว"
|
||||
scheduledNotePosted: "โน้ตที่กำหนดเวลาไว้ได้ถูกโพสต์แล้ว"
|
||||
scheduledNotePostFailed: "ล้มเหลวในการโพสต์โน้ตที่กำหนดเวลาไว้"
|
||||
newNote: "โพสต์ใหม่"
|
||||
unreadAntennaNote: "เสาอากาศ {name}"
|
||||
roleAssigned: "ได้รับบทบาท"
|
||||
|
@ -3165,7 +3193,9 @@ _watermarkEditor:
|
|||
opacity: "ความทึบแสง"
|
||||
scale: "ขนาด"
|
||||
text: "ข้อความ"
|
||||
qr: "QR โค้ด"
|
||||
position: "ตำแหน่ง"
|
||||
margin: "ระยะขอบ"
|
||||
type: "รูปแบบ"
|
||||
image: "รูปภาพ"
|
||||
advanced: "ขั้นสูง"
|
||||
|
@ -3180,6 +3210,7 @@ _watermarkEditor:
|
|||
polkadotSubDotOpacity: "ความทึบของจุดรอง"
|
||||
polkadotSubDotRadius: "ขนาดของจุดรอง"
|
||||
polkadotSubDotDivisions: "จำนวนจุดรอง"
|
||||
leaveBlankToAccountUrl: "เว้นว่างไว้หากต้องการใช้ URL ของบัญชีแทน"
|
||||
_imageEffector:
|
||||
title: "เอฟเฟกต์"
|
||||
addEffect: "เพิ่มเอฟเฟกต์"
|
||||
|
@ -3191,6 +3222,8 @@ _imageEffector:
|
|||
mirror: "กระจก"
|
||||
invert: "กลับสี"
|
||||
grayscale: "ขาวดำเทา"
|
||||
blur: "มัว"
|
||||
pixelate: "โมเสก"
|
||||
colorAdjust: "ปรับแก้สี"
|
||||
colorClamp: "บีบอัดสี"
|
||||
colorClampAdvanced: "บีบอัดสี (ขั้นสูง)"
|
||||
|
@ -3202,10 +3235,13 @@ _imageEffector:
|
|||
checker: "ช่องตาราง"
|
||||
blockNoise: "บล็อกที่มีการรบกวน"
|
||||
tearing: "ฉีกขาด"
|
||||
fill: "เติมเต็ม"
|
||||
_fxProps:
|
||||
angle: "แองเกิล"
|
||||
scale: "ขนาด"
|
||||
size: "ขนาด"
|
||||
radius: "รัศสี"
|
||||
samples: "จำนวนตัวอย่าง"
|
||||
offset: "ตำแหน่ง"
|
||||
color: "สี"
|
||||
opacity: "ความทึบแสง"
|
||||
|
@ -3235,6 +3271,7 @@ _imageEffector:
|
|||
zoomLinesThreshold: "ความกว้างเส้นรวมศูนย์"
|
||||
zoomLinesMaskSize: "ขนาดพื้นที่ตรงกลาง"
|
||||
zoomLinesBlack: "ทำให้ดำ"
|
||||
circle: "ทรงกลม"
|
||||
drafts: "ร่าง"
|
||||
_drafts:
|
||||
select: "เลือกฉบับร่าง"
|
||||
|
@ -3250,6 +3287,22 @@ _drafts:
|
|||
restoreFromDraft: "คืนค่าจากฉบับร่าง"
|
||||
restore: "กู้คืน"
|
||||
listDrafts: "รายการฉบับร่าง"
|
||||
schedule: "โพสต์กำหนดเวลา"
|
||||
listScheduledNotes: "รายการโน้ตที่กำหนดเวลาไว้"
|
||||
cancelSchedule: "ยกเลิกกำหนดเวลา"
|
||||
qr: "QR โค้ด"
|
||||
_qr:
|
||||
showTabTitle: "แสดงผล"
|
||||
readTabTitle: "แสกน"
|
||||
shareTitle: "{name}{acct}"
|
||||
shareText: "โปรดติดตามฉันบน Fediverse ด้วย!"
|
||||
chooseCamera: "เลือกกล้อง"
|
||||
cannotToggleFlash: "ไม่สามารถเลือกแสงแฟลชได้"
|
||||
turnOnFlash: "ปิดแสงแฟลช"
|
||||
turnOffFlash: "เปิดแสงแฟลช"
|
||||
startQr: "เริ่มตัวอ่าน QR โค้ด"
|
||||
stopQr: "หยุดตัวอ่าน QR โค้ด"
|
||||
noQrCodeFound: "ไม่พบ QR โค้ด"
|
||||
scanFile: "สแกนภาพจากอุปกรณ์"
|
||||
raw: "ข้อความ"
|
||||
mfm: "MFM"
|
||||
|
|
|
@ -87,7 +87,7 @@ exportRequested: "导出请求已提交,这可能需要花一些时间,导
|
|||
importRequested: "导入请求已提交,这可能需要花一点时间。"
|
||||
lists: "列表"
|
||||
noLists: "列表为空"
|
||||
note: "帖子"
|
||||
note: "发帖"
|
||||
notes: "帖子"
|
||||
following: "关注中"
|
||||
followers: "关注者"
|
||||
|
@ -144,15 +144,15 @@ markAsSensitive: "标记为敏感内容"
|
|||
unmarkAsSensitive: "取消标记为敏感内容"
|
||||
enterFileName: "输入文件名"
|
||||
mute: "屏蔽"
|
||||
unmute: "取消隐藏"
|
||||
renoteMute: "隐藏转帖"
|
||||
renoteUnmute: "解除隐藏转帖"
|
||||
block: "屏蔽"
|
||||
unblock: "取消屏蔽"
|
||||
unmute: "取消屏蔽"
|
||||
renoteMute: "屏蔽转帖"
|
||||
renoteUnmute: "取消屏蔽转帖"
|
||||
block: "拉黑"
|
||||
unblock: "取消拉黑"
|
||||
suspend: "冻结"
|
||||
unsuspend: "解除冻结"
|
||||
blockConfirm: "确定要屏蔽吗?"
|
||||
unblockConfirm: "确定要取消屏蔽吗?"
|
||||
blockConfirm: "确定要拉黑吗?"
|
||||
unblockConfirm: "确定要取消拉黑吗?"
|
||||
suspendConfirm: "要冻结吗?"
|
||||
unsuspendConfirm: "要解除冻结吗?"
|
||||
selectList: "选择列表"
|
||||
|
@ -244,22 +244,23 @@ mediaSilencedInstances: "已隐藏媒体文件的服务器"
|
|||
mediaSilencedInstancesDescription: "设置要隐藏媒体文件的服务器,以换行分隔。被设置的服务器内所有账号的文件均按照「敏感内容」处理,且将无法使用自定义表情符号。被阻止的实例不受影响。"
|
||||
federationAllowedHosts: "允许联合的服务器"
|
||||
federationAllowedHostsDescription: "设定允许联合的服务器,以换行分隔。"
|
||||
muteAndBlock: "隐藏和屏蔽"
|
||||
mutedUsers: "已隐藏用户"
|
||||
blockedUsers: "已屏蔽的用户"
|
||||
muteAndBlock: "屏蔽/拉黑"
|
||||
mutedUsers: "已屏蔽用户"
|
||||
blockedUsers: "已拉黑的用户"
|
||||
noUsers: "无用户"
|
||||
editProfile: "编辑资料"
|
||||
noteDeleteConfirm: "确定要删除该帖子吗?"
|
||||
pinLimitExceeded: "无法置顶更多了"
|
||||
done: "完成"
|
||||
processing: "正在处理"
|
||||
preprocessing: "准备中"
|
||||
preview: "预览"
|
||||
default: "默认"
|
||||
defaultValueIs: "默认值: {value}"
|
||||
noCustomEmojis: "没有自定义表情符号"
|
||||
noJobs: "没有任务"
|
||||
federating: "联合中"
|
||||
blocked: "已屏蔽"
|
||||
blocked: "已拉黑"
|
||||
suspended: "停止投递"
|
||||
all: "全部"
|
||||
subscribing: "已订阅"
|
||||
|
@ -303,7 +304,7 @@ explore: "发现"
|
|||
messageRead: "已读"
|
||||
noMoreHistory: "没有更多的历史记录"
|
||||
startChat: "开始聊天"
|
||||
nUsersRead: "{n} 人已读"
|
||||
nUsersRead: "{n}人已读"
|
||||
agreeTo: "勾选则表示已阅读并同意 {0}"
|
||||
agree: "同意"
|
||||
agreeBelow: "同意以下内容"
|
||||
|
@ -395,7 +396,7 @@ basicInfo: "基本信息"
|
|||
pinnedUsers: "置顶用户"
|
||||
pinnedUsersDescription: "输入您想要固定到“发现”页面的用户,一行一个。"
|
||||
pinnedPages: "固定页面"
|
||||
pinnedPagesDescription: "输入您要固定到服务器首页的页面路径,一行一个。"
|
||||
pinnedPagesDescription: "输入您要固定到服务器首页的页面路径,以换行符分隔。"
|
||||
pinnedClipId: "置顶的便签 ID"
|
||||
pinnedNotes: "已置顶的帖子"
|
||||
hcaptcha: "hCaptcha"
|
||||
|
@ -428,7 +429,7 @@ notifyAntenna: "开启通知"
|
|||
withFileAntenna: "仅带有附件的帖子"
|
||||
excludeNotesInSensitiveChannel: "排除敏感频道内的帖子"
|
||||
enableServiceworker: "启用 ServiceWorker"
|
||||
antennaUsersDescription: "指定用户名,一行一个"
|
||||
antennaUsersDescription: "指定用户名,用换行符进行分隔"
|
||||
caseSensitive: "区分大小写"
|
||||
withReplies: "包括回复"
|
||||
connectedTo: "您的账号已连到接以下第三方账号"
|
||||
|
@ -460,7 +461,7 @@ moderationNote: "管理笔记"
|
|||
moderationNoteDescription: "可以用来记录仅在管理员之间共享的笔记。"
|
||||
addModerationNote: "添加管理笔记"
|
||||
moderationLogs: "管理日志"
|
||||
nUsersMentioned: "{n} 被提到"
|
||||
nUsersMentioned: "{n}人投稿"
|
||||
securityKeyAndPasskey: "安全密钥或 Passkey"
|
||||
securityKey: "安全密钥"
|
||||
lastUsed: "最后使用:"
|
||||
|
@ -477,7 +478,7 @@ notFoundDescription: "没有与指定 URL 对应的页面。"
|
|||
uploadFolder: "默认上传文件夹"
|
||||
markAsReadAllNotifications: "将所有通知标为已读"
|
||||
markAsReadAllUnreadNotes: "将所有帖子标记为已读"
|
||||
markAsReadAllTalkMessages: "将所有聊天标记为已读"
|
||||
markAsReadAllTalkMessages: "将所有私信标记为已读"
|
||||
help: "帮助"
|
||||
inputMessageHere: "在此键入信息"
|
||||
close: "关闭"
|
||||
|
@ -687,10 +688,10 @@ emptyToDisableSmtpAuth: "用户名和密码留空可以禁用 SMTP 验证"
|
|||
smtpSecure: "在 SMTP 连接中使用隐式 SSL / TLS"
|
||||
smtpSecureInfo: "使用 STARTTLS 时关闭。"
|
||||
testEmail: "邮件发送测试"
|
||||
wordMute: "隐藏关键词"
|
||||
wordMute: "屏蔽关键词"
|
||||
wordMuteDescription: "折叠包含指定关键词的帖子。被折叠的帖子可单击展开。"
|
||||
hardWordMute: "隐藏硬关键词"
|
||||
showMutedWord: "显示已隐藏的关键词"
|
||||
hardWordMute: "强屏蔽关键词"
|
||||
showMutedWord: "显示屏蔽关键词"
|
||||
hardWordMuteDescription: "隐藏包含指定关键词的帖子。与隐藏关键词不同,帖子将完全不会显示。"
|
||||
regexpError: "正则表达式错误"
|
||||
regexpErrorDescription: "{tab} 隐藏文字的第 {line} 行的正则表达式有错误:"
|
||||
|
@ -779,7 +780,7 @@ emailVerified: "电子邮件地址已验证"
|
|||
noteFavoritesCount: "收藏的帖子数"
|
||||
pageLikesCount: "页面点赞次数"
|
||||
pageLikedCount: "页面被点赞次数"
|
||||
contact: "联系人"
|
||||
contact: "联系方式"
|
||||
useSystemFont: "使用系统默认字体"
|
||||
clips: "便签"
|
||||
experimentalFeatures: "实验性功能"
|
||||
|
@ -800,7 +801,7 @@ showTitlebar: "显示标题栏"
|
|||
clearCache: "清除缓存"
|
||||
onlineUsersCount: "{n} 人在线"
|
||||
nUsers: "{n} 用户"
|
||||
nNotes: "{n} 帖子"
|
||||
nNotes: "{n}帖子"
|
||||
sendErrorReports: "发送错误报告"
|
||||
sendErrorReportsDescription: "启用后,如果出现问题,可以与 Misskey 共享详细的错误信息,从而帮助提高软件的质量。错误信息包括操作系统版本、浏览器类型、行为历史记录等。"
|
||||
myTheme: "我的主题"
|
||||
|
@ -824,7 +825,7 @@ youAreRunningUpToDateClient: "您所使用的客户端已经是最新的。"
|
|||
newVersionOfClientAvailable: "新版本的客户端可用。"
|
||||
usageAmount: "使用量"
|
||||
capacity: "容量"
|
||||
inUse: "已使用"
|
||||
inUse: "使用中"
|
||||
editCode: "编辑代码"
|
||||
apply: "应用"
|
||||
receiveAnnouncementFromInstance: "从服务器接收通知"
|
||||
|
@ -869,12 +870,12 @@ noMaintainerInformationWarning: "尚未设置管理员信息。"
|
|||
noInquiryUrlWarning: "尚未设置联络地址。"
|
||||
noBotProtectionWarning: "尚未设置 Bot 防御。"
|
||||
configure: "设置"
|
||||
postToGallery: "发送到图库"
|
||||
postToGallery: "添加至相册"
|
||||
postToHashtag: "投稿到这个标签"
|
||||
gallery: "图库"
|
||||
gallery: "相册"
|
||||
recentPosts: "最新发布"
|
||||
popularPosts: "热门投稿"
|
||||
shareWithNote: "在帖子中分享"
|
||||
shareWithNote: "分享到贴文"
|
||||
ads: "广告"
|
||||
expiration: "截止时间"
|
||||
startingperiod: "开始时间"
|
||||
|
@ -885,7 +886,7 @@ middle: "中"
|
|||
low: "低"
|
||||
emailNotConfiguredWarning: "尚未设置电子邮件地址。"
|
||||
ratio: "比率"
|
||||
previewNoteText: "预览文本"
|
||||
previewNoteText: "预览正文"
|
||||
customCss: "自定义 CSS"
|
||||
customCssWarn: "这些设置必须有相关的基础知识,不当的配置可能导致客户端无法正常使用。"
|
||||
global: "全局"
|
||||
|
@ -924,8 +925,8 @@ manageAccounts: "管理账户"
|
|||
makeReactionsPublic: "将回应设置为公开"
|
||||
makeReactionsPublicDescription: "将您发表过的回应设置成公开可见。"
|
||||
classic: "经典"
|
||||
muteThread: "隐藏帖子列表"
|
||||
unmuteThread: "取消隐藏帖子列表"
|
||||
muteThread: "屏蔽帖文串"
|
||||
unmuteThread: "取消屏蔽帖文串"
|
||||
followingVisibility: "关注的人的公开范围"
|
||||
followersVisibility: "关注者的公开范围"
|
||||
continueThread: "查看更多帖子"
|
||||
|
@ -948,17 +949,17 @@ searchByGoogle: "Google"
|
|||
instanceDefaultLightTheme: "服务器默认浅色主题"
|
||||
instanceDefaultDarkTheme: "服务器默认深色主题"
|
||||
instanceDefaultThemeDescription: "以对象格式输入主题代码"
|
||||
mutePeriod: "隐藏期限"
|
||||
mutePeriod: "屏蔽期限"
|
||||
period: "截止时间"
|
||||
indefinitely: "永久"
|
||||
tenMinutes: "10 分钟"
|
||||
tenMinutes: "10分钟"
|
||||
oneHour: "1 小时"
|
||||
oneDay: "1 天"
|
||||
oneDay: "1天"
|
||||
oneWeek: "1 周"
|
||||
oneMonth: "1 个月"
|
||||
threeMonths: "3 个月"
|
||||
oneMonth: "1个月"
|
||||
threeMonths: "3个月"
|
||||
oneYear: "1 年"
|
||||
threeDays: "3 天"
|
||||
threeDays: "3天"
|
||||
reflectMayTakeTime: "可能需要一些时间才能体现出效果。"
|
||||
failedToFetchAccountInformation: "获取账户信息失败"
|
||||
rateLimitExceeded: "已超过速率限制"
|
||||
|
@ -967,8 +968,8 @@ cropImageAsk: "是否要裁剪图像?"
|
|||
cropYes: "去裁剪"
|
||||
cropNo: "就这样吧!"
|
||||
file: "文件"
|
||||
recentNHours: "最近 {n} 小时"
|
||||
recentNDays: "最近 {n} 天"
|
||||
recentNHours: "最近{n}小时"
|
||||
recentNDays: "最近{n}天"
|
||||
noEmailServerWarning: "电子邮件服务器未设置。"
|
||||
thereIsUnresolvedAbuseReportWarning: "有未解决的报告"
|
||||
recommended: "推荐"
|
||||
|
@ -1079,7 +1080,7 @@ postToTheChannel: "发布到频道"
|
|||
cannotBeChangedLater: "之后不能再更改。"
|
||||
reactionAcceptance: "接受表情回应"
|
||||
likeOnly: "仅点赞"
|
||||
likeOnlyForRemote: "远程仅点赞"
|
||||
likeOnlyForRemote: "全部(远程仅点赞)"
|
||||
nonSensitiveOnly: "仅限非敏感内容"
|
||||
nonSensitiveOnlyForLocalLikeOnlyForRemote: "仅限非敏感内容(远程仅点赞)"
|
||||
rolesAssignedToMe: "指派给自己的角色"
|
||||
|
@ -1150,7 +1151,7 @@ youFollowing: "正在关注"
|
|||
preventAiLearning: "拒绝接受生成式 AI 的学习"
|
||||
preventAiLearningDescription: "要求文章生成 AI 或图像生成 AI 不能够以发布的帖子和图像等内容作为学习对象。这是通过在 HTML 响应中包含 noai 标志来实现的,这不能完全阻止 AI 学习你的发布内容,并不是所有 AI 都会遵守这类请求。"
|
||||
options: "选项"
|
||||
specifyUser: "用户指定"
|
||||
specifyUser: "指定用户"
|
||||
lookupConfirm: "确定查询?"
|
||||
openTagPageConfirm: "确定打开话题标签页面?"
|
||||
specifyHost: "指定主机名"
|
||||
|
@ -1265,7 +1266,7 @@ replaying: "重播中"
|
|||
endReplay: "结束回放"
|
||||
copyReplayData: "复制回放数据"
|
||||
ranking: "排行榜"
|
||||
lastNDays: "最近 {n} 天"
|
||||
lastNDays: "最近{n}天"
|
||||
backToTitle: "返回标题"
|
||||
hemisphere: "居住地区"
|
||||
withSensitive: "显示包含敏感媒体的帖子"
|
||||
|
@ -1316,6 +1317,7 @@ acknowledgeNotesAndEnable: "理解注意事项后再开启。"
|
|||
federationSpecified: "此服务器已开启联合白名单。只能与管理员指定的服务器通信。"
|
||||
federationDisabled: "此服务器已禁用联合。无法与其它服务器上的用户通信。"
|
||||
draft: "草稿"
|
||||
draftsAndScheduledNotes: "草稿和定时发送"
|
||||
confirmOnReact: "发送回应前需要确认"
|
||||
reactAreYouSure: "要用「{emoji}」进行回应吗?"
|
||||
markAsSensitiveConfirm: "要将此媒体标记为敏感吗?"
|
||||
|
@ -1343,7 +1345,7 @@ postForm: "投稿窗口"
|
|||
textCount: "字数"
|
||||
information: "关于"
|
||||
chat: "聊天"
|
||||
directMessage: "聊天"
|
||||
directMessage: "私信"
|
||||
directMessage_short: "消息"
|
||||
migrateOldSettings: "迁移旧设置信息"
|
||||
migrateOldSettings_description: "通常设置信息将自动迁移。但如果由于某种原因迁移不成功,则可以手动触发迁移过程。当前的配置信息将被覆盖。"
|
||||
|
@ -1362,24 +1364,40 @@ advice: "建议"
|
|||
realtimeMode: "实时模式"
|
||||
turnItOn: "开启"
|
||||
turnItOff: "关闭"
|
||||
emojiMute: "隐藏表情符号"
|
||||
emojiUnmute: "解除隐藏表情符号"
|
||||
muteX: "隐藏{x}"
|
||||
unmuteX: "解除隐藏{x}"
|
||||
emojiMute: "屏蔽表情符号"
|
||||
emojiUnmute: "取消屏蔽表情符号"
|
||||
muteX: "屏蔽{x}"
|
||||
unmuteX: "取消屏蔽{x}"
|
||||
abort: "中止"
|
||||
tip: "提示和技巧"
|
||||
redisplayAllTips: "重新显示所有的提示和技巧"
|
||||
hideAllTips: "隐藏所有的提示和技巧"
|
||||
defaultImageCompressionLevel: "默认图像压缩等级"
|
||||
defaultImageCompressionLevel_description: "较低的等级可以保持画质,但会增加文件大小。<br>较高的等级可以减少文件大小,但相对应的画质将会降低。"
|
||||
inMinutes: "分"
|
||||
inDays: "日"
|
||||
defaultCompressionLevel: "默认压缩等级"
|
||||
defaultCompressionLevel_description: "较低的等级可以保持质量,但会增加文件大小。<br>较高的等级可以减少文件大小,但相对应的质量将会降低。"
|
||||
inMinutes: "分钟"
|
||||
inDays: "天"
|
||||
safeModeEnabled: "已启用安全模式"
|
||||
pluginsAreDisabledBecauseSafeMode: "因启用了安全模式,所有插件均已被禁用。"
|
||||
customCssIsDisabledBecauseSafeMode: "因启用了安全模式,无法应用自定义 CSS。"
|
||||
themeIsDefaultBecauseSafeMode: "启用安全模式时将使用默认主题。关闭安全模式后将还原。"
|
||||
thankYouForTestingBeta: "感谢您协助测试 beta 版!"
|
||||
createUserSpecifiedNote: "创建指定用户的帖子"
|
||||
schedulePost: "定时发布"
|
||||
scheduleToPostOnX: "预定在 {x} 发出"
|
||||
scheduledToPostOnX: "已预定在 {x} 发出"
|
||||
schedule: "定时"
|
||||
scheduled: "定时"
|
||||
_compression:
|
||||
_quality:
|
||||
high: "高质量"
|
||||
medium: "中质量"
|
||||
low: "低质量"
|
||||
_size:
|
||||
large: "大"
|
||||
medium: "中"
|
||||
small: "小"
|
||||
_order:
|
||||
newest: "从新到旧"
|
||||
oldest: "从旧到新"
|
||||
|
@ -1390,16 +1408,16 @@ _chat:
|
|||
individualChat: "私聊"
|
||||
individualChat_description: "可以与特定用户进行一对一聊天。"
|
||||
roomChat: "群聊"
|
||||
roomChat_description: "可以进行多人聊天。\n就算用户未允许私聊,只要接受了邀请,仍可以聊天。"
|
||||
createRoom: "创建房间"
|
||||
roomChat_description: "支持多人同时进行消息交流。\n即使部分用户未开放私信权限,只要接受了邀请,仍可进行聊天。"
|
||||
createRoom: "创建群组"
|
||||
inviteUserToChat: "邀请用户来开始聊天"
|
||||
yourRooms: "已创建的房间"
|
||||
joiningRooms: "已加入的房间"
|
||||
yourRooms: "创建的群组"
|
||||
joiningRooms: "已加入的群组"
|
||||
invitations: "邀请"
|
||||
noInvitations: "没有邀请"
|
||||
history: "历史"
|
||||
noHistory: "没有历史记录"
|
||||
noRooms: "没有房间"
|
||||
noRooms: "没有群组"
|
||||
inviteUser: "邀请用户"
|
||||
sentInvitations: "已发送的邀请"
|
||||
join: "加入"
|
||||
|
@ -1410,16 +1428,16 @@ _chat:
|
|||
home: "首页"
|
||||
send: "发送"
|
||||
newline: "换行"
|
||||
muteThisRoom: "静音此房间"
|
||||
deleteRoom: "删除房间"
|
||||
muteThisRoom: "屏蔽该群组"
|
||||
deleteRoom: "删除群组"
|
||||
chatNotAvailableForThisAccountOrServer: "此服务器或者账户还未开启聊天功能。"
|
||||
chatIsReadOnlyForThisAccountOrServer: "此服务器或者账户内的聊天为只读。无法发布新信息或创建及加入群聊。"
|
||||
chatNotAvailableInOtherAccount: "对方账户目前处于无法使用聊天的状态。"
|
||||
cannotChatWithTheUser: "无法与此用户聊天"
|
||||
chatNotAvailableInOtherAccount: "对方的账户当前无法使用私信。"
|
||||
cannotChatWithTheUser: "无法私信该用户"
|
||||
cannotChatWithTheUser_description: "可能现在无法使用聊天,或者对方未开启聊天。"
|
||||
youAreNotAMemberOfThisRoomButInvited: "您还未加入此房间,但已收到邀请。如要加入,请接受邀请。"
|
||||
doYouAcceptInvitation: "要接受邀请吗?"
|
||||
chatWithThisUser: "聊天"
|
||||
chatWithThisUser: "私信"
|
||||
thisUserAllowsChatOnlyFromFollowers: "此用户仅接受关注者发起的聊天。"
|
||||
thisUserAllowsChatOnlyFromFollowing: "此用户仅接受关注的人发起的聊天。"
|
||||
thisUserAllowsChatOnlyFromMutualFollowing: "此用户仅接受互相关注的人发起的聊天。"
|
||||
|
@ -1897,7 +1915,7 @@ _achievements:
|
|||
description: "试图对网盘中的文件夹进行循环嵌套"
|
||||
_reactWithoutRead:
|
||||
title: "有好好读过吗?"
|
||||
description: "在含有 100 字以上的帖子被发出三秒内做出回应"
|
||||
description: "在含有100字以上的帖子被发出三秒内做出回应"
|
||||
_clickedClickHere:
|
||||
title: "点这里"
|
||||
description: "点了这里"
|
||||
|
@ -1999,7 +2017,7 @@ _role:
|
|||
canUpdateBioMedia: "可以更新头像和横幅"
|
||||
pinMax: "帖子置顶数量限制"
|
||||
antennaMax: "可创建的最大天线数量"
|
||||
wordMuteMax: "隐藏词的字数限制"
|
||||
wordMuteMax: "屏蔽词的字数限制"
|
||||
webhookMax: "Webhook 创建数量限制"
|
||||
clipMax: "便签创建数量限制"
|
||||
noteEachClipsMax: "单个便签内的贴文数量限制"
|
||||
|
@ -2017,11 +2035,12 @@ _role:
|
|||
canImportFollowing: "允许导入关注列表"
|
||||
canImportMuting: "允许导入隐藏列表"
|
||||
canImportUserLists: "允许导入用户列表"
|
||||
chatAvailability: "允许聊天"
|
||||
chatAvailability: "允许私信"
|
||||
uploadableFileTypes: "可上传的文件类型"
|
||||
uploadableFileTypes_caption: "指定 MIME 类型。可用换行指定多个类型,也可以用星号(*)作为通配符。(如 image/*)"
|
||||
uploadableFileTypes_caption2: "文件根据文件的不同,可能无法判断其类型。若要允许此类文件,请在指定中添加 {x}。"
|
||||
noteDraftLimit: "可在服务器上创建多少草稿"
|
||||
scheduledNoteLimit: "可同时创建的定时帖子数量"
|
||||
watermarkAvailable: "能否使用水印功能"
|
||||
_condition:
|
||||
roleAssignedTo: "已分配给手动角色"
|
||||
|
@ -2087,9 +2106,9 @@ _forgotPassword:
|
|||
ifNoEmail: "如果您没有设置电子邮件地址,请联系管理员。"
|
||||
contactAdmin: "该服务器不支持发送电子邮件。如果您想重设密码,请联系管理员。"
|
||||
_gallery:
|
||||
my: "我的图库"
|
||||
liked: "喜欢的图片"
|
||||
like: "喜欢"
|
||||
my: "我的相册"
|
||||
liked: "喜欢的相册"
|
||||
like: "喜欢!"
|
||||
unlike: "取消喜欢"
|
||||
_email:
|
||||
_follow:
|
||||
|
@ -2155,14 +2174,14 @@ _channel:
|
|||
edit: "编辑频道"
|
||||
setBanner: "设置横幅"
|
||||
removeBanner: "删除横幅"
|
||||
featured: "热点"
|
||||
owned: "管理中"
|
||||
featured: "热门"
|
||||
owned: "正在管理"
|
||||
following: "正在关注"
|
||||
usersCount: "有 {n} 人参与"
|
||||
notesCount: "有 {n} 个帖子"
|
||||
usersCount: "有{n}人参与"
|
||||
notesCount: "有{n}个帖子"
|
||||
nameAndDescription: "名称与描述"
|
||||
nameOnly: "仅名称"
|
||||
allowRenoteToExternal: "允许在频道外转帖及引用"
|
||||
allowRenoteToExternal: "允许转发到频道外和引用"
|
||||
_menuDisplay:
|
||||
sideFull: "横向"
|
||||
sideIcon: "横向(图标)"
|
||||
|
@ -2173,10 +2192,10 @@ _wordMute:
|
|||
muteWordsDescription: "AND 条件用空格分隔,OR 条件用换行符分隔。"
|
||||
muteWordsDescription2: "正则表达式用斜线包裹"
|
||||
_instanceMute:
|
||||
instanceMuteDescription: "隐藏服务器中所有的帖子和转帖,包括这些服务器上用户的回复。"
|
||||
instanceMuteDescription2: "一行一个"
|
||||
instanceMuteDescription: "屏蔽服务器中所有的帖子和转帖,包括该服务器内用户的回复。"
|
||||
instanceMuteDescription2: "通过换行符分隔进行设置"
|
||||
title: "下面实例中的帖子将被隐藏。"
|
||||
heading: "已隐藏的服务器"
|
||||
heading: "已屏蔽的服务器"
|
||||
_theme:
|
||||
explore: "寻找主题"
|
||||
install: "安装主题"
|
||||
|
@ -2249,7 +2268,7 @@ _sfx:
|
|||
noteMy: "我的帖子"
|
||||
notification: "通知"
|
||||
reaction: "选择回应时"
|
||||
chatMessage: "聊天信息"
|
||||
chatMessage: "私信"
|
||||
_soundSettings:
|
||||
driveFile: "使用网盘内的音频"
|
||||
driveFileWarn: "选择网盘上的文件"
|
||||
|
@ -2260,28 +2279,28 @@ _soundSettings:
|
|||
driveFileError: "无法读取声音。请更改设置。"
|
||||
_ago:
|
||||
future: "未来"
|
||||
justNow: "最近"
|
||||
secondsAgo: "{n} 秒前"
|
||||
minutesAgo: "{n} 分前"
|
||||
hoursAgo: "{n} 小时前"
|
||||
daysAgo: "{n} 日前"
|
||||
weeksAgo: "{n} 周前"
|
||||
monthsAgo: "{n} 月前"
|
||||
yearsAgo: "{n} 年前"
|
||||
justNow: "刚刚"
|
||||
secondsAgo: "{n}秒前"
|
||||
minutesAgo: "{n}分钟前"
|
||||
hoursAgo: "{n}小时前"
|
||||
daysAgo: "{n}天前"
|
||||
weeksAgo: "{n}周前"
|
||||
monthsAgo: "{n}个月前"
|
||||
yearsAgo: "{n}年前"
|
||||
invalid: "没有"
|
||||
_timeIn:
|
||||
seconds: "{n}秒后"
|
||||
minutes: "{n} 分后"
|
||||
hours: "{n} 小时后"
|
||||
minutes: "{n}分钟后"
|
||||
hours: "{n}小时后"
|
||||
days: "{n}天后"
|
||||
weeks: "{n} 周后"
|
||||
months: "{n} 月后"
|
||||
years: "{n} 年后"
|
||||
weeks: "{n}周后"
|
||||
months: "{n}个月后"
|
||||
years: "{n}年后"
|
||||
_time:
|
||||
second: "秒"
|
||||
minute: "分"
|
||||
minute: "分钟"
|
||||
hour: "小时"
|
||||
day: "日"
|
||||
day: "天"
|
||||
month: "个月"
|
||||
_2fa:
|
||||
alreadyRegistered: "此设备已被注册"
|
||||
|
@ -2315,36 +2334,36 @@ _2fa:
|
|||
_permissions:
|
||||
"read:account": "查看账户信息"
|
||||
"write:account": "更改帐户信息"
|
||||
"read:blocks": "查看屏蔽列表"
|
||||
"write:blocks": "编辑屏蔽列表"
|
||||
"read:blocks": "查看黑名单"
|
||||
"write:blocks": "编辑黑名单"
|
||||
"read:drive": "查看网盘"
|
||||
"write:drive": "管理网盘文件"
|
||||
"read:favorites": "查看收藏夹"
|
||||
"write:favorites": "编辑收藏夹"
|
||||
"read:following": "查看关注信息"
|
||||
"write:following": "关注/取消关注"
|
||||
"read:messaging": "查看消息"
|
||||
"read:messaging": "查看私信"
|
||||
"write:messaging": "撰写或删除消息"
|
||||
"read:mutes": "查看隐藏列表"
|
||||
"write:mutes": "编辑隐藏列表"
|
||||
"read:mutes": "查看屏蔽列表"
|
||||
"write:mutes": "编辑屏蔽列表"
|
||||
"write:notes": "撰写或删除帖子"
|
||||
"read:notifications": "查看通知"
|
||||
"write:notifications": "管理通知"
|
||||
"read:reactions": "查看回应"
|
||||
"write:reactions": "回应操作"
|
||||
"write:reactions": "编辑回应"
|
||||
"write:votes": "投票"
|
||||
"read:pages": "查看页面"
|
||||
"write:pages": "操作页面"
|
||||
"write:pages": "编辑页面"
|
||||
"read:page-likes": "查看喜欢的页面"
|
||||
"write:page-likes": "操作喜欢的页面"
|
||||
"write:page-likes": "管理喜欢的页面"
|
||||
"read:user-groups": "查看用户组"
|
||||
"write:user-groups": "操作用户组"
|
||||
"write:user-groups": "编辑用户组"
|
||||
"read:channels": "查看频道"
|
||||
"write:channels": "管理频道"
|
||||
"read:gallery": "浏览图库"
|
||||
"write:gallery": "操作图库"
|
||||
"read:gallery-likes": "读取喜欢的图片"
|
||||
"write:gallery-likes": "操作喜欢的图片"
|
||||
"read:gallery": "浏览相册"
|
||||
"write:gallery": "编辑相册"
|
||||
"read:gallery-likes": "浏览喜欢的相册"
|
||||
"write:gallery-likes": "管理喜欢的相册"
|
||||
"read:flash": "查看 Play"
|
||||
"write:flash": "编辑 Play"
|
||||
"read:flash-likes": "查看 Play 的点赞"
|
||||
|
@ -2372,33 +2391,33 @@ _permissions:
|
|||
"read:admin:roles": "查看角色"
|
||||
"write:admin:relays": "编辑中继"
|
||||
"read:admin:relays": "查看中继"
|
||||
"write:admin:invite-codes": "编辑邀请码"
|
||||
"write:admin:invite-codes": "管理邀请码"
|
||||
"read:admin:invite-codes": "查看邀请码"
|
||||
"write:admin:announcements": "编辑公告"
|
||||
"write:admin:announcements": "管理公告"
|
||||
"read:admin:announcements": "查看公告"
|
||||
"write:admin:avatar-decorations": "编辑头像挂件"
|
||||
"read:admin:avatar-decorations": "查看头像挂件"
|
||||
"write:admin:federation": "编辑联合相关信息"
|
||||
"write:admin:account": "编辑用户账户"
|
||||
"read:admin:account": "查看用户相关情报"
|
||||
"write:admin:emoji": "编辑表情文字"
|
||||
"read:admin:emoji": "查看表情文字"
|
||||
"write:admin:emoji": "编辑表情符号"
|
||||
"read:admin:emoji": "查看表情符号"
|
||||
"write:admin:queue": "编辑作业队列"
|
||||
"read:admin:queue": "查看作业队列相关情报"
|
||||
"write:admin:promo": "运营推广说明"
|
||||
"write:admin:drive": "编辑用户网盘"
|
||||
"write:admin:drive": "管理用户网盘"
|
||||
"read:admin:drive": "查看用户网盘相关情报"
|
||||
"read:admin:stream": "使用管理员用的 Websocket API"
|
||||
"write:admin:ad": "编辑广告"
|
||||
"write:admin:ad": "管理广告"
|
||||
"read:admin:ad": "查看广告"
|
||||
"write:invite-codes": "生成邀请码"
|
||||
"read:invite-codes": "获取已发行的邀请码"
|
||||
"write:clip-favorite": "编辑便签的点赞"
|
||||
"write:clip-favorite": "管理喜欢的便签"
|
||||
"read:clip-favorite": "查看便签的点赞"
|
||||
"read:federation": "查看联合相关信息"
|
||||
"write:report-abuse": "举报用户"
|
||||
"write:chat": "撰写或删除消息"
|
||||
"read:chat": "查看聊天"
|
||||
"read:chat": "查看私信"
|
||||
_auth:
|
||||
shareAccessTitle: "应用程序授权许可"
|
||||
shareAccess: "您要授权允许 “{name}” 访问您的帐户吗?"
|
||||
|
@ -2417,7 +2436,7 @@ _antennaSources:
|
|||
homeTimeline: "已关注用户的帖子"
|
||||
users: "来自指定用户的帖子"
|
||||
userList: "来自指定列表中的帖子"
|
||||
userBlacklist: "除掉已选择用户后所有的帖子"
|
||||
userBlacklist: "过滤指定用户后的所有帖子"
|
||||
_weekday:
|
||||
sunday: "星期日"
|
||||
monday: "星期一"
|
||||
|
@ -2457,7 +2476,7 @@ _widgets:
|
|||
chooseList: "选择列表"
|
||||
clicker: "点击器"
|
||||
birthdayFollowings: "今天是他们的生日"
|
||||
chat: "聊天"
|
||||
chat: "私信"
|
||||
_cw:
|
||||
hide: "隐藏"
|
||||
show: "查看更多"
|
||||
|
@ -2465,26 +2484,26 @@ _cw:
|
|||
files: "{count} 个文件"
|
||||
_poll:
|
||||
noOnlyOneChoice: "需要至少两个选项"
|
||||
choiceN: "选择 {n}"
|
||||
choiceN: "选项{n}"
|
||||
noMore: "无法再添加更多了"
|
||||
canMultipleVote: "允许多个投票"
|
||||
canMultipleVote: "允许选择多个选项"
|
||||
expiration: "截止时间"
|
||||
infinite: "永久"
|
||||
at: "指定日期"
|
||||
after: "指定时间"
|
||||
deadlineDate: "截止日期"
|
||||
deadlineTime: "小时"
|
||||
duration: "时长"
|
||||
votesCount: "{n} 票"
|
||||
deadlineTime: "时间"
|
||||
duration: "期限"
|
||||
votesCount: "{n}票"
|
||||
totalVotes: "总票数 {n}"
|
||||
vote: "投票"
|
||||
showResult: "显示结果"
|
||||
voted: "已投票"
|
||||
closed: "已截止"
|
||||
remainingDays: "{d} 天 {h} 小时后截止"
|
||||
remainingDays: "{d}天{h}小时后截止"
|
||||
remainingHours: "{h} 小时 {m} 分后截止"
|
||||
remainingMinutes: "{m} 分 {s} 秒后截止"
|
||||
remainingSeconds: "{s} 秒后截止"
|
||||
remainingMinutes: "{m}分{s}秒后截止"
|
||||
remainingSeconds: "{s}秒后截止"
|
||||
_visibility:
|
||||
public: "公开"
|
||||
publicDescription: "您的帖子将出现在全局时间线上"
|
||||
|
@ -2503,9 +2522,9 @@ _postForm:
|
|||
quotePlaceholder: "引用这个帖子..."
|
||||
channelPlaceholder: "发布到频道…"
|
||||
_placeholders:
|
||||
a: "现在如何?"
|
||||
b: "发生了什么?"
|
||||
c: "你有什么想法?"
|
||||
a: "现在怎么样?"
|
||||
b: "想好发些什么了吗?"
|
||||
c: "在想些什么呢?"
|
||||
d: "你想要发布些什么吗?"
|
||||
e: "请写下来吧"
|
||||
f: "等待您的发布..."
|
||||
|
@ -2531,8 +2550,8 @@ _exportOrImport:
|
|||
favoritedNotes: "收藏的帖子"
|
||||
clips: "便签"
|
||||
followingList: "关注中"
|
||||
muteList: "隐藏"
|
||||
blockingList: "屏蔽"
|
||||
muteList: "屏蔽"
|
||||
blockingList: "拉黑"
|
||||
userLists: "列表"
|
||||
excludeMutingUsers: "排除屏蔽用户"
|
||||
excludeInactiveUsers: "排除不活跃用户"
|
||||
|
@ -2578,7 +2597,7 @@ _play:
|
|||
editThisPage: "编辑此 Play"
|
||||
viewSource: "查看源代码"
|
||||
my: "我的 Play"
|
||||
liked: "点赞的 Play"
|
||||
liked: "喜欢的 Play"
|
||||
featured: "热门"
|
||||
title: "标题"
|
||||
script: "脚本"
|
||||
|
@ -2595,7 +2614,7 @@ _pages:
|
|||
editThisPage: "编辑此页面"
|
||||
viewSource: "查看源代码"
|
||||
viewPage: "查看页面"
|
||||
like: "赞"
|
||||
like: "喜欢"
|
||||
unlike: "取消喜欢"
|
||||
my: "我的页面"
|
||||
liked: "喜欢的页面"
|
||||
|
@ -2643,10 +2662,12 @@ _notification:
|
|||
youGotReply: "来自{name}的回复"
|
||||
youGotQuote: "来自{name}的引用"
|
||||
youRenoted: "来自{name}的转发"
|
||||
youWereFollowed: "关注了你。"
|
||||
youWereFollowed: "关注了你"
|
||||
youReceivedFollowRequest: "您有新的关注请求"
|
||||
yourFollowRequestAccepted: "您的关注请求已通过"
|
||||
pollEnded: "问卷调查结果已生成。"
|
||||
scheduledNotePosted: "定时帖子已发布"
|
||||
scheduledNotePostFailed: "定时帖子发布失败"
|
||||
newNote: "新的帖子"
|
||||
unreadAntennaNote: "天线 {name}"
|
||||
roleAssigned: "授予的角色"
|
||||
|
@ -2726,7 +2747,7 @@ _deck:
|
|||
mentions: "提及"
|
||||
direct: "指定用户"
|
||||
roleTimeline: "角色时间线"
|
||||
chat: "聊天"
|
||||
chat: "私信"
|
||||
_dialog:
|
||||
charactersExceeded: "已经超过了最大字符数! 当前字符数 {current} / 限制字符数 {max}"
|
||||
charactersBelow: "低于最小字符数!当前字符数 {current} / 限制字符数 {min}"
|
||||
|
@ -2822,7 +2843,7 @@ _moderationLogTypes:
|
|||
deleteAccount: "删除了账户"
|
||||
deletePage: "删除了页面"
|
||||
deleteFlash: "删除了 Play"
|
||||
deleteGalleryPost: "删除了图库稿件"
|
||||
deleteGalleryPost: "删除相册内容"
|
||||
deleteChatRoom: "删除聊天室"
|
||||
updateProxyAccountDescription: "更新代理账户的简介"
|
||||
_fileViewer:
|
||||
|
@ -3078,7 +3099,7 @@ _bootErrors:
|
|||
serverError: "请稍等片刻再重试。若问题仍无法解决,请将以下 Error ID 一起发送给管理员。"
|
||||
solution: "以下方法或许可以解决问题:"
|
||||
solution1: "将浏览器及操作系统更新到最新版本"
|
||||
solution2: "禁用广告屏蔽插件"
|
||||
solution2: "禁用广告拦截插件"
|
||||
solution3: "清除浏览器缓存"
|
||||
solution4: "(Tor Browser)将 dom.webaudio.enabled 设定为 true"
|
||||
otherOption: "其它选项"
|
||||
|
@ -3201,6 +3222,8 @@ _imageEffector:
|
|||
mirror: "镜像"
|
||||
invert: "反转颜色"
|
||||
grayscale: "黑白"
|
||||
blur: "模糊"
|
||||
pixelate: "马赛克"
|
||||
colorAdjust: "色彩校正"
|
||||
colorClamp: "颜色限制"
|
||||
colorClampAdvanced: "颜色限制(高级)"
|
||||
|
@ -3212,11 +3235,13 @@ _imageEffector:
|
|||
checker: "检查"
|
||||
blockNoise: "块状噪点"
|
||||
tearing: "撕裂"
|
||||
fillSquare: "填充(四角)"
|
||||
fill: "填充"
|
||||
_fxProps:
|
||||
angle: "角度"
|
||||
scale: "大小"
|
||||
size: "大小"
|
||||
radius: "半径"
|
||||
samples: "采样数"
|
||||
offset: "位置"
|
||||
color: "颜色"
|
||||
opacity: "不透明度"
|
||||
|
@ -3246,6 +3271,7 @@ _imageEffector:
|
|||
zoomLinesThreshold: "集中线宽度"
|
||||
zoomLinesMaskSize: "中心直径"
|
||||
zoomLinesBlack: "变成黑色"
|
||||
circle: "圆形"
|
||||
drafts: "草稿"
|
||||
_drafts:
|
||||
select: "选择草稿"
|
||||
|
@ -3261,6 +3287,9 @@ _drafts:
|
|||
restoreFromDraft: "从草稿恢复"
|
||||
restore: "恢复"
|
||||
listDrafts: "草稿一览"
|
||||
schedule: "定时发布"
|
||||
listScheduledNotes: "定时发布列表"
|
||||
cancelSchedule: "取消定时"
|
||||
qr: "二维码"
|
||||
_qr:
|
||||
showTabTitle: "显示"
|
||||
|
|
|
@ -253,6 +253,7 @@ noteDeleteConfirm: "確定刪除此貼文嗎?"
|
|||
pinLimitExceeded: "不能置頂更多貼文了"
|
||||
done: "完成"
|
||||
processing: "處理中"
|
||||
preprocessing: "準備中"
|
||||
preview: "預覽"
|
||||
default: "預設"
|
||||
defaultValueIs: "預設值:{value}"
|
||||
|
@ -1316,6 +1317,7 @@ acknowledgeNotesAndEnable: "了解注意事項後再開啟。"
|
|||
federationSpecified: "此伺服器以白名單聯邦的方式運作。除了管理員指定的伺服器外,它無法與其他伺服器互動。"
|
||||
federationDisabled: "此伺服器未開啟站台聯邦。無法與其他伺服器上的使用者互動。"
|
||||
draft: "草稿\n"
|
||||
draftsAndScheduledNotes: "草稿與排定發布"
|
||||
confirmOnReact: "在做出反應前先確認"
|
||||
reactAreYouSure: "用「 {emoji} 」反應嗎?"
|
||||
markAsSensitiveConfirm: "要將這個媒體設定為敏感嗎?"
|
||||
|
@ -1372,6 +1374,8 @@ redisplayAllTips: "重新顯示所有「提示與技巧」"
|
|||
hideAllTips: "隱藏所有「提示與技巧」"
|
||||
defaultImageCompressionLevel: "預設的影像壓縮程度"
|
||||
defaultImageCompressionLevel_description: "低的話可以保留畫質,但是會增加檔案的大小。<br>高的話可以減少檔案大小,但是會降低畫質。"
|
||||
defaultCompressionLevel: "預設的壓縮程度"
|
||||
defaultCompressionLevel_description: "低的話可以保留品質,但是會增加檔案的大小。<br>高的話可以減少檔案大小,但是會降低品質。"
|
||||
inMinutes: "分鐘"
|
||||
inDays: "日"
|
||||
safeModeEnabled: "啟用安全模式"
|
||||
|
@ -1380,6 +1384,20 @@ customCssIsDisabledBecauseSafeMode: "由於啟用安全模式,所有的客製
|
|||
themeIsDefaultBecauseSafeMode: "在安全模式啟用期間將使用預設主題。關閉安全模式後會恢復原本的設定。"
|
||||
thankYouForTestingBeta: "感謝您協助驗證 beta 版!"
|
||||
createUserSpecifiedNote: "建立使用者指定的筆記"
|
||||
schedulePost: "排定發布"
|
||||
scheduleToPostOnX: "排定在 {x} 發布"
|
||||
scheduledToPostOnX: "已排定在 {x} 發布貼文"
|
||||
schedule: "排定"
|
||||
scheduled: "排定"
|
||||
_compression:
|
||||
_quality:
|
||||
high: "高品質"
|
||||
medium: "中品質"
|
||||
low: "低品質"
|
||||
_size:
|
||||
large: "大"
|
||||
medium: "中"
|
||||
small: "小"
|
||||
_order:
|
||||
newest: "最新的在前"
|
||||
oldest: "最舊的在前"
|
||||
|
@ -2022,6 +2040,7 @@ _role:
|
|||
uploadableFileTypes_caption: "請指定 MIME 類型。可以用換行區隔多個類型,也可以使用星號(*)作為萬用字元進行指定。(例如:image/*)\n"
|
||||
uploadableFileTypes_caption2: "有些檔案可能無法判斷其類型。若要允許這類檔案,請在指定中加入 {x}。"
|
||||
noteDraftLimit: "伺服器端可建立的貼文草稿數量上限\n"
|
||||
scheduledNoteLimit: "同時建立的排定發布數量"
|
||||
watermarkAvailable: "浮水印功能是否可用"
|
||||
_condition:
|
||||
roleAssignedTo: "手動指派角色完成"
|
||||
|
@ -2647,6 +2666,8 @@ _notification:
|
|||
youReceivedFollowRequest: "您有新的追隨請求"
|
||||
yourFollowRequestAccepted: "您的追隨請求已被核准"
|
||||
pollEnded: "問卷調查已產生結果"
|
||||
scheduledNotePosted: "已排定發布貼文"
|
||||
scheduledNotePostFailed: "排定發布貼文失敗了"
|
||||
newNote: "新的貼文"
|
||||
unreadAntennaNote: "天線 {name}"
|
||||
roleAssigned: "已授予角色"
|
||||
|
@ -3172,7 +3193,9 @@ _watermarkEditor:
|
|||
opacity: "透明度"
|
||||
scale: "大小"
|
||||
text: "文字"
|
||||
qr: "二維條碼"
|
||||
position: "位置"
|
||||
margin: "邊界"
|
||||
type: "類型"
|
||||
image: "圖片"
|
||||
advanced: "進階"
|
||||
|
@ -3187,6 +3210,7 @@ _watermarkEditor:
|
|||
polkadotSubDotOpacity: "子圓點的不透明度"
|
||||
polkadotSubDotRadius: "子圓點的尺寸"
|
||||
polkadotSubDotDivisions: "子圓點的數量"
|
||||
leaveBlankToAccountUrl: "若留空則使用帳戶的 URL"
|
||||
_imageEffector:
|
||||
title: "特效"
|
||||
addEffect: "新增特效"
|
||||
|
@ -3198,6 +3222,8 @@ _imageEffector:
|
|||
mirror: "鏡像"
|
||||
invert: "反轉色彩"
|
||||
grayscale: "黑白"
|
||||
blur: "模糊"
|
||||
pixelate: "馬賽克"
|
||||
colorAdjust: "色彩校正"
|
||||
colorClamp: "壓縮色彩"
|
||||
colorClampAdvanced: "壓縮色彩(進階)"
|
||||
|
@ -3209,11 +3235,13 @@ _imageEffector:
|
|||
checker: "棋盤格"
|
||||
blockNoise: "阻擋雜訊"
|
||||
tearing: "撕裂"
|
||||
fillSquare: "填充(方形)"
|
||||
fill: "填充"
|
||||
_fxProps:
|
||||
angle: "角度"
|
||||
scale: "大小"
|
||||
size: "大小"
|
||||
radius: "半徑"
|
||||
samples: "取樣數"
|
||||
offset: "位置"
|
||||
color: "顏色"
|
||||
opacity: "透明度"
|
||||
|
@ -3243,6 +3271,7 @@ _imageEffector:
|
|||
zoomLinesThreshold: "集中線的寬度"
|
||||
zoomLinesMaskSize: "中心直徑"
|
||||
zoomLinesBlack: "變成黑色"
|
||||
circle: "圓形"
|
||||
drafts: "草稿\n"
|
||||
_drafts:
|
||||
select: "選擇草槁"
|
||||
|
@ -3258,6 +3287,22 @@ _drafts:
|
|||
restoreFromDraft: "從草稿復原\n"
|
||||
restore: "還原"
|
||||
listDrafts: "草稿清單"
|
||||
schedule: "排定發布"
|
||||
listScheduledNotes: "排定發布列表"
|
||||
cancelSchedule: "解除排定"
|
||||
qr: "二維條碼"
|
||||
_qr:
|
||||
showTabTitle: "檢視"
|
||||
readTabTitle: "讀取"
|
||||
shareTitle: "{name} {acct}"
|
||||
shareText: "請在聯邦宇宙追隨我吧!"
|
||||
chooseCamera: "選擇相機"
|
||||
cannotToggleFlash: "無法切換閃光燈"
|
||||
turnOnFlash: "開啟閃光燈"
|
||||
turnOffFlash: "關閉閃光燈"
|
||||
startQr: "啟動條碼掃描器"
|
||||
stopQr: "停止條碼掃描器"
|
||||
noQrCodeFound: "找不到 QR code"
|
||||
scanFile: "掃描在裝置上的影像"
|
||||
raw: "文字"
|
||||
mfm: "MFM"
|
||||
|
|
|
@ -1,12 +1,12 @@
|
|||
{
|
||||
"name": "misskey",
|
||||
"version": "2025.9.0",
|
||||
"version": "2025.10.0-beta.0",
|
||||
"codename": "nasubi",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/misskey-dev/misskey.git"
|
||||
},
|
||||
"packageManager": "pnpm@10.15.1",
|
||||
"packageManager": "pnpm@10.16.0",
|
||||
"workspaces": [
|
||||
"packages/frontend-shared",
|
||||
"packages/frontend",
|
||||
|
@ -76,7 +76,7 @@
|
|||
"eslint": "9.35.0",
|
||||
"globals": "16.3.0",
|
||||
"ncp": "2.0.0",
|
||||
"pnpm": "10.15.1",
|
||||
"pnpm": "10.16.0",
|
||||
"start-server-and-test": "2.1.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
|
|
|
@ -0,0 +1,16 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
export class SensitiveAd1757823175259 {
|
||||
name = 'SensitiveAd1757823175259'
|
||||
|
||||
async up(queryRunner) {
|
||||
await queryRunner.query(`ALTER TABLE "ad" ADD "isSensitive" boolean NOT NULL DEFAULT false`);
|
||||
}
|
||||
|
||||
async down(queryRunner) {
|
||||
await queryRunner.query(`ALTER TABLE "ad" DROP COLUMN "isSensitive"`);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,24 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
export class ScheduledPost1758677617888 {
|
||||
name = 'ScheduledPost1758677617888'
|
||||
|
||||
/**
|
||||
* @param {QueryRunner} queryRunner
|
||||
*/
|
||||
async up(queryRunner) {
|
||||
await queryRunner.query(`ALTER TABLE "note_draft" ADD "scheduledAt" TIMESTAMP WITH TIME ZONE`);
|
||||
await queryRunner.query(`ALTER TABLE "note_draft" ADD "isActuallyScheduled" boolean NOT NULL DEFAULT false`);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {QueryRunner} queryRunner
|
||||
*/
|
||||
async down(queryRunner) {
|
||||
await queryRunner.query(`ALTER TABLE "note_draft" DROP COLUMN "isActuallyScheduled"`);
|
||||
await queryRunner.query(`ALTER TABLE "note_draft" DROP COLUMN "scheduledAt"`);
|
||||
}
|
||||
}
|
|
@ -39,17 +39,17 @@
|
|||
},
|
||||
"optionalDependencies": {
|
||||
"@swc/core-android-arm64": "1.3.11",
|
||||
"@swc/core-darwin-arm64": "1.13.5",
|
||||
"@swc/core-darwin-x64": "1.13.5",
|
||||
"@swc/core-darwin-arm64": "1.13.19",
|
||||
"@swc/core-darwin-x64": "1.13.19",
|
||||
"@swc/core-freebsd-x64": "1.3.11",
|
||||
"@swc/core-linux-arm-gnueabihf": "1.13.5",
|
||||
"@swc/core-linux-arm64-gnu": "1.13.5",
|
||||
"@swc/core-linux-arm64-musl": "1.13.5",
|
||||
"@swc/core-linux-x64-gnu": "1.13.5",
|
||||
"@swc/core-linux-x64-musl": "1.13.5",
|
||||
"@swc/core-win32-arm64-msvc": "1.13.5",
|
||||
"@swc/core-win32-ia32-msvc": "1.13.5",
|
||||
"@swc/core-win32-x64-msvc": "1.13.5",
|
||||
"@swc/core-linux-arm-gnueabihf": "1.13.19",
|
||||
"@swc/core-linux-arm64-gnu": "1.13.19",
|
||||
"@swc/core-linux-arm64-musl": "1.13.19",
|
||||
"@swc/core-linux-x64-gnu": "1.13.19",
|
||||
"@swc/core-linux-x64-musl": "1.13.19",
|
||||
"@swc/core-win32-arm64-msvc": "1.13.19",
|
||||
"@swc/core-win32-ia32-msvc": "1.13.19",
|
||||
"@swc/core-win32-x64-msvc": "1.13.19",
|
||||
"@tensorflow/tfjs": "4.22.0",
|
||||
"@tensorflow/tfjs-node": "4.22.0",
|
||||
"bufferutil": "4.0.9",
|
||||
|
@ -69,8 +69,8 @@
|
|||
"utf-8-validate": "6.0.5"
|
||||
},
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-s3": "3.883.0",
|
||||
"@aws-sdk/lib-storage": "3.883.0",
|
||||
"@aws-sdk/client-s3": "3.896.0",
|
||||
"@aws-sdk/lib-storage": "3.895.0",
|
||||
"@discordapp/twemoji": "16.0.1",
|
||||
"@fastify/accepts": "5.0.2",
|
||||
"@fastify/cookie": "11.0.2",
|
||||
|
@ -82,7 +82,7 @@
|
|||
"@fastify/view": "10.0.2",
|
||||
"@misskey-dev/sharp-read-bmp": "1.2.0",
|
||||
"@misskey-dev/summaly": "5.2.3",
|
||||
"@napi-rs/canvas": "0.1.79",
|
||||
"@napi-rs/canvas": "0.1.80",
|
||||
"@nestjs/common": "11.1.6",
|
||||
"@nestjs/core": "11.1.6",
|
||||
"@nestjs/testing": "11.1.6",
|
||||
|
@ -103,29 +103,29 @@
|
|||
"bcryptjs": "2.4.3",
|
||||
"blurhash": "2.0.5",
|
||||
"body-parser": "1.20.3",
|
||||
"bullmq": "5.58.5",
|
||||
"bullmq": "5.58.8",
|
||||
"cacheable-lookup": "7.0.0",
|
||||
"cbor": "9.0.2",
|
||||
"chalk": "5.6.0",
|
||||
"chalk-template": "1.1.0",
|
||||
"chalk": "5.6.2",
|
||||
"chalk-template": "1.1.2",
|
||||
"chokidar": "4.0.3",
|
||||
"cli-highlight": "2.1.11",
|
||||
"color-convert": "2.0.1",
|
||||
"content-disposition": "0.5.4",
|
||||
"date-fns": "2.30.0",
|
||||
"deep-email-validator": "0.1.21",
|
||||
"fastify": "5.6.0",
|
||||
"fastify": "5.6.1",
|
||||
"fastify-raw-body": "5.0.0",
|
||||
"feed": "4.2.2",
|
||||
"file-type": "19.6.0",
|
||||
"fluent-ffmpeg": "2.1.3",
|
||||
"form-data": "4.0.4",
|
||||
"got": "14.4.8",
|
||||
"got": "14.4.9",
|
||||
"happy-dom": "16.8.1",
|
||||
"hpagent": "1.2.0",
|
||||
"htmlescape": "1.1.1",
|
||||
"http-link-header": "1.1.3",
|
||||
"ioredis": "5.7.0",
|
||||
"ioredis": "5.8.0",
|
||||
"ip-cidr": "4.0.2",
|
||||
"ipaddr.js": "2.2.0",
|
||||
"is-svg": "5.1.0",
|
||||
|
@ -135,14 +135,14 @@
|
|||
"jsonld": "8.3.3",
|
||||
"jsrsasign": "11.1.0",
|
||||
"juice": "11.0.1",
|
||||
"meilisearch": "0.52.0",
|
||||
"meilisearch": "0.53.0",
|
||||
"mfm-js": "0.25.0",
|
||||
"microformats-parser": "2.0.4",
|
||||
"mime-types": "2.1.35",
|
||||
"misskey-js": "workspace:*",
|
||||
"misskey-reversi": "workspace:*",
|
||||
"ms": "3.0.0-canary.202508261828",
|
||||
"nanoid": "5.1.5",
|
||||
"nanoid": "5.1.6",
|
||||
"nested-property": "4.0.0",
|
||||
"node-fetch": "3.3.2",
|
||||
"nodemailer": "6.10.1",
|
||||
|
@ -175,12 +175,12 @@
|
|||
"slacc": "0.0.10",
|
||||
"strict-event-emitter-types": "2.0.0",
|
||||
"stringz": "2.1.0",
|
||||
"systeminformation": "5.27.8",
|
||||
"systeminformation": "5.27.10",
|
||||
"tinycolor2": "1.6.0",
|
||||
"tmp": "0.2.5",
|
||||
"tsc-alias": "1.8.16",
|
||||
"tsconfig-paths": "4.2.0",
|
||||
"typeorm": "0.3.26",
|
||||
"typeorm": "0.3.27",
|
||||
"typescript": "5.9.2",
|
||||
"ulid": "2.4.0",
|
||||
"vary": "1.1.2",
|
||||
|
@ -210,7 +210,7 @@
|
|||
"@types/jsrsasign": "10.5.15",
|
||||
"@types/mime-types": "2.1.4",
|
||||
"@types/ms": "0.7.34",
|
||||
"@types/node": "22.18.1",
|
||||
"@types/node": "22.18.6",
|
||||
"@types/nodemailer": "6.4.19",
|
||||
"@types/oauth": "0.9.6",
|
||||
"@types/oauth2orize": "1.11.5",
|
||||
|
@ -231,8 +231,8 @@
|
|||
"@types/vary": "1.1.3",
|
||||
"@types/web-push": "3.6.4",
|
||||
"@types/ws": "8.18.1",
|
||||
"@typescript-eslint/eslint-plugin": "8.42.0",
|
||||
"@typescript-eslint/parser": "8.42.0",
|
||||
"@typescript-eslint/eslint-plugin": "8.44.1",
|
||||
"@typescript-eslint/parser": "8.44.1",
|
||||
"aws-sdk-client-mock": "4.1.0",
|
||||
"cross-env": "7.0.3",
|
||||
"eslint-plugin-import": "2.32.0",
|
||||
|
|
|
@ -7,6 +7,7 @@ import * as fs from 'node:fs';
|
|||
import { fileURLToPath } from 'node:url';
|
||||
import { dirname, resolve } from 'node:path';
|
||||
import * as yaml from 'js-yaml';
|
||||
import { type FastifyServerOptions } from 'fastify';
|
||||
import type * as Sentry from '@sentry/node';
|
||||
import type * as SentryVue from '@sentry/vue';
|
||||
import type { RedisOptions } from 'ioredis';
|
||||
|
@ -27,6 +28,7 @@ type Source = {
|
|||
url?: string;
|
||||
port?: number;
|
||||
socket?: string;
|
||||
trustProxy?: FastifyServerOptions['trustProxy'];
|
||||
chmodSocket?: string;
|
||||
disableHsts?: boolean;
|
||||
db: {
|
||||
|
@ -118,6 +120,7 @@ export type Config = {
|
|||
url: string;
|
||||
port: number;
|
||||
socket: string | undefined;
|
||||
trustProxy: FastifyServerOptions['trustProxy'];
|
||||
chmodSocket: string | undefined;
|
||||
disableHsts: boolean | undefined;
|
||||
db: {
|
||||
|
@ -266,6 +269,7 @@ export function loadConfig(): Config {
|
|||
url: url.origin,
|
||||
port: config.port ?? parseInt(process.env.PORT ?? '', 10),
|
||||
socket: config.socket,
|
||||
trustProxy: config.trustProxy,
|
||||
chmodSocket: config.chmodSocket,
|
||||
disableHsts: config.disableHsts,
|
||||
host,
|
||||
|
|
|
@ -37,17 +37,23 @@ class HttpRequestServiceAgent extends http.Agent {
|
|||
|
||||
@bindThis
|
||||
public createConnection(options: http.ClientRequestArgs, callback?: (err: Error | null, stream: stream.Duplex) => void): stream.Duplex {
|
||||
const socket = super.createConnection(options, callback)
|
||||
.on('connect', () => {
|
||||
if (socket instanceof net.Socket && process.env.NODE_ENV === 'production') {
|
||||
const address = socket.remoteAddress;
|
||||
if (address && ipaddr.isValid(address)) {
|
||||
if (this.isPrivateIp(address)) {
|
||||
socket.destroy(new Error(`Blocked address: ${address}`));
|
||||
}
|
||||
const socket = super.createConnection(options, callback);
|
||||
|
||||
if (socket == null) {
|
||||
throw new Error('Failed to create socket');
|
||||
}
|
||||
|
||||
socket.on('connect', () => {
|
||||
if (socket instanceof net.Socket && process.env.NODE_ENV === 'production') {
|
||||
const address = socket.remoteAddress;
|
||||
if (address && ipaddr.isValid(address)) {
|
||||
if (this.isPrivateIp(address)) {
|
||||
socket.destroy(new Error(`Blocked address: ${address}`));
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return socket;
|
||||
}
|
||||
|
||||
|
@ -76,17 +82,23 @@ class HttpsRequestServiceAgent extends https.Agent {
|
|||
|
||||
@bindThis
|
||||
public createConnection(options: http.ClientRequestArgs, callback?: (err: Error | null, stream: stream.Duplex) => void): stream.Duplex {
|
||||
const socket = super.createConnection(options, callback)
|
||||
.on('connect', () => {
|
||||
if (socket instanceof net.Socket && process.env.NODE_ENV === 'production') {
|
||||
const address = socket.remoteAddress;
|
||||
if (address && ipaddr.isValid(address)) {
|
||||
if (this.isPrivateIp(address)) {
|
||||
socket.destroy(new Error(`Blocked address: ${address}`));
|
||||
}
|
||||
const socket = super.createConnection(options, callback);
|
||||
|
||||
if (socket == null) {
|
||||
throw new Error('Failed to create socket');
|
||||
}
|
||||
|
||||
socket.on('connect', () => {
|
||||
if (socket instanceof net.Socket && process.env.NODE_ENV === 'production') {
|
||||
const address = socket.remoteAddress;
|
||||
if (address && ipaddr.isValid(address)) {
|
||||
if (this.isPrivateIp(address)) {
|
||||
socket.destroy(new Error(`Blocked address: ${address}`));
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return socket;
|
||||
}
|
||||
|
||||
|
|
|
@ -13,7 +13,7 @@ import { extractCustomEmojisFromMfm } from '@/misc/extract-custom-emojis-from-mf
|
|||
import { extractHashtags } from '@/misc/extract-hashtags.js';
|
||||
import type { IMentionedRemoteUsers } from '@/models/Note.js';
|
||||
import { MiNote } from '@/models/Note.js';
|
||||
import type { ChannelFollowingsRepository, ChannelsRepository, FollowingsRepository, InstancesRepository, MiFollowing, MiMeta, MutingsRepository, NotesRepository, NoteThreadMutingsRepository, UserListMembershipsRepository, UserProfilesRepository, UsersRepository } from '@/models/_.js';
|
||||
import type { BlockingsRepository, ChannelFollowingsRepository, ChannelsRepository, DriveFilesRepository, FollowingsRepository, InstancesRepository, MiFollowing, MiMeta, MutingsRepository, NotesRepository, NoteThreadMutingsRepository, UserListMembershipsRepository, UserProfilesRepository, UsersRepository } from '@/models/_.js';
|
||||
import type { MiDriveFile } from '@/models/DriveFile.js';
|
||||
import type { MiApp } from '@/models/App.js';
|
||||
import { concat } from '@/misc/prelude/array.js';
|
||||
|
@ -56,6 +56,7 @@ import { trackPromise } from '@/misc/promise-tracker.js';
|
|||
import { IdentifiableError } from '@/misc/identifiable-error.js';
|
||||
import { CollapsedQueue } from '@/misc/collapsed-queue.js';
|
||||
import { CacheService } from '@/core/CacheService.js';
|
||||
import { isQuote, isRenote } from '@/misc/is-renote.js';
|
||||
|
||||
type NotificationType = 'reply' | 'renote' | 'quote' | 'mention';
|
||||
|
||||
|
@ -192,6 +193,12 @@ export class NoteCreateService implements OnApplicationShutdown {
|
|||
@Inject(DI.channelFollowingsRepository)
|
||||
private channelFollowingsRepository: ChannelFollowingsRepository,
|
||||
|
||||
@Inject(DI.blockingsRepository)
|
||||
private blockingsRepository: BlockingsRepository,
|
||||
|
||||
@Inject(DI.driveFilesRepository)
|
||||
private driveFilesRepository: DriveFilesRepository,
|
||||
|
||||
private userEntityService: UserEntityService,
|
||||
private noteEntityService: NoteEntityService,
|
||||
private idService: IdService,
|
||||
|
@ -221,6 +228,167 @@ export class NoteCreateService implements OnApplicationShutdown {
|
|||
this.updateNotesCountQueue = new CollapsedQueue(process.env.NODE_ENV !== 'test' ? 60 * 1000 * 5 : 0, this.collapseNotesCount, this.performUpdateNotesCount);
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async fetchAndCreate(user: {
|
||||
id: MiUser['id'];
|
||||
username: MiUser['username'];
|
||||
host: MiUser['host'];
|
||||
isBot: MiUser['isBot'];
|
||||
isCat: MiUser['isCat'];
|
||||
}, data: {
|
||||
createdAt: Date;
|
||||
replyId: MiNote['id'] | null;
|
||||
renoteId: MiNote['id'] | null;
|
||||
fileIds: MiDriveFile['id'][];
|
||||
text: string | null;
|
||||
cw: string | null;
|
||||
visibility: string;
|
||||
visibleUserIds: MiUser['id'][];
|
||||
channelId: MiChannel['id'] | null;
|
||||
localOnly: boolean;
|
||||
reactionAcceptance: MiNote['reactionAcceptance'];
|
||||
poll: IPoll | null;
|
||||
apMentions?: MinimumUser[] | null;
|
||||
apHashtags?: string[] | null;
|
||||
apEmojis?: string[] | null;
|
||||
}): Promise<MiNote> {
|
||||
const visibleUsers = data.visibleUserIds.length > 0 ? await this.usersRepository.findBy({
|
||||
id: In(data.visibleUserIds),
|
||||
}) : [];
|
||||
|
||||
let files: MiDriveFile[] = [];
|
||||
if (data.fileIds.length > 0) {
|
||||
files = await this.driveFilesRepository.createQueryBuilder('file')
|
||||
.where('file.userId = :userId AND file.id IN (:...fileIds)', {
|
||||
userId: user.id,
|
||||
fileIds: data.fileIds,
|
||||
})
|
||||
.orderBy('array_position(ARRAY[:...fileIds], "id"::text)')
|
||||
.setParameters({ fileIds: data.fileIds })
|
||||
.getMany();
|
||||
|
||||
if (files.length !== data.fileIds.length) {
|
||||
throw new IdentifiableError('801c046c-5bf5-4234-ad2b-e78fc20a2ac7', 'No such file');
|
||||
}
|
||||
}
|
||||
|
||||
let renote: MiNote | null = null;
|
||||
if (data.renoteId != null) {
|
||||
// Fetch renote to note
|
||||
renote = await this.notesRepository.findOne({
|
||||
where: { id: data.renoteId },
|
||||
relations: ['user', 'renote', 'reply'],
|
||||
});
|
||||
|
||||
if (renote == null) {
|
||||
throw new IdentifiableError('53983c56-e163-45a6-942f-4ddc485d4290', 'No such renote target');
|
||||
} else if (isRenote(renote) && !isQuote(renote)) {
|
||||
throw new IdentifiableError('bde24c37-121f-4e7d-980d-cec52f599f02', 'Cannot renote pure renote');
|
||||
}
|
||||
|
||||
// Check blocking
|
||||
if (renote.userId !== user.id) {
|
||||
const blockExist = await this.blockingsRepository.exists({
|
||||
where: {
|
||||
blockerId: renote.userId,
|
||||
blockeeId: user.id,
|
||||
},
|
||||
});
|
||||
if (blockExist) {
|
||||
throw new IdentifiableError('2b4fe776-4414-4a2d-ae39-f3418b8fd4d3', 'You have been blocked by the user');
|
||||
}
|
||||
}
|
||||
|
||||
if (renote.visibility === 'followers' && renote.userId !== user.id) {
|
||||
// 他人のfollowers noteはreject
|
||||
throw new IdentifiableError('90b9d6f0-893a-4fef-b0f1-e9a33989f71a', 'Renote target visibility');
|
||||
} else if (renote.visibility === 'specified') {
|
||||
// specified / direct noteはreject
|
||||
throw new IdentifiableError('48d7a997-da5c-4716-b3c3-92db3f37bf7d', 'Renote target visibility');
|
||||
}
|
||||
|
||||
if (renote.channelId && renote.channelId !== data.channelId) {
|
||||
// チャンネルのノートに対しリノート要求がきたとき、チャンネル外へのリノート可否をチェック
|
||||
// リノートのユースケースのうち、チャンネル内→チャンネル外は少数だと考えられるため、JOINはせず必要な時に都度取得する
|
||||
const renoteChannel = await this.channelsRepository.findOneBy({ id: renote.channelId });
|
||||
if (renoteChannel == null) {
|
||||
// リノートしたいノートが書き込まれているチャンネルが無い
|
||||
throw new IdentifiableError('b060f9a6-8909-4080-9e0b-94d9fa6f6a77', 'No such channel');
|
||||
} else if (!renoteChannel.allowRenoteToExternal) {
|
||||
// リノート作成のリクエストだが、対象チャンネルがリノート禁止だった場合
|
||||
throw new IdentifiableError('7e435f4a-780d-4cfc-a15a-42519bd6fb67', 'Channel does not allow renote to external');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let reply: MiNote | null = null;
|
||||
if (data.replyId != null) {
|
||||
// Fetch reply
|
||||
reply = await this.notesRepository.findOne({
|
||||
where: { id: data.replyId },
|
||||
relations: ['user'],
|
||||
});
|
||||
|
||||
if (reply == null) {
|
||||
throw new IdentifiableError('60142edb-1519-408e-926d-4f108d27bee0', 'No such reply target');
|
||||
} else if (isRenote(reply) && !isQuote(reply)) {
|
||||
throw new IdentifiableError('f089e4e2-c0e7-4f60-8a23-e5a6bf786b36', 'Cannot reply to pure renote');
|
||||
} else if (!await this.noteEntityService.isVisibleForMe(reply, user.id)) {
|
||||
throw new IdentifiableError('11cd37b3-a411-4f77-8633-c580ce6a8dce', 'No such reply target');
|
||||
} else if (reply.visibility === 'specified' && data.visibility !== 'specified') {
|
||||
throw new IdentifiableError('ced780a1-2012-4caf-bc7e-a95a291294cb', 'Cannot reply to specified note with different visibility');
|
||||
}
|
||||
|
||||
// Check blocking
|
||||
if (reply.userId !== user.id) {
|
||||
const blockExist = await this.blockingsRepository.exists({
|
||||
where: {
|
||||
blockerId: reply.userId,
|
||||
blockeeId: user.id,
|
||||
},
|
||||
});
|
||||
if (blockExist) {
|
||||
throw new IdentifiableError('b0df6025-f2e8-44b4-a26a-17ad99104612', 'You have been blocked by the user');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (data.poll) {
|
||||
if (data.poll.expiresAt != null) {
|
||||
if (data.poll.expiresAt.getTime() < Date.now()) {
|
||||
throw new IdentifiableError('0c11c11e-0c8d-48e7-822c-76ccef660068', 'Poll expiration must be future time');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let channel: MiChannel | null = null;
|
||||
if (data.channelId != null) {
|
||||
channel = await this.channelsRepository.findOneBy({ id: data.channelId, isArchived: false });
|
||||
|
||||
if (channel == null) {
|
||||
throw new IdentifiableError('bfa3905b-25f5-4894-b430-da331a490e4b', 'No such channel');
|
||||
}
|
||||
}
|
||||
|
||||
return this.create(user, {
|
||||
createdAt: data.createdAt,
|
||||
files: files,
|
||||
poll: data.poll,
|
||||
text: data.text,
|
||||
reply,
|
||||
renote,
|
||||
cw: data.cw,
|
||||
localOnly: data.localOnly,
|
||||
reactionAcceptance: data.reactionAcceptance,
|
||||
visibility: data.visibility,
|
||||
visibleUsers,
|
||||
channel,
|
||||
apMentions: data.apMentions,
|
||||
apHashtags: data.apHashtags,
|
||||
apEmojis: data.apEmojis,
|
||||
});
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async create(user: {
|
||||
id: MiUser['id'];
|
||||
|
|
|
@ -5,32 +5,18 @@
|
|||
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { In } from 'typeorm';
|
||||
import type { noteVisibilities, noteReactionAcceptances } from '@/types.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import type { MiNoteDraft, NoteDraftsRepository, MiNote, MiDriveFile, MiChannel, UsersRepository, DriveFilesRepository, NotesRepository, BlockingsRepository, ChannelsRepository } from '@/models/_.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { RoleService } from '@/core/RoleService.js';
|
||||
import { IdService } from '@/core/IdService.js';
|
||||
import type { MiLocalUser, MiUser } from '@/models/User.js';
|
||||
import { IPoll } from '@/models/Poll.js';
|
||||
import { IdentifiableError } from '@/misc/identifiable-error.js';
|
||||
import { isRenote, isQuote } from '@/misc/is-renote.js';
|
||||
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
|
||||
import { QueueService } from '@/core/QueueService.js';
|
||||
|
||||
export type NoteDraftOptions = {
|
||||
replyId?: MiNote['id'] | null;
|
||||
renoteId?: MiNote['id'] | null;
|
||||
text?: string | null;
|
||||
cw?: string | null;
|
||||
localOnly?: boolean | null;
|
||||
reactionAcceptance?: typeof noteReactionAcceptances[number];
|
||||
visibility?: typeof noteVisibilities[number];
|
||||
fileIds?: MiDriveFile['id'][];
|
||||
visibleUserIds?: MiUser['id'][];
|
||||
hashtag?: string;
|
||||
channelId?: MiChannel['id'] | null;
|
||||
poll?: (IPoll & { expiredAfter?: number | null }) | null;
|
||||
};
|
||||
export type NoteDraftOptions = Omit<MiNoteDraft, 'id' | 'userId' | 'user' | 'reply' | 'renote' | 'channel'>;
|
||||
|
||||
@Injectable()
|
||||
export class NoteDraftService {
|
||||
|
@ -56,6 +42,7 @@ export class NoteDraftService {
|
|||
private roleService: RoleService,
|
||||
private idService: IdService,
|
||||
private noteEntityService: NoteEntityService,
|
||||
private queueService: QueueService,
|
||||
) {
|
||||
}
|
||||
|
||||
|
@ -72,36 +59,43 @@ export class NoteDraftService {
|
|||
@bindThis
|
||||
public async create(me: MiLocalUser, data: NoteDraftOptions): Promise<MiNoteDraft> {
|
||||
//#region check draft limit
|
||||
const policies = await this.roleService.getUserPolicies(me.id);
|
||||
|
||||
const currentCount = await this.noteDraftsRepository.countBy({
|
||||
userId: me.id,
|
||||
});
|
||||
if (currentCount >= (await this.roleService.getUserPolicies(me.id)).noteDraftLimit) {
|
||||
if (currentCount >= policies.noteDraftLimit) {
|
||||
throw new IdentifiableError('9ee33bbe-fde3-4c71-9b51-e50492c6b9c8', 'Too many drafts');
|
||||
}
|
||||
|
||||
if (data.isActuallyScheduled) {
|
||||
const currentScheduledCount = await this.noteDraftsRepository.countBy({
|
||||
userId: me.id,
|
||||
isActuallyScheduled: true,
|
||||
});
|
||||
if (currentScheduledCount >= policies.scheduledNoteLimit) {
|
||||
throw new IdentifiableError('c3275f19-4558-4c59-83e1-4f684b5fab66', 'Too many scheduled notes');
|
||||
}
|
||||
}
|
||||
//#endregion
|
||||
|
||||
if (data.poll) {
|
||||
if (typeof data.poll.expiresAt === 'number') {
|
||||
if (data.poll.expiresAt < Date.now()) {
|
||||
throw new IdentifiableError('04da457d-b083-4055-9082-955525eda5a5', 'Cannot create expired poll');
|
||||
}
|
||||
} else if (typeof data.poll.expiredAfter === 'number') {
|
||||
data.poll.expiresAt = new Date(Date.now() + data.poll.expiredAfter);
|
||||
}
|
||||
await this.validate(me, data);
|
||||
|
||||
const draft = await this.noteDraftsRepository.insertOne({
|
||||
...data,
|
||||
id: this.idService.gen(),
|
||||
userId: me.id,
|
||||
});
|
||||
|
||||
if (draft.scheduledAt && draft.isActuallyScheduled) {
|
||||
this.schedule(draft);
|
||||
}
|
||||
|
||||
const appliedDraft = await this.checkAndSetDraftNoteOptions(me, this.noteDraftsRepository.create(), data);
|
||||
|
||||
appliedDraft.id = this.idService.gen();
|
||||
appliedDraft.userId = me.id;
|
||||
const draft = this.noteDraftsRepository.save(appliedDraft);
|
||||
|
||||
return draft;
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async update(me: MiLocalUser, draftId: MiNoteDraft['id'], data: NoteDraftOptions): Promise<MiNoteDraft> {
|
||||
public async update(me: MiLocalUser, draftId: MiNoteDraft['id'], data: Partial<NoteDraftOptions>): Promise<MiNoteDraft> {
|
||||
const draft = await this.noteDraftsRepository.findOneBy({
|
||||
id: draftId,
|
||||
userId: me.id,
|
||||
|
@ -111,19 +105,36 @@ export class NoteDraftService {
|
|||
throw new IdentifiableError('49cd6b9d-848e-41ee-b0b9-adaca711a6b1', 'No such note draft');
|
||||
}
|
||||
|
||||
if (data.poll) {
|
||||
if (typeof data.poll.expiresAt === 'number') {
|
||||
if (data.poll.expiresAt < Date.now()) {
|
||||
throw new IdentifiableError('04da457d-b083-4055-9082-955525eda5a5', 'Cannot create expired poll');
|
||||
}
|
||||
} else if (typeof data.poll.expiredAfter === 'number') {
|
||||
data.poll.expiresAt = new Date(Date.now() + data.poll.expiredAfter);
|
||||
//#region check draft limit
|
||||
const policies = await this.roleService.getUserPolicies(me.id);
|
||||
|
||||
if (!draft.isActuallyScheduled && data.isActuallyScheduled) {
|
||||
const currentScheduledCount = await this.noteDraftsRepository.countBy({
|
||||
userId: me.id,
|
||||
isActuallyScheduled: true,
|
||||
});
|
||||
if (currentScheduledCount >= policies.scheduledNoteLimit) {
|
||||
throw new IdentifiableError('bacdf856-5c51-4159-b88a-804fa5103be5', 'Too many scheduled notes');
|
||||
}
|
||||
}
|
||||
//#endregion
|
||||
|
||||
const appliedDraft = await this.checkAndSetDraftNoteOptions(me, draft, data);
|
||||
await this.validate(me, data);
|
||||
|
||||
return await this.noteDraftsRepository.save(appliedDraft);
|
||||
const updatedDraft = await this.noteDraftsRepository.createQueryBuilder().update()
|
||||
.set(data)
|
||||
.where('id = :id', { id: draftId })
|
||||
.returning('*')
|
||||
.execute()
|
||||
.then((response) => response.raw[0]);
|
||||
|
||||
this.clearSchedule(draftId).then(() => {
|
||||
if (updatedDraft.scheduledAt != null && updatedDraft.isActuallyScheduled) {
|
||||
this.schedule(updatedDraft);
|
||||
}
|
||||
});
|
||||
|
||||
return updatedDraft;
|
||||
}
|
||||
|
||||
@bindThis
|
||||
|
@ -138,6 +149,8 @@ export class NoteDraftService {
|
|||
}
|
||||
|
||||
await this.noteDraftsRepository.delete(draft.id);
|
||||
|
||||
this.clearSchedule(draftId);
|
||||
}
|
||||
|
||||
@bindThis
|
||||
|
@ -154,27 +167,20 @@ export class NoteDraftService {
|
|||
return draft;
|
||||
}
|
||||
|
||||
// 関連エンティティを取得し紐づける部分を共通化する
|
||||
@bindThis
|
||||
public async checkAndSetDraftNoteOptions(
|
||||
public async validate(
|
||||
me: MiLocalUser,
|
||||
draft: MiNoteDraft,
|
||||
data: NoteDraftOptions,
|
||||
): Promise<MiNoteDraft> {
|
||||
data.visibility ??= 'public';
|
||||
data.localOnly ??= false;
|
||||
if (data.reactionAcceptance === undefined) data.reactionAcceptance = null;
|
||||
if (data.channelId != null) {
|
||||
data.visibility = 'public';
|
||||
data.visibleUserIds = [];
|
||||
data.localOnly = true;
|
||||
data: Partial<NoteDraftOptions>,
|
||||
): Promise<void> {
|
||||
if (data.pollExpiresAt != null) {
|
||||
if (data.pollExpiresAt.getTime() < Date.now()) {
|
||||
throw new IdentifiableError('04da457d-b083-4055-9082-955525eda5a5', 'Cannot create expired poll');
|
||||
}
|
||||
}
|
||||
|
||||
let appliedDraft = draft;
|
||||
|
||||
//#region visibleUsers
|
||||
let visibleUsers: MiUser[] = [];
|
||||
if (data.visibleUserIds != null) {
|
||||
if (data.visibleUserIds != null && data.visibleUserIds.length > 0) {
|
||||
visibleUsers = await this.usersRepository.findBy({
|
||||
id: In(data.visibleUserIds),
|
||||
});
|
||||
|
@ -184,7 +190,7 @@ export class NoteDraftService {
|
|||
//#region files
|
||||
let files: MiDriveFile[] = [];
|
||||
const fileIds = data.fileIds ?? null;
|
||||
if (fileIds != null) {
|
||||
if (fileIds != null && fileIds.length > 0) {
|
||||
files = await this.driveFilesRepository.createQueryBuilder('file')
|
||||
.where('file.userId = :userId AND file.id IN (:...fileIds)', {
|
||||
userId: me.id,
|
||||
|
@ -288,27 +294,37 @@ export class NoteDraftService {
|
|||
}
|
||||
}
|
||||
//#endregion
|
||||
}
|
||||
|
||||
appliedDraft = {
|
||||
...appliedDraft,
|
||||
visibility: data.visibility,
|
||||
cw: data.cw ?? null,
|
||||
fileIds: fileIds ?? [],
|
||||
replyId: data.replyId ?? null,
|
||||
renoteId: data.renoteId ?? null,
|
||||
channelId: data.channelId ?? null,
|
||||
text: data.text ?? null,
|
||||
hashtag: data.hashtag ?? null,
|
||||
hasPoll: data.poll != null,
|
||||
pollChoices: data.poll ? data.poll.choices : [],
|
||||
pollMultiple: data.poll ? data.poll.multiple : false,
|
||||
pollExpiresAt: data.poll ? data.poll.expiresAt : null,
|
||||
pollExpiredAfter: data.poll ? data.poll.expiredAfter ?? null : null,
|
||||
visibleUserIds: data.visibleUserIds ?? [],
|
||||
localOnly: data.localOnly,
|
||||
reactionAcceptance: data.reactionAcceptance,
|
||||
} satisfies MiNoteDraft;
|
||||
@bindThis
|
||||
public async schedule(draft: MiNoteDraft): Promise<void> {
|
||||
if (!draft.isActuallyScheduled) return;
|
||||
if (draft.scheduledAt == null) return;
|
||||
if (draft.scheduledAt.getTime() <= Date.now()) return;
|
||||
|
||||
return appliedDraft;
|
||||
const delay = draft.scheduledAt.getTime() - Date.now();
|
||||
this.queueService.postScheduledNoteQueue.add(draft.id, {
|
||||
noteDraftId: draft.id,
|
||||
}, {
|
||||
delay,
|
||||
removeOnComplete: {
|
||||
age: 3600 * 24 * 7, // keep up to 7 days
|
||||
count: 30,
|
||||
},
|
||||
removeOnFail: {
|
||||
age: 3600 * 24 * 7, // keep up to 7 days
|
||||
count: 100,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async clearSchedule(draftId: MiNoteDraft['id']): Promise<void> {
|
||||
const jobs = await this.queueService.postScheduledNoteQueue.getJobs(['delayed', 'waiting', 'active']);
|
||||
for (const job of jobs) {
|
||||
if (job.data.noteDraftId === draftId) {
|
||||
await job.remove();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -16,11 +16,13 @@ import {
|
|||
RelationshipJobData,
|
||||
UserWebhookDeliverJobData,
|
||||
SystemWebhookDeliverJobData,
|
||||
PostScheduledNoteJobData,
|
||||
} from '../queue/types.js';
|
||||
import type { Provider } from '@nestjs/common';
|
||||
|
||||
export type SystemQueue = Bull.Queue<Record<string, unknown>>;
|
||||
export type EndedPollNotificationQueue = Bull.Queue<EndedPollNotificationJobData>;
|
||||
export type PostScheduledNoteQueue = Bull.Queue<PostScheduledNoteJobData>;
|
||||
export type DeliverQueue = Bull.Queue<DeliverJobData>;
|
||||
export type InboxQueue = Bull.Queue<InboxJobData>;
|
||||
export type DbQueue = Bull.Queue;
|
||||
|
@ -41,6 +43,12 @@ const $endedPollNotification: Provider = {
|
|||
inject: [DI.config],
|
||||
};
|
||||
|
||||
const $postScheduledNote: Provider = {
|
||||
provide: 'queue:postScheduledNote',
|
||||
useFactory: (config: Config) => new Bull.Queue(QUEUE.POST_SCHEDULED_NOTE, baseQueueOptions(config, QUEUE.POST_SCHEDULED_NOTE)),
|
||||
inject: [DI.config],
|
||||
};
|
||||
|
||||
const $deliver: Provider = {
|
||||
provide: 'queue:deliver',
|
||||
useFactory: (config: Config) => new Bull.Queue(QUEUE.DELIVER, baseQueueOptions(config, QUEUE.DELIVER)),
|
||||
|
@ -89,6 +97,7 @@ const $systemWebhookDeliver: Provider = {
|
|||
providers: [
|
||||
$system,
|
||||
$endedPollNotification,
|
||||
$postScheduledNote,
|
||||
$deliver,
|
||||
$inbox,
|
||||
$db,
|
||||
|
@ -100,6 +109,7 @@ const $systemWebhookDeliver: Provider = {
|
|||
exports: [
|
||||
$system,
|
||||
$endedPollNotification,
|
||||
$postScheduledNote,
|
||||
$deliver,
|
||||
$inbox,
|
||||
$db,
|
||||
|
@ -113,6 +123,7 @@ export class QueueModule implements OnApplicationShutdown {
|
|||
constructor(
|
||||
@Inject('queue:system') public systemQueue: SystemQueue,
|
||||
@Inject('queue:endedPollNotification') public endedPollNotificationQueue: EndedPollNotificationQueue,
|
||||
@Inject('queue:postScheduledNote') public postScheduledNoteQueue: PostScheduledNoteQueue,
|
||||
@Inject('queue:deliver') public deliverQueue: DeliverQueue,
|
||||
@Inject('queue:inbox') public inboxQueue: InboxQueue,
|
||||
@Inject('queue:db') public dbQueue: DbQueue,
|
||||
|
@ -129,6 +140,7 @@ export class QueueModule implements OnApplicationShutdown {
|
|||
await Promise.all([
|
||||
this.systemQueue.close(),
|
||||
this.endedPollNotificationQueue.close(),
|
||||
this.postScheduledNoteQueue.close(),
|
||||
this.deliverQueue.close(),
|
||||
this.inboxQueue.close(),
|
||||
this.dbQueue.close(),
|
||||
|
|
|
@ -31,6 +31,7 @@ import type {
|
|||
DbQueue,
|
||||
DeliverQueue,
|
||||
EndedPollNotificationQueue,
|
||||
PostScheduledNoteQueue,
|
||||
InboxQueue,
|
||||
ObjectStorageQueue,
|
||||
RelationshipQueue,
|
||||
|
@ -44,6 +45,7 @@ import type * as Bull from 'bullmq';
|
|||
export const QUEUE_TYPES = [
|
||||
'system',
|
||||
'endedPollNotification',
|
||||
'postScheduledNote',
|
||||
'deliver',
|
||||
'inbox',
|
||||
'db',
|
||||
|
@ -92,6 +94,7 @@ export class QueueService {
|
|||
|
||||
@Inject('queue:system') public systemQueue: SystemQueue,
|
||||
@Inject('queue:endedPollNotification') public endedPollNotificationQueue: EndedPollNotificationQueue,
|
||||
@Inject('queue:postScheduledNote') public postScheduledNoteQueue: PostScheduledNoteQueue,
|
||||
@Inject('queue:deliver') public deliverQueue: DeliverQueue,
|
||||
@Inject('queue:inbox') public inboxQueue: InboxQueue,
|
||||
@Inject('queue:db') public dbQueue: DbQueue,
|
||||
|
@ -717,6 +720,7 @@ export class QueueService {
|
|||
switch (type) {
|
||||
case 'system': return this.systemQueue;
|
||||
case 'endedPollNotification': return this.endedPollNotificationQueue;
|
||||
case 'postScheduledNote': return this.postScheduledNoteQueue;
|
||||
case 'deliver': return this.deliverQueue;
|
||||
case 'inbox': return this.inboxQueue;
|
||||
case 'db': return this.dbQueue;
|
||||
|
|
|
@ -69,6 +69,7 @@ export type RolePolicies = {
|
|||
chatAvailability: 'available' | 'readonly' | 'unavailable';
|
||||
uploadableFileTypes: string[];
|
||||
noteDraftLimit: number;
|
||||
scheduledNoteLimit: number;
|
||||
watermarkAvailable: boolean;
|
||||
};
|
||||
|
||||
|
@ -101,20 +102,22 @@ export const DEFAULT_POLICIES: RolePolicies = {
|
|||
userEachUserListsLimit: 50,
|
||||
rateLimitFactor: 1,
|
||||
avatarDecorationLimit: 1,
|
||||
canImportAntennas: true,
|
||||
canImportBlocking: true,
|
||||
canImportFollowing: true,
|
||||
canImportMuting: true,
|
||||
canImportUserLists: true,
|
||||
canImportAntennas: false,
|
||||
canImportBlocking: false,
|
||||
canImportFollowing: false,
|
||||
canImportMuting: false,
|
||||
canImportUserLists: false,
|
||||
chatAvailability: 'available',
|
||||
uploadableFileTypes: [
|
||||
'text/plain',
|
||||
'text/csv',
|
||||
'application/json',
|
||||
'image/*',
|
||||
'video/*',
|
||||
'audio/*',
|
||||
],
|
||||
noteDraftLimit: 10,
|
||||
scheduledNoteLimit: 1,
|
||||
watermarkAvailable: true,
|
||||
};
|
||||
|
||||
|
@ -439,6 +442,7 @@ export class RoleService implements OnApplicationShutdown, OnModuleInit {
|
|||
return [...set];
|
||||
}),
|
||||
noteDraftLimit: calc('noteDraftLimit', vs => Math.max(...vs)),
|
||||
scheduledNoteLimit: calc('scheduledNoteLimit', vs => Math.max(...vs)),
|
||||
watermarkAvailable: calc('watermarkAvailable', vs => vs.some(v => v === true)),
|
||||
};
|
||||
}
|
||||
|
|
|
@ -117,6 +117,7 @@ export class MetaEntityService {
|
|||
ratio: ad.ratio,
|
||||
imageUrl: ad.imageUrl,
|
||||
dayOfWeek: ad.dayOfWeek,
|
||||
isSensitive: ad.isSensitive ? true : undefined,
|
||||
})),
|
||||
notesPerOneAd: instance.notesPerOneAd,
|
||||
enableEmail: instance.enableEmail,
|
||||
|
|
|
@ -105,6 +105,8 @@ export class NoteDraftEntityService implements OnModuleInit {
|
|||
const packed: Packed<'NoteDraft'> = await awaitAll({
|
||||
id: noteDraft.id,
|
||||
createdAt: this.idService.parse(noteDraft.id).date.toISOString(),
|
||||
scheduledAt: noteDraft.scheduledAt?.getTime() ?? null,
|
||||
isActuallyScheduled: noteDraft.isActuallyScheduled,
|
||||
userId: noteDraft.userId,
|
||||
user: packedUsers?.get(noteDraft.userId) ?? this.userEntityService.pack(noteDraft.user ?? noteDraft.userId, me),
|
||||
text: text,
|
||||
|
@ -112,13 +114,13 @@ export class NoteDraftEntityService implements OnModuleInit {
|
|||
visibility: noteDraft.visibility,
|
||||
localOnly: noteDraft.localOnly,
|
||||
reactionAcceptance: noteDraft.reactionAcceptance,
|
||||
visibleUserIds: noteDraft.visibility === 'specified' ? noteDraft.visibleUserIds : undefined,
|
||||
hashtag: noteDraft.hashtag ?? undefined,
|
||||
visibleUserIds: noteDraft.visibleUserIds,
|
||||
hashtag: noteDraft.hashtag,
|
||||
fileIds: noteDraft.fileIds,
|
||||
files: packedFiles != null ? this.packAttachedFiles(noteDraft.fileIds, packedFiles) : this.driveFileEntityService.packManyByIds(noteDraft.fileIds),
|
||||
replyId: noteDraft.replyId,
|
||||
renoteId: noteDraft.renoteId,
|
||||
channelId: noteDraft.channelId ?? undefined,
|
||||
channelId: noteDraft.channelId,
|
||||
channel: channel ? {
|
||||
id: channel.id,
|
||||
name: channel.name,
|
||||
|
@ -127,6 +129,12 @@ export class NoteDraftEntityService implements OnModuleInit {
|
|||
allowRenoteToExternal: channel.allowRenoteToExternal,
|
||||
userId: channel.userId,
|
||||
} : undefined,
|
||||
poll: noteDraft.hasPoll ? {
|
||||
choices: noteDraft.pollChoices,
|
||||
multiple: noteDraft.pollMultiple,
|
||||
expiresAt: noteDraft.pollExpiresAt?.toISOString(),
|
||||
expiredAfter: noteDraft.pollExpiredAfter,
|
||||
} : null,
|
||||
|
||||
...(opts.detail ? {
|
||||
reply: noteDraft.replyId ? nullIfEntityNotFound(this.noteEntityService.pack(noteDraft.replyId, me, {
|
||||
|
@ -138,13 +146,6 @@ export class NoteDraftEntityService implements OnModuleInit {
|
|||
detail: true,
|
||||
skipHide: opts.skipHide,
|
||||
})) : undefined,
|
||||
|
||||
poll: noteDraft.hasPoll ? {
|
||||
choices: noteDraft.pollChoices,
|
||||
multiple: noteDraft.pollMultiple,
|
||||
expiresAt: noteDraft.pollExpiresAt?.toISOString(),
|
||||
expiredAfter: noteDraft.pollExpiredAfter,
|
||||
} : undefined,
|
||||
} : {} ),
|
||||
});
|
||||
|
||||
|
|
|
@ -21,7 +21,18 @@ import type { OnModuleInit } from '@nestjs/common';
|
|||
import type { UserEntityService } from './UserEntityService.js';
|
||||
import type { NoteEntityService } from './NoteEntityService.js';
|
||||
|
||||
const NOTE_REQUIRED_NOTIFICATION_TYPES = new Set(['note', 'mention', 'reply', 'renote', 'renote:grouped', 'quote', 'reaction', 'reaction:grouped', 'pollEnded'] as (typeof groupedNotificationTypes[number])[]);
|
||||
const NOTE_REQUIRED_NOTIFICATION_TYPES = new Set([
|
||||
'note',
|
||||
'mention',
|
||||
'reply',
|
||||
'renote',
|
||||
'renote:grouped',
|
||||
'quote',
|
||||
'reaction',
|
||||
'reaction:grouped',
|
||||
'pollEnded',
|
||||
'scheduledNotePosted',
|
||||
] as (typeof groupedNotificationTypes[number])[]);
|
||||
|
||||
@Injectable()
|
||||
export class NotificationEntityService implements OnModuleInit {
|
||||
|
|
|
@ -54,10 +54,17 @@ export class MiAd {
|
|||
length: 8192, nullable: false,
|
||||
})
|
||||
public memo: string;
|
||||
|
||||
@Column('integer', {
|
||||
default: 0, nullable: false,
|
||||
})
|
||||
public dayOfWeek: number;
|
||||
|
||||
@Column('boolean', {
|
||||
default: false,
|
||||
})
|
||||
public isSensitive: boolean;
|
||||
|
||||
constructor(data: Partial<MiAd>) {
|
||||
if (data == null) return;
|
||||
|
||||
|
|
|
@ -126,7 +126,7 @@ export class MiNoteDraft {
|
|||
@JoinColumn()
|
||||
public channel: MiChannel | null;
|
||||
|
||||
// 以下、Pollについて追加
|
||||
//#region 以下、Pollについて追加
|
||||
|
||||
@Column('boolean', {
|
||||
default: false,
|
||||
|
@ -151,13 +151,15 @@ export class MiNoteDraft {
|
|||
})
|
||||
public pollExpiredAfter: number | null;
|
||||
|
||||
// ここまで追加
|
||||
//#endregion
|
||||
|
||||
constructor(data: Partial<MiNoteDraft>) {
|
||||
if (data == null) return;
|
||||
@Column('timestamp with time zone', {
|
||||
nullable: true,
|
||||
})
|
||||
public scheduledAt: Date | null;
|
||||
|
||||
for (const [k, v] of Object.entries(data)) {
|
||||
(this as any)[k] = v;
|
||||
}
|
||||
}
|
||||
@Column('boolean', {
|
||||
default: false,
|
||||
})
|
||||
public isActuallyScheduled: boolean;
|
||||
}
|
||||
|
|
|
@ -9,6 +9,7 @@ import { MiNote } from './Note.js';
|
|||
import { MiAccessToken } from './AccessToken.js';
|
||||
import { MiRole } from './Role.js';
|
||||
import { MiDriveFile } from './DriveFile.js';
|
||||
import { MiNoteDraft } from './NoteDraft.js';
|
||||
|
||||
// misskey-js の notificationTypes と同期すべし
|
||||
export type MiNotification = {
|
||||
|
@ -60,6 +61,16 @@ export type MiNotification = {
|
|||
createdAt: string;
|
||||
notifierId: MiUser['id'];
|
||||
noteId: MiNote['id'];
|
||||
} | {
|
||||
type: 'scheduledNotePosted';
|
||||
id: string;
|
||||
createdAt: string;
|
||||
noteId: MiNote['id'];
|
||||
} | {
|
||||
type: 'scheduledNotePostFailed';
|
||||
id: string;
|
||||
createdAt: string;
|
||||
noteDraftId: MiNoteDraft['id'];
|
||||
} | {
|
||||
type: 'receiveFollowRequest';
|
||||
id: string;
|
||||
|
|
|
@ -60,5 +60,10 @@ export const packedAdSchema = {
|
|||
optional: false,
|
||||
nullable: false,
|
||||
},
|
||||
isSensitive: {
|
||||
type: 'boolean',
|
||||
optional: false,
|
||||
nullable: false,
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
|
|
|
@ -195,6 +195,10 @@ export const packedMetaLiteSchema = {
|
|||
type: 'integer',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
isSensitive: {
|
||||
type: 'boolean',
|
||||
optional: true, nullable: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
|
@ -23,7 +23,7 @@ export const packedNoteDraftSchema = {
|
|||
},
|
||||
cw: {
|
||||
type: 'string',
|
||||
optional: true, nullable: true,
|
||||
optional: false, nullable: true,
|
||||
},
|
||||
userId: {
|
||||
type: 'string',
|
||||
|
@ -37,27 +37,23 @@ export const packedNoteDraftSchema = {
|
|||
},
|
||||
replyId: {
|
||||
type: 'string',
|
||||
optional: true, nullable: true,
|
||||
optional: false, nullable: true,
|
||||
format: 'id',
|
||||
example: 'xxxxxxxxxx',
|
||||
},
|
||||
renoteId: {
|
||||
type: 'string',
|
||||
optional: true, nullable: true,
|
||||
optional: false, nullable: true,
|
||||
format: 'id',
|
||||
example: 'xxxxxxxxxx',
|
||||
},
|
||||
reply: {
|
||||
type: 'object',
|
||||
optional: true, nullable: true,
|
||||
ref: 'Note',
|
||||
description: 'The reply target note contents if exists. If the reply target has been deleted since the draft was created, this will be null while replyId is not null.',
|
||||
},
|
||||
renote: {
|
||||
type: 'object',
|
||||
optional: true, nullable: true,
|
||||
ref: 'Note',
|
||||
description: 'The renote target note contents if exists. If the renote target has been deleted since the draft was created, this will be null while renoteId is not null.',
|
||||
},
|
||||
visibility: {
|
||||
type: 'string',
|
||||
|
@ -66,7 +62,7 @@ export const packedNoteDraftSchema = {
|
|||
},
|
||||
visibleUserIds: {
|
||||
type: 'array',
|
||||
optional: true, nullable: false,
|
||||
optional: false, nullable: false,
|
||||
items: {
|
||||
type: 'string',
|
||||
optional: false, nullable: false,
|
||||
|
@ -75,7 +71,7 @@ export const packedNoteDraftSchema = {
|
|||
},
|
||||
fileIds: {
|
||||
type: 'array',
|
||||
optional: true, nullable: false,
|
||||
optional: false, nullable: false,
|
||||
items: {
|
||||
type: 'string',
|
||||
optional: false, nullable: false,
|
||||
|
@ -93,11 +89,11 @@ export const packedNoteDraftSchema = {
|
|||
},
|
||||
hashtag: {
|
||||
type: 'string',
|
||||
optional: true, nullable: false,
|
||||
optional: false, nullable: true,
|
||||
},
|
||||
poll: {
|
||||
type: 'object',
|
||||
optional: true, nullable: true,
|
||||
optional: false, nullable: true,
|
||||
properties: {
|
||||
expiresAt: {
|
||||
type: 'string',
|
||||
|
@ -124,9 +120,8 @@ export const packedNoteDraftSchema = {
|
|||
},
|
||||
channelId: {
|
||||
type: 'string',
|
||||
optional: true, nullable: true,
|
||||
optional: false, nullable: true,
|
||||
format: 'id',
|
||||
example: 'xxxxxxxxxx',
|
||||
},
|
||||
channel: {
|
||||
type: 'object',
|
||||
|
@ -160,12 +155,20 @@ export const packedNoteDraftSchema = {
|
|||
},
|
||||
localOnly: {
|
||||
type: 'boolean',
|
||||
optional: true, nullable: false,
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
reactionAcceptance: {
|
||||
type: 'string',
|
||||
optional: false, nullable: true,
|
||||
enum: ['likeOnly', 'likeOnlyForRemote', 'nonSensitiveOnly', 'nonSensitiveOnlyForLocalLikeOnlyForRemote', null],
|
||||
},
|
||||
scheduledAt: {
|
||||
type: 'number',
|
||||
optional: false, nullable: true,
|
||||
},
|
||||
isActuallyScheduled: {
|
||||
type: 'boolean',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
|
|
|
@ -207,6 +207,36 @@ export const packedNotificationSchema = {
|
|||
optional: false, nullable: false,
|
||||
},
|
||||
},
|
||||
}, {
|
||||
type: 'object',
|
||||
properties: {
|
||||
...baseSchema.properties,
|
||||
type: {
|
||||
type: 'string',
|
||||
optional: false, nullable: false,
|
||||
enum: ['scheduledNotePosted'],
|
||||
},
|
||||
note: {
|
||||
type: 'object',
|
||||
ref: 'Note',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
},
|
||||
}, {
|
||||
type: 'object',
|
||||
properties: {
|
||||
...baseSchema.properties,
|
||||
type: {
|
||||
type: 'string',
|
||||
optional: false, nullable: false,
|
||||
enum: ['scheduledNotePostFailed'],
|
||||
},
|
||||
noteDraft: {
|
||||
type: 'object',
|
||||
ref: 'NoteDraft',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
},
|
||||
}, {
|
||||
type: 'object',
|
||||
properties: {
|
||||
|
|
|
@ -317,6 +317,10 @@ export const packedRolePoliciesSchema = {
|
|||
type: 'integer',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
scheduledNoteLimit: {
|
||||
type: 'integer',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
watermarkAvailable: {
|
||||
type: 'boolean',
|
||||
optional: false, nullable: false,
|
||||
|
|
|
@ -609,6 +609,8 @@ export const packedMeDetailedOnlySchema = {
|
|||
quote: { optional: true, ...notificationRecieveConfig },
|
||||
reaction: { optional: true, ...notificationRecieveConfig },
|
||||
pollEnded: { optional: true, ...notificationRecieveConfig },
|
||||
scheduledNotePosted: { optional: true, ...notificationRecieveConfig },
|
||||
scheduledNotePostFailed: { optional: true, ...notificationRecieveConfig },
|
||||
receiveFollowRequest: { optional: true, ...notificationRecieveConfig },
|
||||
followRequestAccepted: { optional: true, ...notificationRecieveConfig },
|
||||
roleAssigned: { optional: true, ...notificationRecieveConfig },
|
||||
|
|
|
@ -10,6 +10,7 @@ import { QueueLoggerService } from './QueueLoggerService.js';
|
|||
import { QueueProcessorService } from './QueueProcessorService.js';
|
||||
import { DeliverProcessorService } from './processors/DeliverProcessorService.js';
|
||||
import { EndedPollNotificationProcessorService } from './processors/EndedPollNotificationProcessorService.js';
|
||||
import { PostScheduledNoteProcessorService } from './processors/PostScheduledNoteProcessorService.js';
|
||||
import { InboxProcessorService } from './processors/InboxProcessorService.js';
|
||||
import { UserWebhookDeliverProcessorService } from './processors/UserWebhookDeliverProcessorService.js';
|
||||
import { SystemWebhookDeliverProcessorService } from './processors/SystemWebhookDeliverProcessorService.js';
|
||||
|
@ -79,6 +80,7 @@ import { RelationshipProcessorService } from './processors/RelationshipProcessor
|
|||
UserWebhookDeliverProcessorService,
|
||||
SystemWebhookDeliverProcessorService,
|
||||
EndedPollNotificationProcessorService,
|
||||
PostScheduledNoteProcessorService,
|
||||
DeliverProcessorService,
|
||||
InboxProcessorService,
|
||||
AggregateRetentionProcessorService,
|
||||
|
|
|
@ -14,6 +14,7 @@ import { CheckModeratorsActivityProcessorService } from '@/queue/processors/Chec
|
|||
import { UserWebhookDeliverProcessorService } from './processors/UserWebhookDeliverProcessorService.js';
|
||||
import { SystemWebhookDeliverProcessorService } from './processors/SystemWebhookDeliverProcessorService.js';
|
||||
import { EndedPollNotificationProcessorService } from './processors/EndedPollNotificationProcessorService.js';
|
||||
import { PostScheduledNoteProcessorService } from './processors/PostScheduledNoteProcessorService.js';
|
||||
import { DeliverProcessorService } from './processors/DeliverProcessorService.js';
|
||||
import { InboxProcessorService } from './processors/InboxProcessorService.js';
|
||||
import { DeleteDriveFilesProcessorService } from './processors/DeleteDriveFilesProcessorService.js';
|
||||
|
@ -85,6 +86,7 @@ export class QueueProcessorService implements OnApplicationShutdown {
|
|||
private relationshipQueueWorker: Bull.Worker;
|
||||
private objectStorageQueueWorker: Bull.Worker;
|
||||
private endedPollNotificationQueueWorker: Bull.Worker;
|
||||
private postScheduledNoteQueueWorker: Bull.Worker;
|
||||
|
||||
constructor(
|
||||
@Inject(DI.config)
|
||||
|
@ -94,6 +96,7 @@ export class QueueProcessorService implements OnApplicationShutdown {
|
|||
private userWebhookDeliverProcessorService: UserWebhookDeliverProcessorService,
|
||||
private systemWebhookDeliverProcessorService: SystemWebhookDeliverProcessorService,
|
||||
private endedPollNotificationProcessorService: EndedPollNotificationProcessorService,
|
||||
private postScheduledNoteProcessorService: PostScheduledNoteProcessorService,
|
||||
private deliverProcessorService: DeliverProcessorService,
|
||||
private inboxProcessorService: InboxProcessorService,
|
||||
private deleteDriveFilesProcessorService: DeleteDriveFilesProcessorService,
|
||||
|
@ -520,6 +523,21 @@ export class QueueProcessorService implements OnApplicationShutdown {
|
|||
});
|
||||
}
|
||||
//#endregion
|
||||
|
||||
//#region post scheduled note
|
||||
{
|
||||
this.postScheduledNoteQueueWorker = new Bull.Worker(QUEUE.POST_SCHEDULED_NOTE, async (job) => {
|
||||
if (this.config.sentryForBackend) {
|
||||
return Sentry.startSpan({ name: 'Queue: PostScheduledNote' }, () => this.postScheduledNoteProcessorService.process(job));
|
||||
} else {
|
||||
return this.postScheduledNoteProcessorService.process(job);
|
||||
}
|
||||
}, {
|
||||
...baseWorkerOptions(this.config, QUEUE.POST_SCHEDULED_NOTE),
|
||||
autorun: false,
|
||||
});
|
||||
}
|
||||
//#endregion
|
||||
}
|
||||
|
||||
@bindThis
|
||||
|
@ -534,6 +552,7 @@ export class QueueProcessorService implements OnApplicationShutdown {
|
|||
this.relationshipQueueWorker.run(),
|
||||
this.objectStorageQueueWorker.run(),
|
||||
this.endedPollNotificationQueueWorker.run(),
|
||||
this.postScheduledNoteQueueWorker.run(),
|
||||
]);
|
||||
}
|
||||
|
||||
|
@ -549,6 +568,7 @@ export class QueueProcessorService implements OnApplicationShutdown {
|
|||
this.relationshipQueueWorker.close(),
|
||||
this.objectStorageQueueWorker.close(),
|
||||
this.endedPollNotificationQueueWorker.close(),
|
||||
this.postScheduledNoteQueueWorker.close(),
|
||||
]);
|
||||
}
|
||||
|
||||
|
|
|
@ -12,6 +12,7 @@ export const QUEUE = {
|
|||
INBOX: 'inbox',
|
||||
SYSTEM: 'system',
|
||||
ENDED_POLL_NOTIFICATION: 'endedPollNotification',
|
||||
POST_SCHEDULED_NOTE: 'postScheduledNote',
|
||||
DB: 'db',
|
||||
RELATIONSHIP: 'relationship',
|
||||
OBJECT_STORAGE: 'objectStorage',
|
||||
|
|
|
@ -0,0 +1,72 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import type { NoteDraftsRepository } from '@/models/_.js';
|
||||
import type Logger from '@/logger.js';
|
||||
import { NotificationService } from '@/core/NotificationService.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { NoteCreateService } from '@/core/NoteCreateService.js';
|
||||
import { QueueLoggerService } from '../QueueLoggerService.js';
|
||||
import type * as Bull from 'bullmq';
|
||||
import type { PostScheduledNoteJobData } from '../types.js';
|
||||
|
||||
@Injectable()
|
||||
export class PostScheduledNoteProcessorService {
|
||||
private logger: Logger;
|
||||
|
||||
constructor(
|
||||
@Inject(DI.noteDraftsRepository)
|
||||
private noteDraftsRepository: NoteDraftsRepository,
|
||||
|
||||
private noteCreateService: NoteCreateService,
|
||||
private notificationService: NotificationService,
|
||||
private queueLoggerService: QueueLoggerService,
|
||||
) {
|
||||
this.logger = this.queueLoggerService.logger.createSubLogger('post-scheduled-note');
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async process(job: Bull.Job<PostScheduledNoteJobData>): Promise<void> {
|
||||
const draft = await this.noteDraftsRepository.findOne({ where: { id: job.data.noteDraftId }, relations: ['user'] });
|
||||
if (draft == null || draft.user == null || draft.scheduledAt == null || !draft.isActuallyScheduled) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const note = await this.noteCreateService.fetchAndCreate(draft.user, {
|
||||
createdAt: new Date(),
|
||||
fileIds: draft.fileIds,
|
||||
poll: draft.hasPoll ? {
|
||||
choices: draft.pollChoices,
|
||||
multiple: draft.pollMultiple,
|
||||
expiresAt: draft.pollExpiredAfter ? new Date(Date.now() + draft.pollExpiredAfter) : draft.pollExpiresAt ? new Date(draft.pollExpiresAt) : null,
|
||||
} : null,
|
||||
text: draft.text ?? null,
|
||||
replyId: draft.replyId,
|
||||
renoteId: draft.renoteId,
|
||||
cw: draft.cw,
|
||||
localOnly: draft.localOnly,
|
||||
reactionAcceptance: draft.reactionAcceptance,
|
||||
visibility: draft.visibility,
|
||||
visibleUserIds: draft.visibleUserIds,
|
||||
channelId: draft.channelId,
|
||||
});
|
||||
|
||||
// await不要
|
||||
this.noteDraftsRepository.remove(draft);
|
||||
|
||||
// await不要
|
||||
this.notificationService.createNotification(draft.userId, 'scheduledNotePosted', {
|
||||
noteId: note.id,
|
||||
});
|
||||
} catch (err) {
|
||||
this.notificationService.createNotification(draft.userId, 'scheduledNotePostFailed', {
|
||||
noteDraftId: draft.id,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
|
@ -109,6 +109,10 @@ export type EndedPollNotificationJobData = {
|
|||
noteId: MiNote['id'];
|
||||
};
|
||||
|
||||
export type PostScheduledNoteJobData = {
|
||||
noteDraftId: string;
|
||||
};
|
||||
|
||||
export type SystemWebhookDeliverJobData<T extends SystemWebhookEventType = SystemWebhookEventType> = {
|
||||
type: T;
|
||||
content: SystemWebhookPayload<T>;
|
||||
|
|
|
@ -75,7 +75,7 @@ export class ServerService implements OnApplicationShutdown {
|
|||
@bindThis
|
||||
public async launch(): Promise<void> {
|
||||
const fastify = Fastify({
|
||||
trustProxy: true,
|
||||
trustProxy: this.config.trustProxy ?? true,
|
||||
logger: false,
|
||||
});
|
||||
this.#fastify = fastify;
|
||||
|
|
|
@ -34,13 +34,22 @@ export const meta = {
|
|||
res: {
|
||||
type: 'object',
|
||||
optional: false, nullable: false,
|
||||
ref: 'MeDetailed',
|
||||
properties: {
|
||||
token: {
|
||||
type: 'string',
|
||||
optional: false, nullable: false,
|
||||
allOf: [
|
||||
{
|
||||
type: 'object',
|
||||
ref: 'MeDetailed',
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'object',
|
||||
optional: false, nullable: false,
|
||||
properties: {
|
||||
token: {
|
||||
type: 'string',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
},
|
||||
}
|
||||
],
|
||||
},
|
||||
} as const;
|
||||
|
||||
|
|
|
@ -36,6 +36,7 @@ export const paramDef = {
|
|||
startsAt: { type: 'integer' },
|
||||
imageUrl: { type: 'string', minLength: 1 },
|
||||
dayOfWeek: { type: 'integer' },
|
||||
isSensitive: { type: 'boolean' },
|
||||
},
|
||||
required: ['url', 'memo', 'place', 'priority', 'ratio', 'expiresAt', 'startsAt', 'imageUrl', 'dayOfWeek'],
|
||||
} as const;
|
||||
|
@ -55,6 +56,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
expiresAt: new Date(ps.expiresAt),
|
||||
startsAt: new Date(ps.startsAt),
|
||||
dayOfWeek: ps.dayOfWeek,
|
||||
isSensitive: ps.isSensitive,
|
||||
url: ps.url,
|
||||
imageUrl: ps.imageUrl,
|
||||
priority: ps.priority,
|
||||
|
@ -73,6 +75,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
expiresAt: ad.expiresAt.toISOString(),
|
||||
startsAt: ad.startsAt.toISOString(),
|
||||
dayOfWeek: ad.dayOfWeek,
|
||||
isSensitive: ad.isSensitive,
|
||||
url: ad.url,
|
||||
imageUrl: ad.imageUrl,
|
||||
priority: ad.priority,
|
||||
|
|
|
@ -63,6 +63,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
expiresAt: ad.expiresAt.toISOString(),
|
||||
startsAt: ad.startsAt.toISOString(),
|
||||
dayOfWeek: ad.dayOfWeek,
|
||||
isSensitive: ad.isSensitive,
|
||||
url: ad.url,
|
||||
imageUrl: ad.imageUrl,
|
||||
memo: ad.memo,
|
||||
|
|
|
@ -39,6 +39,7 @@ export const paramDef = {
|
|||
expiresAt: { type: 'integer' },
|
||||
startsAt: { type: 'integer' },
|
||||
dayOfWeek: { type: 'integer' },
|
||||
isSensitive: { type: 'boolean' },
|
||||
},
|
||||
required: ['id'],
|
||||
} as const;
|
||||
|
@ -66,6 +67,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
expiresAt: ps.expiresAt ? new Date(ps.expiresAt) : undefined,
|
||||
startsAt: ps.startsAt ? new Date(ps.startsAt) : undefined,
|
||||
dayOfWeek: ps.dayOfWeek,
|
||||
isSensitive: ps.isSensitive,
|
||||
});
|
||||
|
||||
const updatedAd = await this.adsRepository.findOneByOrFail({ id: ad.id });
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||
import type { DbQueue, DeliverQueue, EndedPollNotificationQueue, InboxQueue, ObjectStorageQueue, SystemQueue, UserWebhookDeliverQueue, SystemWebhookDeliverQueue } from '@/core/QueueModule.js';
|
||||
import type { DbQueue, DeliverQueue, EndedPollNotificationQueue, PostScheduledNoteQueue, InboxQueue, ObjectStorageQueue, SystemQueue, UserWebhookDeliverQueue, SystemWebhookDeliverQueue } from '@/core/QueueModule.js';
|
||||
|
||||
export const meta = {
|
||||
tags: ['admin'],
|
||||
|
@ -49,6 +49,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
constructor(
|
||||
@Inject('queue:system') public systemQueue: SystemQueue,
|
||||
@Inject('queue:endedPollNotification') public endedPollNotificationQueue: EndedPollNotificationQueue,
|
||||
@Inject('queue:postScheduledNote') public postScheduledNoteQueue: PostScheduledNoteQueue,
|
||||
@Inject('queue:deliver') public deliverQueue: DeliverQueue,
|
||||
@Inject('queue:inbox') public inboxQueue: InboxQueue,
|
||||
@Inject('queue:db') public dbQueue: DbQueue,
|
||||
|
|
|
@ -103,6 +103,8 @@ export const meta = {
|
|||
quote: { optional: true, ...notificationRecieveConfig },
|
||||
reaction: { optional: true, ...notificationRecieveConfig },
|
||||
pollEnded: { optional: true, ...notificationRecieveConfig },
|
||||
scheduledNotePosted: { optional: true, ...notificationRecieveConfig },
|
||||
scheduledNotePostFailed: { optional: true, ...notificationRecieveConfig },
|
||||
receiveFollowRequest: { optional: true, ...notificationRecieveConfig },
|
||||
followRequestAccepted: { optional: true, ...notificationRecieveConfig },
|
||||
roleAssigned: { optional: true, ...notificationRecieveConfig },
|
||||
|
|
|
@ -18,9 +18,9 @@ import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
|||
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
|
||||
import { UtilityService } from '@/core/UtilityService.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { ApiError } from '../../error.js';
|
||||
import { IdentifiableError } from '@/misc/identifiable-error.js';
|
||||
import { FetchAllowSoftFailMask } from '@/core/activitypub/misc/check-against-url.js';
|
||||
import { ApiError } from '../../error.js';
|
||||
|
||||
export const meta = {
|
||||
tags: ['federation'],
|
||||
|
|
|
@ -209,6 +209,8 @@ export const paramDef = {
|
|||
quote: notificationRecieveConfig,
|
||||
reaction: notificationRecieveConfig,
|
||||
pollEnded: notificationRecieveConfig,
|
||||
scheduledNotePosted: notificationRecieveConfig,
|
||||
scheduledNotePostFailed: notificationRecieveConfig,
|
||||
receiveFollowRequest: notificationRecieveConfig,
|
||||
followRequestAccepted: notificationRecieveConfig,
|
||||
roleAssigned: notificationRecieveConfig,
|
||||
|
|
|
@ -6,17 +6,10 @@
|
|||
import ms from 'ms';
|
||||
import { In } from 'typeorm';
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import type { MiUser } from '@/models/User.js';
|
||||
import type { UsersRepository, NotesRepository, BlockingsRepository, DriveFilesRepository, ChannelsRepository } from '@/models/_.js';
|
||||
import type { MiDriveFile } from '@/models/DriveFile.js';
|
||||
import type { MiNote } from '@/models/Note.js';
|
||||
import type { MiChannel } from '@/models/Channel.js';
|
||||
import { MAX_NOTE_TEXT_LENGTH } from '@/const.js';
|
||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
|
||||
import { NoteCreateService } from '@/core/NoteCreateService.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { isQuote, isRenote } from '@/misc/is-renote.js';
|
||||
import { IdentifiableError } from '@/misc/identifiable-error.js';
|
||||
import { ApiError } from '../../error.js';
|
||||
|
||||
|
@ -223,168 +216,28 @@ export const paramDef = {
|
|||
@Injectable()
|
||||
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
|
||||
constructor(
|
||||
@Inject(DI.usersRepository)
|
||||
private usersRepository: UsersRepository,
|
||||
|
||||
@Inject(DI.notesRepository)
|
||||
private notesRepository: NotesRepository,
|
||||
|
||||
@Inject(DI.blockingsRepository)
|
||||
private blockingsRepository: BlockingsRepository,
|
||||
|
||||
@Inject(DI.driveFilesRepository)
|
||||
private driveFilesRepository: DriveFilesRepository,
|
||||
|
||||
@Inject(DI.channelsRepository)
|
||||
private channelsRepository: ChannelsRepository,
|
||||
|
||||
private noteEntityService: NoteEntityService,
|
||||
private noteCreateService: NoteCreateService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
let visibleUsers: MiUser[] = [];
|
||||
if (ps.visibleUserIds) {
|
||||
visibleUsers = await this.usersRepository.findBy({
|
||||
id: In(ps.visibleUserIds),
|
||||
});
|
||||
}
|
||||
|
||||
let files: MiDriveFile[] = [];
|
||||
const fileIds = ps.fileIds ?? ps.mediaIds ?? null;
|
||||
if (fileIds != null) {
|
||||
files = await this.driveFilesRepository.createQueryBuilder('file')
|
||||
.where('file.userId = :userId AND file.id IN (:...fileIds)', {
|
||||
userId: me.id,
|
||||
fileIds,
|
||||
})
|
||||
.orderBy('array_position(ARRAY[:...fileIds], "id"::text)')
|
||||
.setParameters({ fileIds })
|
||||
.getMany();
|
||||
|
||||
if (files.length !== fileIds.length) {
|
||||
throw new ApiError(meta.errors.noSuchFile);
|
||||
}
|
||||
}
|
||||
|
||||
let renote: MiNote | null = null;
|
||||
if (ps.renoteId != null) {
|
||||
// Fetch renote to note
|
||||
renote = await this.notesRepository.findOne({
|
||||
where: { id: ps.renoteId },
|
||||
relations: ['user', 'renote', 'reply'],
|
||||
});
|
||||
|
||||
if (renote == null) {
|
||||
throw new ApiError(meta.errors.noSuchRenoteTarget);
|
||||
} else if (isRenote(renote) && !isQuote(renote)) {
|
||||
throw new ApiError(meta.errors.cannotReRenote);
|
||||
}
|
||||
|
||||
// Check blocking
|
||||
if (renote.userId !== me.id) {
|
||||
const blockExist = await this.blockingsRepository.exists({
|
||||
where: {
|
||||
blockerId: renote.userId,
|
||||
blockeeId: me.id,
|
||||
},
|
||||
});
|
||||
if (blockExist) {
|
||||
throw new ApiError(meta.errors.youHaveBeenBlocked);
|
||||
}
|
||||
}
|
||||
|
||||
if (renote.visibility === 'followers' && renote.userId !== me.id) {
|
||||
// 他人のfollowers noteはreject
|
||||
throw new ApiError(meta.errors.cannotRenoteDueToVisibility);
|
||||
} else if (renote.visibility === 'specified') {
|
||||
// specified / direct noteはreject
|
||||
throw new ApiError(meta.errors.cannotRenoteDueToVisibility);
|
||||
}
|
||||
|
||||
if (renote.channelId && renote.channelId !== ps.channelId) {
|
||||
// チャンネルのノートに対しリノート要求がきたとき、チャンネル外へのリノート可否をチェック
|
||||
// リノートのユースケースのうち、チャンネル内→チャンネル外は少数だと考えられるため、JOINはせず必要な時に都度取得する
|
||||
const renoteChannel = await this.channelsRepository.findOneBy({ id: renote.channelId });
|
||||
if (renoteChannel == null) {
|
||||
// リノートしたいノートが書き込まれているチャンネルが無い
|
||||
throw new ApiError(meta.errors.noSuchChannel);
|
||||
} else if (!renoteChannel.allowRenoteToExternal) {
|
||||
// リノート作成のリクエストだが、対象チャンネルがリノート禁止だった場合
|
||||
throw new ApiError(meta.errors.cannotRenoteOutsideOfChannel);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let reply: MiNote | null = null;
|
||||
if (ps.replyId != null) {
|
||||
// Fetch reply
|
||||
reply = await this.notesRepository.findOne({
|
||||
where: { id: ps.replyId },
|
||||
relations: ['user'],
|
||||
});
|
||||
|
||||
if (reply == null) {
|
||||
throw new ApiError(meta.errors.noSuchReplyTarget);
|
||||
} else if (isRenote(reply) && !isQuote(reply)) {
|
||||
throw new ApiError(meta.errors.cannotReplyToPureRenote);
|
||||
} else if (!await this.noteEntityService.isVisibleForMe(reply, me.id)) {
|
||||
throw new ApiError(meta.errors.cannotReplyToInvisibleNote);
|
||||
} else if (reply.visibility === 'specified' && ps.visibility !== 'specified') {
|
||||
throw new ApiError(meta.errors.cannotReplyToSpecifiedVisibilityNoteWithExtendedVisibility);
|
||||
}
|
||||
|
||||
// Check blocking
|
||||
if (reply.userId !== me.id) {
|
||||
const blockExist = await this.blockingsRepository.exists({
|
||||
where: {
|
||||
blockerId: reply.userId,
|
||||
blockeeId: me.id,
|
||||
},
|
||||
});
|
||||
if (blockExist) {
|
||||
throw new ApiError(meta.errors.youHaveBeenBlocked);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (ps.poll) {
|
||||
if (typeof ps.poll.expiresAt === 'number') {
|
||||
if (ps.poll.expiresAt < Date.now()) {
|
||||
throw new ApiError(meta.errors.cannotCreateAlreadyExpiredPoll);
|
||||
}
|
||||
} else if (typeof ps.poll.expiredAfter === 'number') {
|
||||
ps.poll.expiresAt = Date.now() + ps.poll.expiredAfter;
|
||||
}
|
||||
}
|
||||
|
||||
let channel: MiChannel | null = null;
|
||||
if (ps.channelId != null) {
|
||||
channel = await this.channelsRepository.findOneBy({ id: ps.channelId, isArchived: false });
|
||||
|
||||
if (channel == null) {
|
||||
throw new ApiError(meta.errors.noSuchChannel);
|
||||
}
|
||||
}
|
||||
|
||||
// 投稿を作成
|
||||
try {
|
||||
const note = await this.noteCreateService.create(me, {
|
||||
const note = await this.noteCreateService.fetchAndCreate(me, {
|
||||
createdAt: new Date(),
|
||||
files: files,
|
||||
fileIds: ps.fileIds ?? ps.mediaIds ?? [],
|
||||
poll: ps.poll ? {
|
||||
choices: ps.poll.choices,
|
||||
multiple: ps.poll.multiple ?? false,
|
||||
expiresAt: ps.poll.expiresAt ? new Date(ps.poll.expiresAt) : null,
|
||||
} : undefined,
|
||||
text: ps.text ?? undefined,
|
||||
reply,
|
||||
renote,
|
||||
cw: ps.cw,
|
||||
expiresAt: ps.poll.expiredAfter ? new Date(Date.now() + ps.poll.expiredAfter) : ps.poll.expiresAt ? new Date(ps.poll.expiresAt) : null,
|
||||
} : null,
|
||||
text: ps.text ?? null,
|
||||
replyId: ps.replyId ?? null,
|
||||
renoteId: ps.renoteId ?? null,
|
||||
cw: ps.cw ?? null,
|
||||
localOnly: ps.localOnly,
|
||||
reactionAcceptance: ps.reactionAcceptance,
|
||||
visibility: ps.visibility,
|
||||
visibleUsers,
|
||||
channel,
|
||||
visibleUserIds: ps.visibleUserIds ?? [],
|
||||
channelId: ps.channelId ?? null,
|
||||
apMentions: ps.noExtractMentions ? [] : undefined,
|
||||
apHashtags: ps.noExtractHashtags ? [] : undefined,
|
||||
apEmojis: ps.noExtractEmojis ? [] : undefined,
|
||||
|
@ -393,16 +246,46 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
return {
|
||||
createdNote: await this.noteEntityService.pack(note, me),
|
||||
};
|
||||
} catch (e) {
|
||||
} catch (err) {
|
||||
// TODO: 他のErrorもここでキャッチしてエラーメッセージを当てるようにしたい
|
||||
if (e instanceof IdentifiableError) {
|
||||
if (e.id === '689ee33f-f97c-479a-ac49-1b9f8140af99') {
|
||||
if (err instanceof IdentifiableError) {
|
||||
if (err.id === '689ee33f-f97c-479a-ac49-1b9f8140af99') {
|
||||
throw new ApiError(meta.errors.containsProhibitedWords);
|
||||
} else if (e.id === '9f466dab-c856-48cd-9e65-ff90ff750580') {
|
||||
} else if (err.id === '9f466dab-c856-48cd-9e65-ff90ff750580') {
|
||||
throw new ApiError(meta.errors.containsTooManyMentions);
|
||||
} else if (err.id === '801c046c-5bf5-4234-ad2b-e78fc20a2ac7') {
|
||||
throw new ApiError(meta.errors.noSuchFile);
|
||||
} else if (err.id === '53983c56-e163-45a6-942f-4ddc485d4290') {
|
||||
throw new ApiError(meta.errors.noSuchRenoteTarget);
|
||||
} else if (err.id === 'bde24c37-121f-4e7d-980d-cec52f599f02') {
|
||||
throw new ApiError(meta.errors.cannotReRenote);
|
||||
} else if (err.id === '2b4fe776-4414-4a2d-ae39-f3418b8fd4d3') {
|
||||
throw new ApiError(meta.errors.youHaveBeenBlocked);
|
||||
} else if (err.id === '90b9d6f0-893a-4fef-b0f1-e9a33989f71a') {
|
||||
throw new ApiError(meta.errors.cannotRenoteDueToVisibility);
|
||||
} else if (err.id === '48d7a997-da5c-4716-b3c3-92db3f37bf7d') {
|
||||
throw new ApiError(meta.errors.cannotRenoteDueToVisibility);
|
||||
} else if (err.id === 'b060f9a6-8909-4080-9e0b-94d9fa6f6a77') {
|
||||
throw new ApiError(meta.errors.noSuchChannel);
|
||||
} else if (err.id === '7e435f4a-780d-4cfc-a15a-42519bd6fb67') {
|
||||
throw new ApiError(meta.errors.cannotRenoteOutsideOfChannel);
|
||||
} else if (err.id === '60142edb-1519-408e-926d-4f108d27bee0') {
|
||||
throw new ApiError(meta.errors.noSuchReplyTarget);
|
||||
} else if (err.id === 'f089e4e2-c0e7-4f60-8a23-e5a6bf786b36') {
|
||||
throw new ApiError(meta.errors.cannotReplyToPureRenote);
|
||||
} else if (err.id === '11cd37b3-a411-4f77-8633-c580ce6a8dce') {
|
||||
throw new ApiError(meta.errors.cannotReplyToInvisibleNote);
|
||||
} else if (err.id === 'ced780a1-2012-4caf-bc7e-a95a291294cb') {
|
||||
throw new ApiError(meta.errors.cannotReplyToSpecifiedVisibilityNoteWithExtendedVisibility);
|
||||
} else if (err.id === 'b0df6025-f2e8-44b4-a26a-17ad99104612') {
|
||||
throw new ApiError(meta.errors.youHaveBeenBlocked);
|
||||
} else if (err.id === '0c11c11e-0c8d-48e7-822c-76ccef660068') {
|
||||
throw new ApiError(meta.errors.cannotCreateAlreadyExpiredPoll);
|
||||
} else if (err.id === 'bfa3905b-25f5-4894-b430-da331a490e4b') {
|
||||
throw new ApiError(meta.errors.noSuchChannel);
|
||||
}
|
||||
}
|
||||
throw e;
|
||||
throw err;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
@ -124,6 +124,12 @@ export const meta = {
|
|||
id: '9ee33bbe-fde3-4c71-9b51-e50492c6b9c8',
|
||||
},
|
||||
|
||||
tooManyScheduledNotes: {
|
||||
message: 'You cannot create scheduled notes any more.',
|
||||
code: 'TOO_MANY_SCHEDULED_NOTES',
|
||||
id: '22ae69eb-09e3-4541-a850-773cfa45e693',
|
||||
},
|
||||
|
||||
cannotRenoteToExternal: {
|
||||
message: 'Cannot Renote to External.',
|
||||
code: 'CANNOT_RENOTE_TO_EXTERNAL',
|
||||
|
@ -162,7 +168,7 @@ export const paramDef = {
|
|||
fileIds: {
|
||||
type: 'array',
|
||||
uniqueItems: true,
|
||||
minItems: 1,
|
||||
minItems: 0,
|
||||
maxItems: 16,
|
||||
items: { type: 'string', format: 'misskey:id' },
|
||||
},
|
||||
|
@ -183,8 +189,10 @@ export const paramDef = {
|
|||
},
|
||||
required: ['choices'],
|
||||
},
|
||||
scheduledAt: { type: 'integer', nullable: true },
|
||||
isActuallyScheduled: { type: 'boolean', default: false },
|
||||
},
|
||||
required: [],
|
||||
required: ['visibility', 'visibleUserIds', 'cw', 'hashtag', 'localOnly', 'reactionAcceptance', 'replyId', 'renoteId', 'channelId', 'text', 'fileIds', 'poll', 'scheduledAt', 'isActuallyScheduled'],
|
||||
} as const;
|
||||
|
||||
@Injectable()
|
||||
|
@ -196,22 +204,23 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
super(meta, paramDef, async (ps, me) => {
|
||||
const draft = await this.noteDraftService.create(me, {
|
||||
fileIds: ps.fileIds,
|
||||
poll: ps.poll ? {
|
||||
choices: ps.poll.choices,
|
||||
multiple: ps.poll.multiple ?? false,
|
||||
expiresAt: ps.poll.expiresAt ? new Date(ps.poll.expiresAt) : null,
|
||||
expiredAfter: ps.poll.expiredAfter ?? null,
|
||||
} : undefined,
|
||||
text: ps.text ?? null,
|
||||
replyId: ps.replyId ?? undefined,
|
||||
renoteId: ps.renoteId ?? undefined,
|
||||
cw: ps.cw ?? null,
|
||||
...(ps.hashtag ? { hashtag: ps.hashtag } : {}),
|
||||
pollChoices: ps.poll?.choices ?? [],
|
||||
pollMultiple: ps.poll?.multiple ?? false,
|
||||
pollExpiresAt: ps.poll?.expiresAt ? new Date(ps.poll.expiresAt) : null,
|
||||
pollExpiredAfter: ps.poll?.expiredAfter ?? null,
|
||||
hasPoll: ps.poll != null,
|
||||
text: ps.text,
|
||||
replyId: ps.replyId,
|
||||
renoteId: ps.renoteId,
|
||||
cw: ps.cw,
|
||||
hashtag: ps.hashtag,
|
||||
localOnly: ps.localOnly,
|
||||
reactionAcceptance: ps.reactionAcceptance,
|
||||
visibility: ps.visibility,
|
||||
visibleUserIds: ps.visibleUserIds ?? [],
|
||||
channelId: ps.channelId ?? undefined,
|
||||
visibleUserIds: ps.visibleUserIds,
|
||||
channelId: ps.channelId,
|
||||
scheduledAt: ps.scheduledAt ? new Date(ps.scheduledAt) : null,
|
||||
isActuallyScheduled: ps.isActuallyScheduled,
|
||||
}).catch((err) => {
|
||||
if (err instanceof IdentifiableError) {
|
||||
switch (err.id) {
|
||||
|
@ -241,6 +250,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
throw new ApiError(meta.errors.cannotReplyToInvisibleNote);
|
||||
case '215dbc76-336c-4d2a-9605-95766ba7dab0':
|
||||
throw new ApiError(meta.errors.cannotReplyToSpecifiedVisibilityNoteWithExtendedVisibility);
|
||||
case 'c3275f19-4558-4c59-83e1-4f684b5fab66':
|
||||
throw new ApiError(meta.errors.tooManyScheduledNotes);
|
||||
default:
|
||||
throw err;
|
||||
}
|
||||
|
|
|
@ -41,6 +41,7 @@ export const paramDef = {
|
|||
untilId: { type: 'string', format: 'misskey:id' },
|
||||
sinceDate: { type: 'integer' },
|
||||
untilDate: { type: 'integer' },
|
||||
scheduled: { type: 'boolean', nullable: true },
|
||||
},
|
||||
required: [],
|
||||
} as const;
|
||||
|
@ -58,6 +59,12 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
const query = this.queryService.makePaginationQuery<MiNoteDraft>(this.noteDraftsRepository.createQueryBuilder('drafts'), ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate)
|
||||
.andWhere('drafts.userId = :meId', { meId: me.id });
|
||||
|
||||
if (ps.scheduled === true) {
|
||||
query.andWhere('drafts.isActuallyScheduled = true');
|
||||
} else if (ps.scheduled === false) {
|
||||
query.andWhere('drafts.isActuallyScheduled = false');
|
||||
}
|
||||
|
||||
const drafts = await query
|
||||
.limit(ps.limit)
|
||||
.getMany();
|
||||
|
|
|
@ -159,6 +159,12 @@ export const meta = {
|
|||
code: 'CANNOT_REPLY_TO_SPECIFIED_VISIBILITY_NOTE_WITH_EXTENDED_VISIBILITY',
|
||||
id: '215dbc76-336c-4d2a-9605-95766ba7dab0',
|
||||
},
|
||||
|
||||
tooManyScheduledNotes: {
|
||||
message: 'You cannot create scheduled notes any more.',
|
||||
code: 'TOO_MANY_SCHEDULED_NOTES',
|
||||
id: '02f5df79-08ae-4a33-8524-f1503c8f6212',
|
||||
},
|
||||
},
|
||||
|
||||
limit: {
|
||||
|
@ -171,14 +177,14 @@ export const paramDef = {
|
|||
type: 'object',
|
||||
properties: {
|
||||
draftId: { type: 'string', nullable: false, format: 'misskey:id' },
|
||||
visibility: { type: 'string', enum: ['public', 'home', 'followers', 'specified'], default: 'public' },
|
||||
visibility: { type: 'string', enum: ['public', 'home', 'followers', 'specified'] },
|
||||
visibleUserIds: { type: 'array', uniqueItems: true, items: {
|
||||
type: 'string', format: 'misskey:id',
|
||||
} },
|
||||
cw: { type: 'string', nullable: true, minLength: 1, maxLength: 100 },
|
||||
hashtag: { type: 'string', nullable: true, maxLength: 200 },
|
||||
localOnly: { type: 'boolean', default: false },
|
||||
reactionAcceptance: { type: 'string', nullable: true, enum: [null, 'likeOnly', 'likeOnlyForRemote', 'nonSensitiveOnly', 'nonSensitiveOnlyForLocalLikeOnlyForRemote'], default: null },
|
||||
localOnly: { type: 'boolean' },
|
||||
reactionAcceptance: { type: 'string', nullable: true, enum: [null, 'likeOnly', 'likeOnlyForRemote', 'nonSensitiveOnly', 'nonSensitiveOnlyForLocalLikeOnlyForRemote'] },
|
||||
replyId: { type: 'string', format: 'misskey:id', nullable: true },
|
||||
renoteId: { type: 'string', format: 'misskey:id', nullable: true },
|
||||
channelId: { type: 'string', format: 'misskey:id', nullable: true },
|
||||
|
@ -194,7 +200,7 @@ export const paramDef = {
|
|||
fileIds: {
|
||||
type: 'array',
|
||||
uniqueItems: true,
|
||||
minItems: 1,
|
||||
minItems: 0,
|
||||
maxItems: 16,
|
||||
items: { type: 'string', format: 'misskey:id' },
|
||||
},
|
||||
|
@ -215,6 +221,8 @@ export const paramDef = {
|
|||
},
|
||||
required: ['choices'],
|
||||
},
|
||||
scheduledAt: { type: 'integer', nullable: true },
|
||||
isActuallyScheduled: { type: 'boolean' },
|
||||
},
|
||||
required: ['draftId'],
|
||||
} as const;
|
||||
|
@ -228,22 +236,22 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
super(meta, paramDef, async (ps, me) => {
|
||||
const draft = await this.noteDraftService.update(me, ps.draftId, {
|
||||
fileIds: ps.fileIds,
|
||||
poll: ps.poll ? {
|
||||
choices: ps.poll.choices,
|
||||
multiple: ps.poll.multiple ?? false,
|
||||
expiresAt: ps.poll.expiresAt ? new Date(ps.poll.expiresAt) : null,
|
||||
expiredAfter: ps.poll.expiredAfter ?? null,
|
||||
} : undefined,
|
||||
text: ps.text ?? null,
|
||||
replyId: ps.replyId ?? undefined,
|
||||
renoteId: ps.renoteId ?? undefined,
|
||||
cw: ps.cw ?? null,
|
||||
...(ps.hashtag ? { hashtag: ps.hashtag } : {}),
|
||||
pollChoices: ps.poll?.choices,
|
||||
pollMultiple: ps.poll?.multiple,
|
||||
pollExpiresAt: ps.poll?.expiresAt ? new Date(ps.poll.expiresAt) : null,
|
||||
pollExpiredAfter: ps.poll?.expiredAfter,
|
||||
text: ps.text,
|
||||
replyId: ps.replyId,
|
||||
renoteId: ps.renoteId,
|
||||
cw: ps.cw,
|
||||
hashtag: ps.hashtag,
|
||||
localOnly: ps.localOnly,
|
||||
reactionAcceptance: ps.reactionAcceptance,
|
||||
visibility: ps.visibility,
|
||||
visibleUserIds: ps.visibleUserIds ?? [],
|
||||
channelId: ps.channelId ?? undefined,
|
||||
visibleUserIds: ps.visibleUserIds,
|
||||
channelId: ps.channelId,
|
||||
scheduledAt: ps.scheduledAt ? new Date(ps.scheduledAt) : null,
|
||||
isActuallyScheduled: ps.isActuallyScheduled,
|
||||
}).catch((err) => {
|
||||
if (err instanceof IdentifiableError) {
|
||||
switch (err.id) {
|
||||
|
@ -285,6 +293,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
throw new ApiError(meta.errors.containsProhibitedWords);
|
||||
case '4de0363a-3046-481b-9b0f-feff3e211025':
|
||||
throw new ApiError(meta.errors.containsTooManyMentions);
|
||||
case 'bacdf856-5c51-4159-b88a-804fa5103be5':
|
||||
throw new ApiError(meta.errors.tooManyScheduledNotes);
|
||||
default:
|
||||
throw err;
|
||||
}
|
||||
|
|
|
@ -29,10 +29,16 @@ export const meta = {
|
|||
id: '24fcbfc6-2e37-42b6-8388-c29b3861a08d',
|
||||
},
|
||||
|
||||
signinRequired: {
|
||||
message: 'Signin required.',
|
||||
code: 'SIGNIN_REQUIRED',
|
||||
id: '8e75455b-738c-471d-9f80-62693f33372e',
|
||||
contentRestrictedByUser: {
|
||||
message: 'Content restricted by user. Please sign in to view.',
|
||||
code: 'CONTENT_RESTRICTED_BY_USER',
|
||||
id: 'fbcc002d-37d9-4944-a6b0-d9e29f2d33ab',
|
||||
},
|
||||
|
||||
contentRestrictedByServer: {
|
||||
message: 'Content restricted by server settings. Please sign in to view.',
|
||||
code: 'CONTENT_RESTRICTED_BY_SERVER',
|
||||
id: '145f88d2-b03d-4087-8143-a78928883c4b',
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
|
@ -61,15 +67,15 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
});
|
||||
|
||||
if (note.user!.requireSigninToViewContents && me == null) {
|
||||
throw new ApiError(meta.errors.signinRequired);
|
||||
throw new ApiError(meta.errors.contentRestrictedByUser);
|
||||
}
|
||||
|
||||
if (this.serverSettings.ugcVisibilityForVisitor === 'none' && me == null) {
|
||||
throw new ApiError(meta.errors.signinRequired);
|
||||
throw new ApiError(meta.errors.contentRestrictedByServer);
|
||||
}
|
||||
|
||||
if (this.serverSettings.ugcVisibilityForVisitor === 'local' && note.userHost != null && me == null) {
|
||||
throw new ApiError(meta.errors.signinRequired);
|
||||
throw new ApiError(meta.errors.contentRestrictedByServer);
|
||||
}
|
||||
|
||||
return await this.noteEntityService.pack(note, me, {
|
||||
|
|
|
@ -22,17 +22,26 @@ export const meta = {
|
|||
res: {
|
||||
type: 'object',
|
||||
optional: false, nullable: false,
|
||||
ref: 'UserList',
|
||||
properties: {
|
||||
likedCount: {
|
||||
type: 'number',
|
||||
optional: true, nullable: false,
|
||||
allOf: [
|
||||
{
|
||||
type: 'object',
|
||||
ref: 'UserList',
|
||||
},
|
||||
isLiked: {
|
||||
type: 'boolean',
|
||||
optional: true, nullable: false,
|
||||
{
|
||||
type: 'object',
|
||||
optional: false, nullable: false,
|
||||
properties: {
|
||||
likedCount: {
|
||||
type: 'number',
|
||||
optional: true, nullable: false,
|
||||
},
|
||||
isLiked: {
|
||||
type: 'boolean',
|
||||
optional: true, nullable: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
errors: {
|
||||
|
|
|
@ -13,7 +13,7 @@
|
|||
};
|
||||
window.onunhandledrejection = (e) => {
|
||||
console.error(e);
|
||||
renderError('SOMETHING_HAPPENED_IN_PROMISE', e);
|
||||
renderError('SOMETHING_HAPPENED_IN_PROMISE', e.reason || e);
|
||||
};
|
||||
|
||||
let forceError = localStorage.getItem('forceError');
|
||||
|
|
|
@ -12,6 +12,8 @@
|
|||
* quote - 投稿が引用Renoteされた
|
||||
* reaction - 投稿にリアクションされた
|
||||
* pollEnded - 自分のアンケートもしくは自分が投票したアンケートが終了した
|
||||
* scheduledNotePosted - 予約したノートが投稿された
|
||||
* scheduledNotePostFailed - 予約したノートの投稿に失敗した
|
||||
* receiveFollowRequest - フォローリクエストされた
|
||||
* followRequestAccepted - 自分の送ったフォローリクエストが承認された
|
||||
* roleAssigned - ロールが付与された
|
||||
|
@ -32,6 +34,8 @@ export const notificationTypes = [
|
|||
'quote',
|
||||
'reaction',
|
||||
'pollEnded',
|
||||
'scheduledNotePosted',
|
||||
'scheduledNotePostFailed',
|
||||
'receiveFollowRequest',
|
||||
'followRequestAccepted',
|
||||
'roleAssigned',
|
||||
|
|
|
@ -68,7 +68,6 @@ async function createAdmin(host: Host): Promise<Misskey.entities.SignupResponse
|
|||
return await client.request('admin/accounts/create', ADMIN_PARAMS).then(res => {
|
||||
ADMIN_CACHE.set(host, {
|
||||
id: res.id,
|
||||
// @ts-expect-error FIXME: openapi-typescript generates incorrect response type for this endpoint, so ignore this
|
||||
i: res.token,
|
||||
});
|
||||
return res as Misskey.entities.SignupResponse;
|
||||
|
|
|
@ -11,15 +11,15 @@
|
|||
},
|
||||
"devDependencies": {
|
||||
"@types/estree": "1.0.8",
|
||||
"@types/node": "22.17.0",
|
||||
"@typescript-eslint/eslint-plugin": "8.38.0",
|
||||
"@typescript-eslint/parser": "8.38.0",
|
||||
"rollup": "4.46.2",
|
||||
"@types/node": "22.18.6",
|
||||
"@typescript-eslint/eslint-plugin": "8.44.1",
|
||||
"@typescript-eslint/parser": "8.44.1",
|
||||
"rollup": "4.52.2",
|
||||
"typescript": "5.9.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"estree-walker": "3.0.3",
|
||||
"magic-string": "0.30.17",
|
||||
"vite": "7.0.6"
|
||||
"magic-string": "0.30.19",
|
||||
"vite": "7.1.7"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -46,9 +46,71 @@ export default [
|
|||
allowSingleExtends: true,
|
||||
}],
|
||||
'import/consistent-type-specifier-style': ['error', 'prefer-top-level'],
|
||||
// window の禁止理由: グローバルスコープと衝突し、予期せぬ結果を招くため
|
||||
// e の禁止理由: error や event など、複数のキーワードの頭文字であり分かりにくいため
|
||||
'id-denylist': ['error', 'window', 'e'],
|
||||
// window ... グローバルスコープと衝突し、予期せぬ結果を招くため
|
||||
// e ... error や event など、複数のキーワードの頭文字であり分かりにくいため
|
||||
// close ... window.closeと衝突 or 紛らわしい
|
||||
// open ... window.openと衝突 or 紛らわしい
|
||||
// fetch ... window.fetchと衝突 or 紛らわしい
|
||||
// location ... window.locationと衝突 or 紛らわしい
|
||||
// document ... window.documentと衝突 or 紛らわしい
|
||||
// history ... window.historyと衝突 or 紛らわしい
|
||||
// scroll ... window.scrollと衝突 or 紛らわしい
|
||||
// setTimeout ... window.setTimeoutと衝突 or 紛らわしい
|
||||
// setInterval ... window.setIntervalと衝突 or 紛らわしい
|
||||
// clearTimeout ... window.clearTimeoutと衝突 or 紛らわしい
|
||||
// clearInterval ... window.clearIntervalと衝突 or 紛らわしい
|
||||
'id-denylist': ['error', 'window', 'e', 'close', 'open', 'fetch', 'location', 'document', 'history', 'scroll', 'setTimeout', 'setInterval', 'clearTimeout', 'clearInterval'],
|
||||
'no-restricted-globals': [
|
||||
'error',
|
||||
{
|
||||
'name': 'open',
|
||||
'message': 'Use `window.open`.',
|
||||
},
|
||||
{
|
||||
'name': 'close',
|
||||
'message': 'Use `window.close`.',
|
||||
},
|
||||
{
|
||||
'name': 'fetch',
|
||||
'message': 'Use `window.fetch`.',
|
||||
},
|
||||
{
|
||||
'name': 'location',
|
||||
'message': 'Use `window.location`.',
|
||||
},
|
||||
{
|
||||
'name': 'document',
|
||||
'message': 'Use `window.document`.',
|
||||
},
|
||||
{
|
||||
'name': 'history',
|
||||
'message': 'Use `window.history`.',
|
||||
},
|
||||
{
|
||||
'name': 'scroll',
|
||||
'message': 'Use `window.scroll`.',
|
||||
},
|
||||
{
|
||||
'name': 'setTimeout',
|
||||
'message': 'Use `window.setTimeout`.',
|
||||
},
|
||||
{
|
||||
'name': 'setInterval',
|
||||
'message': 'Use `window.setInterval`.',
|
||||
},
|
||||
{
|
||||
'name': 'clearTimeout',
|
||||
'message': 'Use `window.clearTimeout`.',
|
||||
},
|
||||
{
|
||||
'name': 'clearInterval',
|
||||
'message': 'Use `window.clearInterval`.',
|
||||
},
|
||||
{
|
||||
'name': 'name',
|
||||
'message': 'Use `window.name`. もしくは name という変数名を定義し忘れている',
|
||||
},
|
||||
],
|
||||
'no-shadow': ['warn'],
|
||||
'vue/attributes-order': ['error', {
|
||||
alphabetical: false,
|
||||
|
|
|
@ -16,7 +16,7 @@
|
|||
"@rollup/pluginutils": "5.3.0",
|
||||
"@twemoji/parser": "16.0.0",
|
||||
"@vitejs/plugin-vue": "6.0.1",
|
||||
"@vue/compiler-sfc": "3.5.21",
|
||||
"@vue/compiler-sfc": "3.5.22",
|
||||
"astring": "1.9.0",
|
||||
"buraha": "0.0.1",
|
||||
"estree-walker": "3.0.3",
|
||||
|
@ -26,47 +26,47 @@
|
|||
"mfm-js": "0.25.0",
|
||||
"misskey-js": "workspace:*",
|
||||
"punycode.js": "2.3.1",
|
||||
"rollup": "4.50.1",
|
||||
"sass": "1.92.1",
|
||||
"shiki": "3.12.2",
|
||||
"rollup": "4.52.2",
|
||||
"sass": "1.93.2",
|
||||
"shiki": "3.13.0",
|
||||
"tinycolor2": "1.6.0",
|
||||
"tsc-alias": "1.8.16",
|
||||
"tsconfig-paths": "4.2.0",
|
||||
"typescript": "5.9.2",
|
||||
"uuid": "11.1.0",
|
||||
"vite": "7.1.4",
|
||||
"vue": "3.5.21"
|
||||
"vite": "7.1.7",
|
||||
"vue": "3.5.22"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@misskey-dev/summaly": "5.2.3",
|
||||
"@tabler/icons-webfont": "3.34.1",
|
||||
"@tabler/icons-webfont": "3.35.0",
|
||||
"@testing-library/vue": "8.1.0",
|
||||
"@types/estree": "1.0.8",
|
||||
"@types/micromatch": "4.0.9",
|
||||
"@types/node": "22.18.1",
|
||||
"@types/node": "22.18.6",
|
||||
"@types/punycode.js": "npm:@types/punycode@2.1.4",
|
||||
"@types/tinycolor2": "1.4.6",
|
||||
"@types/ws": "8.18.1",
|
||||
"@typescript-eslint/eslint-plugin": "8.42.0",
|
||||
"@typescript-eslint/parser": "8.42.0",
|
||||
"@typescript-eslint/eslint-plugin": "8.44.1",
|
||||
"@typescript-eslint/parser": "8.44.1",
|
||||
"@vitest/coverage-v8": "3.2.4",
|
||||
"@vue/runtime-core": "3.5.21",
|
||||
"@vue/runtime-core": "3.5.22",
|
||||
"acorn": "8.15.0",
|
||||
"cross-env": "10.0.0",
|
||||
"eslint-plugin-import": "2.32.0",
|
||||
"eslint-plugin-vue": "10.4.0",
|
||||
"eslint-plugin-vue": "10.5.0",
|
||||
"fast-glob": "3.3.3",
|
||||
"happy-dom": "18.0.1",
|
||||
"intersection-observer": "0.12.2",
|
||||
"micromatch": "4.0.8",
|
||||
"msw": "2.11.1",
|
||||
"msw": "2.11.3",
|
||||
"nodemon": "3.1.10",
|
||||
"prettier": "3.6.2",
|
||||
"start-server-and-test": "2.1.0",
|
||||
"tsx": "4.20.5",
|
||||
"start-server-and-test": "2.1.2",
|
||||
"tsx": "4.20.6",
|
||||
"vite-plugin-turbosnap": "1.0.3",
|
||||
"vue-component-type-helpers": "3.0.6",
|
||||
"vue-component-type-helpers": "3.0.8",
|
||||
"vue-eslint-parser": "10.2.0",
|
||||
"vue-tsc": "3.0.6"
|
||||
"vue-tsc": "3.0.8"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -33,7 +33,7 @@ import type { Theme } from '@/theme.js';
|
|||
console.log('Misskey Embed');
|
||||
|
||||
//#region Embedパラメータの取得・パース
|
||||
const params = new URLSearchParams(location.search);
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
const embedParams = parseEmbedParams(params);
|
||||
if (_DEV_) console.log(embedParams);
|
||||
//#endregion
|
||||
|
@ -81,7 +81,7 @@ storeBootloaderErrors({ ...i18n.ts._bootErrors, reload: i18n.ts.reload });
|
|||
//#endregion
|
||||
|
||||
// サイズの制限
|
||||
document.documentElement.style.maxWidth = '500px';
|
||||
window.document.documentElement.style.maxWidth = '500px';
|
||||
|
||||
// iframeIdの設定
|
||||
function setIframeIdHandler(event: MessageEvent) {
|
||||
|
@ -114,16 +114,16 @@ app.provide(DI.embedParams, embedParams);
|
|||
const rootEl = ((): HTMLElement => {
|
||||
const MISSKEY_MOUNT_DIV_ID = 'misskey_app';
|
||||
|
||||
const currentRoot = document.getElementById(MISSKEY_MOUNT_DIV_ID);
|
||||
const currentRoot = window.document.getElementById(MISSKEY_MOUNT_DIV_ID);
|
||||
|
||||
if (currentRoot) {
|
||||
console.warn('multiple import detected');
|
||||
return currentRoot;
|
||||
}
|
||||
|
||||
const root = document.createElement('div');
|
||||
const root = window.document.createElement('div');
|
||||
root.id = MISSKEY_MOUNT_DIV_ID;
|
||||
document.body.appendChild(root);
|
||||
window.document.body.appendChild(root);
|
||||
return root;
|
||||
})();
|
||||
|
||||
|
@ -159,7 +159,7 @@ console.log(i18n.tsx._selfXssPrevention.description3({ link: 'https://misskey-hu
|
|||
//#endregion
|
||||
|
||||
function removeSplash() {
|
||||
const splash = document.getElementById('splash');
|
||||
const splash = window.document.getElementById('splash');
|
||||
if (splash) {
|
||||
splash.style.opacity = '0';
|
||||
splash.style.pointerEvents = 'none';
|
||||
|
|
|
@ -19,7 +19,7 @@ import { extractAvgColorFromBlurhash } from '@@/js/extract-avg-color-from-blurha
|
|||
const canvasPromise = new Promise<WorkerMultiDispatch | HTMLCanvasElement>(resolve => {
|
||||
// テスト環境で Web Worker インスタンスは作成できない
|
||||
if (import.meta.env.MODE === 'test') {
|
||||
const canvas = document.createElement('canvas');
|
||||
const canvas = window.document.createElement('canvas');
|
||||
canvas.width = 64;
|
||||
canvas.height = 64;
|
||||
resolve(canvas);
|
||||
|
@ -34,7 +34,7 @@ const canvasPromise = new Promise<WorkerMultiDispatch | HTMLCanvasElement>(resol
|
|||
);
|
||||
resolve(workers);
|
||||
} else {
|
||||
const canvas = document.createElement('canvas');
|
||||
const canvas = window.document.createElement('canvas');
|
||||
canvas.width = 64;
|
||||
canvas.height = 64;
|
||||
resolve(canvas);
|
||||
|
|
|
@ -29,7 +29,7 @@ const props = defineProps<{
|
|||
// if no instance data is given, this is for the local instance
|
||||
const instance = props.instance ?? {
|
||||
name: serverMetadata.name,
|
||||
themeColor: (document.querySelector('meta[name="theme-color-orig"]') as HTMLMetaElement)?.content,
|
||||
themeColor: (window.document.querySelector('meta[name="theme-color-orig"]') as HTMLMetaElement)?.content,
|
||||
};
|
||||
|
||||
const faviconUrl = computed(() => props.instance ? mediaProxy.getProxiedImageUrlNullable(props.instance.faviconUrl, 'preview') : mediaProxy.getProxiedImageUrlNullable(serverMetadata.iconUrl, 'preview') ?? '/favicon.ico');
|
||||
|
|
|
@ -27,7 +27,7 @@ const canonical = props.host === localHost ? `@${props.username}` : `@${props.us
|
|||
|
||||
const url = `/${canonical}`;
|
||||
|
||||
const bg = tinycolor(getComputedStyle(document.documentElement).getPropertyValue('--MI_THEME-mention'));
|
||||
const bg = tinycolor(getComputedStyle(window.document.documentElement).getPropertyValue('--MI_THEME-mention'));
|
||||
bg.setAlpha(0.1);
|
||||
const bgCss = bg.toRgbString();
|
||||
</script>
|
||||
|
|
|
@ -134,7 +134,7 @@ const isBackTop = ref(false);
|
|||
const empty = computed(() => items.value.size === 0);
|
||||
const error = ref(false);
|
||||
|
||||
const scrollableElement = computed(() => rootEl.value ? getScrollContainer(rootEl.value) : document.body);
|
||||
const scrollableElement = computed(() => rootEl.value ? getScrollContainer(rootEl.value) : window.document.body);
|
||||
|
||||
const visibility = useDocumentVisibility();
|
||||
|
||||
|
@ -353,7 +353,7 @@ watch(visibility, () => {
|
|||
BACKGROUND_PAUSE_WAIT_SEC * 1000);
|
||||
} else { // 'visible'
|
||||
if (timerForSetPause) {
|
||||
clearTimeout(timerForSetPause);
|
||||
window.clearTimeout(timerForSetPause);
|
||||
timerForSetPause = null;
|
||||
} else {
|
||||
isPausingUpdate = false;
|
||||
|
@ -447,11 +447,11 @@ onBeforeMount(() => {
|
|||
init().then(() => {
|
||||
if (props.pagination.reversed) {
|
||||
nextTick(() => {
|
||||
setTimeout(toBottom, 800);
|
||||
window.setTimeout(toBottom, 800);
|
||||
|
||||
// scrollToBottomでmoreFetchingボタンが画面外まで出るまで
|
||||
// more = trueを遅らせる
|
||||
setTimeout(() => {
|
||||
window.setTimeout(() => {
|
||||
moreFetching.value = false;
|
||||
}, 2000);
|
||||
});
|
||||
|
@ -461,11 +461,11 @@ onBeforeMount(() => {
|
|||
|
||||
onBeforeUnmount(() => {
|
||||
if (timerForSetPause) {
|
||||
clearTimeout(timerForSetPause);
|
||||
window.clearTimeout(timerForSetPause);
|
||||
timerForSetPause = null;
|
||||
}
|
||||
if (preventAppearFetchMoreTimer.value) {
|
||||
clearTimeout(preventAppearFetchMoreTimer.value);
|
||||
window.clearTimeout(preventAppearFetchMoreTimer.value);
|
||||
preventAppearFetchMoreTimer.value = null;
|
||||
}
|
||||
scrollObserver.value?.disconnect();
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
*/
|
||||
import * as Misskey from 'misskey-js';
|
||||
|
||||
const providedContextEl = document.getElementById('misskey_embedCtx');
|
||||
const providedContextEl = window.document.getElementById('misskey_embedCtx');
|
||||
|
||||
export type ServerContext = {
|
||||
clip?: Misskey.entities.Clip;
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
import * as Misskey from 'misskey-js';
|
||||
import { misskeyApi } from '@/misskey-api.js';
|
||||
|
||||
const providedMetaEl = document.getElementById('misskey_meta');
|
||||
const providedMetaEl = window.document.getElementById('misskey_meta');
|
||||
|
||||
const _serverMetadata: Misskey.entities.MetaDetailed | null = (providedMetaEl && providedMetaEl.textContent) ? JSON.parse(providedMetaEl.textContent) : null;
|
||||
|
||||
|
|
|
@ -35,15 +35,15 @@ export function assertIsTheme(theme: Record<string, unknown>): theme is Theme {
|
|||
export function applyTheme(theme: Theme, persist = true) {
|
||||
if (timeout) window.clearTimeout(timeout);
|
||||
|
||||
document.documentElement.classList.add('_themeChanging_');
|
||||
window.document.documentElement.classList.add('_themeChanging_');
|
||||
|
||||
timeout = window.setTimeout(() => {
|
||||
document.documentElement.classList.remove('_themeChanging_');
|
||||
window.document.documentElement.classList.remove('_themeChanging_');
|
||||
}, 1000);
|
||||
|
||||
const colorScheme = theme.base === 'dark' ? 'dark' : 'light';
|
||||
|
||||
document.documentElement.dataset.colorScheme = colorScheme;
|
||||
window.document.documentElement.dataset.colorScheme = colorScheme;
|
||||
|
||||
// Deep copy
|
||||
const _theme = JSON.parse(JSON.stringify(theme));
|
||||
|
@ -55,7 +55,7 @@ export function applyTheme(theme: Theme, persist = true) {
|
|||
|
||||
const props = compile(_theme);
|
||||
|
||||
for (const tag of document.head.children) {
|
||||
for (const tag of window.document.head.children) {
|
||||
if (tag.tagName === 'META' && tag.getAttribute('name') === 'theme-color') {
|
||||
tag.setAttribute('content', props['htmlThemeColor']);
|
||||
break;
|
||||
|
@ -63,7 +63,7 @@ export function applyTheme(theme: Theme, persist = true) {
|
|||
}
|
||||
|
||||
for (const [k, v] of Object.entries(props)) {
|
||||
document.documentElement.style.setProperty(`--MI_THEME-${k}`, v.toString());
|
||||
window.document.documentElement.style.setProperty(`--MI_THEME-${k}`, v.toString());
|
||||
}
|
||||
|
||||
// iframeを正常に透過させるために、cssのcolor-schemeは `light dark;` 固定にしてある。style.scss参照
|
||||
|
|
|
@ -52,8 +52,8 @@ function safeURIDecode(str: string): string {
|
|||
}
|
||||
}
|
||||
|
||||
const page = location.pathname.split('/')[2];
|
||||
const contentId = safeURIDecode(location.pathname.split('/')[3]);
|
||||
const page = window.location.pathname.split('/')[2];
|
||||
const contentId = safeURIDecode(window.location.pathname.split('/')[3]);
|
||||
if (_DEV_) console.log(page, contentId);
|
||||
|
||||
const embedParams = inject(DI.embedParams, defaultEmbedParams);
|
||||
|
|
|
@ -64,6 +64,8 @@ function toBase62(n: number): string {
|
|||
}
|
||||
|
||||
export function getConfig(): UserConfig {
|
||||
const localesHash = toBase62(hash(JSON.stringify(locales)));
|
||||
|
||||
return {
|
||||
base: '/embed_vite/',
|
||||
|
||||
|
@ -148,9 +150,9 @@ export function getConfig(): UserConfig {
|
|||
// dependencies of i18n.ts
|
||||
'config': ['@@/js/config.js'],
|
||||
},
|
||||
entryFileNames: 'scripts/[hash:8].js',
|
||||
chunkFileNames: 'scripts/[hash:8].js',
|
||||
assetFileNames: 'assets/[hash:8][extname]',
|
||||
entryFileNames: `scripts/${localesHash}-[hash:8].js`,
|
||||
chunkFileNames: `scripts/${localesHash}-[hash:8].js`,
|
||||
assetFileNames: `assets/${localesHash}-[hash:8][extname]`,
|
||||
paths(id) {
|
||||
for (const p of externalPackages) {
|
||||
if (p.match.test(id)) {
|
||||
|
|
|
@ -51,9 +51,71 @@ export default [
|
|||
allowSingleExtends: true,
|
||||
}],
|
||||
'import/consistent-type-specifier-style': ['error', 'prefer-top-level'],
|
||||
// window の禁止理由: グローバルスコープと衝突し、予期せぬ結果を招くため
|
||||
// e の禁止理由: error や event など、複数のキーワードの頭文字であり分かりにくいため
|
||||
'id-denylist': ['error', 'window', 'e'],
|
||||
// window ... グローバルスコープと衝突し、予期せぬ結果を招くため
|
||||
// e ... error や event など、複数のキーワードの頭文字であり分かりにくいため
|
||||
// close ... window.closeと衝突 or 紛らわしい
|
||||
// open ... window.openと衝突 or 紛らわしい
|
||||
// fetch ... window.fetchと衝突 or 紛らわしい
|
||||
// location ... window.locationと衝突 or 紛らわしい
|
||||
// document ... window.documentと衝突 or 紛らわしい
|
||||
// history ... window.historyと衝突 or 紛らわしい
|
||||
// scroll ... window.scrollと衝突 or 紛らわしい
|
||||
// setTimeout ... window.setTimeoutと衝突 or 紛らわしい
|
||||
// setInterval ... window.setIntervalと衝突 or 紛らわしい
|
||||
// clearTimeout ... window.clearTimeoutと衝突 or 紛らわしい
|
||||
// clearInterval ... window.clearIntervalと衝突 or 紛らわしい
|
||||
'id-denylist': ['error', 'window', 'e', 'close', 'open', 'fetch', 'location', 'document', 'history', 'scroll', 'setTimeout', 'setInterval', 'clearTimeout', 'clearInterval'],
|
||||
'no-restricted-globals': [
|
||||
'error',
|
||||
{
|
||||
'name': 'open',
|
||||
'message': 'Use `window.open`.',
|
||||
},
|
||||
{
|
||||
'name': 'close',
|
||||
'message': 'Use `window.close`.',
|
||||
},
|
||||
{
|
||||
'name': 'fetch',
|
||||
'message': 'Use `window.fetch`.',
|
||||
},
|
||||
{
|
||||
'name': 'location',
|
||||
'message': 'Use `window.location`.',
|
||||
},
|
||||
{
|
||||
'name': 'document',
|
||||
'message': 'Use `window.document`.',
|
||||
},
|
||||
{
|
||||
'name': 'history',
|
||||
'message': 'Use `window.history`.',
|
||||
},
|
||||
{
|
||||
'name': 'scroll',
|
||||
'message': 'Use `window.scroll`.',
|
||||
},
|
||||
{
|
||||
'name': 'setTimeout',
|
||||
'message': 'Use `window.setTimeout`.',
|
||||
},
|
||||
{
|
||||
'name': 'setInterval',
|
||||
'message': 'Use `window.setInterval`.',
|
||||
},
|
||||
{
|
||||
'name': 'clearTimeout',
|
||||
'message': 'Use `window.clearTimeout`.',
|
||||
},
|
||||
{
|
||||
'name': 'clearInterval',
|
||||
'message': 'Use `window.clearInterval`.',
|
||||
},
|
||||
{
|
||||
'name': 'name',
|
||||
'message': 'Use `window.name`. もしくは name という変数名を定義し忘れている',
|
||||
},
|
||||
],
|
||||
'no-shadow': ['warn'],
|
||||
'vue/attributes-order': ['error', {
|
||||
alphabetical: false,
|
||||
|
|
|
@ -4,15 +4,15 @@
|
|||
*/
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
|
||||
const address = new URL(document.querySelector<HTMLMetaElement>('meta[property="instance_url"]')?.content || location.href);
|
||||
const siteName = document.querySelector<HTMLMetaElement>('meta[property="og:site_name"]')?.content;
|
||||
const address = new URL(window.document.querySelector<HTMLMetaElement>('meta[property="instance_url"]')?.content || window.location.href);
|
||||
const siteName = window.document.querySelector<HTMLMetaElement>('meta[property="og:site_name"]')?.content;
|
||||
|
||||
export const host = address.host;
|
||||
export const hostname = address.hostname;
|
||||
export const url = address.origin;
|
||||
export const port = address.port;
|
||||
export const apiUrl = location.origin + '/api';
|
||||
export const wsOrigin = location.origin;
|
||||
export const apiUrl = window.location.origin + '/api';
|
||||
export const wsOrigin = window.location.origin;
|
||||
export const lang = localStorage.getItem('lang') ?? 'en-US';
|
||||
export const langs = _LANGS_;
|
||||
export const version = _VERSION_;
|
||||
|
|
|
@ -51,7 +51,7 @@ export function onScrollTop(el: HTMLElement, cb: (topVisible: boolean) => unknow
|
|||
// - toleranceの範囲内に収まる程度の微量なスクロールが発生した
|
||||
let prevTopVisible = firstTopVisible;
|
||||
const onScroll = () => {
|
||||
if (!document.body.contains(el)) return;
|
||||
if (!window.document.body.contains(el)) return;
|
||||
|
||||
const topVisible = isHeadVisible(el, tolerance);
|
||||
if (topVisible !== prevTopVisible) {
|
||||
|
@ -78,7 +78,7 @@ export function onScrollBottom(el: HTMLElement, cb: () => unknown, tolerance = 1
|
|||
|
||||
const containerOrWindow = container ?? window;
|
||||
const onScroll = () => {
|
||||
if (!document.body.contains(el)) return;
|
||||
if (!window.document.body.contains(el)) return;
|
||||
if (isTailVisible(el, 1, container)) {
|
||||
cb();
|
||||
if (once) removeListener();
|
||||
|
@ -145,8 +145,8 @@ export function isTailVisible(el: HTMLElement, tolerance = 1, container = getScr
|
|||
// https://ja.javascript.info/size-and-scroll-window#ref-932
|
||||
export function getBodyScrollHeight() {
|
||||
return Math.max(
|
||||
document.body.scrollHeight, document.documentElement.scrollHeight,
|
||||
document.body.offsetHeight, document.documentElement.offsetHeight,
|
||||
document.body.clientHeight, document.documentElement.clientHeight,
|
||||
window.document.body.scrollHeight, window.document.documentElement.scrollHeight,
|
||||
window.document.body.offsetHeight, window.document.documentElement.offsetHeight,
|
||||
window.document.body.clientHeight, window.document.documentElement.clientHeight,
|
||||
);
|
||||
}
|
||||
|
|
|
@ -7,18 +7,18 @@ import { onMounted, onUnmounted, ref } from 'vue';
|
|||
import type { Ref } from 'vue';
|
||||
|
||||
export function useDocumentVisibility(): Ref<DocumentVisibilityState> {
|
||||
const visibility = ref(document.visibilityState);
|
||||
const visibility = ref(window.document.visibilityState);
|
||||
|
||||
const onChange = (): void => {
|
||||
visibility.value = document.visibilityState;
|
||||
visibility.value = window.document.visibilityState;
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
document.addEventListener('visibilitychange', onChange);
|
||||
window.document.addEventListener('visibilitychange', onChange);
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
document.removeEventListener('visibilitychange', onChange);
|
||||
window.document.removeEventListener('visibilitychange', onChange);
|
||||
});
|
||||
|
||||
return visibility;
|
||||
|
|
|
@ -21,11 +21,11 @@
|
|||
"lint": "pnpm typecheck && pnpm eslint"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "22.18.1",
|
||||
"@typescript-eslint/eslint-plugin": "8.42.0",
|
||||
"@typescript-eslint/parser": "8.42.0",
|
||||
"esbuild": "0.25.9",
|
||||
"eslint-plugin-vue": "10.4.0",
|
||||
"@types/node": "22.18.6",
|
||||
"@typescript-eslint/eslint-plugin": "8.44.1",
|
||||
"@typescript-eslint/parser": "8.44.1",
|
||||
"esbuild": "0.25.10",
|
||||
"eslint-plugin-vue": "10.5.0",
|
||||
"nodemon": "3.1.10",
|
||||
"typescript": "5.9.2",
|
||||
"vue-eslint-parser": "10.2.0"
|
||||
|
@ -35,6 +35,6 @@
|
|||
],
|
||||
"dependencies": {
|
||||
"misskey-js": "workspace:*",
|
||||
"vue": "3.5.21"
|
||||
"vue": "3.5.22"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -24,12 +24,12 @@
|
|||
"@rollup/plugin-json": "6.1.0",
|
||||
"@rollup/plugin-replace": "6.0.2",
|
||||
"@rollup/pluginutils": "5.3.0",
|
||||
"@sentry/vue": "10.10.0",
|
||||
"@syuilo/aiscript": "1.1.0",
|
||||
"@sentry/vue": "10.15.0",
|
||||
"@syuilo/aiscript": "1.1.2",
|
||||
"@syuilo/aiscript-0-19-0": "npm:@syuilo/aiscript@^0.19.0",
|
||||
"@twemoji/parser": "16.0.0",
|
||||
"@vitejs/plugin-vue": "6.0.1",
|
||||
"@vue/compiler-sfc": "3.5.21",
|
||||
"@vue/compiler-sfc": "3.5.22",
|
||||
"aiscript-vscode": "github:aiscript-dev/aiscript-vscode#v0.1.15",
|
||||
"analytics": "0.8.19",
|
||||
"astring": "1.9.0",
|
||||
|
@ -41,7 +41,7 @@
|
|||
"chartjs-chart-matrix": "3.0.0",
|
||||
"chartjs-plugin-gradient": "0.6.1",
|
||||
"chartjs-plugin-zoom": "2.2.0",
|
||||
"chromatic": "13.1.4",
|
||||
"chromatic": "13.2.1",
|
||||
"compare-versions": "6.1.1",
|
||||
"cropperjs": "2.0.1",
|
||||
"date-fns": "4.1.0",
|
||||
|
@ -52,21 +52,24 @@
|
|||
"icons-subsetter": "workspace:*",
|
||||
"idb-keyval": "6.2.2",
|
||||
"insert-text-at-cursor": "0.3.0",
|
||||
"ios-haptics": "0.1.0",
|
||||
"ios-haptics": "0.1.4",
|
||||
"is-file-animated": "1.0.2",
|
||||
"json5": "2.2.3",
|
||||
"magic-string": "0.30.18",
|
||||
"magic-string": "0.30.19",
|
||||
"matter-js": "0.20.0",
|
||||
"mediabunny": "1.21.0",
|
||||
"mfm-js": "0.25.0",
|
||||
"misskey-bubble-game": "workspace:*",
|
||||
"misskey-js": "workspace:*",
|
||||
"misskey-reversi": "workspace:*",
|
||||
"photoswipe": "5.4.4",
|
||||
"punycode.js": "2.3.1",
|
||||
"rollup": "4.50.1",
|
||||
"qr-code-styling": "1.9.2",
|
||||
"qr-scanner": "1.4.2",
|
||||
"rollup": "4.52.2",
|
||||
"sanitize-html": "2.17.0",
|
||||
"sass": "1.92.1",
|
||||
"shiki": "3.12.2",
|
||||
"sass": "1.93.2",
|
||||
"shiki": "3.13.0",
|
||||
"strict-event-emitter-types": "2.0.0",
|
||||
"textarea-caret": "3.1.0",
|
||||
"three": "0.180.0",
|
||||
|
@ -76,8 +79,8 @@
|
|||
"tsconfig-paths": "4.2.0",
|
||||
"typescript": "5.9.2",
|
||||
"v-code-diff": "1.13.1",
|
||||
"vite": "7.1.4",
|
||||
"vue": "3.5.21",
|
||||
"vite": "7.1.7",
|
||||
"vue": "3.5.22",
|
||||
"vuedraggable": "next",
|
||||
"wanakana": "5.3.1"
|
||||
},
|
||||
|
@ -85,7 +88,7 @@
|
|||
"@misskey-dev/summaly": "5.2.3",
|
||||
"@storybook/addon-essentials": "8.6.14",
|
||||
"@storybook/addon-interactions": "8.6.14",
|
||||
"@storybook/addon-links": "9.1.5",
|
||||
"@storybook/addon-links": "9.1.8",
|
||||
"@storybook/addon-mdx-gfm": "8.6.14",
|
||||
"@storybook/addon-storysource": "8.6.14",
|
||||
"@storybook/blocks": "8.6.14",
|
||||
|
@ -93,57 +96,57 @@
|
|||
"@storybook/core-events": "8.6.14",
|
||||
"@storybook/manager-api": "8.6.14",
|
||||
"@storybook/preview-api": "8.6.14",
|
||||
"@storybook/react": "9.1.5",
|
||||
"@storybook/react-vite": "9.1.5",
|
||||
"@storybook/react": "9.1.8",
|
||||
"@storybook/react-vite": "9.1.8",
|
||||
"@storybook/test": "8.6.14",
|
||||
"@storybook/theming": "8.6.14",
|
||||
"@storybook/types": "8.6.14",
|
||||
"@storybook/vue3": "9.1.5",
|
||||
"@storybook/vue3-vite": "9.1.5",
|
||||
"@tabler/icons-webfont": "3.34.1",
|
||||
"@storybook/vue3": "9.1.8",
|
||||
"@storybook/vue3-vite": "9.1.8",
|
||||
"@tabler/icons-webfont": "3.35.0",
|
||||
"@testing-library/vue": "8.1.0",
|
||||
"@types/canvas-confetti": "1.9.0",
|
||||
"@types/estree": "1.0.8",
|
||||
"@types/matter-js": "0.20.0",
|
||||
"@types/matter-js": "0.20.2",
|
||||
"@types/micromatch": "4.0.9",
|
||||
"@types/node": "22.18.1",
|
||||
"@types/node": "22.18.6",
|
||||
"@types/punycode.js": "npm:@types/punycode@2.1.4",
|
||||
"@types/sanitize-html": "2.16.0",
|
||||
"@types/seedrandom": "3.0.8",
|
||||
"@types/throttle-debounce": "5.0.2",
|
||||
"@types/tinycolor2": "1.4.6",
|
||||
"@types/ws": "8.18.1",
|
||||
"@typescript-eslint/eslint-plugin": "8.42.0",
|
||||
"@typescript-eslint/parser": "8.42.0",
|
||||
"@typescript-eslint/eslint-plugin": "8.44.1",
|
||||
"@typescript-eslint/parser": "8.44.1",
|
||||
"@vitest/coverage-v8": "3.2.4",
|
||||
"@vue/compiler-core": "3.5.21",
|
||||
"@vue/runtime-core": "3.5.21",
|
||||
"@vue/compiler-core": "3.5.22",
|
||||
"@vue/runtime-core": "3.5.22",
|
||||
"acorn": "8.15.0",
|
||||
"cross-env": "10.0.0",
|
||||
"cypress": "14.5.4",
|
||||
"eslint-plugin-import": "2.32.0",
|
||||
"eslint-plugin-vue": "10.4.0",
|
||||
"eslint-plugin-vue": "10.5.0",
|
||||
"fast-glob": "3.3.3",
|
||||
"happy-dom": "18.0.1",
|
||||
"intersection-observer": "0.12.2",
|
||||
"micromatch": "4.0.8",
|
||||
"minimatch": "10.0.3",
|
||||
"msw": "2.11.1",
|
||||
"msw": "2.11.3",
|
||||
"msw-storybook-addon": "2.0.5",
|
||||
"nodemon": "3.1.10",
|
||||
"prettier": "3.6.2",
|
||||
"react": "19.1.1",
|
||||
"react-dom": "19.1.1",
|
||||
"seedrandom": "3.0.5",
|
||||
"start-server-and-test": "2.1.0",
|
||||
"storybook": "9.1.5",
|
||||
"start-server-and-test": "2.1.2",
|
||||
"storybook": "9.1.8",
|
||||
"storybook-addon-misskey-theme": "github:misskey-dev/storybook-addon-misskey-theme",
|
||||
"tsx": "4.20.5",
|
||||
"tsx": "4.20.6",
|
||||
"vite-plugin-turbosnap": "1.0.3",
|
||||
"vitest": "3.2.4",
|
||||
"vitest-fetch-mock": "0.4.5",
|
||||
"vue-component-type-helpers": "3.0.6",
|
||||
"vue-component-type-helpers": "3.0.8",
|
||||
"vue-eslint-parser": "10.2.0",
|
||||
"vue-tsc": "3.0.6"
|
||||
"vue-tsc": "3.0.8"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -151,7 +151,21 @@ export async function common(createVue: () => Promise<App<Element>>) {
|
|||
}
|
||||
//#endregion
|
||||
|
||||
//#region Sync dark mode
|
||||
if (prefer.s.syncDeviceDarkMode) {
|
||||
store.set('darkMode', isDeviceDarkmode());
|
||||
}
|
||||
|
||||
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', (mql) => {
|
||||
if (prefer.s.syncDeviceDarkMode) {
|
||||
store.set('darkMode', mql.matches);
|
||||
}
|
||||
});
|
||||
//#endregion
|
||||
|
||||
// NOTE: この処理は必ずクライアント更新チェック処理より後に来ること(テーマ再構築のため)
|
||||
// NOTE: この処理は必ずダークモード判定処理より後に来ること(初回のテーマ適用のため)
|
||||
// see: https://github.com/misskey-dev/misskey/issues/16562
|
||||
watch(store.r.darkMode, (darkMode) => {
|
||||
const theme = (() => {
|
||||
if (darkMode) {
|
||||
|
@ -183,18 +197,6 @@ export async function common(createVue: () => Promise<App<Element>>) {
|
|||
});
|
||||
}
|
||||
|
||||
//#region Sync dark mode
|
||||
if (prefer.s.syncDeviceDarkMode) {
|
||||
store.set('darkMode', isDeviceDarkmode());
|
||||
}
|
||||
|
||||
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', (mql) => {
|
||||
if (prefer.s.syncDeviceDarkMode) {
|
||||
store.set('darkMode', mql.matches);
|
||||
}
|
||||
});
|
||||
//#endregion
|
||||
|
||||
if (!isSafeMode) {
|
||||
if (prefer.s.darkTheme && store.s.darkMode) {
|
||||
if (miLocalStorage.getItem('themeId') !== prefer.s.darkTheme.id) applyTheme(prefer.s.darkTheme);
|
||||
|
|
|
@ -10,6 +10,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<script lang="ts" setup>
|
||||
import { onMounted, onUnmounted, useTemplateRef } from 'vue';
|
||||
import isChromatic from 'chromatic/isChromatic';
|
||||
import { initShaderProgram } from '@/utility/webgl.js';
|
||||
|
||||
const canvasEl = useTemplateRef('canvasEl');
|
||||
|
||||
|
@ -21,47 +22,6 @@ const props = withDefaults(defineProps<{
|
|||
focus: 1.0,
|
||||
});
|
||||
|
||||
function loadShader(gl: WebGLRenderingContext, type: number, source: string) {
|
||||
const shader = gl.createShader(type);
|
||||
if (shader == null) return null;
|
||||
|
||||
gl.shaderSource(shader, source);
|
||||
gl.compileShader(shader);
|
||||
|
||||
if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
|
||||
alert(
|
||||
`falied to compile shader: ${gl.getShaderInfoLog(shader)}`,
|
||||
);
|
||||
gl.deleteShader(shader);
|
||||
return null;
|
||||
}
|
||||
|
||||
return shader;
|
||||
}
|
||||
|
||||
function initShaderProgram(gl: WebGLRenderingContext, vsSource: string, fsSource: string) {
|
||||
const vertexShader = loadShader(gl, gl.VERTEX_SHADER, vsSource);
|
||||
const fragmentShader = loadShader(gl, gl.FRAGMENT_SHADER, fsSource);
|
||||
|
||||
const shaderProgram = gl.createProgram();
|
||||
if (vertexShader == null || fragmentShader == null) return null;
|
||||
|
||||
gl.attachShader(shaderProgram, vertexShader);
|
||||
gl.attachShader(shaderProgram, fragmentShader);
|
||||
gl.linkProgram(shaderProgram);
|
||||
|
||||
if (!gl.getProgramParameter(shaderProgram, gl.LINK_STATUS)) {
|
||||
alert(
|
||||
`failed to init shader: ${gl.getProgramInfoLog(
|
||||
shaderProgram,
|
||||
)}`,
|
||||
);
|
||||
return null;
|
||||
}
|
||||
|
||||
return shaderProgram;
|
||||
}
|
||||
|
||||
let handle: ReturnType<typeof window['requestAnimationFrame']> | null = null;
|
||||
|
||||
onMounted(() => {
|
||||
|
@ -71,7 +31,7 @@ onMounted(() => {
|
|||
canvas.width = width;
|
||||
canvas.height = height;
|
||||
|
||||
const maybeGl = canvas.getContext('webgl', { premultipliedAlpha: true });
|
||||
const maybeGl = canvas.getContext('webgl2', { premultipliedAlpha: true });
|
||||
if (maybeGl == null) return;
|
||||
|
||||
const gl = maybeGl;
|
||||
|
@ -82,18 +42,16 @@ onMounted(() => {
|
|||
const positionBuffer = gl.createBuffer();
|
||||
gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
|
||||
|
||||
const shaderProgram = initShaderProgram(gl, `
|
||||
attribute vec2 vertex;
|
||||
|
||||
const shaderProgram = initShaderProgram(gl, `#version 300 es
|
||||
in vec2 position;
|
||||
uniform vec2 u_scale;
|
||||
|
||||
varying vec2 v_pos;
|
||||
out vec2 in_uv;
|
||||
|
||||
void main() {
|
||||
gl_Position = vec4(vertex, 0.0, 1.0);
|
||||
v_pos = vertex / u_scale;
|
||||
gl_Position = vec4(position, 0.0, 1.0);
|
||||
in_uv = position / u_scale;
|
||||
}
|
||||
`, `
|
||||
`, `#version 300 es
|
||||
precision mediump float;
|
||||
|
||||
vec3 mod289(vec3 x) {
|
||||
|
@ -143,6 +101,7 @@ onMounted(() => {
|
|||
return 130.0 * dot(m, g);
|
||||
}
|
||||
|
||||
in vec2 in_uv;
|
||||
uniform float u_time;
|
||||
uniform vec2 u_resolution;
|
||||
uniform float u_spread;
|
||||
|
@ -150,8 +109,7 @@ onMounted(() => {
|
|||
uniform float u_warp;
|
||||
uniform float u_focus;
|
||||
uniform float u_itensity;
|
||||
|
||||
varying vec2 v_pos;
|
||||
out vec4 out_color;
|
||||
|
||||
float circle( in vec2 _pos, in vec2 _origin, in float _radius ) {
|
||||
float SPREAD = 0.7 * u_spread;
|
||||
|
@ -182,13 +140,13 @@ onMounted(() => {
|
|||
|
||||
float ratio = u_resolution.x / u_resolution.y;
|
||||
|
||||
vec2 uv = vec2( v_pos.x, v_pos.y / ratio ) * 0.5 + 0.5;
|
||||
vec2 uv = vec2( in_uv.x, in_uv.y / ratio ) * 0.5 + 0.5;
|
||||
|
||||
vec3 color = vec3( 0.0 );
|
||||
|
||||
float greenMix = snoise( v_pos * 1.31 + u_time * 0.8 * 0.00017 ) * 0.5 + 0.5;
|
||||
float purpleMix = snoise( v_pos * 1.26 + u_time * 0.8 * -0.0001 ) * 0.5 + 0.5;
|
||||
float orangeMix = snoise( v_pos * 1.34 + u_time * 0.8 * 0.00015 ) * 0.5 + 0.5;
|
||||
float greenMix = snoise( in_uv * 1.31 + u_time * 0.8 * 0.00017 ) * 0.5 + 0.5;
|
||||
float purpleMix = snoise( in_uv * 1.26 + u_time * 0.8 * -0.0001 ) * 0.5 + 0.5;
|
||||
float orangeMix = snoise( in_uv * 1.34 + u_time * 0.8 * 0.00015 ) * 0.5 + 0.5;
|
||||
|
||||
float alphaOne = 0.35 + 0.65 * pow( snoise( vec2( u_time * 0.00012, uv.x ) ) * 0.5 + 0.5, 1.2 );
|
||||
float alphaTwo = 0.35 + 0.65 * pow( snoise( vec2( ( u_time + 1561.0 ) * 0.00014, uv.x ) ) * 0.5 + 0.5, 1.2 );
|
||||
|
@ -198,10 +156,10 @@ onMounted(() => {
|
|||
color += vec3( circle( uv, vec2( 0.90 + cos( u_time * 0.000166 ) * 0.06, 0.42 + sin( u_time * 0.000138 ) * 0.06 ), 0.18 ) ) * alphaTwo * ( green * greenMix + purple * purpleMix );
|
||||
color += vec3( circle( uv, vec2( 0.19 + sin( u_time * 0.000112 ) * 0.06, 0.25 + sin( u_time * 0.000192 ) * 0.06 ), 0.09 ) ) * alphaThree * ( orange * orangeMix );
|
||||
|
||||
color *= u_itensity + 1.0 * pow( snoise( vec2( v_pos.y + u_time * 0.00013, v_pos.x + u_time * -0.00009 ) ) * 0.5 + 0.5, 2.0 );
|
||||
color *= u_itensity + 1.0 * pow( snoise( vec2( in_uv.y + u_time * 0.00013, in_uv.x + u_time * -0.00009 ) ) * 0.5 + 0.5, 2.0 );
|
||||
|
||||
vec3 inverted = vec3( 1.0 ) - color;
|
||||
gl_FragColor = vec4( color, max(max(color.x, color.y), color.z) );
|
||||
out_color = vec4(color, max(max(color.x, color.y), color.z));
|
||||
}
|
||||
`);
|
||||
if (shaderProgram == null) return;
|
||||
|
@ -223,7 +181,7 @@ onMounted(() => {
|
|||
gl.uniform1f(u_itensity, 0.5);
|
||||
gl.uniform2fv(u_scale, [props.scale, props.scale]);
|
||||
|
||||
const vertex = gl.getAttribLocation(shaderProgram, 'vertex');
|
||||
const vertex = gl.getAttribLocation(shaderProgram, 'position');
|
||||
gl.enableVertexAttribArray(vertex);
|
||||
gl.vertexAttribPointer(vertex, 2, gl.FLOAT, false, 0, 0);
|
||||
|
||||
|
|
|
@ -10,17 +10,11 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<MkInput v-model="name">
|
||||
<template #label>{{ i18n.ts.name }}</template>
|
||||
</MkInput>
|
||||
<MkSelect v-model="src">
|
||||
<MkSelect v-model="src" :items="antennaSourcesSelectDef">
|
||||
<template #label>{{ i18n.ts.antennaSource }}</template>
|
||||
<option value="all">{{ i18n.ts._antennaSources.all }}</option>
|
||||
<!--<option value="home">{{ i18n.ts._antennaSources.homeTimeline }}</option>-->
|
||||
<option value="users">{{ i18n.ts._antennaSources.users }}</option>
|
||||
<!--<option value="list">{{ i18n.ts._antennaSources.userList }}</option>-->
|
||||
<option value="users_blacklist">{{ i18n.ts._antennaSources.userBlacklist }}</option>
|
||||
</MkSelect>
|
||||
<MkSelect v-if="src === 'list'" v-model="userListId">
|
||||
<MkSelect v-if="src === 'list'" v-model="userListId" :items="userListsSelectDef">
|
||||
<template #label>{{ i18n.ts.userList }}</template>
|
||||
<option v-for="list in userLists" :key="list.id" :value="list.id">{{ list.name }}</option>
|
||||
</MkSelect>
|
||||
<MkTextarea v-else-if="src === 'users' || src === 'users_blacklist'" v-model="users">
|
||||
<template #label>{{ i18n.ts.users }}</template>
|
||||
|
@ -52,7 +46,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { watch, ref } from 'vue';
|
||||
import { watch, ref, computed } from 'vue';
|
||||
import * as Misskey from 'misskey-js';
|
||||
import type { DeepPartial } from '@/utility/merge.js';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
|
@ -64,6 +58,7 @@ import * as os from '@/os.js';
|
|||
import { misskeyApi } from '@/utility/misskey-api.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { deepMerge } from '@/utility/merge.js';
|
||||
import { useMkSelect } from '@/composables/use-mkselect.js';
|
||||
|
||||
type PartialAllowedAntenna = Omit<Misskey.entities.Antenna, 'id' | 'createdAt' | 'updatedAt'> & {
|
||||
id?: string;
|
||||
|
@ -99,9 +94,35 @@ const emit = defineEmits<{
|
|||
(ev: 'deleted'): void,
|
||||
}>();
|
||||
|
||||
const {
|
||||
model: src,
|
||||
def: antennaSourcesSelectDef,
|
||||
} = useMkSelect({
|
||||
items: [
|
||||
{ value: 'all', label: i18n.ts._antennaSources.all },
|
||||
//{ value: 'home', label: i18n.ts._antennaSources.homeTimeline },
|
||||
{ value: 'users', label: i18n.ts._antennaSources.users },
|
||||
//{ value: 'list', label: i18n.ts._antennaSources.userList },
|
||||
{ value: 'users_blacklist', label: i18n.ts._antennaSources.userBlacklist },
|
||||
],
|
||||
initialValue: initialAntenna.src,
|
||||
});
|
||||
|
||||
const {
|
||||
model: userListId,
|
||||
def: userListsSelectDef,
|
||||
} = useMkSelect({
|
||||
items: computed(() => {
|
||||
if (userLists.value == null) return [];
|
||||
return userLists.value.map(list => ({
|
||||
value: list.id,
|
||||
label: list.name,
|
||||
}));
|
||||
}),
|
||||
initialValue: initialAntenna.userListId,
|
||||
});
|
||||
|
||||
const name = ref<string>(initialAntenna.name);
|
||||
const src = ref<Misskey.entities.AntennasCreateRequest['src']>(initialAntenna.src);
|
||||
const userListId = ref<string | null>(initialAntenna.userListId);
|
||||
const users = ref<string>(initialAntenna.users.join('\n'));
|
||||
const keywords = ref<string>(initialAntenna.keywords.map(x => x.join(' ')).join('\n'));
|
||||
const excludeKeywords = ref<string>(initialAntenna.excludeKeywords.map(x => x.join(' ')).join('\n'));
|
||||
|
|
|
@ -32,10 +32,9 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<template v-if="c.label" #label>{{ c.label }}</template>
|
||||
<template v-if="c.caption" #caption>{{ c.caption }}</template>
|
||||
</MkInput>
|
||||
<MkSelect v-else-if="c.type === 'select'" :small="size === 'small'" :modelValue="valueForSelect" @update:modelValue="onSelectUpdate">
|
||||
<MkSelect v-else-if="c.type === 'select'" :small="size === 'small'" :modelValue="valueForSelect" :items="selectDef" @update:modelValue="onSelectUpdate">
|
||||
<template v-if="c.label" #label>{{ c.label }}</template>
|
||||
<template v-if="c.caption" #caption>{{ c.caption }}</template>
|
||||
<option v-for="item in c.items" :key="item.value" :value="item.value">{{ item.text }}</option>
|
||||
</MkSelect>
|
||||
<MkButton v-else-if="c.type === 'postFormButton'" :primary="c.primary" :rounded="c.rounded" :small="size === 'small'" inline @click="openPostForm">{{ c.text }}</MkButton>
|
||||
<div v-else-if="c.type === 'postForm'" :class="$style.postForm">
|
||||
|
@ -74,6 +73,7 @@ import MkSelect from '@/components/MkSelect.vue';
|
|||
import type { AsUiComponent, AsUiRoot, AsUiPostFormButton } from '@/aiscript/ui.js';
|
||||
import MkFolder from '@/components/MkFolder.vue';
|
||||
import MkPostForm from '@/components/MkPostForm.vue';
|
||||
import { useMkSelect } from '@/composables/use-mkselect.js';
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
component: AsUiComponent;
|
||||
|
@ -130,7 +130,19 @@ function onSwitchUpdate(v: boolean) {
|
|||
}
|
||||
}
|
||||
|
||||
const valueForSelect = ref('default' in c && typeof c.default !== 'boolean' ? c.default ?? null : null);
|
||||
const {
|
||||
model: valueForSelect,
|
||||
def: selectDef,
|
||||
} = useMkSelect({
|
||||
items: computed(() => {
|
||||
if (c.type !== 'select') return [];
|
||||
return (c.items ?? []).map(item => ({
|
||||
value: item.value,
|
||||
label: item.text,
|
||||
}));
|
||||
}),
|
||||
initialValue: (c.type === 'select' && 'default' in c && typeof c.default !== 'boolean') ? c.default ?? null : null,
|
||||
});
|
||||
|
||||
function onSelectUpdate(v) {
|
||||
valueForSelect.value = v;
|
||||
|
|
|
@ -29,6 +29,6 @@ const users = ref<Misskey.entities.UserLite[]>([]);
|
|||
onMounted(async () => {
|
||||
users.value = await misskeyApi('users/show', {
|
||||
userIds: props.userIds,
|
||||
}) as unknown as Misskey.entities.UserLite[];
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
|
|
@ -29,16 +29,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<span v-else-if="okButtonDisabledReason === 'charactersBelow'" v-text="i18n.tsx._dialog.charactersBelow({ current: (inputValue as string)?.length ?? 0, min: input.minLength ?? 'NaN' })"/>
|
||||
</template>
|
||||
</MkInput>
|
||||
<MkSelect v-if="select" v-model="selectedValue" autofocus>
|
||||
<template v-if="select.items">
|
||||
<template v-for="item in select.items">
|
||||
<optgroup v-if="'sectionTitle' in item" :label="item.sectionTitle">
|
||||
<option v-for="subItem in item.items" :value="subItem.value">{{ subItem.text }}</option>
|
||||
</optgroup>
|
||||
<option v-else :value="item.value">{{ item.text }}</option>
|
||||
</template>
|
||||
</template>
|
||||
</MkSelect>
|
||||
<MkSelect v-if="select" v-model="selectedValue" :items="selectDef" autofocus></MkSelect>
|
||||
<div v-if="(showOkButton || showCancelButton) && !actions" :class="$style.buttons">
|
||||
<MkButton v-if="showOkButton" data-cy-modal-dialog-ok inline primary rounded :autofocus="!input && !select" :disabled="okButtonDisabledReason != null" @click="ok">{{ okText ?? ((showCancelButton || input || select) ? i18n.ts.ok : i18n.ts.gotIt) }}</MkButton>
|
||||
<MkButton v-if="showCancelButton || input || select" data-cy-modal-dialog-cancel inline rounded @click="cancel">{{ cancelText ?? i18n.ts.cancel }}</MkButton>
|
||||
|
@ -56,6 +47,8 @@ import MkModal from '@/components/MkModal.vue';
|
|||
import MkButton from '@/components/MkButton.vue';
|
||||
import MkInput from '@/components/MkInput.vue';
|
||||
import MkSelect from '@/components/MkSelect.vue';
|
||||
import type { MkSelectItem, OptionValue } from '@/components/MkSelect.vue';
|
||||
import { useMkSelect } from '@/composables/use-mkselect.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
|
||||
type Input = {
|
||||
|
@ -67,17 +60,9 @@ type Input = {
|
|||
maxLength?: number;
|
||||
};
|
||||
|
||||
type SelectItem = {
|
||||
value: any;
|
||||
text: string;
|
||||
};
|
||||
|
||||
type Select = {
|
||||
items: (SelectItem | {
|
||||
sectionTitle: string;
|
||||
items: SelectItem[];
|
||||
})[];
|
||||
default: string | null;
|
||||
items: MkSelectItem[];
|
||||
default: OptionValue | null;
|
||||
};
|
||||
|
||||
type Result = string | number | true | null;
|
||||
|
@ -115,7 +100,6 @@ const emit = defineEmits<{
|
|||
const modal = useTemplateRef('modal');
|
||||
|
||||
const inputValue = ref<string | number | null>(props.input?.default ?? null);
|
||||
const selectedValue = ref(props.select?.default ?? null);
|
||||
|
||||
const okButtonDisabledReason = computed<null | 'charactersExceeded' | 'charactersBelow'>(() => {
|
||||
if (props.input) {
|
||||
|
@ -134,6 +118,14 @@ const okButtonDisabledReason = computed<null | 'charactersExceeded' | 'character
|
|||
return null;
|
||||
});
|
||||
|
||||
const {
|
||||
def: selectDef,
|
||||
model: selectedValue,
|
||||
} = useMkSelect({
|
||||
items: computed(() => props.select?.items ?? []),
|
||||
initialValue: props.select?.default ?? null,
|
||||
});
|
||||
|
||||
// overload function を使いたいので lint エラーを無視する
|
||||
function done(canceled: true): void;
|
||||
function done(canceled: false, result: Result): void; // eslint-disable-line no-redeclare
|
||||
|
|
|
@ -52,11 +52,8 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<template #suffix>px</template>
|
||||
<template #caption>{{ i18n.ts._embedCodeGen.maxHeightDescription }}</template>
|
||||
</MkInput>
|
||||
<MkSelect v-model="colorMode">
|
||||
<MkSelect v-model="colorMode" :items="colorModeDef">
|
||||
<template #label>{{ i18n.ts.theme }}</template>
|
||||
<option value="auto">{{ i18n.ts.syncDeviceDarkMode }}</option>
|
||||
<option value="light">{{ i18n.ts.light }}</option>
|
||||
<option value="dark">{{ i18n.ts.dark }}</option>
|
||||
</MkSelect>
|
||||
<MkSwitch v-if="isEmbedWithScrollbar" v-model="header">{{ i18n.ts._embedCodeGen.header }}</MkSwitch>
|
||||
<MkSwitch v-model="rounded">{{ i18n.ts._embedCodeGen.rounded }}</MkSwitch>
|
||||
|
@ -105,6 +102,7 @@ import MkInfo from '@/components/MkInfo.vue';
|
|||
|
||||
import * as os from '@/os.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { useMkSelect } from '@/composables/use-mkselect.js';
|
||||
import { copyToClipboard } from '@/utility/copy-to-clipboard.js';
|
||||
import { normalizeEmbedParams, getEmbedCode } from '@/utility/get-embed-code.js';
|
||||
|
||||
|
@ -162,7 +160,18 @@ const isEmbedWithScrollbar = computed(() => embedRouteWithScrollbar.includes(pro
|
|||
const header = ref(props.params?.header ?? true);
|
||||
const maxHeight = ref(props.params?.maxHeight !== 0 ? props.params?.maxHeight ?? null : 500);
|
||||
|
||||
const colorMode = ref<'light' | 'dark' | 'auto'>(props.params?.colorMode ?? 'auto');
|
||||
const {
|
||||
model: colorMode,
|
||||
def: colorModeDef,
|
||||
} = useMkSelect({
|
||||
items: [
|
||||
{ value: 'auto', label: i18n.ts.syncDeviceDarkMode },
|
||||
{ value: 'light', label: i18n.ts.light },
|
||||
{ value: 'dark', label: i18n.ts.dark },
|
||||
],
|
||||
initialValue: props.params?.colorMode ?? 'auto',
|
||||
});
|
||||
|
||||
const rounded = ref(props.params?.rounded ?? true);
|
||||
const border = ref(props.params?.border ?? true);
|
||||
|
||||
|
|
|
@ -530,6 +530,14 @@ defineExpose({
|
|||
--eachSize: 50px;
|
||||
}
|
||||
|
||||
&.s4 {
|
||||
--eachSize: 55px;
|
||||
}
|
||||
|
||||
&.s5 {
|
||||
--eachSize: 60px;
|
||||
}
|
||||
|
||||
&.w1 {
|
||||
width: calc((var(--eachSize) * 5) + (#{$pad} * 2));
|
||||
--columns: 1fr 1fr 1fr 1fr 1fr;
|
||||
|
|
|
@ -39,9 +39,8 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<span v-text="v.label || k"></span>
|
||||
<template v-if="v.description" #caption>{{ v.description }}</template>
|
||||
</MkSwitch>
|
||||
<MkSelect v-else-if="v.type === 'enum'" v-model="values[k]">
|
||||
<MkSelect v-else-if="v.type === 'enum'" v-model="values[k]" :items="getMkSelectDef(v)">
|
||||
<template #label><span v-text="v.label || k"></span><span v-if="v.required === false"> ({{ i18n.ts.optional }})</span></template>
|
||||
<option v-for="option in v.enum" :key="getEnumKey(option)" :value="getEnumValue(option)">{{ getEnumLabel(option) }}</option>
|
||||
</MkSelect>
|
||||
<MkRadios v-else-if="v.type === 'radio'" v-model="values[k]">
|
||||
<template #label><span v-text="v.label || k"></span><span v-if="v.required === false"> ({{ i18n.ts.optional }})</span></template>
|
||||
|
@ -77,7 +76,8 @@ import MkRange from './MkRange.vue';
|
|||
import MkButton from './MkButton.vue';
|
||||
import MkRadios from './MkRadios.vue';
|
||||
import XFile from './MkFormDialog.file.vue';
|
||||
import type { EnumItem, Form, RadioFormItem } from '@/utility/form.js';
|
||||
import type { MkSelectItem } from '@/components/MkSelect.vue';
|
||||
import type { Form, EnumFormItem, RadioFormItem } from '@/utility/form.js';
|
||||
import MkModalWindow from '@/components/MkModalWindow.vue';
|
||||
import { i18n } from '@/i18n.js';
|
||||
|
||||
|
@ -120,16 +120,14 @@ function cancel() {
|
|||
dialog.value?.close();
|
||||
}
|
||||
|
||||
function getEnumLabel(e: EnumItem) {
|
||||
return typeof e === 'string' ? e : e.label;
|
||||
}
|
||||
|
||||
function getEnumValue(e: EnumItem) {
|
||||
return typeof e === 'string' ? e : e.value;
|
||||
}
|
||||
|
||||
function getEnumKey(e: EnumItem) {
|
||||
return typeof e === 'string' ? e : typeof e.value === 'string' ? e.value : JSON.stringify(e.value);
|
||||
function getMkSelectDef(def: EnumFormItem): MkSelectItem[] {
|
||||
return def.enum.map((v) => {
|
||||
if (typeof v === 'string') {
|
||||
return { value: v, label: v };
|
||||
} else {
|
||||
return { value: v.value, label: v.label };
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function getRadioKey(e: RadioFormItem['options'][number]) {
|
||||
|
|
|
@ -19,9 +19,12 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<div :class="$style.root">
|
||||
<div :class="$style.container">
|
||||
<div :class="$style.preview">
|
||||
<canvas ref="canvasEl" :class="$style.previewCanvas"></canvas>
|
||||
<canvas ref="canvasEl" :class="$style.previewCanvas" @pointerdown="onImagePointerdown"></canvas>
|
||||
<div :class="$style.previewContainer">
|
||||
<div class="_acrylic" :class="$style.previewTitle">{{ i18n.ts.preview }}</div>
|
||||
<div class="_acrylic" :class="$style.editControls">
|
||||
<button class="_button" :class="[$style.previewControlsButton, penMode != null ? $style.active : null]" @click="showPenMenu"><i class="ti ti-pencil"></i></button>
|
||||
</div>
|
||||
<div class="_acrylic" :class="$style.previewControls">
|
||||
<button class="_button" :class="[$style.previewControlsButton, !enabled ? $style.active : null]" @click="enabled = false">Before</button>
|
||||
<button class="_button" :class="[$style.previewControlsButton, enabled ? $style.active : null]" @click="enabled = true">After</button>
|
||||
|
@ -212,6 +215,147 @@ watch(enabled, () => {
|
|||
renderer.render();
|
||||
}
|
||||
});
|
||||
|
||||
const penMode = ref<'fill' | 'blur' | 'pixelate' | null>(null);
|
||||
|
||||
function showPenMenu(ev: MouseEvent) {
|
||||
os.popupMenu([{
|
||||
text: i18n.ts._imageEffector._fxs.fill,
|
||||
action: () => {
|
||||
penMode.value = 'fill';
|
||||
},
|
||||
}, {
|
||||
text: i18n.ts._imageEffector._fxs.blur,
|
||||
action: () => {
|
||||
penMode.value = 'blur';
|
||||
},
|
||||
}, {
|
||||
text: i18n.ts._imageEffector._fxs.pixelate,
|
||||
action: () => {
|
||||
penMode.value = 'pixelate';
|
||||
},
|
||||
}], ev.currentTarget ?? ev.target);
|
||||
}
|
||||
|
||||
function onImagePointerdown(ev: PointerEvent) {
|
||||
if (canvasEl.value == null || imageBitmap == null || penMode.value == null) return;
|
||||
|
||||
const AW = canvasEl.value.clientWidth;
|
||||
const AH = canvasEl.value.clientHeight;
|
||||
const BW = imageBitmap.width;
|
||||
const BH = imageBitmap.height;
|
||||
|
||||
let xOffset = 0;
|
||||
let yOffset = 0;
|
||||
|
||||
if (AW / AH < BW / BH) { // 横長
|
||||
yOffset = AH - BH * (AW / BW);
|
||||
} else { // 縦長
|
||||
xOffset = AW - BW * (AH / BH);
|
||||
}
|
||||
|
||||
xOffset /= 2;
|
||||
yOffset /= 2;
|
||||
|
||||
let startX = ev.offsetX - xOffset;
|
||||
let startY = ev.offsetY - yOffset;
|
||||
|
||||
if (AW / AH < BW / BH) { // 横長
|
||||
startX = startX / (Math.max(AW, AH) / Math.max(BH / BW, 1));
|
||||
startY = startY / (Math.max(AW, AH) / Math.max(BW / BH, 1));
|
||||
} else { // 縦長
|
||||
startX = startX / (Math.min(AW, AH) / Math.max(BH / BW, 1));
|
||||
startY = startY / (Math.min(AW, AH) / Math.max(BW / BH, 1));
|
||||
}
|
||||
|
||||
const id = genId();
|
||||
if (penMode.value === 'fill') {
|
||||
layers.push({
|
||||
id,
|
||||
fxId: 'fill',
|
||||
params: {
|
||||
offsetX: 0,
|
||||
offsetY: 0,
|
||||
scaleX: 0.1,
|
||||
scaleY: 0.1,
|
||||
angle: 0,
|
||||
opacity: 1,
|
||||
color: [1, 1, 1],
|
||||
},
|
||||
});
|
||||
} else if (penMode.value === 'blur') {
|
||||
layers.push({
|
||||
id,
|
||||
fxId: 'blur',
|
||||
params: {
|
||||
offsetX: 0,
|
||||
offsetY: 0,
|
||||
scaleX: 0.1,
|
||||
scaleY: 0.1,
|
||||
angle: 0,
|
||||
radius: 3,
|
||||
},
|
||||
});
|
||||
} else if (penMode.value === 'pixelate') {
|
||||
layers.push({
|
||||
id,
|
||||
fxId: 'pixelate',
|
||||
params: {
|
||||
offsetX: 0,
|
||||
offsetY: 0,
|
||||
scaleX: 0.1,
|
||||
scaleY: 0.1,
|
||||
angle: 0,
|
||||
strength: 0.2,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
_move(ev.offsetX, ev.offsetY);
|
||||
|
||||
function _move(pointerX: number, pointerY: number) {
|
||||
let x = pointerX - xOffset;
|
||||
let y = pointerY - yOffset;
|
||||
|
||||
if (AW / AH < BW / BH) { // 横長
|
||||
x = x / (Math.max(AW, AH) / Math.max(BH / BW, 1));
|
||||
y = y / (Math.max(AW, AH) / Math.max(BW / BH, 1));
|
||||
} else { // 縦長
|
||||
x = x / (Math.min(AW, AH) / Math.max(BH / BW, 1));
|
||||
y = y / (Math.min(AW, AH) / Math.max(BW / BH, 1));
|
||||
}
|
||||
|
||||
const scaleX = Math.abs(x - startX);
|
||||
const scaleY = Math.abs(y - startY);
|
||||
|
||||
const layerIndex = layers.findIndex((l) => l.id === id);
|
||||
const layer = layerIndex !== -1 ? layers[layerIndex] : null;
|
||||
if (layer != null) {
|
||||
layer.params.offsetX = (x + startX) - 1;
|
||||
layer.params.offsetY = (y + startY) - 1;
|
||||
layer.params.scaleX = scaleX;
|
||||
layer.params.scaleY = scaleY;
|
||||
layers[layerIndex] = layer;
|
||||
}
|
||||
}
|
||||
|
||||
function move(ev: PointerEvent) {
|
||||
_move(ev.offsetX, ev.offsetY);
|
||||
}
|
||||
|
||||
function up() {
|
||||
canvasEl.value?.removeEventListener('pointermove', move);
|
||||
canvasEl.value?.removeEventListener('pointerup', up);
|
||||
canvasEl.value?.removeEventListener('pointercancel', up);
|
||||
canvasEl.value?.releasePointerCapture(ev.pointerId);
|
||||
|
||||
penMode.value = null;
|
||||
}
|
||||
|
||||
canvasEl.value.addEventListener('pointermove', move);
|
||||
canvasEl.value.addEventListener('pointerup', up);
|
||||
canvasEl.value.setPointerCapture(ev.pointerId);
|
||||
}
|
||||
</script>
|
||||
|
||||
<style module>
|
||||
|
@ -251,6 +395,18 @@ watch(enabled, () => {
|
|||
font-size: 85%;
|
||||
}
|
||||
|
||||
.editControls {
|
||||
position: absolute;
|
||||
z-index: 100;
|
||||
bottom: 8px;
|
||||
left: 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 6px 10px;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.previewControls {
|
||||
position: absolute;
|
||||
z-index: 100;
|
||||
|
@ -283,9 +439,13 @@ watch(enabled, () => {
|
|||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
padding: 20px;
|
||||
/* なんかiOSでレンダリングがおかしい
|
||||
width: stretch;
|
||||
height: stretch;
|
||||
*/
|
||||
width: calc(100% - 40px);
|
||||
height: calc(100% - 40px);
|
||||
margin: 20px;
|
||||
box-sizing: border-box;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
|
|
@ -9,31 +9,8 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<template #header>Chart</template>
|
||||
<div :class="$style.chart">
|
||||
<div class="selects">
|
||||
<MkSelect v-model="chartSrc" style="margin: 0; flex: 1;">
|
||||
<optgroup v-if="shouldShowFederation" :label="i18n.ts.federation">
|
||||
<option value="federation">{{ i18n.ts._charts.federation }}</option>
|
||||
<option value="ap-request">{{ i18n.ts._charts.apRequest }}</option>
|
||||
</optgroup>
|
||||
<optgroup :label="i18n.ts.users">
|
||||
<option value="users">{{ i18n.ts._charts.usersIncDec }}</option>
|
||||
<option value="users-total">{{ i18n.ts._charts.usersTotal }}</option>
|
||||
<option value="active-users">{{ i18n.ts._charts.activeUsers }}</option>
|
||||
</optgroup>
|
||||
<optgroup :label="i18n.ts.notes">
|
||||
<option value="notes">{{ i18n.ts._charts.notesIncDec }}</option>
|
||||
<option value="local-notes">{{ i18n.ts._charts.localNotesIncDec }}</option>
|
||||
<option v-if="shouldShowFederation" value="remote-notes">{{ i18n.ts._charts.remoteNotesIncDec }}</option>
|
||||
<option value="notes-total">{{ i18n.ts._charts.notesTotal }}</option>
|
||||
</optgroup>
|
||||
<optgroup :label="i18n.ts.drive">
|
||||
<option value="drive-files">{{ i18n.ts._charts.filesIncDec }}</option>
|
||||
<option value="drive">{{ i18n.ts._charts.storageUsageIncDec }}</option>
|
||||
</optgroup>
|
||||
</MkSelect>
|
||||
<MkSelect v-model="chartSpan" style="margin: 0 0 0 10px;">
|
||||
<option value="hour">{{ i18n.ts.perHour }}</option>
|
||||
<option value="day">{{ i18n.ts.perDay }}</option>
|
||||
</MkSelect>
|
||||
<MkSelect v-model="chartSrc" :items="chartSrcDef" style="margin: 0; flex: 1;"></MkSelect>
|
||||
<MkSelect v-model="chartSpan" :items="chartSpanDef" style="margin: 0 0 0 10px;"></MkSelect>
|
||||
</div>
|
||||
<div class="chart _panel">
|
||||
<MkChart :src="chartSrc" :span="chartSpan" :limit="chartLimit" :detailed="true"></MkChart>
|
||||
|
@ -43,13 +20,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
|
||||
<MkFoldableSection class="item">
|
||||
<template #header>Active users heatmap</template>
|
||||
<MkSelect v-model="heatmapSrc" style="margin: 0 0 12px 0;">
|
||||
<option value="active-users">Active users</option>
|
||||
<option value="notes">Notes</option>
|
||||
<option v-if="shouldShowFederation" value="ap-requests-inbox-received">AP Requests: inboxReceived</option>
|
||||
<option v-if="shouldShowFederation" value="ap-requests-deliver-succeeded">AP Requests: deliverSucceeded</option>
|
||||
<option v-if="shouldShowFederation" value="ap-requests-deliver-failed">AP Requests: deliverFailed</option>
|
||||
</MkSelect>
|
||||
<MkSelect v-model="heatmapSrc" :items="heatmapSrcDef" style="margin: 0 0 12px 0;"></MkSelect>
|
||||
<div class="_panel" :class="$style.heatmap">
|
||||
<MkHeatmap :src="heatmapSrc" :label="'Read & Write'"/>
|
||||
</div>
|
||||
|
@ -84,10 +55,10 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { onMounted, ref, computed, useTemplateRef } from 'vue';
|
||||
import { onMounted, computed, useTemplateRef } from 'vue';
|
||||
import { Chart } from 'chart.js';
|
||||
import type { HeatmapSource } from '@/components/MkHeatmap.vue';
|
||||
import MkSelect from '@/components/MkSelect.vue';
|
||||
import type { MkSelectItem, ItemOption } from '@/components/MkSelect.vue';
|
||||
import MkChart from '@/components/MkChart.vue';
|
||||
import type { ChartSrc } from '@/components/MkChart.vue';
|
||||
import { useChartTooltip } from '@/composables/use-chart-tooltip.js';
|
||||
|
@ -101,15 +72,96 @@ import MkFoldableSection from '@/components/MkFoldableSection.vue';
|
|||
import MkRetentionHeatmap from '@/components/MkRetentionHeatmap.vue';
|
||||
import MkRetentionLineChart from '@/components/MkRetentionLineChart.vue';
|
||||
import { initChart } from '@/utility/init-chart.js';
|
||||
import { useMkSelect } from '@/composables/use-mkselect.js';
|
||||
|
||||
initChart();
|
||||
|
||||
const shouldShowFederation = computed(() => instance.federation !== 'none' || $i?.isModerator);
|
||||
|
||||
const chartLimit = 500;
|
||||
const chartSpan = ref<'hour' | 'day'>('hour');
|
||||
const chartSrc = ref<ChartSrc>('active-users');
|
||||
const heatmapSrc = ref<HeatmapSource>('active-users');
|
||||
const {
|
||||
model: chartSpan,
|
||||
def: chartSpanDef,
|
||||
} = useMkSelect({
|
||||
items: [
|
||||
{ value: 'hour', label: i18n.ts.perHour },
|
||||
{ value: 'day', label: i18n.ts.perDay },
|
||||
],
|
||||
initialValue: 'hour',
|
||||
});
|
||||
const {
|
||||
model: chartSrc,
|
||||
def: chartSrcDef,
|
||||
} = useMkSelect({
|
||||
items: computed<MkSelectItem<ChartSrc>[]>(() => {
|
||||
const items: MkSelectItem<ChartSrc>[] = [];
|
||||
|
||||
if (shouldShowFederation.value) {
|
||||
items.push({
|
||||
type: 'group',
|
||||
label: i18n.ts.federation,
|
||||
items: [
|
||||
{ value: 'federation', label: i18n.ts._charts.federation },
|
||||
{ value: 'ap-request', label: i18n.ts._charts.apRequest },
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
items.push({
|
||||
type: 'group',
|
||||
label: i18n.ts.users,
|
||||
items: [
|
||||
{ value: 'users', label: i18n.ts._charts.usersIncDec },
|
||||
{ value: 'users-total', label: i18n.ts._charts.usersTotal },
|
||||
{ value: 'active-users', label: i18n.ts._charts.activeUsers },
|
||||
],
|
||||
});
|
||||
|
||||
const notesItems: ItemOption<ChartSrc>[] = [
|
||||
{ value: 'notes', label: i18n.ts._charts.notesIncDec },
|
||||
{ value: 'local-notes', label: i18n.ts._charts.localNotesIncDec },
|
||||
];
|
||||
|
||||
if (shouldShowFederation.value) notesItems.push({ value: 'remote-notes', label: i18n.ts._charts.remoteNotesIncDec });
|
||||
|
||||
notesItems.push(
|
||||
{ value: 'notes-total', label: i18n.ts._charts.notesTotal },
|
||||
);
|
||||
|
||||
items.push({
|
||||
type: 'group',
|
||||
label: i18n.ts.notes,
|
||||
items: notesItems,
|
||||
});
|
||||
|
||||
items.push({
|
||||
type: 'group',
|
||||
label: i18n.ts.drive,
|
||||
items: [
|
||||
{ value: 'drive-files', label: i18n.ts._charts.filesIncDec },
|
||||
{ value: 'drive', label: i18n.ts._charts.storageUsageIncDec },
|
||||
],
|
||||
});
|
||||
|
||||
return items;
|
||||
}),
|
||||
initialValue: 'active-users',
|
||||
});
|
||||
const {
|
||||
model: heatmapSrc,
|
||||
def: heatmapSrcDef,
|
||||
} = useMkSelect({
|
||||
items: computed(() => [
|
||||
{ value: 'active-users' as const, label: 'Active Users' },
|
||||
{ value: 'notes' as const, label: 'Notes' },
|
||||
...(shouldShowFederation.value ? [
|
||||
{ value: 'ap-requests-inbox-received' as const, label: 'AP Requests: inboxReceived' },
|
||||
{ value: 'ap-requests-deliver-succeeded' as const, label: 'AP Requests: deliverSucceeded' },
|
||||
{ value: 'ap-requests-deliver-failed' as const, label: 'AP Requests: deliverFailed' },
|
||||
] : []),
|
||||
]),
|
||||
initialValue: 'active-users',
|
||||
});
|
||||
const subDoughnutEl = useTemplateRef('subDoughnutEl');
|
||||
const pubDoughnutEl = useTemplateRef('pubDoughnutEl');
|
||||
|
||||
|
|
|
@ -15,101 +15,151 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
@esc="cancel()"
|
||||
>
|
||||
<template #header>
|
||||
{{ i18n.ts.drafts }} ({{ currentDraftsCount }}/{{ $i?.policies.noteDraftLimit }})
|
||||
{{ i18n.ts.draftsAndScheduledNotes }} ({{ currentDraftsCount }}/{{ $i?.policies.noteDraftLimit }})
|
||||
</template>
|
||||
<div class="_spacer">
|
||||
<MkPagination :paginator="paginator" withControl>
|
||||
<template #empty>
|
||||
<MkResult type="empty" :text="i18n.ts._drafts.noDrafts"/>
|
||||
</template>
|
||||
|
||||
<template #default="{ items }">
|
||||
<div class="_gaps_s">
|
||||
<div
|
||||
v-for="draft in (items as unknown as Misskey.entities.NoteDraft[])"
|
||||
:key="draft.id"
|
||||
v-panel
|
||||
:class="[$style.draft]"
|
||||
>
|
||||
<div :class="$style.draftBody" class="_gaps_s">
|
||||
<div :class="$style.draftInfo">
|
||||
<div :class="$style.draftMeta">
|
||||
<div v-if="draft.reply" class="_nowrap">
|
||||
<i class="ti ti-arrow-back-up"></i> <I18n :src="i18n.ts._drafts.replyTo" tag="span">
|
||||
<template #user>
|
||||
<Mfm v-if="draft.reply.user.name != null" :text="draft.reply.user.name" :plain="true" :nowrap="true"/>
|
||||
<MkAcct v-else :user="draft.reply.user"/>
|
||||
</template>
|
||||
</I18n>
|
||||
</div>
|
||||
<div v-else-if="draft.replyId" class="_nowrap">
|
||||
<i class="ti ti-arrow-back-up"></i> <I18n :src="i18n.ts._drafts.replyTo" tag="span">
|
||||
<template #user>
|
||||
{{ i18n.ts.deletedNote }}
|
||||
</template>
|
||||
</I18n>
|
||||
</div>
|
||||
<div v-if="draft.renote && draft.text != null" class="_nowrap">
|
||||
<i class="ti ti-quote"></i> <I18n :src="i18n.ts._drafts.quoteOf" tag="span">
|
||||
<template #user>
|
||||
<Mfm v-if="draft.renote.user.name != null" :text="draft.renote.user.name" :plain="true" :nowrap="true"/>
|
||||
<MkAcct v-else :user="draft.renote.user"/>
|
||||
</template>
|
||||
</I18n>
|
||||
</div>
|
||||
<div v-else-if="draft.renoteId" class="_nowrap">
|
||||
<i class="ti ti-quote"></i> <I18n :src="i18n.ts._drafts.quoteOf" tag="span">
|
||||
<template #user>
|
||||
{{ i18n.ts.deletedNote }}
|
||||
</template>
|
||||
</I18n>
|
||||
</div>
|
||||
<div v-if="draft.channel" class="_nowrap">
|
||||
<i class="ti ti-device-tv"></i> {{ i18n.tsx._drafts.postTo({ channel: draft.channel.name }) }}
|
||||
<MkStickyContainer>
|
||||
<template #header>
|
||||
<MkTabs
|
||||
v-model:tab="tab"
|
||||
centered
|
||||
:class="$style.tabs"
|
||||
:tabs="[
|
||||
{
|
||||
key: 'drafts',
|
||||
title: i18n.ts.drafts,
|
||||
icon: 'ti ti-pencil-question',
|
||||
},
|
||||
{
|
||||
key: 'scheduled',
|
||||
title: i18n.ts.scheduled,
|
||||
icon: 'ti ti-calendar-clock',
|
||||
},
|
||||
]"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<div class="_spacer">
|
||||
<MkPagination :key="tab" :paginator="tab === 'scheduled' ? scheduledPaginator : draftsPaginator" withControl>
|
||||
<template #empty>
|
||||
<MkResult type="empty" :text="i18n.ts._drafts.noDrafts"/>
|
||||
</template>
|
||||
|
||||
<template #default="{ items }">
|
||||
<div class="_gaps_s">
|
||||
<div
|
||||
v-for="draft in (items as unknown as Misskey.entities.NoteDraft[])"
|
||||
:key="draft.id"
|
||||
v-panel
|
||||
:class="[$style.draft]"
|
||||
>
|
||||
<div :class="$style.draftBody" class="_gaps_s">
|
||||
<MkInfo v-if="draft.scheduledAt != null && draft.isActuallyScheduled">
|
||||
<I18n :src="i18n.ts.scheduledToPostOnX" tag="span">
|
||||
<template #x>
|
||||
<MkTime :time="draft.scheduledAt" :mode="'detail'" style="font-weight: bold;"/>
|
||||
</template>
|
||||
</I18n>
|
||||
</MkInfo>
|
||||
<div :class="$style.draftInfo">
|
||||
<div :class="$style.draftMeta">
|
||||
<div v-if="draft.reply" class="_nowrap">
|
||||
<i class="ti ti-arrow-back-up"></i> <I18n :src="i18n.ts._drafts.replyTo" tag="span">
|
||||
<template #user>
|
||||
<Mfm v-if="draft.reply.user.name != null" :text="draft.reply.user.name" :plain="true" :nowrap="true"/>
|
||||
<MkAcct v-else :user="draft.reply.user"/>
|
||||
</template>
|
||||
</I18n>
|
||||
</div>
|
||||
<div v-else-if="draft.replyId" class="_nowrap">
|
||||
<i class="ti ti-arrow-back-up"></i> <I18n :src="i18n.ts._drafts.replyTo" tag="span">
|
||||
<template #user>
|
||||
{{ i18n.ts.deletedNote }}
|
||||
</template>
|
||||
</I18n>
|
||||
</div>
|
||||
<div v-if="draft.renote && draft.text != null" class="_nowrap">
|
||||
<i class="ti ti-quote"></i> <I18n :src="i18n.ts._drafts.quoteOf" tag="span">
|
||||
<template #user>
|
||||
<Mfm v-if="draft.renote.user.name != null" :text="draft.renote.user.name" :plain="true" :nowrap="true"/>
|
||||
<MkAcct v-else :user="draft.renote.user"/>
|
||||
</template>
|
||||
</I18n>
|
||||
</div>
|
||||
<div v-else-if="draft.renoteId" class="_nowrap">
|
||||
<i class="ti ti-quote"></i> <I18n :src="i18n.ts._drafts.quoteOf" tag="span">
|
||||
<template #user>
|
||||
{{ i18n.ts.deletedNote }}
|
||||
</template>
|
||||
</I18n>
|
||||
</div>
|
||||
<div v-if="draft.channel" class="_nowrap">
|
||||
<i class="ti ti-device-tv"></i> {{ i18n.tsx._drafts.postTo({ channel: draft.channel.name }) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div :class="$style.draftContent">
|
||||
<Mfm :text="getNoteSummary(draft, { showRenote: false, showReply: false })" :plain="true" :author="draft.user"/>
|
||||
</div>
|
||||
<div :class="$style.draftFooter">
|
||||
<div :class="$style.draftVisibility">
|
||||
<span :title="i18n.ts._visibility[draft.visibility]">
|
||||
<i v-if="draft.visibility === 'public'" class="ti ti-world"></i>
|
||||
<i v-else-if="draft.visibility === 'home'" class="ti ti-home"></i>
|
||||
<i v-else-if="draft.visibility === 'followers'" class="ti ti-lock"></i>
|
||||
<i v-else-if="draft.visibility === 'specified'" class="ti ti-mail"></i>
|
||||
</span>
|
||||
<span v-if="draft.localOnly" :title="i18n.ts._visibility['disableFederation']"><i class="ti ti-rocket-off"></i></span>
|
||||
<div :class="$style.draftContent">
|
||||
<Mfm :text="getNoteSummary(draft, { showRenote: false, showReply: false })" :plain="true" :author="draft.user"/>
|
||||
</div>
|
||||
<div :class="$style.draftFooter">
|
||||
<div :class="$style.draftVisibility">
|
||||
<span :title="i18n.ts._visibility[draft.visibility]">
|
||||
<i v-if="draft.visibility === 'public'" class="ti ti-world"></i>
|
||||
<i v-else-if="draft.visibility === 'home'" class="ti ti-home"></i>
|
||||
<i v-else-if="draft.visibility === 'followers'" class="ti ti-lock"></i>
|
||||
<i v-else-if="draft.visibility === 'specified'" class="ti ti-mail"></i>
|
||||
</span>
|
||||
<span v-if="draft.localOnly" :title="i18n.ts._visibility['disableFederation']"><i class="ti ti-rocket-off"></i></span>
|
||||
</div>
|
||||
<MkTime :time="draft.createdAt" :class="$style.draftCreatedAt" mode="detail" colored/>
|
||||
</div>
|
||||
<MkTime :time="draft.createdAt" :class="$style.draftCreatedAt" mode="detail" colored/>
|
||||
</div>
|
||||
</div>
|
||||
<div :class="$style.draftActions" class="_buttons">
|
||||
<MkButton
|
||||
:class="$style.itemButton"
|
||||
small
|
||||
@click="restoreDraft(draft)"
|
||||
>
|
||||
<i class="ti ti-corner-up-left"></i>
|
||||
{{ i18n.ts._drafts.restore }}
|
||||
</MkButton>
|
||||
<MkButton
|
||||
v-tooltip="i18n.ts._drafts.delete"
|
||||
danger
|
||||
small
|
||||
:iconOnly="true"
|
||||
:class="$style.itemButton"
|
||||
@click="deleteDraft(draft)"
|
||||
>
|
||||
<i class="ti ti-trash"></i>
|
||||
</MkButton>
|
||||
|
||||
<div :class="$style.draftActions" class="_buttons">
|
||||
<template v-if="draft.scheduledAt != null && draft.isActuallyScheduled">
|
||||
<MkButton
|
||||
:class="$style.itemButton"
|
||||
small
|
||||
@click="cancelSchedule(draft)"
|
||||
>
|
||||
<i class="ti ti-calendar-x"></i> {{ i18n.ts._drafts.cancelSchedule }}
|
||||
</MkButton>
|
||||
<!-- TODO
|
||||
<MkButton
|
||||
:class="$style.itemButton"
|
||||
small
|
||||
@click="reSchedule(draft)"
|
||||
>
|
||||
<i class="ti ti-calendar-time"></i> {{ i18n.ts._drafts.reSchedule }}
|
||||
</MkButton>
|
||||
-->
|
||||
</template>
|
||||
<MkButton
|
||||
v-else
|
||||
:class="$style.itemButton"
|
||||
small
|
||||
@click="restoreDraft(draft)"
|
||||
>
|
||||
<i class="ti ti-corner-up-left"></i> {{ i18n.ts._drafts.restore }}
|
||||
</MkButton>
|
||||
<MkButton
|
||||
v-tooltip="i18n.ts._drafts.delete"
|
||||
danger
|
||||
small
|
||||
:iconOnly="true"
|
||||
:class="$style.itemButton"
|
||||
style="margin-left: auto;"
|
||||
@click="deleteDraft(draft)"
|
||||
>
|
||||
<i class="ti ti-trash"></i>
|
||||
</MkButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</MkPagination>
|
||||
</div>
|
||||
</template>
|
||||
</MkPagination>
|
||||
</div>
|
||||
</MkStickyContainer>
|
||||
</MkModalWindow>
|
||||
</template>
|
||||
|
||||
|
@ -125,6 +175,12 @@ import * as os from '@/os.js';
|
|||
import { $i } from '@/i.js';
|
||||
import { misskeyApi } from '@/utility/misskey-api';
|
||||
import { Paginator } from '@/utility/paginator.js';
|
||||
import MkTabs from '@/components/MkTabs.vue';
|
||||
import MkInfo from '@/components/MkInfo.vue';
|
||||
|
||||
const props = defineProps<{
|
||||
scheduled?: boolean;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
(ev: 'restore', draft: Misskey.entities.NoteDraft): void;
|
||||
|
@ -132,8 +188,20 @@ const emit = defineEmits<{
|
|||
(ev: 'closed'): void;
|
||||
}>();
|
||||
|
||||
const paginator = markRaw(new Paginator('notes/drafts/list', {
|
||||
const tab = ref<'drafts' | 'scheduled'>(props.scheduled ? 'scheduled' : 'drafts');
|
||||
|
||||
const draftsPaginator = markRaw(new Paginator('notes/drafts/list', {
|
||||
limit: 10,
|
||||
params: {
|
||||
scheduled: false,
|
||||
},
|
||||
}));
|
||||
|
||||
const scheduledPaginator = markRaw(new Paginator('notes/drafts/list', {
|
||||
limit: 10,
|
||||
params: {
|
||||
scheduled: true,
|
||||
},
|
||||
}));
|
||||
|
||||
const currentDraftsCount = ref(0);
|
||||
|
@ -162,7 +230,17 @@ async function deleteDraft(draft: Misskey.entities.NoteDraft) {
|
|||
if (canceled) return;
|
||||
|
||||
os.apiWithDialog('notes/drafts/delete', { draftId: draft.id }).then(() => {
|
||||
paginator.reload();
|
||||
draftsPaginator.reload();
|
||||
});
|
||||
}
|
||||
|
||||
async function cancelSchedule(draft: Misskey.entities.NoteDraft) {
|
||||
os.apiWithDialog('notes/drafts/update', {
|
||||
draftId: draft.id,
|
||||
isActuallyScheduled: false,
|
||||
scheduledAt: null,
|
||||
}).then(() => {
|
||||
scheduledPaginator.reload();
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
@ -220,4 +298,11 @@ async function deleteDraft(draft: Misskey.entities.NoteDraft) {
|
|||
padding-top: 16px;
|
||||
border-top: solid 1px var(--MI_THEME-divider);
|
||||
}
|
||||
|
||||
.tabs {
|
||||
background: color(from var(--MI_THEME-bg) srgb r g b / 0.75);
|
||||
-webkit-backdrop-filter: var(--MI-blur, blur(15px));
|
||||
backdrop-filter: var(--MI-blur, blur(15px));
|
||||
border-bottom: solid 0.5px var(--MI_THEME-divider);
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -7,7 +7,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<div :class="$style.root">
|
||||
<div :class="$style.head">
|
||||
<MkAvatar v-if="['pollEnded', 'note'].includes(notification.type) && 'note' in notification" :class="$style.icon" :user="notification.note.user" link preview/>
|
||||
<MkAvatar v-else-if="['roleAssigned', 'achievementEarned', 'exportCompleted', 'login', 'createToken'].includes(notification.type)" :class="$style.icon" :user="$i" link preview/>
|
||||
<MkAvatar v-else-if="['roleAssigned', 'achievementEarned', 'exportCompleted', 'login', 'createToken', 'scheduledNotePosted', 'scheduledNotePostFailed'].includes(notification.type)" :class="$style.icon" :user="$i" link preview/>
|
||||
<div v-else-if="notification.type === 'reaction:grouped' && notification.note.reactionAcceptance === 'likeOnly'" :class="[$style.icon, $style.icon_reactionGroupHeart]"><i class="ti ti-heart" style="line-height: 1;"></i></div>
|
||||
<div v-else-if="notification.type === 'reaction:grouped'" :class="[$style.icon, $style.icon_reactionGroup]"><i class="ti ti-plus" style="line-height: 1;"></i></div>
|
||||
<div v-else-if="notification.type === 'renote:grouped'" :class="[$style.icon, $style.icon_renoteGroup]"><i class="ti ti-repeat" style="line-height: 1;"></i></div>
|
||||
|
@ -23,6 +23,8 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
[$style.t_mention]: notification.type === 'mention',
|
||||
[$style.t_quote]: notification.type === 'quote',
|
||||
[$style.t_pollEnded]: notification.type === 'pollEnded',
|
||||
[$style.t_scheduledNotePosted]: notification.type === 'scheduledNotePosted',
|
||||
[$style.t_scheduledNotePostFailed]: notification.type === 'scheduledNotePostFailed',
|
||||
[$style.t_achievementEarned]: notification.type === 'achievementEarned',
|
||||
[$style.t_exportCompleted]: notification.type === 'exportCompleted',
|
||||
[$style.t_login]: notification.type === 'login',
|
||||
|
@ -39,6 +41,8 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<i v-else-if="notification.type === 'mention'" class="ti ti-at"></i>
|
||||
<i v-else-if="notification.type === 'quote'" class="ti ti-quote"></i>
|
||||
<i v-else-if="notification.type === 'pollEnded'" class="ti ti-chart-arrows"></i>
|
||||
<i v-else-if="notification.type === 'scheduledNotePosted'" class="ti ti-send"></i>
|
||||
<i v-else-if="notification.type === 'scheduledNotePostFailed'" class="ti ti-alert-triangle"></i>
|
||||
<i v-else-if="notification.type === 'achievementEarned'" class="ti ti-medal"></i>
|
||||
<i v-else-if="notification.type === 'exportCompleted'" class="ti ti-archive"></i>
|
||||
<i v-else-if="notification.type === 'login'" class="ti ti-login-2"></i>
|
||||
|
@ -60,6 +64,8 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<div :class="$style.tail">
|
||||
<header :class="$style.header">
|
||||
<span v-if="notification.type === 'pollEnded'">{{ i18n.ts._notification.pollEnded }}</span>
|
||||
<span v-else-if="notification.type === 'scheduledNotePosted'">{{ i18n.ts._notification.scheduledNotePosted }}</span>
|
||||
<span v-else-if="notification.type === 'scheduledNotePostFailed'">{{ i18n.ts._notification.scheduledNotePostFailed }}</span>
|
||||
<span v-else-if="notification.type === 'note'">{{ i18n.ts._notification.newNote }}: <MkUserName :user="notification.note.user"/></span>
|
||||
<span v-else-if="notification.type === 'roleAssigned'">{{ i18n.ts._notification.roleAssigned }}</span>
|
||||
<span v-else-if="notification.type === 'chatRoomInvitationReceived'">{{ i18n.ts._notification.chatRoomInvitationReceived }}</span>
|
||||
|
@ -103,6 +109,11 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<Mfm :text="getNoteSummary(notification.note)" :plain="true" :nowrap="true" :author="notification.note.user"/>
|
||||
<i class="ti ti-quote" :class="$style.quote"></i>
|
||||
</MkA>
|
||||
<MkA v-else-if="notification.type === 'scheduledNotePosted'" :class="$style.text" :to="notePage(notification.note)" :title="getNoteSummary(notification.note)">
|
||||
<i class="ti ti-quote" :class="$style.quote"></i>
|
||||
<Mfm :text="getNoteSummary(notification.note)" :plain="true" :nowrap="true" :author="notification.note.user"/>
|
||||
<i class="ti ti-quote" :class="$style.quote"></i>
|
||||
</MkA>
|
||||
<div v-else-if="notification.type === 'roleAssigned'" :class="$style.text">
|
||||
{{ notification.role.name }}
|
||||
</div>
|
||||
|
@ -338,6 +349,16 @@ function getActualReactedUsersCount(notification: Misskey.entities.Notification)
|
|||
pointer-events: none;
|
||||
}
|
||||
|
||||
.t_scheduledNotePosted {
|
||||
background: var(--eventOther);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.t_scheduledNotePostFailed {
|
||||
background: var(--eventOther);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.t_achievementEarned {
|
||||
background: var(--eventAchievement);
|
||||
pointer-events: none;
|
||||
|
|
|
@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<template>
|
||||
<div :class="$style.root">
|
||||
<div :class="$style.control">
|
||||
<MkSelect v-model="order" :class="$style.order" :items="[{ label: i18n.ts._order.newest, value: 'newest' }, { label: i18n.ts._order.oldest, value: 'oldest' }]">
|
||||
<MkSelect v-model="order" :class="$style.order" :items="orderDef">
|
||||
<template #prefix><i class="ti ti-arrows-sort"></i></template>
|
||||
</MkSelect>
|
||||
<MkButton v-if="paginator.canSearch" v-tooltip="i18n.ts.search" iconOnly transparent rounded :active="searchOpened" @click="searchOpened = !searchOpened"><i class="ti ti-search"></i></MkButton>
|
||||
|
@ -45,6 +45,7 @@ import { i18n } from '@/i18n.js';
|
|||
import MkSelect from '@/components/MkSelect.vue';
|
||||
import MkInput from '@/components/MkInput.vue';
|
||||
import { formatDateTimeString } from '@/utility/format-time-string.js';
|
||||
import { useMkSelect } from '@/composables/use-mkselect.js';
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
paginator: T;
|
||||
|
@ -58,7 +59,16 @@ const props = withDefaults(defineProps<{
|
|||
const searchOpened = ref(false);
|
||||
const filterOpened = ref(props.filterOpened);
|
||||
|
||||
const order = ref<'newest' | 'oldest'>('newest');
|
||||
const {
|
||||
model: order,
|
||||
def: orderDef,
|
||||
} = useMkSelect({
|
||||
items: [
|
||||
{ label: i18n.ts._order.newest, value: 'newest' },
|
||||
{ label: i18n.ts._order.oldest, value: 'oldest' },
|
||||
],
|
||||
initialValue: 'newest',
|
||||
});
|
||||
const date = ref<number | null>(null);
|
||||
const q = ref<string | null>(null);
|
||||
|
||||
|
|
|
@ -4,14 +4,18 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
-->
|
||||
|
||||
<template>
|
||||
<div :class="[$style.root, accented ? $style.accented : null]"></div>
|
||||
<div :class="[$style.root, accented ? $style.accented : null, revered ? $style.revered : null]"/>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
const props = withDefaults(defineProps<{
|
||||
accented?: boolean;
|
||||
revered?: boolean;
|
||||
height?: number;
|
||||
}>(), {
|
||||
accented: false,
|
||||
revered: false,
|
||||
height: 200,
|
||||
});
|
||||
</script>
|
||||
|
||||
|
@ -27,14 +31,17 @@ const props = withDefaults(defineProps<{
|
|||
--dot-size: 2px;
|
||||
--gap-size: 40px;
|
||||
--offset: calc(var(--gap-size) / 2);
|
||||
--height: v-bind('props.height + "px"');
|
||||
|
||||
height: 200px;
|
||||
margin-bottom: -200px;
|
||||
|
||||
height: var(--height);
|
||||
background-image: linear-gradient(transparent 60%, transparent 100%), radial-gradient(var(--c) var(--dot-size), transparent var(--dot-size)), radial-gradient(var(--c) var(--dot-size), transparent var(--dot-size));
|
||||
background-position: 0 0, 0 0, var(--offset) var(--offset);
|
||||
background-size: 100% 100%, var(--gap-size) var(--gap-size), var(--gap-size) var(--gap-size);
|
||||
mask-image: linear-gradient(to bottom, black 0%, transparent 100%);
|
||||
pointer-events: none;
|
||||
|
||||
&.revered {
|
||||
mask-image: linear-gradient(to top, black 0%, transparent 100%);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -27,16 +27,16 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, ref } from 'vue';
|
||||
import { computed, ref, watch } from 'vue';
|
||||
import * as Misskey from 'misskey-js';
|
||||
import { host } from '@@/js/config.js';
|
||||
import { useInterval } from '@@/js/use-interval.js';
|
||||
import type { OpenOnRemoteOptions } from '@/utility/please-login.js';
|
||||
import { sum } from '@/utility/array.js';
|
||||
import { pleaseLogin } from '@/utility/please-login.js';
|
||||
import * as os from '@/os.js';
|
||||
import { misskeyApi } from '@/utility/misskey-api.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { useLowresTime } from '@/composables/use-lowres-time.js';
|
||||
|
||||
const props = defineProps<{
|
||||
noteId: string;
|
||||
|
@ -48,7 +48,21 @@ const props = defineProps<{
|
|||
author?: Misskey.entities.UserLite;
|
||||
}>();
|
||||
|
||||
const remaining = ref(-1);
|
||||
const now = useLowresTime();
|
||||
|
||||
const expiresAtTime = computed(() => props.expiresAt ? new Date(props.expiresAt).getTime() : null);
|
||||
|
||||
const remaining = computed(() => {
|
||||
if (expiresAtTime.value == null) return -1;
|
||||
return Math.floor(Math.max(expiresAtTime.value - now.value, 0) / 1000);
|
||||
});
|
||||
|
||||
const remainingWatchStop = watch(remaining, (to) => {
|
||||
if (to <= 0) {
|
||||
showResult.value = true;
|
||||
remainingWatchStop();
|
||||
}
|
||||
}, { immediate: true });
|
||||
|
||||
const total = computed(() => sum(props.choices.map(x => x.votes)));
|
||||
const closed = computed(() => remaining.value === 0);
|
||||
|
@ -71,22 +85,7 @@ const pleaseLoginContext = computed<OpenOnRemoteOptions>(() => ({
|
|||
url: `https://${host}/notes/${props.noteId}`,
|
||||
}));
|
||||
|
||||
// 期限付きアンケート
|
||||
if (props.expiresAt) {
|
||||
const tick = () => {
|
||||
remaining.value = Math.floor(Math.max(new Date(props.expiresAt!).getTime() - Date.now(), 0) / 1000);
|
||||
if (remaining.value === 0) {
|
||||
showResult.value = true;
|
||||
}
|
||||
};
|
||||
|
||||
useInterval(tick, 3000, {
|
||||
immediate: true,
|
||||
afterMounted: false,
|
||||
});
|
||||
}
|
||||
|
||||
const vote = async (id) => {
|
||||
const vote = async (id: number) => {
|
||||
if (props.readOnly || closed.value || isVoted.value) return;
|
||||
|
||||
pleaseLogin({ openOnRemote: pleaseLoginContext.value });
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue