summaryrefslogtreecommitdiff
path: root/packages/frontend/src
diff options
context:
space:
mode:
authorsyuilo <Syuilotan@yahoo.co.jp>2023-05-12 12:41:53 +0900
committerGitHub <noreply@github.com>2023-05-12 12:41:53 +0900
commitde6348e8a00a72c2410a8907d59a9bd29142a200 (patch)
treef163329a67ac5c6dc1c33845b4fc131a8c59a39b /packages/frontend/src
parentMerge pull request #10814 from misskey-dev/develop (diff)
parentfix(frontend): fix retention rate heatmap rendering (diff)
downloadmisskey-de6348e8a00a72c2410a8907d59a9bd29142a200.tar.gz
misskey-de6348e8a00a72c2410a8907d59a9bd29142a200.tar.bz2
misskey-de6348e8a00a72c2410a8907d59a9bd29142a200.zip
Merge pull request #10833 from misskey-dev/develop
* refactor(frontend): use css modules * feat: 投稿したコンテンツのAIによる学習を軽減するオプションを追加 Resolve #10819 * enhance(backend): publicReactionsをデフォルトtrueに * 念のためnoimageaiもつける * add X-Robots-Tag: noai * Update ja-JP.yml * fix(frontend): ブラーエフェクトを有効にしている状態で高負荷になる問題を修正 * enhance(backend): graceful shutdown for job queue and refactor * fix(backend): テスト時は一部のサービスを停止 * fix test * New Crowdin updates (#10815) * New translations ja-JP.yml (English) * New translations ja-JP.yml (German) * New translations ja-JP.yml (Korean) * New translations ja-JP.yml (Chinese Traditional) * New translations ja-JP.yml (Chinese Traditional) * refactor * bump * refactor(frontend): use css module * refactor(frontend): use css module * delete unused component * センシティブワードを正規表現、CWにも適用するように (#10688) * cwにセンシティブが効いてない * CWが無いときにTextを見るように * 比較演算子間違えた * とりあえずチェック * 正規表現対応 * /test/giにも対応 * matchでしなくてもいいのでは感 * レビュー修正 * Update packages/backend/src/core/NoteCreateService.ts Co-authored-by: Acid Chicken (硫酸鶏) <root@acid-chicken.com> * Update packages/backend/src/core/NoteCreateService.ts Co-authored-by: Acid Chicken (硫酸鶏) <root@acid-chicken.com> * 修正 * wipかも * wordsでスペース区切りのものできたかも * なんか動いたかも * test作成 * 文言の修正 * 修正 * note参照 --------- Co-authored-by: Acid Chicken (硫酸鶏) <root@acid-chicken.com> * Update CHANGELOG.md * New Crowdin updates (#10823) * New translations ja-JP.yml (English) * New translations ja-JP.yml (German) * ci: fix typo * fix(frontend): より明確な説明にしたのとtypo修正 * fix typo * fix(frontend): カラーバーがリプライには表示されないのを修正 * fix(frontend): チャンネル内の検索ボックスが挙動不審な問題を修正 Fix #10793 * enhance(backend): ノートのハッシュタグもMeilisearchに突っ込むように 今後ハッシュタグ検索とか実装するときのため * feat(frontend): ユーザー指定ノート検索 * fix(frontend): fix retention chart rendering * Update about-misskey.vue * meta: Remove @rinsuki from reviewer-lottery (#10830) * New Crowdin updates (#10824) * New translations ja-JP.yml (English) * New translations ja-JP.yml (German) * New translations ja-JP.yml (Chinese Traditional) * New translations ja-JP.yml (English) * New translations ja-JP.yml (Chinese Traditional) * New translations ja-JP.yml (German) * New translations ja-JP.yml (English) * New translations ja-JP.yml (Chinese Traditional) * New translations ja-JP.yml (French) * New translations ja-JP.yml (German) * New translations ja-JP.yml (English) * New translations ja-JP.yml (Japanese, Kansai) * New translations ja-JP.yml (Chinese Traditional) * New translations ja-JP.yml (Spanish) * New translations ja-JP.yml (German) * New translations ja-JP.yml (Italian) * New translations ja-JP.yml (Korean) * New translations ja-JP.yml (Norwegian) * New translations ja-JP.yml (Russian) * New translations ja-JP.yml (Chinese Simplified) * New translations ja-JP.yml (Indonesian) * New translations ja-JP.yml (Thai) * enhance(frontend): アカウント初期設定ウィザードにプライバシー設定を追加 * Update CHANGELOG.md * fix(backend): ひとつのMeilisearchサーバーを複数のMisskeyサーバーで使えない問題を修正 * fix MkUserSetupDialog.Privacy.vue * ci: skip non-Japanese locale on TurboSnap * ci: notify on changes for push events * ci: fix missing branch * Update basic.cy.js * [ci skip] New Crowdin updates (#10834) * New translations ja-JP.yml (English) * New translations ja-JP.yml (Arabic) * New translations ja-JP.yml (German) * New translations ja-JP.yml (Chinese Simplified) * New translations ja-JP.yml (Japanese, Kansai) * New translations ja-JP.yml (Arabic) * :art: * :art: * enhance(frontend): add retention line chart * update deps * refactor * fix(frontend): Pageにおいて画像ブロックに画像を設定できない問題を修正 Fix #10837 --------- Co-authored-by: nenohi <kimutipartylove@gmail.com> Co-authored-by: Acid Chicken (硫酸鶏) <root@acid-chicken.com> Co-authored-by: rinsuki <428rinsuki+git@gmail.com>
Diffstat (limited to 'packages/frontend/src')
-rw-r--r--packages/frontend/src/components/MkCheckbox.vue144
-rw-r--r--packages/frontend/src/components/MkInstanceStats.vue13
-rw-r--r--packages/frontend/src/components/MkModalWindow.vue97
-rw-r--r--packages/frontend/src/components/MkNoteSub.vue12
-rw-r--r--packages/frontend/src/components/MkRadio.vue93
-rw-r--r--packages/frontend/src/components/MkRetentionHeatmap.vue22
-rw-r--r--packages/frontend/src/components/MkRetentionLineChart.vue130
-rw-r--r--packages/frontend/src/components/MkSelect.vue197
-rw-r--r--packages/frontend/src/components/MkSwitch.vue149
-rw-r--r--packages/frontend/src/components/MkUserSetupDialog.Follow.vue4
-rw-r--r--packages/frontend/src/components/MkUserSetupDialog.Privacy.stories.impl.ts31
-rw-r--r--packages/frontend/src/components/MkUserSetupDialog.Privacy.vue64
-rw-r--r--packages/frontend/src/components/MkUserSetupDialog.Profile.vue4
-rw-r--r--packages/frontend/src/components/MkUserSetupDialog.vue50
-rw-r--r--packages/frontend/src/components/MkWidgets.vue2
-rw-r--r--packages/frontend/src/components/global/MkAvatar.vue20
-rw-r--r--packages/frontend/src/components/page/page.image.vue12
-rw-r--r--packages/frontend/src/pages/about-misskey.vue1
-rw-r--r--packages/frontend/src/pages/admin/moderation.vue2
-rw-r--r--packages/frontend/src/pages/channel.vue7
-rw-r--r--packages/frontend/src/pages/explore.users.vue18
-rw-r--r--packages/frontend/src/pages/page-editor/els/page-editor.el.image.vue2
-rw-r--r--packages/frontend/src/pages/search.note.vue98
-rw-r--r--packages/frontend/src/pages/search.user.vue77
-rw-r--r--packages/frontend/src/pages/search.vue113
-rw-r--r--packages/frontend/src/pages/settings/privacy.vue6
26 files changed, 778 insertions, 590 deletions
diff --git a/packages/frontend/src/components/MkCheckbox.vue b/packages/frontend/src/components/MkCheckbox.vue
deleted file mode 100644
index a8e24dd839..0000000000
--- a/packages/frontend/src/components/MkCheckbox.vue
+++ /dev/null
@@ -1,144 +0,0 @@
-<template>
-<div
- class="ziffeoms"
- :class="{ disabled, checked }"
->
- <input
- ref="input"
- type="checkbox"
- :disabled="disabled"
- @keydown.enter="toggle"
- >
- <span ref="button" v-adaptive-border v-tooltip="checked ? i18n.ts.itsOn : i18n.ts.itsOff" class="button" @click.prevent="toggle">
- <i class="check ti ti-check"></i>
- </span>
- <span class="label">
- <!-- TODO: 無名slotの方は廃止 -->
- <span @click="toggle"><slot name="label"></slot><slot></slot></span>
- <p class="caption"><slot name="caption"></slot></p>
- </span>
-</div>
-</template>
-
-<script lang="ts" setup>
-import { toRefs, Ref } from 'vue';
-import * as os from '@/os';
-import MkRippleEffect from '@/components/MkRippleEffect.vue';
-import { i18n } from '@/i18n';
-
-const props = defineProps<{
- modelValue: boolean | Ref<boolean>;
- disabled?: boolean;
-}>();
-
-const emit = defineEmits<{
- (ev: 'update:modelValue', v: boolean): void;
-}>();
-
-let button = $shallowRef<HTMLElement>();
-const checked = toRefs(props).modelValue;
-const toggle = () => {
- if (props.disabled) return;
- emit('update:modelValue', !checked.value);
-
- if (!checked.value) {
- const rect = button.getBoundingClientRect();
- const x = rect.left + (button.offsetWidth / 2);
- const y = rect.top + (button.offsetHeight / 2);
- os.popup(MkRippleEffect, { x, y, particle: false }, {}, 'end');
- }
-};
-</script>
-
-<style lang="scss" scoped>
-.ziffeoms {
- position: relative;
- display: flex;
- transition: all 0.2s ease;
-
- > * {
- user-select: none;
- }
-
- > input {
- position: absolute;
- width: 0;
- height: 0;
- opacity: 0;
- margin: 0;
- }
-
- > .button {
- position: relative;
- display: inline-flex;
- flex-shrink: 0;
- margin: 0;
- box-sizing: border-box;
- width: 23px;
- height: 23px;
- outline: none;
- background: var(--panel);
- border: solid 1px var(--panel);
- border-radius: 4px;
- cursor: pointer;
- transition: inherit;
-
- > .check {
- margin: auto;
- opacity: 0;
- color: var(--fgOnAccent);
- font-size: 13px;
- transform: scale(0.5);
- transition: all 0.2s ease;
- }
- }
-
- &:hover {
- > .button {
- border-color: var(--inputBorderHover) !important;
- }
- }
-
- > .label {
- margin-left: 12px;
- margin-top: 2px;
- display: block;
- transition: inherit;
- color: var(--fg);
-
- > span {
- display: block;
- line-height: 20px;
- cursor: pointer;
- transition: inherit;
- }
-
- > .caption {
- margin: 8px 0 0 0;
- color: var(--fgTransparentWeak);
- font-size: 0.85em;
-
- &:empty {
- display: none;
- }
- }
- }
-
- &.disabled {
- opacity: 0.6;
- cursor: not-allowed;
- }
-
- &.checked {
- > .button {
- background-color: var(--accent) !important;
- border-color: var(--accent) !important;
-
- > .check {
- opacity: 1;
- transform: scale(1);
- }
- }
- }
-}
-</style>
diff --git a/packages/frontend/src/components/MkInstanceStats.vue b/packages/frontend/src/components/MkInstanceStats.vue
index 0f87fef6b1..6fcd8f7811 100644
--- a/packages/frontend/src/components/MkInstanceStats.vue
+++ b/packages/frontend/src/components/MkInstanceStats.vue
@@ -52,9 +52,12 @@
<MkFoldableSection class="item">
<template #header>Retention rate</template>
- <div class="_panel" :class="$style.retention">
+ <div class="_panel" :class="$style.retentionHeatmap">
<MkRetentionHeatmap/>
</div>
+ <div class="_panel" :class="$style.retentionLine">
+ <MkRetentionLineChart/>
+ </div>
</MkFoldableSection>
<MkFoldableSection class="item">
@@ -86,6 +89,7 @@ import { i18n } from '@/i18n';
import MkHeatmap from '@/components/MkHeatmap.vue';
import MkFoldableSection from '@/components/MkFoldableSection.vue';
import MkRetentionHeatmap from '@/components/MkRetentionHeatmap.vue';
+import MkRetentionLineChart from '@/components/MkRetentionLineChart.vue';
import { initChart } from '@/scripts/init-chart';
initChart();
@@ -202,7 +206,12 @@ onMounted(() => {
margin-bottom: 16px;
}
-.retention {
+.retentionHeatmap {
+ padding: 16px;
+ margin-bottom: 16px;
+}
+
+.retentionLine {
padding: 16px;
margin-bottom: 16px;
}
diff --git a/packages/frontend/src/components/MkModalWindow.vue b/packages/frontend/src/components/MkModalWindow.vue
index ad7dc4da11..63c55b904a 100644
--- a/packages/frontend/src/components/MkModalWindow.vue
+++ b/packages/frontend/src/components/MkModalWindow.vue
@@ -1,15 +1,15 @@
<template>
<MkModal ref="modal" :prefer-type="'dialog'" @click="onBgClick" @closed="$emit('closed')">
- <div ref="rootEl" class="ebkgoccj" :style="{ width: `${width}px`, height: `min(${height}px, 100%)` }" @keydown="onKeydown">
- <div ref="headerEl" class="header">
- <button v-if="withOkButton" class="_button" @click="$emit('close')"><i class="ti ti-x"></i></button>
- <span class="title">
+ <div ref="rootEl" :class="$style.root" :style="{ width: `${width}px`, height: `min(${height}px, 100%)` }" @keydown="onKeydown">
+ <div ref="headerEl" :class="$style.header">
+ <button v-if="withOkButton" :class="$style.headerButton" class="_button" @click="$emit('close')"><i class="ti ti-x"></i></button>
+ <span :class="$style.title">
<slot name="header"></slot>
</span>
- <button v-if="!withOkButton" class="_button" data-cy-modal-window-close @click="$emit('close')"><i class="ti ti-x"></i></button>
- <button v-if="withOkButton" class="_button" :disabled="okButtonDisabled" @click="$emit('ok')"><i class="ti ti-check"></i></button>
+ <button v-if="!withOkButton" :class="$style.headerButton" class="_button" data-cy-modal-window-close @click="$emit('close')"><i class="ti ti-x"></i></button>
+ <button v-if="withOkButton" :class="$style.headerButton" class="_button" :disabled="okButtonDisabled" @click="$emit('ok')"><i class="ti ti-check"></i></button>
</div>
- <div class="body">
+ <div :class="$style.body">
<slot :width="bodyWidth" :height="bodyHeight"></slot>
</div>
</div>
@@ -81,8 +81,8 @@ defineExpose({
});
</script>
-<style lang="scss" scoped>
-.ebkgoccj {
+<style lang="scss" module>
+.root {
margin: auto;
overflow: hidden;
display: flex;
@@ -96,51 +96,52 @@ defineExpose({
--root-margin: 16px;
}
- > .header {
- $height: 46px;
- $height-narrow: 42px;
- display: flex;
- flex-shrink: 0;
- background: var(--windowHeader);
- -webkit-backdrop-filter: var(--blur, blur(15px));
- backdrop-filter: var(--blur, blur(15px));
+ --headerHeight: 46px;
+ --headerHeightNarrow: 42px;
+}
- > button {
- height: $height;
- width: $height;
+.header {
+ display: flex;
+ flex-shrink: 0;
+ background: var(--windowHeader);
+ -webkit-backdrop-filter: var(--blur, blur(15px));
+ backdrop-filter: var(--blur, blur(15px));
+}
- @media (max-width: 500px) {
- height: $height-narrow;
- width: $height-narrow;
- }
- }
+.headerButton {
+ height: var(--headerHeight);
+ width: var(--headerHeight);
- > .title {
- flex: 1;
- line-height: $height;
- padding-left: 32px;
- font-weight: bold;
- white-space: nowrap;
- overflow: hidden;
- text-overflow: ellipsis;
- pointer-events: none;
+ @media (max-width: 500px) {
+ height: var(--headerHeightNarrow);
+ width: var(--headerHeightNarrow);
+ }
+}
- @media (max-width: 500px) {
- line-height: $height-narrow;
- padding-left: 16px;
- }
- }
+.title {
+ flex: 1;
+ line-height: var(--headerHeight);
+ padding-left: 32px;
+ font-weight: bold;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ pointer-events: none;
- > button + .title {
- padding-left: 0;
- }
+ @media (max-width: 500px) {
+ line-height: var(--headerHeightNarrow);
+ padding-left: 16px;
}
+}
- > .body {
- flex: 1;
- overflow: auto;
- background: var(--panel);
- container-type: size;
- }
+.headerButton + .title {
+ padding-left: 0;
+}
+
+.body {
+ flex: 1;
+ overflow: auto;
+ background: var(--panel);
+ container-type: size;
}
</style>
diff --git a/packages/frontend/src/components/MkNoteSub.vue b/packages/frontend/src/components/MkNoteSub.vue
index c293641355..9ac0b7858f 100644
--- a/packages/frontend/src/components/MkNoteSub.vue
+++ b/packages/frontend/src/components/MkNoteSub.vue
@@ -1,6 +1,7 @@
<template>
<div :class="[$style.root, { [$style.children]: depth > 1 }]">
<div :class="$style.main">
+ <div v-if="note.channel" :class="$style.colorBar" :style="{ background: note.channel.color }"></div>
<MkAvatar :class="$style.avatar" :user="note.user" link preview/>
<div :class="$style.body">
<MkNoteHeader :class="$style.header" :note="note" :mini="true"/>
@@ -62,6 +63,7 @@ if (props.detail) {
.root {
padding: 16px 32px;
font-size: 0.9em;
+ position: relative;
&.children {
padding: 10px 0 0 16px;
@@ -73,6 +75,16 @@ if (props.detail) {
display: flex;
}
+.colorBar {
+ position: absolute;
+ top: 8px;
+ left: 8px;
+ width: 5px;
+ height: calc(100% - 8px);
+ border-radius: 999px;
+ pointer-events: none;
+}
+
.avatar {
flex-shrink: 0;
display: block;
diff --git a/packages/frontend/src/components/MkRadio.vue b/packages/frontend/src/components/MkRadio.vue
index 5db2f5ee6d..eea94d4692 100644
--- a/packages/frontend/src/components/MkRadio.vue
+++ b/packages/frontend/src/components/MkRadio.vue
@@ -1,8 +1,7 @@
<template>
<div
v-adaptive-border
- class="novjtctn"
- :class="{ disabled, checked }"
+ :class="[$style.root, { [$style.disabled]: disabled, [$style.checked]: checked }]"
:aria-checked="checked"
:aria-disabled="disabled"
@click="toggle"
@@ -10,11 +9,12 @@
<input
type="radio"
:disabled="disabled"
+ :class="$style.input"
>
- <span class="button">
+ <span :class="$style.button">
<span></span>
</span>
- <span class="label"><slot></slot></span>
+ <span :class="$style.label"><slot></slot></span>
</div>
</template>
@@ -39,8 +39,8 @@ function toggle(): void {
}
</script>
-<style lang="scss" scoped>
-.novjtctn {
+<style lang="scss" module>
+.root {
position: relative;
display: inline-block;
text-align: left;
@@ -53,17 +53,11 @@ function toggle(): void {
border-radius: 6px;
font-size: 90%;
transition: all 0.2s;
-
- > * {
- user-select: none;
- }
+ user-select: none;
&.disabled {
opacity: 0.6;
-
- &, * {
- cursor: not-allowed !important;
- }
+ cursor: not-allowed !important;
}
&:hover {
@@ -74,10 +68,7 @@ function toggle(): void {
background-color: var(--accentedBg) !important;
border-color: var(--accentedBg) !important;
color: var(--accent);
-
- &, * {
- cursor: default !important;
- }
+ cursor: default !important;
> .button {
border-color: var(--accent);
@@ -89,44 +80,44 @@ function toggle(): void {
}
}
}
+}
- > input {
- position: absolute;
- width: 0;
- height: 0;
- opacity: 0;
- margin: 0;
- }
+.input {
+ position: absolute;
+ width: 0;
+ height: 0;
+ opacity: 0;
+ margin: 0;
+}
+
+.button {
+ position: absolute;
+ width: 14px;
+ height: 14px;
+ background: none;
+ border: solid 2px var(--inputBorder);
+ border-radius: 100%;
+ transition: inherit;
- > .button {
+ &:after {
+ content: '';
+ display: block;
position: absolute;
- width: 14px;
- height: 14px;
- background: none;
- border: solid 2px var(--inputBorder);
+ top: 3px;
+ right: 3px;
+ bottom: 3px;
+ left: 3px;
border-radius: 100%;
- transition: inherit;
-
- &:after {
- content: '';
- display: block;
- position: absolute;
- top: 3px;
- right: 3px;
- bottom: 3px;
- left: 3px;
- border-radius: 100%;
- opacity: 0;
- transform: scale(0);
- transition: 0.4s cubic-bezier(0.25, 0.8, 0.25, 1);
- }
+ opacity: 0;
+ transform: scale(0);
+ transition: 0.4s cubic-bezier(0.25, 0.8, 0.25, 1);
}
+}
- > .label {
- margin-left: 28px;
- display: block;
- line-height: 20px;
- cursor: pointer;
- }
+.label {
+ margin-left: 28px;
+ display: block;
+ line-height: 20px;
+ cursor: pointer;
}
</style>
diff --git a/packages/frontend/src/components/MkRetentionHeatmap.vue b/packages/frontend/src/components/MkRetentionHeatmap.vue
index f33f68cab7..311d5c425c 100644
--- a/packages/frontend/src/components/MkRetentionHeatmap.vue
+++ b/packages/frontend/src/components/MkRetentionHeatmap.vue
@@ -40,7 +40,7 @@ async function renderChart() {
let raw = await os.api('retention', { });
- raw = raw.slice(0, maxDays);
+ raw = raw.slice(0, maxDays + 1);
const data = [];
for (const record of raw) {
@@ -90,8 +90,13 @@ async function renderChart() {
borderRadius: 3,
backgroundColor(c) {
const value = c.dataset.data[c.dataIndex].v;
- const a = value / max(c.dataset.data[c.dataIndex].y);
- return alpha(color, a);
+ const m = max(c.dataset.data[c.dataIndex].y);
+ if (m === 0) {
+ return alpha(color, 0);
+ } else {
+ const a = value / m;
+ return alpha(color, a);
+ }
},
fill: true,
width(c) {
@@ -129,6 +134,10 @@ async function renderChart() {
autoSkip: false,
callback: (value, index, values) => value,
},
+ title: {
+ display: true,
+ text: 'Days later',
+ },
},
y: {
type: 'time',
@@ -166,7 +175,12 @@ async function renderChart() {
},
label(context) {
const v = context.dataset.data[context.dataIndex];
- return [`Active: ${v.v} (${Math.round((v.v / max(v.y)) * 100)}%)`];
+ const m = max(v.y);
+ if (m === 0) {
+ return [`Active: ${v.v} (-%)`];
+ } else {
+ return [`Active: ${v.v} (${Math.round((v.v / m) * 100)}%)`];
+ }
},
},
//mode: 'index',
diff --git a/packages/frontend/src/components/MkRetentionLineChart.vue b/packages/frontend/src/components/MkRetentionLineChart.vue
new file mode 100644
index 0000000000..8bd0279806
--- /dev/null
+++ b/packages/frontend/src/components/MkRetentionLineChart.vue
@@ -0,0 +1,130 @@
+<template>
+<canvas ref="chartEl"></canvas>
+</template>
+
+<script lang="ts" setup>
+import { onMounted, shallowRef } from 'vue';
+import { Chart } from 'chart.js';
+import tinycolor from 'tinycolor2';
+import { defaultStore } from '@/store';
+import { useChartTooltip } from '@/scripts/use-chart-tooltip';
+import { chartVLine } from '@/scripts/chart-vline';
+import { alpha } from '@/scripts/color';
+import { initChart } from '@/scripts/init-chart';
+import * as os from '@/os';
+
+initChart();
+
+const chartEl = shallowRef<HTMLCanvasElement>(null);
+
+const { handler: externalTooltipHandler } = useChartTooltip();
+
+let chartInstance: Chart;
+
+const getYYYYMMDD = (date: Date) => {
+ const y = date.getFullYear().toString().padStart(2, '0');
+ const m = (date.getMonth() + 1).toString().padStart(2, '0');
+ const d = date.getDate().toString().padStart(2, '0');
+ return `${y}/${m}/${d}`;
+};
+
+const getDate = (ymd: string) => {
+ const [y, m, d] = ymd.split('-').map(x => parseInt(x, 10));
+ const date = new Date(y, m + 1, d, 0, 0, 0, 0);
+ return date;
+};
+
+onMounted(async () => {
+ let raw = await os.api('retention', { });
+
+ const vLineColor = defaultStore.state.darkMode ? 'rgba(255, 255, 255, 0.2)' : 'rgba(0, 0, 0, 0.2)';
+
+ const accent = tinycolor(getComputedStyle(document.documentElement).getPropertyValue('--accent'));
+ const color = accent.toHex();
+
+ chartInstance = new Chart(chartEl.value, {
+ type: 'line',
+ data: {
+ labels: [],
+ datasets: raw.map((record, i) => ({
+ label: getYYYYMMDD(new Date(record.createdAt)),
+ pointRadius: 0,
+ borderWidth: 2,
+ borderJoinStyle: 'round',
+ borderColor: alpha(color, Math.min(1, (raw.length - (i - 1)) / raw.length)),
+ fill: false,
+ tension: 0.4,
+ data: [{
+ x: '0',
+ y: 100,
+ d: getYYYYMMDD(new Date(record.createdAt)),
+ }, ...Object.entries(record.data).sort((a, b) => getDate(a[0]) > getDate(b[0]) ? 1 : -1).map(([k, v], i) => ({
+ x: (i + 1).toString(),
+ y: (v / record.users) * 100,
+ d: getYYYYMMDD(new Date(record.createdAt)),
+ }))],
+ })),
+ },
+ options: {
+ aspectRatio: 2.5,
+ layout: {
+ padding: {
+ left: 0,
+ right: 0,
+ top: 0,
+ bottom: 0,
+ },
+ },
+ scales: {
+ x: {
+ title: {
+ display: true,
+ text: 'Days later',
+ },
+ },
+ y: {
+ title: {
+ display: true,
+ text: 'Rate (%)',
+ },
+ ticks: {
+ callback: (value, index, values) => value + '%',
+ },
+ },
+ },
+ interaction: {
+ intersect: false,
+ },
+ plugins: {
+ legend: {
+ display: false,
+ },
+ tooltip: {
+ enabled: false,
+ callbacks: {
+ title(context) {
+ const v = context[0].dataset.data[context[0].dataIndex];
+ return `${v.x} days later`;
+ },
+ label(context) {
+ const v = context.dataset.data[context.dataIndex];
+ const p = Math.round(v.y) + '%';
+ return `${v.d} ${p}`;
+ },
+ },
+ mode: 'index',
+ animation: {
+ duration: 0,
+ },
+ external: externalTooltipHandler,
+ },
+ },
+ },
+ plugins: [chartVLine(vLineColor)],
+ });
+});
+</script>
+
+<style lang="scss" scoped>
+
+</style>
diff --git a/packages/frontend/src/components/MkSelect.vue b/packages/frontend/src/components/MkSelect.vue
index 2de890186a..4efb65c287 100644
--- a/packages/frontend/src/components/MkSelect.vue
+++ b/packages/frontend/src/components/MkSelect.vue
@@ -1,13 +1,13 @@
<template>
-<div class="vblkjoeq">
- <div class="label" @click="focus"><slot name="label"></slot></div>
- <div ref="container" class="input" :class="{ inline, disabled, focused }" @mousedown.prevent="show">
- <div ref="prefixEl" class="prefix"><slot name="prefix"></slot></div>
+<div>
+ <div :class="$style.label" @click="focus"><slot name="label"></slot></div>
+ <div ref="container" :class="[$style.input, { [$style.inline]: inline, [$style.disabled]: disabled, [$style.focused]: focused }]" @mousedown.prevent="show">
+ <div ref="prefixEl" :class="$style.prefix"><slot name="prefix"></slot></div>
<select
ref="inputEl"
v-model="v"
v-adaptive-border
- class="select"
+ :class="$style.inputCore"
:disabled="disabled"
:required="required"
:readonly="readonly"
@@ -18,9 +18,9 @@
>
<slot></slot>
</select>
- <div ref="suffixEl" class="suffix"><i class="ti ti-chevron-down" :class="[$style.chevron, { [$style.chevronOpening]: opening }]"></i></div>
+ <div ref="suffixEl" :class="$style.suffix"><i class="ti ti-chevron-down" :class="[$style.chevron, { [$style.chevronOpening]: opening }]"></i></div>
</div>
- <div class="caption"><slot name="caption"></slot></div>
+ <div :class="$style.caption"><slot name="caption"></slot></div>
<MkButton v-if="manualSave && changed" primary @click="updated"><i class="ti ti-device-floppy"></i> {{ i18n.ts.save }}</MkButton>
</div>
@@ -169,121 +169,116 @@ function show(ev: MouseEvent) {
}
</script>
-<style lang="scss" scoped>
-.vblkjoeq {
- > .label {
- font-size: 0.85em;
- padding: 0 0 8px 0;
- user-select: none;
+<style lang="scss" module>
+.label {
+ font-size: 0.85em;
+ padding: 0 0 8px 0;
+ user-select: none;
- &:empty {
- display: none;
- }
+ &:empty {
+ display: none;
}
+}
- > .caption {
- font-size: 0.85em;
- padding: 8px 0 0 0;
- color: var(--fgTransparentWeak);
+.caption {
+ font-size: 0.85em;
+ padding: 8px 0 0 0;
+ color: var(--fgTransparentWeak);
- &:empty {
- display: none;
- }
+ &:empty {
+ display: none;
}
+}
- > .input {
- position: relative;
- cursor: pointer;
+.input {
+ position: relative;
+ cursor: pointer;
- &:hover {
- > .select {
- border-color: var(--inputBorderHover) !important;
- }
- }
+ &.inline {
+ display: inline-block;
+ margin: 0;
+ }
- > .select {
- appearance: none;
- -webkit-appearance: none;
- display: block;
- height: v-bind("height + 'px'");
- width: 100%;
- margin: 0;
- padding: 0 12px;
- font: inherit;
- font-weight: normal;
- font-size: 1em;
- color: var(--fg);
- background: var(--panel);
- border: solid 1px var(--panel);
- border-radius: 6px;
- outline: none;
- box-shadow: none;
- box-sizing: border-box;
- cursor: pointer;
- transition: border-color 0.1s ease-out;
- pointer-events: none;
- user-select: none;
+ &.focused {
+ > .inputCore {
+ border-color: var(--accent) !important;
+ //box-shadow: 0 0 0 4px var(--focus);
}
+ }
- > .prefix,
- > .suffix {
- display: flex;
- align-items: center;
- position: absolute;
- z-index: 1;
- top: 0;
- padding: 0 12px;
- font-size: 1em;
- height: v-bind("height + 'px'");
- pointer-events: none;
-
- &:empty {
- display: none;
- }
+ &.disabled {
+ opacity: 0.7;
- > * {
- display: inline-block;
- min-width: 16px;
- max-width: 150px;
- overflow: hidden;
- white-space: nowrap;
- text-overflow: ellipsis;
- }
+ &,
+ > .inputCore {
+ cursor: not-allowed !important;
}
+ }
- > .prefix {
- left: 0;
- padding-right: 6px;
+ &:hover {
+ > .inputCore {
+ border-color: var(--inputBorderHover) !important;
}
+ }
+}
- > .suffix {
- right: 0;
- padding-left: 6px;
- }
+.inputCore {
+ appearance: none;
+ -webkit-appearance: none;
+ display: block;
+ height: v-bind("height + 'px'");
+ width: 100%;
+ margin: 0;
+ padding: 0 12px;
+ font: inherit;
+ font-weight: normal;
+ font-size: 1em;
+ color: var(--fg);
+ background: var(--panel);
+ border: solid 1px var(--panel);
+ border-radius: 6px;
+ outline: none;
+ box-shadow: none;
+ box-sizing: border-box;
+ transition: border-color 0.1s ease-out;
+ cursor: pointer;
+ pointer-events: none;
+ user-select: none;
+}
- &.inline {
- display: inline-block;
- margin: 0;
- }
+.prefix,
+.suffix {
+ display: flex;
+ align-items: center;
+ position: absolute;
+ z-index: 1;
+ top: 0;
+ padding: 0 12px;
+ font-size: 1em;
+ height: v-bind("height + 'px'");
+ min-width: 16px;
+ max-width: 150px;
+ overflow: hidden;
+ white-space: nowrap;
+ text-overflow: ellipsis;
+ box-sizing: border-box;
+ pointer-events: none;
- &.focused {
- > select {
- border-color: var(--accent) !important;
- }
- }
+ &:empty {
+ display: none;
+ }
+}
- &.disabled {
- opacity: 0.7;
+.prefix {
+ left: 0;
+ padding-right: 6px;
+}
- &, * {
- cursor: not-allowed !important;
- }
- }
- }
+.suffix {
+ right: 0;
+ padding-left: 6px;
}
-</style>
-<style lang="scss" module>
.chevron {
transition: transform 0.1s ease-out;
}
diff --git a/packages/frontend/src/components/MkSwitch.vue b/packages/frontend/src/components/MkSwitch.vue
index d9f6716f92..63738b6a44 100644
--- a/packages/frontend/src/components/MkSwitch.vue
+++ b/packages/frontend/src/components/MkSwitch.vue
@@ -1,21 +1,19 @@
<template>
-<div
- class="ziffeomt"
- :class="{ disabled, checked }"
->
+<div :class="[$style.root, { [$style.disabled]: disabled, [$style.checked]: checked }]">
<input
ref="input"
type="checkbox"
:disabled="disabled"
+ :class="$style.input"
@keydown.enter="toggle"
>
- <span ref="button" v-tooltip="checked ? i18n.ts.itsOn : i18n.ts.itsOff" class="button" data-cy-switch-toggle @click.prevent="toggle">
- <div class="knob"></div>
+ <span ref="button" v-tooltip="checked ? i18n.ts.itsOn : i18n.ts.itsOff" :class="$style.button" data-cy-switch-toggle @click.prevent="toggle">
+ <div :class="$style.knob"></div>
</span>
- <span class="label">
+ <span :class="$style.body">
<!-- TODO: 無名slotの方は廃止 -->
- <span @click="toggle"><slot name="label"></slot><slot></slot></span>
- <p class="caption"><slot name="caption"></slot></p>
+ <span :class="$style.label" @click="toggle"><slot name="label"></slot><slot></slot></span>
+ <p :class="$style.caption"><slot name="caption"></slot></p>
</span>
</div>
</template>
@@ -45,52 +43,12 @@ const toggle = () => {
};
</script>
-<style lang="scss" scoped>
-.ziffeomt {
+<style lang="scss" module>
+.root {
position: relative;
display: flex;
transition: all 0.2s ease;
-
- > * {
- user-select: none;
- }
-
- > input {
- position: absolute;
- width: 0;
- height: 0;
- opacity: 0;
- margin: 0;
- }
-
- > .button {
- position: relative;
- display: inline-flex;
- flex-shrink: 0;
- margin: 0;
- box-sizing: border-box;
- width: 32px;
- height: 23px;
- outline: none;
- background: var(--switchOffBg);
- background-clip: content-box;
- border: solid 1px var(--switchOffBg);
- border-radius: 999px;
- cursor: pointer;
- transition: inherit;
- user-select: none;
-
- > .knob {
- position: absolute;
- top: 3px;
- left: 3px;
- width: 15px;
- height: 15px;
- background: var(--switchOffFg);
- border-radius: 999px;
- transition: all 0.2s ease;
- }
- }
+ user-select: none;
&:hover {
> .button {
@@ -98,31 +56,6 @@ const toggle = () => {
}
}
- > .label {
- margin-left: 12px;
- margin-top: 2px;
- display: block;
- transition: inherit;
- color: var(--fg);
-
- > span {
- display: block;
- line-height: 20px;
- cursor: pointer;
- transition: inherit;
- }
-
- > .caption {
- margin: 8px 0 0 0;
- color: var(--fgTransparentWeak);
- font-size: 0.85em;
-
- &:empty {
- display: none;
- }
- }
- }
-
&.disabled {
opacity: 0.6;
cursor: not-allowed;
@@ -140,4 +73,66 @@ const toggle = () => {
}
}
}
+
+.input {
+ position: absolute;
+ width: 0;
+ height: 0;
+ opacity: 0;
+ margin: 0;
+}
+
+.button {
+ position: relative;
+ display: inline-flex;
+ flex-shrink: 0;
+ margin: 0;
+ box-sizing: border-box;
+ width: 32px;
+ height: 23px;
+ outline: none;
+ background: var(--switchOffBg);
+ background-clip: content-box;
+ border: solid 1px var(--switchOffBg);
+ border-radius: 999px;
+ cursor: pointer;
+ transition: inherit;
+ user-select: none;
+}
+
+.knob {
+ position: absolute;
+ top: 3px;
+ left: 3px;
+ width: 15px;
+ height: 15px;
+ background: var(--switchOffFg);
+ border-radius: 999px;
+ transition: all 0.2s ease;
+}
+
+.body {
+ margin-left: 12px;
+ margin-top: 2px;
+ display: block;
+ transition: inherit;
+ color: var(--fg);
+}
+
+.label {
+ display: block;
+ line-height: 20px;
+ cursor: pointer;
+ transition: inherit;
+}
+
+.caption {
+ margin: 8px 0 0 0;
+ color: var(--fgTransparentWeak);
+ font-size: 0.85em;
+
+ &:empty {
+ display: none;
+ }
+}
</style>
diff --git a/packages/frontend/src/components/MkUserSetupDialog.Follow.vue b/packages/frontend/src/components/MkUserSetupDialog.Follow.vue
index b89e3e4c9d..a2a195cb09 100644
--- a/packages/frontend/src/components/MkUserSetupDialog.Follow.vue
+++ b/packages/frontend/src/components/MkUserSetupDialog.Follow.vue
@@ -40,10 +40,6 @@ import * as os from '@/os';
import { $i } from '@/account';
import MkPagination from '@/components/MkPagination.vue';
-const emit = defineEmits<{
- (ev: 'done'): void;
-}>();
-
const pinnedUsers = { endpoint: 'pinned-users', noPaging: true };
const popularUsers = { endpoint: 'users', limit: 10, noPaging: true, params: {
diff --git a/packages/frontend/src/components/MkUserSetupDialog.Privacy.stories.impl.ts b/packages/frontend/src/components/MkUserSetupDialog.Privacy.stories.impl.ts
new file mode 100644
index 0000000000..70817d83c3
--- /dev/null
+++ b/packages/frontend/src/components/MkUserSetupDialog.Privacy.stories.impl.ts
@@ -0,0 +1,31 @@
+/* eslint-disable @typescript-eslint/explicit-function-return-type */
+import { StoryObj } from '@storybook/vue3';
+import MkUserSetupDialog_Privacy from './MkUserSetupDialog.Privacy.vue';
+export const Default = {
+ render(args) {
+ return {
+ components: {
+ MkUserSetupDialog_Privacy,
+ },
+ setup() {
+ return {
+ args,
+ };
+ },
+ computed: {
+ props() {
+ return {
+ ...this.args,
+ };
+ },
+ },
+ template: '<MkUserSetupDialog_Privacy v-bind="props" />',
+ };
+ },
+ args: {
+
+ },
+ parameters: {
+ layout: 'centered',
+ },
+} satisfies StoryObj<typeof MkUserSetupDialog_Privacy>;
diff --git a/packages/frontend/src/components/MkUserSetupDialog.Privacy.vue b/packages/frontend/src/components/MkUserSetupDialog.Privacy.vue
new file mode 100644
index 0000000000..e9f4f68df8
--- /dev/null
+++ b/packages/frontend/src/components/MkUserSetupDialog.Privacy.vue
@@ -0,0 +1,64 @@
+<template>
+<div class="_gaps">
+ <MkInfo>{{ i18n.ts._initialAccountSetting.theseSettingsCanEditLater }}</MkInfo>
+
+ <MkFolder>
+ <template #label>{{ i18n.ts.makeFollowManuallyApprove }}</template>
+ <template #suffix>{{ isLocked ? i18n.ts.on : i18n.ts.off }}</template>
+
+ <MkSwitch v-model="isLocked">{{ i18n.ts.makeFollowManuallyApprove }}<template #caption>{{ i18n.ts.lockedAccountInfo }}</template></MkSwitch>
+ </MkFolder>
+
+ <MkFolder>
+ <template #label>{{ i18n.ts.hideOnlineStatus }}</template>
+ <template #suffix>{{ hideOnlineStatus ? i18n.ts.on : i18n.ts.off }}</template>
+
+ <MkSwitch v-model="hideOnlineStatus">{{ i18n.ts.hideOnlineStatus }}<template #caption>{{ i18n.ts.hideOnlineStatusDescription }}</template></MkSwitch>
+ </MkFolder>
+
+ <MkFolder>
+ <template #label>{{ i18n.ts.noCrawle }}</template>
+ <template #suffix>{{ noCrawle ? i18n.ts.on : i18n.ts.off }}</template>
+
+ <MkSwitch v-model="noCrawle">{{ i18n.ts.noCrawle }}<template #caption>{{ i18n.ts.noCrawleDescription }}</template></MkSwitch>
+ </MkFolder>
+
+ <MkFolder>
+ <template #label>{{ i18n.ts.preventAiLearning }}</template>
+ <template #suffix>{{ preventAiLearning ? i18n.ts.on : i18n.ts.off }}</template>
+
+ <MkSwitch v-model="preventAiLearning">{{ i18n.ts.preventAiLearning }}<template #caption>{{ i18n.ts.preventAiLearningDescription }}</template></MkSwitch>
+ </MkFolder>
+
+ <MkInfo>{{ i18n.ts._initialAccountSetting.youCanEditMoreSettingsInSettingsPageLater }}</MkInfo>
+</div>
+</template>
+
+<script lang="ts" setup>
+import { computed, ref, watch } from 'vue';
+import { instance } from '@/instance';
+import { i18n } from '@/i18n';
+import MkSwitch from '@/components/MkSwitch.vue';
+import MkInfo from '@/components/MkInfo.vue';
+import MkFolder from '@/components/MkFolder.vue';
+import * as os from '@/os';
+import { $i } from '@/account';
+
+let isLocked = ref(false);
+let hideOnlineStatus = ref(false);
+let noCrawle = ref(false);
+let preventAiLearning = ref(true);
+
+watch([isLocked, hideOnlineStatus, noCrawle, preventAiLearning], () => {
+ os.api('i/update', {
+ isLocked: !!isLocked.value,
+ hideOnlineStatus: !!hideOnlineStatus.value,
+ noCrawle: !!noCrawle.value,
+ preventAiLearning: !!preventAiLearning.value,
+ });
+});
+</script>
+
+<style lang="scss" module>
+
+</style>
diff --git a/packages/frontend/src/components/MkUserSetupDialog.Profile.vue b/packages/frontend/src/components/MkUserSetupDialog.Profile.vue
index adb8d43349..f26ea11214 100644
--- a/packages/frontend/src/components/MkUserSetupDialog.Profile.vue
+++ b/packages/frontend/src/components/MkUserSetupDialog.Profile.vue
@@ -37,10 +37,6 @@ import { chooseFileFromPc } from '@/scripts/select-file';
import * as os from '@/os';
import { $i } from '@/account';
-const emit = defineEmits<{
- (ev: 'done'): void;
-}>();
-
const name = ref('');
const description = ref('');
diff --git a/packages/frontend/src/components/MkUserSetupDialog.vue b/packages/frontend/src/components/MkUserSetupDialog.vue
index 096b88c309..4e80a5c0fb 100644
--- a/packages/frontend/src/components/MkUserSetupDialog.vue
+++ b/packages/frontend/src/components/MkUserSetupDialog.vue
@@ -7,9 +7,17 @@
@close="close(true)"
@closed="emit('closed')"
>
- <template #header>{{ i18n.ts.initialAccountSetting }}</template>
+ <template v-if="page === 1" #header>{{ i18n.ts._initialAccountSetting.profileSetting }}</template>
+ <template v-else-if="page === 2" #header>{{ i18n.ts._initialAccountSetting.privacySetting }}</template>
+ <template v-else-if="page === 3" #header>{{ i18n.ts.follow }}</template>
+ <template v-else-if="page === 4" #header>{{ i18n.ts.pushNotification }}</template>
+ <template v-else-if="page === 5" #header>{{ i18n.ts.done }}</template>
+ <template v-else #header>{{ i18n.ts.initialAccountSetting }}</template>
<div style="overflow-x: clip;">
+ <div :class="$style.progressBar">
+ <div :class="$style.progressBarValue" :style="{ width: `${(page / 5) * 100}%` }"></div>
+ </div>
<Transition
mode="out-in"
:enter-active-class="$style.transition_x_enterActive"
@@ -40,12 +48,22 @@
<template v-else-if="page === 2">
<div style="height: 100cqh; overflow: auto;">
<MkSpacer :margin-min="20" :margin-max="28">
- <XFollow/>
+ <XPrivacy/>
<MkButton primary rounded gradate style="margin: 16px auto 0 auto;" data-cy-user-setup-continue @click="page++">{{ i18n.ts.continue }} <i class="ti ti-arrow-right"></i></MkButton>
</MkSpacer>
</div>
</template>
<template v-else-if="page === 3">
+ <div style="height: 100cqh; overflow: auto;">
+ <MkSpacer :margin-min="20" :margin-max="28">
+ <XFollow/>
+ </MkSpacer>
+ <div :class="$style.pageFooter">
+ <MkButton primary rounded gradate style="margin: 0 auto;" data-cy-user-setup-continue @click="page++">{{ i18n.ts.continue }} <i class="ti ti-arrow-right"></i></MkButton>
+ </div>
+ </div>
+ </template>
+ <template v-else-if="page === 4">
<div :class="$style.centerPage">
<MkSpacer :margin-min="20" :margin-max="28">
<div class="_gaps" style="text-align: center;">
@@ -58,7 +76,7 @@
</MkSpacer>
</div>
</template>
- <template v-else-if="page === 4">
+ <template v-else-if="page === 5">
<div :class="$style.centerPage">
<MkSpacer :margin-min="20" :margin-max="28">
<div class="_gaps" style="text-align: center;">
@@ -87,6 +105,7 @@ import MkModalWindow from '@/components/MkModalWindow.vue';
import MkButton from '@/components/MkButton.vue';
import XProfile from '@/components/MkUserSetupDialog.Profile.vue';
import XFollow from '@/components/MkUserSetupDialog.Follow.vue';
+import XPrivacy from '@/components/MkUserSetupDialog.Privacy.vue';
import { i18n } from '@/i18n';
import { instance } from '@/instance';
import { host } from '@/config';
@@ -134,6 +153,21 @@ async function close(skip: boolean) {
transform: translateX(-50px);
}
+.progressBar {
+ position: absolute;
+ top: 0;
+ left: 0;
+ z-index: 10;
+ width: 100%;
+ height: 4px;
+}
+
+.progressBarValue {
+ height: 100%;
+ background: linear-gradient(90deg, var(--buttonGradateA), var(--buttonGradateB));
+ transition: all 0.5s cubic-bezier(0,.5,.5,1);
+}
+
.centerPage {
display: flex;
justify-content: center;
@@ -142,4 +176,14 @@ async function close(skip: boolean) {
padding-bottom: 30px;
box-sizing: border-box;
}
+
+.pageFooter {
+ position: sticky;
+ bottom: 0;
+ left: 0;
+ padding: 12px;
+ border-top: solid 0.5px var(--divider);
+ -webkit-backdrop-filter: var(--blur, blur(15px));
+ backdrop-filter: var(--blur, blur(15px));
+}
</style>
diff --git a/packages/frontend/src/components/MkWidgets.vue b/packages/frontend/src/components/MkWidgets.vue
index 33e594acd8..ad1c02a488 100644
--- a/packages/frontend/src/components/MkWidgets.vue
+++ b/packages/frontend/src/components/MkWidgets.vue
@@ -32,6 +32,7 @@
<component :is="`widget-${widget.name}`" v-for="widget in widgets" v-else :key="widget.id" :ref="el => widgetRefs[widget.id] = el" :class="$style.widget" :widget="widget" @update-props="updateWidget(widget.id, $event)" @contextmenu.stop="onContextmenu(widget, $event)"/>
</div>
</template>
+
<script lang="ts">
export type Widget = {
name: string;
@@ -42,6 +43,7 @@ export type DefaultStoredWidget = {
place: string | null;
} & Widget;
</script>
+
<script lang="ts" setup>
import { defineAsyncComponent, ref } from 'vue';
import { v4 as uuid } from 'uuid';
diff --git a/packages/frontend/src/components/global/MkAvatar.vue b/packages/frontend/src/components/global/MkAvatar.vue
index ad36dcabe4..42abdcbdcc 100644
--- a/packages/frontend/src/components/global/MkAvatar.vue
+++ b/packages/frontend/src/components/global/MkAvatar.vue
@@ -2,7 +2,7 @@
<component :is="link ? MkA : 'span'" v-user-preview="preview ? user.id : undefined" v-bind="bound" class="_noSelect" :class="[$style.root, { [$style.animation]: animation, [$style.cat]: user.isCat, [$style.square]: squareAvatars }]" :style="{ color }" :title="acct(user)" @click="onClick">
<img :class="$style.inner" :src="url" decoding="async"/>
<MkUserOnlineIndicator v-if="indicator" :class="$style.indicator" :user="user"/>
- <div v-if="user.isCat" :class="[$style.ears, { [$style.mask]: useBlurEffect }]">
+ <div v-if="user.isCat" :class="[$style.ears]">
<div :class="$style.earLeft">
<div v-if="false" :class="$style.layer">
<div :class="$style.plot" :style="{ backgroundImage: `url(${JSON.stringify(url)})` }"/>
@@ -154,24 +154,6 @@ watch(() => props.user.avatarBlurhash, () => {
padding: 50%;
pointer-events: none;
- &.mask {
- -webkit-mask:
- url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32"><filter id="a"><feGaussianBlur in="SourceGraphic" stdDeviation="1"/></filter><circle cx="16" cy="16" r="15" filter="url(%23a)"/></svg>') center / 50% 50%,
- linear-gradient(#fff, #fff);
- -webkit-mask-composite: destination-out, source-over;
- mask:
- url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32"><filter id="a"><feGaussianBlur in="SourceGraphic" stdDeviation="1"/></filter><circle cx="16" cy="16" r="15" filter="url(%23a)"/></svg>') exclude center / 50% 50%,
- linear-gradient(#fff, #fff); // polyfill of `image(#fff)`
-
- > .earLeft {
- animation: eartightleft 6s infinite;
- }
-
- > .earRight {
- animation: eartightright 6s infinite;
- }
- }
-
> .earLeft,
> .earRight {
contain: strict;
diff --git a/packages/frontend/src/components/page/page.image.vue b/packages/frontend/src/components/page/page.image.vue
index 0237644d29..6ea81d257f 100644
--- a/packages/frontend/src/components/page/page.image.vue
+++ b/packages/frontend/src/components/page/page.image.vue
@@ -1,6 +1,6 @@
<template>
-<div class="lzyxtsnt">
- <ImgWithBlurhash v-if="image" :hash="image.blurhash" :src="image.url" :alt="image.comment" :title="image.comment" :cover="false"/>
+<div>
+ <ImgWithBlurhash v-if="image" style="max-width: 100%;" :hash="image.blurhash" :src="image.url" :alt="image.comment" :title="image.comment" :width="image.properties.width" :height="image.properties.height" :cover="false"/>
</div>
</template>
@@ -17,11 +17,3 @@ const props = defineProps<{
const image = props.hpml.page.attachedFiles.find(x => x.id === props.block.fileId);
</script>
-
-<style lang="scss" scoped>
-.lzyxtsnt {
- > img {
- max-width: 100%;
- }
-}
-</style>
diff --git a/packages/frontend/src/pages/about-misskey.vue b/packages/frontend/src/pages/about-misskey.vue
index e592c629ce..9e0594db3c 100644
--- a/packages/frontend/src/pages/about-misskey.vue
+++ b/packages/frontend/src/pages/about-misskey.vue
@@ -238,6 +238,7 @@ const patrons = [
'ずも',
'binvinyl',
'渡志郎',
+ 'ぷーざ',
];
let thereIsTreasure = $ref($i && !claimedAchievements.includes('foundTreasure'));
diff --git a/packages/frontend/src/pages/admin/moderation.vue b/packages/frontend/src/pages/admin/moderation.vue
index ffd3b6e233..bf788e3609 100644
--- a/packages/frontend/src/pages/admin/moderation.vue
+++ b/packages/frontend/src/pages/admin/moderation.vue
@@ -27,7 +27,7 @@
<MkTextarea v-model="sensitiveWords">
<template #label>{{ i18n.ts.sensitiveWords }}</template>
- <template #caption>{{ i18n.ts.sensitiveWordsDescription }}</template>
+ <template #caption>{{ i18n.ts.sensitiveWordsDescription }}<br>{{ i18n.ts.sensitiveWordsDescription2 }}</template>
</MkTextarea>
</div>
</FormSuspense>
diff --git a/packages/frontend/src/pages/channel.vue b/packages/frontend/src/pages/channel.vue
index af1b4d2056..9aa564a7da 100644
--- a/packages/frontend/src/pages/channel.vue
+++ b/packages/frontend/src/pages/channel.vue
@@ -46,7 +46,7 @@
</MkInput>
<MkButton primary rounded style="margin-top: 8px;" @click="search()">{{ i18n.ts.search }}</MkButton>
</div>
- <MkNotes v-if="searchPagination" :key="searchQuery" :pagination="searchPagination"/>
+ <MkNotes v-if="searchPagination" :key="searchKey" :pagination="searchPagination"/>
</div>
</div>
</MkSpacer>
@@ -93,6 +93,7 @@ let channel = $ref(null);
let favorited = $ref(false);
let searchQuery = $ref('');
let searchPagination = $ref();
+let searchKey = $ref('');
const featuredPagination = $computed(() => ({
endpoint: 'notes/featured' as const,
limit: 10,
@@ -149,10 +150,12 @@ async function search() {
endpoint: 'notes/search',
limit: 10,
params: {
- query: searchQuery,
+ query: query,
channelId: channel.id,
},
};
+
+ searchKey = query;
}
const headerActions = $computed(() => {
diff --git a/packages/frontend/src/pages/explore.users.vue b/packages/frontend/src/pages/explore.users.vue
index 3f4ff5182b..f9c833dd29 100644
--- a/packages/frontend/src/pages/explore.users.vue
+++ b/packages/frontend/src/pages/explore.users.vue
@@ -28,9 +28,9 @@
<MkFoldableSection ref="tagsEl" :foldable="true" :expanded="false" class="_margin">
<template #header><i class="ti ti-hash ti-fw" style="margin-right: 0.5em;"></i>{{ i18n.ts.popularTags }}</template>
- <div class="vxjfqztj">
- <MkA v-for="tag in tagsLocal" :key="'local:' + tag.tag" :to="`/user-tags/${tag.tag}`" class="local">{{ tag.tag }}</MkA>
- <MkA v-for="tag in tagsRemote" :key="'remote:' + tag.tag" :to="`/user-tags/${tag.tag}`">{{ tag.tag }}</MkA>
+ <div>
+ <MkA v-for="tag in tagsLocal" :key="'local:' + tag.tag" :to="`/user-tags/${tag.tag}`" style="margin-right: 16px; font-weight: bold;">{{ tag.tag }}</MkA>
+ <MkA v-for="tag in tagsRemote" :key="'remote:' + tag.tag" :to="`/user-tags/${tag.tag}`" style="margin-right: 16px;">{{ tag.tag }}</MkA>
</div>
</MkFoldableSection>
@@ -132,15 +132,3 @@ os.api('hashtags/list', {
tagsRemote = tags;
});
</script>
-
-<style lang="scss" scoped>
-.vxjfqztj {
- > * {
- margin-right: 16px;
-
- &.local {
- font-weight: bold;
- }
- }
-}
-</style>
diff --git a/packages/frontend/src/pages/page-editor/els/page-editor.el.image.vue b/packages/frontend/src/pages/page-editor/els/page-editor.el.image.vue
index e97a4b07f1..1b292e8f3c 100644
--- a/packages/frontend/src/pages/page-editor/els/page-editor.el.image.vue
+++ b/packages/frontend/src/pages/page-editor/els/page-editor.el.image.vue
@@ -37,7 +37,7 @@ async function choose() {
file = fileResponse[0];
emit('update:modelValue', {
...props.modelValue,
- fileId: fileResponse.id,
+ fileId: file.id,
});
});
}
diff --git a/packages/frontend/src/pages/search.note.vue b/packages/frontend/src/pages/search.note.vue
new file mode 100644
index 0000000000..d9b44d15f5
--- /dev/null
+++ b/packages/frontend/src/pages/search.note.vue
@@ -0,0 +1,98 @@
+<template>
+<div class="_gaps">
+ <div class="_gaps">
+ <MkInput v-model="searchQuery" :large="true" :autofocus="true" type="search">
+ <template #prefix><i class="ti ti-search"></i></template>
+ </MkInput>
+ <MkFolder>
+ <template #label>{{ i18n.ts.options }}</template>
+
+ <MkFolder>
+ <template #label>{{ i18n.ts.specifyUser }}</template>
+ <template v-if="user" #suffix>@{{ user.username }}</template>
+
+ <div style="text-align: center;" class="_gaps">
+ <div v-if="user">@{{ user.username }}</div>
+ <div>
+ <MkButton v-if="user == null" primary rounded inline @click="selectUser">{{ i18n.ts.selectUser }}</MkButton>
+ <MkButton v-else danger rounded inline @click="user = null">{{ i18n.ts.remove }}</MkButton>
+ </div>
+ </div>
+ </MkFolder>
+ </MkFolder>
+ <div>
+ <MkButton large primary gradate rounded style="margin: 0 auto;" @click="search">{{ i18n.ts.search }}</MkButton>
+ </div>
+ </div>
+
+ <MkFoldableSection v-if="notePagination">
+ <template #header>{{ i18n.ts.searchResult }}</template>
+ <MkNotes :key="key" :pagination="notePagination"/>
+ </MkFoldableSection>
+</div>
+</template>
+
+<script lang="ts" setup>
+import { computed, onMounted } from 'vue';
+import MkNotes from '@/components/MkNotes.vue';
+import MkInput from '@/components/MkInput.vue';
+import MkRadios from '@/components/MkRadios.vue';
+import MkButton from '@/components/MkButton.vue';
+import { i18n } from '@/i18n';
+import * as os from '@/os';
+import MkFoldableSection from '@/components/MkFoldableSection.vue';
+import { $i } from '@/account';
+import { instance } from '@/instance';
+import MkInfo from '@/components/MkInfo.vue';
+import { useRouter } from '@/router';
+import MkFolder from '@/components/MkFolder.vue';
+
+const router = useRouter();
+
+let key = $ref(0);
+let searchQuery = $ref('');
+let searchOrigin = $ref('combined');
+let notePagination = $ref();
+let user = $ref(null);
+
+function selectUser() {
+ os.selectUser().then(_user => {
+ user = _user;
+ });
+}
+
+async function search() {
+ const query = searchQuery.toString().trim();
+
+ if (query == null || query === '') return;
+
+ if (query.startsWith('https://')) {
+ const promise = os.api('ap/show', {
+ uri: query,
+ });
+
+ os.promiseDialog(promise, null, null, i18n.ts.fetchingAsApObject);
+
+ const res = await promise;
+
+ if (res.type === 'User') {
+ router.push(`/@${res.object.username}@${res.object.host}`);
+ } else if (res.type === 'Note') {
+ router.push(`/notes/${res.object.id}`);
+ }
+
+ return;
+ }
+
+ notePagination = {
+ endpoint: 'notes/search',
+ limit: 10,
+ params: {
+ query: searchQuery,
+ userId: user ? user.id : null,
+ },
+ };
+
+ key++;
+}
+</script>
diff --git a/packages/frontend/src/pages/search.user.vue b/packages/frontend/src/pages/search.user.vue
new file mode 100644
index 0000000000..23a8978fd1
--- /dev/null
+++ b/packages/frontend/src/pages/search.user.vue
@@ -0,0 +1,77 @@
+<template>
+<div class="_gaps">
+ <div class="_gaps">
+ <MkInput v-model="searchQuery" :large="true" :autofocus="true" type="search">
+ <template #prefix><i class="ti ti-search"></i></template>
+ </MkInput>
+ <MkRadios v-model="searchOrigin" @update:model-value="search()">
+ <option value="combined">{{ i18n.ts.all }}</option>
+ <option value="local">{{ i18n.ts.local }}</option>
+ <option value="remote">{{ i18n.ts.remote }}</option>
+ </MkRadios>
+ <MkButton large primary gradate rounded @click="search">{{ i18n.ts.search }}</MkButton>
+ </div>
+
+ <MkFoldableSection v-if="userPagination">
+ <template #header>{{ i18n.ts.searchResult }}</template>
+ <MkUserList :key="key" :pagination="userPagination"/>
+ </MkFoldableSection>
+</div>
+</template>
+
+<script lang="ts" setup>
+import { computed, defineAsyncComponent, onMounted } from 'vue';
+import MkUserList from '@/components/MkUserList.vue';
+import MkInput from '@/components/MkInput.vue';
+import MkRadios from '@/components/MkRadios.vue';
+import MkButton from '@/components/MkButton.vue';
+import { i18n } from '@/i18n';
+import * as os from '@/os';
+import MkFoldableSection from '@/components/MkFoldableSection.vue';
+import { $i } from '@/account';
+import { instance } from '@/instance';
+import MkInfo from '@/components/MkInfo.vue';
+import { useRouter } from '@/router';
+
+const router = useRouter();
+
+let key = $ref('');
+let searchQuery = $ref('');
+let searchOrigin = $ref('combined');
+let userPagination = $ref();
+
+async function search() {
+ const query = searchQuery.toString().trim();
+
+ if (query == null || query === '') return;
+
+ if (query.startsWith('https://')) {
+ const promise = os.api('ap/show', {
+ uri: query,
+ });
+
+ os.promiseDialog(promise, null, null, i18n.ts.fetchingAsApObject);
+
+ const res = await promise;
+
+ if (res.type === 'User') {
+ router.push(`/@${res.object.username}@${res.object.host}`);
+ } else if (res.type === 'Note') {
+ router.push(`/notes/${res.object.id}`);
+ }
+
+ return;
+ }
+
+ userPagination = {
+ endpoint: 'users/search',
+ limit: 10,
+ params: {
+ query: searchQuery,
+ origin: searchOrigin,
+ },
+ };
+
+ key = query;
+}
+</script>
diff --git a/packages/frontend/src/pages/search.vue b/packages/frontend/src/pages/search.vue
index 5523d5cf4d..9f3d8da560 100644
--- a/packages/frontend/src/pages/search.vue
+++ b/packages/frontend/src/pages/search.vue
@@ -1,133 +1,38 @@
<template>
<MkStickyContainer>
<template #header><MkPageHeader v-model:tab="tab" :actions="headerActions" :tabs="headerTabs"/></template>
- <MkSpacer v-if="tab === 'note'" :content-max="800">
- <div v-if="notesSearchAvailable" class="_gaps">
- <div class="_gaps">
- <MkInput v-model="searchQuery" :large="true" :autofocus="true" type="search">
- <template #prefix><i class="ti ti-search"></i></template>
- </MkInput>
- <MkButton large primary gradate rounded @click="search">{{ i18n.ts.search }}</MkButton>
- </div>
- <MkFoldableSection v-if="notePagination">
- <template #header>{{ i18n.ts.searchResult }}</template>
- <MkNotes :key="key" :pagination="notePagination"/>
- </MkFoldableSection>
+ <MkSpacer v-if="tab === 'note'" :content-max="800">
+ <div v-if="notesSearchAvailable">
+ <XNote/>
</div>
<div v-else>
<MkInfo warn>{{ i18n.ts.notesSearchNotAvailable }}</MkInfo>
</div>
</MkSpacer>
- <MkSpacer v-else-if="tab === 'user'" :content-max="800">
- <div class="_gaps">
- <div class="_gaps">
- <MkInput v-model="searchQuery" :large="true" :autofocus="true" type="search">
- <template #prefix><i class="ti ti-search"></i></template>
- </MkInput>
- <MkRadios v-model="searchOrigin" @update:model-value="search()">
- <option value="combined">{{ i18n.ts.all }}</option>
- <option value="local">{{ i18n.ts.local }}</option>
- <option value="remote">{{ i18n.ts.remote }}</option>
- </MkRadios>
- <MkButton large primary gradate rounded @click="search">{{ i18n.ts.search }}</MkButton>
- </div>
- <MkFoldableSection v-if="userPagination">
- <template #header>{{ i18n.ts.searchResult }}</template>
- <MkUserList :key="key" :pagination="userPagination"/>
- </MkFoldableSection>
- </div>
+ <MkSpacer v-else-if="tab === 'user'" :content-max="800">
+ <XUser/>
</MkSpacer>
</MkStickyContainer>
</template>
<script lang="ts" setup>
-import { computed, onMounted } from 'vue';
-import MkNotes from '@/components/MkNotes.vue';
-import MkUserList from '@/components/MkUserList.vue';
-import MkInput from '@/components/MkInput.vue';
-import MkRadios from '@/components/MkRadios.vue';
-import MkButton from '@/components/MkButton.vue';
+import { computed, defineAsyncComponent, onMounted } from 'vue';
import { i18n } from '@/i18n';
import { definePageMetadata } from '@/scripts/page-metadata';
import * as os from '@/os';
-import MkFoldableSection from '@/components/MkFoldableSection.vue';
import { $i } from '@/account';
import { instance } from '@/instance';
import MkInfo from '@/components/MkInfo.vue';
-import { useRouter } from '@/router';
-
-const router = useRouter();
-const props = defineProps<{
- query: string;
- channel?: string;
- type?: string;
- origin?: string;
-}>();
+const XNote = defineAsyncComponent(() => import('./search.note.vue'));
+const XUser = defineAsyncComponent(() => import('./search.user.vue'));
-let key = $ref('');
let tab = $ref('note');
-let searchQuery = $ref('');
-let searchOrigin = $ref('combined');
-let notePagination = $ref();
-let userPagination = $ref();
const notesSearchAvailable = (($i == null && instance.policies.canSearchNotes) || ($i != null && $i.policies.canSearchNotes));
-onMounted(() => {
- tab = props.type ?? 'note';
- searchQuery = props.query ?? '';
- searchOrigin = props.origin ?? 'combined';
-});
-
-async function search() {
- const query = searchQuery.toString().trim();
-
- if (query == null || query === '') return;
-
- if (query.startsWith('https://')) {
- const promise = os.api('ap/show', {
- uri: query,
- });
-
- os.promiseDialog(promise, null, null, i18n.ts.fetchingAsApObject);
-
- const res = await promise;
-
- if (res.type === 'User') {
- router.push(`/@${res.object.username}@${res.object.host}`);
- } else if (res.type === 'Note') {
- router.push(`/notes/${res.object.id}`);
- }
-
- return;
- }
-
- if (tab === 'note') {
- notePagination = {
- endpoint: 'notes/search',
- limit: 10,
- params: {
- query: searchQuery,
- channelId: props.channel,
- },
- };
- } else if (tab === 'user') {
- userPagination = {
- endpoint: 'users/search',
- limit: 10,
- params: {
- query: searchQuery,
- origin: searchOrigin,
- },
- };
- }
-
- key = query;
-}
-
const headerActions = $computed(() => []);
const headerTabs = $computed(() => [{
@@ -141,7 +46,7 @@ const headerTabs = $computed(() => [{
}]);
definePageMetadata(computed(() => ({
- title: searchQuery ? i18n.t('searchWith', { q: searchQuery }) : i18n.ts.search,
+ title: i18n.ts.search,
icon: 'ti ti-search',
})));
</script>
diff --git a/packages/frontend/src/pages/settings/privacy.vue b/packages/frontend/src/pages/settings/privacy.vue
index c83c48d5ad..a1af0ba80b 100644
--- a/packages/frontend/src/pages/settings/privacy.vue
+++ b/packages/frontend/src/pages/settings/privacy.vue
@@ -24,6 +24,10 @@
{{ i18n.ts.noCrawle }}
<template #caption>{{ i18n.ts.noCrawleDescription }}</template>
</MkSwitch>
+ <MkSwitch v-model="preventAiLearning" @update:model-value="save()">
+ {{ i18n.ts.preventAiLearning }}<span class="_beta">{{ i18n.ts.beta }}</span>
+ <template #caption>{{ i18n.ts.preventAiLearningDescription }}</template>
+ </MkSwitch>
<MkSwitch v-model="isExplorable" @update:model-value="save()">
{{ i18n.ts.makeExplorable }}
<template #caption>{{ i18n.ts.makeExplorableDescription }}</template>
@@ -71,6 +75,7 @@ import { definePageMetadata } from '@/scripts/page-metadata';
let isLocked = $ref($i.isLocked);
let autoAcceptFollowed = $ref($i.autoAcceptFollowed);
let noCrawle = $ref($i.noCrawle);
+let preventAiLearning = $ref($i.preventAiLearning);
let isExplorable = $ref($i.isExplorable);
let hideOnlineStatus = $ref($i.hideOnlineStatus);
let publicReactions = $ref($i.publicReactions);
@@ -86,6 +91,7 @@ function save() {
isLocked: !!isLocked,
autoAcceptFollowed: !!autoAcceptFollowed,
noCrawle: !!noCrawle,
+ preventAiLearning: !!preventAiLearning,
isExplorable: !!isExplorable,
hideOnlineStatus: !!hideOnlineStatus,
publicReactions: !!publicReactions,